specatlas 0.1.30 → 0.1.32
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 +872 -316
- package/package.json +2 -2
package/dist/bin.js
CHANGED
|
@@ -1586,7 +1586,7 @@ var require_core = __commonJS({
|
|
|
1586
1586
|
});
|
|
1587
1587
|
|
|
1588
1588
|
// src/cli.ts
|
|
1589
|
-
import
|
|
1589
|
+
import path54 from "path";
|
|
1590
1590
|
|
|
1591
1591
|
// ../core/dist/index.js
|
|
1592
1592
|
import path from "path";
|
|
@@ -1595,29 +1595,33 @@ import { z } from "zod";
|
|
|
1595
1595
|
import { parse as parseYaml2, stringify as stringifyYaml2 } from "yaml";
|
|
1596
1596
|
import { parse as parseYaml3 } from "yaml";
|
|
1597
1597
|
import { z as z2 } from "zod";
|
|
1598
|
-
import path2 from "path";
|
|
1599
1598
|
import path3 from "path";
|
|
1599
|
+
import path2 from "path";
|
|
1600
1600
|
import { parse as parseYaml4 } from "yaml";
|
|
1601
|
+
import path4 from "path";
|
|
1602
|
+
import { parse as parseYaml5 } from "yaml";
|
|
1601
1603
|
import { z as z3 } from "zod";
|
|
1604
|
+
import path7 from "path";
|
|
1602
1605
|
import path5 from "path";
|
|
1603
|
-
import path4 from "path";
|
|
1604
1606
|
import { stringify as stringifyYaml3 } from "yaml";
|
|
1605
|
-
import { stringify as stringifyYaml4 } from "yaml";
|
|
1606
1607
|
import path6 from "path";
|
|
1607
|
-
import { parse as
|
|
1608
|
-
import {
|
|
1609
|
-
import path7 from "path";
|
|
1608
|
+
import { parse as parseYaml6, stringify as stringifyYaml4 } from "yaml";
|
|
1609
|
+
import { stringify as stringifyYaml5 } from "yaml";
|
|
1610
1610
|
import path8 from "path";
|
|
1611
|
+
import { parse as parseYaml7 } from "yaml";
|
|
1612
|
+
import { execFile } from "child_process";
|
|
1613
|
+
import path9 from "path";
|
|
1614
|
+
import path10 from "path";
|
|
1611
1615
|
import { randomBytes } from "crypto";
|
|
1616
|
+
import path13 from "path";
|
|
1612
1617
|
import path11 from "path";
|
|
1613
|
-
import
|
|
1614
|
-
import { parse as parseYaml6, stringify as stringifyYaml5 } from "yaml";
|
|
1618
|
+
import { parse as parseYaml8, stringify as stringifyYaml6 } from "yaml";
|
|
1615
1619
|
import { z as z4 } from "zod";
|
|
1616
|
-
import path10 from "path";
|
|
1617
|
-
import { parse as parseYaml7 } from "yaml";
|
|
1618
|
-
import { z as z5 } from "zod";
|
|
1619
1620
|
import path12 from "path";
|
|
1620
|
-
import
|
|
1621
|
+
import { parse as parseYaml9 } from "yaml";
|
|
1622
|
+
import { z as z5 } from "zod";
|
|
1623
|
+
import path14 from "path";
|
|
1624
|
+
import path15 from "path";
|
|
1621
1625
|
|
|
1622
1626
|
// ../../node_modules/.pnpm/highlight.js@11.12.0/node_modules/highlight.js/es/core.js
|
|
1623
1627
|
var import_core = __toESM(require_core(), 1);
|
|
@@ -9022,19 +9026,19 @@ function renderMarkdown(markdown2, opts = {}) {
|
|
|
9022
9026
|
}
|
|
9023
9027
|
|
|
9024
9028
|
// ../core/dist/index.js
|
|
9025
|
-
import path14 from "path";
|
|
9026
|
-
import path15 from "path";
|
|
9027
9029
|
import path16 from "path";
|
|
9028
|
-
import { promises as fs } from "fs";
|
|
9029
|
-
import { cp, rename, rm } from "fs/promises";
|
|
9030
9030
|
import path17 from "path";
|
|
9031
|
-
import { promises as fs2 } from "fs";
|
|
9032
9031
|
import path18 from "path";
|
|
9033
|
-
import {
|
|
9032
|
+
import { promises as fs } from "fs";
|
|
9033
|
+
import { cp, rename, rm } from "fs/promises";
|
|
9034
9034
|
import path19 from "path";
|
|
9035
|
+
import { promises as fs2 } from "fs";
|
|
9035
9036
|
import path20 from "path";
|
|
9037
|
+
import { parse as parseYaml10, stringify as stringifyYaml7 } from "yaml";
|
|
9036
9038
|
import path21 from "path";
|
|
9037
9039
|
import path22 from "path";
|
|
9040
|
+
import path23 from "path";
|
|
9041
|
+
import path24 from "path";
|
|
9038
9042
|
var CORE_VERSION = "0.0.1";
|
|
9039
9043
|
var REQ_ID_RE = /^REQ-[A-Z0-9]+(?:-[A-Z0-9]+)*-\d{3}$/;
|
|
9040
9044
|
var SCENARIO_ID_RE = /^REQ-[A-Z0-9]+(?:-[A-Z0-9]+)*-\d{3}-S\d+$/;
|
|
@@ -9059,6 +9063,7 @@ var atlasConfigSchema = z.object({
|
|
|
9059
9063
|
review: z.object({ mode: z.enum(["off", "advisory", "blocking"]).default("advisory") }).default({}),
|
|
9060
9064
|
clarify: z.object({ mode: z.enum(["off", "advisory", "blocking"]).default("advisory") }).default({}),
|
|
9061
9065
|
docs: z.object({ mode: z.enum(["off", "advisory", "blocking"]).default("blocking") }).default({}),
|
|
9066
|
+
contracts: z.object({ mode: z.enum(["off", "advisory", "blocking"]).default("advisory") }).default({}),
|
|
9062
9067
|
mockup: z.object({ require_approval: z.boolean().default(false), compare_in_verify: z.boolean().default(false) }).default({})
|
|
9063
9068
|
}).default({}),
|
|
9064
9069
|
trace: z.object({ mode: z.enum(["off", "advisory", "blocking"]).default("blocking"), prefix: z.string().default("REQ") }).default({}),
|
|
@@ -9181,6 +9186,7 @@ var SCENARIO_HEAD_RE = /^####\s+(?:Scenario|Escenario):\s+(REQ-[A-Z0-9-]+-S\d+)\
|
|
|
9181
9186
|
var RULE_RE = /^-\s*(?:Rule|Regla)\s+(BR-[A-Z0-9-]+)\s*:\s*(.+?)\s*$/;
|
|
9182
9187
|
var WHEN_RE = /^-\s*\*\*\s*(?:WHEN|CUANDO|DADO QUE)\s*\*\*\s*(.+?)\s*$/i;
|
|
9183
9188
|
var THEN_RE = /^-\s*\*\*\s*(?:THEN|ENTONCES|Y|AND)\s*\*\*\s*(.+?)\s*$/i;
|
|
9189
|
+
var CONTRACT_RE = /^-\s*\*\*\s*(?:CONTRATO|CONTRACT)\s*\*\*\s*:\s*(.+?)\s*$/i;
|
|
9184
9190
|
function newDraft(id, title, line) {
|
|
9185
9191
|
return { id, title, line, prose: [], rules: [], scenarios: [] };
|
|
9186
9192
|
}
|
|
@@ -9265,6 +9271,16 @@ function parseRequirementBlocks(body, startLine = 1, filePath) {
|
|
|
9265
9271
|
currentScenario.then.push(thenMatch[1] ?? "");
|
|
9266
9272
|
continue;
|
|
9267
9273
|
}
|
|
9274
|
+
const contractMatch = CONTRACT_RE.exec(line);
|
|
9275
|
+
if (contractMatch && currentScenario) {
|
|
9276
|
+
const ref = (contractMatch[1] ?? "").trim();
|
|
9277
|
+
if (ref !== "") {
|
|
9278
|
+
const list = currentScenario.contracts ?? [];
|
|
9279
|
+
if (!list.includes(ref)) list.push(ref);
|
|
9280
|
+
currentScenario.contracts = list;
|
|
9281
|
+
}
|
|
9282
|
+
continue;
|
|
9283
|
+
}
|
|
9268
9284
|
if (current && !currentScenario) {
|
|
9269
9285
|
if (line.trim() !== "") current.prose.push(line.trim());
|
|
9270
9286
|
}
|
|
@@ -9718,6 +9734,7 @@ function impactOfRequirement(workspace, reqId) {
|
|
|
9718
9734
|
const id = reqId.toUpperCase();
|
|
9719
9735
|
const scenarios = /* @__PURE__ */ new Set();
|
|
9720
9736
|
let exists2 = false;
|
|
9737
|
+
let origin;
|
|
9721
9738
|
for (const spec of workspace.specs) {
|
|
9722
9739
|
for (const req of spec.spec.requirements) {
|
|
9723
9740
|
if (req.id !== id) continue;
|
|
@@ -9734,6 +9751,18 @@ function impactOfRequirement(workspace, reqId) {
|
|
|
9734
9751
|
if ((change.delta?.removed ?? []).some((r) => r.id === id)) exists2 = true;
|
|
9735
9752
|
if ((change.delta?.renamed ?? []).some((r) => r.from.id === id || r.to.id === id)) exists2 = true;
|
|
9736
9753
|
}
|
|
9754
|
+
if (!exists2) {
|
|
9755
|
+
for (const link of workspace.links?.entries ?? []) {
|
|
9756
|
+
const spec = workspace.links?.specs.find((candidate) => candidate.spec.requirements.some((req) => req.id === id));
|
|
9757
|
+
if (!spec) continue;
|
|
9758
|
+
const requirement = spec.spec.requirements.find((candidate) => candidate.id === id);
|
|
9759
|
+
if (!requirement) continue;
|
|
9760
|
+
exists2 = true;
|
|
9761
|
+
origin = link.name;
|
|
9762
|
+
for (const sc of requirement.scenarios) scenarios.add(sc.id);
|
|
9763
|
+
break;
|
|
9764
|
+
}
|
|
9765
|
+
}
|
|
9737
9766
|
const report = emptyReport(id, "requirement");
|
|
9738
9767
|
if (!exists2) return report;
|
|
9739
9768
|
report.exists = true;
|
|
@@ -9741,6 +9770,10 @@ function impactOfRequirement(workspace, reqId) {
|
|
|
9741
9770
|
const reqOf = scenarioToReq(workspace);
|
|
9742
9771
|
collect(workspace, report, /* @__PURE__ */ new Set([id, ...scenarios]), reqOf);
|
|
9743
9772
|
if (!report.requirements.includes(id)) report.requirements.unshift(id);
|
|
9773
|
+
if (origin !== void 0) {
|
|
9774
|
+
report.external = true;
|
|
9775
|
+
report.origin = origin;
|
|
9776
|
+
}
|
|
9744
9777
|
return report;
|
|
9745
9778
|
}
|
|
9746
9779
|
function impactOfFile(workspace, file) {
|
|
@@ -9779,14 +9812,14 @@ function wordBoundary(text, term) {
|
|
|
9779
9812
|
const re = new RegExp(`(?<![\\p{L}\\p{N}])${escaped}(?![\\p{L}\\p{N}])`, "iu");
|
|
9780
9813
|
return re.test(text);
|
|
9781
9814
|
}
|
|
9782
|
-
function lintText(text, code, terms, label,
|
|
9815
|
+
function lintText(text, code, terms, label, path252, line) {
|
|
9783
9816
|
const out = [];
|
|
9784
9817
|
const lower = text.toLowerCase();
|
|
9785
9818
|
for (const term of terms) {
|
|
9786
9819
|
if (wordBoundary(lower, term)) {
|
|
9787
9820
|
out.push(
|
|
9788
9821
|
diag(code, "error", `${label}: "${term}"`, {
|
|
9789
|
-
path:
|
|
9822
|
+
path: path252,
|
|
9790
9823
|
line,
|
|
9791
9824
|
suggestion: "La especificaci\xF3n es funcional y de negocio: describe comportamiento, no tecnolog\xEDa ni adjetivos vagos"
|
|
9792
9825
|
})
|
|
@@ -9795,7 +9828,7 @@ function lintText(text, code, terms, label, path232, line) {
|
|
|
9795
9828
|
}
|
|
9796
9829
|
return out;
|
|
9797
9830
|
}
|
|
9798
|
-
function lintRequirement(req,
|
|
9831
|
+
function lintRequirement(req, path252, opts = {}) {
|
|
9799
9832
|
const out = [];
|
|
9800
9833
|
const vague = opts.language === "en" ? VAGUE_EN : VAGUE_ES;
|
|
9801
9834
|
const tech = opts.language === "en" ? TECH_EN : TECH_ES;
|
|
@@ -9806,32 +9839,32 @@ function lintRequirement(req, path232, opts = {}) {
|
|
|
9806
9839
|
...req.scenarios.flatMap((s) => [...s.when.map((w) => ({ text: w, line: s.line })), ...s.then.map((t) => ({ text: t, line: s.line }))])
|
|
9807
9840
|
];
|
|
9808
9841
|
for (const part of parts) {
|
|
9809
|
-
out.push(...lintText(part.text, "LINT-BIZ-002", vague, "Palabra vaga en la especificaci\xF3n",
|
|
9842
|
+
out.push(...lintText(part.text, "LINT-BIZ-002", vague, "Palabra vaga en la especificaci\xF3n", path252, part.line));
|
|
9810
9843
|
if (opts.businessOnly !== false) {
|
|
9811
|
-
out.push(...lintText(part.text, "LINT-BIZ-001", tech, "Jerga t\xE9cnica en la especificaci\xF3n de negocio",
|
|
9844
|
+
out.push(...lintText(part.text, "LINT-BIZ-001", tech, "Jerga t\xE9cnica en la especificaci\xF3n de negocio", path252, part.line));
|
|
9812
9845
|
}
|
|
9813
9846
|
}
|
|
9814
9847
|
if (req.scenarios.length === 0) {
|
|
9815
|
-
out.push(diag("TRACE-001", "error", `El requisito ${req.id} no tiene ning\xFAn escenario`, { path:
|
|
9848
|
+
out.push(diag("TRACE-001", "error", `El requisito ${req.id} no tiene ning\xFAn escenario`, { path: path252, line: req.line, suggestion: "A\xF1ade al menos un escenario CUANDO/ENTONCES" }));
|
|
9816
9849
|
}
|
|
9817
9850
|
return out;
|
|
9818
9851
|
}
|
|
9819
|
-
function lintDelta(delta, livingRequirements,
|
|
9852
|
+
function lintDelta(delta, livingRequirements, path252, opts = {}) {
|
|
9820
9853
|
const out = [...delta.diagnostics];
|
|
9821
9854
|
for (const req of [...delta.added, ...delta.modified]) {
|
|
9822
|
-
out.push(...lintRequirement(req,
|
|
9855
|
+
out.push(...lintRequirement(req, path252, opts));
|
|
9823
9856
|
}
|
|
9824
9857
|
for (const req of delta.modified) {
|
|
9825
9858
|
const living = livingRequirements.get(req.id);
|
|
9826
9859
|
if (!living) {
|
|
9827
|
-
out.push(diag("TRACE-007", "error", `MODIFIED ${req.id} no existe en la spec viva; usa ADDED`, { path:
|
|
9860
|
+
out.push(diag("TRACE-007", "error", `MODIFIED ${req.id} no existe en la spec viva; usa ADDED`, { path: path252, line: req.line }));
|
|
9828
9861
|
continue;
|
|
9829
9862
|
}
|
|
9830
9863
|
for (const existing of living.scenarios) {
|
|
9831
9864
|
if (!req.scenarios.some((s) => s.id === existing.id)) {
|
|
9832
9865
|
out.push(
|
|
9833
9866
|
diag("TRACE-007", "error", `MODIFIED ${req.id} pierde el escenario ${existing.id}: copia el bloque completo`, {
|
|
9834
|
-
path:
|
|
9867
|
+
path: path252,
|
|
9835
9868
|
line: req.line,
|
|
9836
9869
|
suggestion: "Copia el bloque completo de la spec viva y ed\xEDtalo; para quitarlo, decl\xE1ralo en REMOVED"
|
|
9837
9870
|
})
|
|
@@ -9842,15 +9875,15 @@ function lintDelta(delta, livingRequirements, path232, opts = {}) {
|
|
|
9842
9875
|
for (const req of delta.removed) {
|
|
9843
9876
|
const living = livingRequirements.get(req.id);
|
|
9844
9877
|
if (!living) {
|
|
9845
|
-
out.push(diag("TRACE-007", "error", `REMOVED ${req.id} no existe en la spec viva`, { path:
|
|
9878
|
+
out.push(diag("TRACE-007", "error", `REMOVED ${req.id} no existe en la spec viva`, { path: path252, line: req.line }));
|
|
9846
9879
|
}
|
|
9847
9880
|
}
|
|
9848
9881
|
for (const rename2 of delta.renamed) {
|
|
9849
9882
|
if (rename2.from.id !== rename2.to.id) {
|
|
9850
|
-
out.push(diag("LINT-DLT-003", "error", `RENAMED cambia el id (${rename2.from.id} \u2192 ${rename2.to.id}); los ids son inmutables`, { path:
|
|
9883
|
+
out.push(diag("LINT-DLT-003", "error", `RENAMED cambia el id (${rename2.from.id} \u2192 ${rename2.to.id}); los ids son inmutables`, { path: path252, line: rename2.line }));
|
|
9851
9884
|
}
|
|
9852
9885
|
if (!livingRequirements.has(rename2.from.id)) {
|
|
9853
|
-
out.push(diag("TRACE-007", "error", `RENAMED ${rename2.from.id} no existe en la spec viva`, { path:
|
|
9886
|
+
out.push(diag("TRACE-007", "error", `RENAMED ${rename2.from.id} no existe en la spec viva`, { path: path252, line: rename2.line }));
|
|
9854
9887
|
}
|
|
9855
9888
|
}
|
|
9856
9889
|
return out;
|
|
@@ -9874,7 +9907,7 @@ var MERMAID_KEYWORDS = [
|
|
|
9874
9907
|
"architecture-beta"
|
|
9875
9908
|
];
|
|
9876
9909
|
var MERMAID_BLOCKS = /^\s*(alt|loop|opt|par|rect|critical|break|subgraph)\b/;
|
|
9877
|
-
function lintPlan(planText,
|
|
9910
|
+
function lintPlan(planText, path252) {
|
|
9878
9911
|
const out = [];
|
|
9879
9912
|
const blocks = [...planText.matchAll(/```mermaid\r?\n([\s\S]*?)```/g)];
|
|
9880
9913
|
for (const [index, block] of blocks.entries()) {
|
|
@@ -9884,7 +9917,7 @@ function lintPlan(planText, path232) {
|
|
|
9884
9917
|
if (!MERMAID_KEYWORDS.some((keyword) => first.startsWith(keyword))) {
|
|
9885
9918
|
out.push(
|
|
9886
9919
|
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)}"`, {
|
|
9887
|
-
path:
|
|
9920
|
+
path: path252,
|
|
9888
9921
|
suggestion: "Corrige el tipo del diagrama o elimina el bloque"
|
|
9889
9922
|
})
|
|
9890
9923
|
);
|
|
@@ -9898,7 +9931,7 @@ function lintPlan(planText, path232) {
|
|
|
9898
9931
|
if (open !== 0) {
|
|
9899
9932
|
out.push(
|
|
9900
9933
|
diag("LINT-PLN-002", "error", `Diagrama mermaid ${index + 1}: faltan ${Math.abs(open)} \`end\` (bloques alt/loop/subgraph sin cerrar)`, {
|
|
9901
|
-
path:
|
|
9934
|
+
path: path252,
|
|
9902
9935
|
suggestion: "Cierra cada bloque alt/loop/opt/par/rect/subgraph con `end`"
|
|
9903
9936
|
})
|
|
9904
9937
|
);
|
|
@@ -9910,7 +9943,7 @@ function lintPlan(planText, path232) {
|
|
|
9910
9943
|
if (message.includes(";")) {
|
|
9911
9944
|
out.push(
|
|
9912
9945
|
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`, {
|
|
9913
|
-
path:
|
|
9946
|
+
path: path252,
|
|
9914
9947
|
suggestion: "Sustituye `;` por `\xB7` o `,` en los mensajes de sequenceDiagram"
|
|
9915
9948
|
})
|
|
9916
9949
|
);
|
|
@@ -10021,15 +10054,25 @@ function checkTrace(input) {
|
|
|
10021
10054
|
);
|
|
10022
10055
|
}
|
|
10023
10056
|
}
|
|
10057
|
+
const linkedIds = new Set(input.linked?.ids ?? []);
|
|
10058
|
+
const unavailableLinks = input.linked?.unavailable ?? [];
|
|
10024
10059
|
for (const task of taskById.values()) {
|
|
10025
10060
|
for (const c of task.covers) {
|
|
10026
|
-
if (!livingReqs.has(c) && !deltaScenarios.has(c) && !allScenarioIds.has(c)) {
|
|
10061
|
+
if (!livingReqs.has(c) && !deltaScenarios.has(c) && !allScenarioIds.has(c) && !linkedIds.has(c)) {
|
|
10027
10062
|
findings.push(
|
|
10028
10063
|
diag("TRACE-003", "error", `La tarea ${task.id} cubre ${c}, que no existe`, {
|
|
10029
10064
|
path: change.tasks?.path,
|
|
10030
|
-
suggestion: "Corrige el id o crea el requisito/escenario"
|
|
10065
|
+
suggestion: unavailableLinks.length > 0 ? "Corrige el id o revisa los enlaces no disponibles (`satlas link list`)" : "Corrige el id o crea el requisito/escenario"
|
|
10031
10066
|
})
|
|
10032
10067
|
);
|
|
10068
|
+
if (unavailableLinks.length > 0) {
|
|
10069
|
+
findings.push(
|
|
10070
|
+
diag("ATLAS-LINK-003", "warning", `Referencia no resuelta: ${c} (hay ${unavailableLinks.length} enlace(s) no disponible(s): ${unavailableLinks.join(", ")})`, {
|
|
10071
|
+
path: change.tasks?.path,
|
|
10072
|
+
suggestion: "Restaura la ruta del enlace o corrige la referencia"
|
|
10073
|
+
})
|
|
10074
|
+
);
|
|
10075
|
+
}
|
|
10033
10076
|
}
|
|
10034
10077
|
}
|
|
10035
10078
|
for (const dep of task.dependsOn) {
|
|
@@ -10192,11 +10235,165 @@ function planBlock(block, maxParallel) {
|
|
|
10192
10235
|
}
|
|
10193
10236
|
return { block: block.id, title: block.title, waves, degraded, diagnostics };
|
|
10194
10237
|
}
|
|
10238
|
+
var CONTRACTS_DIR = "contracts";
|
|
10239
|
+
var OPENAPI_METHODS = ["get", "post", "put", "patch", "delete", "head", "options"];
|
|
10240
|
+
var SUPPORTED_HINT = "Formatos esperados: OpenAPI 3.x (.yaml/.json), GraphQL SDL (.graphql/.gql) y protobuf (.proto)";
|
|
10241
|
+
function lineOf(content, needle) {
|
|
10242
|
+
const index = content.indexOf(needle);
|
|
10243
|
+
if (index < 0) return 1;
|
|
10244
|
+
return content.slice(0, index).split("\n").length;
|
|
10245
|
+
}
|
|
10246
|
+
function formatOf(file, content) {
|
|
10247
|
+
const ext = path2.extname(file).toLowerCase();
|
|
10248
|
+
if (ext === ".proto") return "protobuf";
|
|
10249
|
+
if (ext === ".graphql" || ext === ".gql") return "graphql";
|
|
10250
|
+
if (ext === ".yaml" || ext === ".yml" || ext === ".json") {
|
|
10251
|
+
return /(^|\n)\s*openapi\s*:/.test(content) || /"openapi"\s*:/.test(content) ? "openapi" : "unsupported";
|
|
10252
|
+
}
|
|
10253
|
+
return "unsupported";
|
|
10254
|
+
}
|
|
10255
|
+
function parseOpenApi(file, content) {
|
|
10256
|
+
const findings = [];
|
|
10257
|
+
let data;
|
|
10258
|
+
try {
|
|
10259
|
+
data = parseYaml4(content);
|
|
10260
|
+
} catch (error) {
|
|
10261
|
+
return { operations: [], findings: [diag("ATLAS-CONTRACT-001", "error", `Contrato OpenAPI ilegible: ${error.message}`, { path: file })] };
|
|
10262
|
+
}
|
|
10263
|
+
const doc = data;
|
|
10264
|
+
if (!doc || typeof doc !== "object" || typeof doc.openapi !== "string" || typeof doc.paths !== "object" || doc.paths === null || Array.isArray(doc.paths)) {
|
|
10265
|
+
return {
|
|
10266
|
+
operations: [],
|
|
10267
|
+
findings: [diag("ATLAS-CONTRACT-001", "error", "Contrato OpenAPI inv\xE1lido: faltan la versi\xF3n (openapi) o los caminos (paths)", { path: file, line: lineOf(content, "paths"), suggestion: SUPPORTED_HINT })]
|
|
10268
|
+
};
|
|
10269
|
+
}
|
|
10270
|
+
const operations = [];
|
|
10271
|
+
for (const [route, value] of Object.entries(doc.paths)) {
|
|
10272
|
+
if (!route.startsWith("/") || typeof value !== "object" || value === null) continue;
|
|
10273
|
+
for (const method of OPENAPI_METHODS) {
|
|
10274
|
+
if (method in value) {
|
|
10275
|
+
operations.push({ id: `${method.toUpperCase()} ${route}`, kind: "openapi", file, line: lineOf(content, route) });
|
|
10276
|
+
}
|
|
10277
|
+
}
|
|
10278
|
+
}
|
|
10279
|
+
return { operations, findings };
|
|
10280
|
+
}
|
|
10281
|
+
function parseGraphql(file, content) {
|
|
10282
|
+
const operations = [];
|
|
10283
|
+
const blockRe = /type\s+(Query|Mutation|Subscription)\s*\{([\s\S]*?)\}/g;
|
|
10284
|
+
for (const match of content.matchAll(blockRe)) {
|
|
10285
|
+
const typeName = match[1] ?? "";
|
|
10286
|
+
const body = match[2] ?? "";
|
|
10287
|
+
for (const field of body.split("\n")) {
|
|
10288
|
+
const fieldMatch = /^\s*([A-Za-z_][A-Za-z0-9_]*)\s*(\(|:)/.exec(field);
|
|
10289
|
+
if (fieldMatch) operations.push({ id: `${typeName}.${fieldMatch[1]}`, kind: "graphql", file, line: lineOf(content, field.trim()) });
|
|
10290
|
+
}
|
|
10291
|
+
}
|
|
10292
|
+
if (operations.length === 0) {
|
|
10293
|
+
return {
|
|
10294
|
+
operations,
|
|
10295
|
+
findings: [diag("ATLAS-CONTRACT-001", "error", "Contrato GraphQL inv\xE1lido: no declara consultas (type Query), mutaciones ni suscripciones", { path: file, suggestion: SUPPORTED_HINT })]
|
|
10296
|
+
};
|
|
10297
|
+
}
|
|
10298
|
+
return { operations, findings: [] };
|
|
10299
|
+
}
|
|
10300
|
+
function parseProtobuf(file, content) {
|
|
10301
|
+
const operations = [];
|
|
10302
|
+
const serviceRe = /service\s+([A-Za-z_][A-Za-z0-9_]*)\s*\{([\s\S]*?)\}/g;
|
|
10303
|
+
for (const match of content.matchAll(serviceRe)) {
|
|
10304
|
+
const service = match[1] ?? "";
|
|
10305
|
+
const body = match[2] ?? "";
|
|
10306
|
+
for (const rpc of body.matchAll(/rpc\s+([A-Za-z_][A-Za-z0-9_]*)/g)) {
|
|
10307
|
+
operations.push({ id: `${service}.${rpc[1]}`, kind: "protobuf", file, line: lineOf(content, `rpc ${rpc[1]}`) });
|
|
10308
|
+
}
|
|
10309
|
+
}
|
|
10310
|
+
if (operations.length === 0 && !/message\s+[A-Za-z_]/.test(content)) {
|
|
10311
|
+
return {
|
|
10312
|
+
operations,
|
|
10313
|
+
findings: [diag("ATLAS-CONTRACT-001", "error", "Contrato protobuf inv\xE1lido: no declara servicios ni mensajes", { path: file, suggestion: SUPPORTED_HINT })]
|
|
10314
|
+
};
|
|
10315
|
+
}
|
|
10316
|
+
return { operations, findings: [] };
|
|
10317
|
+
}
|
|
10318
|
+
function parseContract(file, content) {
|
|
10319
|
+
const format = formatOf(file, content);
|
|
10320
|
+
if (format === "unsupported") {
|
|
10321
|
+
return {
|
|
10322
|
+
path: file,
|
|
10323
|
+
format,
|
|
10324
|
+
operations: [],
|
|
10325
|
+
findings: [diag("ATLAS-CONTRACT-002", "warning", "Formato de contrato no soportado; el archivo no se interpreta ni se toca", { path: file, suggestion: SUPPORTED_HINT })]
|
|
10326
|
+
};
|
|
10327
|
+
}
|
|
10328
|
+
const parsed = format === "openapi" ? parseOpenApi(file, content) : format === "graphql" ? parseGraphql(file, content) : parseProtobuf(file, content);
|
|
10329
|
+
return { path: file, format, operations: parsed.operations, findings: parsed.findings };
|
|
10330
|
+
}
|
|
10331
|
+
async function loadContracts(changeDir) {
|
|
10332
|
+
const dir = path2.join(changeDir, CONTRACTS_DIR);
|
|
10333
|
+
const files = [];
|
|
10334
|
+
const operations = [];
|
|
10335
|
+
const findings = [];
|
|
10336
|
+
const entries = (await listDir(dir)).sort();
|
|
10337
|
+
for (const entry of entries) {
|
|
10338
|
+
const file = path2.join(dir, entry);
|
|
10339
|
+
const content = await readTextIfExists(file);
|
|
10340
|
+
if (content === void 0) continue;
|
|
10341
|
+
const parsed = parseContract(file, content);
|
|
10342
|
+
files.push({ path: parsed.path, format: parsed.format, operations: parsed.operations });
|
|
10343
|
+
operations.push(...parsed.operations);
|
|
10344
|
+
findings.push(...parsed.findings);
|
|
10345
|
+
}
|
|
10346
|
+
return { files, operations, findings };
|
|
10347
|
+
}
|
|
10348
|
+
function contractCoverage(change, mode) {
|
|
10349
|
+
const state = change.contracts;
|
|
10350
|
+
if (mode === "off" || !state || state.files.length === 0 || state.operations.length === 0) return [];
|
|
10351
|
+
const scenarioContracts = /* @__PURE__ */ new Map();
|
|
10352
|
+
for (const requirement of [...change.delta?.added ?? [], ...change.delta?.modified ?? []]) {
|
|
10353
|
+
for (const scenario of requirement.scenarios) {
|
|
10354
|
+
for (const ref of scenario.contracts ?? []) {
|
|
10355
|
+
if (!scenarioContracts.has(ref)) scenarioContracts.set(ref, scenario.id);
|
|
10356
|
+
}
|
|
10357
|
+
}
|
|
10358
|
+
}
|
|
10359
|
+
const findings = [];
|
|
10360
|
+
for (const operation of state.operations) {
|
|
10361
|
+
if (!scenarioContracts.has(operation.id)) {
|
|
10362
|
+
findings.push(
|
|
10363
|
+
diag("ATLAS-CONTRACT-003", "warning", `Operaci\xF3n sin escenario: ${operation.id}`, {
|
|
10364
|
+
path: operation.file,
|
|
10365
|
+
line: operation.line,
|
|
10366
|
+
suggestion: `Declara la operaci\xF3n en un escenario con \xB7 **Contrato**: ${operation.id}`
|
|
10367
|
+
})
|
|
10368
|
+
);
|
|
10369
|
+
}
|
|
10370
|
+
}
|
|
10371
|
+
const known = new Set(state.operations.map((operation) => operation.id));
|
|
10372
|
+
for (const [ref, scenarioId] of scenarioContracts) {
|
|
10373
|
+
if (!known.has(ref)) {
|
|
10374
|
+
findings.push(
|
|
10375
|
+
diag("ATLAS-CONTRACT-004", "warning", `Referencia rota: el escenario ${scenarioId} declara "${ref}", que no existe en el contrato`, {
|
|
10376
|
+
path: change.delta?.path,
|
|
10377
|
+
suggestion: "Corrige la referencia o a\xF1ade la operaci\xF3n al contrato"
|
|
10378
|
+
})
|
|
10379
|
+
);
|
|
10380
|
+
}
|
|
10381
|
+
}
|
|
10382
|
+
return findings;
|
|
10383
|
+
}
|
|
10384
|
+
function contractsAdvisory(change, cfg) {
|
|
10385
|
+
const state = change.contracts;
|
|
10386
|
+
if (!state || state.files.length === 0) return [];
|
|
10387
|
+
const mode = cfg.gates.contracts.mode;
|
|
10388
|
+
const form = state.findings;
|
|
10389
|
+
if (mode === "advisory") return [...form, ...contractCoverage(change, mode)];
|
|
10390
|
+
return form;
|
|
10391
|
+
}
|
|
10195
10392
|
function verifyApproval(change, approvals, cfg, specContent) {
|
|
10196
10393
|
if (cfg.gates.approval === "none") return { status: "not_required" };
|
|
10197
10394
|
if ((change.meta?.overrides ?? []).some((o) => o.gate === "approval")) return { status: "overridden" };
|
|
10198
10395
|
if (!change.delta) return { status: "missing" };
|
|
10199
|
-
const key =
|
|
10396
|
+
const key = path3.posix.join("changes", change.slug, "spec.md");
|
|
10200
10397
|
const approval = approvals.get(key) ?? approvals.get(`${key}`);
|
|
10201
10398
|
if (!approval) return { status: "missing" };
|
|
10202
10399
|
if (specContent === void 0) return { status: "valid", approvedBy: approval.by, approvedAt: approval.at };
|
|
@@ -10324,6 +10521,13 @@ function deriveState(input) {
|
|
|
10324
10521
|
blockedBy.push("documentaci\xF3n pendiente");
|
|
10325
10522
|
return { state: "reviewed", blockedBy, nextAction: next(`/satlas.docs ${change.slug}`, "Generar la documentaci\xF3n t\xE9cnica y manual del cambio", true), progress };
|
|
10326
10523
|
}
|
|
10524
|
+
if (cfg.gates.contracts.mode === "blocking") {
|
|
10525
|
+
const contractFindings = contractCoverage(change, "blocking");
|
|
10526
|
+
if (contractFindings.length > 0) {
|
|
10527
|
+
blockedBy.push(`contratos con hallazgos (${contractFindings.length})`);
|
|
10528
|
+
return { state: "verified", blockedBy, nextAction: next(`satlas contracts ${change.slug}`, "Resolver los hallazgos de contrato antes de archivar"), progress };
|
|
10529
|
+
}
|
|
10530
|
+
}
|
|
10327
10531
|
return { state: "ready", blockedBy, nextAction: next(`satlas archive ${change.slug}`, "Archivar el cambio y plegar los deltas"), progress };
|
|
10328
10532
|
}
|
|
10329
10533
|
function stateLabel(state) {
|
|
@@ -10388,7 +10592,7 @@ async function loadProfileFile(filePath) {
|
|
|
10388
10592
|
if (raw === void 0) return void 0;
|
|
10389
10593
|
let data;
|
|
10390
10594
|
try {
|
|
10391
|
-
data =
|
|
10595
|
+
data = parseYaml5(raw);
|
|
10392
10596
|
} catch {
|
|
10393
10597
|
return void 0;
|
|
10394
10598
|
}
|
|
@@ -10400,7 +10604,7 @@ async function loadProfilesFromDir(dir) {
|
|
|
10400
10604
|
const out = [];
|
|
10401
10605
|
for (const entry of entries) {
|
|
10402
10606
|
if (!/\.ya?ml$/i.test(entry)) continue;
|
|
10403
|
-
const profile = await loadProfileFile(
|
|
10607
|
+
const profile = await loadProfileFile(path4.join(dir, entry));
|
|
10404
10608
|
if (profile) out.push(profile);
|
|
10405
10609
|
}
|
|
10406
10610
|
return out;
|
|
@@ -10438,10 +10642,10 @@ async function detectProfiles(root, profiles) {
|
|
|
10438
10642
|
return result;
|
|
10439
10643
|
}
|
|
10440
10644
|
async function loadDetectedBest(sddDir) {
|
|
10441
|
-
const raw = await readTextIfExists(
|
|
10645
|
+
const raw = await readTextIfExists(path4.join(sddDir, "profiles", "detected.yaml"));
|
|
10442
10646
|
if (raw === void 0) return void 0;
|
|
10443
10647
|
try {
|
|
10444
|
-
const data =
|
|
10648
|
+
const data = parseYaml5(raw);
|
|
10445
10649
|
return typeof data?.best === "string" ? data.best : void 0;
|
|
10446
10650
|
} catch {
|
|
10447
10651
|
return void 0;
|
|
@@ -10476,7 +10680,7 @@ function detectionToYaml(result, generatedAt) {
|
|
|
10476
10680
|
}
|
|
10477
10681
|
return lines.join("\n") + "\n";
|
|
10478
10682
|
}
|
|
10479
|
-
var LIVING_FIXES_DIR =
|
|
10683
|
+
var LIVING_FIXES_DIR = path5.join(".sdd", "fixes");
|
|
10480
10684
|
var COMMENT_RE = /<!--[\s\S]*?-->/g;
|
|
10481
10685
|
function parseFixCovers(content) {
|
|
10482
10686
|
const clean = content.replace(COMMENT_RE, "");
|
|
@@ -10503,7 +10707,7 @@ function coversOf(value) {
|
|
|
10503
10707
|
function parseLivingFix(content, file) {
|
|
10504
10708
|
const fm = parseFrontmatter(content, file);
|
|
10505
10709
|
const data = fm.data;
|
|
10506
|
-
const base =
|
|
10710
|
+
const base = path5.basename(file).replace(/\.md$/i, "");
|
|
10507
10711
|
const slug = typeof data["slug"] === "string" && data["slug"].trim() !== "" ? data["slug"].trim() : base.replace(/^\d{4}-\d{2}-/, "");
|
|
10508
10712
|
const fix = {
|
|
10509
10713
|
slug,
|
|
@@ -10519,14 +10723,14 @@ function parseLivingFix(content, file) {
|
|
|
10519
10723
|
return fix;
|
|
10520
10724
|
}
|
|
10521
10725
|
async function archivedFixes(root, known) {
|
|
10522
|
-
const archiveDir =
|
|
10726
|
+
const archiveDir = path5.join(root, ".sdd", "changes", "archive");
|
|
10523
10727
|
const fixes = [];
|
|
10524
10728
|
for (const entry of await listDirs(archiveDir)) {
|
|
10525
|
-
const dir =
|
|
10526
|
-
const fixFile =
|
|
10729
|
+
const dir = path5.join(archiveDir, entry);
|
|
10730
|
+
const fixFile = path5.join(dir, "fix.md");
|
|
10527
10731
|
const fixRaw = await readTextIfExists(fixFile);
|
|
10528
10732
|
if (fixRaw === void 0) continue;
|
|
10529
|
-
const metaFile =
|
|
10733
|
+
const metaFile = path5.join(dir, "meta.yaml");
|
|
10530
10734
|
const metaRaw = await readTextIfExists(metaFile);
|
|
10531
10735
|
const meta = metaRaw !== void 0 ? parseChangeMeta(metaRaw, metaFile).meta : void 0;
|
|
10532
10736
|
if (meta?.lane !== "fix") continue;
|
|
@@ -10549,11 +10753,11 @@ async function archivedFixes(root, known) {
|
|
|
10549
10753
|
return fixes;
|
|
10550
10754
|
}
|
|
10551
10755
|
async function loadLivingFixes(root) {
|
|
10552
|
-
const dir =
|
|
10756
|
+
const dir = path5.join(root, LIVING_FIXES_DIR);
|
|
10553
10757
|
const entries = (await listDir(dir)).filter((entry) => entry.toLowerCase().endsWith(".md")).sort();
|
|
10554
10758
|
const fixes = [];
|
|
10555
10759
|
for (const entry of entries) {
|
|
10556
|
-
const file =
|
|
10760
|
+
const file = path5.join(dir, entry);
|
|
10557
10761
|
const content = await readTextIfExists(file);
|
|
10558
10762
|
if (content === void 0) continue;
|
|
10559
10763
|
fixes.push(parseLivingFix(content, file));
|
|
@@ -10562,10 +10766,10 @@ async function loadLivingFixes(root) {
|
|
|
10562
10766
|
return fixes.sort((a, b) => b.date.localeCompare(a.date) || b.slug.localeCompare(a.slug));
|
|
10563
10767
|
}
|
|
10564
10768
|
async function writeLivingFix(root, input) {
|
|
10565
|
-
const dir =
|
|
10769
|
+
const dir = path5.join(root, LIVING_FIXES_DIR);
|
|
10566
10770
|
const month = input.date.slice(0, 7);
|
|
10567
|
-
const file =
|
|
10568
|
-
const relativePath = toPosix(
|
|
10771
|
+
const file = path5.join(dir, `${month}-${input.slug}.md`);
|
|
10772
|
+
const relativePath = toPosix(path5.relative(root, file));
|
|
10569
10773
|
if (await exists(file)) return { file, relativePath, created: false };
|
|
10570
10774
|
const data = {
|
|
10571
10775
|
slug: input.slug,
|
|
@@ -10584,34 +10788,160 @@ ${body}
|
|
|
10584
10788
|
await writeText(file, text);
|
|
10585
10789
|
return { file, relativePath, created: true };
|
|
10586
10790
|
}
|
|
10791
|
+
var LINKS_FILE = "links.yaml";
|
|
10792
|
+
function parseLinksDocument(raw, file) {
|
|
10793
|
+
let data;
|
|
10794
|
+
try {
|
|
10795
|
+
data = parseYaml6(raw);
|
|
10796
|
+
} catch (error) {
|
|
10797
|
+
return { diagnostics: [diag("ATLAS-LINK-001", "error", `links.yaml ilegible: ${error.message}`, { path: file })] };
|
|
10798
|
+
}
|
|
10799
|
+
const doc = data;
|
|
10800
|
+
if (!doc || typeof doc !== "object" || !Array.isArray(doc.links)) {
|
|
10801
|
+
return { diagnostics: [diag("ATLAS-LINK-001", "error", "links.yaml inv\xE1lido: falta la lista de enlaces", { path: file })] };
|
|
10802
|
+
}
|
|
10803
|
+
const links = [];
|
|
10804
|
+
const diagnostics = [];
|
|
10805
|
+
for (const item of doc.links) {
|
|
10806
|
+
const entry = item;
|
|
10807
|
+
if (typeof entry?.name !== "string" || typeof entry?.path !== "string" || entry.name.trim() === "" || entry.path.trim() === "") {
|
|
10808
|
+
diagnostics.push(diag("ATLAS-LINK-001", "error", "Enlace inv\xE1lido en links.yaml: cada enlace necesita nombre y ruta", { path: file }));
|
|
10809
|
+
continue;
|
|
10810
|
+
}
|
|
10811
|
+
links.push({ name: entry.name.trim(), path: entry.path.trim() });
|
|
10812
|
+
}
|
|
10813
|
+
return { document: { schema_version: typeof doc.schema_version === "number" ? doc.schema_version : 1, links }, diagnostics };
|
|
10814
|
+
}
|
|
10815
|
+
async function readLinks(root) {
|
|
10816
|
+
const file = path6.join(root, ".sdd", LINKS_FILE);
|
|
10817
|
+
const raw = await readTextIfExists(file);
|
|
10818
|
+
if (raw === void 0) return { document: { schema_version: 1, links: [] }, file, diagnostics: [] };
|
|
10819
|
+
const parsed = parseLinksDocument(raw, file);
|
|
10820
|
+
return { document: parsed.document ?? { schema_version: 1, links: [] }, file, diagnostics: parsed.diagnostics };
|
|
10821
|
+
}
|
|
10822
|
+
async function writeLinks(root, document2) {
|
|
10823
|
+
const file = path6.join(root, ".sdd", LINKS_FILE);
|
|
10824
|
+
const lines = ["# Enlaces a otros proyectos (specs compartidas en solo lectura). Generado por `satlas link`.", stringifyYaml4(document2, { lineWidth: 120 }).trimEnd(), ""];
|
|
10825
|
+
await writeText(file, lines.join("\n"));
|
|
10826
|
+
}
|
|
10827
|
+
function resolveLinkPath(root, linkPath) {
|
|
10828
|
+
return path6.isAbsolute(linkPath) ? path6.normalize(linkPath) : path6.resolve(root, linkPath);
|
|
10829
|
+
}
|
|
10830
|
+
async function linkedSpecs(absPath) {
|
|
10831
|
+
const specsDir = path6.join(absPath, ".sdd", "specs");
|
|
10832
|
+
const specs = [];
|
|
10833
|
+
for (const domain of await listDirs(specsDir)) {
|
|
10834
|
+
const file = path6.join(specsDir, domain, "spec.md");
|
|
10835
|
+
const content = await readTextIfExists(file);
|
|
10836
|
+
if (content === void 0) continue;
|
|
10837
|
+
specs.push({ domain, path: file, spec: parseSpecFile(content, file) });
|
|
10838
|
+
}
|
|
10839
|
+
return specs;
|
|
10840
|
+
}
|
|
10841
|
+
async function loadLinks(root) {
|
|
10842
|
+
const { document: document2, diagnostics } = await readLinks(root);
|
|
10843
|
+
const entries = [];
|
|
10844
|
+
const specs = [];
|
|
10845
|
+
const unavailable = [];
|
|
10846
|
+
for (const entry of document2.links) {
|
|
10847
|
+
const abs = resolveLinkPath(root, entry.path);
|
|
10848
|
+
const sddDir = path6.join(abs, ".sdd");
|
|
10849
|
+
const available = await isDirectory(sddDir) && await isDirectory(path6.join(sddDir, "specs"));
|
|
10850
|
+
if (!available) {
|
|
10851
|
+
unavailable.push(entry.name);
|
|
10852
|
+
entries.push({ name: entry.name, path: abs, available: false, requirements: 0, domains: [], error: "el enlace no est\xE1 disponible" });
|
|
10853
|
+
continue;
|
|
10854
|
+
}
|
|
10855
|
+
const linked = await linkedSpecs(abs);
|
|
10856
|
+
specs.push(...linked);
|
|
10857
|
+
entries.push({
|
|
10858
|
+
name: entry.name,
|
|
10859
|
+
path: abs,
|
|
10860
|
+
available: true,
|
|
10861
|
+
requirements: linked.reduce((total, spec) => total + spec.spec.requirements.length, 0),
|
|
10862
|
+
domains: linked.map((spec) => spec.domain)
|
|
10863
|
+
});
|
|
10864
|
+
}
|
|
10865
|
+
void diagnostics;
|
|
10866
|
+
return { entries, specs, unavailable };
|
|
10867
|
+
}
|
|
10868
|
+
async function addLink(root, opts) {
|
|
10869
|
+
const abs = resolveLinkPath(root, opts.path);
|
|
10870
|
+
const sddDir = path6.join(abs, ".sdd");
|
|
10871
|
+
if (!await isDirectory(sddDir)) {
|
|
10872
|
+
return {
|
|
10873
|
+
diagnostics: [
|
|
10874
|
+
diag("ATLAS-LINK-002", "error", `El enlace no es un proyecto inicializado: ${abs}`, {
|
|
10875
|
+
suggestion: "Indica la ruta de un proyecto con su estado inicializado (satlas init)"
|
|
10876
|
+
})
|
|
10877
|
+
]
|
|
10878
|
+
};
|
|
10879
|
+
}
|
|
10880
|
+
const { document: document2 } = await readLinks(root);
|
|
10881
|
+
let name = opts.name?.trim();
|
|
10882
|
+
if (!name) {
|
|
10883
|
+
const linkedConfig = await loadConfig(sddDir);
|
|
10884
|
+
name = linkedConfig.config.project.name || path6.basename(abs);
|
|
10885
|
+
}
|
|
10886
|
+
const entry = { name, path: toPosix(abs) };
|
|
10887
|
+
const existing = document2.links.findIndex((link) => link.name === name || resolveLinkPath(root, link.path) === abs);
|
|
10888
|
+
if (existing >= 0) document2.links[existing] = entry;
|
|
10889
|
+
else document2.links.push(entry);
|
|
10890
|
+
await writeLinks(root, document2);
|
|
10891
|
+
return { entry, diagnostics: [] };
|
|
10892
|
+
}
|
|
10893
|
+
async function removeLink(root, ref) {
|
|
10894
|
+
const { document: document2 } = await readLinks(root);
|
|
10895
|
+
const index = document2.links.findIndex((link) => link.name === ref || link.path === ref || resolveLinkPath(root, link.path) === resolveLinkPath(root, ref));
|
|
10896
|
+
if (index < 0) {
|
|
10897
|
+
return {
|
|
10898
|
+
diagnostics: [diag("ATLAS-LINK-003", "warning", `No existe un enlace "${ref}"`, { suggestion: "Consulta los enlaces con `satlas link list`" })]
|
|
10899
|
+
};
|
|
10900
|
+
}
|
|
10901
|
+
const [removed] = document2.links.splice(index, 1);
|
|
10902
|
+
await writeLinks(root, document2);
|
|
10903
|
+
return { removed: removed?.name, diagnostics: [] };
|
|
10904
|
+
}
|
|
10905
|
+
function linkedRequirementIds(state) {
|
|
10906
|
+
if (!state) return [];
|
|
10907
|
+
const ids = [];
|
|
10908
|
+
for (const spec of state.specs) {
|
|
10909
|
+
for (const requirement of spec.spec.requirements) ids.push(requirement.id);
|
|
10910
|
+
}
|
|
10911
|
+
return ids;
|
|
10912
|
+
}
|
|
10913
|
+
function linkedTraceInput(workspace) {
|
|
10914
|
+
if (!workspace.links || workspace.links.entries.length === 0) return void 0;
|
|
10915
|
+
return { ids: linkedRequirementIds(workspace.links), unavailable: workspace.links.unavailable };
|
|
10916
|
+
}
|
|
10587
10917
|
var SDD_DIR = ".sdd";
|
|
10588
10918
|
async function findWorkspaceRoot(start) {
|
|
10589
|
-
let current =
|
|
10919
|
+
let current = path7.resolve(start);
|
|
10590
10920
|
for (let i = 0; i < 40; i += 1) {
|
|
10591
|
-
if (await isDirectory(
|
|
10592
|
-
const parent =
|
|
10921
|
+
if (await isDirectory(path7.join(current, SDD_DIR))) return current;
|
|
10922
|
+
const parent = path7.dirname(current);
|
|
10593
10923
|
if (parent === current) return void 0;
|
|
10594
10924
|
current = parent;
|
|
10595
10925
|
}
|
|
10596
10926
|
return void 0;
|
|
10597
10927
|
}
|
|
10598
10928
|
async function loadApprovals(sddDir) {
|
|
10599
|
-
const file =
|
|
10929
|
+
const file = path7.join(sddDir, "approvals.yaml");
|
|
10600
10930
|
const raw = await readTextIfExists(file);
|
|
10601
10931
|
if (raw === void 0) return { byArtifact: /* @__PURE__ */ new Map(), diagnostics: [] };
|
|
10602
10932
|
const parsed = parseApprovals(raw, file);
|
|
10603
10933
|
const map = /* @__PURE__ */ new Map();
|
|
10604
10934
|
for (const a of parsed.approvals?.approvals ?? []) {
|
|
10605
|
-
map.set(a.artifact.split(
|
|
10935
|
+
map.set(a.artifact.split(path7.sep).join("/"), { hash: a.artifactHash, by: a.approvedBy, at: a.approvedAt });
|
|
10606
10936
|
}
|
|
10607
10937
|
return { byArtifact: map, diagnostics: parsed.diagnostics };
|
|
10608
10938
|
}
|
|
10609
10939
|
async function loadSpecs(sddDir) {
|
|
10610
|
-
const specsDir =
|
|
10940
|
+
const specsDir = path7.join(sddDir, "specs");
|
|
10611
10941
|
const domains = await listDirs(specsDir);
|
|
10612
10942
|
const out = [];
|
|
10613
10943
|
for (const domain of domains) {
|
|
10614
|
-
const file =
|
|
10944
|
+
const file = path7.join(specsDir, domain, "spec.md");
|
|
10615
10945
|
if (!await exists(file)) continue;
|
|
10616
10946
|
const content = await readText(file);
|
|
10617
10947
|
out.push({ domain, path: file, spec: parseSpecFile(content, file) });
|
|
@@ -10619,10 +10949,10 @@ async function loadSpecs(sddDir) {
|
|
|
10619
10949
|
return out;
|
|
10620
10950
|
}
|
|
10621
10951
|
async function loadChange(root, slug, relDir) {
|
|
10622
|
-
const dir =
|
|
10952
|
+
const dir = path7.join(root, SDD_DIR, "changes", relDir ?? slug);
|
|
10623
10953
|
const diagnostics = [];
|
|
10624
10954
|
const change = { slug, dir, diagnostics };
|
|
10625
|
-
const metaFile =
|
|
10955
|
+
const metaFile = path7.join(dir, "meta.yaml");
|
|
10626
10956
|
const metaRaw = await readTextIfExists(metaFile);
|
|
10627
10957
|
if (metaRaw === void 0) {
|
|
10628
10958
|
diagnostics.push(diag("ATLAS-FILES-001", "error", `El cambio "${slug}" no tiene meta.yaml`, { path: metaFile, suggestion: "Crea meta.yaml con slug, lane y dominio" }));
|
|
@@ -10631,7 +10961,7 @@ async function loadChange(root, slug, relDir) {
|
|
|
10631
10961
|
diagnostics.push(...parsed.diagnostics);
|
|
10632
10962
|
if (parsed.meta) change.meta = parsed.meta;
|
|
10633
10963
|
}
|
|
10634
|
-
const deltaFile =
|
|
10964
|
+
const deltaFile = path7.join(dir, "spec.md");
|
|
10635
10965
|
const deltaRaw = await readTextIfExists(deltaFile);
|
|
10636
10966
|
if (deltaRaw !== void 0) {
|
|
10637
10967
|
change.delta = parseDelta(deltaRaw, deltaFile);
|
|
@@ -10639,32 +10969,32 @@ async function loadChange(root, slug, relDir) {
|
|
|
10639
10969
|
} else {
|
|
10640
10970
|
diagnostics.push(diag("ATLAS-FILES-002", "warning", `El cambio "${slug}" no tiene spec.md (delta)`, { path: deltaFile }));
|
|
10641
10971
|
}
|
|
10642
|
-
const planFile =
|
|
10972
|
+
const planFile = path7.join(dir, "plan.md");
|
|
10643
10973
|
if (await exists(planFile)) change.planPath = planFile;
|
|
10644
|
-
const reviewFile =
|
|
10974
|
+
const reviewFile = path7.join(dir, "review.md");
|
|
10645
10975
|
if (await exists(reviewFile)) change.reviewPath = reviewFile;
|
|
10646
|
-
const presentationFile =
|
|
10976
|
+
const presentationFile = path7.join(dir, "presentation", "index.html");
|
|
10647
10977
|
if (await exists(presentationFile)) change.presentationPath = presentationFile;
|
|
10648
|
-
const tasksFile =
|
|
10978
|
+
const tasksFile = path7.join(dir, "tasks.md");
|
|
10649
10979
|
const tasksRaw = await readTextIfExists(tasksFile);
|
|
10650
10980
|
if (tasksRaw !== void 0) {
|
|
10651
10981
|
change.tasks = parseTasksFile(tasksRaw, tasksFile);
|
|
10652
10982
|
diagnostics.push(...change.tasks.diagnostics);
|
|
10653
10983
|
}
|
|
10654
|
-
const verifyFile =
|
|
10984
|
+
const verifyFile = path7.join(dir, "verify.md");
|
|
10655
10985
|
const verifyRaw = await readTextIfExists(verifyFile);
|
|
10656
10986
|
if (verifyRaw !== void 0) {
|
|
10657
10987
|
change.verify = parseVerifyFile(verifyRaw, verifyFile);
|
|
10658
10988
|
diagnostics.push(...change.verify.diagnostics);
|
|
10659
10989
|
}
|
|
10660
|
-
const fixFile =
|
|
10990
|
+
const fixFile = path7.join(dir, "fix.md");
|
|
10661
10991
|
const fixRaw = await readTextIfExists(fixFile);
|
|
10662
10992
|
if (fixRaw !== void 0) {
|
|
10663
10993
|
change.fix = parseVerifyFile(fixRaw, fixFile);
|
|
10664
10994
|
diagnostics.push(...change.fix.diagnostics);
|
|
10665
10995
|
change.fixCovers = parseFixCovers(fixRaw);
|
|
10666
10996
|
}
|
|
10667
|
-
const clarifyFile =
|
|
10997
|
+
const clarifyFile = path7.join(dir, "clarify.md");
|
|
10668
10998
|
const clarifyRaw = await readTextIfExists(clarifyFile);
|
|
10669
10999
|
if (clarifyRaw !== void 0) {
|
|
10670
11000
|
change.clarify = parseClarify(clarifyRaw, clarifyFile);
|
|
@@ -10673,21 +11003,23 @@ async function loadChange(root, slug, relDir) {
|
|
|
10673
11003
|
}
|
|
10674
11004
|
const docsPaths = [];
|
|
10675
11005
|
for (const name of ["tecnica.md", "manual.md"]) {
|
|
10676
|
-
const docFile =
|
|
11006
|
+
const docFile = path7.join(dir, "docs", name);
|
|
10677
11007
|
if (await exists(docFile)) docsPaths.push(docFile);
|
|
10678
11008
|
}
|
|
10679
11009
|
if (docsPaths.length > 0) change.docsPaths = docsPaths;
|
|
10680
|
-
const
|
|
11010
|
+
const contracts = await loadContracts(dir);
|
|
11011
|
+
if (contracts.files.length > 0) change.contracts = contracts;
|
|
11012
|
+
const mockupManifest = path7.join(dir, "mockups", "manifest.yaml");
|
|
10681
11013
|
if (await exists(mockupManifest)) change.mockupManifestPath = mockupManifest;
|
|
10682
11014
|
return change;
|
|
10683
11015
|
}
|
|
10684
11016
|
async function listChangeSlugs(root, opts = {}) {
|
|
10685
|
-
const changesDir =
|
|
11017
|
+
const changesDir = path7.join(root, SDD_DIR, "changes");
|
|
10686
11018
|
const dirs = await listDirs(changesDir);
|
|
10687
11019
|
return dirs.filter((d) => opts.includeArchived ? true : d !== "archive");
|
|
10688
11020
|
}
|
|
10689
11021
|
async function loadWorkspace(root) {
|
|
10690
|
-
const sddDir =
|
|
11022
|
+
const sddDir = path7.join(root, SDD_DIR);
|
|
10691
11023
|
const { config, diagnostics: configDiags } = await loadConfig(sddDir);
|
|
10692
11024
|
const diagnostics = [...configDiags];
|
|
10693
11025
|
const specs = await loadSpecs(sddDir);
|
|
@@ -10700,20 +11032,24 @@ async function loadWorkspace(root) {
|
|
|
10700
11032
|
diagnostics.push(...change.diagnostics);
|
|
10701
11033
|
}
|
|
10702
11034
|
const archived = [];
|
|
10703
|
-
const archivedDir =
|
|
11035
|
+
const archivedDir = path7.join(sddDir, "changes", "archive");
|
|
10704
11036
|
if (await isDirectory(archivedDir)) {
|
|
10705
11037
|
for (const entry of await listDirs(archivedDir)) {
|
|
10706
|
-
archived.push(await loadChange(root, entry.replace(/^\d{4}-\d{2}-/, ""),
|
|
11038
|
+
archived.push(await loadChange(root, entry.replace(/^\d{4}-\d{2}-/, ""), path7.join("archive", entry)));
|
|
10707
11039
|
}
|
|
10708
11040
|
}
|
|
10709
|
-
|
|
11041
|
+
const workspace = { root, sddDir, specs, changes, archived, diagnostics };
|
|
11042
|
+
if (await exists(path7.join(sddDir, LINKS_FILE))) {
|
|
11043
|
+
workspace.links = await loadLinks(root);
|
|
11044
|
+
}
|
|
11045
|
+
return { workspace, config };
|
|
10710
11046
|
}
|
|
10711
11047
|
async function ensureSddDirs(sddDir) {
|
|
10712
11048
|
const created = [];
|
|
10713
|
-
const dirs = ["specs", "changes", "runs", "metrics", "fixes",
|
|
11049
|
+
const dirs = ["specs", "changes", "runs", "metrics", "fixes", path7.join("profiles", "custom")];
|
|
10714
11050
|
const { ensureDir: ensureDir2 } = await import("./fsx-VF2P7ALA-XKEWTMDH.js");
|
|
10715
11051
|
for (const d of dirs) {
|
|
10716
|
-
const full =
|
|
11052
|
+
const full = path7.join(sddDir, d);
|
|
10717
11053
|
if (!await exists(full)) {
|
|
10718
11054
|
await ensureDir2(full);
|
|
10719
11055
|
created.push(full);
|
|
@@ -11112,7 +11448,7 @@ function changeMetaYaml(meta) {
|
|
|
11112
11448
|
if (meta.owner) doc["owner"] = meta.owner;
|
|
11113
11449
|
if (meta.mockups) doc["mockups"] = meta.mockups;
|
|
11114
11450
|
return `# Estado del cambio. La fase se DERIVA de los artefactos; aqu\xED solo hechos.
|
|
11115
|
-
` +
|
|
11451
|
+
` + stringifyYaml5(doc, { lineWidth: 120 });
|
|
11116
11452
|
}
|
|
11117
11453
|
function indexMarkdown(input) {
|
|
11118
11454
|
const es = input.language !== "en";
|
|
@@ -11286,8 +11622,8 @@ function issueBody(input) {
|
|
|
11286
11622
|
lines.push(`- Siguiente: \`${input.next}\` \u2014 ${input.nextDescription}`);
|
|
11287
11623
|
lines.push("");
|
|
11288
11624
|
lines.push("### Artefactos");
|
|
11289
|
-
lines.push(`- Spec: \`${
|
|
11290
|
-
if (change.planPath) lines.push(`- Plan: \`${
|
|
11625
|
+
lines.push(`- Spec: \`${path8.posix.join(".sdd", "changes", change.slug, "spec.md")}\``);
|
|
11626
|
+
if (change.planPath) lines.push(`- Plan: \`${path8.posix.join(".sdd", "changes", change.slug, "plan.md")}\``);
|
|
11291
11627
|
if (change.tasks) lines.push(`- Tareas: ${change.tasks.counts.done}/${change.tasks.counts.total}`);
|
|
11292
11628
|
if (change.verify) lines.push(`- Evidencia registrada: ${change.verify.evidence.length}`);
|
|
11293
11629
|
lines.push("");
|
|
@@ -11351,14 +11687,14 @@ async function issueLabels(root, number, runner = defaultRunner) {
|
|
|
11351
11687
|
}
|
|
11352
11688
|
}
|
|
11353
11689
|
async function patchChangeMeta(root, slug, patch) {
|
|
11354
|
-
const file =
|
|
11690
|
+
const file = path8.join(path8.resolve(root), ".sdd", "changes", slug, "meta.yaml");
|
|
11355
11691
|
const raw = await readTextIfExists(file);
|
|
11356
11692
|
if (raw === void 0) {
|
|
11357
|
-
return { path: file, diagnostics: [diag("ATLAS-GH-007", "error", `No existe ${
|
|
11693
|
+
return { path: file, diagnostics: [diag("ATLAS-GH-007", "error", `No existe ${path8.relative(root, file)}`)] };
|
|
11358
11694
|
}
|
|
11359
11695
|
let data;
|
|
11360
11696
|
try {
|
|
11361
|
-
data =
|
|
11697
|
+
data = parseYaml7(raw) ?? {};
|
|
11362
11698
|
} catch (err) {
|
|
11363
11699
|
return { path: file, diagnostics: [diag("ATLAS-GH-007", "error", `meta.yaml inv\xE1lido: ${err.message}`)] };
|
|
11364
11700
|
}
|
|
@@ -11408,7 +11744,7 @@ async function approveFromGithub(opts) {
|
|
|
11408
11744
|
const { signApproval: signApproval2 } = await import("./approvals-2PO6223I-DENKLEXU.js");
|
|
11409
11745
|
const signed = await signApproval2({
|
|
11410
11746
|
root: opts.root,
|
|
11411
|
-
artifact:
|
|
11747
|
+
artifact: path8.posix.join("changes", opts.slug, "spec.md"),
|
|
11412
11748
|
by,
|
|
11413
11749
|
channel: "tracker",
|
|
11414
11750
|
note: `Aprobado v\xEDa GitHub issue #${number} (etiqueta "${label}")`
|
|
@@ -11503,10 +11839,10 @@ function buildEvidenceYaml(evidence) {
|
|
|
11503
11839
|
return lines.join("\n");
|
|
11504
11840
|
}
|
|
11505
11841
|
async function recordEvidence(opts) {
|
|
11506
|
-
const root =
|
|
11842
|
+
const root = path9.resolve(opts.root);
|
|
11507
11843
|
const slug = opts.slug;
|
|
11508
11844
|
const fileKind = opts.file ?? "verify";
|
|
11509
|
-
const file =
|
|
11845
|
+
const file = path9.join(root, ".sdd", "changes", slug, fileKind === "fix" ? "fix.md" : "verify.md");
|
|
11510
11846
|
const diagnostics = [];
|
|
11511
11847
|
const scenario = opts.scenario.toUpperCase();
|
|
11512
11848
|
if (!SCENARIO_ID_RE.test(scenario)) {
|
|
@@ -11596,7 +11932,7 @@ function generateRunId(now = /* @__PURE__ */ new Date()) {
|
|
|
11596
11932
|
return `${iso.slice(0, 8)}-${iso.slice(8, 14)}-${randomBytes(3).toString("hex")}`;
|
|
11597
11933
|
}
|
|
11598
11934
|
function runsDir(root) {
|
|
11599
|
-
return
|
|
11935
|
+
return path10.join(path10.resolve(root), ".sdd", "runs");
|
|
11600
11936
|
}
|
|
11601
11937
|
async function createRun(root, slug, phase, inputs, now = /* @__PURE__ */ new Date()) {
|
|
11602
11938
|
const runId = generateRunId(now);
|
|
@@ -11609,25 +11945,25 @@ async function createRun(root, slug, phase, inputs, now = /* @__PURE__ */ new Da
|
|
|
11609
11945
|
updatedAt: localStamp(now)
|
|
11610
11946
|
};
|
|
11611
11947
|
if (inputs) state.inputs = inputs;
|
|
11612
|
-
const dir =
|
|
11948
|
+
const dir = path10.join(runsDir(root), runId);
|
|
11613
11949
|
await ensureDir(dir);
|
|
11614
|
-
await writeText(
|
|
11950
|
+
await writeText(path10.join(dir, "state.json"), `${JSON.stringify(state, null, 2)}
|
|
11615
11951
|
`);
|
|
11616
|
-
await writeText(
|
|
11952
|
+
await writeText(path10.join(dir, "events.jsonl"), "");
|
|
11617
11953
|
await appendRunEvent(root, runId, "run_started", { slug, phase });
|
|
11618
11954
|
return state;
|
|
11619
11955
|
}
|
|
11620
11956
|
async function appendRunEvent(root, runId, type, data, now = /* @__PURE__ */ new Date()) {
|
|
11621
11957
|
const event = { eventId: randomBytes(4).toString("hex"), at: localStamp(now), type };
|
|
11622
11958
|
if (data) event.data = data;
|
|
11623
|
-
const file =
|
|
11959
|
+
const file = path10.join(runsDir(root), runId, "events.jsonl");
|
|
11624
11960
|
const previous = await readTextIfExists(file) ?? "";
|
|
11625
11961
|
await writeText(file, `${previous}${JSON.stringify(event)}
|
|
11626
11962
|
`);
|
|
11627
11963
|
return event;
|
|
11628
11964
|
}
|
|
11629
11965
|
async function updateRunStatus(root, runId, status, now = /* @__PURE__ */ new Date()) {
|
|
11630
|
-
const file =
|
|
11966
|
+
const file = path10.join(runsDir(root), runId, "state.json");
|
|
11631
11967
|
const raw = await readTextIfExists(file);
|
|
11632
11968
|
if (raw === void 0) return;
|
|
11633
11969
|
const state = JSON.parse(raw);
|
|
@@ -11637,13 +11973,13 @@ async function updateRunStatus(root, runId, status, now = /* @__PURE__ */ new Da
|
|
|
11637
11973
|
`);
|
|
11638
11974
|
}
|
|
11639
11975
|
async function readRun(root, runId) {
|
|
11640
|
-
const dir =
|
|
11641
|
-
const stateRaw = await readTextIfExists(
|
|
11976
|
+
const dir = path10.join(runsDir(root), runId);
|
|
11977
|
+
const stateRaw = await readTextIfExists(path10.join(dir, "state.json"));
|
|
11642
11978
|
if (stateRaw === void 0) {
|
|
11643
11979
|
return { diagnostics: [diag("ATLAS-RUN-001", "error", `No existe el run ${runId}`, { path: dir })] };
|
|
11644
11980
|
}
|
|
11645
11981
|
const state = JSON.parse(stateRaw);
|
|
11646
|
-
const eventsRaw = await readTextIfExists(
|
|
11982
|
+
const eventsRaw = await readTextIfExists(path10.join(dir, "events.jsonl")) ?? "";
|
|
11647
11983
|
const events = [];
|
|
11648
11984
|
for (const line of eventsRaw.split("\n")) {
|
|
11649
11985
|
if (line.trim() === "") continue;
|
|
@@ -11660,7 +11996,7 @@ async function listRuns(root, filter = {}) {
|
|
|
11660
11996
|
const ids = await listDirs(dir);
|
|
11661
11997
|
const out = [];
|
|
11662
11998
|
for (const id of ids) {
|
|
11663
|
-
const raw = await readTextIfExists(
|
|
11999
|
+
const raw = await readTextIfExists(path10.join(dir, id, "state.json"));
|
|
11664
12000
|
if (raw === void 0) continue;
|
|
11665
12001
|
try {
|
|
11666
12002
|
const state = JSON.parse(raw);
|
|
@@ -11695,20 +12031,20 @@ var mockupManifestSchema = z4.object({
|
|
|
11695
12031
|
});
|
|
11696
12032
|
var TOKEN_CANDIDATES = ["design/tokens.json", "DESIGN.md", "design/DESIGN.md", ".sdd/design/tokens.json"];
|
|
11697
12033
|
function mockupsDir(root, slug) {
|
|
11698
|
-
return
|
|
12034
|
+
return path11.join(path11.resolve(root), ".sdd", "changes", slug, "mockups");
|
|
11699
12035
|
}
|
|
11700
12036
|
async function tokensFile(root) {
|
|
11701
12037
|
for (const candidate of TOKEN_CANDIDATES) {
|
|
11702
|
-
const abs =
|
|
12038
|
+
const abs = path11.join(root, candidate);
|
|
11703
12039
|
if (await exists(abs)) return candidate;
|
|
11704
12040
|
}
|
|
11705
12041
|
return void 0;
|
|
11706
12042
|
}
|
|
11707
12043
|
async function computeInputsHash(root, change) {
|
|
11708
|
-
const deltaPath =
|
|
12044
|
+
const deltaPath = path11.join(change.dir, "spec.md");
|
|
11709
12045
|
const delta = await readTextIfExists(deltaPath) ?? "";
|
|
11710
12046
|
const tokens = await tokensFile(root);
|
|
11711
|
-
const tokensContent = tokens ? await readTextIfExists(
|
|
12047
|
+
const tokensContent = tokens ? await readTextIfExists(path11.join(root, tokens)) ?? "" : "";
|
|
11712
12048
|
return artifactHash(`${delta}
|
|
11713
12049
|
---tokens---
|
|
11714
12050
|
${tokensContent}`);
|
|
@@ -11733,12 +12069,12 @@ function planMockups(change, platform = "web") {
|
|
|
11733
12069
|
return { platform, screens };
|
|
11734
12070
|
}
|
|
11735
12071
|
async function readMockupManifest(root, slug) {
|
|
11736
|
-
const file =
|
|
12072
|
+
const file = path11.join(mockupsDir(root, slug), "manifest.yaml");
|
|
11737
12073
|
const raw = await readTextIfExists(file);
|
|
11738
12074
|
if (raw === void 0) return { diagnostics: [] };
|
|
11739
12075
|
let data;
|
|
11740
12076
|
try {
|
|
11741
|
-
data =
|
|
12077
|
+
data = parseYaml8(raw);
|
|
11742
12078
|
} catch (err) {
|
|
11743
12079
|
return { path: file, diagnostics: [diag("ATLAS-MKP-002", "error", `manifest.yaml inv\xE1lido: ${err.message}`, { path: file })] };
|
|
11744
12080
|
}
|
|
@@ -11825,7 +12161,7 @@ async function checkMockups(root, slug, change) {
|
|
|
11825
12161
|
const { manifest, path: manifestPath, diagnostics } = await readMockupManifest(root, slug);
|
|
11826
12162
|
const findings = [...diagnostics];
|
|
11827
12163
|
if (!manifest || !manifestPath) {
|
|
11828
|
-
return { findings: [...findings, diag("LINT-MKP-004", "error", "No existe mockups/manifest.yaml", { path:
|
|
12164
|
+
return { findings: [...findings, diag("LINT-MKP-004", "error", "No existe mockups/manifest.yaml", { path: path11.join(dir, "manifest.yaml") })], stale: false };
|
|
11829
12165
|
}
|
|
11830
12166
|
const known = /* @__PURE__ */ new Set();
|
|
11831
12167
|
for (const req of [...change.delta?.added ?? [], ...change.delta?.modified ?? []]) {
|
|
@@ -11833,12 +12169,12 @@ async function checkMockups(root, slug, change) {
|
|
|
11833
12169
|
}
|
|
11834
12170
|
findings.push(...lintMockupManifest(manifest, known, manifestPath));
|
|
11835
12171
|
for (const screen of manifest.screens) {
|
|
11836
|
-
const html = await readTextIfExists(
|
|
12172
|
+
const html = await readTextIfExists(path11.join(dir, screen.file));
|
|
11837
12173
|
if (html === void 0) {
|
|
11838
|
-
findings.push(diag("ATLAS-MKP-004", "error", `Falta el archivo del mockup: ${screen.file}`, { path:
|
|
12174
|
+
findings.push(diag("ATLAS-MKP-004", "error", `Falta el archivo del mockup: ${screen.file}`, { path: path11.join(dir, screen.file) }));
|
|
11839
12175
|
continue;
|
|
11840
12176
|
}
|
|
11841
|
-
findings.push(...lintMockupHtml(html,
|
|
12177
|
+
findings.push(...lintMockupHtml(html, path11.join(dir, screen.file)));
|
|
11842
12178
|
}
|
|
11843
12179
|
const inputsHash = await computeInputsHash(root, change);
|
|
11844
12180
|
const stale = manifest.inputsHash !== void 0 && manifest.inputsHash !== inputsHash;
|
|
@@ -11853,7 +12189,7 @@ async function checkMockups(root, slug, change) {
|
|
|
11853
12189
|
return { findings, stale, manifest, manifestPath };
|
|
11854
12190
|
}
|
|
11855
12191
|
async function writeMockupPlan(root, slug, plan, now = /* @__PURE__ */ new Date()) {
|
|
11856
|
-
const file =
|
|
12192
|
+
const file = path11.join(mockupsDir(root, slug), "plan.yaml");
|
|
11857
12193
|
if (await exists(file)) return file;
|
|
11858
12194
|
const doc = {
|
|
11859
12195
|
schema_version: 1,
|
|
@@ -11861,11 +12197,11 @@ async function writeMockupPlan(root, slug, plan, now = /* @__PURE__ */ new Date(
|
|
|
11861
12197
|
platform: plan.platform,
|
|
11862
12198
|
screens: plan.screens.map((s) => ({ id: s.id, title: s.title, file: s.file, illustrates: s.illustrates, states: s.states, breakpoints: s.breakpoints }))
|
|
11863
12199
|
};
|
|
11864
|
-
await writeText(file,
|
|
12200
|
+
await writeText(file, stringifyYaml6(doc, { lineWidth: 120 }));
|
|
11865
12201
|
return file;
|
|
11866
12202
|
}
|
|
11867
12203
|
async function writeMockupManifest(root, slug, plan, inputsHash, now = /* @__PURE__ */ new Date()) {
|
|
11868
|
-
const file =
|
|
12204
|
+
const file = path11.join(mockupsDir(root, slug), "manifest.yaml");
|
|
11869
12205
|
if (await exists(file)) return file;
|
|
11870
12206
|
const doc = {
|
|
11871
12207
|
schema_version: 1,
|
|
@@ -11877,16 +12213,16 @@ async function writeMockupManifest(root, slug, plan, inputsHash, now = /* @__PUR
|
|
|
11877
12213
|
screens: plan.screens.map((s) => ({ id: s.id, title: s.title, file: s.file, illustrates: s.illustrates, states: s.states, breakpoints: s.breakpoints })),
|
|
11878
12214
|
screenshots: []
|
|
11879
12215
|
};
|
|
11880
|
-
await writeText(file,
|
|
12216
|
+
await writeText(file, stringifyYaml6(doc, { lineWidth: 120 }));
|
|
11881
12217
|
return file;
|
|
11882
12218
|
}
|
|
11883
12219
|
async function updateMockupScreenshots(root, slug, screenshots) {
|
|
11884
|
-
const file =
|
|
12220
|
+
const file = path11.join(mockupsDir(root, slug), "manifest.yaml");
|
|
11885
12221
|
const raw = await readTextIfExists(file);
|
|
11886
12222
|
if (raw === void 0) return void 0;
|
|
11887
|
-
const data =
|
|
12223
|
+
const data = parseYaml8(raw) ?? {};
|
|
11888
12224
|
data["screenshots"] = screenshots;
|
|
11889
|
-
await writeText(file,
|
|
12225
|
+
await writeText(file, stringifyYaml6(data, { lineWidth: 120 }));
|
|
11890
12226
|
return file;
|
|
11891
12227
|
}
|
|
11892
12228
|
async function captureMockups(root, slug, manifest, now = /* @__PURE__ */ new Date()) {
|
|
@@ -11911,9 +12247,9 @@ async function captureMockups(root, slug, manifest, now = /* @__PURE__ */ new Da
|
|
|
11911
12247
|
const page2 = await browser.newPage();
|
|
11912
12248
|
for (const bp of screen.breakpoints && screen.breakpoints.length > 0 ? screen.breakpoints : [1440]) {
|
|
11913
12249
|
await page2.setViewportSize({ width: bp, height: 900 });
|
|
11914
|
-
await page2.goto(`file://${
|
|
11915
|
-
const rel =
|
|
11916
|
-
await page2.screenshot({ path:
|
|
12250
|
+
await page2.goto(`file://${path11.join(dir, screen.file).split(path11.sep).join("/")}`);
|
|
12251
|
+
const rel = path11.posix.join("screens", `${screen.id}-${bp}.png`);
|
|
12252
|
+
await page2.screenshot({ path: path11.join(dir, rel) });
|
|
11917
12253
|
screenshots.push(rel);
|
|
11918
12254
|
}
|
|
11919
12255
|
await page2.close();
|
|
@@ -11929,7 +12265,7 @@ async function mockupsReady(root, slug, change) {
|
|
|
11929
12265
|
return Boolean(check.manifest && check.manifest.screens.length > 0 && !check.stale);
|
|
11930
12266
|
}
|
|
11931
12267
|
async function setMockupRequirement(root, slug, value) {
|
|
11932
|
-
const file =
|
|
12268
|
+
const file = path11.join(root, ".sdd", "changes", slug, "meta.yaml");
|
|
11933
12269
|
const raw = await readTextIfExists(file) ?? `schema_version: 1
|
|
11934
12270
|
slug: ${slug}
|
|
11935
12271
|
lane: standard
|
|
@@ -12159,17 +12495,17 @@ ${texts.scenarios}`);
|
|
|
12159
12495
|
}
|
|
12160
12496
|
}
|
|
12161
12497
|
async function loadProjectPacks(sddDir) {
|
|
12162
|
-
const dir =
|
|
12498
|
+
const dir = path12.join(sddDir, "packs");
|
|
12163
12499
|
const diagnostics = [];
|
|
12164
12500
|
const packs = [];
|
|
12165
12501
|
for (const entry of await listDir(dir)) {
|
|
12166
12502
|
if (!/\.ya?ml$/i.test(entry)) continue;
|
|
12167
|
-
const file =
|
|
12503
|
+
const file = path12.join(dir, entry);
|
|
12168
12504
|
const raw = await readTextIfExists(file);
|
|
12169
12505
|
if (raw === void 0) continue;
|
|
12170
12506
|
let data;
|
|
12171
12507
|
try {
|
|
12172
|
-
data =
|
|
12508
|
+
data = parseYaml9(raw);
|
|
12173
12509
|
} catch (err) {
|
|
12174
12510
|
diagnostics.push(diag("PACK-000", "error", `Pack inv\xE1lido (${entry}): ${err.message}`, { path: file }));
|
|
12175
12511
|
continue;
|
|
@@ -12251,7 +12587,7 @@ function packFindings(evaluations, change) {
|
|
|
12251
12587
|
}
|
|
12252
12588
|
var UI_DOMAINS = /* @__PURE__ */ new Set(["frontend", "mobile", "fullstack"]);
|
|
12253
12589
|
async function runAnalyze(opts) {
|
|
12254
|
-
const root =
|
|
12590
|
+
const root = path13.resolve(opts.root);
|
|
12255
12591
|
const { workspace, config } = await loadWorkspace(root);
|
|
12256
12592
|
const change = workspace.changes.find((c) => c.slug === opts.slug);
|
|
12257
12593
|
if (!change) {
|
|
@@ -12269,9 +12605,9 @@ async function runAnalyze(opts) {
|
|
|
12269
12605
|
for (const req of spec.spec.requirements) living.set(req.id, req);
|
|
12270
12606
|
}
|
|
12271
12607
|
if (change.delta) {
|
|
12272
|
-
findings.push(...lintDelta(change.delta, living,
|
|
12608
|
+
findings.push(...lintDelta(change.delta, living, path13.join(change.dir, "spec.md"), { language: config.spec.language }));
|
|
12273
12609
|
}
|
|
12274
|
-
const trace = checkTrace({ specs: workspace.specs, change, requireEvidence: false });
|
|
12610
|
+
const trace = checkTrace({ specs: workspace.specs, change, requireEvidence: false, linked: linkedTraceInput(workspace) });
|
|
12275
12611
|
findings.push(...trace.findings);
|
|
12276
12612
|
let waves;
|
|
12277
12613
|
if (change.tasks && change.tasks.counts.total > 0) {
|
|
@@ -12318,7 +12654,7 @@ async function runAnalyze(opts) {
|
|
|
12318
12654
|
if (mockups) result.mockups = mockups;
|
|
12319
12655
|
if (packs) result.packs = packs;
|
|
12320
12656
|
if (opts.write !== false) {
|
|
12321
|
-
const file =
|
|
12657
|
+
const file = path13.join(change.dir, "analyze.md");
|
|
12322
12658
|
await writeText(file, renderAnalyze(change, result, localStamp(opts.now)));
|
|
12323
12659
|
result.path = file;
|
|
12324
12660
|
}
|
|
@@ -12360,7 +12696,7 @@ function renderAnalyze(change, result, generatedAt) {
|
|
|
12360
12696
|
return lines.join("\n");
|
|
12361
12697
|
}
|
|
12362
12698
|
async function collectMetrics(root, now = /* @__PURE__ */ new Date()) {
|
|
12363
|
-
const resolved =
|
|
12699
|
+
const resolved = path14.resolve(root);
|
|
12364
12700
|
const { workspace, config } = await loadWorkspace(resolved);
|
|
12365
12701
|
const approvals = await loadApprovals(workspace.sddDir);
|
|
12366
12702
|
const living = /* @__PURE__ */ new Map();
|
|
@@ -12375,11 +12711,12 @@ async function collectMetrics(root, now = /* @__PURE__ */ new Date()) {
|
|
|
12375
12711
|
const deltaContent = await readDelta(change);
|
|
12376
12712
|
const approval = verifyApproval(change, approvals.byArtifact, config, deltaContent);
|
|
12377
12713
|
const findings = [];
|
|
12378
|
-
if (change.delta) findings.push(...lintDelta(change.delta, living,
|
|
12714
|
+
if (change.delta) findings.push(...lintDelta(change.delta, living, path14.join(change.dir, "spec.md"), { language: config.spec.language }));
|
|
12379
12715
|
const trace = checkTrace({
|
|
12380
12716
|
specs: workspace.specs,
|
|
12381
12717
|
change,
|
|
12382
|
-
requireEvidence: config.gates.verify.mode !== "off" && config.gates.verify.require_evidence
|
|
12718
|
+
requireEvidence: config.gates.verify.mode !== "off" && config.gates.verify.require_evidence,
|
|
12719
|
+
linked: linkedTraceInput(workspace)
|
|
12383
12720
|
});
|
|
12384
12721
|
findings.push(...trace.findings);
|
|
12385
12722
|
const state = deriveState({ change, cfg: config, approval, blockingFindings: change.delta ? countBySeverity(findings).errors : 0 });
|
|
@@ -12440,7 +12777,7 @@ async function collectMetrics(root, now = /* @__PURE__ */ new Date()) {
|
|
|
12440
12777
|
attention.push({ slug: change.slug, kind: "missing-evidence", detail: `evidencia ${change.scenariosPassed}/${change.scenariosTotal}` });
|
|
12441
12778
|
}
|
|
12442
12779
|
}
|
|
12443
|
-
const archiveDir =
|
|
12780
|
+
const archiveDir = path14.join(workspace.sddDir, "changes", "archive");
|
|
12444
12781
|
const archivedEntries = await listDirs(archiveDir);
|
|
12445
12782
|
const throughputByMonth = {};
|
|
12446
12783
|
for (const entry of archivedEntries) {
|
|
@@ -12478,10 +12815,10 @@ async function collectMetrics(root, now = /* @__PURE__ */ new Date()) {
|
|
|
12478
12815
|
}
|
|
12479
12816
|
async function readDelta(change) {
|
|
12480
12817
|
const { readTextIfExists: readTextIfExists2 } = await import("./fsx-VF2P7ALA-XKEWTMDH.js");
|
|
12481
|
-
return readTextIfExists2(
|
|
12818
|
+
return readTextIfExists2(path14.join(change.dir, "spec.md"));
|
|
12482
12819
|
}
|
|
12483
12820
|
async function generatePresentation(opts) {
|
|
12484
|
-
const root =
|
|
12821
|
+
const root = path15.resolve(opts.root);
|
|
12485
12822
|
const { workspace, config } = await loadWorkspace(root);
|
|
12486
12823
|
const language = config.project.language;
|
|
12487
12824
|
const change = workspace.changes.find((c) => c.slug === opts.slug);
|
|
@@ -12491,17 +12828,17 @@ async function generatePresentation(opts) {
|
|
|
12491
12828
|
if (!change.delta) {
|
|
12492
12829
|
return { slug: opts.slug, diagnostics: [diag("ATLAS-PRESENT-001", "error", `El cambio "${opts.slug}" no tiene spec.md (delta)`)] };
|
|
12493
12830
|
}
|
|
12494
|
-
const deltaPath =
|
|
12831
|
+
const deltaPath = path15.join(change.dir, "spec.md");
|
|
12495
12832
|
const deltaContent = await readTextIfExists(deltaPath) ?? "";
|
|
12496
12833
|
const hash = artifactHash(deltaContent);
|
|
12497
12834
|
const now = opts.now ?? /* @__PURE__ */ new Date();
|
|
12498
|
-
const presentationDir =
|
|
12499
|
-
const mockupsCopyDir =
|
|
12835
|
+
const presentationDir = path15.join(change.dir, "presentation");
|
|
12836
|
+
const mockupsCopyDir = path15.join(presentationDir, "mockups");
|
|
12500
12837
|
await ensureDir(presentationDir);
|
|
12501
12838
|
const mockupInfo = await copyMockups(root, change, mockupsCopyDir);
|
|
12502
|
-
const proposalRaw = await readTextIfExists(
|
|
12839
|
+
const proposalRaw = await readTextIfExists(path15.join(change.dir, "proposal.md"));
|
|
12503
12840
|
const proposalBody = proposalRaw ? parseFrontmatter(proposalRaw).body : "";
|
|
12504
|
-
const approvals = await loadApprovals(
|
|
12841
|
+
const approvals = await loadApprovals(path15.join(root, ".sdd"));
|
|
12505
12842
|
const approval = verifyApproval(change, approvals.byArtifact, config, deltaContent);
|
|
12506
12843
|
const labels = labelsFor(language);
|
|
12507
12844
|
const html = page({
|
|
@@ -12522,7 +12859,7 @@ async function generatePresentation(opts) {
|
|
|
12522
12859
|
approveHint: `satlas approve ${change.slug} --by "<nombre>" --channel presentation`,
|
|
12523
12860
|
...approval.status === "valid" && approval.approvedBy ? { approval: { by: approval.approvedBy, at: approval.approvedAt ?? "" } } : {}
|
|
12524
12861
|
});
|
|
12525
|
-
const outFile =
|
|
12862
|
+
const outFile = path15.join(presentationDir, "index.html");
|
|
12526
12863
|
await writeText(outFile, html);
|
|
12527
12864
|
return { slug: change.slug, path: outFile, hash, diagnostics: mockupInfo.diagnostics };
|
|
12528
12865
|
}
|
|
@@ -12534,12 +12871,12 @@ async function copyMockups(root, change, destDir) {
|
|
|
12534
12871
|
await ensureDir(destDir);
|
|
12535
12872
|
const screens = [];
|
|
12536
12873
|
for (const screen of manifest.screens) {
|
|
12537
|
-
const src =
|
|
12874
|
+
const src = path15.join(dir, screen.file);
|
|
12538
12875
|
if (!await exists(src)) {
|
|
12539
12876
|
diagnostics.push(diag("ATLAS-PRESENT-002", "warning", `El mockup ${screen.file} no existe y no se incluir\xE1`, { path: src }));
|
|
12540
12877
|
continue;
|
|
12541
12878
|
}
|
|
12542
|
-
await copyFile(src,
|
|
12879
|
+
await copyFile(src, path15.join(destDir, screen.file));
|
|
12543
12880
|
screens.push({
|
|
12544
12881
|
id: screen.id,
|
|
12545
12882
|
title: screen.title ?? screen.id,
|
|
@@ -12549,13 +12886,13 @@ async function copyMockups(root, change, destDir) {
|
|
|
12549
12886
|
});
|
|
12550
12887
|
}
|
|
12551
12888
|
const screenshots = [];
|
|
12552
|
-
const shotsDir =
|
|
12889
|
+
const shotsDir = path15.join(dir, "screens");
|
|
12553
12890
|
if (await exists(shotsDir)) {
|
|
12554
|
-
await ensureDir(
|
|
12891
|
+
await ensureDir(path15.join(destDir, "screens"));
|
|
12555
12892
|
const { listDir: listDir4 } = await import("./fsx-VF2P7ALA-XKEWTMDH.js");
|
|
12556
12893
|
for (const file of await listDir4(shotsDir)) {
|
|
12557
12894
|
if (!/\.(png|jpe?g|webp)$/i.test(file)) continue;
|
|
12558
|
-
await copyFile(
|
|
12895
|
+
await copyFile(path15.join(shotsDir, file), path15.join(destDir, "screens", file));
|
|
12559
12896
|
screenshots.push(`mockups/screens/${file}`);
|
|
12560
12897
|
}
|
|
12561
12898
|
}
|
|
@@ -12771,11 +13108,11 @@ var SOURCE_EXT = /* @__PURE__ */ new Set([
|
|
|
12771
13108
|
".groovy"
|
|
12772
13109
|
]);
|
|
12773
13110
|
async function adoptWorkspace(opts) {
|
|
12774
|
-
const root =
|
|
13111
|
+
const root = path16.resolve(opts.root);
|
|
12775
13112
|
const dryRun = opts.dryRun ?? false;
|
|
12776
13113
|
const diagnostics = [];
|
|
12777
|
-
const sddDir =
|
|
12778
|
-
if (!await exists(
|
|
13114
|
+
const sddDir = path16.join(root, ".sdd");
|
|
13115
|
+
if (!await exists(path16.join(sddDir, "config.yaml"))) {
|
|
12779
13116
|
return {
|
|
12780
13117
|
root,
|
|
12781
13118
|
domains: [],
|
|
@@ -12786,8 +13123,8 @@ async function adoptWorkspace(opts) {
|
|
|
12786
13123
|
}
|
|
12787
13124
|
const { config } = await loadConfig(sddDir);
|
|
12788
13125
|
const files = await walkFiles(root, { skipDirs: [...SKIP_DIRS] });
|
|
12789
|
-
const inventoried = files.map((f) => toPosix(
|
|
12790
|
-
const relative = inventoried.filter((f) => SOURCE_EXT.has(
|
|
13126
|
+
const inventoried = files.map((f) => toPosix(path16.relative(root, f.path)));
|
|
13127
|
+
const relative = inventoried.filter((f) => SOURCE_EXT.has(path16.extname(f).toLowerCase()));
|
|
12791
13128
|
if (relative.length === 0) {
|
|
12792
13129
|
diagnostics.push(diag("ATLAS-ADOPT-002", "warning", "No se encontraron archivos de c\xF3digo para inventariar", { path: root, suggestion: "A\xF1ade el dominio a mano con --domains <nombre>" }));
|
|
12793
13130
|
}
|
|
@@ -12815,13 +13152,13 @@ async function adoptWorkspace(opts) {
|
|
|
12815
13152
|
const createdSpecs = [];
|
|
12816
13153
|
for (const name of domainNames.sort()) {
|
|
12817
13154
|
const domainFiles = (counts.get(name) ?? []).sort();
|
|
12818
|
-
const existingSpec = await exists(
|
|
13155
|
+
const existingSpec = await exists(path16.join(sddDir, "specs", name, "spec.md"));
|
|
12819
13156
|
domains.push({ name, files: domainFiles.length, samples: domainFiles.slice(0, 8), existingSpec });
|
|
12820
13157
|
if (existingSpec) {
|
|
12821
|
-
diagnostics.push(diag("ATLAS-ADOPT-003", "info", `La spec de "${name}" ya existe; se conserva`, { path:
|
|
13158
|
+
diagnostics.push(diag("ATLAS-ADOPT-003", "info", `La spec de "${name}" ya existe; se conserva`, { path: path16.join(sddDir, "specs", name, "spec.md") }));
|
|
12822
13159
|
continue;
|
|
12823
13160
|
}
|
|
12824
|
-
const specPath =
|
|
13161
|
+
const specPath = path16.join(sddDir, "specs", name, "spec.md");
|
|
12825
13162
|
if (!dryRun) {
|
|
12826
13163
|
await writeText(specPath, baselineSpec(name, domainFiles));
|
|
12827
13164
|
createdSpecs.push(specPath);
|
|
@@ -12829,7 +13166,7 @@ async function adoptWorkspace(opts) {
|
|
|
12829
13166
|
createdSpecs.push(specPath);
|
|
12830
13167
|
}
|
|
12831
13168
|
}
|
|
12832
|
-
const reportPath =
|
|
13169
|
+
const reportPath = path16.join(sddDir, "adopt-report.md");
|
|
12833
13170
|
if (!dryRun) {
|
|
12834
13171
|
await writeText(reportPath, adoptReport({ root, config, domains, detection: detection?.best, language: config.project.language, files: relative.length, inventoried: inventoried.length, now: opts.now ?? /* @__PURE__ */ new Date() }));
|
|
12835
13172
|
}
|
|
@@ -12849,12 +13186,12 @@ async function loadProjectProfiles(root, dirs) {
|
|
|
12849
13186
|
if (!await exists(dir)) continue;
|
|
12850
13187
|
out.push(...await loadProfilesFromDir(dir));
|
|
12851
13188
|
}
|
|
12852
|
-
const custom =
|
|
13189
|
+
const custom = path16.join(root, ".sdd", "profiles", "custom");
|
|
12853
13190
|
if (await exists(custom)) out.push(...await loadProfilesFromDir(custom));
|
|
12854
13191
|
return out;
|
|
12855
13192
|
}
|
|
12856
13193
|
async function fallbackDomainName(root, projectName) {
|
|
12857
|
-
const pkg = await readTextIfExists(
|
|
13194
|
+
const pkg = await readTextIfExists(path16.join(root, "package.json"));
|
|
12858
13195
|
if (pkg) {
|
|
12859
13196
|
try {
|
|
12860
13197
|
const data = JSON.parse(pkg);
|
|
@@ -12941,11 +13278,11 @@ function adoptReport(input) {
|
|
|
12941
13278
|
return lines.join("\n");
|
|
12942
13279
|
}
|
|
12943
13280
|
async function initWorkspace(opts) {
|
|
12944
|
-
const root =
|
|
12945
|
-
const sddDir =
|
|
13281
|
+
const root = path17.resolve(opts.root);
|
|
13282
|
+
const sddDir = path17.join(root, ".sdd");
|
|
12946
13283
|
const diagnostics = [];
|
|
12947
13284
|
const created = [];
|
|
12948
|
-
if (await exists(
|
|
13285
|
+
if (await exists(path17.join(sddDir, "config.yaml"))) {
|
|
12949
13286
|
return {
|
|
12950
13287
|
sddDir,
|
|
12951
13288
|
created,
|
|
@@ -12955,29 +13292,29 @@ async function initWorkspace(opts) {
|
|
|
12955
13292
|
created.push(...await ensureSddDirs(sddDir));
|
|
12956
13293
|
const language = opts.language ?? "es";
|
|
12957
13294
|
const cfg = defaultConfig({ name: opts.name, language });
|
|
12958
|
-
cfg.project.name = opts.name ??
|
|
13295
|
+
cfg.project.name = opts.name ?? path17.basename(root);
|
|
12959
13296
|
await writeConfig(sddDir, cfg);
|
|
12960
|
-
created.push(
|
|
13297
|
+
created.push(path17.join(sddDir, "config.yaml"));
|
|
12961
13298
|
const templates = templatesFor(language);
|
|
12962
|
-
await writeText(
|
|
12963
|
-
created.push(
|
|
12964
|
-
await writeText(
|
|
12965
|
-
created.push(
|
|
13299
|
+
await writeText(path17.join(sddDir, "constitution.md"), templates.constitution);
|
|
13300
|
+
created.push(path17.join(sddDir, "constitution.md"));
|
|
13301
|
+
await writeText(path17.join(sddDir, "glossary.md"), templates.glossary);
|
|
13302
|
+
created.push(path17.join(sddDir, "glossary.md"));
|
|
12966
13303
|
const detected = await detectIfPossible(root, opts.profilesDir);
|
|
12967
13304
|
if (detected) {
|
|
12968
13305
|
const yaml2 = detectionToYaml(detected, localStamp(opts.now));
|
|
12969
|
-
await writeText(
|
|
12970
|
-
created.push(
|
|
13306
|
+
await writeText(path17.join(sddDir, "profiles", "detected.yaml"), yaml2);
|
|
13307
|
+
created.push(path17.join(sddDir, "profiles", "detected.yaml"));
|
|
12971
13308
|
if (detected.best) {
|
|
12972
13309
|
diagnostics.push(diag("ATLAS-INIT-002", "info", `Stack detectado: ${detected.best.displayName} (${detected.best.score} puntos)`, { suggestion: "Revisa .sdd/profiles/detected.yaml" }));
|
|
12973
13310
|
} else {
|
|
12974
13311
|
diagnostics.push(diag("ATLAS-INIT-003", "info", "No se detect\xF3 un stack conocido: se usar\xE1 el perfil gen\xE9rico", { suggestion: "Crea un perfil propio con `satlas profile` (F2) o edita .sdd/profiles/custom/" }));
|
|
12975
13312
|
}
|
|
12976
13313
|
}
|
|
12977
|
-
await writeText(
|
|
12978
|
-
created.push(
|
|
13314
|
+
await writeText(path17.join(sddDir, "INDEX.md"), initialIndex(language, cfg.project.name));
|
|
13315
|
+
created.push(path17.join(sddDir, "INDEX.md"));
|
|
12979
13316
|
if (opts.local) {
|
|
12980
|
-
const gitignore =
|
|
13317
|
+
const gitignore = path17.join(root, ".gitignore");
|
|
12981
13318
|
const current = await readTextIfExists(gitignore) ?? "";
|
|
12982
13319
|
if (!/(^|\n)\.sdd\/?(\n|$)/.test(current)) {
|
|
12983
13320
|
const next = current.endsWith("\n") || current === "" ? `${current}.sdd/
|
|
@@ -13015,7 +13352,7 @@ async function createChange(opts) {
|
|
|
13015
13352
|
const diagnostics = [];
|
|
13016
13353
|
const slug = opts.slug.trim().toLowerCase();
|
|
13017
13354
|
const language = opts.language ?? opts.cfg?.project.language ?? "es";
|
|
13018
|
-
const dir =
|
|
13355
|
+
const dir = path18.join(path18.resolve(opts.root), ".sdd", "changes", slug);
|
|
13019
13356
|
if (!SLUG_RE.test(slug)) {
|
|
13020
13357
|
return {
|
|
13021
13358
|
slug,
|
|
@@ -13046,20 +13383,20 @@ async function createChange(opts) {
|
|
|
13046
13383
|
await ensureDir(dir);
|
|
13047
13384
|
const templates = templatesFor(language);
|
|
13048
13385
|
const files = [];
|
|
13049
|
-
const metaFile =
|
|
13386
|
+
const metaFile = path18.join(dir, "meta.yaml");
|
|
13050
13387
|
await writeText(metaFile, changeMetaYaml(meta));
|
|
13051
13388
|
files.push(metaFile);
|
|
13052
13389
|
if (lane === "fix") {
|
|
13053
|
-
const fixFile =
|
|
13390
|
+
const fixFile = path18.join(dir, "fix.md");
|
|
13054
13391
|
await writeText(fixFile, renderTemplate(templates.fix, { TITLE: title }));
|
|
13055
13392
|
files.push(fixFile);
|
|
13056
13393
|
return { slug, dir, files, diagnostics };
|
|
13057
13394
|
}
|
|
13058
|
-
const proposalFile =
|
|
13395
|
+
const proposalFile = path18.join(dir, "proposal.md");
|
|
13059
13396
|
await writeText(proposalFile, renderTemplate(templates.proposal, { TITLE: title }));
|
|
13060
13397
|
files.push(proposalFile);
|
|
13061
13398
|
const domainUpper = domain.replace(/[^a-z0-9]/gi, "").toUpperCase() || "GEN";
|
|
13062
|
-
const deltaFile =
|
|
13399
|
+
const deltaFile = path18.join(dir, "spec.md");
|
|
13063
13400
|
await writeText(deltaFile, templates.specDelta({ title, domainUpper, domain }));
|
|
13064
13401
|
files.push(deltaFile);
|
|
13065
13402
|
return { slug, dir, files, diagnostics };
|
|
@@ -13172,7 +13509,7 @@ async function restoreFile(file, previous) {
|
|
|
13172
13509
|
}
|
|
13173
13510
|
}
|
|
13174
13511
|
async function archiveChange(opts) {
|
|
13175
|
-
const root =
|
|
13512
|
+
const root = path19.resolve(opts.root);
|
|
13176
13513
|
const diagnostics = [];
|
|
13177
13514
|
const { config } = await loadWorkspace(root);
|
|
13178
13515
|
const language = opts.language ?? config.project.language;
|
|
@@ -13188,7 +13525,7 @@ async function archiveChange(opts) {
|
|
|
13188
13525
|
return { slug: opts.slug, fold: emptyFold, diagnostics, dryRun: opts.dryRun ?? false };
|
|
13189
13526
|
}
|
|
13190
13527
|
const nowFix = opts.now ?? /* @__PURE__ */ new Date();
|
|
13191
|
-
const targetFix =
|
|
13528
|
+
const targetFix = path19.join(root, ".sdd", "changes", "archive", `${localMonth(nowFix)}-${opts.slug}`);
|
|
13192
13529
|
if (await exists(targetFix)) {
|
|
13193
13530
|
diagnostics.push(diag("ATLAS-ARCH-002", "error", `Ya existe un cambio archivado en ${targetFix}`, { path: targetFix }));
|
|
13194
13531
|
return { slug: opts.slug, fold: emptyFold, diagnostics, dryRun: opts.dryRun ?? false };
|
|
@@ -13196,7 +13533,7 @@ async function archiveChange(opts) {
|
|
|
13196
13533
|
let livingFix;
|
|
13197
13534
|
let livingCreated = false;
|
|
13198
13535
|
if (!opts.dryRun) {
|
|
13199
|
-
const fixRaw = await readTextIfExists(
|
|
13536
|
+
const fixRaw = await readTextIfExists(path19.join(change.dir, "fix.md")) ?? "";
|
|
13200
13537
|
try {
|
|
13201
13538
|
const written = await writeLivingFix(root, {
|
|
13202
13539
|
slug: opts.slug,
|
|
@@ -13212,7 +13549,7 @@ async function archiveChange(opts) {
|
|
|
13212
13549
|
} catch (error) {
|
|
13213
13550
|
diagnostics.push(
|
|
13214
13551
|
diag("ATLAS-ARCH-006", "error", `No se pudo conservar el fix vivo: ${error.message}`, {
|
|
13215
|
-
path:
|
|
13552
|
+
path: path19.join(root, ".sdd", "fixes"),
|
|
13216
13553
|
suggestion: "Revisa los permisos de .sdd/fixes y vuelve a intentar; el fix sigue sin archivar"
|
|
13217
13554
|
})
|
|
13218
13555
|
);
|
|
@@ -13220,11 +13557,11 @@ async function archiveChange(opts) {
|
|
|
13220
13557
|
}
|
|
13221
13558
|
}
|
|
13222
13559
|
if (!opts.dryRun) {
|
|
13223
|
-
await ensureDir(
|
|
13560
|
+
await ensureDir(path19.dirname(targetFix));
|
|
13224
13561
|
try {
|
|
13225
13562
|
await moveDirectory(change.dir, targetFix);
|
|
13226
13563
|
} catch (error) {
|
|
13227
|
-
if (livingCreated && livingFix) await removeFile(
|
|
13564
|
+
if (livingCreated && livingFix) await removeFile(path19.join(root, livingFix));
|
|
13228
13565
|
diagnostics.push(
|
|
13229
13566
|
diag("ATLAS-ARCH-004", "error", `No se pudo mover el cambio al hist\xF3rico: ${error.message}`, {
|
|
13230
13567
|
path: change.dir,
|
|
@@ -13250,10 +13587,10 @@ async function archiveChange(opts) {
|
|
|
13250
13587
|
}
|
|
13251
13588
|
const domain = change.meta.domain;
|
|
13252
13589
|
if (!domain) {
|
|
13253
|
-
diagnostics.push(diag("ATLAS-ARCH-001", "error", `El cambio "${opts.slug}" no declara domain en meta.yaml`, { path:
|
|
13590
|
+
diagnostics.push(diag("ATLAS-ARCH-001", "error", `El cambio "${opts.slug}" no declara domain en meta.yaml`, { path: path19.join(change.dir, "meta.yaml") }));
|
|
13254
13591
|
return { slug: opts.slug, fold: { content: "", applied: { added: [], modified: [], removed: [], renamed: [] }, diagnostics }, diagnostics, dryRun: opts.dryRun ?? false };
|
|
13255
13592
|
}
|
|
13256
|
-
const specFile =
|
|
13593
|
+
const specFile = path19.join(root, ".sdd", "specs", domain, "spec.md");
|
|
13257
13594
|
const existing = await readTextIfExists(specFile);
|
|
13258
13595
|
const base = existing ?? `---
|
|
13259
13596
|
domain: ${domain}
|
|
@@ -13283,8 +13620,8 @@ ${Object.entries(nextFm).map(([k, v]) => `${k}: ${String(v)}`).join("\n")}
|
|
|
13283
13620
|
|
|
13284
13621
|
${body}`;
|
|
13285
13622
|
const month = localMonth(now);
|
|
13286
|
-
const archiveDir =
|
|
13287
|
-
const target =
|
|
13623
|
+
const archiveDir = path19.join(root, ".sdd", "changes", "archive");
|
|
13624
|
+
const target = path19.join(archiveDir, `${month}-${opts.slug}`);
|
|
13288
13625
|
if (await exists(target)) {
|
|
13289
13626
|
diagnostics.push(diag("ATLAS-ARCH-002", "error", `Ya existe un cambio archivado en ${target}`, { path: target }));
|
|
13290
13627
|
return { slug: opts.slug, domain, fold, diagnostics, dryRun: opts.dryRun ?? false };
|
|
@@ -13310,7 +13647,7 @@ ${body}`;
|
|
|
13310
13647
|
}
|
|
13311
13648
|
async function regenerateIndex(root, cfg, now = /* @__PURE__ */ new Date()) {
|
|
13312
13649
|
const { workspace, config } = cfg ? { workspace: (await loadWorkspace(root)).workspace, config: cfg } : await loadWorkspace(root);
|
|
13313
|
-
const archiveDir =
|
|
13650
|
+
const archiveDir = path19.join(root, ".sdd", "changes", "archive");
|
|
13314
13651
|
const archived = await exists(archiveDir) ? (await fs.readdir(archiveDir)).filter((e) => !e.startsWith(".")).length : 0;
|
|
13315
13652
|
const fixes = await loadLivingFixes(root);
|
|
13316
13653
|
const markdown2 = indexMarkdown({
|
|
@@ -13322,7 +13659,7 @@ async function regenerateIndex(root, cfg, now = /* @__PURE__ */ new Date()) {
|
|
|
13322
13659
|
archived
|
|
13323
13660
|
});
|
|
13324
13661
|
void now;
|
|
13325
|
-
await writeText(
|
|
13662
|
+
await writeText(path19.join(root, ".sdd", "INDEX.md"), markdown2);
|
|
13326
13663
|
}
|
|
13327
13664
|
async function removeFile(file) {
|
|
13328
13665
|
try {
|
|
@@ -13341,35 +13678,35 @@ var SCHEMA_MIGRATIONS = [
|
|
|
13341
13678
|
to: SCHEMA_VERSION
|
|
13342
13679
|
}
|
|
13343
13680
|
];
|
|
13344
|
-
var FIXED_ARTIFACTS = ["config.yaml", "approvals.yaml",
|
|
13345
|
-
var CHANGE_ARTIFACTS = ["meta.yaml",
|
|
13681
|
+
var FIXED_ARTIFACTS = ["config.yaml", "approvals.yaml", path20.join("profiles", "detected.yaml")];
|
|
13682
|
+
var CHANGE_ARTIFACTS = ["meta.yaml", path20.join("mockups", "manifest.yaml")];
|
|
13346
13683
|
async function pushChangeArtifacts(out, sddDir, relDir) {
|
|
13347
13684
|
for (const rel of CHANGE_ARTIFACTS) {
|
|
13348
|
-
const abs =
|
|
13349
|
-
if (await exists(abs)) out.push({ artifact: toPosix(
|
|
13685
|
+
const abs = path20.join(sddDir, relDir, rel);
|
|
13686
|
+
if (await exists(abs)) out.push({ artifact: toPosix(path20.join(relDir, rel)), path: abs });
|
|
13350
13687
|
}
|
|
13351
13688
|
}
|
|
13352
13689
|
async function collectVersionedArtifacts(root) {
|
|
13353
|
-
const sddDir =
|
|
13690
|
+
const sddDir = path20.join(root, ".sdd");
|
|
13354
13691
|
const out = [];
|
|
13355
13692
|
for (const rel of FIXED_ARTIFACTS) {
|
|
13356
|
-
const abs =
|
|
13693
|
+
const abs = path20.join(sddDir, rel);
|
|
13357
13694
|
if (await exists(abs)) out.push({ artifact: toPosix(rel), path: abs });
|
|
13358
13695
|
}
|
|
13359
|
-
const changesDir =
|
|
13696
|
+
const changesDir = path20.join(sddDir, "changes");
|
|
13360
13697
|
for (const slug of await listDirs(changesDir)) {
|
|
13361
13698
|
if (slug === "archive") continue;
|
|
13362
|
-
await pushChangeArtifacts(out, sddDir,
|
|
13699
|
+
await pushChangeArtifacts(out, sddDir, path20.join("changes", slug));
|
|
13363
13700
|
}
|
|
13364
|
-
for (const entry of await listDirs(
|
|
13365
|
-
await pushChangeArtifacts(out, sddDir,
|
|
13701
|
+
for (const entry of await listDirs(path20.join(changesDir, "archive"))) {
|
|
13702
|
+
await pushChangeArtifacts(out, sddDir, path20.join("changes", "archive", entry));
|
|
13366
13703
|
}
|
|
13367
13704
|
return out;
|
|
13368
13705
|
}
|
|
13369
13706
|
function readArtifactVersion(content) {
|
|
13370
13707
|
let data;
|
|
13371
13708
|
try {
|
|
13372
|
-
data =
|
|
13709
|
+
data = parseYaml10(content);
|
|
13373
13710
|
} catch (err) {
|
|
13374
13711
|
return { state: "unreadable", reason: `YAML inv\xE1lido (${err.message})` };
|
|
13375
13712
|
}
|
|
@@ -13459,22 +13796,22 @@ function backupName(now) {
|
|
|
13459
13796
|
async function applyUpgrade(root, now = /* @__PURE__ */ new Date()) {
|
|
13460
13797
|
const plan = await planUpgrade(root);
|
|
13461
13798
|
if (plan.pending.length === 0) return { status: "up-to-date", plan, applied: [] };
|
|
13462
|
-
const sddDir =
|
|
13463
|
-
const backupRoot =
|
|
13799
|
+
const sddDir = path20.join(root, ".sdd");
|
|
13800
|
+
const backupRoot = path20.join(sddDir, BACKUP_DIRNAME);
|
|
13464
13801
|
const name = backupName(now);
|
|
13465
|
-
const backupDir =
|
|
13466
|
-
const pointerFile =
|
|
13802
|
+
const backupDir = path20.join(backupRoot, name);
|
|
13803
|
+
const pointerFile = path20.join(backupRoot, BACKUP_POINTER);
|
|
13467
13804
|
const previous = [];
|
|
13468
13805
|
try {
|
|
13469
13806
|
for (const item of plan.pending) {
|
|
13470
13807
|
const content = await readTextIfExists(item.path);
|
|
13471
13808
|
if (content === void 0) throw new Error("el elemento desapareci\xF3 mientras se respaldaba");
|
|
13472
13809
|
previous.push({ item, contents: content });
|
|
13473
|
-
await writeText(
|
|
13810
|
+
await writeText(path20.join(backupDir, "files", ...item.artifact.split("/")), content);
|
|
13474
13811
|
}
|
|
13475
13812
|
await writeText(
|
|
13476
|
-
|
|
13477
|
-
|
|
13813
|
+
path20.join(backupDir, "backup.yaml"),
|
|
13814
|
+
stringifyYaml7(
|
|
13478
13815
|
{
|
|
13479
13816
|
schema_version: SCHEMA_VERSION,
|
|
13480
13817
|
created_at: localStamp(now),
|
|
@@ -13514,7 +13851,7 @@ async function applyUpgrade(root, now = /* @__PURE__ */ new Date()) {
|
|
|
13514
13851
|
}
|
|
13515
13852
|
}
|
|
13516
13853
|
for (const dir of await listDirs(backupRoot)) {
|
|
13517
|
-
if (dir !== name) await removeDir(
|
|
13854
|
+
if (dir !== name) await removeDir(path20.join(backupRoot, dir));
|
|
13518
13855
|
}
|
|
13519
13856
|
return {
|
|
13520
13857
|
status: "applied",
|
|
@@ -13523,32 +13860,32 @@ async function applyUpgrade(root, now = /* @__PURE__ */ new Date()) {
|
|
|
13523
13860
|
backup: {
|
|
13524
13861
|
name,
|
|
13525
13862
|
dir: backupDir,
|
|
13526
|
-
relativeDir: toPosix(
|
|
13863
|
+
relativeDir: toPosix(path20.relative(root, backupDir)),
|
|
13527
13864
|
createdAt: localStamp(now),
|
|
13528
13865
|
files: plan.pending.map((i) => i.artifact)
|
|
13529
13866
|
}
|
|
13530
13867
|
};
|
|
13531
13868
|
}
|
|
13532
13869
|
async function rollbackUpgrade(root) {
|
|
13533
|
-
const sddDir =
|
|
13534
|
-
const backupRoot =
|
|
13535
|
-
const pointerFile =
|
|
13870
|
+
const sddDir = path20.join(root, ".sdd");
|
|
13871
|
+
const backupRoot = path20.join(sddDir, BACKUP_DIRNAME);
|
|
13872
|
+
const pointerFile = path20.join(backupRoot, BACKUP_POINTER);
|
|
13536
13873
|
const name = (await readTextIfExists(pointerFile))?.trim();
|
|
13537
13874
|
if (!name) return { status: "no-backup", restored: [] };
|
|
13538
|
-
const backupDir =
|
|
13539
|
-
const manifestRaw = await readTextIfExists(
|
|
13875
|
+
const backupDir = path20.join(backupRoot, name);
|
|
13876
|
+
const manifestRaw = await readTextIfExists(path20.join(backupDir, "backup.yaml"));
|
|
13540
13877
|
if (manifestRaw === void 0) return { status: "no-backup", restored: [] };
|
|
13541
13878
|
let files = [];
|
|
13542
13879
|
try {
|
|
13543
|
-
const parsed =
|
|
13880
|
+
const parsed = parseYaml10(manifestRaw);
|
|
13544
13881
|
files = (parsed?.files ?? []).filter((f) => typeof f?.artifact === "string");
|
|
13545
13882
|
} catch {
|
|
13546
13883
|
return { status: "no-backup", restored: [] };
|
|
13547
13884
|
}
|
|
13548
13885
|
const restored = [];
|
|
13549
13886
|
for (const file of files) {
|
|
13550
|
-
const from =
|
|
13551
|
-
const to =
|
|
13887
|
+
const from = path20.join(backupDir, "files", ...file.artifact.split("/"));
|
|
13888
|
+
const to = path20.join(sddDir, ...file.artifact.split("/"));
|
|
13552
13889
|
const content = await readTextIfExists(from);
|
|
13553
13890
|
if (content === void 0) continue;
|
|
13554
13891
|
try {
|
|
@@ -13643,7 +13980,7 @@ ${notes}
|
|
|
13643
13980
|
${existing}`;
|
|
13644
13981
|
}
|
|
13645
13982
|
async function generateDocs(opts) {
|
|
13646
|
-
const root =
|
|
13983
|
+
const root = path21.resolve(opts.root);
|
|
13647
13984
|
const { config } = await loadWorkspace(root);
|
|
13648
13985
|
const change = await loadChange(root, opts.slug);
|
|
13649
13986
|
if (!change.meta) {
|
|
@@ -13660,7 +13997,7 @@ async function generateDocs(opts) {
|
|
|
13660
13997
|
const data = docData(change, language, opts.now ?? /* @__PURE__ */ new Date());
|
|
13661
13998
|
const files = [];
|
|
13662
13999
|
for (const current of tipos) {
|
|
13663
|
-
const file =
|
|
14000
|
+
const file = path21.join(change.dir, "docs", `${current}.md`);
|
|
13664
14001
|
const block = renderTemplate(current === "tecnica" ? templates.docTecnica : templates.docManual, data);
|
|
13665
14002
|
const existing = await readTextIfExists(file);
|
|
13666
14003
|
await writeText(file, mergeManaged(existing, block, language));
|
|
@@ -13705,7 +14042,7 @@ function resultOf(diagnostic, root) {
|
|
|
13705
14042
|
message: { text: diagnostic.message }
|
|
13706
14043
|
};
|
|
13707
14044
|
if (diagnostic.path) {
|
|
13708
|
-
const rel =
|
|
14045
|
+
const rel = path22.relative(root, diagnostic.path);
|
|
13709
14046
|
if (rel !== "") {
|
|
13710
14047
|
const physicalLocation = {
|
|
13711
14048
|
artifactLocation: { uri: toPosix(rel) },
|
|
@@ -13760,7 +14097,7 @@ async function runDoctor(root) {
|
|
|
13760
14097
|
const approvals = await loadApprovals(workspace.sddDir);
|
|
13761
14098
|
findings.push(...approvals.diagnostics);
|
|
13762
14099
|
for (const change of workspace.changes) {
|
|
13763
|
-
const deltaPath =
|
|
14100
|
+
const deltaPath = path23.join(change.dir, "spec.md");
|
|
13764
14101
|
const deltaContent = await readTextIfExists(deltaPath);
|
|
13765
14102
|
const approval = verifyApproval(change, approvals.byArtifact, config, deltaContent ?? void 0);
|
|
13766
14103
|
if ((change.planPath || change.tasks) && (approval.status === "missing" || approval.status === "stale")) {
|
|
@@ -13782,7 +14119,7 @@ async function runDoctor(root) {
|
|
|
13782
14119
|
}
|
|
13783
14120
|
for (const override of change.meta?.overrides ?? []) {
|
|
13784
14121
|
if (!override.reason.trim() || !override.by.trim()) {
|
|
13785
|
-
findings.push(diag("ATLAS-LIFECYCLE-004", "error", `Override del gate "${override.gate}" sin motivo o autor`, { path:
|
|
14122
|
+
findings.push(diag("ATLAS-LIFECYCLE-004", "error", `Override del gate "${override.gate}" sin motivo o autor`, { path: path23.join(change.dir, "meta.yaml") }));
|
|
13786
14123
|
}
|
|
13787
14124
|
}
|
|
13788
14125
|
}
|
|
@@ -13814,7 +14151,7 @@ function livingRequirementsMap(specs) {
|
|
|
13814
14151
|
return map;
|
|
13815
14152
|
}
|
|
13816
14153
|
async function runCiGate(opts) {
|
|
13817
|
-
const root =
|
|
14154
|
+
const root = path24.resolve(opts.root);
|
|
13818
14155
|
const { workspace, config } = await loadWorkspace(root);
|
|
13819
14156
|
const diagnostics = [];
|
|
13820
14157
|
const checks = [];
|
|
@@ -13825,11 +14162,12 @@ async function runCiGate(opts) {
|
|
|
13825
14162
|
let changesErrors = 0;
|
|
13826
14163
|
let changesWarnings = 0;
|
|
13827
14164
|
for (const change of workspace.changes) {
|
|
13828
|
-
const lintFindings = change.delta ? lintDelta(change.delta, living,
|
|
14165
|
+
const lintFindings = change.delta ? lintDelta(change.delta, living, path24.join(change.dir, "spec.md"), { language: config.spec.language }) : [];
|
|
13829
14166
|
const trace = checkTrace({
|
|
13830
14167
|
specs: workspace.specs,
|
|
13831
14168
|
change,
|
|
13832
|
-
requireEvidence: config.gates.verify.mode !== "off" && config.gates.verify.require_evidence
|
|
14169
|
+
requireEvidence: config.gates.verify.mode !== "off" && config.gates.verify.require_evidence,
|
|
14170
|
+
linked: linkedTraceInput(workspace)
|
|
13833
14171
|
});
|
|
13834
14172
|
const changeDiags = [...lintFindings, ...trace.findings];
|
|
13835
14173
|
if (change.planPath) {
|
|
@@ -13936,6 +14274,8 @@ var CATALOG = [
|
|
|
13936
14274
|
{ name: "doctor", description: "Diagn\xF3stico de salud del workspace", flags: ["json"], usage: "satlas doctor [--json]" },
|
|
13937
14275
|
{ name: "clarify", description: "Preguntas abiertas y aclaraciones del cambio (informe; aclarar es fase del agente)", flags: ["json"], usage: "satlas clarify <slug>" },
|
|
13938
14276
|
{ name: "docs", description: "Genera la documentaci\xF3n t\xE9cnica y manual del cambio (carril completo) desde la evidencia", flags: ["tipo", "json"], usage: "satlas docs <slug> [--tipo tecnica|manual|all]" },
|
|
14277
|
+
{ name: "contracts", description: "Comprueba los contratos del cambio (forma y cobertura con los escenarios)", flags: ["json"], usage: "satlas contracts <slug>" },
|
|
14278
|
+
{ name: "link", description: "Enlaza otros proyectos para consultar sus specs en solo lectura (multi-repo)", flags: ["name", "json"], usage: "satlas link add <ruta> [--name <nombre>] | satlas link list | satlas link remove <nombre|ruta>" },
|
|
13939
14279
|
{ 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]" },
|
|
13940
14280
|
{ 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' },
|
|
13941
14281
|
{ 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>" },
|
|
@@ -13994,6 +14334,8 @@ var ES2 = {
|
|
|
13994
14334
|
"docs.title": "Documentaci\xF3n del cambio",
|
|
13995
14335
|
"docs.created": "creado",
|
|
13996
14336
|
"docs.updated": "actualizado",
|
|
14337
|
+
"contracts.title": "Contratos del cambio",
|
|
14338
|
+
"link.title": "Enlaces entre proyectos",
|
|
13997
14339
|
"summary.findings": "hallazgos",
|
|
13998
14340
|
"label.errors": "errores",
|
|
13999
14341
|
"label.warnings": "avisos",
|
|
@@ -14036,6 +14378,8 @@ var EN2 = {
|
|
|
14036
14378
|
"docs.title": "Change documentation",
|
|
14037
14379
|
"docs.created": "created",
|
|
14038
14380
|
"docs.updated": "updated",
|
|
14381
|
+
"contracts.title": "Change contracts",
|
|
14382
|
+
"link.title": "Project links",
|
|
14039
14383
|
"summary.findings": "findings",
|
|
14040
14384
|
"label.errors": "errors",
|
|
14041
14385
|
"label.warnings": "warnings",
|
|
@@ -14063,26 +14407,26 @@ function cliVersion() {
|
|
|
14063
14407
|
}
|
|
14064
14408
|
|
|
14065
14409
|
// src/commands/adapters.ts
|
|
14066
|
-
import
|
|
14410
|
+
import path28 from "path";
|
|
14067
14411
|
|
|
14068
14412
|
// ../adapters/dist/index.js
|
|
14069
|
-
import
|
|
14070
|
-
import { stringify as
|
|
14071
|
-
import
|
|
14413
|
+
import path25 from "path";
|
|
14414
|
+
import { stringify as stringifyYaml8 } from "yaml";
|
|
14415
|
+
import path26 from "path";
|
|
14072
14416
|
async function loadWorkflow(workflowDir) {
|
|
14073
|
-
const phasesDir =
|
|
14074
|
-
const snippetsDir =
|
|
14417
|
+
const phasesDir = path25.join(workflowDir, "phases");
|
|
14418
|
+
const snippetsDir = path25.join(workflowDir, "snippets");
|
|
14075
14419
|
const phases = [];
|
|
14076
14420
|
const snippets = /* @__PURE__ */ new Map();
|
|
14077
14421
|
const hashParts = [];
|
|
14078
14422
|
for (const entry of (await listDir(phasesDir)).sort()) {
|
|
14079
14423
|
if (!entry.endsWith(".md")) continue;
|
|
14080
|
-
const filePath =
|
|
14424
|
+
const filePath = path25.join(phasesDir, entry);
|
|
14081
14425
|
const raw = await readText(filePath);
|
|
14082
14426
|
hashParts.push(raw);
|
|
14083
14427
|
const fm = parseFrontmatter(raw, filePath);
|
|
14084
14428
|
const data = fm.data;
|
|
14085
|
-
const id = typeof data["id"] === "string" ? data["id"] :
|
|
14429
|
+
const id = typeof data["id"] === "string" ? data["id"] : path25.basename(entry, ".md");
|
|
14086
14430
|
phases.push({
|
|
14087
14431
|
id,
|
|
14088
14432
|
title: typeof data["title"] === "string" ? data["title"] : id,
|
|
@@ -14096,9 +14440,9 @@ async function loadWorkflow(workflowDir) {
|
|
|
14096
14440
|
}
|
|
14097
14441
|
for (const entry of (await listDir(snippetsDir)).sort()) {
|
|
14098
14442
|
if (!entry.endsWith(".md")) continue;
|
|
14099
|
-
const raw = await readTextIfExists(
|
|
14443
|
+
const raw = await readTextIfExists(path25.join(snippetsDir, entry)) ?? "";
|
|
14100
14444
|
hashParts.push(raw);
|
|
14101
|
-
snippets.set(
|
|
14445
|
+
snippets.set(path25.basename(entry, ".md"), raw.trim());
|
|
14102
14446
|
}
|
|
14103
14447
|
return { phases: phases.sort((a, b) => a.id.localeCompare(b.id)), snippets, sourceHash: sha256(hashParts.join("\n---\n")) };
|
|
14104
14448
|
}
|
|
@@ -14122,7 +14466,7 @@ function vars(language, slugToken, commandPrefix) {
|
|
|
14122
14466
|
return { SLUG: slugToken, LANGUAGE: language, LANGUAGE_NAME: LANGUAGE_NAMES[language], SDD_DIR: ".sdd", COMMAND_PREFIX: commandPrefix };
|
|
14123
14467
|
}
|
|
14124
14468
|
function frontmatter(data, body) {
|
|
14125
|
-
const yaml2 =
|
|
14469
|
+
const yaml2 = stringifyYaml8(data, { lineWidth: 0 }).trimEnd();
|
|
14126
14470
|
return `---
|
|
14127
14471
|
${yaml2}
|
|
14128
14472
|
---
|
|
@@ -14280,11 +14624,11 @@ Reglas: la spec es funcional y de negocio (sin tecnolog\xEDa); la trazabilidad e
|
|
|
14280
14624
|
function tomlString(value) {
|
|
14281
14625
|
return `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
|
|
14282
14626
|
}
|
|
14283
|
-
var MANIFEST_REL =
|
|
14627
|
+
var MANIFEST_REL = path26.join(".sdd", ".generated", "manifest.json");
|
|
14284
14628
|
var BEGIN = "<!-- BEGIN specatlas -->";
|
|
14285
14629
|
var END = "<!-- END specatlas -->";
|
|
14286
14630
|
async function compileTargets(opts) {
|
|
14287
|
-
const root =
|
|
14631
|
+
const root = path26.resolve(opts.root);
|
|
14288
14632
|
const language = opts.language ?? "es";
|
|
14289
14633
|
const diagnostics = [];
|
|
14290
14634
|
const sources = await loadWorkflow(opts.workflowDir);
|
|
@@ -14292,7 +14636,7 @@ async function compileTargets(opts) {
|
|
|
14292
14636
|
diagnostics.push({
|
|
14293
14637
|
code: "ATLAS-ADAPTERS-002",
|
|
14294
14638
|
severity: "error",
|
|
14295
|
-
message: `No hay fases en ${
|
|
14639
|
+
message: `No hay fases en ${path26.join(opts.workflowDir, "phases")}`,
|
|
14296
14640
|
suggestion: "Revisa la carpeta workflow/phases del proyecto"
|
|
14297
14641
|
});
|
|
14298
14642
|
}
|
|
@@ -14304,7 +14648,7 @@ async function compileTargets(opts) {
|
|
|
14304
14648
|
}
|
|
14305
14649
|
}
|
|
14306
14650
|
const compiled = [...deduped.values()];
|
|
14307
|
-
const manifestPath =
|
|
14651
|
+
const manifestPath = path26.join(root, MANIFEST_REL);
|
|
14308
14652
|
const previous = await readManifest(manifestPath);
|
|
14309
14653
|
const previousHashes = /* @__PURE__ */ new Map();
|
|
14310
14654
|
for (const files2 of Object.values(previous?.targets ?? {})) {
|
|
@@ -14315,7 +14659,7 @@ async function compileTargets(opts) {
|
|
|
14315
14659
|
const stale = [];
|
|
14316
14660
|
const missing = [];
|
|
14317
14661
|
for (const file of compiled) {
|
|
14318
|
-
const abs =
|
|
14662
|
+
const abs = path26.join(root, file.path);
|
|
14319
14663
|
const finalContent = file.path === "AGENTS.md" ? mergeAgentsBlock(await readTextIfExists(abs) ?? "", file.content) : file.content;
|
|
14320
14664
|
const hash = artifactHash(finalContent);
|
|
14321
14665
|
const existing = await readTextIfExists(abs);
|
|
@@ -14331,7 +14675,7 @@ async function compileTargets(opts) {
|
|
|
14331
14675
|
stale.push(file.path);
|
|
14332
14676
|
}
|
|
14333
14677
|
if (!opts.check && status !== "unchanged") {
|
|
14334
|
-
await ensureDir(
|
|
14678
|
+
await ensureDir(path26.dirname(abs));
|
|
14335
14679
|
await writeText(abs, finalContent);
|
|
14336
14680
|
written.push(file.path);
|
|
14337
14681
|
}
|
|
@@ -14385,7 +14729,7 @@ async function readManifest(manifestPath) {
|
|
|
14385
14729
|
}
|
|
14386
14730
|
}
|
|
14387
14731
|
async function checkAdapters(opts) {
|
|
14388
|
-
const manifestPath =
|
|
14732
|
+
const manifestPath = path26.join(path26.resolve(opts.root), MANIFEST_REL);
|
|
14389
14733
|
const manifestRaw = await readTextIfExists(manifestPath);
|
|
14390
14734
|
if (manifestRaw === void 0) return { ok: false, manifest: false, stale: [], missing: [], orphaned: [] };
|
|
14391
14735
|
const manifest = await readManifest(manifestPath);
|
|
@@ -14402,26 +14746,53 @@ async function checkAdapters(opts) {
|
|
|
14402
14746
|
}
|
|
14403
14747
|
|
|
14404
14748
|
// src/paths.ts
|
|
14405
|
-
import
|
|
14749
|
+
import path27 from "path";
|
|
14406
14750
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
14751
|
+
async function isSourceRepoRoot(dir) {
|
|
14752
|
+
if (!await exists(path27.join(dir, "pnpm-workspace.yaml"))) return false;
|
|
14753
|
+
const raw = await readTextIfExists(path27.join(dir, "packages", "cli", "package.json"));
|
|
14754
|
+
if (raw === void 0) return false;
|
|
14755
|
+
try {
|
|
14756
|
+
return JSON.parse(raw).name === "specatlas";
|
|
14757
|
+
} catch {
|
|
14758
|
+
return false;
|
|
14759
|
+
}
|
|
14760
|
+
}
|
|
14761
|
+
async function sourceRepoRoot(startDir) {
|
|
14762
|
+
let dir = startDir;
|
|
14763
|
+
for (let i = 0; i < 5; i += 1) {
|
|
14764
|
+
if (await isSourceRepoRoot(dir)) return dir;
|
|
14765
|
+
const parent = path27.dirname(dir);
|
|
14766
|
+
if (parent === dir) break;
|
|
14767
|
+
dir = parent;
|
|
14768
|
+
}
|
|
14769
|
+
return void 0;
|
|
14770
|
+
}
|
|
14771
|
+
async function resolveFromSource(relative) {
|
|
14772
|
+
const start = path27.dirname(fileURLToPath2(import.meta.url));
|
|
14773
|
+
const root = await sourceRepoRoot(start);
|
|
14774
|
+
if (!root) return void 0;
|
|
14775
|
+
const candidate = path27.join(root, relative);
|
|
14776
|
+
return await exists(candidate) ? candidate : void 0;
|
|
14777
|
+
}
|
|
14407
14778
|
async function resolveProfilesDir() {
|
|
14408
14779
|
const env = process.env["SPECATLAS_PROFILES_DIR"];
|
|
14409
14780
|
if (env && await exists(env)) return env;
|
|
14410
|
-
return walkUpFor("profiles");
|
|
14781
|
+
return await resolveFromSource("profiles") ?? walkUpFor("profiles");
|
|
14411
14782
|
}
|
|
14412
14783
|
async function resolveWorkflowDir() {
|
|
14413
14784
|
const env = process.env["SPECATLAS_WORKFLOW_DIR"];
|
|
14414
14785
|
if (env && await exists(env)) return env;
|
|
14415
|
-
return walkUpFor(
|
|
14786
|
+
return await resolveFromSource("workflow") ?? walkUpFor(path27.join("workflow", "phases"));
|
|
14416
14787
|
}
|
|
14417
14788
|
async function walkUpFor(relative) {
|
|
14418
|
-
let dir =
|
|
14789
|
+
let dir = path27.dirname(fileURLToPath2(import.meta.url));
|
|
14419
14790
|
for (let i = 0; i < 8; i += 1) {
|
|
14420
|
-
const candidate =
|
|
14791
|
+
const candidate = path27.join(dir, relative);
|
|
14421
14792
|
if (await exists(candidate)) {
|
|
14422
|
-
return relative.includes(
|
|
14793
|
+
return relative.includes(path27.sep) ? path27.dirname(candidate) : candidate;
|
|
14423
14794
|
}
|
|
14424
|
-
const parent =
|
|
14795
|
+
const parent = path27.dirname(dir);
|
|
14425
14796
|
if (parent === dir) break;
|
|
14426
14797
|
dir = parent;
|
|
14427
14798
|
}
|
|
@@ -14499,7 +14870,7 @@ async function runAdapters(ctx) {
|
|
|
14499
14870
|
for (const [target, count2] of byTarget) lines.push(` ${target}: ${count2} archivo(s)`);
|
|
14500
14871
|
if (report.written.length > 0) {
|
|
14501
14872
|
lines.push("");
|
|
14502
|
-
for (const p of report.written) lines.push(` + ${
|
|
14873
|
+
for (const p of report.written) lines.push(` + ${path28.relative(ctx.cwd, path28.join(root, p))}`);
|
|
14503
14874
|
} else {
|
|
14504
14875
|
lines.push("");
|
|
14505
14876
|
lines.push("Sin cambios: los adaptadores ya estaban al d\xEDa.");
|
|
@@ -14510,7 +14881,7 @@ async function runAdapters(ctx) {
|
|
|
14510
14881
|
}
|
|
14511
14882
|
|
|
14512
14883
|
// src/commands/adopt.ts
|
|
14513
|
-
import
|
|
14884
|
+
import path29 from "path";
|
|
14514
14885
|
async function runAdopt(ctx) {
|
|
14515
14886
|
const { root } = await requireWorkspace(ctx);
|
|
14516
14887
|
const domainsFlag = flagString(ctx.flags, "domains") ?? flagString(ctx.flags, "domain");
|
|
@@ -14530,11 +14901,11 @@ async function runAdopt(ctx) {
|
|
|
14530
14901
|
}
|
|
14531
14902
|
if (result.createdSpecs.length > 0) {
|
|
14532
14903
|
lines.push("");
|
|
14533
|
-
for (const spec of result.createdSpecs) lines.push(` ${result.dryRun ? "[dry-run] " : "+ "}${
|
|
14904
|
+
for (const spec of result.createdSpecs) lines.push(` ${result.dryRun ? "[dry-run] " : "+ "}${path29.relative(ctx.cwd, spec)}`);
|
|
14534
14905
|
}
|
|
14535
14906
|
if (result.reportPath && !result.dryRun) {
|
|
14536
14907
|
lines.push("");
|
|
14537
|
-
lines.push(` Informe: ${
|
|
14908
|
+
lines.push(` Informe: ${path29.relative(ctx.cwd, result.reportPath)}`);
|
|
14538
14909
|
}
|
|
14539
14910
|
lines.push("");
|
|
14540
14911
|
lines.push("Siguiente: fase /satlas-adopt por dominio (requisitos AS-IS) y luego:");
|
|
@@ -14547,8 +14918,8 @@ async function runAdopt(ctx) {
|
|
|
14547
14918
|
diagnostics: result.diagnostics,
|
|
14548
14919
|
data: {
|
|
14549
14920
|
domains: result.domains.map((d) => ({ name: d.name, files: d.files, existingSpec: d.existingSpec })),
|
|
14550
|
-
createdSpecs: result.createdSpecs.map((s) =>
|
|
14551
|
-
reportPath: result.reportPath ?
|
|
14921
|
+
createdSpecs: result.createdSpecs.map((s) => path29.relative(ctx.cwd, s)),
|
|
14922
|
+
reportPath: result.reportPath ? path29.relative(ctx.cwd, result.reportPath) : void 0,
|
|
14552
14923
|
stack: result.stack,
|
|
14553
14924
|
dryRun: result.dryRun
|
|
14554
14925
|
},
|
|
@@ -14557,7 +14928,7 @@ async function runAdopt(ctx) {
|
|
|
14557
14928
|
}
|
|
14558
14929
|
|
|
14559
14930
|
// src/commands/analyze.ts
|
|
14560
|
-
import
|
|
14931
|
+
import path30 from "path";
|
|
14561
14932
|
async function runAnalyzeCommand(ctx) {
|
|
14562
14933
|
const { root } = await requireWorkspace(ctx);
|
|
14563
14934
|
const slug = ctx.positionals[0];
|
|
@@ -14568,23 +14939,23 @@ async function runAnalyzeCommand(ctx) {
|
|
|
14568
14939
|
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`];
|
|
14569
14940
|
if (result.waves) lines.push(` olas: ${result.waves.blocks} bloque(s), ${result.waves.waves} ola(s), ${result.waves.tasks} tarea(s)`);
|
|
14570
14941
|
if (result.mockups) lines.push(` mockups: ${result.mockups.screens} pantalla(s)${result.mockups.stale ? " (desactualizados)" : ""}`);
|
|
14571
|
-
if (result.path) lines.push("", ` informe: ${
|
|
14942
|
+
if (result.path) lines.push("", ` informe: ${path30.relative(ctx.cwd, result.path)}`);
|
|
14572
14943
|
for (const finding of result.findings) {
|
|
14573
14944
|
lines.push(` ${finding.severity.toUpperCase()} ${finding.code} \u2014 ${finding.message}`);
|
|
14574
14945
|
}
|
|
14575
14946
|
return {
|
|
14576
14947
|
exitCode: result.status === "blocked" ? 1 : 0,
|
|
14577
14948
|
diagnostics: result.findings,
|
|
14578
|
-
data: result.path ? { ...result, path:
|
|
14949
|
+
data: result.path ? { ...result, path: path30.relative(ctx.cwd, result.path) } : result,
|
|
14579
14950
|
text: lines
|
|
14580
14951
|
};
|
|
14581
14952
|
}
|
|
14582
14953
|
|
|
14583
14954
|
// src/commands/approve.ts
|
|
14584
|
-
import
|
|
14955
|
+
import path32 from "path";
|
|
14585
14956
|
|
|
14586
14957
|
// src/commands/issue.ts
|
|
14587
|
-
import
|
|
14958
|
+
import path31 from "path";
|
|
14588
14959
|
async function runIssue(ctx) {
|
|
14589
14960
|
const action = ctx.positionals[0] ?? "status";
|
|
14590
14961
|
const slug = ctx.positionals[1];
|
|
@@ -14612,7 +14983,7 @@ async function runIssue(ctx) {
|
|
|
14612
14983
|
return { exitCode: 2, diagnostics: [{ code: "ATLAS-ISSUE-000", severity: "error", message: "Falta el slug: satlas issue status <slug>" }] };
|
|
14613
14984
|
}
|
|
14614
14985
|
const change = await loadChange(root, slug);
|
|
14615
|
-
const { config } = await loadConfig(
|
|
14986
|
+
const { config } = await loadConfig(path31.join(root, ".sdd"));
|
|
14616
14987
|
const tracker = change.meta?.tracker;
|
|
14617
14988
|
const lines = [`Issue de ${slug}`, ""];
|
|
14618
14989
|
if (!tracker || tracker.provider !== "github") {
|
|
@@ -14683,7 +15054,7 @@ async function runApprove(ctx) {
|
|
|
14683
15054
|
};
|
|
14684
15055
|
}
|
|
14685
15056
|
const { root, workspace, config } = await requireWorkspace(ctx);
|
|
14686
|
-
const artifact = target.includes("/") || target.includes("\\") ? target :
|
|
15057
|
+
const artifact = target.includes("/") || target.includes("\\") ? target : path32.join("changes", target, "spec.md");
|
|
14687
15058
|
const channelFlag = flagString(ctx.flags, "channel");
|
|
14688
15059
|
const channel = channelFlag === "presentation" || channelFlag === "editor" || channelFlag === "pr" || channelFlag === "tracker" ? channelFlag : "cli";
|
|
14689
15060
|
const change = workspace.changes.find(
|
|
@@ -14724,13 +15095,13 @@ async function runApprove(ctx) {
|
|
|
14724
15095
|
return {
|
|
14725
15096
|
exitCode: hasErrors2 ? 1 : 0,
|
|
14726
15097
|
diagnostics: result.diagnostics,
|
|
14727
|
-
data: result.approval ? { ...result.approval, file:
|
|
15098
|
+
data: result.approval ? { ...result.approval, file: path32.relative(ctx.cwd, result.file) } : void 0,
|
|
14728
15099
|
text: lines
|
|
14729
15100
|
};
|
|
14730
15101
|
}
|
|
14731
15102
|
|
|
14732
15103
|
// src/commands/archive.ts
|
|
14733
|
-
import
|
|
15104
|
+
import path33 from "path";
|
|
14734
15105
|
async function runArchive(ctx) {
|
|
14735
15106
|
const slug = ctx.positionals[0];
|
|
14736
15107
|
if (!slug) {
|
|
@@ -14767,7 +15138,7 @@ async function runArchive(ctx) {
|
|
|
14767
15138
|
} else {
|
|
14768
15139
|
lines.push("El delta no contiene operaciones (ADDED/MODIFIED/REMOVED/RENAMED).");
|
|
14769
15140
|
}
|
|
14770
|
-
if (result.archivedTo) lines.push(` archivado en: ${
|
|
15141
|
+
if (result.archivedTo) lines.push(` archivado en: ${path33.relative(ctx.cwd, result.archivedTo)}`);
|
|
14771
15142
|
const hasErrors2 = result.diagnostics.some((d) => d.severity === "error");
|
|
14772
15143
|
return {
|
|
14773
15144
|
exitCode: hasErrors2 ? 1 : 0,
|
|
@@ -14778,7 +15149,7 @@ async function runArchive(ctx) {
|
|
|
14778
15149
|
}
|
|
14779
15150
|
|
|
14780
15151
|
// src/commands/ci.ts
|
|
14781
|
-
import
|
|
15152
|
+
import path34 from "path";
|
|
14782
15153
|
async function runCi(ctx) {
|
|
14783
15154
|
const { root, config } = await requireWorkspace(ctx);
|
|
14784
15155
|
const strict = flagBool(ctx.flags, "strict");
|
|
@@ -14804,10 +15175,10 @@ async function runCi(ctx) {
|
|
|
14804
15175
|
const lines = ["CI de SpecAtlas", ""];
|
|
14805
15176
|
let sarifWritten;
|
|
14806
15177
|
if (sarifPath) {
|
|
14807
|
-
const target =
|
|
15178
|
+
const target = path34.resolve(ctx.cwd, sarifPath);
|
|
14808
15179
|
try {
|
|
14809
15180
|
await writeText(target, toSarifText({ diagnostics: gate.diagnostics, root, version: cliVersion(), failed: gate.failed }));
|
|
14810
|
-
sarifWritten =
|
|
15181
|
+
sarifWritten = path34.relative(ctx.cwd, target);
|
|
14811
15182
|
} catch (err) {
|
|
14812
15183
|
diagnostics.push({
|
|
14813
15184
|
code: "ATLAS-CI-SARIF-001",
|
|
@@ -14882,8 +15253,52 @@ async function runClarify(ctx) {
|
|
|
14882
15253
|
};
|
|
14883
15254
|
}
|
|
14884
15255
|
|
|
15256
|
+
// src/commands/contracts.ts
|
|
15257
|
+
import path35 from "path";
|
|
15258
|
+
async function runContracts(ctx) {
|
|
15259
|
+
const slug = ctx.positionals[0];
|
|
15260
|
+
if (!slug) {
|
|
15261
|
+
return { exitCode: 2, diagnostics: [{ code: "ATLAS-CONTRACTS-000", severity: "error", message: "Falta el slug: satlas contracts <slug>" }] };
|
|
15262
|
+
}
|
|
15263
|
+
const { workspace, config } = await requireWorkspace(ctx);
|
|
15264
|
+
const change = workspace.changes.find((c) => c.slug === slug);
|
|
15265
|
+
if (!change) {
|
|
15266
|
+
return { exitCode: 2, diagnostics: [{ code: "ATLAS-CONTRACTS-000", severity: "error", message: `No existe el cambio "${slug}"` }] };
|
|
15267
|
+
}
|
|
15268
|
+
const lines = [msg("contracts.title", ctx.language), ""];
|
|
15269
|
+
const state = change.contracts;
|
|
15270
|
+
if (!state || state.files.length === 0) {
|
|
15271
|
+
lines.push("El cambio no declara contratos (no hay archivos en su carpeta de contratos).");
|
|
15272
|
+
return { exitCode: 0, diagnostics: [], data: { slug, mode: config.gates.contracts.mode, contracts: [], findings: [] }, text: lines };
|
|
15273
|
+
}
|
|
15274
|
+
const coverage = contractCoverage(change, config.gates.contracts.mode);
|
|
15275
|
+
const findings = [...state.findings, ...coverage];
|
|
15276
|
+
for (const file of state.files) {
|
|
15277
|
+
lines.push(` ${path35.relative(ctx.cwd, file.path)} (${file.format}) \u2014 ${file.operations.length} operaci\xF3n(es)`);
|
|
15278
|
+
for (const operation of file.operations) lines.push(` ${operation.id}`);
|
|
15279
|
+
}
|
|
15280
|
+
lines.push("");
|
|
15281
|
+
if (findings.length === 0) {
|
|
15282
|
+
lines.push("Contratos conformes: forma v\xE1lida y cobertura completa.");
|
|
15283
|
+
} else {
|
|
15284
|
+
for (const finding of findings) lines.push(` ${finding.severity.toUpperCase()} ${finding.code} \u2014 ${finding.message}`);
|
|
15285
|
+
}
|
|
15286
|
+
const errors = findings.filter((finding) => finding.severity === "error").length;
|
|
15287
|
+
return {
|
|
15288
|
+
exitCode: errors > 0 ? 1 : 0,
|
|
15289
|
+
diagnostics: findings,
|
|
15290
|
+
data: {
|
|
15291
|
+
slug,
|
|
15292
|
+
mode: config.gates.contracts.mode,
|
|
15293
|
+
contracts: state.files.map((file) => ({ path: path35.relative(ctx.cwd, file.path), format: file.format, operations: file.operations.map((operation) => operation.id) })),
|
|
15294
|
+
findings
|
|
15295
|
+
},
|
|
15296
|
+
text: lines
|
|
15297
|
+
};
|
|
15298
|
+
}
|
|
15299
|
+
|
|
14885
15300
|
// src/commands/docs.ts
|
|
14886
|
-
import
|
|
15301
|
+
import path36 from "path";
|
|
14887
15302
|
async function runDocs(ctx) {
|
|
14888
15303
|
const slug = ctx.positionals[0];
|
|
14889
15304
|
if (!slug) {
|
|
@@ -14896,7 +15311,7 @@ async function runDocs(ctx) {
|
|
|
14896
15311
|
const errors = result.diagnostics.filter((d) => d.severity === "error");
|
|
14897
15312
|
const lines = [msg("docs.title", ctx.language), ""];
|
|
14898
15313
|
for (const file of result.files) {
|
|
14899
|
-
lines.push(` ${file.created ? msg("docs.created", ctx.language) : msg("docs.updated", ctx.language)}: ${
|
|
15314
|
+
lines.push(` ${file.created ? msg("docs.created", ctx.language) : msg("docs.updated", ctx.language)}: ${path36.relative(ctx.cwd, file.path)}`);
|
|
14900
15315
|
}
|
|
14901
15316
|
for (const error of errors) lines.push(` ERROR ${error.code} \u2014 ${error.message}`);
|
|
14902
15317
|
return {
|
|
@@ -14905,19 +15320,85 @@ async function runDocs(ctx) {
|
|
|
14905
15320
|
data: {
|
|
14906
15321
|
slug: result.slug,
|
|
14907
15322
|
tipo,
|
|
14908
|
-
files: result.files.map((file) => ({ tipo: file.tipo, path:
|
|
15323
|
+
files: result.files.map((file) => ({ tipo: file.tipo, path: path36.relative(ctx.cwd, file.path), created: file.created }))
|
|
14909
15324
|
},
|
|
14910
15325
|
text: lines
|
|
14911
15326
|
};
|
|
14912
15327
|
}
|
|
14913
15328
|
|
|
15329
|
+
// src/commands/link.ts
|
|
15330
|
+
import path37 from "path";
|
|
15331
|
+
async function runLink(ctx) {
|
|
15332
|
+
const { root } = await requireWorkspace(ctx);
|
|
15333
|
+
const action = ctx.positionals[0] ?? "list";
|
|
15334
|
+
if (action === "list") {
|
|
15335
|
+
const state = await loadLinks(root);
|
|
15336
|
+
const lines = [msg("link.title", ctx.language), ""];
|
|
15337
|
+
if (state.entries.length === 0) {
|
|
15338
|
+
lines.push("Sin enlaces. A\xF1ade uno con `satlas link add <ruta>`.");
|
|
15339
|
+
} else {
|
|
15340
|
+
for (const entry of state.entries) {
|
|
15341
|
+
const status = entry.available ? `${entry.requirements} requisito(s) \xB7 ${entry.domains.join(", ") || "sin dominios"}` : "NO DISPONIBLE";
|
|
15342
|
+
lines.push(` ${entry.name} ${path37.relative(ctx.cwd, entry.path)} ${status}`);
|
|
15343
|
+
}
|
|
15344
|
+
}
|
|
15345
|
+
return {
|
|
15346
|
+
exitCode: 0,
|
|
15347
|
+
diagnostics: [],
|
|
15348
|
+
data: {
|
|
15349
|
+
links: state.entries.map((entry) => ({
|
|
15350
|
+
name: entry.name,
|
|
15351
|
+
path: path37.relative(ctx.cwd, entry.path),
|
|
15352
|
+
available: entry.available,
|
|
15353
|
+
requirements: entry.requirements,
|
|
15354
|
+
domains: entry.domains
|
|
15355
|
+
}))
|
|
15356
|
+
},
|
|
15357
|
+
text: lines
|
|
15358
|
+
};
|
|
15359
|
+
}
|
|
15360
|
+
if (action === "add") {
|
|
15361
|
+
const target = ctx.positionals[1];
|
|
15362
|
+
if (!target) {
|
|
15363
|
+
return { exitCode: 2, diagnostics: [{ code: "ATLAS-LINK-000", severity: "error", message: "Falta la ruta: satlas link add <ruta> [--name <nombre>]" }] };
|
|
15364
|
+
}
|
|
15365
|
+
const name = flagString(ctx.flags, "name");
|
|
15366
|
+
const result = await addLink(root, { path: target, ...name !== void 0 ? { name } : {} });
|
|
15367
|
+
const errors = result.diagnostics.filter((d) => d.severity === "error");
|
|
15368
|
+
if (errors.length > 0) {
|
|
15369
|
+
return { exitCode: 2, diagnostics: result.diagnostics, data: { action: "add" }, text: errors.map((error) => `${error.code} \u2014 ${error.message}`) };
|
|
15370
|
+
}
|
|
15371
|
+
return {
|
|
15372
|
+
exitCode: 0,
|
|
15373
|
+
diagnostics: result.diagnostics,
|
|
15374
|
+
data: { action: "add", link: result.entry },
|
|
15375
|
+
text: [`Enlace a\xF1adido: ${result.entry?.name} \u2192 ${result.entry?.path}`]
|
|
15376
|
+
};
|
|
15377
|
+
}
|
|
15378
|
+
if (action === "remove") {
|
|
15379
|
+
const ref = ctx.positionals[1];
|
|
15380
|
+
if (!ref) {
|
|
15381
|
+
return { exitCode: 2, diagnostics: [{ code: "ATLAS-LINK-000", severity: "error", message: "Falta el nombre o la ruta: satlas link remove <nombre|ruta>" }] };
|
|
15382
|
+
}
|
|
15383
|
+
const result = await removeLink(root, ref);
|
|
15384
|
+
if (result.removed === void 0) {
|
|
15385
|
+
return { exitCode: 2, diagnostics: result.diagnostics, data: { action: "remove" }, text: result.diagnostics.map((diagnostic) => `${diagnostic.code} \u2014 ${diagnostic.message}`) };
|
|
15386
|
+
}
|
|
15387
|
+
return { exitCode: 0, diagnostics: result.diagnostics, data: { action: "remove", removed: result.removed }, text: [`Enlace quitado: ${result.removed}`] };
|
|
15388
|
+
}
|
|
15389
|
+
return {
|
|
15390
|
+
exitCode: 2,
|
|
15391
|
+
diagnostics: [{ code: "ATLAS-LINK-000", severity: "error", message: `Acci\xF3n desconocida: ${action}`, suggestion: "Usa: satlas link add <ruta> | satlas link list | satlas link remove <nombre|ruta>" }]
|
|
15392
|
+
};
|
|
15393
|
+
}
|
|
15394
|
+
|
|
14914
15395
|
// src/commands/profile.ts
|
|
14915
|
-
import
|
|
15396
|
+
import path38 from "path";
|
|
14916
15397
|
async function runProfile(ctx) {
|
|
14917
15398
|
const action = ctx.positionals[0] ?? "detect";
|
|
14918
15399
|
const { root, workspace } = await requireWorkspace(ctx);
|
|
14919
15400
|
const officialDir = await resolveProfilesDir();
|
|
14920
|
-
const customDir =
|
|
15401
|
+
const customDir = path38.join(workspace.sddDir, "profiles", "custom");
|
|
14921
15402
|
const profiles = [...officialDir ? await loadProfilesFromDir(officialDir) : [], ...await loadProfilesFromDir(customDir)];
|
|
14922
15403
|
if (action === "detect") {
|
|
14923
15404
|
const result = await detectProfiles(root, profiles);
|
|
@@ -14946,7 +15427,7 @@ async function runProfile(ctx) {
|
|
|
14946
15427
|
if (!name) {
|
|
14947
15428
|
return { exitCode: 2, diagnostics: [{ code: "ATLAS-PROFILE-001", severity: "error", message: "Falta el nombre del perfil: satlas profile create <nombre>" }] };
|
|
14948
15429
|
}
|
|
14949
|
-
const file =
|
|
15430
|
+
const file = path38.join(customDir, `${name}.yaml`);
|
|
14950
15431
|
if (await exists(file)) {
|
|
14951
15432
|
return { exitCode: 2, diagnostics: [{ code: "ATLAS-PROFILE-002", severity: "error", message: `El perfil ya existe: ${file}` }] };
|
|
14952
15433
|
}
|
|
@@ -14955,7 +15436,7 @@ async function runProfile(ctx) {
|
|
|
14955
15436
|
exitCode: 0,
|
|
14956
15437
|
diagnostics: [],
|
|
14957
15438
|
data: { file },
|
|
14958
|
-
text: [`Perfil creado: ${
|
|
15439
|
+
text: [`Perfil creado: ${path38.relative(ctx.cwd, file)}`, "", "Ed\xEDtalo con los comandos reales de tu stack (build/lint/test) y tus anti-patrones."]
|
|
14959
15440
|
};
|
|
14960
15441
|
}
|
|
14961
15442
|
return {
|
|
@@ -14964,7 +15445,7 @@ async function runProfile(ctx) {
|
|
|
14964
15445
|
};
|
|
14965
15446
|
}
|
|
14966
15447
|
async function isInside(dir, name) {
|
|
14967
|
-
return exists(
|
|
15448
|
+
return exists(path38.join(dir, `${name}.yaml`));
|
|
14968
15449
|
}
|
|
14969
15450
|
function profileTemplate(name) {
|
|
14970
15451
|
return `name: ${name}
|
|
@@ -14992,7 +15473,7 @@ assumptions: []
|
|
|
14992
15473
|
}
|
|
14993
15474
|
|
|
14994
15475
|
// src/commands/doctor.ts
|
|
14995
|
-
import
|
|
15476
|
+
import path39 from "path";
|
|
14996
15477
|
async function runDoctorCommand(ctx) {
|
|
14997
15478
|
const { root, config } = await requireWorkspace(ctx);
|
|
14998
15479
|
const report = await runDoctor(root);
|
|
@@ -15017,7 +15498,7 @@ async function runDoctorCommand(ctx) {
|
|
|
15017
15498
|
lines.push("Sin hallazgos. El workspace est\xE1 sano.");
|
|
15018
15499
|
} else {
|
|
15019
15500
|
for (const finding of report.findings) {
|
|
15020
|
-
const location = finding.path ? ` ${
|
|
15501
|
+
const location = finding.path ? ` ${path39.relative(ctx.cwd, finding.path)}${finding.line ? `:${finding.line}` : ""}` : "";
|
|
15021
15502
|
lines.push(` ${finding.severity.toUpperCase()} ${finding.code}${location} \u2014 ${finding.message}`);
|
|
15022
15503
|
if (finding.suggestion) lines.push(` \u21B3 ${finding.suggestion}`);
|
|
15023
15504
|
}
|
|
@@ -15091,7 +15572,7 @@ async function runHelp(ctx, commandName) {
|
|
|
15091
15572
|
}
|
|
15092
15573
|
|
|
15093
15574
|
// src/commands/init.ts
|
|
15094
|
-
import
|
|
15575
|
+
import path40 from "path";
|
|
15095
15576
|
async function runInit(ctx) {
|
|
15096
15577
|
const languageFlag = flagString(ctx.flags, "language");
|
|
15097
15578
|
const language = languageFlag === "en" ? "en" : languageFlag === "es" ? "es" : void 0;
|
|
@@ -15138,11 +15619,11 @@ async function runInit(ctx) {
|
|
|
15138
15619
|
const lines = [msg("init.title", ctx.language), ""];
|
|
15139
15620
|
if (result.created.length > 0) {
|
|
15140
15621
|
lines.push(msg("init.done", ctx.language));
|
|
15141
|
-
for (const file of result.created) lines.push(` + ${
|
|
15622
|
+
for (const file of result.created) lines.push(` + ${path40.relative(ctx.cwd, file)}`);
|
|
15142
15623
|
if (compiled.length > 0) {
|
|
15143
15624
|
lines.push("");
|
|
15144
15625
|
lines.push("Adaptadores compilados:");
|
|
15145
|
-
for (const file of compiled) lines.push(` + ${
|
|
15626
|
+
for (const file of compiled) lines.push(` + ${path40.relative(ctx.cwd, path40.join(ctx.cwd, file))}`);
|
|
15146
15627
|
}
|
|
15147
15628
|
lines.push("");
|
|
15148
15629
|
lines.push("Siguiente: crea un cambio con `satlas new <slug>` y escribe la spec funcional.");
|
|
@@ -15152,8 +15633,8 @@ async function runInit(ctx) {
|
|
|
15152
15633
|
exitCode: hasErrors2 ? 1 : 0,
|
|
15153
15634
|
diagnostics: result.diagnostics,
|
|
15154
15635
|
data: {
|
|
15155
|
-
sddDir:
|
|
15156
|
-
created: result.created.map((f) =>
|
|
15636
|
+
sddDir: path40.relative(ctx.cwd, result.sddDir),
|
|
15637
|
+
created: result.created.map((f) => path40.relative(ctx.cwd, f)),
|
|
15157
15638
|
detected: result.detected?.matches.map((m) => ({ name: m.name, score: m.score })),
|
|
15158
15639
|
best: result.detected?.best?.name
|
|
15159
15640
|
},
|
|
@@ -15194,7 +15675,7 @@ async function runMetrics(ctx) {
|
|
|
15194
15675
|
}
|
|
15195
15676
|
|
|
15196
15677
|
// src/commands/mockup.ts
|
|
15197
|
-
import
|
|
15678
|
+
import path41 from "path";
|
|
15198
15679
|
async function runMockup(ctx) {
|
|
15199
15680
|
const { root, workspace, config } = await requireWorkspace(ctx);
|
|
15200
15681
|
const slug = ctx.positionals[0];
|
|
@@ -15243,17 +15724,17 @@ async function runMockup(ctx) {
|
|
|
15243
15724
|
` pantallas: ${plan.screens.length}`,
|
|
15244
15725
|
...plan.screens.map((s) => ` - ${s.id}: ${s.title} (${s.illustrates.length} escenario(s))`),
|
|
15245
15726
|
"",
|
|
15246
|
-
` plan: ${
|
|
15247
|
-
` manifiesto: ${
|
|
15727
|
+
` plan: ${path41.relative(ctx.cwd, planFile)}`,
|
|
15728
|
+
` manifiesto: ${path41.relative(ctx.cwd, manifestFile)}`,
|
|
15248
15729
|
"",
|
|
15249
15730
|
"Siguiente: genera el HTML de cada pantalla (fase /satlas-mockup) y valida con `satlas mockup " + slug + " --check`."
|
|
15250
15731
|
];
|
|
15251
|
-
return { exitCode: 0, diagnostics: [], data: { plan, planFile:
|
|
15732
|
+
return { exitCode: 0, diagnostics: [], data: { plan, planFile: path41.relative(ctx.cwd, planFile), manifestFile: path41.relative(ctx.cwd, manifestFile) }, text: lines };
|
|
15252
15733
|
}
|
|
15253
15734
|
|
|
15254
15735
|
// src/mcp/server.ts
|
|
15255
15736
|
import { promises as fs3 } from "fs";
|
|
15256
|
-
import
|
|
15737
|
+
import path46 from "path";
|
|
15257
15738
|
import { createInterface } from "readline/promises";
|
|
15258
15739
|
|
|
15259
15740
|
// src/mcp/protocol.ts
|
|
@@ -15314,6 +15795,35 @@ function stringArg(args, key) {
|
|
|
15314
15795
|
return typeof value === "string" && value.trim() !== "" ? value.trim() : void 0;
|
|
15315
15796
|
}
|
|
15316
15797
|
|
|
15798
|
+
// src/mcp/tools/contracts.ts
|
|
15799
|
+
import path42 from "path";
|
|
15800
|
+
async function runAtlasContracts(args, host) {
|
|
15801
|
+
const resolved = await host.getWorkspace();
|
|
15802
|
+
if (!resolved) return noWorkspaceResult();
|
|
15803
|
+
const slug = stringArg(args, "slug");
|
|
15804
|
+
if (!slug) return errorResult("ATLAS-MCP-CONTRACTS-001", "Falta el nombre del cambio", { usage: "atlas_contracts { slug }" });
|
|
15805
|
+
const change = resolved.workspace.changes.find((candidate) => candidate.slug === slug);
|
|
15806
|
+
if (!change) {
|
|
15807
|
+
return errorResult("ATLAS-MCP-CONTRACTS-002", `El cambio "${slug}" no existe`, { available: resolved.workspace.changes.map((candidate) => candidate.slug) });
|
|
15808
|
+
}
|
|
15809
|
+
const mode = resolved.config.gates.contracts.mode;
|
|
15810
|
+
const state = change.contracts;
|
|
15811
|
+
if (!state || state.files.length === 0) {
|
|
15812
|
+
return jsonResult({ slug, mode, contracts: [], findings: [], message: "El cambio no declara contratos." });
|
|
15813
|
+
}
|
|
15814
|
+
const findings = [...state.findings, ...contractCoverage(change, mode)];
|
|
15815
|
+
return jsonResult({
|
|
15816
|
+
slug,
|
|
15817
|
+
mode,
|
|
15818
|
+
contracts: state.files.map((file) => ({
|
|
15819
|
+
path: path42.relative(resolved.root, file.path),
|
|
15820
|
+
format: file.format,
|
|
15821
|
+
operations: file.operations.map((operation) => operation.id)
|
|
15822
|
+
})),
|
|
15823
|
+
findings
|
|
15824
|
+
});
|
|
15825
|
+
}
|
|
15826
|
+
|
|
15317
15827
|
// src/mcp/tools/fixes.ts
|
|
15318
15828
|
async function runAtlasFixes(_args, host) {
|
|
15319
15829
|
const resolved = await host.getWorkspace();
|
|
@@ -15342,12 +15852,12 @@ async function runAtlasFixes(_args, host) {
|
|
|
15342
15852
|
}
|
|
15343
15853
|
|
|
15344
15854
|
// src/mcp/tools/glossary.ts
|
|
15345
|
-
import
|
|
15855
|
+
import path43 from "path";
|
|
15346
15856
|
async function runAtlasGlossary(_args, host) {
|
|
15347
15857
|
const resolved = await host.getWorkspace();
|
|
15348
15858
|
if (!resolved) return noWorkspaceResult();
|
|
15349
15859
|
const { root, config } = resolved;
|
|
15350
|
-
const glossaryPath =
|
|
15860
|
+
const glossaryPath = path43.resolve(root, config.spec.glossary);
|
|
15351
15861
|
const raw = await readTextIfExists(glossaryPath);
|
|
15352
15862
|
if (raw === void 0) {
|
|
15353
15863
|
return jsonResult({
|
|
@@ -15367,6 +15877,28 @@ async function runAtlasGlossary(_args, host) {
|
|
|
15367
15877
|
return jsonResult({ terms: parsed.terms });
|
|
15368
15878
|
}
|
|
15369
15879
|
|
|
15880
|
+
// src/mcp/tools/links.ts
|
|
15881
|
+
import path44 from "path";
|
|
15882
|
+
async function runAtlasLinks(_args, host) {
|
|
15883
|
+
const resolved = await host.getWorkspace();
|
|
15884
|
+
if (!resolved) return noWorkspaceResult();
|
|
15885
|
+
const state = resolved.workspace.links;
|
|
15886
|
+
if (!state || state.entries.length === 0) {
|
|
15887
|
+
return jsonResult({ links: [], message: "El proyecto no tiene enlaces.", action: "satlas link add <ruta>" });
|
|
15888
|
+
}
|
|
15889
|
+
return jsonResult({
|
|
15890
|
+
links: state.entries.map((entry) => ({
|
|
15891
|
+
name: entry.name,
|
|
15892
|
+
path: path44.relative(resolved.root, entry.path),
|
|
15893
|
+
available: entry.available,
|
|
15894
|
+
requirements: entry.requirements,
|
|
15895
|
+
domains: entry.domains
|
|
15896
|
+
})),
|
|
15897
|
+
externalRequirements: linkedRequirementIds(state).length,
|
|
15898
|
+
unavailable: state.unavailable
|
|
15899
|
+
});
|
|
15900
|
+
}
|
|
15901
|
+
|
|
15370
15902
|
// src/mcp/tools/impact.ts
|
|
15371
15903
|
var REQ_TARGET_RE = /^REQ-[A-Z0-9]+(?:-[A-Z0-9]+)*-\d{3}(?:-S\d+)?$/i;
|
|
15372
15904
|
async function runAtlasImpact(args, host) {
|
|
@@ -15393,7 +15925,7 @@ async function runAtlasImpact(args, host) {
|
|
|
15393
15925
|
}
|
|
15394
15926
|
|
|
15395
15927
|
// src/evaluate.ts
|
|
15396
|
-
import
|
|
15928
|
+
import path45 from "path";
|
|
15397
15929
|
function livingRequirementsMap2(specs) {
|
|
15398
15930
|
const map = /* @__PURE__ */ new Map();
|
|
15399
15931
|
for (const spec of specs) {
|
|
@@ -15402,7 +15934,7 @@ function livingRequirementsMap2(specs) {
|
|
|
15402
15934
|
return map;
|
|
15403
15935
|
}
|
|
15404
15936
|
async function evaluateChange(workspace, config, change, approvals) {
|
|
15405
|
-
const deltaPath =
|
|
15937
|
+
const deltaPath = path45.join(change.dir, "spec.md");
|
|
15406
15938
|
const deltaContent = await readTextIfExists(deltaPath);
|
|
15407
15939
|
const approval = verifyApproval(change, approvals, config, deltaContent ?? void 0);
|
|
15408
15940
|
const living = livingRequirementsMap2(workspace.specs);
|
|
@@ -15410,7 +15942,8 @@ async function evaluateChange(workspace, config, change, approvals) {
|
|
|
15410
15942
|
const trace = checkTrace({
|
|
15411
15943
|
specs: workspace.specs,
|
|
15412
15944
|
change,
|
|
15413
|
-
requireEvidence: config.gates.verify.mode !== "off" && config.gates.verify.require_evidence
|
|
15945
|
+
requireEvidence: config.gates.verify.mode !== "off" && config.gates.verify.require_evidence,
|
|
15946
|
+
linked: linkedTraceInput(workspace)
|
|
15414
15947
|
});
|
|
15415
15948
|
const blocking = lintFindings.filter((d) => d.severity === "error").length + trace.findings.filter((d) => d.severity === "error").length;
|
|
15416
15949
|
const mockupsAreReady = requiresMockups(change.meta, config) ? await mockupsReady(workspace.root, change.slug, change) : void 0;
|
|
@@ -15421,7 +15954,7 @@ async function evaluateChange(workspace, config, change, approvals) {
|
|
|
15421
15954
|
blockingFindings: change.delta ? blocking : 0,
|
|
15422
15955
|
...mockupsAreReady !== void 0 ? { mockupsReady: mockupsAreReady } : {}
|
|
15423
15956
|
});
|
|
15424
|
-
const phaseAdvisories = [...clarifyAdvisory(change, config), ...docsAdvisory(change, config)];
|
|
15957
|
+
const phaseAdvisories = [...clarifyAdvisory(change, config), ...docsAdvisory(change, config), ...contractsAdvisory(change, config)];
|
|
15425
15958
|
return { change, approval, lintFindings: [...lintFindings, ...phaseAdvisories], trace, blocking, state };
|
|
15426
15959
|
}
|
|
15427
15960
|
|
|
@@ -15580,7 +16113,9 @@ var HANDLERS = {
|
|
|
15580
16113
|
atlas_trace: runAtlasTrace,
|
|
15581
16114
|
atlas_impact: runAtlasImpact,
|
|
15582
16115
|
atlas_glossary: runAtlasGlossary,
|
|
15583
|
-
atlas_fixes: runAtlasFixes
|
|
16116
|
+
atlas_fixes: runAtlasFixes,
|
|
16117
|
+
atlas_contracts: runAtlasContracts,
|
|
16118
|
+
atlas_links: runAtlasLinks
|
|
15584
16119
|
};
|
|
15585
16120
|
async function runToolByName(name, args, host) {
|
|
15586
16121
|
const handler = HANDLERS[name];
|
|
@@ -15658,6 +16193,25 @@ function listTools() {
|
|
|
15658
16193
|
properties: {},
|
|
15659
16194
|
additionalProperties: false
|
|
15660
16195
|
}
|
|
16196
|
+
},
|
|
16197
|
+
{
|
|
16198
|
+
name: "atlas_contracts",
|
|
16199
|
+
description: "Contratos del cambio: operaciones declaradas y hallazgos de forma y cobertura con los escenarios. Solo lectura.",
|
|
16200
|
+
inputSchema: {
|
|
16201
|
+
type: "object",
|
|
16202
|
+
properties: { slug: { type: "string", description: "Nombre del cambio" } },
|
|
16203
|
+
required: ["slug"],
|
|
16204
|
+
additionalProperties: false
|
|
16205
|
+
}
|
|
16206
|
+
},
|
|
16207
|
+
{
|
|
16208
|
+
name: "atlas_links",
|
|
16209
|
+
description: "Enlaces a otros proyectos: nombre, disponibilidad, requisitos externos que aportan y enlaces no disponibles. Solo lectura.",
|
|
16210
|
+
inputSchema: {
|
|
16211
|
+
type: "object",
|
|
16212
|
+
properties: {},
|
|
16213
|
+
additionalProperties: false
|
|
16214
|
+
}
|
|
15661
16215
|
}
|
|
15662
16216
|
];
|
|
15663
16217
|
}
|
|
@@ -15675,8 +16229,8 @@ var WorkspaceCache = class {
|
|
|
15675
16229
|
}
|
|
15676
16230
|
};
|
|
15677
16231
|
async function workspaceStamp(root) {
|
|
15678
|
-
const sddDir =
|
|
15679
|
-
const markers = [sddDir,
|
|
16232
|
+
const sddDir = path46.join(root, ".sdd");
|
|
16233
|
+
const markers = [sddDir, path46.join(sddDir, "config.yaml"), path46.join(sddDir, "changes"), path46.join(sddDir, "specs"), path46.join(sddDir, "approvals.yaml")];
|
|
15680
16234
|
let stamp = 0;
|
|
15681
16235
|
for (const marker of markers) {
|
|
15682
16236
|
try {
|
|
@@ -15694,7 +16248,7 @@ async function resolveMcpWorkspace(cwd, cache) {
|
|
|
15694
16248
|
const cached2 = cache.get(root);
|
|
15695
16249
|
if (cached2 && cached2.mtimeMs === stamp) return cached2.value;
|
|
15696
16250
|
const { workspace, config } = await loadWorkspace(root);
|
|
15697
|
-
const approvals = await loadApprovals(
|
|
16251
|
+
const approvals = await loadApprovals(path46.join(root, ".sdd"));
|
|
15698
16252
|
const value = { root, workspace, config, approvals: approvals.byArtifact };
|
|
15699
16253
|
cache.set(root, stamp, value);
|
|
15700
16254
|
return value;
|
|
@@ -15786,7 +16340,7 @@ async function runMcp(ctx) {
|
|
|
15786
16340
|
}
|
|
15787
16341
|
|
|
15788
16342
|
// src/commands/packs.ts
|
|
15789
|
-
import
|
|
16343
|
+
import path47 from "path";
|
|
15790
16344
|
async function runPacks(ctx) {
|
|
15791
16345
|
const { root, config, workspace } = await requireWorkspace(ctx);
|
|
15792
16346
|
const slug = flagString(ctx.flags, "check");
|
|
@@ -15844,14 +16398,14 @@ async function runPacks(ctx) {
|
|
|
15844
16398
|
data: {
|
|
15845
16399
|
slug,
|
|
15846
16400
|
packs: evaluations.map((evaluation) => ({ id: evaluation.pack.id, status: evaluation.status, passed: evaluation.passed, failed: evaluation.failed, results: evaluation.results })),
|
|
15847
|
-
analyzePath:
|
|
16401
|
+
analyzePath: path47.posix.join(".sdd", "changes", slug, "analyze.md")
|
|
15848
16402
|
},
|
|
15849
16403
|
text: lines
|
|
15850
16404
|
};
|
|
15851
16405
|
}
|
|
15852
16406
|
|
|
15853
16407
|
// src/commands/new.ts
|
|
15854
|
-
import
|
|
16408
|
+
import path48 from "path";
|
|
15855
16409
|
async function runNew(ctx) {
|
|
15856
16410
|
const slug = ctx.positionals[0];
|
|
15857
16411
|
if (!slug) {
|
|
@@ -15877,14 +16431,14 @@ async function runNew(ctx) {
|
|
|
15877
16431
|
msg("new.title", ctx.language),
|
|
15878
16432
|
"",
|
|
15879
16433
|
msg("new.done", ctx.language),
|
|
15880
|
-
...result.files.map((f) => ` + ${
|
|
16434
|
+
...result.files.map((f) => ` + ${path48.relative(ctx.cwd, f)}`),
|
|
15881
16435
|
"",
|
|
15882
16436
|
`Siguiente: /satlas.specify ${result.slug} \u2014 escribe la especificaci\xF3n 100% funcional y de negocio.`
|
|
15883
16437
|
];
|
|
15884
16438
|
return {
|
|
15885
16439
|
exitCode: hasErrors2 ? 1 : 0,
|
|
15886
16440
|
diagnostics: result.diagnostics,
|
|
15887
|
-
data: { slug: result.slug, files: result.files.map((f) =>
|
|
16441
|
+
data: { slug: result.slug, files: result.files.map((f) => path48.relative(ctx.cwd, f)) },
|
|
15888
16442
|
text: lines
|
|
15889
16443
|
};
|
|
15890
16444
|
}
|
|
@@ -15919,7 +16473,7 @@ async function runNext(ctx) {
|
|
|
15919
16473
|
}
|
|
15920
16474
|
|
|
15921
16475
|
// src/commands/present.ts
|
|
15922
|
-
import
|
|
16476
|
+
import path49 from "path";
|
|
15923
16477
|
async function runPresentCommand(ctx) {
|
|
15924
16478
|
const { root } = await requireWorkspace(ctx);
|
|
15925
16479
|
const slug = ctx.positionals[0];
|
|
@@ -15931,7 +16485,7 @@ async function runPresentCommand(ctx) {
|
|
|
15931
16485
|
if (result.path) {
|
|
15932
16486
|
lines.push("Propuesta generada");
|
|
15933
16487
|
lines.push("");
|
|
15934
|
-
lines.push(` archivo: ${
|
|
16488
|
+
lines.push(` archivo: ${path49.relative(ctx.cwd, result.path)}`);
|
|
15935
16489
|
if (result.hash) lines.push(` hash de la spec: ${shortHash(result.hash)}`);
|
|
15936
16490
|
lines.push("");
|
|
15937
16491
|
lines.push("\xC1brela en el navegador y comp\xE1rtela con el stakeholder.");
|
|
@@ -15941,7 +16495,7 @@ async function runPresentCommand(ctx) {
|
|
|
15941
16495
|
return {
|
|
15942
16496
|
exitCode: hasErrors2 ? 1 : 0,
|
|
15943
16497
|
diagnostics: result.diagnostics,
|
|
15944
|
-
data: result.path ? { path:
|
|
16498
|
+
data: result.path ? { path: path49.relative(ctx.cwd, result.path), hash: result.hash } : void 0,
|
|
15945
16499
|
text: lines
|
|
15946
16500
|
};
|
|
15947
16501
|
}
|
|
@@ -16089,7 +16643,7 @@ async function runStatus(ctx) {
|
|
|
16089
16643
|
}
|
|
16090
16644
|
|
|
16091
16645
|
// src/commands/trace.ts
|
|
16092
|
-
import
|
|
16646
|
+
import path50 from "path";
|
|
16093
16647
|
async function runTrace(ctx) {
|
|
16094
16648
|
const { workspace, config } = await requireWorkspace(ctx);
|
|
16095
16649
|
const slug = flagString(ctx.flags, "change");
|
|
@@ -16109,7 +16663,7 @@ async function runTrace(ctx) {
|
|
|
16109
16663
|
data.push({ slug: change.slug, nodes: result.graph.nodes.length, edges: result.graph.edges.length, summary: result.summary, findings: result.findings });
|
|
16110
16664
|
lines.push(` ${change.slug}: ${result.graph.nodes.length} nodos, ${result.graph.edges.length} aristas, ${errors} errores, ${result.summary.warnings} avisos`);
|
|
16111
16665
|
for (const finding of result.findings) {
|
|
16112
|
-
const location = finding.path ? ` (${
|
|
16666
|
+
const location = finding.path ? ` (${path50.relative(ctx.cwd, finding.path)}${finding.line ? `:${finding.line}` : ""})` : "";
|
|
16113
16667
|
lines.push(` ${finding.severity.toUpperCase()} ${finding.code} \u2014 ${finding.message}${location}`);
|
|
16114
16668
|
}
|
|
16115
16669
|
}
|
|
@@ -16119,7 +16673,7 @@ async function runTrace(ctx) {
|
|
|
16119
16673
|
}
|
|
16120
16674
|
|
|
16121
16675
|
// src/commands/upgrade.ts
|
|
16122
|
-
import
|
|
16676
|
+
import path51 from "path";
|
|
16123
16677
|
async function runUpgrade(ctx) {
|
|
16124
16678
|
const { root } = await requireWorkspace(ctx);
|
|
16125
16679
|
const apply = flagBool(ctx.flags, "apply");
|
|
@@ -16173,7 +16727,7 @@ function renderPreview(plan, root, ctx) {
|
|
|
16173
16727
|
}
|
|
16174
16728
|
for (const item of plan.unreadable) {
|
|
16175
16729
|
lines.push(` Ilegible: "${item.artifact}" \u2014 ${item.reason}`);
|
|
16176
|
-
lines.push(` \u21B3 ${
|
|
16730
|
+
lines.push(` \u21B3 ${path51.relative(ctx.cwd, item.path)}`);
|
|
16177
16731
|
}
|
|
16178
16732
|
return { exitCode: 0, diagnostics: [], data: summarizable(plan), text: lines };
|
|
16179
16733
|
}
|
|
@@ -16236,7 +16790,7 @@ function renderRollback(report, ctx) {
|
|
|
16236
16790
|
}
|
|
16237
16791
|
|
|
16238
16792
|
// src/commands/validate.ts
|
|
16239
|
-
import
|
|
16793
|
+
import path52 from "path";
|
|
16240
16794
|
async function runValidate(ctx) {
|
|
16241
16795
|
const { root, workspace, config, approvals } = await requireWorkspace(ctx);
|
|
16242
16796
|
const slug = flagString(ctx.flags, "change");
|
|
@@ -16263,7 +16817,7 @@ async function runValidate(ctx) {
|
|
|
16263
16817
|
for (const finding of evaluation.lintFindings) {
|
|
16264
16818
|
lines.push(` ${finding.severity.toUpperCase()} ${finding.code}${finding.line ? `:${finding.line}` : ""} \u2014 ${finding.message}`);
|
|
16265
16819
|
}
|
|
16266
|
-
lines.push(` archivo: ${
|
|
16820
|
+
lines.push(` archivo: ${path52.relative(ctx.cwd, path52.join(change.dir, "spec.md"))}`);
|
|
16267
16821
|
}
|
|
16268
16822
|
if (targets.length === 0) lines.push("Sin cambios activos.");
|
|
16269
16823
|
if (advisory && advisory.plan.pending.length > 0) {
|
|
@@ -16291,7 +16845,7 @@ async function runValidate(ctx) {
|
|
|
16291
16845
|
}
|
|
16292
16846
|
|
|
16293
16847
|
// src/commands/verify.ts
|
|
16294
|
-
import
|
|
16848
|
+
import path53 from "path";
|
|
16295
16849
|
async function runVerify(ctx) {
|
|
16296
16850
|
const { root, workspace } = await requireWorkspace(ctx);
|
|
16297
16851
|
const slug = ctx.positionals[0];
|
|
@@ -16334,7 +16888,7 @@ async function runVerify(ctx) {
|
|
|
16334
16888
|
return { exitCode: 2, diagnostics: [{ code: "ATLAS-VERIFY-001", severity: "error", message: "Falta --by <nombre> (o define meta.owner)" }] };
|
|
16335
16889
|
}
|
|
16336
16890
|
const profilesDir = await resolveProfilesDir();
|
|
16337
|
-
const profile = await loadActiveProfile(
|
|
16891
|
+
const profile = await loadActiveProfile(path53.join(root, ".sdd"), [profilesDir ?? "", path53.join(root, ".sdd", "profiles", "custom")].filter(Boolean));
|
|
16338
16892
|
const record = await recordEvidence({
|
|
16339
16893
|
root,
|
|
16340
16894
|
slug,
|
|
@@ -16356,7 +16910,7 @@ async function runVerify(ctx) {
|
|
|
16356
16910
|
if (record.evidence.command) lines.push(` comando: ${record.evidence.command}`);
|
|
16357
16911
|
lines.push(` resultado: ${record.evidence.result}`);
|
|
16358
16912
|
if (record.evidence.outputHash) lines.push(` hash: ${record.evidence.outputHash}`);
|
|
16359
|
-
lines.push(` archivo: ${
|
|
16913
|
+
lines.push(` archivo: ${path53.relative(ctx.cwd, record.path)}`);
|
|
16360
16914
|
if (record.stdout) {
|
|
16361
16915
|
lines.push("");
|
|
16362
16916
|
lines.push(" salida (\xFAltimas l\xEDneas):");
|
|
@@ -16371,7 +16925,7 @@ async function runVerify(ctx) {
|
|
|
16371
16925
|
return {
|
|
16372
16926
|
exitCode: record.exitCode === 0 ? 0 : 1,
|
|
16373
16927
|
diagnostics: record.diagnostics,
|
|
16374
|
-
data: { evidence: record.evidence, path:
|
|
16928
|
+
data: { evidence: record.evidence, path: path53.relative(ctx.cwd, record.path), exitCode: record.exitCode },
|
|
16375
16929
|
text: lines
|
|
16376
16930
|
};
|
|
16377
16931
|
}
|
|
@@ -16428,6 +16982,8 @@ var HANDLERS2 = {
|
|
|
16428
16982
|
doctor: runDoctorCommand,
|
|
16429
16983
|
clarify: runClarify,
|
|
16430
16984
|
docs: runDocs,
|
|
16985
|
+
contracts: runContracts,
|
|
16986
|
+
link: runLink,
|
|
16431
16987
|
upgrade: runUpgrade,
|
|
16432
16988
|
approve: runApprove,
|
|
16433
16989
|
issue: runIssue,
|
|
@@ -16473,7 +17029,7 @@ async function requireWorkspace(ctx) {
|
|
|
16473
17029
|
throw new CliError("ATLAS-WS-001", msg("cli.noWorkspace", ctx.language), 2);
|
|
16474
17030
|
}
|
|
16475
17031
|
const { workspace, config } = await loadWorkspace(root);
|
|
16476
|
-
const approvals = await loadApprovals(
|
|
17032
|
+
const approvals = await loadApprovals(path54.join(root, ".sdd"));
|
|
16477
17033
|
return { root, workspace, config, approvals: approvals.byArtifact };
|
|
16478
17034
|
}
|
|
16479
17035
|
var CliError = class extends Error {
|
|
@@ -16540,7 +17096,7 @@ function printResult(command, result, json2, language) {
|
|
|
16540
17096
|
}
|
|
16541
17097
|
const lines = [...result.text ?? []];
|
|
16542
17098
|
for (const d of result.diagnostics) {
|
|
16543
|
-
const location = d.path ? ` ${
|
|
17099
|
+
const location = d.path ? ` ${path54.relative(process.cwd(), d.path)}${d.line ? `:${d.line}` : ""}` : "";
|
|
16544
17100
|
lines.push(`${d.severity === "error" ? "ERROR" : d.severity === "warning" ? "AVISO" : "NOTA"} ${d.code}${location} \u2014 ${d.message}`);
|
|
16545
17101
|
if (d.suggestion) lines.push(` \u21B3 ${d.suggestion}`);
|
|
16546
17102
|
}
|