@specatlas/core 0.1.26 → 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.
Files changed (3) hide show
  1. package/dist/index.d.ts +177 -3
  2. package/dist/index.js +833 -224
  3. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -61,6 +61,9 @@ var atlasConfigSchema = z.object({
61
61
  analyze: z.object({ mode: z.enum(["off", "advisory", "blocking"]).default("blocking"), min_severity: z.enum(["low", "medium", "high"]).default("medium") }).default({}),
62
62
  verify: z.object({ mode: z.enum(["off", "advisory", "blocking"]).default("blocking"), require_evidence: z.boolean().default(true) }).default({}),
63
63
  review: z.object({ mode: z.enum(["off", "advisory", "blocking"]).default("advisory") }).default({}),
64
+ clarify: z.object({ mode: z.enum(["off", "advisory", "blocking"]).default("advisory") }).default({}),
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({}),
64
67
  mockup: z.object({ require_approval: z.boolean().default(false), compare_in_verify: z.boolean().default(false) }).default({})
65
68
  }).default({}),
66
69
  trace: z.object({ mode: z.enum(["off", "advisory", "blocking"]).default("blocking"), prefix: z.string().default("REQ") }).default({}),
@@ -209,6 +212,7 @@ var SCENARIO_HEAD_RE = /^####\s+(?:Scenario|Escenario):\s+(REQ-[A-Z0-9-]+-S\d+)\
209
212
  var RULE_RE = /^-\s*(?:Rule|Regla)\s+(BR-[A-Z0-9-]+)\s*:\s*(.+?)\s*$/;
210
213
  var WHEN_RE = /^-\s*\*\*\s*(?:WHEN|CUANDO|DADO QUE)\s*\*\*\s*(.+?)\s*$/i;
211
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;
212
216
  function newDraft(id, title, line) {
213
217
  return { id, title, line, prose: [], rules: [], scenarios: [] };
214
218
  }
@@ -293,6 +297,16 @@ function parseRequirementBlocks(body, startLine = 1, filePath) {
293
297
  currentScenario.then.push(thenMatch[1] ?? "");
294
298
  continue;
295
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
+ }
296
310
  if (current && !currentScenario) {
297
311
  if (line.trim() !== "") current.prose.push(line.trim());
298
312
  }
@@ -640,6 +654,35 @@ function findScenarioHeading(lines, blockLineIndex) {
640
654
  return void 0;
641
655
  }
642
656
 
657
+ // src/parse/clarify.ts
658
+ var OPEN_RE = /^\s*-\s*\[\s\]\s+(.+?)\s*$/;
659
+ var DONE_RE = /^\s*-\s*\[[xX]\]\s+(.+?)\s*$/;
660
+ var ANSWER_SEPARATOR = " \u2014 ";
661
+ function splitAnswer(raw) {
662
+ const index = raw.indexOf(ANSWER_SEPARATOR);
663
+ if (index === -1) return { text: raw.trim() };
664
+ return { text: raw.slice(0, index).trim(), answer: raw.slice(index + ANSWER_SEPARATOR.length).trim() };
665
+ }
666
+ function parseClarify(content, filePath) {
667
+ const fm = parseFrontmatter(content, filePath);
668
+ const lines = fm.body.replace(/\r\n?/g, "\n").split("\n");
669
+ const open = [];
670
+ const resolved = [];
671
+ for (let i = 0; i < lines.length; i += 1) {
672
+ const line = lines[i] ?? "";
673
+ const lineNo = fm.bodyStartLine + i;
674
+ const done = DONE_RE.exec(line);
675
+ if (done) {
676
+ const item = splitAnswer(done[1] ?? "");
677
+ resolved.push({ text: item.text, line: lineNo, ...item.answer !== void 0 ? { answer: item.answer } : {} });
678
+ continue;
679
+ }
680
+ const pending = OPEN_RE.exec(line);
681
+ if (pending) open.push({ text: pending[1] ?? "", line: lineNo });
682
+ }
683
+ return { path: filePath, open, resolved, diagnostics: [] };
684
+ }
685
+
643
686
  // src/parse/glossary.ts
644
687
  var HEADER_KEYS = /* @__PURE__ */ new Set(["t\xE9rmino", "termino", "term", "definici\xF3n", "definicion", "definition", "sin\xF3nimos", "sinonimos", "synonyms"]);
645
688
  var SEPARATOR_RE = /^:?-{2,}:?$/;
@@ -731,6 +774,7 @@ function impactOfRequirement(workspace, reqId) {
731
774
  const id = reqId.toUpperCase();
732
775
  const scenarios = /* @__PURE__ */ new Set();
733
776
  let exists2 = false;
777
+ let origin;
734
778
  for (const spec of workspace.specs) {
735
779
  for (const req of spec.spec.requirements) {
736
780
  if (req.id !== id) continue;
@@ -747,6 +791,18 @@ function impactOfRequirement(workspace, reqId) {
747
791
  if ((change.delta?.removed ?? []).some((r) => r.id === id)) exists2 = true;
748
792
  if ((change.delta?.renamed ?? []).some((r) => r.from.id === id || r.to.id === id)) exists2 = true;
749
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
+ }
750
806
  const report = emptyReport(id, "requirement");
751
807
  if (!exists2) return report;
752
808
  report.exists = true;
@@ -754,6 +810,10 @@ function impactOfRequirement(workspace, reqId) {
754
810
  const reqOf = scenarioToReq(workspace);
755
811
  collect(workspace, report, /* @__PURE__ */ new Set([id, ...scenarios]), reqOf);
756
812
  if (!report.requirements.includes(id)) report.requirements.unshift(id);
813
+ if (origin !== void 0) {
814
+ report.external = true;
815
+ report.origin = origin;
816
+ }
757
817
  return report;
758
818
  }
759
819
  function impactOfFile(workspace, file) {
@@ -794,14 +854,14 @@ function wordBoundary(text, term) {
794
854
  const re = new RegExp(`(?<![\\p{L}\\p{N}])${escaped}(?![\\p{L}\\p{N}])`, "iu");
795
855
  return re.test(text);
796
856
  }
797
- function lintText(text, code, terms, label, path22, line) {
857
+ function lintText(text, code, terms, label, path25, line) {
798
858
  const out = [];
799
859
  const lower = text.toLowerCase();
800
860
  for (const term of terms) {
801
861
  if (wordBoundary(lower, term)) {
802
862
  out.push(
803
863
  diag(code, "error", `${label}: "${term}"`, {
804
- path: path22,
864
+ path: path25,
805
865
  line,
806
866
  suggestion: "La especificaci\xF3n es funcional y de negocio: describe comportamiento, no tecnolog\xEDa ni adjetivos vagos"
807
867
  })
@@ -810,7 +870,7 @@ function lintText(text, code, terms, label, path22, line) {
810
870
  }
811
871
  return out;
812
872
  }
813
- function lintRequirement(req, path22, opts = {}) {
873
+ function lintRequirement(req, path25, opts = {}) {
814
874
  const out = [];
815
875
  const vague = opts.language === "en" ? VAGUE_EN : VAGUE_ES;
816
876
  const tech = opts.language === "en" ? TECH_EN : TECH_ES;
@@ -821,32 +881,32 @@ function lintRequirement(req, path22, opts = {}) {
821
881
  ...req.scenarios.flatMap((s) => [...s.when.map((w) => ({ text: w, line: s.line })), ...s.then.map((t) => ({ text: t, line: s.line }))])
822
882
  ];
823
883
  for (const part of parts) {
824
- out.push(...lintText(part.text, "LINT-BIZ-002", vague, "Palabra vaga en la especificaci\xF3n", path22, part.line));
884
+ out.push(...lintText(part.text, "LINT-BIZ-002", vague, "Palabra vaga en la especificaci\xF3n", path25, part.line));
825
885
  if (opts.businessOnly !== false) {
826
- out.push(...lintText(part.text, "LINT-BIZ-001", tech, "Jerga t\xE9cnica en la especificaci\xF3n de negocio", path22, part.line));
886
+ out.push(...lintText(part.text, "LINT-BIZ-001", tech, "Jerga t\xE9cnica en la especificaci\xF3n de negocio", path25, part.line));
827
887
  }
828
888
  }
829
889
  if (req.scenarios.length === 0) {
830
- out.push(diag("TRACE-001", "error", `El requisito ${req.id} no tiene ning\xFAn escenario`, { path: path22, 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" }));
831
891
  }
832
892
  return out;
833
893
  }
834
- function lintDelta(delta, livingRequirements, path22, opts = {}) {
894
+ function lintDelta(delta, livingRequirements, path25, opts = {}) {
835
895
  const out = [...delta.diagnostics];
836
896
  for (const req of [...delta.added, ...delta.modified]) {
837
- out.push(...lintRequirement(req, path22, opts));
897
+ out.push(...lintRequirement(req, path25, opts));
838
898
  }
839
899
  for (const req of delta.modified) {
840
900
  const living = livingRequirements.get(req.id);
841
901
  if (!living) {
842
- out.push(diag("TRACE-007", "error", `MODIFIED ${req.id} no existe en la spec viva; usa ADDED`, { path: path22, 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 }));
843
903
  continue;
844
904
  }
845
905
  for (const existing of living.scenarios) {
846
906
  if (!req.scenarios.some((s) => s.id === existing.id)) {
847
907
  out.push(
848
908
  diag("TRACE-007", "error", `MODIFIED ${req.id} pierde el escenario ${existing.id}: copia el bloque completo`, {
849
- path: path22,
909
+ path: path25,
850
910
  line: req.line,
851
911
  suggestion: "Copia el bloque completo de la spec viva y ed\xEDtalo; para quitarlo, decl\xE1ralo en REMOVED"
852
912
  })
@@ -857,15 +917,15 @@ function lintDelta(delta, livingRequirements, path22, opts = {}) {
857
917
  for (const req of delta.removed) {
858
918
  const living = livingRequirements.get(req.id);
859
919
  if (!living) {
860
- out.push(diag("TRACE-007", "error", `REMOVED ${req.id} no existe en la spec viva`, { path: path22, line: req.line }));
920
+ out.push(diag("TRACE-007", "error", `REMOVED ${req.id} no existe en la spec viva`, { path: path25, line: req.line }));
861
921
  }
862
922
  }
863
923
  for (const rename2 of delta.renamed) {
864
924
  if (rename2.from.id !== rename2.to.id) {
865
- out.push(diag("LINT-DLT-003", "error", `RENAMED cambia el id (${rename2.from.id} \u2192 ${rename2.to.id}); los ids son inmutables`, { path: path22, 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 }));
866
926
  }
867
927
  if (!livingRequirements.has(rename2.from.id)) {
868
- out.push(diag("TRACE-007", "error", `RENAMED ${rename2.from.id} no existe en la spec viva`, { path: path22, 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 }));
869
929
  }
870
930
  }
871
931
  return out;
@@ -895,7 +955,7 @@ var MERMAID_KEYWORDS = [
895
955
  "architecture-beta"
896
956
  ];
897
957
  var MERMAID_BLOCKS = /^\s*(alt|loop|opt|par|rect|critical|break|subgraph)\b/;
898
- function lintPlan(planText, path22) {
958
+ function lintPlan(planText, path25) {
899
959
  const out = [];
900
960
  const blocks = [...planText.matchAll(/```mermaid\r?\n([\s\S]*?)```/g)];
901
961
  for (const [index, block] of blocks.entries()) {
@@ -905,7 +965,7 @@ function lintPlan(planText, path22) {
905
965
  if (!MERMAID_KEYWORDS.some((keyword) => first.startsWith(keyword))) {
906
966
  out.push(
907
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)}"`, {
908
- path: path22,
968
+ path: path25,
909
969
  suggestion: "Corrige el tipo del diagrama o elimina el bloque"
910
970
  })
911
971
  );
@@ -919,7 +979,7 @@ function lintPlan(planText, path22) {
919
979
  if (open !== 0) {
920
980
  out.push(
921
981
  diag("LINT-PLN-002", "error", `Diagrama mermaid ${index + 1}: faltan ${Math.abs(open)} \`end\` (bloques alt/loop/subgraph sin cerrar)`, {
922
- path: path22,
982
+ path: path25,
923
983
  suggestion: "Cierra cada bloque alt/loop/opt/par/rect/subgraph con `end`"
924
984
  })
925
985
  );
@@ -931,7 +991,7 @@ function lintPlan(planText, path22) {
931
991
  if (message.includes(";")) {
932
992
  out.push(
933
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`, {
934
- path: path22,
994
+ path: path25,
935
995
  suggestion: "Sustituye `;` por `\xB7` o `,` en los mensajes de sequenceDiagram"
936
996
  })
937
997
  );
@@ -1044,15 +1104,25 @@ function checkTrace(input) {
1044
1104
  );
1045
1105
  }
1046
1106
  }
1107
+ const linkedIds = new Set(input.linked?.ids ?? []);
1108
+ const unavailableLinks = input.linked?.unavailable ?? [];
1047
1109
  for (const task of taskById.values()) {
1048
1110
  for (const c of task.covers) {
1049
- if (!livingReqs.has(c) && !deltaScenarios.has(c) && !allScenarioIds.has(c)) {
1111
+ if (!livingReqs.has(c) && !deltaScenarios.has(c) && !allScenarioIds.has(c) && !linkedIds.has(c)) {
1050
1112
  findings.push(
1051
1113
  diag("TRACE-003", "error", `La tarea ${task.id} cubre ${c}, que no existe`, {
1052
1114
  path: change.tasks?.path,
1053
- 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"
1054
1116
  })
1055
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
+ }
1056
1126
  }
1057
1127
  }
1058
1128
  for (const dep of task.dependsOn) {
@@ -1219,12 +1289,172 @@ function planBlock(block, maxParallel) {
1219
1289
  }
1220
1290
 
1221
1291
  // src/lifecycle.ts
1292
+ import path3 from "path";
1293
+
1294
+ // src/contracts.ts
1222
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
1223
1453
  function verifyApproval(change, approvals, cfg, specContent) {
1224
1454
  if (cfg.gates.approval === "none") return { status: "not_required" };
1225
1455
  if ((change.meta?.overrides ?? []).some((o) => o.gate === "approval")) return { status: "overridden" };
1226
1456
  if (!change.delta) return { status: "missing" };
1227
- const key = path2.posix.join("changes", change.slug, "spec.md");
1457
+ const key = path3.posix.join("changes", change.slug, "spec.md");
1228
1458
  const approval = approvals.get(key) ?? approvals.get(`${key}`);
1229
1459
  if (!approval) return { status: "missing" };
1230
1460
  if (specContent === void 0) return { status: "valid", approvedBy: approval.by, approvedAt: approval.at };
@@ -1240,6 +1470,29 @@ function requiresMockups(meta, cfg) {
1240
1470
  function mockupOverride(change) {
1241
1471
  return (change.meta?.overrides ?? []).some((override) => override.gate === "mockup");
1242
1472
  }
1473
+ function docsReady(change) {
1474
+ const paths = change.docsPaths ?? [];
1475
+ return paths.some((p) => p.endsWith("tecnica.md")) && paths.some((p) => p.endsWith("manual.md"));
1476
+ }
1477
+ function clarifyAdvisory(change, cfg) {
1478
+ const open = change.clarify?.open.length ?? 0;
1479
+ if (cfg.gates.clarify.mode !== "advisory" || open === 0) return [];
1480
+ return [
1481
+ diag("ATLAS-CLARIFY-001", "warning", `El cambio "${change.slug}" tiene ${open} pregunta(s) sin aclarar`, {
1482
+ ...change.clarifyPath !== void 0 ? { path: change.clarifyPath } : {},
1483
+ suggestion: `Aclara antes de planificar: /satlas.clarify ${change.slug} (o satlas clarify ${change.slug})`
1484
+ })
1485
+ ];
1486
+ }
1487
+ function docsAdvisory(change, cfg) {
1488
+ const lane = change.meta?.lane ?? cfg.lanes.default;
1489
+ if (lane !== "full" || cfg.gates.docs.mode !== "advisory" || docsReady(change)) return [];
1490
+ return [
1491
+ diag("ATLAS-DOCS-001", "warning", `El cambio "${change.slug}" (carril completo) no tiene su documentaci\xF3n t\xE9cnica y manual`, {
1492
+ suggestion: `Genera la documentaci\xF3n: satlas docs ${change.slug} (o /satlas.docs ${change.slug})`
1493
+ })
1494
+ ];
1495
+ }
1243
1496
  function deriveState(input) {
1244
1497
  const { change, cfg, approval, blockingFindings } = input;
1245
1498
  const lane = change.meta?.lane ?? cfg.lanes.default;
@@ -1302,6 +1555,16 @@ function deriveState(input) {
1302
1555
  };
1303
1556
  }
1304
1557
  if (!change.planPath && !change.tasks) {
1558
+ const openQuestions = change.clarify?.open.length ?? 0;
1559
+ if (openQuestions > 0 && cfg.gates.clarify.mode === "blocking") {
1560
+ blockedBy.push(`aclaraci\xF3n pendiente (${openQuestions})`);
1561
+ return {
1562
+ state: "approved",
1563
+ blockedBy,
1564
+ nextAction: next(`/satlas.clarify ${change.slug}`, `Aclarar ${openQuestions} pregunta(s) antes de planificar`, true),
1565
+ progress
1566
+ };
1567
+ }
1305
1568
  return { state: "approved", blockedBy, nextAction: next(`/satlas.plan ${change.slug}`, "Crear el plan t\xE9cnico y las tareas", true), progress };
1306
1569
  }
1307
1570
  if (tasksTotal > 0 && tasksDone < tasksTotal) {
@@ -1315,6 +1578,17 @@ function deriveState(input) {
1315
1578
  blockedBy.push("review pendiente");
1316
1579
  return { state: "verified", blockedBy, nextAction: next(`/satlas.review ${change.slug}`, "Revisi\xF3n de c\xF3digo", true), progress };
1317
1580
  }
1581
+ if (lane === "full" && cfg.gates.docs.mode === "blocking" && !docsReady(change)) {
1582
+ blockedBy.push("documentaci\xF3n pendiente");
1583
+ return { state: "reviewed", blockedBy, nextAction: next(`/satlas.docs ${change.slug}`, "Generar la documentaci\xF3n t\xE9cnica y manual del cambio", true), progress };
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
+ }
1318
1592
  return { state: "ready", blockedBy, nextAction: next(`satlas archive ${change.slug}`, "Archivar el cambio y plegar los deltas"), progress };
1319
1593
  }
1320
1594
  function stateLabel(state) {
@@ -1328,6 +1602,7 @@ function stateLabel(state) {
1328
1602
  building: "construyendo",
1329
1603
  built: "construido",
1330
1604
  verified: "verificado",
1605
+ reviewed: "revisado",
1331
1606
  ready: "listo para archivar",
1332
1607
  archived: "archivado"
1333
1608
  };
@@ -1335,8 +1610,8 @@ function stateLabel(state) {
1335
1610
  }
1336
1611
 
1337
1612
  // src/profiles.ts
1338
- import path3 from "path";
1339
- import { parse as parseYaml4 } from "yaml";
1613
+ import path4 from "path";
1614
+ import { parse as parseYaml5 } from "yaml";
1340
1615
  import { z as z3 } from "zod";
1341
1616
  var profileSchema = z3.object({
1342
1617
  name: z3.string().min(1),
@@ -1383,7 +1658,7 @@ async function loadProfileFile(filePath) {
1383
1658
  if (raw === void 0) return void 0;
1384
1659
  let data;
1385
1660
  try {
1386
- data = parseYaml4(raw);
1661
+ data = parseYaml5(raw);
1387
1662
  } catch {
1388
1663
  return void 0;
1389
1664
  }
@@ -1395,7 +1670,7 @@ async function loadProfilesFromDir(dir) {
1395
1670
  const out = [];
1396
1671
  for (const entry of entries) {
1397
1672
  if (!/\.ya?ml$/i.test(entry)) continue;
1398
- const profile = await loadProfileFile(path3.join(dir, entry));
1673
+ const profile = await loadProfileFile(path4.join(dir, entry));
1399
1674
  if (profile) out.push(profile);
1400
1675
  }
1401
1676
  return out;
@@ -1433,10 +1708,10 @@ async function detectProfiles(root, profiles) {
1433
1708
  return result;
1434
1709
  }
1435
1710
  async function loadDetectedBest(sddDir) {
1436
- const raw = await readTextIfExists(path3.join(sddDir, "profiles", "detected.yaml"));
1711
+ const raw = await readTextIfExists(path4.join(sddDir, "profiles", "detected.yaml"));
1437
1712
  if (raw === void 0) return void 0;
1438
1713
  try {
1439
- const data = parseYaml4(raw);
1714
+ const data = parseYaml5(raw);
1440
1715
  return typeof data?.best === "string" ? data.best : void 0;
1441
1716
  } catch {
1442
1717
  return void 0;
@@ -1473,12 +1748,12 @@ function detectionToYaml(result, generatedAt) {
1473
1748
  }
1474
1749
 
1475
1750
  // src/workspace.ts
1476
- import path5 from "path";
1751
+ import path7 from "path";
1477
1752
 
1478
1753
  // src/fixes.ts
1479
- import path4 from "path";
1754
+ import path5 from "path";
1480
1755
  import { stringify as stringifyYaml3 } from "yaml";
1481
- var LIVING_FIXES_DIR = path4.join(".sdd", "fixes");
1756
+ var LIVING_FIXES_DIR = path5.join(".sdd", "fixes");
1482
1757
  var COMMENT_RE = /<!--[\s\S]*?-->/g;
1483
1758
  function parseFixCovers(content) {
1484
1759
  const clean = content.replace(COMMENT_RE, "");
@@ -1505,7 +1780,7 @@ function coversOf(value) {
1505
1780
  function parseLivingFix(content, file) {
1506
1781
  const fm = parseFrontmatter(content, file);
1507
1782
  const data = fm.data;
1508
- const base = path4.basename(file).replace(/\.md$/i, "");
1783
+ const base = path5.basename(file).replace(/\.md$/i, "");
1509
1784
  const slug = typeof data["slug"] === "string" && data["slug"].trim() !== "" ? data["slug"].trim() : base.replace(/^\d{4}-\d{2}-/, "");
1510
1785
  const fix = {
1511
1786
  slug,
@@ -1521,14 +1796,14 @@ function parseLivingFix(content, file) {
1521
1796
  return fix;
1522
1797
  }
1523
1798
  async function archivedFixes(root, known) {
1524
- const archiveDir = path4.join(root, ".sdd", "changes", "archive");
1799
+ const archiveDir = path5.join(root, ".sdd", "changes", "archive");
1525
1800
  const fixes = [];
1526
1801
  for (const entry of await listDirs(archiveDir)) {
1527
- const dir = path4.join(archiveDir, entry);
1528
- const fixFile = path4.join(dir, "fix.md");
1802
+ const dir = path5.join(archiveDir, entry);
1803
+ const fixFile = path5.join(dir, "fix.md");
1529
1804
  const fixRaw = await readTextIfExists(fixFile);
1530
1805
  if (fixRaw === void 0) continue;
1531
- const metaFile = path4.join(dir, "meta.yaml");
1806
+ const metaFile = path5.join(dir, "meta.yaml");
1532
1807
  const metaRaw = await readTextIfExists(metaFile);
1533
1808
  const meta = metaRaw !== void 0 ? parseChangeMeta(metaRaw, metaFile).meta : void 0;
1534
1809
  if (meta?.lane !== "fix") continue;
@@ -1551,11 +1826,11 @@ async function archivedFixes(root, known) {
1551
1826
  return fixes;
1552
1827
  }
1553
1828
  async function loadLivingFixes(root) {
1554
- const dir = path4.join(root, LIVING_FIXES_DIR);
1829
+ const dir = path5.join(root, LIVING_FIXES_DIR);
1555
1830
  const entries = (await listDir(dir)).filter((entry) => entry.toLowerCase().endsWith(".md")).sort();
1556
1831
  const fixes = [];
1557
1832
  for (const entry of entries) {
1558
- const file = path4.join(dir, entry);
1833
+ const file = path5.join(dir, entry);
1559
1834
  const content = await readTextIfExists(file);
1560
1835
  if (content === void 0) continue;
1561
1836
  fixes.push(parseLivingFix(content, file));
@@ -1564,10 +1839,10 @@ async function loadLivingFixes(root) {
1564
1839
  return fixes.sort((a, b) => b.date.localeCompare(a.date) || b.slug.localeCompare(a.slug));
1565
1840
  }
1566
1841
  async function writeLivingFix(root, input) {
1567
- const dir = path4.join(root, LIVING_FIXES_DIR);
1842
+ const dir = path5.join(root, LIVING_FIXES_DIR);
1568
1843
  const month = input.date.slice(0, 7);
1569
- const file = path4.join(dir, `${month}-${input.slug}.md`);
1570
- 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));
1571
1846
  if (await exists(file)) return { file, relativePath, created: false };
1572
1847
  const data = {
1573
1848
  slug: input.slug,
@@ -1587,35 +1862,170 @@ ${body}
1587
1862
  return { file, relativePath, created: true };
1588
1863
  }
1589
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
+
1590
2000
  // src/workspace.ts
1591
2001
  var SDD_DIR = ".sdd";
1592
2002
  async function findWorkspaceRoot(start) {
1593
- let current = path5.resolve(start);
2003
+ let current = path7.resolve(start);
1594
2004
  for (let i = 0; i < 40; i += 1) {
1595
- if (await isDirectory(path5.join(current, SDD_DIR))) return current;
1596
- const parent = path5.dirname(current);
2005
+ if (await isDirectory(path7.join(current, SDD_DIR))) return current;
2006
+ const parent = path7.dirname(current);
1597
2007
  if (parent === current) return void 0;
1598
2008
  current = parent;
1599
2009
  }
1600
2010
  return void 0;
1601
2011
  }
1602
2012
  async function loadApprovals(sddDir) {
1603
- const file = path5.join(sddDir, "approvals.yaml");
2013
+ const file = path7.join(sddDir, "approvals.yaml");
1604
2014
  const raw = await readTextIfExists(file);
1605
2015
  if (raw === void 0) return { byArtifact: /* @__PURE__ */ new Map(), diagnostics: [] };
1606
2016
  const parsed = parseApprovals(raw, file);
1607
2017
  const map = /* @__PURE__ */ new Map();
1608
2018
  for (const a of parsed.approvals?.approvals ?? []) {
1609
- 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 });
1610
2020
  }
1611
2021
  return { byArtifact: map, diagnostics: parsed.diagnostics };
1612
2022
  }
1613
2023
  async function loadSpecs(sddDir) {
1614
- const specsDir = path5.join(sddDir, "specs");
2024
+ const specsDir = path7.join(sddDir, "specs");
1615
2025
  const domains = await listDirs(specsDir);
1616
2026
  const out = [];
1617
2027
  for (const domain of domains) {
1618
- const file = path5.join(specsDir, domain, "spec.md");
2028
+ const file = path7.join(specsDir, domain, "spec.md");
1619
2029
  if (!await exists(file)) continue;
1620
2030
  const content = await readText(file);
1621
2031
  out.push({ domain, path: file, spec: parseSpecFile(content, file) });
@@ -1623,10 +2033,10 @@ async function loadSpecs(sddDir) {
1623
2033
  return out;
1624
2034
  }
1625
2035
  async function loadChange(root, slug, relDir) {
1626
- const dir = path5.join(root, SDD_DIR, "changes", relDir ?? slug);
2036
+ const dir = path7.join(root, SDD_DIR, "changes", relDir ?? slug);
1627
2037
  const diagnostics = [];
1628
2038
  const change = { slug, dir, diagnostics };
1629
- const metaFile = path5.join(dir, "meta.yaml");
2039
+ const metaFile = path7.join(dir, "meta.yaml");
1630
2040
  const metaRaw = await readTextIfExists(metaFile);
1631
2041
  if (metaRaw === void 0) {
1632
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" }));
@@ -1635,7 +2045,7 @@ async function loadChange(root, slug, relDir) {
1635
2045
  diagnostics.push(...parsed.diagnostics);
1636
2046
  if (parsed.meta) change.meta = parsed.meta;
1637
2047
  }
1638
- const deltaFile = path5.join(dir, "spec.md");
2048
+ const deltaFile = path7.join(dir, "spec.md");
1639
2049
  const deltaRaw = await readTextIfExists(deltaFile);
1640
2050
  if (deltaRaw !== void 0) {
1641
2051
  change.delta = parseDelta(deltaRaw, deltaFile);
@@ -1643,42 +2053,57 @@ async function loadChange(root, slug, relDir) {
1643
2053
  } else {
1644
2054
  diagnostics.push(diag("ATLAS-FILES-002", "warning", `El cambio "${slug}" no tiene spec.md (delta)`, { path: deltaFile }));
1645
2055
  }
1646
- const planFile = path5.join(dir, "plan.md");
2056
+ const planFile = path7.join(dir, "plan.md");
1647
2057
  if (await exists(planFile)) change.planPath = planFile;
1648
- const reviewFile = path5.join(dir, "review.md");
2058
+ const reviewFile = path7.join(dir, "review.md");
1649
2059
  if (await exists(reviewFile)) change.reviewPath = reviewFile;
1650
- const presentationFile = path5.join(dir, "presentation", "index.html");
2060
+ const presentationFile = path7.join(dir, "presentation", "index.html");
1651
2061
  if (await exists(presentationFile)) change.presentationPath = presentationFile;
1652
- const tasksFile = path5.join(dir, "tasks.md");
2062
+ const tasksFile = path7.join(dir, "tasks.md");
1653
2063
  const tasksRaw = await readTextIfExists(tasksFile);
1654
2064
  if (tasksRaw !== void 0) {
1655
2065
  change.tasks = parseTasksFile(tasksRaw, tasksFile);
1656
2066
  diagnostics.push(...change.tasks.diagnostics);
1657
2067
  }
1658
- const verifyFile = path5.join(dir, "verify.md");
2068
+ const verifyFile = path7.join(dir, "verify.md");
1659
2069
  const verifyRaw = await readTextIfExists(verifyFile);
1660
2070
  if (verifyRaw !== void 0) {
1661
2071
  change.verify = parseVerifyFile(verifyRaw, verifyFile);
1662
2072
  diagnostics.push(...change.verify.diagnostics);
1663
2073
  }
1664
- const fixFile = path5.join(dir, "fix.md");
2074
+ const fixFile = path7.join(dir, "fix.md");
1665
2075
  const fixRaw = await readTextIfExists(fixFile);
1666
2076
  if (fixRaw !== void 0) {
1667
2077
  change.fix = parseVerifyFile(fixRaw, fixFile);
1668
2078
  diagnostics.push(...change.fix.diagnostics);
1669
2079
  change.fixCovers = parseFixCovers(fixRaw);
1670
2080
  }
1671
- const mockupManifest = path5.join(dir, "mockups", "manifest.yaml");
2081
+ const clarifyFile = path7.join(dir, "clarify.md");
2082
+ const clarifyRaw = await readTextIfExists(clarifyFile);
2083
+ if (clarifyRaw !== void 0) {
2084
+ change.clarify = parseClarify(clarifyRaw, clarifyFile);
2085
+ change.clarifyPath = clarifyFile;
2086
+ diagnostics.push(...change.clarify.diagnostics);
2087
+ }
2088
+ const docsPaths = [];
2089
+ for (const name of ["tecnica.md", "manual.md"]) {
2090
+ const docFile = path7.join(dir, "docs", name);
2091
+ if (await exists(docFile)) docsPaths.push(docFile);
2092
+ }
2093
+ if (docsPaths.length > 0) change.docsPaths = docsPaths;
2094
+ const contracts = await loadContracts(dir);
2095
+ if (contracts.files.length > 0) change.contracts = contracts;
2096
+ const mockupManifest = path7.join(dir, "mockups", "manifest.yaml");
1672
2097
  if (await exists(mockupManifest)) change.mockupManifestPath = mockupManifest;
1673
2098
  return change;
1674
2099
  }
1675
2100
  async function listChangeSlugs(root, opts = {}) {
1676
- const changesDir = path5.join(root, SDD_DIR, "changes");
2101
+ const changesDir = path7.join(root, SDD_DIR, "changes");
1677
2102
  const dirs = await listDirs(changesDir);
1678
2103
  return dirs.filter((d) => opts.includeArchived ? true : d !== "archive");
1679
2104
  }
1680
2105
  async function loadWorkspace(root) {
1681
- const sddDir = path5.join(root, SDD_DIR);
2106
+ const sddDir = path7.join(root, SDD_DIR);
1682
2107
  const { config, diagnostics: configDiags } = await loadConfig(sddDir);
1683
2108
  const diagnostics = [...configDiags];
1684
2109
  const specs = await loadSpecs(sddDir);
@@ -1691,20 +2116,24 @@ async function loadWorkspace(root) {
1691
2116
  diagnostics.push(...change.diagnostics);
1692
2117
  }
1693
2118
  const archived = [];
1694
- const archivedDir = path5.join(sddDir, "changes", "archive");
2119
+ const archivedDir = path7.join(sddDir, "changes", "archive");
1695
2120
  if (await isDirectory(archivedDir)) {
1696
2121
  for (const entry of await listDirs(archivedDir)) {
1697
- 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)));
1698
2123
  }
1699
2124
  }
1700
- 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 };
1701
2130
  }
1702
2131
  async function ensureSddDirs(sddDir) {
1703
2132
  const created = [];
1704
- const dirs = ["specs", "changes", "runs", "metrics", "fixes", path5.join("profiles", "custom")];
2133
+ const dirs = ["specs", "changes", "runs", "metrics", "fixes", path7.join("profiles", "custom")];
1705
2134
  const { ensureDir: ensureDir2 } = await import("./fsx-VF2P7ALA.js");
1706
2135
  for (const d of dirs) {
1707
- const full = path5.join(sddDir, d);
2136
+ const full = path7.join(sddDir, d);
1708
2137
  if (!await exists(full)) {
1709
2138
  await ensureDir2(full);
1710
2139
  created.push(full);
@@ -1714,7 +2143,7 @@ async function ensureSddDirs(sddDir) {
1714
2143
  }
1715
2144
 
1716
2145
  // src/templates.ts
1717
- import { stringify as stringifyYaml4 } from "yaml";
2146
+ import { stringify as stringifyYaml5 } from "yaml";
1718
2147
  var ES = {
1719
2148
  constitution: `# Constituci\xF3n del proyecto
1720
2149
 
@@ -1884,6 +2313,44 @@ satlas verify <slug> --file fix --scenario REQ-DOMINIO-001-S1 --command "<comand
1884
2313
  o, si es manual:
1885
2314
  satlas verify <slug> --file fix --scenario REQ-DOMINIO-001-S1 --method manual --result pass --by "<nombre>" --notes "<c\xF3mo se comprob\xF3>"
1886
2315
  -->
2316
+ `,
2317
+ docTecnica: `# Documentaci\xF3n t\xE9cnica \u2014 {{TITLE}}
2318
+
2319
+ ## Resumen del cambio
2320
+
2321
+ - **Cambio**: \`{{SLUG}}\` \xB7 dominio \`{{DOMAIN}}\` \xB7 carril \`{{LANE}}\`
2322
+ - **Actualizado**: {{DATE}}
2323
+ - **Tareas**: {{TASKS}}
2324
+
2325
+ ## Requisitos y escenarios
2326
+
2327
+ {{REQUIREMENTS}}
2328
+
2329
+ ## Evidencia registrada
2330
+
2331
+ {{EVIDENCE}}
2332
+
2333
+ ## Pendiente de evidencia
2334
+
2335
+ {{PENDING}}
2336
+ `,
2337
+ docManual: `# Manual \u2014 {{TITLE}}
2338
+
2339
+ ## Qu\xE9 hace este cambio
2340
+
2341
+ {{TITLE}} \u2014 dominio \`{{DOMAIN}}\` (cambio \`{{SLUG}}\`, carril \`{{LANE}}\`).
2342
+
2343
+ ## C\xF3mo se usa
2344
+
2345
+ {{SCENARIOS}}
2346
+
2347
+ ## C\xF3mo se comprob\xF3
2348
+
2349
+ {{EVIDENCE}}
2350
+
2351
+ ## Pendiente de comprobar
2352
+
2353
+ {{PENDING}}
1887
2354
  `
1888
2355
  };
1889
2356
  var EN = {
@@ -2009,6 +2476,44 @@ Cubre: REQ-DOMAIN-001
2009
2476
  <!-- Register real evidence with:
2010
2477
  satlas verify <slug> --file fix --scenario REQ-DOMAIN-001-S1 --command "<command>" --by "<name>"
2011
2478
  -->
2479
+ `,
2480
+ docTecnica: `# Technical documentation \u2014 {{TITLE}}
2481
+
2482
+ ## Change summary
2483
+
2484
+ - **Change**: \`{{SLUG}}\` \xB7 domain \`{{DOMAIN}}\` \xB7 lane \`{{LANE}}\`
2485
+ - **Updated**: {{DATE}}
2486
+ - **Tasks**: {{TASKS}}
2487
+
2488
+ ## Requirements and scenarios
2489
+
2490
+ {{REQUIREMENTS}}
2491
+
2492
+ ## Recorded evidence
2493
+
2494
+ {{EVIDENCE}}
2495
+
2496
+ ## Pending evidence
2497
+
2498
+ {{PENDING}}
2499
+ `,
2500
+ docManual: `# Manual \u2014 {{TITLE}}
2501
+
2502
+ ## What this change does
2503
+
2504
+ {{TITLE}} \u2014 domain \`{{DOMAIN}}\` (change \`{{SLUG}}\`, lane \`{{LANE}}\`).
2505
+
2506
+ ## How to use it
2507
+
2508
+ {{SCENARIOS}}
2509
+
2510
+ ## How it was verified
2511
+
2512
+ {{EVIDENCE}}
2513
+
2514
+ ## Pending verification
2515
+
2516
+ {{PENDING}}
2012
2517
  `
2013
2518
  };
2014
2519
  function templatesFor(language) {
@@ -2030,7 +2535,7 @@ function changeMetaYaml(meta) {
2030
2535
  if (meta.owner) doc["owner"] = meta.owner;
2031
2536
  if (meta.mockups) doc["mockups"] = meta.mockups;
2032
2537
  return `# Estado del cambio. La fase se DERIVA de los artefactos; aqu\xED solo hechos.
2033
- ` + stringifyYaml4(doc, { lineWidth: 120 });
2538
+ ` + stringifyYaml5(doc, { lineWidth: 120 });
2034
2539
  }
2035
2540
  function indexMarkdown(input) {
2036
2541
  const es = input.language !== "en";
@@ -2068,8 +2573,8 @@ ${input.archived} change(s) in \`changes/archive/\`.`);
2068
2573
  }
2069
2574
 
2070
2575
  // src/github.ts
2071
- import path6 from "path";
2072
- import { parse as parseYaml5 } from "yaml";
2576
+ import path8 from "path";
2577
+ import { parse as parseYaml7 } from "yaml";
2073
2578
 
2074
2579
  // src/exec.ts
2075
2580
  import { execFile } from "child_process";
@@ -2213,8 +2718,8 @@ function issueBody(input) {
2213
2718
  lines.push(`- Siguiente: \`${input.next}\` \u2014 ${input.nextDescription}`);
2214
2719
  lines.push("");
2215
2720
  lines.push("### Artefactos");
2216
- lines.push(`- Spec: \`${path6.posix.join(".sdd", "changes", change.slug, "spec.md")}\``);
2217
- 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")}\``);
2218
2723
  if (change.tasks) lines.push(`- Tareas: ${change.tasks.counts.done}/${change.tasks.counts.total}`);
2219
2724
  if (change.verify) lines.push(`- Evidencia registrada: ${change.verify.evidence.length}`);
2220
2725
  lines.push("");
@@ -2282,14 +2787,14 @@ async function commentIssue(root, number, body, runner = defaultRunner) {
2282
2787
  return result.ok ? [] : [diag("ATLAS-GH-006", "error", `No se pudo comentar el issue #${number}: ${(result.stderr || result.stdout).trim().slice(0, 300)}`)];
2283
2788
  }
2284
2789
  async function patchChangeMeta(root, slug, patch) {
2285
- 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");
2286
2791
  const raw = await readTextIfExists(file);
2287
2792
  if (raw === void 0) {
2288
- 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)}`)] };
2289
2794
  }
2290
2795
  let data;
2291
2796
  try {
2292
- data = parseYaml5(raw) ?? {};
2797
+ data = parseYaml7(raw) ?? {};
2293
2798
  } catch (err) {
2294
2799
  return { path: file, diagnostics: [diag("ATLAS-GH-007", "error", `meta.yaml inv\xE1lido: ${err.message}`)] };
2295
2800
  }
@@ -2339,7 +2844,7 @@ async function approveFromGithub(opts) {
2339
2844
  const { signApproval: signApproval2 } = await import("./approvals-2PO6223I.js");
2340
2845
  const signed = await signApproval2({
2341
2846
  root: opts.root,
2342
- artifact: path6.posix.join("changes", opts.slug, "spec.md"),
2847
+ artifact: path8.posix.join("changes", opts.slug, "spec.md"),
2343
2848
  by,
2344
2849
  channel: "tracker",
2345
2850
  note: `Aprobado v\xEDa GitHub issue #${number} (etiqueta "${label}")`
@@ -2399,7 +2904,7 @@ async function syncGithubIssue(root, slug, opts = {}) {
2399
2904
  }
2400
2905
 
2401
2906
  // src/evidence.ts
2402
- import path7 from "path";
2907
+ import path9 from "path";
2403
2908
  var SCENARIO_HEAD_RE2 = /^###\s+(REQ-[A-Z0-9-]+-S\d+)\b/;
2404
2909
  function locateBlocks(content) {
2405
2910
  const normalized = content.replace(/\r\n?/g, "\n");
@@ -2437,10 +2942,10 @@ function buildEvidenceYaml(evidence) {
2437
2942
  return lines.join("\n");
2438
2943
  }
2439
2944
  async function recordEvidence(opts) {
2440
- const root = path7.resolve(opts.root);
2945
+ const root = path9.resolve(opts.root);
2441
2946
  const slug = opts.slug;
2442
2947
  const fileKind = opts.file ?? "verify";
2443
- 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");
2444
2949
  const diagnostics = [];
2445
2950
  const scenario = opts.scenario.toUpperCase();
2446
2951
  if (!SCENARIO_ID_RE.test(scenario)) {
@@ -2537,7 +3042,7 @@ function evidenceSummary(content) {
2537
3042
  }
2538
3043
 
2539
3044
  // src/runs.ts
2540
- import path8 from "path";
3045
+ import path10 from "path";
2541
3046
  import { randomBytes } from "crypto";
2542
3047
  var RUN_EVENT_TYPES = [
2543
3048
  "run_started",
@@ -2563,7 +3068,7 @@ function generateRunId(now = /* @__PURE__ */ new Date()) {
2563
3068
  return `${iso.slice(0, 8)}-${iso.slice(8, 14)}-${randomBytes(3).toString("hex")}`;
2564
3069
  }
2565
3070
  function runsDir(root) {
2566
- return path8.join(path8.resolve(root), ".sdd", "runs");
3071
+ return path10.join(path10.resolve(root), ".sdd", "runs");
2567
3072
  }
2568
3073
  async function createRun(root, slug, phase, inputs, now = /* @__PURE__ */ new Date()) {
2569
3074
  const runId = generateRunId(now);
@@ -2576,25 +3081,25 @@ async function createRun(root, slug, phase, inputs, now = /* @__PURE__ */ new Da
2576
3081
  updatedAt: localStamp(now)
2577
3082
  };
2578
3083
  if (inputs) state.inputs = inputs;
2579
- const dir = path8.join(runsDir(root), runId);
3084
+ const dir = path10.join(runsDir(root), runId);
2580
3085
  await ensureDir(dir);
2581
- 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)}
2582
3087
  `);
2583
- await writeText(path8.join(dir, "events.jsonl"), "");
3088
+ await writeText(path10.join(dir, "events.jsonl"), "");
2584
3089
  await appendRunEvent(root, runId, "run_started", { slug, phase });
2585
3090
  return state;
2586
3091
  }
2587
3092
  async function appendRunEvent(root, runId, type, data, now = /* @__PURE__ */ new Date()) {
2588
3093
  const event = { eventId: randomBytes(4).toString("hex"), at: localStamp(now), type };
2589
3094
  if (data) event.data = data;
2590
- const file = path8.join(runsDir(root), runId, "events.jsonl");
3095
+ const file = path10.join(runsDir(root), runId, "events.jsonl");
2591
3096
  const previous = await readTextIfExists(file) ?? "";
2592
3097
  await writeText(file, `${previous}${JSON.stringify(event)}
2593
3098
  `);
2594
3099
  return event;
2595
3100
  }
2596
3101
  async function updateRunStatus(root, runId, status, now = /* @__PURE__ */ new Date()) {
2597
- const file = path8.join(runsDir(root), runId, "state.json");
3102
+ const file = path10.join(runsDir(root), runId, "state.json");
2598
3103
  const raw = await readTextIfExists(file);
2599
3104
  if (raw === void 0) return;
2600
3105
  const state = JSON.parse(raw);
@@ -2604,13 +3109,13 @@ async function updateRunStatus(root, runId, status, now = /* @__PURE__ */ new Da
2604
3109
  `);
2605
3110
  }
2606
3111
  async function readRun(root, runId) {
2607
- const dir = path8.join(runsDir(root), runId);
2608
- 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"));
2609
3114
  if (stateRaw === void 0) {
2610
3115
  return { diagnostics: [diag("ATLAS-RUN-001", "error", `No existe el run ${runId}`, { path: dir })] };
2611
3116
  }
2612
3117
  const state = JSON.parse(stateRaw);
2613
- const eventsRaw = await readTextIfExists(path8.join(dir, "events.jsonl")) ?? "";
3118
+ const eventsRaw = await readTextIfExists(path10.join(dir, "events.jsonl")) ?? "";
2614
3119
  const events = [];
2615
3120
  for (const line of eventsRaw.split("\n")) {
2616
3121
  if (line.trim() === "") continue;
@@ -2627,7 +3132,7 @@ async function listRuns(root, filter = {}) {
2627
3132
  const ids = await listDirs(dir);
2628
3133
  const out = [];
2629
3134
  for (const id of ids) {
2630
- const raw = await readTextIfExists(path8.join(dir, id, "state.json"));
3135
+ const raw = await readTextIfExists(path10.join(dir, id, "state.json"));
2631
3136
  if (raw === void 0) continue;
2632
3137
  try {
2633
3138
  const state = JSON.parse(raw);
@@ -2641,11 +3146,11 @@ async function listRuns(root, filter = {}) {
2641
3146
  }
2642
3147
 
2643
3148
  // src/analyze.ts
2644
- import path11 from "path";
3149
+ import path13 from "path";
2645
3150
 
2646
3151
  // src/mockups.ts
2647
- import path9 from "path";
2648
- import { parse as parseYaml6, stringify as stringifyYaml5 } from "yaml";
3152
+ import path11 from "path";
3153
+ import { parse as parseYaml8, stringify as stringifyYaml6 } from "yaml";
2649
3154
  import { z as z4 } from "zod";
2650
3155
  var mockupManifestSchema = z4.object({
2651
3156
  schema_version: z4.number().int().positive().default(1),
@@ -2670,20 +3175,20 @@ var mockupManifestSchema = z4.object({
2670
3175
  });
2671
3176
  var TOKEN_CANDIDATES = ["design/tokens.json", "DESIGN.md", "design/DESIGN.md", ".sdd/design/tokens.json"];
2672
3177
  function mockupsDir(root, slug) {
2673
- return path9.join(path9.resolve(root), ".sdd", "changes", slug, "mockups");
3178
+ return path11.join(path11.resolve(root), ".sdd", "changes", slug, "mockups");
2674
3179
  }
2675
3180
  async function tokensFile(root) {
2676
3181
  for (const candidate of TOKEN_CANDIDATES) {
2677
- const abs = path9.join(root, candidate);
3182
+ const abs = path11.join(root, candidate);
2678
3183
  if (await exists(abs)) return candidate;
2679
3184
  }
2680
3185
  return void 0;
2681
3186
  }
2682
3187
  async function computeInputsHash(root, change) {
2683
- const deltaPath = path9.join(change.dir, "spec.md");
3188
+ const deltaPath = path11.join(change.dir, "spec.md");
2684
3189
  const delta = await readTextIfExists(deltaPath) ?? "";
2685
3190
  const tokens = await tokensFile(root);
2686
- const tokensContent = tokens ? await readTextIfExists(path9.join(root, tokens)) ?? "" : "";
3191
+ const tokensContent = tokens ? await readTextIfExists(path11.join(root, tokens)) ?? "" : "";
2687
3192
  return artifactHash(`${delta}
2688
3193
  ---tokens---
2689
3194
  ${tokensContent}`);
@@ -2708,12 +3213,12 @@ function planMockups(change, platform = "web") {
2708
3213
  return { platform, screens };
2709
3214
  }
2710
3215
  async function readMockupManifest(root, slug) {
2711
- const file = path9.join(mockupsDir(root, slug), "manifest.yaml");
3216
+ const file = path11.join(mockupsDir(root, slug), "manifest.yaml");
2712
3217
  const raw = await readTextIfExists(file);
2713
3218
  if (raw === void 0) return { diagnostics: [] };
2714
3219
  let data;
2715
3220
  try {
2716
- data = parseYaml6(raw);
3221
+ data = parseYaml8(raw);
2717
3222
  } catch (err) {
2718
3223
  return { path: file, diagnostics: [diag("ATLAS-MKP-002", "error", `manifest.yaml inv\xE1lido: ${err.message}`, { path: file })] };
2719
3224
  }
@@ -2800,7 +3305,7 @@ async function checkMockups(root, slug, change) {
2800
3305
  const { manifest, path: manifestPath, diagnostics } = await readMockupManifest(root, slug);
2801
3306
  const findings = [...diagnostics];
2802
3307
  if (!manifest || !manifestPath) {
2803
- 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 };
2804
3309
  }
2805
3310
  const known = /* @__PURE__ */ new Set();
2806
3311
  for (const req of [...change.delta?.added ?? [], ...change.delta?.modified ?? []]) {
@@ -2808,12 +3313,12 @@ async function checkMockups(root, slug, change) {
2808
3313
  }
2809
3314
  findings.push(...lintMockupManifest(manifest, known, manifestPath));
2810
3315
  for (const screen of manifest.screens) {
2811
- const html = await readTextIfExists(path9.join(dir, screen.file));
3316
+ const html = await readTextIfExists(path11.join(dir, screen.file));
2812
3317
  if (html === void 0) {
2813
- 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) }));
2814
3319
  continue;
2815
3320
  }
2816
- findings.push(...lintMockupHtml(html, path9.join(dir, screen.file)));
3321
+ findings.push(...lintMockupHtml(html, path11.join(dir, screen.file)));
2817
3322
  }
2818
3323
  const inputsHash = await computeInputsHash(root, change);
2819
3324
  const stale = manifest.inputsHash !== void 0 && manifest.inputsHash !== inputsHash;
@@ -2828,7 +3333,7 @@ async function checkMockups(root, slug, change) {
2828
3333
  return { findings, stale, manifest, manifestPath };
2829
3334
  }
2830
3335
  async function writeMockupPlan(root, slug, plan, now = /* @__PURE__ */ new Date()) {
2831
- const file = path9.join(mockupsDir(root, slug), "plan.yaml");
3336
+ const file = path11.join(mockupsDir(root, slug), "plan.yaml");
2832
3337
  if (await exists(file)) return file;
2833
3338
  const doc = {
2834
3339
  schema_version: 1,
@@ -2836,11 +3341,11 @@ async function writeMockupPlan(root, slug, plan, now = /* @__PURE__ */ new Date(
2836
3341
  platform: plan.platform,
2837
3342
  screens: plan.screens.map((s) => ({ id: s.id, title: s.title, file: s.file, illustrates: s.illustrates, states: s.states, breakpoints: s.breakpoints }))
2838
3343
  };
2839
- await writeText(file, stringifyYaml5(doc, { lineWidth: 120 }));
3344
+ await writeText(file, stringifyYaml6(doc, { lineWidth: 120 }));
2840
3345
  return file;
2841
3346
  }
2842
3347
  async function writeMockupManifest(root, slug, plan, inputsHash, now = /* @__PURE__ */ new Date()) {
2843
- const file = path9.join(mockupsDir(root, slug), "manifest.yaml");
3348
+ const file = path11.join(mockupsDir(root, slug), "manifest.yaml");
2844
3349
  if (await exists(file)) return file;
2845
3350
  const doc = {
2846
3351
  schema_version: 1,
@@ -2852,16 +3357,16 @@ async function writeMockupManifest(root, slug, plan, inputsHash, now = /* @__PUR
2852
3357
  screens: plan.screens.map((s) => ({ id: s.id, title: s.title, file: s.file, illustrates: s.illustrates, states: s.states, breakpoints: s.breakpoints })),
2853
3358
  screenshots: []
2854
3359
  };
2855
- await writeText(file, stringifyYaml5(doc, { lineWidth: 120 }));
3360
+ await writeText(file, stringifyYaml6(doc, { lineWidth: 120 }));
2856
3361
  return file;
2857
3362
  }
2858
3363
  async function updateMockupScreenshots(root, slug, screenshots) {
2859
- const file = path9.join(mockupsDir(root, slug), "manifest.yaml");
3364
+ const file = path11.join(mockupsDir(root, slug), "manifest.yaml");
2860
3365
  const raw = await readTextIfExists(file);
2861
3366
  if (raw === void 0) return void 0;
2862
- const data = parseYaml6(raw) ?? {};
3367
+ const data = parseYaml8(raw) ?? {};
2863
3368
  data["screenshots"] = screenshots;
2864
- await writeText(file, stringifyYaml5(data, { lineWidth: 120 }));
3369
+ await writeText(file, stringifyYaml6(data, { lineWidth: 120 }));
2865
3370
  return file;
2866
3371
  }
2867
3372
  async function captureMockups(root, slug, manifest, now = /* @__PURE__ */ new Date()) {
@@ -2886,9 +3391,9 @@ async function captureMockups(root, slug, manifest, now = /* @__PURE__ */ new Da
2886
3391
  const page2 = await browser.newPage();
2887
3392
  for (const bp of screen.breakpoints && screen.breakpoints.length > 0 ? screen.breakpoints : [1440]) {
2888
3393
  await page2.setViewportSize({ width: bp, height: 900 });
2889
- await page2.goto(`file://${path9.join(dir, screen.file).split(path9.sep).join("/")}`);
2890
- const rel = path9.posix.join("screens", `${screen.id}-${bp}.png`);
2891
- 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) });
2892
3397
  screenshots.push(rel);
2893
3398
  }
2894
3399
  await page2.close();
@@ -2904,7 +3409,7 @@ async function mockupsReady(root, slug, change) {
2904
3409
  return Boolean(check.manifest && check.manifest.screens.length > 0 && !check.stale);
2905
3410
  }
2906
3411
  async function setMockupRequirement(root, slug, value) {
2907
- const file = path9.join(root, ".sdd", "changes", slug, "meta.yaml");
3412
+ const file = path11.join(root, ".sdd", "changes", slug, "meta.yaml");
2908
3413
  const raw = await readTextIfExists(file) ?? `schema_version: 1
2909
3414
  slug: ${slug}
2910
3415
  lane: standard
@@ -2923,8 +3428,8 @@ lane: standard
2923
3428
  }
2924
3429
 
2925
3430
  // src/packs.ts
2926
- import path10 from "path";
2927
- import { parse as parseYaml7 } from "yaml";
3431
+ import path12 from "path";
3432
+ import { parse as parseYaml9 } from "yaml";
2928
3433
  import { z as z5 } from "zod";
2929
3434
  var checkSchema = z5.object({
2930
3435
  id: z5.string().min(1),
@@ -3139,17 +3644,17 @@ ${texts.scenarios}`);
3139
3644
  }
3140
3645
  }
3141
3646
  async function loadProjectPacks(sddDir) {
3142
- const dir = path10.join(sddDir, "packs");
3647
+ const dir = path12.join(sddDir, "packs");
3143
3648
  const diagnostics = [];
3144
3649
  const packs = [];
3145
3650
  for (const entry of await listDir(dir)) {
3146
3651
  if (!/\.ya?ml$/i.test(entry)) continue;
3147
- const file = path10.join(dir, entry);
3652
+ const file = path12.join(dir, entry);
3148
3653
  const raw = await readTextIfExists(file);
3149
3654
  if (raw === void 0) continue;
3150
3655
  let data;
3151
3656
  try {
3152
- data = parseYaml7(raw);
3657
+ data = parseYaml9(raw);
3153
3658
  } catch (err) {
3154
3659
  diagnostics.push(diag("PACK-000", "error", `Pack inv\xE1lido (${entry}): ${err.message}`, { path: file }));
3155
3660
  continue;
@@ -3233,7 +3738,7 @@ function packFindings(evaluations, change) {
3233
3738
  // src/analyze.ts
3234
3739
  var UI_DOMAINS = /* @__PURE__ */ new Set(["frontend", "mobile", "fullstack"]);
3235
3740
  async function runAnalyze(opts) {
3236
- const root = path11.resolve(opts.root);
3741
+ const root = path13.resolve(opts.root);
3237
3742
  const { workspace, config } = await loadWorkspace(root);
3238
3743
  const change = workspace.changes.find((c) => c.slug === opts.slug);
3239
3744
  if (!change) {
@@ -3251,9 +3756,9 @@ async function runAnalyze(opts) {
3251
3756
  for (const req of spec.spec.requirements) living.set(req.id, req);
3252
3757
  }
3253
3758
  if (change.delta) {
3254
- 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 }));
3255
3760
  }
3256
- const trace = checkTrace({ specs: workspace.specs, change, requireEvidence: false });
3761
+ const trace = checkTrace({ specs: workspace.specs, change, requireEvidence: false, linked: linkedTraceInput(workspace) });
3257
3762
  findings.push(...trace.findings);
3258
3763
  let waves;
3259
3764
  if (change.tasks && change.tasks.counts.total > 0) {
@@ -3300,7 +3805,7 @@ async function runAnalyze(opts) {
3300
3805
  if (mockups) result.mockups = mockups;
3301
3806
  if (packs) result.packs = packs;
3302
3807
  if (opts.write !== false) {
3303
- const file = path11.join(change.dir, "analyze.md");
3808
+ const file = path13.join(change.dir, "analyze.md");
3304
3809
  await writeText(file, renderAnalyze(change, result, localStamp(opts.now)));
3305
3810
  result.path = file;
3306
3811
  }
@@ -3343,9 +3848,9 @@ function renderAnalyze(change, result, generatedAt) {
3343
3848
  }
3344
3849
 
3345
3850
  // src/metrics.ts
3346
- import path12 from "path";
3851
+ import path14 from "path";
3347
3852
  async function collectMetrics(root, now = /* @__PURE__ */ new Date()) {
3348
- const resolved = path12.resolve(root);
3853
+ const resolved = path14.resolve(root);
3349
3854
  const { workspace, config } = await loadWorkspace(resolved);
3350
3855
  const approvals = await loadApprovals(workspace.sddDir);
3351
3856
  const living = /* @__PURE__ */ new Map();
@@ -3360,11 +3865,12 @@ async function collectMetrics(root, now = /* @__PURE__ */ new Date()) {
3360
3865
  const deltaContent = await readDelta(change);
3361
3866
  const approval = verifyApproval(change, approvals.byArtifact, config, deltaContent);
3362
3867
  const findings = [];
3363
- 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 }));
3364
3869
  const trace = checkTrace({
3365
3870
  specs: workspace.specs,
3366
3871
  change,
3367
- 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)
3368
3874
  });
3369
3875
  findings.push(...trace.findings);
3370
3876
  const state = deriveState({ change, cfg: config, approval, blockingFindings: change.delta ? countBySeverity(findings).errors : 0 });
@@ -3425,7 +3931,7 @@ async function collectMetrics(root, now = /* @__PURE__ */ new Date()) {
3425
3931
  attention.push({ slug: change.slug, kind: "missing-evidence", detail: `evidencia ${change.scenariosPassed}/${change.scenariosTotal}` });
3426
3932
  }
3427
3933
  }
3428
- const archiveDir = path12.join(workspace.sddDir, "changes", "archive");
3934
+ const archiveDir = path14.join(workspace.sddDir, "changes", "archive");
3429
3935
  const archivedEntries = await listDirs(archiveDir);
3430
3936
  const throughputByMonth = {};
3431
3937
  for (const entry of archivedEntries) {
@@ -3463,15 +3969,15 @@ async function collectMetrics(root, now = /* @__PURE__ */ new Date()) {
3463
3969
  }
3464
3970
  async function readDelta(change) {
3465
3971
  const { readTextIfExists: readTextIfExists2 } = await import("./fsx-VF2P7ALA.js");
3466
- return readTextIfExists2(path12.join(change.dir, "spec.md"));
3972
+ return readTextIfExists2(path14.join(change.dir, "spec.md"));
3467
3973
  }
3468
3974
 
3469
3975
  // src/present.ts
3470
- import path13 from "path";
3976
+ import path15 from "path";
3471
3977
  import { escapeHtml, inlineMarkdown, renderMarkdown as renderRich } from "@specatlas/render";
3472
3978
  import { defaultTokens, renderDocument, renderStyles } from "@specatlas/render";
3473
3979
  async function generatePresentation(opts) {
3474
- const root = path13.resolve(opts.root);
3980
+ const root = path15.resolve(opts.root);
3475
3981
  const { workspace, config } = await loadWorkspace(root);
3476
3982
  const language = config.project.language;
3477
3983
  const change = workspace.changes.find((c) => c.slug === opts.slug);
@@ -3481,17 +3987,17 @@ async function generatePresentation(opts) {
3481
3987
  if (!change.delta) {
3482
3988
  return { slug: opts.slug, diagnostics: [diag("ATLAS-PRESENT-001", "error", `El cambio "${opts.slug}" no tiene spec.md (delta)`)] };
3483
3989
  }
3484
- const deltaPath = path13.join(change.dir, "spec.md");
3990
+ const deltaPath = path15.join(change.dir, "spec.md");
3485
3991
  const deltaContent = await readTextIfExists(deltaPath) ?? "";
3486
3992
  const hash = artifactHash(deltaContent);
3487
3993
  const now = opts.now ?? /* @__PURE__ */ new Date();
3488
- const presentationDir = path13.join(change.dir, "presentation");
3489
- const mockupsCopyDir = path13.join(presentationDir, "mockups");
3994
+ const presentationDir = path15.join(change.dir, "presentation");
3995
+ const mockupsCopyDir = path15.join(presentationDir, "mockups");
3490
3996
  await ensureDir(presentationDir);
3491
3997
  const mockupInfo = await copyMockups(root, change, mockupsCopyDir);
3492
- const proposalRaw = await readTextIfExists(path13.join(change.dir, "proposal.md"));
3998
+ const proposalRaw = await readTextIfExists(path15.join(change.dir, "proposal.md"));
3493
3999
  const proposalBody = proposalRaw ? parseFrontmatter(proposalRaw).body : "";
3494
- const approvals = await loadApprovals(path13.join(root, ".sdd"));
4000
+ const approvals = await loadApprovals(path15.join(root, ".sdd"));
3495
4001
  const approval = verifyApproval(change, approvals.byArtifact, config, deltaContent);
3496
4002
  const labels = labelsFor(language);
3497
4003
  const html = page({
@@ -3512,7 +4018,7 @@ async function generatePresentation(opts) {
3512
4018
  approveHint: `satlas approve ${change.slug} --by "<nombre>" --channel presentation`,
3513
4019
  ...approval.status === "valid" && approval.approvedBy ? { approval: { by: approval.approvedBy, at: approval.approvedAt ?? "" } } : {}
3514
4020
  });
3515
- const outFile = path13.join(presentationDir, "index.html");
4021
+ const outFile = path15.join(presentationDir, "index.html");
3516
4022
  await writeText(outFile, html);
3517
4023
  return { slug: change.slug, path: outFile, hash, diagnostics: mockupInfo.diagnostics };
3518
4024
  }
@@ -3524,12 +4030,12 @@ async function copyMockups(root, change, destDir) {
3524
4030
  await ensureDir(destDir);
3525
4031
  const screens = [];
3526
4032
  for (const screen of manifest.screens) {
3527
- const src = path13.join(dir, screen.file);
4033
+ const src = path15.join(dir, screen.file);
3528
4034
  if (!await exists(src)) {
3529
4035
  diagnostics.push(diag("ATLAS-PRESENT-002", "warning", `El mockup ${screen.file} no existe y no se incluir\xE1`, { path: src }));
3530
4036
  continue;
3531
4037
  }
3532
- await copyFile(src, path13.join(destDir, screen.file));
4038
+ await copyFile(src, path15.join(destDir, screen.file));
3533
4039
  screens.push({
3534
4040
  id: screen.id,
3535
4041
  title: screen.title ?? screen.id,
@@ -3539,13 +4045,13 @@ async function copyMockups(root, change, destDir) {
3539
4045
  });
3540
4046
  }
3541
4047
  const screenshots = [];
3542
- const shotsDir = path13.join(dir, "screens");
4048
+ const shotsDir = path15.join(dir, "screens");
3543
4049
  if (await exists(shotsDir)) {
3544
- await ensureDir(path13.join(destDir, "screens"));
4050
+ await ensureDir(path15.join(destDir, "screens"));
3545
4051
  const { listDir: listDir4 } = await import("./fsx-VF2P7ALA.js");
3546
4052
  for (const file of await listDir4(shotsDir)) {
3547
4053
  if (!/\.(png|jpe?g|webp)$/i.test(file)) continue;
3548
- await copyFile(path13.join(shotsDir, file), path13.join(destDir, "screens", file));
4054
+ await copyFile(path15.join(shotsDir, file), path15.join(destDir, "screens", file));
3549
4055
  screenshots.push(`mockups/screens/${file}`);
3550
4056
  }
3551
4057
  }
@@ -3720,7 +4226,7 @@ function renderMarkdown(markdown) {
3720
4226
  }
3721
4227
 
3722
4228
  // src/adopt.ts
3723
- import path14 from "path";
4229
+ import path16 from "path";
3724
4230
  var COMMON_ROOTS = ["src", "app", "lib", "modules", "services", "packages", "api", "features", "domain", "internal", "components"];
3725
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"]);
3726
4232
  var SOURCE_EXT = /* @__PURE__ */ new Set([
@@ -3764,11 +4270,11 @@ var SOURCE_EXT = /* @__PURE__ */ new Set([
3764
4270
  ".groovy"
3765
4271
  ]);
3766
4272
  async function adoptWorkspace(opts) {
3767
- const root = path14.resolve(opts.root);
4273
+ const root = path16.resolve(opts.root);
3768
4274
  const dryRun = opts.dryRun ?? false;
3769
4275
  const diagnostics = [];
3770
- const sddDir = path14.join(root, ".sdd");
3771
- if (!await exists(path14.join(sddDir, "config.yaml"))) {
4276
+ const sddDir = path16.join(root, ".sdd");
4277
+ if (!await exists(path16.join(sddDir, "config.yaml"))) {
3772
4278
  return {
3773
4279
  root,
3774
4280
  domains: [],
@@ -3779,8 +4285,8 @@ async function adoptWorkspace(opts) {
3779
4285
  }
3780
4286
  const { config } = await loadConfig(sddDir);
3781
4287
  const files = await walkFiles(root, { skipDirs: [...SKIP_DIRS] });
3782
- const inventoried = files.map((f) => toPosix(path14.relative(root, f.path)));
3783
- 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()));
3784
4290
  if (relative.length === 0) {
3785
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>" }));
3786
4292
  }
@@ -3808,13 +4314,13 @@ async function adoptWorkspace(opts) {
3808
4314
  const createdSpecs = [];
3809
4315
  for (const name of domainNames.sort()) {
3810
4316
  const domainFiles = (counts.get(name) ?? []).sort();
3811
- const existingSpec = await exists(path14.join(sddDir, "specs", name, "spec.md"));
4317
+ const existingSpec = await exists(path16.join(sddDir, "specs", name, "spec.md"));
3812
4318
  domains.push({ name, files: domainFiles.length, samples: domainFiles.slice(0, 8), existingSpec });
3813
4319
  if (existingSpec) {
3814
- 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") }));
3815
4321
  continue;
3816
4322
  }
3817
- const specPath = path14.join(sddDir, "specs", name, "spec.md");
4323
+ const specPath = path16.join(sddDir, "specs", name, "spec.md");
3818
4324
  if (!dryRun) {
3819
4325
  await writeText(specPath, baselineSpec(name, domainFiles));
3820
4326
  createdSpecs.push(specPath);
@@ -3822,7 +4328,7 @@ async function adoptWorkspace(opts) {
3822
4328
  createdSpecs.push(specPath);
3823
4329
  }
3824
4330
  }
3825
- const reportPath = path14.join(sddDir, "adopt-report.md");
4331
+ const reportPath = path16.join(sddDir, "adopt-report.md");
3826
4332
  if (!dryRun) {
3827
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() }));
3828
4334
  }
@@ -3842,12 +4348,12 @@ async function loadProjectProfiles(root, dirs) {
3842
4348
  if (!await exists(dir)) continue;
3843
4349
  out.push(...await loadProfilesFromDir(dir));
3844
4350
  }
3845
- const custom = path14.join(root, ".sdd", "profiles", "custom");
4351
+ const custom = path16.join(root, ".sdd", "profiles", "custom");
3846
4352
  if (await exists(custom)) out.push(...await loadProfilesFromDir(custom));
3847
4353
  return out;
3848
4354
  }
3849
4355
  async function fallbackDomainName(root, projectName) {
3850
- const pkg = await readTextIfExists(path14.join(root, "package.json"));
4356
+ const pkg = await readTextIfExists(path16.join(root, "package.json"));
3851
4357
  if (pkg) {
3852
4358
  try {
3853
4359
  const data = JSON.parse(pkg);
@@ -3935,13 +4441,13 @@ function adoptReport(input) {
3935
4441
  }
3936
4442
 
3937
4443
  // src/init.ts
3938
- import path15 from "path";
4444
+ import path17 from "path";
3939
4445
  async function initWorkspace(opts) {
3940
- const root = path15.resolve(opts.root);
3941
- const sddDir = path15.join(root, ".sdd");
4446
+ const root = path17.resolve(opts.root);
4447
+ const sddDir = path17.join(root, ".sdd");
3942
4448
  const diagnostics = [];
3943
4449
  const created = [];
3944
- if (await exists(path15.join(sddDir, "config.yaml"))) {
4450
+ if (await exists(path17.join(sddDir, "config.yaml"))) {
3945
4451
  return {
3946
4452
  sddDir,
3947
4453
  created,
@@ -3951,29 +4457,29 @@ async function initWorkspace(opts) {
3951
4457
  created.push(...await ensureSddDirs(sddDir));
3952
4458
  const language = opts.language ?? "es";
3953
4459
  const cfg = defaultConfig({ name: opts.name, language });
3954
- cfg.project.name = opts.name ?? path15.basename(root);
4460
+ cfg.project.name = opts.name ?? path17.basename(root);
3955
4461
  await writeConfig(sddDir, cfg);
3956
- created.push(path15.join(sddDir, "config.yaml"));
4462
+ created.push(path17.join(sddDir, "config.yaml"));
3957
4463
  const templates = templatesFor(language);
3958
- await writeText(path15.join(sddDir, "constitution.md"), templates.constitution);
3959
- created.push(path15.join(sddDir, "constitution.md"));
3960
- await writeText(path15.join(sddDir, "glossary.md"), templates.glossary);
3961
- 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"));
3962
4468
  const detected = await detectIfPossible(root, opts.profilesDir);
3963
4469
  if (detected) {
3964
4470
  const yaml = detectionToYaml(detected, localStamp(opts.now));
3965
- await writeText(path15.join(sddDir, "profiles", "detected.yaml"), yaml);
3966
- 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"));
3967
4473
  if (detected.best) {
3968
4474
  diagnostics.push(diag("ATLAS-INIT-002", "info", `Stack detectado: ${detected.best.displayName} (${detected.best.score} puntos)`, { suggestion: "Revisa .sdd/profiles/detected.yaml" }));
3969
4475
  } else {
3970
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/" }));
3971
4477
  }
3972
4478
  }
3973
- await writeText(path15.join(sddDir, "INDEX.md"), initialIndex(language, cfg.project.name));
3974
- 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"));
3975
4481
  if (opts.local) {
3976
- const gitignore = path15.join(root, ".gitignore");
4482
+ const gitignore = path17.join(root, ".gitignore");
3977
4483
  const current = await readTextIfExists(gitignore) ?? "";
3978
4484
  if (!/(^|\n)\.sdd\/?(\n|$)/.test(current)) {
3979
4485
  const next = current.endsWith("\n") || current === "" ? `${current}.sdd/
@@ -4008,13 +4514,13 @@ ${es ? "## Archivados\n\n0 cambio(s) en `changes/archive/`." : "## Archived\n\n0
4008
4514
  }
4009
4515
 
4010
4516
  // src/new.ts
4011
- import path16 from "path";
4517
+ import path18 from "path";
4012
4518
  var SLUG_RE = /^[a-z0-9][a-z0-9-]{1,49}$/;
4013
4519
  async function createChange(opts) {
4014
4520
  const diagnostics = [];
4015
4521
  const slug = opts.slug.trim().toLowerCase();
4016
4522
  const language = opts.language ?? opts.cfg?.project.language ?? "es";
4017
- const dir = path16.join(path16.resolve(opts.root), ".sdd", "changes", slug);
4523
+ const dir = path18.join(path18.resolve(opts.root), ".sdd", "changes", slug);
4018
4524
  if (!SLUG_RE.test(slug)) {
4019
4525
  return {
4020
4526
  slug,
@@ -4045,20 +4551,20 @@ async function createChange(opts) {
4045
4551
  await ensureDir(dir);
4046
4552
  const templates = templatesFor(language);
4047
4553
  const files = [];
4048
- const metaFile = path16.join(dir, "meta.yaml");
4554
+ const metaFile = path18.join(dir, "meta.yaml");
4049
4555
  await writeText(metaFile, changeMetaYaml(meta));
4050
4556
  files.push(metaFile);
4051
4557
  if (lane === "fix") {
4052
- const fixFile = path16.join(dir, "fix.md");
4558
+ const fixFile = path18.join(dir, "fix.md");
4053
4559
  await writeText(fixFile, renderTemplate(templates.fix, { TITLE: title }));
4054
4560
  files.push(fixFile);
4055
4561
  return { slug, dir, files, diagnostics };
4056
4562
  }
4057
- const proposalFile = path16.join(dir, "proposal.md");
4563
+ const proposalFile = path18.join(dir, "proposal.md");
4058
4564
  await writeText(proposalFile, renderTemplate(templates.proposal, { TITLE: title }));
4059
4565
  files.push(proposalFile);
4060
4566
  const domainUpper = domain.replace(/[^a-z0-9]/gi, "").toUpperCase() || "GEN";
4061
- const deltaFile = path16.join(dir, "spec.md");
4567
+ const deltaFile = path18.join(dir, "spec.md");
4062
4568
  await writeText(deltaFile, templates.specDelta({ title, domainUpper, domain }));
4063
4569
  files.push(deltaFile);
4064
4570
  return { slug, dir, files, diagnostics };
@@ -4067,7 +4573,7 @@ async function createChange(opts) {
4067
4573
  // src/archive.ts
4068
4574
  import { promises as fs } from "fs";
4069
4575
  import { cp, rename, rm } from "fs/promises";
4070
- import path17 from "path";
4576
+ import path19 from "path";
4071
4577
  var REQ_HEADER_RE = /^###\s+(?:Requisito|Requirement):\s+(REQ-[A-Z0-9-]+)\s*(?:—|-|–)\s*(.*)$/;
4072
4578
  function foldDelta(livingBody, delta, language = "es") {
4073
4579
  const diagnostics = [];
@@ -4176,7 +4682,7 @@ async function restoreFile(file, previous) {
4176
4682
  }
4177
4683
  }
4178
4684
  async function archiveChange(opts) {
4179
- const root = path17.resolve(opts.root);
4685
+ const root = path19.resolve(opts.root);
4180
4686
  const diagnostics = [];
4181
4687
  const { config } = await loadWorkspace(root);
4182
4688
  const language = opts.language ?? config.project.language;
@@ -4192,7 +4698,7 @@ async function archiveChange(opts) {
4192
4698
  return { slug: opts.slug, fold: emptyFold, diagnostics, dryRun: opts.dryRun ?? false };
4193
4699
  }
4194
4700
  const nowFix = opts.now ?? /* @__PURE__ */ new Date();
4195
- 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}`);
4196
4702
  if (await exists(targetFix)) {
4197
4703
  diagnostics.push(diag("ATLAS-ARCH-002", "error", `Ya existe un cambio archivado en ${targetFix}`, { path: targetFix }));
4198
4704
  return { slug: opts.slug, fold: emptyFold, diagnostics, dryRun: opts.dryRun ?? false };
@@ -4200,7 +4706,7 @@ async function archiveChange(opts) {
4200
4706
  let livingFix;
4201
4707
  let livingCreated = false;
4202
4708
  if (!opts.dryRun) {
4203
- const fixRaw = await readTextIfExists(path17.join(change.dir, "fix.md")) ?? "";
4709
+ const fixRaw = await readTextIfExists(path19.join(change.dir, "fix.md")) ?? "";
4204
4710
  try {
4205
4711
  const written = await writeLivingFix(root, {
4206
4712
  slug: opts.slug,
@@ -4216,7 +4722,7 @@ async function archiveChange(opts) {
4216
4722
  } catch (error) {
4217
4723
  diagnostics.push(
4218
4724
  diag("ATLAS-ARCH-006", "error", `No se pudo conservar el fix vivo: ${error.message}`, {
4219
- path: path17.join(root, ".sdd", "fixes"),
4725
+ path: path19.join(root, ".sdd", "fixes"),
4220
4726
  suggestion: "Revisa los permisos de .sdd/fixes y vuelve a intentar; el fix sigue sin archivar"
4221
4727
  })
4222
4728
  );
@@ -4224,11 +4730,11 @@ async function archiveChange(opts) {
4224
4730
  }
4225
4731
  }
4226
4732
  if (!opts.dryRun) {
4227
- await ensureDir(path17.dirname(targetFix));
4733
+ await ensureDir(path19.dirname(targetFix));
4228
4734
  try {
4229
4735
  await moveDirectory(change.dir, targetFix);
4230
4736
  } catch (error) {
4231
- if (livingCreated && livingFix) await removeFile(path17.join(root, livingFix));
4737
+ if (livingCreated && livingFix) await removeFile(path19.join(root, livingFix));
4232
4738
  diagnostics.push(
4233
4739
  diag("ATLAS-ARCH-004", "error", `No se pudo mover el cambio al hist\xF3rico: ${error.message}`, {
4234
4740
  path: change.dir,
@@ -4254,10 +4760,10 @@ async function archiveChange(opts) {
4254
4760
  }
4255
4761
  const domain = change.meta.domain;
4256
4762
  if (!domain) {
4257
- 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") }));
4258
4764
  return { slug: opts.slug, fold: { content: "", applied: { added: [], modified: [], removed: [], renamed: [] }, diagnostics }, diagnostics, dryRun: opts.dryRun ?? false };
4259
4765
  }
4260
- const specFile = path17.join(root, ".sdd", "specs", domain, "spec.md");
4766
+ const specFile = path19.join(root, ".sdd", "specs", domain, "spec.md");
4261
4767
  const existing = await readTextIfExists(specFile);
4262
4768
  const base = existing ?? `---
4263
4769
  domain: ${domain}
@@ -4287,8 +4793,8 @@ ${Object.entries(nextFm).map(([k, v]) => `${k}: ${String(v)}`).join("\n")}
4287
4793
 
4288
4794
  ${body}`;
4289
4795
  const month = localMonth(now);
4290
- const archiveDir = path17.join(root, ".sdd", "changes", "archive");
4291
- 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}`);
4292
4798
  if (await exists(target)) {
4293
4799
  diagnostics.push(diag("ATLAS-ARCH-002", "error", `Ya existe un cambio archivado en ${target}`, { path: target }));
4294
4800
  return { slug: opts.slug, domain, fold, diagnostics, dryRun: opts.dryRun ?? false };
@@ -4314,7 +4820,7 @@ ${body}`;
4314
4820
  }
4315
4821
  async function regenerateIndex(root, cfg, now = /* @__PURE__ */ new Date()) {
4316
4822
  const { workspace, config } = cfg ? { workspace: (await loadWorkspace(root)).workspace, config: cfg } : await loadWorkspace(root);
4317
- const archiveDir = path17.join(root, ".sdd", "changes", "archive");
4823
+ const archiveDir = path19.join(root, ".sdd", "changes", "archive");
4318
4824
  const archived = await exists(archiveDir) ? (await fs.readdir(archiveDir)).filter((e) => !e.startsWith(".")).length : 0;
4319
4825
  const fixes = await loadLivingFixes(root);
4320
4826
  const markdown = indexMarkdown({
@@ -4326,7 +4832,7 @@ async function regenerateIndex(root, cfg, now = /* @__PURE__ */ new Date()) {
4326
4832
  archived
4327
4833
  });
4328
4834
  void now;
4329
- await writeText(path17.join(root, ".sdd", "INDEX.md"), markdown);
4835
+ await writeText(path19.join(root, ".sdd", "INDEX.md"), markdown);
4330
4836
  }
4331
4837
  async function removeFile(file) {
4332
4838
  try {
@@ -4337,8 +4843,8 @@ async function removeFile(file) {
4337
4843
 
4338
4844
  // src/migrations.ts
4339
4845
  import { promises as fs2 } from "fs";
4340
- import path18 from "path";
4341
- import { parse as parseYaml8, stringify as stringifyYaml6 } from "yaml";
4846
+ import path20 from "path";
4847
+ import { parse as parseYaml10, stringify as stringifyYaml7 } from "yaml";
4342
4848
  var SCHEMA_VERSION = 1;
4343
4849
  var BACKUP_DIRNAME = ".backup";
4344
4850
  var BACKUP_POINTER = ".latest";
@@ -4350,35 +4856,35 @@ var SCHEMA_MIGRATIONS = [
4350
4856
  to: SCHEMA_VERSION
4351
4857
  }
4352
4858
  ];
4353
- var FIXED_ARTIFACTS = ["config.yaml", "approvals.yaml", path18.join("profiles", "detected.yaml")];
4354
- 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")];
4355
4861
  async function pushChangeArtifacts(out, sddDir, relDir) {
4356
4862
  for (const rel of CHANGE_ARTIFACTS) {
4357
- const abs = path18.join(sddDir, relDir, rel);
4358
- 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 });
4359
4865
  }
4360
4866
  }
4361
4867
  async function collectVersionedArtifacts(root) {
4362
- const sddDir = path18.join(root, ".sdd");
4868
+ const sddDir = path20.join(root, ".sdd");
4363
4869
  const out = [];
4364
4870
  for (const rel of FIXED_ARTIFACTS) {
4365
- const abs = path18.join(sddDir, rel);
4871
+ const abs = path20.join(sddDir, rel);
4366
4872
  if (await exists(abs)) out.push({ artifact: toPosix(rel), path: abs });
4367
4873
  }
4368
- const changesDir = path18.join(sddDir, "changes");
4874
+ const changesDir = path20.join(sddDir, "changes");
4369
4875
  for (const slug of await listDirs(changesDir)) {
4370
4876
  if (slug === "archive") continue;
4371
- await pushChangeArtifacts(out, sddDir, path18.join("changes", slug));
4877
+ await pushChangeArtifacts(out, sddDir, path20.join("changes", slug));
4372
4878
  }
4373
- for (const entry of await listDirs(path18.join(changesDir, "archive"))) {
4374
- 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));
4375
4881
  }
4376
4882
  return out;
4377
4883
  }
4378
4884
  function readArtifactVersion(content) {
4379
4885
  let data;
4380
4886
  try {
4381
- data = parseYaml8(content);
4887
+ data = parseYaml10(content);
4382
4888
  } catch (err) {
4383
4889
  return { state: "unreadable", reason: `YAML inv\xE1lido (${err.message})` };
4384
4890
  }
@@ -4468,22 +4974,22 @@ function backupName(now) {
4468
4974
  async function applyUpgrade(root, now = /* @__PURE__ */ new Date()) {
4469
4975
  const plan = await planUpgrade(root);
4470
4976
  if (plan.pending.length === 0) return { status: "up-to-date", plan, applied: [] };
4471
- const sddDir = path18.join(root, ".sdd");
4472
- const backupRoot = path18.join(sddDir, BACKUP_DIRNAME);
4977
+ const sddDir = path20.join(root, ".sdd");
4978
+ const backupRoot = path20.join(sddDir, BACKUP_DIRNAME);
4473
4979
  const name = backupName(now);
4474
- const backupDir = path18.join(backupRoot, name);
4475
- const pointerFile = path18.join(backupRoot, BACKUP_POINTER);
4980
+ const backupDir = path20.join(backupRoot, name);
4981
+ const pointerFile = path20.join(backupRoot, BACKUP_POINTER);
4476
4982
  const previous = [];
4477
4983
  try {
4478
4984
  for (const item of plan.pending) {
4479
4985
  const content = await readTextIfExists(item.path);
4480
4986
  if (content === void 0) throw new Error("el elemento desapareci\xF3 mientras se respaldaba");
4481
4987
  previous.push({ item, contents: content });
4482
- await writeText(path18.join(backupDir, "files", ...item.artifact.split("/")), content);
4988
+ await writeText(path20.join(backupDir, "files", ...item.artifact.split("/")), content);
4483
4989
  }
4484
4990
  await writeText(
4485
- path18.join(backupDir, "backup.yaml"),
4486
- stringifyYaml6(
4991
+ path20.join(backupDir, "backup.yaml"),
4992
+ stringifyYaml7(
4487
4993
  {
4488
4994
  schema_version: SCHEMA_VERSION,
4489
4995
  created_at: localStamp(now),
@@ -4523,7 +5029,7 @@ async function applyUpgrade(root, now = /* @__PURE__ */ new Date()) {
4523
5029
  }
4524
5030
  }
4525
5031
  for (const dir of await listDirs(backupRoot)) {
4526
- if (dir !== name) await removeDir(path18.join(backupRoot, dir));
5032
+ if (dir !== name) await removeDir(path20.join(backupRoot, dir));
4527
5033
  }
4528
5034
  return {
4529
5035
  status: "applied",
@@ -4532,32 +5038,32 @@ async function applyUpgrade(root, now = /* @__PURE__ */ new Date()) {
4532
5038
  backup: {
4533
5039
  name,
4534
5040
  dir: backupDir,
4535
- relativeDir: toPosix(path18.relative(root, backupDir)),
5041
+ relativeDir: toPosix(path20.relative(root, backupDir)),
4536
5042
  createdAt: localStamp(now),
4537
5043
  files: plan.pending.map((i) => i.artifact)
4538
5044
  }
4539
5045
  };
4540
5046
  }
4541
5047
  async function rollbackUpgrade(root) {
4542
- const sddDir = path18.join(root, ".sdd");
4543
- const backupRoot = path18.join(sddDir, BACKUP_DIRNAME);
4544
- 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);
4545
5051
  const name = (await readTextIfExists(pointerFile))?.trim();
4546
5052
  if (!name) return { status: "no-backup", restored: [] };
4547
- const backupDir = path18.join(backupRoot, name);
4548
- 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"));
4549
5055
  if (manifestRaw === void 0) return { status: "no-backup", restored: [] };
4550
5056
  let files = [];
4551
5057
  try {
4552
- const parsed = parseYaml8(manifestRaw);
5058
+ const parsed = parseYaml10(manifestRaw);
4553
5059
  files = (parsed?.files ?? []).filter((f) => typeof f?.artifact === "string");
4554
5060
  } catch {
4555
5061
  return { status: "no-backup", restored: [] };
4556
5062
  }
4557
5063
  const restored = [];
4558
5064
  for (const file of files) {
4559
- const from = path18.join(backupDir, "files", ...file.artifact.split("/"));
4560
- 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("/"));
4561
5067
  const content = await readTextIfExists(from);
4562
5068
  if (content === void 0) continue;
4563
5069
  try {
@@ -4600,8 +5106,89 @@ async function upgradeAdvisory(root) {
4600
5106
  return { plan, diagnostics };
4601
5107
  }
4602
5108
 
5109
+ // src/docs.ts
5110
+ import path21 from "path";
5111
+ var DOCS_MARKER_START = "<!-- specatlas:generado:inicio -->";
5112
+ var DOCS_MARKER_END = "<!-- specatlas:generado:fin -->";
5113
+ function docData(change, language, now) {
5114
+ const es = language !== "en";
5115
+ const requirements = [...change.delta?.added ?? [], ...change.delta?.modified ?? []];
5116
+ const evidence = change.verify?.evidence ?? [];
5117
+ const passed = new Set(evidence.filter((entry) => entry.result === "pass").map((entry) => entry.scenario));
5118
+ const requirementsText = requirements.length === 0 ? es ? "_Sin requisitos en el delta._" : "_No requirements in the delta._" : requirements.map((requirement) => {
5119
+ const lines = [`### ${requirement.id} \u2014 ${requirement.title}`, ""];
5120
+ for (const scenario of requirement.scenarios) lines.push(`- \`${scenario.id}\` \u2014 ${scenario.title}`);
5121
+ return lines.join("\n");
5122
+ }).join("\n\n");
5123
+ const scenarioList = requirements.flatMap((requirement) => requirement.scenarios);
5124
+ const scenariosText = scenarioList.length === 0 ? es ? "_Sin escenarios._" : "_No scenarios._" : scenarioList.map((scenario) => `- \`${scenario.id}\` \u2014 ${scenario.title}`).join("\n");
5125
+ const evidenceText = evidence.length === 0 ? es ? "_Sin evidencia registrada._" : "_No evidence recorded._" : evidence.map((entry) => `- \`${entry.scenario}\` \u2014 ${entry.method} \xB7 ${entry.result}${entry.date ? ` \xB7 ${entry.date}` : ""}`).join("\n");
5126
+ const pending = scenarioList.filter((scenario) => !passed.has(scenario.id));
5127
+ const pendingText = pending.length === 0 ? es ? "_Nada pendiente: todos los escenarios tienen evidencia en pass._" : "_Nothing pending: every scenario has passing evidence._" : pending.map((scenario) => `- \`${scenario.id}\` \u2014 ${scenario.title}`).join("\n");
5128
+ return {
5129
+ TITLE: change.meta?.title ?? change.slug,
5130
+ SLUG: change.slug,
5131
+ DOMAIN: change.meta?.domain ?? "\u2014",
5132
+ LANE: change.meta?.lane ?? "standard",
5133
+ DATE: localDate(now),
5134
+ TASKS: change.tasks ? `${change.tasks.counts.done}/${change.tasks.counts.total}` : es ? "sin tareas" : "no tasks",
5135
+ REQUIREMENTS: requirementsText,
5136
+ SCENARIOS: scenariosText,
5137
+ EVIDENCE: evidenceText,
5138
+ PENDING: pendingText
5139
+ };
5140
+ }
5141
+ function mergeManaged(existing, block, language) {
5142
+ const managed = `${DOCS_MARKER_START}
5143
+ ${block.trimEnd()}
5144
+ ${DOCS_MARKER_END}`;
5145
+ if (existing === void 0) {
5146
+ const notes = language === "en" ? "## Notes\n\n(Write here whatever you want to keep across regenerations.)" : "## Notas\n\n(Escribe aqu\xED lo que quieras conservar entre regeneraciones.)";
5147
+ return `${managed}
5148
+
5149
+ ${notes}
5150
+ `;
5151
+ }
5152
+ const start = existing.indexOf(DOCS_MARKER_START);
5153
+ const end = existing.indexOf(DOCS_MARKER_END);
5154
+ if (start >= 0 && end > start) {
5155
+ const before = existing.slice(0, start);
5156
+ const after = existing.slice(end + DOCS_MARKER_END.length);
5157
+ return `${before}${managed}${after}`;
5158
+ }
5159
+ return `${managed}
5160
+
5161
+ ${existing}`;
5162
+ }
5163
+ async function generateDocs(opts) {
5164
+ const root = path21.resolve(opts.root);
5165
+ const { config } = await loadWorkspace(root);
5166
+ const change = await loadChange(root, opts.slug);
5167
+ if (!change.meta) {
5168
+ return {
5169
+ slug: opts.slug,
5170
+ files: [],
5171
+ diagnostics: [diag("ATLAS-DOCS-002", "error", `No existe el cambio "${opts.slug}"`, { suggestion: "Comprueba el nombre del cambio" })]
5172
+ };
5173
+ }
5174
+ const language = config.project.language;
5175
+ const tipo = opts.tipo ?? "all";
5176
+ const tipos = tipo === "all" ? ["tecnica", "manual"] : [tipo];
5177
+ const templates = templatesFor(language);
5178
+ const data = docData(change, language, opts.now ?? /* @__PURE__ */ new Date());
5179
+ const files = [];
5180
+ for (const current of tipos) {
5181
+ const file = path21.join(change.dir, "docs", `${current}.md`);
5182
+ const block = renderTemplate(current === "tecnica" ? templates.docTecnica : templates.docManual, data);
5183
+ const existing = await readTextIfExists(file);
5184
+ await writeText(file, mergeManaged(existing, block, language));
5185
+ files.push({ tipo: current, path: file, created: existing === void 0 });
5186
+ }
5187
+ return { slug: opts.slug, files, diagnostics: [] };
5188
+ }
5189
+
4603
5190
  // src/sarif.ts
4604
- import path19 from "path";
5191
+ import path22 from "path";
4605
5192
  var SARIF_SCHEMA = "https://json.schemastore.org/sarif-2.1.0.json";
4606
5193
  var SARIF_VERSION = "2.1.0";
4607
5194
  var TOOL_NAME = "SpecAtlas";
@@ -4639,7 +5226,7 @@ function resultOf(diagnostic, root) {
4639
5226
  message: { text: diagnostic.message }
4640
5227
  };
4641
5228
  if (diagnostic.path) {
4642
- const rel = path19.relative(root, diagnostic.path);
5229
+ const rel = path22.relative(root, diagnostic.path);
4643
5230
  if (rel !== "") {
4644
5231
  const physicalLocation = {
4645
5232
  artifactLocation: { uri: toPosix(rel) },
@@ -4689,7 +5276,7 @@ function toSarifText(opts) {
4689
5276
  }
4690
5277
 
4691
5278
  // src/doctor.ts
4692
- import path20 from "path";
5279
+ import path23 from "path";
4693
5280
  async function runDoctor(root) {
4694
5281
  const findings = [];
4695
5282
  const { workspace, config } = await loadWorkspace(root);
@@ -4697,7 +5284,7 @@ async function runDoctor(root) {
4697
5284
  const approvals = await loadApprovals(workspace.sddDir);
4698
5285
  findings.push(...approvals.diagnostics);
4699
5286
  for (const change of workspace.changes) {
4700
- const deltaPath = path20.join(change.dir, "spec.md");
5287
+ const deltaPath = path23.join(change.dir, "spec.md");
4701
5288
  const deltaContent = await readTextIfExists(deltaPath);
4702
5289
  const approval = verifyApproval(change, approvals.byArtifact, config, deltaContent ?? void 0);
4703
5290
  if ((change.planPath || change.tasks) && (approval.status === "missing" || approval.status === "stale")) {
@@ -4719,7 +5306,7 @@ async function runDoctor(root) {
4719
5306
  }
4720
5307
  for (const override of change.meta?.overrides ?? []) {
4721
5308
  if (!override.reason.trim() || !override.by.trim()) {
4722
- findings.push(diag("ATLAS-LIFECYCLE-004", "error", `Override del gate "${override.gate}" sin motivo o autor`, { path: path20.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") }));
4723
5310
  }
4724
5311
  }
4725
5312
  }
@@ -4742,7 +5329,7 @@ async function specHashOf(filePath) {
4742
5329
  }
4743
5330
 
4744
5331
  // src/gate.ts
4745
- import path21 from "path";
5332
+ import path24 from "path";
4746
5333
  var UI_DOMAINS2 = /* @__PURE__ */ new Set(["frontend", "mobile", "fullstack"]);
4747
5334
  function count(name, diagnostics) {
4748
5335
  return {
@@ -4759,7 +5346,7 @@ function livingRequirementsMap(specs) {
4759
5346
  return map;
4760
5347
  }
4761
5348
  async function runCiGate(opts) {
4762
- const root = path21.resolve(opts.root);
5349
+ const root = path24.resolve(opts.root);
4763
5350
  const { workspace, config } = await loadWorkspace(root);
4764
5351
  const diagnostics = [];
4765
5352
  const checks = [];
@@ -4770,11 +5357,12 @@ async function runCiGate(opts) {
4770
5357
  let changesErrors = 0;
4771
5358
  let changesWarnings = 0;
4772
5359
  for (const change of workspace.changes) {
4773
- const lintFindings = change.delta ? lintDelta(change.delta, living, path21.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 }) : [];
4774
5361
  const trace = checkTrace({
4775
5362
  specs: workspace.specs,
4776
5363
  change,
4777
- 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)
4778
5366
  });
4779
5367
  const changeDiags = [...lintFindings, ...trace.findings];
4780
5368
  if (change.planPath) {
@@ -4819,7 +5407,11 @@ export {
4819
5407
  BLOCK_HEAD_RE,
4820
5408
  BUILTIN_PACKS,
4821
5409
  CONFIG_FILE,
5410
+ CONTRACTS_DIR,
4822
5411
  CORE_VERSION,
5412
+ DOCS_MARKER_END,
5413
+ DOCS_MARKER_START,
5414
+ LINKS_FILE,
4823
5415
  LIVING_FIXES_DIR,
4824
5416
  REQ_HEAD_RE,
4825
5417
  REQ_ID_RE,
@@ -4838,6 +5430,7 @@ export {
4838
5430
  TASK_LINE_RE,
4839
5431
  TOOL_NAME,
4840
5432
  TOOL_URL,
5433
+ addLink,
4841
5434
  adoptWorkspace,
4842
5435
  appendRunEvent,
4843
5436
  applyUpgrade,
@@ -4854,12 +5447,15 @@ export {
4854
5447
  changeMetaYaml,
4855
5448
  checkMockups,
4856
5449
  checkTrace,
5450
+ clarifyAdvisory,
4857
5451
  collectMetrics,
4858
5452
  collectVersionedArtifacts,
4859
5453
  commentIssue,
4860
5454
  compareTaskIds,
4861
5455
  computeInputsHash,
4862
5456
  configToYaml,
5457
+ contractCoverage,
5458
+ contractsAdvisory,
4863
5459
  copyFile,
4864
5460
  countBySeverity,
4865
5461
  createChange,
@@ -4872,9 +5468,12 @@ export {
4872
5468
  detectRepo,
4873
5469
  detectionToYaml,
4874
5470
  diag,
5471
+ docsAdvisory,
5472
+ docsReady,
4875
5473
  editIssue,
4876
5474
  emitFrontmatter,
4877
5475
  ensureDir,
5476
+ ensureLinksFile,
4878
5477
  ensureSddDirs,
4879
5478
  esc,
4880
5479
  evaluatePacks,
@@ -4884,6 +5483,8 @@ export {
4884
5483
  findWorkspaceRoot,
4885
5484
  firstToken,
4886
5485
  foldDelta,
5486
+ formatOf,
5487
+ generateDocs,
4887
5488
  generatePresentation,
4888
5489
  generateRunId,
4889
5490
  getNumber,
@@ -4902,6 +5503,8 @@ export {
4902
5503
  issueBody,
4903
5504
  issueLabels,
4904
5505
  laneOrDefault,
5506
+ linkedRequirementIds,
5507
+ linkedTraceInput,
4905
5508
  lintDelta,
4906
5509
  lintMockupHtml,
4907
5510
  lintMockupManifest,
@@ -4919,7 +5522,9 @@ export {
4919
5522
  loadApprovals,
4920
5523
  loadChange,
4921
5524
  loadConfig,
5525
+ loadContracts,
4922
5526
  loadDetectedBest,
5527
+ loadLinks,
4923
5528
  loadLivingFixes,
4924
5529
  loadProfileFile,
4925
5530
  loadProfilesFromDir,
@@ -4939,7 +5544,9 @@ export {
4939
5544
  packFindings,
4940
5545
  parseApprovals,
4941
5546
  parseChangeMeta,
5547
+ parseClarify,
4942
5548
  parseConfig,
5549
+ parseContract,
4943
5550
  parseDelta,
4944
5551
  parseFixCovers,
4945
5552
  parseFrontmatter,
@@ -4964,12 +5571,14 @@ export {
4964
5571
  readTextIfExists,
4965
5572
  recordEvidence,
4966
5573
  regenerateIndex,
5574
+ removeLink,
4967
5575
  renderDocument,
4968
5576
  renderMarkdown,
4969
5577
  renderRequirement,
4970
5578
  renderStyles,
4971
5579
  renderTemplate,
4972
5580
  requiresMockups,
5581
+ resolveLinkPath,
4973
5582
  resolvePacks,
4974
5583
  rollbackUpgrade,
4975
5584
  ruleDescription,