specatlas 0.1.30 → 0.1.31
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 +844 -315
- 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,7 +14746,7 @@ 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";
|
|
14407
14751
|
async function resolveProfilesDir() {
|
|
14408
14752
|
const env = process.env["SPECATLAS_PROFILES_DIR"];
|
|
@@ -14412,16 +14756,16 @@ async function resolveProfilesDir() {
|
|
|
14412
14756
|
async function resolveWorkflowDir() {
|
|
14413
14757
|
const env = process.env["SPECATLAS_WORKFLOW_DIR"];
|
|
14414
14758
|
if (env && await exists(env)) return env;
|
|
14415
|
-
return walkUpFor(
|
|
14759
|
+
return walkUpFor(path27.join("workflow", "phases"));
|
|
14416
14760
|
}
|
|
14417
14761
|
async function walkUpFor(relative) {
|
|
14418
|
-
let dir =
|
|
14762
|
+
let dir = path27.dirname(fileURLToPath2(import.meta.url));
|
|
14419
14763
|
for (let i = 0; i < 8; i += 1) {
|
|
14420
|
-
const candidate =
|
|
14764
|
+
const candidate = path27.join(dir, relative);
|
|
14421
14765
|
if (await exists(candidate)) {
|
|
14422
|
-
return relative.includes(
|
|
14766
|
+
return relative.includes(path27.sep) ? path27.dirname(candidate) : candidate;
|
|
14423
14767
|
}
|
|
14424
|
-
const parent =
|
|
14768
|
+
const parent = path27.dirname(dir);
|
|
14425
14769
|
if (parent === dir) break;
|
|
14426
14770
|
dir = parent;
|
|
14427
14771
|
}
|
|
@@ -14499,7 +14843,7 @@ async function runAdapters(ctx) {
|
|
|
14499
14843
|
for (const [target, count2] of byTarget) lines.push(` ${target}: ${count2} archivo(s)`);
|
|
14500
14844
|
if (report.written.length > 0) {
|
|
14501
14845
|
lines.push("");
|
|
14502
|
-
for (const p of report.written) lines.push(` + ${
|
|
14846
|
+
for (const p of report.written) lines.push(` + ${path28.relative(ctx.cwd, path28.join(root, p))}`);
|
|
14503
14847
|
} else {
|
|
14504
14848
|
lines.push("");
|
|
14505
14849
|
lines.push("Sin cambios: los adaptadores ya estaban al d\xEDa.");
|
|
@@ -14510,7 +14854,7 @@ async function runAdapters(ctx) {
|
|
|
14510
14854
|
}
|
|
14511
14855
|
|
|
14512
14856
|
// src/commands/adopt.ts
|
|
14513
|
-
import
|
|
14857
|
+
import path29 from "path";
|
|
14514
14858
|
async function runAdopt(ctx) {
|
|
14515
14859
|
const { root } = await requireWorkspace(ctx);
|
|
14516
14860
|
const domainsFlag = flagString(ctx.flags, "domains") ?? flagString(ctx.flags, "domain");
|
|
@@ -14530,11 +14874,11 @@ async function runAdopt(ctx) {
|
|
|
14530
14874
|
}
|
|
14531
14875
|
if (result.createdSpecs.length > 0) {
|
|
14532
14876
|
lines.push("");
|
|
14533
|
-
for (const spec of result.createdSpecs) lines.push(` ${result.dryRun ? "[dry-run] " : "+ "}${
|
|
14877
|
+
for (const spec of result.createdSpecs) lines.push(` ${result.dryRun ? "[dry-run] " : "+ "}${path29.relative(ctx.cwd, spec)}`);
|
|
14534
14878
|
}
|
|
14535
14879
|
if (result.reportPath && !result.dryRun) {
|
|
14536
14880
|
lines.push("");
|
|
14537
|
-
lines.push(` Informe: ${
|
|
14881
|
+
lines.push(` Informe: ${path29.relative(ctx.cwd, result.reportPath)}`);
|
|
14538
14882
|
}
|
|
14539
14883
|
lines.push("");
|
|
14540
14884
|
lines.push("Siguiente: fase /satlas-adopt por dominio (requisitos AS-IS) y luego:");
|
|
@@ -14547,8 +14891,8 @@ async function runAdopt(ctx) {
|
|
|
14547
14891
|
diagnostics: result.diagnostics,
|
|
14548
14892
|
data: {
|
|
14549
14893
|
domains: result.domains.map((d) => ({ name: d.name, files: d.files, existingSpec: d.existingSpec })),
|
|
14550
|
-
createdSpecs: result.createdSpecs.map((s) =>
|
|
14551
|
-
reportPath: result.reportPath ?
|
|
14894
|
+
createdSpecs: result.createdSpecs.map((s) => path29.relative(ctx.cwd, s)),
|
|
14895
|
+
reportPath: result.reportPath ? path29.relative(ctx.cwd, result.reportPath) : void 0,
|
|
14552
14896
|
stack: result.stack,
|
|
14553
14897
|
dryRun: result.dryRun
|
|
14554
14898
|
},
|
|
@@ -14557,7 +14901,7 @@ async function runAdopt(ctx) {
|
|
|
14557
14901
|
}
|
|
14558
14902
|
|
|
14559
14903
|
// src/commands/analyze.ts
|
|
14560
|
-
import
|
|
14904
|
+
import path30 from "path";
|
|
14561
14905
|
async function runAnalyzeCommand(ctx) {
|
|
14562
14906
|
const { root } = await requireWorkspace(ctx);
|
|
14563
14907
|
const slug = ctx.positionals[0];
|
|
@@ -14568,23 +14912,23 @@ async function runAnalyzeCommand(ctx) {
|
|
|
14568
14912
|
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
14913
|
if (result.waves) lines.push(` olas: ${result.waves.blocks} bloque(s), ${result.waves.waves} ola(s), ${result.waves.tasks} tarea(s)`);
|
|
14570
14914
|
if (result.mockups) lines.push(` mockups: ${result.mockups.screens} pantalla(s)${result.mockups.stale ? " (desactualizados)" : ""}`);
|
|
14571
|
-
if (result.path) lines.push("", ` informe: ${
|
|
14915
|
+
if (result.path) lines.push("", ` informe: ${path30.relative(ctx.cwd, result.path)}`);
|
|
14572
14916
|
for (const finding of result.findings) {
|
|
14573
14917
|
lines.push(` ${finding.severity.toUpperCase()} ${finding.code} \u2014 ${finding.message}`);
|
|
14574
14918
|
}
|
|
14575
14919
|
return {
|
|
14576
14920
|
exitCode: result.status === "blocked" ? 1 : 0,
|
|
14577
14921
|
diagnostics: result.findings,
|
|
14578
|
-
data: result.path ? { ...result, path:
|
|
14922
|
+
data: result.path ? { ...result, path: path30.relative(ctx.cwd, result.path) } : result,
|
|
14579
14923
|
text: lines
|
|
14580
14924
|
};
|
|
14581
14925
|
}
|
|
14582
14926
|
|
|
14583
14927
|
// src/commands/approve.ts
|
|
14584
|
-
import
|
|
14928
|
+
import path32 from "path";
|
|
14585
14929
|
|
|
14586
14930
|
// src/commands/issue.ts
|
|
14587
|
-
import
|
|
14931
|
+
import path31 from "path";
|
|
14588
14932
|
async function runIssue(ctx) {
|
|
14589
14933
|
const action = ctx.positionals[0] ?? "status";
|
|
14590
14934
|
const slug = ctx.positionals[1];
|
|
@@ -14612,7 +14956,7 @@ async function runIssue(ctx) {
|
|
|
14612
14956
|
return { exitCode: 2, diagnostics: [{ code: "ATLAS-ISSUE-000", severity: "error", message: "Falta el slug: satlas issue status <slug>" }] };
|
|
14613
14957
|
}
|
|
14614
14958
|
const change = await loadChange(root, slug);
|
|
14615
|
-
const { config } = await loadConfig(
|
|
14959
|
+
const { config } = await loadConfig(path31.join(root, ".sdd"));
|
|
14616
14960
|
const tracker = change.meta?.tracker;
|
|
14617
14961
|
const lines = [`Issue de ${slug}`, ""];
|
|
14618
14962
|
if (!tracker || tracker.provider !== "github") {
|
|
@@ -14683,7 +15027,7 @@ async function runApprove(ctx) {
|
|
|
14683
15027
|
};
|
|
14684
15028
|
}
|
|
14685
15029
|
const { root, workspace, config } = await requireWorkspace(ctx);
|
|
14686
|
-
const artifact = target.includes("/") || target.includes("\\") ? target :
|
|
15030
|
+
const artifact = target.includes("/") || target.includes("\\") ? target : path32.join("changes", target, "spec.md");
|
|
14687
15031
|
const channelFlag = flagString(ctx.flags, "channel");
|
|
14688
15032
|
const channel = channelFlag === "presentation" || channelFlag === "editor" || channelFlag === "pr" || channelFlag === "tracker" ? channelFlag : "cli";
|
|
14689
15033
|
const change = workspace.changes.find(
|
|
@@ -14724,13 +15068,13 @@ async function runApprove(ctx) {
|
|
|
14724
15068
|
return {
|
|
14725
15069
|
exitCode: hasErrors2 ? 1 : 0,
|
|
14726
15070
|
diagnostics: result.diagnostics,
|
|
14727
|
-
data: result.approval ? { ...result.approval, file:
|
|
15071
|
+
data: result.approval ? { ...result.approval, file: path32.relative(ctx.cwd, result.file) } : void 0,
|
|
14728
15072
|
text: lines
|
|
14729
15073
|
};
|
|
14730
15074
|
}
|
|
14731
15075
|
|
|
14732
15076
|
// src/commands/archive.ts
|
|
14733
|
-
import
|
|
15077
|
+
import path33 from "path";
|
|
14734
15078
|
async function runArchive(ctx) {
|
|
14735
15079
|
const slug = ctx.positionals[0];
|
|
14736
15080
|
if (!slug) {
|
|
@@ -14767,7 +15111,7 @@ async function runArchive(ctx) {
|
|
|
14767
15111
|
} else {
|
|
14768
15112
|
lines.push("El delta no contiene operaciones (ADDED/MODIFIED/REMOVED/RENAMED).");
|
|
14769
15113
|
}
|
|
14770
|
-
if (result.archivedTo) lines.push(` archivado en: ${
|
|
15114
|
+
if (result.archivedTo) lines.push(` archivado en: ${path33.relative(ctx.cwd, result.archivedTo)}`);
|
|
14771
15115
|
const hasErrors2 = result.diagnostics.some((d) => d.severity === "error");
|
|
14772
15116
|
return {
|
|
14773
15117
|
exitCode: hasErrors2 ? 1 : 0,
|
|
@@ -14778,7 +15122,7 @@ async function runArchive(ctx) {
|
|
|
14778
15122
|
}
|
|
14779
15123
|
|
|
14780
15124
|
// src/commands/ci.ts
|
|
14781
|
-
import
|
|
15125
|
+
import path34 from "path";
|
|
14782
15126
|
async function runCi(ctx) {
|
|
14783
15127
|
const { root, config } = await requireWorkspace(ctx);
|
|
14784
15128
|
const strict = flagBool(ctx.flags, "strict");
|
|
@@ -14804,10 +15148,10 @@ async function runCi(ctx) {
|
|
|
14804
15148
|
const lines = ["CI de SpecAtlas", ""];
|
|
14805
15149
|
let sarifWritten;
|
|
14806
15150
|
if (sarifPath) {
|
|
14807
|
-
const target =
|
|
15151
|
+
const target = path34.resolve(ctx.cwd, sarifPath);
|
|
14808
15152
|
try {
|
|
14809
15153
|
await writeText(target, toSarifText({ diagnostics: gate.diagnostics, root, version: cliVersion(), failed: gate.failed }));
|
|
14810
|
-
sarifWritten =
|
|
15154
|
+
sarifWritten = path34.relative(ctx.cwd, target);
|
|
14811
15155
|
} catch (err) {
|
|
14812
15156
|
diagnostics.push({
|
|
14813
15157
|
code: "ATLAS-CI-SARIF-001",
|
|
@@ -14882,8 +15226,52 @@ async function runClarify(ctx) {
|
|
|
14882
15226
|
};
|
|
14883
15227
|
}
|
|
14884
15228
|
|
|
15229
|
+
// src/commands/contracts.ts
|
|
15230
|
+
import path35 from "path";
|
|
15231
|
+
async function runContracts(ctx) {
|
|
15232
|
+
const slug = ctx.positionals[0];
|
|
15233
|
+
if (!slug) {
|
|
15234
|
+
return { exitCode: 2, diagnostics: [{ code: "ATLAS-CONTRACTS-000", severity: "error", message: "Falta el slug: satlas contracts <slug>" }] };
|
|
15235
|
+
}
|
|
15236
|
+
const { workspace, config } = await requireWorkspace(ctx);
|
|
15237
|
+
const change = workspace.changes.find((c) => c.slug === slug);
|
|
15238
|
+
if (!change) {
|
|
15239
|
+
return { exitCode: 2, diagnostics: [{ code: "ATLAS-CONTRACTS-000", severity: "error", message: `No existe el cambio "${slug}"` }] };
|
|
15240
|
+
}
|
|
15241
|
+
const lines = [msg("contracts.title", ctx.language), ""];
|
|
15242
|
+
const state = change.contracts;
|
|
15243
|
+
if (!state || state.files.length === 0) {
|
|
15244
|
+
lines.push("El cambio no declara contratos (no hay archivos en su carpeta de contratos).");
|
|
15245
|
+
return { exitCode: 0, diagnostics: [], data: { slug, mode: config.gates.contracts.mode, contracts: [], findings: [] }, text: lines };
|
|
15246
|
+
}
|
|
15247
|
+
const coverage = contractCoverage(change, config.gates.contracts.mode);
|
|
15248
|
+
const findings = [...state.findings, ...coverage];
|
|
15249
|
+
for (const file of state.files) {
|
|
15250
|
+
lines.push(` ${path35.relative(ctx.cwd, file.path)} (${file.format}) \u2014 ${file.operations.length} operaci\xF3n(es)`);
|
|
15251
|
+
for (const operation of file.operations) lines.push(` ${operation.id}`);
|
|
15252
|
+
}
|
|
15253
|
+
lines.push("");
|
|
15254
|
+
if (findings.length === 0) {
|
|
15255
|
+
lines.push("Contratos conformes: forma v\xE1lida y cobertura completa.");
|
|
15256
|
+
} else {
|
|
15257
|
+
for (const finding of findings) lines.push(` ${finding.severity.toUpperCase()} ${finding.code} \u2014 ${finding.message}`);
|
|
15258
|
+
}
|
|
15259
|
+
const errors = findings.filter((finding) => finding.severity === "error").length;
|
|
15260
|
+
return {
|
|
15261
|
+
exitCode: errors > 0 ? 1 : 0,
|
|
15262
|
+
diagnostics: findings,
|
|
15263
|
+
data: {
|
|
15264
|
+
slug,
|
|
15265
|
+
mode: config.gates.contracts.mode,
|
|
15266
|
+
contracts: state.files.map((file) => ({ path: path35.relative(ctx.cwd, file.path), format: file.format, operations: file.operations.map((operation) => operation.id) })),
|
|
15267
|
+
findings
|
|
15268
|
+
},
|
|
15269
|
+
text: lines
|
|
15270
|
+
};
|
|
15271
|
+
}
|
|
15272
|
+
|
|
14885
15273
|
// src/commands/docs.ts
|
|
14886
|
-
import
|
|
15274
|
+
import path36 from "path";
|
|
14887
15275
|
async function runDocs(ctx) {
|
|
14888
15276
|
const slug = ctx.positionals[0];
|
|
14889
15277
|
if (!slug) {
|
|
@@ -14896,7 +15284,7 @@ async function runDocs(ctx) {
|
|
|
14896
15284
|
const errors = result.diagnostics.filter((d) => d.severity === "error");
|
|
14897
15285
|
const lines = [msg("docs.title", ctx.language), ""];
|
|
14898
15286
|
for (const file of result.files) {
|
|
14899
|
-
lines.push(` ${file.created ? msg("docs.created", ctx.language) : msg("docs.updated", ctx.language)}: ${
|
|
15287
|
+
lines.push(` ${file.created ? msg("docs.created", ctx.language) : msg("docs.updated", ctx.language)}: ${path36.relative(ctx.cwd, file.path)}`);
|
|
14900
15288
|
}
|
|
14901
15289
|
for (const error of errors) lines.push(` ERROR ${error.code} \u2014 ${error.message}`);
|
|
14902
15290
|
return {
|
|
@@ -14905,19 +15293,85 @@ async function runDocs(ctx) {
|
|
|
14905
15293
|
data: {
|
|
14906
15294
|
slug: result.slug,
|
|
14907
15295
|
tipo,
|
|
14908
|
-
files: result.files.map((file) => ({ tipo: file.tipo, path:
|
|
15296
|
+
files: result.files.map((file) => ({ tipo: file.tipo, path: path36.relative(ctx.cwd, file.path), created: file.created }))
|
|
14909
15297
|
},
|
|
14910
15298
|
text: lines
|
|
14911
15299
|
};
|
|
14912
15300
|
}
|
|
14913
15301
|
|
|
15302
|
+
// src/commands/link.ts
|
|
15303
|
+
import path37 from "path";
|
|
15304
|
+
async function runLink(ctx) {
|
|
15305
|
+
const { root } = await requireWorkspace(ctx);
|
|
15306
|
+
const action = ctx.positionals[0] ?? "list";
|
|
15307
|
+
if (action === "list") {
|
|
15308
|
+
const state = await loadLinks(root);
|
|
15309
|
+
const lines = [msg("link.title", ctx.language), ""];
|
|
15310
|
+
if (state.entries.length === 0) {
|
|
15311
|
+
lines.push("Sin enlaces. A\xF1ade uno con `satlas link add <ruta>`.");
|
|
15312
|
+
} else {
|
|
15313
|
+
for (const entry of state.entries) {
|
|
15314
|
+
const status = entry.available ? `${entry.requirements} requisito(s) \xB7 ${entry.domains.join(", ") || "sin dominios"}` : "NO DISPONIBLE";
|
|
15315
|
+
lines.push(` ${entry.name} ${path37.relative(ctx.cwd, entry.path)} ${status}`);
|
|
15316
|
+
}
|
|
15317
|
+
}
|
|
15318
|
+
return {
|
|
15319
|
+
exitCode: 0,
|
|
15320
|
+
diagnostics: [],
|
|
15321
|
+
data: {
|
|
15322
|
+
links: state.entries.map((entry) => ({
|
|
15323
|
+
name: entry.name,
|
|
15324
|
+
path: path37.relative(ctx.cwd, entry.path),
|
|
15325
|
+
available: entry.available,
|
|
15326
|
+
requirements: entry.requirements,
|
|
15327
|
+
domains: entry.domains
|
|
15328
|
+
}))
|
|
15329
|
+
},
|
|
15330
|
+
text: lines
|
|
15331
|
+
};
|
|
15332
|
+
}
|
|
15333
|
+
if (action === "add") {
|
|
15334
|
+
const target = ctx.positionals[1];
|
|
15335
|
+
if (!target) {
|
|
15336
|
+
return { exitCode: 2, diagnostics: [{ code: "ATLAS-LINK-000", severity: "error", message: "Falta la ruta: satlas link add <ruta> [--name <nombre>]" }] };
|
|
15337
|
+
}
|
|
15338
|
+
const name = flagString(ctx.flags, "name");
|
|
15339
|
+
const result = await addLink(root, { path: target, ...name !== void 0 ? { name } : {} });
|
|
15340
|
+
const errors = result.diagnostics.filter((d) => d.severity === "error");
|
|
15341
|
+
if (errors.length > 0) {
|
|
15342
|
+
return { exitCode: 2, diagnostics: result.diagnostics, data: { action: "add" }, text: errors.map((error) => `${error.code} \u2014 ${error.message}`) };
|
|
15343
|
+
}
|
|
15344
|
+
return {
|
|
15345
|
+
exitCode: 0,
|
|
15346
|
+
diagnostics: result.diagnostics,
|
|
15347
|
+
data: { action: "add", link: result.entry },
|
|
15348
|
+
text: [`Enlace a\xF1adido: ${result.entry?.name} \u2192 ${result.entry?.path}`]
|
|
15349
|
+
};
|
|
15350
|
+
}
|
|
15351
|
+
if (action === "remove") {
|
|
15352
|
+
const ref = ctx.positionals[1];
|
|
15353
|
+
if (!ref) {
|
|
15354
|
+
return { exitCode: 2, diagnostics: [{ code: "ATLAS-LINK-000", severity: "error", message: "Falta el nombre o la ruta: satlas link remove <nombre|ruta>" }] };
|
|
15355
|
+
}
|
|
15356
|
+
const result = await removeLink(root, ref);
|
|
15357
|
+
if (result.removed === void 0) {
|
|
15358
|
+
return { exitCode: 2, diagnostics: result.diagnostics, data: { action: "remove" }, text: result.diagnostics.map((diagnostic) => `${diagnostic.code} \u2014 ${diagnostic.message}`) };
|
|
15359
|
+
}
|
|
15360
|
+
return { exitCode: 0, diagnostics: result.diagnostics, data: { action: "remove", removed: result.removed }, text: [`Enlace quitado: ${result.removed}`] };
|
|
15361
|
+
}
|
|
15362
|
+
return {
|
|
15363
|
+
exitCode: 2,
|
|
15364
|
+
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>" }]
|
|
15365
|
+
};
|
|
15366
|
+
}
|
|
15367
|
+
|
|
14914
15368
|
// src/commands/profile.ts
|
|
14915
|
-
import
|
|
15369
|
+
import path38 from "path";
|
|
14916
15370
|
async function runProfile(ctx) {
|
|
14917
15371
|
const action = ctx.positionals[0] ?? "detect";
|
|
14918
15372
|
const { root, workspace } = await requireWorkspace(ctx);
|
|
14919
15373
|
const officialDir = await resolveProfilesDir();
|
|
14920
|
-
const customDir =
|
|
15374
|
+
const customDir = path38.join(workspace.sddDir, "profiles", "custom");
|
|
14921
15375
|
const profiles = [...officialDir ? await loadProfilesFromDir(officialDir) : [], ...await loadProfilesFromDir(customDir)];
|
|
14922
15376
|
if (action === "detect") {
|
|
14923
15377
|
const result = await detectProfiles(root, profiles);
|
|
@@ -14946,7 +15400,7 @@ async function runProfile(ctx) {
|
|
|
14946
15400
|
if (!name) {
|
|
14947
15401
|
return { exitCode: 2, diagnostics: [{ code: "ATLAS-PROFILE-001", severity: "error", message: "Falta el nombre del perfil: satlas profile create <nombre>" }] };
|
|
14948
15402
|
}
|
|
14949
|
-
const file =
|
|
15403
|
+
const file = path38.join(customDir, `${name}.yaml`);
|
|
14950
15404
|
if (await exists(file)) {
|
|
14951
15405
|
return { exitCode: 2, diagnostics: [{ code: "ATLAS-PROFILE-002", severity: "error", message: `El perfil ya existe: ${file}` }] };
|
|
14952
15406
|
}
|
|
@@ -14955,7 +15409,7 @@ async function runProfile(ctx) {
|
|
|
14955
15409
|
exitCode: 0,
|
|
14956
15410
|
diagnostics: [],
|
|
14957
15411
|
data: { file },
|
|
14958
|
-
text: [`Perfil creado: ${
|
|
15412
|
+
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
15413
|
};
|
|
14960
15414
|
}
|
|
14961
15415
|
return {
|
|
@@ -14964,7 +15418,7 @@ async function runProfile(ctx) {
|
|
|
14964
15418
|
};
|
|
14965
15419
|
}
|
|
14966
15420
|
async function isInside(dir, name) {
|
|
14967
|
-
return exists(
|
|
15421
|
+
return exists(path38.join(dir, `${name}.yaml`));
|
|
14968
15422
|
}
|
|
14969
15423
|
function profileTemplate(name) {
|
|
14970
15424
|
return `name: ${name}
|
|
@@ -14992,7 +15446,7 @@ assumptions: []
|
|
|
14992
15446
|
}
|
|
14993
15447
|
|
|
14994
15448
|
// src/commands/doctor.ts
|
|
14995
|
-
import
|
|
15449
|
+
import path39 from "path";
|
|
14996
15450
|
async function runDoctorCommand(ctx) {
|
|
14997
15451
|
const { root, config } = await requireWorkspace(ctx);
|
|
14998
15452
|
const report = await runDoctor(root);
|
|
@@ -15017,7 +15471,7 @@ async function runDoctorCommand(ctx) {
|
|
|
15017
15471
|
lines.push("Sin hallazgos. El workspace est\xE1 sano.");
|
|
15018
15472
|
} else {
|
|
15019
15473
|
for (const finding of report.findings) {
|
|
15020
|
-
const location = finding.path ? ` ${
|
|
15474
|
+
const location = finding.path ? ` ${path39.relative(ctx.cwd, finding.path)}${finding.line ? `:${finding.line}` : ""}` : "";
|
|
15021
15475
|
lines.push(` ${finding.severity.toUpperCase()} ${finding.code}${location} \u2014 ${finding.message}`);
|
|
15022
15476
|
if (finding.suggestion) lines.push(` \u21B3 ${finding.suggestion}`);
|
|
15023
15477
|
}
|
|
@@ -15091,7 +15545,7 @@ async function runHelp(ctx, commandName) {
|
|
|
15091
15545
|
}
|
|
15092
15546
|
|
|
15093
15547
|
// src/commands/init.ts
|
|
15094
|
-
import
|
|
15548
|
+
import path40 from "path";
|
|
15095
15549
|
async function runInit(ctx) {
|
|
15096
15550
|
const languageFlag = flagString(ctx.flags, "language");
|
|
15097
15551
|
const language = languageFlag === "en" ? "en" : languageFlag === "es" ? "es" : void 0;
|
|
@@ -15138,11 +15592,11 @@ async function runInit(ctx) {
|
|
|
15138
15592
|
const lines = [msg("init.title", ctx.language), ""];
|
|
15139
15593
|
if (result.created.length > 0) {
|
|
15140
15594
|
lines.push(msg("init.done", ctx.language));
|
|
15141
|
-
for (const file of result.created) lines.push(` + ${
|
|
15595
|
+
for (const file of result.created) lines.push(` + ${path40.relative(ctx.cwd, file)}`);
|
|
15142
15596
|
if (compiled.length > 0) {
|
|
15143
15597
|
lines.push("");
|
|
15144
15598
|
lines.push("Adaptadores compilados:");
|
|
15145
|
-
for (const file of compiled) lines.push(` + ${
|
|
15599
|
+
for (const file of compiled) lines.push(` + ${path40.relative(ctx.cwd, path40.join(ctx.cwd, file))}`);
|
|
15146
15600
|
}
|
|
15147
15601
|
lines.push("");
|
|
15148
15602
|
lines.push("Siguiente: crea un cambio con `satlas new <slug>` y escribe la spec funcional.");
|
|
@@ -15152,8 +15606,8 @@ async function runInit(ctx) {
|
|
|
15152
15606
|
exitCode: hasErrors2 ? 1 : 0,
|
|
15153
15607
|
diagnostics: result.diagnostics,
|
|
15154
15608
|
data: {
|
|
15155
|
-
sddDir:
|
|
15156
|
-
created: result.created.map((f) =>
|
|
15609
|
+
sddDir: path40.relative(ctx.cwd, result.sddDir),
|
|
15610
|
+
created: result.created.map((f) => path40.relative(ctx.cwd, f)),
|
|
15157
15611
|
detected: result.detected?.matches.map((m) => ({ name: m.name, score: m.score })),
|
|
15158
15612
|
best: result.detected?.best?.name
|
|
15159
15613
|
},
|
|
@@ -15194,7 +15648,7 @@ async function runMetrics(ctx) {
|
|
|
15194
15648
|
}
|
|
15195
15649
|
|
|
15196
15650
|
// src/commands/mockup.ts
|
|
15197
|
-
import
|
|
15651
|
+
import path41 from "path";
|
|
15198
15652
|
async function runMockup(ctx) {
|
|
15199
15653
|
const { root, workspace, config } = await requireWorkspace(ctx);
|
|
15200
15654
|
const slug = ctx.positionals[0];
|
|
@@ -15243,17 +15697,17 @@ async function runMockup(ctx) {
|
|
|
15243
15697
|
` pantallas: ${plan.screens.length}`,
|
|
15244
15698
|
...plan.screens.map((s) => ` - ${s.id}: ${s.title} (${s.illustrates.length} escenario(s))`),
|
|
15245
15699
|
"",
|
|
15246
|
-
` plan: ${
|
|
15247
|
-
` manifiesto: ${
|
|
15700
|
+
` plan: ${path41.relative(ctx.cwd, planFile)}`,
|
|
15701
|
+
` manifiesto: ${path41.relative(ctx.cwd, manifestFile)}`,
|
|
15248
15702
|
"",
|
|
15249
15703
|
"Siguiente: genera el HTML de cada pantalla (fase /satlas-mockup) y valida con `satlas mockup " + slug + " --check`."
|
|
15250
15704
|
];
|
|
15251
|
-
return { exitCode: 0, diagnostics: [], data: { plan, planFile:
|
|
15705
|
+
return { exitCode: 0, diagnostics: [], data: { plan, planFile: path41.relative(ctx.cwd, planFile), manifestFile: path41.relative(ctx.cwd, manifestFile) }, text: lines };
|
|
15252
15706
|
}
|
|
15253
15707
|
|
|
15254
15708
|
// src/mcp/server.ts
|
|
15255
15709
|
import { promises as fs3 } from "fs";
|
|
15256
|
-
import
|
|
15710
|
+
import path46 from "path";
|
|
15257
15711
|
import { createInterface } from "readline/promises";
|
|
15258
15712
|
|
|
15259
15713
|
// src/mcp/protocol.ts
|
|
@@ -15314,6 +15768,35 @@ function stringArg(args, key) {
|
|
|
15314
15768
|
return typeof value === "string" && value.trim() !== "" ? value.trim() : void 0;
|
|
15315
15769
|
}
|
|
15316
15770
|
|
|
15771
|
+
// src/mcp/tools/contracts.ts
|
|
15772
|
+
import path42 from "path";
|
|
15773
|
+
async function runAtlasContracts(args, host) {
|
|
15774
|
+
const resolved = await host.getWorkspace();
|
|
15775
|
+
if (!resolved) return noWorkspaceResult();
|
|
15776
|
+
const slug = stringArg(args, "slug");
|
|
15777
|
+
if (!slug) return errorResult("ATLAS-MCP-CONTRACTS-001", "Falta el nombre del cambio", { usage: "atlas_contracts { slug }" });
|
|
15778
|
+
const change = resolved.workspace.changes.find((candidate) => candidate.slug === slug);
|
|
15779
|
+
if (!change) {
|
|
15780
|
+
return errorResult("ATLAS-MCP-CONTRACTS-002", `El cambio "${slug}" no existe`, { available: resolved.workspace.changes.map((candidate) => candidate.slug) });
|
|
15781
|
+
}
|
|
15782
|
+
const mode = resolved.config.gates.contracts.mode;
|
|
15783
|
+
const state = change.contracts;
|
|
15784
|
+
if (!state || state.files.length === 0) {
|
|
15785
|
+
return jsonResult({ slug, mode, contracts: [], findings: [], message: "El cambio no declara contratos." });
|
|
15786
|
+
}
|
|
15787
|
+
const findings = [...state.findings, ...contractCoverage(change, mode)];
|
|
15788
|
+
return jsonResult({
|
|
15789
|
+
slug,
|
|
15790
|
+
mode,
|
|
15791
|
+
contracts: state.files.map((file) => ({
|
|
15792
|
+
path: path42.relative(resolved.root, file.path),
|
|
15793
|
+
format: file.format,
|
|
15794
|
+
operations: file.operations.map((operation) => operation.id)
|
|
15795
|
+
})),
|
|
15796
|
+
findings
|
|
15797
|
+
});
|
|
15798
|
+
}
|
|
15799
|
+
|
|
15317
15800
|
// src/mcp/tools/fixes.ts
|
|
15318
15801
|
async function runAtlasFixes(_args, host) {
|
|
15319
15802
|
const resolved = await host.getWorkspace();
|
|
@@ -15342,12 +15825,12 @@ async function runAtlasFixes(_args, host) {
|
|
|
15342
15825
|
}
|
|
15343
15826
|
|
|
15344
15827
|
// src/mcp/tools/glossary.ts
|
|
15345
|
-
import
|
|
15828
|
+
import path43 from "path";
|
|
15346
15829
|
async function runAtlasGlossary(_args, host) {
|
|
15347
15830
|
const resolved = await host.getWorkspace();
|
|
15348
15831
|
if (!resolved) return noWorkspaceResult();
|
|
15349
15832
|
const { root, config } = resolved;
|
|
15350
|
-
const glossaryPath =
|
|
15833
|
+
const glossaryPath = path43.resolve(root, config.spec.glossary);
|
|
15351
15834
|
const raw = await readTextIfExists(glossaryPath);
|
|
15352
15835
|
if (raw === void 0) {
|
|
15353
15836
|
return jsonResult({
|
|
@@ -15367,6 +15850,28 @@ async function runAtlasGlossary(_args, host) {
|
|
|
15367
15850
|
return jsonResult({ terms: parsed.terms });
|
|
15368
15851
|
}
|
|
15369
15852
|
|
|
15853
|
+
// src/mcp/tools/links.ts
|
|
15854
|
+
import path44 from "path";
|
|
15855
|
+
async function runAtlasLinks(_args, host) {
|
|
15856
|
+
const resolved = await host.getWorkspace();
|
|
15857
|
+
if (!resolved) return noWorkspaceResult();
|
|
15858
|
+
const state = resolved.workspace.links;
|
|
15859
|
+
if (!state || state.entries.length === 0) {
|
|
15860
|
+
return jsonResult({ links: [], message: "El proyecto no tiene enlaces.", action: "satlas link add <ruta>" });
|
|
15861
|
+
}
|
|
15862
|
+
return jsonResult({
|
|
15863
|
+
links: state.entries.map((entry) => ({
|
|
15864
|
+
name: entry.name,
|
|
15865
|
+
path: path44.relative(resolved.root, entry.path),
|
|
15866
|
+
available: entry.available,
|
|
15867
|
+
requirements: entry.requirements,
|
|
15868
|
+
domains: entry.domains
|
|
15869
|
+
})),
|
|
15870
|
+
externalRequirements: linkedRequirementIds(state).length,
|
|
15871
|
+
unavailable: state.unavailable
|
|
15872
|
+
});
|
|
15873
|
+
}
|
|
15874
|
+
|
|
15370
15875
|
// src/mcp/tools/impact.ts
|
|
15371
15876
|
var REQ_TARGET_RE = /^REQ-[A-Z0-9]+(?:-[A-Z0-9]+)*-\d{3}(?:-S\d+)?$/i;
|
|
15372
15877
|
async function runAtlasImpact(args, host) {
|
|
@@ -15393,7 +15898,7 @@ async function runAtlasImpact(args, host) {
|
|
|
15393
15898
|
}
|
|
15394
15899
|
|
|
15395
15900
|
// src/evaluate.ts
|
|
15396
|
-
import
|
|
15901
|
+
import path45 from "path";
|
|
15397
15902
|
function livingRequirementsMap2(specs) {
|
|
15398
15903
|
const map = /* @__PURE__ */ new Map();
|
|
15399
15904
|
for (const spec of specs) {
|
|
@@ -15402,7 +15907,7 @@ function livingRequirementsMap2(specs) {
|
|
|
15402
15907
|
return map;
|
|
15403
15908
|
}
|
|
15404
15909
|
async function evaluateChange(workspace, config, change, approvals) {
|
|
15405
|
-
const deltaPath =
|
|
15910
|
+
const deltaPath = path45.join(change.dir, "spec.md");
|
|
15406
15911
|
const deltaContent = await readTextIfExists(deltaPath);
|
|
15407
15912
|
const approval = verifyApproval(change, approvals, config, deltaContent ?? void 0);
|
|
15408
15913
|
const living = livingRequirementsMap2(workspace.specs);
|
|
@@ -15410,7 +15915,8 @@ async function evaluateChange(workspace, config, change, approvals) {
|
|
|
15410
15915
|
const trace = checkTrace({
|
|
15411
15916
|
specs: workspace.specs,
|
|
15412
15917
|
change,
|
|
15413
|
-
requireEvidence: config.gates.verify.mode !== "off" && config.gates.verify.require_evidence
|
|
15918
|
+
requireEvidence: config.gates.verify.mode !== "off" && config.gates.verify.require_evidence,
|
|
15919
|
+
linked: linkedTraceInput(workspace)
|
|
15414
15920
|
});
|
|
15415
15921
|
const blocking = lintFindings.filter((d) => d.severity === "error").length + trace.findings.filter((d) => d.severity === "error").length;
|
|
15416
15922
|
const mockupsAreReady = requiresMockups(change.meta, config) ? await mockupsReady(workspace.root, change.slug, change) : void 0;
|
|
@@ -15421,7 +15927,7 @@ async function evaluateChange(workspace, config, change, approvals) {
|
|
|
15421
15927
|
blockingFindings: change.delta ? blocking : 0,
|
|
15422
15928
|
...mockupsAreReady !== void 0 ? { mockupsReady: mockupsAreReady } : {}
|
|
15423
15929
|
});
|
|
15424
|
-
const phaseAdvisories = [...clarifyAdvisory(change, config), ...docsAdvisory(change, config)];
|
|
15930
|
+
const phaseAdvisories = [...clarifyAdvisory(change, config), ...docsAdvisory(change, config), ...contractsAdvisory(change, config)];
|
|
15425
15931
|
return { change, approval, lintFindings: [...lintFindings, ...phaseAdvisories], trace, blocking, state };
|
|
15426
15932
|
}
|
|
15427
15933
|
|
|
@@ -15580,7 +16086,9 @@ var HANDLERS = {
|
|
|
15580
16086
|
atlas_trace: runAtlasTrace,
|
|
15581
16087
|
atlas_impact: runAtlasImpact,
|
|
15582
16088
|
atlas_glossary: runAtlasGlossary,
|
|
15583
|
-
atlas_fixes: runAtlasFixes
|
|
16089
|
+
atlas_fixes: runAtlasFixes,
|
|
16090
|
+
atlas_contracts: runAtlasContracts,
|
|
16091
|
+
atlas_links: runAtlasLinks
|
|
15584
16092
|
};
|
|
15585
16093
|
async function runToolByName(name, args, host) {
|
|
15586
16094
|
const handler = HANDLERS[name];
|
|
@@ -15658,6 +16166,25 @@ function listTools() {
|
|
|
15658
16166
|
properties: {},
|
|
15659
16167
|
additionalProperties: false
|
|
15660
16168
|
}
|
|
16169
|
+
},
|
|
16170
|
+
{
|
|
16171
|
+
name: "atlas_contracts",
|
|
16172
|
+
description: "Contratos del cambio: operaciones declaradas y hallazgos de forma y cobertura con los escenarios. Solo lectura.",
|
|
16173
|
+
inputSchema: {
|
|
16174
|
+
type: "object",
|
|
16175
|
+
properties: { slug: { type: "string", description: "Nombre del cambio" } },
|
|
16176
|
+
required: ["slug"],
|
|
16177
|
+
additionalProperties: false
|
|
16178
|
+
}
|
|
16179
|
+
},
|
|
16180
|
+
{
|
|
16181
|
+
name: "atlas_links",
|
|
16182
|
+
description: "Enlaces a otros proyectos: nombre, disponibilidad, requisitos externos que aportan y enlaces no disponibles. Solo lectura.",
|
|
16183
|
+
inputSchema: {
|
|
16184
|
+
type: "object",
|
|
16185
|
+
properties: {},
|
|
16186
|
+
additionalProperties: false
|
|
16187
|
+
}
|
|
15661
16188
|
}
|
|
15662
16189
|
];
|
|
15663
16190
|
}
|
|
@@ -15675,8 +16202,8 @@ var WorkspaceCache = class {
|
|
|
15675
16202
|
}
|
|
15676
16203
|
};
|
|
15677
16204
|
async function workspaceStamp(root) {
|
|
15678
|
-
const sddDir =
|
|
15679
|
-
const markers = [sddDir,
|
|
16205
|
+
const sddDir = path46.join(root, ".sdd");
|
|
16206
|
+
const markers = [sddDir, path46.join(sddDir, "config.yaml"), path46.join(sddDir, "changes"), path46.join(sddDir, "specs"), path46.join(sddDir, "approvals.yaml")];
|
|
15680
16207
|
let stamp = 0;
|
|
15681
16208
|
for (const marker of markers) {
|
|
15682
16209
|
try {
|
|
@@ -15694,7 +16221,7 @@ async function resolveMcpWorkspace(cwd, cache) {
|
|
|
15694
16221
|
const cached2 = cache.get(root);
|
|
15695
16222
|
if (cached2 && cached2.mtimeMs === stamp) return cached2.value;
|
|
15696
16223
|
const { workspace, config } = await loadWorkspace(root);
|
|
15697
|
-
const approvals = await loadApprovals(
|
|
16224
|
+
const approvals = await loadApprovals(path46.join(root, ".sdd"));
|
|
15698
16225
|
const value = { root, workspace, config, approvals: approvals.byArtifact };
|
|
15699
16226
|
cache.set(root, stamp, value);
|
|
15700
16227
|
return value;
|
|
@@ -15786,7 +16313,7 @@ async function runMcp(ctx) {
|
|
|
15786
16313
|
}
|
|
15787
16314
|
|
|
15788
16315
|
// src/commands/packs.ts
|
|
15789
|
-
import
|
|
16316
|
+
import path47 from "path";
|
|
15790
16317
|
async function runPacks(ctx) {
|
|
15791
16318
|
const { root, config, workspace } = await requireWorkspace(ctx);
|
|
15792
16319
|
const slug = flagString(ctx.flags, "check");
|
|
@@ -15844,14 +16371,14 @@ async function runPacks(ctx) {
|
|
|
15844
16371
|
data: {
|
|
15845
16372
|
slug,
|
|
15846
16373
|
packs: evaluations.map((evaluation) => ({ id: evaluation.pack.id, status: evaluation.status, passed: evaluation.passed, failed: evaluation.failed, results: evaluation.results })),
|
|
15847
|
-
analyzePath:
|
|
16374
|
+
analyzePath: path47.posix.join(".sdd", "changes", slug, "analyze.md")
|
|
15848
16375
|
},
|
|
15849
16376
|
text: lines
|
|
15850
16377
|
};
|
|
15851
16378
|
}
|
|
15852
16379
|
|
|
15853
16380
|
// src/commands/new.ts
|
|
15854
|
-
import
|
|
16381
|
+
import path48 from "path";
|
|
15855
16382
|
async function runNew(ctx) {
|
|
15856
16383
|
const slug = ctx.positionals[0];
|
|
15857
16384
|
if (!slug) {
|
|
@@ -15877,14 +16404,14 @@ async function runNew(ctx) {
|
|
|
15877
16404
|
msg("new.title", ctx.language),
|
|
15878
16405
|
"",
|
|
15879
16406
|
msg("new.done", ctx.language),
|
|
15880
|
-
...result.files.map((f) => ` + ${
|
|
16407
|
+
...result.files.map((f) => ` + ${path48.relative(ctx.cwd, f)}`),
|
|
15881
16408
|
"",
|
|
15882
16409
|
`Siguiente: /satlas.specify ${result.slug} \u2014 escribe la especificaci\xF3n 100% funcional y de negocio.`
|
|
15883
16410
|
];
|
|
15884
16411
|
return {
|
|
15885
16412
|
exitCode: hasErrors2 ? 1 : 0,
|
|
15886
16413
|
diagnostics: result.diagnostics,
|
|
15887
|
-
data: { slug: result.slug, files: result.files.map((f) =>
|
|
16414
|
+
data: { slug: result.slug, files: result.files.map((f) => path48.relative(ctx.cwd, f)) },
|
|
15888
16415
|
text: lines
|
|
15889
16416
|
};
|
|
15890
16417
|
}
|
|
@@ -15919,7 +16446,7 @@ async function runNext(ctx) {
|
|
|
15919
16446
|
}
|
|
15920
16447
|
|
|
15921
16448
|
// src/commands/present.ts
|
|
15922
|
-
import
|
|
16449
|
+
import path49 from "path";
|
|
15923
16450
|
async function runPresentCommand(ctx) {
|
|
15924
16451
|
const { root } = await requireWorkspace(ctx);
|
|
15925
16452
|
const slug = ctx.positionals[0];
|
|
@@ -15931,7 +16458,7 @@ async function runPresentCommand(ctx) {
|
|
|
15931
16458
|
if (result.path) {
|
|
15932
16459
|
lines.push("Propuesta generada");
|
|
15933
16460
|
lines.push("");
|
|
15934
|
-
lines.push(` archivo: ${
|
|
16461
|
+
lines.push(` archivo: ${path49.relative(ctx.cwd, result.path)}`);
|
|
15935
16462
|
if (result.hash) lines.push(` hash de la spec: ${shortHash(result.hash)}`);
|
|
15936
16463
|
lines.push("");
|
|
15937
16464
|
lines.push("\xC1brela en el navegador y comp\xE1rtela con el stakeholder.");
|
|
@@ -15941,7 +16468,7 @@ async function runPresentCommand(ctx) {
|
|
|
15941
16468
|
return {
|
|
15942
16469
|
exitCode: hasErrors2 ? 1 : 0,
|
|
15943
16470
|
diagnostics: result.diagnostics,
|
|
15944
|
-
data: result.path ? { path:
|
|
16471
|
+
data: result.path ? { path: path49.relative(ctx.cwd, result.path), hash: result.hash } : void 0,
|
|
15945
16472
|
text: lines
|
|
15946
16473
|
};
|
|
15947
16474
|
}
|
|
@@ -16089,7 +16616,7 @@ async function runStatus(ctx) {
|
|
|
16089
16616
|
}
|
|
16090
16617
|
|
|
16091
16618
|
// src/commands/trace.ts
|
|
16092
|
-
import
|
|
16619
|
+
import path50 from "path";
|
|
16093
16620
|
async function runTrace(ctx) {
|
|
16094
16621
|
const { workspace, config } = await requireWorkspace(ctx);
|
|
16095
16622
|
const slug = flagString(ctx.flags, "change");
|
|
@@ -16109,7 +16636,7 @@ async function runTrace(ctx) {
|
|
|
16109
16636
|
data.push({ slug: change.slug, nodes: result.graph.nodes.length, edges: result.graph.edges.length, summary: result.summary, findings: result.findings });
|
|
16110
16637
|
lines.push(` ${change.slug}: ${result.graph.nodes.length} nodos, ${result.graph.edges.length} aristas, ${errors} errores, ${result.summary.warnings} avisos`);
|
|
16111
16638
|
for (const finding of result.findings) {
|
|
16112
|
-
const location = finding.path ? ` (${
|
|
16639
|
+
const location = finding.path ? ` (${path50.relative(ctx.cwd, finding.path)}${finding.line ? `:${finding.line}` : ""})` : "";
|
|
16113
16640
|
lines.push(` ${finding.severity.toUpperCase()} ${finding.code} \u2014 ${finding.message}${location}`);
|
|
16114
16641
|
}
|
|
16115
16642
|
}
|
|
@@ -16119,7 +16646,7 @@ async function runTrace(ctx) {
|
|
|
16119
16646
|
}
|
|
16120
16647
|
|
|
16121
16648
|
// src/commands/upgrade.ts
|
|
16122
|
-
import
|
|
16649
|
+
import path51 from "path";
|
|
16123
16650
|
async function runUpgrade(ctx) {
|
|
16124
16651
|
const { root } = await requireWorkspace(ctx);
|
|
16125
16652
|
const apply = flagBool(ctx.flags, "apply");
|
|
@@ -16173,7 +16700,7 @@ function renderPreview(plan, root, ctx) {
|
|
|
16173
16700
|
}
|
|
16174
16701
|
for (const item of plan.unreadable) {
|
|
16175
16702
|
lines.push(` Ilegible: "${item.artifact}" \u2014 ${item.reason}`);
|
|
16176
|
-
lines.push(` \u21B3 ${
|
|
16703
|
+
lines.push(` \u21B3 ${path51.relative(ctx.cwd, item.path)}`);
|
|
16177
16704
|
}
|
|
16178
16705
|
return { exitCode: 0, diagnostics: [], data: summarizable(plan), text: lines };
|
|
16179
16706
|
}
|
|
@@ -16236,7 +16763,7 @@ function renderRollback(report, ctx) {
|
|
|
16236
16763
|
}
|
|
16237
16764
|
|
|
16238
16765
|
// src/commands/validate.ts
|
|
16239
|
-
import
|
|
16766
|
+
import path52 from "path";
|
|
16240
16767
|
async function runValidate(ctx) {
|
|
16241
16768
|
const { root, workspace, config, approvals } = await requireWorkspace(ctx);
|
|
16242
16769
|
const slug = flagString(ctx.flags, "change");
|
|
@@ -16263,7 +16790,7 @@ async function runValidate(ctx) {
|
|
|
16263
16790
|
for (const finding of evaluation.lintFindings) {
|
|
16264
16791
|
lines.push(` ${finding.severity.toUpperCase()} ${finding.code}${finding.line ? `:${finding.line}` : ""} \u2014 ${finding.message}`);
|
|
16265
16792
|
}
|
|
16266
|
-
lines.push(` archivo: ${
|
|
16793
|
+
lines.push(` archivo: ${path52.relative(ctx.cwd, path52.join(change.dir, "spec.md"))}`);
|
|
16267
16794
|
}
|
|
16268
16795
|
if (targets.length === 0) lines.push("Sin cambios activos.");
|
|
16269
16796
|
if (advisory && advisory.plan.pending.length > 0) {
|
|
@@ -16291,7 +16818,7 @@ async function runValidate(ctx) {
|
|
|
16291
16818
|
}
|
|
16292
16819
|
|
|
16293
16820
|
// src/commands/verify.ts
|
|
16294
|
-
import
|
|
16821
|
+
import path53 from "path";
|
|
16295
16822
|
async function runVerify(ctx) {
|
|
16296
16823
|
const { root, workspace } = await requireWorkspace(ctx);
|
|
16297
16824
|
const slug = ctx.positionals[0];
|
|
@@ -16334,7 +16861,7 @@ async function runVerify(ctx) {
|
|
|
16334
16861
|
return { exitCode: 2, diagnostics: [{ code: "ATLAS-VERIFY-001", severity: "error", message: "Falta --by <nombre> (o define meta.owner)" }] };
|
|
16335
16862
|
}
|
|
16336
16863
|
const profilesDir = await resolveProfilesDir();
|
|
16337
|
-
const profile = await loadActiveProfile(
|
|
16864
|
+
const profile = await loadActiveProfile(path53.join(root, ".sdd"), [profilesDir ?? "", path53.join(root, ".sdd", "profiles", "custom")].filter(Boolean));
|
|
16338
16865
|
const record = await recordEvidence({
|
|
16339
16866
|
root,
|
|
16340
16867
|
slug,
|
|
@@ -16356,7 +16883,7 @@ async function runVerify(ctx) {
|
|
|
16356
16883
|
if (record.evidence.command) lines.push(` comando: ${record.evidence.command}`);
|
|
16357
16884
|
lines.push(` resultado: ${record.evidence.result}`);
|
|
16358
16885
|
if (record.evidence.outputHash) lines.push(` hash: ${record.evidence.outputHash}`);
|
|
16359
|
-
lines.push(` archivo: ${
|
|
16886
|
+
lines.push(` archivo: ${path53.relative(ctx.cwd, record.path)}`);
|
|
16360
16887
|
if (record.stdout) {
|
|
16361
16888
|
lines.push("");
|
|
16362
16889
|
lines.push(" salida (\xFAltimas l\xEDneas):");
|
|
@@ -16371,7 +16898,7 @@ async function runVerify(ctx) {
|
|
|
16371
16898
|
return {
|
|
16372
16899
|
exitCode: record.exitCode === 0 ? 0 : 1,
|
|
16373
16900
|
diagnostics: record.diagnostics,
|
|
16374
|
-
data: { evidence: record.evidence, path:
|
|
16901
|
+
data: { evidence: record.evidence, path: path53.relative(ctx.cwd, record.path), exitCode: record.exitCode },
|
|
16375
16902
|
text: lines
|
|
16376
16903
|
};
|
|
16377
16904
|
}
|
|
@@ -16428,6 +16955,8 @@ var HANDLERS2 = {
|
|
|
16428
16955
|
doctor: runDoctorCommand,
|
|
16429
16956
|
clarify: runClarify,
|
|
16430
16957
|
docs: runDocs,
|
|
16958
|
+
contracts: runContracts,
|
|
16959
|
+
link: runLink,
|
|
16431
16960
|
upgrade: runUpgrade,
|
|
16432
16961
|
approve: runApprove,
|
|
16433
16962
|
issue: runIssue,
|
|
@@ -16473,7 +17002,7 @@ async function requireWorkspace(ctx) {
|
|
|
16473
17002
|
throw new CliError("ATLAS-WS-001", msg("cli.noWorkspace", ctx.language), 2);
|
|
16474
17003
|
}
|
|
16475
17004
|
const { workspace, config } = await loadWorkspace(root);
|
|
16476
|
-
const approvals = await loadApprovals(
|
|
17005
|
+
const approvals = await loadApprovals(path54.join(root, ".sdd"));
|
|
16477
17006
|
return { root, workspace, config, approvals: approvals.byArtifact };
|
|
16478
17007
|
}
|
|
16479
17008
|
var CliError = class extends Error {
|
|
@@ -16540,7 +17069,7 @@ function printResult(command, result, json2, language) {
|
|
|
16540
17069
|
}
|
|
16541
17070
|
const lines = [...result.text ?? []];
|
|
16542
17071
|
for (const d of result.diagnostics) {
|
|
16543
|
-
const location = d.path ? ` ${
|
|
17072
|
+
const location = d.path ? ` ${path54.relative(process.cwd(), d.path)}${d.line ? `:${d.line}` : ""}` : "";
|
|
16544
17073
|
lines.push(`${d.severity === "error" ? "ERROR" : d.severity === "warning" ? "AVISO" : "NOTA"} ${d.code}${location} \u2014 ${d.message}`);
|
|
16545
17074
|
if (d.suggestion) lines.push(` \u21B3 ${d.suggestion}`);
|
|
16546
17075
|
}
|