yarramate 0.5.0 → 0.7.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 +39 -10
- package/catalogues/core-enrichment.yaml +700 -0
- package/dist/adapters/likec4-cli.js +179 -55
- 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 +55 -49
- package/dist/apply-command.d.ts +2 -0
- package/dist/apply-command.js +199 -0
- package/dist/{new-command.d.ts → ask-command.d.ts} +1 -1
- package/dist/ask-command.js +729 -0
- package/dist/brief.d.ts +3 -0
- package/dist/brief.js +235 -0
- package/dist/check-command.js +70 -4
- package/dist/cli-support.d.ts +4 -1
- package/dist/cli-support.js +3 -2
- package/dist/cli.js +22 -586
- package/dist/compiler.js +45 -7
- package/dist/core-contract.d.ts +1 -1
- package/dist/{status-command.d.ts → design-command.d.ts} +1 -1
- package/dist/design-command.js +218 -0
- package/dist/export-command.d.ts +2 -0
- package/dist/export-command.js +228 -0
- package/dist/interrogate-command.d.ts +104 -0
- package/dist/interrogate-command.js +268 -0
- package/dist/next-command.d.ts +22 -0
- package/dist/next-command.js +167 -0
- package/dist/reconciliation.d.ts +2 -0
- package/dist/reconciliation.js +38 -0
- package/docs/CONSUMING-YARRAMATE.md +28 -21
- package/package.json +13 -10
- package/schema/yarramate-apply-result.schema.json +32 -0
- package/schema/yarramate-ask-result.schema.json +391 -0
- package/schema/yarramate-check-result.schema.json +12 -0
- package/schema/yarramate-core-contract.schema.json +5 -10
- package/schema/yarramate-design-step.schema.json +152 -0
- package/schema/yarramate-document.schema.json +76 -9
- package/schema/yarramate-interrogation-report.schema.json +89 -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-operations.schema.json +303 -0
- package/schema/yarramate-question-catalogue.schema.json +431 -0
- package/schema/yarramate-reconciliation-report.schema.json +8 -2
- package/skills/yarramate-architecture/SKILL.md +42 -17
- package/skills/yarramate-architecture/references/native-authoring.md +73 -17
- package/dist/new-command.js +0 -111
- package/dist/status-command.js +0 -171
- package/schema/yarramate-status-result.schema.json +0 -273
|
@@ -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,
|
|
@@ -89,10 +89,13 @@ const checkJson = (ok, diagnostics) => `${JSON.stringify({
|
|
|
89
89
|
diagnostics,
|
|
90
90
|
}, null, 2)}\n`;
|
|
91
91
|
const unmappedSubjectPreviewLength = 3;
|
|
92
|
-
const
|
|
93
|
-
|
|
92
|
+
const summarizeUnmappedSubjects = (diagnostics) => [
|
|
93
|
+
['YMLC102', 'concepts'],
|
|
94
|
+
['YMLC111', 'relationships'],
|
|
95
|
+
].reduce((current, [code, noun]) => {
|
|
96
|
+
const unmapped = current.filter((diagnostic) => diagnostic.code === code);
|
|
94
97
|
if (unmapped.length <= unmappedSubjectPreviewLength)
|
|
95
|
-
return
|
|
98
|
+
return current;
|
|
96
99
|
const preview = unmapped
|
|
97
100
|
.slice(0, unmappedSubjectPreviewLength)
|
|
98
101
|
.map((diagnostic) => 'subject' in diagnostic && diagnostic.subject !== undefined
|
|
@@ -101,21 +104,20 @@ const summarizeUnmappedConcepts = (diagnostics) => {
|
|
|
101
104
|
.join(', ');
|
|
102
105
|
const summary = {
|
|
103
106
|
...unmapped[0],
|
|
104
|
-
message: `${unmapped.length} projected
|
|
107
|
+
message: `${unmapped.length} projected ${noun} have no LikeC4 mapping ` +
|
|
105
108
|
`(first: ${preview}); run "yarramate-likec4 map --sync" to add ` +
|
|
106
109
|
'the missing mappings',
|
|
107
110
|
};
|
|
108
111
|
let summarized = false;
|
|
109
|
-
return
|
|
110
|
-
if (diagnostic.code !==
|
|
112
|
+
return current.flatMap((diagnostic) => {
|
|
113
|
+
if (diagnostic.code !== code)
|
|
111
114
|
return [diagnostic];
|
|
112
115
|
if (summarized)
|
|
113
116
|
return [];
|
|
114
117
|
summarized = true;
|
|
115
118
|
return [summary];
|
|
116
119
|
});
|
|
117
|
-
};
|
|
118
|
-
const sameJson = (left, right) => JSON.stringify(left) === JSON.stringify(right);
|
|
120
|
+
}, diagnostics);
|
|
119
121
|
const lowerCamel = (value) => value.replaceAll(/-([a-z0-9])/g, (_, character) => character.toUpperCase());
|
|
120
122
|
const runLikeC4MapSync = (args, cwd) => {
|
|
121
123
|
const sync = args[0];
|
|
@@ -290,6 +292,33 @@ const runLikeC4MapSync = (args, cwd) => {
|
|
|
290
292
|
return { exitCode: 2, stdout: '', stderr: `${message}\n` };
|
|
291
293
|
}
|
|
292
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
|
+
};
|
|
293
322
|
const publishGeneratedProject = (cwd, outputDirectory, input) => {
|
|
294
323
|
const projectPath = resolve(cwd, outputDirectory);
|
|
295
324
|
const markerPath = resolve(projectPath, 'yarramate.generated.json');
|
|
@@ -319,37 +348,21 @@ const publishGeneratedProject = (cwd, outputDirectory, input) => {
|
|
|
319
348
|
stderr: `Generated project contains an unsafe file: ${unsafeFile}\n`,
|
|
320
349
|
};
|
|
321
350
|
}
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
marker = JSON.parse(readFileSync(markerPath, 'utf8'));
|
|
325
|
-
}
|
|
326
|
-
catch {
|
|
327
|
-
marker = undefined;
|
|
328
|
-
}
|
|
329
|
-
if (!input.validateMarker(marker) ||
|
|
330
|
-
typeof marker !== 'object' ||
|
|
331
|
-
marker === null ||
|
|
332
|
-
Object.entries(input.ownership).some(([key, value]) => !sameJson(marker[key], value))) {
|
|
351
|
+
const marker = readGeneratedProjectMarker(markerPath);
|
|
352
|
+
if (marker === undefined) {
|
|
333
353
|
return {
|
|
334
354
|
exitCode: 2,
|
|
335
355
|
stdout: '',
|
|
336
|
-
stderr: `Output directory
|
|
356
|
+
stderr: `Output directory exists but is not a YarraMate-generated project: ${projectPath}\n`,
|
|
337
357
|
};
|
|
338
358
|
}
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
}
|
|
346
|
-
if (changedFile !== undefined) {
|
|
347
|
-
return {
|
|
348
|
-
exitCode: 2,
|
|
349
|
-
stdout: '',
|
|
350
|
-
stderr: `Generated project file has changed: ${resolve(projectPath, changedFile)}\n`,
|
|
351
|
-
};
|
|
352
|
-
}
|
|
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
|
+
};
|
|
353
366
|
}
|
|
354
367
|
}
|
|
355
368
|
else {
|
|
@@ -373,6 +386,7 @@ const publishGeneratedProject = (cwd, outputDirectory, input) => {
|
|
|
373
386
|
'model.likec4': sha256(input.modelSource),
|
|
374
387
|
'specification.likec4': sha256(specificationSource),
|
|
375
388
|
},
|
|
389
|
+
inputDigests: input.inputDigests,
|
|
376
390
|
}, null, 2)}\n`;
|
|
377
391
|
publishFiles([
|
|
378
392
|
{
|
|
@@ -397,6 +411,85 @@ const publishGeneratedProject = (cwd, outputDirectory, input) => {
|
|
|
397
411
|
stderr: '',
|
|
398
412
|
};
|
|
399
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)));
|
|
400
493
|
export function runLikeC4Cli(args, cwd = process.cwd()) {
|
|
401
494
|
if (args[0] === '--version') {
|
|
402
495
|
return versionResult('yarramate-likec4');
|
|
@@ -404,7 +497,10 @@ export function runLikeC4Cli(args, cwd = process.cwd()) {
|
|
|
404
497
|
if (args[0] === 'map') {
|
|
405
498
|
return runLikeC4MapSync(args.slice(1), cwd);
|
|
406
499
|
}
|
|
407
|
-
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;
|
|
408
504
|
let projectDefinitionMode = false;
|
|
409
505
|
if ((command === 'check' || command === 'export-project') &&
|
|
410
506
|
projectionPath !== undefined) {
|
|
@@ -493,7 +589,7 @@ export function runLikeC4Cli(args, cwd = process.cwd()) {
|
|
|
493
589
|
const diagnosticOutput = (diagnostics) => json
|
|
494
590
|
? checkJson(false, diagnostics)
|
|
495
591
|
: diagnosticJson(command === 'check'
|
|
496
|
-
?
|
|
592
|
+
? summarizeUnmappedSubjects(diagnostics)
|
|
497
593
|
: diagnostics);
|
|
498
594
|
try {
|
|
499
595
|
const resolved = resolveCliWorkspaceSources(sourcePaths, cwd);
|
|
@@ -584,6 +680,7 @@ export function runLikeC4Cli(args, cwd = process.cwd()) {
|
|
|
584
680
|
? {}
|
|
585
681
|
: { comparison: view.compare }),
|
|
586
682
|
vocabulary: 'bundled',
|
|
683
|
+
requireMappedRelationships: command === 'check',
|
|
587
684
|
}),
|
|
588
685
|
}));
|
|
589
686
|
const failed = preparedViews.find(({ prepared }) => !prepared.ok);
|
|
@@ -813,6 +910,17 @@ export function runLikeC4Cli(args, cwd = process.cwd()) {
|
|
|
813
910
|
? undefined
|
|
814
911
|
: `${first.prepared.kindMapping.id}@${first.prepared.kindMapping.version}`;
|
|
815
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
|
+
}
|
|
816
924
|
return publishGeneratedProject(cwd, outputDirectory, {
|
|
817
925
|
projectName: `yarramate-${projectIdentity}`.replaceAll(/[^A-Za-z0-9_-]/g, '-'),
|
|
818
926
|
title: loadedProject.document.value.title,
|
|
@@ -832,29 +940,33 @@ export function runLikeC4Cli(args, cwd = process.cwd()) {
|
|
|
832
940
|
: { comparison }),
|
|
833
941
|
})),
|
|
834
942
|
},
|
|
835
|
-
|
|
943
|
+
inputDigests,
|
|
836
944
|
});
|
|
837
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
|
+
};
|
|
838
960
|
const prepared = prepareLikeC4Export({
|
|
839
961
|
sources,
|
|
840
|
-
projection:
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
},
|
|
844
|
-
subjectMapping: {
|
|
845
|
-
path: mappingPath,
|
|
846
|
-
source: readFileSync(resolve(cwd, mappingPath), 'utf8'),
|
|
847
|
-
},
|
|
848
|
-
...(kindMappingPath === undefined
|
|
962
|
+
projection: projectionSource,
|
|
963
|
+
subjectMapping: mappingSource,
|
|
964
|
+
...(kindMappingSource === undefined
|
|
849
965
|
? {}
|
|
850
|
-
: {
|
|
851
|
-
kindMapping: {
|
|
852
|
-
path: kindMappingPath,
|
|
853
|
-
source: readFileSync(resolve(cwd, kindMappingPath), 'utf8'),
|
|
854
|
-
},
|
|
855
|
-
}),
|
|
966
|
+
: { kindMapping: kindMappingSource }),
|
|
856
967
|
...(comparison === undefined ? {} : { comparison }),
|
|
857
968
|
vocabulary: command === 'export' ? 'consumer' : 'bundled',
|
|
969
|
+
requireMappedRelationships: command === 'check',
|
|
858
970
|
});
|
|
859
971
|
if (!prepared.ok) {
|
|
860
972
|
return {
|
|
@@ -881,6 +993,18 @@ export function runLikeC4Cli(args, cwd = process.cwd()) {
|
|
|
881
993
|
? undefined
|
|
882
994
|
: `${prepared.kindMapping.id}@${prepared.kindMapping.version}`;
|
|
883
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
|
+
}
|
|
884
1008
|
return publishGeneratedProject(cwd, outputDirectory, {
|
|
885
1009
|
projectName: ['yarramate', projectionIdentity]
|
|
886
1010
|
.join('-')
|
|
@@ -899,7 +1023,7 @@ export function runLikeC4Cli(args, cwd = process.cwd()) {
|
|
|
899
1023
|
? {}
|
|
900
1024
|
: { comparison: comparisonIdentity }),
|
|
901
1025
|
},
|
|
902
|
-
|
|
1026
|
+
inputDigests,
|
|
903
1027
|
});
|
|
904
1028
|
}
|
|
905
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
|
@@ -10,14 +10,65 @@ const workspaceProperty = {
|
|
|
10
10
|
};
|
|
11
11
|
const tools = [
|
|
12
12
|
{
|
|
13
|
-
name: '
|
|
14
|
-
description: '
|
|
13
|
+
name: 'yarramate_ask',
|
|
14
|
+
description: 'The consumed-now read surface. Without a query: orientation — check verdict, drift summary, open-question count, and the backlog in dependency order. With a query: free text matches concept ids, names, and descriptions and returns the connected slice; exact subject ids (document-id#local-id) and projection paths address precisely. Set mode for the roster (subjects), declarable vocabulary (kinds), build order (next), or the full open-questions report (open).',
|
|
15
15
|
inputSchema: {
|
|
16
16
|
type: 'object',
|
|
17
17
|
required: ['workspace'],
|
|
18
|
-
properties:
|
|
18
|
+
properties: {
|
|
19
|
+
...workspaceProperty,
|
|
20
|
+
query: {
|
|
21
|
+
type: 'string',
|
|
22
|
+
description: 'Free text, a globally qualified subject id, or a projection path',
|
|
23
|
+
},
|
|
24
|
+
mode: {
|
|
25
|
+
type: 'string',
|
|
26
|
+
enum: ['subjects', 'kinds', 'next', 'open'],
|
|
27
|
+
description: 'Optional flag mode instead of a query: the filterable roster, the declarable kind vocabulary, dependency-ordered planned work, or the open-questions report',
|
|
28
|
+
},
|
|
29
|
+
budget: {
|
|
30
|
+
type: 'integer',
|
|
31
|
+
minimum: 1,
|
|
32
|
+
description: 'Approximate token budget for the compact slice rendering (query form only)',
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
arguments: (input) => {
|
|
37
|
+
const workspace = String(input.workspace);
|
|
38
|
+
if (typeof input.mode === 'string') {
|
|
39
|
+
return ['ask', workspace, `--${input.mode}`, '--json'];
|
|
40
|
+
}
|
|
41
|
+
if (typeof input.query === 'string' && input.query.length > 0) {
|
|
42
|
+
const budget = typeof input.budget === 'number'
|
|
43
|
+
? ['--budget', String(input.budget)]
|
|
44
|
+
: ['--json'];
|
|
45
|
+
return ['ask', workspace, input.query, ...budget];
|
|
46
|
+
}
|
|
47
|
+
return ['ask', workspace, '--json'];
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
name: 'yarramate_design',
|
|
52
|
+
description: 'The design interview, one stateless step: the top open question with its subject slice, materiality, and progress. Read-only — answers land through the CLI apply command in the repository, not through this server.',
|
|
53
|
+
inputSchema: {
|
|
54
|
+
type: 'object',
|
|
55
|
+
required: ['workspace'],
|
|
56
|
+
properties: {
|
|
57
|
+
...workspaceProperty,
|
|
58
|
+
subject: {
|
|
59
|
+
type: 'string',
|
|
60
|
+
description: 'Optional globally qualified subject id to narrow the interview',
|
|
61
|
+
},
|
|
62
|
+
},
|
|
19
63
|
},
|
|
20
|
-
arguments: (input) => [
|
|
64
|
+
arguments: (input) => [
|
|
65
|
+
'design',
|
|
66
|
+
String(input.workspace),
|
|
67
|
+
...(typeof input.subject === 'string'
|
|
68
|
+
? ['--subject', input.subject]
|
|
69
|
+
: []),
|
|
70
|
+
'--json',
|
|
71
|
+
],
|
|
21
72
|
},
|
|
22
73
|
{
|
|
23
74
|
name: 'yarramate_check',
|
|
@@ -39,51 +90,6 @@ const tools = [
|
|
|
39
90
|
},
|
|
40
91
|
arguments: (input) => ['reconcile', String(input.workspace)],
|
|
41
92
|
},
|
|
42
|
-
{
|
|
43
|
-
name: 'yarramate_context',
|
|
44
|
-
description: 'Bounded architecture context. Provide either a projection path or one or more globally qualified subjects (document-id#local-id) for an ad-hoc connected neighbourhood. Optional token budget switches to a compact ranked rendering.',
|
|
45
|
-
inputSchema: {
|
|
46
|
-
type: 'object',
|
|
47
|
-
required: ['workspace'],
|
|
48
|
-
properties: {
|
|
49
|
-
...workspaceProperty,
|
|
50
|
-
projection: {
|
|
51
|
-
type: 'string',
|
|
52
|
-
description: 'Path to an authored projection definition',
|
|
53
|
-
},
|
|
54
|
-
subjects: {
|
|
55
|
-
type: 'array',
|
|
56
|
-
items: { type: 'string' },
|
|
57
|
-
description: 'Globally qualified subject identities for ad-hoc context',
|
|
58
|
-
},
|
|
59
|
-
budget: {
|
|
60
|
-
type: 'integer',
|
|
61
|
-
minimum: 1,
|
|
62
|
-
description: 'Approximate token budget for the compact rendering',
|
|
63
|
-
},
|
|
64
|
-
},
|
|
65
|
-
},
|
|
66
|
-
arguments: (input) => {
|
|
67
|
-
const budget = typeof input.budget === 'number'
|
|
68
|
-
? ['--budget', String(input.budget)]
|
|
69
|
-
: [];
|
|
70
|
-
if (typeof input.projection === 'string') {
|
|
71
|
-
return [
|
|
72
|
-
'context',
|
|
73
|
-
input.projection,
|
|
74
|
-
String(input.workspace),
|
|
75
|
-
...budget,
|
|
76
|
-
];
|
|
77
|
-
}
|
|
78
|
-
const subjects = Array.isArray(input.subjects)
|
|
79
|
-
? input.subjects.flatMap((subject) => [
|
|
80
|
-
'--subject',
|
|
81
|
-
String(subject),
|
|
82
|
-
])
|
|
83
|
-
: [];
|
|
84
|
-
return ['context', ...subjects, String(input.workspace), ...budget];
|
|
85
|
-
},
|
|
86
|
-
},
|
|
87
93
|
];
|
|
88
94
|
const respond = (id, result) => {
|
|
89
95
|
process.stdout.write(`${JSON.stringify({ jsonrpc: '2.0', id, result })}\n`);
|