docviz-builder 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (233) hide show
  1. package/AGENTS.md +371 -0
  2. package/LICENSE +21 -0
  3. package/README.md +837 -0
  4. package/bin/docviz-mcp.mjs +10 -0
  5. package/bin/docviz.mjs +11 -0
  6. package/dist/build/builder.d.ts +62 -0
  7. package/dist/build/builder.d.ts.map +1 -0
  8. package/dist/build/builder.js +337 -0
  9. package/dist/build/builder.js.map +1 -0
  10. package/dist/build/diff.d.ts +64 -0
  11. package/dist/build/diff.d.ts.map +1 -0
  12. package/dist/build/diff.js +156 -0
  13. package/dist/build/diff.js.map +1 -0
  14. package/dist/build/doctor.d.ts +39 -0
  15. package/dist/build/doctor.d.ts.map +1 -0
  16. package/dist/build/doctor.js +190 -0
  17. package/dist/build/doctor.js.map +1 -0
  18. package/dist/build/init.d.ts +28 -0
  19. package/dist/build/init.d.ts.map +1 -0
  20. package/dist/build/init.js +274 -0
  21. package/dist/build/init.js.map +1 -0
  22. package/dist/build/preview.d.ts +13 -0
  23. package/dist/build/preview.d.ts.map +1 -0
  24. package/dist/build/preview.js +277 -0
  25. package/dist/build/preview.js.map +1 -0
  26. package/dist/build/skill.d.ts +41 -0
  27. package/dist/build/skill.d.ts.map +1 -0
  28. package/dist/build/skill.js +63 -0
  29. package/dist/build/skill.js.map +1 -0
  30. package/dist/build/verify.d.ts +26 -0
  31. package/dist/build/verify.d.ts.map +1 -0
  32. package/dist/build/verify.js +99 -0
  33. package/dist/build/verify.js.map +1 -0
  34. package/dist/cli.d.ts +20 -0
  35. package/dist/cli.d.ts.map +1 -0
  36. package/dist/cli.js +427 -0
  37. package/dist/cli.js.map +1 -0
  38. package/dist/config/load.d.ts +32 -0
  39. package/dist/config/load.d.ts.map +1 -0
  40. package/dist/config/load.js +279 -0
  41. package/dist/config/load.js.map +1 -0
  42. package/dist/config/types.d.ts +89 -0
  43. package/dist/config/types.d.ts.map +1 -0
  44. package/dist/config/types.js +5 -0
  45. package/dist/config/types.js.map +1 -0
  46. package/dist/core/cache.d.ts +32 -0
  47. package/dist/core/cache.d.ts.map +1 -0
  48. package/dist/core/cache.js +69 -0
  49. package/dist/core/cache.js.map +1 -0
  50. package/dist/core/errors.d.ts +115 -0
  51. package/dist/core/errors.d.ts.map +1 -0
  52. package/dist/core/errors.js +164 -0
  53. package/dist/core/errors.js.map +1 -0
  54. package/dist/core/hash.d.ts +57 -0
  55. package/dist/core/hash.d.ts.map +1 -0
  56. package/dist/core/hash.js +0 -0
  57. package/dist/core/hash.js.map +1 -0
  58. package/dist/core/package-version.d.ts +9 -0
  59. package/dist/core/package-version.d.ts.map +1 -0
  60. package/dist/core/package-version.js +44 -0
  61. package/dist/core/package-version.js.map +1 -0
  62. package/dist/core/paths.d.ts +46 -0
  63. package/dist/core/paths.d.ts.map +1 -0
  64. package/dist/core/paths.js +105 -0
  65. package/dist/core/paths.js.map +1 -0
  66. package/dist/core/registry.d.ts +24 -0
  67. package/dist/core/registry.d.ts.map +1 -0
  68. package/dist/core/registry.js +72 -0
  69. package/dist/core/registry.js.map +1 -0
  70. package/dist/core/types.d.ts +78 -0
  71. package/dist/core/types.d.ts.map +1 -0
  72. package/dist/core/types.js +11 -0
  73. package/dist/core/types.js.map +1 -0
  74. package/dist/dsl/architecture.d.ts +15 -0
  75. package/dist/dsl/architecture.d.ts.map +1 -0
  76. package/dist/dsl/architecture.js +242 -0
  77. package/dist/dsl/architecture.js.map +1 -0
  78. package/dist/dsl/catalog.d.ts +62 -0
  79. package/dist/dsl/catalog.d.ts.map +1 -0
  80. package/dist/dsl/catalog.en.d.ts +18 -0
  81. package/dist/dsl/catalog.en.d.ts.map +1 -0
  82. package/dist/dsl/catalog.en.js +299 -0
  83. package/dist/dsl/catalog.en.js.map +1 -0
  84. package/dist/dsl/catalog.js +1082 -0
  85. package/dist/dsl/catalog.js.map +1 -0
  86. package/dist/dsl/chart.d.ts +14 -0
  87. package/dist/dsl/chart.d.ts.map +1 -0
  88. package/dist/dsl/chart.js +435 -0
  89. package/dist/dsl/chart.js.map +1 -0
  90. package/dist/dsl/compile.d.ts +31 -0
  91. package/dist/dsl/compile.d.ts.map +1 -0
  92. package/dist/dsl/compile.js +120 -0
  93. package/dist/dsl/compile.js.map +1 -0
  94. package/dist/dsl/diagram-bpmn.d.ts +13 -0
  95. package/dist/dsl/diagram-bpmn.d.ts.map +1 -0
  96. package/dist/dsl/diagram-bpmn.js +215 -0
  97. package/dist/dsl/diagram-bpmn.js.map +1 -0
  98. package/dist/dsl/diagram-product.d.ts +15 -0
  99. package/dist/dsl/diagram-product.d.ts.map +1 -0
  100. package/dist/dsl/diagram-product.js +291 -0
  101. package/dist/dsl/diagram-product.js.map +1 -0
  102. package/dist/dsl/diagram-technical.d.ts +28 -0
  103. package/dist/dsl/diagram-technical.d.ts.map +1 -0
  104. package/dist/dsl/diagram-technical.js +365 -0
  105. package/dist/dsl/diagram-technical.js.map +1 -0
  106. package/dist/dsl/diagram.d.ts +22 -0
  107. package/dist/dsl/diagram.d.ts.map +1 -0
  108. package/dist/dsl/diagram.js +542 -0
  109. package/dist/dsl/diagram.js.map +1 -0
  110. package/dist/dsl/fallbacks-d2.d.ts +27 -0
  111. package/dist/dsl/fallbacks-d2.d.ts.map +1 -0
  112. package/dist/dsl/fallbacks-d2.js +265 -0
  113. package/dist/dsl/fallbacks-d2.js.map +1 -0
  114. package/dist/dsl/fallbacks.d.ts +25 -0
  115. package/dist/dsl/fallbacks.d.ts.map +1 -0
  116. package/dist/dsl/fallbacks.js +264 -0
  117. package/dist/dsl/fallbacks.js.map +1 -0
  118. package/dist/dsl/fields.d.ts +93 -0
  119. package/dist/dsl/fields.d.ts.map +1 -0
  120. package/dist/dsl/fields.js +233 -0
  121. package/dist/dsl/fields.js.map +1 -0
  122. package/dist/dsl/index.d.ts +50 -0
  123. package/dist/dsl/index.d.ts.map +1 -0
  124. package/dist/dsl/index.js +114 -0
  125. package/dist/dsl/index.js.map +1 -0
  126. package/dist/dsl/util.d.ts +105 -0
  127. package/dist/dsl/util.d.ts.map +1 -0
  128. package/dist/dsl/util.js +261 -0
  129. package/dist/dsl/util.js.map +1 -0
  130. package/dist/index.d.ts +27 -0
  131. package/dist/index.d.ts.map +1 -0
  132. package/dist/index.js +21 -0
  133. package/dist/index.js.map +1 -0
  134. package/dist/markdown/scan.d.ts +73 -0
  135. package/dist/markdown/scan.d.ts.map +1 -0
  136. package/dist/markdown/scan.js +150 -0
  137. package/dist/markdown/scan.js.map +1 -0
  138. package/dist/markdown/transform.d.ts +28 -0
  139. package/dist/markdown/transform.d.ts.map +1 -0
  140. package/dist/markdown/transform.js +38 -0
  141. package/dist/markdown/transform.js.map +1 -0
  142. package/dist/mcp/server.d.ts +18 -0
  143. package/dist/mcp/server.d.ts.map +1 -0
  144. package/dist/mcp/server.js +118 -0
  145. package/dist/mcp/server.js.map +1 -0
  146. package/dist/mcp/tools.d.ts +118 -0
  147. package/dist/mcp/tools.d.ts.map +1 -0
  148. package/dist/mcp/tools.js +573 -0
  149. package/dist/mcp/tools.js.map +1 -0
  150. package/dist/renderers/base.d.ts +18 -0
  151. package/dist/renderers/base.d.ts.map +1 -0
  152. package/dist/renderers/base.js +59 -0
  153. package/dist/renderers/base.js.map +1 -0
  154. package/dist/renderers/bpmn.d.ts +68 -0
  155. package/dist/renderers/bpmn.d.ts.map +1 -0
  156. package/dist/renderers/bpmn.js +158 -0
  157. package/dist/renderers/bpmn.js.map +1 -0
  158. package/dist/renderers/browser.d.ts +30 -0
  159. package/dist/renderers/browser.d.ts.map +1 -0
  160. package/dist/renderers/browser.js +143 -0
  161. package/dist/renderers/browser.js.map +1 -0
  162. package/dist/renderers/color-scheme.d.ts +40 -0
  163. package/dist/renderers/color-scheme.d.ts.map +1 -0
  164. package/dist/renderers/color-scheme.js +122 -0
  165. package/dist/renderers/color-scheme.js.map +1 -0
  166. package/dist/renderers/d2.d.ts +37 -0
  167. package/dist/renderers/d2.d.ts.map +1 -0
  168. package/dist/renderers/d2.js +108 -0
  169. package/dist/renderers/d2.js.map +1 -0
  170. package/dist/renderers/graphviz.d.ts +33 -0
  171. package/dist/renderers/graphviz.d.ts.map +1 -0
  172. package/dist/renderers/graphviz.js +88 -0
  173. package/dist/renderers/graphviz.js.map +1 -0
  174. package/dist/renderers/in-page.d.ts +33 -0
  175. package/dist/renderers/in-page.d.ts.map +1 -0
  176. package/dist/renderers/in-page.js +76 -0
  177. package/dist/renderers/in-page.js.map +1 -0
  178. package/dist/renderers/index.d.ts +20 -0
  179. package/dist/renderers/index.d.ts.map +1 -0
  180. package/dist/renderers/index.js +94 -0
  181. package/dist/renderers/index.js.map +1 -0
  182. package/dist/renderers/kroki.d.ts +38 -0
  183. package/dist/renderers/kroki.d.ts.map +1 -0
  184. package/dist/renderers/kroki.js +151 -0
  185. package/dist/renderers/kroki.js.map +1 -0
  186. package/dist/renderers/likec4-svg.d.ts +82 -0
  187. package/dist/renderers/likec4-svg.d.ts.map +1 -0
  188. package/dist/renderers/likec4-svg.js +435 -0
  189. package/dist/renderers/likec4-svg.js.map +1 -0
  190. package/dist/renderers/likec4.d.ts +22 -0
  191. package/dist/renderers/likec4.d.ts.map +1 -0
  192. package/dist/renderers/likec4.js +77 -0
  193. package/dist/renderers/likec4.js.map +1 -0
  194. package/dist/renderers/mermaid.d.ts +64 -0
  195. package/dist/renderers/mermaid.d.ts.map +1 -0
  196. package/dist/renderers/mermaid.js +196 -0
  197. package/dist/renderers/mermaid.js.map +1 -0
  198. package/dist/renderers/plantuml.d.ts +56 -0
  199. package/dist/renderers/plantuml.d.ts.map +1 -0
  200. package/dist/renderers/plantuml.js +195 -0
  201. package/dist/renderers/plantuml.js.map +1 -0
  202. package/dist/renderers/svg-utils.d.ts +46 -0
  203. package/dist/renderers/svg-utils.d.ts.map +1 -0
  204. package/dist/renderers/svg-utils.js +439 -0
  205. package/dist/renderers/svg-utils.js.map +1 -0
  206. package/dist/renderers/svgbob.d.ts +26 -0
  207. package/dist/renderers/svgbob.d.ts.map +1 -0
  208. package/dist/renderers/svgbob.js +70 -0
  209. package/dist/renderers/svgbob.js.map +1 -0
  210. package/dist/renderers/vega-lite.d.ts +18 -0
  211. package/dist/renderers/vega-lite.d.ts.map +1 -0
  212. package/dist/renderers/vega-lite.js +94 -0
  213. package/dist/renderers/vega-lite.js.map +1 -0
  214. package/dist/themes/index.d.ts +17 -0
  215. package/dist/themes/index.d.ts.map +1 -0
  216. package/dist/themes/index.js +419 -0
  217. package/dist/themes/index.js.map +1 -0
  218. package/dist/themes/types.d.ts +99 -0
  219. package/dist/themes/types.d.ts.map +1 -0
  220. package/dist/themes/types.js +9 -0
  221. package/dist/themes/types.js.map +1 -0
  222. package/eval/casos.json +513 -0
  223. package/package.json +114 -0
  224. package/scripts/capture-preview.mjs +101 -0
  225. package/scripts/check-github.mjs +128 -0
  226. package/scripts/eval.d.mts +8 -0
  227. package/scripts/eval.mjs +284 -0
  228. package/scripts/fetch-plantuml.mjs +122 -0
  229. package/scripts/generate-catalog-doc.mjs +106 -0
  230. package/scripts/rasterize.mjs +68 -0
  231. package/scripts/sync-docs.mjs +158 -0
  232. package/skills/docviz/SKILL.md +111 -0
  233. package/vendor/.gitkeep +0 -0
@@ -0,0 +1,101 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Captura el documento compilado tal y como lo ve un visor real.
4
+ *
5
+ * Levanta el servidor de previsualizacion, abre el documento en Chromium y
6
+ * guarda una captura de pagina completa. Ademas informa de cada imagen que el
7
+ * navegador no haya podido cargar, que es la comprobacion que exige la
8
+ * validacion manual ("no hay imagenes rotas").
9
+ *
10
+ * node scripts/capture-preview.mjs <dirCompilado> <documento.md> <salida.png>
11
+ */
12
+
13
+ import path from 'node:path';
14
+ import process from 'node:process';
15
+ import { writeFile } from 'node:fs/promises';
16
+ import puppeteer from 'puppeteer-core';
17
+ import { startPreview } from '../dist/build/preview.js';
18
+ import { browserNotFoundHelp, findBrowser } from '../dist/renderers/browser.js';
19
+
20
+ const [, , dir, document_, output] = process.argv;
21
+ if (dir === undefined || document_ === undefined || output === undefined) {
22
+ process.stderr.write('uso: node scripts/capture-preview.mjs <dirCompilado> <documento.md> <salida.png>\n');
23
+ process.exit(2);
24
+ }
25
+
26
+ const executablePath = findBrowser();
27
+ if (executablePath === undefined) {
28
+ process.stderr.write(`${browserNotFoundHelp()}\n`);
29
+ process.exit(1);
30
+ }
31
+
32
+ const server = await startPreview(path.resolve(dir), 0);
33
+ const browser = await puppeteer.launch({ executablePath, headless: true, args: ['--no-sandbox'] });
34
+ const page = await browser.newPage();
35
+ await page.setViewport({ width: 1200, height: 900, deviceScaleFactor: 1 });
36
+
37
+ const failed = [];
38
+ page.on('requestfailed', (req) => failed.push(req.url()));
39
+ page.on('response', (res) => {
40
+ // El favicon no forma parte del documento: su ausencia no es una imagen rota.
41
+ if (res.status() >= 400 && !res.url().endsWith('/favicon.ico')) {
42
+ failed.push(`${res.status()} ${res.url()}`);
43
+ }
44
+ });
45
+
46
+ await page.goto(new URL(document_, server.url).href, { waitUntil: 'networkidle0' });
47
+
48
+ // El visor marca las imagenes como `loading="lazy"`: hay que recorrer la pagina
49
+ // y esperar a que todas terminen, o las de mas abajo se medirian como 0x0.
50
+ await page.evaluate(async () => {
51
+ for (const img of document.querySelectorAll('img')) img.loading = 'eager';
52
+ await new Promise((resolve) => {
53
+ let y = 0;
54
+ const step = () => {
55
+ y += 600;
56
+ window.scrollTo(0, y);
57
+ if (y < document.body.scrollHeight) setTimeout(step, 30);
58
+ else {
59
+ window.scrollTo(0, 0);
60
+ resolve(undefined);
61
+ }
62
+ };
63
+ step();
64
+ });
65
+ await Promise.all(
66
+ [...document.querySelectorAll('img')].map(
67
+ (img) =>
68
+ img.complete ||
69
+ new Promise((resolve) => {
70
+ img.addEventListener('load', resolve, { once: true });
71
+ img.addEventListener('error', resolve, { once: true });
72
+ }),
73
+ ),
74
+ );
75
+ });
76
+
77
+ // Comprueba en el DOM que cada <img> tiene dimensiones reales.
78
+ const images = await page.evaluate(() =>
79
+ [...document.querySelectorAll('img')].map((img) => ({
80
+ src: img.getAttribute('src') ?? '',
81
+ ok: img.complete && img.naturalWidth > 0 && img.naturalHeight > 0,
82
+ width: img.naturalWidth,
83
+ height: img.naturalHeight,
84
+ })),
85
+ );
86
+
87
+ const png = await page.screenshot({ type: 'png', fullPage: true });
88
+ await writeFile(output, png);
89
+
90
+ await browser.close();
91
+ await server.close();
92
+
93
+ const broken = images.filter((i) => !i.ok);
94
+ process.stdout.write(`imagenes en el documento: ${images.length}\n`);
95
+ for (const image of images) {
96
+ process.stdout.write(` ${image.ok ? 'OK ' : 'ROTA'} ${image.width}x${image.height} ${image.src}\n`);
97
+ }
98
+ if (failed.length > 0) process.stdout.write(`peticiones fallidas: ${failed.join(', ')}\n`);
99
+ process.stdout.write(`captura: ${output}\n`);
100
+
101
+ process.exit(broken.length === 0 && failed.length === 0 ? 0 : 1);
@@ -0,0 +1,128 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Comprueba como se veria la documentacion compilada en GitHub.
4
+ *
5
+ * GitHub expone la misma API que usa para renderizar Markdown en su web
6
+ * (`POST /markdown`), asi que se le puede pedir el HTML resultante y verificar
7
+ * sobre el que las imagenes sobreviven, conservan su texto alternativo y siguen
8
+ * apuntando a rutas relativas. No hace falta publicar el repositorio ni abrir
9
+ * un navegador.
10
+ *
11
+ * node scripts/check-github.mjs [dirCompilado] [--repo owner/name]
12
+ *
13
+ * Requiere el CLI `gh` autenticado.
14
+ *
15
+ * Lo que esta comprobacion **no** puede demostrar: que el servidor de imagenes
16
+ * de GitHub conserve el `<style>` interno del SVG, del que depende la variante
17
+ * oscura. Eso solo se ve abriendo el archivo con una sesion iniciada; aqui se
18
+ * verifica lo verificable y se dice cual es el limite.
19
+ */
20
+
21
+ import { execFile } from 'node:child_process';
22
+ import { readFile, writeFile } from 'node:fs/promises';
23
+ import { tmpdir } from 'node:os';
24
+ import path from 'node:path';
25
+ import process from 'node:process';
26
+ import { promisify } from 'node:util';
27
+ import { visit } from 'unist-util-visit';
28
+ import { collectMarkdown } from '../dist/build/builder.js';
29
+ import { parseMarkdown } from '../dist/markdown/scan.js';
30
+
31
+ const run = promisify(execFile);
32
+
33
+ const args = process.argv.slice(2);
34
+ const repoIndex = args.indexOf('--repo');
35
+ const repo = repoIndex >= 0 ? args[repoIndex + 1] : 'TilsonF/docviz-builder';
36
+ const dir = path.resolve(args.find((a) => !a.startsWith('--') && a !== repo) ?? 'docs');
37
+
38
+ /** Pide a GitHub que renderice el Markdown igual que en su web. */
39
+ async function renderizarEnGitHub(markdown) {
40
+ const payload = path.join(tmpdir(), `docviz-gh-${process.pid}.json`);
41
+ await writeFile(payload, JSON.stringify({ text: markdown, mode: 'gfm', context: repo }), 'utf8');
42
+ const { stdout } = await run('gh', ['api', '/markdown', '--method', 'POST', '--input', payload], {
43
+ maxBuffer: 32 * 1024 * 1024,
44
+ });
45
+ return stdout;
46
+ }
47
+
48
+ const documentos = await collectMarkdown(dir);
49
+ if (documentos.length === 0) {
50
+ process.stderr.write(`no hay documentos Markdown en ${dir}\n`);
51
+ process.exit(1);
52
+ }
53
+
54
+ let problemas = 0;
55
+ let imagenesTotales = 0;
56
+
57
+ for (const archivo of documentos) {
58
+ const relativo = path.relative(dir, archivo);
59
+ const markdown = await readFile(archivo, 'utf8');
60
+
61
+ // Se recorre el AST, no el texto: un ejemplo de sintaxis dentro de un bloque
62
+ // de codigo se parece a una imagen, pero no lo es, y contarlo daria un fallo
63
+ // donde no lo hay.
64
+ const arbol = parseMarkdown(markdown);
65
+ const esperadas = [];
66
+ let bloquesDeCodigo = 0;
67
+ visit(arbol, 'image', (nodo) => {
68
+ esperadas.push({ alt: nodo.alt ?? '', src: nodo.url });
69
+ });
70
+ visit(arbol, 'code', () => {
71
+ bloquesDeCodigo += 1;
72
+ });
73
+
74
+ const html = await renderizarEnGitHub(markdown);
75
+ const renderizadas = [...html.matchAll(/<img\s[^>]*>/g)].map((m) => m[0]);
76
+
77
+ process.stdout.write(`\n${relativo}\n`);
78
+ process.stdout.write(` imagenes en el fuente: ${esperadas.length}\n`);
79
+ process.stdout.write(` imagenes renderizadas: ${renderizadas.length}\n`);
80
+ imagenesTotales += esperadas.length;
81
+
82
+ if (renderizadas.length !== esperadas.length) {
83
+ process.stdout.write(' ERROR: GitHub no renderizo todas las imagenes\n');
84
+ problemas += 1;
85
+ }
86
+
87
+ for (const { alt, src } of esperadas) {
88
+ const etiqueta = renderizadas.find((t) => t.includes(`src="${src}"`));
89
+ if (etiqueta === undefined) {
90
+ process.stdout.write(` ERROR: no se renderizo ${src}\n`);
91
+ problemas += 1;
92
+ continue;
93
+ }
94
+ if (!etiqueta.includes(`alt="${alt}"`)) {
95
+ process.stdout.write(` ERROR: ${src} perdio su texto alternativo\n`);
96
+ problemas += 1;
97
+ }
98
+ if (/^[a-z][a-z0-9+.-]*:/i.test(src)) {
99
+ process.stdout.write(` ERROR: ${src} no es una ruta relativa\n`);
100
+ problemas += 1;
101
+ }
102
+ }
103
+
104
+ // Los bloques de codigo que no son diagramas deben seguir siendo codigo.
105
+ const pre = (html.match(/<pre[\s>]/g) ?? []).length;
106
+ if (bloquesDeCodigo > 0 && pre === 0) {
107
+ process.stdout.write(' ERROR: los bloques de codigo no sobrevivieron\n');
108
+ problemas += 1;
109
+ }
110
+
111
+ if (/<script/i.test(html)) {
112
+ process.stdout.write(' ERROR: el HTML renderizado contiene un script\n');
113
+ problemas += 1;
114
+ }
115
+ }
116
+
117
+ process.stdout.write(
118
+ `\n${documentos.length} documento(s), ${imagenesTotales} imagen(es), ${problemas} problema(s)\n`,
119
+ );
120
+ process.stdout.write(
121
+ '\nComprobado: GitHub renderiza cada imagen, conserva su texto alternativo y\n' +
122
+ 'su ruta relativa, y mantiene los bloques de codigo ajenos.\n' +
123
+ 'No comprobado: si su servidor de imagenes conserva el `<style>` interno del\n' +
124
+ 'SVG, del que depende la variante oscura. Eso exige abrir el archivo en\n' +
125
+ 'github.com con la sesion iniciada.\n',
126
+ );
127
+
128
+ process.exit(problemas === 0 ? 0 : 1);
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Tipos del script de eval, para poder probarlo desde TypeScript.
3
+ *
4
+ * El script es `.mjs` a proposito: se ejecuta con node sin compilar, igual que
5
+ * el resto de `scripts/`. Esta declaracion solo describe lo que exporta.
6
+ */
7
+
8
+ export declare function extraerBloque(texto: string): { lang: string; source: string } | undefined;
@@ -0,0 +1,284 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Mide si DocViz se deja usar por un modelo de lenguaje.
4
+ *
5
+ * Es la metrica del producto y hasta ahora era una intuicion: el catalogo, los
6
+ * mensajes de error y AGENTS.md existen para que un agente acierte, pero nadie
7
+ * habia comprobado si acierta. Aqui se mide, sobre los mismos casos, en dos
8
+ * modos:
9
+ *
10
+ * node scripts/eval.mjs catalogo (sin red, sin coste)
11
+ * node scripts/eval.mjs --modelo con un modelo de verdad (gasta dinero)
12
+ *
13
+ * El modo **catalogo** pregunta a `docviz suggest` que tipo usaria para cada
14
+ * necesidad. No usa un modelo, pero mide justo lo que un modelo lee para
15
+ * decidir: los `keywords`, el `purpose` y el `whenToUse` del catalogo. Es
16
+ * determinista, dura un segundo y por eso puede ser una compuerta de CI.
17
+ *
18
+ * El modo **modelo** es la medida real: se le entrega el mismo AGENTS.md que
19
+ * recibiria en un proyecto, se le pide el bloque y se compila. Si falla, se le
20
+ * devuelve el error tal cual —con su codigo y su errata senalada— y se le deja
21
+ * reintentar. Lo que se mide entonces no es solo si acierta, sino si nuestros
22
+ * mensajes de error le permiten recuperarse.
23
+ */
24
+
25
+ import { readFile } from 'node:fs/promises';
26
+ import path from 'node:path';
27
+ import process from 'node:process';
28
+ import { fileURLToPath } from 'node:url';
29
+ import { compileDsl } from '../dist/dsl/index.js';
30
+ import { suggestType } from '../dist/mcp/tools.js';
31
+
32
+ const raiz = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
33
+ const argv = process.argv.slice(2);
34
+ const opciones = {
35
+ modelo: argv.includes('--modelo'),
36
+ json: argv.includes('--json'),
37
+ modeloId: valorDe('--modelo-id') ?? 'claude-opus-5',
38
+ particion: valorDe('--particion'),
39
+ reintentos: Number.parseInt(valorDe('--reintentos') ?? '2', 10),
40
+ minimo: Number.parseFloat(valorDe('--minimo') ?? '0'),
41
+ soloCaso: valorDe('--caso'),
42
+ };
43
+
44
+ function valorDe(bandera) {
45
+ const i = argv.indexOf(bandera);
46
+ return i >= 0 && argv[i + 1] !== undefined ? argv[i + 1] : undefined;
47
+ }
48
+
49
+ async function cargarCasos() {
50
+ const todos = JSON.parse(await readFile(path.join(raiz, 'eval', 'casos.json'), 'utf8'));
51
+ return todos.filter(
52
+ (c) =>
53
+ (opciones.soloCaso === undefined || c.id === opciones.soloCaso) &&
54
+ (opciones.particion === undefined || (c.particion ?? 'entrenamiento') === opciones.particion),
55
+ );
56
+ }
57
+
58
+ /** Tipos que se dan por buenos para un caso. */
59
+ const aceptados = (caso) => caso.acepta ?? [caso.tipo];
60
+
61
+ // --------------------------------------------------------------------------
62
+ // Modo catalogo
63
+ // --------------------------------------------------------------------------
64
+
65
+ async function evaluarCatalogo() {
66
+ const casos = await cargarCasos();
67
+ const filas = casos.map((caso) => {
68
+ const resultado = suggestType({ need: caso.necesidad, limit: 3 });
69
+ const propuestos = (resultado.matches ?? []).map((m) => m.type);
70
+ const validos = aceptados(caso);
71
+ return {
72
+ id: caso.id,
73
+ idioma: caso.idioma ?? 'es',
74
+ particion: caso.particion ?? 'entrenamiento',
75
+ esperado: caso.tipo,
76
+ propuestos,
77
+ top1: propuestos.length > 0 && validos.includes(propuestos[0]),
78
+ top3: propuestos.some((t) => validos.includes(t)),
79
+ };
80
+ });
81
+
82
+ const agrupar = (clave) => {
83
+ const grupos = {};
84
+ for (const f of filas) {
85
+ const g = (grupos[f[clave]] ??= { casos: 0, top1: 0, top3: 0 });
86
+ g.casos += 1;
87
+ if (f.top1) g.top1 += 1;
88
+ if (f.top3) g.top3 += 1;
89
+ }
90
+ return grupos;
91
+ };
92
+ const porIdioma = agrupar('idioma');
93
+ const porParticion = agrupar('particion');
94
+
95
+ return {
96
+ modo: 'catalogo',
97
+ casos: filas.length,
98
+ top1: filas.filter((f) => f.top1).length,
99
+ top3: filas.filter((f) => f.top3).length,
100
+ porIdioma,
101
+ porParticion,
102
+ filas,
103
+ };
104
+ }
105
+
106
+ // --------------------------------------------------------------------------
107
+ // Modo modelo
108
+ // --------------------------------------------------------------------------
109
+
110
+ const VALLAS = ['diagram', 'chart', 'architecture'];
111
+
112
+ /** Primer bloque de DocViz que aparezca en la respuesta. */
113
+ export function extraerBloque(texto) {
114
+ const re = /```(diagram|chart|architecture)[^\n]*\n([\s\S]*?)```/;
115
+ const m = re.exec(texto);
116
+ return m === null ? undefined : { lang: m[1], source: m[2] };
117
+ }
118
+
119
+ const INSTRUCCIONES = [
120
+ 'Escribe UN solo bloque de DocViz que explique lo que se te pide.',
121
+ 'Responde unicamente con el bloque entre vallas, sin texto antes ni despues.',
122
+ 'Elige el tipo mas adecuado del catalogo que aparece en las instrucciones.',
123
+ ].join(' ');
124
+
125
+ async function evaluarModelo() {
126
+ const casos = await cargarCasos();
127
+ const { default: Anthropic } = await import('@anthropic-ai/sdk');
128
+ const client = new Anthropic();
129
+ const contrato = await readFile(path.join(raiz, 'AGENTS.md'), 'utf8');
130
+
131
+ const filas = [];
132
+ for (const caso of casos) {
133
+ const fila = await resolverCaso(client, contrato, caso);
134
+ filas.push(fila);
135
+ if (!opciones.json) {
136
+ const marca = fila.compilaFinal ? (fila.intentos === 1 ? 'ok ' : 'ok*') : 'NO ';
137
+ process.stdout.write(
138
+ ` ${marca} ${caso.id.padEnd(16)} ${String(fila.tipoUsado ?? '-').padEnd(16)} ${fila.intentos} intento(s)\n`,
139
+ );
140
+ }
141
+ }
142
+
143
+ return {
144
+ modo: 'modelo',
145
+ modeloId: opciones.modeloId,
146
+ casos: filas.length,
147
+ tipoCorrecto: filas.filter((f) => f.tipoCorrecto).length,
148
+ compilaPrimera: filas.filter((f) => f.compilaPrimera).length,
149
+ compilaFinal: filas.filter((f) => f.compilaFinal).length,
150
+ intentosMedios: filas.reduce((a, f) => a + f.intentos, 0) / (filas.length || 1),
151
+ filas,
152
+ };
153
+ }
154
+
155
+ async function resolverCaso(client, contrato, caso) {
156
+ const mensajes = [{ role: 'user', content: `${INSTRUCCIONES}\n\nNecesidad: ${caso.necesidad}` }];
157
+ const fila = {
158
+ id: caso.id,
159
+ esperado: caso.tipo,
160
+ intentos: 0,
161
+ tipoUsado: undefined,
162
+ tipoCorrecto: false,
163
+ compilaPrimera: false,
164
+ compilaFinal: false,
165
+ errores: [],
166
+ };
167
+
168
+ for (let intento = 1; intento <= opciones.reintentos + 1; intento += 1) {
169
+ fila.intentos = intento;
170
+ const respuesta = await client.messages.create({
171
+ model: opciones.modeloId,
172
+ max_tokens: 16000,
173
+ system: [{ type: 'text', text: contrato, cache_control: { type: 'ephemeral' } }],
174
+ messages: mensajes,
175
+ });
176
+
177
+ const texto = respuesta.content
178
+ .filter((b) => b.type === 'text')
179
+ .map((b) => b.text)
180
+ .join('\n');
181
+ const bloque = extraerBloque(texto);
182
+
183
+ if (bloque === undefined) {
184
+ fila.errores.push('la respuesta no contenia ningun bloque de DocViz');
185
+ mensajes.push({ role: 'assistant', content: texto });
186
+ mensajes.push({
187
+ role: 'user',
188
+ content: `No encuentro ningun bloque entre vallas \`\`\`${VALLAS.join('/')}\`\`\`. Devuelve solo el bloque.`,
189
+ });
190
+ continue;
191
+ }
192
+
193
+ // El tipo se anota aunque el bloque no compile: acertar el tipo y
194
+ // equivocarse en un campo son dos fallos distintos y se corrigen distinto.
195
+ const declarado = /^\s*type:\s*(\S+)/m.exec(bloque.source);
196
+ fila.tipoUsado = declarado?.[1] ?? (bloque.lang === 'architecture' ? 'c4-context' : undefined);
197
+ fila.tipoCorrecto = fila.tipoUsado !== undefined && aceptados(caso).includes(fila.tipoUsado);
198
+
199
+ try {
200
+ compileDsl(bloque.lang, bloque.source);
201
+ fila.compilaFinal = true;
202
+ if (intento === 1) fila.compilaPrimera = true;
203
+ return fila;
204
+ } catch (err) {
205
+ // Aqui se mide de verdad la calidad del reporte: se le devuelve el error
206
+ // tal cual lo veria en su terminal, sin ayuda anadida.
207
+ const reporte = typeof err.format === 'function' ? err.format() : String(err.message ?? err);
208
+ fila.errores.push(reporte);
209
+ mensajes.push({ role: 'assistant', content: texto });
210
+ mensajes.push({ role: 'user', content: `El bloque no compila:\n\n${reporte}\n\nDevuelve el bloque corregido.` });
211
+ }
212
+ }
213
+
214
+ return fila;
215
+ }
216
+
217
+ // --------------------------------------------------------------------------
218
+
219
+ /**
220
+ * El cuerpo solo corre si se invoca el script directamente.
221
+ *
222
+ * Sin esta guarda, importar el modulo para probar una de sus funciones
223
+ * ejecutaria el eval entero como efecto secundario.
224
+ */
225
+ const invocadoDirectamente =
226
+ process.argv[1] !== undefined && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url);
227
+
228
+ if (invocadoDirectamente) await main();
229
+
230
+ async function main() {
231
+ const resultado = opciones.modelo ? await evaluarModelo() : await evaluarCatalogo();
232
+
233
+ if (opciones.json) {
234
+ process.stdout.write(`${JSON.stringify(resultado, null, 2)}\n`);
235
+ } else if (resultado.modo === 'catalogo') {
236
+ for (const f of resultado.filas) {
237
+ if (f.top1) continue;
238
+ process.stdout.write(
239
+ ` ${(f.top3 ? 'top3' : 'FALLA').padEnd(5)} ${f.id.padEnd(16)} esperaba ${f.esperado.padEnd(16)} propuso ${f.propuestos.join(', ') || '(nada)'}\n`,
240
+ );
241
+ }
242
+ const pct = (n, total) => ((n / total) * 100).toFixed(1);
243
+ process.stdout.write(
244
+ `\ncatalogo: ${resultado.casos} casos\n` +
245
+ ` acierto en la primera propuesta: ${resultado.top1}/${resultado.casos} (${pct(resultado.top1, resultado.casos)} %)\n` +
246
+ ` acierto entre las tres primeras: ${resultado.top3}/${resultado.casos} (${pct(resultado.top3, resultado.casos)} %)\n`,
247
+ );
248
+ // El desglose por idioma no es decorativo: el catalogo se escribio en
249
+ // español y las palabras en ingles se anadieron despues, asi que la unica
250
+ // forma de saber si sirven de algo es medirlas por separado.
251
+ for (const [idioma, d] of Object.entries(resultado.porIdioma).sort()) {
252
+ process.stdout.write(
253
+ ` ${idioma}: ${d.casos} casos -> ${pct(d.top1, d.casos)} % / ${pct(d.top3, d.casos)} %\n`,
254
+ );
255
+ }
256
+ // La particion reservada es la unica cifra que se puede citar: los casos de
257
+ // entrenamiento se han mirado al ajustar la puntuacion, y lo que se ajusta
258
+ // mirando deja de medir.
259
+ process.stdout.write('\n');
260
+ for (const [particion, d] of Object.entries(resultado.porParticion).sort()) {
261
+ process.stdout.write(
262
+ ` ${particion.padEnd(14)} ${String(d.casos).padStart(2)} casos -> ${pct(d.top1, d.casos)} % / ${pct(d.top3, d.casos)} %\n`,
263
+ );
264
+ }
265
+ } else {
266
+ const pct = (n) => ((n / resultado.casos) * 100).toFixed(1);
267
+ process.stdout.write(
268
+ `\nmodelo ${resultado.modeloId}: ${resultado.casos} casos\n` +
269
+ ` tipo correcto: ${resultado.tipoCorrecto}/${resultado.casos} (${pct(resultado.tipoCorrecto)} %)\n` +
270
+ ` compila a la primera: ${resultado.compilaPrimera}/${resultado.casos} (${pct(resultado.compilaPrimera)} %)\n` +
271
+ ` compila al final: ${resultado.compilaFinal}/${resultado.casos} (${pct(resultado.compilaFinal)} %)\n` +
272
+ ` intentos de media: ${resultado.intentosMedios.toFixed(2)}\n`,
273
+ );
274
+ }
275
+
276
+ // La compuerta mide el acierto entre las tres primeras: un agente ve las tres.
277
+ const medida = resultado.modo === 'catalogo' ? resultado.top3 / resultado.casos : resultado.compilaFinal / resultado.casos;
278
+ if (opciones.minimo > 0 && medida < opciones.minimo) {
279
+ process.stderr.write(
280
+ `\nla medida (${(medida * 100).toFixed(1)} %) esta por debajo del minimo exigido (${(opciones.minimo * 100).toFixed(1)} %)\n`,
281
+ );
282
+ process.exit(1);
283
+ }
284
+ }
@@ -0,0 +1,122 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Descarga `vendor/plantuml.jar` desde Maven Central.
4
+ *
5
+ * Es la unica operacion del proyecto que usa la red, y ocurre una sola vez
6
+ * durante la instalacion: despues, el build compila sin conexion. El jar no se
7
+ * versiona en el repositorio porque ocupa unos 26 MB.
8
+ *
9
+ * node scripts/fetch-plantuml.mjs [version]
10
+ */
11
+
12
+ import { createHash } from 'node:crypto';
13
+ import { mkdir, readFile, writeFile } from 'node:fs/promises';
14
+ import path from 'node:path';
15
+ import process from 'node:process';
16
+ import { fileURLToPath } from 'node:url';
17
+
18
+ const DEFAULT_VERSION = '1.2026.0';
19
+
20
+ /**
21
+ * Digest esperado de cada version conocida.
22
+ *
23
+ * Sin esto, la unica garantia de que el jar es el que dice ser es el TLS de
24
+ * Maven Central. Es el unico punto de DocViz que trae bytes de fuera y los deja
25
+ * listos para ejecutarse en una JVM, asi que se comprueba contra un valor
26
+ * fijado en el repositorio y verificado a mano contra los checksums publicados
27
+ * (`plantuml-<version>.jar.sha256`).
28
+ *
29
+ * Para una version que no este aqui, pasa el digest con `--sha256 <hex>` o
30
+ * `DOCVIZ_PLANTUML_SHA256`. Descargar sin verificar exige decirlo en voz alta.
31
+ */
32
+ const DIGESTS = {
33
+ '1.2026.0': 'b3da2f352a835615ecb63eb754930f8aab57363d5fe1a0660bf2a12827b6b553',
34
+ };
35
+
36
+ const args = process.argv.slice(2);
37
+ const flag = (nombre) => {
38
+ const i = args.indexOf(nombre);
39
+ return i >= 0 && args[i + 1] !== undefined ? args[i + 1] : undefined;
40
+ };
41
+ const sinVerificar = args.includes('--sin-verificar');
42
+ const version = args.find((a) => !a.startsWith('--') && args[args.indexOf(a) - 1] !== '--sha256')
43
+ ?? process.env['DOCVIZ_PLANTUML_VERSION']
44
+ ?? DEFAULT_VERSION;
45
+ const esperado = (flag('--sha256') ?? process.env['DOCVIZ_PLANTUML_SHA256'] ?? DIGESTS[version])?.toLowerCase();
46
+
47
+ if (esperado === undefined && !sinVerificar) {
48
+ process.stderr.write(
49
+ `no hay digest conocido para PlantUML ${version}.\n` +
50
+ 'Consulta el checksum publicado:\n' +
51
+ ` curl -s https://repo1.maven.org/maven2/net/sourceforge/plantuml/plantuml/${version}/plantuml-${version}.jar.sha256\n` +
52
+ 'y pasalo con --sha256 <hex>, o descarga sin verificar con --sin-verificar.\n',
53
+ );
54
+ process.exit(1);
55
+ }
56
+
57
+ const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
58
+ const target = path.join(root, 'vendor', 'plantuml.jar');
59
+ const url = `https://repo1.maven.org/maven2/net/sourceforge/plantuml/plantuml/${version}/plantuml-${version}.jar`;
60
+
61
+ // Si ya existe un jar utilizable, no se vuelve a descargar.
62
+ try {
63
+ const existing = await readFile(target);
64
+ if (existing.byteLength > 1_000_000) {
65
+ process.stdout.write(`plantuml.jar ya presente (${(existing.byteLength / 1e6).toFixed(1)} MB): ${target}\n`);
66
+ process.exit(0);
67
+ }
68
+ } catch {
69
+ // no existe: se descarga
70
+ }
71
+
72
+ process.stdout.write(`descargando PlantUML ${version}\n desde ${url}\n`);
73
+
74
+ let response;
75
+ try {
76
+ response = await fetch(url, { redirect: 'follow' });
77
+ } catch (err) {
78
+ process.stderr.write(
79
+ `no se pudo contactar con Maven Central: ${err instanceof Error ? err.message : String(err)}\n` +
80
+ 'si tu organizacion distribuye el jar, colocalo en vendor/plantuml.jar o apuntalo\n' +
81
+ 'con renderers.plantuml.jar en docviz.config.yaml\n',
82
+ );
83
+ process.exit(1);
84
+ }
85
+
86
+ if (!response.ok) {
87
+ process.stderr.write(`Maven Central respondio ${response.status} ${response.statusText}\n`);
88
+ process.exit(1);
89
+ }
90
+
91
+ const bytes = Buffer.from(await response.arrayBuffer());
92
+ // Un jar de PlantUML pesa decenas de MB: cualquier cosa menor es una pagina de error.
93
+ if (bytes.byteLength < 1_000_000) {
94
+ process.stderr.write(`la descarga solo trajo ${bytes.byteLength} bytes; no parece un jar\n`);
95
+ process.exit(1);
96
+ }
97
+ if (bytes.subarray(0, 2).toString('latin1') !== 'PK') {
98
+ process.stderr.write('el archivo descargado no es un jar valido\n');
99
+ process.exit(1);
100
+ }
101
+
102
+ const digest = createHash('sha256').update(bytes).digest('hex');
103
+
104
+ // La comprobacion va ANTES de escribir: un jar que no es el esperado no debe
105
+ // llegar al disco, porque el siguiente build lo daria por bueno.
106
+ if (esperado !== undefined && digest !== esperado) {
107
+ process.stderr.write(
108
+ 'el jar descargado no coincide con el digest esperado; no se ha escrito nada.\n' +
109
+ ` esperado: ${esperado}\n` +
110
+ ` obtenido: ${digest}\n` +
111
+ 'Puede ser una version distinta, una descarga corrupta o un intermediario.\n',
112
+ );
113
+ process.exit(1);
114
+ }
115
+
116
+ await mkdir(path.dirname(target), { recursive: true });
117
+ await writeFile(target, bytes);
118
+
119
+ process.stdout.write(
120
+ `listo: ${target}\n tamano: ${(bytes.byteLength / 1e6).toFixed(1)} MB\n` +
121
+ ` sha256: ${digest}${esperado !== undefined ? ' (verificado)' : ' (SIN VERIFICAR)'}\n`,
122
+ );