specatlas 0.1.29 → 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 +1161 -308
- package/package.json +2 -2
- package/workflow/phases/clarify.md +39 -0
- package/workflow/phases/docs.md +36 -0
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,18 +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";
|
|
9039
|
+
import path22 from "path";
|
|
9040
|
+
import path23 from "path";
|
|
9041
|
+
import path24 from "path";
|
|
9037
9042
|
var CORE_VERSION = "0.0.1";
|
|
9038
9043
|
var REQ_ID_RE = /^REQ-[A-Z0-9]+(?:-[A-Z0-9]+)*-\d{3}$/;
|
|
9039
9044
|
var SCENARIO_ID_RE = /^REQ-[A-Z0-9]+(?:-[A-Z0-9]+)*-\d{3}-S\d+$/;
|
|
@@ -9056,6 +9061,9 @@ var atlasConfigSchema = z.object({
|
|
|
9056
9061
|
analyze: z.object({ mode: z.enum(["off", "advisory", "blocking"]).default("blocking"), min_severity: z.enum(["low", "medium", "high"]).default("medium") }).default({}),
|
|
9057
9062
|
verify: z.object({ mode: z.enum(["off", "advisory", "blocking"]).default("blocking"), require_evidence: z.boolean().default(true) }).default({}),
|
|
9058
9063
|
review: z.object({ mode: z.enum(["off", "advisory", "blocking"]).default("advisory") }).default({}),
|
|
9064
|
+
clarify: z.object({ mode: z.enum(["off", "advisory", "blocking"]).default("advisory") }).default({}),
|
|
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({}),
|
|
9059
9067
|
mockup: z.object({ require_approval: z.boolean().default(false), compare_in_verify: z.boolean().default(false) }).default({})
|
|
9060
9068
|
}).default({}),
|
|
9061
9069
|
trace: z.object({ mode: z.enum(["off", "advisory", "blocking"]).default("blocking"), prefix: z.string().default("REQ") }).default({}),
|
|
@@ -9178,6 +9186,7 @@ var SCENARIO_HEAD_RE = /^####\s+(?:Scenario|Escenario):\s+(REQ-[A-Z0-9-]+-S\d+)\
|
|
|
9178
9186
|
var RULE_RE = /^-\s*(?:Rule|Regla)\s+(BR-[A-Z0-9-]+)\s*:\s*(.+?)\s*$/;
|
|
9179
9187
|
var WHEN_RE = /^-\s*\*\*\s*(?:WHEN|CUANDO|DADO QUE)\s*\*\*\s*(.+?)\s*$/i;
|
|
9180
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;
|
|
9181
9190
|
function newDraft(id, title, line) {
|
|
9182
9191
|
return { id, title, line, prose: [], rules: [], scenarios: [] };
|
|
9183
9192
|
}
|
|
@@ -9262,6 +9271,16 @@ function parseRequirementBlocks(body, startLine = 1, filePath) {
|
|
|
9262
9271
|
currentScenario.then.push(thenMatch[1] ?? "");
|
|
9263
9272
|
continue;
|
|
9264
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
|
+
}
|
|
9265
9284
|
if (current && !currentScenario) {
|
|
9266
9285
|
if (line.trim() !== "") current.prose.push(line.trim());
|
|
9267
9286
|
}
|
|
@@ -9600,6 +9619,33 @@ function findScenarioHeading(lines, blockLineIndex) {
|
|
|
9600
9619
|
}
|
|
9601
9620
|
return void 0;
|
|
9602
9621
|
}
|
|
9622
|
+
var OPEN_RE = /^\s*-\s*\[\s\]\s+(.+?)\s*$/;
|
|
9623
|
+
var DONE_RE = /^\s*-\s*\[[xX]\]\s+(.+?)\s*$/;
|
|
9624
|
+
var ANSWER_SEPARATOR = " \u2014 ";
|
|
9625
|
+
function splitAnswer(raw) {
|
|
9626
|
+
const index = raw.indexOf(ANSWER_SEPARATOR);
|
|
9627
|
+
if (index === -1) return { text: raw.trim() };
|
|
9628
|
+
return { text: raw.slice(0, index).trim(), answer: raw.slice(index + ANSWER_SEPARATOR.length).trim() };
|
|
9629
|
+
}
|
|
9630
|
+
function parseClarify(content, filePath) {
|
|
9631
|
+
const fm = parseFrontmatter(content, filePath);
|
|
9632
|
+
const lines = fm.body.replace(/\r\n?/g, "\n").split("\n");
|
|
9633
|
+
const open = [];
|
|
9634
|
+
const resolved = [];
|
|
9635
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
9636
|
+
const line = lines[i] ?? "";
|
|
9637
|
+
const lineNo = fm.bodyStartLine + i;
|
|
9638
|
+
const done = DONE_RE.exec(line);
|
|
9639
|
+
if (done) {
|
|
9640
|
+
const item = splitAnswer(done[1] ?? "");
|
|
9641
|
+
resolved.push({ text: item.text, line: lineNo, ...item.answer !== void 0 ? { answer: item.answer } : {} });
|
|
9642
|
+
continue;
|
|
9643
|
+
}
|
|
9644
|
+
const pending = OPEN_RE.exec(line);
|
|
9645
|
+
if (pending) open.push({ text: pending[1] ?? "", line: lineNo });
|
|
9646
|
+
}
|
|
9647
|
+
return { path: filePath, open, resolved, diagnostics: [] };
|
|
9648
|
+
}
|
|
9603
9649
|
var HEADER_KEYS = /* @__PURE__ */ new Set(["t\xE9rmino", "termino", "term", "definici\xF3n", "definicion", "definition", "sin\xF3nimos", "sinonimos", "synonyms"]);
|
|
9604
9650
|
var SEPARATOR_RE = /^:?-{2,}:?$/;
|
|
9605
9651
|
function parseGlossary(md, filePath) {
|
|
@@ -9688,6 +9734,7 @@ function impactOfRequirement(workspace, reqId) {
|
|
|
9688
9734
|
const id = reqId.toUpperCase();
|
|
9689
9735
|
const scenarios = /* @__PURE__ */ new Set();
|
|
9690
9736
|
let exists2 = false;
|
|
9737
|
+
let origin;
|
|
9691
9738
|
for (const spec of workspace.specs) {
|
|
9692
9739
|
for (const req of spec.spec.requirements) {
|
|
9693
9740
|
if (req.id !== id) continue;
|
|
@@ -9704,6 +9751,18 @@ function impactOfRequirement(workspace, reqId) {
|
|
|
9704
9751
|
if ((change.delta?.removed ?? []).some((r) => r.id === id)) exists2 = true;
|
|
9705
9752
|
if ((change.delta?.renamed ?? []).some((r) => r.from.id === id || r.to.id === id)) exists2 = true;
|
|
9706
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
|
+
}
|
|
9707
9766
|
const report = emptyReport(id, "requirement");
|
|
9708
9767
|
if (!exists2) return report;
|
|
9709
9768
|
report.exists = true;
|
|
@@ -9711,6 +9770,10 @@ function impactOfRequirement(workspace, reqId) {
|
|
|
9711
9770
|
const reqOf = scenarioToReq(workspace);
|
|
9712
9771
|
collect(workspace, report, /* @__PURE__ */ new Set([id, ...scenarios]), reqOf);
|
|
9713
9772
|
if (!report.requirements.includes(id)) report.requirements.unshift(id);
|
|
9773
|
+
if (origin !== void 0) {
|
|
9774
|
+
report.external = true;
|
|
9775
|
+
report.origin = origin;
|
|
9776
|
+
}
|
|
9714
9777
|
return report;
|
|
9715
9778
|
}
|
|
9716
9779
|
function impactOfFile(workspace, file) {
|
|
@@ -9749,14 +9812,14 @@ function wordBoundary(text, term) {
|
|
|
9749
9812
|
const re = new RegExp(`(?<![\\p{L}\\p{N}])${escaped}(?![\\p{L}\\p{N}])`, "iu");
|
|
9750
9813
|
return re.test(text);
|
|
9751
9814
|
}
|
|
9752
|
-
function lintText(text, code, terms, label,
|
|
9815
|
+
function lintText(text, code, terms, label, path252, line) {
|
|
9753
9816
|
const out = [];
|
|
9754
9817
|
const lower = text.toLowerCase();
|
|
9755
9818
|
for (const term of terms) {
|
|
9756
9819
|
if (wordBoundary(lower, term)) {
|
|
9757
9820
|
out.push(
|
|
9758
9821
|
diag(code, "error", `${label}: "${term}"`, {
|
|
9759
|
-
path:
|
|
9822
|
+
path: path252,
|
|
9760
9823
|
line,
|
|
9761
9824
|
suggestion: "La especificaci\xF3n es funcional y de negocio: describe comportamiento, no tecnolog\xEDa ni adjetivos vagos"
|
|
9762
9825
|
})
|
|
@@ -9765,7 +9828,7 @@ function lintText(text, code, terms, label, path222, line) {
|
|
|
9765
9828
|
}
|
|
9766
9829
|
return out;
|
|
9767
9830
|
}
|
|
9768
|
-
function lintRequirement(req,
|
|
9831
|
+
function lintRequirement(req, path252, opts = {}) {
|
|
9769
9832
|
const out = [];
|
|
9770
9833
|
const vague = opts.language === "en" ? VAGUE_EN : VAGUE_ES;
|
|
9771
9834
|
const tech = opts.language === "en" ? TECH_EN : TECH_ES;
|
|
@@ -9776,32 +9839,32 @@ function lintRequirement(req, path222, opts = {}) {
|
|
|
9776
9839
|
...req.scenarios.flatMap((s) => [...s.when.map((w) => ({ text: w, line: s.line })), ...s.then.map((t) => ({ text: t, line: s.line }))])
|
|
9777
9840
|
];
|
|
9778
9841
|
for (const part of parts) {
|
|
9779
|
-
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));
|
|
9780
9843
|
if (opts.businessOnly !== false) {
|
|
9781
|
-
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));
|
|
9782
9845
|
}
|
|
9783
9846
|
}
|
|
9784
9847
|
if (req.scenarios.length === 0) {
|
|
9785
|
-
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" }));
|
|
9786
9849
|
}
|
|
9787
9850
|
return out;
|
|
9788
9851
|
}
|
|
9789
|
-
function lintDelta(delta, livingRequirements,
|
|
9852
|
+
function lintDelta(delta, livingRequirements, path252, opts = {}) {
|
|
9790
9853
|
const out = [...delta.diagnostics];
|
|
9791
9854
|
for (const req of [...delta.added, ...delta.modified]) {
|
|
9792
|
-
out.push(...lintRequirement(req,
|
|
9855
|
+
out.push(...lintRequirement(req, path252, opts));
|
|
9793
9856
|
}
|
|
9794
9857
|
for (const req of delta.modified) {
|
|
9795
9858
|
const living = livingRequirements.get(req.id);
|
|
9796
9859
|
if (!living) {
|
|
9797
|
-
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 }));
|
|
9798
9861
|
continue;
|
|
9799
9862
|
}
|
|
9800
9863
|
for (const existing of living.scenarios) {
|
|
9801
9864
|
if (!req.scenarios.some((s) => s.id === existing.id)) {
|
|
9802
9865
|
out.push(
|
|
9803
9866
|
diag("TRACE-007", "error", `MODIFIED ${req.id} pierde el escenario ${existing.id}: copia el bloque completo`, {
|
|
9804
|
-
path:
|
|
9867
|
+
path: path252,
|
|
9805
9868
|
line: req.line,
|
|
9806
9869
|
suggestion: "Copia el bloque completo de la spec viva y ed\xEDtalo; para quitarlo, decl\xE1ralo en REMOVED"
|
|
9807
9870
|
})
|
|
@@ -9812,15 +9875,15 @@ function lintDelta(delta, livingRequirements, path222, opts = {}) {
|
|
|
9812
9875
|
for (const req of delta.removed) {
|
|
9813
9876
|
const living = livingRequirements.get(req.id);
|
|
9814
9877
|
if (!living) {
|
|
9815
|
-
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 }));
|
|
9816
9879
|
}
|
|
9817
9880
|
}
|
|
9818
9881
|
for (const rename2 of delta.renamed) {
|
|
9819
9882
|
if (rename2.from.id !== rename2.to.id) {
|
|
9820
|
-
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 }));
|
|
9821
9884
|
}
|
|
9822
9885
|
if (!livingRequirements.has(rename2.from.id)) {
|
|
9823
|
-
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 }));
|
|
9824
9887
|
}
|
|
9825
9888
|
}
|
|
9826
9889
|
return out;
|
|
@@ -9844,7 +9907,7 @@ var MERMAID_KEYWORDS = [
|
|
|
9844
9907
|
"architecture-beta"
|
|
9845
9908
|
];
|
|
9846
9909
|
var MERMAID_BLOCKS = /^\s*(alt|loop|opt|par|rect|critical|break|subgraph)\b/;
|
|
9847
|
-
function lintPlan(planText,
|
|
9910
|
+
function lintPlan(planText, path252) {
|
|
9848
9911
|
const out = [];
|
|
9849
9912
|
const blocks = [...planText.matchAll(/```mermaid\r?\n([\s\S]*?)```/g)];
|
|
9850
9913
|
for (const [index, block] of blocks.entries()) {
|
|
@@ -9854,7 +9917,7 @@ function lintPlan(planText, path222) {
|
|
|
9854
9917
|
if (!MERMAID_KEYWORDS.some((keyword) => first.startsWith(keyword))) {
|
|
9855
9918
|
out.push(
|
|
9856
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)}"`, {
|
|
9857
|
-
path:
|
|
9920
|
+
path: path252,
|
|
9858
9921
|
suggestion: "Corrige el tipo del diagrama o elimina el bloque"
|
|
9859
9922
|
})
|
|
9860
9923
|
);
|
|
@@ -9868,7 +9931,7 @@ function lintPlan(planText, path222) {
|
|
|
9868
9931
|
if (open !== 0) {
|
|
9869
9932
|
out.push(
|
|
9870
9933
|
diag("LINT-PLN-002", "error", `Diagrama mermaid ${index + 1}: faltan ${Math.abs(open)} \`end\` (bloques alt/loop/subgraph sin cerrar)`, {
|
|
9871
|
-
path:
|
|
9934
|
+
path: path252,
|
|
9872
9935
|
suggestion: "Cierra cada bloque alt/loop/opt/par/rect/subgraph con `end`"
|
|
9873
9936
|
})
|
|
9874
9937
|
);
|
|
@@ -9880,7 +9943,7 @@ function lintPlan(planText, path222) {
|
|
|
9880
9943
|
if (message.includes(";")) {
|
|
9881
9944
|
out.push(
|
|
9882
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`, {
|
|
9883
|
-
path:
|
|
9946
|
+
path: path252,
|
|
9884
9947
|
suggestion: "Sustituye `;` por `\xB7` o `,` en los mensajes de sequenceDiagram"
|
|
9885
9948
|
})
|
|
9886
9949
|
);
|
|
@@ -9991,15 +10054,25 @@ function checkTrace(input) {
|
|
|
9991
10054
|
);
|
|
9992
10055
|
}
|
|
9993
10056
|
}
|
|
10057
|
+
const linkedIds = new Set(input.linked?.ids ?? []);
|
|
10058
|
+
const unavailableLinks = input.linked?.unavailable ?? [];
|
|
9994
10059
|
for (const task of taskById.values()) {
|
|
9995
10060
|
for (const c of task.covers) {
|
|
9996
|
-
if (!livingReqs.has(c) && !deltaScenarios.has(c) && !allScenarioIds.has(c)) {
|
|
10061
|
+
if (!livingReqs.has(c) && !deltaScenarios.has(c) && !allScenarioIds.has(c) && !linkedIds.has(c)) {
|
|
9997
10062
|
findings.push(
|
|
9998
10063
|
diag("TRACE-003", "error", `La tarea ${task.id} cubre ${c}, que no existe`, {
|
|
9999
10064
|
path: change.tasks?.path,
|
|
10000
|
-
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"
|
|
10001
10066
|
})
|
|
10002
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
|
+
}
|
|
10003
10076
|
}
|
|
10004
10077
|
}
|
|
10005
10078
|
for (const dep of task.dependsOn) {
|
|
@@ -10162,11 +10235,165 @@ function planBlock(block, maxParallel) {
|
|
|
10162
10235
|
}
|
|
10163
10236
|
return { block: block.id, title: block.title, waves, degraded, diagnostics };
|
|
10164
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
|
+
}
|
|
10165
10392
|
function verifyApproval(change, approvals, cfg, specContent) {
|
|
10166
10393
|
if (cfg.gates.approval === "none") return { status: "not_required" };
|
|
10167
10394
|
if ((change.meta?.overrides ?? []).some((o) => o.gate === "approval")) return { status: "overridden" };
|
|
10168
10395
|
if (!change.delta) return { status: "missing" };
|
|
10169
|
-
const key =
|
|
10396
|
+
const key = path3.posix.join("changes", change.slug, "spec.md");
|
|
10170
10397
|
const approval = approvals.get(key) ?? approvals.get(`${key}`);
|
|
10171
10398
|
if (!approval) return { status: "missing" };
|
|
10172
10399
|
if (specContent === void 0) return { status: "valid", approvedBy: approval.by, approvedAt: approval.at };
|
|
@@ -10182,6 +10409,29 @@ function requiresMockups(meta, cfg) {
|
|
|
10182
10409
|
function mockupOverride(change) {
|
|
10183
10410
|
return (change.meta?.overrides ?? []).some((override) => override.gate === "mockup");
|
|
10184
10411
|
}
|
|
10412
|
+
function docsReady(change) {
|
|
10413
|
+
const paths = change.docsPaths ?? [];
|
|
10414
|
+
return paths.some((p) => p.endsWith("tecnica.md")) && paths.some((p) => p.endsWith("manual.md"));
|
|
10415
|
+
}
|
|
10416
|
+
function clarifyAdvisory(change, cfg) {
|
|
10417
|
+
const open = change.clarify?.open.length ?? 0;
|
|
10418
|
+
if (cfg.gates.clarify.mode !== "advisory" || open === 0) return [];
|
|
10419
|
+
return [
|
|
10420
|
+
diag("ATLAS-CLARIFY-001", "warning", `El cambio "${change.slug}" tiene ${open} pregunta(s) sin aclarar`, {
|
|
10421
|
+
...change.clarifyPath !== void 0 ? { path: change.clarifyPath } : {},
|
|
10422
|
+
suggestion: `Aclara antes de planificar: /satlas.clarify ${change.slug} (o satlas clarify ${change.slug})`
|
|
10423
|
+
})
|
|
10424
|
+
];
|
|
10425
|
+
}
|
|
10426
|
+
function docsAdvisory(change, cfg) {
|
|
10427
|
+
const lane = change.meta?.lane ?? cfg.lanes.default;
|
|
10428
|
+
if (lane !== "full" || cfg.gates.docs.mode !== "advisory" || docsReady(change)) return [];
|
|
10429
|
+
return [
|
|
10430
|
+
diag("ATLAS-DOCS-001", "warning", `El cambio "${change.slug}" (carril completo) no tiene su documentaci\xF3n t\xE9cnica y manual`, {
|
|
10431
|
+
suggestion: `Genera la documentaci\xF3n: satlas docs ${change.slug} (o /satlas.docs ${change.slug})`
|
|
10432
|
+
})
|
|
10433
|
+
];
|
|
10434
|
+
}
|
|
10185
10435
|
function deriveState(input) {
|
|
10186
10436
|
const { change, cfg, approval, blockingFindings } = input;
|
|
10187
10437
|
const lane = change.meta?.lane ?? cfg.lanes.default;
|
|
@@ -10244,6 +10494,16 @@ function deriveState(input) {
|
|
|
10244
10494
|
};
|
|
10245
10495
|
}
|
|
10246
10496
|
if (!change.planPath && !change.tasks) {
|
|
10497
|
+
const openQuestions = change.clarify?.open.length ?? 0;
|
|
10498
|
+
if (openQuestions > 0 && cfg.gates.clarify.mode === "blocking") {
|
|
10499
|
+
blockedBy.push(`aclaraci\xF3n pendiente (${openQuestions})`);
|
|
10500
|
+
return {
|
|
10501
|
+
state: "approved",
|
|
10502
|
+
blockedBy,
|
|
10503
|
+
nextAction: next(`/satlas.clarify ${change.slug}`, `Aclarar ${openQuestions} pregunta(s) antes de planificar`, true),
|
|
10504
|
+
progress
|
|
10505
|
+
};
|
|
10506
|
+
}
|
|
10247
10507
|
return { state: "approved", blockedBy, nextAction: next(`/satlas.plan ${change.slug}`, "Crear el plan t\xE9cnico y las tareas", true), progress };
|
|
10248
10508
|
}
|
|
10249
10509
|
if (tasksTotal > 0 && tasksDone < tasksTotal) {
|
|
@@ -10257,6 +10517,17 @@ function deriveState(input) {
|
|
|
10257
10517
|
blockedBy.push("review pendiente");
|
|
10258
10518
|
return { state: "verified", blockedBy, nextAction: next(`/satlas.review ${change.slug}`, "Revisi\xF3n de c\xF3digo", true), progress };
|
|
10259
10519
|
}
|
|
10520
|
+
if (lane === "full" && cfg.gates.docs.mode === "blocking" && !docsReady(change)) {
|
|
10521
|
+
blockedBy.push("documentaci\xF3n pendiente");
|
|
10522
|
+
return { state: "reviewed", blockedBy, nextAction: next(`/satlas.docs ${change.slug}`, "Generar la documentaci\xF3n t\xE9cnica y manual del cambio", true), progress };
|
|
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
|
+
}
|
|
10260
10531
|
return { state: "ready", blockedBy, nextAction: next(`satlas archive ${change.slug}`, "Archivar el cambio y plegar los deltas"), progress };
|
|
10261
10532
|
}
|
|
10262
10533
|
function stateLabel(state) {
|
|
@@ -10270,6 +10541,7 @@ function stateLabel(state) {
|
|
|
10270
10541
|
building: "construyendo",
|
|
10271
10542
|
built: "construido",
|
|
10272
10543
|
verified: "verificado",
|
|
10544
|
+
reviewed: "revisado",
|
|
10273
10545
|
ready: "listo para archivar",
|
|
10274
10546
|
archived: "archivado"
|
|
10275
10547
|
};
|
|
@@ -10320,7 +10592,7 @@ async function loadProfileFile(filePath) {
|
|
|
10320
10592
|
if (raw === void 0) return void 0;
|
|
10321
10593
|
let data;
|
|
10322
10594
|
try {
|
|
10323
|
-
data =
|
|
10595
|
+
data = parseYaml5(raw);
|
|
10324
10596
|
} catch {
|
|
10325
10597
|
return void 0;
|
|
10326
10598
|
}
|
|
@@ -10332,7 +10604,7 @@ async function loadProfilesFromDir(dir) {
|
|
|
10332
10604
|
const out = [];
|
|
10333
10605
|
for (const entry of entries) {
|
|
10334
10606
|
if (!/\.ya?ml$/i.test(entry)) continue;
|
|
10335
|
-
const profile = await loadProfileFile(
|
|
10607
|
+
const profile = await loadProfileFile(path4.join(dir, entry));
|
|
10336
10608
|
if (profile) out.push(profile);
|
|
10337
10609
|
}
|
|
10338
10610
|
return out;
|
|
@@ -10370,10 +10642,10 @@ async function detectProfiles(root, profiles) {
|
|
|
10370
10642
|
return result;
|
|
10371
10643
|
}
|
|
10372
10644
|
async function loadDetectedBest(sddDir) {
|
|
10373
|
-
const raw = await readTextIfExists(
|
|
10645
|
+
const raw = await readTextIfExists(path4.join(sddDir, "profiles", "detected.yaml"));
|
|
10374
10646
|
if (raw === void 0) return void 0;
|
|
10375
10647
|
try {
|
|
10376
|
-
const data =
|
|
10648
|
+
const data = parseYaml5(raw);
|
|
10377
10649
|
return typeof data?.best === "string" ? data.best : void 0;
|
|
10378
10650
|
} catch {
|
|
10379
10651
|
return void 0;
|
|
@@ -10408,7 +10680,7 @@ function detectionToYaml(result, generatedAt) {
|
|
|
10408
10680
|
}
|
|
10409
10681
|
return lines.join("\n") + "\n";
|
|
10410
10682
|
}
|
|
10411
|
-
var LIVING_FIXES_DIR =
|
|
10683
|
+
var LIVING_FIXES_DIR = path5.join(".sdd", "fixes");
|
|
10412
10684
|
var COMMENT_RE = /<!--[\s\S]*?-->/g;
|
|
10413
10685
|
function parseFixCovers(content) {
|
|
10414
10686
|
const clean = content.replace(COMMENT_RE, "");
|
|
@@ -10435,7 +10707,7 @@ function coversOf(value) {
|
|
|
10435
10707
|
function parseLivingFix(content, file) {
|
|
10436
10708
|
const fm = parseFrontmatter(content, file);
|
|
10437
10709
|
const data = fm.data;
|
|
10438
|
-
const base =
|
|
10710
|
+
const base = path5.basename(file).replace(/\.md$/i, "");
|
|
10439
10711
|
const slug = typeof data["slug"] === "string" && data["slug"].trim() !== "" ? data["slug"].trim() : base.replace(/^\d{4}-\d{2}-/, "");
|
|
10440
10712
|
const fix = {
|
|
10441
10713
|
slug,
|
|
@@ -10451,14 +10723,14 @@ function parseLivingFix(content, file) {
|
|
|
10451
10723
|
return fix;
|
|
10452
10724
|
}
|
|
10453
10725
|
async function archivedFixes(root, known) {
|
|
10454
|
-
const archiveDir =
|
|
10726
|
+
const archiveDir = path5.join(root, ".sdd", "changes", "archive");
|
|
10455
10727
|
const fixes = [];
|
|
10456
10728
|
for (const entry of await listDirs(archiveDir)) {
|
|
10457
|
-
const dir =
|
|
10458
|
-
const fixFile =
|
|
10729
|
+
const dir = path5.join(archiveDir, entry);
|
|
10730
|
+
const fixFile = path5.join(dir, "fix.md");
|
|
10459
10731
|
const fixRaw = await readTextIfExists(fixFile);
|
|
10460
10732
|
if (fixRaw === void 0) continue;
|
|
10461
|
-
const metaFile =
|
|
10733
|
+
const metaFile = path5.join(dir, "meta.yaml");
|
|
10462
10734
|
const metaRaw = await readTextIfExists(metaFile);
|
|
10463
10735
|
const meta = metaRaw !== void 0 ? parseChangeMeta(metaRaw, metaFile).meta : void 0;
|
|
10464
10736
|
if (meta?.lane !== "fix") continue;
|
|
@@ -10481,11 +10753,11 @@ async function archivedFixes(root, known) {
|
|
|
10481
10753
|
return fixes;
|
|
10482
10754
|
}
|
|
10483
10755
|
async function loadLivingFixes(root) {
|
|
10484
|
-
const dir =
|
|
10756
|
+
const dir = path5.join(root, LIVING_FIXES_DIR);
|
|
10485
10757
|
const entries = (await listDir(dir)).filter((entry) => entry.toLowerCase().endsWith(".md")).sort();
|
|
10486
10758
|
const fixes = [];
|
|
10487
10759
|
for (const entry of entries) {
|
|
10488
|
-
const file =
|
|
10760
|
+
const file = path5.join(dir, entry);
|
|
10489
10761
|
const content = await readTextIfExists(file);
|
|
10490
10762
|
if (content === void 0) continue;
|
|
10491
10763
|
fixes.push(parseLivingFix(content, file));
|
|
@@ -10494,10 +10766,10 @@ async function loadLivingFixes(root) {
|
|
|
10494
10766
|
return fixes.sort((a, b) => b.date.localeCompare(a.date) || b.slug.localeCompare(a.slug));
|
|
10495
10767
|
}
|
|
10496
10768
|
async function writeLivingFix(root, input) {
|
|
10497
|
-
const dir =
|
|
10769
|
+
const dir = path5.join(root, LIVING_FIXES_DIR);
|
|
10498
10770
|
const month = input.date.slice(0, 7);
|
|
10499
|
-
const file =
|
|
10500
|
-
const relativePath = toPosix(
|
|
10771
|
+
const file = path5.join(dir, `${month}-${input.slug}.md`);
|
|
10772
|
+
const relativePath = toPosix(path5.relative(root, file));
|
|
10501
10773
|
if (await exists(file)) return { file, relativePath, created: false };
|
|
10502
10774
|
const data = {
|
|
10503
10775
|
slug: input.slug,
|
|
@@ -10516,34 +10788,160 @@ ${body}
|
|
|
10516
10788
|
await writeText(file, text);
|
|
10517
10789
|
return { file, relativePath, created: true };
|
|
10518
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
|
+
}
|
|
10519
10917
|
var SDD_DIR = ".sdd";
|
|
10520
10918
|
async function findWorkspaceRoot(start) {
|
|
10521
|
-
let current =
|
|
10919
|
+
let current = path7.resolve(start);
|
|
10522
10920
|
for (let i = 0; i < 40; i += 1) {
|
|
10523
|
-
if (await isDirectory(
|
|
10524
|
-
const parent =
|
|
10921
|
+
if (await isDirectory(path7.join(current, SDD_DIR))) return current;
|
|
10922
|
+
const parent = path7.dirname(current);
|
|
10525
10923
|
if (parent === current) return void 0;
|
|
10526
10924
|
current = parent;
|
|
10527
10925
|
}
|
|
10528
10926
|
return void 0;
|
|
10529
10927
|
}
|
|
10530
10928
|
async function loadApprovals(sddDir) {
|
|
10531
|
-
const file =
|
|
10929
|
+
const file = path7.join(sddDir, "approvals.yaml");
|
|
10532
10930
|
const raw = await readTextIfExists(file);
|
|
10533
10931
|
if (raw === void 0) return { byArtifact: /* @__PURE__ */ new Map(), diagnostics: [] };
|
|
10534
10932
|
const parsed = parseApprovals(raw, file);
|
|
10535
10933
|
const map = /* @__PURE__ */ new Map();
|
|
10536
10934
|
for (const a of parsed.approvals?.approvals ?? []) {
|
|
10537
|
-
map.set(a.artifact.split(
|
|
10935
|
+
map.set(a.artifact.split(path7.sep).join("/"), { hash: a.artifactHash, by: a.approvedBy, at: a.approvedAt });
|
|
10538
10936
|
}
|
|
10539
10937
|
return { byArtifact: map, diagnostics: parsed.diagnostics };
|
|
10540
10938
|
}
|
|
10541
10939
|
async function loadSpecs(sddDir) {
|
|
10542
|
-
const specsDir =
|
|
10940
|
+
const specsDir = path7.join(sddDir, "specs");
|
|
10543
10941
|
const domains = await listDirs(specsDir);
|
|
10544
10942
|
const out = [];
|
|
10545
10943
|
for (const domain of domains) {
|
|
10546
|
-
const file =
|
|
10944
|
+
const file = path7.join(specsDir, domain, "spec.md");
|
|
10547
10945
|
if (!await exists(file)) continue;
|
|
10548
10946
|
const content = await readText(file);
|
|
10549
10947
|
out.push({ domain, path: file, spec: parseSpecFile(content, file) });
|
|
@@ -10551,10 +10949,10 @@ async function loadSpecs(sddDir) {
|
|
|
10551
10949
|
return out;
|
|
10552
10950
|
}
|
|
10553
10951
|
async function loadChange(root, slug, relDir) {
|
|
10554
|
-
const dir =
|
|
10952
|
+
const dir = path7.join(root, SDD_DIR, "changes", relDir ?? slug);
|
|
10555
10953
|
const diagnostics = [];
|
|
10556
10954
|
const change = { slug, dir, diagnostics };
|
|
10557
|
-
const metaFile =
|
|
10955
|
+
const metaFile = path7.join(dir, "meta.yaml");
|
|
10558
10956
|
const metaRaw = await readTextIfExists(metaFile);
|
|
10559
10957
|
if (metaRaw === void 0) {
|
|
10560
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" }));
|
|
@@ -10563,7 +10961,7 @@ async function loadChange(root, slug, relDir) {
|
|
|
10563
10961
|
diagnostics.push(...parsed.diagnostics);
|
|
10564
10962
|
if (parsed.meta) change.meta = parsed.meta;
|
|
10565
10963
|
}
|
|
10566
|
-
const deltaFile =
|
|
10964
|
+
const deltaFile = path7.join(dir, "spec.md");
|
|
10567
10965
|
const deltaRaw = await readTextIfExists(deltaFile);
|
|
10568
10966
|
if (deltaRaw !== void 0) {
|
|
10569
10967
|
change.delta = parseDelta(deltaRaw, deltaFile);
|
|
@@ -10571,42 +10969,57 @@ async function loadChange(root, slug, relDir) {
|
|
|
10571
10969
|
} else {
|
|
10572
10970
|
diagnostics.push(diag("ATLAS-FILES-002", "warning", `El cambio "${slug}" no tiene spec.md (delta)`, { path: deltaFile }));
|
|
10573
10971
|
}
|
|
10574
|
-
const planFile =
|
|
10972
|
+
const planFile = path7.join(dir, "plan.md");
|
|
10575
10973
|
if (await exists(planFile)) change.planPath = planFile;
|
|
10576
|
-
const reviewFile =
|
|
10974
|
+
const reviewFile = path7.join(dir, "review.md");
|
|
10577
10975
|
if (await exists(reviewFile)) change.reviewPath = reviewFile;
|
|
10578
|
-
const presentationFile =
|
|
10976
|
+
const presentationFile = path7.join(dir, "presentation", "index.html");
|
|
10579
10977
|
if (await exists(presentationFile)) change.presentationPath = presentationFile;
|
|
10580
|
-
const tasksFile =
|
|
10978
|
+
const tasksFile = path7.join(dir, "tasks.md");
|
|
10581
10979
|
const tasksRaw = await readTextIfExists(tasksFile);
|
|
10582
10980
|
if (tasksRaw !== void 0) {
|
|
10583
10981
|
change.tasks = parseTasksFile(tasksRaw, tasksFile);
|
|
10584
10982
|
diagnostics.push(...change.tasks.diagnostics);
|
|
10585
10983
|
}
|
|
10586
|
-
const verifyFile =
|
|
10984
|
+
const verifyFile = path7.join(dir, "verify.md");
|
|
10587
10985
|
const verifyRaw = await readTextIfExists(verifyFile);
|
|
10588
10986
|
if (verifyRaw !== void 0) {
|
|
10589
10987
|
change.verify = parseVerifyFile(verifyRaw, verifyFile);
|
|
10590
10988
|
diagnostics.push(...change.verify.diagnostics);
|
|
10591
10989
|
}
|
|
10592
|
-
const fixFile =
|
|
10990
|
+
const fixFile = path7.join(dir, "fix.md");
|
|
10593
10991
|
const fixRaw = await readTextIfExists(fixFile);
|
|
10594
10992
|
if (fixRaw !== void 0) {
|
|
10595
10993
|
change.fix = parseVerifyFile(fixRaw, fixFile);
|
|
10596
10994
|
diagnostics.push(...change.fix.diagnostics);
|
|
10597
10995
|
change.fixCovers = parseFixCovers(fixRaw);
|
|
10598
10996
|
}
|
|
10599
|
-
const
|
|
10997
|
+
const clarifyFile = path7.join(dir, "clarify.md");
|
|
10998
|
+
const clarifyRaw = await readTextIfExists(clarifyFile);
|
|
10999
|
+
if (clarifyRaw !== void 0) {
|
|
11000
|
+
change.clarify = parseClarify(clarifyRaw, clarifyFile);
|
|
11001
|
+
change.clarifyPath = clarifyFile;
|
|
11002
|
+
diagnostics.push(...change.clarify.diagnostics);
|
|
11003
|
+
}
|
|
11004
|
+
const docsPaths = [];
|
|
11005
|
+
for (const name of ["tecnica.md", "manual.md"]) {
|
|
11006
|
+
const docFile = path7.join(dir, "docs", name);
|
|
11007
|
+
if (await exists(docFile)) docsPaths.push(docFile);
|
|
11008
|
+
}
|
|
11009
|
+
if (docsPaths.length > 0) change.docsPaths = docsPaths;
|
|
11010
|
+
const contracts = await loadContracts(dir);
|
|
11011
|
+
if (contracts.files.length > 0) change.contracts = contracts;
|
|
11012
|
+
const mockupManifest = path7.join(dir, "mockups", "manifest.yaml");
|
|
10600
11013
|
if (await exists(mockupManifest)) change.mockupManifestPath = mockupManifest;
|
|
10601
11014
|
return change;
|
|
10602
11015
|
}
|
|
10603
11016
|
async function listChangeSlugs(root, opts = {}) {
|
|
10604
|
-
const changesDir =
|
|
11017
|
+
const changesDir = path7.join(root, SDD_DIR, "changes");
|
|
10605
11018
|
const dirs = await listDirs(changesDir);
|
|
10606
11019
|
return dirs.filter((d) => opts.includeArchived ? true : d !== "archive");
|
|
10607
11020
|
}
|
|
10608
11021
|
async function loadWorkspace(root) {
|
|
10609
|
-
const sddDir =
|
|
11022
|
+
const sddDir = path7.join(root, SDD_DIR);
|
|
10610
11023
|
const { config, diagnostics: configDiags } = await loadConfig(sddDir);
|
|
10611
11024
|
const diagnostics = [...configDiags];
|
|
10612
11025
|
const specs = await loadSpecs(sddDir);
|
|
@@ -10619,20 +11032,24 @@ async function loadWorkspace(root) {
|
|
|
10619
11032
|
diagnostics.push(...change.diagnostics);
|
|
10620
11033
|
}
|
|
10621
11034
|
const archived = [];
|
|
10622
|
-
const archivedDir =
|
|
11035
|
+
const archivedDir = path7.join(sddDir, "changes", "archive");
|
|
10623
11036
|
if (await isDirectory(archivedDir)) {
|
|
10624
11037
|
for (const entry of await listDirs(archivedDir)) {
|
|
10625
|
-
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)));
|
|
10626
11039
|
}
|
|
10627
11040
|
}
|
|
10628
|
-
|
|
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 };
|
|
10629
11046
|
}
|
|
10630
11047
|
async function ensureSddDirs(sddDir) {
|
|
10631
11048
|
const created = [];
|
|
10632
|
-
const dirs = ["specs", "changes", "runs", "metrics", "fixes",
|
|
11049
|
+
const dirs = ["specs", "changes", "runs", "metrics", "fixes", path7.join("profiles", "custom")];
|
|
10633
11050
|
const { ensureDir: ensureDir2 } = await import("./fsx-VF2P7ALA-XKEWTMDH.js");
|
|
10634
11051
|
for (const d of dirs) {
|
|
10635
|
-
const full =
|
|
11052
|
+
const full = path7.join(sddDir, d);
|
|
10636
11053
|
if (!await exists(full)) {
|
|
10637
11054
|
await ensureDir2(full);
|
|
10638
11055
|
created.push(full);
|
|
@@ -10809,6 +11226,44 @@ satlas verify <slug> --file fix --scenario REQ-DOMINIO-001-S1 --command "<comand
|
|
|
10809
11226
|
o, si es manual:
|
|
10810
11227
|
satlas verify <slug> --file fix --scenario REQ-DOMINIO-001-S1 --method manual --result pass --by "<nombre>" --notes "<c\xF3mo se comprob\xF3>"
|
|
10811
11228
|
-->
|
|
11229
|
+
`,
|
|
11230
|
+
docTecnica: `# Documentaci\xF3n t\xE9cnica \u2014 {{TITLE}}
|
|
11231
|
+
|
|
11232
|
+
## Resumen del cambio
|
|
11233
|
+
|
|
11234
|
+
- **Cambio**: \`{{SLUG}}\` \xB7 dominio \`{{DOMAIN}}\` \xB7 carril \`{{LANE}}\`
|
|
11235
|
+
- **Actualizado**: {{DATE}}
|
|
11236
|
+
- **Tareas**: {{TASKS}}
|
|
11237
|
+
|
|
11238
|
+
## Requisitos y escenarios
|
|
11239
|
+
|
|
11240
|
+
{{REQUIREMENTS}}
|
|
11241
|
+
|
|
11242
|
+
## Evidencia registrada
|
|
11243
|
+
|
|
11244
|
+
{{EVIDENCE}}
|
|
11245
|
+
|
|
11246
|
+
## Pendiente de evidencia
|
|
11247
|
+
|
|
11248
|
+
{{PENDING}}
|
|
11249
|
+
`,
|
|
11250
|
+
docManual: `# Manual \u2014 {{TITLE}}
|
|
11251
|
+
|
|
11252
|
+
## Qu\xE9 hace este cambio
|
|
11253
|
+
|
|
11254
|
+
{{TITLE}} \u2014 dominio \`{{DOMAIN}}\` (cambio \`{{SLUG}}\`, carril \`{{LANE}}\`).
|
|
11255
|
+
|
|
11256
|
+
## C\xF3mo se usa
|
|
11257
|
+
|
|
11258
|
+
{{SCENARIOS}}
|
|
11259
|
+
|
|
11260
|
+
## C\xF3mo se comprob\xF3
|
|
11261
|
+
|
|
11262
|
+
{{EVIDENCE}}
|
|
11263
|
+
|
|
11264
|
+
## Pendiente de comprobar
|
|
11265
|
+
|
|
11266
|
+
{{PENDING}}
|
|
10812
11267
|
`
|
|
10813
11268
|
};
|
|
10814
11269
|
var EN = {
|
|
@@ -10934,6 +11389,44 @@ Cubre: REQ-DOMAIN-001
|
|
|
10934
11389
|
<!-- Register real evidence with:
|
|
10935
11390
|
satlas verify <slug> --file fix --scenario REQ-DOMAIN-001-S1 --command "<command>" --by "<name>"
|
|
10936
11391
|
-->
|
|
11392
|
+
`,
|
|
11393
|
+
docTecnica: `# Technical documentation \u2014 {{TITLE}}
|
|
11394
|
+
|
|
11395
|
+
## Change summary
|
|
11396
|
+
|
|
11397
|
+
- **Change**: \`{{SLUG}}\` \xB7 domain \`{{DOMAIN}}\` \xB7 lane \`{{LANE}}\`
|
|
11398
|
+
- **Updated**: {{DATE}}
|
|
11399
|
+
- **Tasks**: {{TASKS}}
|
|
11400
|
+
|
|
11401
|
+
## Requirements and scenarios
|
|
11402
|
+
|
|
11403
|
+
{{REQUIREMENTS}}
|
|
11404
|
+
|
|
11405
|
+
## Recorded evidence
|
|
11406
|
+
|
|
11407
|
+
{{EVIDENCE}}
|
|
11408
|
+
|
|
11409
|
+
## Pending evidence
|
|
11410
|
+
|
|
11411
|
+
{{PENDING}}
|
|
11412
|
+
`,
|
|
11413
|
+
docManual: `# Manual \u2014 {{TITLE}}
|
|
11414
|
+
|
|
11415
|
+
## What this change does
|
|
11416
|
+
|
|
11417
|
+
{{TITLE}} \u2014 domain \`{{DOMAIN}}\` (change \`{{SLUG}}\`, lane \`{{LANE}}\`).
|
|
11418
|
+
|
|
11419
|
+
## How to use it
|
|
11420
|
+
|
|
11421
|
+
{{SCENARIOS}}
|
|
11422
|
+
|
|
11423
|
+
## How it was verified
|
|
11424
|
+
|
|
11425
|
+
{{EVIDENCE}}
|
|
11426
|
+
|
|
11427
|
+
## Pending verification
|
|
11428
|
+
|
|
11429
|
+
{{PENDING}}
|
|
10937
11430
|
`
|
|
10938
11431
|
};
|
|
10939
11432
|
function templatesFor(language) {
|
|
@@ -10955,7 +11448,7 @@ function changeMetaYaml(meta) {
|
|
|
10955
11448
|
if (meta.owner) doc["owner"] = meta.owner;
|
|
10956
11449
|
if (meta.mockups) doc["mockups"] = meta.mockups;
|
|
10957
11450
|
return `# Estado del cambio. La fase se DERIVA de los artefactos; aqu\xED solo hechos.
|
|
10958
|
-
` +
|
|
11451
|
+
` + stringifyYaml5(doc, { lineWidth: 120 });
|
|
10959
11452
|
}
|
|
10960
11453
|
function indexMarkdown(input) {
|
|
10961
11454
|
const es = input.language !== "en";
|
|
@@ -11129,8 +11622,8 @@ function issueBody(input) {
|
|
|
11129
11622
|
lines.push(`- Siguiente: \`${input.next}\` \u2014 ${input.nextDescription}`);
|
|
11130
11623
|
lines.push("");
|
|
11131
11624
|
lines.push("### Artefactos");
|
|
11132
|
-
lines.push(`- Spec: \`${
|
|
11133
|
-
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")}\``);
|
|
11134
11627
|
if (change.tasks) lines.push(`- Tareas: ${change.tasks.counts.done}/${change.tasks.counts.total}`);
|
|
11135
11628
|
if (change.verify) lines.push(`- Evidencia registrada: ${change.verify.evidence.length}`);
|
|
11136
11629
|
lines.push("");
|
|
@@ -11194,14 +11687,14 @@ async function issueLabels(root, number, runner = defaultRunner) {
|
|
|
11194
11687
|
}
|
|
11195
11688
|
}
|
|
11196
11689
|
async function patchChangeMeta(root, slug, patch) {
|
|
11197
|
-
const file =
|
|
11690
|
+
const file = path8.join(path8.resolve(root), ".sdd", "changes", slug, "meta.yaml");
|
|
11198
11691
|
const raw = await readTextIfExists(file);
|
|
11199
11692
|
if (raw === void 0) {
|
|
11200
|
-
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)}`)] };
|
|
11201
11694
|
}
|
|
11202
11695
|
let data;
|
|
11203
11696
|
try {
|
|
11204
|
-
data =
|
|
11697
|
+
data = parseYaml7(raw) ?? {};
|
|
11205
11698
|
} catch (err) {
|
|
11206
11699
|
return { path: file, diagnostics: [diag("ATLAS-GH-007", "error", `meta.yaml inv\xE1lido: ${err.message}`)] };
|
|
11207
11700
|
}
|
|
@@ -11251,7 +11744,7 @@ async function approveFromGithub(opts) {
|
|
|
11251
11744
|
const { signApproval: signApproval2 } = await import("./approvals-2PO6223I-DENKLEXU.js");
|
|
11252
11745
|
const signed = await signApproval2({
|
|
11253
11746
|
root: opts.root,
|
|
11254
|
-
artifact:
|
|
11747
|
+
artifact: path8.posix.join("changes", opts.slug, "spec.md"),
|
|
11255
11748
|
by,
|
|
11256
11749
|
channel: "tracker",
|
|
11257
11750
|
note: `Aprobado v\xEDa GitHub issue #${number} (etiqueta "${label}")`
|
|
@@ -11346,10 +11839,10 @@ function buildEvidenceYaml(evidence) {
|
|
|
11346
11839
|
return lines.join("\n");
|
|
11347
11840
|
}
|
|
11348
11841
|
async function recordEvidence(opts) {
|
|
11349
|
-
const root =
|
|
11842
|
+
const root = path9.resolve(opts.root);
|
|
11350
11843
|
const slug = opts.slug;
|
|
11351
11844
|
const fileKind = opts.file ?? "verify";
|
|
11352
|
-
const file =
|
|
11845
|
+
const file = path9.join(root, ".sdd", "changes", slug, fileKind === "fix" ? "fix.md" : "verify.md");
|
|
11353
11846
|
const diagnostics = [];
|
|
11354
11847
|
const scenario = opts.scenario.toUpperCase();
|
|
11355
11848
|
if (!SCENARIO_ID_RE.test(scenario)) {
|
|
@@ -11439,7 +11932,7 @@ function generateRunId(now = /* @__PURE__ */ new Date()) {
|
|
|
11439
11932
|
return `${iso.slice(0, 8)}-${iso.slice(8, 14)}-${randomBytes(3).toString("hex")}`;
|
|
11440
11933
|
}
|
|
11441
11934
|
function runsDir(root) {
|
|
11442
|
-
return
|
|
11935
|
+
return path10.join(path10.resolve(root), ".sdd", "runs");
|
|
11443
11936
|
}
|
|
11444
11937
|
async function createRun(root, slug, phase, inputs, now = /* @__PURE__ */ new Date()) {
|
|
11445
11938
|
const runId = generateRunId(now);
|
|
@@ -11452,25 +11945,25 @@ async function createRun(root, slug, phase, inputs, now = /* @__PURE__ */ new Da
|
|
|
11452
11945
|
updatedAt: localStamp(now)
|
|
11453
11946
|
};
|
|
11454
11947
|
if (inputs) state.inputs = inputs;
|
|
11455
|
-
const dir =
|
|
11948
|
+
const dir = path10.join(runsDir(root), runId);
|
|
11456
11949
|
await ensureDir(dir);
|
|
11457
|
-
await writeText(
|
|
11950
|
+
await writeText(path10.join(dir, "state.json"), `${JSON.stringify(state, null, 2)}
|
|
11458
11951
|
`);
|
|
11459
|
-
await writeText(
|
|
11952
|
+
await writeText(path10.join(dir, "events.jsonl"), "");
|
|
11460
11953
|
await appendRunEvent(root, runId, "run_started", { slug, phase });
|
|
11461
11954
|
return state;
|
|
11462
11955
|
}
|
|
11463
11956
|
async function appendRunEvent(root, runId, type, data, now = /* @__PURE__ */ new Date()) {
|
|
11464
11957
|
const event = { eventId: randomBytes(4).toString("hex"), at: localStamp(now), type };
|
|
11465
11958
|
if (data) event.data = data;
|
|
11466
|
-
const file =
|
|
11959
|
+
const file = path10.join(runsDir(root), runId, "events.jsonl");
|
|
11467
11960
|
const previous = await readTextIfExists(file) ?? "";
|
|
11468
11961
|
await writeText(file, `${previous}${JSON.stringify(event)}
|
|
11469
11962
|
`);
|
|
11470
11963
|
return event;
|
|
11471
11964
|
}
|
|
11472
11965
|
async function updateRunStatus(root, runId, status, now = /* @__PURE__ */ new Date()) {
|
|
11473
|
-
const file =
|
|
11966
|
+
const file = path10.join(runsDir(root), runId, "state.json");
|
|
11474
11967
|
const raw = await readTextIfExists(file);
|
|
11475
11968
|
if (raw === void 0) return;
|
|
11476
11969
|
const state = JSON.parse(raw);
|
|
@@ -11480,13 +11973,13 @@ async function updateRunStatus(root, runId, status, now = /* @__PURE__ */ new Da
|
|
|
11480
11973
|
`);
|
|
11481
11974
|
}
|
|
11482
11975
|
async function readRun(root, runId) {
|
|
11483
|
-
const dir =
|
|
11484
|
-
const stateRaw = await readTextIfExists(
|
|
11976
|
+
const dir = path10.join(runsDir(root), runId);
|
|
11977
|
+
const stateRaw = await readTextIfExists(path10.join(dir, "state.json"));
|
|
11485
11978
|
if (stateRaw === void 0) {
|
|
11486
11979
|
return { diagnostics: [diag("ATLAS-RUN-001", "error", `No existe el run ${runId}`, { path: dir })] };
|
|
11487
11980
|
}
|
|
11488
11981
|
const state = JSON.parse(stateRaw);
|
|
11489
|
-
const eventsRaw = await readTextIfExists(
|
|
11982
|
+
const eventsRaw = await readTextIfExists(path10.join(dir, "events.jsonl")) ?? "";
|
|
11490
11983
|
const events = [];
|
|
11491
11984
|
for (const line of eventsRaw.split("\n")) {
|
|
11492
11985
|
if (line.trim() === "") continue;
|
|
@@ -11503,7 +11996,7 @@ async function listRuns(root, filter = {}) {
|
|
|
11503
11996
|
const ids = await listDirs(dir);
|
|
11504
11997
|
const out = [];
|
|
11505
11998
|
for (const id of ids) {
|
|
11506
|
-
const raw = await readTextIfExists(
|
|
11999
|
+
const raw = await readTextIfExists(path10.join(dir, id, "state.json"));
|
|
11507
12000
|
if (raw === void 0) continue;
|
|
11508
12001
|
try {
|
|
11509
12002
|
const state = JSON.parse(raw);
|
|
@@ -11538,20 +12031,20 @@ var mockupManifestSchema = z4.object({
|
|
|
11538
12031
|
});
|
|
11539
12032
|
var TOKEN_CANDIDATES = ["design/tokens.json", "DESIGN.md", "design/DESIGN.md", ".sdd/design/tokens.json"];
|
|
11540
12033
|
function mockupsDir(root, slug) {
|
|
11541
|
-
return
|
|
12034
|
+
return path11.join(path11.resolve(root), ".sdd", "changes", slug, "mockups");
|
|
11542
12035
|
}
|
|
11543
12036
|
async function tokensFile(root) {
|
|
11544
12037
|
for (const candidate of TOKEN_CANDIDATES) {
|
|
11545
|
-
const abs =
|
|
12038
|
+
const abs = path11.join(root, candidate);
|
|
11546
12039
|
if (await exists(abs)) return candidate;
|
|
11547
12040
|
}
|
|
11548
12041
|
return void 0;
|
|
11549
12042
|
}
|
|
11550
12043
|
async function computeInputsHash(root, change) {
|
|
11551
|
-
const deltaPath =
|
|
12044
|
+
const deltaPath = path11.join(change.dir, "spec.md");
|
|
11552
12045
|
const delta = await readTextIfExists(deltaPath) ?? "";
|
|
11553
12046
|
const tokens = await tokensFile(root);
|
|
11554
|
-
const tokensContent = tokens ? await readTextIfExists(
|
|
12047
|
+
const tokensContent = tokens ? await readTextIfExists(path11.join(root, tokens)) ?? "" : "";
|
|
11555
12048
|
return artifactHash(`${delta}
|
|
11556
12049
|
---tokens---
|
|
11557
12050
|
${tokensContent}`);
|
|
@@ -11576,12 +12069,12 @@ function planMockups(change, platform = "web") {
|
|
|
11576
12069
|
return { platform, screens };
|
|
11577
12070
|
}
|
|
11578
12071
|
async function readMockupManifest(root, slug) {
|
|
11579
|
-
const file =
|
|
12072
|
+
const file = path11.join(mockupsDir(root, slug), "manifest.yaml");
|
|
11580
12073
|
const raw = await readTextIfExists(file);
|
|
11581
12074
|
if (raw === void 0) return { diagnostics: [] };
|
|
11582
12075
|
let data;
|
|
11583
12076
|
try {
|
|
11584
|
-
data =
|
|
12077
|
+
data = parseYaml8(raw);
|
|
11585
12078
|
} catch (err) {
|
|
11586
12079
|
return { path: file, diagnostics: [diag("ATLAS-MKP-002", "error", `manifest.yaml inv\xE1lido: ${err.message}`, { path: file })] };
|
|
11587
12080
|
}
|
|
@@ -11668,7 +12161,7 @@ async function checkMockups(root, slug, change) {
|
|
|
11668
12161
|
const { manifest, path: manifestPath, diagnostics } = await readMockupManifest(root, slug);
|
|
11669
12162
|
const findings = [...diagnostics];
|
|
11670
12163
|
if (!manifest || !manifestPath) {
|
|
11671
|
-
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 };
|
|
11672
12165
|
}
|
|
11673
12166
|
const known = /* @__PURE__ */ new Set();
|
|
11674
12167
|
for (const req of [...change.delta?.added ?? [], ...change.delta?.modified ?? []]) {
|
|
@@ -11676,12 +12169,12 @@ async function checkMockups(root, slug, change) {
|
|
|
11676
12169
|
}
|
|
11677
12170
|
findings.push(...lintMockupManifest(manifest, known, manifestPath));
|
|
11678
12171
|
for (const screen of manifest.screens) {
|
|
11679
|
-
const html = await readTextIfExists(
|
|
12172
|
+
const html = await readTextIfExists(path11.join(dir, screen.file));
|
|
11680
12173
|
if (html === void 0) {
|
|
11681
|
-
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) }));
|
|
11682
12175
|
continue;
|
|
11683
12176
|
}
|
|
11684
|
-
findings.push(...lintMockupHtml(html,
|
|
12177
|
+
findings.push(...lintMockupHtml(html, path11.join(dir, screen.file)));
|
|
11685
12178
|
}
|
|
11686
12179
|
const inputsHash = await computeInputsHash(root, change);
|
|
11687
12180
|
const stale = manifest.inputsHash !== void 0 && manifest.inputsHash !== inputsHash;
|
|
@@ -11696,7 +12189,7 @@ async function checkMockups(root, slug, change) {
|
|
|
11696
12189
|
return { findings, stale, manifest, manifestPath };
|
|
11697
12190
|
}
|
|
11698
12191
|
async function writeMockupPlan(root, slug, plan, now = /* @__PURE__ */ new Date()) {
|
|
11699
|
-
const file =
|
|
12192
|
+
const file = path11.join(mockupsDir(root, slug), "plan.yaml");
|
|
11700
12193
|
if (await exists(file)) return file;
|
|
11701
12194
|
const doc = {
|
|
11702
12195
|
schema_version: 1,
|
|
@@ -11704,11 +12197,11 @@ async function writeMockupPlan(root, slug, plan, now = /* @__PURE__ */ new Date(
|
|
|
11704
12197
|
platform: plan.platform,
|
|
11705
12198
|
screens: plan.screens.map((s) => ({ id: s.id, title: s.title, file: s.file, illustrates: s.illustrates, states: s.states, breakpoints: s.breakpoints }))
|
|
11706
12199
|
};
|
|
11707
|
-
await writeText(file,
|
|
12200
|
+
await writeText(file, stringifyYaml6(doc, { lineWidth: 120 }));
|
|
11708
12201
|
return file;
|
|
11709
12202
|
}
|
|
11710
12203
|
async function writeMockupManifest(root, slug, plan, inputsHash, now = /* @__PURE__ */ new Date()) {
|
|
11711
|
-
const file =
|
|
12204
|
+
const file = path11.join(mockupsDir(root, slug), "manifest.yaml");
|
|
11712
12205
|
if (await exists(file)) return file;
|
|
11713
12206
|
const doc = {
|
|
11714
12207
|
schema_version: 1,
|
|
@@ -11720,16 +12213,16 @@ async function writeMockupManifest(root, slug, plan, inputsHash, now = /* @__PUR
|
|
|
11720
12213
|
screens: plan.screens.map((s) => ({ id: s.id, title: s.title, file: s.file, illustrates: s.illustrates, states: s.states, breakpoints: s.breakpoints })),
|
|
11721
12214
|
screenshots: []
|
|
11722
12215
|
};
|
|
11723
|
-
await writeText(file,
|
|
12216
|
+
await writeText(file, stringifyYaml6(doc, { lineWidth: 120 }));
|
|
11724
12217
|
return file;
|
|
11725
12218
|
}
|
|
11726
12219
|
async function updateMockupScreenshots(root, slug, screenshots) {
|
|
11727
|
-
const file =
|
|
12220
|
+
const file = path11.join(mockupsDir(root, slug), "manifest.yaml");
|
|
11728
12221
|
const raw = await readTextIfExists(file);
|
|
11729
12222
|
if (raw === void 0) return void 0;
|
|
11730
|
-
const data =
|
|
12223
|
+
const data = parseYaml8(raw) ?? {};
|
|
11731
12224
|
data["screenshots"] = screenshots;
|
|
11732
|
-
await writeText(file,
|
|
12225
|
+
await writeText(file, stringifyYaml6(data, { lineWidth: 120 }));
|
|
11733
12226
|
return file;
|
|
11734
12227
|
}
|
|
11735
12228
|
async function captureMockups(root, slug, manifest, now = /* @__PURE__ */ new Date()) {
|
|
@@ -11754,9 +12247,9 @@ async function captureMockups(root, slug, manifest, now = /* @__PURE__ */ new Da
|
|
|
11754
12247
|
const page2 = await browser.newPage();
|
|
11755
12248
|
for (const bp of screen.breakpoints && screen.breakpoints.length > 0 ? screen.breakpoints : [1440]) {
|
|
11756
12249
|
await page2.setViewportSize({ width: bp, height: 900 });
|
|
11757
|
-
await page2.goto(`file://${
|
|
11758
|
-
const rel =
|
|
11759
|
-
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) });
|
|
11760
12253
|
screenshots.push(rel);
|
|
11761
12254
|
}
|
|
11762
12255
|
await page2.close();
|
|
@@ -11772,7 +12265,7 @@ async function mockupsReady(root, slug, change) {
|
|
|
11772
12265
|
return Boolean(check.manifest && check.manifest.screens.length > 0 && !check.stale);
|
|
11773
12266
|
}
|
|
11774
12267
|
async function setMockupRequirement(root, slug, value) {
|
|
11775
|
-
const file =
|
|
12268
|
+
const file = path11.join(root, ".sdd", "changes", slug, "meta.yaml");
|
|
11776
12269
|
const raw = await readTextIfExists(file) ?? `schema_version: 1
|
|
11777
12270
|
slug: ${slug}
|
|
11778
12271
|
lane: standard
|
|
@@ -12002,17 +12495,17 @@ ${texts.scenarios}`);
|
|
|
12002
12495
|
}
|
|
12003
12496
|
}
|
|
12004
12497
|
async function loadProjectPacks(sddDir) {
|
|
12005
|
-
const dir =
|
|
12498
|
+
const dir = path12.join(sddDir, "packs");
|
|
12006
12499
|
const diagnostics = [];
|
|
12007
12500
|
const packs = [];
|
|
12008
12501
|
for (const entry of await listDir(dir)) {
|
|
12009
12502
|
if (!/\.ya?ml$/i.test(entry)) continue;
|
|
12010
|
-
const file =
|
|
12503
|
+
const file = path12.join(dir, entry);
|
|
12011
12504
|
const raw = await readTextIfExists(file);
|
|
12012
12505
|
if (raw === void 0) continue;
|
|
12013
12506
|
let data;
|
|
12014
12507
|
try {
|
|
12015
|
-
data =
|
|
12508
|
+
data = parseYaml9(raw);
|
|
12016
12509
|
} catch (err) {
|
|
12017
12510
|
diagnostics.push(diag("PACK-000", "error", `Pack inv\xE1lido (${entry}): ${err.message}`, { path: file }));
|
|
12018
12511
|
continue;
|
|
@@ -12094,7 +12587,7 @@ function packFindings(evaluations, change) {
|
|
|
12094
12587
|
}
|
|
12095
12588
|
var UI_DOMAINS = /* @__PURE__ */ new Set(["frontend", "mobile", "fullstack"]);
|
|
12096
12589
|
async function runAnalyze(opts) {
|
|
12097
|
-
const root =
|
|
12590
|
+
const root = path13.resolve(opts.root);
|
|
12098
12591
|
const { workspace, config } = await loadWorkspace(root);
|
|
12099
12592
|
const change = workspace.changes.find((c) => c.slug === opts.slug);
|
|
12100
12593
|
if (!change) {
|
|
@@ -12112,9 +12605,9 @@ async function runAnalyze(opts) {
|
|
|
12112
12605
|
for (const req of spec.spec.requirements) living.set(req.id, req);
|
|
12113
12606
|
}
|
|
12114
12607
|
if (change.delta) {
|
|
12115
|
-
findings.push(...lintDelta(change.delta, living,
|
|
12608
|
+
findings.push(...lintDelta(change.delta, living, path13.join(change.dir, "spec.md"), { language: config.spec.language }));
|
|
12116
12609
|
}
|
|
12117
|
-
const trace = checkTrace({ specs: workspace.specs, change, requireEvidence: false });
|
|
12610
|
+
const trace = checkTrace({ specs: workspace.specs, change, requireEvidence: false, linked: linkedTraceInput(workspace) });
|
|
12118
12611
|
findings.push(...trace.findings);
|
|
12119
12612
|
let waves;
|
|
12120
12613
|
if (change.tasks && change.tasks.counts.total > 0) {
|
|
@@ -12161,7 +12654,7 @@ async function runAnalyze(opts) {
|
|
|
12161
12654
|
if (mockups) result.mockups = mockups;
|
|
12162
12655
|
if (packs) result.packs = packs;
|
|
12163
12656
|
if (opts.write !== false) {
|
|
12164
|
-
const file =
|
|
12657
|
+
const file = path13.join(change.dir, "analyze.md");
|
|
12165
12658
|
await writeText(file, renderAnalyze(change, result, localStamp(opts.now)));
|
|
12166
12659
|
result.path = file;
|
|
12167
12660
|
}
|
|
@@ -12203,7 +12696,7 @@ function renderAnalyze(change, result, generatedAt) {
|
|
|
12203
12696
|
return lines.join("\n");
|
|
12204
12697
|
}
|
|
12205
12698
|
async function collectMetrics(root, now = /* @__PURE__ */ new Date()) {
|
|
12206
|
-
const resolved =
|
|
12699
|
+
const resolved = path14.resolve(root);
|
|
12207
12700
|
const { workspace, config } = await loadWorkspace(resolved);
|
|
12208
12701
|
const approvals = await loadApprovals(workspace.sddDir);
|
|
12209
12702
|
const living = /* @__PURE__ */ new Map();
|
|
@@ -12218,11 +12711,12 @@ async function collectMetrics(root, now = /* @__PURE__ */ new Date()) {
|
|
|
12218
12711
|
const deltaContent = await readDelta(change);
|
|
12219
12712
|
const approval = verifyApproval(change, approvals.byArtifact, config, deltaContent);
|
|
12220
12713
|
const findings = [];
|
|
12221
|
-
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 }));
|
|
12222
12715
|
const trace = checkTrace({
|
|
12223
12716
|
specs: workspace.specs,
|
|
12224
12717
|
change,
|
|
12225
|
-
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)
|
|
12226
12720
|
});
|
|
12227
12721
|
findings.push(...trace.findings);
|
|
12228
12722
|
const state = deriveState({ change, cfg: config, approval, blockingFindings: change.delta ? countBySeverity(findings).errors : 0 });
|
|
@@ -12283,7 +12777,7 @@ async function collectMetrics(root, now = /* @__PURE__ */ new Date()) {
|
|
|
12283
12777
|
attention.push({ slug: change.slug, kind: "missing-evidence", detail: `evidencia ${change.scenariosPassed}/${change.scenariosTotal}` });
|
|
12284
12778
|
}
|
|
12285
12779
|
}
|
|
12286
|
-
const archiveDir =
|
|
12780
|
+
const archiveDir = path14.join(workspace.sddDir, "changes", "archive");
|
|
12287
12781
|
const archivedEntries = await listDirs(archiveDir);
|
|
12288
12782
|
const throughputByMonth = {};
|
|
12289
12783
|
for (const entry of archivedEntries) {
|
|
@@ -12321,10 +12815,10 @@ async function collectMetrics(root, now = /* @__PURE__ */ new Date()) {
|
|
|
12321
12815
|
}
|
|
12322
12816
|
async function readDelta(change) {
|
|
12323
12817
|
const { readTextIfExists: readTextIfExists2 } = await import("./fsx-VF2P7ALA-XKEWTMDH.js");
|
|
12324
|
-
return readTextIfExists2(
|
|
12818
|
+
return readTextIfExists2(path14.join(change.dir, "spec.md"));
|
|
12325
12819
|
}
|
|
12326
12820
|
async function generatePresentation(opts) {
|
|
12327
|
-
const root =
|
|
12821
|
+
const root = path15.resolve(opts.root);
|
|
12328
12822
|
const { workspace, config } = await loadWorkspace(root);
|
|
12329
12823
|
const language = config.project.language;
|
|
12330
12824
|
const change = workspace.changes.find((c) => c.slug === opts.slug);
|
|
@@ -12334,17 +12828,17 @@ async function generatePresentation(opts) {
|
|
|
12334
12828
|
if (!change.delta) {
|
|
12335
12829
|
return { slug: opts.slug, diagnostics: [diag("ATLAS-PRESENT-001", "error", `El cambio "${opts.slug}" no tiene spec.md (delta)`)] };
|
|
12336
12830
|
}
|
|
12337
|
-
const deltaPath =
|
|
12831
|
+
const deltaPath = path15.join(change.dir, "spec.md");
|
|
12338
12832
|
const deltaContent = await readTextIfExists(deltaPath) ?? "";
|
|
12339
12833
|
const hash = artifactHash(deltaContent);
|
|
12340
12834
|
const now = opts.now ?? /* @__PURE__ */ new Date();
|
|
12341
|
-
const presentationDir =
|
|
12342
|
-
const mockupsCopyDir =
|
|
12835
|
+
const presentationDir = path15.join(change.dir, "presentation");
|
|
12836
|
+
const mockupsCopyDir = path15.join(presentationDir, "mockups");
|
|
12343
12837
|
await ensureDir(presentationDir);
|
|
12344
12838
|
const mockupInfo = await copyMockups(root, change, mockupsCopyDir);
|
|
12345
|
-
const proposalRaw = await readTextIfExists(
|
|
12839
|
+
const proposalRaw = await readTextIfExists(path15.join(change.dir, "proposal.md"));
|
|
12346
12840
|
const proposalBody = proposalRaw ? parseFrontmatter(proposalRaw).body : "";
|
|
12347
|
-
const approvals = await loadApprovals(
|
|
12841
|
+
const approvals = await loadApprovals(path15.join(root, ".sdd"));
|
|
12348
12842
|
const approval = verifyApproval(change, approvals.byArtifact, config, deltaContent);
|
|
12349
12843
|
const labels = labelsFor(language);
|
|
12350
12844
|
const html = page({
|
|
@@ -12365,7 +12859,7 @@ async function generatePresentation(opts) {
|
|
|
12365
12859
|
approveHint: `satlas approve ${change.slug} --by "<nombre>" --channel presentation`,
|
|
12366
12860
|
...approval.status === "valid" && approval.approvedBy ? { approval: { by: approval.approvedBy, at: approval.approvedAt ?? "" } } : {}
|
|
12367
12861
|
});
|
|
12368
|
-
const outFile =
|
|
12862
|
+
const outFile = path15.join(presentationDir, "index.html");
|
|
12369
12863
|
await writeText(outFile, html);
|
|
12370
12864
|
return { slug: change.slug, path: outFile, hash, diagnostics: mockupInfo.diagnostics };
|
|
12371
12865
|
}
|
|
@@ -12377,12 +12871,12 @@ async function copyMockups(root, change, destDir) {
|
|
|
12377
12871
|
await ensureDir(destDir);
|
|
12378
12872
|
const screens = [];
|
|
12379
12873
|
for (const screen of manifest.screens) {
|
|
12380
|
-
const src =
|
|
12874
|
+
const src = path15.join(dir, screen.file);
|
|
12381
12875
|
if (!await exists(src)) {
|
|
12382
12876
|
diagnostics.push(diag("ATLAS-PRESENT-002", "warning", `El mockup ${screen.file} no existe y no se incluir\xE1`, { path: src }));
|
|
12383
12877
|
continue;
|
|
12384
12878
|
}
|
|
12385
|
-
await copyFile(src,
|
|
12879
|
+
await copyFile(src, path15.join(destDir, screen.file));
|
|
12386
12880
|
screens.push({
|
|
12387
12881
|
id: screen.id,
|
|
12388
12882
|
title: screen.title ?? screen.id,
|
|
@@ -12392,13 +12886,13 @@ async function copyMockups(root, change, destDir) {
|
|
|
12392
12886
|
});
|
|
12393
12887
|
}
|
|
12394
12888
|
const screenshots = [];
|
|
12395
|
-
const shotsDir =
|
|
12889
|
+
const shotsDir = path15.join(dir, "screens");
|
|
12396
12890
|
if (await exists(shotsDir)) {
|
|
12397
|
-
await ensureDir(
|
|
12891
|
+
await ensureDir(path15.join(destDir, "screens"));
|
|
12398
12892
|
const { listDir: listDir4 } = await import("./fsx-VF2P7ALA-XKEWTMDH.js");
|
|
12399
12893
|
for (const file of await listDir4(shotsDir)) {
|
|
12400
12894
|
if (!/\.(png|jpe?g|webp)$/i.test(file)) continue;
|
|
12401
|
-
await copyFile(
|
|
12895
|
+
await copyFile(path15.join(shotsDir, file), path15.join(destDir, "screens", file));
|
|
12402
12896
|
screenshots.push(`mockups/screens/${file}`);
|
|
12403
12897
|
}
|
|
12404
12898
|
}
|
|
@@ -12614,11 +13108,11 @@ var SOURCE_EXT = /* @__PURE__ */ new Set([
|
|
|
12614
13108
|
".groovy"
|
|
12615
13109
|
]);
|
|
12616
13110
|
async function adoptWorkspace(opts) {
|
|
12617
|
-
const root =
|
|
13111
|
+
const root = path16.resolve(opts.root);
|
|
12618
13112
|
const dryRun = opts.dryRun ?? false;
|
|
12619
13113
|
const diagnostics = [];
|
|
12620
|
-
const sddDir =
|
|
12621
|
-
if (!await exists(
|
|
13114
|
+
const sddDir = path16.join(root, ".sdd");
|
|
13115
|
+
if (!await exists(path16.join(sddDir, "config.yaml"))) {
|
|
12622
13116
|
return {
|
|
12623
13117
|
root,
|
|
12624
13118
|
domains: [],
|
|
@@ -12629,8 +13123,8 @@ async function adoptWorkspace(opts) {
|
|
|
12629
13123
|
}
|
|
12630
13124
|
const { config } = await loadConfig(sddDir);
|
|
12631
13125
|
const files = await walkFiles(root, { skipDirs: [...SKIP_DIRS] });
|
|
12632
|
-
const inventoried = files.map((f) => toPosix(
|
|
12633
|
-
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()));
|
|
12634
13128
|
if (relative.length === 0) {
|
|
12635
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>" }));
|
|
12636
13130
|
}
|
|
@@ -12658,13 +13152,13 @@ async function adoptWorkspace(opts) {
|
|
|
12658
13152
|
const createdSpecs = [];
|
|
12659
13153
|
for (const name of domainNames.sort()) {
|
|
12660
13154
|
const domainFiles = (counts.get(name) ?? []).sort();
|
|
12661
|
-
const existingSpec = await exists(
|
|
13155
|
+
const existingSpec = await exists(path16.join(sddDir, "specs", name, "spec.md"));
|
|
12662
13156
|
domains.push({ name, files: domainFiles.length, samples: domainFiles.slice(0, 8), existingSpec });
|
|
12663
13157
|
if (existingSpec) {
|
|
12664
|
-
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") }));
|
|
12665
13159
|
continue;
|
|
12666
13160
|
}
|
|
12667
|
-
const specPath =
|
|
13161
|
+
const specPath = path16.join(sddDir, "specs", name, "spec.md");
|
|
12668
13162
|
if (!dryRun) {
|
|
12669
13163
|
await writeText(specPath, baselineSpec(name, domainFiles));
|
|
12670
13164
|
createdSpecs.push(specPath);
|
|
@@ -12672,7 +13166,7 @@ async function adoptWorkspace(opts) {
|
|
|
12672
13166
|
createdSpecs.push(specPath);
|
|
12673
13167
|
}
|
|
12674
13168
|
}
|
|
12675
|
-
const reportPath =
|
|
13169
|
+
const reportPath = path16.join(sddDir, "adopt-report.md");
|
|
12676
13170
|
if (!dryRun) {
|
|
12677
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() }));
|
|
12678
13172
|
}
|
|
@@ -12692,12 +13186,12 @@ async function loadProjectProfiles(root, dirs) {
|
|
|
12692
13186
|
if (!await exists(dir)) continue;
|
|
12693
13187
|
out.push(...await loadProfilesFromDir(dir));
|
|
12694
13188
|
}
|
|
12695
|
-
const custom =
|
|
13189
|
+
const custom = path16.join(root, ".sdd", "profiles", "custom");
|
|
12696
13190
|
if (await exists(custom)) out.push(...await loadProfilesFromDir(custom));
|
|
12697
13191
|
return out;
|
|
12698
13192
|
}
|
|
12699
13193
|
async function fallbackDomainName(root, projectName) {
|
|
12700
|
-
const pkg = await readTextIfExists(
|
|
13194
|
+
const pkg = await readTextIfExists(path16.join(root, "package.json"));
|
|
12701
13195
|
if (pkg) {
|
|
12702
13196
|
try {
|
|
12703
13197
|
const data = JSON.parse(pkg);
|
|
@@ -12784,11 +13278,11 @@ function adoptReport(input) {
|
|
|
12784
13278
|
return lines.join("\n");
|
|
12785
13279
|
}
|
|
12786
13280
|
async function initWorkspace(opts) {
|
|
12787
|
-
const root =
|
|
12788
|
-
const sddDir =
|
|
13281
|
+
const root = path17.resolve(opts.root);
|
|
13282
|
+
const sddDir = path17.join(root, ".sdd");
|
|
12789
13283
|
const diagnostics = [];
|
|
12790
13284
|
const created = [];
|
|
12791
|
-
if (await exists(
|
|
13285
|
+
if (await exists(path17.join(sddDir, "config.yaml"))) {
|
|
12792
13286
|
return {
|
|
12793
13287
|
sddDir,
|
|
12794
13288
|
created,
|
|
@@ -12798,29 +13292,29 @@ async function initWorkspace(opts) {
|
|
|
12798
13292
|
created.push(...await ensureSddDirs(sddDir));
|
|
12799
13293
|
const language = opts.language ?? "es";
|
|
12800
13294
|
const cfg = defaultConfig({ name: opts.name, language });
|
|
12801
|
-
cfg.project.name = opts.name ??
|
|
13295
|
+
cfg.project.name = opts.name ?? path17.basename(root);
|
|
12802
13296
|
await writeConfig(sddDir, cfg);
|
|
12803
|
-
created.push(
|
|
13297
|
+
created.push(path17.join(sddDir, "config.yaml"));
|
|
12804
13298
|
const templates = templatesFor(language);
|
|
12805
|
-
await writeText(
|
|
12806
|
-
created.push(
|
|
12807
|
-
await writeText(
|
|
12808
|
-
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"));
|
|
12809
13303
|
const detected = await detectIfPossible(root, opts.profilesDir);
|
|
12810
13304
|
if (detected) {
|
|
12811
13305
|
const yaml2 = detectionToYaml(detected, localStamp(opts.now));
|
|
12812
|
-
await writeText(
|
|
12813
|
-
created.push(
|
|
13306
|
+
await writeText(path17.join(sddDir, "profiles", "detected.yaml"), yaml2);
|
|
13307
|
+
created.push(path17.join(sddDir, "profiles", "detected.yaml"));
|
|
12814
13308
|
if (detected.best) {
|
|
12815
13309
|
diagnostics.push(diag("ATLAS-INIT-002", "info", `Stack detectado: ${detected.best.displayName} (${detected.best.score} puntos)`, { suggestion: "Revisa .sdd/profiles/detected.yaml" }));
|
|
12816
13310
|
} else {
|
|
12817
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/" }));
|
|
12818
13312
|
}
|
|
12819
13313
|
}
|
|
12820
|
-
await writeText(
|
|
12821
|
-
created.push(
|
|
13314
|
+
await writeText(path17.join(sddDir, "INDEX.md"), initialIndex(language, cfg.project.name));
|
|
13315
|
+
created.push(path17.join(sddDir, "INDEX.md"));
|
|
12822
13316
|
if (opts.local) {
|
|
12823
|
-
const gitignore =
|
|
13317
|
+
const gitignore = path17.join(root, ".gitignore");
|
|
12824
13318
|
const current = await readTextIfExists(gitignore) ?? "";
|
|
12825
13319
|
if (!/(^|\n)\.sdd\/?(\n|$)/.test(current)) {
|
|
12826
13320
|
const next = current.endsWith("\n") || current === "" ? `${current}.sdd/
|
|
@@ -12858,7 +13352,7 @@ async function createChange(opts) {
|
|
|
12858
13352
|
const diagnostics = [];
|
|
12859
13353
|
const slug = opts.slug.trim().toLowerCase();
|
|
12860
13354
|
const language = opts.language ?? opts.cfg?.project.language ?? "es";
|
|
12861
|
-
const dir =
|
|
13355
|
+
const dir = path18.join(path18.resolve(opts.root), ".sdd", "changes", slug);
|
|
12862
13356
|
if (!SLUG_RE.test(slug)) {
|
|
12863
13357
|
return {
|
|
12864
13358
|
slug,
|
|
@@ -12889,20 +13383,20 @@ async function createChange(opts) {
|
|
|
12889
13383
|
await ensureDir(dir);
|
|
12890
13384
|
const templates = templatesFor(language);
|
|
12891
13385
|
const files = [];
|
|
12892
|
-
const metaFile =
|
|
13386
|
+
const metaFile = path18.join(dir, "meta.yaml");
|
|
12893
13387
|
await writeText(metaFile, changeMetaYaml(meta));
|
|
12894
13388
|
files.push(metaFile);
|
|
12895
13389
|
if (lane === "fix") {
|
|
12896
|
-
const fixFile =
|
|
13390
|
+
const fixFile = path18.join(dir, "fix.md");
|
|
12897
13391
|
await writeText(fixFile, renderTemplate(templates.fix, { TITLE: title }));
|
|
12898
13392
|
files.push(fixFile);
|
|
12899
13393
|
return { slug, dir, files, diagnostics };
|
|
12900
13394
|
}
|
|
12901
|
-
const proposalFile =
|
|
13395
|
+
const proposalFile = path18.join(dir, "proposal.md");
|
|
12902
13396
|
await writeText(proposalFile, renderTemplate(templates.proposal, { TITLE: title }));
|
|
12903
13397
|
files.push(proposalFile);
|
|
12904
13398
|
const domainUpper = domain.replace(/[^a-z0-9]/gi, "").toUpperCase() || "GEN";
|
|
12905
|
-
const deltaFile =
|
|
13399
|
+
const deltaFile = path18.join(dir, "spec.md");
|
|
12906
13400
|
await writeText(deltaFile, templates.specDelta({ title, domainUpper, domain }));
|
|
12907
13401
|
files.push(deltaFile);
|
|
12908
13402
|
return { slug, dir, files, diagnostics };
|
|
@@ -13015,7 +13509,7 @@ async function restoreFile(file, previous) {
|
|
|
13015
13509
|
}
|
|
13016
13510
|
}
|
|
13017
13511
|
async function archiveChange(opts) {
|
|
13018
|
-
const root =
|
|
13512
|
+
const root = path19.resolve(opts.root);
|
|
13019
13513
|
const diagnostics = [];
|
|
13020
13514
|
const { config } = await loadWorkspace(root);
|
|
13021
13515
|
const language = opts.language ?? config.project.language;
|
|
@@ -13031,7 +13525,7 @@ async function archiveChange(opts) {
|
|
|
13031
13525
|
return { slug: opts.slug, fold: emptyFold, diagnostics, dryRun: opts.dryRun ?? false };
|
|
13032
13526
|
}
|
|
13033
13527
|
const nowFix = opts.now ?? /* @__PURE__ */ new Date();
|
|
13034
|
-
const targetFix =
|
|
13528
|
+
const targetFix = path19.join(root, ".sdd", "changes", "archive", `${localMonth(nowFix)}-${opts.slug}`);
|
|
13035
13529
|
if (await exists(targetFix)) {
|
|
13036
13530
|
diagnostics.push(diag("ATLAS-ARCH-002", "error", `Ya existe un cambio archivado en ${targetFix}`, { path: targetFix }));
|
|
13037
13531
|
return { slug: opts.slug, fold: emptyFold, diagnostics, dryRun: opts.dryRun ?? false };
|
|
@@ -13039,7 +13533,7 @@ async function archiveChange(opts) {
|
|
|
13039
13533
|
let livingFix;
|
|
13040
13534
|
let livingCreated = false;
|
|
13041
13535
|
if (!opts.dryRun) {
|
|
13042
|
-
const fixRaw = await readTextIfExists(
|
|
13536
|
+
const fixRaw = await readTextIfExists(path19.join(change.dir, "fix.md")) ?? "";
|
|
13043
13537
|
try {
|
|
13044
13538
|
const written = await writeLivingFix(root, {
|
|
13045
13539
|
slug: opts.slug,
|
|
@@ -13055,7 +13549,7 @@ async function archiveChange(opts) {
|
|
|
13055
13549
|
} catch (error) {
|
|
13056
13550
|
diagnostics.push(
|
|
13057
13551
|
diag("ATLAS-ARCH-006", "error", `No se pudo conservar el fix vivo: ${error.message}`, {
|
|
13058
|
-
path:
|
|
13552
|
+
path: path19.join(root, ".sdd", "fixes"),
|
|
13059
13553
|
suggestion: "Revisa los permisos de .sdd/fixes y vuelve a intentar; el fix sigue sin archivar"
|
|
13060
13554
|
})
|
|
13061
13555
|
);
|
|
@@ -13063,11 +13557,11 @@ async function archiveChange(opts) {
|
|
|
13063
13557
|
}
|
|
13064
13558
|
}
|
|
13065
13559
|
if (!opts.dryRun) {
|
|
13066
|
-
await ensureDir(
|
|
13560
|
+
await ensureDir(path19.dirname(targetFix));
|
|
13067
13561
|
try {
|
|
13068
13562
|
await moveDirectory(change.dir, targetFix);
|
|
13069
13563
|
} catch (error) {
|
|
13070
|
-
if (livingCreated && livingFix) await removeFile(
|
|
13564
|
+
if (livingCreated && livingFix) await removeFile(path19.join(root, livingFix));
|
|
13071
13565
|
diagnostics.push(
|
|
13072
13566
|
diag("ATLAS-ARCH-004", "error", `No se pudo mover el cambio al hist\xF3rico: ${error.message}`, {
|
|
13073
13567
|
path: change.dir,
|
|
@@ -13093,10 +13587,10 @@ async function archiveChange(opts) {
|
|
|
13093
13587
|
}
|
|
13094
13588
|
const domain = change.meta.domain;
|
|
13095
13589
|
if (!domain) {
|
|
13096
|
-
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") }));
|
|
13097
13591
|
return { slug: opts.slug, fold: { content: "", applied: { added: [], modified: [], removed: [], renamed: [] }, diagnostics }, diagnostics, dryRun: opts.dryRun ?? false };
|
|
13098
13592
|
}
|
|
13099
|
-
const specFile =
|
|
13593
|
+
const specFile = path19.join(root, ".sdd", "specs", domain, "spec.md");
|
|
13100
13594
|
const existing = await readTextIfExists(specFile);
|
|
13101
13595
|
const base = existing ?? `---
|
|
13102
13596
|
domain: ${domain}
|
|
@@ -13126,8 +13620,8 @@ ${Object.entries(nextFm).map(([k, v]) => `${k}: ${String(v)}`).join("\n")}
|
|
|
13126
13620
|
|
|
13127
13621
|
${body}`;
|
|
13128
13622
|
const month = localMonth(now);
|
|
13129
|
-
const archiveDir =
|
|
13130
|
-
const target =
|
|
13623
|
+
const archiveDir = path19.join(root, ".sdd", "changes", "archive");
|
|
13624
|
+
const target = path19.join(archiveDir, `${month}-${opts.slug}`);
|
|
13131
13625
|
if (await exists(target)) {
|
|
13132
13626
|
diagnostics.push(diag("ATLAS-ARCH-002", "error", `Ya existe un cambio archivado en ${target}`, { path: target }));
|
|
13133
13627
|
return { slug: opts.slug, domain, fold, diagnostics, dryRun: opts.dryRun ?? false };
|
|
@@ -13153,7 +13647,7 @@ ${body}`;
|
|
|
13153
13647
|
}
|
|
13154
13648
|
async function regenerateIndex(root, cfg, now = /* @__PURE__ */ new Date()) {
|
|
13155
13649
|
const { workspace, config } = cfg ? { workspace: (await loadWorkspace(root)).workspace, config: cfg } : await loadWorkspace(root);
|
|
13156
|
-
const archiveDir =
|
|
13650
|
+
const archiveDir = path19.join(root, ".sdd", "changes", "archive");
|
|
13157
13651
|
const archived = await exists(archiveDir) ? (await fs.readdir(archiveDir)).filter((e) => !e.startsWith(".")).length : 0;
|
|
13158
13652
|
const fixes = await loadLivingFixes(root);
|
|
13159
13653
|
const markdown2 = indexMarkdown({
|
|
@@ -13165,7 +13659,7 @@ async function regenerateIndex(root, cfg, now = /* @__PURE__ */ new Date()) {
|
|
|
13165
13659
|
archived
|
|
13166
13660
|
});
|
|
13167
13661
|
void now;
|
|
13168
|
-
await writeText(
|
|
13662
|
+
await writeText(path19.join(root, ".sdd", "INDEX.md"), markdown2);
|
|
13169
13663
|
}
|
|
13170
13664
|
async function removeFile(file) {
|
|
13171
13665
|
try {
|
|
@@ -13184,35 +13678,35 @@ var SCHEMA_MIGRATIONS = [
|
|
|
13184
13678
|
to: SCHEMA_VERSION
|
|
13185
13679
|
}
|
|
13186
13680
|
];
|
|
13187
|
-
var FIXED_ARTIFACTS = ["config.yaml", "approvals.yaml",
|
|
13188
|
-
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")];
|
|
13189
13683
|
async function pushChangeArtifacts(out, sddDir, relDir) {
|
|
13190
13684
|
for (const rel of CHANGE_ARTIFACTS) {
|
|
13191
|
-
const abs =
|
|
13192
|
-
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 });
|
|
13193
13687
|
}
|
|
13194
13688
|
}
|
|
13195
13689
|
async function collectVersionedArtifacts(root) {
|
|
13196
|
-
const sddDir =
|
|
13690
|
+
const sddDir = path20.join(root, ".sdd");
|
|
13197
13691
|
const out = [];
|
|
13198
13692
|
for (const rel of FIXED_ARTIFACTS) {
|
|
13199
|
-
const abs =
|
|
13693
|
+
const abs = path20.join(sddDir, rel);
|
|
13200
13694
|
if (await exists(abs)) out.push({ artifact: toPosix(rel), path: abs });
|
|
13201
13695
|
}
|
|
13202
|
-
const changesDir =
|
|
13696
|
+
const changesDir = path20.join(sddDir, "changes");
|
|
13203
13697
|
for (const slug of await listDirs(changesDir)) {
|
|
13204
13698
|
if (slug === "archive") continue;
|
|
13205
|
-
await pushChangeArtifacts(out, sddDir,
|
|
13699
|
+
await pushChangeArtifacts(out, sddDir, path20.join("changes", slug));
|
|
13206
13700
|
}
|
|
13207
|
-
for (const entry of await listDirs(
|
|
13208
|
-
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));
|
|
13209
13703
|
}
|
|
13210
13704
|
return out;
|
|
13211
13705
|
}
|
|
13212
13706
|
function readArtifactVersion(content) {
|
|
13213
13707
|
let data;
|
|
13214
13708
|
try {
|
|
13215
|
-
data =
|
|
13709
|
+
data = parseYaml10(content);
|
|
13216
13710
|
} catch (err) {
|
|
13217
13711
|
return { state: "unreadable", reason: `YAML inv\xE1lido (${err.message})` };
|
|
13218
13712
|
}
|
|
@@ -13302,22 +13796,22 @@ function backupName(now) {
|
|
|
13302
13796
|
async function applyUpgrade(root, now = /* @__PURE__ */ new Date()) {
|
|
13303
13797
|
const plan = await planUpgrade(root);
|
|
13304
13798
|
if (plan.pending.length === 0) return { status: "up-to-date", plan, applied: [] };
|
|
13305
|
-
const sddDir =
|
|
13306
|
-
const backupRoot =
|
|
13799
|
+
const sddDir = path20.join(root, ".sdd");
|
|
13800
|
+
const backupRoot = path20.join(sddDir, BACKUP_DIRNAME);
|
|
13307
13801
|
const name = backupName(now);
|
|
13308
|
-
const backupDir =
|
|
13309
|
-
const pointerFile =
|
|
13802
|
+
const backupDir = path20.join(backupRoot, name);
|
|
13803
|
+
const pointerFile = path20.join(backupRoot, BACKUP_POINTER);
|
|
13310
13804
|
const previous = [];
|
|
13311
13805
|
try {
|
|
13312
13806
|
for (const item of plan.pending) {
|
|
13313
13807
|
const content = await readTextIfExists(item.path);
|
|
13314
13808
|
if (content === void 0) throw new Error("el elemento desapareci\xF3 mientras se respaldaba");
|
|
13315
13809
|
previous.push({ item, contents: content });
|
|
13316
|
-
await writeText(
|
|
13810
|
+
await writeText(path20.join(backupDir, "files", ...item.artifact.split("/")), content);
|
|
13317
13811
|
}
|
|
13318
13812
|
await writeText(
|
|
13319
|
-
|
|
13320
|
-
|
|
13813
|
+
path20.join(backupDir, "backup.yaml"),
|
|
13814
|
+
stringifyYaml7(
|
|
13321
13815
|
{
|
|
13322
13816
|
schema_version: SCHEMA_VERSION,
|
|
13323
13817
|
created_at: localStamp(now),
|
|
@@ -13357,7 +13851,7 @@ async function applyUpgrade(root, now = /* @__PURE__ */ new Date()) {
|
|
|
13357
13851
|
}
|
|
13358
13852
|
}
|
|
13359
13853
|
for (const dir of await listDirs(backupRoot)) {
|
|
13360
|
-
if (dir !== name) await removeDir(
|
|
13854
|
+
if (dir !== name) await removeDir(path20.join(backupRoot, dir));
|
|
13361
13855
|
}
|
|
13362
13856
|
return {
|
|
13363
13857
|
status: "applied",
|
|
@@ -13366,32 +13860,32 @@ async function applyUpgrade(root, now = /* @__PURE__ */ new Date()) {
|
|
|
13366
13860
|
backup: {
|
|
13367
13861
|
name,
|
|
13368
13862
|
dir: backupDir,
|
|
13369
|
-
relativeDir: toPosix(
|
|
13863
|
+
relativeDir: toPosix(path20.relative(root, backupDir)),
|
|
13370
13864
|
createdAt: localStamp(now),
|
|
13371
13865
|
files: plan.pending.map((i) => i.artifact)
|
|
13372
13866
|
}
|
|
13373
13867
|
};
|
|
13374
13868
|
}
|
|
13375
13869
|
async function rollbackUpgrade(root) {
|
|
13376
|
-
const sddDir =
|
|
13377
|
-
const backupRoot =
|
|
13378
|
-
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);
|
|
13379
13873
|
const name = (await readTextIfExists(pointerFile))?.trim();
|
|
13380
13874
|
if (!name) return { status: "no-backup", restored: [] };
|
|
13381
|
-
const backupDir =
|
|
13382
|
-
const manifestRaw = await readTextIfExists(
|
|
13875
|
+
const backupDir = path20.join(backupRoot, name);
|
|
13876
|
+
const manifestRaw = await readTextIfExists(path20.join(backupDir, "backup.yaml"));
|
|
13383
13877
|
if (manifestRaw === void 0) return { status: "no-backup", restored: [] };
|
|
13384
13878
|
let files = [];
|
|
13385
13879
|
try {
|
|
13386
|
-
const parsed =
|
|
13880
|
+
const parsed = parseYaml10(manifestRaw);
|
|
13387
13881
|
files = (parsed?.files ?? []).filter((f) => typeof f?.artifact === "string");
|
|
13388
13882
|
} catch {
|
|
13389
13883
|
return { status: "no-backup", restored: [] };
|
|
13390
13884
|
}
|
|
13391
13885
|
const restored = [];
|
|
13392
13886
|
for (const file of files) {
|
|
13393
|
-
const from =
|
|
13394
|
-
const to =
|
|
13887
|
+
const from = path20.join(backupDir, "files", ...file.artifact.split("/"));
|
|
13888
|
+
const to = path20.join(sddDir, ...file.artifact.split("/"));
|
|
13395
13889
|
const content = await readTextIfExists(from);
|
|
13396
13890
|
if (content === void 0) continue;
|
|
13397
13891
|
try {
|
|
@@ -13433,6 +13927,84 @@ async function upgradeAdvisory(root) {
|
|
|
13433
13927
|
}
|
|
13434
13928
|
return { plan, diagnostics };
|
|
13435
13929
|
}
|
|
13930
|
+
var DOCS_MARKER_START = "<!-- specatlas:generado:inicio -->";
|
|
13931
|
+
var DOCS_MARKER_END = "<!-- specatlas:generado:fin -->";
|
|
13932
|
+
function docData(change, language, now) {
|
|
13933
|
+
const es = language !== "en";
|
|
13934
|
+
const requirements = [...change.delta?.added ?? [], ...change.delta?.modified ?? []];
|
|
13935
|
+
const evidence = change.verify?.evidence ?? [];
|
|
13936
|
+
const passed = new Set(evidence.filter((entry) => entry.result === "pass").map((entry) => entry.scenario));
|
|
13937
|
+
const requirementsText = requirements.length === 0 ? es ? "_Sin requisitos en el delta._" : "_No requirements in the delta._" : requirements.map((requirement) => {
|
|
13938
|
+
const lines = [`### ${requirement.id} \u2014 ${requirement.title}`, ""];
|
|
13939
|
+
for (const scenario of requirement.scenarios) lines.push(`- \`${scenario.id}\` \u2014 ${scenario.title}`);
|
|
13940
|
+
return lines.join("\n");
|
|
13941
|
+
}).join("\n\n");
|
|
13942
|
+
const scenarioList = requirements.flatMap((requirement) => requirement.scenarios);
|
|
13943
|
+
const scenariosText = scenarioList.length === 0 ? es ? "_Sin escenarios._" : "_No scenarios._" : scenarioList.map((scenario) => `- \`${scenario.id}\` \u2014 ${scenario.title}`).join("\n");
|
|
13944
|
+
const evidenceText = evidence.length === 0 ? es ? "_Sin evidencia registrada._" : "_No evidence recorded._" : evidence.map((entry) => `- \`${entry.scenario}\` \u2014 ${entry.method} \xB7 ${entry.result}${entry.date ? ` \xB7 ${entry.date}` : ""}`).join("\n");
|
|
13945
|
+
const pending = scenarioList.filter((scenario) => !passed.has(scenario.id));
|
|
13946
|
+
const pendingText = pending.length === 0 ? es ? "_Nada pendiente: todos los escenarios tienen evidencia en pass._" : "_Nothing pending: every scenario has passing evidence._" : pending.map((scenario) => `- \`${scenario.id}\` \u2014 ${scenario.title}`).join("\n");
|
|
13947
|
+
return {
|
|
13948
|
+
TITLE: change.meta?.title ?? change.slug,
|
|
13949
|
+
SLUG: change.slug,
|
|
13950
|
+
DOMAIN: change.meta?.domain ?? "\u2014",
|
|
13951
|
+
LANE: change.meta?.lane ?? "standard",
|
|
13952
|
+
DATE: localDate(now),
|
|
13953
|
+
TASKS: change.tasks ? `${change.tasks.counts.done}/${change.tasks.counts.total}` : es ? "sin tareas" : "no tasks",
|
|
13954
|
+
REQUIREMENTS: requirementsText,
|
|
13955
|
+
SCENARIOS: scenariosText,
|
|
13956
|
+
EVIDENCE: evidenceText,
|
|
13957
|
+
PENDING: pendingText
|
|
13958
|
+
};
|
|
13959
|
+
}
|
|
13960
|
+
function mergeManaged(existing, block, language) {
|
|
13961
|
+
const managed = `${DOCS_MARKER_START}
|
|
13962
|
+
${block.trimEnd()}
|
|
13963
|
+
${DOCS_MARKER_END}`;
|
|
13964
|
+
if (existing === void 0) {
|
|
13965
|
+
const notes = language === "en" ? "## Notes\n\n(Write here whatever you want to keep across regenerations.)" : "## Notas\n\n(Escribe aqu\xED lo que quieras conservar entre regeneraciones.)";
|
|
13966
|
+
return `${managed}
|
|
13967
|
+
|
|
13968
|
+
${notes}
|
|
13969
|
+
`;
|
|
13970
|
+
}
|
|
13971
|
+
const start = existing.indexOf(DOCS_MARKER_START);
|
|
13972
|
+
const end = existing.indexOf(DOCS_MARKER_END);
|
|
13973
|
+
if (start >= 0 && end > start) {
|
|
13974
|
+
const before = existing.slice(0, start);
|
|
13975
|
+
const after = existing.slice(end + DOCS_MARKER_END.length);
|
|
13976
|
+
return `${before}${managed}${after}`;
|
|
13977
|
+
}
|
|
13978
|
+
return `${managed}
|
|
13979
|
+
|
|
13980
|
+
${existing}`;
|
|
13981
|
+
}
|
|
13982
|
+
async function generateDocs(opts) {
|
|
13983
|
+
const root = path21.resolve(opts.root);
|
|
13984
|
+
const { config } = await loadWorkspace(root);
|
|
13985
|
+
const change = await loadChange(root, opts.slug);
|
|
13986
|
+
if (!change.meta) {
|
|
13987
|
+
return {
|
|
13988
|
+
slug: opts.slug,
|
|
13989
|
+
files: [],
|
|
13990
|
+
diagnostics: [diag("ATLAS-DOCS-002", "error", `No existe el cambio "${opts.slug}"`, { suggestion: "Comprueba el nombre del cambio" })]
|
|
13991
|
+
};
|
|
13992
|
+
}
|
|
13993
|
+
const language = config.project.language;
|
|
13994
|
+
const tipo = opts.tipo ?? "all";
|
|
13995
|
+
const tipos = tipo === "all" ? ["tecnica", "manual"] : [tipo];
|
|
13996
|
+
const templates = templatesFor(language);
|
|
13997
|
+
const data = docData(change, language, opts.now ?? /* @__PURE__ */ new Date());
|
|
13998
|
+
const files = [];
|
|
13999
|
+
for (const current of tipos) {
|
|
14000
|
+
const file = path21.join(change.dir, "docs", `${current}.md`);
|
|
14001
|
+
const block = renderTemplate(current === "tecnica" ? templates.docTecnica : templates.docManual, data);
|
|
14002
|
+
const existing = await readTextIfExists(file);
|
|
14003
|
+
await writeText(file, mergeManaged(existing, block, language));
|
|
14004
|
+
files.push({ tipo: current, path: file, created: existing === void 0 });
|
|
14005
|
+
}
|
|
14006
|
+
return { slug: opts.slug, files, diagnostics: [] };
|
|
14007
|
+
}
|
|
13436
14008
|
var SARIF_SCHEMA = "https://json.schemastore.org/sarif-2.1.0.json";
|
|
13437
14009
|
var SARIF_VERSION = "2.1.0";
|
|
13438
14010
|
var TOOL_NAME = "SpecAtlas";
|
|
@@ -13470,7 +14042,7 @@ function resultOf(diagnostic, root) {
|
|
|
13470
14042
|
message: { text: diagnostic.message }
|
|
13471
14043
|
};
|
|
13472
14044
|
if (diagnostic.path) {
|
|
13473
|
-
const rel =
|
|
14045
|
+
const rel = path22.relative(root, diagnostic.path);
|
|
13474
14046
|
if (rel !== "") {
|
|
13475
14047
|
const physicalLocation = {
|
|
13476
14048
|
artifactLocation: { uri: toPosix(rel) },
|
|
@@ -13525,7 +14097,7 @@ async function runDoctor(root) {
|
|
|
13525
14097
|
const approvals = await loadApprovals(workspace.sddDir);
|
|
13526
14098
|
findings.push(...approvals.diagnostics);
|
|
13527
14099
|
for (const change of workspace.changes) {
|
|
13528
|
-
const deltaPath =
|
|
14100
|
+
const deltaPath = path23.join(change.dir, "spec.md");
|
|
13529
14101
|
const deltaContent = await readTextIfExists(deltaPath);
|
|
13530
14102
|
const approval = verifyApproval(change, approvals.byArtifact, config, deltaContent ?? void 0);
|
|
13531
14103
|
if ((change.planPath || change.tasks) && (approval.status === "missing" || approval.status === "stale")) {
|
|
@@ -13547,7 +14119,7 @@ async function runDoctor(root) {
|
|
|
13547
14119
|
}
|
|
13548
14120
|
for (const override of change.meta?.overrides ?? []) {
|
|
13549
14121
|
if (!override.reason.trim() || !override.by.trim()) {
|
|
13550
|
-
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") }));
|
|
13551
14123
|
}
|
|
13552
14124
|
}
|
|
13553
14125
|
}
|
|
@@ -13579,7 +14151,7 @@ function livingRequirementsMap(specs) {
|
|
|
13579
14151
|
return map;
|
|
13580
14152
|
}
|
|
13581
14153
|
async function runCiGate(opts) {
|
|
13582
|
-
const root =
|
|
14154
|
+
const root = path24.resolve(opts.root);
|
|
13583
14155
|
const { workspace, config } = await loadWorkspace(root);
|
|
13584
14156
|
const diagnostics = [];
|
|
13585
14157
|
const checks = [];
|
|
@@ -13590,11 +14162,12 @@ async function runCiGate(opts) {
|
|
|
13590
14162
|
let changesErrors = 0;
|
|
13591
14163
|
let changesWarnings = 0;
|
|
13592
14164
|
for (const change of workspace.changes) {
|
|
13593
|
-
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 }) : [];
|
|
13594
14166
|
const trace = checkTrace({
|
|
13595
14167
|
specs: workspace.specs,
|
|
13596
14168
|
change,
|
|
13597
|
-
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)
|
|
13598
14171
|
});
|
|
13599
14172
|
const changeDiags = [...lintFindings, ...trace.findings];
|
|
13600
14173
|
if (change.planPath) {
|
|
@@ -13699,6 +14272,10 @@ var CATALOG = [
|
|
|
13699
14272
|
{ name: "trace", description: "Verifica la trazabilidad requisito \u2192 escenario \u2192 tarea \u2192 evidencia", flags: ["change", "require-evidence", "json"], usage: "satlas trace [--change <slug>] [--require-evidence] [--json]" },
|
|
13700
14273
|
{ name: "waves", description: "Calcula las olas paralelas de construcci\xF3n", flags: ["change", "max-parallel", "json"], usage: "satlas waves [--change <slug>] [--max-parallel N] [--json]" },
|
|
13701
14274
|
{ name: "doctor", description: "Diagn\xF3stico de salud del workspace", flags: ["json"], usage: "satlas doctor [--json]" },
|
|
14275
|
+
{ name: "clarify", description: "Preguntas abiertas y aclaraciones del cambio (informe; aclarar es fase del agente)", flags: ["json"], usage: "satlas clarify <slug>" },
|
|
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>" },
|
|
13702
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]" },
|
|
13703
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' },
|
|
13704
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>" },
|
|
@@ -13750,6 +14327,15 @@ var ES2 = {
|
|
|
13750
14327
|
"new.done": "Cambio creado. Siguiente: escribe la spec funcional.",
|
|
13751
14328
|
"upgrade.title": "Actualizar el estado del proyecto",
|
|
13752
14329
|
"upgrade.preview": "Vista previa",
|
|
14330
|
+
"clarify.title": "Aclaraciones del cambio",
|
|
14331
|
+
"clarify.mode": "modo",
|
|
14332
|
+
"clarify.open": "preguntas abiertas",
|
|
14333
|
+
"clarify.resolved": "aclaradas",
|
|
14334
|
+
"docs.title": "Documentaci\xF3n del cambio",
|
|
14335
|
+
"docs.created": "creado",
|
|
14336
|
+
"docs.updated": "actualizado",
|
|
14337
|
+
"contracts.title": "Contratos del cambio",
|
|
14338
|
+
"link.title": "Enlaces entre proyectos",
|
|
13753
14339
|
"summary.findings": "hallazgos",
|
|
13754
14340
|
"label.errors": "errores",
|
|
13755
14341
|
"label.warnings": "avisos",
|
|
@@ -13785,6 +14371,15 @@ var EN2 = {
|
|
|
13785
14371
|
"new.done": "Change created. Next: write the business spec.",
|
|
13786
14372
|
"upgrade.title": "Upgrade project state",
|
|
13787
14373
|
"upgrade.preview": "Preview",
|
|
14374
|
+
"clarify.title": "Change clarifications",
|
|
14375
|
+
"clarify.mode": "mode",
|
|
14376
|
+
"clarify.open": "open questions",
|
|
14377
|
+
"clarify.resolved": "clarified",
|
|
14378
|
+
"docs.title": "Change documentation",
|
|
14379
|
+
"docs.created": "created",
|
|
14380
|
+
"docs.updated": "updated",
|
|
14381
|
+
"contracts.title": "Change contracts",
|
|
14382
|
+
"link.title": "Project links",
|
|
13788
14383
|
"summary.findings": "findings",
|
|
13789
14384
|
"label.errors": "errors",
|
|
13790
14385
|
"label.warnings": "warnings",
|
|
@@ -13812,26 +14407,26 @@ function cliVersion() {
|
|
|
13812
14407
|
}
|
|
13813
14408
|
|
|
13814
14409
|
// src/commands/adapters.ts
|
|
13815
|
-
import
|
|
14410
|
+
import path28 from "path";
|
|
13816
14411
|
|
|
13817
14412
|
// ../adapters/dist/index.js
|
|
13818
|
-
import
|
|
13819
|
-
import { stringify as
|
|
13820
|
-
import
|
|
14413
|
+
import path25 from "path";
|
|
14414
|
+
import { stringify as stringifyYaml8 } from "yaml";
|
|
14415
|
+
import path26 from "path";
|
|
13821
14416
|
async function loadWorkflow(workflowDir) {
|
|
13822
|
-
const phasesDir =
|
|
13823
|
-
const snippetsDir =
|
|
14417
|
+
const phasesDir = path25.join(workflowDir, "phases");
|
|
14418
|
+
const snippetsDir = path25.join(workflowDir, "snippets");
|
|
13824
14419
|
const phases = [];
|
|
13825
14420
|
const snippets = /* @__PURE__ */ new Map();
|
|
13826
14421
|
const hashParts = [];
|
|
13827
14422
|
for (const entry of (await listDir(phasesDir)).sort()) {
|
|
13828
14423
|
if (!entry.endsWith(".md")) continue;
|
|
13829
|
-
const filePath =
|
|
14424
|
+
const filePath = path25.join(phasesDir, entry);
|
|
13830
14425
|
const raw = await readText(filePath);
|
|
13831
14426
|
hashParts.push(raw);
|
|
13832
14427
|
const fm = parseFrontmatter(raw, filePath);
|
|
13833
14428
|
const data = fm.data;
|
|
13834
|
-
const id = typeof data["id"] === "string" ? data["id"] :
|
|
14429
|
+
const id = typeof data["id"] === "string" ? data["id"] : path25.basename(entry, ".md");
|
|
13835
14430
|
phases.push({
|
|
13836
14431
|
id,
|
|
13837
14432
|
title: typeof data["title"] === "string" ? data["title"] : id,
|
|
@@ -13845,9 +14440,9 @@ async function loadWorkflow(workflowDir) {
|
|
|
13845
14440
|
}
|
|
13846
14441
|
for (const entry of (await listDir(snippetsDir)).sort()) {
|
|
13847
14442
|
if (!entry.endsWith(".md")) continue;
|
|
13848
|
-
const raw = await readTextIfExists(
|
|
14443
|
+
const raw = await readTextIfExists(path25.join(snippetsDir, entry)) ?? "";
|
|
13849
14444
|
hashParts.push(raw);
|
|
13850
|
-
snippets.set(
|
|
14445
|
+
snippets.set(path25.basename(entry, ".md"), raw.trim());
|
|
13851
14446
|
}
|
|
13852
14447
|
return { phases: phases.sort((a, b) => a.id.localeCompare(b.id)), snippets, sourceHash: sha256(hashParts.join("\n---\n")) };
|
|
13853
14448
|
}
|
|
@@ -13871,7 +14466,7 @@ function vars(language, slugToken, commandPrefix) {
|
|
|
13871
14466
|
return { SLUG: slugToken, LANGUAGE: language, LANGUAGE_NAME: LANGUAGE_NAMES[language], SDD_DIR: ".sdd", COMMAND_PREFIX: commandPrefix };
|
|
13872
14467
|
}
|
|
13873
14468
|
function frontmatter(data, body) {
|
|
13874
|
-
const yaml2 =
|
|
14469
|
+
const yaml2 = stringifyYaml8(data, { lineWidth: 0 }).trimEnd();
|
|
13875
14470
|
return `---
|
|
13876
14471
|
${yaml2}
|
|
13877
14472
|
---
|
|
@@ -14029,11 +14624,11 @@ Reglas: la spec es funcional y de negocio (sin tecnolog\xEDa); la trazabilidad e
|
|
|
14029
14624
|
function tomlString(value) {
|
|
14030
14625
|
return `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
|
|
14031
14626
|
}
|
|
14032
|
-
var MANIFEST_REL =
|
|
14627
|
+
var MANIFEST_REL = path26.join(".sdd", ".generated", "manifest.json");
|
|
14033
14628
|
var BEGIN = "<!-- BEGIN specatlas -->";
|
|
14034
14629
|
var END = "<!-- END specatlas -->";
|
|
14035
14630
|
async function compileTargets(opts) {
|
|
14036
|
-
const root =
|
|
14631
|
+
const root = path26.resolve(opts.root);
|
|
14037
14632
|
const language = opts.language ?? "es";
|
|
14038
14633
|
const diagnostics = [];
|
|
14039
14634
|
const sources = await loadWorkflow(opts.workflowDir);
|
|
@@ -14041,7 +14636,7 @@ async function compileTargets(opts) {
|
|
|
14041
14636
|
diagnostics.push({
|
|
14042
14637
|
code: "ATLAS-ADAPTERS-002",
|
|
14043
14638
|
severity: "error",
|
|
14044
|
-
message: `No hay fases en ${
|
|
14639
|
+
message: `No hay fases en ${path26.join(opts.workflowDir, "phases")}`,
|
|
14045
14640
|
suggestion: "Revisa la carpeta workflow/phases del proyecto"
|
|
14046
14641
|
});
|
|
14047
14642
|
}
|
|
@@ -14053,7 +14648,7 @@ async function compileTargets(opts) {
|
|
|
14053
14648
|
}
|
|
14054
14649
|
}
|
|
14055
14650
|
const compiled = [...deduped.values()];
|
|
14056
|
-
const manifestPath =
|
|
14651
|
+
const manifestPath = path26.join(root, MANIFEST_REL);
|
|
14057
14652
|
const previous = await readManifest(manifestPath);
|
|
14058
14653
|
const previousHashes = /* @__PURE__ */ new Map();
|
|
14059
14654
|
for (const files2 of Object.values(previous?.targets ?? {})) {
|
|
@@ -14064,7 +14659,7 @@ async function compileTargets(opts) {
|
|
|
14064
14659
|
const stale = [];
|
|
14065
14660
|
const missing = [];
|
|
14066
14661
|
for (const file of compiled) {
|
|
14067
|
-
const abs =
|
|
14662
|
+
const abs = path26.join(root, file.path);
|
|
14068
14663
|
const finalContent = file.path === "AGENTS.md" ? mergeAgentsBlock(await readTextIfExists(abs) ?? "", file.content) : file.content;
|
|
14069
14664
|
const hash = artifactHash(finalContent);
|
|
14070
14665
|
const existing = await readTextIfExists(abs);
|
|
@@ -14080,7 +14675,7 @@ async function compileTargets(opts) {
|
|
|
14080
14675
|
stale.push(file.path);
|
|
14081
14676
|
}
|
|
14082
14677
|
if (!opts.check && status !== "unchanged") {
|
|
14083
|
-
await ensureDir(
|
|
14678
|
+
await ensureDir(path26.dirname(abs));
|
|
14084
14679
|
await writeText(abs, finalContent);
|
|
14085
14680
|
written.push(file.path);
|
|
14086
14681
|
}
|
|
@@ -14134,7 +14729,7 @@ async function readManifest(manifestPath) {
|
|
|
14134
14729
|
}
|
|
14135
14730
|
}
|
|
14136
14731
|
async function checkAdapters(opts) {
|
|
14137
|
-
const manifestPath =
|
|
14732
|
+
const manifestPath = path26.join(path26.resolve(opts.root), MANIFEST_REL);
|
|
14138
14733
|
const manifestRaw = await readTextIfExists(manifestPath);
|
|
14139
14734
|
if (manifestRaw === void 0) return { ok: false, manifest: false, stale: [], missing: [], orphaned: [] };
|
|
14140
14735
|
const manifest = await readManifest(manifestPath);
|
|
@@ -14151,7 +14746,7 @@ async function checkAdapters(opts) {
|
|
|
14151
14746
|
}
|
|
14152
14747
|
|
|
14153
14748
|
// src/paths.ts
|
|
14154
|
-
import
|
|
14749
|
+
import path27 from "path";
|
|
14155
14750
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
14156
14751
|
async function resolveProfilesDir() {
|
|
14157
14752
|
const env = process.env["SPECATLAS_PROFILES_DIR"];
|
|
@@ -14161,16 +14756,16 @@ async function resolveProfilesDir() {
|
|
|
14161
14756
|
async function resolveWorkflowDir() {
|
|
14162
14757
|
const env = process.env["SPECATLAS_WORKFLOW_DIR"];
|
|
14163
14758
|
if (env && await exists(env)) return env;
|
|
14164
|
-
return walkUpFor(
|
|
14759
|
+
return walkUpFor(path27.join("workflow", "phases"));
|
|
14165
14760
|
}
|
|
14166
14761
|
async function walkUpFor(relative) {
|
|
14167
|
-
let dir =
|
|
14762
|
+
let dir = path27.dirname(fileURLToPath2(import.meta.url));
|
|
14168
14763
|
for (let i = 0; i < 8; i += 1) {
|
|
14169
|
-
const candidate =
|
|
14764
|
+
const candidate = path27.join(dir, relative);
|
|
14170
14765
|
if (await exists(candidate)) {
|
|
14171
|
-
return relative.includes(
|
|
14766
|
+
return relative.includes(path27.sep) ? path27.dirname(candidate) : candidate;
|
|
14172
14767
|
}
|
|
14173
|
-
const parent =
|
|
14768
|
+
const parent = path27.dirname(dir);
|
|
14174
14769
|
if (parent === dir) break;
|
|
14175
14770
|
dir = parent;
|
|
14176
14771
|
}
|
|
@@ -14248,7 +14843,7 @@ async function runAdapters(ctx) {
|
|
|
14248
14843
|
for (const [target, count2] of byTarget) lines.push(` ${target}: ${count2} archivo(s)`);
|
|
14249
14844
|
if (report.written.length > 0) {
|
|
14250
14845
|
lines.push("");
|
|
14251
|
-
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))}`);
|
|
14252
14847
|
} else {
|
|
14253
14848
|
lines.push("");
|
|
14254
14849
|
lines.push("Sin cambios: los adaptadores ya estaban al d\xEDa.");
|
|
@@ -14259,7 +14854,7 @@ async function runAdapters(ctx) {
|
|
|
14259
14854
|
}
|
|
14260
14855
|
|
|
14261
14856
|
// src/commands/adopt.ts
|
|
14262
|
-
import
|
|
14857
|
+
import path29 from "path";
|
|
14263
14858
|
async function runAdopt(ctx) {
|
|
14264
14859
|
const { root } = await requireWorkspace(ctx);
|
|
14265
14860
|
const domainsFlag = flagString(ctx.flags, "domains") ?? flagString(ctx.flags, "domain");
|
|
@@ -14279,11 +14874,11 @@ async function runAdopt(ctx) {
|
|
|
14279
14874
|
}
|
|
14280
14875
|
if (result.createdSpecs.length > 0) {
|
|
14281
14876
|
lines.push("");
|
|
14282
|
-
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)}`);
|
|
14283
14878
|
}
|
|
14284
14879
|
if (result.reportPath && !result.dryRun) {
|
|
14285
14880
|
lines.push("");
|
|
14286
|
-
lines.push(` Informe: ${
|
|
14881
|
+
lines.push(` Informe: ${path29.relative(ctx.cwd, result.reportPath)}`);
|
|
14287
14882
|
}
|
|
14288
14883
|
lines.push("");
|
|
14289
14884
|
lines.push("Siguiente: fase /satlas-adopt por dominio (requisitos AS-IS) y luego:");
|
|
@@ -14296,8 +14891,8 @@ async function runAdopt(ctx) {
|
|
|
14296
14891
|
diagnostics: result.diagnostics,
|
|
14297
14892
|
data: {
|
|
14298
14893
|
domains: result.domains.map((d) => ({ name: d.name, files: d.files, existingSpec: d.existingSpec })),
|
|
14299
|
-
createdSpecs: result.createdSpecs.map((s) =>
|
|
14300
|
-
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,
|
|
14301
14896
|
stack: result.stack,
|
|
14302
14897
|
dryRun: result.dryRun
|
|
14303
14898
|
},
|
|
@@ -14306,7 +14901,7 @@ async function runAdopt(ctx) {
|
|
|
14306
14901
|
}
|
|
14307
14902
|
|
|
14308
14903
|
// src/commands/analyze.ts
|
|
14309
|
-
import
|
|
14904
|
+
import path30 from "path";
|
|
14310
14905
|
async function runAnalyzeCommand(ctx) {
|
|
14311
14906
|
const { root } = await requireWorkspace(ctx);
|
|
14312
14907
|
const slug = ctx.positionals[0];
|
|
@@ -14317,23 +14912,23 @@ async function runAnalyzeCommand(ctx) {
|
|
|
14317
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`];
|
|
14318
14913
|
if (result.waves) lines.push(` olas: ${result.waves.blocks} bloque(s), ${result.waves.waves} ola(s), ${result.waves.tasks} tarea(s)`);
|
|
14319
14914
|
if (result.mockups) lines.push(` mockups: ${result.mockups.screens} pantalla(s)${result.mockups.stale ? " (desactualizados)" : ""}`);
|
|
14320
|
-
if (result.path) lines.push("", ` informe: ${
|
|
14915
|
+
if (result.path) lines.push("", ` informe: ${path30.relative(ctx.cwd, result.path)}`);
|
|
14321
14916
|
for (const finding of result.findings) {
|
|
14322
14917
|
lines.push(` ${finding.severity.toUpperCase()} ${finding.code} \u2014 ${finding.message}`);
|
|
14323
14918
|
}
|
|
14324
14919
|
return {
|
|
14325
14920
|
exitCode: result.status === "blocked" ? 1 : 0,
|
|
14326
14921
|
diagnostics: result.findings,
|
|
14327
|
-
data: result.path ? { ...result, path:
|
|
14922
|
+
data: result.path ? { ...result, path: path30.relative(ctx.cwd, result.path) } : result,
|
|
14328
14923
|
text: lines
|
|
14329
14924
|
};
|
|
14330
14925
|
}
|
|
14331
14926
|
|
|
14332
14927
|
// src/commands/approve.ts
|
|
14333
|
-
import
|
|
14928
|
+
import path32 from "path";
|
|
14334
14929
|
|
|
14335
14930
|
// src/commands/issue.ts
|
|
14336
|
-
import
|
|
14931
|
+
import path31 from "path";
|
|
14337
14932
|
async function runIssue(ctx) {
|
|
14338
14933
|
const action = ctx.positionals[0] ?? "status";
|
|
14339
14934
|
const slug = ctx.positionals[1];
|
|
@@ -14361,7 +14956,7 @@ async function runIssue(ctx) {
|
|
|
14361
14956
|
return { exitCode: 2, diagnostics: [{ code: "ATLAS-ISSUE-000", severity: "error", message: "Falta el slug: satlas issue status <slug>" }] };
|
|
14362
14957
|
}
|
|
14363
14958
|
const change = await loadChange(root, slug);
|
|
14364
|
-
const { config } = await loadConfig(
|
|
14959
|
+
const { config } = await loadConfig(path31.join(root, ".sdd"));
|
|
14365
14960
|
const tracker = change.meta?.tracker;
|
|
14366
14961
|
const lines = [`Issue de ${slug}`, ""];
|
|
14367
14962
|
if (!tracker || tracker.provider !== "github") {
|
|
@@ -14432,7 +15027,7 @@ async function runApprove(ctx) {
|
|
|
14432
15027
|
};
|
|
14433
15028
|
}
|
|
14434
15029
|
const { root, workspace, config } = await requireWorkspace(ctx);
|
|
14435
|
-
const artifact = target.includes("/") || target.includes("\\") ? target :
|
|
15030
|
+
const artifact = target.includes("/") || target.includes("\\") ? target : path32.join("changes", target, "spec.md");
|
|
14436
15031
|
const channelFlag = flagString(ctx.flags, "channel");
|
|
14437
15032
|
const channel = channelFlag === "presentation" || channelFlag === "editor" || channelFlag === "pr" || channelFlag === "tracker" ? channelFlag : "cli";
|
|
14438
15033
|
const change = workspace.changes.find(
|
|
@@ -14473,13 +15068,13 @@ async function runApprove(ctx) {
|
|
|
14473
15068
|
return {
|
|
14474
15069
|
exitCode: hasErrors2 ? 1 : 0,
|
|
14475
15070
|
diagnostics: result.diagnostics,
|
|
14476
|
-
data: result.approval ? { ...result.approval, file:
|
|
15071
|
+
data: result.approval ? { ...result.approval, file: path32.relative(ctx.cwd, result.file) } : void 0,
|
|
14477
15072
|
text: lines
|
|
14478
15073
|
};
|
|
14479
15074
|
}
|
|
14480
15075
|
|
|
14481
15076
|
// src/commands/archive.ts
|
|
14482
|
-
import
|
|
15077
|
+
import path33 from "path";
|
|
14483
15078
|
async function runArchive(ctx) {
|
|
14484
15079
|
const slug = ctx.positionals[0];
|
|
14485
15080
|
if (!slug) {
|
|
@@ -14516,7 +15111,7 @@ async function runArchive(ctx) {
|
|
|
14516
15111
|
} else {
|
|
14517
15112
|
lines.push("El delta no contiene operaciones (ADDED/MODIFIED/REMOVED/RENAMED).");
|
|
14518
15113
|
}
|
|
14519
|
-
if (result.archivedTo) lines.push(` archivado en: ${
|
|
15114
|
+
if (result.archivedTo) lines.push(` archivado en: ${path33.relative(ctx.cwd, result.archivedTo)}`);
|
|
14520
15115
|
const hasErrors2 = result.diagnostics.some((d) => d.severity === "error");
|
|
14521
15116
|
return {
|
|
14522
15117
|
exitCode: hasErrors2 ? 1 : 0,
|
|
@@ -14527,7 +15122,7 @@ async function runArchive(ctx) {
|
|
|
14527
15122
|
}
|
|
14528
15123
|
|
|
14529
15124
|
// src/commands/ci.ts
|
|
14530
|
-
import
|
|
15125
|
+
import path34 from "path";
|
|
14531
15126
|
async function runCi(ctx) {
|
|
14532
15127
|
const { root, config } = await requireWorkspace(ctx);
|
|
14533
15128
|
const strict = flagBool(ctx.flags, "strict");
|
|
@@ -14553,10 +15148,10 @@ async function runCi(ctx) {
|
|
|
14553
15148
|
const lines = ["CI de SpecAtlas", ""];
|
|
14554
15149
|
let sarifWritten;
|
|
14555
15150
|
if (sarifPath) {
|
|
14556
|
-
const target =
|
|
15151
|
+
const target = path34.resolve(ctx.cwd, sarifPath);
|
|
14557
15152
|
try {
|
|
14558
15153
|
await writeText(target, toSarifText({ diagnostics: gate.diagnostics, root, version: cliVersion(), failed: gate.failed }));
|
|
14559
|
-
sarifWritten =
|
|
15154
|
+
sarifWritten = path34.relative(ctx.cwd, target);
|
|
14560
15155
|
} catch (err) {
|
|
14561
15156
|
diagnostics.push({
|
|
14562
15157
|
code: "ATLAS-CI-SARIF-001",
|
|
@@ -14590,13 +15185,193 @@ async function runCi(ctx) {
|
|
|
14590
15185
|
};
|
|
14591
15186
|
}
|
|
14592
15187
|
|
|
15188
|
+
// src/commands/clarify.ts
|
|
15189
|
+
async function runClarify(ctx) {
|
|
15190
|
+
const slug = ctx.positionals[0];
|
|
15191
|
+
if (!slug) {
|
|
15192
|
+
return { exitCode: 2, diagnostics: [{ code: "ATLAS-CLARIFY-000", severity: "error", message: "Falta el slug: satlas clarify <slug>" }] };
|
|
15193
|
+
}
|
|
15194
|
+
const { workspace, config } = await requireWorkspace(ctx);
|
|
15195
|
+
const change = workspace.changes.find((c) => c.slug === slug);
|
|
15196
|
+
if (!change) {
|
|
15197
|
+
return { exitCode: 2, diagnostics: [{ code: "ATLAS-CLARIFY-000", severity: "error", message: `No existe el cambio "${slug}"` }] };
|
|
15198
|
+
}
|
|
15199
|
+
const open = change.clarify?.open ?? [];
|
|
15200
|
+
const resolved = change.clarify?.resolved ?? [];
|
|
15201
|
+
const mode = config.gates.clarify.mode;
|
|
15202
|
+
const lines = [msg("clarify.title", ctx.language), ""];
|
|
15203
|
+
lines.push(` ${msg("clarify.mode", ctx.language)}: ${mode}`);
|
|
15204
|
+
lines.push(` ${msg("clarify.open", ctx.language)}: ${open.length}`);
|
|
15205
|
+
for (const item of open) lines.push(` [ ] ${item.text}`);
|
|
15206
|
+
lines.push(` ${msg("clarify.resolved", ctx.language)}: ${resolved.length}`);
|
|
15207
|
+
for (const item of resolved) lines.push(` [x] ${item.text}${item.answer ? ` \u2014 ${item.answer}` : ""}`);
|
|
15208
|
+
if (open.length > 0) {
|
|
15209
|
+
lines.push("");
|
|
15210
|
+
lines.push(`Aclara con la fase del agente: /satlas.clarify ${slug}`);
|
|
15211
|
+
} else if (resolved.length === 0) {
|
|
15212
|
+
lines.push("");
|
|
15213
|
+
lines.push("El cambio no tiene preguntas abiertas ni aclaraciones registradas.");
|
|
15214
|
+
}
|
|
15215
|
+
return {
|
|
15216
|
+
exitCode: 0,
|
|
15217
|
+
diagnostics: [],
|
|
15218
|
+
data: {
|
|
15219
|
+
slug,
|
|
15220
|
+
mode,
|
|
15221
|
+
open: open.map((item) => ({ text: item.text, line: item.line })),
|
|
15222
|
+
resolved: resolved.map((item) => ({ text: item.text, line: item.line, answer: item.answer })),
|
|
15223
|
+
...open.length > 0 ? { action: `/satlas.clarify ${slug}` } : {}
|
|
15224
|
+
},
|
|
15225
|
+
text: lines
|
|
15226
|
+
};
|
|
15227
|
+
}
|
|
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
|
+
|
|
15273
|
+
// src/commands/docs.ts
|
|
15274
|
+
import path36 from "path";
|
|
15275
|
+
async function runDocs(ctx) {
|
|
15276
|
+
const slug = ctx.positionals[0];
|
|
15277
|
+
if (!slug) {
|
|
15278
|
+
return { exitCode: 2, diagnostics: [{ code: "ATLAS-DOCS-000", severity: "error", message: "Falta el slug: satlas docs <slug> [--tipo tecnica|manual|all]" }] };
|
|
15279
|
+
}
|
|
15280
|
+
const { root } = await requireWorkspace(ctx);
|
|
15281
|
+
const tipoFlag = flagString(ctx.flags, "tipo");
|
|
15282
|
+
const tipo = tipoFlag === "tecnica" || tipoFlag === "manual" || tipoFlag === "all" ? tipoFlag : "all";
|
|
15283
|
+
const result = await generateDocs({ root, slug, tipo });
|
|
15284
|
+
const errors = result.diagnostics.filter((d) => d.severity === "error");
|
|
15285
|
+
const lines = [msg("docs.title", ctx.language), ""];
|
|
15286
|
+
for (const file of result.files) {
|
|
15287
|
+
lines.push(` ${file.created ? msg("docs.created", ctx.language) : msg("docs.updated", ctx.language)}: ${path36.relative(ctx.cwd, file.path)}`);
|
|
15288
|
+
}
|
|
15289
|
+
for (const error of errors) lines.push(` ERROR ${error.code} \u2014 ${error.message}`);
|
|
15290
|
+
return {
|
|
15291
|
+
exitCode: errors.length > 0 ? 2 : 0,
|
|
15292
|
+
diagnostics: result.diagnostics,
|
|
15293
|
+
data: {
|
|
15294
|
+
slug: result.slug,
|
|
15295
|
+
tipo,
|
|
15296
|
+
files: result.files.map((file) => ({ tipo: file.tipo, path: path36.relative(ctx.cwd, file.path), created: file.created }))
|
|
15297
|
+
},
|
|
15298
|
+
text: lines
|
|
15299
|
+
};
|
|
15300
|
+
}
|
|
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
|
+
|
|
14593
15368
|
// src/commands/profile.ts
|
|
14594
|
-
import
|
|
15369
|
+
import path38 from "path";
|
|
14595
15370
|
async function runProfile(ctx) {
|
|
14596
15371
|
const action = ctx.positionals[0] ?? "detect";
|
|
14597
15372
|
const { root, workspace } = await requireWorkspace(ctx);
|
|
14598
15373
|
const officialDir = await resolveProfilesDir();
|
|
14599
|
-
const customDir =
|
|
15374
|
+
const customDir = path38.join(workspace.sddDir, "profiles", "custom");
|
|
14600
15375
|
const profiles = [...officialDir ? await loadProfilesFromDir(officialDir) : [], ...await loadProfilesFromDir(customDir)];
|
|
14601
15376
|
if (action === "detect") {
|
|
14602
15377
|
const result = await detectProfiles(root, profiles);
|
|
@@ -14625,7 +15400,7 @@ async function runProfile(ctx) {
|
|
|
14625
15400
|
if (!name) {
|
|
14626
15401
|
return { exitCode: 2, diagnostics: [{ code: "ATLAS-PROFILE-001", severity: "error", message: "Falta el nombre del perfil: satlas profile create <nombre>" }] };
|
|
14627
15402
|
}
|
|
14628
|
-
const file =
|
|
15403
|
+
const file = path38.join(customDir, `${name}.yaml`);
|
|
14629
15404
|
if (await exists(file)) {
|
|
14630
15405
|
return { exitCode: 2, diagnostics: [{ code: "ATLAS-PROFILE-002", severity: "error", message: `El perfil ya existe: ${file}` }] };
|
|
14631
15406
|
}
|
|
@@ -14634,7 +15409,7 @@ async function runProfile(ctx) {
|
|
|
14634
15409
|
exitCode: 0,
|
|
14635
15410
|
diagnostics: [],
|
|
14636
15411
|
data: { file },
|
|
14637
|
-
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."]
|
|
14638
15413
|
};
|
|
14639
15414
|
}
|
|
14640
15415
|
return {
|
|
@@ -14643,7 +15418,7 @@ async function runProfile(ctx) {
|
|
|
14643
15418
|
};
|
|
14644
15419
|
}
|
|
14645
15420
|
async function isInside(dir, name) {
|
|
14646
|
-
return exists(
|
|
15421
|
+
return exists(path38.join(dir, `${name}.yaml`));
|
|
14647
15422
|
}
|
|
14648
15423
|
function profileTemplate(name) {
|
|
14649
15424
|
return `name: ${name}
|
|
@@ -14671,7 +15446,7 @@ assumptions: []
|
|
|
14671
15446
|
}
|
|
14672
15447
|
|
|
14673
15448
|
// src/commands/doctor.ts
|
|
14674
|
-
import
|
|
15449
|
+
import path39 from "path";
|
|
14675
15450
|
async function runDoctorCommand(ctx) {
|
|
14676
15451
|
const { root, config } = await requireWorkspace(ctx);
|
|
14677
15452
|
const report = await runDoctor(root);
|
|
@@ -14696,7 +15471,7 @@ async function runDoctorCommand(ctx) {
|
|
|
14696
15471
|
lines.push("Sin hallazgos. El workspace est\xE1 sano.");
|
|
14697
15472
|
} else {
|
|
14698
15473
|
for (const finding of report.findings) {
|
|
14699
|
-
const location = finding.path ? ` ${
|
|
15474
|
+
const location = finding.path ? ` ${path39.relative(ctx.cwd, finding.path)}${finding.line ? `:${finding.line}` : ""}` : "";
|
|
14700
15475
|
lines.push(` ${finding.severity.toUpperCase()} ${finding.code}${location} \u2014 ${finding.message}`);
|
|
14701
15476
|
if (finding.suggestion) lines.push(` \u21B3 ${finding.suggestion}`);
|
|
14702
15477
|
}
|
|
@@ -14770,7 +15545,7 @@ async function runHelp(ctx, commandName) {
|
|
|
14770
15545
|
}
|
|
14771
15546
|
|
|
14772
15547
|
// src/commands/init.ts
|
|
14773
|
-
import
|
|
15548
|
+
import path40 from "path";
|
|
14774
15549
|
async function runInit(ctx) {
|
|
14775
15550
|
const languageFlag = flagString(ctx.flags, "language");
|
|
14776
15551
|
const language = languageFlag === "en" ? "en" : languageFlag === "es" ? "es" : void 0;
|
|
@@ -14817,11 +15592,11 @@ async function runInit(ctx) {
|
|
|
14817
15592
|
const lines = [msg("init.title", ctx.language), ""];
|
|
14818
15593
|
if (result.created.length > 0) {
|
|
14819
15594
|
lines.push(msg("init.done", ctx.language));
|
|
14820
|
-
for (const file of result.created) lines.push(` + ${
|
|
15595
|
+
for (const file of result.created) lines.push(` + ${path40.relative(ctx.cwd, file)}`);
|
|
14821
15596
|
if (compiled.length > 0) {
|
|
14822
15597
|
lines.push("");
|
|
14823
15598
|
lines.push("Adaptadores compilados:");
|
|
14824
|
-
for (const file of compiled) lines.push(` + ${
|
|
15599
|
+
for (const file of compiled) lines.push(` + ${path40.relative(ctx.cwd, path40.join(ctx.cwd, file))}`);
|
|
14825
15600
|
}
|
|
14826
15601
|
lines.push("");
|
|
14827
15602
|
lines.push("Siguiente: crea un cambio con `satlas new <slug>` y escribe la spec funcional.");
|
|
@@ -14831,8 +15606,8 @@ async function runInit(ctx) {
|
|
|
14831
15606
|
exitCode: hasErrors2 ? 1 : 0,
|
|
14832
15607
|
diagnostics: result.diagnostics,
|
|
14833
15608
|
data: {
|
|
14834
|
-
sddDir:
|
|
14835
|
-
created: result.created.map((f) =>
|
|
15609
|
+
sddDir: path40.relative(ctx.cwd, result.sddDir),
|
|
15610
|
+
created: result.created.map((f) => path40.relative(ctx.cwd, f)),
|
|
14836
15611
|
detected: result.detected?.matches.map((m) => ({ name: m.name, score: m.score })),
|
|
14837
15612
|
best: result.detected?.best?.name
|
|
14838
15613
|
},
|
|
@@ -14873,7 +15648,7 @@ async function runMetrics(ctx) {
|
|
|
14873
15648
|
}
|
|
14874
15649
|
|
|
14875
15650
|
// src/commands/mockup.ts
|
|
14876
|
-
import
|
|
15651
|
+
import path41 from "path";
|
|
14877
15652
|
async function runMockup(ctx) {
|
|
14878
15653
|
const { root, workspace, config } = await requireWorkspace(ctx);
|
|
14879
15654
|
const slug = ctx.positionals[0];
|
|
@@ -14922,17 +15697,17 @@ async function runMockup(ctx) {
|
|
|
14922
15697
|
` pantallas: ${plan.screens.length}`,
|
|
14923
15698
|
...plan.screens.map((s) => ` - ${s.id}: ${s.title} (${s.illustrates.length} escenario(s))`),
|
|
14924
15699
|
"",
|
|
14925
|
-
` plan: ${
|
|
14926
|
-
` manifiesto: ${
|
|
15700
|
+
` plan: ${path41.relative(ctx.cwd, planFile)}`,
|
|
15701
|
+
` manifiesto: ${path41.relative(ctx.cwd, manifestFile)}`,
|
|
14927
15702
|
"",
|
|
14928
15703
|
"Siguiente: genera el HTML de cada pantalla (fase /satlas-mockup) y valida con `satlas mockup " + slug + " --check`."
|
|
14929
15704
|
];
|
|
14930
|
-
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 };
|
|
14931
15706
|
}
|
|
14932
15707
|
|
|
14933
15708
|
// src/mcp/server.ts
|
|
14934
15709
|
import { promises as fs3 } from "fs";
|
|
14935
|
-
import
|
|
15710
|
+
import path46 from "path";
|
|
14936
15711
|
import { createInterface } from "readline/promises";
|
|
14937
15712
|
|
|
14938
15713
|
// src/mcp/protocol.ts
|
|
@@ -14993,6 +15768,35 @@ function stringArg(args, key) {
|
|
|
14993
15768
|
return typeof value === "string" && value.trim() !== "" ? value.trim() : void 0;
|
|
14994
15769
|
}
|
|
14995
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
|
+
|
|
14996
15800
|
// src/mcp/tools/fixes.ts
|
|
14997
15801
|
async function runAtlasFixes(_args, host) {
|
|
14998
15802
|
const resolved = await host.getWorkspace();
|
|
@@ -15021,12 +15825,12 @@ async function runAtlasFixes(_args, host) {
|
|
|
15021
15825
|
}
|
|
15022
15826
|
|
|
15023
15827
|
// src/mcp/tools/glossary.ts
|
|
15024
|
-
import
|
|
15828
|
+
import path43 from "path";
|
|
15025
15829
|
async function runAtlasGlossary(_args, host) {
|
|
15026
15830
|
const resolved = await host.getWorkspace();
|
|
15027
15831
|
if (!resolved) return noWorkspaceResult();
|
|
15028
15832
|
const { root, config } = resolved;
|
|
15029
|
-
const glossaryPath =
|
|
15833
|
+
const glossaryPath = path43.resolve(root, config.spec.glossary);
|
|
15030
15834
|
const raw = await readTextIfExists(glossaryPath);
|
|
15031
15835
|
if (raw === void 0) {
|
|
15032
15836
|
return jsonResult({
|
|
@@ -15046,6 +15850,28 @@ async function runAtlasGlossary(_args, host) {
|
|
|
15046
15850
|
return jsonResult({ terms: parsed.terms });
|
|
15047
15851
|
}
|
|
15048
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
|
+
|
|
15049
15875
|
// src/mcp/tools/impact.ts
|
|
15050
15876
|
var REQ_TARGET_RE = /^REQ-[A-Z0-9]+(?:-[A-Z0-9]+)*-\d{3}(?:-S\d+)?$/i;
|
|
15051
15877
|
async function runAtlasImpact(args, host) {
|
|
@@ -15072,7 +15898,7 @@ async function runAtlasImpact(args, host) {
|
|
|
15072
15898
|
}
|
|
15073
15899
|
|
|
15074
15900
|
// src/evaluate.ts
|
|
15075
|
-
import
|
|
15901
|
+
import path45 from "path";
|
|
15076
15902
|
function livingRequirementsMap2(specs) {
|
|
15077
15903
|
const map = /* @__PURE__ */ new Map();
|
|
15078
15904
|
for (const spec of specs) {
|
|
@@ -15081,7 +15907,7 @@ function livingRequirementsMap2(specs) {
|
|
|
15081
15907
|
return map;
|
|
15082
15908
|
}
|
|
15083
15909
|
async function evaluateChange(workspace, config, change, approvals) {
|
|
15084
|
-
const deltaPath =
|
|
15910
|
+
const deltaPath = path45.join(change.dir, "spec.md");
|
|
15085
15911
|
const deltaContent = await readTextIfExists(deltaPath);
|
|
15086
15912
|
const approval = verifyApproval(change, approvals, config, deltaContent ?? void 0);
|
|
15087
15913
|
const living = livingRequirementsMap2(workspace.specs);
|
|
@@ -15089,7 +15915,8 @@ async function evaluateChange(workspace, config, change, approvals) {
|
|
|
15089
15915
|
const trace = checkTrace({
|
|
15090
15916
|
specs: workspace.specs,
|
|
15091
15917
|
change,
|
|
15092
|
-
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)
|
|
15093
15920
|
});
|
|
15094
15921
|
const blocking = lintFindings.filter((d) => d.severity === "error").length + trace.findings.filter((d) => d.severity === "error").length;
|
|
15095
15922
|
const mockupsAreReady = requiresMockups(change.meta, config) ? await mockupsReady(workspace.root, change.slug, change) : void 0;
|
|
@@ -15100,7 +15927,8 @@ async function evaluateChange(workspace, config, change, approvals) {
|
|
|
15100
15927
|
blockingFindings: change.delta ? blocking : 0,
|
|
15101
15928
|
...mockupsAreReady !== void 0 ? { mockupsReady: mockupsAreReady } : {}
|
|
15102
15929
|
});
|
|
15103
|
-
|
|
15930
|
+
const phaseAdvisories = [...clarifyAdvisory(change, config), ...docsAdvisory(change, config), ...contractsAdvisory(change, config)];
|
|
15931
|
+
return { change, approval, lintFindings: [...lintFindings, ...phaseAdvisories], trace, blocking, state };
|
|
15104
15932
|
}
|
|
15105
15933
|
|
|
15106
15934
|
// src/mcp/tools/next.ts
|
|
@@ -15258,7 +16086,9 @@ var HANDLERS = {
|
|
|
15258
16086
|
atlas_trace: runAtlasTrace,
|
|
15259
16087
|
atlas_impact: runAtlasImpact,
|
|
15260
16088
|
atlas_glossary: runAtlasGlossary,
|
|
15261
|
-
atlas_fixes: runAtlasFixes
|
|
16089
|
+
atlas_fixes: runAtlasFixes,
|
|
16090
|
+
atlas_contracts: runAtlasContracts,
|
|
16091
|
+
atlas_links: runAtlasLinks
|
|
15262
16092
|
};
|
|
15263
16093
|
async function runToolByName(name, args, host) {
|
|
15264
16094
|
const handler = HANDLERS[name];
|
|
@@ -15336,6 +16166,25 @@ function listTools() {
|
|
|
15336
16166
|
properties: {},
|
|
15337
16167
|
additionalProperties: false
|
|
15338
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
|
+
}
|
|
15339
16188
|
}
|
|
15340
16189
|
];
|
|
15341
16190
|
}
|
|
@@ -15353,8 +16202,8 @@ var WorkspaceCache = class {
|
|
|
15353
16202
|
}
|
|
15354
16203
|
};
|
|
15355
16204
|
async function workspaceStamp(root) {
|
|
15356
|
-
const sddDir =
|
|
15357
|
-
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")];
|
|
15358
16207
|
let stamp = 0;
|
|
15359
16208
|
for (const marker of markers) {
|
|
15360
16209
|
try {
|
|
@@ -15372,7 +16221,7 @@ async function resolveMcpWorkspace(cwd, cache) {
|
|
|
15372
16221
|
const cached2 = cache.get(root);
|
|
15373
16222
|
if (cached2 && cached2.mtimeMs === stamp) return cached2.value;
|
|
15374
16223
|
const { workspace, config } = await loadWorkspace(root);
|
|
15375
|
-
const approvals = await loadApprovals(
|
|
16224
|
+
const approvals = await loadApprovals(path46.join(root, ".sdd"));
|
|
15376
16225
|
const value = { root, workspace, config, approvals: approvals.byArtifact };
|
|
15377
16226
|
cache.set(root, stamp, value);
|
|
15378
16227
|
return value;
|
|
@@ -15464,7 +16313,7 @@ async function runMcp(ctx) {
|
|
|
15464
16313
|
}
|
|
15465
16314
|
|
|
15466
16315
|
// src/commands/packs.ts
|
|
15467
|
-
import
|
|
16316
|
+
import path47 from "path";
|
|
15468
16317
|
async function runPacks(ctx) {
|
|
15469
16318
|
const { root, config, workspace } = await requireWorkspace(ctx);
|
|
15470
16319
|
const slug = flagString(ctx.flags, "check");
|
|
@@ -15522,14 +16371,14 @@ async function runPacks(ctx) {
|
|
|
15522
16371
|
data: {
|
|
15523
16372
|
slug,
|
|
15524
16373
|
packs: evaluations.map((evaluation) => ({ id: evaluation.pack.id, status: evaluation.status, passed: evaluation.passed, failed: evaluation.failed, results: evaluation.results })),
|
|
15525
|
-
analyzePath:
|
|
16374
|
+
analyzePath: path47.posix.join(".sdd", "changes", slug, "analyze.md")
|
|
15526
16375
|
},
|
|
15527
16376
|
text: lines
|
|
15528
16377
|
};
|
|
15529
16378
|
}
|
|
15530
16379
|
|
|
15531
16380
|
// src/commands/new.ts
|
|
15532
|
-
import
|
|
16381
|
+
import path48 from "path";
|
|
15533
16382
|
async function runNew(ctx) {
|
|
15534
16383
|
const slug = ctx.positionals[0];
|
|
15535
16384
|
if (!slug) {
|
|
@@ -15555,14 +16404,14 @@ async function runNew(ctx) {
|
|
|
15555
16404
|
msg("new.title", ctx.language),
|
|
15556
16405
|
"",
|
|
15557
16406
|
msg("new.done", ctx.language),
|
|
15558
|
-
...result.files.map((f) => ` + ${
|
|
16407
|
+
...result.files.map((f) => ` + ${path48.relative(ctx.cwd, f)}`),
|
|
15559
16408
|
"",
|
|
15560
16409
|
`Siguiente: /satlas.specify ${result.slug} \u2014 escribe la especificaci\xF3n 100% funcional y de negocio.`
|
|
15561
16410
|
];
|
|
15562
16411
|
return {
|
|
15563
16412
|
exitCode: hasErrors2 ? 1 : 0,
|
|
15564
16413
|
diagnostics: result.diagnostics,
|
|
15565
|
-
data: { slug: result.slug, files: result.files.map((f) =>
|
|
16414
|
+
data: { slug: result.slug, files: result.files.map((f) => path48.relative(ctx.cwd, f)) },
|
|
15566
16415
|
text: lines
|
|
15567
16416
|
};
|
|
15568
16417
|
}
|
|
@@ -15597,7 +16446,7 @@ async function runNext(ctx) {
|
|
|
15597
16446
|
}
|
|
15598
16447
|
|
|
15599
16448
|
// src/commands/present.ts
|
|
15600
|
-
import
|
|
16449
|
+
import path49 from "path";
|
|
15601
16450
|
async function runPresentCommand(ctx) {
|
|
15602
16451
|
const { root } = await requireWorkspace(ctx);
|
|
15603
16452
|
const slug = ctx.positionals[0];
|
|
@@ -15609,7 +16458,7 @@ async function runPresentCommand(ctx) {
|
|
|
15609
16458
|
if (result.path) {
|
|
15610
16459
|
lines.push("Propuesta generada");
|
|
15611
16460
|
lines.push("");
|
|
15612
|
-
lines.push(` archivo: ${
|
|
16461
|
+
lines.push(` archivo: ${path49.relative(ctx.cwd, result.path)}`);
|
|
15613
16462
|
if (result.hash) lines.push(` hash de la spec: ${shortHash(result.hash)}`);
|
|
15614
16463
|
lines.push("");
|
|
15615
16464
|
lines.push("\xC1brela en el navegador y comp\xE1rtela con el stakeholder.");
|
|
@@ -15619,7 +16468,7 @@ async function runPresentCommand(ctx) {
|
|
|
15619
16468
|
return {
|
|
15620
16469
|
exitCode: hasErrors2 ? 1 : 0,
|
|
15621
16470
|
diagnostics: result.diagnostics,
|
|
15622
|
-
data: result.path ? { path:
|
|
16471
|
+
data: result.path ? { path: path49.relative(ctx.cwd, result.path), hash: result.hash } : void 0,
|
|
15623
16472
|
text: lines
|
|
15624
16473
|
};
|
|
15625
16474
|
}
|
|
@@ -15767,7 +16616,7 @@ async function runStatus(ctx) {
|
|
|
15767
16616
|
}
|
|
15768
16617
|
|
|
15769
16618
|
// src/commands/trace.ts
|
|
15770
|
-
import
|
|
16619
|
+
import path50 from "path";
|
|
15771
16620
|
async function runTrace(ctx) {
|
|
15772
16621
|
const { workspace, config } = await requireWorkspace(ctx);
|
|
15773
16622
|
const slug = flagString(ctx.flags, "change");
|
|
@@ -15787,7 +16636,7 @@ async function runTrace(ctx) {
|
|
|
15787
16636
|
data.push({ slug: change.slug, nodes: result.graph.nodes.length, edges: result.graph.edges.length, summary: result.summary, findings: result.findings });
|
|
15788
16637
|
lines.push(` ${change.slug}: ${result.graph.nodes.length} nodos, ${result.graph.edges.length} aristas, ${errors} errores, ${result.summary.warnings} avisos`);
|
|
15789
16638
|
for (const finding of result.findings) {
|
|
15790
|
-
const location = finding.path ? ` (${
|
|
16639
|
+
const location = finding.path ? ` (${path50.relative(ctx.cwd, finding.path)}${finding.line ? `:${finding.line}` : ""})` : "";
|
|
15791
16640
|
lines.push(` ${finding.severity.toUpperCase()} ${finding.code} \u2014 ${finding.message}${location}`);
|
|
15792
16641
|
}
|
|
15793
16642
|
}
|
|
@@ -15797,7 +16646,7 @@ async function runTrace(ctx) {
|
|
|
15797
16646
|
}
|
|
15798
16647
|
|
|
15799
16648
|
// src/commands/upgrade.ts
|
|
15800
|
-
import
|
|
16649
|
+
import path51 from "path";
|
|
15801
16650
|
async function runUpgrade(ctx) {
|
|
15802
16651
|
const { root } = await requireWorkspace(ctx);
|
|
15803
16652
|
const apply = flagBool(ctx.flags, "apply");
|
|
@@ -15851,7 +16700,7 @@ function renderPreview(plan, root, ctx) {
|
|
|
15851
16700
|
}
|
|
15852
16701
|
for (const item of plan.unreadable) {
|
|
15853
16702
|
lines.push(` Ilegible: "${item.artifact}" \u2014 ${item.reason}`);
|
|
15854
|
-
lines.push(` \u21B3 ${
|
|
16703
|
+
lines.push(` \u21B3 ${path51.relative(ctx.cwd, item.path)}`);
|
|
15855
16704
|
}
|
|
15856
16705
|
return { exitCode: 0, diagnostics: [], data: summarizable(plan), text: lines };
|
|
15857
16706
|
}
|
|
@@ -15914,7 +16763,7 @@ function renderRollback(report, ctx) {
|
|
|
15914
16763
|
}
|
|
15915
16764
|
|
|
15916
16765
|
// src/commands/validate.ts
|
|
15917
|
-
import
|
|
16766
|
+
import path52 from "path";
|
|
15918
16767
|
async function runValidate(ctx) {
|
|
15919
16768
|
const { root, workspace, config, approvals } = await requireWorkspace(ctx);
|
|
15920
16769
|
const slug = flagString(ctx.flags, "change");
|
|
@@ -15941,7 +16790,7 @@ async function runValidate(ctx) {
|
|
|
15941
16790
|
for (const finding of evaluation.lintFindings) {
|
|
15942
16791
|
lines.push(` ${finding.severity.toUpperCase()} ${finding.code}${finding.line ? `:${finding.line}` : ""} \u2014 ${finding.message}`);
|
|
15943
16792
|
}
|
|
15944
|
-
lines.push(` archivo: ${
|
|
16793
|
+
lines.push(` archivo: ${path52.relative(ctx.cwd, path52.join(change.dir, "spec.md"))}`);
|
|
15945
16794
|
}
|
|
15946
16795
|
if (targets.length === 0) lines.push("Sin cambios activos.");
|
|
15947
16796
|
if (advisory && advisory.plan.pending.length > 0) {
|
|
@@ -15969,7 +16818,7 @@ async function runValidate(ctx) {
|
|
|
15969
16818
|
}
|
|
15970
16819
|
|
|
15971
16820
|
// src/commands/verify.ts
|
|
15972
|
-
import
|
|
16821
|
+
import path53 from "path";
|
|
15973
16822
|
async function runVerify(ctx) {
|
|
15974
16823
|
const { root, workspace } = await requireWorkspace(ctx);
|
|
15975
16824
|
const slug = ctx.positionals[0];
|
|
@@ -16012,7 +16861,7 @@ async function runVerify(ctx) {
|
|
|
16012
16861
|
return { exitCode: 2, diagnostics: [{ code: "ATLAS-VERIFY-001", severity: "error", message: "Falta --by <nombre> (o define meta.owner)" }] };
|
|
16013
16862
|
}
|
|
16014
16863
|
const profilesDir = await resolveProfilesDir();
|
|
16015
|
-
const profile = await loadActiveProfile(
|
|
16864
|
+
const profile = await loadActiveProfile(path53.join(root, ".sdd"), [profilesDir ?? "", path53.join(root, ".sdd", "profiles", "custom")].filter(Boolean));
|
|
16016
16865
|
const record = await recordEvidence({
|
|
16017
16866
|
root,
|
|
16018
16867
|
slug,
|
|
@@ -16034,7 +16883,7 @@ async function runVerify(ctx) {
|
|
|
16034
16883
|
if (record.evidence.command) lines.push(` comando: ${record.evidence.command}`);
|
|
16035
16884
|
lines.push(` resultado: ${record.evidence.result}`);
|
|
16036
16885
|
if (record.evidence.outputHash) lines.push(` hash: ${record.evidence.outputHash}`);
|
|
16037
|
-
lines.push(` archivo: ${
|
|
16886
|
+
lines.push(` archivo: ${path53.relative(ctx.cwd, record.path)}`);
|
|
16038
16887
|
if (record.stdout) {
|
|
16039
16888
|
lines.push("");
|
|
16040
16889
|
lines.push(" salida (\xFAltimas l\xEDneas):");
|
|
@@ -16049,7 +16898,7 @@ async function runVerify(ctx) {
|
|
|
16049
16898
|
return {
|
|
16050
16899
|
exitCode: record.exitCode === 0 ? 0 : 1,
|
|
16051
16900
|
diagnostics: record.diagnostics,
|
|
16052
|
-
data: { evidence: record.evidence, path:
|
|
16901
|
+
data: { evidence: record.evidence, path: path53.relative(ctx.cwd, record.path), exitCode: record.exitCode },
|
|
16053
16902
|
text: lines
|
|
16054
16903
|
};
|
|
16055
16904
|
}
|
|
@@ -16104,6 +16953,10 @@ var HANDLERS2 = {
|
|
|
16104
16953
|
trace: runTrace,
|
|
16105
16954
|
waves: runWaves,
|
|
16106
16955
|
doctor: runDoctorCommand,
|
|
16956
|
+
clarify: runClarify,
|
|
16957
|
+
docs: runDocs,
|
|
16958
|
+
contracts: runContracts,
|
|
16959
|
+
link: runLink,
|
|
16107
16960
|
upgrade: runUpgrade,
|
|
16108
16961
|
approve: runApprove,
|
|
16109
16962
|
issue: runIssue,
|
|
@@ -16149,7 +17002,7 @@ async function requireWorkspace(ctx) {
|
|
|
16149
17002
|
throw new CliError("ATLAS-WS-001", msg("cli.noWorkspace", ctx.language), 2);
|
|
16150
17003
|
}
|
|
16151
17004
|
const { workspace, config } = await loadWorkspace(root);
|
|
16152
|
-
const approvals = await loadApprovals(
|
|
17005
|
+
const approvals = await loadApprovals(path54.join(root, ".sdd"));
|
|
16153
17006
|
return { root, workspace, config, approvals: approvals.byArtifact };
|
|
16154
17007
|
}
|
|
16155
17008
|
var CliError = class extends Error {
|
|
@@ -16216,7 +17069,7 @@ function printResult(command, result, json2, language) {
|
|
|
16216
17069
|
}
|
|
16217
17070
|
const lines = [...result.text ?? []];
|
|
16218
17071
|
for (const d of result.diagnostics) {
|
|
16219
|
-
const location = d.path ? ` ${
|
|
17072
|
+
const location = d.path ? ` ${path54.relative(process.cwd(), d.path)}${d.line ? `:${d.line}` : ""}` : "";
|
|
16220
17073
|
lines.push(`${d.severity === "error" ? "ERROR" : d.severity === "warning" ? "AVISO" : "NOTA"} ${d.code}${location} \u2014 ${d.message}`);
|
|
16221
17074
|
if (d.suggestion) lines.push(` \u21B3 ${d.suggestion}`);
|
|
16222
17075
|
}
|