@specatlas/core 0.1.27 → 0.1.28

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -63,6 +63,7 @@ var atlasConfigSchema = z.object({
63
63
  review: z.object({ mode: z.enum(["off", "advisory", "blocking"]).default("advisory") }).default({}),
64
64
  clarify: z.object({ mode: z.enum(["off", "advisory", "blocking"]).default("advisory") }).default({}),
65
65
  docs: z.object({ mode: z.enum(["off", "advisory", "blocking"]).default("blocking") }).default({}),
66
+ contracts: z.object({ mode: z.enum(["off", "advisory", "blocking"]).default("advisory") }).default({}),
66
67
  mockup: z.object({ require_approval: z.boolean().default(false), compare_in_verify: z.boolean().default(false) }).default({})
67
68
  }).default({}),
68
69
  trace: z.object({ mode: z.enum(["off", "advisory", "blocking"]).default("blocking"), prefix: z.string().default("REQ") }).default({}),
@@ -211,6 +212,7 @@ var SCENARIO_HEAD_RE = /^####\s+(?:Scenario|Escenario):\s+(REQ-[A-Z0-9-]+-S\d+)\
211
212
  var RULE_RE = /^-\s*(?:Rule|Regla)\s+(BR-[A-Z0-9-]+)\s*:\s*(.+?)\s*$/;
212
213
  var WHEN_RE = /^-\s*\*\*\s*(?:WHEN|CUANDO|DADO QUE)\s*\*\*\s*(.+?)\s*$/i;
213
214
  var THEN_RE = /^-\s*\*\*\s*(?:THEN|ENTONCES|Y|AND)\s*\*\*\s*(.+?)\s*$/i;
215
+ var CONTRACT_RE = /^-\s*\*\*\s*(?:CONTRATO|CONTRACT)\s*\*\*\s*:\s*(.+?)\s*$/i;
214
216
  function newDraft(id, title, line) {
215
217
  return { id, title, line, prose: [], rules: [], scenarios: [] };
216
218
  }
@@ -295,6 +297,16 @@ function parseRequirementBlocks(body, startLine = 1, filePath) {
295
297
  currentScenario.then.push(thenMatch[1] ?? "");
296
298
  continue;
297
299
  }
300
+ const contractMatch = CONTRACT_RE.exec(line);
301
+ if (contractMatch && currentScenario) {
302
+ const ref = (contractMatch[1] ?? "").trim();
303
+ if (ref !== "") {
304
+ const list = currentScenario.contracts ?? [];
305
+ if (!list.includes(ref)) list.push(ref);
306
+ currentScenario.contracts = list;
307
+ }
308
+ continue;
309
+ }
298
310
  if (current && !currentScenario) {
299
311
  if (line.trim() !== "") current.prose.push(line.trim());
300
312
  }
@@ -762,6 +774,7 @@ function impactOfRequirement(workspace, reqId) {
762
774
  const id = reqId.toUpperCase();
763
775
  const scenarios = /* @__PURE__ */ new Set();
764
776
  let exists2 = false;
777
+ let origin;
765
778
  for (const spec of workspace.specs) {
766
779
  for (const req of spec.spec.requirements) {
767
780
  if (req.id !== id) continue;
@@ -778,6 +791,18 @@ function impactOfRequirement(workspace, reqId) {
778
791
  if ((change.delta?.removed ?? []).some((r) => r.id === id)) exists2 = true;
779
792
  if ((change.delta?.renamed ?? []).some((r) => r.from.id === id || r.to.id === id)) exists2 = true;
780
793
  }
794
+ if (!exists2) {
795
+ for (const link of workspace.links?.entries ?? []) {
796
+ const spec = workspace.links?.specs.find((candidate) => candidate.spec.requirements.some((req) => req.id === id));
797
+ if (!spec) continue;
798
+ const requirement = spec.spec.requirements.find((candidate) => candidate.id === id);
799
+ if (!requirement) continue;
800
+ exists2 = true;
801
+ origin = link.name;
802
+ for (const sc of requirement.scenarios) scenarios.add(sc.id);
803
+ break;
804
+ }
805
+ }
781
806
  const report = emptyReport(id, "requirement");
782
807
  if (!exists2) return report;
783
808
  report.exists = true;
@@ -785,6 +810,10 @@ function impactOfRequirement(workspace, reqId) {
785
810
  const reqOf = scenarioToReq(workspace);
786
811
  collect(workspace, report, /* @__PURE__ */ new Set([id, ...scenarios]), reqOf);
787
812
  if (!report.requirements.includes(id)) report.requirements.unshift(id);
813
+ if (origin !== void 0) {
814
+ report.external = true;
815
+ report.origin = origin;
816
+ }
788
817
  return report;
789
818
  }
790
819
  function impactOfFile(workspace, file) {
@@ -825,14 +854,14 @@ function wordBoundary(text, term) {
825
854
  const re = new RegExp(`(?<![\\p{L}\\p{N}])${escaped}(?![\\p{L}\\p{N}])`, "iu");
826
855
  return re.test(text);
827
856
  }
828
- function lintText(text, code, terms, label, path23, line) {
857
+ function lintText(text, code, terms, label, path25, line) {
829
858
  const out = [];
830
859
  const lower = text.toLowerCase();
831
860
  for (const term of terms) {
832
861
  if (wordBoundary(lower, term)) {
833
862
  out.push(
834
863
  diag(code, "error", `${label}: "${term}"`, {
835
- path: path23,
864
+ path: path25,
836
865
  line,
837
866
  suggestion: "La especificaci\xF3n es funcional y de negocio: describe comportamiento, no tecnolog\xEDa ni adjetivos vagos"
838
867
  })
@@ -841,7 +870,7 @@ function lintText(text, code, terms, label, path23, line) {
841
870
  }
842
871
  return out;
843
872
  }
844
- function lintRequirement(req, path23, opts = {}) {
873
+ function lintRequirement(req, path25, opts = {}) {
845
874
  const out = [];
846
875
  const vague = opts.language === "en" ? VAGUE_EN : VAGUE_ES;
847
876
  const tech = opts.language === "en" ? TECH_EN : TECH_ES;
@@ -852,32 +881,32 @@ function lintRequirement(req, path23, opts = {}) {
852
881
  ...req.scenarios.flatMap((s) => [...s.when.map((w) => ({ text: w, line: s.line })), ...s.then.map((t) => ({ text: t, line: s.line }))])
853
882
  ];
854
883
  for (const part of parts) {
855
- out.push(...lintText(part.text, "LINT-BIZ-002", vague, "Palabra vaga en la especificaci\xF3n", path23, part.line));
884
+ out.push(...lintText(part.text, "LINT-BIZ-002", vague, "Palabra vaga en la especificaci\xF3n", path25, part.line));
856
885
  if (opts.businessOnly !== false) {
857
- out.push(...lintText(part.text, "LINT-BIZ-001", tech, "Jerga t\xE9cnica en la especificaci\xF3n de negocio", path23, part.line));
886
+ out.push(...lintText(part.text, "LINT-BIZ-001", tech, "Jerga t\xE9cnica en la especificaci\xF3n de negocio", path25, part.line));
858
887
  }
859
888
  }
860
889
  if (req.scenarios.length === 0) {
861
- out.push(diag("TRACE-001", "error", `El requisito ${req.id} no tiene ning\xFAn escenario`, { path: path23, line: req.line, suggestion: "A\xF1ade al menos un escenario CUANDO/ENTONCES" }));
890
+ out.push(diag("TRACE-001", "error", `El requisito ${req.id} no tiene ning\xFAn escenario`, { path: path25, line: req.line, suggestion: "A\xF1ade al menos un escenario CUANDO/ENTONCES" }));
862
891
  }
863
892
  return out;
864
893
  }
865
- function lintDelta(delta, livingRequirements, path23, opts = {}) {
894
+ function lintDelta(delta, livingRequirements, path25, opts = {}) {
866
895
  const out = [...delta.diagnostics];
867
896
  for (const req of [...delta.added, ...delta.modified]) {
868
- out.push(...lintRequirement(req, path23, opts));
897
+ out.push(...lintRequirement(req, path25, opts));
869
898
  }
870
899
  for (const req of delta.modified) {
871
900
  const living = livingRequirements.get(req.id);
872
901
  if (!living) {
873
- out.push(diag("TRACE-007", "error", `MODIFIED ${req.id} no existe en la spec viva; usa ADDED`, { path: path23, line: req.line }));
902
+ out.push(diag("TRACE-007", "error", `MODIFIED ${req.id} no existe en la spec viva; usa ADDED`, { path: path25, line: req.line }));
874
903
  continue;
875
904
  }
876
905
  for (const existing of living.scenarios) {
877
906
  if (!req.scenarios.some((s) => s.id === existing.id)) {
878
907
  out.push(
879
908
  diag("TRACE-007", "error", `MODIFIED ${req.id} pierde el escenario ${existing.id}: copia el bloque completo`, {
880
- path: path23,
909
+ path: path25,
881
910
  line: req.line,
882
911
  suggestion: "Copia el bloque completo de la spec viva y ed\xEDtalo; para quitarlo, decl\xE1ralo en REMOVED"
883
912
  })
@@ -888,15 +917,15 @@ function lintDelta(delta, livingRequirements, path23, opts = {}) {
888
917
  for (const req of delta.removed) {
889
918
  const living = livingRequirements.get(req.id);
890
919
  if (!living) {
891
- out.push(diag("TRACE-007", "error", `REMOVED ${req.id} no existe en la spec viva`, { path: path23, line: req.line }));
920
+ out.push(diag("TRACE-007", "error", `REMOVED ${req.id} no existe en la spec viva`, { path: path25, line: req.line }));
892
921
  }
893
922
  }
894
923
  for (const rename2 of delta.renamed) {
895
924
  if (rename2.from.id !== rename2.to.id) {
896
- out.push(diag("LINT-DLT-003", "error", `RENAMED cambia el id (${rename2.from.id} \u2192 ${rename2.to.id}); los ids son inmutables`, { path: path23, line: rename2.line }));
925
+ out.push(diag("LINT-DLT-003", "error", `RENAMED cambia el id (${rename2.from.id} \u2192 ${rename2.to.id}); los ids son inmutables`, { path: path25, line: rename2.line }));
897
926
  }
898
927
  if (!livingRequirements.has(rename2.from.id)) {
899
- out.push(diag("TRACE-007", "error", `RENAMED ${rename2.from.id} no existe en la spec viva`, { path: path23, line: rename2.line }));
928
+ out.push(diag("TRACE-007", "error", `RENAMED ${rename2.from.id} no existe en la spec viva`, { path: path25, line: rename2.line }));
900
929
  }
901
930
  }
902
931
  return out;
@@ -926,7 +955,7 @@ var MERMAID_KEYWORDS = [
926
955
  "architecture-beta"
927
956
  ];
928
957
  var MERMAID_BLOCKS = /^\s*(alt|loop|opt|par|rect|critical|break|subgraph)\b/;
929
- function lintPlan(planText, path23) {
958
+ function lintPlan(planText, path25) {
930
959
  const out = [];
931
960
  const blocks = [...planText.matchAll(/```mermaid\r?\n([\s\S]*?)```/g)];
932
961
  for (const [index, block] of blocks.entries()) {
@@ -936,7 +965,7 @@ function lintPlan(planText, path23) {
936
965
  if (!MERMAID_KEYWORDS.some((keyword) => first.startsWith(keyword))) {
937
966
  out.push(
938
967
  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)}"`, {
939
- path: path23,
968
+ path: path25,
940
969
  suggestion: "Corrige el tipo del diagrama o elimina el bloque"
941
970
  })
942
971
  );
@@ -950,7 +979,7 @@ function lintPlan(planText, path23) {
950
979
  if (open !== 0) {
951
980
  out.push(
952
981
  diag("LINT-PLN-002", "error", `Diagrama mermaid ${index + 1}: faltan ${Math.abs(open)} \`end\` (bloques alt/loop/subgraph sin cerrar)`, {
953
- path: path23,
982
+ path: path25,
954
983
  suggestion: "Cierra cada bloque alt/loop/opt/par/rect/subgraph con `end`"
955
984
  })
956
985
  );
@@ -962,7 +991,7 @@ function lintPlan(planText, path23) {
962
991
  if (message.includes(";")) {
963
992
  out.push(
964
993
  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`, {
965
- path: path23,
994
+ path: path25,
966
995
  suggestion: "Sustituye `;` por `\xB7` o `,` en los mensajes de sequenceDiagram"
967
996
  })
968
997
  );
@@ -1075,15 +1104,25 @@ function checkTrace(input) {
1075
1104
  );
1076
1105
  }
1077
1106
  }
1107
+ const linkedIds = new Set(input.linked?.ids ?? []);
1108
+ const unavailableLinks = input.linked?.unavailable ?? [];
1078
1109
  for (const task of taskById.values()) {
1079
1110
  for (const c of task.covers) {
1080
- if (!livingReqs.has(c) && !deltaScenarios.has(c) && !allScenarioIds.has(c)) {
1111
+ if (!livingReqs.has(c) && !deltaScenarios.has(c) && !allScenarioIds.has(c) && !linkedIds.has(c)) {
1081
1112
  findings.push(
1082
1113
  diag("TRACE-003", "error", `La tarea ${task.id} cubre ${c}, que no existe`, {
1083
1114
  path: change.tasks?.path,
1084
- suggestion: "Corrige el id o crea el requisito/escenario"
1115
+ suggestion: unavailableLinks.length > 0 ? "Corrige el id o revisa los enlaces no disponibles (`satlas link list`)" : "Corrige el id o crea el requisito/escenario"
1085
1116
  })
1086
1117
  );
1118
+ if (unavailableLinks.length > 0) {
1119
+ findings.push(
1120
+ diag("ATLAS-LINK-003", "warning", `Referencia no resuelta: ${c} (hay ${unavailableLinks.length} enlace(s) no disponible(s): ${unavailableLinks.join(", ")})`, {
1121
+ path: change.tasks?.path,
1122
+ suggestion: "Restaura la ruta del enlace o corrige la referencia"
1123
+ })
1124
+ );
1125
+ }
1087
1126
  }
1088
1127
  }
1089
1128
  for (const dep of task.dependsOn) {
@@ -1250,12 +1289,172 @@ function planBlock(block, maxParallel) {
1250
1289
  }
1251
1290
 
1252
1291
  // src/lifecycle.ts
1292
+ import path3 from "path";
1293
+
1294
+ // src/contracts.ts
1253
1295
  import path2 from "path";
1296
+ import { parse as parseYaml4 } from "yaml";
1297
+ var CONTRACTS_DIR = "contracts";
1298
+ var OPENAPI_METHODS = ["get", "post", "put", "patch", "delete", "head", "options"];
1299
+ var SUPPORTED_HINT = "Formatos esperados: OpenAPI 3.x (.yaml/.json), GraphQL SDL (.graphql/.gql) y protobuf (.proto)";
1300
+ function lineOf(content, needle) {
1301
+ const index = content.indexOf(needle);
1302
+ if (index < 0) return 1;
1303
+ return content.slice(0, index).split("\n").length;
1304
+ }
1305
+ function formatOf(file, content) {
1306
+ const ext = path2.extname(file).toLowerCase();
1307
+ if (ext === ".proto") return "protobuf";
1308
+ if (ext === ".graphql" || ext === ".gql") return "graphql";
1309
+ if (ext === ".yaml" || ext === ".yml" || ext === ".json") {
1310
+ return /(^|\n)\s*openapi\s*:/.test(content) || /"openapi"\s*:/.test(content) ? "openapi" : "unsupported";
1311
+ }
1312
+ return "unsupported";
1313
+ }
1314
+ function parseOpenApi(file, content) {
1315
+ const findings = [];
1316
+ let data;
1317
+ try {
1318
+ data = parseYaml4(content);
1319
+ } catch (error) {
1320
+ return { operations: [], findings: [diag("ATLAS-CONTRACT-001", "error", `Contrato OpenAPI ilegible: ${error.message}`, { path: file })] };
1321
+ }
1322
+ const doc = data;
1323
+ if (!doc || typeof doc !== "object" || typeof doc.openapi !== "string" || typeof doc.paths !== "object" || doc.paths === null || Array.isArray(doc.paths)) {
1324
+ return {
1325
+ operations: [],
1326
+ 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 })]
1327
+ };
1328
+ }
1329
+ const operations = [];
1330
+ for (const [route, value] of Object.entries(doc.paths)) {
1331
+ if (!route.startsWith("/") || typeof value !== "object" || value === null) continue;
1332
+ for (const method of OPENAPI_METHODS) {
1333
+ if (method in value) {
1334
+ operations.push({ id: `${method.toUpperCase()} ${route}`, kind: "openapi", file, line: lineOf(content, route) });
1335
+ }
1336
+ }
1337
+ }
1338
+ return { operations, findings };
1339
+ }
1340
+ function parseGraphql(file, content) {
1341
+ const operations = [];
1342
+ const blockRe = /type\s+(Query|Mutation|Subscription)\s*\{([\s\S]*?)\}/g;
1343
+ for (const match of content.matchAll(blockRe)) {
1344
+ const typeName = match[1] ?? "";
1345
+ const body = match[2] ?? "";
1346
+ for (const field of body.split("\n")) {
1347
+ const fieldMatch = /^\s*([A-Za-z_][A-Za-z0-9_]*)\s*(\(|:)/.exec(field);
1348
+ if (fieldMatch) operations.push({ id: `${typeName}.${fieldMatch[1]}`, kind: "graphql", file, line: lineOf(content, field.trim()) });
1349
+ }
1350
+ }
1351
+ if (operations.length === 0) {
1352
+ return {
1353
+ operations,
1354
+ findings: [diag("ATLAS-CONTRACT-001", "error", "Contrato GraphQL inv\xE1lido: no declara consultas (type Query), mutaciones ni suscripciones", { path: file, suggestion: SUPPORTED_HINT })]
1355
+ };
1356
+ }
1357
+ return { operations, findings: [] };
1358
+ }
1359
+ function parseProtobuf(file, content) {
1360
+ const operations = [];
1361
+ const serviceRe = /service\s+([A-Za-z_][A-Za-z0-9_]*)\s*\{([\s\S]*?)\}/g;
1362
+ for (const match of content.matchAll(serviceRe)) {
1363
+ const service = match[1] ?? "";
1364
+ const body = match[2] ?? "";
1365
+ for (const rpc of body.matchAll(/rpc\s+([A-Za-z_][A-Za-z0-9_]*)/g)) {
1366
+ operations.push({ id: `${service}.${rpc[1]}`, kind: "protobuf", file, line: lineOf(content, `rpc ${rpc[1]}`) });
1367
+ }
1368
+ }
1369
+ if (operations.length === 0 && !/message\s+[A-Za-z_]/.test(content)) {
1370
+ return {
1371
+ operations,
1372
+ findings: [diag("ATLAS-CONTRACT-001", "error", "Contrato protobuf inv\xE1lido: no declara servicios ni mensajes", { path: file, suggestion: SUPPORTED_HINT })]
1373
+ };
1374
+ }
1375
+ return { operations, findings: [] };
1376
+ }
1377
+ function parseContract(file, content) {
1378
+ const format = formatOf(file, content);
1379
+ if (format === "unsupported") {
1380
+ return {
1381
+ path: file,
1382
+ format,
1383
+ operations: [],
1384
+ findings: [diag("ATLAS-CONTRACT-002", "warning", "Formato de contrato no soportado; el archivo no se interpreta ni se toca", { path: file, suggestion: SUPPORTED_HINT })]
1385
+ };
1386
+ }
1387
+ const parsed = format === "openapi" ? parseOpenApi(file, content) : format === "graphql" ? parseGraphql(file, content) : parseProtobuf(file, content);
1388
+ return { path: file, format, operations: parsed.operations, findings: parsed.findings };
1389
+ }
1390
+ async function loadContracts(changeDir) {
1391
+ const dir = path2.join(changeDir, CONTRACTS_DIR);
1392
+ const files = [];
1393
+ const operations = [];
1394
+ const findings = [];
1395
+ const entries = (await listDir(dir)).sort();
1396
+ for (const entry of entries) {
1397
+ const file = path2.join(dir, entry);
1398
+ const content = await readTextIfExists(file);
1399
+ if (content === void 0) continue;
1400
+ const parsed = parseContract(file, content);
1401
+ files.push({ path: parsed.path, format: parsed.format, operations: parsed.operations });
1402
+ operations.push(...parsed.operations);
1403
+ findings.push(...parsed.findings);
1404
+ }
1405
+ return { files, operations, findings };
1406
+ }
1407
+ function contractCoverage(change, mode) {
1408
+ const state = change.contracts;
1409
+ if (mode === "off" || !state || state.files.length === 0 || state.operations.length === 0) return [];
1410
+ const scenarioContracts = /* @__PURE__ */ new Map();
1411
+ for (const requirement of [...change.delta?.added ?? [], ...change.delta?.modified ?? []]) {
1412
+ for (const scenario of requirement.scenarios) {
1413
+ for (const ref of scenario.contracts ?? []) {
1414
+ if (!scenarioContracts.has(ref)) scenarioContracts.set(ref, scenario.id);
1415
+ }
1416
+ }
1417
+ }
1418
+ const findings = [];
1419
+ for (const operation of state.operations) {
1420
+ if (!scenarioContracts.has(operation.id)) {
1421
+ findings.push(
1422
+ diag("ATLAS-CONTRACT-003", "warning", `Operaci\xF3n sin escenario: ${operation.id}`, {
1423
+ path: operation.file,
1424
+ line: operation.line,
1425
+ suggestion: `Declara la operaci\xF3n en un escenario con \xB7 **Contrato**: ${operation.id}`
1426
+ })
1427
+ );
1428
+ }
1429
+ }
1430
+ const known = new Set(state.operations.map((operation) => operation.id));
1431
+ for (const [ref, scenarioId] of scenarioContracts) {
1432
+ if (!known.has(ref)) {
1433
+ findings.push(
1434
+ diag("ATLAS-CONTRACT-004", "warning", `Referencia rota: el escenario ${scenarioId} declara "${ref}", que no existe en el contrato`, {
1435
+ path: change.delta?.path,
1436
+ suggestion: "Corrige la referencia o a\xF1ade la operaci\xF3n al contrato"
1437
+ })
1438
+ );
1439
+ }
1440
+ }
1441
+ return findings;
1442
+ }
1443
+ function contractsAdvisory(change, cfg) {
1444
+ const state = change.contracts;
1445
+ if (!state || state.files.length === 0) return [];
1446
+ const mode = cfg.gates.contracts.mode;
1447
+ const form = state.findings;
1448
+ if (mode === "advisory") return [...form, ...contractCoverage(change, mode)];
1449
+ return form;
1450
+ }
1451
+
1452
+ // src/lifecycle.ts
1254
1453
  function verifyApproval(change, approvals, cfg, specContent) {
1255
1454
  if (cfg.gates.approval === "none") return { status: "not_required" };
1256
1455
  if ((change.meta?.overrides ?? []).some((o) => o.gate === "approval")) return { status: "overridden" };
1257
1456
  if (!change.delta) return { status: "missing" };
1258
- const key = path2.posix.join("changes", change.slug, "spec.md");
1457
+ const key = path3.posix.join("changes", change.slug, "spec.md");
1259
1458
  const approval = approvals.get(key) ?? approvals.get(`${key}`);
1260
1459
  if (!approval) return { status: "missing" };
1261
1460
  if (specContent === void 0) return { status: "valid", approvedBy: approval.by, approvedAt: approval.at };
@@ -1383,6 +1582,13 @@ function deriveState(input) {
1383
1582
  blockedBy.push("documentaci\xF3n pendiente");
1384
1583
  return { state: "reviewed", blockedBy, nextAction: next(`/satlas.docs ${change.slug}`, "Generar la documentaci\xF3n t\xE9cnica y manual del cambio", true), progress };
1385
1584
  }
1585
+ if (cfg.gates.contracts.mode === "blocking") {
1586
+ const contractFindings = contractCoverage(change, "blocking");
1587
+ if (contractFindings.length > 0) {
1588
+ blockedBy.push(`contratos con hallazgos (${contractFindings.length})`);
1589
+ return { state: "verified", blockedBy, nextAction: next(`satlas contracts ${change.slug}`, "Resolver los hallazgos de contrato antes de archivar"), progress };
1590
+ }
1591
+ }
1386
1592
  return { state: "ready", blockedBy, nextAction: next(`satlas archive ${change.slug}`, "Archivar el cambio y plegar los deltas"), progress };
1387
1593
  }
1388
1594
  function stateLabel(state) {
@@ -1404,8 +1610,8 @@ function stateLabel(state) {
1404
1610
  }
1405
1611
 
1406
1612
  // src/profiles.ts
1407
- import path3 from "path";
1408
- import { parse as parseYaml4 } from "yaml";
1613
+ import path4 from "path";
1614
+ import { parse as parseYaml5 } from "yaml";
1409
1615
  import { z as z3 } from "zod";
1410
1616
  var profileSchema = z3.object({
1411
1617
  name: z3.string().min(1),
@@ -1452,7 +1658,7 @@ async function loadProfileFile(filePath) {
1452
1658
  if (raw === void 0) return void 0;
1453
1659
  let data;
1454
1660
  try {
1455
- data = parseYaml4(raw);
1661
+ data = parseYaml5(raw);
1456
1662
  } catch {
1457
1663
  return void 0;
1458
1664
  }
@@ -1464,7 +1670,7 @@ async function loadProfilesFromDir(dir) {
1464
1670
  const out = [];
1465
1671
  for (const entry of entries) {
1466
1672
  if (!/\.ya?ml$/i.test(entry)) continue;
1467
- const profile = await loadProfileFile(path3.join(dir, entry));
1673
+ const profile = await loadProfileFile(path4.join(dir, entry));
1468
1674
  if (profile) out.push(profile);
1469
1675
  }
1470
1676
  return out;
@@ -1502,10 +1708,10 @@ async function detectProfiles(root, profiles) {
1502
1708
  return result;
1503
1709
  }
1504
1710
  async function loadDetectedBest(sddDir) {
1505
- const raw = await readTextIfExists(path3.join(sddDir, "profiles", "detected.yaml"));
1711
+ const raw = await readTextIfExists(path4.join(sddDir, "profiles", "detected.yaml"));
1506
1712
  if (raw === void 0) return void 0;
1507
1713
  try {
1508
- const data = parseYaml4(raw);
1714
+ const data = parseYaml5(raw);
1509
1715
  return typeof data?.best === "string" ? data.best : void 0;
1510
1716
  } catch {
1511
1717
  return void 0;
@@ -1542,12 +1748,12 @@ function detectionToYaml(result, generatedAt) {
1542
1748
  }
1543
1749
 
1544
1750
  // src/workspace.ts
1545
- import path5 from "path";
1751
+ import path7 from "path";
1546
1752
 
1547
1753
  // src/fixes.ts
1548
- import path4 from "path";
1754
+ import path5 from "path";
1549
1755
  import { stringify as stringifyYaml3 } from "yaml";
1550
- var LIVING_FIXES_DIR = path4.join(".sdd", "fixes");
1756
+ var LIVING_FIXES_DIR = path5.join(".sdd", "fixes");
1551
1757
  var COMMENT_RE = /<!--[\s\S]*?-->/g;
1552
1758
  function parseFixCovers(content) {
1553
1759
  const clean = content.replace(COMMENT_RE, "");
@@ -1574,7 +1780,7 @@ function coversOf(value) {
1574
1780
  function parseLivingFix(content, file) {
1575
1781
  const fm = parseFrontmatter(content, file);
1576
1782
  const data = fm.data;
1577
- const base = path4.basename(file).replace(/\.md$/i, "");
1783
+ const base = path5.basename(file).replace(/\.md$/i, "");
1578
1784
  const slug = typeof data["slug"] === "string" && data["slug"].trim() !== "" ? data["slug"].trim() : base.replace(/^\d{4}-\d{2}-/, "");
1579
1785
  const fix = {
1580
1786
  slug,
@@ -1590,14 +1796,14 @@ function parseLivingFix(content, file) {
1590
1796
  return fix;
1591
1797
  }
1592
1798
  async function archivedFixes(root, known) {
1593
- const archiveDir = path4.join(root, ".sdd", "changes", "archive");
1799
+ const archiveDir = path5.join(root, ".sdd", "changes", "archive");
1594
1800
  const fixes = [];
1595
1801
  for (const entry of await listDirs(archiveDir)) {
1596
- const dir = path4.join(archiveDir, entry);
1597
- const fixFile = path4.join(dir, "fix.md");
1802
+ const dir = path5.join(archiveDir, entry);
1803
+ const fixFile = path5.join(dir, "fix.md");
1598
1804
  const fixRaw = await readTextIfExists(fixFile);
1599
1805
  if (fixRaw === void 0) continue;
1600
- const metaFile = path4.join(dir, "meta.yaml");
1806
+ const metaFile = path5.join(dir, "meta.yaml");
1601
1807
  const metaRaw = await readTextIfExists(metaFile);
1602
1808
  const meta = metaRaw !== void 0 ? parseChangeMeta(metaRaw, metaFile).meta : void 0;
1603
1809
  if (meta?.lane !== "fix") continue;
@@ -1620,11 +1826,11 @@ async function archivedFixes(root, known) {
1620
1826
  return fixes;
1621
1827
  }
1622
1828
  async function loadLivingFixes(root) {
1623
- const dir = path4.join(root, LIVING_FIXES_DIR);
1829
+ const dir = path5.join(root, LIVING_FIXES_DIR);
1624
1830
  const entries = (await listDir(dir)).filter((entry) => entry.toLowerCase().endsWith(".md")).sort();
1625
1831
  const fixes = [];
1626
1832
  for (const entry of entries) {
1627
- const file = path4.join(dir, entry);
1833
+ const file = path5.join(dir, entry);
1628
1834
  const content = await readTextIfExists(file);
1629
1835
  if (content === void 0) continue;
1630
1836
  fixes.push(parseLivingFix(content, file));
@@ -1633,10 +1839,10 @@ async function loadLivingFixes(root) {
1633
1839
  return fixes.sort((a, b) => b.date.localeCompare(a.date) || b.slug.localeCompare(a.slug));
1634
1840
  }
1635
1841
  async function writeLivingFix(root, input) {
1636
- const dir = path4.join(root, LIVING_FIXES_DIR);
1842
+ const dir = path5.join(root, LIVING_FIXES_DIR);
1637
1843
  const month = input.date.slice(0, 7);
1638
- const file = path4.join(dir, `${month}-${input.slug}.md`);
1639
- const relativePath = toPosix(path4.relative(root, file));
1844
+ const file = path5.join(dir, `${month}-${input.slug}.md`);
1845
+ const relativePath = toPosix(path5.relative(root, file));
1640
1846
  if (await exists(file)) return { file, relativePath, created: false };
1641
1847
  const data = {
1642
1848
  slug: input.slug,
@@ -1656,35 +1862,170 @@ ${body}
1656
1862
  return { file, relativePath, created: true };
1657
1863
  }
1658
1864
 
1865
+ // src/links.ts
1866
+ import path6 from "path";
1867
+ import { parse as parseYaml6, stringify as stringifyYaml4 } from "yaml";
1868
+ var LINKS_FILE = "links.yaml";
1869
+ function parseLinksDocument(raw, file) {
1870
+ let data;
1871
+ try {
1872
+ data = parseYaml6(raw);
1873
+ } catch (error) {
1874
+ return { diagnostics: [diag("ATLAS-LINK-001", "error", `links.yaml ilegible: ${error.message}`, { path: file })] };
1875
+ }
1876
+ const doc = data;
1877
+ if (!doc || typeof doc !== "object" || !Array.isArray(doc.links)) {
1878
+ return { diagnostics: [diag("ATLAS-LINK-001", "error", "links.yaml inv\xE1lido: falta la lista de enlaces", { path: file })] };
1879
+ }
1880
+ const links = [];
1881
+ const diagnostics = [];
1882
+ for (const item of doc.links) {
1883
+ const entry = item;
1884
+ if (typeof entry?.name !== "string" || typeof entry?.path !== "string" || entry.name.trim() === "" || entry.path.trim() === "") {
1885
+ diagnostics.push(diag("ATLAS-LINK-001", "error", "Enlace inv\xE1lido en links.yaml: cada enlace necesita nombre y ruta", { path: file }));
1886
+ continue;
1887
+ }
1888
+ links.push({ name: entry.name.trim(), path: entry.path.trim() });
1889
+ }
1890
+ return { document: { schema_version: typeof doc.schema_version === "number" ? doc.schema_version : 1, links }, diagnostics };
1891
+ }
1892
+ async function readLinks(root) {
1893
+ const file = path6.join(root, ".sdd", LINKS_FILE);
1894
+ const raw = await readTextIfExists(file);
1895
+ if (raw === void 0) return { document: { schema_version: 1, links: [] }, file, diagnostics: [] };
1896
+ const parsed = parseLinksDocument(raw, file);
1897
+ return { document: parsed.document ?? { schema_version: 1, links: [] }, file, diagnostics: parsed.diagnostics };
1898
+ }
1899
+ async function writeLinks(root, document) {
1900
+ const file = path6.join(root, ".sdd", LINKS_FILE);
1901
+ const lines = ["# Enlaces a otros proyectos (specs compartidas en solo lectura). Generado por `satlas link`.", stringifyYaml4(document, { lineWidth: 120 }).trimEnd(), ""];
1902
+ await writeText(file, lines.join("\n"));
1903
+ }
1904
+ function resolveLinkPath(root, linkPath) {
1905
+ return path6.isAbsolute(linkPath) ? path6.normalize(linkPath) : path6.resolve(root, linkPath);
1906
+ }
1907
+ async function linkedSpecs(absPath) {
1908
+ const specsDir = path6.join(absPath, ".sdd", "specs");
1909
+ const specs = [];
1910
+ for (const domain of await listDirs(specsDir)) {
1911
+ const file = path6.join(specsDir, domain, "spec.md");
1912
+ const content = await readTextIfExists(file);
1913
+ if (content === void 0) continue;
1914
+ specs.push({ domain, path: file, spec: parseSpecFile(content, file) });
1915
+ }
1916
+ return specs;
1917
+ }
1918
+ async function loadLinks(root) {
1919
+ const { document, diagnostics } = await readLinks(root);
1920
+ const entries = [];
1921
+ const specs = [];
1922
+ const unavailable = [];
1923
+ for (const entry of document.links) {
1924
+ const abs = resolveLinkPath(root, entry.path);
1925
+ const sddDir = path6.join(abs, ".sdd");
1926
+ const available = await isDirectory(sddDir) && await isDirectory(path6.join(sddDir, "specs"));
1927
+ if (!available) {
1928
+ unavailable.push(entry.name);
1929
+ entries.push({ name: entry.name, path: abs, available: false, requirements: 0, domains: [], error: "el enlace no est\xE1 disponible" });
1930
+ continue;
1931
+ }
1932
+ const linked = await linkedSpecs(abs);
1933
+ specs.push(...linked);
1934
+ entries.push({
1935
+ name: entry.name,
1936
+ path: abs,
1937
+ available: true,
1938
+ requirements: linked.reduce((total, spec) => total + spec.spec.requirements.length, 0),
1939
+ domains: linked.map((spec) => spec.domain)
1940
+ });
1941
+ }
1942
+ void diagnostics;
1943
+ return { entries, specs, unavailable };
1944
+ }
1945
+ async function addLink(root, opts) {
1946
+ const abs = resolveLinkPath(root, opts.path);
1947
+ const sddDir = path6.join(abs, ".sdd");
1948
+ if (!await isDirectory(sddDir)) {
1949
+ return {
1950
+ diagnostics: [
1951
+ diag("ATLAS-LINK-002", "error", `El enlace no es un proyecto inicializado: ${abs}`, {
1952
+ suggestion: "Indica la ruta de un proyecto con su estado inicializado (satlas init)"
1953
+ })
1954
+ ]
1955
+ };
1956
+ }
1957
+ const { document } = await readLinks(root);
1958
+ let name = opts.name?.trim();
1959
+ if (!name) {
1960
+ const linkedConfig = await loadConfig(sddDir);
1961
+ name = linkedConfig.config.project.name || path6.basename(abs);
1962
+ }
1963
+ const entry = { name, path: toPosix(abs) };
1964
+ const existing = document.links.findIndex((link) => link.name === name || resolveLinkPath(root, link.path) === abs);
1965
+ if (existing >= 0) document.links[existing] = entry;
1966
+ else document.links.push(entry);
1967
+ await writeLinks(root, document);
1968
+ return { entry, diagnostics: [] };
1969
+ }
1970
+ async function removeLink(root, ref) {
1971
+ const { document } = await readLinks(root);
1972
+ const index = document.links.findIndex((link) => link.name === ref || link.path === ref || resolveLinkPath(root, link.path) === resolveLinkPath(root, ref));
1973
+ if (index < 0) {
1974
+ return {
1975
+ diagnostics: [diag("ATLAS-LINK-003", "warning", `No existe un enlace "${ref}"`, { suggestion: "Consulta los enlaces con `satlas link list`" })]
1976
+ };
1977
+ }
1978
+ const [removed] = document.links.splice(index, 1);
1979
+ await writeLinks(root, document);
1980
+ return { removed: removed?.name, diagnostics: [] };
1981
+ }
1982
+ function linkedRequirementIds(state) {
1983
+ if (!state) return [];
1984
+ const ids = [];
1985
+ for (const spec of state.specs) {
1986
+ for (const requirement of spec.spec.requirements) ids.push(requirement.id);
1987
+ }
1988
+ return ids;
1989
+ }
1990
+ function linkedTraceInput(workspace) {
1991
+ if (!workspace.links || workspace.links.entries.length === 0) return void 0;
1992
+ return { ids: linkedRequirementIds(workspace.links), unavailable: workspace.links.unavailable };
1993
+ }
1994
+ async function ensureLinksFile(root) {
1995
+ const file = path6.join(root, ".sdd", LINKS_FILE);
1996
+ if (await exists(file)) return;
1997
+ await writeLinks(root, { schema_version: 1, links: [] });
1998
+ }
1999
+
1659
2000
  // src/workspace.ts
1660
2001
  var SDD_DIR = ".sdd";
1661
2002
  async function findWorkspaceRoot(start) {
1662
- let current = path5.resolve(start);
2003
+ let current = path7.resolve(start);
1663
2004
  for (let i = 0; i < 40; i += 1) {
1664
- if (await isDirectory(path5.join(current, SDD_DIR))) return current;
1665
- const parent = path5.dirname(current);
2005
+ if (await isDirectory(path7.join(current, SDD_DIR))) return current;
2006
+ const parent = path7.dirname(current);
1666
2007
  if (parent === current) return void 0;
1667
2008
  current = parent;
1668
2009
  }
1669
2010
  return void 0;
1670
2011
  }
1671
2012
  async function loadApprovals(sddDir) {
1672
- const file = path5.join(sddDir, "approvals.yaml");
2013
+ const file = path7.join(sddDir, "approvals.yaml");
1673
2014
  const raw = await readTextIfExists(file);
1674
2015
  if (raw === void 0) return { byArtifact: /* @__PURE__ */ new Map(), diagnostics: [] };
1675
2016
  const parsed = parseApprovals(raw, file);
1676
2017
  const map = /* @__PURE__ */ new Map();
1677
2018
  for (const a of parsed.approvals?.approvals ?? []) {
1678
- map.set(a.artifact.split(path5.sep).join("/"), { hash: a.artifactHash, by: a.approvedBy, at: a.approvedAt });
2019
+ map.set(a.artifact.split(path7.sep).join("/"), { hash: a.artifactHash, by: a.approvedBy, at: a.approvedAt });
1679
2020
  }
1680
2021
  return { byArtifact: map, diagnostics: parsed.diagnostics };
1681
2022
  }
1682
2023
  async function loadSpecs(sddDir) {
1683
- const specsDir = path5.join(sddDir, "specs");
2024
+ const specsDir = path7.join(sddDir, "specs");
1684
2025
  const domains = await listDirs(specsDir);
1685
2026
  const out = [];
1686
2027
  for (const domain of domains) {
1687
- const file = path5.join(specsDir, domain, "spec.md");
2028
+ const file = path7.join(specsDir, domain, "spec.md");
1688
2029
  if (!await exists(file)) continue;
1689
2030
  const content = await readText(file);
1690
2031
  out.push({ domain, path: file, spec: parseSpecFile(content, file) });
@@ -1692,10 +2033,10 @@ async function loadSpecs(sddDir) {
1692
2033
  return out;
1693
2034
  }
1694
2035
  async function loadChange(root, slug, relDir) {
1695
- const dir = path5.join(root, SDD_DIR, "changes", relDir ?? slug);
2036
+ const dir = path7.join(root, SDD_DIR, "changes", relDir ?? slug);
1696
2037
  const diagnostics = [];
1697
2038
  const change = { slug, dir, diagnostics };
1698
- const metaFile = path5.join(dir, "meta.yaml");
2039
+ const metaFile = path7.join(dir, "meta.yaml");
1699
2040
  const metaRaw = await readTextIfExists(metaFile);
1700
2041
  if (metaRaw === void 0) {
1701
2042
  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" }));
@@ -1704,7 +2045,7 @@ async function loadChange(root, slug, relDir) {
1704
2045
  diagnostics.push(...parsed.diagnostics);
1705
2046
  if (parsed.meta) change.meta = parsed.meta;
1706
2047
  }
1707
- const deltaFile = path5.join(dir, "spec.md");
2048
+ const deltaFile = path7.join(dir, "spec.md");
1708
2049
  const deltaRaw = await readTextIfExists(deltaFile);
1709
2050
  if (deltaRaw !== void 0) {
1710
2051
  change.delta = parseDelta(deltaRaw, deltaFile);
@@ -1712,32 +2053,32 @@ async function loadChange(root, slug, relDir) {
1712
2053
  } else {
1713
2054
  diagnostics.push(diag("ATLAS-FILES-002", "warning", `El cambio "${slug}" no tiene spec.md (delta)`, { path: deltaFile }));
1714
2055
  }
1715
- const planFile = path5.join(dir, "plan.md");
2056
+ const planFile = path7.join(dir, "plan.md");
1716
2057
  if (await exists(planFile)) change.planPath = planFile;
1717
- const reviewFile = path5.join(dir, "review.md");
2058
+ const reviewFile = path7.join(dir, "review.md");
1718
2059
  if (await exists(reviewFile)) change.reviewPath = reviewFile;
1719
- const presentationFile = path5.join(dir, "presentation", "index.html");
2060
+ const presentationFile = path7.join(dir, "presentation", "index.html");
1720
2061
  if (await exists(presentationFile)) change.presentationPath = presentationFile;
1721
- const tasksFile = path5.join(dir, "tasks.md");
2062
+ const tasksFile = path7.join(dir, "tasks.md");
1722
2063
  const tasksRaw = await readTextIfExists(tasksFile);
1723
2064
  if (tasksRaw !== void 0) {
1724
2065
  change.tasks = parseTasksFile(tasksRaw, tasksFile);
1725
2066
  diagnostics.push(...change.tasks.diagnostics);
1726
2067
  }
1727
- const verifyFile = path5.join(dir, "verify.md");
2068
+ const verifyFile = path7.join(dir, "verify.md");
1728
2069
  const verifyRaw = await readTextIfExists(verifyFile);
1729
2070
  if (verifyRaw !== void 0) {
1730
2071
  change.verify = parseVerifyFile(verifyRaw, verifyFile);
1731
2072
  diagnostics.push(...change.verify.diagnostics);
1732
2073
  }
1733
- const fixFile = path5.join(dir, "fix.md");
2074
+ const fixFile = path7.join(dir, "fix.md");
1734
2075
  const fixRaw = await readTextIfExists(fixFile);
1735
2076
  if (fixRaw !== void 0) {
1736
2077
  change.fix = parseVerifyFile(fixRaw, fixFile);
1737
2078
  diagnostics.push(...change.fix.diagnostics);
1738
2079
  change.fixCovers = parseFixCovers(fixRaw);
1739
2080
  }
1740
- const clarifyFile = path5.join(dir, "clarify.md");
2081
+ const clarifyFile = path7.join(dir, "clarify.md");
1741
2082
  const clarifyRaw = await readTextIfExists(clarifyFile);
1742
2083
  if (clarifyRaw !== void 0) {
1743
2084
  change.clarify = parseClarify(clarifyRaw, clarifyFile);
@@ -1746,21 +2087,23 @@ async function loadChange(root, slug, relDir) {
1746
2087
  }
1747
2088
  const docsPaths = [];
1748
2089
  for (const name of ["tecnica.md", "manual.md"]) {
1749
- const docFile = path5.join(dir, "docs", name);
2090
+ const docFile = path7.join(dir, "docs", name);
1750
2091
  if (await exists(docFile)) docsPaths.push(docFile);
1751
2092
  }
1752
2093
  if (docsPaths.length > 0) change.docsPaths = docsPaths;
1753
- const mockupManifest = path5.join(dir, "mockups", "manifest.yaml");
2094
+ const contracts = await loadContracts(dir);
2095
+ if (contracts.files.length > 0) change.contracts = contracts;
2096
+ const mockupManifest = path7.join(dir, "mockups", "manifest.yaml");
1754
2097
  if (await exists(mockupManifest)) change.mockupManifestPath = mockupManifest;
1755
2098
  return change;
1756
2099
  }
1757
2100
  async function listChangeSlugs(root, opts = {}) {
1758
- const changesDir = path5.join(root, SDD_DIR, "changes");
2101
+ const changesDir = path7.join(root, SDD_DIR, "changes");
1759
2102
  const dirs = await listDirs(changesDir);
1760
2103
  return dirs.filter((d) => opts.includeArchived ? true : d !== "archive");
1761
2104
  }
1762
2105
  async function loadWorkspace(root) {
1763
- const sddDir = path5.join(root, SDD_DIR);
2106
+ const sddDir = path7.join(root, SDD_DIR);
1764
2107
  const { config, diagnostics: configDiags } = await loadConfig(sddDir);
1765
2108
  const diagnostics = [...configDiags];
1766
2109
  const specs = await loadSpecs(sddDir);
@@ -1773,20 +2116,24 @@ async function loadWorkspace(root) {
1773
2116
  diagnostics.push(...change.diagnostics);
1774
2117
  }
1775
2118
  const archived = [];
1776
- const archivedDir = path5.join(sddDir, "changes", "archive");
2119
+ const archivedDir = path7.join(sddDir, "changes", "archive");
1777
2120
  if (await isDirectory(archivedDir)) {
1778
2121
  for (const entry of await listDirs(archivedDir)) {
1779
- archived.push(await loadChange(root, entry.replace(/^\d{4}-\d{2}-/, ""), path5.join("archive", entry)));
2122
+ archived.push(await loadChange(root, entry.replace(/^\d{4}-\d{2}-/, ""), path7.join("archive", entry)));
1780
2123
  }
1781
2124
  }
1782
- return { workspace: { root, sddDir, specs, changes, archived, diagnostics }, config };
2125
+ const workspace = { root, sddDir, specs, changes, archived, diagnostics };
2126
+ if (await exists(path7.join(sddDir, LINKS_FILE))) {
2127
+ workspace.links = await loadLinks(root);
2128
+ }
2129
+ return { workspace, config };
1783
2130
  }
1784
2131
  async function ensureSddDirs(sddDir) {
1785
2132
  const created = [];
1786
- const dirs = ["specs", "changes", "runs", "metrics", "fixes", path5.join("profiles", "custom")];
2133
+ const dirs = ["specs", "changes", "runs", "metrics", "fixes", path7.join("profiles", "custom")];
1787
2134
  const { ensureDir: ensureDir2 } = await import("./fsx-VF2P7ALA.js");
1788
2135
  for (const d of dirs) {
1789
- const full = path5.join(sddDir, d);
2136
+ const full = path7.join(sddDir, d);
1790
2137
  if (!await exists(full)) {
1791
2138
  await ensureDir2(full);
1792
2139
  created.push(full);
@@ -1796,7 +2143,7 @@ async function ensureSddDirs(sddDir) {
1796
2143
  }
1797
2144
 
1798
2145
  // src/templates.ts
1799
- import { stringify as stringifyYaml4 } from "yaml";
2146
+ import { stringify as stringifyYaml5 } from "yaml";
1800
2147
  var ES = {
1801
2148
  constitution: `# Constituci\xF3n del proyecto
1802
2149
 
@@ -2188,7 +2535,7 @@ function changeMetaYaml(meta) {
2188
2535
  if (meta.owner) doc["owner"] = meta.owner;
2189
2536
  if (meta.mockups) doc["mockups"] = meta.mockups;
2190
2537
  return `# Estado del cambio. La fase se DERIVA de los artefactos; aqu\xED solo hechos.
2191
- ` + stringifyYaml4(doc, { lineWidth: 120 });
2538
+ ` + stringifyYaml5(doc, { lineWidth: 120 });
2192
2539
  }
2193
2540
  function indexMarkdown(input) {
2194
2541
  const es = input.language !== "en";
@@ -2226,8 +2573,8 @@ ${input.archived} change(s) in \`changes/archive/\`.`);
2226
2573
  }
2227
2574
 
2228
2575
  // src/github.ts
2229
- import path6 from "path";
2230
- import { parse as parseYaml5 } from "yaml";
2576
+ import path8 from "path";
2577
+ import { parse as parseYaml7 } from "yaml";
2231
2578
 
2232
2579
  // src/exec.ts
2233
2580
  import { execFile } from "child_process";
@@ -2371,8 +2718,8 @@ function issueBody(input) {
2371
2718
  lines.push(`- Siguiente: \`${input.next}\` \u2014 ${input.nextDescription}`);
2372
2719
  lines.push("");
2373
2720
  lines.push("### Artefactos");
2374
- lines.push(`- Spec: \`${path6.posix.join(".sdd", "changes", change.slug, "spec.md")}\``);
2375
- if (change.planPath) lines.push(`- Plan: \`${path6.posix.join(".sdd", "changes", change.slug, "plan.md")}\``);
2721
+ lines.push(`- Spec: \`${path8.posix.join(".sdd", "changes", change.slug, "spec.md")}\``);
2722
+ if (change.planPath) lines.push(`- Plan: \`${path8.posix.join(".sdd", "changes", change.slug, "plan.md")}\``);
2376
2723
  if (change.tasks) lines.push(`- Tareas: ${change.tasks.counts.done}/${change.tasks.counts.total}`);
2377
2724
  if (change.verify) lines.push(`- Evidencia registrada: ${change.verify.evidence.length}`);
2378
2725
  lines.push("");
@@ -2440,14 +2787,14 @@ async function commentIssue(root, number, body, runner = defaultRunner) {
2440
2787
  return result.ok ? [] : [diag("ATLAS-GH-006", "error", `No se pudo comentar el issue #${number}: ${(result.stderr || result.stdout).trim().slice(0, 300)}`)];
2441
2788
  }
2442
2789
  async function patchChangeMeta(root, slug, patch) {
2443
- const file = path6.join(path6.resolve(root), ".sdd", "changes", slug, "meta.yaml");
2790
+ const file = path8.join(path8.resolve(root), ".sdd", "changes", slug, "meta.yaml");
2444
2791
  const raw = await readTextIfExists(file);
2445
2792
  if (raw === void 0) {
2446
- return { path: file, diagnostics: [diag("ATLAS-GH-007", "error", `No existe ${path6.relative(root, file)}`)] };
2793
+ return { path: file, diagnostics: [diag("ATLAS-GH-007", "error", `No existe ${path8.relative(root, file)}`)] };
2447
2794
  }
2448
2795
  let data;
2449
2796
  try {
2450
- data = parseYaml5(raw) ?? {};
2797
+ data = parseYaml7(raw) ?? {};
2451
2798
  } catch (err) {
2452
2799
  return { path: file, diagnostics: [diag("ATLAS-GH-007", "error", `meta.yaml inv\xE1lido: ${err.message}`)] };
2453
2800
  }
@@ -2497,7 +2844,7 @@ async function approveFromGithub(opts) {
2497
2844
  const { signApproval: signApproval2 } = await import("./approvals-2PO6223I.js");
2498
2845
  const signed = await signApproval2({
2499
2846
  root: opts.root,
2500
- artifact: path6.posix.join("changes", opts.slug, "spec.md"),
2847
+ artifact: path8.posix.join("changes", opts.slug, "spec.md"),
2501
2848
  by,
2502
2849
  channel: "tracker",
2503
2850
  note: `Aprobado v\xEDa GitHub issue #${number} (etiqueta "${label}")`
@@ -2557,7 +2904,7 @@ async function syncGithubIssue(root, slug, opts = {}) {
2557
2904
  }
2558
2905
 
2559
2906
  // src/evidence.ts
2560
- import path7 from "path";
2907
+ import path9 from "path";
2561
2908
  var SCENARIO_HEAD_RE2 = /^###\s+(REQ-[A-Z0-9-]+-S\d+)\b/;
2562
2909
  function locateBlocks(content) {
2563
2910
  const normalized = content.replace(/\r\n?/g, "\n");
@@ -2595,10 +2942,10 @@ function buildEvidenceYaml(evidence) {
2595
2942
  return lines.join("\n");
2596
2943
  }
2597
2944
  async function recordEvidence(opts) {
2598
- const root = path7.resolve(opts.root);
2945
+ const root = path9.resolve(opts.root);
2599
2946
  const slug = opts.slug;
2600
2947
  const fileKind = opts.file ?? "verify";
2601
- const file = path7.join(root, ".sdd", "changes", slug, fileKind === "fix" ? "fix.md" : "verify.md");
2948
+ const file = path9.join(root, ".sdd", "changes", slug, fileKind === "fix" ? "fix.md" : "verify.md");
2602
2949
  const diagnostics = [];
2603
2950
  const scenario = opts.scenario.toUpperCase();
2604
2951
  if (!SCENARIO_ID_RE.test(scenario)) {
@@ -2695,7 +3042,7 @@ function evidenceSummary(content) {
2695
3042
  }
2696
3043
 
2697
3044
  // src/runs.ts
2698
- import path8 from "path";
3045
+ import path10 from "path";
2699
3046
  import { randomBytes } from "crypto";
2700
3047
  var RUN_EVENT_TYPES = [
2701
3048
  "run_started",
@@ -2721,7 +3068,7 @@ function generateRunId(now = /* @__PURE__ */ new Date()) {
2721
3068
  return `${iso.slice(0, 8)}-${iso.slice(8, 14)}-${randomBytes(3).toString("hex")}`;
2722
3069
  }
2723
3070
  function runsDir(root) {
2724
- return path8.join(path8.resolve(root), ".sdd", "runs");
3071
+ return path10.join(path10.resolve(root), ".sdd", "runs");
2725
3072
  }
2726
3073
  async function createRun(root, slug, phase, inputs, now = /* @__PURE__ */ new Date()) {
2727
3074
  const runId = generateRunId(now);
@@ -2734,25 +3081,25 @@ async function createRun(root, slug, phase, inputs, now = /* @__PURE__ */ new Da
2734
3081
  updatedAt: localStamp(now)
2735
3082
  };
2736
3083
  if (inputs) state.inputs = inputs;
2737
- const dir = path8.join(runsDir(root), runId);
3084
+ const dir = path10.join(runsDir(root), runId);
2738
3085
  await ensureDir(dir);
2739
- await writeText(path8.join(dir, "state.json"), `${JSON.stringify(state, null, 2)}
3086
+ await writeText(path10.join(dir, "state.json"), `${JSON.stringify(state, null, 2)}
2740
3087
  `);
2741
- await writeText(path8.join(dir, "events.jsonl"), "");
3088
+ await writeText(path10.join(dir, "events.jsonl"), "");
2742
3089
  await appendRunEvent(root, runId, "run_started", { slug, phase });
2743
3090
  return state;
2744
3091
  }
2745
3092
  async function appendRunEvent(root, runId, type, data, now = /* @__PURE__ */ new Date()) {
2746
3093
  const event = { eventId: randomBytes(4).toString("hex"), at: localStamp(now), type };
2747
3094
  if (data) event.data = data;
2748
- const file = path8.join(runsDir(root), runId, "events.jsonl");
3095
+ const file = path10.join(runsDir(root), runId, "events.jsonl");
2749
3096
  const previous = await readTextIfExists(file) ?? "";
2750
3097
  await writeText(file, `${previous}${JSON.stringify(event)}
2751
3098
  `);
2752
3099
  return event;
2753
3100
  }
2754
3101
  async function updateRunStatus(root, runId, status, now = /* @__PURE__ */ new Date()) {
2755
- const file = path8.join(runsDir(root), runId, "state.json");
3102
+ const file = path10.join(runsDir(root), runId, "state.json");
2756
3103
  const raw = await readTextIfExists(file);
2757
3104
  if (raw === void 0) return;
2758
3105
  const state = JSON.parse(raw);
@@ -2762,13 +3109,13 @@ async function updateRunStatus(root, runId, status, now = /* @__PURE__ */ new Da
2762
3109
  `);
2763
3110
  }
2764
3111
  async function readRun(root, runId) {
2765
- const dir = path8.join(runsDir(root), runId);
2766
- const stateRaw = await readTextIfExists(path8.join(dir, "state.json"));
3112
+ const dir = path10.join(runsDir(root), runId);
3113
+ const stateRaw = await readTextIfExists(path10.join(dir, "state.json"));
2767
3114
  if (stateRaw === void 0) {
2768
3115
  return { diagnostics: [diag("ATLAS-RUN-001", "error", `No existe el run ${runId}`, { path: dir })] };
2769
3116
  }
2770
3117
  const state = JSON.parse(stateRaw);
2771
- const eventsRaw = await readTextIfExists(path8.join(dir, "events.jsonl")) ?? "";
3118
+ const eventsRaw = await readTextIfExists(path10.join(dir, "events.jsonl")) ?? "";
2772
3119
  const events = [];
2773
3120
  for (const line of eventsRaw.split("\n")) {
2774
3121
  if (line.trim() === "") continue;
@@ -2785,7 +3132,7 @@ async function listRuns(root, filter = {}) {
2785
3132
  const ids = await listDirs(dir);
2786
3133
  const out = [];
2787
3134
  for (const id of ids) {
2788
- const raw = await readTextIfExists(path8.join(dir, id, "state.json"));
3135
+ const raw = await readTextIfExists(path10.join(dir, id, "state.json"));
2789
3136
  if (raw === void 0) continue;
2790
3137
  try {
2791
3138
  const state = JSON.parse(raw);
@@ -2799,11 +3146,11 @@ async function listRuns(root, filter = {}) {
2799
3146
  }
2800
3147
 
2801
3148
  // src/analyze.ts
2802
- import path11 from "path";
3149
+ import path13 from "path";
2803
3150
 
2804
3151
  // src/mockups.ts
2805
- import path9 from "path";
2806
- import { parse as parseYaml6, stringify as stringifyYaml5 } from "yaml";
3152
+ import path11 from "path";
3153
+ import { parse as parseYaml8, stringify as stringifyYaml6 } from "yaml";
2807
3154
  import { z as z4 } from "zod";
2808
3155
  var mockupManifestSchema = z4.object({
2809
3156
  schema_version: z4.number().int().positive().default(1),
@@ -2828,20 +3175,20 @@ var mockupManifestSchema = z4.object({
2828
3175
  });
2829
3176
  var TOKEN_CANDIDATES = ["design/tokens.json", "DESIGN.md", "design/DESIGN.md", ".sdd/design/tokens.json"];
2830
3177
  function mockupsDir(root, slug) {
2831
- return path9.join(path9.resolve(root), ".sdd", "changes", slug, "mockups");
3178
+ return path11.join(path11.resolve(root), ".sdd", "changes", slug, "mockups");
2832
3179
  }
2833
3180
  async function tokensFile(root) {
2834
3181
  for (const candidate of TOKEN_CANDIDATES) {
2835
- const abs = path9.join(root, candidate);
3182
+ const abs = path11.join(root, candidate);
2836
3183
  if (await exists(abs)) return candidate;
2837
3184
  }
2838
3185
  return void 0;
2839
3186
  }
2840
3187
  async function computeInputsHash(root, change) {
2841
- const deltaPath = path9.join(change.dir, "spec.md");
3188
+ const deltaPath = path11.join(change.dir, "spec.md");
2842
3189
  const delta = await readTextIfExists(deltaPath) ?? "";
2843
3190
  const tokens = await tokensFile(root);
2844
- const tokensContent = tokens ? await readTextIfExists(path9.join(root, tokens)) ?? "" : "";
3191
+ const tokensContent = tokens ? await readTextIfExists(path11.join(root, tokens)) ?? "" : "";
2845
3192
  return artifactHash(`${delta}
2846
3193
  ---tokens---
2847
3194
  ${tokensContent}`);
@@ -2866,12 +3213,12 @@ function planMockups(change, platform = "web") {
2866
3213
  return { platform, screens };
2867
3214
  }
2868
3215
  async function readMockupManifest(root, slug) {
2869
- const file = path9.join(mockupsDir(root, slug), "manifest.yaml");
3216
+ const file = path11.join(mockupsDir(root, slug), "manifest.yaml");
2870
3217
  const raw = await readTextIfExists(file);
2871
3218
  if (raw === void 0) return { diagnostics: [] };
2872
3219
  let data;
2873
3220
  try {
2874
- data = parseYaml6(raw);
3221
+ data = parseYaml8(raw);
2875
3222
  } catch (err) {
2876
3223
  return { path: file, diagnostics: [diag("ATLAS-MKP-002", "error", `manifest.yaml inv\xE1lido: ${err.message}`, { path: file })] };
2877
3224
  }
@@ -2958,7 +3305,7 @@ async function checkMockups(root, slug, change) {
2958
3305
  const { manifest, path: manifestPath, diagnostics } = await readMockupManifest(root, slug);
2959
3306
  const findings = [...diagnostics];
2960
3307
  if (!manifest || !manifestPath) {
2961
- return { findings: [...findings, diag("LINT-MKP-004", "error", "No existe mockups/manifest.yaml", { path: path9.join(dir, "manifest.yaml") })], stale: false };
3308
+ return { findings: [...findings, diag("LINT-MKP-004", "error", "No existe mockups/manifest.yaml", { path: path11.join(dir, "manifest.yaml") })], stale: false };
2962
3309
  }
2963
3310
  const known = /* @__PURE__ */ new Set();
2964
3311
  for (const req of [...change.delta?.added ?? [], ...change.delta?.modified ?? []]) {
@@ -2966,12 +3313,12 @@ async function checkMockups(root, slug, change) {
2966
3313
  }
2967
3314
  findings.push(...lintMockupManifest(manifest, known, manifestPath));
2968
3315
  for (const screen of manifest.screens) {
2969
- const html = await readTextIfExists(path9.join(dir, screen.file));
3316
+ const html = await readTextIfExists(path11.join(dir, screen.file));
2970
3317
  if (html === void 0) {
2971
- findings.push(diag("ATLAS-MKP-004", "error", `Falta el archivo del mockup: ${screen.file}`, { path: path9.join(dir, screen.file) }));
3318
+ findings.push(diag("ATLAS-MKP-004", "error", `Falta el archivo del mockup: ${screen.file}`, { path: path11.join(dir, screen.file) }));
2972
3319
  continue;
2973
3320
  }
2974
- findings.push(...lintMockupHtml(html, path9.join(dir, screen.file)));
3321
+ findings.push(...lintMockupHtml(html, path11.join(dir, screen.file)));
2975
3322
  }
2976
3323
  const inputsHash = await computeInputsHash(root, change);
2977
3324
  const stale = manifest.inputsHash !== void 0 && manifest.inputsHash !== inputsHash;
@@ -2986,7 +3333,7 @@ async function checkMockups(root, slug, change) {
2986
3333
  return { findings, stale, manifest, manifestPath };
2987
3334
  }
2988
3335
  async function writeMockupPlan(root, slug, plan, now = /* @__PURE__ */ new Date()) {
2989
- const file = path9.join(mockupsDir(root, slug), "plan.yaml");
3336
+ const file = path11.join(mockupsDir(root, slug), "plan.yaml");
2990
3337
  if (await exists(file)) return file;
2991
3338
  const doc = {
2992
3339
  schema_version: 1,
@@ -2994,11 +3341,11 @@ async function writeMockupPlan(root, slug, plan, now = /* @__PURE__ */ new Date(
2994
3341
  platform: plan.platform,
2995
3342
  screens: plan.screens.map((s) => ({ id: s.id, title: s.title, file: s.file, illustrates: s.illustrates, states: s.states, breakpoints: s.breakpoints }))
2996
3343
  };
2997
- await writeText(file, stringifyYaml5(doc, { lineWidth: 120 }));
3344
+ await writeText(file, stringifyYaml6(doc, { lineWidth: 120 }));
2998
3345
  return file;
2999
3346
  }
3000
3347
  async function writeMockupManifest(root, slug, plan, inputsHash, now = /* @__PURE__ */ new Date()) {
3001
- const file = path9.join(mockupsDir(root, slug), "manifest.yaml");
3348
+ const file = path11.join(mockupsDir(root, slug), "manifest.yaml");
3002
3349
  if (await exists(file)) return file;
3003
3350
  const doc = {
3004
3351
  schema_version: 1,
@@ -3010,16 +3357,16 @@ async function writeMockupManifest(root, slug, plan, inputsHash, now = /* @__PUR
3010
3357
  screens: plan.screens.map((s) => ({ id: s.id, title: s.title, file: s.file, illustrates: s.illustrates, states: s.states, breakpoints: s.breakpoints })),
3011
3358
  screenshots: []
3012
3359
  };
3013
- await writeText(file, stringifyYaml5(doc, { lineWidth: 120 }));
3360
+ await writeText(file, stringifyYaml6(doc, { lineWidth: 120 }));
3014
3361
  return file;
3015
3362
  }
3016
3363
  async function updateMockupScreenshots(root, slug, screenshots) {
3017
- const file = path9.join(mockupsDir(root, slug), "manifest.yaml");
3364
+ const file = path11.join(mockupsDir(root, slug), "manifest.yaml");
3018
3365
  const raw = await readTextIfExists(file);
3019
3366
  if (raw === void 0) return void 0;
3020
- const data = parseYaml6(raw) ?? {};
3367
+ const data = parseYaml8(raw) ?? {};
3021
3368
  data["screenshots"] = screenshots;
3022
- await writeText(file, stringifyYaml5(data, { lineWidth: 120 }));
3369
+ await writeText(file, stringifyYaml6(data, { lineWidth: 120 }));
3023
3370
  return file;
3024
3371
  }
3025
3372
  async function captureMockups(root, slug, manifest, now = /* @__PURE__ */ new Date()) {
@@ -3044,9 +3391,9 @@ async function captureMockups(root, slug, manifest, now = /* @__PURE__ */ new Da
3044
3391
  const page2 = await browser.newPage();
3045
3392
  for (const bp of screen.breakpoints && screen.breakpoints.length > 0 ? screen.breakpoints : [1440]) {
3046
3393
  await page2.setViewportSize({ width: bp, height: 900 });
3047
- await page2.goto(`file://${path9.join(dir, screen.file).split(path9.sep).join("/")}`);
3048
- const rel = path9.posix.join("screens", `${screen.id}-${bp}.png`);
3049
- await page2.screenshot({ path: path9.join(dir, rel) });
3394
+ await page2.goto(`file://${path11.join(dir, screen.file).split(path11.sep).join("/")}`);
3395
+ const rel = path11.posix.join("screens", `${screen.id}-${bp}.png`);
3396
+ await page2.screenshot({ path: path11.join(dir, rel) });
3050
3397
  screenshots.push(rel);
3051
3398
  }
3052
3399
  await page2.close();
@@ -3062,7 +3409,7 @@ async function mockupsReady(root, slug, change) {
3062
3409
  return Boolean(check.manifest && check.manifest.screens.length > 0 && !check.stale);
3063
3410
  }
3064
3411
  async function setMockupRequirement(root, slug, value) {
3065
- const file = path9.join(root, ".sdd", "changes", slug, "meta.yaml");
3412
+ const file = path11.join(root, ".sdd", "changes", slug, "meta.yaml");
3066
3413
  const raw = await readTextIfExists(file) ?? `schema_version: 1
3067
3414
  slug: ${slug}
3068
3415
  lane: standard
@@ -3081,8 +3428,8 @@ lane: standard
3081
3428
  }
3082
3429
 
3083
3430
  // src/packs.ts
3084
- import path10 from "path";
3085
- import { parse as parseYaml7 } from "yaml";
3431
+ import path12 from "path";
3432
+ import { parse as parseYaml9 } from "yaml";
3086
3433
  import { z as z5 } from "zod";
3087
3434
  var checkSchema = z5.object({
3088
3435
  id: z5.string().min(1),
@@ -3297,17 +3644,17 @@ ${texts.scenarios}`);
3297
3644
  }
3298
3645
  }
3299
3646
  async function loadProjectPacks(sddDir) {
3300
- const dir = path10.join(sddDir, "packs");
3647
+ const dir = path12.join(sddDir, "packs");
3301
3648
  const diagnostics = [];
3302
3649
  const packs = [];
3303
3650
  for (const entry of await listDir(dir)) {
3304
3651
  if (!/\.ya?ml$/i.test(entry)) continue;
3305
- const file = path10.join(dir, entry);
3652
+ const file = path12.join(dir, entry);
3306
3653
  const raw = await readTextIfExists(file);
3307
3654
  if (raw === void 0) continue;
3308
3655
  let data;
3309
3656
  try {
3310
- data = parseYaml7(raw);
3657
+ data = parseYaml9(raw);
3311
3658
  } catch (err) {
3312
3659
  diagnostics.push(diag("PACK-000", "error", `Pack inv\xE1lido (${entry}): ${err.message}`, { path: file }));
3313
3660
  continue;
@@ -3391,7 +3738,7 @@ function packFindings(evaluations, change) {
3391
3738
  // src/analyze.ts
3392
3739
  var UI_DOMAINS = /* @__PURE__ */ new Set(["frontend", "mobile", "fullstack"]);
3393
3740
  async function runAnalyze(opts) {
3394
- const root = path11.resolve(opts.root);
3741
+ const root = path13.resolve(opts.root);
3395
3742
  const { workspace, config } = await loadWorkspace(root);
3396
3743
  const change = workspace.changes.find((c) => c.slug === opts.slug);
3397
3744
  if (!change) {
@@ -3409,9 +3756,9 @@ async function runAnalyze(opts) {
3409
3756
  for (const req of spec.spec.requirements) living.set(req.id, req);
3410
3757
  }
3411
3758
  if (change.delta) {
3412
- findings.push(...lintDelta(change.delta, living, path11.join(change.dir, "spec.md"), { language: config.spec.language }));
3759
+ findings.push(...lintDelta(change.delta, living, path13.join(change.dir, "spec.md"), { language: config.spec.language }));
3413
3760
  }
3414
- const trace = checkTrace({ specs: workspace.specs, change, requireEvidence: false });
3761
+ const trace = checkTrace({ specs: workspace.specs, change, requireEvidence: false, linked: linkedTraceInput(workspace) });
3415
3762
  findings.push(...trace.findings);
3416
3763
  let waves;
3417
3764
  if (change.tasks && change.tasks.counts.total > 0) {
@@ -3458,7 +3805,7 @@ async function runAnalyze(opts) {
3458
3805
  if (mockups) result.mockups = mockups;
3459
3806
  if (packs) result.packs = packs;
3460
3807
  if (opts.write !== false) {
3461
- const file = path11.join(change.dir, "analyze.md");
3808
+ const file = path13.join(change.dir, "analyze.md");
3462
3809
  await writeText(file, renderAnalyze(change, result, localStamp(opts.now)));
3463
3810
  result.path = file;
3464
3811
  }
@@ -3501,9 +3848,9 @@ function renderAnalyze(change, result, generatedAt) {
3501
3848
  }
3502
3849
 
3503
3850
  // src/metrics.ts
3504
- import path12 from "path";
3851
+ import path14 from "path";
3505
3852
  async function collectMetrics(root, now = /* @__PURE__ */ new Date()) {
3506
- const resolved = path12.resolve(root);
3853
+ const resolved = path14.resolve(root);
3507
3854
  const { workspace, config } = await loadWorkspace(resolved);
3508
3855
  const approvals = await loadApprovals(workspace.sddDir);
3509
3856
  const living = /* @__PURE__ */ new Map();
@@ -3518,11 +3865,12 @@ async function collectMetrics(root, now = /* @__PURE__ */ new Date()) {
3518
3865
  const deltaContent = await readDelta(change);
3519
3866
  const approval = verifyApproval(change, approvals.byArtifact, config, deltaContent);
3520
3867
  const findings = [];
3521
- if (change.delta) findings.push(...lintDelta(change.delta, living, path12.join(change.dir, "spec.md"), { language: config.spec.language }));
3868
+ if (change.delta) findings.push(...lintDelta(change.delta, living, path14.join(change.dir, "spec.md"), { language: config.spec.language }));
3522
3869
  const trace = checkTrace({
3523
3870
  specs: workspace.specs,
3524
3871
  change,
3525
- requireEvidence: config.gates.verify.mode !== "off" && config.gates.verify.require_evidence
3872
+ requireEvidence: config.gates.verify.mode !== "off" && config.gates.verify.require_evidence,
3873
+ linked: linkedTraceInput(workspace)
3526
3874
  });
3527
3875
  findings.push(...trace.findings);
3528
3876
  const state = deriveState({ change, cfg: config, approval, blockingFindings: change.delta ? countBySeverity(findings).errors : 0 });
@@ -3583,7 +3931,7 @@ async function collectMetrics(root, now = /* @__PURE__ */ new Date()) {
3583
3931
  attention.push({ slug: change.slug, kind: "missing-evidence", detail: `evidencia ${change.scenariosPassed}/${change.scenariosTotal}` });
3584
3932
  }
3585
3933
  }
3586
- const archiveDir = path12.join(workspace.sddDir, "changes", "archive");
3934
+ const archiveDir = path14.join(workspace.sddDir, "changes", "archive");
3587
3935
  const archivedEntries = await listDirs(archiveDir);
3588
3936
  const throughputByMonth = {};
3589
3937
  for (const entry of archivedEntries) {
@@ -3621,15 +3969,15 @@ async function collectMetrics(root, now = /* @__PURE__ */ new Date()) {
3621
3969
  }
3622
3970
  async function readDelta(change) {
3623
3971
  const { readTextIfExists: readTextIfExists2 } = await import("./fsx-VF2P7ALA.js");
3624
- return readTextIfExists2(path12.join(change.dir, "spec.md"));
3972
+ return readTextIfExists2(path14.join(change.dir, "spec.md"));
3625
3973
  }
3626
3974
 
3627
3975
  // src/present.ts
3628
- import path13 from "path";
3976
+ import path15 from "path";
3629
3977
  import { escapeHtml, inlineMarkdown, renderMarkdown as renderRich } from "@specatlas/render";
3630
3978
  import { defaultTokens, renderDocument, renderStyles } from "@specatlas/render";
3631
3979
  async function generatePresentation(opts) {
3632
- const root = path13.resolve(opts.root);
3980
+ const root = path15.resolve(opts.root);
3633
3981
  const { workspace, config } = await loadWorkspace(root);
3634
3982
  const language = config.project.language;
3635
3983
  const change = workspace.changes.find((c) => c.slug === opts.slug);
@@ -3639,17 +3987,17 @@ async function generatePresentation(opts) {
3639
3987
  if (!change.delta) {
3640
3988
  return { slug: opts.slug, diagnostics: [diag("ATLAS-PRESENT-001", "error", `El cambio "${opts.slug}" no tiene spec.md (delta)`)] };
3641
3989
  }
3642
- const deltaPath = path13.join(change.dir, "spec.md");
3990
+ const deltaPath = path15.join(change.dir, "spec.md");
3643
3991
  const deltaContent = await readTextIfExists(deltaPath) ?? "";
3644
3992
  const hash = artifactHash(deltaContent);
3645
3993
  const now = opts.now ?? /* @__PURE__ */ new Date();
3646
- const presentationDir = path13.join(change.dir, "presentation");
3647
- const mockupsCopyDir = path13.join(presentationDir, "mockups");
3994
+ const presentationDir = path15.join(change.dir, "presentation");
3995
+ const mockupsCopyDir = path15.join(presentationDir, "mockups");
3648
3996
  await ensureDir(presentationDir);
3649
3997
  const mockupInfo = await copyMockups(root, change, mockupsCopyDir);
3650
- const proposalRaw = await readTextIfExists(path13.join(change.dir, "proposal.md"));
3998
+ const proposalRaw = await readTextIfExists(path15.join(change.dir, "proposal.md"));
3651
3999
  const proposalBody = proposalRaw ? parseFrontmatter(proposalRaw).body : "";
3652
- const approvals = await loadApprovals(path13.join(root, ".sdd"));
4000
+ const approvals = await loadApprovals(path15.join(root, ".sdd"));
3653
4001
  const approval = verifyApproval(change, approvals.byArtifact, config, deltaContent);
3654
4002
  const labels = labelsFor(language);
3655
4003
  const html = page({
@@ -3670,7 +4018,7 @@ async function generatePresentation(opts) {
3670
4018
  approveHint: `satlas approve ${change.slug} --by "<nombre>" --channel presentation`,
3671
4019
  ...approval.status === "valid" && approval.approvedBy ? { approval: { by: approval.approvedBy, at: approval.approvedAt ?? "" } } : {}
3672
4020
  });
3673
- const outFile = path13.join(presentationDir, "index.html");
4021
+ const outFile = path15.join(presentationDir, "index.html");
3674
4022
  await writeText(outFile, html);
3675
4023
  return { slug: change.slug, path: outFile, hash, diagnostics: mockupInfo.diagnostics };
3676
4024
  }
@@ -3682,12 +4030,12 @@ async function copyMockups(root, change, destDir) {
3682
4030
  await ensureDir(destDir);
3683
4031
  const screens = [];
3684
4032
  for (const screen of manifest.screens) {
3685
- const src = path13.join(dir, screen.file);
4033
+ const src = path15.join(dir, screen.file);
3686
4034
  if (!await exists(src)) {
3687
4035
  diagnostics.push(diag("ATLAS-PRESENT-002", "warning", `El mockup ${screen.file} no existe y no se incluir\xE1`, { path: src }));
3688
4036
  continue;
3689
4037
  }
3690
- await copyFile(src, path13.join(destDir, screen.file));
4038
+ await copyFile(src, path15.join(destDir, screen.file));
3691
4039
  screens.push({
3692
4040
  id: screen.id,
3693
4041
  title: screen.title ?? screen.id,
@@ -3697,13 +4045,13 @@ async function copyMockups(root, change, destDir) {
3697
4045
  });
3698
4046
  }
3699
4047
  const screenshots = [];
3700
- const shotsDir = path13.join(dir, "screens");
4048
+ const shotsDir = path15.join(dir, "screens");
3701
4049
  if (await exists(shotsDir)) {
3702
- await ensureDir(path13.join(destDir, "screens"));
4050
+ await ensureDir(path15.join(destDir, "screens"));
3703
4051
  const { listDir: listDir4 } = await import("./fsx-VF2P7ALA.js");
3704
4052
  for (const file of await listDir4(shotsDir)) {
3705
4053
  if (!/\.(png|jpe?g|webp)$/i.test(file)) continue;
3706
- await copyFile(path13.join(shotsDir, file), path13.join(destDir, "screens", file));
4054
+ await copyFile(path15.join(shotsDir, file), path15.join(destDir, "screens", file));
3707
4055
  screenshots.push(`mockups/screens/${file}`);
3708
4056
  }
3709
4057
  }
@@ -3878,7 +4226,7 @@ function renderMarkdown(markdown) {
3878
4226
  }
3879
4227
 
3880
4228
  // src/adopt.ts
3881
- import path14 from "path";
4229
+ import path16 from "path";
3882
4230
  var COMMON_ROOTS = ["src", "app", "lib", "modules", "services", "packages", "api", "features", "domain", "internal", "components"];
3883
4231
  var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", "dist", "build", "out", "coverage", ".git", ".sdd", ".opencode", ".claude", "test", "tests", "__tests__", "e2e", "docs", "scripts", "assets", "public", "migrations", "vendor", "bin", "obj"]);
3884
4232
  var SOURCE_EXT = /* @__PURE__ */ new Set([
@@ -3922,11 +4270,11 @@ var SOURCE_EXT = /* @__PURE__ */ new Set([
3922
4270
  ".groovy"
3923
4271
  ]);
3924
4272
  async function adoptWorkspace(opts) {
3925
- const root = path14.resolve(opts.root);
4273
+ const root = path16.resolve(opts.root);
3926
4274
  const dryRun = opts.dryRun ?? false;
3927
4275
  const diagnostics = [];
3928
- const sddDir = path14.join(root, ".sdd");
3929
- if (!await exists(path14.join(sddDir, "config.yaml"))) {
4276
+ const sddDir = path16.join(root, ".sdd");
4277
+ if (!await exists(path16.join(sddDir, "config.yaml"))) {
3930
4278
  return {
3931
4279
  root,
3932
4280
  domains: [],
@@ -3937,8 +4285,8 @@ async function adoptWorkspace(opts) {
3937
4285
  }
3938
4286
  const { config } = await loadConfig(sddDir);
3939
4287
  const files = await walkFiles(root, { skipDirs: [...SKIP_DIRS] });
3940
- const inventoried = files.map((f) => toPosix(path14.relative(root, f.path)));
3941
- const relative = inventoried.filter((f) => SOURCE_EXT.has(path14.extname(f).toLowerCase()));
4288
+ const inventoried = files.map((f) => toPosix(path16.relative(root, f.path)));
4289
+ const relative = inventoried.filter((f) => SOURCE_EXT.has(path16.extname(f).toLowerCase()));
3942
4290
  if (relative.length === 0) {
3943
4291
  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>" }));
3944
4292
  }
@@ -3966,13 +4314,13 @@ async function adoptWorkspace(opts) {
3966
4314
  const createdSpecs = [];
3967
4315
  for (const name of domainNames.sort()) {
3968
4316
  const domainFiles = (counts.get(name) ?? []).sort();
3969
- const existingSpec = await exists(path14.join(sddDir, "specs", name, "spec.md"));
4317
+ const existingSpec = await exists(path16.join(sddDir, "specs", name, "spec.md"));
3970
4318
  domains.push({ name, files: domainFiles.length, samples: domainFiles.slice(0, 8), existingSpec });
3971
4319
  if (existingSpec) {
3972
- diagnostics.push(diag("ATLAS-ADOPT-003", "info", `La spec de "${name}" ya existe; se conserva`, { path: path14.join(sddDir, "specs", name, "spec.md") }));
4320
+ diagnostics.push(diag("ATLAS-ADOPT-003", "info", `La spec de "${name}" ya existe; se conserva`, { path: path16.join(sddDir, "specs", name, "spec.md") }));
3973
4321
  continue;
3974
4322
  }
3975
- const specPath = path14.join(sddDir, "specs", name, "spec.md");
4323
+ const specPath = path16.join(sddDir, "specs", name, "spec.md");
3976
4324
  if (!dryRun) {
3977
4325
  await writeText(specPath, baselineSpec(name, domainFiles));
3978
4326
  createdSpecs.push(specPath);
@@ -3980,7 +4328,7 @@ async function adoptWorkspace(opts) {
3980
4328
  createdSpecs.push(specPath);
3981
4329
  }
3982
4330
  }
3983
- const reportPath = path14.join(sddDir, "adopt-report.md");
4331
+ const reportPath = path16.join(sddDir, "adopt-report.md");
3984
4332
  if (!dryRun) {
3985
4333
  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() }));
3986
4334
  }
@@ -4000,12 +4348,12 @@ async function loadProjectProfiles(root, dirs) {
4000
4348
  if (!await exists(dir)) continue;
4001
4349
  out.push(...await loadProfilesFromDir(dir));
4002
4350
  }
4003
- const custom = path14.join(root, ".sdd", "profiles", "custom");
4351
+ const custom = path16.join(root, ".sdd", "profiles", "custom");
4004
4352
  if (await exists(custom)) out.push(...await loadProfilesFromDir(custom));
4005
4353
  return out;
4006
4354
  }
4007
4355
  async function fallbackDomainName(root, projectName) {
4008
- const pkg = await readTextIfExists(path14.join(root, "package.json"));
4356
+ const pkg = await readTextIfExists(path16.join(root, "package.json"));
4009
4357
  if (pkg) {
4010
4358
  try {
4011
4359
  const data = JSON.parse(pkg);
@@ -4093,13 +4441,13 @@ function adoptReport(input) {
4093
4441
  }
4094
4442
 
4095
4443
  // src/init.ts
4096
- import path15 from "path";
4444
+ import path17 from "path";
4097
4445
  async function initWorkspace(opts) {
4098
- const root = path15.resolve(opts.root);
4099
- const sddDir = path15.join(root, ".sdd");
4446
+ const root = path17.resolve(opts.root);
4447
+ const sddDir = path17.join(root, ".sdd");
4100
4448
  const diagnostics = [];
4101
4449
  const created = [];
4102
- if (await exists(path15.join(sddDir, "config.yaml"))) {
4450
+ if (await exists(path17.join(sddDir, "config.yaml"))) {
4103
4451
  return {
4104
4452
  sddDir,
4105
4453
  created,
@@ -4109,29 +4457,29 @@ async function initWorkspace(opts) {
4109
4457
  created.push(...await ensureSddDirs(sddDir));
4110
4458
  const language = opts.language ?? "es";
4111
4459
  const cfg = defaultConfig({ name: opts.name, language });
4112
- cfg.project.name = opts.name ?? path15.basename(root);
4460
+ cfg.project.name = opts.name ?? path17.basename(root);
4113
4461
  await writeConfig(sddDir, cfg);
4114
- created.push(path15.join(sddDir, "config.yaml"));
4462
+ created.push(path17.join(sddDir, "config.yaml"));
4115
4463
  const templates = templatesFor(language);
4116
- await writeText(path15.join(sddDir, "constitution.md"), templates.constitution);
4117
- created.push(path15.join(sddDir, "constitution.md"));
4118
- await writeText(path15.join(sddDir, "glossary.md"), templates.glossary);
4119
- created.push(path15.join(sddDir, "glossary.md"));
4464
+ await writeText(path17.join(sddDir, "constitution.md"), templates.constitution);
4465
+ created.push(path17.join(sddDir, "constitution.md"));
4466
+ await writeText(path17.join(sddDir, "glossary.md"), templates.glossary);
4467
+ created.push(path17.join(sddDir, "glossary.md"));
4120
4468
  const detected = await detectIfPossible(root, opts.profilesDir);
4121
4469
  if (detected) {
4122
4470
  const yaml = detectionToYaml(detected, localStamp(opts.now));
4123
- await writeText(path15.join(sddDir, "profiles", "detected.yaml"), yaml);
4124
- created.push(path15.join(sddDir, "profiles", "detected.yaml"));
4471
+ await writeText(path17.join(sddDir, "profiles", "detected.yaml"), yaml);
4472
+ created.push(path17.join(sddDir, "profiles", "detected.yaml"));
4125
4473
  if (detected.best) {
4126
4474
  diagnostics.push(diag("ATLAS-INIT-002", "info", `Stack detectado: ${detected.best.displayName} (${detected.best.score} puntos)`, { suggestion: "Revisa .sdd/profiles/detected.yaml" }));
4127
4475
  } else {
4128
4476
  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/" }));
4129
4477
  }
4130
4478
  }
4131
- await writeText(path15.join(sddDir, "INDEX.md"), initialIndex(language, cfg.project.name));
4132
- created.push(path15.join(sddDir, "INDEX.md"));
4479
+ await writeText(path17.join(sddDir, "INDEX.md"), initialIndex(language, cfg.project.name));
4480
+ created.push(path17.join(sddDir, "INDEX.md"));
4133
4481
  if (opts.local) {
4134
- const gitignore = path15.join(root, ".gitignore");
4482
+ const gitignore = path17.join(root, ".gitignore");
4135
4483
  const current = await readTextIfExists(gitignore) ?? "";
4136
4484
  if (!/(^|\n)\.sdd\/?(\n|$)/.test(current)) {
4137
4485
  const next = current.endsWith("\n") || current === "" ? `${current}.sdd/
@@ -4166,13 +4514,13 @@ ${es ? "## Archivados\n\n0 cambio(s) en `changes/archive/`." : "## Archived\n\n0
4166
4514
  }
4167
4515
 
4168
4516
  // src/new.ts
4169
- import path16 from "path";
4517
+ import path18 from "path";
4170
4518
  var SLUG_RE = /^[a-z0-9][a-z0-9-]{1,49}$/;
4171
4519
  async function createChange(opts) {
4172
4520
  const diagnostics = [];
4173
4521
  const slug = opts.slug.trim().toLowerCase();
4174
4522
  const language = opts.language ?? opts.cfg?.project.language ?? "es";
4175
- const dir = path16.join(path16.resolve(opts.root), ".sdd", "changes", slug);
4523
+ const dir = path18.join(path18.resolve(opts.root), ".sdd", "changes", slug);
4176
4524
  if (!SLUG_RE.test(slug)) {
4177
4525
  return {
4178
4526
  slug,
@@ -4203,20 +4551,20 @@ async function createChange(opts) {
4203
4551
  await ensureDir(dir);
4204
4552
  const templates = templatesFor(language);
4205
4553
  const files = [];
4206
- const metaFile = path16.join(dir, "meta.yaml");
4554
+ const metaFile = path18.join(dir, "meta.yaml");
4207
4555
  await writeText(metaFile, changeMetaYaml(meta));
4208
4556
  files.push(metaFile);
4209
4557
  if (lane === "fix") {
4210
- const fixFile = path16.join(dir, "fix.md");
4558
+ const fixFile = path18.join(dir, "fix.md");
4211
4559
  await writeText(fixFile, renderTemplate(templates.fix, { TITLE: title }));
4212
4560
  files.push(fixFile);
4213
4561
  return { slug, dir, files, diagnostics };
4214
4562
  }
4215
- const proposalFile = path16.join(dir, "proposal.md");
4563
+ const proposalFile = path18.join(dir, "proposal.md");
4216
4564
  await writeText(proposalFile, renderTemplate(templates.proposal, { TITLE: title }));
4217
4565
  files.push(proposalFile);
4218
4566
  const domainUpper = domain.replace(/[^a-z0-9]/gi, "").toUpperCase() || "GEN";
4219
- const deltaFile = path16.join(dir, "spec.md");
4567
+ const deltaFile = path18.join(dir, "spec.md");
4220
4568
  await writeText(deltaFile, templates.specDelta({ title, domainUpper, domain }));
4221
4569
  files.push(deltaFile);
4222
4570
  return { slug, dir, files, diagnostics };
@@ -4225,7 +4573,7 @@ async function createChange(opts) {
4225
4573
  // src/archive.ts
4226
4574
  import { promises as fs } from "fs";
4227
4575
  import { cp, rename, rm } from "fs/promises";
4228
- import path17 from "path";
4576
+ import path19 from "path";
4229
4577
  var REQ_HEADER_RE = /^###\s+(?:Requisito|Requirement):\s+(REQ-[A-Z0-9-]+)\s*(?:—|-|–)\s*(.*)$/;
4230
4578
  function foldDelta(livingBody, delta, language = "es") {
4231
4579
  const diagnostics = [];
@@ -4334,7 +4682,7 @@ async function restoreFile(file, previous) {
4334
4682
  }
4335
4683
  }
4336
4684
  async function archiveChange(opts) {
4337
- const root = path17.resolve(opts.root);
4685
+ const root = path19.resolve(opts.root);
4338
4686
  const diagnostics = [];
4339
4687
  const { config } = await loadWorkspace(root);
4340
4688
  const language = opts.language ?? config.project.language;
@@ -4350,7 +4698,7 @@ async function archiveChange(opts) {
4350
4698
  return { slug: opts.slug, fold: emptyFold, diagnostics, dryRun: opts.dryRun ?? false };
4351
4699
  }
4352
4700
  const nowFix = opts.now ?? /* @__PURE__ */ new Date();
4353
- const targetFix = path17.join(root, ".sdd", "changes", "archive", `${localMonth(nowFix)}-${opts.slug}`);
4701
+ const targetFix = path19.join(root, ".sdd", "changes", "archive", `${localMonth(nowFix)}-${opts.slug}`);
4354
4702
  if (await exists(targetFix)) {
4355
4703
  diagnostics.push(diag("ATLAS-ARCH-002", "error", `Ya existe un cambio archivado en ${targetFix}`, { path: targetFix }));
4356
4704
  return { slug: opts.slug, fold: emptyFold, diagnostics, dryRun: opts.dryRun ?? false };
@@ -4358,7 +4706,7 @@ async function archiveChange(opts) {
4358
4706
  let livingFix;
4359
4707
  let livingCreated = false;
4360
4708
  if (!opts.dryRun) {
4361
- const fixRaw = await readTextIfExists(path17.join(change.dir, "fix.md")) ?? "";
4709
+ const fixRaw = await readTextIfExists(path19.join(change.dir, "fix.md")) ?? "";
4362
4710
  try {
4363
4711
  const written = await writeLivingFix(root, {
4364
4712
  slug: opts.slug,
@@ -4374,7 +4722,7 @@ async function archiveChange(opts) {
4374
4722
  } catch (error) {
4375
4723
  diagnostics.push(
4376
4724
  diag("ATLAS-ARCH-006", "error", `No se pudo conservar el fix vivo: ${error.message}`, {
4377
- path: path17.join(root, ".sdd", "fixes"),
4725
+ path: path19.join(root, ".sdd", "fixes"),
4378
4726
  suggestion: "Revisa los permisos de .sdd/fixes y vuelve a intentar; el fix sigue sin archivar"
4379
4727
  })
4380
4728
  );
@@ -4382,11 +4730,11 @@ async function archiveChange(opts) {
4382
4730
  }
4383
4731
  }
4384
4732
  if (!opts.dryRun) {
4385
- await ensureDir(path17.dirname(targetFix));
4733
+ await ensureDir(path19.dirname(targetFix));
4386
4734
  try {
4387
4735
  await moveDirectory(change.dir, targetFix);
4388
4736
  } catch (error) {
4389
- if (livingCreated && livingFix) await removeFile(path17.join(root, livingFix));
4737
+ if (livingCreated && livingFix) await removeFile(path19.join(root, livingFix));
4390
4738
  diagnostics.push(
4391
4739
  diag("ATLAS-ARCH-004", "error", `No se pudo mover el cambio al hist\xF3rico: ${error.message}`, {
4392
4740
  path: change.dir,
@@ -4412,10 +4760,10 @@ async function archiveChange(opts) {
4412
4760
  }
4413
4761
  const domain = change.meta.domain;
4414
4762
  if (!domain) {
4415
- diagnostics.push(diag("ATLAS-ARCH-001", "error", `El cambio "${opts.slug}" no declara domain en meta.yaml`, { path: path17.join(change.dir, "meta.yaml") }));
4763
+ diagnostics.push(diag("ATLAS-ARCH-001", "error", `El cambio "${opts.slug}" no declara domain en meta.yaml`, { path: path19.join(change.dir, "meta.yaml") }));
4416
4764
  return { slug: opts.slug, fold: { content: "", applied: { added: [], modified: [], removed: [], renamed: [] }, diagnostics }, diagnostics, dryRun: opts.dryRun ?? false };
4417
4765
  }
4418
- const specFile = path17.join(root, ".sdd", "specs", domain, "spec.md");
4766
+ const specFile = path19.join(root, ".sdd", "specs", domain, "spec.md");
4419
4767
  const existing = await readTextIfExists(specFile);
4420
4768
  const base = existing ?? `---
4421
4769
  domain: ${domain}
@@ -4445,8 +4793,8 @@ ${Object.entries(nextFm).map(([k, v]) => `${k}: ${String(v)}`).join("\n")}
4445
4793
 
4446
4794
  ${body}`;
4447
4795
  const month = localMonth(now);
4448
- const archiveDir = path17.join(root, ".sdd", "changes", "archive");
4449
- const target = path17.join(archiveDir, `${month}-${opts.slug}`);
4796
+ const archiveDir = path19.join(root, ".sdd", "changes", "archive");
4797
+ const target = path19.join(archiveDir, `${month}-${opts.slug}`);
4450
4798
  if (await exists(target)) {
4451
4799
  diagnostics.push(diag("ATLAS-ARCH-002", "error", `Ya existe un cambio archivado en ${target}`, { path: target }));
4452
4800
  return { slug: opts.slug, domain, fold, diagnostics, dryRun: opts.dryRun ?? false };
@@ -4472,7 +4820,7 @@ ${body}`;
4472
4820
  }
4473
4821
  async function regenerateIndex(root, cfg, now = /* @__PURE__ */ new Date()) {
4474
4822
  const { workspace, config } = cfg ? { workspace: (await loadWorkspace(root)).workspace, config: cfg } : await loadWorkspace(root);
4475
- const archiveDir = path17.join(root, ".sdd", "changes", "archive");
4823
+ const archiveDir = path19.join(root, ".sdd", "changes", "archive");
4476
4824
  const archived = await exists(archiveDir) ? (await fs.readdir(archiveDir)).filter((e) => !e.startsWith(".")).length : 0;
4477
4825
  const fixes = await loadLivingFixes(root);
4478
4826
  const markdown = indexMarkdown({
@@ -4484,7 +4832,7 @@ async function regenerateIndex(root, cfg, now = /* @__PURE__ */ new Date()) {
4484
4832
  archived
4485
4833
  });
4486
4834
  void now;
4487
- await writeText(path17.join(root, ".sdd", "INDEX.md"), markdown);
4835
+ await writeText(path19.join(root, ".sdd", "INDEX.md"), markdown);
4488
4836
  }
4489
4837
  async function removeFile(file) {
4490
4838
  try {
@@ -4495,8 +4843,8 @@ async function removeFile(file) {
4495
4843
 
4496
4844
  // src/migrations.ts
4497
4845
  import { promises as fs2 } from "fs";
4498
- import path18 from "path";
4499
- import { parse as parseYaml8, stringify as stringifyYaml6 } from "yaml";
4846
+ import path20 from "path";
4847
+ import { parse as parseYaml10, stringify as stringifyYaml7 } from "yaml";
4500
4848
  var SCHEMA_VERSION = 1;
4501
4849
  var BACKUP_DIRNAME = ".backup";
4502
4850
  var BACKUP_POINTER = ".latest";
@@ -4508,35 +4856,35 @@ var SCHEMA_MIGRATIONS = [
4508
4856
  to: SCHEMA_VERSION
4509
4857
  }
4510
4858
  ];
4511
- var FIXED_ARTIFACTS = ["config.yaml", "approvals.yaml", path18.join("profiles", "detected.yaml")];
4512
- var CHANGE_ARTIFACTS = ["meta.yaml", path18.join("mockups", "manifest.yaml")];
4859
+ var FIXED_ARTIFACTS = ["config.yaml", "approvals.yaml", path20.join("profiles", "detected.yaml")];
4860
+ var CHANGE_ARTIFACTS = ["meta.yaml", path20.join("mockups", "manifest.yaml")];
4513
4861
  async function pushChangeArtifacts(out, sddDir, relDir) {
4514
4862
  for (const rel of CHANGE_ARTIFACTS) {
4515
- const abs = path18.join(sddDir, relDir, rel);
4516
- if (await exists(abs)) out.push({ artifact: toPosix(path18.join(relDir, rel)), path: abs });
4863
+ const abs = path20.join(sddDir, relDir, rel);
4864
+ if (await exists(abs)) out.push({ artifact: toPosix(path20.join(relDir, rel)), path: abs });
4517
4865
  }
4518
4866
  }
4519
4867
  async function collectVersionedArtifacts(root) {
4520
- const sddDir = path18.join(root, ".sdd");
4868
+ const sddDir = path20.join(root, ".sdd");
4521
4869
  const out = [];
4522
4870
  for (const rel of FIXED_ARTIFACTS) {
4523
- const abs = path18.join(sddDir, rel);
4871
+ const abs = path20.join(sddDir, rel);
4524
4872
  if (await exists(abs)) out.push({ artifact: toPosix(rel), path: abs });
4525
4873
  }
4526
- const changesDir = path18.join(sddDir, "changes");
4874
+ const changesDir = path20.join(sddDir, "changes");
4527
4875
  for (const slug of await listDirs(changesDir)) {
4528
4876
  if (slug === "archive") continue;
4529
- await pushChangeArtifacts(out, sddDir, path18.join("changes", slug));
4877
+ await pushChangeArtifacts(out, sddDir, path20.join("changes", slug));
4530
4878
  }
4531
- for (const entry of await listDirs(path18.join(changesDir, "archive"))) {
4532
- await pushChangeArtifacts(out, sddDir, path18.join("changes", "archive", entry));
4879
+ for (const entry of await listDirs(path20.join(changesDir, "archive"))) {
4880
+ await pushChangeArtifacts(out, sddDir, path20.join("changes", "archive", entry));
4533
4881
  }
4534
4882
  return out;
4535
4883
  }
4536
4884
  function readArtifactVersion(content) {
4537
4885
  let data;
4538
4886
  try {
4539
- data = parseYaml8(content);
4887
+ data = parseYaml10(content);
4540
4888
  } catch (err) {
4541
4889
  return { state: "unreadable", reason: `YAML inv\xE1lido (${err.message})` };
4542
4890
  }
@@ -4626,22 +4974,22 @@ function backupName(now) {
4626
4974
  async function applyUpgrade(root, now = /* @__PURE__ */ new Date()) {
4627
4975
  const plan = await planUpgrade(root);
4628
4976
  if (plan.pending.length === 0) return { status: "up-to-date", plan, applied: [] };
4629
- const sddDir = path18.join(root, ".sdd");
4630
- const backupRoot = path18.join(sddDir, BACKUP_DIRNAME);
4977
+ const sddDir = path20.join(root, ".sdd");
4978
+ const backupRoot = path20.join(sddDir, BACKUP_DIRNAME);
4631
4979
  const name = backupName(now);
4632
- const backupDir = path18.join(backupRoot, name);
4633
- const pointerFile = path18.join(backupRoot, BACKUP_POINTER);
4980
+ const backupDir = path20.join(backupRoot, name);
4981
+ const pointerFile = path20.join(backupRoot, BACKUP_POINTER);
4634
4982
  const previous = [];
4635
4983
  try {
4636
4984
  for (const item of plan.pending) {
4637
4985
  const content = await readTextIfExists(item.path);
4638
4986
  if (content === void 0) throw new Error("el elemento desapareci\xF3 mientras se respaldaba");
4639
4987
  previous.push({ item, contents: content });
4640
- await writeText(path18.join(backupDir, "files", ...item.artifact.split("/")), content);
4988
+ await writeText(path20.join(backupDir, "files", ...item.artifact.split("/")), content);
4641
4989
  }
4642
4990
  await writeText(
4643
- path18.join(backupDir, "backup.yaml"),
4644
- stringifyYaml6(
4991
+ path20.join(backupDir, "backup.yaml"),
4992
+ stringifyYaml7(
4645
4993
  {
4646
4994
  schema_version: SCHEMA_VERSION,
4647
4995
  created_at: localStamp(now),
@@ -4681,7 +5029,7 @@ async function applyUpgrade(root, now = /* @__PURE__ */ new Date()) {
4681
5029
  }
4682
5030
  }
4683
5031
  for (const dir of await listDirs(backupRoot)) {
4684
- if (dir !== name) await removeDir(path18.join(backupRoot, dir));
5032
+ if (dir !== name) await removeDir(path20.join(backupRoot, dir));
4685
5033
  }
4686
5034
  return {
4687
5035
  status: "applied",
@@ -4690,32 +5038,32 @@ async function applyUpgrade(root, now = /* @__PURE__ */ new Date()) {
4690
5038
  backup: {
4691
5039
  name,
4692
5040
  dir: backupDir,
4693
- relativeDir: toPosix(path18.relative(root, backupDir)),
5041
+ relativeDir: toPosix(path20.relative(root, backupDir)),
4694
5042
  createdAt: localStamp(now),
4695
5043
  files: plan.pending.map((i) => i.artifact)
4696
5044
  }
4697
5045
  };
4698
5046
  }
4699
5047
  async function rollbackUpgrade(root) {
4700
- const sddDir = path18.join(root, ".sdd");
4701
- const backupRoot = path18.join(sddDir, BACKUP_DIRNAME);
4702
- const pointerFile = path18.join(backupRoot, BACKUP_POINTER);
5048
+ const sddDir = path20.join(root, ".sdd");
5049
+ const backupRoot = path20.join(sddDir, BACKUP_DIRNAME);
5050
+ const pointerFile = path20.join(backupRoot, BACKUP_POINTER);
4703
5051
  const name = (await readTextIfExists(pointerFile))?.trim();
4704
5052
  if (!name) return { status: "no-backup", restored: [] };
4705
- const backupDir = path18.join(backupRoot, name);
4706
- const manifestRaw = await readTextIfExists(path18.join(backupDir, "backup.yaml"));
5053
+ const backupDir = path20.join(backupRoot, name);
5054
+ const manifestRaw = await readTextIfExists(path20.join(backupDir, "backup.yaml"));
4707
5055
  if (manifestRaw === void 0) return { status: "no-backup", restored: [] };
4708
5056
  let files = [];
4709
5057
  try {
4710
- const parsed = parseYaml8(manifestRaw);
5058
+ const parsed = parseYaml10(manifestRaw);
4711
5059
  files = (parsed?.files ?? []).filter((f) => typeof f?.artifact === "string");
4712
5060
  } catch {
4713
5061
  return { status: "no-backup", restored: [] };
4714
5062
  }
4715
5063
  const restored = [];
4716
5064
  for (const file of files) {
4717
- const from = path18.join(backupDir, "files", ...file.artifact.split("/"));
4718
- const to = path18.join(sddDir, ...file.artifact.split("/"));
5065
+ const from = path20.join(backupDir, "files", ...file.artifact.split("/"));
5066
+ const to = path20.join(sddDir, ...file.artifact.split("/"));
4719
5067
  const content = await readTextIfExists(from);
4720
5068
  if (content === void 0) continue;
4721
5069
  try {
@@ -4759,7 +5107,7 @@ async function upgradeAdvisory(root) {
4759
5107
  }
4760
5108
 
4761
5109
  // src/docs.ts
4762
- import path19 from "path";
5110
+ import path21 from "path";
4763
5111
  var DOCS_MARKER_START = "<!-- specatlas:generado:inicio -->";
4764
5112
  var DOCS_MARKER_END = "<!-- specatlas:generado:fin -->";
4765
5113
  function docData(change, language, now) {
@@ -4813,7 +5161,7 @@ ${notes}
4813
5161
  ${existing}`;
4814
5162
  }
4815
5163
  async function generateDocs(opts) {
4816
- const root = path19.resolve(opts.root);
5164
+ const root = path21.resolve(opts.root);
4817
5165
  const { config } = await loadWorkspace(root);
4818
5166
  const change = await loadChange(root, opts.slug);
4819
5167
  if (!change.meta) {
@@ -4830,7 +5178,7 @@ async function generateDocs(opts) {
4830
5178
  const data = docData(change, language, opts.now ?? /* @__PURE__ */ new Date());
4831
5179
  const files = [];
4832
5180
  for (const current of tipos) {
4833
- const file = path19.join(change.dir, "docs", `${current}.md`);
5181
+ const file = path21.join(change.dir, "docs", `${current}.md`);
4834
5182
  const block = renderTemplate(current === "tecnica" ? templates.docTecnica : templates.docManual, data);
4835
5183
  const existing = await readTextIfExists(file);
4836
5184
  await writeText(file, mergeManaged(existing, block, language));
@@ -4840,7 +5188,7 @@ async function generateDocs(opts) {
4840
5188
  }
4841
5189
 
4842
5190
  // src/sarif.ts
4843
- import path20 from "path";
5191
+ import path22 from "path";
4844
5192
  var SARIF_SCHEMA = "https://json.schemastore.org/sarif-2.1.0.json";
4845
5193
  var SARIF_VERSION = "2.1.0";
4846
5194
  var TOOL_NAME = "SpecAtlas";
@@ -4878,7 +5226,7 @@ function resultOf(diagnostic, root) {
4878
5226
  message: { text: diagnostic.message }
4879
5227
  };
4880
5228
  if (diagnostic.path) {
4881
- const rel = path20.relative(root, diagnostic.path);
5229
+ const rel = path22.relative(root, diagnostic.path);
4882
5230
  if (rel !== "") {
4883
5231
  const physicalLocation = {
4884
5232
  artifactLocation: { uri: toPosix(rel) },
@@ -4928,7 +5276,7 @@ function toSarifText(opts) {
4928
5276
  }
4929
5277
 
4930
5278
  // src/doctor.ts
4931
- import path21 from "path";
5279
+ import path23 from "path";
4932
5280
  async function runDoctor(root) {
4933
5281
  const findings = [];
4934
5282
  const { workspace, config } = await loadWorkspace(root);
@@ -4936,7 +5284,7 @@ async function runDoctor(root) {
4936
5284
  const approvals = await loadApprovals(workspace.sddDir);
4937
5285
  findings.push(...approvals.diagnostics);
4938
5286
  for (const change of workspace.changes) {
4939
- const deltaPath = path21.join(change.dir, "spec.md");
5287
+ const deltaPath = path23.join(change.dir, "spec.md");
4940
5288
  const deltaContent = await readTextIfExists(deltaPath);
4941
5289
  const approval = verifyApproval(change, approvals.byArtifact, config, deltaContent ?? void 0);
4942
5290
  if ((change.planPath || change.tasks) && (approval.status === "missing" || approval.status === "stale")) {
@@ -4958,7 +5306,7 @@ async function runDoctor(root) {
4958
5306
  }
4959
5307
  for (const override of change.meta?.overrides ?? []) {
4960
5308
  if (!override.reason.trim() || !override.by.trim()) {
4961
- findings.push(diag("ATLAS-LIFECYCLE-004", "error", `Override del gate "${override.gate}" sin motivo o autor`, { path: path21.join(change.dir, "meta.yaml") }));
5309
+ findings.push(diag("ATLAS-LIFECYCLE-004", "error", `Override del gate "${override.gate}" sin motivo o autor`, { path: path23.join(change.dir, "meta.yaml") }));
4962
5310
  }
4963
5311
  }
4964
5312
  }
@@ -4981,7 +5329,7 @@ async function specHashOf(filePath) {
4981
5329
  }
4982
5330
 
4983
5331
  // src/gate.ts
4984
- import path22 from "path";
5332
+ import path24 from "path";
4985
5333
  var UI_DOMAINS2 = /* @__PURE__ */ new Set(["frontend", "mobile", "fullstack"]);
4986
5334
  function count(name, diagnostics) {
4987
5335
  return {
@@ -4998,7 +5346,7 @@ function livingRequirementsMap(specs) {
4998
5346
  return map;
4999
5347
  }
5000
5348
  async function runCiGate(opts) {
5001
- const root = path22.resolve(opts.root);
5349
+ const root = path24.resolve(opts.root);
5002
5350
  const { workspace, config } = await loadWorkspace(root);
5003
5351
  const diagnostics = [];
5004
5352
  const checks = [];
@@ -5009,11 +5357,12 @@ async function runCiGate(opts) {
5009
5357
  let changesErrors = 0;
5010
5358
  let changesWarnings = 0;
5011
5359
  for (const change of workspace.changes) {
5012
- const lintFindings = change.delta ? lintDelta(change.delta, living, path22.join(change.dir, "spec.md"), { language: config.spec.language }) : [];
5360
+ const lintFindings = change.delta ? lintDelta(change.delta, living, path24.join(change.dir, "spec.md"), { language: config.spec.language }) : [];
5013
5361
  const trace = checkTrace({
5014
5362
  specs: workspace.specs,
5015
5363
  change,
5016
- requireEvidence: config.gates.verify.mode !== "off" && config.gates.verify.require_evidence
5364
+ requireEvidence: config.gates.verify.mode !== "off" && config.gates.verify.require_evidence,
5365
+ linked: linkedTraceInput(workspace)
5017
5366
  });
5018
5367
  const changeDiags = [...lintFindings, ...trace.findings];
5019
5368
  if (change.planPath) {
@@ -5058,9 +5407,11 @@ export {
5058
5407
  BLOCK_HEAD_RE,
5059
5408
  BUILTIN_PACKS,
5060
5409
  CONFIG_FILE,
5410
+ CONTRACTS_DIR,
5061
5411
  CORE_VERSION,
5062
5412
  DOCS_MARKER_END,
5063
5413
  DOCS_MARKER_START,
5414
+ LINKS_FILE,
5064
5415
  LIVING_FIXES_DIR,
5065
5416
  REQ_HEAD_RE,
5066
5417
  REQ_ID_RE,
@@ -5079,6 +5430,7 @@ export {
5079
5430
  TASK_LINE_RE,
5080
5431
  TOOL_NAME,
5081
5432
  TOOL_URL,
5433
+ addLink,
5082
5434
  adoptWorkspace,
5083
5435
  appendRunEvent,
5084
5436
  applyUpgrade,
@@ -5102,6 +5454,8 @@ export {
5102
5454
  compareTaskIds,
5103
5455
  computeInputsHash,
5104
5456
  configToYaml,
5457
+ contractCoverage,
5458
+ contractsAdvisory,
5105
5459
  copyFile,
5106
5460
  countBySeverity,
5107
5461
  createChange,
@@ -5119,6 +5473,7 @@ export {
5119
5473
  editIssue,
5120
5474
  emitFrontmatter,
5121
5475
  ensureDir,
5476
+ ensureLinksFile,
5122
5477
  ensureSddDirs,
5123
5478
  esc,
5124
5479
  evaluatePacks,
@@ -5128,6 +5483,7 @@ export {
5128
5483
  findWorkspaceRoot,
5129
5484
  firstToken,
5130
5485
  foldDelta,
5486
+ formatOf,
5131
5487
  generateDocs,
5132
5488
  generatePresentation,
5133
5489
  generateRunId,
@@ -5147,6 +5503,8 @@ export {
5147
5503
  issueBody,
5148
5504
  issueLabels,
5149
5505
  laneOrDefault,
5506
+ linkedRequirementIds,
5507
+ linkedTraceInput,
5150
5508
  lintDelta,
5151
5509
  lintMockupHtml,
5152
5510
  lintMockupManifest,
@@ -5164,7 +5522,9 @@ export {
5164
5522
  loadApprovals,
5165
5523
  loadChange,
5166
5524
  loadConfig,
5525
+ loadContracts,
5167
5526
  loadDetectedBest,
5527
+ loadLinks,
5168
5528
  loadLivingFixes,
5169
5529
  loadProfileFile,
5170
5530
  loadProfilesFromDir,
@@ -5186,6 +5546,7 @@ export {
5186
5546
  parseChangeMeta,
5187
5547
  parseClarify,
5188
5548
  parseConfig,
5549
+ parseContract,
5189
5550
  parseDelta,
5190
5551
  parseFixCovers,
5191
5552
  parseFrontmatter,
@@ -5210,12 +5571,14 @@ export {
5210
5571
  readTextIfExists,
5211
5572
  recordEvidence,
5212
5573
  regenerateIndex,
5574
+ removeLink,
5213
5575
  renderDocument,
5214
5576
  renderMarkdown,
5215
5577
  renderRequirement,
5216
5578
  renderStyles,
5217
5579
  renderTemplate,
5218
5580
  requiresMockups,
5581
+ resolveLinkPath,
5219
5582
  resolvePacks,
5220
5583
  rollbackUpgrade,
5221
5584
  ruleDescription,