specatlas 0.1.6 → 0.1.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/bin.js +171 -140
  2. package/package.json +2 -2
package/dist/bin.js CHANGED
@@ -1581,7 +1581,7 @@ var require_core = __commonJS({
1581
1581
  });
1582
1582
 
1583
1583
  // src/cli.ts
1584
- import path38 from "path";
1584
+ import path39 from "path";
1585
1585
 
1586
1586
  // ../core/dist/index.js
1587
1587
  import path from "path";
@@ -8938,6 +8938,7 @@ import path15 from "path";
8938
8938
  import { promises as fs } from "fs";
8939
8939
  import path16 from "path";
8940
8940
  import path17 from "path";
8941
+ import path18 from "path";
8941
8942
  var CORE_VERSION = "0.0.1";
8942
8943
  var REQ_ID_RE = /^REQ-[A-Z0-9]+(?:-[A-Z0-9]+)*-\d{3}$/;
8943
8944
  var SCENARIO_ID_RE = /^REQ-[A-Z0-9]+(?:-[A-Z0-9]+)*-\d{3}-S\d+$/;
@@ -9513,14 +9514,14 @@ function wordBoundary(text, term) {
9513
9514
  const re = new RegExp(`(?<![\\p{L}\\p{N}])${escaped}(?![\\p{L}\\p{N}])`, "iu");
9514
9515
  return re.test(text);
9515
9516
  }
9516
- function lintText(text, code, terms, label, path182, line) {
9517
+ function lintText(text, code, terms, label, path192, line) {
9517
9518
  const out = [];
9518
9519
  const lower = text.toLowerCase();
9519
9520
  for (const term of terms) {
9520
9521
  if (wordBoundary(lower, term)) {
9521
9522
  out.push(
9522
9523
  diag(code, "error", `${label}: "${term}"`, {
9523
- path: path182,
9524
+ path: path192,
9524
9525
  line,
9525
9526
  suggestion: "La especificaci\xF3n es funcional y de negocio: describe comportamiento, no tecnolog\xEDa ni adjetivos vagos"
9526
9527
  })
@@ -9529,7 +9530,7 @@ function lintText(text, code, terms, label, path182, line) {
9529
9530
  }
9530
9531
  return out;
9531
9532
  }
9532
- function lintRequirement(req, path182, opts = {}) {
9533
+ function lintRequirement(req, path192, opts = {}) {
9533
9534
  const out = [];
9534
9535
  const vague = opts.language === "en" ? VAGUE_EN : VAGUE_ES;
9535
9536
  const tech = opts.language === "en" ? TECH_EN : TECH_ES;
@@ -9540,32 +9541,32 @@ function lintRequirement(req, path182, opts = {}) {
9540
9541
  ...req.scenarios.flatMap((s) => [...s.when.map((w) => ({ text: w, line: s.line })), ...s.then.map((t) => ({ text: t, line: s.line }))])
9541
9542
  ];
9542
9543
  for (const part of parts) {
9543
- out.push(...lintText(part.text, "LINT-BIZ-002", vague, "Palabra vaga en la especificaci\xF3n", path182, part.line));
9544
+ out.push(...lintText(part.text, "LINT-BIZ-002", vague, "Palabra vaga en la especificaci\xF3n", path192, part.line));
9544
9545
  if (opts.businessOnly !== false) {
9545
- out.push(...lintText(part.text, "LINT-BIZ-001", tech, "Jerga t\xE9cnica en la especificaci\xF3n de negocio", path182, part.line));
9546
+ out.push(...lintText(part.text, "LINT-BIZ-001", tech, "Jerga t\xE9cnica en la especificaci\xF3n de negocio", path192, part.line));
9546
9547
  }
9547
9548
  }
9548
9549
  if (req.scenarios.length === 0) {
9549
- out.push(diag("TRACE-001", "error", `El requisito ${req.id} no tiene ning\xFAn escenario`, { path: path182, line: req.line, suggestion: "A\xF1ade al menos un escenario CUANDO/ENTONCES" }));
9550
+ out.push(diag("TRACE-001", "error", `El requisito ${req.id} no tiene ning\xFAn escenario`, { path: path192, line: req.line, suggestion: "A\xF1ade al menos un escenario CUANDO/ENTONCES" }));
9550
9551
  }
9551
9552
  return out;
9552
9553
  }
9553
- function lintDelta(delta, livingRequirements, path182, opts = {}) {
9554
+ function lintDelta(delta, livingRequirements, path192, opts = {}) {
9554
9555
  const out = [...delta.diagnostics];
9555
9556
  for (const req of [...delta.added, ...delta.modified]) {
9556
- out.push(...lintRequirement(req, path182, opts));
9557
+ out.push(...lintRequirement(req, path192, opts));
9557
9558
  }
9558
9559
  for (const req of delta.modified) {
9559
9560
  const living = livingRequirements.get(req.id);
9560
9561
  if (!living) {
9561
- out.push(diag("TRACE-007", "error", `MODIFIED ${req.id} no existe en la spec viva; usa ADDED`, { path: path182, line: req.line }));
9562
+ out.push(diag("TRACE-007", "error", `MODIFIED ${req.id} no existe en la spec viva; usa ADDED`, { path: path192, line: req.line }));
9562
9563
  continue;
9563
9564
  }
9564
9565
  for (const existing of living.scenarios) {
9565
9566
  if (!req.scenarios.some((s) => s.id === existing.id)) {
9566
9567
  out.push(
9567
9568
  diag("TRACE-007", "error", `MODIFIED ${req.id} pierde el escenario ${existing.id}: copia el bloque completo`, {
9568
- path: path182,
9569
+ path: path192,
9569
9570
  line: req.line,
9570
9571
  suggestion: "Copia el bloque completo de la spec viva y ed\xEDtalo; para quitarlo, decl\xE1ralo en REMOVED"
9571
9572
  })
@@ -9576,15 +9577,15 @@ function lintDelta(delta, livingRequirements, path182, opts = {}) {
9576
9577
  for (const req of delta.removed) {
9577
9578
  const living = livingRequirements.get(req.id);
9578
9579
  if (!living) {
9579
- out.push(diag("TRACE-007", "error", `REMOVED ${req.id} no existe en la spec viva`, { path: path182, line: req.line }));
9580
+ out.push(diag("TRACE-007", "error", `REMOVED ${req.id} no existe en la spec viva`, { path: path192, line: req.line }));
9580
9581
  }
9581
9582
  }
9582
9583
  for (const rename of delta.renamed) {
9583
9584
  if (rename.from.id !== rename.to.id) {
9584
- out.push(diag("LINT-DLT-003", "error", `RENAMED cambia el id (${rename.from.id} \u2192 ${rename.to.id}); los ids son inmutables`, { path: path182, line: rename.line }));
9585
+ out.push(diag("LINT-DLT-003", "error", `RENAMED cambia el id (${rename.from.id} \u2192 ${rename.to.id}); los ids son inmutables`, { path: path192, line: rename.line }));
9585
9586
  }
9586
9587
  if (!livingRequirements.has(rename.from.id)) {
9587
- out.push(diag("TRACE-007", "error", `RENAMED ${rename.from.id} no existe en la spec viva`, { path: path182, line: rename.line }));
9588
+ out.push(diag("TRACE-007", "error", `RENAMED ${rename.from.id} no existe en la spec viva`, { path: path192, line: rename.line }));
9588
9589
  }
9589
9590
  }
9590
9591
  return out;
@@ -12615,6 +12616,72 @@ async function runDoctor(root) {
12615
12616
  }
12616
12617
  return { findings, summary: countBySeverity(findings) };
12617
12618
  }
12619
+ var UI_DOMAINS2 = /* @__PURE__ */ new Set(["frontend", "mobile", "fullstack"]);
12620
+ function count(name, diagnostics) {
12621
+ return {
12622
+ name,
12623
+ errors: diagnostics.filter((d) => d.severity === "error").length,
12624
+ warnings: diagnostics.filter((d) => d.severity === "warning").length
12625
+ };
12626
+ }
12627
+ function livingRequirementsMap(specs) {
12628
+ const map = /* @__PURE__ */ new Map();
12629
+ for (const spec of specs) {
12630
+ for (const requirement of spec.spec.requirements) map.set(requirement.id, requirement);
12631
+ }
12632
+ return map;
12633
+ }
12634
+ async function runCiGate(opts) {
12635
+ const root = path18.resolve(opts.root);
12636
+ const { workspace, config } = await loadWorkspace(root);
12637
+ const diagnostics = [];
12638
+ const checks = [];
12639
+ const specFindings = workspace.specs.flatMap((spec) => lintSpec(spec.spec, { language: config.spec.language, businessOnly: config.spec.business_only }));
12640
+ diagnostics.push(...specFindings);
12641
+ checks.push(count("specs vivas", specFindings));
12642
+ const living = livingRequirementsMap(workspace.specs);
12643
+ let changesErrors = 0;
12644
+ let changesWarnings = 0;
12645
+ for (const change of workspace.changes) {
12646
+ const lintFindings = change.delta ? lintDelta(change.delta, living, path18.join(change.dir, "spec.md"), { language: config.spec.language }) : [];
12647
+ const trace = checkTrace({
12648
+ specs: workspace.specs,
12649
+ change,
12650
+ requireEvidence: config.gates.verify.mode !== "off" && config.gates.verify.require_evidence
12651
+ });
12652
+ const changeDiags = [...lintFindings, ...trace.findings];
12653
+ if (change.tasks && change.tasks.counts.total > 0) {
12654
+ const plan = planWaves(change.tasks, { maxParallel: config.waves.max_parallel });
12655
+ changeDiags.push(...plan.blocks.flatMap((block) => block.diagnostics));
12656
+ }
12657
+ if (UI_DOMAINS2.has(change.meta?.domain ?? "")) {
12658
+ const mockups = await checkMockups(root, change.slug, change);
12659
+ changeDiags.push(...mockups.findings);
12660
+ }
12661
+ if (config.packs.length > 0) {
12662
+ const resolved = await resolvePacks(workspace.sddDir, config);
12663
+ const evaluations = evaluatePacks(resolved.packs, change, config);
12664
+ changeDiags.push(...packFindings(evaluations, change));
12665
+ }
12666
+ diagnostics.push(...changeDiags);
12667
+ const entry = count(change.slug, changeDiags);
12668
+ changesErrors += entry.errors;
12669
+ changesWarnings += entry.warnings;
12670
+ }
12671
+ checks.push({ name: "cambios (lint + trace + waves + mockups)", errors: changesErrors, warnings: changesWarnings });
12672
+ const doctor = await runDoctor(root);
12673
+ const doctorErrors = doctor.findings.filter((d) => d.severity === "error");
12674
+ diagnostics.push(...doctorErrors);
12675
+ checks.push({ name: "doctor", errors: doctorErrors.length, warnings: 0 });
12676
+ for (const extra of opts.extra ?? []) {
12677
+ diagnostics.push(...extra.diagnostics);
12678
+ checks.push(count(extra.name, extra.diagnostics));
12679
+ }
12680
+ const errors = diagnostics.filter((d) => d.severity === "error").length;
12681
+ const warnings = diagnostics.filter((d) => d.severity === "warning").length;
12682
+ const failed = errors > 0 || (opts.strict ?? false) && warnings > 0;
12683
+ return { checks, diagnostics, errors, warnings, failed };
12684
+ }
12618
12685
 
12619
12686
  // src/args.ts
12620
12687
  var BOOLEAN_FLAGS = /* @__PURE__ */ new Set(["json", "local", "strict", "yes", "dry-run", "require-evidence", "help", "version"]);
@@ -12786,26 +12853,26 @@ function cliVersion() {
12786
12853
  }
12787
12854
 
12788
12855
  // src/commands/adapters.ts
12789
- import path20 from "path";
12856
+ import path21 from "path";
12790
12857
 
12791
12858
  // ../adapters/dist/index.js
12792
- import path18 from "path";
12859
+ import path19 from "path";
12793
12860
  import { stringify as stringifyYaml5 } from "yaml";
12794
12861
  import path22 from "path";
12795
12862
  async function loadWorkflow(workflowDir) {
12796
- const phasesDir = path18.join(workflowDir, "phases");
12797
- const snippetsDir = path18.join(workflowDir, "snippets");
12863
+ const phasesDir = path19.join(workflowDir, "phases");
12864
+ const snippetsDir = path19.join(workflowDir, "snippets");
12798
12865
  const phases = [];
12799
12866
  const snippets = /* @__PURE__ */ new Map();
12800
12867
  const hashParts = [];
12801
12868
  for (const entry of (await listDir(phasesDir)).sort()) {
12802
12869
  if (!entry.endsWith(".md")) continue;
12803
- const filePath = path18.join(phasesDir, entry);
12870
+ const filePath = path19.join(phasesDir, entry);
12804
12871
  const raw = await readText(filePath);
12805
12872
  hashParts.push(raw);
12806
12873
  const fm = parseFrontmatter(raw, filePath);
12807
12874
  const data = fm.data;
12808
- const id = typeof data["id"] === "string" ? data["id"] : path18.basename(entry, ".md");
12875
+ const id = typeof data["id"] === "string" ? data["id"] : path19.basename(entry, ".md");
12809
12876
  phases.push({
12810
12877
  id,
12811
12878
  title: typeof data["title"] === "string" ? data["title"] : id,
@@ -12819,9 +12886,9 @@ async function loadWorkflow(workflowDir) {
12819
12886
  }
12820
12887
  for (const entry of (await listDir(snippetsDir)).sort()) {
12821
12888
  if (!entry.endsWith(".md")) continue;
12822
- const raw = await readTextIfExists(path18.join(snippetsDir, entry)) ?? "";
12889
+ const raw = await readTextIfExists(path19.join(snippetsDir, entry)) ?? "";
12823
12890
  hashParts.push(raw);
12824
- snippets.set(path18.basename(entry, ".md"), raw.trim());
12891
+ snippets.set(path19.basename(entry, ".md"), raw.trim());
12825
12892
  }
12826
12893
  return { phases: phases.sort((a, b) => a.id.localeCompare(b.id)), snippets, sourceHash: sha256(hashParts.join("\n---\n")) };
12827
12894
  }
@@ -13125,7 +13192,7 @@ async function checkAdapters(opts) {
13125
13192
  }
13126
13193
 
13127
13194
  // src/paths.ts
13128
- import path19 from "path";
13195
+ import path20 from "path";
13129
13196
  import { fileURLToPath as fileURLToPath2 } from "url";
13130
13197
  async function resolveProfilesDir() {
13131
13198
  const env = process.env["SPECATLAS_PROFILES_DIR"];
@@ -13135,16 +13202,16 @@ async function resolveProfilesDir() {
13135
13202
  async function resolveWorkflowDir() {
13136
13203
  const env = process.env["SPECATLAS_WORKFLOW_DIR"];
13137
13204
  if (env && await exists(env)) return env;
13138
- return walkUpFor(path19.join("workflow", "phases"));
13205
+ return walkUpFor(path20.join("workflow", "phases"));
13139
13206
  }
13140
13207
  async function walkUpFor(relative) {
13141
- let dir = path19.dirname(fileURLToPath2(import.meta.url));
13208
+ let dir = path20.dirname(fileURLToPath2(import.meta.url));
13142
13209
  for (let i = 0; i < 8; i += 1) {
13143
- const candidate = path19.join(dir, relative);
13210
+ const candidate = path20.join(dir, relative);
13144
13211
  if (await exists(candidate)) {
13145
- return relative.includes(path19.sep) ? path19.dirname(candidate) : candidate;
13212
+ return relative.includes(path20.sep) ? path20.dirname(candidate) : candidate;
13146
13213
  }
13147
- const parent = path19.dirname(dir);
13214
+ const parent = path20.dirname(dir);
13148
13215
  if (parent === dir) break;
13149
13216
  dir = parent;
13150
13217
  }
@@ -13222,7 +13289,7 @@ async function runAdapters(ctx) {
13222
13289
  for (const [target, count2] of byTarget) lines.push(` ${target}: ${count2} archivo(s)`);
13223
13290
  if (report.written.length > 0) {
13224
13291
  lines.push("");
13225
- for (const p of report.written) lines.push(` + ${path20.relative(ctx.cwd, path20.join(root, p))}`);
13292
+ for (const p of report.written) lines.push(` + ${path21.relative(ctx.cwd, path21.join(root, p))}`);
13226
13293
  } else {
13227
13294
  lines.push("");
13228
13295
  lines.push("Sin cambios: los adaptadores ya estaban al d\xEDa.");
@@ -13233,7 +13300,7 @@ async function runAdapters(ctx) {
13233
13300
  }
13234
13301
 
13235
13302
  // src/commands/adopt.ts
13236
- import path21 from "path";
13303
+ import path23 from "path";
13237
13304
  async function runAdopt(ctx) {
13238
13305
  const { root } = await requireWorkspace(ctx);
13239
13306
  const domainsFlag = flagString(ctx.flags, "domains") ?? flagString(ctx.flags, "domain");
@@ -13253,11 +13320,11 @@ async function runAdopt(ctx) {
13253
13320
  }
13254
13321
  if (result.createdSpecs.length > 0) {
13255
13322
  lines.push("");
13256
- for (const spec of result.createdSpecs) lines.push(` ${result.dryRun ? "[dry-run] " : "+ "}${path21.relative(ctx.cwd, spec)}`);
13323
+ for (const spec of result.createdSpecs) lines.push(` ${result.dryRun ? "[dry-run] " : "+ "}${path23.relative(ctx.cwd, spec)}`);
13257
13324
  }
13258
13325
  if (result.reportPath && !result.dryRun) {
13259
13326
  lines.push("");
13260
- lines.push(` Informe: ${path21.relative(ctx.cwd, result.reportPath)}`);
13327
+ lines.push(` Informe: ${path23.relative(ctx.cwd, result.reportPath)}`);
13261
13328
  }
13262
13329
  lines.push("");
13263
13330
  lines.push("Siguiente: fase /satlas-adopt por dominio (requisitos AS-IS) y luego:");
@@ -13270,8 +13337,8 @@ async function runAdopt(ctx) {
13270
13337
  diagnostics: result.diagnostics,
13271
13338
  data: {
13272
13339
  domains: result.domains.map((d) => ({ name: d.name, files: d.files, existingSpec: d.existingSpec })),
13273
- createdSpecs: result.createdSpecs.map((s) => path21.relative(ctx.cwd, s)),
13274
- reportPath: result.reportPath ? path21.relative(ctx.cwd, result.reportPath) : void 0,
13340
+ createdSpecs: result.createdSpecs.map((s) => path23.relative(ctx.cwd, s)),
13341
+ reportPath: result.reportPath ? path23.relative(ctx.cwd, result.reportPath) : void 0,
13275
13342
  stack: result.stack,
13276
13343
  dryRun: result.dryRun
13277
13344
  },
@@ -13280,7 +13347,7 @@ async function runAdopt(ctx) {
13280
13347
  }
13281
13348
 
13282
13349
  // src/commands/analyze.ts
13283
- import path23 from "path";
13350
+ import path24 from "path";
13284
13351
  async function runAnalyzeCommand(ctx) {
13285
13352
  const { root } = await requireWorkspace(ctx);
13286
13353
  const slug = ctx.positionals[0];
@@ -13291,23 +13358,23 @@ async function runAnalyzeCommand(ctx) {
13291
13358
  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`];
13292
13359
  if (result.waves) lines.push(` olas: ${result.waves.blocks} bloque(s), ${result.waves.waves} ola(s), ${result.waves.tasks} tarea(s)`);
13293
13360
  if (result.mockups) lines.push(` mockups: ${result.mockups.screens} pantalla(s)${result.mockups.stale ? " (desactualizados)" : ""}`);
13294
- if (result.path) lines.push("", ` informe: ${path23.relative(ctx.cwd, result.path)}`);
13361
+ if (result.path) lines.push("", ` informe: ${path24.relative(ctx.cwd, result.path)}`);
13295
13362
  for (const finding of result.findings) {
13296
13363
  lines.push(` ${finding.severity.toUpperCase()} ${finding.code} \u2014 ${finding.message}`);
13297
13364
  }
13298
13365
  return {
13299
13366
  exitCode: result.status === "blocked" ? 1 : 0,
13300
13367
  diagnostics: result.findings,
13301
- data: result.path ? { ...result, path: path23.relative(ctx.cwd, result.path) } : result,
13368
+ data: result.path ? { ...result, path: path24.relative(ctx.cwd, result.path) } : result,
13302
13369
  text: lines
13303
13370
  };
13304
13371
  }
13305
13372
 
13306
13373
  // src/commands/approve.ts
13307
- import path25 from "path";
13374
+ import path26 from "path";
13308
13375
 
13309
13376
  // src/commands/issue.ts
13310
- import path24 from "path";
13377
+ import path25 from "path";
13311
13378
  async function runIssue(ctx) {
13312
13379
  const action = ctx.positionals[0] ?? "status";
13313
13380
  const slug = ctx.positionals[1];
@@ -13335,7 +13402,7 @@ async function runIssue(ctx) {
13335
13402
  return { exitCode: 2, diagnostics: [{ code: "ATLAS-ISSUE-000", severity: "error", message: "Falta el slug: satlas issue status <slug>" }] };
13336
13403
  }
13337
13404
  const change = await loadChange(root, slug);
13338
- const { config } = await loadConfig(path24.join(root, ".sdd"));
13405
+ const { config } = await loadConfig(path25.join(root, ".sdd"));
13339
13406
  const tracker = change.meta?.tracker;
13340
13407
  const lines = [`Issue de ${slug}`, ""];
13341
13408
  if (!tracker || tracker.provider !== "github") {
@@ -13406,7 +13473,7 @@ async function runApprove(ctx) {
13406
13473
  };
13407
13474
  }
13408
13475
  const { root } = await requireWorkspace(ctx);
13409
- const artifact = target.includes("/") || target.includes("\\") ? target : path25.join("changes", target, "spec.md");
13476
+ const artifact = target.includes("/") || target.includes("\\") ? target : path26.join("changes", target, "spec.md");
13410
13477
  const channelFlag = flagString(ctx.flags, "channel");
13411
13478
  const channel = channelFlag === "presentation" || channelFlag === "editor" || channelFlag === "pr" || channelFlag === "tracker" ? channelFlag : "cli";
13412
13479
  const result = await signApproval({
@@ -13431,13 +13498,13 @@ async function runApprove(ctx) {
13431
13498
  return {
13432
13499
  exitCode: hasErrors2 ? 1 : 0,
13433
13500
  diagnostics: result.diagnostics,
13434
- data: result.approval ? { ...result.approval, file: path25.relative(ctx.cwd, result.file) } : void 0,
13501
+ data: result.approval ? { ...result.approval, file: path26.relative(ctx.cwd, result.file) } : void 0,
13435
13502
  text: lines
13436
13503
  };
13437
13504
  }
13438
13505
 
13439
13506
  // src/commands/archive.ts
13440
- import path26 from "path";
13507
+ import path27 from "path";
13441
13508
  async function runArchive(ctx) {
13442
13509
  const slug = ctx.positionals[0];
13443
13510
  if (!slug) {
@@ -13467,7 +13534,7 @@ async function runArchive(ctx) {
13467
13534
  lines.push(` modificados: ${result.fold.applied.modified.join(", ") || "\u2014"}`);
13468
13535
  lines.push(` eliminados: ${result.fold.applied.removed.join(", ") || "\u2014"}`);
13469
13536
  lines.push(` renombrados: ${result.fold.applied.renamed.join(", ") || "\u2014"}`);
13470
- if (result.archivedTo) lines.push(` archivado en: ${path26.relative(ctx.cwd, result.archivedTo)}`);
13537
+ if (result.archivedTo) lines.push(` archivado en: ${path27.relative(ctx.cwd, result.archivedTo)}`);
13471
13538
  } else {
13472
13539
  lines.push("El delta no contiene operaciones (ADDED/MODIFIED/REMOVED/RENAMED).");
13473
13540
  }
@@ -13480,100 +13547,39 @@ async function runArchive(ctx) {
13480
13547
  };
13481
13548
  }
13482
13549
 
13483
- // src/evaluate.ts
13484
- import path27 from "path";
13485
- function livingRequirementsMap(specs) {
13486
- const map = /* @__PURE__ */ new Map();
13487
- for (const spec of specs) {
13488
- for (const req of spec.spec.requirements) map.set(req.id, req);
13489
- }
13490
- return map;
13491
- }
13492
- async function evaluateChange(workspace, config, change, approvals) {
13493
- const deltaPath = path27.join(change.dir, "spec.md");
13494
- const deltaContent = await readTextIfExists(deltaPath);
13495
- const approval = verifyApproval(change, approvals, config, deltaContent ?? void 0);
13496
- const living = livingRequirementsMap(workspace.specs);
13497
- const lintFindings = change.delta ? lintDelta(change.delta, living, deltaPath, { language: config.spec.language }) : [];
13498
- const trace = checkTrace({
13499
- specs: workspace.specs,
13500
- change,
13501
- requireEvidence: config.gates.verify.mode !== "off" && config.gates.verify.require_evidence
13502
- });
13503
- const blocking = lintFindings.filter((d) => d.severity === "error").length + trace.findings.filter((d) => d.severity === "error").length;
13504
- const state = deriveState({ change, cfg: config, approval, blockingFindings: change.delta ? blocking : 0 });
13505
- return { change, approval, lintFindings, trace, blocking, state };
13506
- }
13507
-
13508
13550
  // src/commands/ci.ts
13509
- var UI_DOMAINS2 = /* @__PURE__ */ new Set(["frontend", "mobile", "fullstack"]);
13510
13551
  async function runCi(ctx) {
13511
- const { root, workspace, config, approvals } = await requireWorkspace(ctx);
13552
+ const { root, config } = await requireWorkspace(ctx);
13512
13553
  const strict = flagBool(ctx.flags, "strict");
13513
- const diagnostics = [];
13514
- const checks = [];
13515
- const specFindings = workspace.specs.flatMap((spec) => lintSpec(spec.spec, { language: config.spec.language, businessOnly: config.spec.business_only }));
13516
- diagnostics.push(...specFindings);
13517
- checks.push(count("specs vivas", specFindings));
13518
- let changesErrors = 0;
13519
- let changesWarnings = 0;
13520
- for (const change of workspace.changes) {
13521
- const evaluation = await evaluateChange(workspace, config, change, approvals);
13522
- const changeDiags = [...evaluation.lintFindings, ...evaluation.trace.findings];
13523
- if (change.tasks && change.tasks.counts.total > 0) {
13524
- const plan = planWaves(change.tasks, { maxParallel: config.waves.max_parallel });
13525
- changeDiags.push(...plan.blocks.flatMap((b) => b.diagnostics));
13526
- }
13527
- if (UI_DOMAINS2.has(change.meta?.domain ?? "")) {
13528
- const mockups = await checkMockups(root, change.slug, change);
13529
- changeDiags.push(...mockups.findings);
13530
- }
13531
- if (config.packs.length > 0) {
13532
- const resolved = await resolvePacks(workspace.sddDir, config);
13533
- const evaluations = evaluatePacks(resolved.packs, change, config);
13534
- changeDiags.push(...packFindings(evaluations, change));
13535
- }
13536
- diagnostics.push(...changeDiags);
13537
- const entry = count(change.slug, changeDiags);
13538
- changesErrors += entry.errors;
13539
- changesWarnings += entry.warnings;
13540
- }
13541
- checks.push({ name: "cambios (lint + trace + waves + mockups)", errors: changesErrors, warnings: changesWarnings });
13542
- const doctor = await runDoctor(root);
13543
- const doctorErrors = doctor.findings.filter((d) => d.severity === "error");
13544
- diagnostics.push(...doctorErrors);
13545
- checks.push({ name: "doctor", errors: doctorErrors.length, warnings: 0 });
13554
+ const extra = [];
13546
13555
  const workflowDir = await resolveWorkflowDir();
13547
13556
  if (workflowDir) {
13548
13557
  const health = await checkAdapters({ root, workflowDir, targets: config.adapters.targets, language: config.project.language });
13549
- if (health.manifest && !health.ok) {
13550
- const finding = {
13551
- code: "ATLAS-ADAPTERS-001",
13552
- severity: "warning",
13553
- message: "Los adaptadores de agente est\xE1n desactualizados respecto a workflow/",
13554
- suggestion: "Ejecuta `satlas adapters` y commitea el resultado"
13555
- };
13556
- diagnostics.push(finding);
13557
- checks.push({ name: "adaptadores", errors: 0, warnings: 1 });
13558
- } else {
13559
- checks.push({ name: "adaptadores", errors: 0, warnings: 0 });
13560
- }
13558
+ extra.push({
13559
+ name: "adaptadores",
13560
+ diagnostics: health.manifest && !health.ok ? [
13561
+ {
13562
+ code: "ATLAS-ADAPTERS-001",
13563
+ severity: "warning",
13564
+ message: "Los adaptadores de agente est\xE1n desactualizados respecto a workflow/",
13565
+ suggestion: "Ejecuta `satlas adapters` y commitea el resultado"
13566
+ }
13567
+ ] : []
13568
+ });
13561
13569
  }
13562
- const errors = diagnostics.filter((d) => d.severity === "error").length;
13563
- const warnings = diagnostics.filter((d) => d.severity === "warning").length;
13564
- const failed = errors > 0 || strict && warnings > 0;
13570
+ const gate = await runCiGate({ root, strict, extra });
13565
13571
  const lines = ["CI de SpecAtlas", ""];
13566
- for (const check of checks) lines.push(` ${check.errors === 0 && check.warnings === 0 ? "OK " : "FALLA"} ${check.name}: ${check.errors} errores, ${check.warnings} avisos`);
13572
+ for (const check of gate.checks) {
13573
+ lines.push(` ${check.errors === 0 && check.warnings === 0 ? "OK " : "FALLA"} ${check.name}: ${check.errors} errores, ${check.warnings} avisos`);
13574
+ }
13567
13575
  lines.push("");
13568
- lines.push(failed ? "Resultado: BLOQUEADO" : "Resultado: OK");
13576
+ lines.push(gate.failed ? "Resultado: BLOQUEADO" : "Resultado: OK");
13569
13577
  if (strict) lines.push("(modo estricto: los avisos tambi\xE9n bloquean)");
13570
- return { exitCode: failed ? 1 : 0, diagnostics, data: { checks, errors, warnings, strict, failed }, text: lines };
13571
- }
13572
- function count(name, diags) {
13573
13578
  return {
13574
- name,
13575
- errors: diags.filter((d) => d.severity === "error").length,
13576
- warnings: diags.filter((d) => d.severity === "warning").length
13579
+ exitCode: gate.failed ? 1 : 0,
13580
+ diagnostics: gate.diagnostics,
13581
+ data: { checks: gate.checks, errors: gate.errors, warnings: gate.warnings, strict, failed: gate.failed },
13582
+ text: lines
13577
13583
  };
13578
13584
  }
13579
13585
 
@@ -14015,6 +14021,31 @@ async function runNew(ctx) {
14015
14021
  };
14016
14022
  }
14017
14023
 
14024
+ // src/evaluate.ts
14025
+ import path34 from "path";
14026
+ function livingRequirementsMap2(specs) {
14027
+ const map = /* @__PURE__ */ new Map();
14028
+ for (const spec of specs) {
14029
+ for (const req of spec.spec.requirements) map.set(req.id, req);
14030
+ }
14031
+ return map;
14032
+ }
14033
+ async function evaluateChange(workspace, config, change, approvals) {
14034
+ const deltaPath = path34.join(change.dir, "spec.md");
14035
+ const deltaContent = await readTextIfExists(deltaPath);
14036
+ const approval = verifyApproval(change, approvals, config, deltaContent ?? void 0);
14037
+ const living = livingRequirementsMap2(workspace.specs);
14038
+ const lintFindings = change.delta ? lintDelta(change.delta, living, deltaPath, { language: config.spec.language }) : [];
14039
+ const trace = checkTrace({
14040
+ specs: workspace.specs,
14041
+ change,
14042
+ requireEvidence: config.gates.verify.mode !== "off" && config.gates.verify.require_evidence
14043
+ });
14044
+ const blocking = lintFindings.filter((d) => d.severity === "error").length + trace.findings.filter((d) => d.severity === "error").length;
14045
+ const state = deriveState({ change, cfg: config, approval, blockingFindings: change.delta ? blocking : 0 });
14046
+ return { change, approval, lintFindings, trace, blocking, state };
14047
+ }
14048
+
14018
14049
  // src/commands/next.ts
14019
14050
  async function runNext(ctx) {
14020
14051
  const { workspace, config, approvals } = await requireWorkspace(ctx);
@@ -14045,7 +14076,7 @@ async function runNext(ctx) {
14045
14076
  }
14046
14077
 
14047
14078
  // src/commands/present.ts
14048
- import path34 from "path";
14079
+ import path35 from "path";
14049
14080
  async function runPresentCommand(ctx) {
14050
14081
  const { root } = await requireWorkspace(ctx);
14051
14082
  const slug = ctx.positionals[0];
@@ -14057,7 +14088,7 @@ async function runPresentCommand(ctx) {
14057
14088
  if (result.path) {
14058
14089
  lines.push("Propuesta generada");
14059
14090
  lines.push("");
14060
- lines.push(` archivo: ${path34.relative(ctx.cwd, result.path)}`);
14091
+ lines.push(` archivo: ${path35.relative(ctx.cwd, result.path)}`);
14061
14092
  if (result.hash) lines.push(` hash de la spec: ${shortHash(result.hash)}`);
14062
14093
  lines.push("");
14063
14094
  lines.push("\xC1brela en el navegador y comp\xE1rtela con el stakeholder.");
@@ -14067,7 +14098,7 @@ async function runPresentCommand(ctx) {
14067
14098
  return {
14068
14099
  exitCode: hasErrors2 ? 1 : 0,
14069
14100
  diagnostics: result.diagnostics,
14070
- data: result.path ? { path: path34.relative(ctx.cwd, result.path), hash: result.hash } : void 0,
14101
+ data: result.path ? { path: path35.relative(ctx.cwd, result.path), hash: result.hash } : void 0,
14071
14102
  text: lines
14072
14103
  };
14073
14104
  }
@@ -14185,7 +14216,7 @@ async function runStatus(ctx) {
14185
14216
  }
14186
14217
 
14187
14218
  // src/commands/trace.ts
14188
- import path35 from "path";
14219
+ import path36 from "path";
14189
14220
  async function runTrace(ctx) {
14190
14221
  const { workspace, config } = await requireWorkspace(ctx);
14191
14222
  const slug = flagString(ctx.flags, "change");
@@ -14205,7 +14236,7 @@ async function runTrace(ctx) {
14205
14236
  data.push({ slug: change.slug, nodes: result.graph.nodes.length, edges: result.graph.edges.length, summary: result.summary, findings: result.findings });
14206
14237
  lines.push(` ${change.slug}: ${result.graph.nodes.length} nodos, ${result.graph.edges.length} aristas, ${errors} errores, ${result.summary.warnings} avisos`);
14207
14238
  for (const finding of result.findings) {
14208
- const location = finding.path ? ` (${path35.relative(ctx.cwd, finding.path)}${finding.line ? `:${finding.line}` : ""})` : "";
14239
+ const location = finding.path ? ` (${path36.relative(ctx.cwd, finding.path)}${finding.line ? `:${finding.line}` : ""})` : "";
14209
14240
  lines.push(` ${finding.severity.toUpperCase()} ${finding.code} \u2014 ${finding.message}${location}`);
14210
14241
  }
14211
14242
  }
@@ -14215,7 +14246,7 @@ async function runTrace(ctx) {
14215
14246
  }
14216
14247
 
14217
14248
  // src/commands/validate.ts
14218
- import path36 from "path";
14249
+ import path37 from "path";
14219
14250
  async function runValidate(ctx) {
14220
14251
  const { workspace, config, approvals } = await requireWorkspace(ctx);
14221
14252
  const slug = flagString(ctx.flags, "change");
@@ -14241,7 +14272,7 @@ async function runValidate(ctx) {
14241
14272
  for (const finding of evaluation.lintFindings) {
14242
14273
  lines.push(` ${finding.severity.toUpperCase()} ${finding.code}${finding.line ? `:${finding.line}` : ""} \u2014 ${finding.message}`);
14243
14274
  }
14244
- lines.push(` archivo: ${path36.relative(ctx.cwd, path36.join(change.dir, "spec.md"))}`);
14275
+ lines.push(` archivo: ${path37.relative(ctx.cwd, path37.join(change.dir, "spec.md"))}`);
14245
14276
  }
14246
14277
  if (targets.length === 0) lines.push("Sin cambios activos.");
14247
14278
  const errors = diagnostics.filter((d) => d.severity === "error").length;
@@ -14256,7 +14287,7 @@ async function runValidate(ctx) {
14256
14287
  }
14257
14288
 
14258
14289
  // src/commands/verify.ts
14259
- import path37 from "path";
14290
+ import path38 from "path";
14260
14291
  async function runVerify(ctx) {
14261
14292
  const { root, workspace } = await requireWorkspace(ctx);
14262
14293
  const slug = ctx.positionals[0];
@@ -14299,7 +14330,7 @@ async function runVerify(ctx) {
14299
14330
  return { exitCode: 2, diagnostics: [{ code: "ATLAS-VERIFY-001", severity: "error", message: "Falta --by <nombre> (o define meta.owner)" }] };
14300
14331
  }
14301
14332
  const profilesDir = await resolveProfilesDir();
14302
- const profile = await loadActiveProfile(path37.join(root, ".sdd"), [profilesDir ?? "", path37.join(root, ".sdd", "profiles", "custom")].filter(Boolean));
14333
+ const profile = await loadActiveProfile(path38.join(root, ".sdd"), [profilesDir ?? "", path38.join(root, ".sdd", "profiles", "custom")].filter(Boolean));
14303
14334
  const record = await recordEvidence({
14304
14335
  root,
14305
14336
  slug,
@@ -14321,7 +14352,7 @@ async function runVerify(ctx) {
14321
14352
  if (record.evidence.command) lines.push(` comando: ${record.evidence.command}`);
14322
14353
  lines.push(` resultado: ${record.evidence.result}`);
14323
14354
  if (record.evidence.outputHash) lines.push(` hash: ${record.evidence.outputHash}`);
14324
- lines.push(` archivo: ${path37.relative(ctx.cwd, record.path)}`);
14355
+ lines.push(` archivo: ${path38.relative(ctx.cwd, record.path)}`);
14325
14356
  if (record.stdout) {
14326
14357
  lines.push("");
14327
14358
  lines.push(" salida (\xFAltimas l\xEDneas):");
@@ -14336,7 +14367,7 @@ async function runVerify(ctx) {
14336
14367
  return {
14337
14368
  exitCode: record.exitCode === 0 ? 0 : 1,
14338
14369
  diagnostics: record.diagnostics,
14339
- data: { evidence: record.evidence, path: path37.relative(ctx.cwd, record.path), exitCode: record.exitCode },
14370
+ data: { evidence: record.evidence, path: path38.relative(ctx.cwd, record.path), exitCode: record.exitCode },
14340
14371
  text: lines
14341
14372
  };
14342
14373
  }
@@ -14434,7 +14465,7 @@ async function requireWorkspace(ctx) {
14434
14465
  throw new CliError("ATLAS-WS-001", msg("cli.noWorkspace", ctx.language), 2);
14435
14466
  }
14436
14467
  const { workspace, config } = await loadWorkspace(root);
14437
- const approvals = await loadApprovals(path38.join(root, ".sdd"));
14468
+ const approvals = await loadApprovals(path39.join(root, ".sdd"));
14438
14469
  return { root, workspace, config, approvals: approvals.byArtifact };
14439
14470
  }
14440
14471
  var CliError = class extends Error {
@@ -14501,7 +14532,7 @@ function printResult(command, result, json2, language) {
14501
14532
  }
14502
14533
  const lines = [...result.text ?? []];
14503
14534
  for (const d of result.diagnostics) {
14504
- const location = d.path ? ` ${path38.relative(process.cwd(), d.path)}${d.line ? `:${d.line}` : ""}` : "";
14535
+ const location = d.path ? ` ${path39.relative(process.cwd(), d.path)}${d.line ? `:${d.line}` : ""}` : "";
14505
14536
  lines.push(`${d.severity === "error" ? "ERROR" : d.severity === "warning" ? "AVISO" : "NOTA"} ${d.code}${location} \u2014 ${d.message}`);
14506
14537
  if (d.suggestion) lines.push(` \u21B3 ${d.suggestion}`);
14507
14538
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "specatlas",
3
- "version": "0.1.6",
3
+ "version": "0.1.7",
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.4"
43
+ "@specatlas/core": "0.1.5"
44
44
  },
45
45
  "scripts": {
46
46
  "typecheck": "tsc --noEmit",