@specatlas/core 0.1.27 → 0.1.28
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +96 -2
- package/dist/index.js +592 -229
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -63,6 +63,7 @@ var atlasConfigSchema = z.object({
|
|
|
63
63
|
review: z.object({ mode: z.enum(["off", "advisory", "blocking"]).default("advisory") }).default({}),
|
|
64
64
|
clarify: z.object({ mode: z.enum(["off", "advisory", "blocking"]).default("advisory") }).default({}),
|
|
65
65
|
docs: z.object({ mode: z.enum(["off", "advisory", "blocking"]).default("blocking") }).default({}),
|
|
66
|
+
contracts: z.object({ mode: z.enum(["off", "advisory", "blocking"]).default("advisory") }).default({}),
|
|
66
67
|
mockup: z.object({ require_approval: z.boolean().default(false), compare_in_verify: z.boolean().default(false) }).default({})
|
|
67
68
|
}).default({}),
|
|
68
69
|
trace: z.object({ mode: z.enum(["off", "advisory", "blocking"]).default("blocking"), prefix: z.string().default("REQ") }).default({}),
|
|
@@ -211,6 +212,7 @@ var SCENARIO_HEAD_RE = /^####\s+(?:Scenario|Escenario):\s+(REQ-[A-Z0-9-]+-S\d+)\
|
|
|
211
212
|
var RULE_RE = /^-\s*(?:Rule|Regla)\s+(BR-[A-Z0-9-]+)\s*:\s*(.+?)\s*$/;
|
|
212
213
|
var WHEN_RE = /^-\s*\*\*\s*(?:WHEN|CUANDO|DADO QUE)\s*\*\*\s*(.+?)\s*$/i;
|
|
213
214
|
var THEN_RE = /^-\s*\*\*\s*(?:THEN|ENTONCES|Y|AND)\s*\*\*\s*(.+?)\s*$/i;
|
|
215
|
+
var CONTRACT_RE = /^-\s*\*\*\s*(?:CONTRATO|CONTRACT)\s*\*\*\s*:\s*(.+?)\s*$/i;
|
|
214
216
|
function newDraft(id, title, line) {
|
|
215
217
|
return { id, title, line, prose: [], rules: [], scenarios: [] };
|
|
216
218
|
}
|
|
@@ -295,6 +297,16 @@ function parseRequirementBlocks(body, startLine = 1, filePath) {
|
|
|
295
297
|
currentScenario.then.push(thenMatch[1] ?? "");
|
|
296
298
|
continue;
|
|
297
299
|
}
|
|
300
|
+
const contractMatch = CONTRACT_RE.exec(line);
|
|
301
|
+
if (contractMatch && currentScenario) {
|
|
302
|
+
const ref = (contractMatch[1] ?? "").trim();
|
|
303
|
+
if (ref !== "") {
|
|
304
|
+
const list = currentScenario.contracts ?? [];
|
|
305
|
+
if (!list.includes(ref)) list.push(ref);
|
|
306
|
+
currentScenario.contracts = list;
|
|
307
|
+
}
|
|
308
|
+
continue;
|
|
309
|
+
}
|
|
298
310
|
if (current && !currentScenario) {
|
|
299
311
|
if (line.trim() !== "") current.prose.push(line.trim());
|
|
300
312
|
}
|
|
@@ -762,6 +774,7 @@ function impactOfRequirement(workspace, reqId) {
|
|
|
762
774
|
const id = reqId.toUpperCase();
|
|
763
775
|
const scenarios = /* @__PURE__ */ new Set();
|
|
764
776
|
let exists2 = false;
|
|
777
|
+
let origin;
|
|
765
778
|
for (const spec of workspace.specs) {
|
|
766
779
|
for (const req of spec.spec.requirements) {
|
|
767
780
|
if (req.id !== id) continue;
|
|
@@ -778,6 +791,18 @@ function impactOfRequirement(workspace, reqId) {
|
|
|
778
791
|
if ((change.delta?.removed ?? []).some((r) => r.id === id)) exists2 = true;
|
|
779
792
|
if ((change.delta?.renamed ?? []).some((r) => r.from.id === id || r.to.id === id)) exists2 = true;
|
|
780
793
|
}
|
|
794
|
+
if (!exists2) {
|
|
795
|
+
for (const link of workspace.links?.entries ?? []) {
|
|
796
|
+
const spec = workspace.links?.specs.find((candidate) => candidate.spec.requirements.some((req) => req.id === id));
|
|
797
|
+
if (!spec) continue;
|
|
798
|
+
const requirement = spec.spec.requirements.find((candidate) => candidate.id === id);
|
|
799
|
+
if (!requirement) continue;
|
|
800
|
+
exists2 = true;
|
|
801
|
+
origin = link.name;
|
|
802
|
+
for (const sc of requirement.scenarios) scenarios.add(sc.id);
|
|
803
|
+
break;
|
|
804
|
+
}
|
|
805
|
+
}
|
|
781
806
|
const report = emptyReport(id, "requirement");
|
|
782
807
|
if (!exists2) return report;
|
|
783
808
|
report.exists = true;
|
|
@@ -785,6 +810,10 @@ function impactOfRequirement(workspace, reqId) {
|
|
|
785
810
|
const reqOf = scenarioToReq(workspace);
|
|
786
811
|
collect(workspace, report, /* @__PURE__ */ new Set([id, ...scenarios]), reqOf);
|
|
787
812
|
if (!report.requirements.includes(id)) report.requirements.unshift(id);
|
|
813
|
+
if (origin !== void 0) {
|
|
814
|
+
report.external = true;
|
|
815
|
+
report.origin = origin;
|
|
816
|
+
}
|
|
788
817
|
return report;
|
|
789
818
|
}
|
|
790
819
|
function impactOfFile(workspace, file) {
|
|
@@ -825,14 +854,14 @@ function wordBoundary(text, term) {
|
|
|
825
854
|
const re = new RegExp(`(?<![\\p{L}\\p{N}])${escaped}(?![\\p{L}\\p{N}])`, "iu");
|
|
826
855
|
return re.test(text);
|
|
827
856
|
}
|
|
828
|
-
function lintText(text, code, terms, label,
|
|
857
|
+
function lintText(text, code, terms, label, path25, line) {
|
|
829
858
|
const out = [];
|
|
830
859
|
const lower = text.toLowerCase();
|
|
831
860
|
for (const term of terms) {
|
|
832
861
|
if (wordBoundary(lower, term)) {
|
|
833
862
|
out.push(
|
|
834
863
|
diag(code, "error", `${label}: "${term}"`, {
|
|
835
|
-
path:
|
|
864
|
+
path: path25,
|
|
836
865
|
line,
|
|
837
866
|
suggestion: "La especificaci\xF3n es funcional y de negocio: describe comportamiento, no tecnolog\xEDa ni adjetivos vagos"
|
|
838
867
|
})
|
|
@@ -841,7 +870,7 @@ function lintText(text, code, terms, label, path23, line) {
|
|
|
841
870
|
}
|
|
842
871
|
return out;
|
|
843
872
|
}
|
|
844
|
-
function lintRequirement(req,
|
|
873
|
+
function lintRequirement(req, path25, opts = {}) {
|
|
845
874
|
const out = [];
|
|
846
875
|
const vague = opts.language === "en" ? VAGUE_EN : VAGUE_ES;
|
|
847
876
|
const tech = opts.language === "en" ? TECH_EN : TECH_ES;
|
|
@@ -852,32 +881,32 @@ function lintRequirement(req, path23, opts = {}) {
|
|
|
852
881
|
...req.scenarios.flatMap((s) => [...s.when.map((w) => ({ text: w, line: s.line })), ...s.then.map((t) => ({ text: t, line: s.line }))])
|
|
853
882
|
];
|
|
854
883
|
for (const part of parts) {
|
|
855
|
-
out.push(...lintText(part.text, "LINT-BIZ-002", vague, "Palabra vaga en la especificaci\xF3n",
|
|
884
|
+
out.push(...lintText(part.text, "LINT-BIZ-002", vague, "Palabra vaga en la especificaci\xF3n", path25, part.line));
|
|
856
885
|
if (opts.businessOnly !== false) {
|
|
857
|
-
out.push(...lintText(part.text, "LINT-BIZ-001", tech, "Jerga t\xE9cnica en la especificaci\xF3n de negocio",
|
|
886
|
+
out.push(...lintText(part.text, "LINT-BIZ-001", tech, "Jerga t\xE9cnica en la especificaci\xF3n de negocio", path25, part.line));
|
|
858
887
|
}
|
|
859
888
|
}
|
|
860
889
|
if (req.scenarios.length === 0) {
|
|
861
|
-
out.push(diag("TRACE-001", "error", `El requisito ${req.id} no tiene ning\xFAn escenario`, { path:
|
|
890
|
+
out.push(diag("TRACE-001", "error", `El requisito ${req.id} no tiene ning\xFAn escenario`, { path: path25, line: req.line, suggestion: "A\xF1ade al menos un escenario CUANDO/ENTONCES" }));
|
|
862
891
|
}
|
|
863
892
|
return out;
|
|
864
893
|
}
|
|
865
|
-
function lintDelta(delta, livingRequirements,
|
|
894
|
+
function lintDelta(delta, livingRequirements, path25, opts = {}) {
|
|
866
895
|
const out = [...delta.diagnostics];
|
|
867
896
|
for (const req of [...delta.added, ...delta.modified]) {
|
|
868
|
-
out.push(...lintRequirement(req,
|
|
897
|
+
out.push(...lintRequirement(req, path25, opts));
|
|
869
898
|
}
|
|
870
899
|
for (const req of delta.modified) {
|
|
871
900
|
const living = livingRequirements.get(req.id);
|
|
872
901
|
if (!living) {
|
|
873
|
-
out.push(diag("TRACE-007", "error", `MODIFIED ${req.id} no existe en la spec viva; usa ADDED`, { path:
|
|
902
|
+
out.push(diag("TRACE-007", "error", `MODIFIED ${req.id} no existe en la spec viva; usa ADDED`, { path: path25, line: req.line }));
|
|
874
903
|
continue;
|
|
875
904
|
}
|
|
876
905
|
for (const existing of living.scenarios) {
|
|
877
906
|
if (!req.scenarios.some((s) => s.id === existing.id)) {
|
|
878
907
|
out.push(
|
|
879
908
|
diag("TRACE-007", "error", `MODIFIED ${req.id} pierde el escenario ${existing.id}: copia el bloque completo`, {
|
|
880
|
-
path:
|
|
909
|
+
path: path25,
|
|
881
910
|
line: req.line,
|
|
882
911
|
suggestion: "Copia el bloque completo de la spec viva y ed\xEDtalo; para quitarlo, decl\xE1ralo en REMOVED"
|
|
883
912
|
})
|
|
@@ -888,15 +917,15 @@ function lintDelta(delta, livingRequirements, path23, opts = {}) {
|
|
|
888
917
|
for (const req of delta.removed) {
|
|
889
918
|
const living = livingRequirements.get(req.id);
|
|
890
919
|
if (!living) {
|
|
891
|
-
out.push(diag("TRACE-007", "error", `REMOVED ${req.id} no existe en la spec viva`, { path:
|
|
920
|
+
out.push(diag("TRACE-007", "error", `REMOVED ${req.id} no existe en la spec viva`, { path: path25, line: req.line }));
|
|
892
921
|
}
|
|
893
922
|
}
|
|
894
923
|
for (const rename2 of delta.renamed) {
|
|
895
924
|
if (rename2.from.id !== rename2.to.id) {
|
|
896
|
-
out.push(diag("LINT-DLT-003", "error", `RENAMED cambia el id (${rename2.from.id} \u2192 ${rename2.to.id}); los ids son inmutables`, { path:
|
|
925
|
+
out.push(diag("LINT-DLT-003", "error", `RENAMED cambia el id (${rename2.from.id} \u2192 ${rename2.to.id}); los ids son inmutables`, { path: path25, line: rename2.line }));
|
|
897
926
|
}
|
|
898
927
|
if (!livingRequirements.has(rename2.from.id)) {
|
|
899
|
-
out.push(diag("TRACE-007", "error", `RENAMED ${rename2.from.id} no existe en la spec viva`, { path:
|
|
928
|
+
out.push(diag("TRACE-007", "error", `RENAMED ${rename2.from.id} no existe en la spec viva`, { path: path25, line: rename2.line }));
|
|
900
929
|
}
|
|
901
930
|
}
|
|
902
931
|
return out;
|
|
@@ -926,7 +955,7 @@ var MERMAID_KEYWORDS = [
|
|
|
926
955
|
"architecture-beta"
|
|
927
956
|
];
|
|
928
957
|
var MERMAID_BLOCKS = /^\s*(alt|loop|opt|par|rect|critical|break|subgraph)\b/;
|
|
929
|
-
function lintPlan(planText,
|
|
958
|
+
function lintPlan(planText, path25) {
|
|
930
959
|
const out = [];
|
|
931
960
|
const blocks = [...planText.matchAll(/```mermaid\r?\n([\s\S]*?)```/g)];
|
|
932
961
|
for (const [index, block] of blocks.entries()) {
|
|
@@ -936,7 +965,7 @@ function lintPlan(planText, path23) {
|
|
|
936
965
|
if (!MERMAID_KEYWORDS.some((keyword) => first.startsWith(keyword))) {
|
|
937
966
|
out.push(
|
|
938
967
|
diag("LINT-PLN-002", "error", `Diagrama mermaid ${index + 1}: la primera l\xEDnea debe declarar el tipo (${MERMAID_KEYWORDS.slice(0, 5).join(", ")}\u2026) y empieza por "${first.slice(0, 30)}"`, {
|
|
939
|
-
path:
|
|
968
|
+
path: path25,
|
|
940
969
|
suggestion: "Corrige el tipo del diagrama o elimina el bloque"
|
|
941
970
|
})
|
|
942
971
|
);
|
|
@@ -950,7 +979,7 @@ function lintPlan(planText, path23) {
|
|
|
950
979
|
if (open !== 0) {
|
|
951
980
|
out.push(
|
|
952
981
|
diag("LINT-PLN-002", "error", `Diagrama mermaid ${index + 1}: faltan ${Math.abs(open)} \`end\` (bloques alt/loop/subgraph sin cerrar)`, {
|
|
953
|
-
path:
|
|
982
|
+
path: path25,
|
|
954
983
|
suggestion: "Cierra cada bloque alt/loop/opt/par/rect/subgraph con `end`"
|
|
955
984
|
})
|
|
956
985
|
);
|
|
@@ -962,7 +991,7 @@ function lintPlan(planText, path23) {
|
|
|
962
991
|
if (message.includes(";")) {
|
|
963
992
|
out.push(
|
|
964
993
|
diag("LINT-PLN-003", "error", `Diagrama mermaid ${index + 1}: el mensaje "${message.trim().slice(0, 40)}\u2026" usa \`;\` y mermaid lo interpreta como fin de sentencia`, {
|
|
965
|
-
path:
|
|
994
|
+
path: path25,
|
|
966
995
|
suggestion: "Sustituye `;` por `\xB7` o `,` en los mensajes de sequenceDiagram"
|
|
967
996
|
})
|
|
968
997
|
);
|
|
@@ -1075,15 +1104,25 @@ function checkTrace(input) {
|
|
|
1075
1104
|
);
|
|
1076
1105
|
}
|
|
1077
1106
|
}
|
|
1107
|
+
const linkedIds = new Set(input.linked?.ids ?? []);
|
|
1108
|
+
const unavailableLinks = input.linked?.unavailable ?? [];
|
|
1078
1109
|
for (const task of taskById.values()) {
|
|
1079
1110
|
for (const c of task.covers) {
|
|
1080
|
-
if (!livingReqs.has(c) && !deltaScenarios.has(c) && !allScenarioIds.has(c)) {
|
|
1111
|
+
if (!livingReqs.has(c) && !deltaScenarios.has(c) && !allScenarioIds.has(c) && !linkedIds.has(c)) {
|
|
1081
1112
|
findings.push(
|
|
1082
1113
|
diag("TRACE-003", "error", `La tarea ${task.id} cubre ${c}, que no existe`, {
|
|
1083
1114
|
path: change.tasks?.path,
|
|
1084
|
-
suggestion: "Corrige el id o crea el requisito/escenario"
|
|
1115
|
+
suggestion: unavailableLinks.length > 0 ? "Corrige el id o revisa los enlaces no disponibles (`satlas link list`)" : "Corrige el id o crea el requisito/escenario"
|
|
1085
1116
|
})
|
|
1086
1117
|
);
|
|
1118
|
+
if (unavailableLinks.length > 0) {
|
|
1119
|
+
findings.push(
|
|
1120
|
+
diag("ATLAS-LINK-003", "warning", `Referencia no resuelta: ${c} (hay ${unavailableLinks.length} enlace(s) no disponible(s): ${unavailableLinks.join(", ")})`, {
|
|
1121
|
+
path: change.tasks?.path,
|
|
1122
|
+
suggestion: "Restaura la ruta del enlace o corrige la referencia"
|
|
1123
|
+
})
|
|
1124
|
+
);
|
|
1125
|
+
}
|
|
1087
1126
|
}
|
|
1088
1127
|
}
|
|
1089
1128
|
for (const dep of task.dependsOn) {
|
|
@@ -1250,12 +1289,172 @@ function planBlock(block, maxParallel) {
|
|
|
1250
1289
|
}
|
|
1251
1290
|
|
|
1252
1291
|
// src/lifecycle.ts
|
|
1292
|
+
import path3 from "path";
|
|
1293
|
+
|
|
1294
|
+
// src/contracts.ts
|
|
1253
1295
|
import path2 from "path";
|
|
1296
|
+
import { parse as parseYaml4 } from "yaml";
|
|
1297
|
+
var CONTRACTS_DIR = "contracts";
|
|
1298
|
+
var OPENAPI_METHODS = ["get", "post", "put", "patch", "delete", "head", "options"];
|
|
1299
|
+
var SUPPORTED_HINT = "Formatos esperados: OpenAPI 3.x (.yaml/.json), GraphQL SDL (.graphql/.gql) y protobuf (.proto)";
|
|
1300
|
+
function lineOf(content, needle) {
|
|
1301
|
+
const index = content.indexOf(needle);
|
|
1302
|
+
if (index < 0) return 1;
|
|
1303
|
+
return content.slice(0, index).split("\n").length;
|
|
1304
|
+
}
|
|
1305
|
+
function formatOf(file, content) {
|
|
1306
|
+
const ext = path2.extname(file).toLowerCase();
|
|
1307
|
+
if (ext === ".proto") return "protobuf";
|
|
1308
|
+
if (ext === ".graphql" || ext === ".gql") return "graphql";
|
|
1309
|
+
if (ext === ".yaml" || ext === ".yml" || ext === ".json") {
|
|
1310
|
+
return /(^|\n)\s*openapi\s*:/.test(content) || /"openapi"\s*:/.test(content) ? "openapi" : "unsupported";
|
|
1311
|
+
}
|
|
1312
|
+
return "unsupported";
|
|
1313
|
+
}
|
|
1314
|
+
function parseOpenApi(file, content) {
|
|
1315
|
+
const findings = [];
|
|
1316
|
+
let data;
|
|
1317
|
+
try {
|
|
1318
|
+
data = parseYaml4(content);
|
|
1319
|
+
} catch (error) {
|
|
1320
|
+
return { operations: [], findings: [diag("ATLAS-CONTRACT-001", "error", `Contrato OpenAPI ilegible: ${error.message}`, { path: file })] };
|
|
1321
|
+
}
|
|
1322
|
+
const doc = data;
|
|
1323
|
+
if (!doc || typeof doc !== "object" || typeof doc.openapi !== "string" || typeof doc.paths !== "object" || doc.paths === null || Array.isArray(doc.paths)) {
|
|
1324
|
+
return {
|
|
1325
|
+
operations: [],
|
|
1326
|
+
findings: [diag("ATLAS-CONTRACT-001", "error", "Contrato OpenAPI inv\xE1lido: faltan la versi\xF3n (openapi) o los caminos (paths)", { path: file, line: lineOf(content, "paths"), suggestion: SUPPORTED_HINT })]
|
|
1327
|
+
};
|
|
1328
|
+
}
|
|
1329
|
+
const operations = [];
|
|
1330
|
+
for (const [route, value] of Object.entries(doc.paths)) {
|
|
1331
|
+
if (!route.startsWith("/") || typeof value !== "object" || value === null) continue;
|
|
1332
|
+
for (const method of OPENAPI_METHODS) {
|
|
1333
|
+
if (method in value) {
|
|
1334
|
+
operations.push({ id: `${method.toUpperCase()} ${route}`, kind: "openapi", file, line: lineOf(content, route) });
|
|
1335
|
+
}
|
|
1336
|
+
}
|
|
1337
|
+
}
|
|
1338
|
+
return { operations, findings };
|
|
1339
|
+
}
|
|
1340
|
+
function parseGraphql(file, content) {
|
|
1341
|
+
const operations = [];
|
|
1342
|
+
const blockRe = /type\s+(Query|Mutation|Subscription)\s*\{([\s\S]*?)\}/g;
|
|
1343
|
+
for (const match of content.matchAll(blockRe)) {
|
|
1344
|
+
const typeName = match[1] ?? "";
|
|
1345
|
+
const body = match[2] ?? "";
|
|
1346
|
+
for (const field of body.split("\n")) {
|
|
1347
|
+
const fieldMatch = /^\s*([A-Za-z_][A-Za-z0-9_]*)\s*(\(|:)/.exec(field);
|
|
1348
|
+
if (fieldMatch) operations.push({ id: `${typeName}.${fieldMatch[1]}`, kind: "graphql", file, line: lineOf(content, field.trim()) });
|
|
1349
|
+
}
|
|
1350
|
+
}
|
|
1351
|
+
if (operations.length === 0) {
|
|
1352
|
+
return {
|
|
1353
|
+
operations,
|
|
1354
|
+
findings: [diag("ATLAS-CONTRACT-001", "error", "Contrato GraphQL inv\xE1lido: no declara consultas (type Query), mutaciones ni suscripciones", { path: file, suggestion: SUPPORTED_HINT })]
|
|
1355
|
+
};
|
|
1356
|
+
}
|
|
1357
|
+
return { operations, findings: [] };
|
|
1358
|
+
}
|
|
1359
|
+
function parseProtobuf(file, content) {
|
|
1360
|
+
const operations = [];
|
|
1361
|
+
const serviceRe = /service\s+([A-Za-z_][A-Za-z0-9_]*)\s*\{([\s\S]*?)\}/g;
|
|
1362
|
+
for (const match of content.matchAll(serviceRe)) {
|
|
1363
|
+
const service = match[1] ?? "";
|
|
1364
|
+
const body = match[2] ?? "";
|
|
1365
|
+
for (const rpc of body.matchAll(/rpc\s+([A-Za-z_][A-Za-z0-9_]*)/g)) {
|
|
1366
|
+
operations.push({ id: `${service}.${rpc[1]}`, kind: "protobuf", file, line: lineOf(content, `rpc ${rpc[1]}`) });
|
|
1367
|
+
}
|
|
1368
|
+
}
|
|
1369
|
+
if (operations.length === 0 && !/message\s+[A-Za-z_]/.test(content)) {
|
|
1370
|
+
return {
|
|
1371
|
+
operations,
|
|
1372
|
+
findings: [diag("ATLAS-CONTRACT-001", "error", "Contrato protobuf inv\xE1lido: no declara servicios ni mensajes", { path: file, suggestion: SUPPORTED_HINT })]
|
|
1373
|
+
};
|
|
1374
|
+
}
|
|
1375
|
+
return { operations, findings: [] };
|
|
1376
|
+
}
|
|
1377
|
+
function parseContract(file, content) {
|
|
1378
|
+
const format = formatOf(file, content);
|
|
1379
|
+
if (format === "unsupported") {
|
|
1380
|
+
return {
|
|
1381
|
+
path: file,
|
|
1382
|
+
format,
|
|
1383
|
+
operations: [],
|
|
1384
|
+
findings: [diag("ATLAS-CONTRACT-002", "warning", "Formato de contrato no soportado; el archivo no se interpreta ni se toca", { path: file, suggestion: SUPPORTED_HINT })]
|
|
1385
|
+
};
|
|
1386
|
+
}
|
|
1387
|
+
const parsed = format === "openapi" ? parseOpenApi(file, content) : format === "graphql" ? parseGraphql(file, content) : parseProtobuf(file, content);
|
|
1388
|
+
return { path: file, format, operations: parsed.operations, findings: parsed.findings };
|
|
1389
|
+
}
|
|
1390
|
+
async function loadContracts(changeDir) {
|
|
1391
|
+
const dir = path2.join(changeDir, CONTRACTS_DIR);
|
|
1392
|
+
const files = [];
|
|
1393
|
+
const operations = [];
|
|
1394
|
+
const findings = [];
|
|
1395
|
+
const entries = (await listDir(dir)).sort();
|
|
1396
|
+
for (const entry of entries) {
|
|
1397
|
+
const file = path2.join(dir, entry);
|
|
1398
|
+
const content = await readTextIfExists(file);
|
|
1399
|
+
if (content === void 0) continue;
|
|
1400
|
+
const parsed = parseContract(file, content);
|
|
1401
|
+
files.push({ path: parsed.path, format: parsed.format, operations: parsed.operations });
|
|
1402
|
+
operations.push(...parsed.operations);
|
|
1403
|
+
findings.push(...parsed.findings);
|
|
1404
|
+
}
|
|
1405
|
+
return { files, operations, findings };
|
|
1406
|
+
}
|
|
1407
|
+
function contractCoverage(change, mode) {
|
|
1408
|
+
const state = change.contracts;
|
|
1409
|
+
if (mode === "off" || !state || state.files.length === 0 || state.operations.length === 0) return [];
|
|
1410
|
+
const scenarioContracts = /* @__PURE__ */ new Map();
|
|
1411
|
+
for (const requirement of [...change.delta?.added ?? [], ...change.delta?.modified ?? []]) {
|
|
1412
|
+
for (const scenario of requirement.scenarios) {
|
|
1413
|
+
for (const ref of scenario.contracts ?? []) {
|
|
1414
|
+
if (!scenarioContracts.has(ref)) scenarioContracts.set(ref, scenario.id);
|
|
1415
|
+
}
|
|
1416
|
+
}
|
|
1417
|
+
}
|
|
1418
|
+
const findings = [];
|
|
1419
|
+
for (const operation of state.operations) {
|
|
1420
|
+
if (!scenarioContracts.has(operation.id)) {
|
|
1421
|
+
findings.push(
|
|
1422
|
+
diag("ATLAS-CONTRACT-003", "warning", `Operaci\xF3n sin escenario: ${operation.id}`, {
|
|
1423
|
+
path: operation.file,
|
|
1424
|
+
line: operation.line,
|
|
1425
|
+
suggestion: `Declara la operaci\xF3n en un escenario con \xB7 **Contrato**: ${operation.id}`
|
|
1426
|
+
})
|
|
1427
|
+
);
|
|
1428
|
+
}
|
|
1429
|
+
}
|
|
1430
|
+
const known = new Set(state.operations.map((operation) => operation.id));
|
|
1431
|
+
for (const [ref, scenarioId] of scenarioContracts) {
|
|
1432
|
+
if (!known.has(ref)) {
|
|
1433
|
+
findings.push(
|
|
1434
|
+
diag("ATLAS-CONTRACT-004", "warning", `Referencia rota: el escenario ${scenarioId} declara "${ref}", que no existe en el contrato`, {
|
|
1435
|
+
path: change.delta?.path,
|
|
1436
|
+
suggestion: "Corrige la referencia o a\xF1ade la operaci\xF3n al contrato"
|
|
1437
|
+
})
|
|
1438
|
+
);
|
|
1439
|
+
}
|
|
1440
|
+
}
|
|
1441
|
+
return findings;
|
|
1442
|
+
}
|
|
1443
|
+
function contractsAdvisory(change, cfg) {
|
|
1444
|
+
const state = change.contracts;
|
|
1445
|
+
if (!state || state.files.length === 0) return [];
|
|
1446
|
+
const mode = cfg.gates.contracts.mode;
|
|
1447
|
+
const form = state.findings;
|
|
1448
|
+
if (mode === "advisory") return [...form, ...contractCoverage(change, mode)];
|
|
1449
|
+
return form;
|
|
1450
|
+
}
|
|
1451
|
+
|
|
1452
|
+
// src/lifecycle.ts
|
|
1254
1453
|
function verifyApproval(change, approvals, cfg, specContent) {
|
|
1255
1454
|
if (cfg.gates.approval === "none") return { status: "not_required" };
|
|
1256
1455
|
if ((change.meta?.overrides ?? []).some((o) => o.gate === "approval")) return { status: "overridden" };
|
|
1257
1456
|
if (!change.delta) return { status: "missing" };
|
|
1258
|
-
const key =
|
|
1457
|
+
const key = path3.posix.join("changes", change.slug, "spec.md");
|
|
1259
1458
|
const approval = approvals.get(key) ?? approvals.get(`${key}`);
|
|
1260
1459
|
if (!approval) return { status: "missing" };
|
|
1261
1460
|
if (specContent === void 0) return { status: "valid", approvedBy: approval.by, approvedAt: approval.at };
|
|
@@ -1383,6 +1582,13 @@ function deriveState(input) {
|
|
|
1383
1582
|
blockedBy.push("documentaci\xF3n pendiente");
|
|
1384
1583
|
return { state: "reviewed", blockedBy, nextAction: next(`/satlas.docs ${change.slug}`, "Generar la documentaci\xF3n t\xE9cnica y manual del cambio", true), progress };
|
|
1385
1584
|
}
|
|
1585
|
+
if (cfg.gates.contracts.mode === "blocking") {
|
|
1586
|
+
const contractFindings = contractCoverage(change, "blocking");
|
|
1587
|
+
if (contractFindings.length > 0) {
|
|
1588
|
+
blockedBy.push(`contratos con hallazgos (${contractFindings.length})`);
|
|
1589
|
+
return { state: "verified", blockedBy, nextAction: next(`satlas contracts ${change.slug}`, "Resolver los hallazgos de contrato antes de archivar"), progress };
|
|
1590
|
+
}
|
|
1591
|
+
}
|
|
1386
1592
|
return { state: "ready", blockedBy, nextAction: next(`satlas archive ${change.slug}`, "Archivar el cambio y plegar los deltas"), progress };
|
|
1387
1593
|
}
|
|
1388
1594
|
function stateLabel(state) {
|
|
@@ -1404,8 +1610,8 @@ function stateLabel(state) {
|
|
|
1404
1610
|
}
|
|
1405
1611
|
|
|
1406
1612
|
// src/profiles.ts
|
|
1407
|
-
import
|
|
1408
|
-
import { parse as
|
|
1613
|
+
import path4 from "path";
|
|
1614
|
+
import { parse as parseYaml5 } from "yaml";
|
|
1409
1615
|
import { z as z3 } from "zod";
|
|
1410
1616
|
var profileSchema = z3.object({
|
|
1411
1617
|
name: z3.string().min(1),
|
|
@@ -1452,7 +1658,7 @@ async function loadProfileFile(filePath) {
|
|
|
1452
1658
|
if (raw === void 0) return void 0;
|
|
1453
1659
|
let data;
|
|
1454
1660
|
try {
|
|
1455
|
-
data =
|
|
1661
|
+
data = parseYaml5(raw);
|
|
1456
1662
|
} catch {
|
|
1457
1663
|
return void 0;
|
|
1458
1664
|
}
|
|
@@ -1464,7 +1670,7 @@ async function loadProfilesFromDir(dir) {
|
|
|
1464
1670
|
const out = [];
|
|
1465
1671
|
for (const entry of entries) {
|
|
1466
1672
|
if (!/\.ya?ml$/i.test(entry)) continue;
|
|
1467
|
-
const profile = await loadProfileFile(
|
|
1673
|
+
const profile = await loadProfileFile(path4.join(dir, entry));
|
|
1468
1674
|
if (profile) out.push(profile);
|
|
1469
1675
|
}
|
|
1470
1676
|
return out;
|
|
@@ -1502,10 +1708,10 @@ async function detectProfiles(root, profiles) {
|
|
|
1502
1708
|
return result;
|
|
1503
1709
|
}
|
|
1504
1710
|
async function loadDetectedBest(sddDir) {
|
|
1505
|
-
const raw = await readTextIfExists(
|
|
1711
|
+
const raw = await readTextIfExists(path4.join(sddDir, "profiles", "detected.yaml"));
|
|
1506
1712
|
if (raw === void 0) return void 0;
|
|
1507
1713
|
try {
|
|
1508
|
-
const data =
|
|
1714
|
+
const data = parseYaml5(raw);
|
|
1509
1715
|
return typeof data?.best === "string" ? data.best : void 0;
|
|
1510
1716
|
} catch {
|
|
1511
1717
|
return void 0;
|
|
@@ -1542,12 +1748,12 @@ function detectionToYaml(result, generatedAt) {
|
|
|
1542
1748
|
}
|
|
1543
1749
|
|
|
1544
1750
|
// src/workspace.ts
|
|
1545
|
-
import
|
|
1751
|
+
import path7 from "path";
|
|
1546
1752
|
|
|
1547
1753
|
// src/fixes.ts
|
|
1548
|
-
import
|
|
1754
|
+
import path5 from "path";
|
|
1549
1755
|
import { stringify as stringifyYaml3 } from "yaml";
|
|
1550
|
-
var LIVING_FIXES_DIR =
|
|
1756
|
+
var LIVING_FIXES_DIR = path5.join(".sdd", "fixes");
|
|
1551
1757
|
var COMMENT_RE = /<!--[\s\S]*?-->/g;
|
|
1552
1758
|
function parseFixCovers(content) {
|
|
1553
1759
|
const clean = content.replace(COMMENT_RE, "");
|
|
@@ -1574,7 +1780,7 @@ function coversOf(value) {
|
|
|
1574
1780
|
function parseLivingFix(content, file) {
|
|
1575
1781
|
const fm = parseFrontmatter(content, file);
|
|
1576
1782
|
const data = fm.data;
|
|
1577
|
-
const base =
|
|
1783
|
+
const base = path5.basename(file).replace(/\.md$/i, "");
|
|
1578
1784
|
const slug = typeof data["slug"] === "string" && data["slug"].trim() !== "" ? data["slug"].trim() : base.replace(/^\d{4}-\d{2}-/, "");
|
|
1579
1785
|
const fix = {
|
|
1580
1786
|
slug,
|
|
@@ -1590,14 +1796,14 @@ function parseLivingFix(content, file) {
|
|
|
1590
1796
|
return fix;
|
|
1591
1797
|
}
|
|
1592
1798
|
async function archivedFixes(root, known) {
|
|
1593
|
-
const archiveDir =
|
|
1799
|
+
const archiveDir = path5.join(root, ".sdd", "changes", "archive");
|
|
1594
1800
|
const fixes = [];
|
|
1595
1801
|
for (const entry of await listDirs(archiveDir)) {
|
|
1596
|
-
const dir =
|
|
1597
|
-
const fixFile =
|
|
1802
|
+
const dir = path5.join(archiveDir, entry);
|
|
1803
|
+
const fixFile = path5.join(dir, "fix.md");
|
|
1598
1804
|
const fixRaw = await readTextIfExists(fixFile);
|
|
1599
1805
|
if (fixRaw === void 0) continue;
|
|
1600
|
-
const metaFile =
|
|
1806
|
+
const metaFile = path5.join(dir, "meta.yaml");
|
|
1601
1807
|
const metaRaw = await readTextIfExists(metaFile);
|
|
1602
1808
|
const meta = metaRaw !== void 0 ? parseChangeMeta(metaRaw, metaFile).meta : void 0;
|
|
1603
1809
|
if (meta?.lane !== "fix") continue;
|
|
@@ -1620,11 +1826,11 @@ async function archivedFixes(root, known) {
|
|
|
1620
1826
|
return fixes;
|
|
1621
1827
|
}
|
|
1622
1828
|
async function loadLivingFixes(root) {
|
|
1623
|
-
const dir =
|
|
1829
|
+
const dir = path5.join(root, LIVING_FIXES_DIR);
|
|
1624
1830
|
const entries = (await listDir(dir)).filter((entry) => entry.toLowerCase().endsWith(".md")).sort();
|
|
1625
1831
|
const fixes = [];
|
|
1626
1832
|
for (const entry of entries) {
|
|
1627
|
-
const file =
|
|
1833
|
+
const file = path5.join(dir, entry);
|
|
1628
1834
|
const content = await readTextIfExists(file);
|
|
1629
1835
|
if (content === void 0) continue;
|
|
1630
1836
|
fixes.push(parseLivingFix(content, file));
|
|
@@ -1633,10 +1839,10 @@ async function loadLivingFixes(root) {
|
|
|
1633
1839
|
return fixes.sort((a, b) => b.date.localeCompare(a.date) || b.slug.localeCompare(a.slug));
|
|
1634
1840
|
}
|
|
1635
1841
|
async function writeLivingFix(root, input) {
|
|
1636
|
-
const dir =
|
|
1842
|
+
const dir = path5.join(root, LIVING_FIXES_DIR);
|
|
1637
1843
|
const month = input.date.slice(0, 7);
|
|
1638
|
-
const file =
|
|
1639
|
-
const relativePath = toPosix(
|
|
1844
|
+
const file = path5.join(dir, `${month}-${input.slug}.md`);
|
|
1845
|
+
const relativePath = toPosix(path5.relative(root, file));
|
|
1640
1846
|
if (await exists(file)) return { file, relativePath, created: false };
|
|
1641
1847
|
const data = {
|
|
1642
1848
|
slug: input.slug,
|
|
@@ -1656,35 +1862,170 @@ ${body}
|
|
|
1656
1862
|
return { file, relativePath, created: true };
|
|
1657
1863
|
}
|
|
1658
1864
|
|
|
1865
|
+
// src/links.ts
|
|
1866
|
+
import path6 from "path";
|
|
1867
|
+
import { parse as parseYaml6, stringify as stringifyYaml4 } from "yaml";
|
|
1868
|
+
var LINKS_FILE = "links.yaml";
|
|
1869
|
+
function parseLinksDocument(raw, file) {
|
|
1870
|
+
let data;
|
|
1871
|
+
try {
|
|
1872
|
+
data = parseYaml6(raw);
|
|
1873
|
+
} catch (error) {
|
|
1874
|
+
return { diagnostics: [diag("ATLAS-LINK-001", "error", `links.yaml ilegible: ${error.message}`, { path: file })] };
|
|
1875
|
+
}
|
|
1876
|
+
const doc = data;
|
|
1877
|
+
if (!doc || typeof doc !== "object" || !Array.isArray(doc.links)) {
|
|
1878
|
+
return { diagnostics: [diag("ATLAS-LINK-001", "error", "links.yaml inv\xE1lido: falta la lista de enlaces", { path: file })] };
|
|
1879
|
+
}
|
|
1880
|
+
const links = [];
|
|
1881
|
+
const diagnostics = [];
|
|
1882
|
+
for (const item of doc.links) {
|
|
1883
|
+
const entry = item;
|
|
1884
|
+
if (typeof entry?.name !== "string" || typeof entry?.path !== "string" || entry.name.trim() === "" || entry.path.trim() === "") {
|
|
1885
|
+
diagnostics.push(diag("ATLAS-LINK-001", "error", "Enlace inv\xE1lido en links.yaml: cada enlace necesita nombre y ruta", { path: file }));
|
|
1886
|
+
continue;
|
|
1887
|
+
}
|
|
1888
|
+
links.push({ name: entry.name.trim(), path: entry.path.trim() });
|
|
1889
|
+
}
|
|
1890
|
+
return { document: { schema_version: typeof doc.schema_version === "number" ? doc.schema_version : 1, links }, diagnostics };
|
|
1891
|
+
}
|
|
1892
|
+
async function readLinks(root) {
|
|
1893
|
+
const file = path6.join(root, ".sdd", LINKS_FILE);
|
|
1894
|
+
const raw = await readTextIfExists(file);
|
|
1895
|
+
if (raw === void 0) return { document: { schema_version: 1, links: [] }, file, diagnostics: [] };
|
|
1896
|
+
const parsed = parseLinksDocument(raw, file);
|
|
1897
|
+
return { document: parsed.document ?? { schema_version: 1, links: [] }, file, diagnostics: parsed.diagnostics };
|
|
1898
|
+
}
|
|
1899
|
+
async function writeLinks(root, document) {
|
|
1900
|
+
const file = path6.join(root, ".sdd", LINKS_FILE);
|
|
1901
|
+
const lines = ["# Enlaces a otros proyectos (specs compartidas en solo lectura). Generado por `satlas link`.", stringifyYaml4(document, { lineWidth: 120 }).trimEnd(), ""];
|
|
1902
|
+
await writeText(file, lines.join("\n"));
|
|
1903
|
+
}
|
|
1904
|
+
function resolveLinkPath(root, linkPath) {
|
|
1905
|
+
return path6.isAbsolute(linkPath) ? path6.normalize(linkPath) : path6.resolve(root, linkPath);
|
|
1906
|
+
}
|
|
1907
|
+
async function linkedSpecs(absPath) {
|
|
1908
|
+
const specsDir = path6.join(absPath, ".sdd", "specs");
|
|
1909
|
+
const specs = [];
|
|
1910
|
+
for (const domain of await listDirs(specsDir)) {
|
|
1911
|
+
const file = path6.join(specsDir, domain, "spec.md");
|
|
1912
|
+
const content = await readTextIfExists(file);
|
|
1913
|
+
if (content === void 0) continue;
|
|
1914
|
+
specs.push({ domain, path: file, spec: parseSpecFile(content, file) });
|
|
1915
|
+
}
|
|
1916
|
+
return specs;
|
|
1917
|
+
}
|
|
1918
|
+
async function loadLinks(root) {
|
|
1919
|
+
const { document, diagnostics } = await readLinks(root);
|
|
1920
|
+
const entries = [];
|
|
1921
|
+
const specs = [];
|
|
1922
|
+
const unavailable = [];
|
|
1923
|
+
for (const entry of document.links) {
|
|
1924
|
+
const abs = resolveLinkPath(root, entry.path);
|
|
1925
|
+
const sddDir = path6.join(abs, ".sdd");
|
|
1926
|
+
const available = await isDirectory(sddDir) && await isDirectory(path6.join(sddDir, "specs"));
|
|
1927
|
+
if (!available) {
|
|
1928
|
+
unavailable.push(entry.name);
|
|
1929
|
+
entries.push({ name: entry.name, path: abs, available: false, requirements: 0, domains: [], error: "el enlace no est\xE1 disponible" });
|
|
1930
|
+
continue;
|
|
1931
|
+
}
|
|
1932
|
+
const linked = await linkedSpecs(abs);
|
|
1933
|
+
specs.push(...linked);
|
|
1934
|
+
entries.push({
|
|
1935
|
+
name: entry.name,
|
|
1936
|
+
path: abs,
|
|
1937
|
+
available: true,
|
|
1938
|
+
requirements: linked.reduce((total, spec) => total + spec.spec.requirements.length, 0),
|
|
1939
|
+
domains: linked.map((spec) => spec.domain)
|
|
1940
|
+
});
|
|
1941
|
+
}
|
|
1942
|
+
void diagnostics;
|
|
1943
|
+
return { entries, specs, unavailable };
|
|
1944
|
+
}
|
|
1945
|
+
async function addLink(root, opts) {
|
|
1946
|
+
const abs = resolveLinkPath(root, opts.path);
|
|
1947
|
+
const sddDir = path6.join(abs, ".sdd");
|
|
1948
|
+
if (!await isDirectory(sddDir)) {
|
|
1949
|
+
return {
|
|
1950
|
+
diagnostics: [
|
|
1951
|
+
diag("ATLAS-LINK-002", "error", `El enlace no es un proyecto inicializado: ${abs}`, {
|
|
1952
|
+
suggestion: "Indica la ruta de un proyecto con su estado inicializado (satlas init)"
|
|
1953
|
+
})
|
|
1954
|
+
]
|
|
1955
|
+
};
|
|
1956
|
+
}
|
|
1957
|
+
const { document } = await readLinks(root);
|
|
1958
|
+
let name = opts.name?.trim();
|
|
1959
|
+
if (!name) {
|
|
1960
|
+
const linkedConfig = await loadConfig(sddDir);
|
|
1961
|
+
name = linkedConfig.config.project.name || path6.basename(abs);
|
|
1962
|
+
}
|
|
1963
|
+
const entry = { name, path: toPosix(abs) };
|
|
1964
|
+
const existing = document.links.findIndex((link) => link.name === name || resolveLinkPath(root, link.path) === abs);
|
|
1965
|
+
if (existing >= 0) document.links[existing] = entry;
|
|
1966
|
+
else document.links.push(entry);
|
|
1967
|
+
await writeLinks(root, document);
|
|
1968
|
+
return { entry, diagnostics: [] };
|
|
1969
|
+
}
|
|
1970
|
+
async function removeLink(root, ref) {
|
|
1971
|
+
const { document } = await readLinks(root);
|
|
1972
|
+
const index = document.links.findIndex((link) => link.name === ref || link.path === ref || resolveLinkPath(root, link.path) === resolveLinkPath(root, ref));
|
|
1973
|
+
if (index < 0) {
|
|
1974
|
+
return {
|
|
1975
|
+
diagnostics: [diag("ATLAS-LINK-003", "warning", `No existe un enlace "${ref}"`, { suggestion: "Consulta los enlaces con `satlas link list`" })]
|
|
1976
|
+
};
|
|
1977
|
+
}
|
|
1978
|
+
const [removed] = document.links.splice(index, 1);
|
|
1979
|
+
await writeLinks(root, document);
|
|
1980
|
+
return { removed: removed?.name, diagnostics: [] };
|
|
1981
|
+
}
|
|
1982
|
+
function linkedRequirementIds(state) {
|
|
1983
|
+
if (!state) return [];
|
|
1984
|
+
const ids = [];
|
|
1985
|
+
for (const spec of state.specs) {
|
|
1986
|
+
for (const requirement of spec.spec.requirements) ids.push(requirement.id);
|
|
1987
|
+
}
|
|
1988
|
+
return ids;
|
|
1989
|
+
}
|
|
1990
|
+
function linkedTraceInput(workspace) {
|
|
1991
|
+
if (!workspace.links || workspace.links.entries.length === 0) return void 0;
|
|
1992
|
+
return { ids: linkedRequirementIds(workspace.links), unavailable: workspace.links.unavailable };
|
|
1993
|
+
}
|
|
1994
|
+
async function ensureLinksFile(root) {
|
|
1995
|
+
const file = path6.join(root, ".sdd", LINKS_FILE);
|
|
1996
|
+
if (await exists(file)) return;
|
|
1997
|
+
await writeLinks(root, { schema_version: 1, links: [] });
|
|
1998
|
+
}
|
|
1999
|
+
|
|
1659
2000
|
// src/workspace.ts
|
|
1660
2001
|
var SDD_DIR = ".sdd";
|
|
1661
2002
|
async function findWorkspaceRoot(start) {
|
|
1662
|
-
let current =
|
|
2003
|
+
let current = path7.resolve(start);
|
|
1663
2004
|
for (let i = 0; i < 40; i += 1) {
|
|
1664
|
-
if (await isDirectory(
|
|
1665
|
-
const parent =
|
|
2005
|
+
if (await isDirectory(path7.join(current, SDD_DIR))) return current;
|
|
2006
|
+
const parent = path7.dirname(current);
|
|
1666
2007
|
if (parent === current) return void 0;
|
|
1667
2008
|
current = parent;
|
|
1668
2009
|
}
|
|
1669
2010
|
return void 0;
|
|
1670
2011
|
}
|
|
1671
2012
|
async function loadApprovals(sddDir) {
|
|
1672
|
-
const file =
|
|
2013
|
+
const file = path7.join(sddDir, "approvals.yaml");
|
|
1673
2014
|
const raw = await readTextIfExists(file);
|
|
1674
2015
|
if (raw === void 0) return { byArtifact: /* @__PURE__ */ new Map(), diagnostics: [] };
|
|
1675
2016
|
const parsed = parseApprovals(raw, file);
|
|
1676
2017
|
const map = /* @__PURE__ */ new Map();
|
|
1677
2018
|
for (const a of parsed.approvals?.approvals ?? []) {
|
|
1678
|
-
map.set(a.artifact.split(
|
|
2019
|
+
map.set(a.artifact.split(path7.sep).join("/"), { hash: a.artifactHash, by: a.approvedBy, at: a.approvedAt });
|
|
1679
2020
|
}
|
|
1680
2021
|
return { byArtifact: map, diagnostics: parsed.diagnostics };
|
|
1681
2022
|
}
|
|
1682
2023
|
async function loadSpecs(sddDir) {
|
|
1683
|
-
const specsDir =
|
|
2024
|
+
const specsDir = path7.join(sddDir, "specs");
|
|
1684
2025
|
const domains = await listDirs(specsDir);
|
|
1685
2026
|
const out = [];
|
|
1686
2027
|
for (const domain of domains) {
|
|
1687
|
-
const file =
|
|
2028
|
+
const file = path7.join(specsDir, domain, "spec.md");
|
|
1688
2029
|
if (!await exists(file)) continue;
|
|
1689
2030
|
const content = await readText(file);
|
|
1690
2031
|
out.push({ domain, path: file, spec: parseSpecFile(content, file) });
|
|
@@ -1692,10 +2033,10 @@ async function loadSpecs(sddDir) {
|
|
|
1692
2033
|
return out;
|
|
1693
2034
|
}
|
|
1694
2035
|
async function loadChange(root, slug, relDir) {
|
|
1695
|
-
const dir =
|
|
2036
|
+
const dir = path7.join(root, SDD_DIR, "changes", relDir ?? slug);
|
|
1696
2037
|
const diagnostics = [];
|
|
1697
2038
|
const change = { slug, dir, diagnostics };
|
|
1698
|
-
const metaFile =
|
|
2039
|
+
const metaFile = path7.join(dir, "meta.yaml");
|
|
1699
2040
|
const metaRaw = await readTextIfExists(metaFile);
|
|
1700
2041
|
if (metaRaw === void 0) {
|
|
1701
2042
|
diagnostics.push(diag("ATLAS-FILES-001", "error", `El cambio "${slug}" no tiene meta.yaml`, { path: metaFile, suggestion: "Crea meta.yaml con slug, lane y dominio" }));
|
|
@@ -1704,7 +2045,7 @@ async function loadChange(root, slug, relDir) {
|
|
|
1704
2045
|
diagnostics.push(...parsed.diagnostics);
|
|
1705
2046
|
if (parsed.meta) change.meta = parsed.meta;
|
|
1706
2047
|
}
|
|
1707
|
-
const deltaFile =
|
|
2048
|
+
const deltaFile = path7.join(dir, "spec.md");
|
|
1708
2049
|
const deltaRaw = await readTextIfExists(deltaFile);
|
|
1709
2050
|
if (deltaRaw !== void 0) {
|
|
1710
2051
|
change.delta = parseDelta(deltaRaw, deltaFile);
|
|
@@ -1712,32 +2053,32 @@ async function loadChange(root, slug, relDir) {
|
|
|
1712
2053
|
} else {
|
|
1713
2054
|
diagnostics.push(diag("ATLAS-FILES-002", "warning", `El cambio "${slug}" no tiene spec.md (delta)`, { path: deltaFile }));
|
|
1714
2055
|
}
|
|
1715
|
-
const planFile =
|
|
2056
|
+
const planFile = path7.join(dir, "plan.md");
|
|
1716
2057
|
if (await exists(planFile)) change.planPath = planFile;
|
|
1717
|
-
const reviewFile =
|
|
2058
|
+
const reviewFile = path7.join(dir, "review.md");
|
|
1718
2059
|
if (await exists(reviewFile)) change.reviewPath = reviewFile;
|
|
1719
|
-
const presentationFile =
|
|
2060
|
+
const presentationFile = path7.join(dir, "presentation", "index.html");
|
|
1720
2061
|
if (await exists(presentationFile)) change.presentationPath = presentationFile;
|
|
1721
|
-
const tasksFile =
|
|
2062
|
+
const tasksFile = path7.join(dir, "tasks.md");
|
|
1722
2063
|
const tasksRaw = await readTextIfExists(tasksFile);
|
|
1723
2064
|
if (tasksRaw !== void 0) {
|
|
1724
2065
|
change.tasks = parseTasksFile(tasksRaw, tasksFile);
|
|
1725
2066
|
diagnostics.push(...change.tasks.diagnostics);
|
|
1726
2067
|
}
|
|
1727
|
-
const verifyFile =
|
|
2068
|
+
const verifyFile = path7.join(dir, "verify.md");
|
|
1728
2069
|
const verifyRaw = await readTextIfExists(verifyFile);
|
|
1729
2070
|
if (verifyRaw !== void 0) {
|
|
1730
2071
|
change.verify = parseVerifyFile(verifyRaw, verifyFile);
|
|
1731
2072
|
diagnostics.push(...change.verify.diagnostics);
|
|
1732
2073
|
}
|
|
1733
|
-
const fixFile =
|
|
2074
|
+
const fixFile = path7.join(dir, "fix.md");
|
|
1734
2075
|
const fixRaw = await readTextIfExists(fixFile);
|
|
1735
2076
|
if (fixRaw !== void 0) {
|
|
1736
2077
|
change.fix = parseVerifyFile(fixRaw, fixFile);
|
|
1737
2078
|
diagnostics.push(...change.fix.diagnostics);
|
|
1738
2079
|
change.fixCovers = parseFixCovers(fixRaw);
|
|
1739
2080
|
}
|
|
1740
|
-
const clarifyFile =
|
|
2081
|
+
const clarifyFile = path7.join(dir, "clarify.md");
|
|
1741
2082
|
const clarifyRaw = await readTextIfExists(clarifyFile);
|
|
1742
2083
|
if (clarifyRaw !== void 0) {
|
|
1743
2084
|
change.clarify = parseClarify(clarifyRaw, clarifyFile);
|
|
@@ -1746,21 +2087,23 @@ async function loadChange(root, slug, relDir) {
|
|
|
1746
2087
|
}
|
|
1747
2088
|
const docsPaths = [];
|
|
1748
2089
|
for (const name of ["tecnica.md", "manual.md"]) {
|
|
1749
|
-
const docFile =
|
|
2090
|
+
const docFile = path7.join(dir, "docs", name);
|
|
1750
2091
|
if (await exists(docFile)) docsPaths.push(docFile);
|
|
1751
2092
|
}
|
|
1752
2093
|
if (docsPaths.length > 0) change.docsPaths = docsPaths;
|
|
1753
|
-
const
|
|
2094
|
+
const contracts = await loadContracts(dir);
|
|
2095
|
+
if (contracts.files.length > 0) change.contracts = contracts;
|
|
2096
|
+
const mockupManifest = path7.join(dir, "mockups", "manifest.yaml");
|
|
1754
2097
|
if (await exists(mockupManifest)) change.mockupManifestPath = mockupManifest;
|
|
1755
2098
|
return change;
|
|
1756
2099
|
}
|
|
1757
2100
|
async function listChangeSlugs(root, opts = {}) {
|
|
1758
|
-
const changesDir =
|
|
2101
|
+
const changesDir = path7.join(root, SDD_DIR, "changes");
|
|
1759
2102
|
const dirs = await listDirs(changesDir);
|
|
1760
2103
|
return dirs.filter((d) => opts.includeArchived ? true : d !== "archive");
|
|
1761
2104
|
}
|
|
1762
2105
|
async function loadWorkspace(root) {
|
|
1763
|
-
const sddDir =
|
|
2106
|
+
const sddDir = path7.join(root, SDD_DIR);
|
|
1764
2107
|
const { config, diagnostics: configDiags } = await loadConfig(sddDir);
|
|
1765
2108
|
const diagnostics = [...configDiags];
|
|
1766
2109
|
const specs = await loadSpecs(sddDir);
|
|
@@ -1773,20 +2116,24 @@ async function loadWorkspace(root) {
|
|
|
1773
2116
|
diagnostics.push(...change.diagnostics);
|
|
1774
2117
|
}
|
|
1775
2118
|
const archived = [];
|
|
1776
|
-
const archivedDir =
|
|
2119
|
+
const archivedDir = path7.join(sddDir, "changes", "archive");
|
|
1777
2120
|
if (await isDirectory(archivedDir)) {
|
|
1778
2121
|
for (const entry of await listDirs(archivedDir)) {
|
|
1779
|
-
archived.push(await loadChange(root, entry.replace(/^\d{4}-\d{2}-/, ""),
|
|
2122
|
+
archived.push(await loadChange(root, entry.replace(/^\d{4}-\d{2}-/, ""), path7.join("archive", entry)));
|
|
1780
2123
|
}
|
|
1781
2124
|
}
|
|
1782
|
-
|
|
2125
|
+
const workspace = { root, sddDir, specs, changes, archived, diagnostics };
|
|
2126
|
+
if (await exists(path7.join(sddDir, LINKS_FILE))) {
|
|
2127
|
+
workspace.links = await loadLinks(root);
|
|
2128
|
+
}
|
|
2129
|
+
return { workspace, config };
|
|
1783
2130
|
}
|
|
1784
2131
|
async function ensureSddDirs(sddDir) {
|
|
1785
2132
|
const created = [];
|
|
1786
|
-
const dirs = ["specs", "changes", "runs", "metrics", "fixes",
|
|
2133
|
+
const dirs = ["specs", "changes", "runs", "metrics", "fixes", path7.join("profiles", "custom")];
|
|
1787
2134
|
const { ensureDir: ensureDir2 } = await import("./fsx-VF2P7ALA.js");
|
|
1788
2135
|
for (const d of dirs) {
|
|
1789
|
-
const full =
|
|
2136
|
+
const full = path7.join(sddDir, d);
|
|
1790
2137
|
if (!await exists(full)) {
|
|
1791
2138
|
await ensureDir2(full);
|
|
1792
2139
|
created.push(full);
|
|
@@ -1796,7 +2143,7 @@ async function ensureSddDirs(sddDir) {
|
|
|
1796
2143
|
}
|
|
1797
2144
|
|
|
1798
2145
|
// src/templates.ts
|
|
1799
|
-
import { stringify as
|
|
2146
|
+
import { stringify as stringifyYaml5 } from "yaml";
|
|
1800
2147
|
var ES = {
|
|
1801
2148
|
constitution: `# Constituci\xF3n del proyecto
|
|
1802
2149
|
|
|
@@ -2188,7 +2535,7 @@ function changeMetaYaml(meta) {
|
|
|
2188
2535
|
if (meta.owner) doc["owner"] = meta.owner;
|
|
2189
2536
|
if (meta.mockups) doc["mockups"] = meta.mockups;
|
|
2190
2537
|
return `# Estado del cambio. La fase se DERIVA de los artefactos; aqu\xED solo hechos.
|
|
2191
|
-
` +
|
|
2538
|
+
` + stringifyYaml5(doc, { lineWidth: 120 });
|
|
2192
2539
|
}
|
|
2193
2540
|
function indexMarkdown(input) {
|
|
2194
2541
|
const es = input.language !== "en";
|
|
@@ -2226,8 +2573,8 @@ ${input.archived} change(s) in \`changes/archive/\`.`);
|
|
|
2226
2573
|
}
|
|
2227
2574
|
|
|
2228
2575
|
// src/github.ts
|
|
2229
|
-
import
|
|
2230
|
-
import { parse as
|
|
2576
|
+
import path8 from "path";
|
|
2577
|
+
import { parse as parseYaml7 } from "yaml";
|
|
2231
2578
|
|
|
2232
2579
|
// src/exec.ts
|
|
2233
2580
|
import { execFile } from "child_process";
|
|
@@ -2371,8 +2718,8 @@ function issueBody(input) {
|
|
|
2371
2718
|
lines.push(`- Siguiente: \`${input.next}\` \u2014 ${input.nextDescription}`);
|
|
2372
2719
|
lines.push("");
|
|
2373
2720
|
lines.push("### Artefactos");
|
|
2374
|
-
lines.push(`- Spec: \`${
|
|
2375
|
-
if (change.planPath) lines.push(`- Plan: \`${
|
|
2721
|
+
lines.push(`- Spec: \`${path8.posix.join(".sdd", "changes", change.slug, "spec.md")}\``);
|
|
2722
|
+
if (change.planPath) lines.push(`- Plan: \`${path8.posix.join(".sdd", "changes", change.slug, "plan.md")}\``);
|
|
2376
2723
|
if (change.tasks) lines.push(`- Tareas: ${change.tasks.counts.done}/${change.tasks.counts.total}`);
|
|
2377
2724
|
if (change.verify) lines.push(`- Evidencia registrada: ${change.verify.evidence.length}`);
|
|
2378
2725
|
lines.push("");
|
|
@@ -2440,14 +2787,14 @@ async function commentIssue(root, number, body, runner = defaultRunner) {
|
|
|
2440
2787
|
return result.ok ? [] : [diag("ATLAS-GH-006", "error", `No se pudo comentar el issue #${number}: ${(result.stderr || result.stdout).trim().slice(0, 300)}`)];
|
|
2441
2788
|
}
|
|
2442
2789
|
async function patchChangeMeta(root, slug, patch) {
|
|
2443
|
-
const file =
|
|
2790
|
+
const file = path8.join(path8.resolve(root), ".sdd", "changes", slug, "meta.yaml");
|
|
2444
2791
|
const raw = await readTextIfExists(file);
|
|
2445
2792
|
if (raw === void 0) {
|
|
2446
|
-
return { path: file, diagnostics: [diag("ATLAS-GH-007", "error", `No existe ${
|
|
2793
|
+
return { path: file, diagnostics: [diag("ATLAS-GH-007", "error", `No existe ${path8.relative(root, file)}`)] };
|
|
2447
2794
|
}
|
|
2448
2795
|
let data;
|
|
2449
2796
|
try {
|
|
2450
|
-
data =
|
|
2797
|
+
data = parseYaml7(raw) ?? {};
|
|
2451
2798
|
} catch (err) {
|
|
2452
2799
|
return { path: file, diagnostics: [diag("ATLAS-GH-007", "error", `meta.yaml inv\xE1lido: ${err.message}`)] };
|
|
2453
2800
|
}
|
|
@@ -2497,7 +2844,7 @@ async function approveFromGithub(opts) {
|
|
|
2497
2844
|
const { signApproval: signApproval2 } = await import("./approvals-2PO6223I.js");
|
|
2498
2845
|
const signed = await signApproval2({
|
|
2499
2846
|
root: opts.root,
|
|
2500
|
-
artifact:
|
|
2847
|
+
artifact: path8.posix.join("changes", opts.slug, "spec.md"),
|
|
2501
2848
|
by,
|
|
2502
2849
|
channel: "tracker",
|
|
2503
2850
|
note: `Aprobado v\xEDa GitHub issue #${number} (etiqueta "${label}")`
|
|
@@ -2557,7 +2904,7 @@ async function syncGithubIssue(root, slug, opts = {}) {
|
|
|
2557
2904
|
}
|
|
2558
2905
|
|
|
2559
2906
|
// src/evidence.ts
|
|
2560
|
-
import
|
|
2907
|
+
import path9 from "path";
|
|
2561
2908
|
var SCENARIO_HEAD_RE2 = /^###\s+(REQ-[A-Z0-9-]+-S\d+)\b/;
|
|
2562
2909
|
function locateBlocks(content) {
|
|
2563
2910
|
const normalized = content.replace(/\r\n?/g, "\n");
|
|
@@ -2595,10 +2942,10 @@ function buildEvidenceYaml(evidence) {
|
|
|
2595
2942
|
return lines.join("\n");
|
|
2596
2943
|
}
|
|
2597
2944
|
async function recordEvidence(opts) {
|
|
2598
|
-
const root =
|
|
2945
|
+
const root = path9.resolve(opts.root);
|
|
2599
2946
|
const slug = opts.slug;
|
|
2600
2947
|
const fileKind = opts.file ?? "verify";
|
|
2601
|
-
const file =
|
|
2948
|
+
const file = path9.join(root, ".sdd", "changes", slug, fileKind === "fix" ? "fix.md" : "verify.md");
|
|
2602
2949
|
const diagnostics = [];
|
|
2603
2950
|
const scenario = opts.scenario.toUpperCase();
|
|
2604
2951
|
if (!SCENARIO_ID_RE.test(scenario)) {
|
|
@@ -2695,7 +3042,7 @@ function evidenceSummary(content) {
|
|
|
2695
3042
|
}
|
|
2696
3043
|
|
|
2697
3044
|
// src/runs.ts
|
|
2698
|
-
import
|
|
3045
|
+
import path10 from "path";
|
|
2699
3046
|
import { randomBytes } from "crypto";
|
|
2700
3047
|
var RUN_EVENT_TYPES = [
|
|
2701
3048
|
"run_started",
|
|
@@ -2721,7 +3068,7 @@ function generateRunId(now = /* @__PURE__ */ new Date()) {
|
|
|
2721
3068
|
return `${iso.slice(0, 8)}-${iso.slice(8, 14)}-${randomBytes(3).toString("hex")}`;
|
|
2722
3069
|
}
|
|
2723
3070
|
function runsDir(root) {
|
|
2724
|
-
return
|
|
3071
|
+
return path10.join(path10.resolve(root), ".sdd", "runs");
|
|
2725
3072
|
}
|
|
2726
3073
|
async function createRun(root, slug, phase, inputs, now = /* @__PURE__ */ new Date()) {
|
|
2727
3074
|
const runId = generateRunId(now);
|
|
@@ -2734,25 +3081,25 @@ async function createRun(root, slug, phase, inputs, now = /* @__PURE__ */ new Da
|
|
|
2734
3081
|
updatedAt: localStamp(now)
|
|
2735
3082
|
};
|
|
2736
3083
|
if (inputs) state.inputs = inputs;
|
|
2737
|
-
const dir =
|
|
3084
|
+
const dir = path10.join(runsDir(root), runId);
|
|
2738
3085
|
await ensureDir(dir);
|
|
2739
|
-
await writeText(
|
|
3086
|
+
await writeText(path10.join(dir, "state.json"), `${JSON.stringify(state, null, 2)}
|
|
2740
3087
|
`);
|
|
2741
|
-
await writeText(
|
|
3088
|
+
await writeText(path10.join(dir, "events.jsonl"), "");
|
|
2742
3089
|
await appendRunEvent(root, runId, "run_started", { slug, phase });
|
|
2743
3090
|
return state;
|
|
2744
3091
|
}
|
|
2745
3092
|
async function appendRunEvent(root, runId, type, data, now = /* @__PURE__ */ new Date()) {
|
|
2746
3093
|
const event = { eventId: randomBytes(4).toString("hex"), at: localStamp(now), type };
|
|
2747
3094
|
if (data) event.data = data;
|
|
2748
|
-
const file =
|
|
3095
|
+
const file = path10.join(runsDir(root), runId, "events.jsonl");
|
|
2749
3096
|
const previous = await readTextIfExists(file) ?? "";
|
|
2750
3097
|
await writeText(file, `${previous}${JSON.stringify(event)}
|
|
2751
3098
|
`);
|
|
2752
3099
|
return event;
|
|
2753
3100
|
}
|
|
2754
3101
|
async function updateRunStatus(root, runId, status, now = /* @__PURE__ */ new Date()) {
|
|
2755
|
-
const file =
|
|
3102
|
+
const file = path10.join(runsDir(root), runId, "state.json");
|
|
2756
3103
|
const raw = await readTextIfExists(file);
|
|
2757
3104
|
if (raw === void 0) return;
|
|
2758
3105
|
const state = JSON.parse(raw);
|
|
@@ -2762,13 +3109,13 @@ async function updateRunStatus(root, runId, status, now = /* @__PURE__ */ new Da
|
|
|
2762
3109
|
`);
|
|
2763
3110
|
}
|
|
2764
3111
|
async function readRun(root, runId) {
|
|
2765
|
-
const dir =
|
|
2766
|
-
const stateRaw = await readTextIfExists(
|
|
3112
|
+
const dir = path10.join(runsDir(root), runId);
|
|
3113
|
+
const stateRaw = await readTextIfExists(path10.join(dir, "state.json"));
|
|
2767
3114
|
if (stateRaw === void 0) {
|
|
2768
3115
|
return { diagnostics: [diag("ATLAS-RUN-001", "error", `No existe el run ${runId}`, { path: dir })] };
|
|
2769
3116
|
}
|
|
2770
3117
|
const state = JSON.parse(stateRaw);
|
|
2771
|
-
const eventsRaw = await readTextIfExists(
|
|
3118
|
+
const eventsRaw = await readTextIfExists(path10.join(dir, "events.jsonl")) ?? "";
|
|
2772
3119
|
const events = [];
|
|
2773
3120
|
for (const line of eventsRaw.split("\n")) {
|
|
2774
3121
|
if (line.trim() === "") continue;
|
|
@@ -2785,7 +3132,7 @@ async function listRuns(root, filter = {}) {
|
|
|
2785
3132
|
const ids = await listDirs(dir);
|
|
2786
3133
|
const out = [];
|
|
2787
3134
|
for (const id of ids) {
|
|
2788
|
-
const raw = await readTextIfExists(
|
|
3135
|
+
const raw = await readTextIfExists(path10.join(dir, id, "state.json"));
|
|
2789
3136
|
if (raw === void 0) continue;
|
|
2790
3137
|
try {
|
|
2791
3138
|
const state = JSON.parse(raw);
|
|
@@ -2799,11 +3146,11 @@ async function listRuns(root, filter = {}) {
|
|
|
2799
3146
|
}
|
|
2800
3147
|
|
|
2801
3148
|
// src/analyze.ts
|
|
2802
|
-
import
|
|
3149
|
+
import path13 from "path";
|
|
2803
3150
|
|
|
2804
3151
|
// src/mockups.ts
|
|
2805
|
-
import
|
|
2806
|
-
import { parse as
|
|
3152
|
+
import path11 from "path";
|
|
3153
|
+
import { parse as parseYaml8, stringify as stringifyYaml6 } from "yaml";
|
|
2807
3154
|
import { z as z4 } from "zod";
|
|
2808
3155
|
var mockupManifestSchema = z4.object({
|
|
2809
3156
|
schema_version: z4.number().int().positive().default(1),
|
|
@@ -2828,20 +3175,20 @@ var mockupManifestSchema = z4.object({
|
|
|
2828
3175
|
});
|
|
2829
3176
|
var TOKEN_CANDIDATES = ["design/tokens.json", "DESIGN.md", "design/DESIGN.md", ".sdd/design/tokens.json"];
|
|
2830
3177
|
function mockupsDir(root, slug) {
|
|
2831
|
-
return
|
|
3178
|
+
return path11.join(path11.resolve(root), ".sdd", "changes", slug, "mockups");
|
|
2832
3179
|
}
|
|
2833
3180
|
async function tokensFile(root) {
|
|
2834
3181
|
for (const candidate of TOKEN_CANDIDATES) {
|
|
2835
|
-
const abs =
|
|
3182
|
+
const abs = path11.join(root, candidate);
|
|
2836
3183
|
if (await exists(abs)) return candidate;
|
|
2837
3184
|
}
|
|
2838
3185
|
return void 0;
|
|
2839
3186
|
}
|
|
2840
3187
|
async function computeInputsHash(root, change) {
|
|
2841
|
-
const deltaPath =
|
|
3188
|
+
const deltaPath = path11.join(change.dir, "spec.md");
|
|
2842
3189
|
const delta = await readTextIfExists(deltaPath) ?? "";
|
|
2843
3190
|
const tokens = await tokensFile(root);
|
|
2844
|
-
const tokensContent = tokens ? await readTextIfExists(
|
|
3191
|
+
const tokensContent = tokens ? await readTextIfExists(path11.join(root, tokens)) ?? "" : "";
|
|
2845
3192
|
return artifactHash(`${delta}
|
|
2846
3193
|
---tokens---
|
|
2847
3194
|
${tokensContent}`);
|
|
@@ -2866,12 +3213,12 @@ function planMockups(change, platform = "web") {
|
|
|
2866
3213
|
return { platform, screens };
|
|
2867
3214
|
}
|
|
2868
3215
|
async function readMockupManifest(root, slug) {
|
|
2869
|
-
const file =
|
|
3216
|
+
const file = path11.join(mockupsDir(root, slug), "manifest.yaml");
|
|
2870
3217
|
const raw = await readTextIfExists(file);
|
|
2871
3218
|
if (raw === void 0) return { diagnostics: [] };
|
|
2872
3219
|
let data;
|
|
2873
3220
|
try {
|
|
2874
|
-
data =
|
|
3221
|
+
data = parseYaml8(raw);
|
|
2875
3222
|
} catch (err) {
|
|
2876
3223
|
return { path: file, diagnostics: [diag("ATLAS-MKP-002", "error", `manifest.yaml inv\xE1lido: ${err.message}`, { path: file })] };
|
|
2877
3224
|
}
|
|
@@ -2958,7 +3305,7 @@ async function checkMockups(root, slug, change) {
|
|
|
2958
3305
|
const { manifest, path: manifestPath, diagnostics } = await readMockupManifest(root, slug);
|
|
2959
3306
|
const findings = [...diagnostics];
|
|
2960
3307
|
if (!manifest || !manifestPath) {
|
|
2961
|
-
return { findings: [...findings, diag("LINT-MKP-004", "error", "No existe mockups/manifest.yaml", { path:
|
|
3308
|
+
return { findings: [...findings, diag("LINT-MKP-004", "error", "No existe mockups/manifest.yaml", { path: path11.join(dir, "manifest.yaml") })], stale: false };
|
|
2962
3309
|
}
|
|
2963
3310
|
const known = /* @__PURE__ */ new Set();
|
|
2964
3311
|
for (const req of [...change.delta?.added ?? [], ...change.delta?.modified ?? []]) {
|
|
@@ -2966,12 +3313,12 @@ async function checkMockups(root, slug, change) {
|
|
|
2966
3313
|
}
|
|
2967
3314
|
findings.push(...lintMockupManifest(manifest, known, manifestPath));
|
|
2968
3315
|
for (const screen of manifest.screens) {
|
|
2969
|
-
const html = await readTextIfExists(
|
|
3316
|
+
const html = await readTextIfExists(path11.join(dir, screen.file));
|
|
2970
3317
|
if (html === void 0) {
|
|
2971
|
-
findings.push(diag("ATLAS-MKP-004", "error", `Falta el archivo del mockup: ${screen.file}`, { path:
|
|
3318
|
+
findings.push(diag("ATLAS-MKP-004", "error", `Falta el archivo del mockup: ${screen.file}`, { path: path11.join(dir, screen.file) }));
|
|
2972
3319
|
continue;
|
|
2973
3320
|
}
|
|
2974
|
-
findings.push(...lintMockupHtml(html,
|
|
3321
|
+
findings.push(...lintMockupHtml(html, path11.join(dir, screen.file)));
|
|
2975
3322
|
}
|
|
2976
3323
|
const inputsHash = await computeInputsHash(root, change);
|
|
2977
3324
|
const stale = manifest.inputsHash !== void 0 && manifest.inputsHash !== inputsHash;
|
|
@@ -2986,7 +3333,7 @@ async function checkMockups(root, slug, change) {
|
|
|
2986
3333
|
return { findings, stale, manifest, manifestPath };
|
|
2987
3334
|
}
|
|
2988
3335
|
async function writeMockupPlan(root, slug, plan, now = /* @__PURE__ */ new Date()) {
|
|
2989
|
-
const file =
|
|
3336
|
+
const file = path11.join(mockupsDir(root, slug), "plan.yaml");
|
|
2990
3337
|
if (await exists(file)) return file;
|
|
2991
3338
|
const doc = {
|
|
2992
3339
|
schema_version: 1,
|
|
@@ -2994,11 +3341,11 @@ async function writeMockupPlan(root, slug, plan, now = /* @__PURE__ */ new Date(
|
|
|
2994
3341
|
platform: plan.platform,
|
|
2995
3342
|
screens: plan.screens.map((s) => ({ id: s.id, title: s.title, file: s.file, illustrates: s.illustrates, states: s.states, breakpoints: s.breakpoints }))
|
|
2996
3343
|
};
|
|
2997
|
-
await writeText(file,
|
|
3344
|
+
await writeText(file, stringifyYaml6(doc, { lineWidth: 120 }));
|
|
2998
3345
|
return file;
|
|
2999
3346
|
}
|
|
3000
3347
|
async function writeMockupManifest(root, slug, plan, inputsHash, now = /* @__PURE__ */ new Date()) {
|
|
3001
|
-
const file =
|
|
3348
|
+
const file = path11.join(mockupsDir(root, slug), "manifest.yaml");
|
|
3002
3349
|
if (await exists(file)) return file;
|
|
3003
3350
|
const doc = {
|
|
3004
3351
|
schema_version: 1,
|
|
@@ -3010,16 +3357,16 @@ async function writeMockupManifest(root, slug, plan, inputsHash, now = /* @__PUR
|
|
|
3010
3357
|
screens: plan.screens.map((s) => ({ id: s.id, title: s.title, file: s.file, illustrates: s.illustrates, states: s.states, breakpoints: s.breakpoints })),
|
|
3011
3358
|
screenshots: []
|
|
3012
3359
|
};
|
|
3013
|
-
await writeText(file,
|
|
3360
|
+
await writeText(file, stringifyYaml6(doc, { lineWidth: 120 }));
|
|
3014
3361
|
return file;
|
|
3015
3362
|
}
|
|
3016
3363
|
async function updateMockupScreenshots(root, slug, screenshots) {
|
|
3017
|
-
const file =
|
|
3364
|
+
const file = path11.join(mockupsDir(root, slug), "manifest.yaml");
|
|
3018
3365
|
const raw = await readTextIfExists(file);
|
|
3019
3366
|
if (raw === void 0) return void 0;
|
|
3020
|
-
const data =
|
|
3367
|
+
const data = parseYaml8(raw) ?? {};
|
|
3021
3368
|
data["screenshots"] = screenshots;
|
|
3022
|
-
await writeText(file,
|
|
3369
|
+
await writeText(file, stringifyYaml6(data, { lineWidth: 120 }));
|
|
3023
3370
|
return file;
|
|
3024
3371
|
}
|
|
3025
3372
|
async function captureMockups(root, slug, manifest, now = /* @__PURE__ */ new Date()) {
|
|
@@ -3044,9 +3391,9 @@ async function captureMockups(root, slug, manifest, now = /* @__PURE__ */ new Da
|
|
|
3044
3391
|
const page2 = await browser.newPage();
|
|
3045
3392
|
for (const bp of screen.breakpoints && screen.breakpoints.length > 0 ? screen.breakpoints : [1440]) {
|
|
3046
3393
|
await page2.setViewportSize({ width: bp, height: 900 });
|
|
3047
|
-
await page2.goto(`file://${
|
|
3048
|
-
const rel =
|
|
3049
|
-
await page2.screenshot({ path:
|
|
3394
|
+
await page2.goto(`file://${path11.join(dir, screen.file).split(path11.sep).join("/")}`);
|
|
3395
|
+
const rel = path11.posix.join("screens", `${screen.id}-${bp}.png`);
|
|
3396
|
+
await page2.screenshot({ path: path11.join(dir, rel) });
|
|
3050
3397
|
screenshots.push(rel);
|
|
3051
3398
|
}
|
|
3052
3399
|
await page2.close();
|
|
@@ -3062,7 +3409,7 @@ async function mockupsReady(root, slug, change) {
|
|
|
3062
3409
|
return Boolean(check.manifest && check.manifest.screens.length > 0 && !check.stale);
|
|
3063
3410
|
}
|
|
3064
3411
|
async function setMockupRequirement(root, slug, value) {
|
|
3065
|
-
const file =
|
|
3412
|
+
const file = path11.join(root, ".sdd", "changes", slug, "meta.yaml");
|
|
3066
3413
|
const raw = await readTextIfExists(file) ?? `schema_version: 1
|
|
3067
3414
|
slug: ${slug}
|
|
3068
3415
|
lane: standard
|
|
@@ -3081,8 +3428,8 @@ lane: standard
|
|
|
3081
3428
|
}
|
|
3082
3429
|
|
|
3083
3430
|
// src/packs.ts
|
|
3084
|
-
import
|
|
3085
|
-
import { parse as
|
|
3431
|
+
import path12 from "path";
|
|
3432
|
+
import { parse as parseYaml9 } from "yaml";
|
|
3086
3433
|
import { z as z5 } from "zod";
|
|
3087
3434
|
var checkSchema = z5.object({
|
|
3088
3435
|
id: z5.string().min(1),
|
|
@@ -3297,17 +3644,17 @@ ${texts.scenarios}`);
|
|
|
3297
3644
|
}
|
|
3298
3645
|
}
|
|
3299
3646
|
async function loadProjectPacks(sddDir) {
|
|
3300
|
-
const dir =
|
|
3647
|
+
const dir = path12.join(sddDir, "packs");
|
|
3301
3648
|
const diagnostics = [];
|
|
3302
3649
|
const packs = [];
|
|
3303
3650
|
for (const entry of await listDir(dir)) {
|
|
3304
3651
|
if (!/\.ya?ml$/i.test(entry)) continue;
|
|
3305
|
-
const file =
|
|
3652
|
+
const file = path12.join(dir, entry);
|
|
3306
3653
|
const raw = await readTextIfExists(file);
|
|
3307
3654
|
if (raw === void 0) continue;
|
|
3308
3655
|
let data;
|
|
3309
3656
|
try {
|
|
3310
|
-
data =
|
|
3657
|
+
data = parseYaml9(raw);
|
|
3311
3658
|
} catch (err) {
|
|
3312
3659
|
diagnostics.push(diag("PACK-000", "error", `Pack inv\xE1lido (${entry}): ${err.message}`, { path: file }));
|
|
3313
3660
|
continue;
|
|
@@ -3391,7 +3738,7 @@ function packFindings(evaluations, change) {
|
|
|
3391
3738
|
// src/analyze.ts
|
|
3392
3739
|
var UI_DOMAINS = /* @__PURE__ */ new Set(["frontend", "mobile", "fullstack"]);
|
|
3393
3740
|
async function runAnalyze(opts) {
|
|
3394
|
-
const root =
|
|
3741
|
+
const root = path13.resolve(opts.root);
|
|
3395
3742
|
const { workspace, config } = await loadWorkspace(root);
|
|
3396
3743
|
const change = workspace.changes.find((c) => c.slug === opts.slug);
|
|
3397
3744
|
if (!change) {
|
|
@@ -3409,9 +3756,9 @@ async function runAnalyze(opts) {
|
|
|
3409
3756
|
for (const req of spec.spec.requirements) living.set(req.id, req);
|
|
3410
3757
|
}
|
|
3411
3758
|
if (change.delta) {
|
|
3412
|
-
findings.push(...lintDelta(change.delta, living,
|
|
3759
|
+
findings.push(...lintDelta(change.delta, living, path13.join(change.dir, "spec.md"), { language: config.spec.language }));
|
|
3413
3760
|
}
|
|
3414
|
-
const trace = checkTrace({ specs: workspace.specs, change, requireEvidence: false });
|
|
3761
|
+
const trace = checkTrace({ specs: workspace.specs, change, requireEvidence: false, linked: linkedTraceInput(workspace) });
|
|
3415
3762
|
findings.push(...trace.findings);
|
|
3416
3763
|
let waves;
|
|
3417
3764
|
if (change.tasks && change.tasks.counts.total > 0) {
|
|
@@ -3458,7 +3805,7 @@ async function runAnalyze(opts) {
|
|
|
3458
3805
|
if (mockups) result.mockups = mockups;
|
|
3459
3806
|
if (packs) result.packs = packs;
|
|
3460
3807
|
if (opts.write !== false) {
|
|
3461
|
-
const file =
|
|
3808
|
+
const file = path13.join(change.dir, "analyze.md");
|
|
3462
3809
|
await writeText(file, renderAnalyze(change, result, localStamp(opts.now)));
|
|
3463
3810
|
result.path = file;
|
|
3464
3811
|
}
|
|
@@ -3501,9 +3848,9 @@ function renderAnalyze(change, result, generatedAt) {
|
|
|
3501
3848
|
}
|
|
3502
3849
|
|
|
3503
3850
|
// src/metrics.ts
|
|
3504
|
-
import
|
|
3851
|
+
import path14 from "path";
|
|
3505
3852
|
async function collectMetrics(root, now = /* @__PURE__ */ new Date()) {
|
|
3506
|
-
const resolved =
|
|
3853
|
+
const resolved = path14.resolve(root);
|
|
3507
3854
|
const { workspace, config } = await loadWorkspace(resolved);
|
|
3508
3855
|
const approvals = await loadApprovals(workspace.sddDir);
|
|
3509
3856
|
const living = /* @__PURE__ */ new Map();
|
|
@@ -3518,11 +3865,12 @@ async function collectMetrics(root, now = /* @__PURE__ */ new Date()) {
|
|
|
3518
3865
|
const deltaContent = await readDelta(change);
|
|
3519
3866
|
const approval = verifyApproval(change, approvals.byArtifact, config, deltaContent);
|
|
3520
3867
|
const findings = [];
|
|
3521
|
-
if (change.delta) findings.push(...lintDelta(change.delta, living,
|
|
3868
|
+
if (change.delta) findings.push(...lintDelta(change.delta, living, path14.join(change.dir, "spec.md"), { language: config.spec.language }));
|
|
3522
3869
|
const trace = checkTrace({
|
|
3523
3870
|
specs: workspace.specs,
|
|
3524
3871
|
change,
|
|
3525
|
-
requireEvidence: config.gates.verify.mode !== "off" && config.gates.verify.require_evidence
|
|
3872
|
+
requireEvidence: config.gates.verify.mode !== "off" && config.gates.verify.require_evidence,
|
|
3873
|
+
linked: linkedTraceInput(workspace)
|
|
3526
3874
|
});
|
|
3527
3875
|
findings.push(...trace.findings);
|
|
3528
3876
|
const state = deriveState({ change, cfg: config, approval, blockingFindings: change.delta ? countBySeverity(findings).errors : 0 });
|
|
@@ -3583,7 +3931,7 @@ async function collectMetrics(root, now = /* @__PURE__ */ new Date()) {
|
|
|
3583
3931
|
attention.push({ slug: change.slug, kind: "missing-evidence", detail: `evidencia ${change.scenariosPassed}/${change.scenariosTotal}` });
|
|
3584
3932
|
}
|
|
3585
3933
|
}
|
|
3586
|
-
const archiveDir =
|
|
3934
|
+
const archiveDir = path14.join(workspace.sddDir, "changes", "archive");
|
|
3587
3935
|
const archivedEntries = await listDirs(archiveDir);
|
|
3588
3936
|
const throughputByMonth = {};
|
|
3589
3937
|
for (const entry of archivedEntries) {
|
|
@@ -3621,15 +3969,15 @@ async function collectMetrics(root, now = /* @__PURE__ */ new Date()) {
|
|
|
3621
3969
|
}
|
|
3622
3970
|
async function readDelta(change) {
|
|
3623
3971
|
const { readTextIfExists: readTextIfExists2 } = await import("./fsx-VF2P7ALA.js");
|
|
3624
|
-
return readTextIfExists2(
|
|
3972
|
+
return readTextIfExists2(path14.join(change.dir, "spec.md"));
|
|
3625
3973
|
}
|
|
3626
3974
|
|
|
3627
3975
|
// src/present.ts
|
|
3628
|
-
import
|
|
3976
|
+
import path15 from "path";
|
|
3629
3977
|
import { escapeHtml, inlineMarkdown, renderMarkdown as renderRich } from "@specatlas/render";
|
|
3630
3978
|
import { defaultTokens, renderDocument, renderStyles } from "@specatlas/render";
|
|
3631
3979
|
async function generatePresentation(opts) {
|
|
3632
|
-
const root =
|
|
3980
|
+
const root = path15.resolve(opts.root);
|
|
3633
3981
|
const { workspace, config } = await loadWorkspace(root);
|
|
3634
3982
|
const language = config.project.language;
|
|
3635
3983
|
const change = workspace.changes.find((c) => c.slug === opts.slug);
|
|
@@ -3639,17 +3987,17 @@ async function generatePresentation(opts) {
|
|
|
3639
3987
|
if (!change.delta) {
|
|
3640
3988
|
return { slug: opts.slug, diagnostics: [diag("ATLAS-PRESENT-001", "error", `El cambio "${opts.slug}" no tiene spec.md (delta)`)] };
|
|
3641
3989
|
}
|
|
3642
|
-
const deltaPath =
|
|
3990
|
+
const deltaPath = path15.join(change.dir, "spec.md");
|
|
3643
3991
|
const deltaContent = await readTextIfExists(deltaPath) ?? "";
|
|
3644
3992
|
const hash = artifactHash(deltaContent);
|
|
3645
3993
|
const now = opts.now ?? /* @__PURE__ */ new Date();
|
|
3646
|
-
const presentationDir =
|
|
3647
|
-
const mockupsCopyDir =
|
|
3994
|
+
const presentationDir = path15.join(change.dir, "presentation");
|
|
3995
|
+
const mockupsCopyDir = path15.join(presentationDir, "mockups");
|
|
3648
3996
|
await ensureDir(presentationDir);
|
|
3649
3997
|
const mockupInfo = await copyMockups(root, change, mockupsCopyDir);
|
|
3650
|
-
const proposalRaw = await readTextIfExists(
|
|
3998
|
+
const proposalRaw = await readTextIfExists(path15.join(change.dir, "proposal.md"));
|
|
3651
3999
|
const proposalBody = proposalRaw ? parseFrontmatter(proposalRaw).body : "";
|
|
3652
|
-
const approvals = await loadApprovals(
|
|
4000
|
+
const approvals = await loadApprovals(path15.join(root, ".sdd"));
|
|
3653
4001
|
const approval = verifyApproval(change, approvals.byArtifact, config, deltaContent);
|
|
3654
4002
|
const labels = labelsFor(language);
|
|
3655
4003
|
const html = page({
|
|
@@ -3670,7 +4018,7 @@ async function generatePresentation(opts) {
|
|
|
3670
4018
|
approveHint: `satlas approve ${change.slug} --by "<nombre>" --channel presentation`,
|
|
3671
4019
|
...approval.status === "valid" && approval.approvedBy ? { approval: { by: approval.approvedBy, at: approval.approvedAt ?? "" } } : {}
|
|
3672
4020
|
});
|
|
3673
|
-
const outFile =
|
|
4021
|
+
const outFile = path15.join(presentationDir, "index.html");
|
|
3674
4022
|
await writeText(outFile, html);
|
|
3675
4023
|
return { slug: change.slug, path: outFile, hash, diagnostics: mockupInfo.diagnostics };
|
|
3676
4024
|
}
|
|
@@ -3682,12 +4030,12 @@ async function copyMockups(root, change, destDir) {
|
|
|
3682
4030
|
await ensureDir(destDir);
|
|
3683
4031
|
const screens = [];
|
|
3684
4032
|
for (const screen of manifest.screens) {
|
|
3685
|
-
const src =
|
|
4033
|
+
const src = path15.join(dir, screen.file);
|
|
3686
4034
|
if (!await exists(src)) {
|
|
3687
4035
|
diagnostics.push(diag("ATLAS-PRESENT-002", "warning", `El mockup ${screen.file} no existe y no se incluir\xE1`, { path: src }));
|
|
3688
4036
|
continue;
|
|
3689
4037
|
}
|
|
3690
|
-
await copyFile(src,
|
|
4038
|
+
await copyFile(src, path15.join(destDir, screen.file));
|
|
3691
4039
|
screens.push({
|
|
3692
4040
|
id: screen.id,
|
|
3693
4041
|
title: screen.title ?? screen.id,
|
|
@@ -3697,13 +4045,13 @@ async function copyMockups(root, change, destDir) {
|
|
|
3697
4045
|
});
|
|
3698
4046
|
}
|
|
3699
4047
|
const screenshots = [];
|
|
3700
|
-
const shotsDir =
|
|
4048
|
+
const shotsDir = path15.join(dir, "screens");
|
|
3701
4049
|
if (await exists(shotsDir)) {
|
|
3702
|
-
await ensureDir(
|
|
4050
|
+
await ensureDir(path15.join(destDir, "screens"));
|
|
3703
4051
|
const { listDir: listDir4 } = await import("./fsx-VF2P7ALA.js");
|
|
3704
4052
|
for (const file of await listDir4(shotsDir)) {
|
|
3705
4053
|
if (!/\.(png|jpe?g|webp)$/i.test(file)) continue;
|
|
3706
|
-
await copyFile(
|
|
4054
|
+
await copyFile(path15.join(shotsDir, file), path15.join(destDir, "screens", file));
|
|
3707
4055
|
screenshots.push(`mockups/screens/${file}`);
|
|
3708
4056
|
}
|
|
3709
4057
|
}
|
|
@@ -3878,7 +4226,7 @@ function renderMarkdown(markdown) {
|
|
|
3878
4226
|
}
|
|
3879
4227
|
|
|
3880
4228
|
// src/adopt.ts
|
|
3881
|
-
import
|
|
4229
|
+
import path16 from "path";
|
|
3882
4230
|
var COMMON_ROOTS = ["src", "app", "lib", "modules", "services", "packages", "api", "features", "domain", "internal", "components"];
|
|
3883
4231
|
var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", "dist", "build", "out", "coverage", ".git", ".sdd", ".opencode", ".claude", "test", "tests", "__tests__", "e2e", "docs", "scripts", "assets", "public", "migrations", "vendor", "bin", "obj"]);
|
|
3884
4232
|
var SOURCE_EXT = /* @__PURE__ */ new Set([
|
|
@@ -3922,11 +4270,11 @@ var SOURCE_EXT = /* @__PURE__ */ new Set([
|
|
|
3922
4270
|
".groovy"
|
|
3923
4271
|
]);
|
|
3924
4272
|
async function adoptWorkspace(opts) {
|
|
3925
|
-
const root =
|
|
4273
|
+
const root = path16.resolve(opts.root);
|
|
3926
4274
|
const dryRun = opts.dryRun ?? false;
|
|
3927
4275
|
const diagnostics = [];
|
|
3928
|
-
const sddDir =
|
|
3929
|
-
if (!await exists(
|
|
4276
|
+
const sddDir = path16.join(root, ".sdd");
|
|
4277
|
+
if (!await exists(path16.join(sddDir, "config.yaml"))) {
|
|
3930
4278
|
return {
|
|
3931
4279
|
root,
|
|
3932
4280
|
domains: [],
|
|
@@ -3937,8 +4285,8 @@ async function adoptWorkspace(opts) {
|
|
|
3937
4285
|
}
|
|
3938
4286
|
const { config } = await loadConfig(sddDir);
|
|
3939
4287
|
const files = await walkFiles(root, { skipDirs: [...SKIP_DIRS] });
|
|
3940
|
-
const inventoried = files.map((f) => toPosix(
|
|
3941
|
-
const relative = inventoried.filter((f) => SOURCE_EXT.has(
|
|
4288
|
+
const inventoried = files.map((f) => toPosix(path16.relative(root, f.path)));
|
|
4289
|
+
const relative = inventoried.filter((f) => SOURCE_EXT.has(path16.extname(f).toLowerCase()));
|
|
3942
4290
|
if (relative.length === 0) {
|
|
3943
4291
|
diagnostics.push(diag("ATLAS-ADOPT-002", "warning", "No se encontraron archivos de c\xF3digo para inventariar", { path: root, suggestion: "A\xF1ade el dominio a mano con --domains <nombre>" }));
|
|
3944
4292
|
}
|
|
@@ -3966,13 +4314,13 @@ async function adoptWorkspace(opts) {
|
|
|
3966
4314
|
const createdSpecs = [];
|
|
3967
4315
|
for (const name of domainNames.sort()) {
|
|
3968
4316
|
const domainFiles = (counts.get(name) ?? []).sort();
|
|
3969
|
-
const existingSpec = await exists(
|
|
4317
|
+
const existingSpec = await exists(path16.join(sddDir, "specs", name, "spec.md"));
|
|
3970
4318
|
domains.push({ name, files: domainFiles.length, samples: domainFiles.slice(0, 8), existingSpec });
|
|
3971
4319
|
if (existingSpec) {
|
|
3972
|
-
diagnostics.push(diag("ATLAS-ADOPT-003", "info", `La spec de "${name}" ya existe; se conserva`, { path:
|
|
4320
|
+
diagnostics.push(diag("ATLAS-ADOPT-003", "info", `La spec de "${name}" ya existe; se conserva`, { path: path16.join(sddDir, "specs", name, "spec.md") }));
|
|
3973
4321
|
continue;
|
|
3974
4322
|
}
|
|
3975
|
-
const specPath =
|
|
4323
|
+
const specPath = path16.join(sddDir, "specs", name, "spec.md");
|
|
3976
4324
|
if (!dryRun) {
|
|
3977
4325
|
await writeText(specPath, baselineSpec(name, domainFiles));
|
|
3978
4326
|
createdSpecs.push(specPath);
|
|
@@ -3980,7 +4328,7 @@ async function adoptWorkspace(opts) {
|
|
|
3980
4328
|
createdSpecs.push(specPath);
|
|
3981
4329
|
}
|
|
3982
4330
|
}
|
|
3983
|
-
const reportPath =
|
|
4331
|
+
const reportPath = path16.join(sddDir, "adopt-report.md");
|
|
3984
4332
|
if (!dryRun) {
|
|
3985
4333
|
await writeText(reportPath, adoptReport({ root, config, domains, detection: detection?.best, language: config.project.language, files: relative.length, inventoried: inventoried.length, now: opts.now ?? /* @__PURE__ */ new Date() }));
|
|
3986
4334
|
}
|
|
@@ -4000,12 +4348,12 @@ async function loadProjectProfiles(root, dirs) {
|
|
|
4000
4348
|
if (!await exists(dir)) continue;
|
|
4001
4349
|
out.push(...await loadProfilesFromDir(dir));
|
|
4002
4350
|
}
|
|
4003
|
-
const custom =
|
|
4351
|
+
const custom = path16.join(root, ".sdd", "profiles", "custom");
|
|
4004
4352
|
if (await exists(custom)) out.push(...await loadProfilesFromDir(custom));
|
|
4005
4353
|
return out;
|
|
4006
4354
|
}
|
|
4007
4355
|
async function fallbackDomainName(root, projectName) {
|
|
4008
|
-
const pkg = await readTextIfExists(
|
|
4356
|
+
const pkg = await readTextIfExists(path16.join(root, "package.json"));
|
|
4009
4357
|
if (pkg) {
|
|
4010
4358
|
try {
|
|
4011
4359
|
const data = JSON.parse(pkg);
|
|
@@ -4093,13 +4441,13 @@ function adoptReport(input) {
|
|
|
4093
4441
|
}
|
|
4094
4442
|
|
|
4095
4443
|
// src/init.ts
|
|
4096
|
-
import
|
|
4444
|
+
import path17 from "path";
|
|
4097
4445
|
async function initWorkspace(opts) {
|
|
4098
|
-
const root =
|
|
4099
|
-
const sddDir =
|
|
4446
|
+
const root = path17.resolve(opts.root);
|
|
4447
|
+
const sddDir = path17.join(root, ".sdd");
|
|
4100
4448
|
const diagnostics = [];
|
|
4101
4449
|
const created = [];
|
|
4102
|
-
if (await exists(
|
|
4450
|
+
if (await exists(path17.join(sddDir, "config.yaml"))) {
|
|
4103
4451
|
return {
|
|
4104
4452
|
sddDir,
|
|
4105
4453
|
created,
|
|
@@ -4109,29 +4457,29 @@ async function initWorkspace(opts) {
|
|
|
4109
4457
|
created.push(...await ensureSddDirs(sddDir));
|
|
4110
4458
|
const language = opts.language ?? "es";
|
|
4111
4459
|
const cfg = defaultConfig({ name: opts.name, language });
|
|
4112
|
-
cfg.project.name = opts.name ??
|
|
4460
|
+
cfg.project.name = opts.name ?? path17.basename(root);
|
|
4113
4461
|
await writeConfig(sddDir, cfg);
|
|
4114
|
-
created.push(
|
|
4462
|
+
created.push(path17.join(sddDir, "config.yaml"));
|
|
4115
4463
|
const templates = templatesFor(language);
|
|
4116
|
-
await writeText(
|
|
4117
|
-
created.push(
|
|
4118
|
-
await writeText(
|
|
4119
|
-
created.push(
|
|
4464
|
+
await writeText(path17.join(sddDir, "constitution.md"), templates.constitution);
|
|
4465
|
+
created.push(path17.join(sddDir, "constitution.md"));
|
|
4466
|
+
await writeText(path17.join(sddDir, "glossary.md"), templates.glossary);
|
|
4467
|
+
created.push(path17.join(sddDir, "glossary.md"));
|
|
4120
4468
|
const detected = await detectIfPossible(root, opts.profilesDir);
|
|
4121
4469
|
if (detected) {
|
|
4122
4470
|
const yaml = detectionToYaml(detected, localStamp(opts.now));
|
|
4123
|
-
await writeText(
|
|
4124
|
-
created.push(
|
|
4471
|
+
await writeText(path17.join(sddDir, "profiles", "detected.yaml"), yaml);
|
|
4472
|
+
created.push(path17.join(sddDir, "profiles", "detected.yaml"));
|
|
4125
4473
|
if (detected.best) {
|
|
4126
4474
|
diagnostics.push(diag("ATLAS-INIT-002", "info", `Stack detectado: ${detected.best.displayName} (${detected.best.score} puntos)`, { suggestion: "Revisa .sdd/profiles/detected.yaml" }));
|
|
4127
4475
|
} else {
|
|
4128
4476
|
diagnostics.push(diag("ATLAS-INIT-003", "info", "No se detect\xF3 un stack conocido: se usar\xE1 el perfil gen\xE9rico", { suggestion: "Crea un perfil propio con `satlas profile` (F2) o edita .sdd/profiles/custom/" }));
|
|
4129
4477
|
}
|
|
4130
4478
|
}
|
|
4131
|
-
await writeText(
|
|
4132
|
-
created.push(
|
|
4479
|
+
await writeText(path17.join(sddDir, "INDEX.md"), initialIndex(language, cfg.project.name));
|
|
4480
|
+
created.push(path17.join(sddDir, "INDEX.md"));
|
|
4133
4481
|
if (opts.local) {
|
|
4134
|
-
const gitignore =
|
|
4482
|
+
const gitignore = path17.join(root, ".gitignore");
|
|
4135
4483
|
const current = await readTextIfExists(gitignore) ?? "";
|
|
4136
4484
|
if (!/(^|\n)\.sdd\/?(\n|$)/.test(current)) {
|
|
4137
4485
|
const next = current.endsWith("\n") || current === "" ? `${current}.sdd/
|
|
@@ -4166,13 +4514,13 @@ ${es ? "## Archivados\n\n0 cambio(s) en `changes/archive/`." : "## Archived\n\n0
|
|
|
4166
4514
|
}
|
|
4167
4515
|
|
|
4168
4516
|
// src/new.ts
|
|
4169
|
-
import
|
|
4517
|
+
import path18 from "path";
|
|
4170
4518
|
var SLUG_RE = /^[a-z0-9][a-z0-9-]{1,49}$/;
|
|
4171
4519
|
async function createChange(opts) {
|
|
4172
4520
|
const diagnostics = [];
|
|
4173
4521
|
const slug = opts.slug.trim().toLowerCase();
|
|
4174
4522
|
const language = opts.language ?? opts.cfg?.project.language ?? "es";
|
|
4175
|
-
const dir =
|
|
4523
|
+
const dir = path18.join(path18.resolve(opts.root), ".sdd", "changes", slug);
|
|
4176
4524
|
if (!SLUG_RE.test(slug)) {
|
|
4177
4525
|
return {
|
|
4178
4526
|
slug,
|
|
@@ -4203,20 +4551,20 @@ async function createChange(opts) {
|
|
|
4203
4551
|
await ensureDir(dir);
|
|
4204
4552
|
const templates = templatesFor(language);
|
|
4205
4553
|
const files = [];
|
|
4206
|
-
const metaFile =
|
|
4554
|
+
const metaFile = path18.join(dir, "meta.yaml");
|
|
4207
4555
|
await writeText(metaFile, changeMetaYaml(meta));
|
|
4208
4556
|
files.push(metaFile);
|
|
4209
4557
|
if (lane === "fix") {
|
|
4210
|
-
const fixFile =
|
|
4558
|
+
const fixFile = path18.join(dir, "fix.md");
|
|
4211
4559
|
await writeText(fixFile, renderTemplate(templates.fix, { TITLE: title }));
|
|
4212
4560
|
files.push(fixFile);
|
|
4213
4561
|
return { slug, dir, files, diagnostics };
|
|
4214
4562
|
}
|
|
4215
|
-
const proposalFile =
|
|
4563
|
+
const proposalFile = path18.join(dir, "proposal.md");
|
|
4216
4564
|
await writeText(proposalFile, renderTemplate(templates.proposal, { TITLE: title }));
|
|
4217
4565
|
files.push(proposalFile);
|
|
4218
4566
|
const domainUpper = domain.replace(/[^a-z0-9]/gi, "").toUpperCase() || "GEN";
|
|
4219
|
-
const deltaFile =
|
|
4567
|
+
const deltaFile = path18.join(dir, "spec.md");
|
|
4220
4568
|
await writeText(deltaFile, templates.specDelta({ title, domainUpper, domain }));
|
|
4221
4569
|
files.push(deltaFile);
|
|
4222
4570
|
return { slug, dir, files, diagnostics };
|
|
@@ -4225,7 +4573,7 @@ async function createChange(opts) {
|
|
|
4225
4573
|
// src/archive.ts
|
|
4226
4574
|
import { promises as fs } from "fs";
|
|
4227
4575
|
import { cp, rename, rm } from "fs/promises";
|
|
4228
|
-
import
|
|
4576
|
+
import path19 from "path";
|
|
4229
4577
|
var REQ_HEADER_RE = /^###\s+(?:Requisito|Requirement):\s+(REQ-[A-Z0-9-]+)\s*(?:—|-|–)\s*(.*)$/;
|
|
4230
4578
|
function foldDelta(livingBody, delta, language = "es") {
|
|
4231
4579
|
const diagnostics = [];
|
|
@@ -4334,7 +4682,7 @@ async function restoreFile(file, previous) {
|
|
|
4334
4682
|
}
|
|
4335
4683
|
}
|
|
4336
4684
|
async function archiveChange(opts) {
|
|
4337
|
-
const root =
|
|
4685
|
+
const root = path19.resolve(opts.root);
|
|
4338
4686
|
const diagnostics = [];
|
|
4339
4687
|
const { config } = await loadWorkspace(root);
|
|
4340
4688
|
const language = opts.language ?? config.project.language;
|
|
@@ -4350,7 +4698,7 @@ async function archiveChange(opts) {
|
|
|
4350
4698
|
return { slug: opts.slug, fold: emptyFold, diagnostics, dryRun: opts.dryRun ?? false };
|
|
4351
4699
|
}
|
|
4352
4700
|
const nowFix = opts.now ?? /* @__PURE__ */ new Date();
|
|
4353
|
-
const targetFix =
|
|
4701
|
+
const targetFix = path19.join(root, ".sdd", "changes", "archive", `${localMonth(nowFix)}-${opts.slug}`);
|
|
4354
4702
|
if (await exists(targetFix)) {
|
|
4355
4703
|
diagnostics.push(diag("ATLAS-ARCH-002", "error", `Ya existe un cambio archivado en ${targetFix}`, { path: targetFix }));
|
|
4356
4704
|
return { slug: opts.slug, fold: emptyFold, diagnostics, dryRun: opts.dryRun ?? false };
|
|
@@ -4358,7 +4706,7 @@ async function archiveChange(opts) {
|
|
|
4358
4706
|
let livingFix;
|
|
4359
4707
|
let livingCreated = false;
|
|
4360
4708
|
if (!opts.dryRun) {
|
|
4361
|
-
const fixRaw = await readTextIfExists(
|
|
4709
|
+
const fixRaw = await readTextIfExists(path19.join(change.dir, "fix.md")) ?? "";
|
|
4362
4710
|
try {
|
|
4363
4711
|
const written = await writeLivingFix(root, {
|
|
4364
4712
|
slug: opts.slug,
|
|
@@ -4374,7 +4722,7 @@ async function archiveChange(opts) {
|
|
|
4374
4722
|
} catch (error) {
|
|
4375
4723
|
diagnostics.push(
|
|
4376
4724
|
diag("ATLAS-ARCH-006", "error", `No se pudo conservar el fix vivo: ${error.message}`, {
|
|
4377
|
-
path:
|
|
4725
|
+
path: path19.join(root, ".sdd", "fixes"),
|
|
4378
4726
|
suggestion: "Revisa los permisos de .sdd/fixes y vuelve a intentar; el fix sigue sin archivar"
|
|
4379
4727
|
})
|
|
4380
4728
|
);
|
|
@@ -4382,11 +4730,11 @@ async function archiveChange(opts) {
|
|
|
4382
4730
|
}
|
|
4383
4731
|
}
|
|
4384
4732
|
if (!opts.dryRun) {
|
|
4385
|
-
await ensureDir(
|
|
4733
|
+
await ensureDir(path19.dirname(targetFix));
|
|
4386
4734
|
try {
|
|
4387
4735
|
await moveDirectory(change.dir, targetFix);
|
|
4388
4736
|
} catch (error) {
|
|
4389
|
-
if (livingCreated && livingFix) await removeFile(
|
|
4737
|
+
if (livingCreated && livingFix) await removeFile(path19.join(root, livingFix));
|
|
4390
4738
|
diagnostics.push(
|
|
4391
4739
|
diag("ATLAS-ARCH-004", "error", `No se pudo mover el cambio al hist\xF3rico: ${error.message}`, {
|
|
4392
4740
|
path: change.dir,
|
|
@@ -4412,10 +4760,10 @@ async function archiveChange(opts) {
|
|
|
4412
4760
|
}
|
|
4413
4761
|
const domain = change.meta.domain;
|
|
4414
4762
|
if (!domain) {
|
|
4415
|
-
diagnostics.push(diag("ATLAS-ARCH-001", "error", `El cambio "${opts.slug}" no declara domain en meta.yaml`, { path:
|
|
4763
|
+
diagnostics.push(diag("ATLAS-ARCH-001", "error", `El cambio "${opts.slug}" no declara domain en meta.yaml`, { path: path19.join(change.dir, "meta.yaml") }));
|
|
4416
4764
|
return { slug: opts.slug, fold: { content: "", applied: { added: [], modified: [], removed: [], renamed: [] }, diagnostics }, diagnostics, dryRun: opts.dryRun ?? false };
|
|
4417
4765
|
}
|
|
4418
|
-
const specFile =
|
|
4766
|
+
const specFile = path19.join(root, ".sdd", "specs", domain, "spec.md");
|
|
4419
4767
|
const existing = await readTextIfExists(specFile);
|
|
4420
4768
|
const base = existing ?? `---
|
|
4421
4769
|
domain: ${domain}
|
|
@@ -4445,8 +4793,8 @@ ${Object.entries(nextFm).map(([k, v]) => `${k}: ${String(v)}`).join("\n")}
|
|
|
4445
4793
|
|
|
4446
4794
|
${body}`;
|
|
4447
4795
|
const month = localMonth(now);
|
|
4448
|
-
const archiveDir =
|
|
4449
|
-
const target =
|
|
4796
|
+
const archiveDir = path19.join(root, ".sdd", "changes", "archive");
|
|
4797
|
+
const target = path19.join(archiveDir, `${month}-${opts.slug}`);
|
|
4450
4798
|
if (await exists(target)) {
|
|
4451
4799
|
diagnostics.push(diag("ATLAS-ARCH-002", "error", `Ya existe un cambio archivado en ${target}`, { path: target }));
|
|
4452
4800
|
return { slug: opts.slug, domain, fold, diagnostics, dryRun: opts.dryRun ?? false };
|
|
@@ -4472,7 +4820,7 @@ ${body}`;
|
|
|
4472
4820
|
}
|
|
4473
4821
|
async function regenerateIndex(root, cfg, now = /* @__PURE__ */ new Date()) {
|
|
4474
4822
|
const { workspace, config } = cfg ? { workspace: (await loadWorkspace(root)).workspace, config: cfg } : await loadWorkspace(root);
|
|
4475
|
-
const archiveDir =
|
|
4823
|
+
const archiveDir = path19.join(root, ".sdd", "changes", "archive");
|
|
4476
4824
|
const archived = await exists(archiveDir) ? (await fs.readdir(archiveDir)).filter((e) => !e.startsWith(".")).length : 0;
|
|
4477
4825
|
const fixes = await loadLivingFixes(root);
|
|
4478
4826
|
const markdown = indexMarkdown({
|
|
@@ -4484,7 +4832,7 @@ async function regenerateIndex(root, cfg, now = /* @__PURE__ */ new Date()) {
|
|
|
4484
4832
|
archived
|
|
4485
4833
|
});
|
|
4486
4834
|
void now;
|
|
4487
|
-
await writeText(
|
|
4835
|
+
await writeText(path19.join(root, ".sdd", "INDEX.md"), markdown);
|
|
4488
4836
|
}
|
|
4489
4837
|
async function removeFile(file) {
|
|
4490
4838
|
try {
|
|
@@ -4495,8 +4843,8 @@ async function removeFile(file) {
|
|
|
4495
4843
|
|
|
4496
4844
|
// src/migrations.ts
|
|
4497
4845
|
import { promises as fs2 } from "fs";
|
|
4498
|
-
import
|
|
4499
|
-
import { parse as
|
|
4846
|
+
import path20 from "path";
|
|
4847
|
+
import { parse as parseYaml10, stringify as stringifyYaml7 } from "yaml";
|
|
4500
4848
|
var SCHEMA_VERSION = 1;
|
|
4501
4849
|
var BACKUP_DIRNAME = ".backup";
|
|
4502
4850
|
var BACKUP_POINTER = ".latest";
|
|
@@ -4508,35 +4856,35 @@ var SCHEMA_MIGRATIONS = [
|
|
|
4508
4856
|
to: SCHEMA_VERSION
|
|
4509
4857
|
}
|
|
4510
4858
|
];
|
|
4511
|
-
var FIXED_ARTIFACTS = ["config.yaml", "approvals.yaml",
|
|
4512
|
-
var CHANGE_ARTIFACTS = ["meta.yaml",
|
|
4859
|
+
var FIXED_ARTIFACTS = ["config.yaml", "approvals.yaml", path20.join("profiles", "detected.yaml")];
|
|
4860
|
+
var CHANGE_ARTIFACTS = ["meta.yaml", path20.join("mockups", "manifest.yaml")];
|
|
4513
4861
|
async function pushChangeArtifacts(out, sddDir, relDir) {
|
|
4514
4862
|
for (const rel of CHANGE_ARTIFACTS) {
|
|
4515
|
-
const abs =
|
|
4516
|
-
if (await exists(abs)) out.push({ artifact: toPosix(
|
|
4863
|
+
const abs = path20.join(sddDir, relDir, rel);
|
|
4864
|
+
if (await exists(abs)) out.push({ artifact: toPosix(path20.join(relDir, rel)), path: abs });
|
|
4517
4865
|
}
|
|
4518
4866
|
}
|
|
4519
4867
|
async function collectVersionedArtifacts(root) {
|
|
4520
|
-
const sddDir =
|
|
4868
|
+
const sddDir = path20.join(root, ".sdd");
|
|
4521
4869
|
const out = [];
|
|
4522
4870
|
for (const rel of FIXED_ARTIFACTS) {
|
|
4523
|
-
const abs =
|
|
4871
|
+
const abs = path20.join(sddDir, rel);
|
|
4524
4872
|
if (await exists(abs)) out.push({ artifact: toPosix(rel), path: abs });
|
|
4525
4873
|
}
|
|
4526
|
-
const changesDir =
|
|
4874
|
+
const changesDir = path20.join(sddDir, "changes");
|
|
4527
4875
|
for (const slug of await listDirs(changesDir)) {
|
|
4528
4876
|
if (slug === "archive") continue;
|
|
4529
|
-
await pushChangeArtifacts(out, sddDir,
|
|
4877
|
+
await pushChangeArtifacts(out, sddDir, path20.join("changes", slug));
|
|
4530
4878
|
}
|
|
4531
|
-
for (const entry of await listDirs(
|
|
4532
|
-
await pushChangeArtifacts(out, sddDir,
|
|
4879
|
+
for (const entry of await listDirs(path20.join(changesDir, "archive"))) {
|
|
4880
|
+
await pushChangeArtifacts(out, sddDir, path20.join("changes", "archive", entry));
|
|
4533
4881
|
}
|
|
4534
4882
|
return out;
|
|
4535
4883
|
}
|
|
4536
4884
|
function readArtifactVersion(content) {
|
|
4537
4885
|
let data;
|
|
4538
4886
|
try {
|
|
4539
|
-
data =
|
|
4887
|
+
data = parseYaml10(content);
|
|
4540
4888
|
} catch (err) {
|
|
4541
4889
|
return { state: "unreadable", reason: `YAML inv\xE1lido (${err.message})` };
|
|
4542
4890
|
}
|
|
@@ -4626,22 +4974,22 @@ function backupName(now) {
|
|
|
4626
4974
|
async function applyUpgrade(root, now = /* @__PURE__ */ new Date()) {
|
|
4627
4975
|
const plan = await planUpgrade(root);
|
|
4628
4976
|
if (plan.pending.length === 0) return { status: "up-to-date", plan, applied: [] };
|
|
4629
|
-
const sddDir =
|
|
4630
|
-
const backupRoot =
|
|
4977
|
+
const sddDir = path20.join(root, ".sdd");
|
|
4978
|
+
const backupRoot = path20.join(sddDir, BACKUP_DIRNAME);
|
|
4631
4979
|
const name = backupName(now);
|
|
4632
|
-
const backupDir =
|
|
4633
|
-
const pointerFile =
|
|
4980
|
+
const backupDir = path20.join(backupRoot, name);
|
|
4981
|
+
const pointerFile = path20.join(backupRoot, BACKUP_POINTER);
|
|
4634
4982
|
const previous = [];
|
|
4635
4983
|
try {
|
|
4636
4984
|
for (const item of plan.pending) {
|
|
4637
4985
|
const content = await readTextIfExists(item.path);
|
|
4638
4986
|
if (content === void 0) throw new Error("el elemento desapareci\xF3 mientras se respaldaba");
|
|
4639
4987
|
previous.push({ item, contents: content });
|
|
4640
|
-
await writeText(
|
|
4988
|
+
await writeText(path20.join(backupDir, "files", ...item.artifact.split("/")), content);
|
|
4641
4989
|
}
|
|
4642
4990
|
await writeText(
|
|
4643
|
-
|
|
4644
|
-
|
|
4991
|
+
path20.join(backupDir, "backup.yaml"),
|
|
4992
|
+
stringifyYaml7(
|
|
4645
4993
|
{
|
|
4646
4994
|
schema_version: SCHEMA_VERSION,
|
|
4647
4995
|
created_at: localStamp(now),
|
|
@@ -4681,7 +5029,7 @@ async function applyUpgrade(root, now = /* @__PURE__ */ new Date()) {
|
|
|
4681
5029
|
}
|
|
4682
5030
|
}
|
|
4683
5031
|
for (const dir of await listDirs(backupRoot)) {
|
|
4684
|
-
if (dir !== name) await removeDir(
|
|
5032
|
+
if (dir !== name) await removeDir(path20.join(backupRoot, dir));
|
|
4685
5033
|
}
|
|
4686
5034
|
return {
|
|
4687
5035
|
status: "applied",
|
|
@@ -4690,32 +5038,32 @@ async function applyUpgrade(root, now = /* @__PURE__ */ new Date()) {
|
|
|
4690
5038
|
backup: {
|
|
4691
5039
|
name,
|
|
4692
5040
|
dir: backupDir,
|
|
4693
|
-
relativeDir: toPosix(
|
|
5041
|
+
relativeDir: toPosix(path20.relative(root, backupDir)),
|
|
4694
5042
|
createdAt: localStamp(now),
|
|
4695
5043
|
files: plan.pending.map((i) => i.artifact)
|
|
4696
5044
|
}
|
|
4697
5045
|
};
|
|
4698
5046
|
}
|
|
4699
5047
|
async function rollbackUpgrade(root) {
|
|
4700
|
-
const sddDir =
|
|
4701
|
-
const backupRoot =
|
|
4702
|
-
const pointerFile =
|
|
5048
|
+
const sddDir = path20.join(root, ".sdd");
|
|
5049
|
+
const backupRoot = path20.join(sddDir, BACKUP_DIRNAME);
|
|
5050
|
+
const pointerFile = path20.join(backupRoot, BACKUP_POINTER);
|
|
4703
5051
|
const name = (await readTextIfExists(pointerFile))?.trim();
|
|
4704
5052
|
if (!name) return { status: "no-backup", restored: [] };
|
|
4705
|
-
const backupDir =
|
|
4706
|
-
const manifestRaw = await readTextIfExists(
|
|
5053
|
+
const backupDir = path20.join(backupRoot, name);
|
|
5054
|
+
const manifestRaw = await readTextIfExists(path20.join(backupDir, "backup.yaml"));
|
|
4707
5055
|
if (manifestRaw === void 0) return { status: "no-backup", restored: [] };
|
|
4708
5056
|
let files = [];
|
|
4709
5057
|
try {
|
|
4710
|
-
const parsed =
|
|
5058
|
+
const parsed = parseYaml10(manifestRaw);
|
|
4711
5059
|
files = (parsed?.files ?? []).filter((f) => typeof f?.artifact === "string");
|
|
4712
5060
|
} catch {
|
|
4713
5061
|
return { status: "no-backup", restored: [] };
|
|
4714
5062
|
}
|
|
4715
5063
|
const restored = [];
|
|
4716
5064
|
for (const file of files) {
|
|
4717
|
-
const from =
|
|
4718
|
-
const to =
|
|
5065
|
+
const from = path20.join(backupDir, "files", ...file.artifact.split("/"));
|
|
5066
|
+
const to = path20.join(sddDir, ...file.artifact.split("/"));
|
|
4719
5067
|
const content = await readTextIfExists(from);
|
|
4720
5068
|
if (content === void 0) continue;
|
|
4721
5069
|
try {
|
|
@@ -4759,7 +5107,7 @@ async function upgradeAdvisory(root) {
|
|
|
4759
5107
|
}
|
|
4760
5108
|
|
|
4761
5109
|
// src/docs.ts
|
|
4762
|
-
import
|
|
5110
|
+
import path21 from "path";
|
|
4763
5111
|
var DOCS_MARKER_START = "<!-- specatlas:generado:inicio -->";
|
|
4764
5112
|
var DOCS_MARKER_END = "<!-- specatlas:generado:fin -->";
|
|
4765
5113
|
function docData(change, language, now) {
|
|
@@ -4813,7 +5161,7 @@ ${notes}
|
|
|
4813
5161
|
${existing}`;
|
|
4814
5162
|
}
|
|
4815
5163
|
async function generateDocs(opts) {
|
|
4816
|
-
const root =
|
|
5164
|
+
const root = path21.resolve(opts.root);
|
|
4817
5165
|
const { config } = await loadWorkspace(root);
|
|
4818
5166
|
const change = await loadChange(root, opts.slug);
|
|
4819
5167
|
if (!change.meta) {
|
|
@@ -4830,7 +5178,7 @@ async function generateDocs(opts) {
|
|
|
4830
5178
|
const data = docData(change, language, opts.now ?? /* @__PURE__ */ new Date());
|
|
4831
5179
|
const files = [];
|
|
4832
5180
|
for (const current of tipos) {
|
|
4833
|
-
const file =
|
|
5181
|
+
const file = path21.join(change.dir, "docs", `${current}.md`);
|
|
4834
5182
|
const block = renderTemplate(current === "tecnica" ? templates.docTecnica : templates.docManual, data);
|
|
4835
5183
|
const existing = await readTextIfExists(file);
|
|
4836
5184
|
await writeText(file, mergeManaged(existing, block, language));
|
|
@@ -4840,7 +5188,7 @@ async function generateDocs(opts) {
|
|
|
4840
5188
|
}
|
|
4841
5189
|
|
|
4842
5190
|
// src/sarif.ts
|
|
4843
|
-
import
|
|
5191
|
+
import path22 from "path";
|
|
4844
5192
|
var SARIF_SCHEMA = "https://json.schemastore.org/sarif-2.1.0.json";
|
|
4845
5193
|
var SARIF_VERSION = "2.1.0";
|
|
4846
5194
|
var TOOL_NAME = "SpecAtlas";
|
|
@@ -4878,7 +5226,7 @@ function resultOf(diagnostic, root) {
|
|
|
4878
5226
|
message: { text: diagnostic.message }
|
|
4879
5227
|
};
|
|
4880
5228
|
if (diagnostic.path) {
|
|
4881
|
-
const rel =
|
|
5229
|
+
const rel = path22.relative(root, diagnostic.path);
|
|
4882
5230
|
if (rel !== "") {
|
|
4883
5231
|
const physicalLocation = {
|
|
4884
5232
|
artifactLocation: { uri: toPosix(rel) },
|
|
@@ -4928,7 +5276,7 @@ function toSarifText(opts) {
|
|
|
4928
5276
|
}
|
|
4929
5277
|
|
|
4930
5278
|
// src/doctor.ts
|
|
4931
|
-
import
|
|
5279
|
+
import path23 from "path";
|
|
4932
5280
|
async function runDoctor(root) {
|
|
4933
5281
|
const findings = [];
|
|
4934
5282
|
const { workspace, config } = await loadWorkspace(root);
|
|
@@ -4936,7 +5284,7 @@ async function runDoctor(root) {
|
|
|
4936
5284
|
const approvals = await loadApprovals(workspace.sddDir);
|
|
4937
5285
|
findings.push(...approvals.diagnostics);
|
|
4938
5286
|
for (const change of workspace.changes) {
|
|
4939
|
-
const deltaPath =
|
|
5287
|
+
const deltaPath = path23.join(change.dir, "spec.md");
|
|
4940
5288
|
const deltaContent = await readTextIfExists(deltaPath);
|
|
4941
5289
|
const approval = verifyApproval(change, approvals.byArtifact, config, deltaContent ?? void 0);
|
|
4942
5290
|
if ((change.planPath || change.tasks) && (approval.status === "missing" || approval.status === "stale")) {
|
|
@@ -4958,7 +5306,7 @@ async function runDoctor(root) {
|
|
|
4958
5306
|
}
|
|
4959
5307
|
for (const override of change.meta?.overrides ?? []) {
|
|
4960
5308
|
if (!override.reason.trim() || !override.by.trim()) {
|
|
4961
|
-
findings.push(diag("ATLAS-LIFECYCLE-004", "error", `Override del gate "${override.gate}" sin motivo o autor`, { path:
|
|
5309
|
+
findings.push(diag("ATLAS-LIFECYCLE-004", "error", `Override del gate "${override.gate}" sin motivo o autor`, { path: path23.join(change.dir, "meta.yaml") }));
|
|
4962
5310
|
}
|
|
4963
5311
|
}
|
|
4964
5312
|
}
|
|
@@ -4981,7 +5329,7 @@ async function specHashOf(filePath) {
|
|
|
4981
5329
|
}
|
|
4982
5330
|
|
|
4983
5331
|
// src/gate.ts
|
|
4984
|
-
import
|
|
5332
|
+
import path24 from "path";
|
|
4985
5333
|
var UI_DOMAINS2 = /* @__PURE__ */ new Set(["frontend", "mobile", "fullstack"]);
|
|
4986
5334
|
function count(name, diagnostics) {
|
|
4987
5335
|
return {
|
|
@@ -4998,7 +5346,7 @@ function livingRequirementsMap(specs) {
|
|
|
4998
5346
|
return map;
|
|
4999
5347
|
}
|
|
5000
5348
|
async function runCiGate(opts) {
|
|
5001
|
-
const root =
|
|
5349
|
+
const root = path24.resolve(opts.root);
|
|
5002
5350
|
const { workspace, config } = await loadWorkspace(root);
|
|
5003
5351
|
const diagnostics = [];
|
|
5004
5352
|
const checks = [];
|
|
@@ -5009,11 +5357,12 @@ async function runCiGate(opts) {
|
|
|
5009
5357
|
let changesErrors = 0;
|
|
5010
5358
|
let changesWarnings = 0;
|
|
5011
5359
|
for (const change of workspace.changes) {
|
|
5012
|
-
const lintFindings = change.delta ? lintDelta(change.delta, living,
|
|
5360
|
+
const lintFindings = change.delta ? lintDelta(change.delta, living, path24.join(change.dir, "spec.md"), { language: config.spec.language }) : [];
|
|
5013
5361
|
const trace = checkTrace({
|
|
5014
5362
|
specs: workspace.specs,
|
|
5015
5363
|
change,
|
|
5016
|
-
requireEvidence: config.gates.verify.mode !== "off" && config.gates.verify.require_evidence
|
|
5364
|
+
requireEvidence: config.gates.verify.mode !== "off" && config.gates.verify.require_evidence,
|
|
5365
|
+
linked: linkedTraceInput(workspace)
|
|
5017
5366
|
});
|
|
5018
5367
|
const changeDiags = [...lintFindings, ...trace.findings];
|
|
5019
5368
|
if (change.planPath) {
|
|
@@ -5058,9 +5407,11 @@ export {
|
|
|
5058
5407
|
BLOCK_HEAD_RE,
|
|
5059
5408
|
BUILTIN_PACKS,
|
|
5060
5409
|
CONFIG_FILE,
|
|
5410
|
+
CONTRACTS_DIR,
|
|
5061
5411
|
CORE_VERSION,
|
|
5062
5412
|
DOCS_MARKER_END,
|
|
5063
5413
|
DOCS_MARKER_START,
|
|
5414
|
+
LINKS_FILE,
|
|
5064
5415
|
LIVING_FIXES_DIR,
|
|
5065
5416
|
REQ_HEAD_RE,
|
|
5066
5417
|
REQ_ID_RE,
|
|
@@ -5079,6 +5430,7 @@ export {
|
|
|
5079
5430
|
TASK_LINE_RE,
|
|
5080
5431
|
TOOL_NAME,
|
|
5081
5432
|
TOOL_URL,
|
|
5433
|
+
addLink,
|
|
5082
5434
|
adoptWorkspace,
|
|
5083
5435
|
appendRunEvent,
|
|
5084
5436
|
applyUpgrade,
|
|
@@ -5102,6 +5454,8 @@ export {
|
|
|
5102
5454
|
compareTaskIds,
|
|
5103
5455
|
computeInputsHash,
|
|
5104
5456
|
configToYaml,
|
|
5457
|
+
contractCoverage,
|
|
5458
|
+
contractsAdvisory,
|
|
5105
5459
|
copyFile,
|
|
5106
5460
|
countBySeverity,
|
|
5107
5461
|
createChange,
|
|
@@ -5119,6 +5473,7 @@ export {
|
|
|
5119
5473
|
editIssue,
|
|
5120
5474
|
emitFrontmatter,
|
|
5121
5475
|
ensureDir,
|
|
5476
|
+
ensureLinksFile,
|
|
5122
5477
|
ensureSddDirs,
|
|
5123
5478
|
esc,
|
|
5124
5479
|
evaluatePacks,
|
|
@@ -5128,6 +5483,7 @@ export {
|
|
|
5128
5483
|
findWorkspaceRoot,
|
|
5129
5484
|
firstToken,
|
|
5130
5485
|
foldDelta,
|
|
5486
|
+
formatOf,
|
|
5131
5487
|
generateDocs,
|
|
5132
5488
|
generatePresentation,
|
|
5133
5489
|
generateRunId,
|
|
@@ -5147,6 +5503,8 @@ export {
|
|
|
5147
5503
|
issueBody,
|
|
5148
5504
|
issueLabels,
|
|
5149
5505
|
laneOrDefault,
|
|
5506
|
+
linkedRequirementIds,
|
|
5507
|
+
linkedTraceInput,
|
|
5150
5508
|
lintDelta,
|
|
5151
5509
|
lintMockupHtml,
|
|
5152
5510
|
lintMockupManifest,
|
|
@@ -5164,7 +5522,9 @@ export {
|
|
|
5164
5522
|
loadApprovals,
|
|
5165
5523
|
loadChange,
|
|
5166
5524
|
loadConfig,
|
|
5525
|
+
loadContracts,
|
|
5167
5526
|
loadDetectedBest,
|
|
5527
|
+
loadLinks,
|
|
5168
5528
|
loadLivingFixes,
|
|
5169
5529
|
loadProfileFile,
|
|
5170
5530
|
loadProfilesFromDir,
|
|
@@ -5186,6 +5546,7 @@ export {
|
|
|
5186
5546
|
parseChangeMeta,
|
|
5187
5547
|
parseClarify,
|
|
5188
5548
|
parseConfig,
|
|
5549
|
+
parseContract,
|
|
5189
5550
|
parseDelta,
|
|
5190
5551
|
parseFixCovers,
|
|
5191
5552
|
parseFrontmatter,
|
|
@@ -5210,12 +5571,14 @@ export {
|
|
|
5210
5571
|
readTextIfExists,
|
|
5211
5572
|
recordEvidence,
|
|
5212
5573
|
regenerateIndex,
|
|
5574
|
+
removeLink,
|
|
5213
5575
|
renderDocument,
|
|
5214
5576
|
renderMarkdown,
|
|
5215
5577
|
renderRequirement,
|
|
5216
5578
|
renderStyles,
|
|
5217
5579
|
renderTemplate,
|
|
5218
5580
|
requiresMockups,
|
|
5581
|
+
resolveLinkPath,
|
|
5219
5582
|
resolvePacks,
|
|
5220
5583
|
rollbackUpgrade,
|
|
5221
5584
|
ruleDescription,
|