@specatlas/core 0.1.26 → 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 +177 -3
- package/dist/index.js +833 -224
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -61,6 +61,9 @@ var atlasConfigSchema = z.object({
|
|
|
61
61
|
analyze: z.object({ mode: z.enum(["off", "advisory", "blocking"]).default("blocking"), min_severity: z.enum(["low", "medium", "high"]).default("medium") }).default({}),
|
|
62
62
|
verify: z.object({ mode: z.enum(["off", "advisory", "blocking"]).default("blocking"), require_evidence: z.boolean().default(true) }).default({}),
|
|
63
63
|
review: z.object({ mode: z.enum(["off", "advisory", "blocking"]).default("advisory") }).default({}),
|
|
64
|
+
clarify: z.object({ mode: z.enum(["off", "advisory", "blocking"]).default("advisory") }).default({}),
|
|
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({}),
|
|
64
67
|
mockup: z.object({ require_approval: z.boolean().default(false), compare_in_verify: z.boolean().default(false) }).default({})
|
|
65
68
|
}).default({}),
|
|
66
69
|
trace: z.object({ mode: z.enum(["off", "advisory", "blocking"]).default("blocking"), prefix: z.string().default("REQ") }).default({}),
|
|
@@ -209,6 +212,7 @@ var SCENARIO_HEAD_RE = /^####\s+(?:Scenario|Escenario):\s+(REQ-[A-Z0-9-]+-S\d+)\
|
|
|
209
212
|
var RULE_RE = /^-\s*(?:Rule|Regla)\s+(BR-[A-Z0-9-]+)\s*:\s*(.+?)\s*$/;
|
|
210
213
|
var WHEN_RE = /^-\s*\*\*\s*(?:WHEN|CUANDO|DADO QUE)\s*\*\*\s*(.+?)\s*$/i;
|
|
211
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;
|
|
212
216
|
function newDraft(id, title, line) {
|
|
213
217
|
return { id, title, line, prose: [], rules: [], scenarios: [] };
|
|
214
218
|
}
|
|
@@ -293,6 +297,16 @@ function parseRequirementBlocks(body, startLine = 1, filePath) {
|
|
|
293
297
|
currentScenario.then.push(thenMatch[1] ?? "");
|
|
294
298
|
continue;
|
|
295
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
|
+
}
|
|
296
310
|
if (current && !currentScenario) {
|
|
297
311
|
if (line.trim() !== "") current.prose.push(line.trim());
|
|
298
312
|
}
|
|
@@ -640,6 +654,35 @@ function findScenarioHeading(lines, blockLineIndex) {
|
|
|
640
654
|
return void 0;
|
|
641
655
|
}
|
|
642
656
|
|
|
657
|
+
// src/parse/clarify.ts
|
|
658
|
+
var OPEN_RE = /^\s*-\s*\[\s\]\s+(.+?)\s*$/;
|
|
659
|
+
var DONE_RE = /^\s*-\s*\[[xX]\]\s+(.+?)\s*$/;
|
|
660
|
+
var ANSWER_SEPARATOR = " \u2014 ";
|
|
661
|
+
function splitAnswer(raw) {
|
|
662
|
+
const index = raw.indexOf(ANSWER_SEPARATOR);
|
|
663
|
+
if (index === -1) return { text: raw.trim() };
|
|
664
|
+
return { text: raw.slice(0, index).trim(), answer: raw.slice(index + ANSWER_SEPARATOR.length).trim() };
|
|
665
|
+
}
|
|
666
|
+
function parseClarify(content, filePath) {
|
|
667
|
+
const fm = parseFrontmatter(content, filePath);
|
|
668
|
+
const lines = fm.body.replace(/\r\n?/g, "\n").split("\n");
|
|
669
|
+
const open = [];
|
|
670
|
+
const resolved = [];
|
|
671
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
672
|
+
const line = lines[i] ?? "";
|
|
673
|
+
const lineNo = fm.bodyStartLine + i;
|
|
674
|
+
const done = DONE_RE.exec(line);
|
|
675
|
+
if (done) {
|
|
676
|
+
const item = splitAnswer(done[1] ?? "");
|
|
677
|
+
resolved.push({ text: item.text, line: lineNo, ...item.answer !== void 0 ? { answer: item.answer } : {} });
|
|
678
|
+
continue;
|
|
679
|
+
}
|
|
680
|
+
const pending = OPEN_RE.exec(line);
|
|
681
|
+
if (pending) open.push({ text: pending[1] ?? "", line: lineNo });
|
|
682
|
+
}
|
|
683
|
+
return { path: filePath, open, resolved, diagnostics: [] };
|
|
684
|
+
}
|
|
685
|
+
|
|
643
686
|
// src/parse/glossary.ts
|
|
644
687
|
var HEADER_KEYS = /* @__PURE__ */ new Set(["t\xE9rmino", "termino", "term", "definici\xF3n", "definicion", "definition", "sin\xF3nimos", "sinonimos", "synonyms"]);
|
|
645
688
|
var SEPARATOR_RE = /^:?-{2,}:?$/;
|
|
@@ -731,6 +774,7 @@ function impactOfRequirement(workspace, reqId) {
|
|
|
731
774
|
const id = reqId.toUpperCase();
|
|
732
775
|
const scenarios = /* @__PURE__ */ new Set();
|
|
733
776
|
let exists2 = false;
|
|
777
|
+
let origin;
|
|
734
778
|
for (const spec of workspace.specs) {
|
|
735
779
|
for (const req of spec.spec.requirements) {
|
|
736
780
|
if (req.id !== id) continue;
|
|
@@ -747,6 +791,18 @@ function impactOfRequirement(workspace, reqId) {
|
|
|
747
791
|
if ((change.delta?.removed ?? []).some((r) => r.id === id)) exists2 = true;
|
|
748
792
|
if ((change.delta?.renamed ?? []).some((r) => r.from.id === id || r.to.id === id)) exists2 = true;
|
|
749
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
|
+
}
|
|
750
806
|
const report = emptyReport(id, "requirement");
|
|
751
807
|
if (!exists2) return report;
|
|
752
808
|
report.exists = true;
|
|
@@ -754,6 +810,10 @@ function impactOfRequirement(workspace, reqId) {
|
|
|
754
810
|
const reqOf = scenarioToReq(workspace);
|
|
755
811
|
collect(workspace, report, /* @__PURE__ */ new Set([id, ...scenarios]), reqOf);
|
|
756
812
|
if (!report.requirements.includes(id)) report.requirements.unshift(id);
|
|
813
|
+
if (origin !== void 0) {
|
|
814
|
+
report.external = true;
|
|
815
|
+
report.origin = origin;
|
|
816
|
+
}
|
|
757
817
|
return report;
|
|
758
818
|
}
|
|
759
819
|
function impactOfFile(workspace, file) {
|
|
@@ -794,14 +854,14 @@ function wordBoundary(text, term) {
|
|
|
794
854
|
const re = new RegExp(`(?<![\\p{L}\\p{N}])${escaped}(?![\\p{L}\\p{N}])`, "iu");
|
|
795
855
|
return re.test(text);
|
|
796
856
|
}
|
|
797
|
-
function lintText(text, code, terms, label,
|
|
857
|
+
function lintText(text, code, terms, label, path25, line) {
|
|
798
858
|
const out = [];
|
|
799
859
|
const lower = text.toLowerCase();
|
|
800
860
|
for (const term of terms) {
|
|
801
861
|
if (wordBoundary(lower, term)) {
|
|
802
862
|
out.push(
|
|
803
863
|
diag(code, "error", `${label}: "${term}"`, {
|
|
804
|
-
path:
|
|
864
|
+
path: path25,
|
|
805
865
|
line,
|
|
806
866
|
suggestion: "La especificaci\xF3n es funcional y de negocio: describe comportamiento, no tecnolog\xEDa ni adjetivos vagos"
|
|
807
867
|
})
|
|
@@ -810,7 +870,7 @@ function lintText(text, code, terms, label, path22, line) {
|
|
|
810
870
|
}
|
|
811
871
|
return out;
|
|
812
872
|
}
|
|
813
|
-
function lintRequirement(req,
|
|
873
|
+
function lintRequirement(req, path25, opts = {}) {
|
|
814
874
|
const out = [];
|
|
815
875
|
const vague = opts.language === "en" ? VAGUE_EN : VAGUE_ES;
|
|
816
876
|
const tech = opts.language === "en" ? TECH_EN : TECH_ES;
|
|
@@ -821,32 +881,32 @@ function lintRequirement(req, path22, opts = {}) {
|
|
|
821
881
|
...req.scenarios.flatMap((s) => [...s.when.map((w) => ({ text: w, line: s.line })), ...s.then.map((t) => ({ text: t, line: s.line }))])
|
|
822
882
|
];
|
|
823
883
|
for (const part of parts) {
|
|
824
|
-
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));
|
|
825
885
|
if (opts.businessOnly !== false) {
|
|
826
|
-
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));
|
|
827
887
|
}
|
|
828
888
|
}
|
|
829
889
|
if (req.scenarios.length === 0) {
|
|
830
|
-
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" }));
|
|
831
891
|
}
|
|
832
892
|
return out;
|
|
833
893
|
}
|
|
834
|
-
function lintDelta(delta, livingRequirements,
|
|
894
|
+
function lintDelta(delta, livingRequirements, path25, opts = {}) {
|
|
835
895
|
const out = [...delta.diagnostics];
|
|
836
896
|
for (const req of [...delta.added, ...delta.modified]) {
|
|
837
|
-
out.push(...lintRequirement(req,
|
|
897
|
+
out.push(...lintRequirement(req, path25, opts));
|
|
838
898
|
}
|
|
839
899
|
for (const req of delta.modified) {
|
|
840
900
|
const living = livingRequirements.get(req.id);
|
|
841
901
|
if (!living) {
|
|
842
|
-
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 }));
|
|
843
903
|
continue;
|
|
844
904
|
}
|
|
845
905
|
for (const existing of living.scenarios) {
|
|
846
906
|
if (!req.scenarios.some((s) => s.id === existing.id)) {
|
|
847
907
|
out.push(
|
|
848
908
|
diag("TRACE-007", "error", `MODIFIED ${req.id} pierde el escenario ${existing.id}: copia el bloque completo`, {
|
|
849
|
-
path:
|
|
909
|
+
path: path25,
|
|
850
910
|
line: req.line,
|
|
851
911
|
suggestion: "Copia el bloque completo de la spec viva y ed\xEDtalo; para quitarlo, decl\xE1ralo en REMOVED"
|
|
852
912
|
})
|
|
@@ -857,15 +917,15 @@ function lintDelta(delta, livingRequirements, path22, opts = {}) {
|
|
|
857
917
|
for (const req of delta.removed) {
|
|
858
918
|
const living = livingRequirements.get(req.id);
|
|
859
919
|
if (!living) {
|
|
860
|
-
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 }));
|
|
861
921
|
}
|
|
862
922
|
}
|
|
863
923
|
for (const rename2 of delta.renamed) {
|
|
864
924
|
if (rename2.from.id !== rename2.to.id) {
|
|
865
|
-
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 }));
|
|
866
926
|
}
|
|
867
927
|
if (!livingRequirements.has(rename2.from.id)) {
|
|
868
|
-
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 }));
|
|
869
929
|
}
|
|
870
930
|
}
|
|
871
931
|
return out;
|
|
@@ -895,7 +955,7 @@ var MERMAID_KEYWORDS = [
|
|
|
895
955
|
"architecture-beta"
|
|
896
956
|
];
|
|
897
957
|
var MERMAID_BLOCKS = /^\s*(alt|loop|opt|par|rect|critical|break|subgraph)\b/;
|
|
898
|
-
function lintPlan(planText,
|
|
958
|
+
function lintPlan(planText, path25) {
|
|
899
959
|
const out = [];
|
|
900
960
|
const blocks = [...planText.matchAll(/```mermaid\r?\n([\s\S]*?)```/g)];
|
|
901
961
|
for (const [index, block] of blocks.entries()) {
|
|
@@ -905,7 +965,7 @@ function lintPlan(planText, path22) {
|
|
|
905
965
|
if (!MERMAID_KEYWORDS.some((keyword) => first.startsWith(keyword))) {
|
|
906
966
|
out.push(
|
|
907
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)}"`, {
|
|
908
|
-
path:
|
|
968
|
+
path: path25,
|
|
909
969
|
suggestion: "Corrige el tipo del diagrama o elimina el bloque"
|
|
910
970
|
})
|
|
911
971
|
);
|
|
@@ -919,7 +979,7 @@ function lintPlan(planText, path22) {
|
|
|
919
979
|
if (open !== 0) {
|
|
920
980
|
out.push(
|
|
921
981
|
diag("LINT-PLN-002", "error", `Diagrama mermaid ${index + 1}: faltan ${Math.abs(open)} \`end\` (bloques alt/loop/subgraph sin cerrar)`, {
|
|
922
|
-
path:
|
|
982
|
+
path: path25,
|
|
923
983
|
suggestion: "Cierra cada bloque alt/loop/opt/par/rect/subgraph con `end`"
|
|
924
984
|
})
|
|
925
985
|
);
|
|
@@ -931,7 +991,7 @@ function lintPlan(planText, path22) {
|
|
|
931
991
|
if (message.includes(";")) {
|
|
932
992
|
out.push(
|
|
933
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`, {
|
|
934
|
-
path:
|
|
994
|
+
path: path25,
|
|
935
995
|
suggestion: "Sustituye `;` por `\xB7` o `,` en los mensajes de sequenceDiagram"
|
|
936
996
|
})
|
|
937
997
|
);
|
|
@@ -1044,15 +1104,25 @@ function checkTrace(input) {
|
|
|
1044
1104
|
);
|
|
1045
1105
|
}
|
|
1046
1106
|
}
|
|
1107
|
+
const linkedIds = new Set(input.linked?.ids ?? []);
|
|
1108
|
+
const unavailableLinks = input.linked?.unavailable ?? [];
|
|
1047
1109
|
for (const task of taskById.values()) {
|
|
1048
1110
|
for (const c of task.covers) {
|
|
1049
|
-
if (!livingReqs.has(c) && !deltaScenarios.has(c) && !allScenarioIds.has(c)) {
|
|
1111
|
+
if (!livingReqs.has(c) && !deltaScenarios.has(c) && !allScenarioIds.has(c) && !linkedIds.has(c)) {
|
|
1050
1112
|
findings.push(
|
|
1051
1113
|
diag("TRACE-003", "error", `La tarea ${task.id} cubre ${c}, que no existe`, {
|
|
1052
1114
|
path: change.tasks?.path,
|
|
1053
|
-
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"
|
|
1054
1116
|
})
|
|
1055
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
|
+
}
|
|
1056
1126
|
}
|
|
1057
1127
|
}
|
|
1058
1128
|
for (const dep of task.dependsOn) {
|
|
@@ -1219,12 +1289,172 @@ function planBlock(block, maxParallel) {
|
|
|
1219
1289
|
}
|
|
1220
1290
|
|
|
1221
1291
|
// src/lifecycle.ts
|
|
1292
|
+
import path3 from "path";
|
|
1293
|
+
|
|
1294
|
+
// src/contracts.ts
|
|
1222
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
|
|
1223
1453
|
function verifyApproval(change, approvals, cfg, specContent) {
|
|
1224
1454
|
if (cfg.gates.approval === "none") return { status: "not_required" };
|
|
1225
1455
|
if ((change.meta?.overrides ?? []).some((o) => o.gate === "approval")) return { status: "overridden" };
|
|
1226
1456
|
if (!change.delta) return { status: "missing" };
|
|
1227
|
-
const key =
|
|
1457
|
+
const key = path3.posix.join("changes", change.slug, "spec.md");
|
|
1228
1458
|
const approval = approvals.get(key) ?? approvals.get(`${key}`);
|
|
1229
1459
|
if (!approval) return { status: "missing" };
|
|
1230
1460
|
if (specContent === void 0) return { status: "valid", approvedBy: approval.by, approvedAt: approval.at };
|
|
@@ -1240,6 +1470,29 @@ function requiresMockups(meta, cfg) {
|
|
|
1240
1470
|
function mockupOverride(change) {
|
|
1241
1471
|
return (change.meta?.overrides ?? []).some((override) => override.gate === "mockup");
|
|
1242
1472
|
}
|
|
1473
|
+
function docsReady(change) {
|
|
1474
|
+
const paths = change.docsPaths ?? [];
|
|
1475
|
+
return paths.some((p) => p.endsWith("tecnica.md")) && paths.some((p) => p.endsWith("manual.md"));
|
|
1476
|
+
}
|
|
1477
|
+
function clarifyAdvisory(change, cfg) {
|
|
1478
|
+
const open = change.clarify?.open.length ?? 0;
|
|
1479
|
+
if (cfg.gates.clarify.mode !== "advisory" || open === 0) return [];
|
|
1480
|
+
return [
|
|
1481
|
+
diag("ATLAS-CLARIFY-001", "warning", `El cambio "${change.slug}" tiene ${open} pregunta(s) sin aclarar`, {
|
|
1482
|
+
...change.clarifyPath !== void 0 ? { path: change.clarifyPath } : {},
|
|
1483
|
+
suggestion: `Aclara antes de planificar: /satlas.clarify ${change.slug} (o satlas clarify ${change.slug})`
|
|
1484
|
+
})
|
|
1485
|
+
];
|
|
1486
|
+
}
|
|
1487
|
+
function docsAdvisory(change, cfg) {
|
|
1488
|
+
const lane = change.meta?.lane ?? cfg.lanes.default;
|
|
1489
|
+
if (lane !== "full" || cfg.gates.docs.mode !== "advisory" || docsReady(change)) return [];
|
|
1490
|
+
return [
|
|
1491
|
+
diag("ATLAS-DOCS-001", "warning", `El cambio "${change.slug}" (carril completo) no tiene su documentaci\xF3n t\xE9cnica y manual`, {
|
|
1492
|
+
suggestion: `Genera la documentaci\xF3n: satlas docs ${change.slug} (o /satlas.docs ${change.slug})`
|
|
1493
|
+
})
|
|
1494
|
+
];
|
|
1495
|
+
}
|
|
1243
1496
|
function deriveState(input) {
|
|
1244
1497
|
const { change, cfg, approval, blockingFindings } = input;
|
|
1245
1498
|
const lane = change.meta?.lane ?? cfg.lanes.default;
|
|
@@ -1302,6 +1555,16 @@ function deriveState(input) {
|
|
|
1302
1555
|
};
|
|
1303
1556
|
}
|
|
1304
1557
|
if (!change.planPath && !change.tasks) {
|
|
1558
|
+
const openQuestions = change.clarify?.open.length ?? 0;
|
|
1559
|
+
if (openQuestions > 0 && cfg.gates.clarify.mode === "blocking") {
|
|
1560
|
+
blockedBy.push(`aclaraci\xF3n pendiente (${openQuestions})`);
|
|
1561
|
+
return {
|
|
1562
|
+
state: "approved",
|
|
1563
|
+
blockedBy,
|
|
1564
|
+
nextAction: next(`/satlas.clarify ${change.slug}`, `Aclarar ${openQuestions} pregunta(s) antes de planificar`, true),
|
|
1565
|
+
progress
|
|
1566
|
+
};
|
|
1567
|
+
}
|
|
1305
1568
|
return { state: "approved", blockedBy, nextAction: next(`/satlas.plan ${change.slug}`, "Crear el plan t\xE9cnico y las tareas", true), progress };
|
|
1306
1569
|
}
|
|
1307
1570
|
if (tasksTotal > 0 && tasksDone < tasksTotal) {
|
|
@@ -1315,6 +1578,17 @@ function deriveState(input) {
|
|
|
1315
1578
|
blockedBy.push("review pendiente");
|
|
1316
1579
|
return { state: "verified", blockedBy, nextAction: next(`/satlas.review ${change.slug}`, "Revisi\xF3n de c\xF3digo", true), progress };
|
|
1317
1580
|
}
|
|
1581
|
+
if (lane === "full" && cfg.gates.docs.mode === "blocking" && !docsReady(change)) {
|
|
1582
|
+
blockedBy.push("documentaci\xF3n pendiente");
|
|
1583
|
+
return { state: "reviewed", blockedBy, nextAction: next(`/satlas.docs ${change.slug}`, "Generar la documentaci\xF3n t\xE9cnica y manual del cambio", true), progress };
|
|
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
|
+
}
|
|
1318
1592
|
return { state: "ready", blockedBy, nextAction: next(`satlas archive ${change.slug}`, "Archivar el cambio y plegar los deltas"), progress };
|
|
1319
1593
|
}
|
|
1320
1594
|
function stateLabel(state) {
|
|
@@ -1328,6 +1602,7 @@ function stateLabel(state) {
|
|
|
1328
1602
|
building: "construyendo",
|
|
1329
1603
|
built: "construido",
|
|
1330
1604
|
verified: "verificado",
|
|
1605
|
+
reviewed: "revisado",
|
|
1331
1606
|
ready: "listo para archivar",
|
|
1332
1607
|
archived: "archivado"
|
|
1333
1608
|
};
|
|
@@ -1335,8 +1610,8 @@ function stateLabel(state) {
|
|
|
1335
1610
|
}
|
|
1336
1611
|
|
|
1337
1612
|
// src/profiles.ts
|
|
1338
|
-
import
|
|
1339
|
-
import { parse as
|
|
1613
|
+
import path4 from "path";
|
|
1614
|
+
import { parse as parseYaml5 } from "yaml";
|
|
1340
1615
|
import { z as z3 } from "zod";
|
|
1341
1616
|
var profileSchema = z3.object({
|
|
1342
1617
|
name: z3.string().min(1),
|
|
@@ -1383,7 +1658,7 @@ async function loadProfileFile(filePath) {
|
|
|
1383
1658
|
if (raw === void 0) return void 0;
|
|
1384
1659
|
let data;
|
|
1385
1660
|
try {
|
|
1386
|
-
data =
|
|
1661
|
+
data = parseYaml5(raw);
|
|
1387
1662
|
} catch {
|
|
1388
1663
|
return void 0;
|
|
1389
1664
|
}
|
|
@@ -1395,7 +1670,7 @@ async function loadProfilesFromDir(dir) {
|
|
|
1395
1670
|
const out = [];
|
|
1396
1671
|
for (const entry of entries) {
|
|
1397
1672
|
if (!/\.ya?ml$/i.test(entry)) continue;
|
|
1398
|
-
const profile = await loadProfileFile(
|
|
1673
|
+
const profile = await loadProfileFile(path4.join(dir, entry));
|
|
1399
1674
|
if (profile) out.push(profile);
|
|
1400
1675
|
}
|
|
1401
1676
|
return out;
|
|
@@ -1433,10 +1708,10 @@ async function detectProfiles(root, profiles) {
|
|
|
1433
1708
|
return result;
|
|
1434
1709
|
}
|
|
1435
1710
|
async function loadDetectedBest(sddDir) {
|
|
1436
|
-
const raw = await readTextIfExists(
|
|
1711
|
+
const raw = await readTextIfExists(path4.join(sddDir, "profiles", "detected.yaml"));
|
|
1437
1712
|
if (raw === void 0) return void 0;
|
|
1438
1713
|
try {
|
|
1439
|
-
const data =
|
|
1714
|
+
const data = parseYaml5(raw);
|
|
1440
1715
|
return typeof data?.best === "string" ? data.best : void 0;
|
|
1441
1716
|
} catch {
|
|
1442
1717
|
return void 0;
|
|
@@ -1473,12 +1748,12 @@ function detectionToYaml(result, generatedAt) {
|
|
|
1473
1748
|
}
|
|
1474
1749
|
|
|
1475
1750
|
// src/workspace.ts
|
|
1476
|
-
import
|
|
1751
|
+
import path7 from "path";
|
|
1477
1752
|
|
|
1478
1753
|
// src/fixes.ts
|
|
1479
|
-
import
|
|
1754
|
+
import path5 from "path";
|
|
1480
1755
|
import { stringify as stringifyYaml3 } from "yaml";
|
|
1481
|
-
var LIVING_FIXES_DIR =
|
|
1756
|
+
var LIVING_FIXES_DIR = path5.join(".sdd", "fixes");
|
|
1482
1757
|
var COMMENT_RE = /<!--[\s\S]*?-->/g;
|
|
1483
1758
|
function parseFixCovers(content) {
|
|
1484
1759
|
const clean = content.replace(COMMENT_RE, "");
|
|
@@ -1505,7 +1780,7 @@ function coversOf(value) {
|
|
|
1505
1780
|
function parseLivingFix(content, file) {
|
|
1506
1781
|
const fm = parseFrontmatter(content, file);
|
|
1507
1782
|
const data = fm.data;
|
|
1508
|
-
const base =
|
|
1783
|
+
const base = path5.basename(file).replace(/\.md$/i, "");
|
|
1509
1784
|
const slug = typeof data["slug"] === "string" && data["slug"].trim() !== "" ? data["slug"].trim() : base.replace(/^\d{4}-\d{2}-/, "");
|
|
1510
1785
|
const fix = {
|
|
1511
1786
|
slug,
|
|
@@ -1521,14 +1796,14 @@ function parseLivingFix(content, file) {
|
|
|
1521
1796
|
return fix;
|
|
1522
1797
|
}
|
|
1523
1798
|
async function archivedFixes(root, known) {
|
|
1524
|
-
const archiveDir =
|
|
1799
|
+
const archiveDir = path5.join(root, ".sdd", "changes", "archive");
|
|
1525
1800
|
const fixes = [];
|
|
1526
1801
|
for (const entry of await listDirs(archiveDir)) {
|
|
1527
|
-
const dir =
|
|
1528
|
-
const fixFile =
|
|
1802
|
+
const dir = path5.join(archiveDir, entry);
|
|
1803
|
+
const fixFile = path5.join(dir, "fix.md");
|
|
1529
1804
|
const fixRaw = await readTextIfExists(fixFile);
|
|
1530
1805
|
if (fixRaw === void 0) continue;
|
|
1531
|
-
const metaFile =
|
|
1806
|
+
const metaFile = path5.join(dir, "meta.yaml");
|
|
1532
1807
|
const metaRaw = await readTextIfExists(metaFile);
|
|
1533
1808
|
const meta = metaRaw !== void 0 ? parseChangeMeta(metaRaw, metaFile).meta : void 0;
|
|
1534
1809
|
if (meta?.lane !== "fix") continue;
|
|
@@ -1551,11 +1826,11 @@ async function archivedFixes(root, known) {
|
|
|
1551
1826
|
return fixes;
|
|
1552
1827
|
}
|
|
1553
1828
|
async function loadLivingFixes(root) {
|
|
1554
|
-
const dir =
|
|
1829
|
+
const dir = path5.join(root, LIVING_FIXES_DIR);
|
|
1555
1830
|
const entries = (await listDir(dir)).filter((entry) => entry.toLowerCase().endsWith(".md")).sort();
|
|
1556
1831
|
const fixes = [];
|
|
1557
1832
|
for (const entry of entries) {
|
|
1558
|
-
const file =
|
|
1833
|
+
const file = path5.join(dir, entry);
|
|
1559
1834
|
const content = await readTextIfExists(file);
|
|
1560
1835
|
if (content === void 0) continue;
|
|
1561
1836
|
fixes.push(parseLivingFix(content, file));
|
|
@@ -1564,10 +1839,10 @@ async function loadLivingFixes(root) {
|
|
|
1564
1839
|
return fixes.sort((a, b) => b.date.localeCompare(a.date) || b.slug.localeCompare(a.slug));
|
|
1565
1840
|
}
|
|
1566
1841
|
async function writeLivingFix(root, input) {
|
|
1567
|
-
const dir =
|
|
1842
|
+
const dir = path5.join(root, LIVING_FIXES_DIR);
|
|
1568
1843
|
const month = input.date.slice(0, 7);
|
|
1569
|
-
const file =
|
|
1570
|
-
const relativePath = toPosix(
|
|
1844
|
+
const file = path5.join(dir, `${month}-${input.slug}.md`);
|
|
1845
|
+
const relativePath = toPosix(path5.relative(root, file));
|
|
1571
1846
|
if (await exists(file)) return { file, relativePath, created: false };
|
|
1572
1847
|
const data = {
|
|
1573
1848
|
slug: input.slug,
|
|
@@ -1587,35 +1862,170 @@ ${body}
|
|
|
1587
1862
|
return { file, relativePath, created: true };
|
|
1588
1863
|
}
|
|
1589
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
|
+
|
|
1590
2000
|
// src/workspace.ts
|
|
1591
2001
|
var SDD_DIR = ".sdd";
|
|
1592
2002
|
async function findWorkspaceRoot(start) {
|
|
1593
|
-
let current =
|
|
2003
|
+
let current = path7.resolve(start);
|
|
1594
2004
|
for (let i = 0; i < 40; i += 1) {
|
|
1595
|
-
if (await isDirectory(
|
|
1596
|
-
const parent =
|
|
2005
|
+
if (await isDirectory(path7.join(current, SDD_DIR))) return current;
|
|
2006
|
+
const parent = path7.dirname(current);
|
|
1597
2007
|
if (parent === current) return void 0;
|
|
1598
2008
|
current = parent;
|
|
1599
2009
|
}
|
|
1600
2010
|
return void 0;
|
|
1601
2011
|
}
|
|
1602
2012
|
async function loadApprovals(sddDir) {
|
|
1603
|
-
const file =
|
|
2013
|
+
const file = path7.join(sddDir, "approvals.yaml");
|
|
1604
2014
|
const raw = await readTextIfExists(file);
|
|
1605
2015
|
if (raw === void 0) return { byArtifact: /* @__PURE__ */ new Map(), diagnostics: [] };
|
|
1606
2016
|
const parsed = parseApprovals(raw, file);
|
|
1607
2017
|
const map = /* @__PURE__ */ new Map();
|
|
1608
2018
|
for (const a of parsed.approvals?.approvals ?? []) {
|
|
1609
|
-
map.set(a.artifact.split(
|
|
2019
|
+
map.set(a.artifact.split(path7.sep).join("/"), { hash: a.artifactHash, by: a.approvedBy, at: a.approvedAt });
|
|
1610
2020
|
}
|
|
1611
2021
|
return { byArtifact: map, diagnostics: parsed.diagnostics };
|
|
1612
2022
|
}
|
|
1613
2023
|
async function loadSpecs(sddDir) {
|
|
1614
|
-
const specsDir =
|
|
2024
|
+
const specsDir = path7.join(sddDir, "specs");
|
|
1615
2025
|
const domains = await listDirs(specsDir);
|
|
1616
2026
|
const out = [];
|
|
1617
2027
|
for (const domain of domains) {
|
|
1618
|
-
const file =
|
|
2028
|
+
const file = path7.join(specsDir, domain, "spec.md");
|
|
1619
2029
|
if (!await exists(file)) continue;
|
|
1620
2030
|
const content = await readText(file);
|
|
1621
2031
|
out.push({ domain, path: file, spec: parseSpecFile(content, file) });
|
|
@@ -1623,10 +2033,10 @@ async function loadSpecs(sddDir) {
|
|
|
1623
2033
|
return out;
|
|
1624
2034
|
}
|
|
1625
2035
|
async function loadChange(root, slug, relDir) {
|
|
1626
|
-
const dir =
|
|
2036
|
+
const dir = path7.join(root, SDD_DIR, "changes", relDir ?? slug);
|
|
1627
2037
|
const diagnostics = [];
|
|
1628
2038
|
const change = { slug, dir, diagnostics };
|
|
1629
|
-
const metaFile =
|
|
2039
|
+
const metaFile = path7.join(dir, "meta.yaml");
|
|
1630
2040
|
const metaRaw = await readTextIfExists(metaFile);
|
|
1631
2041
|
if (metaRaw === void 0) {
|
|
1632
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" }));
|
|
@@ -1635,7 +2045,7 @@ async function loadChange(root, slug, relDir) {
|
|
|
1635
2045
|
diagnostics.push(...parsed.diagnostics);
|
|
1636
2046
|
if (parsed.meta) change.meta = parsed.meta;
|
|
1637
2047
|
}
|
|
1638
|
-
const deltaFile =
|
|
2048
|
+
const deltaFile = path7.join(dir, "spec.md");
|
|
1639
2049
|
const deltaRaw = await readTextIfExists(deltaFile);
|
|
1640
2050
|
if (deltaRaw !== void 0) {
|
|
1641
2051
|
change.delta = parseDelta(deltaRaw, deltaFile);
|
|
@@ -1643,42 +2053,57 @@ async function loadChange(root, slug, relDir) {
|
|
|
1643
2053
|
} else {
|
|
1644
2054
|
diagnostics.push(diag("ATLAS-FILES-002", "warning", `El cambio "${slug}" no tiene spec.md (delta)`, { path: deltaFile }));
|
|
1645
2055
|
}
|
|
1646
|
-
const planFile =
|
|
2056
|
+
const planFile = path7.join(dir, "plan.md");
|
|
1647
2057
|
if (await exists(planFile)) change.planPath = planFile;
|
|
1648
|
-
const reviewFile =
|
|
2058
|
+
const reviewFile = path7.join(dir, "review.md");
|
|
1649
2059
|
if (await exists(reviewFile)) change.reviewPath = reviewFile;
|
|
1650
|
-
const presentationFile =
|
|
2060
|
+
const presentationFile = path7.join(dir, "presentation", "index.html");
|
|
1651
2061
|
if (await exists(presentationFile)) change.presentationPath = presentationFile;
|
|
1652
|
-
const tasksFile =
|
|
2062
|
+
const tasksFile = path7.join(dir, "tasks.md");
|
|
1653
2063
|
const tasksRaw = await readTextIfExists(tasksFile);
|
|
1654
2064
|
if (tasksRaw !== void 0) {
|
|
1655
2065
|
change.tasks = parseTasksFile(tasksRaw, tasksFile);
|
|
1656
2066
|
diagnostics.push(...change.tasks.diagnostics);
|
|
1657
2067
|
}
|
|
1658
|
-
const verifyFile =
|
|
2068
|
+
const verifyFile = path7.join(dir, "verify.md");
|
|
1659
2069
|
const verifyRaw = await readTextIfExists(verifyFile);
|
|
1660
2070
|
if (verifyRaw !== void 0) {
|
|
1661
2071
|
change.verify = parseVerifyFile(verifyRaw, verifyFile);
|
|
1662
2072
|
diagnostics.push(...change.verify.diagnostics);
|
|
1663
2073
|
}
|
|
1664
|
-
const fixFile =
|
|
2074
|
+
const fixFile = path7.join(dir, "fix.md");
|
|
1665
2075
|
const fixRaw = await readTextIfExists(fixFile);
|
|
1666
2076
|
if (fixRaw !== void 0) {
|
|
1667
2077
|
change.fix = parseVerifyFile(fixRaw, fixFile);
|
|
1668
2078
|
diagnostics.push(...change.fix.diagnostics);
|
|
1669
2079
|
change.fixCovers = parseFixCovers(fixRaw);
|
|
1670
2080
|
}
|
|
1671
|
-
const
|
|
2081
|
+
const clarifyFile = path7.join(dir, "clarify.md");
|
|
2082
|
+
const clarifyRaw = await readTextIfExists(clarifyFile);
|
|
2083
|
+
if (clarifyRaw !== void 0) {
|
|
2084
|
+
change.clarify = parseClarify(clarifyRaw, clarifyFile);
|
|
2085
|
+
change.clarifyPath = clarifyFile;
|
|
2086
|
+
diagnostics.push(...change.clarify.diagnostics);
|
|
2087
|
+
}
|
|
2088
|
+
const docsPaths = [];
|
|
2089
|
+
for (const name of ["tecnica.md", "manual.md"]) {
|
|
2090
|
+
const docFile = path7.join(dir, "docs", name);
|
|
2091
|
+
if (await exists(docFile)) docsPaths.push(docFile);
|
|
2092
|
+
}
|
|
2093
|
+
if (docsPaths.length > 0) change.docsPaths = docsPaths;
|
|
2094
|
+
const contracts = await loadContracts(dir);
|
|
2095
|
+
if (contracts.files.length > 0) change.contracts = contracts;
|
|
2096
|
+
const mockupManifest = path7.join(dir, "mockups", "manifest.yaml");
|
|
1672
2097
|
if (await exists(mockupManifest)) change.mockupManifestPath = mockupManifest;
|
|
1673
2098
|
return change;
|
|
1674
2099
|
}
|
|
1675
2100
|
async function listChangeSlugs(root, opts = {}) {
|
|
1676
|
-
const changesDir =
|
|
2101
|
+
const changesDir = path7.join(root, SDD_DIR, "changes");
|
|
1677
2102
|
const dirs = await listDirs(changesDir);
|
|
1678
2103
|
return dirs.filter((d) => opts.includeArchived ? true : d !== "archive");
|
|
1679
2104
|
}
|
|
1680
2105
|
async function loadWorkspace(root) {
|
|
1681
|
-
const sddDir =
|
|
2106
|
+
const sddDir = path7.join(root, SDD_DIR);
|
|
1682
2107
|
const { config, diagnostics: configDiags } = await loadConfig(sddDir);
|
|
1683
2108
|
const diagnostics = [...configDiags];
|
|
1684
2109
|
const specs = await loadSpecs(sddDir);
|
|
@@ -1691,20 +2116,24 @@ async function loadWorkspace(root) {
|
|
|
1691
2116
|
diagnostics.push(...change.diagnostics);
|
|
1692
2117
|
}
|
|
1693
2118
|
const archived = [];
|
|
1694
|
-
const archivedDir =
|
|
2119
|
+
const archivedDir = path7.join(sddDir, "changes", "archive");
|
|
1695
2120
|
if (await isDirectory(archivedDir)) {
|
|
1696
2121
|
for (const entry of await listDirs(archivedDir)) {
|
|
1697
|
-
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)));
|
|
1698
2123
|
}
|
|
1699
2124
|
}
|
|
1700
|
-
|
|
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 };
|
|
1701
2130
|
}
|
|
1702
2131
|
async function ensureSddDirs(sddDir) {
|
|
1703
2132
|
const created = [];
|
|
1704
|
-
const dirs = ["specs", "changes", "runs", "metrics", "fixes",
|
|
2133
|
+
const dirs = ["specs", "changes", "runs", "metrics", "fixes", path7.join("profiles", "custom")];
|
|
1705
2134
|
const { ensureDir: ensureDir2 } = await import("./fsx-VF2P7ALA.js");
|
|
1706
2135
|
for (const d of dirs) {
|
|
1707
|
-
const full =
|
|
2136
|
+
const full = path7.join(sddDir, d);
|
|
1708
2137
|
if (!await exists(full)) {
|
|
1709
2138
|
await ensureDir2(full);
|
|
1710
2139
|
created.push(full);
|
|
@@ -1714,7 +2143,7 @@ async function ensureSddDirs(sddDir) {
|
|
|
1714
2143
|
}
|
|
1715
2144
|
|
|
1716
2145
|
// src/templates.ts
|
|
1717
|
-
import { stringify as
|
|
2146
|
+
import { stringify as stringifyYaml5 } from "yaml";
|
|
1718
2147
|
var ES = {
|
|
1719
2148
|
constitution: `# Constituci\xF3n del proyecto
|
|
1720
2149
|
|
|
@@ -1884,6 +2313,44 @@ satlas verify <slug> --file fix --scenario REQ-DOMINIO-001-S1 --command "<comand
|
|
|
1884
2313
|
o, si es manual:
|
|
1885
2314
|
satlas verify <slug> --file fix --scenario REQ-DOMINIO-001-S1 --method manual --result pass --by "<nombre>" --notes "<c\xF3mo se comprob\xF3>"
|
|
1886
2315
|
-->
|
|
2316
|
+
`,
|
|
2317
|
+
docTecnica: `# Documentaci\xF3n t\xE9cnica \u2014 {{TITLE}}
|
|
2318
|
+
|
|
2319
|
+
## Resumen del cambio
|
|
2320
|
+
|
|
2321
|
+
- **Cambio**: \`{{SLUG}}\` \xB7 dominio \`{{DOMAIN}}\` \xB7 carril \`{{LANE}}\`
|
|
2322
|
+
- **Actualizado**: {{DATE}}
|
|
2323
|
+
- **Tareas**: {{TASKS}}
|
|
2324
|
+
|
|
2325
|
+
## Requisitos y escenarios
|
|
2326
|
+
|
|
2327
|
+
{{REQUIREMENTS}}
|
|
2328
|
+
|
|
2329
|
+
## Evidencia registrada
|
|
2330
|
+
|
|
2331
|
+
{{EVIDENCE}}
|
|
2332
|
+
|
|
2333
|
+
## Pendiente de evidencia
|
|
2334
|
+
|
|
2335
|
+
{{PENDING}}
|
|
2336
|
+
`,
|
|
2337
|
+
docManual: `# Manual \u2014 {{TITLE}}
|
|
2338
|
+
|
|
2339
|
+
## Qu\xE9 hace este cambio
|
|
2340
|
+
|
|
2341
|
+
{{TITLE}} \u2014 dominio \`{{DOMAIN}}\` (cambio \`{{SLUG}}\`, carril \`{{LANE}}\`).
|
|
2342
|
+
|
|
2343
|
+
## C\xF3mo se usa
|
|
2344
|
+
|
|
2345
|
+
{{SCENARIOS}}
|
|
2346
|
+
|
|
2347
|
+
## C\xF3mo se comprob\xF3
|
|
2348
|
+
|
|
2349
|
+
{{EVIDENCE}}
|
|
2350
|
+
|
|
2351
|
+
## Pendiente de comprobar
|
|
2352
|
+
|
|
2353
|
+
{{PENDING}}
|
|
1887
2354
|
`
|
|
1888
2355
|
};
|
|
1889
2356
|
var EN = {
|
|
@@ -2009,6 +2476,44 @@ Cubre: REQ-DOMAIN-001
|
|
|
2009
2476
|
<!-- Register real evidence with:
|
|
2010
2477
|
satlas verify <slug> --file fix --scenario REQ-DOMAIN-001-S1 --command "<command>" --by "<name>"
|
|
2011
2478
|
-->
|
|
2479
|
+
`,
|
|
2480
|
+
docTecnica: `# Technical documentation \u2014 {{TITLE}}
|
|
2481
|
+
|
|
2482
|
+
## Change summary
|
|
2483
|
+
|
|
2484
|
+
- **Change**: \`{{SLUG}}\` \xB7 domain \`{{DOMAIN}}\` \xB7 lane \`{{LANE}}\`
|
|
2485
|
+
- **Updated**: {{DATE}}
|
|
2486
|
+
- **Tasks**: {{TASKS}}
|
|
2487
|
+
|
|
2488
|
+
## Requirements and scenarios
|
|
2489
|
+
|
|
2490
|
+
{{REQUIREMENTS}}
|
|
2491
|
+
|
|
2492
|
+
## Recorded evidence
|
|
2493
|
+
|
|
2494
|
+
{{EVIDENCE}}
|
|
2495
|
+
|
|
2496
|
+
## Pending evidence
|
|
2497
|
+
|
|
2498
|
+
{{PENDING}}
|
|
2499
|
+
`,
|
|
2500
|
+
docManual: `# Manual \u2014 {{TITLE}}
|
|
2501
|
+
|
|
2502
|
+
## What this change does
|
|
2503
|
+
|
|
2504
|
+
{{TITLE}} \u2014 domain \`{{DOMAIN}}\` (change \`{{SLUG}}\`, lane \`{{LANE}}\`).
|
|
2505
|
+
|
|
2506
|
+
## How to use it
|
|
2507
|
+
|
|
2508
|
+
{{SCENARIOS}}
|
|
2509
|
+
|
|
2510
|
+
## How it was verified
|
|
2511
|
+
|
|
2512
|
+
{{EVIDENCE}}
|
|
2513
|
+
|
|
2514
|
+
## Pending verification
|
|
2515
|
+
|
|
2516
|
+
{{PENDING}}
|
|
2012
2517
|
`
|
|
2013
2518
|
};
|
|
2014
2519
|
function templatesFor(language) {
|
|
@@ -2030,7 +2535,7 @@ function changeMetaYaml(meta) {
|
|
|
2030
2535
|
if (meta.owner) doc["owner"] = meta.owner;
|
|
2031
2536
|
if (meta.mockups) doc["mockups"] = meta.mockups;
|
|
2032
2537
|
return `# Estado del cambio. La fase se DERIVA de los artefactos; aqu\xED solo hechos.
|
|
2033
|
-
` +
|
|
2538
|
+
` + stringifyYaml5(doc, { lineWidth: 120 });
|
|
2034
2539
|
}
|
|
2035
2540
|
function indexMarkdown(input) {
|
|
2036
2541
|
const es = input.language !== "en";
|
|
@@ -2068,8 +2573,8 @@ ${input.archived} change(s) in \`changes/archive/\`.`);
|
|
|
2068
2573
|
}
|
|
2069
2574
|
|
|
2070
2575
|
// src/github.ts
|
|
2071
|
-
import
|
|
2072
|
-
import { parse as
|
|
2576
|
+
import path8 from "path";
|
|
2577
|
+
import { parse as parseYaml7 } from "yaml";
|
|
2073
2578
|
|
|
2074
2579
|
// src/exec.ts
|
|
2075
2580
|
import { execFile } from "child_process";
|
|
@@ -2213,8 +2718,8 @@ function issueBody(input) {
|
|
|
2213
2718
|
lines.push(`- Siguiente: \`${input.next}\` \u2014 ${input.nextDescription}`);
|
|
2214
2719
|
lines.push("");
|
|
2215
2720
|
lines.push("### Artefactos");
|
|
2216
|
-
lines.push(`- Spec: \`${
|
|
2217
|
-
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")}\``);
|
|
2218
2723
|
if (change.tasks) lines.push(`- Tareas: ${change.tasks.counts.done}/${change.tasks.counts.total}`);
|
|
2219
2724
|
if (change.verify) lines.push(`- Evidencia registrada: ${change.verify.evidence.length}`);
|
|
2220
2725
|
lines.push("");
|
|
@@ -2282,14 +2787,14 @@ async function commentIssue(root, number, body, runner = defaultRunner) {
|
|
|
2282
2787
|
return result.ok ? [] : [diag("ATLAS-GH-006", "error", `No se pudo comentar el issue #${number}: ${(result.stderr || result.stdout).trim().slice(0, 300)}`)];
|
|
2283
2788
|
}
|
|
2284
2789
|
async function patchChangeMeta(root, slug, patch) {
|
|
2285
|
-
const file =
|
|
2790
|
+
const file = path8.join(path8.resolve(root), ".sdd", "changes", slug, "meta.yaml");
|
|
2286
2791
|
const raw = await readTextIfExists(file);
|
|
2287
2792
|
if (raw === void 0) {
|
|
2288
|
-
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)}`)] };
|
|
2289
2794
|
}
|
|
2290
2795
|
let data;
|
|
2291
2796
|
try {
|
|
2292
|
-
data =
|
|
2797
|
+
data = parseYaml7(raw) ?? {};
|
|
2293
2798
|
} catch (err) {
|
|
2294
2799
|
return { path: file, diagnostics: [diag("ATLAS-GH-007", "error", `meta.yaml inv\xE1lido: ${err.message}`)] };
|
|
2295
2800
|
}
|
|
@@ -2339,7 +2844,7 @@ async function approveFromGithub(opts) {
|
|
|
2339
2844
|
const { signApproval: signApproval2 } = await import("./approvals-2PO6223I.js");
|
|
2340
2845
|
const signed = await signApproval2({
|
|
2341
2846
|
root: opts.root,
|
|
2342
|
-
artifact:
|
|
2847
|
+
artifact: path8.posix.join("changes", opts.slug, "spec.md"),
|
|
2343
2848
|
by,
|
|
2344
2849
|
channel: "tracker",
|
|
2345
2850
|
note: `Aprobado v\xEDa GitHub issue #${number} (etiqueta "${label}")`
|
|
@@ -2399,7 +2904,7 @@ async function syncGithubIssue(root, slug, opts = {}) {
|
|
|
2399
2904
|
}
|
|
2400
2905
|
|
|
2401
2906
|
// src/evidence.ts
|
|
2402
|
-
import
|
|
2907
|
+
import path9 from "path";
|
|
2403
2908
|
var SCENARIO_HEAD_RE2 = /^###\s+(REQ-[A-Z0-9-]+-S\d+)\b/;
|
|
2404
2909
|
function locateBlocks(content) {
|
|
2405
2910
|
const normalized = content.replace(/\r\n?/g, "\n");
|
|
@@ -2437,10 +2942,10 @@ function buildEvidenceYaml(evidence) {
|
|
|
2437
2942
|
return lines.join("\n");
|
|
2438
2943
|
}
|
|
2439
2944
|
async function recordEvidence(opts) {
|
|
2440
|
-
const root =
|
|
2945
|
+
const root = path9.resolve(opts.root);
|
|
2441
2946
|
const slug = opts.slug;
|
|
2442
2947
|
const fileKind = opts.file ?? "verify";
|
|
2443
|
-
const file =
|
|
2948
|
+
const file = path9.join(root, ".sdd", "changes", slug, fileKind === "fix" ? "fix.md" : "verify.md");
|
|
2444
2949
|
const diagnostics = [];
|
|
2445
2950
|
const scenario = opts.scenario.toUpperCase();
|
|
2446
2951
|
if (!SCENARIO_ID_RE.test(scenario)) {
|
|
@@ -2537,7 +3042,7 @@ function evidenceSummary(content) {
|
|
|
2537
3042
|
}
|
|
2538
3043
|
|
|
2539
3044
|
// src/runs.ts
|
|
2540
|
-
import
|
|
3045
|
+
import path10 from "path";
|
|
2541
3046
|
import { randomBytes } from "crypto";
|
|
2542
3047
|
var RUN_EVENT_TYPES = [
|
|
2543
3048
|
"run_started",
|
|
@@ -2563,7 +3068,7 @@ function generateRunId(now = /* @__PURE__ */ new Date()) {
|
|
|
2563
3068
|
return `${iso.slice(0, 8)}-${iso.slice(8, 14)}-${randomBytes(3).toString("hex")}`;
|
|
2564
3069
|
}
|
|
2565
3070
|
function runsDir(root) {
|
|
2566
|
-
return
|
|
3071
|
+
return path10.join(path10.resolve(root), ".sdd", "runs");
|
|
2567
3072
|
}
|
|
2568
3073
|
async function createRun(root, slug, phase, inputs, now = /* @__PURE__ */ new Date()) {
|
|
2569
3074
|
const runId = generateRunId(now);
|
|
@@ -2576,25 +3081,25 @@ async function createRun(root, slug, phase, inputs, now = /* @__PURE__ */ new Da
|
|
|
2576
3081
|
updatedAt: localStamp(now)
|
|
2577
3082
|
};
|
|
2578
3083
|
if (inputs) state.inputs = inputs;
|
|
2579
|
-
const dir =
|
|
3084
|
+
const dir = path10.join(runsDir(root), runId);
|
|
2580
3085
|
await ensureDir(dir);
|
|
2581
|
-
await writeText(
|
|
3086
|
+
await writeText(path10.join(dir, "state.json"), `${JSON.stringify(state, null, 2)}
|
|
2582
3087
|
`);
|
|
2583
|
-
await writeText(
|
|
3088
|
+
await writeText(path10.join(dir, "events.jsonl"), "");
|
|
2584
3089
|
await appendRunEvent(root, runId, "run_started", { slug, phase });
|
|
2585
3090
|
return state;
|
|
2586
3091
|
}
|
|
2587
3092
|
async function appendRunEvent(root, runId, type, data, now = /* @__PURE__ */ new Date()) {
|
|
2588
3093
|
const event = { eventId: randomBytes(4).toString("hex"), at: localStamp(now), type };
|
|
2589
3094
|
if (data) event.data = data;
|
|
2590
|
-
const file =
|
|
3095
|
+
const file = path10.join(runsDir(root), runId, "events.jsonl");
|
|
2591
3096
|
const previous = await readTextIfExists(file) ?? "";
|
|
2592
3097
|
await writeText(file, `${previous}${JSON.stringify(event)}
|
|
2593
3098
|
`);
|
|
2594
3099
|
return event;
|
|
2595
3100
|
}
|
|
2596
3101
|
async function updateRunStatus(root, runId, status, now = /* @__PURE__ */ new Date()) {
|
|
2597
|
-
const file =
|
|
3102
|
+
const file = path10.join(runsDir(root), runId, "state.json");
|
|
2598
3103
|
const raw = await readTextIfExists(file);
|
|
2599
3104
|
if (raw === void 0) return;
|
|
2600
3105
|
const state = JSON.parse(raw);
|
|
@@ -2604,13 +3109,13 @@ async function updateRunStatus(root, runId, status, now = /* @__PURE__ */ new Da
|
|
|
2604
3109
|
`);
|
|
2605
3110
|
}
|
|
2606
3111
|
async function readRun(root, runId) {
|
|
2607
|
-
const dir =
|
|
2608
|
-
const stateRaw = await readTextIfExists(
|
|
3112
|
+
const dir = path10.join(runsDir(root), runId);
|
|
3113
|
+
const stateRaw = await readTextIfExists(path10.join(dir, "state.json"));
|
|
2609
3114
|
if (stateRaw === void 0) {
|
|
2610
3115
|
return { diagnostics: [diag("ATLAS-RUN-001", "error", `No existe el run ${runId}`, { path: dir })] };
|
|
2611
3116
|
}
|
|
2612
3117
|
const state = JSON.parse(stateRaw);
|
|
2613
|
-
const eventsRaw = await readTextIfExists(
|
|
3118
|
+
const eventsRaw = await readTextIfExists(path10.join(dir, "events.jsonl")) ?? "";
|
|
2614
3119
|
const events = [];
|
|
2615
3120
|
for (const line of eventsRaw.split("\n")) {
|
|
2616
3121
|
if (line.trim() === "") continue;
|
|
@@ -2627,7 +3132,7 @@ async function listRuns(root, filter = {}) {
|
|
|
2627
3132
|
const ids = await listDirs(dir);
|
|
2628
3133
|
const out = [];
|
|
2629
3134
|
for (const id of ids) {
|
|
2630
|
-
const raw = await readTextIfExists(
|
|
3135
|
+
const raw = await readTextIfExists(path10.join(dir, id, "state.json"));
|
|
2631
3136
|
if (raw === void 0) continue;
|
|
2632
3137
|
try {
|
|
2633
3138
|
const state = JSON.parse(raw);
|
|
@@ -2641,11 +3146,11 @@ async function listRuns(root, filter = {}) {
|
|
|
2641
3146
|
}
|
|
2642
3147
|
|
|
2643
3148
|
// src/analyze.ts
|
|
2644
|
-
import
|
|
3149
|
+
import path13 from "path";
|
|
2645
3150
|
|
|
2646
3151
|
// src/mockups.ts
|
|
2647
|
-
import
|
|
2648
|
-
import { parse as
|
|
3152
|
+
import path11 from "path";
|
|
3153
|
+
import { parse as parseYaml8, stringify as stringifyYaml6 } from "yaml";
|
|
2649
3154
|
import { z as z4 } from "zod";
|
|
2650
3155
|
var mockupManifestSchema = z4.object({
|
|
2651
3156
|
schema_version: z4.number().int().positive().default(1),
|
|
@@ -2670,20 +3175,20 @@ var mockupManifestSchema = z4.object({
|
|
|
2670
3175
|
});
|
|
2671
3176
|
var TOKEN_CANDIDATES = ["design/tokens.json", "DESIGN.md", "design/DESIGN.md", ".sdd/design/tokens.json"];
|
|
2672
3177
|
function mockupsDir(root, slug) {
|
|
2673
|
-
return
|
|
3178
|
+
return path11.join(path11.resolve(root), ".sdd", "changes", slug, "mockups");
|
|
2674
3179
|
}
|
|
2675
3180
|
async function tokensFile(root) {
|
|
2676
3181
|
for (const candidate of TOKEN_CANDIDATES) {
|
|
2677
|
-
const abs =
|
|
3182
|
+
const abs = path11.join(root, candidate);
|
|
2678
3183
|
if (await exists(abs)) return candidate;
|
|
2679
3184
|
}
|
|
2680
3185
|
return void 0;
|
|
2681
3186
|
}
|
|
2682
3187
|
async function computeInputsHash(root, change) {
|
|
2683
|
-
const deltaPath =
|
|
3188
|
+
const deltaPath = path11.join(change.dir, "spec.md");
|
|
2684
3189
|
const delta = await readTextIfExists(deltaPath) ?? "";
|
|
2685
3190
|
const tokens = await tokensFile(root);
|
|
2686
|
-
const tokensContent = tokens ? await readTextIfExists(
|
|
3191
|
+
const tokensContent = tokens ? await readTextIfExists(path11.join(root, tokens)) ?? "" : "";
|
|
2687
3192
|
return artifactHash(`${delta}
|
|
2688
3193
|
---tokens---
|
|
2689
3194
|
${tokensContent}`);
|
|
@@ -2708,12 +3213,12 @@ function planMockups(change, platform = "web") {
|
|
|
2708
3213
|
return { platform, screens };
|
|
2709
3214
|
}
|
|
2710
3215
|
async function readMockupManifest(root, slug) {
|
|
2711
|
-
const file =
|
|
3216
|
+
const file = path11.join(mockupsDir(root, slug), "manifest.yaml");
|
|
2712
3217
|
const raw = await readTextIfExists(file);
|
|
2713
3218
|
if (raw === void 0) return { diagnostics: [] };
|
|
2714
3219
|
let data;
|
|
2715
3220
|
try {
|
|
2716
|
-
data =
|
|
3221
|
+
data = parseYaml8(raw);
|
|
2717
3222
|
} catch (err) {
|
|
2718
3223
|
return { path: file, diagnostics: [diag("ATLAS-MKP-002", "error", `manifest.yaml inv\xE1lido: ${err.message}`, { path: file })] };
|
|
2719
3224
|
}
|
|
@@ -2800,7 +3305,7 @@ async function checkMockups(root, slug, change) {
|
|
|
2800
3305
|
const { manifest, path: manifestPath, diagnostics } = await readMockupManifest(root, slug);
|
|
2801
3306
|
const findings = [...diagnostics];
|
|
2802
3307
|
if (!manifest || !manifestPath) {
|
|
2803
|
-
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 };
|
|
2804
3309
|
}
|
|
2805
3310
|
const known = /* @__PURE__ */ new Set();
|
|
2806
3311
|
for (const req of [...change.delta?.added ?? [], ...change.delta?.modified ?? []]) {
|
|
@@ -2808,12 +3313,12 @@ async function checkMockups(root, slug, change) {
|
|
|
2808
3313
|
}
|
|
2809
3314
|
findings.push(...lintMockupManifest(manifest, known, manifestPath));
|
|
2810
3315
|
for (const screen of manifest.screens) {
|
|
2811
|
-
const html = await readTextIfExists(
|
|
3316
|
+
const html = await readTextIfExists(path11.join(dir, screen.file));
|
|
2812
3317
|
if (html === void 0) {
|
|
2813
|
-
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) }));
|
|
2814
3319
|
continue;
|
|
2815
3320
|
}
|
|
2816
|
-
findings.push(...lintMockupHtml(html,
|
|
3321
|
+
findings.push(...lintMockupHtml(html, path11.join(dir, screen.file)));
|
|
2817
3322
|
}
|
|
2818
3323
|
const inputsHash = await computeInputsHash(root, change);
|
|
2819
3324
|
const stale = manifest.inputsHash !== void 0 && manifest.inputsHash !== inputsHash;
|
|
@@ -2828,7 +3333,7 @@ async function checkMockups(root, slug, change) {
|
|
|
2828
3333
|
return { findings, stale, manifest, manifestPath };
|
|
2829
3334
|
}
|
|
2830
3335
|
async function writeMockupPlan(root, slug, plan, now = /* @__PURE__ */ new Date()) {
|
|
2831
|
-
const file =
|
|
3336
|
+
const file = path11.join(mockupsDir(root, slug), "plan.yaml");
|
|
2832
3337
|
if (await exists(file)) return file;
|
|
2833
3338
|
const doc = {
|
|
2834
3339
|
schema_version: 1,
|
|
@@ -2836,11 +3341,11 @@ async function writeMockupPlan(root, slug, plan, now = /* @__PURE__ */ new Date(
|
|
|
2836
3341
|
platform: plan.platform,
|
|
2837
3342
|
screens: plan.screens.map((s) => ({ id: s.id, title: s.title, file: s.file, illustrates: s.illustrates, states: s.states, breakpoints: s.breakpoints }))
|
|
2838
3343
|
};
|
|
2839
|
-
await writeText(file,
|
|
3344
|
+
await writeText(file, stringifyYaml6(doc, { lineWidth: 120 }));
|
|
2840
3345
|
return file;
|
|
2841
3346
|
}
|
|
2842
3347
|
async function writeMockupManifest(root, slug, plan, inputsHash, now = /* @__PURE__ */ new Date()) {
|
|
2843
|
-
const file =
|
|
3348
|
+
const file = path11.join(mockupsDir(root, slug), "manifest.yaml");
|
|
2844
3349
|
if (await exists(file)) return file;
|
|
2845
3350
|
const doc = {
|
|
2846
3351
|
schema_version: 1,
|
|
@@ -2852,16 +3357,16 @@ async function writeMockupManifest(root, slug, plan, inputsHash, now = /* @__PUR
|
|
|
2852
3357
|
screens: plan.screens.map((s) => ({ id: s.id, title: s.title, file: s.file, illustrates: s.illustrates, states: s.states, breakpoints: s.breakpoints })),
|
|
2853
3358
|
screenshots: []
|
|
2854
3359
|
};
|
|
2855
|
-
await writeText(file,
|
|
3360
|
+
await writeText(file, stringifyYaml6(doc, { lineWidth: 120 }));
|
|
2856
3361
|
return file;
|
|
2857
3362
|
}
|
|
2858
3363
|
async function updateMockupScreenshots(root, slug, screenshots) {
|
|
2859
|
-
const file =
|
|
3364
|
+
const file = path11.join(mockupsDir(root, slug), "manifest.yaml");
|
|
2860
3365
|
const raw = await readTextIfExists(file);
|
|
2861
3366
|
if (raw === void 0) return void 0;
|
|
2862
|
-
const data =
|
|
3367
|
+
const data = parseYaml8(raw) ?? {};
|
|
2863
3368
|
data["screenshots"] = screenshots;
|
|
2864
|
-
await writeText(file,
|
|
3369
|
+
await writeText(file, stringifyYaml6(data, { lineWidth: 120 }));
|
|
2865
3370
|
return file;
|
|
2866
3371
|
}
|
|
2867
3372
|
async function captureMockups(root, slug, manifest, now = /* @__PURE__ */ new Date()) {
|
|
@@ -2886,9 +3391,9 @@ async function captureMockups(root, slug, manifest, now = /* @__PURE__ */ new Da
|
|
|
2886
3391
|
const page2 = await browser.newPage();
|
|
2887
3392
|
for (const bp of screen.breakpoints && screen.breakpoints.length > 0 ? screen.breakpoints : [1440]) {
|
|
2888
3393
|
await page2.setViewportSize({ width: bp, height: 900 });
|
|
2889
|
-
await page2.goto(`file://${
|
|
2890
|
-
const rel =
|
|
2891
|
-
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) });
|
|
2892
3397
|
screenshots.push(rel);
|
|
2893
3398
|
}
|
|
2894
3399
|
await page2.close();
|
|
@@ -2904,7 +3409,7 @@ async function mockupsReady(root, slug, change) {
|
|
|
2904
3409
|
return Boolean(check.manifest && check.manifest.screens.length > 0 && !check.stale);
|
|
2905
3410
|
}
|
|
2906
3411
|
async function setMockupRequirement(root, slug, value) {
|
|
2907
|
-
const file =
|
|
3412
|
+
const file = path11.join(root, ".sdd", "changes", slug, "meta.yaml");
|
|
2908
3413
|
const raw = await readTextIfExists(file) ?? `schema_version: 1
|
|
2909
3414
|
slug: ${slug}
|
|
2910
3415
|
lane: standard
|
|
@@ -2923,8 +3428,8 @@ lane: standard
|
|
|
2923
3428
|
}
|
|
2924
3429
|
|
|
2925
3430
|
// src/packs.ts
|
|
2926
|
-
import
|
|
2927
|
-
import { parse as
|
|
3431
|
+
import path12 from "path";
|
|
3432
|
+
import { parse as parseYaml9 } from "yaml";
|
|
2928
3433
|
import { z as z5 } from "zod";
|
|
2929
3434
|
var checkSchema = z5.object({
|
|
2930
3435
|
id: z5.string().min(1),
|
|
@@ -3139,17 +3644,17 @@ ${texts.scenarios}`);
|
|
|
3139
3644
|
}
|
|
3140
3645
|
}
|
|
3141
3646
|
async function loadProjectPacks(sddDir) {
|
|
3142
|
-
const dir =
|
|
3647
|
+
const dir = path12.join(sddDir, "packs");
|
|
3143
3648
|
const diagnostics = [];
|
|
3144
3649
|
const packs = [];
|
|
3145
3650
|
for (const entry of await listDir(dir)) {
|
|
3146
3651
|
if (!/\.ya?ml$/i.test(entry)) continue;
|
|
3147
|
-
const file =
|
|
3652
|
+
const file = path12.join(dir, entry);
|
|
3148
3653
|
const raw = await readTextIfExists(file);
|
|
3149
3654
|
if (raw === void 0) continue;
|
|
3150
3655
|
let data;
|
|
3151
3656
|
try {
|
|
3152
|
-
data =
|
|
3657
|
+
data = parseYaml9(raw);
|
|
3153
3658
|
} catch (err) {
|
|
3154
3659
|
diagnostics.push(diag("PACK-000", "error", `Pack inv\xE1lido (${entry}): ${err.message}`, { path: file }));
|
|
3155
3660
|
continue;
|
|
@@ -3233,7 +3738,7 @@ function packFindings(evaluations, change) {
|
|
|
3233
3738
|
// src/analyze.ts
|
|
3234
3739
|
var UI_DOMAINS = /* @__PURE__ */ new Set(["frontend", "mobile", "fullstack"]);
|
|
3235
3740
|
async function runAnalyze(opts) {
|
|
3236
|
-
const root =
|
|
3741
|
+
const root = path13.resolve(opts.root);
|
|
3237
3742
|
const { workspace, config } = await loadWorkspace(root);
|
|
3238
3743
|
const change = workspace.changes.find((c) => c.slug === opts.slug);
|
|
3239
3744
|
if (!change) {
|
|
@@ -3251,9 +3756,9 @@ async function runAnalyze(opts) {
|
|
|
3251
3756
|
for (const req of spec.spec.requirements) living.set(req.id, req);
|
|
3252
3757
|
}
|
|
3253
3758
|
if (change.delta) {
|
|
3254
|
-
findings.push(...lintDelta(change.delta, living,
|
|
3759
|
+
findings.push(...lintDelta(change.delta, living, path13.join(change.dir, "spec.md"), { language: config.spec.language }));
|
|
3255
3760
|
}
|
|
3256
|
-
const trace = checkTrace({ specs: workspace.specs, change, requireEvidence: false });
|
|
3761
|
+
const trace = checkTrace({ specs: workspace.specs, change, requireEvidence: false, linked: linkedTraceInput(workspace) });
|
|
3257
3762
|
findings.push(...trace.findings);
|
|
3258
3763
|
let waves;
|
|
3259
3764
|
if (change.tasks && change.tasks.counts.total > 0) {
|
|
@@ -3300,7 +3805,7 @@ async function runAnalyze(opts) {
|
|
|
3300
3805
|
if (mockups) result.mockups = mockups;
|
|
3301
3806
|
if (packs) result.packs = packs;
|
|
3302
3807
|
if (opts.write !== false) {
|
|
3303
|
-
const file =
|
|
3808
|
+
const file = path13.join(change.dir, "analyze.md");
|
|
3304
3809
|
await writeText(file, renderAnalyze(change, result, localStamp(opts.now)));
|
|
3305
3810
|
result.path = file;
|
|
3306
3811
|
}
|
|
@@ -3343,9 +3848,9 @@ function renderAnalyze(change, result, generatedAt) {
|
|
|
3343
3848
|
}
|
|
3344
3849
|
|
|
3345
3850
|
// src/metrics.ts
|
|
3346
|
-
import
|
|
3851
|
+
import path14 from "path";
|
|
3347
3852
|
async function collectMetrics(root, now = /* @__PURE__ */ new Date()) {
|
|
3348
|
-
const resolved =
|
|
3853
|
+
const resolved = path14.resolve(root);
|
|
3349
3854
|
const { workspace, config } = await loadWorkspace(resolved);
|
|
3350
3855
|
const approvals = await loadApprovals(workspace.sddDir);
|
|
3351
3856
|
const living = /* @__PURE__ */ new Map();
|
|
@@ -3360,11 +3865,12 @@ async function collectMetrics(root, now = /* @__PURE__ */ new Date()) {
|
|
|
3360
3865
|
const deltaContent = await readDelta(change);
|
|
3361
3866
|
const approval = verifyApproval(change, approvals.byArtifact, config, deltaContent);
|
|
3362
3867
|
const findings = [];
|
|
3363
|
-
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 }));
|
|
3364
3869
|
const trace = checkTrace({
|
|
3365
3870
|
specs: workspace.specs,
|
|
3366
3871
|
change,
|
|
3367
|
-
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)
|
|
3368
3874
|
});
|
|
3369
3875
|
findings.push(...trace.findings);
|
|
3370
3876
|
const state = deriveState({ change, cfg: config, approval, blockingFindings: change.delta ? countBySeverity(findings).errors : 0 });
|
|
@@ -3425,7 +3931,7 @@ async function collectMetrics(root, now = /* @__PURE__ */ new Date()) {
|
|
|
3425
3931
|
attention.push({ slug: change.slug, kind: "missing-evidence", detail: `evidencia ${change.scenariosPassed}/${change.scenariosTotal}` });
|
|
3426
3932
|
}
|
|
3427
3933
|
}
|
|
3428
|
-
const archiveDir =
|
|
3934
|
+
const archiveDir = path14.join(workspace.sddDir, "changes", "archive");
|
|
3429
3935
|
const archivedEntries = await listDirs(archiveDir);
|
|
3430
3936
|
const throughputByMonth = {};
|
|
3431
3937
|
for (const entry of archivedEntries) {
|
|
@@ -3463,15 +3969,15 @@ async function collectMetrics(root, now = /* @__PURE__ */ new Date()) {
|
|
|
3463
3969
|
}
|
|
3464
3970
|
async function readDelta(change) {
|
|
3465
3971
|
const { readTextIfExists: readTextIfExists2 } = await import("./fsx-VF2P7ALA.js");
|
|
3466
|
-
return readTextIfExists2(
|
|
3972
|
+
return readTextIfExists2(path14.join(change.dir, "spec.md"));
|
|
3467
3973
|
}
|
|
3468
3974
|
|
|
3469
3975
|
// src/present.ts
|
|
3470
|
-
import
|
|
3976
|
+
import path15 from "path";
|
|
3471
3977
|
import { escapeHtml, inlineMarkdown, renderMarkdown as renderRich } from "@specatlas/render";
|
|
3472
3978
|
import { defaultTokens, renderDocument, renderStyles } from "@specatlas/render";
|
|
3473
3979
|
async function generatePresentation(opts) {
|
|
3474
|
-
const root =
|
|
3980
|
+
const root = path15.resolve(opts.root);
|
|
3475
3981
|
const { workspace, config } = await loadWorkspace(root);
|
|
3476
3982
|
const language = config.project.language;
|
|
3477
3983
|
const change = workspace.changes.find((c) => c.slug === opts.slug);
|
|
@@ -3481,17 +3987,17 @@ async function generatePresentation(opts) {
|
|
|
3481
3987
|
if (!change.delta) {
|
|
3482
3988
|
return { slug: opts.slug, diagnostics: [diag("ATLAS-PRESENT-001", "error", `El cambio "${opts.slug}" no tiene spec.md (delta)`)] };
|
|
3483
3989
|
}
|
|
3484
|
-
const deltaPath =
|
|
3990
|
+
const deltaPath = path15.join(change.dir, "spec.md");
|
|
3485
3991
|
const deltaContent = await readTextIfExists(deltaPath) ?? "";
|
|
3486
3992
|
const hash = artifactHash(deltaContent);
|
|
3487
3993
|
const now = opts.now ?? /* @__PURE__ */ new Date();
|
|
3488
|
-
const presentationDir =
|
|
3489
|
-
const mockupsCopyDir =
|
|
3994
|
+
const presentationDir = path15.join(change.dir, "presentation");
|
|
3995
|
+
const mockupsCopyDir = path15.join(presentationDir, "mockups");
|
|
3490
3996
|
await ensureDir(presentationDir);
|
|
3491
3997
|
const mockupInfo = await copyMockups(root, change, mockupsCopyDir);
|
|
3492
|
-
const proposalRaw = await readTextIfExists(
|
|
3998
|
+
const proposalRaw = await readTextIfExists(path15.join(change.dir, "proposal.md"));
|
|
3493
3999
|
const proposalBody = proposalRaw ? parseFrontmatter(proposalRaw).body : "";
|
|
3494
|
-
const approvals = await loadApprovals(
|
|
4000
|
+
const approvals = await loadApprovals(path15.join(root, ".sdd"));
|
|
3495
4001
|
const approval = verifyApproval(change, approvals.byArtifact, config, deltaContent);
|
|
3496
4002
|
const labels = labelsFor(language);
|
|
3497
4003
|
const html = page({
|
|
@@ -3512,7 +4018,7 @@ async function generatePresentation(opts) {
|
|
|
3512
4018
|
approveHint: `satlas approve ${change.slug} --by "<nombre>" --channel presentation`,
|
|
3513
4019
|
...approval.status === "valid" && approval.approvedBy ? { approval: { by: approval.approvedBy, at: approval.approvedAt ?? "" } } : {}
|
|
3514
4020
|
});
|
|
3515
|
-
const outFile =
|
|
4021
|
+
const outFile = path15.join(presentationDir, "index.html");
|
|
3516
4022
|
await writeText(outFile, html);
|
|
3517
4023
|
return { slug: change.slug, path: outFile, hash, diagnostics: mockupInfo.diagnostics };
|
|
3518
4024
|
}
|
|
@@ -3524,12 +4030,12 @@ async function copyMockups(root, change, destDir) {
|
|
|
3524
4030
|
await ensureDir(destDir);
|
|
3525
4031
|
const screens = [];
|
|
3526
4032
|
for (const screen of manifest.screens) {
|
|
3527
|
-
const src =
|
|
4033
|
+
const src = path15.join(dir, screen.file);
|
|
3528
4034
|
if (!await exists(src)) {
|
|
3529
4035
|
diagnostics.push(diag("ATLAS-PRESENT-002", "warning", `El mockup ${screen.file} no existe y no se incluir\xE1`, { path: src }));
|
|
3530
4036
|
continue;
|
|
3531
4037
|
}
|
|
3532
|
-
await copyFile(src,
|
|
4038
|
+
await copyFile(src, path15.join(destDir, screen.file));
|
|
3533
4039
|
screens.push({
|
|
3534
4040
|
id: screen.id,
|
|
3535
4041
|
title: screen.title ?? screen.id,
|
|
@@ -3539,13 +4045,13 @@ async function copyMockups(root, change, destDir) {
|
|
|
3539
4045
|
});
|
|
3540
4046
|
}
|
|
3541
4047
|
const screenshots = [];
|
|
3542
|
-
const shotsDir =
|
|
4048
|
+
const shotsDir = path15.join(dir, "screens");
|
|
3543
4049
|
if (await exists(shotsDir)) {
|
|
3544
|
-
await ensureDir(
|
|
4050
|
+
await ensureDir(path15.join(destDir, "screens"));
|
|
3545
4051
|
const { listDir: listDir4 } = await import("./fsx-VF2P7ALA.js");
|
|
3546
4052
|
for (const file of await listDir4(shotsDir)) {
|
|
3547
4053
|
if (!/\.(png|jpe?g|webp)$/i.test(file)) continue;
|
|
3548
|
-
await copyFile(
|
|
4054
|
+
await copyFile(path15.join(shotsDir, file), path15.join(destDir, "screens", file));
|
|
3549
4055
|
screenshots.push(`mockups/screens/${file}`);
|
|
3550
4056
|
}
|
|
3551
4057
|
}
|
|
@@ -3720,7 +4226,7 @@ function renderMarkdown(markdown) {
|
|
|
3720
4226
|
}
|
|
3721
4227
|
|
|
3722
4228
|
// src/adopt.ts
|
|
3723
|
-
import
|
|
4229
|
+
import path16 from "path";
|
|
3724
4230
|
var COMMON_ROOTS = ["src", "app", "lib", "modules", "services", "packages", "api", "features", "domain", "internal", "components"];
|
|
3725
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"]);
|
|
3726
4232
|
var SOURCE_EXT = /* @__PURE__ */ new Set([
|
|
@@ -3764,11 +4270,11 @@ var SOURCE_EXT = /* @__PURE__ */ new Set([
|
|
|
3764
4270
|
".groovy"
|
|
3765
4271
|
]);
|
|
3766
4272
|
async function adoptWorkspace(opts) {
|
|
3767
|
-
const root =
|
|
4273
|
+
const root = path16.resolve(opts.root);
|
|
3768
4274
|
const dryRun = opts.dryRun ?? false;
|
|
3769
4275
|
const diagnostics = [];
|
|
3770
|
-
const sddDir =
|
|
3771
|
-
if (!await exists(
|
|
4276
|
+
const sddDir = path16.join(root, ".sdd");
|
|
4277
|
+
if (!await exists(path16.join(sddDir, "config.yaml"))) {
|
|
3772
4278
|
return {
|
|
3773
4279
|
root,
|
|
3774
4280
|
domains: [],
|
|
@@ -3779,8 +4285,8 @@ async function adoptWorkspace(opts) {
|
|
|
3779
4285
|
}
|
|
3780
4286
|
const { config } = await loadConfig(sddDir);
|
|
3781
4287
|
const files = await walkFiles(root, { skipDirs: [...SKIP_DIRS] });
|
|
3782
|
-
const inventoried = files.map((f) => toPosix(
|
|
3783
|
-
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()));
|
|
3784
4290
|
if (relative.length === 0) {
|
|
3785
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>" }));
|
|
3786
4292
|
}
|
|
@@ -3808,13 +4314,13 @@ async function adoptWorkspace(opts) {
|
|
|
3808
4314
|
const createdSpecs = [];
|
|
3809
4315
|
for (const name of domainNames.sort()) {
|
|
3810
4316
|
const domainFiles = (counts.get(name) ?? []).sort();
|
|
3811
|
-
const existingSpec = await exists(
|
|
4317
|
+
const existingSpec = await exists(path16.join(sddDir, "specs", name, "spec.md"));
|
|
3812
4318
|
domains.push({ name, files: domainFiles.length, samples: domainFiles.slice(0, 8), existingSpec });
|
|
3813
4319
|
if (existingSpec) {
|
|
3814
|
-
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") }));
|
|
3815
4321
|
continue;
|
|
3816
4322
|
}
|
|
3817
|
-
const specPath =
|
|
4323
|
+
const specPath = path16.join(sddDir, "specs", name, "spec.md");
|
|
3818
4324
|
if (!dryRun) {
|
|
3819
4325
|
await writeText(specPath, baselineSpec(name, domainFiles));
|
|
3820
4326
|
createdSpecs.push(specPath);
|
|
@@ -3822,7 +4328,7 @@ async function adoptWorkspace(opts) {
|
|
|
3822
4328
|
createdSpecs.push(specPath);
|
|
3823
4329
|
}
|
|
3824
4330
|
}
|
|
3825
|
-
const reportPath =
|
|
4331
|
+
const reportPath = path16.join(sddDir, "adopt-report.md");
|
|
3826
4332
|
if (!dryRun) {
|
|
3827
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() }));
|
|
3828
4334
|
}
|
|
@@ -3842,12 +4348,12 @@ async function loadProjectProfiles(root, dirs) {
|
|
|
3842
4348
|
if (!await exists(dir)) continue;
|
|
3843
4349
|
out.push(...await loadProfilesFromDir(dir));
|
|
3844
4350
|
}
|
|
3845
|
-
const custom =
|
|
4351
|
+
const custom = path16.join(root, ".sdd", "profiles", "custom");
|
|
3846
4352
|
if (await exists(custom)) out.push(...await loadProfilesFromDir(custom));
|
|
3847
4353
|
return out;
|
|
3848
4354
|
}
|
|
3849
4355
|
async function fallbackDomainName(root, projectName) {
|
|
3850
|
-
const pkg = await readTextIfExists(
|
|
4356
|
+
const pkg = await readTextIfExists(path16.join(root, "package.json"));
|
|
3851
4357
|
if (pkg) {
|
|
3852
4358
|
try {
|
|
3853
4359
|
const data = JSON.parse(pkg);
|
|
@@ -3935,13 +4441,13 @@ function adoptReport(input) {
|
|
|
3935
4441
|
}
|
|
3936
4442
|
|
|
3937
4443
|
// src/init.ts
|
|
3938
|
-
import
|
|
4444
|
+
import path17 from "path";
|
|
3939
4445
|
async function initWorkspace(opts) {
|
|
3940
|
-
const root =
|
|
3941
|
-
const sddDir =
|
|
4446
|
+
const root = path17.resolve(opts.root);
|
|
4447
|
+
const sddDir = path17.join(root, ".sdd");
|
|
3942
4448
|
const diagnostics = [];
|
|
3943
4449
|
const created = [];
|
|
3944
|
-
if (await exists(
|
|
4450
|
+
if (await exists(path17.join(sddDir, "config.yaml"))) {
|
|
3945
4451
|
return {
|
|
3946
4452
|
sddDir,
|
|
3947
4453
|
created,
|
|
@@ -3951,29 +4457,29 @@ async function initWorkspace(opts) {
|
|
|
3951
4457
|
created.push(...await ensureSddDirs(sddDir));
|
|
3952
4458
|
const language = opts.language ?? "es";
|
|
3953
4459
|
const cfg = defaultConfig({ name: opts.name, language });
|
|
3954
|
-
cfg.project.name = opts.name ??
|
|
4460
|
+
cfg.project.name = opts.name ?? path17.basename(root);
|
|
3955
4461
|
await writeConfig(sddDir, cfg);
|
|
3956
|
-
created.push(
|
|
4462
|
+
created.push(path17.join(sddDir, "config.yaml"));
|
|
3957
4463
|
const templates = templatesFor(language);
|
|
3958
|
-
await writeText(
|
|
3959
|
-
created.push(
|
|
3960
|
-
await writeText(
|
|
3961
|
-
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"));
|
|
3962
4468
|
const detected = await detectIfPossible(root, opts.profilesDir);
|
|
3963
4469
|
if (detected) {
|
|
3964
4470
|
const yaml = detectionToYaml(detected, localStamp(opts.now));
|
|
3965
|
-
await writeText(
|
|
3966
|
-
created.push(
|
|
4471
|
+
await writeText(path17.join(sddDir, "profiles", "detected.yaml"), yaml);
|
|
4472
|
+
created.push(path17.join(sddDir, "profiles", "detected.yaml"));
|
|
3967
4473
|
if (detected.best) {
|
|
3968
4474
|
diagnostics.push(diag("ATLAS-INIT-002", "info", `Stack detectado: ${detected.best.displayName} (${detected.best.score} puntos)`, { suggestion: "Revisa .sdd/profiles/detected.yaml" }));
|
|
3969
4475
|
} else {
|
|
3970
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/" }));
|
|
3971
4477
|
}
|
|
3972
4478
|
}
|
|
3973
|
-
await writeText(
|
|
3974
|
-
created.push(
|
|
4479
|
+
await writeText(path17.join(sddDir, "INDEX.md"), initialIndex(language, cfg.project.name));
|
|
4480
|
+
created.push(path17.join(sddDir, "INDEX.md"));
|
|
3975
4481
|
if (opts.local) {
|
|
3976
|
-
const gitignore =
|
|
4482
|
+
const gitignore = path17.join(root, ".gitignore");
|
|
3977
4483
|
const current = await readTextIfExists(gitignore) ?? "";
|
|
3978
4484
|
if (!/(^|\n)\.sdd\/?(\n|$)/.test(current)) {
|
|
3979
4485
|
const next = current.endsWith("\n") || current === "" ? `${current}.sdd/
|
|
@@ -4008,13 +4514,13 @@ ${es ? "## Archivados\n\n0 cambio(s) en `changes/archive/`." : "## Archived\n\n0
|
|
|
4008
4514
|
}
|
|
4009
4515
|
|
|
4010
4516
|
// src/new.ts
|
|
4011
|
-
import
|
|
4517
|
+
import path18 from "path";
|
|
4012
4518
|
var SLUG_RE = /^[a-z0-9][a-z0-9-]{1,49}$/;
|
|
4013
4519
|
async function createChange(opts) {
|
|
4014
4520
|
const diagnostics = [];
|
|
4015
4521
|
const slug = opts.slug.trim().toLowerCase();
|
|
4016
4522
|
const language = opts.language ?? opts.cfg?.project.language ?? "es";
|
|
4017
|
-
const dir =
|
|
4523
|
+
const dir = path18.join(path18.resolve(opts.root), ".sdd", "changes", slug);
|
|
4018
4524
|
if (!SLUG_RE.test(slug)) {
|
|
4019
4525
|
return {
|
|
4020
4526
|
slug,
|
|
@@ -4045,20 +4551,20 @@ async function createChange(opts) {
|
|
|
4045
4551
|
await ensureDir(dir);
|
|
4046
4552
|
const templates = templatesFor(language);
|
|
4047
4553
|
const files = [];
|
|
4048
|
-
const metaFile =
|
|
4554
|
+
const metaFile = path18.join(dir, "meta.yaml");
|
|
4049
4555
|
await writeText(metaFile, changeMetaYaml(meta));
|
|
4050
4556
|
files.push(metaFile);
|
|
4051
4557
|
if (lane === "fix") {
|
|
4052
|
-
const fixFile =
|
|
4558
|
+
const fixFile = path18.join(dir, "fix.md");
|
|
4053
4559
|
await writeText(fixFile, renderTemplate(templates.fix, { TITLE: title }));
|
|
4054
4560
|
files.push(fixFile);
|
|
4055
4561
|
return { slug, dir, files, diagnostics };
|
|
4056
4562
|
}
|
|
4057
|
-
const proposalFile =
|
|
4563
|
+
const proposalFile = path18.join(dir, "proposal.md");
|
|
4058
4564
|
await writeText(proposalFile, renderTemplate(templates.proposal, { TITLE: title }));
|
|
4059
4565
|
files.push(proposalFile);
|
|
4060
4566
|
const domainUpper = domain.replace(/[^a-z0-9]/gi, "").toUpperCase() || "GEN";
|
|
4061
|
-
const deltaFile =
|
|
4567
|
+
const deltaFile = path18.join(dir, "spec.md");
|
|
4062
4568
|
await writeText(deltaFile, templates.specDelta({ title, domainUpper, domain }));
|
|
4063
4569
|
files.push(deltaFile);
|
|
4064
4570
|
return { slug, dir, files, diagnostics };
|
|
@@ -4067,7 +4573,7 @@ async function createChange(opts) {
|
|
|
4067
4573
|
// src/archive.ts
|
|
4068
4574
|
import { promises as fs } from "fs";
|
|
4069
4575
|
import { cp, rename, rm } from "fs/promises";
|
|
4070
|
-
import
|
|
4576
|
+
import path19 from "path";
|
|
4071
4577
|
var REQ_HEADER_RE = /^###\s+(?:Requisito|Requirement):\s+(REQ-[A-Z0-9-]+)\s*(?:—|-|–)\s*(.*)$/;
|
|
4072
4578
|
function foldDelta(livingBody, delta, language = "es") {
|
|
4073
4579
|
const diagnostics = [];
|
|
@@ -4176,7 +4682,7 @@ async function restoreFile(file, previous) {
|
|
|
4176
4682
|
}
|
|
4177
4683
|
}
|
|
4178
4684
|
async function archiveChange(opts) {
|
|
4179
|
-
const root =
|
|
4685
|
+
const root = path19.resolve(opts.root);
|
|
4180
4686
|
const diagnostics = [];
|
|
4181
4687
|
const { config } = await loadWorkspace(root);
|
|
4182
4688
|
const language = opts.language ?? config.project.language;
|
|
@@ -4192,7 +4698,7 @@ async function archiveChange(opts) {
|
|
|
4192
4698
|
return { slug: opts.slug, fold: emptyFold, diagnostics, dryRun: opts.dryRun ?? false };
|
|
4193
4699
|
}
|
|
4194
4700
|
const nowFix = opts.now ?? /* @__PURE__ */ new Date();
|
|
4195
|
-
const targetFix =
|
|
4701
|
+
const targetFix = path19.join(root, ".sdd", "changes", "archive", `${localMonth(nowFix)}-${opts.slug}`);
|
|
4196
4702
|
if (await exists(targetFix)) {
|
|
4197
4703
|
diagnostics.push(diag("ATLAS-ARCH-002", "error", `Ya existe un cambio archivado en ${targetFix}`, { path: targetFix }));
|
|
4198
4704
|
return { slug: opts.slug, fold: emptyFold, diagnostics, dryRun: opts.dryRun ?? false };
|
|
@@ -4200,7 +4706,7 @@ async function archiveChange(opts) {
|
|
|
4200
4706
|
let livingFix;
|
|
4201
4707
|
let livingCreated = false;
|
|
4202
4708
|
if (!opts.dryRun) {
|
|
4203
|
-
const fixRaw = await readTextIfExists(
|
|
4709
|
+
const fixRaw = await readTextIfExists(path19.join(change.dir, "fix.md")) ?? "";
|
|
4204
4710
|
try {
|
|
4205
4711
|
const written = await writeLivingFix(root, {
|
|
4206
4712
|
slug: opts.slug,
|
|
@@ -4216,7 +4722,7 @@ async function archiveChange(opts) {
|
|
|
4216
4722
|
} catch (error) {
|
|
4217
4723
|
diagnostics.push(
|
|
4218
4724
|
diag("ATLAS-ARCH-006", "error", `No se pudo conservar el fix vivo: ${error.message}`, {
|
|
4219
|
-
path:
|
|
4725
|
+
path: path19.join(root, ".sdd", "fixes"),
|
|
4220
4726
|
suggestion: "Revisa los permisos de .sdd/fixes y vuelve a intentar; el fix sigue sin archivar"
|
|
4221
4727
|
})
|
|
4222
4728
|
);
|
|
@@ -4224,11 +4730,11 @@ async function archiveChange(opts) {
|
|
|
4224
4730
|
}
|
|
4225
4731
|
}
|
|
4226
4732
|
if (!opts.dryRun) {
|
|
4227
|
-
await ensureDir(
|
|
4733
|
+
await ensureDir(path19.dirname(targetFix));
|
|
4228
4734
|
try {
|
|
4229
4735
|
await moveDirectory(change.dir, targetFix);
|
|
4230
4736
|
} catch (error) {
|
|
4231
|
-
if (livingCreated && livingFix) await removeFile(
|
|
4737
|
+
if (livingCreated && livingFix) await removeFile(path19.join(root, livingFix));
|
|
4232
4738
|
diagnostics.push(
|
|
4233
4739
|
diag("ATLAS-ARCH-004", "error", `No se pudo mover el cambio al hist\xF3rico: ${error.message}`, {
|
|
4234
4740
|
path: change.dir,
|
|
@@ -4254,10 +4760,10 @@ async function archiveChange(opts) {
|
|
|
4254
4760
|
}
|
|
4255
4761
|
const domain = change.meta.domain;
|
|
4256
4762
|
if (!domain) {
|
|
4257
|
-
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") }));
|
|
4258
4764
|
return { slug: opts.slug, fold: { content: "", applied: { added: [], modified: [], removed: [], renamed: [] }, diagnostics }, diagnostics, dryRun: opts.dryRun ?? false };
|
|
4259
4765
|
}
|
|
4260
|
-
const specFile =
|
|
4766
|
+
const specFile = path19.join(root, ".sdd", "specs", domain, "spec.md");
|
|
4261
4767
|
const existing = await readTextIfExists(specFile);
|
|
4262
4768
|
const base = existing ?? `---
|
|
4263
4769
|
domain: ${domain}
|
|
@@ -4287,8 +4793,8 @@ ${Object.entries(nextFm).map(([k, v]) => `${k}: ${String(v)}`).join("\n")}
|
|
|
4287
4793
|
|
|
4288
4794
|
${body}`;
|
|
4289
4795
|
const month = localMonth(now);
|
|
4290
|
-
const archiveDir =
|
|
4291
|
-
const target =
|
|
4796
|
+
const archiveDir = path19.join(root, ".sdd", "changes", "archive");
|
|
4797
|
+
const target = path19.join(archiveDir, `${month}-${opts.slug}`);
|
|
4292
4798
|
if (await exists(target)) {
|
|
4293
4799
|
diagnostics.push(diag("ATLAS-ARCH-002", "error", `Ya existe un cambio archivado en ${target}`, { path: target }));
|
|
4294
4800
|
return { slug: opts.slug, domain, fold, diagnostics, dryRun: opts.dryRun ?? false };
|
|
@@ -4314,7 +4820,7 @@ ${body}`;
|
|
|
4314
4820
|
}
|
|
4315
4821
|
async function regenerateIndex(root, cfg, now = /* @__PURE__ */ new Date()) {
|
|
4316
4822
|
const { workspace, config } = cfg ? { workspace: (await loadWorkspace(root)).workspace, config: cfg } : await loadWorkspace(root);
|
|
4317
|
-
const archiveDir =
|
|
4823
|
+
const archiveDir = path19.join(root, ".sdd", "changes", "archive");
|
|
4318
4824
|
const archived = await exists(archiveDir) ? (await fs.readdir(archiveDir)).filter((e) => !e.startsWith(".")).length : 0;
|
|
4319
4825
|
const fixes = await loadLivingFixes(root);
|
|
4320
4826
|
const markdown = indexMarkdown({
|
|
@@ -4326,7 +4832,7 @@ async function regenerateIndex(root, cfg, now = /* @__PURE__ */ new Date()) {
|
|
|
4326
4832
|
archived
|
|
4327
4833
|
});
|
|
4328
4834
|
void now;
|
|
4329
|
-
await writeText(
|
|
4835
|
+
await writeText(path19.join(root, ".sdd", "INDEX.md"), markdown);
|
|
4330
4836
|
}
|
|
4331
4837
|
async function removeFile(file) {
|
|
4332
4838
|
try {
|
|
@@ -4337,8 +4843,8 @@ async function removeFile(file) {
|
|
|
4337
4843
|
|
|
4338
4844
|
// src/migrations.ts
|
|
4339
4845
|
import { promises as fs2 } from "fs";
|
|
4340
|
-
import
|
|
4341
|
-
import { parse as
|
|
4846
|
+
import path20 from "path";
|
|
4847
|
+
import { parse as parseYaml10, stringify as stringifyYaml7 } from "yaml";
|
|
4342
4848
|
var SCHEMA_VERSION = 1;
|
|
4343
4849
|
var BACKUP_DIRNAME = ".backup";
|
|
4344
4850
|
var BACKUP_POINTER = ".latest";
|
|
@@ -4350,35 +4856,35 @@ var SCHEMA_MIGRATIONS = [
|
|
|
4350
4856
|
to: SCHEMA_VERSION
|
|
4351
4857
|
}
|
|
4352
4858
|
];
|
|
4353
|
-
var FIXED_ARTIFACTS = ["config.yaml", "approvals.yaml",
|
|
4354
|
-
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")];
|
|
4355
4861
|
async function pushChangeArtifacts(out, sddDir, relDir) {
|
|
4356
4862
|
for (const rel of CHANGE_ARTIFACTS) {
|
|
4357
|
-
const abs =
|
|
4358
|
-
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 });
|
|
4359
4865
|
}
|
|
4360
4866
|
}
|
|
4361
4867
|
async function collectVersionedArtifacts(root) {
|
|
4362
|
-
const sddDir =
|
|
4868
|
+
const sddDir = path20.join(root, ".sdd");
|
|
4363
4869
|
const out = [];
|
|
4364
4870
|
for (const rel of FIXED_ARTIFACTS) {
|
|
4365
|
-
const abs =
|
|
4871
|
+
const abs = path20.join(sddDir, rel);
|
|
4366
4872
|
if (await exists(abs)) out.push({ artifact: toPosix(rel), path: abs });
|
|
4367
4873
|
}
|
|
4368
|
-
const changesDir =
|
|
4874
|
+
const changesDir = path20.join(sddDir, "changes");
|
|
4369
4875
|
for (const slug of await listDirs(changesDir)) {
|
|
4370
4876
|
if (slug === "archive") continue;
|
|
4371
|
-
await pushChangeArtifacts(out, sddDir,
|
|
4877
|
+
await pushChangeArtifacts(out, sddDir, path20.join("changes", slug));
|
|
4372
4878
|
}
|
|
4373
|
-
for (const entry of await listDirs(
|
|
4374
|
-
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));
|
|
4375
4881
|
}
|
|
4376
4882
|
return out;
|
|
4377
4883
|
}
|
|
4378
4884
|
function readArtifactVersion(content) {
|
|
4379
4885
|
let data;
|
|
4380
4886
|
try {
|
|
4381
|
-
data =
|
|
4887
|
+
data = parseYaml10(content);
|
|
4382
4888
|
} catch (err) {
|
|
4383
4889
|
return { state: "unreadable", reason: `YAML inv\xE1lido (${err.message})` };
|
|
4384
4890
|
}
|
|
@@ -4468,22 +4974,22 @@ function backupName(now) {
|
|
|
4468
4974
|
async function applyUpgrade(root, now = /* @__PURE__ */ new Date()) {
|
|
4469
4975
|
const plan = await planUpgrade(root);
|
|
4470
4976
|
if (plan.pending.length === 0) return { status: "up-to-date", plan, applied: [] };
|
|
4471
|
-
const sddDir =
|
|
4472
|
-
const backupRoot =
|
|
4977
|
+
const sddDir = path20.join(root, ".sdd");
|
|
4978
|
+
const backupRoot = path20.join(sddDir, BACKUP_DIRNAME);
|
|
4473
4979
|
const name = backupName(now);
|
|
4474
|
-
const backupDir =
|
|
4475
|
-
const pointerFile =
|
|
4980
|
+
const backupDir = path20.join(backupRoot, name);
|
|
4981
|
+
const pointerFile = path20.join(backupRoot, BACKUP_POINTER);
|
|
4476
4982
|
const previous = [];
|
|
4477
4983
|
try {
|
|
4478
4984
|
for (const item of plan.pending) {
|
|
4479
4985
|
const content = await readTextIfExists(item.path);
|
|
4480
4986
|
if (content === void 0) throw new Error("el elemento desapareci\xF3 mientras se respaldaba");
|
|
4481
4987
|
previous.push({ item, contents: content });
|
|
4482
|
-
await writeText(
|
|
4988
|
+
await writeText(path20.join(backupDir, "files", ...item.artifact.split("/")), content);
|
|
4483
4989
|
}
|
|
4484
4990
|
await writeText(
|
|
4485
|
-
|
|
4486
|
-
|
|
4991
|
+
path20.join(backupDir, "backup.yaml"),
|
|
4992
|
+
stringifyYaml7(
|
|
4487
4993
|
{
|
|
4488
4994
|
schema_version: SCHEMA_VERSION,
|
|
4489
4995
|
created_at: localStamp(now),
|
|
@@ -4523,7 +5029,7 @@ async function applyUpgrade(root, now = /* @__PURE__ */ new Date()) {
|
|
|
4523
5029
|
}
|
|
4524
5030
|
}
|
|
4525
5031
|
for (const dir of await listDirs(backupRoot)) {
|
|
4526
|
-
if (dir !== name) await removeDir(
|
|
5032
|
+
if (dir !== name) await removeDir(path20.join(backupRoot, dir));
|
|
4527
5033
|
}
|
|
4528
5034
|
return {
|
|
4529
5035
|
status: "applied",
|
|
@@ -4532,32 +5038,32 @@ async function applyUpgrade(root, now = /* @__PURE__ */ new Date()) {
|
|
|
4532
5038
|
backup: {
|
|
4533
5039
|
name,
|
|
4534
5040
|
dir: backupDir,
|
|
4535
|
-
relativeDir: toPosix(
|
|
5041
|
+
relativeDir: toPosix(path20.relative(root, backupDir)),
|
|
4536
5042
|
createdAt: localStamp(now),
|
|
4537
5043
|
files: plan.pending.map((i) => i.artifact)
|
|
4538
5044
|
}
|
|
4539
5045
|
};
|
|
4540
5046
|
}
|
|
4541
5047
|
async function rollbackUpgrade(root) {
|
|
4542
|
-
const sddDir =
|
|
4543
|
-
const backupRoot =
|
|
4544
|
-
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);
|
|
4545
5051
|
const name = (await readTextIfExists(pointerFile))?.trim();
|
|
4546
5052
|
if (!name) return { status: "no-backup", restored: [] };
|
|
4547
|
-
const backupDir =
|
|
4548
|
-
const manifestRaw = await readTextIfExists(
|
|
5053
|
+
const backupDir = path20.join(backupRoot, name);
|
|
5054
|
+
const manifestRaw = await readTextIfExists(path20.join(backupDir, "backup.yaml"));
|
|
4549
5055
|
if (manifestRaw === void 0) return { status: "no-backup", restored: [] };
|
|
4550
5056
|
let files = [];
|
|
4551
5057
|
try {
|
|
4552
|
-
const parsed =
|
|
5058
|
+
const parsed = parseYaml10(manifestRaw);
|
|
4553
5059
|
files = (parsed?.files ?? []).filter((f) => typeof f?.artifact === "string");
|
|
4554
5060
|
} catch {
|
|
4555
5061
|
return { status: "no-backup", restored: [] };
|
|
4556
5062
|
}
|
|
4557
5063
|
const restored = [];
|
|
4558
5064
|
for (const file of files) {
|
|
4559
|
-
const from =
|
|
4560
|
-
const to =
|
|
5065
|
+
const from = path20.join(backupDir, "files", ...file.artifact.split("/"));
|
|
5066
|
+
const to = path20.join(sddDir, ...file.artifact.split("/"));
|
|
4561
5067
|
const content = await readTextIfExists(from);
|
|
4562
5068
|
if (content === void 0) continue;
|
|
4563
5069
|
try {
|
|
@@ -4600,8 +5106,89 @@ async function upgradeAdvisory(root) {
|
|
|
4600
5106
|
return { plan, diagnostics };
|
|
4601
5107
|
}
|
|
4602
5108
|
|
|
5109
|
+
// src/docs.ts
|
|
5110
|
+
import path21 from "path";
|
|
5111
|
+
var DOCS_MARKER_START = "<!-- specatlas:generado:inicio -->";
|
|
5112
|
+
var DOCS_MARKER_END = "<!-- specatlas:generado:fin -->";
|
|
5113
|
+
function docData(change, language, now) {
|
|
5114
|
+
const es = language !== "en";
|
|
5115
|
+
const requirements = [...change.delta?.added ?? [], ...change.delta?.modified ?? []];
|
|
5116
|
+
const evidence = change.verify?.evidence ?? [];
|
|
5117
|
+
const passed = new Set(evidence.filter((entry) => entry.result === "pass").map((entry) => entry.scenario));
|
|
5118
|
+
const requirementsText = requirements.length === 0 ? es ? "_Sin requisitos en el delta._" : "_No requirements in the delta._" : requirements.map((requirement) => {
|
|
5119
|
+
const lines = [`### ${requirement.id} \u2014 ${requirement.title}`, ""];
|
|
5120
|
+
for (const scenario of requirement.scenarios) lines.push(`- \`${scenario.id}\` \u2014 ${scenario.title}`);
|
|
5121
|
+
return lines.join("\n");
|
|
5122
|
+
}).join("\n\n");
|
|
5123
|
+
const scenarioList = requirements.flatMap((requirement) => requirement.scenarios);
|
|
5124
|
+
const scenariosText = scenarioList.length === 0 ? es ? "_Sin escenarios._" : "_No scenarios._" : scenarioList.map((scenario) => `- \`${scenario.id}\` \u2014 ${scenario.title}`).join("\n");
|
|
5125
|
+
const evidenceText = evidence.length === 0 ? es ? "_Sin evidencia registrada._" : "_No evidence recorded._" : evidence.map((entry) => `- \`${entry.scenario}\` \u2014 ${entry.method} \xB7 ${entry.result}${entry.date ? ` \xB7 ${entry.date}` : ""}`).join("\n");
|
|
5126
|
+
const pending = scenarioList.filter((scenario) => !passed.has(scenario.id));
|
|
5127
|
+
const pendingText = pending.length === 0 ? es ? "_Nada pendiente: todos los escenarios tienen evidencia en pass._" : "_Nothing pending: every scenario has passing evidence._" : pending.map((scenario) => `- \`${scenario.id}\` \u2014 ${scenario.title}`).join("\n");
|
|
5128
|
+
return {
|
|
5129
|
+
TITLE: change.meta?.title ?? change.slug,
|
|
5130
|
+
SLUG: change.slug,
|
|
5131
|
+
DOMAIN: change.meta?.domain ?? "\u2014",
|
|
5132
|
+
LANE: change.meta?.lane ?? "standard",
|
|
5133
|
+
DATE: localDate(now),
|
|
5134
|
+
TASKS: change.tasks ? `${change.tasks.counts.done}/${change.tasks.counts.total}` : es ? "sin tareas" : "no tasks",
|
|
5135
|
+
REQUIREMENTS: requirementsText,
|
|
5136
|
+
SCENARIOS: scenariosText,
|
|
5137
|
+
EVIDENCE: evidenceText,
|
|
5138
|
+
PENDING: pendingText
|
|
5139
|
+
};
|
|
5140
|
+
}
|
|
5141
|
+
function mergeManaged(existing, block, language) {
|
|
5142
|
+
const managed = `${DOCS_MARKER_START}
|
|
5143
|
+
${block.trimEnd()}
|
|
5144
|
+
${DOCS_MARKER_END}`;
|
|
5145
|
+
if (existing === void 0) {
|
|
5146
|
+
const notes = language === "en" ? "## Notes\n\n(Write here whatever you want to keep across regenerations.)" : "## Notas\n\n(Escribe aqu\xED lo que quieras conservar entre regeneraciones.)";
|
|
5147
|
+
return `${managed}
|
|
5148
|
+
|
|
5149
|
+
${notes}
|
|
5150
|
+
`;
|
|
5151
|
+
}
|
|
5152
|
+
const start = existing.indexOf(DOCS_MARKER_START);
|
|
5153
|
+
const end = existing.indexOf(DOCS_MARKER_END);
|
|
5154
|
+
if (start >= 0 && end > start) {
|
|
5155
|
+
const before = existing.slice(0, start);
|
|
5156
|
+
const after = existing.slice(end + DOCS_MARKER_END.length);
|
|
5157
|
+
return `${before}${managed}${after}`;
|
|
5158
|
+
}
|
|
5159
|
+
return `${managed}
|
|
5160
|
+
|
|
5161
|
+
${existing}`;
|
|
5162
|
+
}
|
|
5163
|
+
async function generateDocs(opts) {
|
|
5164
|
+
const root = path21.resolve(opts.root);
|
|
5165
|
+
const { config } = await loadWorkspace(root);
|
|
5166
|
+
const change = await loadChange(root, opts.slug);
|
|
5167
|
+
if (!change.meta) {
|
|
5168
|
+
return {
|
|
5169
|
+
slug: opts.slug,
|
|
5170
|
+
files: [],
|
|
5171
|
+
diagnostics: [diag("ATLAS-DOCS-002", "error", `No existe el cambio "${opts.slug}"`, { suggestion: "Comprueba el nombre del cambio" })]
|
|
5172
|
+
};
|
|
5173
|
+
}
|
|
5174
|
+
const language = config.project.language;
|
|
5175
|
+
const tipo = opts.tipo ?? "all";
|
|
5176
|
+
const tipos = tipo === "all" ? ["tecnica", "manual"] : [tipo];
|
|
5177
|
+
const templates = templatesFor(language);
|
|
5178
|
+
const data = docData(change, language, opts.now ?? /* @__PURE__ */ new Date());
|
|
5179
|
+
const files = [];
|
|
5180
|
+
for (const current of tipos) {
|
|
5181
|
+
const file = path21.join(change.dir, "docs", `${current}.md`);
|
|
5182
|
+
const block = renderTemplate(current === "tecnica" ? templates.docTecnica : templates.docManual, data);
|
|
5183
|
+
const existing = await readTextIfExists(file);
|
|
5184
|
+
await writeText(file, mergeManaged(existing, block, language));
|
|
5185
|
+
files.push({ tipo: current, path: file, created: existing === void 0 });
|
|
5186
|
+
}
|
|
5187
|
+
return { slug: opts.slug, files, diagnostics: [] };
|
|
5188
|
+
}
|
|
5189
|
+
|
|
4603
5190
|
// src/sarif.ts
|
|
4604
|
-
import
|
|
5191
|
+
import path22 from "path";
|
|
4605
5192
|
var SARIF_SCHEMA = "https://json.schemastore.org/sarif-2.1.0.json";
|
|
4606
5193
|
var SARIF_VERSION = "2.1.0";
|
|
4607
5194
|
var TOOL_NAME = "SpecAtlas";
|
|
@@ -4639,7 +5226,7 @@ function resultOf(diagnostic, root) {
|
|
|
4639
5226
|
message: { text: diagnostic.message }
|
|
4640
5227
|
};
|
|
4641
5228
|
if (diagnostic.path) {
|
|
4642
|
-
const rel =
|
|
5229
|
+
const rel = path22.relative(root, diagnostic.path);
|
|
4643
5230
|
if (rel !== "") {
|
|
4644
5231
|
const physicalLocation = {
|
|
4645
5232
|
artifactLocation: { uri: toPosix(rel) },
|
|
@@ -4689,7 +5276,7 @@ function toSarifText(opts) {
|
|
|
4689
5276
|
}
|
|
4690
5277
|
|
|
4691
5278
|
// src/doctor.ts
|
|
4692
|
-
import
|
|
5279
|
+
import path23 from "path";
|
|
4693
5280
|
async function runDoctor(root) {
|
|
4694
5281
|
const findings = [];
|
|
4695
5282
|
const { workspace, config } = await loadWorkspace(root);
|
|
@@ -4697,7 +5284,7 @@ async function runDoctor(root) {
|
|
|
4697
5284
|
const approvals = await loadApprovals(workspace.sddDir);
|
|
4698
5285
|
findings.push(...approvals.diagnostics);
|
|
4699
5286
|
for (const change of workspace.changes) {
|
|
4700
|
-
const deltaPath =
|
|
5287
|
+
const deltaPath = path23.join(change.dir, "spec.md");
|
|
4701
5288
|
const deltaContent = await readTextIfExists(deltaPath);
|
|
4702
5289
|
const approval = verifyApproval(change, approvals.byArtifact, config, deltaContent ?? void 0);
|
|
4703
5290
|
if ((change.planPath || change.tasks) && (approval.status === "missing" || approval.status === "stale")) {
|
|
@@ -4719,7 +5306,7 @@ async function runDoctor(root) {
|
|
|
4719
5306
|
}
|
|
4720
5307
|
for (const override of change.meta?.overrides ?? []) {
|
|
4721
5308
|
if (!override.reason.trim() || !override.by.trim()) {
|
|
4722
|
-
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") }));
|
|
4723
5310
|
}
|
|
4724
5311
|
}
|
|
4725
5312
|
}
|
|
@@ -4742,7 +5329,7 @@ async function specHashOf(filePath) {
|
|
|
4742
5329
|
}
|
|
4743
5330
|
|
|
4744
5331
|
// src/gate.ts
|
|
4745
|
-
import
|
|
5332
|
+
import path24 from "path";
|
|
4746
5333
|
var UI_DOMAINS2 = /* @__PURE__ */ new Set(["frontend", "mobile", "fullstack"]);
|
|
4747
5334
|
function count(name, diagnostics) {
|
|
4748
5335
|
return {
|
|
@@ -4759,7 +5346,7 @@ function livingRequirementsMap(specs) {
|
|
|
4759
5346
|
return map;
|
|
4760
5347
|
}
|
|
4761
5348
|
async function runCiGate(opts) {
|
|
4762
|
-
const root =
|
|
5349
|
+
const root = path24.resolve(opts.root);
|
|
4763
5350
|
const { workspace, config } = await loadWorkspace(root);
|
|
4764
5351
|
const diagnostics = [];
|
|
4765
5352
|
const checks = [];
|
|
@@ -4770,11 +5357,12 @@ async function runCiGate(opts) {
|
|
|
4770
5357
|
let changesErrors = 0;
|
|
4771
5358
|
let changesWarnings = 0;
|
|
4772
5359
|
for (const change of workspace.changes) {
|
|
4773
|
-
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 }) : [];
|
|
4774
5361
|
const trace = checkTrace({
|
|
4775
5362
|
specs: workspace.specs,
|
|
4776
5363
|
change,
|
|
4777
|
-
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)
|
|
4778
5366
|
});
|
|
4779
5367
|
const changeDiags = [...lintFindings, ...trace.findings];
|
|
4780
5368
|
if (change.planPath) {
|
|
@@ -4819,7 +5407,11 @@ export {
|
|
|
4819
5407
|
BLOCK_HEAD_RE,
|
|
4820
5408
|
BUILTIN_PACKS,
|
|
4821
5409
|
CONFIG_FILE,
|
|
5410
|
+
CONTRACTS_DIR,
|
|
4822
5411
|
CORE_VERSION,
|
|
5412
|
+
DOCS_MARKER_END,
|
|
5413
|
+
DOCS_MARKER_START,
|
|
5414
|
+
LINKS_FILE,
|
|
4823
5415
|
LIVING_FIXES_DIR,
|
|
4824
5416
|
REQ_HEAD_RE,
|
|
4825
5417
|
REQ_ID_RE,
|
|
@@ -4838,6 +5430,7 @@ export {
|
|
|
4838
5430
|
TASK_LINE_RE,
|
|
4839
5431
|
TOOL_NAME,
|
|
4840
5432
|
TOOL_URL,
|
|
5433
|
+
addLink,
|
|
4841
5434
|
adoptWorkspace,
|
|
4842
5435
|
appendRunEvent,
|
|
4843
5436
|
applyUpgrade,
|
|
@@ -4854,12 +5447,15 @@ export {
|
|
|
4854
5447
|
changeMetaYaml,
|
|
4855
5448
|
checkMockups,
|
|
4856
5449
|
checkTrace,
|
|
5450
|
+
clarifyAdvisory,
|
|
4857
5451
|
collectMetrics,
|
|
4858
5452
|
collectVersionedArtifacts,
|
|
4859
5453
|
commentIssue,
|
|
4860
5454
|
compareTaskIds,
|
|
4861
5455
|
computeInputsHash,
|
|
4862
5456
|
configToYaml,
|
|
5457
|
+
contractCoverage,
|
|
5458
|
+
contractsAdvisory,
|
|
4863
5459
|
copyFile,
|
|
4864
5460
|
countBySeverity,
|
|
4865
5461
|
createChange,
|
|
@@ -4872,9 +5468,12 @@ export {
|
|
|
4872
5468
|
detectRepo,
|
|
4873
5469
|
detectionToYaml,
|
|
4874
5470
|
diag,
|
|
5471
|
+
docsAdvisory,
|
|
5472
|
+
docsReady,
|
|
4875
5473
|
editIssue,
|
|
4876
5474
|
emitFrontmatter,
|
|
4877
5475
|
ensureDir,
|
|
5476
|
+
ensureLinksFile,
|
|
4878
5477
|
ensureSddDirs,
|
|
4879
5478
|
esc,
|
|
4880
5479
|
evaluatePacks,
|
|
@@ -4884,6 +5483,8 @@ export {
|
|
|
4884
5483
|
findWorkspaceRoot,
|
|
4885
5484
|
firstToken,
|
|
4886
5485
|
foldDelta,
|
|
5486
|
+
formatOf,
|
|
5487
|
+
generateDocs,
|
|
4887
5488
|
generatePresentation,
|
|
4888
5489
|
generateRunId,
|
|
4889
5490
|
getNumber,
|
|
@@ -4902,6 +5503,8 @@ export {
|
|
|
4902
5503
|
issueBody,
|
|
4903
5504
|
issueLabels,
|
|
4904
5505
|
laneOrDefault,
|
|
5506
|
+
linkedRequirementIds,
|
|
5507
|
+
linkedTraceInput,
|
|
4905
5508
|
lintDelta,
|
|
4906
5509
|
lintMockupHtml,
|
|
4907
5510
|
lintMockupManifest,
|
|
@@ -4919,7 +5522,9 @@ export {
|
|
|
4919
5522
|
loadApprovals,
|
|
4920
5523
|
loadChange,
|
|
4921
5524
|
loadConfig,
|
|
5525
|
+
loadContracts,
|
|
4922
5526
|
loadDetectedBest,
|
|
5527
|
+
loadLinks,
|
|
4923
5528
|
loadLivingFixes,
|
|
4924
5529
|
loadProfileFile,
|
|
4925
5530
|
loadProfilesFromDir,
|
|
@@ -4939,7 +5544,9 @@ export {
|
|
|
4939
5544
|
packFindings,
|
|
4940
5545
|
parseApprovals,
|
|
4941
5546
|
parseChangeMeta,
|
|
5547
|
+
parseClarify,
|
|
4942
5548
|
parseConfig,
|
|
5549
|
+
parseContract,
|
|
4943
5550
|
parseDelta,
|
|
4944
5551
|
parseFixCovers,
|
|
4945
5552
|
parseFrontmatter,
|
|
@@ -4964,12 +5571,14 @@ export {
|
|
|
4964
5571
|
readTextIfExists,
|
|
4965
5572
|
recordEvidence,
|
|
4966
5573
|
regenerateIndex,
|
|
5574
|
+
removeLink,
|
|
4967
5575
|
renderDocument,
|
|
4968
5576
|
renderMarkdown,
|
|
4969
5577
|
renderRequirement,
|
|
4970
5578
|
renderStyles,
|
|
4971
5579
|
renderTemplate,
|
|
4972
5580
|
requiresMockups,
|
|
5581
|
+
resolveLinkPath,
|
|
4973
5582
|
resolvePacks,
|
|
4974
5583
|
rollbackUpgrade,
|
|
4975
5584
|
ruleDescription,
|