@loj-lang/cli 0.5.0 → 0.5.1

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/README.md CHANGED
@@ -65,6 +65,9 @@ npx @loj-lang/cli agent install codex --scope project
65
65
  # add a skill bundle from a local or remote source
66
66
  npx @loj-lang/cli agent add codex --from ./tooling/skills/loj-authoring
67
67
 
68
+ # add directly from the GitHub release asset
69
+ npx @loj-lang/cli agent add codex --from https://github.com/juliusrl/loj/releases/download/v0.5.0/loj-authoring-0.5.0.tgz
70
+
68
71
  # install into any explicit skills directory
69
72
  npx @loj-lang/cli agent install generic --skills-dir ~/.my-agent/skills
70
73
 
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAuCA,MAAM,WAAW,KAAK;IACpB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAChC,MAAM,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAChC,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC;IACzC,OAAO,CAAC,EAAE,UAAU,CAAC;IACrB,YAAY,CAAC,EAAE,CAAC,OAAO,EAAE,UAAU,KAAK,IAAI,CAAC;CAC9C;AAiUD,MAAM,WAAW,UAAU;IACzB,KAAK,IAAI,IAAI,CAAC;CACf;AAED,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACjC,QAAQ,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IACnC,KAAK,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC9B;AAED,MAAM,WAAW,mBAAmB;IAClC,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC;IACzC,YAAY,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IACtC,YAAY,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IACtC,MAAM,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI,KAAK,IAAI,CAAC;CAC/D;AAED,MAAM,WAAW,UAAU;IACzB,KAAK,CAAC,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,KAAK,IAAI,GAAG,UAAU,CAAC;IAC/F,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,OAAO,EAAE,mBAAmB,GAAG,UAAU,CAAC;IACjF,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC;IAC9B,UAAU,IAAI,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,UAAU;IACzB,KAAK,IAAI,IAAI,CAAC;IACd,OAAO,CAAC,OAAO,CAAC,EAAE,QAAQ,GAAG,QAAQ,GAAG,IAAI,CAAC;CAC9C;AAkUD,wBAAgB,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,GAAE,KAAU,GAAG,MAAM,CAmD7D"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAwCA,MAAM,WAAW,KAAK;IACpB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAChC,MAAM,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAChC,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC;IACzC,OAAO,CAAC,EAAE,UAAU,CAAC;IACrB,YAAY,CAAC,EAAE,CAAC,OAAO,EAAE,UAAU,KAAK,IAAI,CAAC;CAC9C;AA2UD,MAAM,WAAW,UAAU;IACzB,KAAK,IAAI,IAAI,CAAC;CACf;AAED,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACjC,QAAQ,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IACnC,KAAK,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC9B;AAED,MAAM,WAAW,mBAAmB;IAClC,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC;IACzC,YAAY,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IACtC,YAAY,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IACtC,MAAM,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI,KAAK,IAAI,CAAC;CAC/D;AAED,MAAM,WAAW,UAAU;IACzB,KAAK,CAAC,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,KAAK,IAAI,GAAG,UAAU,CAAC;IAC/F,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,OAAO,EAAE,mBAAmB,GAAG,UAAU,CAAC;IACjF,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC;IAC9B,UAAU,IAAI,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,UAAU;IACzB,KAAK,IAAI,IAAI,CAAC;IACd,OAAO,CAAC,OAAO,CAAC,EAAE,QAAQ,GAAG,QAAQ,GAAG,IAAI,CAAC;CAC9C;AAkUD,wBAAgB,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,GAAE,KAAU,GAAG,MAAM,CAqD7D"}
package/dist/index.js CHANGED
@@ -6,6 +6,7 @@ import { basename, dirname, relative, resolve } from 'node:path';
6
6
  import { fileURLToPath } from 'node:url';
7
7
  import YAML from 'yaml';
8
8
  import { runCli as runReactCli } from '@loj-lang/rdsl-cli';
9
+ import { compileProject as compileFrontendProject } from '@loj-lang/rdsl-compiler';
9
10
  import { runCli as runSpringCli } from '@loj-lang/sdsl-cli';
10
11
  import { compileProject as compileBackendProject } from '@loj-lang/sdsl-compiler';
11
12
  import { buildRulesManifestFileName, countRulesEntries, compileRulesSource, isRulesSourceFile, } from './rules-proof.js';
@@ -251,6 +252,8 @@ export function runCli(args, io = {}) {
251
252
  return handleStop(parseStopArgs(rest), context);
252
253
  case 'doctor':
253
254
  return handleDoctor(parseDoctorArgs(rest), context);
255
+ case 'graph':
256
+ return handleGraph(parseGraphArgs(rest), context);
254
257
  case 'rules':
255
258
  return handleRules(parseRulesArgs(rest), context);
256
259
  case 'flow':
@@ -1345,6 +1348,111 @@ function handleRules(options, context) {
1345
1348
  }
1346
1349
  return 0;
1347
1350
  }
1351
+ function handleGraph(options, context) {
1352
+ if ('error' in options) {
1353
+ context.stderr(`${options.error}\n`);
1354
+ return 1;
1355
+ }
1356
+ const loaded = loadProject(options.projectFile, context.cwd, context.env);
1357
+ if ('error' in loaded) {
1358
+ writeFailure(loaded.error, context.stderr, context.stdout, options.json);
1359
+ return 1;
1360
+ }
1361
+ const selectedTargets = selectProjectTargets(loaded.project, options.targetAliases);
1362
+ if ('error' in selectedTargets) {
1363
+ writeFailure(selectedTargets.error, context.stderr, context.stdout, options.json);
1364
+ return 1;
1365
+ }
1366
+ const activeProject = selectedTargets.project;
1367
+ const graphDocuments = [];
1368
+ const compileErrors = [];
1369
+ if (options.surface === 'source' || options.surface === 'all') {
1370
+ const sourceGraphs = buildProjectSourceGraphs(activeProject);
1371
+ if ('error' in sourceGraphs) {
1372
+ compileErrors.push(sourceGraphs.error);
1373
+ }
1374
+ else {
1375
+ graphDocuments.push(...sourceGraphs.documents);
1376
+ }
1377
+ }
1378
+ if (options.surface === 'frontend' || options.surface === 'all') {
1379
+ for (const target of activeProject.targets.filter((entry) => entry.type === 'rdsl')) {
1380
+ const frontendGraph = buildFrontendGeneratedGraph(target, activeProject.projectDir);
1381
+ if ('error' in frontendGraph) {
1382
+ compileErrors.push(frontendGraph.error);
1383
+ continue;
1384
+ }
1385
+ graphDocuments.push(frontendGraph.document);
1386
+ }
1387
+ }
1388
+ if (options.surface === 'backend' || options.surface === 'all') {
1389
+ for (const target of activeProject.targets.filter((entry) => entry.type === 'sdsl')) {
1390
+ const backendGraph = buildBackendGeneratedGraph(target, activeProject.projectDir);
1391
+ if ('error' in backendGraph) {
1392
+ compileErrors.push(backendGraph.error);
1393
+ continue;
1394
+ }
1395
+ graphDocuments.push(backendGraph.document);
1396
+ }
1397
+ }
1398
+ if (compileErrors.length > 0) {
1399
+ if (options.json) {
1400
+ writeJsonArtifact(context.stdout, 'loj.graph.result', {
1401
+ success: false,
1402
+ projectFile: options.projectFile,
1403
+ surface: options.surface,
1404
+ app: { name: activeProject.appName },
1405
+ targets: activeProject.targets.map((target) => ({
1406
+ alias: target.alias,
1407
+ type: presentTargetType(target.type),
1408
+ entry: target.entry,
1409
+ })),
1410
+ errors: compileErrors,
1411
+ graphs: [],
1412
+ });
1413
+ return 1;
1414
+ }
1415
+ writeFailure(compileErrors.join('\n'), context.stderr, context.stdout, false);
1416
+ return 1;
1417
+ }
1418
+ if (options.json) {
1419
+ writeJsonArtifact(context.stdout, 'loj.graph.result', {
1420
+ success: true,
1421
+ projectFile: options.projectFile,
1422
+ surface: options.surface,
1423
+ app: { name: activeProject.appName },
1424
+ targets: activeProject.targets.map((target) => ({
1425
+ alias: target.alias,
1426
+ type: presentTargetType(target.type),
1427
+ entry: target.entry,
1428
+ })),
1429
+ graphs: graphDocuments,
1430
+ });
1431
+ return 0;
1432
+ }
1433
+ if (options.outDir) {
1434
+ const absoluteOutDir = resolve(activeProject.projectDir, options.outDir);
1435
+ mkdirSync(absoluteOutDir, { recursive: true });
1436
+ for (const graph of graphDocuments) {
1437
+ const graphFile = resolve(absoluteOutDir, `${graph.id}.mmd`);
1438
+ writeFileSync(graphFile, `${graph.mermaid}\n`, 'utf8');
1439
+ }
1440
+ }
1441
+ writeCliBanner(context.stdout, 'loj graph', 'emit mermaid architecture views for the selected project');
1442
+ context.stdout(`Project graph: ${normalizePath(resolveProjectAbsoluteFile(options.projectFile, context.cwd))}\n\n`);
1443
+ for (const graph of graphDocuments) {
1444
+ context.stdout(`${graph.title}\n`);
1445
+ context.stdout('```mermaid\n');
1446
+ context.stdout(`${graph.mermaid}\n`);
1447
+ context.stdout('```\n\n');
1448
+ }
1449
+ writeCliSection(context.stdout, 'next', [
1450
+ ...(options.outDir ? [`wrote mermaid files to: ${normalizePath(options.outDir)}`] : []),
1451
+ `copy a graph into docs or release notes`,
1452
+ `re-run with JSON output: loj graph ${options.projectFile} --json`,
1453
+ ]);
1454
+ return 0;
1455
+ }
1348
1456
  function handleFlow(options, context) {
1349
1457
  if ('error' in options) {
1350
1458
  context.stderr(`${options.error}\n`);
@@ -1568,6 +1676,45 @@ function parseDoctorArgs(args) {
1568
1676
  }
1569
1677
  return parsed;
1570
1678
  }
1679
+ function parseGraphArgs(args) {
1680
+ if (args.length === 0) {
1681
+ return { error: 'Usage: loj graph <loj.project.yaml> [--surface source|frontend|backend|all] [--target <alias>] [--out-dir <dir>] [--json]' };
1682
+ }
1683
+ let surface = 'all';
1684
+ let outDir;
1685
+ const filteredArgs = [];
1686
+ for (let index = 0; index < args.length; index += 1) {
1687
+ const arg = args[index];
1688
+ if (arg === '--surface') {
1689
+ const value = (args[index + 1] ?? '').trim();
1690
+ if (value !== 'source' && value !== 'frontend' && value !== 'backend' && value !== 'all') {
1691
+ return { error: 'loj graph --surface must be one of: source, frontend, backend, all' };
1692
+ }
1693
+ surface = value;
1694
+ index += 1;
1695
+ continue;
1696
+ }
1697
+ if (arg === '--out-dir') {
1698
+ const value = (args[index + 1] ?? '').trim();
1699
+ if (!value) {
1700
+ return { error: 'loj graph --out-dir must be a non-empty path' };
1701
+ }
1702
+ outDir = value;
1703
+ index += 1;
1704
+ continue;
1705
+ }
1706
+ filteredArgs.push(arg);
1707
+ }
1708
+ const parsed = parseProjectTargetArgs('graph', filteredArgs);
1709
+ if ('error' in parsed) {
1710
+ return parsed;
1711
+ }
1712
+ return {
1713
+ ...parsed,
1714
+ surface,
1715
+ outDir,
1716
+ };
1717
+ }
1571
1718
  function parseProjectTargetArgs(command, args, options = {}) {
1572
1719
  let json = false;
1573
1720
  let debug = false;
@@ -4096,6 +4243,350 @@ function compileBackendIrForTarget(target, projectDir) {
4096
4243
  };
4097
4244
  }
4098
4245
  }
4246
+ function compileFrontendSemanticForTarget(target, projectDir) {
4247
+ try {
4248
+ const compiled = compileFrontendProject({
4249
+ entryFile: target.entry,
4250
+ projectRoot: '.',
4251
+ readFile(fileName) {
4252
+ return readFileSync(resolve(projectDir, fileName), 'utf8');
4253
+ },
4254
+ listFiles(directory) {
4255
+ const absoluteDirectory = resolve(projectDir, directory);
4256
+ return nodeFs.readdirSync(absoluteDirectory)
4257
+ .map((entry) => normalizePath(directory === '.' ? entry : `${directory}/${entry}`));
4258
+ },
4259
+ });
4260
+ if (!compiled.success || !compiled.ir) {
4261
+ const firstError = compiled.errors[0];
4262
+ return {
4263
+ error: `failed to compile frontend graph for target "${target.alias}"${firstError ? `: ${firstError.message}` : ''}`,
4264
+ };
4265
+ }
4266
+ return {
4267
+ ir: compiled.ir,
4268
+ files: compiled.files.map((file) => ({ path: file.path })),
4269
+ sourceFiles: compiled.semanticManifest?.sourceFiles ?? [target.entry],
4270
+ moduleGraph: compiled.semanticManifest?.moduleGraph,
4271
+ };
4272
+ }
4273
+ catch (error) {
4274
+ return {
4275
+ error: `failed to compile frontend graph for target "${target.alias}": ${error instanceof Error ? error.message : String(error)}`,
4276
+ };
4277
+ }
4278
+ }
4279
+ function buildProjectSourceGraphs(project) {
4280
+ const documents = [];
4281
+ for (const target of project.targets) {
4282
+ if (target.type === 'rdsl') {
4283
+ const compiled = compileFrontendSemanticForTarget(target, project.projectDir);
4284
+ if ('error' in compiled) {
4285
+ return compiled;
4286
+ }
4287
+ documents.push({
4288
+ id: `${target.alias}.source`,
4289
+ title: `Source graph (${target.alias})`,
4290
+ mermaid: buildFrontendSourceMermaid(project, target, compiled.ir, compiled.sourceFiles, compiled.moduleGraph),
4291
+ });
4292
+ continue;
4293
+ }
4294
+ const compiled = compileBackendIrForTarget(target, project.projectDir);
4295
+ if ('error' in compiled) {
4296
+ return compiled;
4297
+ }
4298
+ documents.push({
4299
+ id: `${target.alias}.source`,
4300
+ title: `Source graph (${target.alias})`,
4301
+ mermaid: buildBackendSourceMermaid(project, target, compiled.ir),
4302
+ });
4303
+ }
4304
+ return { documents };
4305
+ }
4306
+ function buildFrontendGeneratedGraph(target, projectDir) {
4307
+ const compiled = compileFrontendSemanticForTarget(target, projectDir);
4308
+ if ('error' in compiled) {
4309
+ return compiled;
4310
+ }
4311
+ return {
4312
+ document: {
4313
+ id: `${target.alias}.frontend`,
4314
+ title: `Generated frontend graph (${target.alias})`,
4315
+ mermaid: buildFrontendGeneratedMermaid(target, compiled.files),
4316
+ },
4317
+ };
4318
+ }
4319
+ function buildBackendGeneratedGraph(target, projectDir) {
4320
+ const compiled = compileBackendProject({
4321
+ entryFile: target.entry,
4322
+ projectRoot: '.',
4323
+ readFile(fileName) {
4324
+ return readFileSync(resolve(projectDir, fileName), 'utf8');
4325
+ },
4326
+ listFiles(directory) {
4327
+ const absoluteDirectory = resolve(projectDir, directory);
4328
+ return nodeFs.readdirSync(absoluteDirectory)
4329
+ .map((entry) => normalizePath(directory === '.' ? entry : `${directory}/${entry}`));
4330
+ },
4331
+ });
4332
+ if (!compiled.success) {
4333
+ const firstError = compiled.errors[0];
4334
+ return {
4335
+ error: `failed to compile backend graph for target "${target.alias}"${firstError ? `: ${firstError.message}` : ''}`,
4336
+ };
4337
+ }
4338
+ return {
4339
+ document: {
4340
+ id: `${target.alias}.backend`,
4341
+ title: `Generated backend graph (${target.alias})`,
4342
+ mermaid: buildBackendGeneratedMermaid(target, compiled.files.map((file) => ({ path: file.path }))),
4343
+ },
4344
+ };
4345
+ }
4346
+ function buildFrontendSourceMermaid(project, target, ir, sourceFiles, moduleGraph) {
4347
+ const lines = ['flowchart TD'];
4348
+ const appId = mermaidNodeId(`app.${target.alias}`);
4349
+ lines.push(` ${appId}[${mermaidLabel(`${project.appName} / ${target.alias}`)}]`);
4350
+ const entryId = mermaidNodeId(`entry.${target.alias}`);
4351
+ lines.push(` ${entryId}[${mermaidLabel(target.entry)}]`);
4352
+ lines.push(` ${appId} --> ${entryId}`);
4353
+ const models = toNamedList(ir.models);
4354
+ const resources = toNamedList(ir.resources);
4355
+ const readModels = toNamedList(ir.readModels);
4356
+ const pages = toNamedList(ir.pages);
4357
+ for (const file of sourceFiles) {
4358
+ const fileId = mermaidNodeId(`sourceFile.${target.alias}.${file}`);
4359
+ lines.push(` ${fileId}[${mermaidLabel(file)}]`);
4360
+ lines.push(` ${entryId} -.imports.-> ${fileId}`);
4361
+ }
4362
+ if (moduleGraph) {
4363
+ for (const [from, imports] of Object.entries(moduleGraph)) {
4364
+ const fromId = mermaidNodeId(`sourceFile.${target.alias}.${from}`);
4365
+ for (const imported of imports) {
4366
+ const importedId = mermaidNodeId(`sourceFile.${target.alias}.${imported}`);
4367
+ lines.push(` ${fromId} -.imports.-> ${importedId}`);
4368
+ }
4369
+ }
4370
+ }
4371
+ for (const model of models) {
4372
+ const modelId = mermaidNodeId(`model.${target.alias}.${model.name}`);
4373
+ lines.push(` ${modelId}[${mermaidLabel(`model ${model.name}`)}]`);
4374
+ lines.push(` ${appId} --> ${modelId}`);
4375
+ }
4376
+ for (const resource of resources) {
4377
+ const resourceId = mermaidNodeId(`resource.${target.alias}.${resource.name}`);
4378
+ lines.push(` ${resourceId}[${mermaidLabel(`resource ${resource.name}`)}]`);
4379
+ lines.push(` ${appId} --> ${resourceId}`);
4380
+ if (typeof resource.model === 'string') {
4381
+ lines.push(` ${resourceId} --> ${mermaidNodeId(`model.${target.alias}.${resource.model}`)}`);
4382
+ }
4383
+ const workflow = resource.workflow;
4384
+ if (workflow && typeof workflow === 'object' && typeof workflow.resolvedPath === 'string') {
4385
+ const workflowPath = workflow.resolvedPath;
4386
+ const workflowId = mermaidNodeId(`workflow.${target.alias}.${workflowPath}`);
4387
+ lines.push(` ${workflowId}[${mermaidLabel(`workflow ${basename(workflowPath)}`)}]`);
4388
+ lines.push(` ${resourceId} --> ${workflowId}`);
4389
+ }
4390
+ }
4391
+ for (const readModel of readModels) {
4392
+ const readModelId = mermaidNodeId(`readModel.${target.alias}.${readModel.name}`);
4393
+ lines.push(` ${readModelId}[${mermaidLabel(`readModel ${readModel.name}`)}]`);
4394
+ lines.push(` ${appId} --> ${readModelId}`);
4395
+ const rules = readModel.rules;
4396
+ if (rules && typeof rules === 'object' && typeof rules.resolvedPath === 'string') {
4397
+ const rulesPath = rules.resolvedPath;
4398
+ const rulesId = mermaidNodeId(`rules.${target.alias}.${rulesPath}`);
4399
+ lines.push(` ${rulesId}[${mermaidLabel(`rules ${basename(rulesPath)}`)}]`);
4400
+ lines.push(` ${readModelId} --> ${rulesId}`);
4401
+ }
4402
+ }
4403
+ for (const page of pages) {
4404
+ const pageId = mermaidNodeId(`page.${target.alias}.${page.name}`);
4405
+ lines.push(` ${pageId}[${mermaidLabel(`page ${page.name}`)}]`);
4406
+ lines.push(` ${appId} --> ${pageId}`);
4407
+ const blocks = Array.isArray(page.blocks) ? page.blocks : [];
4408
+ for (const block of blocks) {
4409
+ if (!block || typeof block !== 'object' || typeof block.id !== 'string') {
4410
+ continue;
4411
+ }
4412
+ const blockTitle = typeof block.title === 'string'
4413
+ ? block.title
4414
+ : block.blockType === 'string'
4415
+ ? block.blockType
4416
+ : 'block';
4417
+ const blockId = mermaidNodeId(`pageBlock.${target.alias}.${block.id}`);
4418
+ lines.push(` ${blockId}[${mermaidLabel(`block ${blockTitle}`)}]`);
4419
+ lines.push(` ${pageId} --> ${blockId}`);
4420
+ const data = block.data;
4421
+ if (typeof data === 'string') {
4422
+ lines.push(` ${blockId} --> ${mermaidNodeId(`readModel.${target.alias}.${data}`)}`);
4423
+ }
4424
+ }
4425
+ }
4426
+ return lines.join('\n');
4427
+ }
4428
+ function buildBackendSourceMermaid(project, target, ir) {
4429
+ const lines = ['flowchart TD'];
4430
+ const appId = mermaidNodeId(`app.${target.alias}`);
4431
+ lines.push(` ${appId}[${mermaidLabel(`${project.appName} / ${target.alias}`)}]`);
4432
+ const entryId = mermaidNodeId(`entry.${target.alias}`);
4433
+ lines.push(` ${entryId}[${mermaidLabel(target.entry)}]`);
4434
+ lines.push(` ${appId} --> ${entryId}`);
4435
+ for (const sourceFile of ir.sourceFiles) {
4436
+ const sourceId = mermaidNodeId(`sourceFile.${target.alias}.${sourceFile}`);
4437
+ lines.push(` ${sourceId}[${mermaidLabel(sourceFile)}]`);
4438
+ lines.push(` ${entryId} -.imports.-> ${sourceId}`);
4439
+ }
4440
+ for (const [from, imports] of Object.entries(ir.moduleGraph)) {
4441
+ const fromId = mermaidNodeId(`sourceFile.${target.alias}.${from}`);
4442
+ for (const imported of imports) {
4443
+ const importedId = mermaidNodeId(`sourceFile.${target.alias}.${imported}`);
4444
+ lines.push(` ${fromId} -.imports.-> ${importedId}`);
4445
+ }
4446
+ }
4447
+ for (const model of ir.models) {
4448
+ const modelId = mermaidNodeId(`model.${target.alias}.${model.name}`);
4449
+ lines.push(` ${modelId}[${mermaidLabel(`model ${model.name}`)}]`);
4450
+ lines.push(` ${appId} --> ${modelId}`);
4451
+ }
4452
+ for (const resource of ir.resources) {
4453
+ const resourceId = mermaidNodeId(`resource.${target.alias}.${resource.name}`);
4454
+ lines.push(` ${resourceId}[${mermaidLabel(`resource ${resource.name}`)}]`);
4455
+ lines.push(` ${appId} --> ${resourceId}`);
4456
+ lines.push(` ${resourceId} --> ${mermaidNodeId(`model.${target.alias}.${resource.model}`)}`);
4457
+ if (resource.workflow) {
4458
+ const workflowId = mermaidNodeId(`workflow.${target.alias}.${resource.workflow.resolvedPath}`);
4459
+ lines.push(` ${workflowId}[${mermaidLabel(`workflow ${basename(resource.workflow.resolvedPath)}`)}]`);
4460
+ lines.push(` ${resourceId} --> ${workflowId}`);
4461
+ }
4462
+ if (resource.create?.rules) {
4463
+ const rulesId = mermaidNodeId(`createRules.${target.alias}.${resource.create.rules.resolvedPath}`);
4464
+ lines.push(` ${rulesId}[${mermaidLabel(`create.rules ${basename(resource.create.rules.resolvedPath)}`)}]`);
4465
+ lines.push(` ${resourceId} --> ${rulesId}`);
4466
+ }
4467
+ }
4468
+ for (const readModel of ir.readModels) {
4469
+ const readModelId = mermaidNodeId(`readModel.${target.alias}.${readModel.name}`);
4470
+ lines.push(` ${readModelId}[${mermaidLabel(`readModel ${readModel.name}`)}]`);
4471
+ lines.push(` ${appId} --> ${readModelId}`);
4472
+ const handlerId = mermaidNodeId(`handler.${target.alias}.${readModel.name}`);
4473
+ lines.push(` ${handlerId}[${mermaidLabel(`${readModel.handler.source} ${basename(readModel.handler.resolvedPath)}`)}]`);
4474
+ lines.push(` ${readModelId} --> ${handlerId}`);
4475
+ if (readModel.rules) {
4476
+ const rulesId = mermaidNodeId(`rules.${target.alias}.${readModel.rules.resolvedPath}`);
4477
+ lines.push(` ${rulesId}[${mermaidLabel(`rules ${basename(readModel.rules.resolvedPath)}`)}]`);
4478
+ lines.push(` ${readModelId} --> ${rulesId}`);
4479
+ }
4480
+ }
4481
+ return lines.join('\n');
4482
+ }
4483
+ function buildFrontendGeneratedMermaid(target, files) {
4484
+ const lines = ['flowchart TD'];
4485
+ const appId = mermaidNodeId(`generated.frontend.${target.alias}`);
4486
+ lines.push(` ${appId}[${mermaidLabel(`generated frontend ${target.alias}`)}]`);
4487
+ const groups = groupGeneratedFrontendFiles(files.map((file) => file.path));
4488
+ for (const [groupName, groupFiles] of groups) {
4489
+ const groupId = mermaidNodeId(`generated.frontend.${target.alias}.${groupName}`);
4490
+ lines.push(` ${groupId}[${mermaidLabel(`${groupName} (${groupFiles.length})`)}]`);
4491
+ lines.push(` ${appId} --> ${groupId}`);
4492
+ for (const file of groupFiles.slice(0, 8)) {
4493
+ const fileId = mermaidNodeId(`generated.frontend.file.${target.alias}.${file}`);
4494
+ lines.push(` ${fileId}[${mermaidLabel(basename(file))}]`);
4495
+ lines.push(` ${groupId} --> ${fileId}`);
4496
+ }
4497
+ if (groupFiles.length > 8) {
4498
+ const moreId = mermaidNodeId(`generated.frontend.more.${target.alias}.${groupName}`);
4499
+ lines.push(` ${moreId}[${mermaidLabel(`… ${groupFiles.length - 8} more`)}]`);
4500
+ lines.push(` ${groupId} --> ${moreId}`);
4501
+ }
4502
+ }
4503
+ return lines.join('\n');
4504
+ }
4505
+ function buildBackendGeneratedMermaid(target, files) {
4506
+ const lines = ['flowchart TD'];
4507
+ const backendId = mermaidNodeId(`generated.backend.${target.alias}`);
4508
+ lines.push(` ${backendId}[${mermaidLabel(`generated backend ${target.alias}`)}]`);
4509
+ const groups = groupGeneratedBackendFiles(files.map((file) => file.path));
4510
+ for (const [groupName, groupFiles] of groups) {
4511
+ const groupId = mermaidNodeId(`generated.backend.${target.alias}.${groupName}`);
4512
+ lines.push(` ${groupId}[${mermaidLabel(`${groupName} (${groupFiles.length})`)}]`);
4513
+ lines.push(` ${backendId} --> ${groupId}`);
4514
+ for (const file of groupFiles.slice(0, 8)) {
4515
+ const fileId = mermaidNodeId(`generated.backend.file.${target.alias}.${file}`);
4516
+ lines.push(` ${fileId}[${mermaidLabel(basename(file))}]`);
4517
+ lines.push(` ${groupId} --> ${fileId}`);
4518
+ }
4519
+ if (groupFiles.length > 8) {
4520
+ const moreId = mermaidNodeId(`generated.backend.more.${target.alias}.${groupName}`);
4521
+ lines.push(` ${moreId}[${mermaidLabel(`… ${groupFiles.length - 8} more`)}]`);
4522
+ lines.push(` ${groupId} --> ${moreId}`);
4523
+ }
4524
+ }
4525
+ return lines.join('\n');
4526
+ }
4527
+ function groupGeneratedFrontendFiles(files) {
4528
+ const groups = new Map();
4529
+ for (const file of files.slice().sort()) {
4530
+ const normalized = normalizePath(file);
4531
+ const group = normalized.startsWith('pages/')
4532
+ ? 'pages'
4533
+ : normalized.startsWith('models/')
4534
+ ? 'models'
4535
+ : normalized.startsWith('layout/')
4536
+ ? 'layout'
4537
+ : normalized.startsWith('views/')
4538
+ ? 'views'
4539
+ : normalized.startsWith('styles/')
4540
+ ? 'styles'
4541
+ : normalized.startsWith('frontend/')
4542
+ ? 'host-files'
4543
+ : normalized.startsWith('components/')
4544
+ ? 'components'
4545
+ : normalized === 'App.tsx' || normalized === 'router.tsx' || normalized === 'GENERATED.md'
4546
+ ? 'app-shell'
4547
+ : 'other';
4548
+ const entries = groups.get(group) ?? [];
4549
+ entries.push(normalized);
4550
+ groups.set(group, entries);
4551
+ }
4552
+ return groups;
4553
+ }
4554
+ function groupGeneratedBackendFiles(files) {
4555
+ const groups = new Map();
4556
+ for (const file of files.slice().sort()) {
4557
+ const normalized = normalizePath(file);
4558
+ const group = normalized.includes('/controller/') || normalized.startsWith('app/routes/') || normalized.startsWith('app/routers/')
4559
+ ? 'routes-controllers'
4560
+ : normalized.includes('/service/') || normalized.startsWith('app/services/')
4561
+ ? 'services'
4562
+ : normalized.includes('/model/') || normalized.includes('/entity/') || normalized.startsWith('app/models/')
4563
+ ? 'models'
4564
+ : normalized.includes('/dto/') || normalized.startsWith('app/dto/')
4565
+ ? 'dto'
4566
+ : normalized.includes('/workflow/') || normalized.startsWith('app/workflow/')
4567
+ ? 'workflow'
4568
+ : normalized.includes('/rules/') || normalized.startsWith('app/rules/') || normalized.startsWith('app/custom/rules/')
4569
+ ? 'rules'
4570
+ : normalized.startsWith('app/custom/read_models/') || normalized.includes('/readmodel/')
4571
+ ? 'read-models'
4572
+ : 'support';
4573
+ const entries = groups.get(group) ?? [];
4574
+ entries.push(normalized);
4575
+ groups.set(group, entries);
4576
+ }
4577
+ return groups;
4578
+ }
4579
+ function mermaidNodeId(value) {
4580
+ return value.replace(/[^A-Za-z0-9_]/g, '_');
4581
+ }
4582
+ function mermaidLabel(value) {
4583
+ return value.replace(/"/g, '#quot;');
4584
+ }
4585
+ function toNamedList(value) {
4586
+ return Array.isArray(value)
4587
+ ? value.filter((entry) => isRecord(entry) && typeof entry.name === 'string')
4588
+ : [];
4589
+ }
4099
4590
  function validateDatabaseConfigForTargetRuntime(target, targetTriple) {
4100
4591
  if (!target.database || !targetTriple) {
4101
4592
  return null;
@@ -5280,6 +5771,7 @@ function writeUsage(write) {
5280
5771
  ' loj validate <loj.project.yaml> [--json]',
5281
5772
  ' loj build <loj.project.yaml> [--json]',
5282
5773
  ' loj dev <loj.project.yaml> [--target <alias>] [--debug] [--json]',
5774
+ ' loj graph <loj.project.yaml> [--surface source|frontend|backend|all] [--target <alias>] [--out-dir <dir>] [--json]',
5283
5775
  ' loj rebuild <loj.project.yaml> [--target <alias>] [--json]',
5284
5776
  ' loj restart <loj.project.yaml> [--service host|server|all] [--json]',
5285
5777
  ' loj status <loj.project.yaml> [--target <alias>] [--json]',
@@ -5296,6 +5788,7 @@ function writeUsage(write) {
5296
5788
  'Common project-shell loop:',
5297
5789
  ' loj doctor <loj.project.yaml> # validate dependencies, generated outputs, and linked artifacts',
5298
5790
  ' loj dev <loj.project.yaml> # watch the project and run managed host/backend services',
5791
+ ' loj graph <loj.project.yaml> # emit mermaid architecture views for source and generated targets',
5299
5792
  ' loj rebuild <loj.project.yaml> # rebuild selected targets without restarting the whole loop',
5300
5793
  ' loj restart <loj.project.yaml> # restart host/server processes inside the active loop',
5301
5794
  ' loj status <loj.project.yaml> # inspect current URLs, probes, services, and debugger endpoints',