specatlas 0.1.30 → 0.1.31

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