specatlas 0.1.29 → 0.1.30

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/bin.js CHANGED
@@ -1586,7 +1586,7 @@ var require_core = __commonJS({
1586
1586
  });
1587
1587
 
1588
1588
  // src/cli.ts
1589
- import path46 from "path";
1589
+ import path48 from "path";
1590
1590
 
1591
1591
  // ../core/dist/index.js
1592
1592
  import path from "path";
@@ -9034,6 +9034,7 @@ import { parse as parseYaml8, stringify as stringifyYaml6 } from "yaml";
9034
9034
  import path19 from "path";
9035
9035
  import path20 from "path";
9036
9036
  import path21 from "path";
9037
+ import path22 from "path";
9037
9038
  var CORE_VERSION = "0.0.1";
9038
9039
  var REQ_ID_RE = /^REQ-[A-Z0-9]+(?:-[A-Z0-9]+)*-\d{3}$/;
9039
9040
  var SCENARIO_ID_RE = /^REQ-[A-Z0-9]+(?:-[A-Z0-9]+)*-\d{3}-S\d+$/;
@@ -9056,6 +9057,8 @@ var atlasConfigSchema = z.object({
9056
9057
  analyze: z.object({ mode: z.enum(["off", "advisory", "blocking"]).default("blocking"), min_severity: z.enum(["low", "medium", "high"]).default("medium") }).default({}),
9057
9058
  verify: z.object({ mode: z.enum(["off", "advisory", "blocking"]).default("blocking"), require_evidence: z.boolean().default(true) }).default({}),
9058
9059
  review: z.object({ mode: z.enum(["off", "advisory", "blocking"]).default("advisory") }).default({}),
9060
+ clarify: z.object({ mode: z.enum(["off", "advisory", "blocking"]).default("advisory") }).default({}),
9061
+ docs: z.object({ mode: z.enum(["off", "advisory", "blocking"]).default("blocking") }).default({}),
9059
9062
  mockup: z.object({ require_approval: z.boolean().default(false), compare_in_verify: z.boolean().default(false) }).default({})
9060
9063
  }).default({}),
9061
9064
  trace: z.object({ mode: z.enum(["off", "advisory", "blocking"]).default("blocking"), prefix: z.string().default("REQ") }).default({}),
@@ -9600,6 +9603,33 @@ function findScenarioHeading(lines, blockLineIndex) {
9600
9603
  }
9601
9604
  return void 0;
9602
9605
  }
9606
+ var OPEN_RE = /^\s*-\s*\[\s\]\s+(.+?)\s*$/;
9607
+ var DONE_RE = /^\s*-\s*\[[xX]\]\s+(.+?)\s*$/;
9608
+ var ANSWER_SEPARATOR = " \u2014 ";
9609
+ function splitAnswer(raw) {
9610
+ const index = raw.indexOf(ANSWER_SEPARATOR);
9611
+ if (index === -1) return { text: raw.trim() };
9612
+ return { text: raw.slice(0, index).trim(), answer: raw.slice(index + ANSWER_SEPARATOR.length).trim() };
9613
+ }
9614
+ function parseClarify(content, filePath) {
9615
+ const fm = parseFrontmatter(content, filePath);
9616
+ const lines = fm.body.replace(/\r\n?/g, "\n").split("\n");
9617
+ const open = [];
9618
+ const resolved = [];
9619
+ for (let i = 0; i < lines.length; i += 1) {
9620
+ const line = lines[i] ?? "";
9621
+ const lineNo = fm.bodyStartLine + i;
9622
+ const done = DONE_RE.exec(line);
9623
+ if (done) {
9624
+ const item = splitAnswer(done[1] ?? "");
9625
+ resolved.push({ text: item.text, line: lineNo, ...item.answer !== void 0 ? { answer: item.answer } : {} });
9626
+ continue;
9627
+ }
9628
+ const pending = OPEN_RE.exec(line);
9629
+ if (pending) open.push({ text: pending[1] ?? "", line: lineNo });
9630
+ }
9631
+ return { path: filePath, open, resolved, diagnostics: [] };
9632
+ }
9603
9633
  var HEADER_KEYS = /* @__PURE__ */ new Set(["t\xE9rmino", "termino", "term", "definici\xF3n", "definicion", "definition", "sin\xF3nimos", "sinonimos", "synonyms"]);
9604
9634
  var SEPARATOR_RE = /^:?-{2,}:?$/;
9605
9635
  function parseGlossary(md, filePath) {
@@ -9749,14 +9779,14 @@ function wordBoundary(text, term) {
9749
9779
  const re = new RegExp(`(?<![\\p{L}\\p{N}])${escaped}(?![\\p{L}\\p{N}])`, "iu");
9750
9780
  return re.test(text);
9751
9781
  }
9752
- function lintText(text, code, terms, label, path222, line) {
9782
+ function lintText(text, code, terms, label, path232, line) {
9753
9783
  const out = [];
9754
9784
  const lower = text.toLowerCase();
9755
9785
  for (const term of terms) {
9756
9786
  if (wordBoundary(lower, term)) {
9757
9787
  out.push(
9758
9788
  diag(code, "error", `${label}: "${term}"`, {
9759
- path: path222,
9789
+ path: path232,
9760
9790
  line,
9761
9791
  suggestion: "La especificaci\xF3n es funcional y de negocio: describe comportamiento, no tecnolog\xEDa ni adjetivos vagos"
9762
9792
  })
@@ -9765,7 +9795,7 @@ function lintText(text, code, terms, label, path222, line) {
9765
9795
  }
9766
9796
  return out;
9767
9797
  }
9768
- function lintRequirement(req, path222, opts = {}) {
9798
+ function lintRequirement(req, path232, opts = {}) {
9769
9799
  const out = [];
9770
9800
  const vague = opts.language === "en" ? VAGUE_EN : VAGUE_ES;
9771
9801
  const tech = opts.language === "en" ? TECH_EN : TECH_ES;
@@ -9776,32 +9806,32 @@ function lintRequirement(req, path222, opts = {}) {
9776
9806
  ...req.scenarios.flatMap((s) => [...s.when.map((w) => ({ text: w, line: s.line })), ...s.then.map((t) => ({ text: t, line: s.line }))])
9777
9807
  ];
9778
9808
  for (const part of parts) {
9779
- out.push(...lintText(part.text, "LINT-BIZ-002", vague, "Palabra vaga en la especificaci\xF3n", path222, part.line));
9809
+ out.push(...lintText(part.text, "LINT-BIZ-002", vague, "Palabra vaga en la especificaci\xF3n", path232, part.line));
9780
9810
  if (opts.businessOnly !== false) {
9781
- out.push(...lintText(part.text, "LINT-BIZ-001", tech, "Jerga t\xE9cnica en la especificaci\xF3n de negocio", path222, part.line));
9811
+ out.push(...lintText(part.text, "LINT-BIZ-001", tech, "Jerga t\xE9cnica en la especificaci\xF3n de negocio", path232, part.line));
9782
9812
  }
9783
9813
  }
9784
9814
  if (req.scenarios.length === 0) {
9785
- out.push(diag("TRACE-001", "error", `El requisito ${req.id} no tiene ning\xFAn escenario`, { path: path222, line: req.line, suggestion: "A\xF1ade al menos un escenario CUANDO/ENTONCES" }));
9815
+ out.push(diag("TRACE-001", "error", `El requisito ${req.id} no tiene ning\xFAn escenario`, { path: path232, line: req.line, suggestion: "A\xF1ade al menos un escenario CUANDO/ENTONCES" }));
9786
9816
  }
9787
9817
  return out;
9788
9818
  }
9789
- function lintDelta(delta, livingRequirements, path222, opts = {}) {
9819
+ function lintDelta(delta, livingRequirements, path232, opts = {}) {
9790
9820
  const out = [...delta.diagnostics];
9791
9821
  for (const req of [...delta.added, ...delta.modified]) {
9792
- out.push(...lintRequirement(req, path222, opts));
9822
+ out.push(...lintRequirement(req, path232, opts));
9793
9823
  }
9794
9824
  for (const req of delta.modified) {
9795
9825
  const living = livingRequirements.get(req.id);
9796
9826
  if (!living) {
9797
- out.push(diag("TRACE-007", "error", `MODIFIED ${req.id} no existe en la spec viva; usa ADDED`, { path: path222, line: req.line }));
9827
+ out.push(diag("TRACE-007", "error", `MODIFIED ${req.id} no existe en la spec viva; usa ADDED`, { path: path232, line: req.line }));
9798
9828
  continue;
9799
9829
  }
9800
9830
  for (const existing of living.scenarios) {
9801
9831
  if (!req.scenarios.some((s) => s.id === existing.id)) {
9802
9832
  out.push(
9803
9833
  diag("TRACE-007", "error", `MODIFIED ${req.id} pierde el escenario ${existing.id}: copia el bloque completo`, {
9804
- path: path222,
9834
+ path: path232,
9805
9835
  line: req.line,
9806
9836
  suggestion: "Copia el bloque completo de la spec viva y ed\xEDtalo; para quitarlo, decl\xE1ralo en REMOVED"
9807
9837
  })
@@ -9812,15 +9842,15 @@ function lintDelta(delta, livingRequirements, path222, opts = {}) {
9812
9842
  for (const req of delta.removed) {
9813
9843
  const living = livingRequirements.get(req.id);
9814
9844
  if (!living) {
9815
- out.push(diag("TRACE-007", "error", `REMOVED ${req.id} no existe en la spec viva`, { path: path222, line: req.line }));
9845
+ out.push(diag("TRACE-007", "error", `REMOVED ${req.id} no existe en la spec viva`, { path: path232, line: req.line }));
9816
9846
  }
9817
9847
  }
9818
9848
  for (const rename2 of delta.renamed) {
9819
9849
  if (rename2.from.id !== rename2.to.id) {
9820
- out.push(diag("LINT-DLT-003", "error", `RENAMED cambia el id (${rename2.from.id} \u2192 ${rename2.to.id}); los ids son inmutables`, { path: path222, line: rename2.line }));
9850
+ out.push(diag("LINT-DLT-003", "error", `RENAMED cambia el id (${rename2.from.id} \u2192 ${rename2.to.id}); los ids son inmutables`, { path: path232, line: rename2.line }));
9821
9851
  }
9822
9852
  if (!livingRequirements.has(rename2.from.id)) {
9823
- out.push(diag("TRACE-007", "error", `RENAMED ${rename2.from.id} no existe en la spec viva`, { path: path222, line: rename2.line }));
9853
+ out.push(diag("TRACE-007", "error", `RENAMED ${rename2.from.id} no existe en la spec viva`, { path: path232, line: rename2.line }));
9824
9854
  }
9825
9855
  }
9826
9856
  return out;
@@ -9844,7 +9874,7 @@ var MERMAID_KEYWORDS = [
9844
9874
  "architecture-beta"
9845
9875
  ];
9846
9876
  var MERMAID_BLOCKS = /^\s*(alt|loop|opt|par|rect|critical|break|subgraph)\b/;
9847
- function lintPlan(planText, path222) {
9877
+ function lintPlan(planText, path232) {
9848
9878
  const out = [];
9849
9879
  const blocks = [...planText.matchAll(/```mermaid\r?\n([\s\S]*?)```/g)];
9850
9880
  for (const [index, block] of blocks.entries()) {
@@ -9854,7 +9884,7 @@ function lintPlan(planText, path222) {
9854
9884
  if (!MERMAID_KEYWORDS.some((keyword) => first.startsWith(keyword))) {
9855
9885
  out.push(
9856
9886
  diag("LINT-PLN-002", "error", `Diagrama mermaid ${index + 1}: la primera l\xEDnea debe declarar el tipo (${MERMAID_KEYWORDS.slice(0, 5).join(", ")}\u2026) y empieza por "${first.slice(0, 30)}"`, {
9857
- path: path222,
9887
+ path: path232,
9858
9888
  suggestion: "Corrige el tipo del diagrama o elimina el bloque"
9859
9889
  })
9860
9890
  );
@@ -9868,7 +9898,7 @@ function lintPlan(planText, path222) {
9868
9898
  if (open !== 0) {
9869
9899
  out.push(
9870
9900
  diag("LINT-PLN-002", "error", `Diagrama mermaid ${index + 1}: faltan ${Math.abs(open)} \`end\` (bloques alt/loop/subgraph sin cerrar)`, {
9871
- path: path222,
9901
+ path: path232,
9872
9902
  suggestion: "Cierra cada bloque alt/loop/opt/par/rect/subgraph con `end`"
9873
9903
  })
9874
9904
  );
@@ -9880,7 +9910,7 @@ function lintPlan(planText, path222) {
9880
9910
  if (message.includes(";")) {
9881
9911
  out.push(
9882
9912
  diag("LINT-PLN-003", "error", `Diagrama mermaid ${index + 1}: el mensaje "${message.trim().slice(0, 40)}\u2026" usa \`;\` y mermaid lo interpreta como fin de sentencia`, {
9883
- path: path222,
9913
+ path: path232,
9884
9914
  suggestion: "Sustituye `;` por `\xB7` o `,` en los mensajes de sequenceDiagram"
9885
9915
  })
9886
9916
  );
@@ -10182,6 +10212,29 @@ function requiresMockups(meta, cfg) {
10182
10212
  function mockupOverride(change) {
10183
10213
  return (change.meta?.overrides ?? []).some((override) => override.gate === "mockup");
10184
10214
  }
10215
+ function docsReady(change) {
10216
+ const paths = change.docsPaths ?? [];
10217
+ return paths.some((p) => p.endsWith("tecnica.md")) && paths.some((p) => p.endsWith("manual.md"));
10218
+ }
10219
+ function clarifyAdvisory(change, cfg) {
10220
+ const open = change.clarify?.open.length ?? 0;
10221
+ if (cfg.gates.clarify.mode !== "advisory" || open === 0) return [];
10222
+ return [
10223
+ diag("ATLAS-CLARIFY-001", "warning", `El cambio "${change.slug}" tiene ${open} pregunta(s) sin aclarar`, {
10224
+ ...change.clarifyPath !== void 0 ? { path: change.clarifyPath } : {},
10225
+ suggestion: `Aclara antes de planificar: /satlas.clarify ${change.slug} (o satlas clarify ${change.slug})`
10226
+ })
10227
+ ];
10228
+ }
10229
+ function docsAdvisory(change, cfg) {
10230
+ const lane = change.meta?.lane ?? cfg.lanes.default;
10231
+ if (lane !== "full" || cfg.gates.docs.mode !== "advisory" || docsReady(change)) return [];
10232
+ return [
10233
+ diag("ATLAS-DOCS-001", "warning", `El cambio "${change.slug}" (carril completo) no tiene su documentaci\xF3n t\xE9cnica y manual`, {
10234
+ suggestion: `Genera la documentaci\xF3n: satlas docs ${change.slug} (o /satlas.docs ${change.slug})`
10235
+ })
10236
+ ];
10237
+ }
10185
10238
  function deriveState(input) {
10186
10239
  const { change, cfg, approval, blockingFindings } = input;
10187
10240
  const lane = change.meta?.lane ?? cfg.lanes.default;
@@ -10244,6 +10297,16 @@ function deriveState(input) {
10244
10297
  };
10245
10298
  }
10246
10299
  if (!change.planPath && !change.tasks) {
10300
+ const openQuestions = change.clarify?.open.length ?? 0;
10301
+ if (openQuestions > 0 && cfg.gates.clarify.mode === "blocking") {
10302
+ blockedBy.push(`aclaraci\xF3n pendiente (${openQuestions})`);
10303
+ return {
10304
+ state: "approved",
10305
+ blockedBy,
10306
+ nextAction: next(`/satlas.clarify ${change.slug}`, `Aclarar ${openQuestions} pregunta(s) antes de planificar`, true),
10307
+ progress
10308
+ };
10309
+ }
10247
10310
  return { state: "approved", blockedBy, nextAction: next(`/satlas.plan ${change.slug}`, "Crear el plan t\xE9cnico y las tareas", true), progress };
10248
10311
  }
10249
10312
  if (tasksTotal > 0 && tasksDone < tasksTotal) {
@@ -10257,6 +10320,10 @@ function deriveState(input) {
10257
10320
  blockedBy.push("review pendiente");
10258
10321
  return { state: "verified", blockedBy, nextAction: next(`/satlas.review ${change.slug}`, "Revisi\xF3n de c\xF3digo", true), progress };
10259
10322
  }
10323
+ if (lane === "full" && cfg.gates.docs.mode === "blocking" && !docsReady(change)) {
10324
+ blockedBy.push("documentaci\xF3n pendiente");
10325
+ return { state: "reviewed", blockedBy, nextAction: next(`/satlas.docs ${change.slug}`, "Generar la documentaci\xF3n t\xE9cnica y manual del cambio", true), progress };
10326
+ }
10260
10327
  return { state: "ready", blockedBy, nextAction: next(`satlas archive ${change.slug}`, "Archivar el cambio y plegar los deltas"), progress };
10261
10328
  }
10262
10329
  function stateLabel(state) {
@@ -10270,6 +10337,7 @@ function stateLabel(state) {
10270
10337
  building: "construyendo",
10271
10338
  built: "construido",
10272
10339
  verified: "verificado",
10340
+ reviewed: "revisado",
10273
10341
  ready: "listo para archivar",
10274
10342
  archived: "archivado"
10275
10343
  };
@@ -10596,6 +10664,19 @@ async function loadChange(root, slug, relDir) {
10596
10664
  diagnostics.push(...change.fix.diagnostics);
10597
10665
  change.fixCovers = parseFixCovers(fixRaw);
10598
10666
  }
10667
+ const clarifyFile = path5.join(dir, "clarify.md");
10668
+ const clarifyRaw = await readTextIfExists(clarifyFile);
10669
+ if (clarifyRaw !== void 0) {
10670
+ change.clarify = parseClarify(clarifyRaw, clarifyFile);
10671
+ change.clarifyPath = clarifyFile;
10672
+ diagnostics.push(...change.clarify.diagnostics);
10673
+ }
10674
+ const docsPaths = [];
10675
+ for (const name of ["tecnica.md", "manual.md"]) {
10676
+ const docFile = path5.join(dir, "docs", name);
10677
+ if (await exists(docFile)) docsPaths.push(docFile);
10678
+ }
10679
+ if (docsPaths.length > 0) change.docsPaths = docsPaths;
10599
10680
  const mockupManifest = path5.join(dir, "mockups", "manifest.yaml");
10600
10681
  if (await exists(mockupManifest)) change.mockupManifestPath = mockupManifest;
10601
10682
  return change;
@@ -10809,6 +10890,44 @@ satlas verify <slug> --file fix --scenario REQ-DOMINIO-001-S1 --command "<comand
10809
10890
  o, si es manual:
10810
10891
  satlas verify <slug> --file fix --scenario REQ-DOMINIO-001-S1 --method manual --result pass --by "<nombre>" --notes "<c\xF3mo se comprob\xF3>"
10811
10892
  -->
10893
+ `,
10894
+ docTecnica: `# Documentaci\xF3n t\xE9cnica \u2014 {{TITLE}}
10895
+
10896
+ ## Resumen del cambio
10897
+
10898
+ - **Cambio**: \`{{SLUG}}\` \xB7 dominio \`{{DOMAIN}}\` \xB7 carril \`{{LANE}}\`
10899
+ - **Actualizado**: {{DATE}}
10900
+ - **Tareas**: {{TASKS}}
10901
+
10902
+ ## Requisitos y escenarios
10903
+
10904
+ {{REQUIREMENTS}}
10905
+
10906
+ ## Evidencia registrada
10907
+
10908
+ {{EVIDENCE}}
10909
+
10910
+ ## Pendiente de evidencia
10911
+
10912
+ {{PENDING}}
10913
+ `,
10914
+ docManual: `# Manual \u2014 {{TITLE}}
10915
+
10916
+ ## Qu\xE9 hace este cambio
10917
+
10918
+ {{TITLE}} \u2014 dominio \`{{DOMAIN}}\` (cambio \`{{SLUG}}\`, carril \`{{LANE}}\`).
10919
+
10920
+ ## C\xF3mo se usa
10921
+
10922
+ {{SCENARIOS}}
10923
+
10924
+ ## C\xF3mo se comprob\xF3
10925
+
10926
+ {{EVIDENCE}}
10927
+
10928
+ ## Pendiente de comprobar
10929
+
10930
+ {{PENDING}}
10812
10931
  `
10813
10932
  };
10814
10933
  var EN = {
@@ -10934,6 +11053,44 @@ Cubre: REQ-DOMAIN-001
10934
11053
  <!-- Register real evidence with:
10935
11054
  satlas verify <slug> --file fix --scenario REQ-DOMAIN-001-S1 --command "<command>" --by "<name>"
10936
11055
  -->
11056
+ `,
11057
+ docTecnica: `# Technical documentation \u2014 {{TITLE}}
11058
+
11059
+ ## Change summary
11060
+
11061
+ - **Change**: \`{{SLUG}}\` \xB7 domain \`{{DOMAIN}}\` \xB7 lane \`{{LANE}}\`
11062
+ - **Updated**: {{DATE}}
11063
+ - **Tasks**: {{TASKS}}
11064
+
11065
+ ## Requirements and scenarios
11066
+
11067
+ {{REQUIREMENTS}}
11068
+
11069
+ ## Recorded evidence
11070
+
11071
+ {{EVIDENCE}}
11072
+
11073
+ ## Pending evidence
11074
+
11075
+ {{PENDING}}
11076
+ `,
11077
+ docManual: `# Manual \u2014 {{TITLE}}
11078
+
11079
+ ## What this change does
11080
+
11081
+ {{TITLE}} \u2014 domain \`{{DOMAIN}}\` (change \`{{SLUG}}\`, lane \`{{LANE}}\`).
11082
+
11083
+ ## How to use it
11084
+
11085
+ {{SCENARIOS}}
11086
+
11087
+ ## How it was verified
11088
+
11089
+ {{EVIDENCE}}
11090
+
11091
+ ## Pending verification
11092
+
11093
+ {{PENDING}}
10937
11094
  `
10938
11095
  };
10939
11096
  function templatesFor(language) {
@@ -13433,6 +13590,84 @@ async function upgradeAdvisory(root) {
13433
13590
  }
13434
13591
  return { plan, diagnostics };
13435
13592
  }
13593
+ var DOCS_MARKER_START = "<!-- specatlas:generado:inicio -->";
13594
+ var DOCS_MARKER_END = "<!-- specatlas:generado:fin -->";
13595
+ function docData(change, language, now) {
13596
+ const es = language !== "en";
13597
+ const requirements = [...change.delta?.added ?? [], ...change.delta?.modified ?? []];
13598
+ const evidence = change.verify?.evidence ?? [];
13599
+ const passed = new Set(evidence.filter((entry) => entry.result === "pass").map((entry) => entry.scenario));
13600
+ const requirementsText = requirements.length === 0 ? es ? "_Sin requisitos en el delta._" : "_No requirements in the delta._" : requirements.map((requirement) => {
13601
+ const lines = [`### ${requirement.id} \u2014 ${requirement.title}`, ""];
13602
+ for (const scenario of requirement.scenarios) lines.push(`- \`${scenario.id}\` \u2014 ${scenario.title}`);
13603
+ return lines.join("\n");
13604
+ }).join("\n\n");
13605
+ const scenarioList = requirements.flatMap((requirement) => requirement.scenarios);
13606
+ const scenariosText = scenarioList.length === 0 ? es ? "_Sin escenarios._" : "_No scenarios._" : scenarioList.map((scenario) => `- \`${scenario.id}\` \u2014 ${scenario.title}`).join("\n");
13607
+ 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");
13608
+ const pending = scenarioList.filter((scenario) => !passed.has(scenario.id));
13609
+ 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");
13610
+ return {
13611
+ TITLE: change.meta?.title ?? change.slug,
13612
+ SLUG: change.slug,
13613
+ DOMAIN: change.meta?.domain ?? "\u2014",
13614
+ LANE: change.meta?.lane ?? "standard",
13615
+ DATE: localDate(now),
13616
+ TASKS: change.tasks ? `${change.tasks.counts.done}/${change.tasks.counts.total}` : es ? "sin tareas" : "no tasks",
13617
+ REQUIREMENTS: requirementsText,
13618
+ SCENARIOS: scenariosText,
13619
+ EVIDENCE: evidenceText,
13620
+ PENDING: pendingText
13621
+ };
13622
+ }
13623
+ function mergeManaged(existing, block, language) {
13624
+ const managed = `${DOCS_MARKER_START}
13625
+ ${block.trimEnd()}
13626
+ ${DOCS_MARKER_END}`;
13627
+ if (existing === void 0) {
13628
+ 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.)";
13629
+ return `${managed}
13630
+
13631
+ ${notes}
13632
+ `;
13633
+ }
13634
+ const start = existing.indexOf(DOCS_MARKER_START);
13635
+ const end = existing.indexOf(DOCS_MARKER_END);
13636
+ if (start >= 0 && end > start) {
13637
+ const before = existing.slice(0, start);
13638
+ const after = existing.slice(end + DOCS_MARKER_END.length);
13639
+ return `${before}${managed}${after}`;
13640
+ }
13641
+ return `${managed}
13642
+
13643
+ ${existing}`;
13644
+ }
13645
+ async function generateDocs(opts) {
13646
+ const root = path19.resolve(opts.root);
13647
+ const { config } = await loadWorkspace(root);
13648
+ const change = await loadChange(root, opts.slug);
13649
+ if (!change.meta) {
13650
+ return {
13651
+ slug: opts.slug,
13652
+ files: [],
13653
+ diagnostics: [diag("ATLAS-DOCS-002", "error", `No existe el cambio "${opts.slug}"`, { suggestion: "Comprueba el nombre del cambio" })]
13654
+ };
13655
+ }
13656
+ const language = config.project.language;
13657
+ const tipo = opts.tipo ?? "all";
13658
+ const tipos = tipo === "all" ? ["tecnica", "manual"] : [tipo];
13659
+ const templates = templatesFor(language);
13660
+ const data = docData(change, language, opts.now ?? /* @__PURE__ */ new Date());
13661
+ const files = [];
13662
+ for (const current of tipos) {
13663
+ const file = path19.join(change.dir, "docs", `${current}.md`);
13664
+ const block = renderTemplate(current === "tecnica" ? templates.docTecnica : templates.docManual, data);
13665
+ const existing = await readTextIfExists(file);
13666
+ await writeText(file, mergeManaged(existing, block, language));
13667
+ files.push({ tipo: current, path: file, created: existing === void 0 });
13668
+ }
13669
+ return { slug: opts.slug, files, diagnostics: [] };
13670
+ }
13436
13671
  var SARIF_SCHEMA = "https://json.schemastore.org/sarif-2.1.0.json";
13437
13672
  var SARIF_VERSION = "2.1.0";
13438
13673
  var TOOL_NAME = "SpecAtlas";
@@ -13470,7 +13705,7 @@ function resultOf(diagnostic, root) {
13470
13705
  message: { text: diagnostic.message }
13471
13706
  };
13472
13707
  if (diagnostic.path) {
13473
- const rel = path19.relative(root, diagnostic.path);
13708
+ const rel = path20.relative(root, diagnostic.path);
13474
13709
  if (rel !== "") {
13475
13710
  const physicalLocation = {
13476
13711
  artifactLocation: { uri: toPosix(rel) },
@@ -13525,7 +13760,7 @@ async function runDoctor(root) {
13525
13760
  const approvals = await loadApprovals(workspace.sddDir);
13526
13761
  findings.push(...approvals.diagnostics);
13527
13762
  for (const change of workspace.changes) {
13528
- const deltaPath = path20.join(change.dir, "spec.md");
13763
+ const deltaPath = path21.join(change.dir, "spec.md");
13529
13764
  const deltaContent = await readTextIfExists(deltaPath);
13530
13765
  const approval = verifyApproval(change, approvals.byArtifact, config, deltaContent ?? void 0);
13531
13766
  if ((change.planPath || change.tasks) && (approval.status === "missing" || approval.status === "stale")) {
@@ -13547,7 +13782,7 @@ async function runDoctor(root) {
13547
13782
  }
13548
13783
  for (const override of change.meta?.overrides ?? []) {
13549
13784
  if (!override.reason.trim() || !override.by.trim()) {
13550
- findings.push(diag("ATLAS-LIFECYCLE-004", "error", `Override del gate "${override.gate}" sin motivo o autor`, { path: path20.join(change.dir, "meta.yaml") }));
13785
+ findings.push(diag("ATLAS-LIFECYCLE-004", "error", `Override del gate "${override.gate}" sin motivo o autor`, { path: path21.join(change.dir, "meta.yaml") }));
13551
13786
  }
13552
13787
  }
13553
13788
  }
@@ -13579,7 +13814,7 @@ function livingRequirementsMap(specs) {
13579
13814
  return map;
13580
13815
  }
13581
13816
  async function runCiGate(opts) {
13582
- const root = path21.resolve(opts.root);
13817
+ const root = path22.resolve(opts.root);
13583
13818
  const { workspace, config } = await loadWorkspace(root);
13584
13819
  const diagnostics = [];
13585
13820
  const checks = [];
@@ -13590,7 +13825,7 @@ async function runCiGate(opts) {
13590
13825
  let changesErrors = 0;
13591
13826
  let changesWarnings = 0;
13592
13827
  for (const change of workspace.changes) {
13593
- const lintFindings = change.delta ? lintDelta(change.delta, living, path21.join(change.dir, "spec.md"), { language: config.spec.language }) : [];
13828
+ const lintFindings = change.delta ? lintDelta(change.delta, living, path22.join(change.dir, "spec.md"), { language: config.spec.language }) : [];
13594
13829
  const trace = checkTrace({
13595
13830
  specs: workspace.specs,
13596
13831
  change,
@@ -13699,6 +13934,8 @@ var CATALOG = [
13699
13934
  { name: "trace", description: "Verifica la trazabilidad requisito \u2192 escenario \u2192 tarea \u2192 evidencia", flags: ["change", "require-evidence", "json"], usage: "satlas trace [--change <slug>] [--require-evidence] [--json]" },
13700
13935
  { name: "waves", description: "Calcula las olas paralelas de construcci\xF3n", flags: ["change", "max-parallel", "json"], usage: "satlas waves [--change <slug>] [--max-parallel N] [--json]" },
13701
13936
  { name: "doctor", description: "Diagn\xF3stico de salud del workspace", flags: ["json"], usage: "satlas doctor [--json]" },
13937
+ { name: "clarify", description: "Preguntas abiertas y aclaraciones del cambio (informe; aclarar es fase del agente)", flags: ["json"], usage: "satlas clarify <slug>" },
13938
+ { name: "docs", description: "Genera la documentaci\xF3n t\xE9cnica y manual del cambio (carril completo) desde la evidencia", flags: ["tipo", "json"], usage: "satlas docs <slug> [--tipo tecnica|manual|all]" },
13702
13939
  { name: "upgrade", description: "Actualiza el estado del proyecto a la versi\xF3n vigente (vista previa por defecto)", flags: ["apply", "rollback", "json"], usage: "satlas upgrade [--apply | --rollback] [--json]" },
13703
13940
  { name: "approve", description: "Firma la aprobaci\xF3n de un artefacto (spec): local o desde la etiqueta de un issue de GitHub", flags: ["by", "channel", "note", "dry-run", "from-github", "label", "json"], usage: 'satlas approve <slug|ruta> --by "<nombre>" | satlas approve <slug> --from-github' },
13704
13941
  { name: "issue", description: "Sincroniza el cambio con un issue de GitHub (tracker, no gate)", flags: ["labels", "json"], usage: "satlas issue sync <slug> [--labels a,b] | satlas issue status <slug>" },
@@ -13750,6 +13987,13 @@ var ES2 = {
13750
13987
  "new.done": "Cambio creado. Siguiente: escribe la spec funcional.",
13751
13988
  "upgrade.title": "Actualizar el estado del proyecto",
13752
13989
  "upgrade.preview": "Vista previa",
13990
+ "clarify.title": "Aclaraciones del cambio",
13991
+ "clarify.mode": "modo",
13992
+ "clarify.open": "preguntas abiertas",
13993
+ "clarify.resolved": "aclaradas",
13994
+ "docs.title": "Documentaci\xF3n del cambio",
13995
+ "docs.created": "creado",
13996
+ "docs.updated": "actualizado",
13753
13997
  "summary.findings": "hallazgos",
13754
13998
  "label.errors": "errores",
13755
13999
  "label.warnings": "avisos",
@@ -13785,6 +14029,13 @@ var EN2 = {
13785
14029
  "new.done": "Change created. Next: write the business spec.",
13786
14030
  "upgrade.title": "Upgrade project state",
13787
14031
  "upgrade.preview": "Preview",
14032
+ "clarify.title": "Change clarifications",
14033
+ "clarify.mode": "mode",
14034
+ "clarify.open": "open questions",
14035
+ "clarify.resolved": "clarified",
14036
+ "docs.title": "Change documentation",
14037
+ "docs.created": "created",
14038
+ "docs.updated": "updated",
13788
14039
  "summary.findings": "findings",
13789
14040
  "label.errors": "errors",
13790
14041
  "label.warnings": "warnings",
@@ -13812,26 +14063,26 @@ function cliVersion() {
13812
14063
  }
13813
14064
 
13814
14065
  // src/commands/adapters.ts
13815
- import path25 from "path";
14066
+ import path26 from "path";
13816
14067
 
13817
14068
  // ../adapters/dist/index.js
13818
- import path22 from "path";
13819
- import { stringify as stringifyYaml7 } from "yaml";
13820
14069
  import path23 from "path";
14070
+ import { stringify as stringifyYaml7 } from "yaml";
14071
+ import path24 from "path";
13821
14072
  async function loadWorkflow(workflowDir) {
13822
- const phasesDir = path22.join(workflowDir, "phases");
13823
- const snippetsDir = path22.join(workflowDir, "snippets");
14073
+ const phasesDir = path23.join(workflowDir, "phases");
14074
+ const snippetsDir = path23.join(workflowDir, "snippets");
13824
14075
  const phases = [];
13825
14076
  const snippets = /* @__PURE__ */ new Map();
13826
14077
  const hashParts = [];
13827
14078
  for (const entry of (await listDir(phasesDir)).sort()) {
13828
14079
  if (!entry.endsWith(".md")) continue;
13829
- const filePath = path22.join(phasesDir, entry);
14080
+ const filePath = path23.join(phasesDir, entry);
13830
14081
  const raw = await readText(filePath);
13831
14082
  hashParts.push(raw);
13832
14083
  const fm = parseFrontmatter(raw, filePath);
13833
14084
  const data = fm.data;
13834
- const id = typeof data["id"] === "string" ? data["id"] : path22.basename(entry, ".md");
14085
+ const id = typeof data["id"] === "string" ? data["id"] : path23.basename(entry, ".md");
13835
14086
  phases.push({
13836
14087
  id,
13837
14088
  title: typeof data["title"] === "string" ? data["title"] : id,
@@ -13845,9 +14096,9 @@ async function loadWorkflow(workflowDir) {
13845
14096
  }
13846
14097
  for (const entry of (await listDir(snippetsDir)).sort()) {
13847
14098
  if (!entry.endsWith(".md")) continue;
13848
- const raw = await readTextIfExists(path22.join(snippetsDir, entry)) ?? "";
14099
+ const raw = await readTextIfExists(path23.join(snippetsDir, entry)) ?? "";
13849
14100
  hashParts.push(raw);
13850
- snippets.set(path22.basename(entry, ".md"), raw.trim());
14101
+ snippets.set(path23.basename(entry, ".md"), raw.trim());
13851
14102
  }
13852
14103
  return { phases: phases.sort((a, b) => a.id.localeCompare(b.id)), snippets, sourceHash: sha256(hashParts.join("\n---\n")) };
13853
14104
  }
@@ -14029,11 +14280,11 @@ Reglas: la spec es funcional y de negocio (sin tecnolog\xEDa); la trazabilidad e
14029
14280
  function tomlString(value) {
14030
14281
  return `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
14031
14282
  }
14032
- var MANIFEST_REL = path23.join(".sdd", ".generated", "manifest.json");
14283
+ var MANIFEST_REL = path24.join(".sdd", ".generated", "manifest.json");
14033
14284
  var BEGIN = "<!-- BEGIN specatlas -->";
14034
14285
  var END = "<!-- END specatlas -->";
14035
14286
  async function compileTargets(opts) {
14036
- const root = path23.resolve(opts.root);
14287
+ const root = path24.resolve(opts.root);
14037
14288
  const language = opts.language ?? "es";
14038
14289
  const diagnostics = [];
14039
14290
  const sources = await loadWorkflow(opts.workflowDir);
@@ -14041,7 +14292,7 @@ async function compileTargets(opts) {
14041
14292
  diagnostics.push({
14042
14293
  code: "ATLAS-ADAPTERS-002",
14043
14294
  severity: "error",
14044
- message: `No hay fases en ${path23.join(opts.workflowDir, "phases")}`,
14295
+ message: `No hay fases en ${path24.join(opts.workflowDir, "phases")}`,
14045
14296
  suggestion: "Revisa la carpeta workflow/phases del proyecto"
14046
14297
  });
14047
14298
  }
@@ -14053,7 +14304,7 @@ async function compileTargets(opts) {
14053
14304
  }
14054
14305
  }
14055
14306
  const compiled = [...deduped.values()];
14056
- const manifestPath = path23.join(root, MANIFEST_REL);
14307
+ const manifestPath = path24.join(root, MANIFEST_REL);
14057
14308
  const previous = await readManifest(manifestPath);
14058
14309
  const previousHashes = /* @__PURE__ */ new Map();
14059
14310
  for (const files2 of Object.values(previous?.targets ?? {})) {
@@ -14064,7 +14315,7 @@ async function compileTargets(opts) {
14064
14315
  const stale = [];
14065
14316
  const missing = [];
14066
14317
  for (const file of compiled) {
14067
- const abs = path23.join(root, file.path);
14318
+ const abs = path24.join(root, file.path);
14068
14319
  const finalContent = file.path === "AGENTS.md" ? mergeAgentsBlock(await readTextIfExists(abs) ?? "", file.content) : file.content;
14069
14320
  const hash = artifactHash(finalContent);
14070
14321
  const existing = await readTextIfExists(abs);
@@ -14080,7 +14331,7 @@ async function compileTargets(opts) {
14080
14331
  stale.push(file.path);
14081
14332
  }
14082
14333
  if (!opts.check && status !== "unchanged") {
14083
- await ensureDir(path23.dirname(abs));
14334
+ await ensureDir(path24.dirname(abs));
14084
14335
  await writeText(abs, finalContent);
14085
14336
  written.push(file.path);
14086
14337
  }
@@ -14134,7 +14385,7 @@ async function readManifest(manifestPath) {
14134
14385
  }
14135
14386
  }
14136
14387
  async function checkAdapters(opts) {
14137
- const manifestPath = path23.join(path23.resolve(opts.root), MANIFEST_REL);
14388
+ const manifestPath = path24.join(path24.resolve(opts.root), MANIFEST_REL);
14138
14389
  const manifestRaw = await readTextIfExists(manifestPath);
14139
14390
  if (manifestRaw === void 0) return { ok: false, manifest: false, stale: [], missing: [], orphaned: [] };
14140
14391
  const manifest = await readManifest(manifestPath);
@@ -14151,7 +14402,7 @@ async function checkAdapters(opts) {
14151
14402
  }
14152
14403
 
14153
14404
  // src/paths.ts
14154
- import path24 from "path";
14405
+ import path25 from "path";
14155
14406
  import { fileURLToPath as fileURLToPath2 } from "url";
14156
14407
  async function resolveProfilesDir() {
14157
14408
  const env = process.env["SPECATLAS_PROFILES_DIR"];
@@ -14161,16 +14412,16 @@ async function resolveProfilesDir() {
14161
14412
  async function resolveWorkflowDir() {
14162
14413
  const env = process.env["SPECATLAS_WORKFLOW_DIR"];
14163
14414
  if (env && await exists(env)) return env;
14164
- return walkUpFor(path24.join("workflow", "phases"));
14415
+ return walkUpFor(path25.join("workflow", "phases"));
14165
14416
  }
14166
14417
  async function walkUpFor(relative) {
14167
- let dir = path24.dirname(fileURLToPath2(import.meta.url));
14418
+ let dir = path25.dirname(fileURLToPath2(import.meta.url));
14168
14419
  for (let i = 0; i < 8; i += 1) {
14169
- const candidate = path24.join(dir, relative);
14420
+ const candidate = path25.join(dir, relative);
14170
14421
  if (await exists(candidate)) {
14171
- return relative.includes(path24.sep) ? path24.dirname(candidate) : candidate;
14422
+ return relative.includes(path25.sep) ? path25.dirname(candidate) : candidate;
14172
14423
  }
14173
- const parent = path24.dirname(dir);
14424
+ const parent = path25.dirname(dir);
14174
14425
  if (parent === dir) break;
14175
14426
  dir = parent;
14176
14427
  }
@@ -14248,7 +14499,7 @@ async function runAdapters(ctx) {
14248
14499
  for (const [target, count2] of byTarget) lines.push(` ${target}: ${count2} archivo(s)`);
14249
14500
  if (report.written.length > 0) {
14250
14501
  lines.push("");
14251
- for (const p of report.written) lines.push(` + ${path25.relative(ctx.cwd, path25.join(root, p))}`);
14502
+ for (const p of report.written) lines.push(` + ${path26.relative(ctx.cwd, path26.join(root, p))}`);
14252
14503
  } else {
14253
14504
  lines.push("");
14254
14505
  lines.push("Sin cambios: los adaptadores ya estaban al d\xEDa.");
@@ -14259,7 +14510,7 @@ async function runAdapters(ctx) {
14259
14510
  }
14260
14511
 
14261
14512
  // src/commands/adopt.ts
14262
- import path26 from "path";
14513
+ import path27 from "path";
14263
14514
  async function runAdopt(ctx) {
14264
14515
  const { root } = await requireWorkspace(ctx);
14265
14516
  const domainsFlag = flagString(ctx.flags, "domains") ?? flagString(ctx.flags, "domain");
@@ -14279,11 +14530,11 @@ async function runAdopt(ctx) {
14279
14530
  }
14280
14531
  if (result.createdSpecs.length > 0) {
14281
14532
  lines.push("");
14282
- for (const spec of result.createdSpecs) lines.push(` ${result.dryRun ? "[dry-run] " : "+ "}${path26.relative(ctx.cwd, spec)}`);
14533
+ for (const spec of result.createdSpecs) lines.push(` ${result.dryRun ? "[dry-run] " : "+ "}${path27.relative(ctx.cwd, spec)}`);
14283
14534
  }
14284
14535
  if (result.reportPath && !result.dryRun) {
14285
14536
  lines.push("");
14286
- lines.push(` Informe: ${path26.relative(ctx.cwd, result.reportPath)}`);
14537
+ lines.push(` Informe: ${path27.relative(ctx.cwd, result.reportPath)}`);
14287
14538
  }
14288
14539
  lines.push("");
14289
14540
  lines.push("Siguiente: fase /satlas-adopt por dominio (requisitos AS-IS) y luego:");
@@ -14296,8 +14547,8 @@ async function runAdopt(ctx) {
14296
14547
  diagnostics: result.diagnostics,
14297
14548
  data: {
14298
14549
  domains: result.domains.map((d) => ({ name: d.name, files: d.files, existingSpec: d.existingSpec })),
14299
- createdSpecs: result.createdSpecs.map((s) => path26.relative(ctx.cwd, s)),
14300
- reportPath: result.reportPath ? path26.relative(ctx.cwd, result.reportPath) : void 0,
14550
+ createdSpecs: result.createdSpecs.map((s) => path27.relative(ctx.cwd, s)),
14551
+ reportPath: result.reportPath ? path27.relative(ctx.cwd, result.reportPath) : void 0,
14301
14552
  stack: result.stack,
14302
14553
  dryRun: result.dryRun
14303
14554
  },
@@ -14306,7 +14557,7 @@ async function runAdopt(ctx) {
14306
14557
  }
14307
14558
 
14308
14559
  // src/commands/analyze.ts
14309
- import path27 from "path";
14560
+ import path28 from "path";
14310
14561
  async function runAnalyzeCommand(ctx) {
14311
14562
  const { root } = await requireWorkspace(ctx);
14312
14563
  const slug = ctx.positionals[0];
@@ -14317,23 +14568,23 @@ async function runAnalyzeCommand(ctx) {
14317
14568
  const lines = [`An\xE1lisis \u2014 ${slug}`, "", ` estado: ${result.status}`, ` hallazgos: ${result.summary.errors} errores, ${result.summary.warnings} avisos`, ` evidencia: ${result.evidence.done}/${result.evidence.total} escenarios`];
14318
14569
  if (result.waves) lines.push(` olas: ${result.waves.blocks} bloque(s), ${result.waves.waves} ola(s), ${result.waves.tasks} tarea(s)`);
14319
14570
  if (result.mockups) lines.push(` mockups: ${result.mockups.screens} pantalla(s)${result.mockups.stale ? " (desactualizados)" : ""}`);
14320
- if (result.path) lines.push("", ` informe: ${path27.relative(ctx.cwd, result.path)}`);
14571
+ if (result.path) lines.push("", ` informe: ${path28.relative(ctx.cwd, result.path)}`);
14321
14572
  for (const finding of result.findings) {
14322
14573
  lines.push(` ${finding.severity.toUpperCase()} ${finding.code} \u2014 ${finding.message}`);
14323
14574
  }
14324
14575
  return {
14325
14576
  exitCode: result.status === "blocked" ? 1 : 0,
14326
14577
  diagnostics: result.findings,
14327
- data: result.path ? { ...result, path: path27.relative(ctx.cwd, result.path) } : result,
14578
+ data: result.path ? { ...result, path: path28.relative(ctx.cwd, result.path) } : result,
14328
14579
  text: lines
14329
14580
  };
14330
14581
  }
14331
14582
 
14332
14583
  // src/commands/approve.ts
14333
- import path29 from "path";
14584
+ import path30 from "path";
14334
14585
 
14335
14586
  // src/commands/issue.ts
14336
- import path28 from "path";
14587
+ import path29 from "path";
14337
14588
  async function runIssue(ctx) {
14338
14589
  const action = ctx.positionals[0] ?? "status";
14339
14590
  const slug = ctx.positionals[1];
@@ -14361,7 +14612,7 @@ async function runIssue(ctx) {
14361
14612
  return { exitCode: 2, diagnostics: [{ code: "ATLAS-ISSUE-000", severity: "error", message: "Falta el slug: satlas issue status <slug>" }] };
14362
14613
  }
14363
14614
  const change = await loadChange(root, slug);
14364
- const { config } = await loadConfig(path28.join(root, ".sdd"));
14615
+ const { config } = await loadConfig(path29.join(root, ".sdd"));
14365
14616
  const tracker = change.meta?.tracker;
14366
14617
  const lines = [`Issue de ${slug}`, ""];
14367
14618
  if (!tracker || tracker.provider !== "github") {
@@ -14432,7 +14683,7 @@ async function runApprove(ctx) {
14432
14683
  };
14433
14684
  }
14434
14685
  const { root, workspace, config } = await requireWorkspace(ctx);
14435
- const artifact = target.includes("/") || target.includes("\\") ? target : path29.join("changes", target, "spec.md");
14686
+ const artifact = target.includes("/") || target.includes("\\") ? target : path30.join("changes", target, "spec.md");
14436
14687
  const channelFlag = flagString(ctx.flags, "channel");
14437
14688
  const channel = channelFlag === "presentation" || channelFlag === "editor" || channelFlag === "pr" || channelFlag === "tracker" ? channelFlag : "cli";
14438
14689
  const change = workspace.changes.find(
@@ -14473,13 +14724,13 @@ async function runApprove(ctx) {
14473
14724
  return {
14474
14725
  exitCode: hasErrors2 ? 1 : 0,
14475
14726
  diagnostics: result.diagnostics,
14476
- data: result.approval ? { ...result.approval, file: path29.relative(ctx.cwd, result.file) } : void 0,
14727
+ data: result.approval ? { ...result.approval, file: path30.relative(ctx.cwd, result.file) } : void 0,
14477
14728
  text: lines
14478
14729
  };
14479
14730
  }
14480
14731
 
14481
14732
  // src/commands/archive.ts
14482
- import path30 from "path";
14733
+ import path31 from "path";
14483
14734
  async function runArchive(ctx) {
14484
14735
  const slug = ctx.positionals[0];
14485
14736
  if (!slug) {
@@ -14516,7 +14767,7 @@ async function runArchive(ctx) {
14516
14767
  } else {
14517
14768
  lines.push("El delta no contiene operaciones (ADDED/MODIFIED/REMOVED/RENAMED).");
14518
14769
  }
14519
- if (result.archivedTo) lines.push(` archivado en: ${path30.relative(ctx.cwd, result.archivedTo)}`);
14770
+ if (result.archivedTo) lines.push(` archivado en: ${path31.relative(ctx.cwd, result.archivedTo)}`);
14520
14771
  const hasErrors2 = result.diagnostics.some((d) => d.severity === "error");
14521
14772
  return {
14522
14773
  exitCode: hasErrors2 ? 1 : 0,
@@ -14527,7 +14778,7 @@ async function runArchive(ctx) {
14527
14778
  }
14528
14779
 
14529
14780
  // src/commands/ci.ts
14530
- import path31 from "path";
14781
+ import path32 from "path";
14531
14782
  async function runCi(ctx) {
14532
14783
  const { root, config } = await requireWorkspace(ctx);
14533
14784
  const strict = flagBool(ctx.flags, "strict");
@@ -14553,10 +14804,10 @@ async function runCi(ctx) {
14553
14804
  const lines = ["CI de SpecAtlas", ""];
14554
14805
  let sarifWritten;
14555
14806
  if (sarifPath) {
14556
- const target = path31.resolve(ctx.cwd, sarifPath);
14807
+ const target = path32.resolve(ctx.cwd, sarifPath);
14557
14808
  try {
14558
14809
  await writeText(target, toSarifText({ diagnostics: gate.diagnostics, root, version: cliVersion(), failed: gate.failed }));
14559
- sarifWritten = path31.relative(ctx.cwd, target);
14810
+ sarifWritten = path32.relative(ctx.cwd, target);
14560
14811
  } catch (err) {
14561
14812
  diagnostics.push({
14562
14813
  code: "ATLAS-CI-SARIF-001",
@@ -14590,13 +14841,83 @@ async function runCi(ctx) {
14590
14841
  };
14591
14842
  }
14592
14843
 
14844
+ // src/commands/clarify.ts
14845
+ async function runClarify(ctx) {
14846
+ const slug = ctx.positionals[0];
14847
+ if (!slug) {
14848
+ return { exitCode: 2, diagnostics: [{ code: "ATLAS-CLARIFY-000", severity: "error", message: "Falta el slug: satlas clarify <slug>" }] };
14849
+ }
14850
+ const { workspace, config } = await requireWorkspace(ctx);
14851
+ const change = workspace.changes.find((c) => c.slug === slug);
14852
+ if (!change) {
14853
+ return { exitCode: 2, diagnostics: [{ code: "ATLAS-CLARIFY-000", severity: "error", message: `No existe el cambio "${slug}"` }] };
14854
+ }
14855
+ const open = change.clarify?.open ?? [];
14856
+ const resolved = change.clarify?.resolved ?? [];
14857
+ const mode = config.gates.clarify.mode;
14858
+ const lines = [msg("clarify.title", ctx.language), ""];
14859
+ lines.push(` ${msg("clarify.mode", ctx.language)}: ${mode}`);
14860
+ lines.push(` ${msg("clarify.open", ctx.language)}: ${open.length}`);
14861
+ for (const item of open) lines.push(` [ ] ${item.text}`);
14862
+ lines.push(` ${msg("clarify.resolved", ctx.language)}: ${resolved.length}`);
14863
+ for (const item of resolved) lines.push(` [x] ${item.text}${item.answer ? ` \u2014 ${item.answer}` : ""}`);
14864
+ if (open.length > 0) {
14865
+ lines.push("");
14866
+ lines.push(`Aclara con la fase del agente: /satlas.clarify ${slug}`);
14867
+ } else if (resolved.length === 0) {
14868
+ lines.push("");
14869
+ lines.push("El cambio no tiene preguntas abiertas ni aclaraciones registradas.");
14870
+ }
14871
+ return {
14872
+ exitCode: 0,
14873
+ diagnostics: [],
14874
+ data: {
14875
+ slug,
14876
+ mode,
14877
+ open: open.map((item) => ({ text: item.text, line: item.line })),
14878
+ resolved: resolved.map((item) => ({ text: item.text, line: item.line, answer: item.answer })),
14879
+ ...open.length > 0 ? { action: `/satlas.clarify ${slug}` } : {}
14880
+ },
14881
+ text: lines
14882
+ };
14883
+ }
14884
+
14885
+ // src/commands/docs.ts
14886
+ import path33 from "path";
14887
+ async function runDocs(ctx) {
14888
+ const slug = ctx.positionals[0];
14889
+ if (!slug) {
14890
+ return { exitCode: 2, diagnostics: [{ code: "ATLAS-DOCS-000", severity: "error", message: "Falta el slug: satlas docs <slug> [--tipo tecnica|manual|all]" }] };
14891
+ }
14892
+ const { root } = await requireWorkspace(ctx);
14893
+ const tipoFlag = flagString(ctx.flags, "tipo");
14894
+ const tipo = tipoFlag === "tecnica" || tipoFlag === "manual" || tipoFlag === "all" ? tipoFlag : "all";
14895
+ const result = await generateDocs({ root, slug, tipo });
14896
+ const errors = result.diagnostics.filter((d) => d.severity === "error");
14897
+ const lines = [msg("docs.title", ctx.language), ""];
14898
+ for (const file of result.files) {
14899
+ lines.push(` ${file.created ? msg("docs.created", ctx.language) : msg("docs.updated", ctx.language)}: ${path33.relative(ctx.cwd, file.path)}`);
14900
+ }
14901
+ for (const error of errors) lines.push(` ERROR ${error.code} \u2014 ${error.message}`);
14902
+ return {
14903
+ exitCode: errors.length > 0 ? 2 : 0,
14904
+ diagnostics: result.diagnostics,
14905
+ data: {
14906
+ slug: result.slug,
14907
+ tipo,
14908
+ files: result.files.map((file) => ({ tipo: file.tipo, path: path33.relative(ctx.cwd, file.path), created: file.created }))
14909
+ },
14910
+ text: lines
14911
+ };
14912
+ }
14913
+
14593
14914
  // src/commands/profile.ts
14594
- import path32 from "path";
14915
+ import path34 from "path";
14595
14916
  async function runProfile(ctx) {
14596
14917
  const action = ctx.positionals[0] ?? "detect";
14597
14918
  const { root, workspace } = await requireWorkspace(ctx);
14598
14919
  const officialDir = await resolveProfilesDir();
14599
- const customDir = path32.join(workspace.sddDir, "profiles", "custom");
14920
+ const customDir = path34.join(workspace.sddDir, "profiles", "custom");
14600
14921
  const profiles = [...officialDir ? await loadProfilesFromDir(officialDir) : [], ...await loadProfilesFromDir(customDir)];
14601
14922
  if (action === "detect") {
14602
14923
  const result = await detectProfiles(root, profiles);
@@ -14625,7 +14946,7 @@ async function runProfile(ctx) {
14625
14946
  if (!name) {
14626
14947
  return { exitCode: 2, diagnostics: [{ code: "ATLAS-PROFILE-001", severity: "error", message: "Falta el nombre del perfil: satlas profile create <nombre>" }] };
14627
14948
  }
14628
- const file = path32.join(customDir, `${name}.yaml`);
14949
+ const file = path34.join(customDir, `${name}.yaml`);
14629
14950
  if (await exists(file)) {
14630
14951
  return { exitCode: 2, diagnostics: [{ code: "ATLAS-PROFILE-002", severity: "error", message: `El perfil ya existe: ${file}` }] };
14631
14952
  }
@@ -14634,7 +14955,7 @@ async function runProfile(ctx) {
14634
14955
  exitCode: 0,
14635
14956
  diagnostics: [],
14636
14957
  data: { file },
14637
- text: [`Perfil creado: ${path32.relative(ctx.cwd, file)}`, "", "Ed\xEDtalo con los comandos reales de tu stack (build/lint/test) y tus anti-patrones."]
14958
+ text: [`Perfil creado: ${path34.relative(ctx.cwd, file)}`, "", "Ed\xEDtalo con los comandos reales de tu stack (build/lint/test) y tus anti-patrones."]
14638
14959
  };
14639
14960
  }
14640
14961
  return {
@@ -14643,7 +14964,7 @@ async function runProfile(ctx) {
14643
14964
  };
14644
14965
  }
14645
14966
  async function isInside(dir, name) {
14646
- return exists(path32.join(dir, `${name}.yaml`));
14967
+ return exists(path34.join(dir, `${name}.yaml`));
14647
14968
  }
14648
14969
  function profileTemplate(name) {
14649
14970
  return `name: ${name}
@@ -14671,7 +14992,7 @@ assumptions: []
14671
14992
  }
14672
14993
 
14673
14994
  // src/commands/doctor.ts
14674
- import path33 from "path";
14995
+ import path35 from "path";
14675
14996
  async function runDoctorCommand(ctx) {
14676
14997
  const { root, config } = await requireWorkspace(ctx);
14677
14998
  const report = await runDoctor(root);
@@ -14696,7 +15017,7 @@ async function runDoctorCommand(ctx) {
14696
15017
  lines.push("Sin hallazgos. El workspace est\xE1 sano.");
14697
15018
  } else {
14698
15019
  for (const finding of report.findings) {
14699
- const location = finding.path ? ` ${path33.relative(ctx.cwd, finding.path)}${finding.line ? `:${finding.line}` : ""}` : "";
15020
+ const location = finding.path ? ` ${path35.relative(ctx.cwd, finding.path)}${finding.line ? `:${finding.line}` : ""}` : "";
14700
15021
  lines.push(` ${finding.severity.toUpperCase()} ${finding.code}${location} \u2014 ${finding.message}`);
14701
15022
  if (finding.suggestion) lines.push(` \u21B3 ${finding.suggestion}`);
14702
15023
  }
@@ -14770,7 +15091,7 @@ async function runHelp(ctx, commandName) {
14770
15091
  }
14771
15092
 
14772
15093
  // src/commands/init.ts
14773
- import path34 from "path";
15094
+ import path36 from "path";
14774
15095
  async function runInit(ctx) {
14775
15096
  const languageFlag = flagString(ctx.flags, "language");
14776
15097
  const language = languageFlag === "en" ? "en" : languageFlag === "es" ? "es" : void 0;
@@ -14817,11 +15138,11 @@ async function runInit(ctx) {
14817
15138
  const lines = [msg("init.title", ctx.language), ""];
14818
15139
  if (result.created.length > 0) {
14819
15140
  lines.push(msg("init.done", ctx.language));
14820
- for (const file of result.created) lines.push(` + ${path34.relative(ctx.cwd, file)}`);
15141
+ for (const file of result.created) lines.push(` + ${path36.relative(ctx.cwd, file)}`);
14821
15142
  if (compiled.length > 0) {
14822
15143
  lines.push("");
14823
15144
  lines.push("Adaptadores compilados:");
14824
- for (const file of compiled) lines.push(` + ${path34.relative(ctx.cwd, path34.join(ctx.cwd, file))}`);
15145
+ for (const file of compiled) lines.push(` + ${path36.relative(ctx.cwd, path36.join(ctx.cwd, file))}`);
14825
15146
  }
14826
15147
  lines.push("");
14827
15148
  lines.push("Siguiente: crea un cambio con `satlas new <slug>` y escribe la spec funcional.");
@@ -14831,8 +15152,8 @@ async function runInit(ctx) {
14831
15152
  exitCode: hasErrors2 ? 1 : 0,
14832
15153
  diagnostics: result.diagnostics,
14833
15154
  data: {
14834
- sddDir: path34.relative(ctx.cwd, result.sddDir),
14835
- created: result.created.map((f) => path34.relative(ctx.cwd, f)),
15155
+ sddDir: path36.relative(ctx.cwd, result.sddDir),
15156
+ created: result.created.map((f) => path36.relative(ctx.cwd, f)),
14836
15157
  detected: result.detected?.matches.map((m) => ({ name: m.name, score: m.score })),
14837
15158
  best: result.detected?.best?.name
14838
15159
  },
@@ -14873,7 +15194,7 @@ async function runMetrics(ctx) {
14873
15194
  }
14874
15195
 
14875
15196
  // src/commands/mockup.ts
14876
- import path35 from "path";
15197
+ import path37 from "path";
14877
15198
  async function runMockup(ctx) {
14878
15199
  const { root, workspace, config } = await requireWorkspace(ctx);
14879
15200
  const slug = ctx.positionals[0];
@@ -14922,17 +15243,17 @@ async function runMockup(ctx) {
14922
15243
  ` pantallas: ${plan.screens.length}`,
14923
15244
  ...plan.screens.map((s) => ` - ${s.id}: ${s.title} (${s.illustrates.length} escenario(s))`),
14924
15245
  "",
14925
- ` plan: ${path35.relative(ctx.cwd, planFile)}`,
14926
- ` manifiesto: ${path35.relative(ctx.cwd, manifestFile)}`,
15246
+ ` plan: ${path37.relative(ctx.cwd, planFile)}`,
15247
+ ` manifiesto: ${path37.relative(ctx.cwd, manifestFile)}`,
14927
15248
  "",
14928
15249
  "Siguiente: genera el HTML de cada pantalla (fase /satlas-mockup) y valida con `satlas mockup " + slug + " --check`."
14929
15250
  ];
14930
- return { exitCode: 0, diagnostics: [], data: { plan, planFile: path35.relative(ctx.cwd, planFile), manifestFile: path35.relative(ctx.cwd, manifestFile) }, text: lines };
15251
+ return { exitCode: 0, diagnostics: [], data: { plan, planFile: path37.relative(ctx.cwd, planFile), manifestFile: path37.relative(ctx.cwd, manifestFile) }, text: lines };
14931
15252
  }
14932
15253
 
14933
15254
  // src/mcp/server.ts
14934
15255
  import { promises as fs3 } from "fs";
14935
- import path38 from "path";
15256
+ import path40 from "path";
14936
15257
  import { createInterface } from "readline/promises";
14937
15258
 
14938
15259
  // src/mcp/protocol.ts
@@ -15021,12 +15342,12 @@ async function runAtlasFixes(_args, host) {
15021
15342
  }
15022
15343
 
15023
15344
  // src/mcp/tools/glossary.ts
15024
- import path36 from "path";
15345
+ import path38 from "path";
15025
15346
  async function runAtlasGlossary(_args, host) {
15026
15347
  const resolved = await host.getWorkspace();
15027
15348
  if (!resolved) return noWorkspaceResult();
15028
15349
  const { root, config } = resolved;
15029
- const glossaryPath = path36.resolve(root, config.spec.glossary);
15350
+ const glossaryPath = path38.resolve(root, config.spec.glossary);
15030
15351
  const raw = await readTextIfExists(glossaryPath);
15031
15352
  if (raw === void 0) {
15032
15353
  return jsonResult({
@@ -15072,7 +15393,7 @@ async function runAtlasImpact(args, host) {
15072
15393
  }
15073
15394
 
15074
15395
  // src/evaluate.ts
15075
- import path37 from "path";
15396
+ import path39 from "path";
15076
15397
  function livingRequirementsMap2(specs) {
15077
15398
  const map = /* @__PURE__ */ new Map();
15078
15399
  for (const spec of specs) {
@@ -15081,7 +15402,7 @@ function livingRequirementsMap2(specs) {
15081
15402
  return map;
15082
15403
  }
15083
15404
  async function evaluateChange(workspace, config, change, approvals) {
15084
- const deltaPath = path37.join(change.dir, "spec.md");
15405
+ const deltaPath = path39.join(change.dir, "spec.md");
15085
15406
  const deltaContent = await readTextIfExists(deltaPath);
15086
15407
  const approval = verifyApproval(change, approvals, config, deltaContent ?? void 0);
15087
15408
  const living = livingRequirementsMap2(workspace.specs);
@@ -15100,7 +15421,8 @@ async function evaluateChange(workspace, config, change, approvals) {
15100
15421
  blockingFindings: change.delta ? blocking : 0,
15101
15422
  ...mockupsAreReady !== void 0 ? { mockupsReady: mockupsAreReady } : {}
15102
15423
  });
15103
- return { change, approval, lintFindings, trace, blocking, state };
15424
+ const phaseAdvisories = [...clarifyAdvisory(change, config), ...docsAdvisory(change, config)];
15425
+ return { change, approval, lintFindings: [...lintFindings, ...phaseAdvisories], trace, blocking, state };
15104
15426
  }
15105
15427
 
15106
15428
  // src/mcp/tools/next.ts
@@ -15353,8 +15675,8 @@ var WorkspaceCache = class {
15353
15675
  }
15354
15676
  };
15355
15677
  async function workspaceStamp(root) {
15356
- const sddDir = path38.join(root, ".sdd");
15357
- const markers = [sddDir, path38.join(sddDir, "config.yaml"), path38.join(sddDir, "changes"), path38.join(sddDir, "specs"), path38.join(sddDir, "approvals.yaml")];
15678
+ const sddDir = path40.join(root, ".sdd");
15679
+ const markers = [sddDir, path40.join(sddDir, "config.yaml"), path40.join(sddDir, "changes"), path40.join(sddDir, "specs"), path40.join(sddDir, "approvals.yaml")];
15358
15680
  let stamp = 0;
15359
15681
  for (const marker of markers) {
15360
15682
  try {
@@ -15372,7 +15694,7 @@ async function resolveMcpWorkspace(cwd, cache) {
15372
15694
  const cached2 = cache.get(root);
15373
15695
  if (cached2 && cached2.mtimeMs === stamp) return cached2.value;
15374
15696
  const { workspace, config } = await loadWorkspace(root);
15375
- const approvals = await loadApprovals(path38.join(root, ".sdd"));
15697
+ const approvals = await loadApprovals(path40.join(root, ".sdd"));
15376
15698
  const value = { root, workspace, config, approvals: approvals.byArtifact };
15377
15699
  cache.set(root, stamp, value);
15378
15700
  return value;
@@ -15464,7 +15786,7 @@ async function runMcp(ctx) {
15464
15786
  }
15465
15787
 
15466
15788
  // src/commands/packs.ts
15467
- import path39 from "path";
15789
+ import path41 from "path";
15468
15790
  async function runPacks(ctx) {
15469
15791
  const { root, config, workspace } = await requireWorkspace(ctx);
15470
15792
  const slug = flagString(ctx.flags, "check");
@@ -15522,14 +15844,14 @@ async function runPacks(ctx) {
15522
15844
  data: {
15523
15845
  slug,
15524
15846
  packs: evaluations.map((evaluation) => ({ id: evaluation.pack.id, status: evaluation.status, passed: evaluation.passed, failed: evaluation.failed, results: evaluation.results })),
15525
- analyzePath: path39.posix.join(".sdd", "changes", slug, "analyze.md")
15847
+ analyzePath: path41.posix.join(".sdd", "changes", slug, "analyze.md")
15526
15848
  },
15527
15849
  text: lines
15528
15850
  };
15529
15851
  }
15530
15852
 
15531
15853
  // src/commands/new.ts
15532
- import path40 from "path";
15854
+ import path42 from "path";
15533
15855
  async function runNew(ctx) {
15534
15856
  const slug = ctx.positionals[0];
15535
15857
  if (!slug) {
@@ -15555,14 +15877,14 @@ async function runNew(ctx) {
15555
15877
  msg("new.title", ctx.language),
15556
15878
  "",
15557
15879
  msg("new.done", ctx.language),
15558
- ...result.files.map((f) => ` + ${path40.relative(ctx.cwd, f)}`),
15880
+ ...result.files.map((f) => ` + ${path42.relative(ctx.cwd, f)}`),
15559
15881
  "",
15560
15882
  `Siguiente: /satlas.specify ${result.slug} \u2014 escribe la especificaci\xF3n 100% funcional y de negocio.`
15561
15883
  ];
15562
15884
  return {
15563
15885
  exitCode: hasErrors2 ? 1 : 0,
15564
15886
  diagnostics: result.diagnostics,
15565
- data: { slug: result.slug, files: result.files.map((f) => path40.relative(ctx.cwd, f)) },
15887
+ data: { slug: result.slug, files: result.files.map((f) => path42.relative(ctx.cwd, f)) },
15566
15888
  text: lines
15567
15889
  };
15568
15890
  }
@@ -15597,7 +15919,7 @@ async function runNext(ctx) {
15597
15919
  }
15598
15920
 
15599
15921
  // src/commands/present.ts
15600
- import path41 from "path";
15922
+ import path43 from "path";
15601
15923
  async function runPresentCommand(ctx) {
15602
15924
  const { root } = await requireWorkspace(ctx);
15603
15925
  const slug = ctx.positionals[0];
@@ -15609,7 +15931,7 @@ async function runPresentCommand(ctx) {
15609
15931
  if (result.path) {
15610
15932
  lines.push("Propuesta generada");
15611
15933
  lines.push("");
15612
- lines.push(` archivo: ${path41.relative(ctx.cwd, result.path)}`);
15934
+ lines.push(` archivo: ${path43.relative(ctx.cwd, result.path)}`);
15613
15935
  if (result.hash) lines.push(` hash de la spec: ${shortHash(result.hash)}`);
15614
15936
  lines.push("");
15615
15937
  lines.push("\xC1brela en el navegador y comp\xE1rtela con el stakeholder.");
@@ -15619,7 +15941,7 @@ async function runPresentCommand(ctx) {
15619
15941
  return {
15620
15942
  exitCode: hasErrors2 ? 1 : 0,
15621
15943
  diagnostics: result.diagnostics,
15622
- data: result.path ? { path: path41.relative(ctx.cwd, result.path), hash: result.hash } : void 0,
15944
+ data: result.path ? { path: path43.relative(ctx.cwd, result.path), hash: result.hash } : void 0,
15623
15945
  text: lines
15624
15946
  };
15625
15947
  }
@@ -15767,7 +16089,7 @@ async function runStatus(ctx) {
15767
16089
  }
15768
16090
 
15769
16091
  // src/commands/trace.ts
15770
- import path42 from "path";
16092
+ import path44 from "path";
15771
16093
  async function runTrace(ctx) {
15772
16094
  const { workspace, config } = await requireWorkspace(ctx);
15773
16095
  const slug = flagString(ctx.flags, "change");
@@ -15787,7 +16109,7 @@ async function runTrace(ctx) {
15787
16109
  data.push({ slug: change.slug, nodes: result.graph.nodes.length, edges: result.graph.edges.length, summary: result.summary, findings: result.findings });
15788
16110
  lines.push(` ${change.slug}: ${result.graph.nodes.length} nodos, ${result.graph.edges.length} aristas, ${errors} errores, ${result.summary.warnings} avisos`);
15789
16111
  for (const finding of result.findings) {
15790
- const location = finding.path ? ` (${path42.relative(ctx.cwd, finding.path)}${finding.line ? `:${finding.line}` : ""})` : "";
16112
+ const location = finding.path ? ` (${path44.relative(ctx.cwd, finding.path)}${finding.line ? `:${finding.line}` : ""})` : "";
15791
16113
  lines.push(` ${finding.severity.toUpperCase()} ${finding.code} \u2014 ${finding.message}${location}`);
15792
16114
  }
15793
16115
  }
@@ -15797,7 +16119,7 @@ async function runTrace(ctx) {
15797
16119
  }
15798
16120
 
15799
16121
  // src/commands/upgrade.ts
15800
- import path43 from "path";
16122
+ import path45 from "path";
15801
16123
  async function runUpgrade(ctx) {
15802
16124
  const { root } = await requireWorkspace(ctx);
15803
16125
  const apply = flagBool(ctx.flags, "apply");
@@ -15851,7 +16173,7 @@ function renderPreview(plan, root, ctx) {
15851
16173
  }
15852
16174
  for (const item of plan.unreadable) {
15853
16175
  lines.push(` Ilegible: "${item.artifact}" \u2014 ${item.reason}`);
15854
- lines.push(` \u21B3 ${path43.relative(ctx.cwd, item.path)}`);
16176
+ lines.push(` \u21B3 ${path45.relative(ctx.cwd, item.path)}`);
15855
16177
  }
15856
16178
  return { exitCode: 0, diagnostics: [], data: summarizable(plan), text: lines };
15857
16179
  }
@@ -15914,7 +16236,7 @@ function renderRollback(report, ctx) {
15914
16236
  }
15915
16237
 
15916
16238
  // src/commands/validate.ts
15917
- import path44 from "path";
16239
+ import path46 from "path";
15918
16240
  async function runValidate(ctx) {
15919
16241
  const { root, workspace, config, approvals } = await requireWorkspace(ctx);
15920
16242
  const slug = flagString(ctx.flags, "change");
@@ -15941,7 +16263,7 @@ async function runValidate(ctx) {
15941
16263
  for (const finding of evaluation.lintFindings) {
15942
16264
  lines.push(` ${finding.severity.toUpperCase()} ${finding.code}${finding.line ? `:${finding.line}` : ""} \u2014 ${finding.message}`);
15943
16265
  }
15944
- lines.push(` archivo: ${path44.relative(ctx.cwd, path44.join(change.dir, "spec.md"))}`);
16266
+ lines.push(` archivo: ${path46.relative(ctx.cwd, path46.join(change.dir, "spec.md"))}`);
15945
16267
  }
15946
16268
  if (targets.length === 0) lines.push("Sin cambios activos.");
15947
16269
  if (advisory && advisory.plan.pending.length > 0) {
@@ -15969,7 +16291,7 @@ async function runValidate(ctx) {
15969
16291
  }
15970
16292
 
15971
16293
  // src/commands/verify.ts
15972
- import path45 from "path";
16294
+ import path47 from "path";
15973
16295
  async function runVerify(ctx) {
15974
16296
  const { root, workspace } = await requireWorkspace(ctx);
15975
16297
  const slug = ctx.positionals[0];
@@ -16012,7 +16334,7 @@ async function runVerify(ctx) {
16012
16334
  return { exitCode: 2, diagnostics: [{ code: "ATLAS-VERIFY-001", severity: "error", message: "Falta --by <nombre> (o define meta.owner)" }] };
16013
16335
  }
16014
16336
  const profilesDir = await resolveProfilesDir();
16015
- const profile = await loadActiveProfile(path45.join(root, ".sdd"), [profilesDir ?? "", path45.join(root, ".sdd", "profiles", "custom")].filter(Boolean));
16337
+ const profile = await loadActiveProfile(path47.join(root, ".sdd"), [profilesDir ?? "", path47.join(root, ".sdd", "profiles", "custom")].filter(Boolean));
16016
16338
  const record = await recordEvidence({
16017
16339
  root,
16018
16340
  slug,
@@ -16034,7 +16356,7 @@ async function runVerify(ctx) {
16034
16356
  if (record.evidence.command) lines.push(` comando: ${record.evidence.command}`);
16035
16357
  lines.push(` resultado: ${record.evidence.result}`);
16036
16358
  if (record.evidence.outputHash) lines.push(` hash: ${record.evidence.outputHash}`);
16037
- lines.push(` archivo: ${path45.relative(ctx.cwd, record.path)}`);
16359
+ lines.push(` archivo: ${path47.relative(ctx.cwd, record.path)}`);
16038
16360
  if (record.stdout) {
16039
16361
  lines.push("");
16040
16362
  lines.push(" salida (\xFAltimas l\xEDneas):");
@@ -16049,7 +16371,7 @@ async function runVerify(ctx) {
16049
16371
  return {
16050
16372
  exitCode: record.exitCode === 0 ? 0 : 1,
16051
16373
  diagnostics: record.diagnostics,
16052
- data: { evidence: record.evidence, path: path45.relative(ctx.cwd, record.path), exitCode: record.exitCode },
16374
+ data: { evidence: record.evidence, path: path47.relative(ctx.cwd, record.path), exitCode: record.exitCode },
16053
16375
  text: lines
16054
16376
  };
16055
16377
  }
@@ -16104,6 +16426,8 @@ var HANDLERS2 = {
16104
16426
  trace: runTrace,
16105
16427
  waves: runWaves,
16106
16428
  doctor: runDoctorCommand,
16429
+ clarify: runClarify,
16430
+ docs: runDocs,
16107
16431
  upgrade: runUpgrade,
16108
16432
  approve: runApprove,
16109
16433
  issue: runIssue,
@@ -16149,7 +16473,7 @@ async function requireWorkspace(ctx) {
16149
16473
  throw new CliError("ATLAS-WS-001", msg("cli.noWorkspace", ctx.language), 2);
16150
16474
  }
16151
16475
  const { workspace, config } = await loadWorkspace(root);
16152
- const approvals = await loadApprovals(path46.join(root, ".sdd"));
16476
+ const approvals = await loadApprovals(path48.join(root, ".sdd"));
16153
16477
  return { root, workspace, config, approvals: approvals.byArtifact };
16154
16478
  }
16155
16479
  var CliError = class extends Error {
@@ -16216,7 +16540,7 @@ function printResult(command, result, json2, language) {
16216
16540
  }
16217
16541
  const lines = [...result.text ?? []];
16218
16542
  for (const d of result.diagnostics) {
16219
- const location = d.path ? ` ${path46.relative(process.cwd(), d.path)}${d.line ? `:${d.line}` : ""}` : "";
16543
+ const location = d.path ? ` ${path48.relative(process.cwd(), d.path)}${d.line ? `:${d.line}` : ""}` : "";
16220
16544
  lines.push(`${d.severity === "error" ? "ERROR" : d.severity === "warning" ? "AVISO" : "NOTA"} ${d.code}${location} \u2014 ${d.message}`);
16221
16545
  if (d.suggestion) lines.push(` \u21B3 ${d.suggestion}`);
16222
16546
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "specatlas",
3
- "version": "0.1.29",
3
+ "version": "0.1.30",
4
4
  "description": "SpecAtlas: kernel determinista de Spec-Driven Development (CLI)",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -40,7 +40,7 @@
40
40
  },
41
41
  "devDependencies": {
42
42
  "@specatlas/adapters": "0.1.1",
43
- "@specatlas/core": "0.1.26"
43
+ "@specatlas/core": "0.1.27"
44
44
  },
45
45
  "scripts": {
46
46
  "typecheck": "tsc --noEmit",
@@ -0,0 +1,39 @@
1
+ ---
2
+ id: clarify
3
+ title: Aclarar
4
+ description: >-
5
+ Resuelve supuestos, dependencias y preguntas abiertas de un cambio ya especificado
6
+ antes de aprobar y planificar. Usar cuando el usuario pide aclarar, resolver dudas
7
+ o cerrar preguntas abiertas de una especificación.
8
+ requires: []
9
+ produces:
10
+ - changes/<slug>/clarify.md
11
+ arguments: true
12
+ agent:
13
+ mode: primary
14
+ ---
15
+
16
+ # Fase: Aclarar (vaciar las preguntas abiertas)
17
+
18
+ ## Objetivo
19
+
20
+ Resolver los supuestos, dependencias y preguntas abiertas del cambio `{{SLUG}}` y dejarlos registrados en `.sdd/changes/{{SLUG}}/clarify.md`.
21
+ El contenido se escribe en **{{LANGUAGE_NAME}}**.
22
+
23
+ ## Pasos
24
+
25
+ 1. Lee `.sdd/changes/{{SLUG}}/spec.md`, `proposal.md` y, si existe, `clarify.md`.
26
+ 2. Detecta supuestos, dependencias y preguntas abiertas **reales** de la especificación; no inventes preguntas.
27
+ 3. Pregunta una a una (máximo 5, las que más cambien el resultado), con opciones concretas cuando ayuden a decidir. No des nada por respondido sin respuesta del humano.
28
+ 4. Escribe o actualiza `clarify.md` con la gramática canónica:
29
+ - `- [ ] pregunta` para lo que sigue abierto.
30
+ - `- [x] pregunta — respuesta` para lo aclarado.
31
+ 5. Refleja el resumen de lo decidido en la propuesta (`proposal.md`), en su sección correspondiente.
32
+ 6. Si una respuesta cambia lo especificado: corrige `spec.md` y avisa de que la firma quedó obsoleta (hay que volver a aprobar con `satlas approve`).
33
+ 7. Cierra con `satlas clarify {{SLUG}}` (estado de la aclaración) y `satlas next {{SLUG}}`.
34
+
35
+ ## Prohibido
36
+
37
+ - Inventar preguntas o respuestas.
38
+ - Cambiar la especificación sin avisar de la re-firma.
39
+ - Marcar como aclarada una pregunta que el humano no respondió.
@@ -0,0 +1,36 @@
1
+ ---
2
+ id: docs
3
+ title: Documentar
4
+ description: >-
5
+ Genera la documentación técnica y manual del cambio (carril completo) desde la
6
+ evidencia registrada. Usar cuando el usuario pide documentar, generar el manual
7
+ o cerrar la documentación antes de archivar.
8
+ requires: []
9
+ produces:
10
+ - changes/<slug>/docs/tecnica.md
11
+ - changes/<slug>/docs/manual.md
12
+ arguments: true
13
+ agent:
14
+ mode: primary
15
+ ---
16
+
17
+ # Fase: Documentar (técnica y manual del cambio)
18
+
19
+ ## Objetivo
20
+
21
+ Dejar la documentación técnica y manual del cambio `{{SLUG}}` en `.sdd/changes/{{SLUG}}/docs/`, construida desde lo real (especificación, tareas y evidencia).
22
+ El contenido se escribe en **{{LANGUAGE_NAME}}**.
23
+
24
+ ## Pasos
25
+
26
+ 1. Ejecuta `satlas docs {{SLUG}}` para generar el esqueleto de los dos documentos desde las plantillas y la evidencia.
27
+ 2. Revisa lo generado: si un dato falta o es incorrecto, corrige **la fuente** (spec, tareas o evidencia) y vuelve a ejecutar el comando; no retoques los datos generados.
28
+ 3. Completa lo que no se puede generar — decisiones, límites, ejemplos de uso y notas — **fuera del bloque gestionado** (lo que está entre los marcadores se reemplaza al regenerar; lo de fuera se conserva).
29
+ 4. Señala expresamente lo que quedó sin evidencia; nunca lo presentes como hecho.
30
+ 5. Cierra con `satlas docs {{SLUG}}` (regenera el bloque gestionado) y `satlas next {{SLUG}}`.
31
+
32
+ ## Prohibido
33
+
34
+ - Documentar como hecho algo sin evidencia.
35
+ - Escribir dentro del bloque gestionado (se reemplaza al regenerar).
36
+ - Tocar la especificación, el plan o las tareas desde esta fase.