yarramate 0.4.0 → 0.6.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.
package/README.md CHANGED
@@ -119,13 +119,15 @@ node dist/cli.js check .yarramate/workspace.yaml --json
119
119
  node dist/cli.js compile .yarramate/workspace.yaml
120
120
  node dist/cli.js context .yarramate/projections/context.yaml .yarramate/workspace.yaml
121
121
  node dist/cli.js view .yarramate/projections/context.yaml .yarramate/workspace.yaml
122
+ node dist/cli.js next .yarramate/projections/context.yaml .yarramate/workspace.yaml
122
123
  node dist/cli.js evidence .yarramate/evidence/repository.yaml .yarramate/workspace.yaml
123
124
  node dist/cli.js reconcile .yarramate/workspace.yaml
124
125
  ```
125
126
 
126
127
  `init` creates `.yarramate/architecture/main.yaml` and
127
128
  `.yarramate/workspace.yaml`. Commands accept explicit source documents or one
128
- explicit workspace manifest.
129
+ explicit workspace manifest. `check --strict` additionally fails when any
130
+ evidence observation contradicts the model, for gates that want one knob.
129
131
 
130
132
  For a local consumer test, create a package artifact:
131
133
 
@@ -3,7 +3,7 @@ import { readFileSync } from 'node:fs';
3
3
  import { resolve } from 'node:path';
4
4
  import { adapterMappingEntryLocation, adapterMappingLocation, loadAdapterMapping, validateAdapterMapping, } from '../adapter-mapping.js';
5
5
  import { compileWorkspace } from '../compiler.js';
6
- import { diagnosticJson, isMainModule, resolveCliWorkspaceSources, } from '../cli-support.js';
6
+ import { diagnosticJson, isMainModule, resolveCliWorkspaceSources, versionResult, } from '../cli-support.js';
7
7
  import { observeGraphify, } from './graphify.js';
8
8
  const usage = 'Usage:\n' +
9
9
  ' yarramate-graphify observe <graph.json> <mapping.yaml> <workspace-or-source...> --id <evidence-id> --version <major.minor>\n';
@@ -35,6 +35,9 @@ const parseOptions = (options) => {
35
35
  };
36
36
  export function runGraphifyCli(args, cwd = process.cwd()) {
37
37
  const [command, ...options] = args;
38
+ if (command === '--version') {
39
+ return versionResult('yarramate-graphify');
40
+ }
38
41
  const parsed = command === 'observe' ? parseOptions(options) : undefined;
39
42
  if (parsed === undefined ||
40
43
  !/^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/.test(parsed.id) ||
@@ -1,11 +1,11 @@
1
1
  #!/usr/bin/env node
2
2
  import { closeSync, existsSync, lstatSync, mkdirSync, openSync, readFileSync, renameSync, unlinkSync, writeFileSync, } from 'node:fs';
3
- import { resolve } from 'node:path';
3
+ import { dirname, resolve } from 'node:path';
4
4
  import { fileURLToPath } from 'node:url';
5
5
  import { createHash, randomUUID } from 'node:crypto';
6
6
  import Ajv2020Module from 'ajv/dist/2020.js';
7
7
  import { isMap, isSeq, parseDocument } from 'yaml';
8
- import { isMainModule, resolveCliWorkspaceSources, } from '../cli-support.js';
8
+ import { isMainModule, resolveCliWorkspaceSources, versionResult, } from '../cli-support.js';
9
9
  import { compileWorkspace } from '../compiler.js';
10
10
  import { adapterMappingLocation, loadAdapterMapping, validateAdapterMapping, } from '../adapter-mapping.js';
11
11
  import { locateSourcePath } from '../source-document.js';
@@ -77,8 +77,8 @@ const usage = 'Usage:\n' +
77
77
  ' yarramate-likec4 check <projection.yaml> <mapping.yaml> [--json] [--kinds <kind-mapping.yaml>] [--compare <from-state> <to-state>] <workspace-or-source...>\n' +
78
78
  ' yarramate-likec4 check <likec4-project.yaml> [--json] <workspace-or-source...>\n' +
79
79
  ' yarramate-likec4 export <projection.yaml> <mapping.yaml> [--kinds <kind-mapping.yaml>] [--compare <from-state> <to-state>] <workspace-or-source...>\n' +
80
- ' yarramate-likec4 export-project <projection.yaml> <mapping.yaml> <output-dir> [--kinds <kind-mapping.yaml>] [--compare <from-state> <to-state>] <workspace-or-source...>\n' +
81
- ' yarramate-likec4 export-project <likec4-project.yaml> <output-dir> <workspace-or-source...>\n';
80
+ ' yarramate-likec4 export-project [--check] <projection.yaml> <mapping.yaml> <output-dir> [--kinds <kind-mapping.yaml>] [--compare <from-state> <to-state>] <workspace-or-source...>\n' +
81
+ ' yarramate-likec4 export-project [--check] <likec4-project.yaml> <output-dir> <workspace-or-source...>\n';
82
82
  const diagnosticJson = (diagnostics) => `${JSON.stringify({
83
83
  format: 'yarramate/likec4-diagnostic-result/v1',
84
84
  diagnostics,
@@ -88,7 +88,36 @@ const checkJson = (ok, diagnostics) => `${JSON.stringify({
88
88
  ok,
89
89
  diagnostics,
90
90
  }, null, 2)}\n`;
91
- const sameJson = (left, right) => JSON.stringify(left) === JSON.stringify(right);
91
+ const unmappedSubjectPreviewLength = 3;
92
+ const summarizeUnmappedSubjects = (diagnostics) => [
93
+ ['YMLC102', 'concepts'],
94
+ ['YMLC111', 'relationships'],
95
+ ].reduce((current, [code, noun]) => {
96
+ const unmapped = current.filter((diagnostic) => diagnostic.code === code);
97
+ if (unmapped.length <= unmappedSubjectPreviewLength)
98
+ return current;
99
+ const preview = unmapped
100
+ .slice(0, unmappedSubjectPreviewLength)
101
+ .map((diagnostic) => 'subject' in diagnostic && diagnostic.subject !== undefined
102
+ ? `"${diagnostic.subject}"`
103
+ : `"${diagnostic.path}:${diagnostic.line}"`)
104
+ .join(', ');
105
+ const summary = {
106
+ ...unmapped[0],
107
+ message: `${unmapped.length} projected ${noun} have no LikeC4 mapping ` +
108
+ `(first: ${preview}); run "yarramate-likec4 map --sync" to add ` +
109
+ 'the missing mappings',
110
+ };
111
+ let summarized = false;
112
+ return current.flatMap((diagnostic) => {
113
+ if (diagnostic.code !== code)
114
+ return [diagnostic];
115
+ if (summarized)
116
+ return [];
117
+ summarized = true;
118
+ return [summary];
119
+ });
120
+ }, diagnostics);
92
121
  const lowerCamel = (value) => value.replaceAll(/-([a-z0-9])/g, (_, character) => character.toUpperCase());
93
122
  const runLikeC4MapSync = (args, cwd) => {
94
123
  const sync = args[0];
@@ -263,6 +292,33 @@ const runLikeC4MapSync = (args, cwd) => {
263
292
  return { exitCode: 2, stdout: '', stderr: `${message}\n` };
264
293
  }
265
294
  };
295
+ // Ownership fields never gate updates: a marker of either format version
296
+ // plus intact output digests proves the directory is entirely
297
+ // machine-generated, so regenerating after the inputs changed loses nothing.
298
+ const readGeneratedProjectMarker = (markerPath) => {
299
+ let marker;
300
+ try {
301
+ marker = JSON.parse(readFileSync(markerPath, 'utf8'));
302
+ }
303
+ catch {
304
+ return undefined;
305
+ }
306
+ return validateGeneratedProjectMarker(marker) ||
307
+ validateGeneratedProjectV2Marker(marker)
308
+ ? marker
309
+ : undefined;
310
+ };
311
+ const changedGeneratedFile = (projectPath, marker) => {
312
+ // Markers written before output digests existed are accepted once and
313
+ // upgraded on regeneration.
314
+ if (marker['digests'] === undefined)
315
+ return undefined;
316
+ const digests = marker['digests'];
317
+ return generatedFileNames.find((file) => {
318
+ const path = resolve(projectPath, file);
319
+ return (!existsSync(path) || sha256(readFileSync(path)) !== digests[file]);
320
+ });
321
+ };
266
322
  const publishGeneratedProject = (cwd, outputDirectory, input) => {
267
323
  const projectPath = resolve(cwd, outputDirectory);
268
324
  const markerPath = resolve(projectPath, 'yarramate.generated.json');
@@ -292,37 +348,21 @@ const publishGeneratedProject = (cwd, outputDirectory, input) => {
292
348
  stderr: `Generated project contains an unsafe file: ${unsafeFile}\n`,
293
349
  };
294
350
  }
295
- let marker;
296
- try {
297
- marker = JSON.parse(readFileSync(markerPath, 'utf8'));
298
- }
299
- catch {
300
- marker = undefined;
301
- }
302
- if (!input.validateMarker(marker) ||
303
- typeof marker !== 'object' ||
304
- marker === null ||
305
- Object.entries(input.ownership).some(([key, value]) => !sameJson(marker[key], value))) {
351
+ const marker = readGeneratedProjectMarker(markerPath);
352
+ if (marker === undefined) {
306
353
  return {
307
354
  exitCode: 2,
308
355
  stdout: '',
309
- stderr: `Output directory already exists: ${projectPath}\n`,
356
+ stderr: `Output directory exists but is not a YarraMate-generated project: ${projectPath}\n`,
310
357
  };
311
358
  }
312
- if ('digests' in marker && marker.digests !== undefined) {
313
- const digests = marker.digests;
314
- const changedFile = generatedFileNames.find((file) => {
315
- const path = resolve(projectPath, file);
316
- return (!existsSync(path) ||
317
- sha256(readFileSync(path)) !== digests[file]);
318
- });
319
- if (changedFile !== undefined) {
320
- return {
321
- exitCode: 2,
322
- stdout: '',
323
- stderr: `Generated project file has changed: ${resolve(projectPath, changedFile)}\n`,
324
- };
325
- }
359
+ const changedFile = changedGeneratedFile(projectPath, marker);
360
+ if (changedFile !== undefined) {
361
+ return {
362
+ exitCode: 2,
363
+ stdout: '',
364
+ stderr: `Generated project file has changed: ${resolve(projectPath, changedFile)}\n`,
365
+ };
326
366
  }
327
367
  }
328
368
  else {
@@ -346,6 +386,7 @@ const publishGeneratedProject = (cwd, outputDirectory, input) => {
346
386
  'model.likec4': sha256(input.modelSource),
347
387
  'specification.likec4': sha256(specificationSource),
348
388
  },
389
+ inputDigests: input.inputDigests,
349
390
  }, null, 2)}\n`;
350
391
  publishFiles([
351
392
  {
@@ -370,11 +411,96 @@ const publishGeneratedProject = (cwd, outputDirectory, input) => {
370
411
  stderr: '',
371
412
  };
372
413
  };
414
+ // Freshness is a pure digest comparison between the marker and the would-be
415
+ // export; nothing on disk is written or semantically judged.
416
+ const checkGeneratedProject = (cwd, outputDirectory, input) => {
417
+ const projectPath = resolve(cwd, outputDirectory);
418
+ if (!existsSync(projectPath)) {
419
+ return {
420
+ exitCode: 1,
421
+ stdout: 'Generated LikeC4 output: absent\n',
422
+ stderr: '',
423
+ };
424
+ }
425
+ const marker = readGeneratedProjectMarker(resolve(projectPath, 'yarramate.generated.json'));
426
+ if (marker === undefined) {
427
+ return {
428
+ exitCode: 2,
429
+ stdout: '',
430
+ stderr: `Output directory exists but is not a YarraMate-generated project: ${projectPath}\n`,
431
+ };
432
+ }
433
+ const changedFile = changedGeneratedFile(projectPath, marker);
434
+ if (changedFile !== undefined) {
435
+ return {
436
+ exitCode: 1,
437
+ stdout: 'Generated LikeC4 output: modified\n' +
438
+ 'Reason:\n' +
439
+ `- generated file changed: ${resolve(projectPath, changedFile)}\n` +
440
+ 'Safe to regenerate: no\n',
441
+ stderr: '',
442
+ };
443
+ }
444
+ const recordedInputs = marker['inputDigests'];
445
+ const reasons = [];
446
+ if (recordedInputs === undefined) {
447
+ reasons.push('marker predates input digests');
448
+ }
449
+ else {
450
+ const inputPaths = [
451
+ ...new Set([
452
+ ...Object.keys(recordedInputs),
453
+ ...Object.keys(input.inputDigests),
454
+ ]),
455
+ ].sort();
456
+ for (const path of inputPaths) {
457
+ const recorded = recordedInputs[path];
458
+ const current = input.inputDigests[path];
459
+ if (recorded === undefined)
460
+ reasons.push(`input added: ${path}`);
461
+ else if (current === undefined) {
462
+ reasons.push(`input removed: ${path}`);
463
+ }
464
+ else if (recorded !== current) {
465
+ reasons.push(`input changed: ${path}`);
466
+ }
467
+ }
468
+ }
469
+ const digests = marker['digests'];
470
+ if (digests !== undefined &&
471
+ sha256(input.modelSource) !== digests['model.likec4']) {
472
+ reasons.push('model source changed');
473
+ }
474
+ if (reasons.length === 0) {
475
+ return {
476
+ exitCode: 0,
477
+ stdout: 'Generated LikeC4 output: fresh\n',
478
+ stderr: '',
479
+ };
480
+ }
481
+ return {
482
+ exitCode: 1,
483
+ stdout: 'Generated LikeC4 output: stale\n' +
484
+ 'Reason:\n' +
485
+ reasons.map((reason) => `- ${reason}\n`).join('') +
486
+ 'Safe to regenerate: yes\n',
487
+ stderr: '',
488
+ };
489
+ };
490
+ const inputDigestsOf = (inputs) => Object.fromEntries(inputs
491
+ .map(({ path, source }) => [path, sha256(source)])
492
+ .sort(([left], [right]) => left.localeCompare(right)));
373
493
  export function runLikeC4Cli(args, cwd = process.cwd()) {
494
+ if (args[0] === '--version') {
495
+ return versionResult('yarramate-likec4');
496
+ }
374
497
  if (args[0] === 'map') {
375
498
  return runLikeC4MapSync(args.slice(1), cwd);
376
499
  }
377
- const [command, projectionPath, mappingPath, ...options] = args;
500
+ const checkFreshness = args[0] === 'export-project' && args.includes('--check');
501
+ const [command, projectionPath, mappingPath, ...options] = checkFreshness
502
+ ? args.filter((argument) => argument !== '--check')
503
+ : args;
378
504
  let projectDefinitionMode = false;
379
505
  if ((command === 'check' || command === 'export-project') &&
380
506
  projectionPath !== undefined) {
@@ -460,7 +586,11 @@ export function runLikeC4Cli(args, cwd = process.cwd()) {
460
586
  sourcePaths.some((argument) => argument.startsWith('-'))) {
461
587
  return { exitCode: 2, stdout: '', stderr: usage };
462
588
  }
463
- const diagnosticOutput = (diagnostics) => (json ? checkJson(false, diagnostics) : diagnosticJson(diagnostics));
589
+ const diagnosticOutput = (diagnostics) => json
590
+ ? checkJson(false, diagnostics)
591
+ : diagnosticJson(command === 'check'
592
+ ? summarizeUnmappedSubjects(diagnostics)
593
+ : diagnostics);
464
594
  try {
465
595
  const resolved = resolveCliWorkspaceSources(sourcePaths, cwd);
466
596
  if (!resolved.ok) {
@@ -487,6 +617,7 @@ export function runLikeC4Cli(args, cwd = process.cwd()) {
487
617
  stderr: '',
488
618
  };
489
619
  }
620
+ const projectDirectory = dirname(resolve(cwd, projectionPath));
490
621
  const referencedSources = new Map();
491
622
  const referenceDiagnostics = [];
492
623
  const readProjectReference = (path, label, yamlPath, pointer) => {
@@ -496,7 +627,7 @@ export function runLikeC4Cli(args, cwd = process.cwd()) {
496
627
  try {
497
628
  const source = {
498
629
  path,
499
- source: readFileSync(resolve(cwd, path), 'utf8'),
630
+ source: readFileSync(resolve(projectDirectory, path), 'utf8'),
500
631
  };
501
632
  referencedSources.set(path, source);
502
633
  return source;
@@ -549,6 +680,7 @@ export function runLikeC4Cli(args, cwd = process.cwd()) {
549
680
  ? {}
550
681
  : { comparison: view.compare }),
551
682
  vocabulary: 'bundled',
683
+ requireMappedRelationships: command === 'check',
552
684
  }),
553
685
  }));
554
686
  const failed = preparedViews.find(({ prepared }) => !prepared.ok);
@@ -778,6 +910,17 @@ export function runLikeC4Cli(args, cwd = process.cwd()) {
778
910
  ? undefined
779
911
  : `${first.prepared.kindMapping.id}@${first.prepared.kindMapping.version}`;
780
912
  const projectIdentity = `${loadedProject.document.value.id}@${loadedProject.document.value.version}`;
913
+ const inputDigests = inputDigestsOf([
914
+ projectSource,
915
+ ...sources,
916
+ ...referencedSources.values(),
917
+ ]);
918
+ if (checkFreshness) {
919
+ return checkGeneratedProject(cwd, outputDirectory, {
920
+ modelSource: exported.source,
921
+ inputDigests,
922
+ });
923
+ }
781
924
  return publishGeneratedProject(cwd, outputDirectory, {
782
925
  projectName: `yarramate-${projectIdentity}`.replaceAll(/[^A-Za-z0-9_-]/g, '-'),
783
926
  title: loadedProject.document.value.title,
@@ -797,29 +940,33 @@ export function runLikeC4Cli(args, cwd = process.cwd()) {
797
940
  : { comparison }),
798
941
  })),
799
942
  },
800
- validateMarker: (value) => validateGeneratedProjectV2Marker(value),
943
+ inputDigests,
801
944
  });
802
945
  }
946
+ const projectionSource = {
947
+ path: projectionPath,
948
+ source: readFileSync(resolve(cwd, projectionPath), 'utf8'),
949
+ };
950
+ const mappingSource = {
951
+ path: mappingPath,
952
+ source: readFileSync(resolve(cwd, mappingPath), 'utf8'),
953
+ };
954
+ const kindMappingSource = kindMappingPath === undefined
955
+ ? undefined
956
+ : {
957
+ path: kindMappingPath,
958
+ source: readFileSync(resolve(cwd, kindMappingPath), 'utf8'),
959
+ };
803
960
  const prepared = prepareLikeC4Export({
804
961
  sources,
805
- projection: {
806
- path: projectionPath,
807
- source: readFileSync(resolve(cwd, projectionPath), 'utf8'),
808
- },
809
- subjectMapping: {
810
- path: mappingPath,
811
- source: readFileSync(resolve(cwd, mappingPath), 'utf8'),
812
- },
813
- ...(kindMappingPath === undefined
962
+ projection: projectionSource,
963
+ subjectMapping: mappingSource,
964
+ ...(kindMappingSource === undefined
814
965
  ? {}
815
- : {
816
- kindMapping: {
817
- path: kindMappingPath,
818
- source: readFileSync(resolve(cwd, kindMappingPath), 'utf8'),
819
- },
820
- }),
966
+ : { kindMapping: kindMappingSource }),
821
967
  ...(comparison === undefined ? {} : { comparison }),
822
968
  vocabulary: command === 'export' ? 'consumer' : 'bundled',
969
+ requireMappedRelationships: command === 'check',
823
970
  });
824
971
  if (!prepared.ok) {
825
972
  return {
@@ -846,6 +993,18 @@ export function runLikeC4Cli(args, cwd = process.cwd()) {
846
993
  ? undefined
847
994
  : `${prepared.kindMapping.id}@${prepared.kindMapping.version}`;
848
995
  const comparisonIdentity = comparison;
996
+ const inputDigests = inputDigestsOf([
997
+ projectionSource,
998
+ mappingSource,
999
+ ...(kindMappingSource === undefined ? [] : [kindMappingSource]),
1000
+ ...sources,
1001
+ ]);
1002
+ if (checkFreshness) {
1003
+ return checkGeneratedProject(cwd, outputDirectory, {
1004
+ modelSource: prepared.source,
1005
+ inputDigests,
1006
+ });
1007
+ }
849
1008
  return publishGeneratedProject(cwd, outputDirectory, {
850
1009
  projectName: ['yarramate', projectionIdentity]
851
1010
  .join('-')
@@ -864,7 +1023,7 @@ export function runLikeC4Cli(args, cwd = process.cwd()) {
864
1023
  ? {}
865
1024
  : { comparison: comparisonIdentity }),
866
1025
  },
867
- validateMarker: (value) => validateGeneratedProjectMarker(value),
1026
+ inputDigests,
868
1027
  });
869
1028
  }
870
1029
  catch (error) {
@@ -4,7 +4,7 @@ import type { ProjectionResult } from '../projection.js';
4
4
  import type { LikeC4KindMapping } from './likec4-kind-mapping.js';
5
5
  export interface LikeC4ExportDiagnostic {
6
6
  readonly severity: 'error';
7
- readonly code: 'YMLC101' | 'YMLC102' | 'YMLC103' | 'YMLC104' | 'YMLC105' | 'YMLC106' | 'YMLC107' | 'YMLC108' | 'YMLC109';
7
+ readonly code: 'YMLC101' | 'YMLC102' | 'YMLC103' | 'YMLC104' | 'YMLC105' | 'YMLC106' | 'YMLC107' | 'YMLC108' | 'YMLC109' | 'YMLC111';
8
8
  readonly message: string;
9
9
  readonly subject?: string;
10
10
  readonly path: string;
@@ -13,6 +13,12 @@ export interface LikeC4PreparationInput {
13
13
  readonly to: string;
14
14
  };
15
15
  readonly vocabulary: 'bundled' | 'consumer';
16
+ /**
17
+ * Gate mode. Rendering a relationship needs no external identity — views
18
+ * select it by metadata — but `map --sync` still writes one, so only a
19
+ * check answering "would sync change anything" requires the entry.
20
+ */
21
+ readonly requireMappedRelationships?: boolean;
16
22
  }
17
23
  export type LikeC4PreparationDiagnostic = Diagnostic | LikeC4ExportDiagnostic;
18
24
  export type LikeC4PreparationResult = {
@@ -70,6 +70,31 @@ const unsupportedBundledKinds = (projection, kindMapping) => {
70
70
  }
71
71
  return diagnostics.sort(diagnosticOrder);
72
72
  };
73
+ const unmappedProjectedRelationships = (projection, mapping) => {
74
+ const mapped = new Set(mapping.mappings
75
+ .filter(({ type }) => type === 'relationship')
76
+ .map(({ native }) => native));
77
+ return projection.subjects
78
+ .filter(({ type, id }) => type === 'relationship' && !mapped.has(id))
79
+ .flatMap(({ id }) => {
80
+ const source = projection.claims.find((claim) => claim.id === id)?.source;
81
+ return source === undefined
82
+ ? []
83
+ : [
84
+ {
85
+ severity: 'error',
86
+ code: 'YMLC111',
87
+ message: `Projected relationship "${id}" has no LikeC4 mapping`,
88
+ subject: id,
89
+ path: source.path,
90
+ pointer: source.pointer,
91
+ line: source.line,
92
+ column: source.column,
93
+ },
94
+ ];
95
+ })
96
+ .sort(diagnosticOrder);
97
+ };
73
98
  export function prepareLikeC4Export(input) {
74
99
  const compilation = compileWorkspaceWithProfileContext(input.sources);
75
100
  if (!compilation.ok)
@@ -137,11 +162,24 @@ export function prepareLikeC4Export(input) {
137
162
  if (diagnostics.length > 0)
138
163
  return { ok: false, diagnostics };
139
164
  }
165
+ const unmappedRelationships = input.requireMappedRelationships === true
166
+ ? unmappedProjectedRelationships(projectionResult, subjectMapping.mapping)
167
+ : [];
140
168
  const exported = exportLikeC4(projectionResult, subjectMapping.mapping, kindMapping?.mapping, comparison === undefined
141
169
  ? undefined
142
170
  : { comparison: comparison.comparison });
143
- if (!exported.ok)
144
- return exported;
171
+ if (!exported.ok) {
172
+ return {
173
+ ok: false,
174
+ diagnostics: [
175
+ ...exported.diagnostics,
176
+ ...unmappedRelationships,
177
+ ].sort(diagnosticOrder),
178
+ };
179
+ }
180
+ if (unmappedRelationships.length > 0) {
181
+ return { ok: false, diagnostics: unmappedRelationships };
182
+ }
145
183
  return {
146
184
  ok: true,
147
185
  source: exported.source,
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import { createInterface } from 'node:readline';
3
- import { isMainModule } from '../cli-support.js';
3
+ import { isMainModule, packageVersion, versionResult, } from '../cli-support.js';
4
4
  import { runCli } from '../cli.js';
5
5
  const workspaceProperty = {
6
6
  workspace: {
@@ -97,7 +97,7 @@ export const handleRequest = (request) => {
97
97
  respond(id, {
98
98
  protocolVersion: '2025-06-18',
99
99
  capabilities: { tools: {} },
100
- serverInfo: { name: 'yarramate', version: '0.1' },
100
+ serverInfo: { name: 'yarramate', version: packageVersion },
101
101
  instructions: 'Read-only architecture context for YarraMate workspaces. The native documents in the repository remain canonical; this server never mutates them.',
102
102
  });
103
103
  return;
@@ -142,24 +142,31 @@ export const handleRequest = (request) => {
142
142
  }
143
143
  };
144
144
  if (isMainModule(import.meta.url, process.argv[1])) {
145
- const lines = createInterface({ input: process.stdin });
146
- lines.on('line', (line) => {
147
- const text = line.trim();
148
- if (text.length === 0)
149
- return;
150
- let request;
151
- try {
152
- request = JSON.parse(text);
153
- }
154
- catch {
155
- respondError(null, -32700, 'Parse error');
156
- return;
157
- }
158
- try {
159
- handleRequest(request);
160
- }
161
- catch (error) {
162
- respondError(request.id ?? null, -32603, error instanceof Error ? error.message : String(error));
163
- }
164
- });
145
+ if (process.argv[2] === '--version') {
146
+ const result = versionResult('yarramate-mcp');
147
+ process.stdout.write(result.stdout);
148
+ process.exitCode = result.exitCode;
149
+ }
150
+ else {
151
+ const lines = createInterface({ input: process.stdin });
152
+ lines.on('line', (line) => {
153
+ const text = line.trim();
154
+ if (text.length === 0)
155
+ return;
156
+ let request;
157
+ try {
158
+ request = JSON.parse(text);
159
+ }
160
+ catch {
161
+ respondError(null, -32700, 'Parse error');
162
+ return;
163
+ }
164
+ try {
165
+ handleRequest(request);
166
+ }
167
+ catch (error) {
168
+ respondError(request.id ?? null, -32603, error instanceof Error ? error.message : String(error));
169
+ }
170
+ });
171
+ }
165
172
  }