specatlas 0.1.6 → 0.1.8
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 +175 -141
- 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
|
|
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,
|
|
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:
|
|
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,
|
|
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",
|
|
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",
|
|
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:
|
|
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,
|
|
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,
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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;
|
|
@@ -9897,10 +9898,11 @@ function deriveState(input) {
|
|
|
9897
9898
|
}
|
|
9898
9899
|
if (approval.status === "missing" || approval.status === "stale") {
|
|
9899
9900
|
blockedBy.push(approval.status === "stale" ? "la firma de la spec qued\xF3 obsoleta (el archivo cambi\xF3)" : "la spec no est\xE1 aprobada");
|
|
9901
|
+
const presented = change.presentationPath !== void 0;
|
|
9900
9902
|
return {
|
|
9901
9903
|
state: "awaiting_approval",
|
|
9902
9904
|
blockedBy,
|
|
9903
|
-
nextAction: next(`satlas present ${change.slug}`, "Presentar la propuesta y firmar la aprobaci\xF3n (satlas approve)"),
|
|
9905
|
+
nextAction: presented ? next(`satlas approve ${change.slug} --by "<nombre>"`, "Firmar la aprobaci\xF3n (la presentaci\xF3n ya est\xE1 generada)") : next(`satlas present ${change.slug}`, "Presentar la propuesta y firmar la aprobaci\xF3n (satlas approve)"),
|
|
9904
9906
|
progress
|
|
9905
9907
|
};
|
|
9906
9908
|
}
|
|
@@ -10127,6 +10129,8 @@ async function loadChange(root, slug, relDir) {
|
|
|
10127
10129
|
if (await exists(planFile)) change.planPath = planFile;
|
|
10128
10130
|
const reviewFile = path4.join(dir, "review.md");
|
|
10129
10131
|
if (await exists(reviewFile)) change.reviewPath = reviewFile;
|
|
10132
|
+
const presentationFile = path4.join(dir, "presentation", "index.html");
|
|
10133
|
+
if (await exists(presentationFile)) change.presentationPath = presentationFile;
|
|
10130
10134
|
const tasksFile = path4.join(dir, "tasks.md");
|
|
10131
10135
|
const tasksRaw = await readTextIfExists(tasksFile);
|
|
10132
10136
|
if (tasksRaw !== void 0) {
|
|
@@ -12615,6 +12619,72 @@ async function runDoctor(root) {
|
|
|
12615
12619
|
}
|
|
12616
12620
|
return { findings, summary: countBySeverity(findings) };
|
|
12617
12621
|
}
|
|
12622
|
+
var UI_DOMAINS2 = /* @__PURE__ */ new Set(["frontend", "mobile", "fullstack"]);
|
|
12623
|
+
function count(name, diagnostics) {
|
|
12624
|
+
return {
|
|
12625
|
+
name,
|
|
12626
|
+
errors: diagnostics.filter((d) => d.severity === "error").length,
|
|
12627
|
+
warnings: diagnostics.filter((d) => d.severity === "warning").length
|
|
12628
|
+
};
|
|
12629
|
+
}
|
|
12630
|
+
function livingRequirementsMap(specs) {
|
|
12631
|
+
const map = /* @__PURE__ */ new Map();
|
|
12632
|
+
for (const spec of specs) {
|
|
12633
|
+
for (const requirement of spec.spec.requirements) map.set(requirement.id, requirement);
|
|
12634
|
+
}
|
|
12635
|
+
return map;
|
|
12636
|
+
}
|
|
12637
|
+
async function runCiGate(opts) {
|
|
12638
|
+
const root = path18.resolve(opts.root);
|
|
12639
|
+
const { workspace, config } = await loadWorkspace(root);
|
|
12640
|
+
const diagnostics = [];
|
|
12641
|
+
const checks = [];
|
|
12642
|
+
const specFindings = workspace.specs.flatMap((spec) => lintSpec(spec.spec, { language: config.spec.language, businessOnly: config.spec.business_only }));
|
|
12643
|
+
diagnostics.push(...specFindings);
|
|
12644
|
+
checks.push(count("specs vivas", specFindings));
|
|
12645
|
+
const living = livingRequirementsMap(workspace.specs);
|
|
12646
|
+
let changesErrors = 0;
|
|
12647
|
+
let changesWarnings = 0;
|
|
12648
|
+
for (const change of workspace.changes) {
|
|
12649
|
+
const lintFindings = change.delta ? lintDelta(change.delta, living, path18.join(change.dir, "spec.md"), { language: config.spec.language }) : [];
|
|
12650
|
+
const trace = checkTrace({
|
|
12651
|
+
specs: workspace.specs,
|
|
12652
|
+
change,
|
|
12653
|
+
requireEvidence: config.gates.verify.mode !== "off" && config.gates.verify.require_evidence
|
|
12654
|
+
});
|
|
12655
|
+
const changeDiags = [...lintFindings, ...trace.findings];
|
|
12656
|
+
if (change.tasks && change.tasks.counts.total > 0) {
|
|
12657
|
+
const plan = planWaves(change.tasks, { maxParallel: config.waves.max_parallel });
|
|
12658
|
+
changeDiags.push(...plan.blocks.flatMap((block) => block.diagnostics));
|
|
12659
|
+
}
|
|
12660
|
+
if (UI_DOMAINS2.has(change.meta?.domain ?? "")) {
|
|
12661
|
+
const mockups = await checkMockups(root, change.slug, change);
|
|
12662
|
+
changeDiags.push(...mockups.findings);
|
|
12663
|
+
}
|
|
12664
|
+
if (config.packs.length > 0) {
|
|
12665
|
+
const resolved = await resolvePacks(workspace.sddDir, config);
|
|
12666
|
+
const evaluations = evaluatePacks(resolved.packs, change, config);
|
|
12667
|
+
changeDiags.push(...packFindings(evaluations, change));
|
|
12668
|
+
}
|
|
12669
|
+
diagnostics.push(...changeDiags);
|
|
12670
|
+
const entry = count(change.slug, changeDiags);
|
|
12671
|
+
changesErrors += entry.errors;
|
|
12672
|
+
changesWarnings += entry.warnings;
|
|
12673
|
+
}
|
|
12674
|
+
checks.push({ name: "cambios (lint + trace + waves + mockups)", errors: changesErrors, warnings: changesWarnings });
|
|
12675
|
+
const doctor = await runDoctor(root);
|
|
12676
|
+
const doctorErrors = doctor.findings.filter((d) => d.severity === "error");
|
|
12677
|
+
diagnostics.push(...doctorErrors);
|
|
12678
|
+
checks.push({ name: "doctor", errors: doctorErrors.length, warnings: 0 });
|
|
12679
|
+
for (const extra of opts.extra ?? []) {
|
|
12680
|
+
diagnostics.push(...extra.diagnostics);
|
|
12681
|
+
checks.push(count(extra.name, extra.diagnostics));
|
|
12682
|
+
}
|
|
12683
|
+
const errors = diagnostics.filter((d) => d.severity === "error").length;
|
|
12684
|
+
const warnings = diagnostics.filter((d) => d.severity === "warning").length;
|
|
12685
|
+
const failed = errors > 0 || (opts.strict ?? false) && warnings > 0;
|
|
12686
|
+
return { checks, diagnostics, errors, warnings, failed };
|
|
12687
|
+
}
|
|
12618
12688
|
|
|
12619
12689
|
// src/args.ts
|
|
12620
12690
|
var BOOLEAN_FLAGS = /* @__PURE__ */ new Set(["json", "local", "strict", "yes", "dry-run", "require-evidence", "help", "version"]);
|
|
@@ -12786,26 +12856,26 @@ function cliVersion() {
|
|
|
12786
12856
|
}
|
|
12787
12857
|
|
|
12788
12858
|
// src/commands/adapters.ts
|
|
12789
|
-
import
|
|
12859
|
+
import path21 from "path";
|
|
12790
12860
|
|
|
12791
12861
|
// ../adapters/dist/index.js
|
|
12792
|
-
import
|
|
12862
|
+
import path19 from "path";
|
|
12793
12863
|
import { stringify as stringifyYaml5 } from "yaml";
|
|
12794
12864
|
import path22 from "path";
|
|
12795
12865
|
async function loadWorkflow(workflowDir) {
|
|
12796
|
-
const phasesDir =
|
|
12797
|
-
const snippetsDir =
|
|
12866
|
+
const phasesDir = path19.join(workflowDir, "phases");
|
|
12867
|
+
const snippetsDir = path19.join(workflowDir, "snippets");
|
|
12798
12868
|
const phases = [];
|
|
12799
12869
|
const snippets = /* @__PURE__ */ new Map();
|
|
12800
12870
|
const hashParts = [];
|
|
12801
12871
|
for (const entry of (await listDir(phasesDir)).sort()) {
|
|
12802
12872
|
if (!entry.endsWith(".md")) continue;
|
|
12803
|
-
const filePath =
|
|
12873
|
+
const filePath = path19.join(phasesDir, entry);
|
|
12804
12874
|
const raw = await readText(filePath);
|
|
12805
12875
|
hashParts.push(raw);
|
|
12806
12876
|
const fm = parseFrontmatter(raw, filePath);
|
|
12807
12877
|
const data = fm.data;
|
|
12808
|
-
const id = typeof data["id"] === "string" ? data["id"] :
|
|
12878
|
+
const id = typeof data["id"] === "string" ? data["id"] : path19.basename(entry, ".md");
|
|
12809
12879
|
phases.push({
|
|
12810
12880
|
id,
|
|
12811
12881
|
title: typeof data["title"] === "string" ? data["title"] : id,
|
|
@@ -12819,9 +12889,9 @@ async function loadWorkflow(workflowDir) {
|
|
|
12819
12889
|
}
|
|
12820
12890
|
for (const entry of (await listDir(snippetsDir)).sort()) {
|
|
12821
12891
|
if (!entry.endsWith(".md")) continue;
|
|
12822
|
-
const raw = await readTextIfExists(
|
|
12892
|
+
const raw = await readTextIfExists(path19.join(snippetsDir, entry)) ?? "";
|
|
12823
12893
|
hashParts.push(raw);
|
|
12824
|
-
snippets.set(
|
|
12894
|
+
snippets.set(path19.basename(entry, ".md"), raw.trim());
|
|
12825
12895
|
}
|
|
12826
12896
|
return { phases: phases.sort((a, b) => a.id.localeCompare(b.id)), snippets, sourceHash: sha256(hashParts.join("\n---\n")) };
|
|
12827
12897
|
}
|
|
@@ -13125,7 +13195,7 @@ async function checkAdapters(opts) {
|
|
|
13125
13195
|
}
|
|
13126
13196
|
|
|
13127
13197
|
// src/paths.ts
|
|
13128
|
-
import
|
|
13198
|
+
import path20 from "path";
|
|
13129
13199
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
13130
13200
|
async function resolveProfilesDir() {
|
|
13131
13201
|
const env = process.env["SPECATLAS_PROFILES_DIR"];
|
|
@@ -13135,16 +13205,16 @@ async function resolveProfilesDir() {
|
|
|
13135
13205
|
async function resolveWorkflowDir() {
|
|
13136
13206
|
const env = process.env["SPECATLAS_WORKFLOW_DIR"];
|
|
13137
13207
|
if (env && await exists(env)) return env;
|
|
13138
|
-
return walkUpFor(
|
|
13208
|
+
return walkUpFor(path20.join("workflow", "phases"));
|
|
13139
13209
|
}
|
|
13140
13210
|
async function walkUpFor(relative) {
|
|
13141
|
-
let dir =
|
|
13211
|
+
let dir = path20.dirname(fileURLToPath2(import.meta.url));
|
|
13142
13212
|
for (let i = 0; i < 8; i += 1) {
|
|
13143
|
-
const candidate =
|
|
13213
|
+
const candidate = path20.join(dir, relative);
|
|
13144
13214
|
if (await exists(candidate)) {
|
|
13145
|
-
return relative.includes(
|
|
13215
|
+
return relative.includes(path20.sep) ? path20.dirname(candidate) : candidate;
|
|
13146
13216
|
}
|
|
13147
|
-
const parent =
|
|
13217
|
+
const parent = path20.dirname(dir);
|
|
13148
13218
|
if (parent === dir) break;
|
|
13149
13219
|
dir = parent;
|
|
13150
13220
|
}
|
|
@@ -13222,7 +13292,7 @@ async function runAdapters(ctx) {
|
|
|
13222
13292
|
for (const [target, count2] of byTarget) lines.push(` ${target}: ${count2} archivo(s)`);
|
|
13223
13293
|
if (report.written.length > 0) {
|
|
13224
13294
|
lines.push("");
|
|
13225
|
-
for (const p of report.written) lines.push(` + ${
|
|
13295
|
+
for (const p of report.written) lines.push(` + ${path21.relative(ctx.cwd, path21.join(root, p))}`);
|
|
13226
13296
|
} else {
|
|
13227
13297
|
lines.push("");
|
|
13228
13298
|
lines.push("Sin cambios: los adaptadores ya estaban al d\xEDa.");
|
|
@@ -13233,7 +13303,7 @@ async function runAdapters(ctx) {
|
|
|
13233
13303
|
}
|
|
13234
13304
|
|
|
13235
13305
|
// src/commands/adopt.ts
|
|
13236
|
-
import
|
|
13306
|
+
import path23 from "path";
|
|
13237
13307
|
async function runAdopt(ctx) {
|
|
13238
13308
|
const { root } = await requireWorkspace(ctx);
|
|
13239
13309
|
const domainsFlag = flagString(ctx.flags, "domains") ?? flagString(ctx.flags, "domain");
|
|
@@ -13253,11 +13323,11 @@ async function runAdopt(ctx) {
|
|
|
13253
13323
|
}
|
|
13254
13324
|
if (result.createdSpecs.length > 0) {
|
|
13255
13325
|
lines.push("");
|
|
13256
|
-
for (const spec of result.createdSpecs) lines.push(` ${result.dryRun ? "[dry-run] " : "+ "}${
|
|
13326
|
+
for (const spec of result.createdSpecs) lines.push(` ${result.dryRun ? "[dry-run] " : "+ "}${path23.relative(ctx.cwd, spec)}`);
|
|
13257
13327
|
}
|
|
13258
13328
|
if (result.reportPath && !result.dryRun) {
|
|
13259
13329
|
lines.push("");
|
|
13260
|
-
lines.push(` Informe: ${
|
|
13330
|
+
lines.push(` Informe: ${path23.relative(ctx.cwd, result.reportPath)}`);
|
|
13261
13331
|
}
|
|
13262
13332
|
lines.push("");
|
|
13263
13333
|
lines.push("Siguiente: fase /satlas-adopt por dominio (requisitos AS-IS) y luego:");
|
|
@@ -13270,8 +13340,8 @@ async function runAdopt(ctx) {
|
|
|
13270
13340
|
diagnostics: result.diagnostics,
|
|
13271
13341
|
data: {
|
|
13272
13342
|
domains: result.domains.map((d) => ({ name: d.name, files: d.files, existingSpec: d.existingSpec })),
|
|
13273
|
-
createdSpecs: result.createdSpecs.map((s) =>
|
|
13274
|
-
reportPath: result.reportPath ?
|
|
13343
|
+
createdSpecs: result.createdSpecs.map((s) => path23.relative(ctx.cwd, s)),
|
|
13344
|
+
reportPath: result.reportPath ? path23.relative(ctx.cwd, result.reportPath) : void 0,
|
|
13275
13345
|
stack: result.stack,
|
|
13276
13346
|
dryRun: result.dryRun
|
|
13277
13347
|
},
|
|
@@ -13280,7 +13350,7 @@ async function runAdopt(ctx) {
|
|
|
13280
13350
|
}
|
|
13281
13351
|
|
|
13282
13352
|
// src/commands/analyze.ts
|
|
13283
|
-
import
|
|
13353
|
+
import path24 from "path";
|
|
13284
13354
|
async function runAnalyzeCommand(ctx) {
|
|
13285
13355
|
const { root } = await requireWorkspace(ctx);
|
|
13286
13356
|
const slug = ctx.positionals[0];
|
|
@@ -13291,23 +13361,23 @@ async function runAnalyzeCommand(ctx) {
|
|
|
13291
13361
|
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
13362
|
if (result.waves) lines.push(` olas: ${result.waves.blocks} bloque(s), ${result.waves.waves} ola(s), ${result.waves.tasks} tarea(s)`);
|
|
13293
13363
|
if (result.mockups) lines.push(` mockups: ${result.mockups.screens} pantalla(s)${result.mockups.stale ? " (desactualizados)" : ""}`);
|
|
13294
|
-
if (result.path) lines.push("", ` informe: ${
|
|
13364
|
+
if (result.path) lines.push("", ` informe: ${path24.relative(ctx.cwd, result.path)}`);
|
|
13295
13365
|
for (const finding of result.findings) {
|
|
13296
13366
|
lines.push(` ${finding.severity.toUpperCase()} ${finding.code} \u2014 ${finding.message}`);
|
|
13297
13367
|
}
|
|
13298
13368
|
return {
|
|
13299
13369
|
exitCode: result.status === "blocked" ? 1 : 0,
|
|
13300
13370
|
diagnostics: result.findings,
|
|
13301
|
-
data: result.path ? { ...result, path:
|
|
13371
|
+
data: result.path ? { ...result, path: path24.relative(ctx.cwd, result.path) } : result,
|
|
13302
13372
|
text: lines
|
|
13303
13373
|
};
|
|
13304
13374
|
}
|
|
13305
13375
|
|
|
13306
13376
|
// src/commands/approve.ts
|
|
13307
|
-
import
|
|
13377
|
+
import path26 from "path";
|
|
13308
13378
|
|
|
13309
13379
|
// src/commands/issue.ts
|
|
13310
|
-
import
|
|
13380
|
+
import path25 from "path";
|
|
13311
13381
|
async function runIssue(ctx) {
|
|
13312
13382
|
const action = ctx.positionals[0] ?? "status";
|
|
13313
13383
|
const slug = ctx.positionals[1];
|
|
@@ -13335,7 +13405,7 @@ async function runIssue(ctx) {
|
|
|
13335
13405
|
return { exitCode: 2, diagnostics: [{ code: "ATLAS-ISSUE-000", severity: "error", message: "Falta el slug: satlas issue status <slug>" }] };
|
|
13336
13406
|
}
|
|
13337
13407
|
const change = await loadChange(root, slug);
|
|
13338
|
-
const { config } = await loadConfig(
|
|
13408
|
+
const { config } = await loadConfig(path25.join(root, ".sdd"));
|
|
13339
13409
|
const tracker = change.meta?.tracker;
|
|
13340
13410
|
const lines = [`Issue de ${slug}`, ""];
|
|
13341
13411
|
if (!tracker || tracker.provider !== "github") {
|
|
@@ -13406,7 +13476,7 @@ async function runApprove(ctx) {
|
|
|
13406
13476
|
};
|
|
13407
13477
|
}
|
|
13408
13478
|
const { root } = await requireWorkspace(ctx);
|
|
13409
|
-
const artifact = target.includes("/") || target.includes("\\") ? target :
|
|
13479
|
+
const artifact = target.includes("/") || target.includes("\\") ? target : path26.join("changes", target, "spec.md");
|
|
13410
13480
|
const channelFlag = flagString(ctx.flags, "channel");
|
|
13411
13481
|
const channel = channelFlag === "presentation" || channelFlag === "editor" || channelFlag === "pr" || channelFlag === "tracker" ? channelFlag : "cli";
|
|
13412
13482
|
const result = await signApproval({
|
|
@@ -13431,13 +13501,13 @@ async function runApprove(ctx) {
|
|
|
13431
13501
|
return {
|
|
13432
13502
|
exitCode: hasErrors2 ? 1 : 0,
|
|
13433
13503
|
diagnostics: result.diagnostics,
|
|
13434
|
-
data: result.approval ? { ...result.approval, file:
|
|
13504
|
+
data: result.approval ? { ...result.approval, file: path26.relative(ctx.cwd, result.file) } : void 0,
|
|
13435
13505
|
text: lines
|
|
13436
13506
|
};
|
|
13437
13507
|
}
|
|
13438
13508
|
|
|
13439
13509
|
// src/commands/archive.ts
|
|
13440
|
-
import
|
|
13510
|
+
import path27 from "path";
|
|
13441
13511
|
async function runArchive(ctx) {
|
|
13442
13512
|
const slug = ctx.positionals[0];
|
|
13443
13513
|
if (!slug) {
|
|
@@ -13467,7 +13537,7 @@ async function runArchive(ctx) {
|
|
|
13467
13537
|
lines.push(` modificados: ${result.fold.applied.modified.join(", ") || "\u2014"}`);
|
|
13468
13538
|
lines.push(` eliminados: ${result.fold.applied.removed.join(", ") || "\u2014"}`);
|
|
13469
13539
|
lines.push(` renombrados: ${result.fold.applied.renamed.join(", ") || "\u2014"}`);
|
|
13470
|
-
if (result.archivedTo) lines.push(` archivado en: ${
|
|
13540
|
+
if (result.archivedTo) lines.push(` archivado en: ${path27.relative(ctx.cwd, result.archivedTo)}`);
|
|
13471
13541
|
} else {
|
|
13472
13542
|
lines.push("El delta no contiene operaciones (ADDED/MODIFIED/REMOVED/RENAMED).");
|
|
13473
13543
|
}
|
|
@@ -13480,100 +13550,39 @@ async function runArchive(ctx) {
|
|
|
13480
13550
|
};
|
|
13481
13551
|
}
|
|
13482
13552
|
|
|
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
13553
|
// src/commands/ci.ts
|
|
13509
|
-
var UI_DOMAINS2 = /* @__PURE__ */ new Set(["frontend", "mobile", "fullstack"]);
|
|
13510
13554
|
async function runCi(ctx) {
|
|
13511
|
-
const { root,
|
|
13555
|
+
const { root, config } = await requireWorkspace(ctx);
|
|
13512
13556
|
const strict = flagBool(ctx.flags, "strict");
|
|
13513
|
-
const
|
|
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 });
|
|
13557
|
+
const extra = [];
|
|
13546
13558
|
const workflowDir = await resolveWorkflowDir();
|
|
13547
13559
|
if (workflowDir) {
|
|
13548
13560
|
const health = await checkAdapters({ root, workflowDir, targets: config.adapters.targets, language: config.project.language });
|
|
13549
|
-
|
|
13550
|
-
|
|
13551
|
-
|
|
13552
|
-
|
|
13553
|
-
|
|
13554
|
-
|
|
13555
|
-
|
|
13556
|
-
|
|
13557
|
-
|
|
13558
|
-
|
|
13559
|
-
|
|
13560
|
-
}
|
|
13561
|
+
extra.push({
|
|
13562
|
+
name: "adaptadores",
|
|
13563
|
+
diagnostics: health.manifest && !health.ok ? [
|
|
13564
|
+
{
|
|
13565
|
+
code: "ATLAS-ADAPTERS-001",
|
|
13566
|
+
severity: "warning",
|
|
13567
|
+
message: "Los adaptadores de agente est\xE1n desactualizados respecto a workflow/",
|
|
13568
|
+
suggestion: "Ejecuta `satlas adapters` y commitea el resultado"
|
|
13569
|
+
}
|
|
13570
|
+
] : []
|
|
13571
|
+
});
|
|
13561
13572
|
}
|
|
13562
|
-
const
|
|
13563
|
-
const warnings = diagnostics.filter((d) => d.severity === "warning").length;
|
|
13564
|
-
const failed = errors > 0 || strict && warnings > 0;
|
|
13573
|
+
const gate = await runCiGate({ root, strict, extra });
|
|
13565
13574
|
const lines = ["CI de SpecAtlas", ""];
|
|
13566
|
-
for (const check of checks)
|
|
13575
|
+
for (const check of gate.checks) {
|
|
13576
|
+
lines.push(` ${check.errors === 0 && check.warnings === 0 ? "OK " : "FALLA"} ${check.name}: ${check.errors} errores, ${check.warnings} avisos`);
|
|
13577
|
+
}
|
|
13567
13578
|
lines.push("");
|
|
13568
|
-
lines.push(failed ? "Resultado: BLOQUEADO" : "Resultado: OK");
|
|
13579
|
+
lines.push(gate.failed ? "Resultado: BLOQUEADO" : "Resultado: OK");
|
|
13569
13580
|
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
13581
|
return {
|
|
13574
|
-
|
|
13575
|
-
|
|
13576
|
-
|
|
13582
|
+
exitCode: gate.failed ? 1 : 0,
|
|
13583
|
+
diagnostics: gate.diagnostics,
|
|
13584
|
+
data: { checks: gate.checks, errors: gate.errors, warnings: gate.warnings, strict, failed: gate.failed },
|
|
13585
|
+
text: lines
|
|
13577
13586
|
};
|
|
13578
13587
|
}
|
|
13579
13588
|
|
|
@@ -14015,6 +14024,31 @@ async function runNew(ctx) {
|
|
|
14015
14024
|
};
|
|
14016
14025
|
}
|
|
14017
14026
|
|
|
14027
|
+
// src/evaluate.ts
|
|
14028
|
+
import path34 from "path";
|
|
14029
|
+
function livingRequirementsMap2(specs) {
|
|
14030
|
+
const map = /* @__PURE__ */ new Map();
|
|
14031
|
+
for (const spec of specs) {
|
|
14032
|
+
for (const req of spec.spec.requirements) map.set(req.id, req);
|
|
14033
|
+
}
|
|
14034
|
+
return map;
|
|
14035
|
+
}
|
|
14036
|
+
async function evaluateChange(workspace, config, change, approvals) {
|
|
14037
|
+
const deltaPath = path34.join(change.dir, "spec.md");
|
|
14038
|
+
const deltaContent = await readTextIfExists(deltaPath);
|
|
14039
|
+
const approval = verifyApproval(change, approvals, config, deltaContent ?? void 0);
|
|
14040
|
+
const living = livingRequirementsMap2(workspace.specs);
|
|
14041
|
+
const lintFindings = change.delta ? lintDelta(change.delta, living, deltaPath, { language: config.spec.language }) : [];
|
|
14042
|
+
const trace = checkTrace({
|
|
14043
|
+
specs: workspace.specs,
|
|
14044
|
+
change,
|
|
14045
|
+
requireEvidence: config.gates.verify.mode !== "off" && config.gates.verify.require_evidence
|
|
14046
|
+
});
|
|
14047
|
+
const blocking = lintFindings.filter((d) => d.severity === "error").length + trace.findings.filter((d) => d.severity === "error").length;
|
|
14048
|
+
const state = deriveState({ change, cfg: config, approval, blockingFindings: change.delta ? blocking : 0 });
|
|
14049
|
+
return { change, approval, lintFindings, trace, blocking, state };
|
|
14050
|
+
}
|
|
14051
|
+
|
|
14018
14052
|
// src/commands/next.ts
|
|
14019
14053
|
async function runNext(ctx) {
|
|
14020
14054
|
const { workspace, config, approvals } = await requireWorkspace(ctx);
|
|
@@ -14045,7 +14079,7 @@ async function runNext(ctx) {
|
|
|
14045
14079
|
}
|
|
14046
14080
|
|
|
14047
14081
|
// src/commands/present.ts
|
|
14048
|
-
import
|
|
14082
|
+
import path35 from "path";
|
|
14049
14083
|
async function runPresentCommand(ctx) {
|
|
14050
14084
|
const { root } = await requireWorkspace(ctx);
|
|
14051
14085
|
const slug = ctx.positionals[0];
|
|
@@ -14057,7 +14091,7 @@ async function runPresentCommand(ctx) {
|
|
|
14057
14091
|
if (result.path) {
|
|
14058
14092
|
lines.push("Propuesta generada");
|
|
14059
14093
|
lines.push("");
|
|
14060
|
-
lines.push(` archivo: ${
|
|
14094
|
+
lines.push(` archivo: ${path35.relative(ctx.cwd, result.path)}`);
|
|
14061
14095
|
if (result.hash) lines.push(` hash de la spec: ${shortHash(result.hash)}`);
|
|
14062
14096
|
lines.push("");
|
|
14063
14097
|
lines.push("\xC1brela en el navegador y comp\xE1rtela con el stakeholder.");
|
|
@@ -14067,7 +14101,7 @@ async function runPresentCommand(ctx) {
|
|
|
14067
14101
|
return {
|
|
14068
14102
|
exitCode: hasErrors2 ? 1 : 0,
|
|
14069
14103
|
diagnostics: result.diagnostics,
|
|
14070
|
-
data: result.path ? { path:
|
|
14104
|
+
data: result.path ? { path: path35.relative(ctx.cwd, result.path), hash: result.hash } : void 0,
|
|
14071
14105
|
text: lines
|
|
14072
14106
|
};
|
|
14073
14107
|
}
|
|
@@ -14185,7 +14219,7 @@ async function runStatus(ctx) {
|
|
|
14185
14219
|
}
|
|
14186
14220
|
|
|
14187
14221
|
// src/commands/trace.ts
|
|
14188
|
-
import
|
|
14222
|
+
import path36 from "path";
|
|
14189
14223
|
async function runTrace(ctx) {
|
|
14190
14224
|
const { workspace, config } = await requireWorkspace(ctx);
|
|
14191
14225
|
const slug = flagString(ctx.flags, "change");
|
|
@@ -14205,7 +14239,7 @@ async function runTrace(ctx) {
|
|
|
14205
14239
|
data.push({ slug: change.slug, nodes: result.graph.nodes.length, edges: result.graph.edges.length, summary: result.summary, findings: result.findings });
|
|
14206
14240
|
lines.push(` ${change.slug}: ${result.graph.nodes.length} nodos, ${result.graph.edges.length} aristas, ${errors} errores, ${result.summary.warnings} avisos`);
|
|
14207
14241
|
for (const finding of result.findings) {
|
|
14208
|
-
const location = finding.path ? ` (${
|
|
14242
|
+
const location = finding.path ? ` (${path36.relative(ctx.cwd, finding.path)}${finding.line ? `:${finding.line}` : ""})` : "";
|
|
14209
14243
|
lines.push(` ${finding.severity.toUpperCase()} ${finding.code} \u2014 ${finding.message}${location}`);
|
|
14210
14244
|
}
|
|
14211
14245
|
}
|
|
@@ -14215,7 +14249,7 @@ async function runTrace(ctx) {
|
|
|
14215
14249
|
}
|
|
14216
14250
|
|
|
14217
14251
|
// src/commands/validate.ts
|
|
14218
|
-
import
|
|
14252
|
+
import path37 from "path";
|
|
14219
14253
|
async function runValidate(ctx) {
|
|
14220
14254
|
const { workspace, config, approvals } = await requireWorkspace(ctx);
|
|
14221
14255
|
const slug = flagString(ctx.flags, "change");
|
|
@@ -14241,7 +14275,7 @@ async function runValidate(ctx) {
|
|
|
14241
14275
|
for (const finding of evaluation.lintFindings) {
|
|
14242
14276
|
lines.push(` ${finding.severity.toUpperCase()} ${finding.code}${finding.line ? `:${finding.line}` : ""} \u2014 ${finding.message}`);
|
|
14243
14277
|
}
|
|
14244
|
-
lines.push(` archivo: ${
|
|
14278
|
+
lines.push(` archivo: ${path37.relative(ctx.cwd, path37.join(change.dir, "spec.md"))}`);
|
|
14245
14279
|
}
|
|
14246
14280
|
if (targets.length === 0) lines.push("Sin cambios activos.");
|
|
14247
14281
|
const errors = diagnostics.filter((d) => d.severity === "error").length;
|
|
@@ -14256,7 +14290,7 @@ async function runValidate(ctx) {
|
|
|
14256
14290
|
}
|
|
14257
14291
|
|
|
14258
14292
|
// src/commands/verify.ts
|
|
14259
|
-
import
|
|
14293
|
+
import path38 from "path";
|
|
14260
14294
|
async function runVerify(ctx) {
|
|
14261
14295
|
const { root, workspace } = await requireWorkspace(ctx);
|
|
14262
14296
|
const slug = ctx.positionals[0];
|
|
@@ -14299,7 +14333,7 @@ async function runVerify(ctx) {
|
|
|
14299
14333
|
return { exitCode: 2, diagnostics: [{ code: "ATLAS-VERIFY-001", severity: "error", message: "Falta --by <nombre> (o define meta.owner)" }] };
|
|
14300
14334
|
}
|
|
14301
14335
|
const profilesDir = await resolveProfilesDir();
|
|
14302
|
-
const profile = await loadActiveProfile(
|
|
14336
|
+
const profile = await loadActiveProfile(path38.join(root, ".sdd"), [profilesDir ?? "", path38.join(root, ".sdd", "profiles", "custom")].filter(Boolean));
|
|
14303
14337
|
const record = await recordEvidence({
|
|
14304
14338
|
root,
|
|
14305
14339
|
slug,
|
|
@@ -14321,7 +14355,7 @@ async function runVerify(ctx) {
|
|
|
14321
14355
|
if (record.evidence.command) lines.push(` comando: ${record.evidence.command}`);
|
|
14322
14356
|
lines.push(` resultado: ${record.evidence.result}`);
|
|
14323
14357
|
if (record.evidence.outputHash) lines.push(` hash: ${record.evidence.outputHash}`);
|
|
14324
|
-
lines.push(` archivo: ${
|
|
14358
|
+
lines.push(` archivo: ${path38.relative(ctx.cwd, record.path)}`);
|
|
14325
14359
|
if (record.stdout) {
|
|
14326
14360
|
lines.push("");
|
|
14327
14361
|
lines.push(" salida (\xFAltimas l\xEDneas):");
|
|
@@ -14336,7 +14370,7 @@ async function runVerify(ctx) {
|
|
|
14336
14370
|
return {
|
|
14337
14371
|
exitCode: record.exitCode === 0 ? 0 : 1,
|
|
14338
14372
|
diagnostics: record.diagnostics,
|
|
14339
|
-
data: { evidence: record.evidence, path:
|
|
14373
|
+
data: { evidence: record.evidence, path: path38.relative(ctx.cwd, record.path), exitCode: record.exitCode },
|
|
14340
14374
|
text: lines
|
|
14341
14375
|
};
|
|
14342
14376
|
}
|
|
@@ -14434,7 +14468,7 @@ async function requireWorkspace(ctx) {
|
|
|
14434
14468
|
throw new CliError("ATLAS-WS-001", msg("cli.noWorkspace", ctx.language), 2);
|
|
14435
14469
|
}
|
|
14436
14470
|
const { workspace, config } = await loadWorkspace(root);
|
|
14437
|
-
const approvals = await loadApprovals(
|
|
14471
|
+
const approvals = await loadApprovals(path39.join(root, ".sdd"));
|
|
14438
14472
|
return { root, workspace, config, approvals: approvals.byArtifact };
|
|
14439
14473
|
}
|
|
14440
14474
|
var CliError = class extends Error {
|
|
@@ -14501,7 +14535,7 @@ function printResult(command, result, json2, language) {
|
|
|
14501
14535
|
}
|
|
14502
14536
|
const lines = [...result.text ?? []];
|
|
14503
14537
|
for (const d of result.diagnostics) {
|
|
14504
|
-
const location = d.path ? ` ${
|
|
14538
|
+
const location = d.path ? ` ${path39.relative(process.cwd(), d.path)}${d.line ? `:${d.line}` : ""}` : "";
|
|
14505
14539
|
lines.push(`${d.severity === "error" ? "ERROR" : d.severity === "warning" ? "AVISO" : "NOTA"} ${d.code}${location} \u2014 ${d.message}`);
|
|
14506
14540
|
if (d.suggestion) lines.push(` \u21B3 ${d.suggestion}`);
|
|
14507
14541
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "specatlas",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.8",
|
|
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.6"
|
|
44
44
|
},
|
|
45
45
|
"scripts": {
|
|
46
46
|
"typecheck": "tsc --noEmit",
|