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 CHANGED
@@ -1586,7 +1586,7 @@ var require_core = __commonJS({
1586
1586
  });
1587
1587
 
1588
1588
  // src/cli.ts
1589
- import path46 from "path";
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 parseYaml5 } from "yaml";
1608
- import { execFile } from "child_process";
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 path9 from "path";
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 path13 from "path";
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 { parse as parseYaml8, stringify as stringifyYaml6 } from "yaml";
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, path222, line) {
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: path222,
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, path222, opts = {}) {
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", path222, part.line));
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", path222, part.line));
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: path222, line: req.line, suggestion: "A\xF1ade al menos un escenario CUANDO/ENTONCES" }));
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, path222, opts = {}) {
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, path222, opts));
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: path222, line: req.line }));
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: path222,
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: path222, line: req.line }));
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: path222, line: rename2.line }));
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: path222, line: rename2.line }));
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, path222) {
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: path222,
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: path222,
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: path222,
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 = path2.posix.join("changes", change.slug, "spec.md");
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 = parseYaml4(raw);
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(path3.join(dir, entry));
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(path3.join(sddDir, "profiles", "detected.yaml"));
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 = parseYaml4(raw);
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 = path4.join(".sdd", "fixes");
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 = path4.basename(file).replace(/\.md$/i, "");
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 = path4.join(root, ".sdd", "changes", "archive");
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 = path4.join(archiveDir, entry);
10458
- const fixFile = path4.join(dir, "fix.md");
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 = path4.join(dir, "meta.yaml");
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 = path4.join(root, LIVING_FIXES_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 = path4.join(dir, entry);
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 = path4.join(root, LIVING_FIXES_DIR);
10769
+ const dir = path5.join(root, LIVING_FIXES_DIR);
10498
10770
  const month = input.date.slice(0, 7);
10499
- const file = path4.join(dir, `${month}-${input.slug}.md`);
10500
- const relativePath = toPosix(path4.relative(root, file));
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 = path5.resolve(start);
10919
+ let current = path7.resolve(start);
10522
10920
  for (let i = 0; i < 40; i += 1) {
10523
- if (await isDirectory(path5.join(current, SDD_DIR))) return current;
10524
- const parent = path5.dirname(current);
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 = path5.join(sddDir, "approvals.yaml");
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(path5.sep).join("/"), { hash: a.artifactHash, by: a.approvedBy, at: a.approvedAt });
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 = path5.join(sddDir, "specs");
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 = path5.join(specsDir, domain, "spec.md");
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 = path5.join(root, SDD_DIR, "changes", relDir ?? slug);
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 = path5.join(dir, "meta.yaml");
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 = path5.join(dir, "spec.md");
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 = path5.join(dir, "plan.md");
10972
+ const planFile = path7.join(dir, "plan.md");
10575
10973
  if (await exists(planFile)) change.planPath = planFile;
10576
- const reviewFile = path5.join(dir, "review.md");
10974
+ const reviewFile = path7.join(dir, "review.md");
10577
10975
  if (await exists(reviewFile)) change.reviewPath = reviewFile;
10578
- const presentationFile = path5.join(dir, "presentation", "index.html");
10976
+ const presentationFile = path7.join(dir, "presentation", "index.html");
10579
10977
  if (await exists(presentationFile)) change.presentationPath = presentationFile;
10580
- const tasksFile = path5.join(dir, "tasks.md");
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 = path5.join(dir, "verify.md");
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 = path5.join(dir, "fix.md");
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 mockupManifest = path5.join(dir, "mockups", "manifest.yaml");
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 = path5.join(root, SDD_DIR, "changes");
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 = path5.join(root, SDD_DIR);
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 = path5.join(sddDir, "changes", "archive");
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}-/, ""), path5.join("archive", entry)));
11038
+ archived.push(await loadChange(root, entry.replace(/^\d{4}-\d{2}-/, ""), path7.join("archive", entry)));
10626
11039
  }
10627
11040
  }
10628
- return { workspace: { root, sddDir, specs, changes, archived, diagnostics }, config };
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", path5.join("profiles", "custom")];
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 = path5.join(sddDir, d);
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
- ` + stringifyYaml4(doc, { lineWidth: 120 });
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: \`${path6.posix.join(".sdd", "changes", change.slug, "spec.md")}\``);
11133
- if (change.planPath) lines.push(`- Plan: \`${path6.posix.join(".sdd", "changes", change.slug, "plan.md")}\``);
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 = path6.join(path6.resolve(root), ".sdd", "changes", slug, "meta.yaml");
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 ${path6.relative(root, file)}`)] };
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 = parseYaml5(raw) ?? {};
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: path6.posix.join("changes", opts.slug, "spec.md"),
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 = path7.resolve(opts.root);
11842
+ const root = path9.resolve(opts.root);
11350
11843
  const slug = opts.slug;
11351
11844
  const fileKind = opts.file ?? "verify";
11352
- const file = path7.join(root, ".sdd", "changes", slug, fileKind === "fix" ? "fix.md" : "verify.md");
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 path8.join(path8.resolve(root), ".sdd", "runs");
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 = path8.join(runsDir(root), runId);
11948
+ const dir = path10.join(runsDir(root), runId);
11456
11949
  await ensureDir(dir);
11457
- await writeText(path8.join(dir, "state.json"), `${JSON.stringify(state, null, 2)}
11950
+ await writeText(path10.join(dir, "state.json"), `${JSON.stringify(state, null, 2)}
11458
11951
  `);
11459
- await writeText(path8.join(dir, "events.jsonl"), "");
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 = path8.join(runsDir(root), runId, "events.jsonl");
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 = path8.join(runsDir(root), runId, "state.json");
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 = path8.join(runsDir(root), runId);
11484
- const stateRaw = await readTextIfExists(path8.join(dir, "state.json"));
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(path8.join(dir, "events.jsonl")) ?? "";
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(path8.join(dir, id, "state.json"));
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 path9.join(path9.resolve(root), ".sdd", "changes", slug, "mockups");
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 = path9.join(root, candidate);
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 = path9.join(change.dir, "spec.md");
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(path9.join(root, tokens)) ?? "" : "";
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 = path9.join(mockupsDir(root, slug), "manifest.yaml");
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 = parseYaml6(raw);
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: path9.join(dir, "manifest.yaml") })], stale: false };
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(path9.join(dir, screen.file));
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: path9.join(dir, screen.file) }));
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, path9.join(dir, screen.file)));
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 = path9.join(mockupsDir(root, slug), "plan.yaml");
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, stringifyYaml5(doc, { lineWidth: 120 }));
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 = path9.join(mockupsDir(root, slug), "manifest.yaml");
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, stringifyYaml5(doc, { lineWidth: 120 }));
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 = path9.join(mockupsDir(root, slug), "manifest.yaml");
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 = parseYaml6(raw) ?? {};
12223
+ const data = parseYaml8(raw) ?? {};
11731
12224
  data["screenshots"] = screenshots;
11732
- await writeText(file, stringifyYaml5(data, { lineWidth: 120 }));
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://${path9.join(dir, screen.file).split(path9.sep).join("/")}`);
11758
- const rel = path9.posix.join("screens", `${screen.id}-${bp}.png`);
11759
- await page2.screenshot({ path: path9.join(dir, rel) });
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 = path9.join(root, ".sdd", "changes", slug, "meta.yaml");
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 = path10.join(sddDir, "packs");
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 = path10.join(dir, entry);
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 = parseYaml7(raw);
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 = path11.resolve(opts.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, path11.join(change.dir, "spec.md"), { language: config.spec.language }));
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 = path11.join(change.dir, "analyze.md");
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 = path12.resolve(root);
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, path12.join(change.dir, "spec.md"), { language: config.spec.language }));
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 = path12.join(workspace.sddDir, "changes", "archive");
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(path12.join(change.dir, "spec.md"));
12818
+ return readTextIfExists2(path14.join(change.dir, "spec.md"));
12325
12819
  }
12326
12820
  async function generatePresentation(opts) {
12327
- const root = path13.resolve(opts.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 = path13.join(change.dir, "spec.md");
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 = path13.join(change.dir, "presentation");
12342
- const mockupsCopyDir = path13.join(presentationDir, "mockups");
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(path13.join(change.dir, "proposal.md"));
12839
+ const proposalRaw = await readTextIfExists(path15.join(change.dir, "proposal.md"));
12346
12840
  const proposalBody = proposalRaw ? parseFrontmatter(proposalRaw).body : "";
12347
- const approvals = await loadApprovals(path13.join(root, ".sdd"));
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 = path13.join(presentationDir, "index.html");
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 = path13.join(dir, screen.file);
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, path13.join(destDir, screen.file));
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 = path13.join(dir, "screens");
12889
+ const shotsDir = path15.join(dir, "screens");
12396
12890
  if (await exists(shotsDir)) {
12397
- await ensureDir(path13.join(destDir, "screens"));
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(path13.join(shotsDir, file), path13.join(destDir, "screens", file));
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 = path14.resolve(opts.root);
13111
+ const root = path16.resolve(opts.root);
12618
13112
  const dryRun = opts.dryRun ?? false;
12619
13113
  const diagnostics = [];
12620
- const sddDir = path14.join(root, ".sdd");
12621
- if (!await exists(path14.join(sddDir, "config.yaml"))) {
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(path14.relative(root, f.path)));
12633
- const relative = inventoried.filter((f) => SOURCE_EXT.has(path14.extname(f).toLowerCase()));
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(path14.join(sddDir, "specs", name, "spec.md"));
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: path14.join(sddDir, "specs", name, "spec.md") }));
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 = path14.join(sddDir, "specs", name, "spec.md");
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 = path14.join(sddDir, "adopt-report.md");
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 = path14.join(root, ".sdd", "profiles", "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(path14.join(root, "package.json"));
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 = path15.resolve(opts.root);
12788
- const sddDir = path15.join(root, ".sdd");
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(path15.join(sddDir, "config.yaml"))) {
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 ?? path15.basename(root);
13295
+ cfg.project.name = opts.name ?? path17.basename(root);
12802
13296
  await writeConfig(sddDir, cfg);
12803
- created.push(path15.join(sddDir, "config.yaml"));
13297
+ created.push(path17.join(sddDir, "config.yaml"));
12804
13298
  const templates = templatesFor(language);
12805
- await writeText(path15.join(sddDir, "constitution.md"), templates.constitution);
12806
- created.push(path15.join(sddDir, "constitution.md"));
12807
- await writeText(path15.join(sddDir, "glossary.md"), templates.glossary);
12808
- created.push(path15.join(sddDir, "glossary.md"));
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(path15.join(sddDir, "profiles", "detected.yaml"), yaml2);
12813
- created.push(path15.join(sddDir, "profiles", "detected.yaml"));
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(path15.join(sddDir, "INDEX.md"), initialIndex(language, cfg.project.name));
12821
- created.push(path15.join(sddDir, "INDEX.md"));
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 = path15.join(root, ".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 = path16.join(path16.resolve(opts.root), ".sdd", "changes", slug);
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 = path16.join(dir, "meta.yaml");
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 = path16.join(dir, "fix.md");
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 = path16.join(dir, "proposal.md");
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 = path16.join(dir, "spec.md");
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 = path17.resolve(opts.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 = path17.join(root, ".sdd", "changes", "archive", `${localMonth(nowFix)}-${opts.slug}`);
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(path17.join(change.dir, "fix.md")) ?? "";
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: path17.join(root, ".sdd", "fixes"),
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(path17.dirname(targetFix));
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(path17.join(root, livingFix));
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: path17.join(change.dir, "meta.yaml") }));
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 = path17.join(root, ".sdd", "specs", domain, "spec.md");
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 = path17.join(root, ".sdd", "changes", "archive");
13130
- const target = path17.join(archiveDir, `${month}-${opts.slug}`);
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 = path17.join(root, ".sdd", "changes", "archive");
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(path17.join(root, ".sdd", "INDEX.md"), markdown2);
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", path18.join("profiles", "detected.yaml")];
13188
- var CHANGE_ARTIFACTS = ["meta.yaml", path18.join("mockups", "manifest.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 = path18.join(sddDir, relDir, rel);
13192
- if (await exists(abs)) out.push({ artifact: toPosix(path18.join(relDir, rel)), path: abs });
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 = path18.join(root, ".sdd");
13690
+ const sddDir = path20.join(root, ".sdd");
13197
13691
  const out = [];
13198
13692
  for (const rel of FIXED_ARTIFACTS) {
13199
- const abs = path18.join(sddDir, rel);
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 = path18.join(sddDir, "changes");
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, path18.join("changes", slug));
13699
+ await pushChangeArtifacts(out, sddDir, path20.join("changes", slug));
13206
13700
  }
13207
- for (const entry of await listDirs(path18.join(changesDir, "archive"))) {
13208
- await pushChangeArtifacts(out, sddDir, path18.join("changes", "archive", entry));
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 = parseYaml8(content);
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 = path18.join(root, ".sdd");
13306
- const backupRoot = path18.join(sddDir, BACKUP_DIRNAME);
13799
+ const sddDir = path20.join(root, ".sdd");
13800
+ const backupRoot = path20.join(sddDir, BACKUP_DIRNAME);
13307
13801
  const name = backupName(now);
13308
- const backupDir = path18.join(backupRoot, name);
13309
- const pointerFile = path18.join(backupRoot, BACKUP_POINTER);
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(path18.join(backupDir, "files", ...item.artifact.split("/")), content);
13810
+ await writeText(path20.join(backupDir, "files", ...item.artifact.split("/")), content);
13317
13811
  }
13318
13812
  await writeText(
13319
- path18.join(backupDir, "backup.yaml"),
13320
- stringifyYaml6(
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(path18.join(backupRoot, dir));
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(path18.relative(root, backupDir)),
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 = path18.join(root, ".sdd");
13377
- const backupRoot = path18.join(sddDir, BACKUP_DIRNAME);
13378
- const pointerFile = path18.join(backupRoot, BACKUP_POINTER);
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 = path18.join(backupRoot, name);
13382
- const manifestRaw = await readTextIfExists(path18.join(backupDir, "backup.yaml"));
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 = parseYaml8(manifestRaw);
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 = path18.join(backupDir, "files", ...file.artifact.split("/"));
13394
- const to = path18.join(sddDir, ...file.artifact.split("/"));
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 = path19.relative(root, diagnostic.path);
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 = path20.join(change.dir, "spec.md");
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: path20.join(change.dir, "meta.yaml") }));
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 = path21.resolve(opts.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, path21.join(change.dir, "spec.md"), { language: config.spec.language }) : [];
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 path25 from "path";
14410
+ import path28 from "path";
13816
14411
 
13817
14412
  // ../adapters/dist/index.js
13818
- import path22 from "path";
13819
- import { stringify as stringifyYaml7 } from "yaml";
13820
- import path23 from "path";
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 = path22.join(workflowDir, "phases");
13823
- const snippetsDir = path22.join(workflowDir, "snippets");
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 = path22.join(phasesDir, entry);
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"] : path22.basename(entry, ".md");
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(path22.join(snippetsDir, entry)) ?? "";
14443
+ const raw = await readTextIfExists(path25.join(snippetsDir, entry)) ?? "";
13849
14444
  hashParts.push(raw);
13850
- snippets.set(path22.basename(entry, ".md"), raw.trim());
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 = stringifyYaml7(data, { lineWidth: 0 }).trimEnd();
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 = path23.join(".sdd", ".generated", "manifest.json");
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 = path23.resolve(opts.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 ${path23.join(opts.workflowDir, "phases")}`,
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 = path23.join(root, MANIFEST_REL);
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 = path23.join(root, file.path);
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(path23.dirname(abs));
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 = path23.join(path23.resolve(opts.root), MANIFEST_REL);
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 path24 from "path";
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(path24.join("workflow", "phases"));
14759
+ return walkUpFor(path27.join("workflow", "phases"));
14165
14760
  }
14166
14761
  async function walkUpFor(relative) {
14167
- let dir = path24.dirname(fileURLToPath2(import.meta.url));
14762
+ let dir = path27.dirname(fileURLToPath2(import.meta.url));
14168
14763
  for (let i = 0; i < 8; i += 1) {
14169
- const candidate = path24.join(dir, relative);
14764
+ const candidate = path27.join(dir, relative);
14170
14765
  if (await exists(candidate)) {
14171
- return relative.includes(path24.sep) ? path24.dirname(candidate) : candidate;
14766
+ return relative.includes(path27.sep) ? path27.dirname(candidate) : candidate;
14172
14767
  }
14173
- const parent = path24.dirname(dir);
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(` + ${path25.relative(ctx.cwd, path25.join(root, p))}`);
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 path26 from "path";
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] " : "+ "}${path26.relative(ctx.cwd, spec)}`);
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: ${path26.relative(ctx.cwd, result.reportPath)}`);
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) => path26.relative(ctx.cwd, s)),
14300
- reportPath: result.reportPath ? path26.relative(ctx.cwd, result.reportPath) : void 0,
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 path27 from "path";
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: ${path27.relative(ctx.cwd, result.path)}`);
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: path27.relative(ctx.cwd, result.path) } : result,
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 path29 from "path";
14928
+ import path32 from "path";
14334
14929
 
14335
14930
  // src/commands/issue.ts
14336
- import path28 from "path";
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(path28.join(root, ".sdd"));
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 : path29.join("changes", target, "spec.md");
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: path29.relative(ctx.cwd, result.file) } : void 0,
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 path30 from "path";
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: ${path30.relative(ctx.cwd, result.archivedTo)}`);
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 path31 from "path";
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 = path31.resolve(ctx.cwd, sarifPath);
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 = path31.relative(ctx.cwd, target);
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 path32 from "path";
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 = path32.join(workspace.sddDir, "profiles", "custom");
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 = path32.join(customDir, `${name}.yaml`);
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: ${path32.relative(ctx.cwd, file)}`, "", "Ed\xEDtalo con los comandos reales de tu stack (build/lint/test) y tus anti-patrones."]
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(path32.join(dir, `${name}.yaml`));
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 path33 from "path";
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 ? ` ${path33.relative(ctx.cwd, finding.path)}${finding.line ? `:${finding.line}` : ""}` : "";
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 path34 from "path";
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(` + ${path34.relative(ctx.cwd, file)}`);
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(` + ${path34.relative(ctx.cwd, path34.join(ctx.cwd, file))}`);
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: path34.relative(ctx.cwd, result.sddDir),
14835
- created: result.created.map((f) => path34.relative(ctx.cwd, 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 path35 from "path";
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: ${path35.relative(ctx.cwd, planFile)}`,
14926
- ` manifiesto: ${path35.relative(ctx.cwd, manifestFile)}`,
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: path35.relative(ctx.cwd, planFile), manifestFile: path35.relative(ctx.cwd, manifestFile) }, text: lines };
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 path38 from "path";
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 path36 from "path";
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 = path36.resolve(root, config.spec.glossary);
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 path37 from "path";
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 = path37.join(change.dir, "spec.md");
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
- return { change, approval, lintFindings, trace, blocking, state };
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 = path38.join(root, ".sdd");
15357
- const markers = [sddDir, path38.join(sddDir, "config.yaml"), path38.join(sddDir, "changes"), path38.join(sddDir, "specs"), path38.join(sddDir, "approvals.yaml")];
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(path38.join(root, ".sdd"));
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 path39 from "path";
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: path39.posix.join(".sdd", "changes", slug, "analyze.md")
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 path40 from "path";
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) => ` + ${path40.relative(ctx.cwd, 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) => path40.relative(ctx.cwd, 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 path41 from "path";
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: ${path41.relative(ctx.cwd, result.path)}`);
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: path41.relative(ctx.cwd, result.path), hash: result.hash } : void 0,
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 path42 from "path";
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 ? ` (${path42.relative(ctx.cwd, finding.path)}${finding.line ? `:${finding.line}` : ""})` : "";
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 path43 from "path";
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 ${path43.relative(ctx.cwd, item.path)}`);
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 path44 from "path";
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: ${path44.relative(ctx.cwd, path44.join(change.dir, "spec.md"))}`);
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 path45 from "path";
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(path45.join(root, ".sdd"), [profilesDir ?? "", path45.join(root, ".sdd", "profiles", "custom")].filter(Boolean));
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: ${path45.relative(ctx.cwd, record.path)}`);
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: path45.relative(ctx.cwd, record.path), exitCode: record.exitCode },
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(path46.join(root, ".sdd"));
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 ? ` ${path46.relative(process.cwd(), d.path)}${d.line ? `:${d.line}` : ""}` : "";
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
  }