yarramate 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 (76) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +174 -0
  3. package/dist/adapter-mapping.d.ts +40 -0
  4. package/dist/adapter-mapping.js +200 -0
  5. package/dist/adapters/graphify-cli.d.ts +3 -0
  6. package/dist/adapters/graphify-cli.js +127 -0
  7. package/dist/adapters/graphify-entry.d.ts +1 -0
  8. package/dist/adapters/graphify-entry.js +1 -0
  9. package/dist/adapters/graphify.d.ts +23 -0
  10. package/dist/adapters/graphify.js +47 -0
  11. package/dist/adapters/likec4-cli.d.ts +3 -0
  12. package/dist/adapters/likec4-cli.js +662 -0
  13. package/dist/adapters/likec4-export.d.ts +25 -0
  14. package/dist/adapters/likec4-export.js +204 -0
  15. package/dist/adapters/likec4-kind-mapping.d.ts +22 -0
  16. package/dist/adapters/likec4-kind-mapping.js +60 -0
  17. package/dist/adapters/likec4-prepare.d.ts +28 -0
  18. package/dist/adapters/likec4-prepare.js +154 -0
  19. package/dist/adapters/likec4-project.d.ts +58 -0
  20. package/dist/adapters/likec4-project.js +176 -0
  21. package/dist/adapters/likec4.d.ts +4 -0
  22. package/dist/adapters/likec4.js +4 -0
  23. package/dist/architecture-state.d.ts +22 -0
  24. package/dist/architecture-state.js +63 -0
  25. package/dist/check-command.d.ts +2 -0
  26. package/dist/check-command.js +232 -0
  27. package/dist/cli-support.d.ts +24 -0
  28. package/dist/cli-support.js +81 -0
  29. package/dist/cli.d.ts +4 -0
  30. package/dist/cli.js +575 -0
  31. package/dist/compiler.d.ts +66 -0
  32. package/dist/compiler.js +940 -0
  33. package/dist/core-contract.d.ts +43 -0
  34. package/dist/core-contract.js +162 -0
  35. package/dist/evidence.d.ts +58 -0
  36. package/dist/evidence.js +161 -0
  37. package/dist/graph.d.ts +2 -0
  38. package/dist/graph.js +37 -0
  39. package/dist/index.d.ts +11 -0
  40. package/dist/index.js +9 -0
  41. package/dist/profile.d.ts +30 -0
  42. package/dist/profile.js +162 -0
  43. package/dist/projection.d.ts +42 -0
  44. package/dist/projection.js +183 -0
  45. package/dist/reconciliation.d.ts +26 -0
  46. package/dist/reconciliation.js +47 -0
  47. package/dist/source-document.d.ts +24 -0
  48. package/dist/source-document.js +80 -0
  49. package/dist/workspace.d.ts +29 -0
  50. package/dist/workspace.js +114 -0
  51. package/docs/CONSUMING-YARRAMATE.md +145 -0
  52. package/package.json +110 -0
  53. package/schema/yarramate-adapter-mapping.schema.json +59 -0
  54. package/schema/yarramate-check-result.schema.json +92 -0
  55. package/schema/yarramate-core-contract.schema.json +132 -0
  56. package/schema/yarramate-diagnostic-result.schema.json +64 -0
  57. package/schema/yarramate-document.schema.json +168 -0
  58. package/schema/yarramate-evidence-report.schema.json +73 -0
  59. package/schema/yarramate-evidence.schema.json +92 -0
  60. package/schema/yarramate-graph-v2.schema.json +170 -0
  61. package/schema/yarramate-likec4-check-result.schema.json +50 -0
  62. package/schema/yarramate-likec4-diagnostic-result.schema.json +69 -0
  63. package/schema/yarramate-likec4-generated-project-v2.schema.json +100 -0
  64. package/schema/yarramate-likec4-generated-project.schema.json +76 -0
  65. package/schema/yarramate-likec4-kind-mapping.schema.json +65 -0
  66. package/schema/yarramate-likec4-project.schema.json +160 -0
  67. package/schema/yarramate-profile.schema.json +113 -0
  68. package/schema/yarramate-projection-result.schema.json +177 -0
  69. package/schema/yarramate-projection.schema.json +126 -0
  70. package/schema/yarramate-reconciliation-report.schema.json +90 -0
  71. package/schema/yarramate-state-comparison.schema.json +61 -0
  72. package/schema/yarramate-workspace.schema.json +55 -0
  73. package/skills/yarramate-architecture/SKILL.md +135 -0
  74. package/skills/yarramate-architecture/agents/openai.yaml +4 -0
  75. package/skills/yarramate-architecture/references/journey-checklists.md +57 -0
  76. package/skills/yarramate-architecture/references/native-authoring.md +167 -0
@@ -0,0 +1,662 @@
1
+ #!/usr/bin/env node
2
+ import { closeSync, existsSync, lstatSync, mkdirSync, openSync, readFileSync, renameSync, unlinkSync, writeFileSync, } from 'node:fs';
3
+ import { resolve } from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+ import { createHash, randomUUID } from 'node:crypto';
6
+ import Ajv2020Module from 'ajv/dist/2020.js';
7
+ import { parseDocument } from 'yaml';
8
+ import { isMainModule, resolveCliWorkspaceSources, } from '../cli-support.js';
9
+ import { locateSourcePath } from '../source-document.js';
10
+ import { prepareLikeC4Export, } from './likec4-prepare.js';
11
+ import { exportLikeC4Project, loadLikeC4ProjectDefinition, } from './likec4-project.js';
12
+ import generatedProjectSchema from '../../schema/yarramate-likec4-generated-project.schema.json' with {
13
+ type: 'json'
14
+ };
15
+ import generatedProjectV2Schema from '../../schema/yarramate-likec4-generated-project-v2.schema.json' with {
16
+ type: 'json'
17
+ };
18
+ const Ajv2020 = Ajv2020Module.default;
19
+ const validateGeneratedProjectMarker = new Ajv2020({
20
+ allErrors: true,
21
+ }).compile(generatedProjectSchema);
22
+ const validateGeneratedProjectV2Marker = new Ajv2020({
23
+ allErrors: true,
24
+ }).compile(generatedProjectV2Schema);
25
+ const generatedFileNames = [
26
+ 'likec4.config.json',
27
+ 'model.likec4',
28
+ 'specification.likec4',
29
+ ];
30
+ const sha256 = (value) => createHash('sha256').update(value).digest('hex');
31
+ const stageFile = (destination, value) => {
32
+ const staged = `${destination}.yarramate-${randomUUID()}.tmp`;
33
+ const descriptor = openSync(staged, 'wx', 0o666);
34
+ let written = false;
35
+ try {
36
+ writeFileSync(descriptor, value);
37
+ written = true;
38
+ }
39
+ finally {
40
+ closeSync(descriptor);
41
+ if (!written && existsSync(staged))
42
+ unlinkSync(staged);
43
+ }
44
+ return {
45
+ publish: () => renameSync(staged, destination),
46
+ cleanup: () => {
47
+ if (existsSync(staged))
48
+ unlinkSync(staged);
49
+ },
50
+ };
51
+ };
52
+ const publishFiles = (files) => {
53
+ const stagedFiles = [];
54
+ try {
55
+ for (const file of files) {
56
+ stagedFiles.push(stageFile(file.destination, file.value));
57
+ }
58
+ }
59
+ catch (error) {
60
+ for (const staged of stagedFiles)
61
+ staged.cleanup();
62
+ throw error;
63
+ }
64
+ try {
65
+ for (const staged of stagedFiles)
66
+ staged.publish();
67
+ }
68
+ finally {
69
+ for (const staged of stagedFiles)
70
+ staged.cleanup();
71
+ }
72
+ };
73
+ const usage = 'Usage:\n' +
74
+ ' yarramate-likec4 check <projection.yaml> <mapping.yaml> [--json] [--kinds <kind-mapping.yaml>] [--compare <from-state> <to-state>] <workspace-or-source...>\n' +
75
+ ' yarramate-likec4 check <likec4-project.yaml> [--json] <workspace-or-source...>\n' +
76
+ ' yarramate-likec4 export <projection.yaml> <mapping.yaml> [--kinds <kind-mapping.yaml>] [--compare <from-state> <to-state>] <workspace-or-source...>\n' +
77
+ ' yarramate-likec4 export-project <projection.yaml> <mapping.yaml> <output-dir> [--kinds <kind-mapping.yaml>] [--compare <from-state> <to-state>] <workspace-or-source...>\n' +
78
+ ' yarramate-likec4 export-project <likec4-project.yaml> <output-dir> <workspace-or-source...>\n';
79
+ const diagnosticJson = (diagnostics) => `${JSON.stringify({
80
+ format: 'yarramate/likec4-diagnostic-result/v1',
81
+ diagnostics,
82
+ }, null, 2)}\n`;
83
+ const checkJson = (ok, diagnostics) => `${JSON.stringify({
84
+ format: 'yarramate/likec4-check-result/v1',
85
+ ok,
86
+ diagnostics,
87
+ }, null, 2)}\n`;
88
+ const sameJson = (left, right) => JSON.stringify(left) === JSON.stringify(right);
89
+ const publishGeneratedProject = (cwd, outputDirectory, input) => {
90
+ const projectPath = resolve(cwd, outputDirectory);
91
+ const markerPath = resolve(projectPath, 'yarramate.generated.json');
92
+ const updating = existsSync(projectPath);
93
+ if (updating) {
94
+ const projectStat = lstatSync(projectPath);
95
+ if (projectStat.isSymbolicLink() || !projectStat.isDirectory()) {
96
+ return {
97
+ exitCode: 2,
98
+ stdout: '',
99
+ stderr: `Output directory already exists: ${projectPath}\n`,
100
+ };
101
+ }
102
+ const unsafeFile = [
103
+ markerPath,
104
+ ...generatedFileNames.map((file) => resolve(projectPath, file)),
105
+ ].find((path) => {
106
+ if (!existsSync(path))
107
+ return false;
108
+ const stat = lstatSync(path);
109
+ return stat.isSymbolicLink() || !stat.isFile();
110
+ });
111
+ if (unsafeFile !== undefined) {
112
+ return {
113
+ exitCode: 2,
114
+ stdout: '',
115
+ stderr: `Generated project contains an unsafe file: ${unsafeFile}\n`,
116
+ };
117
+ }
118
+ let marker;
119
+ try {
120
+ marker = JSON.parse(readFileSync(markerPath, 'utf8'));
121
+ }
122
+ catch {
123
+ marker = undefined;
124
+ }
125
+ if (!input.validateMarker(marker) ||
126
+ typeof marker !== 'object' ||
127
+ marker === null ||
128
+ Object.entries(input.ownership).some(([key, value]) => !sameJson(marker[key], value))) {
129
+ return {
130
+ exitCode: 2,
131
+ stdout: '',
132
+ stderr: `Output directory already exists: ${projectPath}\n`,
133
+ };
134
+ }
135
+ if ('digests' in marker && marker.digests !== undefined) {
136
+ const digests = marker.digests;
137
+ const changedFile = generatedFileNames.find((file) => {
138
+ const path = resolve(projectPath, file);
139
+ return (!existsSync(path) ||
140
+ sha256(readFileSync(path)) !== digests[file]);
141
+ });
142
+ if (changedFile !== undefined) {
143
+ return {
144
+ exitCode: 2,
145
+ stdout: '',
146
+ stderr: `Generated project file has changed: ${resolve(projectPath, changedFile)}\n`,
147
+ };
148
+ }
149
+ }
150
+ }
151
+ else {
152
+ mkdirSync(projectPath, { recursive: true });
153
+ }
154
+ const specificationSource = readFileSync(fileURLToPath(new URL('../../.yarramate/integrations/likec4/prototype/specification.likec4', import.meta.url)));
155
+ const configSource = `${JSON.stringify({
156
+ $schema: 'https://likec4.dev/schemas/config.json',
157
+ name: input.projectName,
158
+ title: input.title,
159
+ }, null, 2)}\n`;
160
+ const markerSource = `${JSON.stringify({
161
+ ...input.ownership,
162
+ files: [
163
+ 'likec4.config.json',
164
+ 'model.likec4',
165
+ 'specification.likec4',
166
+ ],
167
+ digests: {
168
+ 'likec4.config.json': sha256(configSource),
169
+ 'model.likec4': sha256(input.modelSource),
170
+ 'specification.likec4': sha256(specificationSource),
171
+ },
172
+ }, null, 2)}\n`;
173
+ publishFiles([
174
+ {
175
+ destination: resolve(projectPath, 'likec4.config.json'),
176
+ value: configSource,
177
+ },
178
+ {
179
+ destination: resolve(projectPath, 'model.likec4'),
180
+ value: input.modelSource,
181
+ },
182
+ {
183
+ destination: resolve(projectPath, 'specification.likec4'),
184
+ value: specificationSource,
185
+ },
186
+ { destination: markerPath, value: markerSource },
187
+ ]);
188
+ return {
189
+ exitCode: 0,
190
+ stdout: updating
191
+ ? `Updated LikeC4 project at ${projectPath}\n`
192
+ : `Wrote LikeC4 project to ${projectPath}\n`,
193
+ stderr: '',
194
+ };
195
+ };
196
+ export function runLikeC4Cli(args, cwd = process.cwd()) {
197
+ const [command, projectionPath, mappingPath, ...options] = args;
198
+ let projectDefinitionMode = false;
199
+ if ((command === 'check' || command === 'export-project') &&
200
+ projectionPath !== undefined) {
201
+ try {
202
+ projectDefinitionMode =
203
+ parseDocument(readFileSync(resolve(cwd, projectionPath), 'utf8')).get('format') === 'yarramate/likec4-project/v1';
204
+ }
205
+ catch {
206
+ projectDefinitionMode = false;
207
+ }
208
+ }
209
+ const outputDirectory = command === 'export-project'
210
+ ? projectDefinitionMode
211
+ ? mappingPath
212
+ : options[0]
213
+ : undefined;
214
+ const sourceOptions = projectDefinitionMode && command === 'check'
215
+ ? [mappingPath, ...options].filter((value) => value !== undefined)
216
+ : command === 'export-project'
217
+ ? projectDefinitionMode
218
+ ? options
219
+ : options.slice(1)
220
+ : options;
221
+ const json = command === 'check' && sourceOptions.includes('--json');
222
+ let invalidOptions = false;
223
+ let kindMappingPath;
224
+ let comparison;
225
+ const sourcePaths = [];
226
+ for (let index = 0; index < sourceOptions.length; index += 1) {
227
+ const option = sourceOptions[index];
228
+ if (option === '--json') {
229
+ if (command !== 'check')
230
+ invalidOptions = true;
231
+ continue;
232
+ }
233
+ if (option === '--kinds') {
234
+ const value = sourceOptions[index + 1];
235
+ if (value === undefined ||
236
+ value.startsWith('-') ||
237
+ kindMappingPath !== undefined) {
238
+ invalidOptions = true;
239
+ }
240
+ else {
241
+ kindMappingPath = value;
242
+ index += 1;
243
+ }
244
+ continue;
245
+ }
246
+ if (option === '--compare') {
247
+ const from = sourceOptions[index + 1];
248
+ const to = sourceOptions[index + 2];
249
+ if (from === undefined ||
250
+ to === undefined ||
251
+ from.startsWith('-') ||
252
+ to.startsWith('-') ||
253
+ comparison !== undefined) {
254
+ invalidOptions = true;
255
+ }
256
+ else {
257
+ comparison = { from, to };
258
+ index += 2;
259
+ }
260
+ continue;
261
+ }
262
+ if (option.startsWith('-'))
263
+ invalidOptions = true;
264
+ else
265
+ sourcePaths.push(option);
266
+ }
267
+ if ((command !== 'check' &&
268
+ command !== 'export' &&
269
+ command !== 'export-project') ||
270
+ projectionPath === undefined ||
271
+ (!projectDefinitionMode && mappingPath === undefined) ||
272
+ (projectDefinitionMode &&
273
+ command === 'export-project' &&
274
+ mappingPath === undefined) ||
275
+ (command === 'export-project' && outputDirectory === undefined) ||
276
+ (projectDefinitionMode &&
277
+ (kindMappingPath !== undefined || comparison !== undefined)) ||
278
+ invalidOptions ||
279
+ sourcePaths.length === 0 ||
280
+ sourcePaths.some((argument) => argument.startsWith('-'))) {
281
+ return { exitCode: 2, stdout: '', stderr: usage };
282
+ }
283
+ const diagnosticOutput = (diagnostics) => (json ? checkJson(false, diagnostics) : diagnosticJson(diagnostics));
284
+ try {
285
+ const resolved = resolveCliWorkspaceSources(sourcePaths, cwd);
286
+ if (!resolved.ok) {
287
+ return {
288
+ exitCode: 1,
289
+ stdout: diagnosticOutput(resolved.diagnostics),
290
+ stderr: '',
291
+ };
292
+ }
293
+ const sources = resolved.paths.map((path) => ({
294
+ path,
295
+ source: readFileSync(resolve(cwd, path), 'utf8'),
296
+ }));
297
+ if (projectDefinitionMode) {
298
+ const projectSource = {
299
+ path: projectionPath,
300
+ source: readFileSync(resolve(cwd, projectionPath), 'utf8'),
301
+ };
302
+ const loadedProject = loadLikeC4ProjectDefinition(projectSource);
303
+ if (!loadedProject.ok) {
304
+ return {
305
+ exitCode: 1,
306
+ stdout: diagnosticOutput(loadedProject.diagnostics),
307
+ stderr: '',
308
+ };
309
+ }
310
+ const preparedViews = loadedProject.document.value.views.map((view) => ({
311
+ view,
312
+ prepared: prepareLikeC4Export({
313
+ sources,
314
+ projection: {
315
+ path: view.projection,
316
+ source: readFileSync(resolve(cwd, view.projection), 'utf8'),
317
+ },
318
+ subjectMapping: {
319
+ path: loadedProject.document.value.mapping,
320
+ source: readFileSync(resolve(cwd, loadedProject.document.value.mapping), 'utf8'),
321
+ },
322
+ ...(loadedProject.document.value.kindMapping === undefined
323
+ ? {}
324
+ : {
325
+ kindMapping: {
326
+ path: loadedProject.document.value.kindMapping,
327
+ source: readFileSync(resolve(cwd, loadedProject.document.value.kindMapping), 'utf8'),
328
+ },
329
+ }),
330
+ ...(view.compare === undefined
331
+ ? {}
332
+ : { comparison: view.compare }),
333
+ vocabulary: 'bundled',
334
+ }),
335
+ }));
336
+ const failed = preparedViews.find(({ prepared }) => !prepared.ok);
337
+ if (failed !== undefined && !failed.prepared.ok) {
338
+ return {
339
+ exitCode: 1,
340
+ stdout: diagnosticOutput(failed.prepared.diagnostics),
341
+ stderr: '',
342
+ };
343
+ }
344
+ const successfulViews = preparedViews.flatMap(({ view, prepared }) => prepared.ok
345
+ ? [
346
+ {
347
+ ...(view.id === undefined ? {} : { id: view.id }),
348
+ prepared,
349
+ ...(view.compare === undefined
350
+ ? {}
351
+ : { comparison: view.compare }),
352
+ ...(view.dynamic === undefined
353
+ ? {}
354
+ : { dynamic: view.dynamic }),
355
+ ...(view.deployment === undefined
356
+ ? {}
357
+ : { deployment: view.deployment }),
358
+ },
359
+ ]
360
+ : []);
361
+ const renderedViewIds = new Set();
362
+ const deploymentIdentities = new Set();
363
+ for (const [index, view] of successfulViews.entries()) {
364
+ const deployment = view.deployment;
365
+ if (deployment !== undefined) {
366
+ const nodeIds = new Set();
367
+ for (const [nodeIndex, node] of deployment.nodes.entries()) {
368
+ const problem = nodeIds.has(node.id)
369
+ ? `Deployment node "${node.id}" is duplicated`
370
+ : deploymentIdentities.has(node.id)
371
+ ? `Deployment identity "${node.id}" is duplicated`
372
+ : node.parent === node.id
373
+ ? `Deployment node "${node.id}" cannot parent itself`
374
+ : undefined;
375
+ if (problem !== undefined) {
376
+ const field = nodeIds.has(node.id) || deploymentIdentities.has(node.id)
377
+ ? 'id'
378
+ : 'parent';
379
+ const pointer = `/views/${index}/deployment/nodes/${nodeIndex}/${field}`;
380
+ const location = locateSourcePath(projectSource.path, loadedProject.document.yaml, loadedProject.document.lineCounter, ['views', index, 'deployment', 'nodes', nodeIndex, field], pointer);
381
+ return {
382
+ exitCode: 1,
383
+ stdout: diagnosticOutput([{
384
+ severity: 'error',
385
+ code: 'YMLC109',
386
+ message: problem,
387
+ ...location,
388
+ }]),
389
+ stderr: '',
390
+ };
391
+ }
392
+ nodeIds.add(node.id);
393
+ deploymentIdentities.add(node.id);
394
+ }
395
+ for (const [nodeIndex, node] of deployment.nodes.entries()) {
396
+ if (node.parent !== undefined &&
397
+ !nodeIds.has(node.parent)) {
398
+ const pointer = `/views/${index}/deployment/nodes/${nodeIndex}/parent`;
399
+ const location = locateSourcePath(projectSource.path, loadedProject.document.yaml, loadedProject.document.lineCounter, [
400
+ 'views',
401
+ index,
402
+ 'deployment',
403
+ 'nodes',
404
+ nodeIndex,
405
+ 'parent',
406
+ ], pointer);
407
+ return {
408
+ exitCode: 1,
409
+ stdout: diagnosticOutput([{
410
+ severity: 'error',
411
+ code: 'YMLC109',
412
+ message: `Deployment parent "${node.parent}" does not exist`,
413
+ ...location,
414
+ }]),
415
+ stderr: '',
416
+ };
417
+ }
418
+ const nodeById = new Map(deployment.nodes.map((candidate) => [
419
+ candidate.id,
420
+ candidate,
421
+ ]));
422
+ const ancestors = new Set([node.id]);
423
+ let parent = node.parent;
424
+ while (parent !== undefined) {
425
+ if (ancestors.has(parent)) {
426
+ const pointer = `/views/${index}/deployment/nodes/${nodeIndex}/parent`;
427
+ const location = locateSourcePath(projectSource.path, loadedProject.document.yaml, loadedProject.document.lineCounter, [
428
+ 'views',
429
+ index,
430
+ 'deployment',
431
+ 'nodes',
432
+ nodeIndex,
433
+ 'parent',
434
+ ], pointer);
435
+ return {
436
+ exitCode: 1,
437
+ stdout: diagnosticOutput([{
438
+ severity: 'error',
439
+ code: 'YMLC109',
440
+ message: `Deployment node "${node.id}" participates in a parent cycle`,
441
+ ...location,
442
+ }]),
443
+ stderr: '',
444
+ };
445
+ }
446
+ ancestors.add(parent);
447
+ parent = nodeById.get(parent)?.parent;
448
+ }
449
+ }
450
+ const instanceIds = new Set();
451
+ for (const [instanceIndex, instance] of (deployment.instances).entries()) {
452
+ const projected = view.prepared.projection.subjects.find(({ id }) => id === instance.subject);
453
+ const problem = instanceIds.has(instance.id)
454
+ ? `Deployment instance "${instance.id}" is duplicated`
455
+ : deploymentIdentities.has(instance.id)
456
+ ? `Deployment identity "${instance.id}" is duplicated`
457
+ : !nodeIds.has(instance.node)
458
+ ? `Deployment instance node "${instance.node}" does not exist`
459
+ : projected?.type !== 'concept'
460
+ ? `Deployment instance subject "${instance.subject}" is not selected as a concept by its projection`
461
+ : undefined;
462
+ if (problem !== undefined) {
463
+ const field = instanceIds.has(instance.id) ||
464
+ deploymentIdentities.has(instance.id)
465
+ ? 'id'
466
+ : !nodeIds.has(instance.node)
467
+ ? 'node'
468
+ : 'subject';
469
+ const pointer = `/views/${index}/deployment/instances/${instanceIndex}/${field}`;
470
+ const location = locateSourcePath(projectSource.path, loadedProject.document.yaml, loadedProject.document.lineCounter, [
471
+ 'views',
472
+ index,
473
+ 'deployment',
474
+ 'instances',
475
+ instanceIndex,
476
+ field,
477
+ ], pointer);
478
+ return {
479
+ exitCode: 1,
480
+ stdout: diagnosticOutput([{
481
+ severity: 'error',
482
+ code: 'YMLC109',
483
+ message: problem,
484
+ ...location,
485
+ }]),
486
+ stderr: '',
487
+ };
488
+ }
489
+ instanceIds.add(instance.id);
490
+ deploymentIdentities.add(instance.id);
491
+ }
492
+ }
493
+ for (const [stepIndex, step] of (view.dynamic?.steps ?? []).entries()) {
494
+ const projected = view.prepared.projection.subjects.find(({ id }) => id === step.relationship);
495
+ if (projected?.type !== 'relationship') {
496
+ const pointer = `/views/${index}/dynamic/steps/${stepIndex}/relationship`;
497
+ const location = locateSourcePath(projectSource.path, loadedProject.document.yaml, loadedProject.document.lineCounter, [
498
+ 'views',
499
+ index,
500
+ 'dynamic',
501
+ 'steps',
502
+ stepIndex,
503
+ 'relationship',
504
+ ], pointer);
505
+ return {
506
+ exitCode: 1,
507
+ stdout: diagnosticOutput([
508
+ {
509
+ severity: 'error',
510
+ code: 'YMLC108',
511
+ message: `Dynamic step relationship "${step.relationship}" is not selected as a relationship by its projection`,
512
+ ...location,
513
+ },
514
+ ]),
515
+ stderr: '',
516
+ };
517
+ }
518
+ }
519
+ const renderedId = view.id ?? view.prepared.projection.projection.split('@')[0];
520
+ if (renderedViewIds.has(renderedId)) {
521
+ const field = view.id === undefined ? 'projection' : 'id';
522
+ const pointer = `/views/${index}/${field}`;
523
+ const location = locateSourcePath(projectSource.path, loadedProject.document.yaml, loadedProject.document.lineCounter, ['views', index, field], pointer);
524
+ return {
525
+ exitCode: 1,
526
+ stdout: diagnosticOutput([
527
+ {
528
+ severity: 'error',
529
+ code: 'YMLC107',
530
+ message: `LikeC4 view identity "${renderedId}" is duplicated`,
531
+ ...location,
532
+ },
533
+ ]),
534
+ stderr: '',
535
+ };
536
+ }
537
+ renderedViewIds.add(renderedId);
538
+ }
539
+ const exported = exportLikeC4Project(loadedProject.document.value, successfulViews);
540
+ if (!exported.ok) {
541
+ return {
542
+ exitCode: 1,
543
+ stdout: diagnosticOutput(exported.diagnostics),
544
+ stderr: '',
545
+ };
546
+ }
547
+ if (command === 'check') {
548
+ const identity = `${loadedProject.document.value.id}@${loadedProject.document.value.version}`;
549
+ return {
550
+ exitCode: 0,
551
+ stdout: json
552
+ ? checkJson(true, [])
553
+ : `Checked LikeC4 project ${identity}: no errors\n`,
554
+ stderr: '',
555
+ };
556
+ }
557
+ const first = successfulViews[0];
558
+ const mappingIdentity = `${first.prepared.subjectMapping.id}@${first.prepared.subjectMapping.version}`;
559
+ const kindMappingIdentity = first.prepared.kindMapping === undefined
560
+ ? undefined
561
+ : `${first.prepared.kindMapping.id}@${first.prepared.kindMapping.version}`;
562
+ const projectIdentity = `${loadedProject.document.value.id}@${loadedProject.document.value.version}`;
563
+ return publishGeneratedProject(cwd, outputDirectory, {
564
+ projectName: `yarramate-${projectIdentity}`.replaceAll(/[^A-Za-z0-9_-]/g, '-'),
565
+ title: loadedProject.document.value.title,
566
+ modelSource: exported.source,
567
+ ownership: {
568
+ format: 'yarramate/likec4-generated-project/v2',
569
+ project: projectIdentity,
570
+ mapping: mappingIdentity,
571
+ ...(kindMappingIdentity === undefined
572
+ ? {}
573
+ : { kindMapping: kindMappingIdentity }),
574
+ views: successfulViews.map(({ id, prepared, comparison }) => ({
575
+ ...(id === undefined ? {} : { id }),
576
+ projection: prepared.projection.projection,
577
+ ...(comparison === undefined
578
+ ? {}
579
+ : { comparison }),
580
+ })),
581
+ },
582
+ validateMarker: (value) => validateGeneratedProjectV2Marker(value),
583
+ });
584
+ }
585
+ const prepared = prepareLikeC4Export({
586
+ sources,
587
+ projection: {
588
+ path: projectionPath,
589
+ source: readFileSync(resolve(cwd, projectionPath), 'utf8'),
590
+ },
591
+ subjectMapping: {
592
+ path: mappingPath,
593
+ source: readFileSync(resolve(cwd, mappingPath), 'utf8'),
594
+ },
595
+ ...(kindMappingPath === undefined
596
+ ? {}
597
+ : {
598
+ kindMapping: {
599
+ path: kindMappingPath,
600
+ source: readFileSync(resolve(cwd, kindMappingPath), 'utf8'),
601
+ },
602
+ }),
603
+ ...(comparison === undefined ? {} : { comparison }),
604
+ vocabulary: command === 'export' ? 'consumer' : 'bundled',
605
+ });
606
+ if (!prepared.ok) {
607
+ return {
608
+ exitCode: 1,
609
+ stdout: diagnosticOutput(prepared.diagnostics),
610
+ stderr: '',
611
+ };
612
+ }
613
+ if (command === 'check') {
614
+ return {
615
+ exitCode: 0,
616
+ stdout: json
617
+ ? checkJson(true, [])
618
+ : `Checked LikeC4 export ${prepared.projection.projection}: no errors\n`,
619
+ stderr: '',
620
+ };
621
+ }
622
+ if (command === 'export') {
623
+ return { exitCode: 0, stdout: prepared.source, stderr: '' };
624
+ }
625
+ const projectionIdentity = prepared.projection.projection;
626
+ const mappingIdentity = `${prepared.subjectMapping.id}@${prepared.subjectMapping.version}`;
627
+ const kindMappingIdentity = prepared.kindMapping === undefined
628
+ ? undefined
629
+ : `${prepared.kindMapping.id}@${prepared.kindMapping.version}`;
630
+ const comparisonIdentity = comparison;
631
+ return publishGeneratedProject(cwd, outputDirectory, {
632
+ projectName: ['yarramate', projectionIdentity]
633
+ .join('-')
634
+ .replaceAll(/[^A-Za-z0-9_-]/g, '-'),
635
+ title: prepared.projection.presentation?.title ??
636
+ projectionIdentity.slice(0, projectionIdentity.lastIndexOf('@')),
637
+ modelSource: prepared.source,
638
+ ownership: {
639
+ format: 'yarramate/likec4-generated-project/v1',
640
+ projection: projectionIdentity,
641
+ mapping: mappingIdentity,
642
+ ...(kindMappingIdentity === undefined
643
+ ? {}
644
+ : { kindMapping: kindMappingIdentity }),
645
+ ...(comparisonIdentity === undefined
646
+ ? {}
647
+ : { comparison: comparisonIdentity }),
648
+ },
649
+ validateMarker: (value) => validateGeneratedProjectMarker(value),
650
+ });
651
+ }
652
+ catch (error) {
653
+ const message = error instanceof Error ? error.message : String(error);
654
+ return { exitCode: 2, stdout: '', stderr: `${message}\n` };
655
+ }
656
+ }
657
+ if (isMainModule(import.meta.url, process.argv[1])) {
658
+ const result = runLikeC4Cli(process.argv.slice(2));
659
+ process.stdout.write(result.stdout);
660
+ process.stderr.write(result.stderr);
661
+ process.exitCode = result.exitCode;
662
+ }
@@ -0,0 +1,25 @@
1
+ import { type AdapterMapping } from '../adapter-mapping.js';
2
+ import type { StateComparison } from '../architecture-state.js';
3
+ import type { ProjectionResult } from '../projection.js';
4
+ import type { LikeC4KindMapping } from './likec4-kind-mapping.js';
5
+ export interface LikeC4ExportDiagnostic {
6
+ readonly severity: 'error';
7
+ readonly code: 'YMLC101' | 'YMLC102' | 'YMLC103' | 'YMLC104' | 'YMLC105' | 'YMLC106' | 'YMLC107' | 'YMLC108' | 'YMLC109';
8
+ readonly message: string;
9
+ readonly subject?: string;
10
+ readonly path: string;
11
+ readonly pointer: string;
12
+ readonly line: number;
13
+ readonly column: number;
14
+ }
15
+ export type LikeC4ExportResult = {
16
+ readonly ok: true;
17
+ readonly source: string;
18
+ } | {
19
+ readonly ok: false;
20
+ readonly diagnostics: readonly LikeC4ExportDiagnostic[];
21
+ };
22
+ export interface LikeC4ExportOptions {
23
+ readonly comparison?: StateComparison;
24
+ }
25
+ export declare function exportLikeC4(projection: ProjectionResult, mapping: AdapterMapping, kindMapping?: LikeC4KindMapping, options?: LikeC4ExportOptions): LikeC4ExportResult;