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 +3 -1
- package/dist/adapters/graphify-cli.js +4 -1
- package/dist/adapters/likec4-cli.js +210 -51
- package/dist/adapters/likec4-export.d.ts +1 -1
- package/dist/adapters/likec4-prepare.d.ts +6 -0
- package/dist/adapters/likec4-prepare.js +40 -2
- package/dist/adapters/mcp-cli.js +29 -22
- package/dist/check-command.js +70 -4
- package/dist/cli-support.d.ts +6 -1
- package/dist/cli-support.js +12 -2
- package/dist/cli.js +39 -22
- package/dist/compiler.js +33 -7
- package/dist/index.d.ts +1 -1
- package/dist/next-command.d.ts +25 -0
- package/dist/next-command.js +272 -0
- package/dist/profile.d.ts +5 -0
- package/dist/profile.js +4 -0
- package/dist/reconciliation.d.ts +11 -1
- package/dist/reconciliation.js +73 -4
- package/dist/status-command.js +4 -1
- package/package.json +6 -5
- package/schema/yarramate-check-result.schema.json +12 -0
- package/schema/yarramate-core-contract.schema.json +1 -0
- package/schema/yarramate-likec4-generated-project-v2.schema.json +6 -0
- package/schema/yarramate-likec4-generated-project.schema.json +6 -0
- package/schema/yarramate-likec4-project.schema.json +1 -1
- package/schema/yarramate-next-result.schema.json +85 -0
- package/schema/yarramate-reconciliation-report.schema.json +24 -2
- package/schema/yarramate-status-result.schema.json +6 -1
- package/skills/yarramate-architecture/SKILL.md +9 -9
- package/skills/yarramate-architecture/references/native-authoring.md +74 -6
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
|
|
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
|
-
|
|
296
|
-
|
|
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
|
|
356
|
+
stderr: `Output directory exists but is not a YarraMate-generated project: ${projectPath}\n`,
|
|
310
357
|
};
|
|
311
358
|
}
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
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 [
|
|
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) =>
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
807
|
-
|
|
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
|
-
|
|
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
|
|
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,
|
package/dist/adapters/mcp-cli.js
CHANGED
|
@@ -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:
|
|
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
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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
|
}
|