specatlas 0.1.24 → 0.1.25
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 +670 -53
- package/package.json +2 -2
package/dist/bin.js
CHANGED
|
@@ -1585,7 +1585,7 @@ var require_core = __commonJS({
|
|
|
1585
1585
|
});
|
|
1586
1586
|
|
|
1587
1587
|
// src/cli.ts
|
|
1588
|
-
import
|
|
1588
|
+
import path41 from "path";
|
|
1589
1589
|
|
|
1590
1590
|
// ../core/dist/index.js
|
|
1591
1591
|
import path from "path";
|
|
@@ -9593,6 +9593,146 @@ function findScenarioHeading(lines, blockLineIndex) {
|
|
|
9593
9593
|
}
|
|
9594
9594
|
return void 0;
|
|
9595
9595
|
}
|
|
9596
|
+
var HEADER_KEYS = /* @__PURE__ */ new Set(["t\xE9rmino", "termino", "term", "definici\xF3n", "definicion", "definition", "sin\xF3nimos", "sinonimos", "synonyms"]);
|
|
9597
|
+
var SEPARATOR_RE = /^:?-{2,}:?$/;
|
|
9598
|
+
function parseGlossary(md, filePath) {
|
|
9599
|
+
const diagnostics = [];
|
|
9600
|
+
const terms = [];
|
|
9601
|
+
const lines = md.replace(/\r\n?/g, "\n").split("\n");
|
|
9602
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
9603
|
+
const raw = lines[i] ?? "";
|
|
9604
|
+
if (!raw.trim().startsWith("|")) continue;
|
|
9605
|
+
const inner = raw.trim().replace(/^\|/, "").replace(/\|$/, "");
|
|
9606
|
+
const cells = inner.split("|").map((c) => c.trim());
|
|
9607
|
+
if (cells.length < 2) {
|
|
9608
|
+
diagnostics.push(diag("LINT-GLO-001", "warning", "Fila del glosario sin columnas suficientes", { path: filePath, line: i + 1 }));
|
|
9609
|
+
continue;
|
|
9610
|
+
}
|
|
9611
|
+
const first = cells[0] ?? "";
|
|
9612
|
+
if (HEADER_KEYS.has(first.toLowerCase()) || SEPARATOR_RE.test(first)) continue;
|
|
9613
|
+
const term = first;
|
|
9614
|
+
const definition = cells[1] ?? "";
|
|
9615
|
+
if (term === "" || definition === "") {
|
|
9616
|
+
diagnostics.push(diag("LINT-GLO-002", "warning", "T\xE9rmino o definici\xF3n vac\xEDos en el glosario", { path: filePath, line: i + 1 }));
|
|
9617
|
+
continue;
|
|
9618
|
+
}
|
|
9619
|
+
const synonyms = (cells[2] ?? "").split(",").map((s) => s.trim()).filter((s) => s.length > 0);
|
|
9620
|
+
terms.push({ term, definition, synonyms });
|
|
9621
|
+
}
|
|
9622
|
+
return { terms, diagnostics };
|
|
9623
|
+
}
|
|
9624
|
+
function normalizePath(p) {
|
|
9625
|
+
return toPosix(p.trim()).replace(/^\.\//, "").replace(/\/+$/, "").toLowerCase();
|
|
9626
|
+
}
|
|
9627
|
+
function fileMatches(query, candidate) {
|
|
9628
|
+
if (query === candidate) return true;
|
|
9629
|
+
return candidate.endsWith(`/${query}`);
|
|
9630
|
+
}
|
|
9631
|
+
function scenarioToReq(workspace) {
|
|
9632
|
+
const map = /* @__PURE__ */ new Map();
|
|
9633
|
+
for (const spec of workspace.specs) {
|
|
9634
|
+
for (const req of spec.spec.requirements) {
|
|
9635
|
+
for (const sc of req.scenarios) map.set(sc.id, req.id);
|
|
9636
|
+
}
|
|
9637
|
+
}
|
|
9638
|
+
for (const change of workspace.changes) {
|
|
9639
|
+
for (const req of [...change.delta?.added ?? [], ...change.delta?.modified ?? []]) {
|
|
9640
|
+
for (const sc of req.scenarios) map.set(sc.id, req.id);
|
|
9641
|
+
}
|
|
9642
|
+
}
|
|
9643
|
+
return map;
|
|
9644
|
+
}
|
|
9645
|
+
function emptyReport(target, kind) {
|
|
9646
|
+
return { target, kind, exists: false, scenarios: [], tasks: [], requirements: [], changes: [], files: [], evidence: [] };
|
|
9647
|
+
}
|
|
9648
|
+
function collect(workspace, report, covers, reqOf) {
|
|
9649
|
+
const requirements = /* @__PURE__ */ new Set();
|
|
9650
|
+
for (const change of workspace.changes) {
|
|
9651
|
+
let changeMatches = false;
|
|
9652
|
+
for (const block of change.tasks?.blocks ?? []) {
|
|
9653
|
+
for (const task of block.tasks) {
|
|
9654
|
+
const hit = task.covers.some((c) => covers.has(c.toUpperCase()));
|
|
9655
|
+
if (!hit) continue;
|
|
9656
|
+
changeMatches = true;
|
|
9657
|
+
report.tasks.push({ id: task.id, change: change.slug, text: task.text, files: task.files, covers: task.covers });
|
|
9658
|
+
for (const c of task.covers) {
|
|
9659
|
+
const upper = c.toUpperCase();
|
|
9660
|
+
if (covers.has(upper)) {
|
|
9661
|
+
const req = reqOf.get(upper) ?? upper;
|
|
9662
|
+
requirements.add(req);
|
|
9663
|
+
}
|
|
9664
|
+
}
|
|
9665
|
+
for (const f of task.files) report.files.push(f);
|
|
9666
|
+
}
|
|
9667
|
+
}
|
|
9668
|
+
if (changeMatches) report.changes.push(change.slug);
|
|
9669
|
+
for (const ev of change.verify?.evidence ?? []) {
|
|
9670
|
+
if (covers.has(ev.scenario.toUpperCase())) {
|
|
9671
|
+
report.evidence.push({ scenario: ev.scenario, result: ev.result, change: change.slug });
|
|
9672
|
+
}
|
|
9673
|
+
}
|
|
9674
|
+
}
|
|
9675
|
+
report.requirements = [...requirements].sort();
|
|
9676
|
+
report.changes = [...new Set(report.changes)].sort();
|
|
9677
|
+
report.files = [...new Set(report.files)].sort();
|
|
9678
|
+
report.evidence.sort((a, b) => a.scenario.localeCompare(b.scenario));
|
|
9679
|
+
}
|
|
9680
|
+
function impactOfRequirement(workspace, reqId) {
|
|
9681
|
+
const id = reqId.toUpperCase();
|
|
9682
|
+
const scenarios = /* @__PURE__ */ new Set();
|
|
9683
|
+
let exists2 = false;
|
|
9684
|
+
for (const spec of workspace.specs) {
|
|
9685
|
+
for (const req of spec.spec.requirements) {
|
|
9686
|
+
if (req.id !== id) continue;
|
|
9687
|
+
exists2 = true;
|
|
9688
|
+
for (const sc of req.scenarios) scenarios.add(sc.id);
|
|
9689
|
+
}
|
|
9690
|
+
}
|
|
9691
|
+
for (const change of workspace.changes) {
|
|
9692
|
+
for (const req of [...change.delta?.added ?? [], ...change.delta?.modified ?? []]) {
|
|
9693
|
+
if (req.id !== id) continue;
|
|
9694
|
+
exists2 = true;
|
|
9695
|
+
for (const sc of req.scenarios) scenarios.add(sc.id);
|
|
9696
|
+
}
|
|
9697
|
+
if ((change.delta?.removed ?? []).some((r) => r.id === id)) exists2 = true;
|
|
9698
|
+
if ((change.delta?.renamed ?? []).some((r) => r.from.id === id || r.to.id === id)) exists2 = true;
|
|
9699
|
+
}
|
|
9700
|
+
const report = emptyReport(id, "requirement");
|
|
9701
|
+
if (!exists2) return report;
|
|
9702
|
+
report.exists = true;
|
|
9703
|
+
report.scenarios = [...scenarios].sort();
|
|
9704
|
+
const reqOf = scenarioToReq(workspace);
|
|
9705
|
+
collect(workspace, report, /* @__PURE__ */ new Set([id, ...scenarios]), reqOf);
|
|
9706
|
+
if (!report.requirements.includes(id)) report.requirements.unshift(id);
|
|
9707
|
+
return report;
|
|
9708
|
+
}
|
|
9709
|
+
function impactOfFile(workspace, file) {
|
|
9710
|
+
const query = normalizePath(file);
|
|
9711
|
+
const report = emptyReport(file, "file");
|
|
9712
|
+
const covers = /* @__PURE__ */ new Set();
|
|
9713
|
+
const matched = /* @__PURE__ */ new Set();
|
|
9714
|
+
const changes = /* @__PURE__ */ new Set();
|
|
9715
|
+
for (const change of workspace.changes) {
|
|
9716
|
+
for (const block of change.tasks?.blocks ?? []) {
|
|
9717
|
+
for (const task of block.tasks) {
|
|
9718
|
+
if (!task.files.some((f) => fileMatches(query, normalizePath(f)))) continue;
|
|
9719
|
+
changes.add(change.slug);
|
|
9720
|
+
for (const f of task.files) matched.add(f);
|
|
9721
|
+
for (const c of task.covers) covers.add(c.toUpperCase());
|
|
9722
|
+
report.tasks.push({ id: task.id, change: change.slug, text: task.text, files: task.files, covers: task.covers });
|
|
9723
|
+
}
|
|
9724
|
+
}
|
|
9725
|
+
}
|
|
9726
|
+
if (matched.size === 0) return report;
|
|
9727
|
+
report.exists = true;
|
|
9728
|
+
report.files = [...matched].sort();
|
|
9729
|
+
report.changes = [...changes].sort();
|
|
9730
|
+
const reqOf = scenarioToReq(workspace);
|
|
9731
|
+
const requirements = /* @__PURE__ */ new Set();
|
|
9732
|
+
for (const c of covers) requirements.add(reqOf.get(c) ?? c);
|
|
9733
|
+
report.requirements = [...requirements].sort();
|
|
9734
|
+
return report;
|
|
9735
|
+
}
|
|
9596
9736
|
var VAGUE_ES = ["r\xE1pido", "r\xE1pida", "r\xE1pidos", "r\xE1pidas", "f\xE1cil", "f\xE1ciles", "varios", "varias", "\xF3ptimo", "\xF3ptima", "robusto", "robusta", "adecuado", "adecuada", "eficiente", "amigable", "moderno", "moderna", "mejor", "mejores", "simple", "sencillo", "intuitivo", "intuitiva", "apropiado", "apropiada", "suficiente", "razonable"];
|
|
9597
9737
|
var VAGUE_EN = ["fast", "quick", "easy", "several", "optimal", "robust", "adequate", "efficient", "friendly", "modern", "better", "best", "simple", "intuitive", "appropriate", "sufficient", "reasonable", "nice", "clean"];
|
|
9598
9738
|
var TECH_ES = ["api", "endpoint", "tabla", "base de datos", "sql", "json", "xml", "frontend", "backend", "react", "vue", "angular", "node", "npm", "docker", "kubernetes", "servidor", "columna", "query", "script", "framework", "librer\xEDa", "microservicio", "cache", "cach\xE9", "deploy", "despliegue", "commit", "merge", "rama", "branch", "endpoint", "crud", "rest", "http", "sdk", "webhook", "orm", "job", "cron", "websocket", "graphql", "api rest", "tablas", "\xEDndice", "primary key", "foreign key"];
|
|
@@ -10055,6 +10195,17 @@ function deriveState(input) {
|
|
|
10055
10195
|
}
|
|
10056
10196
|
if (blockingFindings > 0) {
|
|
10057
10197
|
blockedBy.push(`${blockingFindings} hallazgo(s) bloqueante(s)`);
|
|
10198
|
+
if (tasksTotal > 0 && tasksDone < tasksTotal) {
|
|
10199
|
+
return {
|
|
10200
|
+
state: "building",
|
|
10201
|
+
blockedBy,
|
|
10202
|
+
nextAction: next(`/satlas.build ${change.slug}`, `Construir en olas (${tasksDone}/${tasksTotal} tareas) \xB7 ${blockingFindings} hallazgo(s) pendientes`, true),
|
|
10203
|
+
progress
|
|
10204
|
+
};
|
|
10205
|
+
}
|
|
10206
|
+
if (tasksTotal > 0) {
|
|
10207
|
+
return { state: "built", blockedBy, nextAction: next(`satlas verify ${change.slug}`, "Registrar evidencia por escenario"), progress };
|
|
10208
|
+
}
|
|
10058
10209
|
return { state: "spec_draft", blockedBy, nextAction: next(`satlas validate --change ${change.slug}`, "Corregir los hallazgos de la especificaci\xF3n"), progress };
|
|
10059
10210
|
}
|
|
10060
10211
|
if ((approval.status === "missing" || approval.status === "stale") && requiresMockups(change.meta, cfg) && input.mockupsReady !== true && !mockupOverride(change)) {
|
|
@@ -13029,6 +13180,7 @@ var CATALOG = [
|
|
|
13029
13180
|
{ name: "mockup", description: "Planifica, valida o captura los mockups del cambio", flags: ["plan", "check", "capture", "require", "json"], usage: "satlas mockup <slug> [--plan|--check|--capture|--require]" },
|
|
13030
13181
|
{ name: "present", description: "Genera el paquete de propuesta para el stakeholder (HTML autocontenido)", flags: ["json"], usage: "satlas present <slug>" },
|
|
13031
13182
|
{ name: "ci", description: "Gate de pipeline: specs + cambios + doctor + adaptadores (sin agente)", flags: ["strict", "json"], usage: "satlas ci [--strict]" },
|
|
13183
|
+
{ name: "mcp", description: "V\xEDa de consulta de solo lectura para asistentes (protocolo MCP sobre entrada/salida est\xE1ndar)", flags: [], usage: "satlas mcp" },
|
|
13032
13184
|
{ name: "metrics", description: "M\xE9tricas locales del workspace (progreso, WIP, throughput, evidencia)", flags: ["json"], usage: "satlas metrics [--json]" },
|
|
13033
13185
|
{ name: "run", description: "Persiste runs y eventos de ejecuci\xF3n (auditor\xEDa y reanudaci\xF3n)", flags: ["phase", "inputs", "data", "slug", "status", "json"], usage: "satlas run start|event|status|show|list" },
|
|
13034
13186
|
{ name: "hash", description: "Calcula el sha256 de un archivo o texto (para evidencia)", flags: ["text", "json"], usage: 'satlas hash <archivo> | satlas hash --text "salida"' },
|
|
@@ -14206,8 +14358,504 @@ async function runMockup(ctx) {
|
|
|
14206
14358
|
return { exitCode: 0, diagnostics: [], data: { plan, planFile: path31.relative(ctx.cwd, planFile), manifestFile: path31.relative(ctx.cwd, manifestFile) }, text: lines };
|
|
14207
14359
|
}
|
|
14208
14360
|
|
|
14209
|
-
// src/
|
|
14361
|
+
// src/mcp/server.ts
|
|
14362
|
+
import { promises as fs2 } from "fs";
|
|
14363
|
+
import path34 from "path";
|
|
14364
|
+
import { createInterface } from "readline/promises";
|
|
14365
|
+
|
|
14366
|
+
// src/mcp/protocol.ts
|
|
14367
|
+
var JSONRPC_VERSION = "2.0";
|
|
14368
|
+
var PARSE_ERROR = -32700;
|
|
14369
|
+
var INVALID_REQUEST = -32600;
|
|
14370
|
+
var METHOD_NOT_FOUND = -32601;
|
|
14371
|
+
var INTERNAL_ERROR = -32603;
|
|
14372
|
+
function isObject(value) {
|
|
14373
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
14374
|
+
}
|
|
14375
|
+
function makeResult(id, result) {
|
|
14376
|
+
return { jsonrpc: JSONRPC_VERSION, id, result };
|
|
14377
|
+
}
|
|
14378
|
+
function makeError(id, code, message, data) {
|
|
14379
|
+
const error = { code, message };
|
|
14380
|
+
if (data !== void 0) error.data = data;
|
|
14381
|
+
return { jsonrpc: JSONRPC_VERSION, id, error };
|
|
14382
|
+
}
|
|
14383
|
+
function parseMessage(line) {
|
|
14384
|
+
let value;
|
|
14385
|
+
try {
|
|
14386
|
+
value = JSON.parse(line);
|
|
14387
|
+
} catch {
|
|
14388
|
+
return { error: { code: PARSE_ERROR, message: "El mensaje no es JSON v\xE1lido" } };
|
|
14389
|
+
}
|
|
14390
|
+
if (!isObject(value) || value["jsonrpc"] !== JSONRPC_VERSION) {
|
|
14391
|
+
return { error: { code: INVALID_REQUEST, message: "Mensaje no es una petici\xF3n JSON-RPC 2.0 v\xE1lida" } };
|
|
14392
|
+
}
|
|
14393
|
+
if (typeof value["method"] !== "string") {
|
|
14394
|
+
return { error: { code: INVALID_REQUEST, message: "El mensaje no declara un m\xE9todo" } };
|
|
14395
|
+
}
|
|
14396
|
+
const idValue = value["id"] ?? null;
|
|
14397
|
+
const id = typeof idValue === "number" || typeof idValue === "string" || idValue === null ? idValue : null;
|
|
14398
|
+
const request = { jsonrpc: JSONRPC_VERSION, id, method: value["method"] };
|
|
14399
|
+
if (value["params"] !== void 0) request.params = value["params"];
|
|
14400
|
+
return { request };
|
|
14401
|
+
}
|
|
14402
|
+
function jsonResult(data) {
|
|
14403
|
+
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
|
14404
|
+
}
|
|
14405
|
+
function errorResult(code, message, extra) {
|
|
14406
|
+
return {
|
|
14407
|
+
isError: true,
|
|
14408
|
+
content: [{ type: "text", text: JSON.stringify({ error: code, message, ...extra }, null, 2) }]
|
|
14409
|
+
};
|
|
14410
|
+
}
|
|
14411
|
+
function noWorkspaceResult() {
|
|
14412
|
+
return errorResult(
|
|
14413
|
+
"ATLAS-MCP-WS-001",
|
|
14414
|
+
"El proyecto no est\xE1 inicializado: no se encontr\xF3 el estado del proyecto en esta carpeta.",
|
|
14415
|
+
{ action: "satlas init" }
|
|
14416
|
+
);
|
|
14417
|
+
}
|
|
14418
|
+
function stringArg(args, key) {
|
|
14419
|
+
if (!isObject(args)) return void 0;
|
|
14420
|
+
const value = args[key];
|
|
14421
|
+
return typeof value === "string" && value.trim() !== "" ? value.trim() : void 0;
|
|
14422
|
+
}
|
|
14423
|
+
|
|
14424
|
+
// src/mcp/tools/glossary.ts
|
|
14210
14425
|
import path32 from "path";
|
|
14426
|
+
async function runAtlasGlossary(_args, host) {
|
|
14427
|
+
const resolved = await host.getWorkspace();
|
|
14428
|
+
if (!resolved) return noWorkspaceResult();
|
|
14429
|
+
const { root, config } = resolved;
|
|
14430
|
+
const glossaryPath = path32.resolve(root, config.spec.glossary);
|
|
14431
|
+
const raw = await readTextIfExists(glossaryPath);
|
|
14432
|
+
if (raw === void 0) {
|
|
14433
|
+
return jsonResult({
|
|
14434
|
+
terms: [],
|
|
14435
|
+
message: "El glosario no tiene t\xE9rminos definidos.",
|
|
14436
|
+
action: "Define los t\xE9rminos del negocio en el glosario del proyecto."
|
|
14437
|
+
});
|
|
14438
|
+
}
|
|
14439
|
+
const parsed = parseGlossary(raw, glossaryPath);
|
|
14440
|
+
if (parsed.terms.length === 0) {
|
|
14441
|
+
return jsonResult({
|
|
14442
|
+
terms: [],
|
|
14443
|
+
message: "El glosario no tiene t\xE9rminos definidos.",
|
|
14444
|
+
action: "Define los t\xE9rminos del negocio en el glosario del proyecto."
|
|
14445
|
+
});
|
|
14446
|
+
}
|
|
14447
|
+
return jsonResult({ terms: parsed.terms });
|
|
14448
|
+
}
|
|
14449
|
+
|
|
14450
|
+
// src/mcp/tools/impact.ts
|
|
14451
|
+
var REQ_TARGET_RE = /^REQ-[A-Z0-9]+(?:-[A-Z0-9]+)*-\d{3}(?:-S\d+)?$/i;
|
|
14452
|
+
async function runAtlasImpact(args, host) {
|
|
14453
|
+
const target = stringArg(args, "target");
|
|
14454
|
+
if (!target) {
|
|
14455
|
+
return errorResult("ATLAS-MCP-IMPACT-001", "Falta el requisito o el archivo a consultar", {
|
|
14456
|
+
action: "atlas_impact con el argumento target (REQ-\u2026 o ruta de archivo)"
|
|
14457
|
+
});
|
|
14458
|
+
}
|
|
14459
|
+
const resolved = await host.getWorkspace();
|
|
14460
|
+
if (!resolved) return noWorkspaceResult();
|
|
14461
|
+
const { workspace } = resolved;
|
|
14462
|
+
let report;
|
|
14463
|
+
if (REQ_TARGET_RE.test(target)) {
|
|
14464
|
+
const reqId = target.toUpperCase().replace(/-S\d+$/, "");
|
|
14465
|
+
report = impactOfRequirement(workspace, reqId);
|
|
14466
|
+
} else {
|
|
14467
|
+
report = impactOfFile(workspace, target);
|
|
14468
|
+
}
|
|
14469
|
+
if (!report.exists) {
|
|
14470
|
+
return jsonResult({ target, exists: false, message: `Sin relaciones registradas para "${target}".` });
|
|
14471
|
+
}
|
|
14472
|
+
return jsonResult(report);
|
|
14473
|
+
}
|
|
14474
|
+
|
|
14475
|
+
// src/evaluate.ts
|
|
14476
|
+
import path33 from "path";
|
|
14477
|
+
function livingRequirementsMap2(specs) {
|
|
14478
|
+
const map = /* @__PURE__ */ new Map();
|
|
14479
|
+
for (const spec of specs) {
|
|
14480
|
+
for (const req of spec.spec.requirements) map.set(req.id, req);
|
|
14481
|
+
}
|
|
14482
|
+
return map;
|
|
14483
|
+
}
|
|
14484
|
+
async function evaluateChange(workspace, config, change, approvals) {
|
|
14485
|
+
const deltaPath = path33.join(change.dir, "spec.md");
|
|
14486
|
+
const deltaContent = await readTextIfExists(deltaPath);
|
|
14487
|
+
const approval = verifyApproval(change, approvals, config, deltaContent ?? void 0);
|
|
14488
|
+
const living = livingRequirementsMap2(workspace.specs);
|
|
14489
|
+
const lintFindings = change.delta ? lintDelta(change.delta, living, deltaPath, { language: config.spec.language }) : [];
|
|
14490
|
+
const trace = checkTrace({
|
|
14491
|
+
specs: workspace.specs,
|
|
14492
|
+
change,
|
|
14493
|
+
requireEvidence: config.gates.verify.mode !== "off" && config.gates.verify.require_evidence
|
|
14494
|
+
});
|
|
14495
|
+
const blocking = lintFindings.filter((d) => d.severity === "error").length + trace.findings.filter((d) => d.severity === "error").length;
|
|
14496
|
+
const mockupsAreReady = requiresMockups(change.meta, config) ? await mockupsReady(workspace.root, change.slug, change) : void 0;
|
|
14497
|
+
const state = deriveState({
|
|
14498
|
+
change,
|
|
14499
|
+
cfg: config,
|
|
14500
|
+
approval,
|
|
14501
|
+
blockingFindings: change.delta ? blocking : 0,
|
|
14502
|
+
...mockupsAreReady !== void 0 ? { mockupsReady: mockupsAreReady } : {}
|
|
14503
|
+
});
|
|
14504
|
+
return { change, approval, lintFindings, trace, blocking, state };
|
|
14505
|
+
}
|
|
14506
|
+
|
|
14507
|
+
// src/mcp/tools/next.ts
|
|
14508
|
+
var HUMAN_COMMANDS = [/^satlas approve/, /^satlas archive/, /^satlas resume/, /^satlas present/];
|
|
14509
|
+
function requiresPerson(nextCommand, state) {
|
|
14510
|
+
if (state === "awaiting_approval" || state === "ready") return true;
|
|
14511
|
+
return HUMAN_COMMANDS.some((re) => re.test(nextCommand));
|
|
14512
|
+
}
|
|
14513
|
+
async function runAtlasNext(args, host) {
|
|
14514
|
+
const slug = stringArg(args, "slug");
|
|
14515
|
+
if (!slug) {
|
|
14516
|
+
return errorResult("ATLAS-MCP-NEXT-001", "Falta el nombre del cambio", { action: "atlas_next con el argumento slug" });
|
|
14517
|
+
}
|
|
14518
|
+
const resolved = await host.getWorkspace();
|
|
14519
|
+
if (!resolved) return noWorkspaceResult();
|
|
14520
|
+
const { workspace, config, approvals } = resolved;
|
|
14521
|
+
const change = workspace.changes.find((c) => c.slug === slug);
|
|
14522
|
+
if (!change) {
|
|
14523
|
+
return errorResult("ATLAS-MCP-NEXT-002", `El cambio "${slug}" no existe`, {
|
|
14524
|
+
available: workspace.changes.map((c) => c.slug)
|
|
14525
|
+
});
|
|
14526
|
+
}
|
|
14527
|
+
const evaluation = await evaluateChange(workspace, config, change, approvals);
|
|
14528
|
+
const { state, nextAction, blockedBy } = evaluation.state;
|
|
14529
|
+
const humanRequired = requiresPerson(nextAction.command, state);
|
|
14530
|
+
return jsonResult({
|
|
14531
|
+
slug,
|
|
14532
|
+
state,
|
|
14533
|
+
label: stateLabel(state),
|
|
14534
|
+
next: { command: nextAction.command, description: nextAction.description, requiresAgent: nextAction.requiresAgent },
|
|
14535
|
+
requiresPerson: humanRequired,
|
|
14536
|
+
blockedBy
|
|
14537
|
+
});
|
|
14538
|
+
}
|
|
14539
|
+
|
|
14540
|
+
// src/mcp/tools/status.ts
|
|
14541
|
+
async function runAtlasStatus(args, host) {
|
|
14542
|
+
const resolved = await host.getWorkspace();
|
|
14543
|
+
if (!resolved) return noWorkspaceResult();
|
|
14544
|
+
const { workspace, config, approvals } = resolved;
|
|
14545
|
+
const slug = stringArg(args, "slug");
|
|
14546
|
+
if (slug && !workspace.changes.some((c) => c.slug === slug)) {
|
|
14547
|
+
return errorResult("ATLAS-MCP-STATUS-001", `El cambio "${slug}" no existe`, {
|
|
14548
|
+
available: workspace.changes.map((c) => c.slug)
|
|
14549
|
+
});
|
|
14550
|
+
}
|
|
14551
|
+
const changes = [];
|
|
14552
|
+
for (const change of workspace.changes) {
|
|
14553
|
+
if (slug && change.slug !== slug) continue;
|
|
14554
|
+
const evaluation = await evaluateChange(workspace, config, change, approvals);
|
|
14555
|
+
const { state, progress, nextAction, blockedBy } = evaluation.state;
|
|
14556
|
+
changes.push({
|
|
14557
|
+
slug: change.slug,
|
|
14558
|
+
lane: change.meta?.lane ?? config.lanes.default,
|
|
14559
|
+
domain: change.meta?.domain,
|
|
14560
|
+
state,
|
|
14561
|
+
label: stateLabel(state),
|
|
14562
|
+
progress,
|
|
14563
|
+
blocking: evaluation.blocking,
|
|
14564
|
+
blockedBy,
|
|
14565
|
+
next: { command: nextAction.command, description: nextAction.description, requiresAgent: nextAction.requiresAgent }
|
|
14566
|
+
});
|
|
14567
|
+
}
|
|
14568
|
+
if (changes.length === 0) {
|
|
14569
|
+
return jsonResult({
|
|
14570
|
+
changes: [],
|
|
14571
|
+
specs: workspace.specs.map((s) => ({ domain: s.domain, requirements: s.spec.requirements.length })),
|
|
14572
|
+
message: "Sin cambios activos.",
|
|
14573
|
+
action: "satlas new <slug>"
|
|
14574
|
+
});
|
|
14575
|
+
}
|
|
14576
|
+
return jsonResult({
|
|
14577
|
+
changes,
|
|
14578
|
+
specs: workspace.specs.map((s) => ({ domain: s.domain, requirements: s.spec.requirements.length }))
|
|
14579
|
+
});
|
|
14580
|
+
}
|
|
14581
|
+
|
|
14582
|
+
// src/mcp/tools/trace.ts
|
|
14583
|
+
async function runAtlasTrace(args, host) {
|
|
14584
|
+
const resolved = await host.getWorkspace();
|
|
14585
|
+
if (!resolved) return noWorkspaceResult();
|
|
14586
|
+
const { workspace } = resolved;
|
|
14587
|
+
const slug = stringArg(args, "slug");
|
|
14588
|
+
const changes = slug ? workspace.changes.filter((c) => c.slug === slug) : workspace.changes;
|
|
14589
|
+
const result = [];
|
|
14590
|
+
for (const change of changes) {
|
|
14591
|
+
const trace = checkTrace({ specs: workspace.specs, change, requireEvidence: false });
|
|
14592
|
+
const coversByTask = /* @__PURE__ */ new Map();
|
|
14593
|
+
for (const block of change.tasks?.blocks ?? []) {
|
|
14594
|
+
for (const task of block.tasks) coversByTask.set(task.id, task.covers.map((c) => c.toUpperCase()));
|
|
14595
|
+
}
|
|
14596
|
+
const evidenceByScenario = /* @__PURE__ */ new Map();
|
|
14597
|
+
for (const ev of change.verify?.evidence ?? []) {
|
|
14598
|
+
const known = evidenceByScenario.get(ev.scenario);
|
|
14599
|
+
if (known === void 0 || ev.result === "pass") evidenceByScenario.set(ev.scenario, ev.result);
|
|
14600
|
+
}
|
|
14601
|
+
const scenarios = [];
|
|
14602
|
+
for (const req of [...change.delta?.added ?? [], ...change.delta?.modified ?? []]) {
|
|
14603
|
+
for (const sc of req.scenarios) {
|
|
14604
|
+
const parent = sc.reqId.toUpperCase();
|
|
14605
|
+
const coveredBy = [...coversByTask.entries()].filter(([, covers]) => covers.includes(sc.id.toUpperCase()) || covers.includes(parent)).map(([taskId]) => taskId);
|
|
14606
|
+
const evidence = evidenceByScenario.get(sc.id) ?? null;
|
|
14607
|
+
scenarios.push({ id: sc.id, title: sc.title, coveredBy, evidence });
|
|
14608
|
+
}
|
|
14609
|
+
}
|
|
14610
|
+
const gaps = trace.findings.filter((f) => f.code === "TRACE-002").map((f) => String(f.target ?? f.message));
|
|
14611
|
+
result.push({ slug: change.slug, scenarios, gaps, summary: { errors: trace.summary.errors, warnings: trace.summary.warnings } });
|
|
14612
|
+
}
|
|
14613
|
+
if (changes.length === 0) {
|
|
14614
|
+
return jsonResult({ changes: [], message: "Sin cambios activos.", action: "satlas new <slug>" });
|
|
14615
|
+
}
|
|
14616
|
+
return jsonResult({ changes: result });
|
|
14617
|
+
}
|
|
14618
|
+
|
|
14619
|
+
// src/mcp/tools/validate.ts
|
|
14620
|
+
async function runAtlasValidate(args, host) {
|
|
14621
|
+
const slug = stringArg(args, "slug");
|
|
14622
|
+
if (!slug) {
|
|
14623
|
+
return errorResult("ATLAS-MCP-VALIDATE-001", "Falta el nombre del cambio", { action: "atlas_validate con el argumento slug" });
|
|
14624
|
+
}
|
|
14625
|
+
const resolved = await host.getWorkspace();
|
|
14626
|
+
if (!resolved) return noWorkspaceResult();
|
|
14627
|
+
const { workspace, config, approvals } = resolved;
|
|
14628
|
+
const change = workspace.changes.find((c) => c.slug === slug);
|
|
14629
|
+
if (!change) {
|
|
14630
|
+
return errorResult("ATLAS-MCP-VALIDATE-002", `El cambio "${slug}" no existe`, {
|
|
14631
|
+
available: workspace.changes.map((c) => c.slug)
|
|
14632
|
+
});
|
|
14633
|
+
}
|
|
14634
|
+
const evaluation = await evaluateChange(workspace, config, change, approvals);
|
|
14635
|
+
const findings = [...evaluation.lintFindings, ...evaluation.trace.findings].map((d) => ({
|
|
14636
|
+
code: d.code,
|
|
14637
|
+
severity: d.severity,
|
|
14638
|
+
message: d.message,
|
|
14639
|
+
path: d.path,
|
|
14640
|
+
line: d.line,
|
|
14641
|
+
suggestion: d.suggestion
|
|
14642
|
+
}));
|
|
14643
|
+
if (findings.length === 0) {
|
|
14644
|
+
return jsonResult({ slug, conforming: true, message: "Sin hallazgos: el cambio est\xE1 conforme.", findings: [] });
|
|
14645
|
+
}
|
|
14646
|
+
return jsonResult({
|
|
14647
|
+
slug,
|
|
14648
|
+
conforming: findings.every((f) => f.severity !== "error"),
|
|
14649
|
+
findings,
|
|
14650
|
+
summary: evaluation.trace.summary
|
|
14651
|
+
});
|
|
14652
|
+
}
|
|
14653
|
+
|
|
14654
|
+
// src/mcp/tools/index.ts
|
|
14655
|
+
var HANDLERS = {
|
|
14656
|
+
atlas_status: runAtlasStatus,
|
|
14657
|
+
atlas_next: runAtlasNext,
|
|
14658
|
+
atlas_validate: runAtlasValidate,
|
|
14659
|
+
atlas_trace: runAtlasTrace,
|
|
14660
|
+
atlas_impact: runAtlasImpact,
|
|
14661
|
+
atlas_glossary: runAtlasGlossary
|
|
14662
|
+
};
|
|
14663
|
+
async function runToolByName(name, args, host) {
|
|
14664
|
+
const handler = HANDLERS[name];
|
|
14665
|
+
if (!handler) {
|
|
14666
|
+
return errorResult("ATLAS-MCP-TOOL-002", `Operaci\xF3n desconocida: ${name}`, { available: Object.keys(HANDLERS).sort() });
|
|
14667
|
+
}
|
|
14668
|
+
return handler(args, host);
|
|
14669
|
+
}
|
|
14670
|
+
|
|
14671
|
+
// src/mcp/tools/list.ts
|
|
14672
|
+
function listTools() {
|
|
14673
|
+
return [
|
|
14674
|
+
{
|
|
14675
|
+
name: "atlas_status",
|
|
14676
|
+
description: "Estado del proyecto: cambios activos con fase, avance de tareas y evidencia, bloqueos y siguiente paso. Opcional: nombre de un cambio.",
|
|
14677
|
+
inputSchema: {
|
|
14678
|
+
type: "object",
|
|
14679
|
+
properties: { slug: { type: "string", description: "Nombre del cambio (opcional): si se indica, solo se informa de ese cambio" } },
|
|
14680
|
+
additionalProperties: false
|
|
14681
|
+
}
|
|
14682
|
+
},
|
|
14683
|
+
{
|
|
14684
|
+
name: "atlas_next",
|
|
14685
|
+
description: "Siguiente acci\xF3n recomendada para un cambio, con la indicaci\xF3n de si requiere una persona o la puede realizar el asistente.",
|
|
14686
|
+
inputSchema: {
|
|
14687
|
+
type: "object",
|
|
14688
|
+
properties: { slug: { type: "string", description: "Nombre del cambio" } },
|
|
14689
|
+
required: ["slug"],
|
|
14690
|
+
additionalProperties: false
|
|
14691
|
+
}
|
|
14692
|
+
},
|
|
14693
|
+
{
|
|
14694
|
+
name: "atlas_validate",
|
|
14695
|
+
description: "Hallazgos vigentes de un cambio (validaci\xF3n y trazabilidad), id\xE9nticos a los que reporta la herramienta. Nunca modifica nada.",
|
|
14696
|
+
inputSchema: {
|
|
14697
|
+
type: "object",
|
|
14698
|
+
properties: { slug: { type: "string", description: "Nombre del cambio" } },
|
|
14699
|
+
required: ["slug"],
|
|
14700
|
+
additionalProperties: false
|
|
14701
|
+
}
|
|
14702
|
+
},
|
|
14703
|
+
{
|
|
14704
|
+
name: "atlas_trace",
|
|
14705
|
+
description: "Cobertura de trazabilidad: por escenario, qu\xE9 tarea lo cubre y su evidencia, y los huecos vigentes.",
|
|
14706
|
+
inputSchema: {
|
|
14707
|
+
type: "object",
|
|
14708
|
+
properties: { slug: { type: "string", description: "Nombre del cambio (opcional)" } },
|
|
14709
|
+
additionalProperties: false
|
|
14710
|
+
}
|
|
14711
|
+
},
|
|
14712
|
+
{
|
|
14713
|
+
name: "atlas_impact",
|
|
14714
|
+
description: "Impacto registrado de un requisito (REQ-\u2026) o de un archivo: escenarios, tareas, cambios, archivos y evidencia relacionados. Solo relaciones registradas.",
|
|
14715
|
+
inputSchema: {
|
|
14716
|
+
type: "object",
|
|
14717
|
+
properties: { target: { type: "string", description: "Id de requisito (por ejemplo REQ-AUTH-001) o ruta de archivo" } },
|
|
14718
|
+
required: ["target"],
|
|
14719
|
+
additionalProperties: false
|
|
14720
|
+
}
|
|
14721
|
+
},
|
|
14722
|
+
{
|
|
14723
|
+
name: "atlas_glossary",
|
|
14724
|
+
description: "T\xE9rminos del glosario del negocio con su definici\xF3n vigente y sin\xF3nimos aceptados. No inventa definiciones.",
|
|
14725
|
+
inputSchema: {
|
|
14726
|
+
type: "object",
|
|
14727
|
+
properties: {},
|
|
14728
|
+
additionalProperties: false
|
|
14729
|
+
}
|
|
14730
|
+
}
|
|
14731
|
+
];
|
|
14732
|
+
}
|
|
14733
|
+
|
|
14734
|
+
// src/mcp/server.ts
|
|
14735
|
+
var PROTOCOL_VERSION = "2024-11-05";
|
|
14736
|
+
var SERVER_NAME = "specatlas";
|
|
14737
|
+
var WorkspaceCache = class {
|
|
14738
|
+
entries = /* @__PURE__ */ new Map();
|
|
14739
|
+
get(root) {
|
|
14740
|
+
return this.entries.get(root);
|
|
14741
|
+
}
|
|
14742
|
+
set(root, mtimeMs, value) {
|
|
14743
|
+
this.entries.set(root, { mtimeMs, value });
|
|
14744
|
+
}
|
|
14745
|
+
};
|
|
14746
|
+
async function workspaceStamp(root) {
|
|
14747
|
+
const sddDir = path34.join(root, ".sdd");
|
|
14748
|
+
const markers = [sddDir, path34.join(sddDir, "config.yaml"), path34.join(sddDir, "changes"), path34.join(sddDir, "specs"), path34.join(sddDir, "approvals.yaml")];
|
|
14749
|
+
let stamp = 0;
|
|
14750
|
+
for (const marker of markers) {
|
|
14751
|
+
try {
|
|
14752
|
+
const st = await fs2.stat(marker);
|
|
14753
|
+
stamp = Math.max(stamp, st.mtimeMs);
|
|
14754
|
+
} catch {
|
|
14755
|
+
}
|
|
14756
|
+
}
|
|
14757
|
+
return stamp;
|
|
14758
|
+
}
|
|
14759
|
+
async function resolveMcpWorkspace(cwd, cache) {
|
|
14760
|
+
const root = await findWorkspaceRoot(cwd);
|
|
14761
|
+
if (!root) return void 0;
|
|
14762
|
+
const stamp = await workspaceStamp(root);
|
|
14763
|
+
const cached2 = cache.get(root);
|
|
14764
|
+
if (cached2 && cached2.mtimeMs === stamp) return cached2.value;
|
|
14765
|
+
const { workspace, config } = await loadWorkspace(root);
|
|
14766
|
+
const approvals = await loadApprovals(path34.join(root, ".sdd"));
|
|
14767
|
+
const value = { root, workspace, config, approvals: approvals.byArtifact };
|
|
14768
|
+
cache.set(root, stamp, value);
|
|
14769
|
+
return value;
|
|
14770
|
+
}
|
|
14771
|
+
function createHost(cwd, cache) {
|
|
14772
|
+
return {
|
|
14773
|
+
cwd,
|
|
14774
|
+
language: "es",
|
|
14775
|
+
getWorkspace: () => resolveMcpWorkspace(cwd, cache)
|
|
14776
|
+
};
|
|
14777
|
+
}
|
|
14778
|
+
async function handleRequest(req, host) {
|
|
14779
|
+
const id = req.id;
|
|
14780
|
+
if (req.method === "initialize") {
|
|
14781
|
+
return makeResult(id, {
|
|
14782
|
+
protocolVersion: PROTOCOL_VERSION,
|
|
14783
|
+
capabilities: { tools: { listChanged: false } },
|
|
14784
|
+
serverInfo: { name: SERVER_NAME, version: cliVersion() }
|
|
14785
|
+
});
|
|
14786
|
+
}
|
|
14787
|
+
if (req.method === "ping") {
|
|
14788
|
+
return makeResult(id, {});
|
|
14789
|
+
}
|
|
14790
|
+
if (req.method === "tools/list") {
|
|
14791
|
+
return makeResult(id, { tools: listTools() });
|
|
14792
|
+
}
|
|
14793
|
+
if (req.method === "tools/call") {
|
|
14794
|
+
const params = isObject(req.params) ? req.params : {};
|
|
14795
|
+
const name = typeof params["name"] === "string" ? params["name"] : "";
|
|
14796
|
+
if (name === "") {
|
|
14797
|
+
return makeResult(id, {
|
|
14798
|
+
content: [{ type: "text", text: JSON.stringify({ error: "ATLAS-MCP-TOOL-001", message: "Falta el nombre de la operaci\xF3n" }, null, 2) }],
|
|
14799
|
+
isError: true
|
|
14800
|
+
});
|
|
14801
|
+
}
|
|
14802
|
+
try {
|
|
14803
|
+
const result = await runToolByName(name, params["arguments"], host);
|
|
14804
|
+
return makeResult(id, result);
|
|
14805
|
+
} catch (err) {
|
|
14806
|
+
return makeError(id, INTERNAL_ERROR, "Error interno al ejecutar la operaci\xF3n", { message: err instanceof Error ? err.message : String(err) });
|
|
14807
|
+
}
|
|
14808
|
+
}
|
|
14809
|
+
if (req.method.startsWith("notifications/")) {
|
|
14810
|
+
return void 0;
|
|
14811
|
+
}
|
|
14812
|
+
return makeError(id, METHOD_NOT_FOUND, `M\xE9todo desconocido: ${req.method}`);
|
|
14813
|
+
}
|
|
14814
|
+
function log(stderr, message) {
|
|
14815
|
+
stderr.write(`[specatlas mcp] ${message}
|
|
14816
|
+
`);
|
|
14817
|
+
}
|
|
14818
|
+
async function runMcpServer(opts) {
|
|
14819
|
+
const { cwd, stdin, stdout, stderr } = opts;
|
|
14820
|
+
const cache = new WorkspaceCache();
|
|
14821
|
+
const host = createHost(cwd, cache);
|
|
14822
|
+
log(stderr, "v\xEDa de consulta iniciada (solo lectura)");
|
|
14823
|
+
const rl = createInterface({ input: stdin, crlfDelay: Infinity });
|
|
14824
|
+
for await (const line of rl) {
|
|
14825
|
+
if (line.trim() === "") continue;
|
|
14826
|
+
const parsed = parseMessage(line);
|
|
14827
|
+
if (parsed.error) {
|
|
14828
|
+
const response = makeError(null, parsed.error.code, parsed.error.message);
|
|
14829
|
+
stdout.write(`${JSON.stringify(response)}
|
|
14830
|
+
`);
|
|
14831
|
+
continue;
|
|
14832
|
+
}
|
|
14833
|
+
const request = parsed.request;
|
|
14834
|
+
try {
|
|
14835
|
+
const response = await handleRequest(request, host);
|
|
14836
|
+
if (response) stdout.write(`${JSON.stringify(response)}
|
|
14837
|
+
`);
|
|
14838
|
+
} catch (err) {
|
|
14839
|
+
log(stderr, err instanceof Error ? err.message : String(err));
|
|
14840
|
+
stdout.write(`${JSON.stringify(makeError(request.id, INTERNAL_ERROR, "Error interno"))}
|
|
14841
|
+
`);
|
|
14842
|
+
}
|
|
14843
|
+
}
|
|
14844
|
+
}
|
|
14845
|
+
|
|
14846
|
+
// src/commands/mcp.ts
|
|
14847
|
+
async function runMcp(ctx) {
|
|
14848
|
+
await runMcpServer({
|
|
14849
|
+
cwd: ctx.cwd,
|
|
14850
|
+
stdin: process.stdin,
|
|
14851
|
+
stdout: process.stdout,
|
|
14852
|
+
stderr: process.stderr
|
|
14853
|
+
});
|
|
14854
|
+
return { exitCode: 0, diagnostics: [], data: { served: true } };
|
|
14855
|
+
}
|
|
14856
|
+
|
|
14857
|
+
// src/commands/packs.ts
|
|
14858
|
+
import path35 from "path";
|
|
14211
14859
|
async function runPacks(ctx) {
|
|
14212
14860
|
const { root, config, workspace } = await requireWorkspace(ctx);
|
|
14213
14861
|
const slug = flagString(ctx.flags, "check");
|
|
@@ -14265,14 +14913,14 @@ async function runPacks(ctx) {
|
|
|
14265
14913
|
data: {
|
|
14266
14914
|
slug,
|
|
14267
14915
|
packs: evaluations.map((evaluation) => ({ id: evaluation.pack.id, status: evaluation.status, passed: evaluation.passed, failed: evaluation.failed, results: evaluation.results })),
|
|
14268
|
-
analyzePath:
|
|
14916
|
+
analyzePath: path35.posix.join(".sdd", "changes", slug, "analyze.md")
|
|
14269
14917
|
},
|
|
14270
14918
|
text: lines
|
|
14271
14919
|
};
|
|
14272
14920
|
}
|
|
14273
14921
|
|
|
14274
14922
|
// src/commands/new.ts
|
|
14275
|
-
import
|
|
14923
|
+
import path36 from "path";
|
|
14276
14924
|
async function runNew(ctx) {
|
|
14277
14925
|
const slug = ctx.positionals[0];
|
|
14278
14926
|
if (!slug) {
|
|
@@ -14298,50 +14946,18 @@ async function runNew(ctx) {
|
|
|
14298
14946
|
msg("new.title", ctx.language),
|
|
14299
14947
|
"",
|
|
14300
14948
|
msg("new.done", ctx.language),
|
|
14301
|
-
...result.files.map((f) => ` + ${
|
|
14949
|
+
...result.files.map((f) => ` + ${path36.relative(ctx.cwd, f)}`),
|
|
14302
14950
|
"",
|
|
14303
14951
|
`Siguiente: /satlas.specify ${result.slug} \u2014 escribe la especificaci\xF3n 100% funcional y de negocio.`
|
|
14304
14952
|
];
|
|
14305
14953
|
return {
|
|
14306
14954
|
exitCode: hasErrors2 ? 1 : 0,
|
|
14307
14955
|
diagnostics: result.diagnostics,
|
|
14308
|
-
data: { slug: result.slug, files: result.files.map((f) =>
|
|
14956
|
+
data: { slug: result.slug, files: result.files.map((f) => path36.relative(ctx.cwd, f)) },
|
|
14309
14957
|
text: lines
|
|
14310
14958
|
};
|
|
14311
14959
|
}
|
|
14312
14960
|
|
|
14313
|
-
// src/evaluate.ts
|
|
14314
|
-
import path34 from "path";
|
|
14315
|
-
function livingRequirementsMap2(specs) {
|
|
14316
|
-
const map = /* @__PURE__ */ new Map();
|
|
14317
|
-
for (const spec of specs) {
|
|
14318
|
-
for (const req of spec.spec.requirements) map.set(req.id, req);
|
|
14319
|
-
}
|
|
14320
|
-
return map;
|
|
14321
|
-
}
|
|
14322
|
-
async function evaluateChange(workspace, config, change, approvals) {
|
|
14323
|
-
const deltaPath = path34.join(change.dir, "spec.md");
|
|
14324
|
-
const deltaContent = await readTextIfExists(deltaPath);
|
|
14325
|
-
const approval = verifyApproval(change, approvals, config, deltaContent ?? void 0);
|
|
14326
|
-
const living = livingRequirementsMap2(workspace.specs);
|
|
14327
|
-
const lintFindings = change.delta ? lintDelta(change.delta, living, deltaPath, { language: config.spec.language }) : [];
|
|
14328
|
-
const trace = checkTrace({
|
|
14329
|
-
specs: workspace.specs,
|
|
14330
|
-
change,
|
|
14331
|
-
requireEvidence: config.gates.verify.mode !== "off" && config.gates.verify.require_evidence
|
|
14332
|
-
});
|
|
14333
|
-
const blocking = lintFindings.filter((d) => d.severity === "error").length + trace.findings.filter((d) => d.severity === "error").length;
|
|
14334
|
-
const mockupsAreReady = requiresMockups(change.meta, config) ? await mockupsReady(workspace.root, change.slug, change) : void 0;
|
|
14335
|
-
const state = deriveState({
|
|
14336
|
-
change,
|
|
14337
|
-
cfg: config,
|
|
14338
|
-
approval,
|
|
14339
|
-
blockingFindings: change.delta ? blocking : 0,
|
|
14340
|
-
...mockupsAreReady !== void 0 ? { mockupsReady: mockupsAreReady } : {}
|
|
14341
|
-
});
|
|
14342
|
-
return { change, approval, lintFindings, trace, blocking, state };
|
|
14343
|
-
}
|
|
14344
|
-
|
|
14345
14961
|
// src/commands/next.ts
|
|
14346
14962
|
async function runNext(ctx) {
|
|
14347
14963
|
const { workspace, config, approvals } = await requireWorkspace(ctx);
|
|
@@ -14372,7 +14988,7 @@ async function runNext(ctx) {
|
|
|
14372
14988
|
}
|
|
14373
14989
|
|
|
14374
14990
|
// src/commands/present.ts
|
|
14375
|
-
import
|
|
14991
|
+
import path37 from "path";
|
|
14376
14992
|
async function runPresentCommand(ctx) {
|
|
14377
14993
|
const { root } = await requireWorkspace(ctx);
|
|
14378
14994
|
const slug = ctx.positionals[0];
|
|
@@ -14384,7 +15000,7 @@ async function runPresentCommand(ctx) {
|
|
|
14384
15000
|
if (result.path) {
|
|
14385
15001
|
lines.push("Propuesta generada");
|
|
14386
15002
|
lines.push("");
|
|
14387
|
-
lines.push(` archivo: ${
|
|
15003
|
+
lines.push(` archivo: ${path37.relative(ctx.cwd, result.path)}`);
|
|
14388
15004
|
if (result.hash) lines.push(` hash de la spec: ${shortHash(result.hash)}`);
|
|
14389
15005
|
lines.push("");
|
|
14390
15006
|
lines.push("\xC1brela en el navegador y comp\xE1rtela con el stakeholder.");
|
|
@@ -14394,7 +15010,7 @@ async function runPresentCommand(ctx) {
|
|
|
14394
15010
|
return {
|
|
14395
15011
|
exitCode: hasErrors2 ? 1 : 0,
|
|
14396
15012
|
diagnostics: result.diagnostics,
|
|
14397
|
-
data: result.path ? { path:
|
|
15013
|
+
data: result.path ? { path: path37.relative(ctx.cwd, result.path), hash: result.hash } : void 0,
|
|
14398
15014
|
text: lines
|
|
14399
15015
|
};
|
|
14400
15016
|
}
|
|
@@ -14512,7 +15128,7 @@ async function runStatus(ctx) {
|
|
|
14512
15128
|
}
|
|
14513
15129
|
|
|
14514
15130
|
// src/commands/trace.ts
|
|
14515
|
-
import
|
|
15131
|
+
import path38 from "path";
|
|
14516
15132
|
async function runTrace(ctx) {
|
|
14517
15133
|
const { workspace, config } = await requireWorkspace(ctx);
|
|
14518
15134
|
const slug = flagString(ctx.flags, "change");
|
|
@@ -14532,7 +15148,7 @@ async function runTrace(ctx) {
|
|
|
14532
15148
|
data.push({ slug: change.slug, nodes: result.graph.nodes.length, edges: result.graph.edges.length, summary: result.summary, findings: result.findings });
|
|
14533
15149
|
lines.push(` ${change.slug}: ${result.graph.nodes.length} nodos, ${result.graph.edges.length} aristas, ${errors} errores, ${result.summary.warnings} avisos`);
|
|
14534
15150
|
for (const finding of result.findings) {
|
|
14535
|
-
const location = finding.path ? ` (${
|
|
15151
|
+
const location = finding.path ? ` (${path38.relative(ctx.cwd, finding.path)}${finding.line ? `:${finding.line}` : ""})` : "";
|
|
14536
15152
|
lines.push(` ${finding.severity.toUpperCase()} ${finding.code} \u2014 ${finding.message}${location}`);
|
|
14537
15153
|
}
|
|
14538
15154
|
}
|
|
@@ -14542,7 +15158,7 @@ async function runTrace(ctx) {
|
|
|
14542
15158
|
}
|
|
14543
15159
|
|
|
14544
15160
|
// src/commands/validate.ts
|
|
14545
|
-
import
|
|
15161
|
+
import path39 from "path";
|
|
14546
15162
|
async function runValidate(ctx) {
|
|
14547
15163
|
const { workspace, config, approvals } = await requireWorkspace(ctx);
|
|
14548
15164
|
const slug = flagString(ctx.flags, "change");
|
|
@@ -14568,7 +15184,7 @@ async function runValidate(ctx) {
|
|
|
14568
15184
|
for (const finding of evaluation.lintFindings) {
|
|
14569
15185
|
lines.push(` ${finding.severity.toUpperCase()} ${finding.code}${finding.line ? `:${finding.line}` : ""} \u2014 ${finding.message}`);
|
|
14570
15186
|
}
|
|
14571
|
-
lines.push(` archivo: ${
|
|
15187
|
+
lines.push(` archivo: ${path39.relative(ctx.cwd, path39.join(change.dir, "spec.md"))}`);
|
|
14572
15188
|
}
|
|
14573
15189
|
if (targets.length === 0) lines.push("Sin cambios activos.");
|
|
14574
15190
|
const errors = diagnostics.filter((d) => d.severity === "error").length;
|
|
@@ -14583,7 +15199,7 @@ async function runValidate(ctx) {
|
|
|
14583
15199
|
}
|
|
14584
15200
|
|
|
14585
15201
|
// src/commands/verify.ts
|
|
14586
|
-
import
|
|
15202
|
+
import path40 from "path";
|
|
14587
15203
|
async function runVerify(ctx) {
|
|
14588
15204
|
const { root, workspace } = await requireWorkspace(ctx);
|
|
14589
15205
|
const slug = ctx.positionals[0];
|
|
@@ -14626,7 +15242,7 @@ async function runVerify(ctx) {
|
|
|
14626
15242
|
return { exitCode: 2, diagnostics: [{ code: "ATLAS-VERIFY-001", severity: "error", message: "Falta --by <nombre> (o define meta.owner)" }] };
|
|
14627
15243
|
}
|
|
14628
15244
|
const profilesDir = await resolveProfilesDir();
|
|
14629
|
-
const profile = await loadActiveProfile(
|
|
15245
|
+
const profile = await loadActiveProfile(path40.join(root, ".sdd"), [profilesDir ?? "", path40.join(root, ".sdd", "profiles", "custom")].filter(Boolean));
|
|
14630
15246
|
const record = await recordEvidence({
|
|
14631
15247
|
root,
|
|
14632
15248
|
slug,
|
|
@@ -14648,7 +15264,7 @@ async function runVerify(ctx) {
|
|
|
14648
15264
|
if (record.evidence.command) lines.push(` comando: ${record.evidence.command}`);
|
|
14649
15265
|
lines.push(` resultado: ${record.evidence.result}`);
|
|
14650
15266
|
if (record.evidence.outputHash) lines.push(` hash: ${record.evidence.outputHash}`);
|
|
14651
|
-
lines.push(` archivo: ${
|
|
15267
|
+
lines.push(` archivo: ${path40.relative(ctx.cwd, record.path)}`);
|
|
14652
15268
|
if (record.stdout) {
|
|
14653
15269
|
lines.push("");
|
|
14654
15270
|
lines.push(" salida (\xFAltimas l\xEDneas):");
|
|
@@ -14663,7 +15279,7 @@ async function runVerify(ctx) {
|
|
|
14663
15279
|
return {
|
|
14664
15280
|
exitCode: record.exitCode === 0 ? 0 : 1,
|
|
14665
15281
|
diagnostics: record.diagnostics,
|
|
14666
|
-
data: { evidence: record.evidence, path:
|
|
15282
|
+
data: { evidence: record.evidence, path: path40.relative(ctx.cwd, record.path), exitCode: record.exitCode },
|
|
14667
15283
|
text: lines
|
|
14668
15284
|
};
|
|
14669
15285
|
}
|
|
@@ -14708,7 +15324,7 @@ El cambio ${slug} no tiene tareas todav\xEDa.`] };
|
|
|
14708
15324
|
}
|
|
14709
15325
|
|
|
14710
15326
|
// src/cli.ts
|
|
14711
|
-
var
|
|
15327
|
+
var HANDLERS2 = {
|
|
14712
15328
|
init: runInit,
|
|
14713
15329
|
adopt: runAdopt,
|
|
14714
15330
|
new: runNew,
|
|
@@ -14729,6 +15345,7 @@ var HANDLERS = {
|
|
|
14729
15345
|
mockup: runMockup,
|
|
14730
15346
|
present: runPresentCommand,
|
|
14731
15347
|
ci: runCi,
|
|
15348
|
+
mcp: runMcp,
|
|
14732
15349
|
metrics: runMetrics,
|
|
14733
15350
|
run: runRun,
|
|
14734
15351
|
archive: runArchive,
|
|
@@ -14736,7 +15353,7 @@ var HANDLERS = {
|
|
|
14736
15353
|
help: (ctx) => runHelp(ctx)
|
|
14737
15354
|
};
|
|
14738
15355
|
var COMMANDS = Object.fromEntries(
|
|
14739
|
-
CATALOG.map((spec) => [spec.name, { description: spec.description, flags: spec.flags, handler:
|
|
15356
|
+
CATALOG.map((spec) => [spec.name, { description: spec.description, flags: spec.flags, handler: HANDLERS2[spec.name] }])
|
|
14740
15357
|
);
|
|
14741
15358
|
function toEnvelope(command, result) {
|
|
14742
15359
|
const warnings = result.diagnostics.filter((d) => d.severity !== "error").map(stripSeverity);
|
|
@@ -14761,7 +15378,7 @@ async function requireWorkspace(ctx) {
|
|
|
14761
15378
|
throw new CliError("ATLAS-WS-001", msg("cli.noWorkspace", ctx.language), 2);
|
|
14762
15379
|
}
|
|
14763
15380
|
const { workspace, config } = await loadWorkspace(root);
|
|
14764
|
-
const approvals = await loadApprovals(
|
|
15381
|
+
const approvals = await loadApprovals(path41.join(root, ".sdd"));
|
|
14765
15382
|
return { root, workspace, config, approvals: approvals.byArtifact };
|
|
14766
15383
|
}
|
|
14767
15384
|
var CliError = class extends Error {
|
|
@@ -14828,7 +15445,7 @@ function printResult(command, result, json2, language) {
|
|
|
14828
15445
|
}
|
|
14829
15446
|
const lines = [...result.text ?? []];
|
|
14830
15447
|
for (const d of result.diagnostics) {
|
|
14831
|
-
const location = d.path ? ` ${
|
|
15448
|
+
const location = d.path ? ` ${path41.relative(process.cwd(), d.path)}${d.line ? `:${d.line}` : ""}` : "";
|
|
14832
15449
|
lines.push(`${d.severity === "error" ? "ERROR" : d.severity === "warning" ? "AVISO" : "NOTA"} ${d.code}${location} \u2014 ${d.message}`);
|
|
14833
15450
|
if (d.suggestion) lines.push(` \u21B3 ${d.suggestion}`);
|
|
14834
15451
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "specatlas",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.25",
|
|
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.
|
|
43
|
+
"@specatlas/core": "0.1.22"
|
|
44
44
|
},
|
|
45
45
|
"scripts": {
|
|
46
46
|
"typecheck": "tsc --noEmit",
|