yarramate 0.5.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/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/check-command.js +70 -4
- package/dist/cli-support.d.ts +4 -1
- package/dist/cli-support.js +3 -2
- package/dist/cli.js +4 -0
- package/dist/compiler.js +32 -7
- package/dist/next-command.d.ts +25 -0
- package/dist/next-command.js +272 -0
- package/dist/reconciliation.d.ts +2 -0
- package/dist/reconciliation.js +38 -0
- package/dist/status-command.js +3 -0
- package/package.json +2 -1
- 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-next-result.schema.json +85 -0
- package/schema/yarramate-reconciliation-report.schema.json +8 -2
- package/schema/yarramate-status-result.schema.json +6 -1
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
|
|
|
@@ -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/check-command.js
CHANGED
|
@@ -8,10 +8,23 @@ import { compileWorkspace } from './compiler.js';
|
|
|
8
8
|
import { checkCoreContract, loadCoreContract, } from './core-contract.js';
|
|
9
9
|
import { evaluateEvidenceWorkspace, loadEvidence, } from './evidence.js';
|
|
10
10
|
import { loadProjection } from './projection.js';
|
|
11
|
+
import { reconcileEvidenceReports, } from './reconciliation.js';
|
|
11
12
|
const Ajv2020 = Ajv2020Module.default;
|
|
13
|
+
const strictFindingMessage = (finding) => {
|
|
14
|
+
const observed = finding.evidence.message === undefined
|
|
15
|
+
? finding.evidence.uri
|
|
16
|
+
: `${finding.evidence.uri}: ${finding.evidence.message}`;
|
|
17
|
+
const assertion = finding.asserted === undefined
|
|
18
|
+
? `Evidence contradicts ${finding.target.type} "${finding.target.id}"`
|
|
19
|
+
: `Evidence contradicts claim "${finding.target.id}": the model asserts ` +
|
|
20
|
+
`${finding.asserted.from} -> ${finding.asserted.to} (${finding.asserted.kind})`;
|
|
21
|
+
return (`${assertion}, but provider "${finding.provider}" observed otherwise ` +
|
|
22
|
+
`(${observed}); align the model or the evidence to pass --strict`);
|
|
23
|
+
};
|
|
12
24
|
export function runCheckCommand(options, cwd) {
|
|
13
25
|
const json = options.includes('--json');
|
|
14
|
-
const
|
|
26
|
+
const strict = options.includes('--strict');
|
|
27
|
+
const paths = options.filter((option) => option !== '--json' && option !== '--strict');
|
|
15
28
|
const unknownOption = paths.find((path) => path.startsWith('-'));
|
|
16
29
|
if (unknownOption !== undefined || paths.length === 0) {
|
|
17
30
|
return { exitCode: 2, stdout: '', stderr: usage };
|
|
@@ -171,6 +184,46 @@ export function runCheckCommand(options, cwd) {
|
|
|
171
184
|
const diagnostics = result.ok
|
|
172
185
|
? optionalDiagnostics
|
|
173
186
|
: result.diagnostics;
|
|
187
|
+
// Strict only tightens a passing check: base diagnostics already fail
|
|
188
|
+
// the gate, so contradictions are folded in only once everything else
|
|
189
|
+
// holds, and each one is anchored at the claim the model declares.
|
|
190
|
+
const strictEvaluation = strict && result.ok && ok
|
|
191
|
+
? (() => {
|
|
192
|
+
const reports = evidenceEvaluation !== undefined && evidenceEvaluation.ok
|
|
193
|
+
? evidenceEvaluation.reports
|
|
194
|
+
: [];
|
|
195
|
+
const graph = result.graph;
|
|
196
|
+
const contradicted = reconcileEvidenceReports('strict', reports, graph).findings.filter(({ result: outcome }) => outcome === 'contradicted');
|
|
197
|
+
return {
|
|
198
|
+
observations: reports.reduce((total, report) => total + report.observations.length, 0),
|
|
199
|
+
diagnostics: sortDiagnostics(contradicted.map((finding) => {
|
|
200
|
+
const anchor = graph.claims.find(({ id }) => id === finding.target.id) ??
|
|
201
|
+
graph.claims.find(({ subject, predicate }) => subject === finding.target.id &&
|
|
202
|
+
predicate === 'yarramate/concept/kind') ??
|
|
203
|
+
graph.claims.find(({ subject }) => subject === finding.target.id);
|
|
204
|
+
return {
|
|
205
|
+
severity: 'error',
|
|
206
|
+
code: 'YM901',
|
|
207
|
+
message: strictFindingMessage(finding),
|
|
208
|
+
path: anchor?.source.path ?? finding.evidenceDocument,
|
|
209
|
+
pointer: anchor?.source.pointer ?? '/',
|
|
210
|
+
line: anchor?.source.line ?? 1,
|
|
211
|
+
column: anchor?.source.column ?? 1,
|
|
212
|
+
};
|
|
213
|
+
})),
|
|
214
|
+
};
|
|
215
|
+
})()
|
|
216
|
+
: undefined;
|
|
217
|
+
const strictSummary = strictEvaluation === undefined
|
|
218
|
+
? undefined
|
|
219
|
+
: {
|
|
220
|
+
observations: strictEvaluation.observations,
|
|
221
|
+
contradicted: strictEvaluation.diagnostics.length,
|
|
222
|
+
};
|
|
223
|
+
const strictOk = strictEvaluation === undefined
|
|
224
|
+
? true
|
|
225
|
+
: strictEvaluation.diagnostics.length === 0;
|
|
226
|
+
const finalOk = ok && strictOk;
|
|
174
227
|
const counted = result.ok
|
|
175
228
|
? (() => {
|
|
176
229
|
const states = new Set(result.graph.claims
|
|
@@ -186,8 +239,15 @@ export function runCheckCommand(options, cwd) {
|
|
|
186
239
|
: undefined;
|
|
187
240
|
if (json) {
|
|
188
241
|
return {
|
|
189
|
-
exitCode:
|
|
190
|
-
stdout: checkResultJson(
|
|
242
|
+
exitCode: finalOk ? 0 : 1,
|
|
243
|
+
stdout: checkResultJson(finalOk, finalOk ? [] : ok ? strictEvaluation.diagnostics : diagnostics, finalOk ? counted : undefined, strictSummary),
|
|
244
|
+
stderr: '',
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
if (ok && !strictOk) {
|
|
248
|
+
return {
|
|
249
|
+
exitCode: 1,
|
|
250
|
+
stdout: humanDiagnostics(strictEvaluation.diagnostics),
|
|
191
251
|
stderr: '',
|
|
192
252
|
};
|
|
193
253
|
}
|
|
@@ -227,13 +287,19 @@ export function runCheckCommand(options, cwd) {
|
|
|
227
287
|
]
|
|
228
288
|
: []),
|
|
229
289
|
].join(' and ');
|
|
290
|
+
const strictLine = strictSummary === undefined
|
|
291
|
+
? ''
|
|
292
|
+
: strictSummary.observations === 0
|
|
293
|
+
? 'Strict: no evidence observations to evaluate\n'
|
|
294
|
+
: `Strict: ${strictSummary.observations} ${strictSummary.observations === 1 ? 'observation' : 'observations'}, 0 contradicted\n`;
|
|
230
295
|
return {
|
|
231
296
|
exitCode: 0,
|
|
232
297
|
stdout: `Checked ${checked} (` +
|
|
233
298
|
`${successfulCounts.concepts} ${successfulCounts.concepts === 1 ? 'concept' : 'concepts'}, ` +
|
|
234
299
|
`${successfulCounts.relationships} ${successfulCounts.relationships === 1 ? 'relationship' : 'relationships'}, ` +
|
|
235
300
|
`${successfulCounts.states} ${successfulCounts.states === 1 ? 'state' : 'states'}` +
|
|
236
|
-
'): no errors\n'
|
|
301
|
+
'): no errors\n' +
|
|
302
|
+
strictLine,
|
|
237
303
|
stderr: '',
|
|
238
304
|
};
|
|
239
305
|
}
|
package/dist/cli-support.d.ts
CHANGED
|
@@ -7,13 +7,16 @@ export interface CliResult {
|
|
|
7
7
|
export declare const isMainModule: (moduleUrl: string, entrypoint: string | undefined) => boolean;
|
|
8
8
|
export declare const packageVersion: string;
|
|
9
9
|
export declare const versionResult: (binary: string) => CliResult;
|
|
10
|
-
export declare const usage = "Usage:\n yarramate init <directory> [--no-pointer]\n yarramate add <document.yaml> --id <id> --kind <kind> --name <name> [--status <status>] [--description <text>] [--owner <ref>] [--constraint <id>=<ref> ...] [--reference <id>=<ref> ...] [--present-in <state-ref> ...] [--source <source.yaml> ...]\n yarramate connect <document.yaml> --id <id> --kind <kind> --from <ref> --to <ref> [--name <name>] [--description <text>] [--status <status>] [--mode <mode>] [--content <text>] [--reference <id>=<ref> ...] [--present-in <state-ref> ...] [--source <source.yaml> ...]\n yarramate check <source.yaml> [source.yaml ...] [--json]\n yarramate new projection <projection.yaml> --id <id> [--version <v>] [--title <text>] [--description <text>] [--document <id> ...] [--subject <ref> ...] [--kind <qualified-kind> ...] [--relationships <mode>]\n yarramate status <workspace.yaml> [--json]\n yarramate compile <source.yaml> [source.yaml ...]\n yarramate context <projection.yaml> <source.yaml> [source.yaml ...] [--budget <tokens>]\n yarramate context --subject <document-id>#<local-id> [--subject ...] <source.yaml> [source.yaml ...] [--budget <tokens>]\n yarramate view <projection.yaml> <source.yaml> [source.yaml ...]\n yarramate compare <from-state> <to-state> <source.yaml> [source.yaml ...]\n yarramate evidence <evidence.yaml> <source.yaml> [source.yaml ...]\n yarramate reconcile <workspace.yaml>\n";
|
|
10
|
+
export declare const usage = "Usage:\n yarramate init <directory> [--no-pointer]\n yarramate add <document.yaml> --id <id> --kind <kind> --name <name> [--status <status>] [--description <text>] [--owner <ref>] [--constraint <id>=<ref> ...] [--reference <id>=<ref> ...] [--present-in <state-ref> ...] [--source <source.yaml> ...]\n yarramate connect <document.yaml> --id <id> --kind <kind> --from <ref> --to <ref> [--name <name>] [--description <text>] [--status <status>] [--mode <mode>] [--content <text>] [--reference <id>=<ref> ...] [--present-in <state-ref> ...] [--source <source.yaml> ...]\n yarramate check <source.yaml> [source.yaml ...] [--json] [--strict]\n yarramate new projection <projection.yaml> --id <id> [--version <v>] [--title <text>] [--description <text>] [--document <id> ...] [--subject <ref> ...] [--kind <qualified-kind> ...] [--relationships <mode>]\n yarramate status <workspace.yaml> [--json]\n yarramate next <projection.yaml> <workspace.yaml> [--json]\n yarramate compile <source.yaml> [source.yaml ...]\n yarramate context <projection.yaml> <source.yaml> [source.yaml ...] [--budget <tokens>]\n yarramate context --subject <document-id>#<local-id> [--subject ...] <source.yaml> [source.yaml ...] [--budget <tokens>]\n yarramate view <projection.yaml> <source.yaml> [source.yaml ...]\n yarramate compare <from-state> <to-state> <source.yaml> [source.yaml ...]\n yarramate evidence <evidence.yaml> <source.yaml> [source.yaml ...]\n yarramate reconcile <workspace.yaml>\n";
|
|
11
11
|
export declare const diagnosticJson: (diagnostics: unknown) => string;
|
|
12
12
|
export declare const checkResultJson: (ok: boolean, diagnostics: unknown, counted?: {
|
|
13
13
|
readonly documents: number;
|
|
14
14
|
readonly concepts: number;
|
|
15
15
|
readonly relationships: number;
|
|
16
16
|
readonly states: number;
|
|
17
|
+
}, strict?: {
|
|
18
|
+
readonly observations: number;
|
|
19
|
+
readonly contradicted: number;
|
|
17
20
|
}) => string;
|
|
18
21
|
export declare const humanDiagnostics: (diagnostics: readonly Pick<Diagnostic, "path" | "line" | "column" | "code" | "message">[]) => string;
|
|
19
22
|
export declare const sortDiagnostics: <T extends Diagnostic>(diagnostics: readonly T[]) => T[];
|
package/dist/cli-support.js
CHANGED
|
@@ -23,16 +23,17 @@ export const versionResult = (binary) => ({
|
|
|
23
23
|
stdout: `${binary} ${packageVersion}\n`,
|
|
24
24
|
stderr: '',
|
|
25
25
|
});
|
|
26
|
-
export const usage = 'Usage:\n yarramate init <directory> [--no-pointer]\n yarramate add <document.yaml> --id <id> --kind <kind> --name <name> [--status <status>] [--description <text>] [--owner <ref>] [--constraint <id>=<ref> ...] [--reference <id>=<ref> ...] [--present-in <state-ref> ...] [--source <source.yaml> ...]\n yarramate connect <document.yaml> --id <id> --kind <kind> --from <ref> --to <ref> [--name <name>] [--description <text>] [--status <status>] [--mode <mode>] [--content <text>] [--reference <id>=<ref> ...] [--present-in <state-ref> ...] [--source <source.yaml> ...]\n yarramate check <source.yaml> [source.yaml ...] [--json]\n yarramate new projection <projection.yaml> --id <id> [--version <v>] [--title <text>] [--description <text>] [--document <id> ...] [--subject <ref> ...] [--kind <qualified-kind> ...] [--relationships <mode>]\n yarramate status <workspace.yaml> [--json]\n yarramate compile <source.yaml> [source.yaml ...]\n yarramate context <projection.yaml> <source.yaml> [source.yaml ...] [--budget <tokens>]\n yarramate context --subject <document-id>#<local-id> [--subject ...] <source.yaml> [source.yaml ...] [--budget <tokens>]\n yarramate view <projection.yaml> <source.yaml> [source.yaml ...]\n yarramate compare <from-state> <to-state> <source.yaml> [source.yaml ...]\n yarramate evidence <evidence.yaml> <source.yaml> [source.yaml ...]\n yarramate reconcile <workspace.yaml>\n';
|
|
26
|
+
export const usage = 'Usage:\n yarramate init <directory> [--no-pointer]\n yarramate add <document.yaml> --id <id> --kind <kind> --name <name> [--status <status>] [--description <text>] [--owner <ref>] [--constraint <id>=<ref> ...] [--reference <id>=<ref> ...] [--present-in <state-ref> ...] [--source <source.yaml> ...]\n yarramate connect <document.yaml> --id <id> --kind <kind> --from <ref> --to <ref> [--name <name>] [--description <text>] [--status <status>] [--mode <mode>] [--content <text>] [--reference <id>=<ref> ...] [--present-in <state-ref> ...] [--source <source.yaml> ...]\n yarramate check <source.yaml> [source.yaml ...] [--json] [--strict]\n yarramate new projection <projection.yaml> --id <id> [--version <v>] [--title <text>] [--description <text>] [--document <id> ...] [--subject <ref> ...] [--kind <qualified-kind> ...] [--relationships <mode>]\n yarramate status <workspace.yaml> [--json]\n yarramate next <projection.yaml> <workspace.yaml> [--json]\n yarramate compile <source.yaml> [source.yaml ...]\n yarramate context <projection.yaml> <source.yaml> [source.yaml ...] [--budget <tokens>]\n yarramate context --subject <document-id>#<local-id> [--subject ...] <source.yaml> [source.yaml ...] [--budget <tokens>]\n yarramate view <projection.yaml> <source.yaml> [source.yaml ...]\n yarramate compare <from-state> <to-state> <source.yaml> [source.yaml ...]\n yarramate evidence <evidence.yaml> <source.yaml> [source.yaml ...]\n yarramate reconcile <workspace.yaml>\n';
|
|
27
27
|
export const diagnosticJson = (diagnostics) => `${JSON.stringify({
|
|
28
28
|
format: 'yarramate/diagnostic-result/v1',
|
|
29
29
|
diagnostics,
|
|
30
30
|
}, null, 2)}\n`;
|
|
31
|
-
export const checkResultJson = (ok, diagnostics, counted) => `${JSON.stringify({
|
|
31
|
+
export const checkResultJson = (ok, diagnostics, counted, strict) => `${JSON.stringify({
|
|
32
32
|
format: 'yarramate/check-result/v1',
|
|
33
33
|
ok,
|
|
34
34
|
diagnostics,
|
|
35
35
|
...(counted === undefined ? {} : { counted }),
|
|
36
|
+
...(strict === undefined ? {} : { strict }),
|
|
36
37
|
}, null, 2)}\n`;
|
|
37
38
|
export const humanDiagnostics = (diagnostics) => diagnostics
|
|
38
39
|
.map((diagnostic) => `${diagnostic.path}:${diagnostic.line}:${diagnostic.column} error ${diagnostic.code} ${diagnostic.message}\n`)
|
package/dist/cli.js
CHANGED
|
@@ -8,6 +8,7 @@ import { serializeSemanticGraph } from './graph.js';
|
|
|
8
8
|
import { diagnosticJson, humanDiagnostics, isMainModule, resolveCliWorkspaceSources, usage, versionResult, } from './cli-support.js';
|
|
9
9
|
import { runCheckCommand } from './check-command.js';
|
|
10
10
|
import { runNewCommand } from './new-command.js';
|
|
11
|
+
import { runNextCommand } from './next-command.js';
|
|
11
12
|
import { runStatusCommand } from './status-command.js';
|
|
12
13
|
import { evaluateEvidence, evaluateEvidenceWorkspace, loadEvidence, } from './evidence.js';
|
|
13
14
|
import { reconcileEvidenceReports } from './reconciliation.js';
|
|
@@ -757,6 +758,9 @@ export function runCli(args, cwd = process.cwd()) {
|
|
|
757
758
|
if (command === 'new') {
|
|
758
759
|
return runNewCommand(options, cwd);
|
|
759
760
|
}
|
|
761
|
+
if (command === 'next') {
|
|
762
|
+
return runNextCommand(options, cwd);
|
|
763
|
+
}
|
|
760
764
|
return { exitCode: 2, stdout: '', stderr: usage };
|
|
761
765
|
}
|
|
762
766
|
if (isMainModule(import.meta.url, process.argv[1])) {
|
package/dist/compiler.js
CHANGED
|
@@ -14,6 +14,23 @@ const validateDocument = new Ajv2020({ allErrors: true }).compile(documentSchema
|
|
|
14
14
|
const validateProfile = new Ajv2020({ allErrors: true }).compile(profileSchema);
|
|
15
15
|
const compareById = (left, right) => left.id.localeCompare(right.id);
|
|
16
16
|
const presenceClaimId = (subject, state) => `${subject}~present-in-${Buffer.from(state, 'utf8').toString('hex')}`;
|
|
17
|
+
const describeAspect = (aspect) => aspect.replace('-', ' ');
|
|
18
|
+
// Candidate order is the policy-matrix declaration order: the resolved kind
|
|
19
|
+
// map inserts core policies first, then extension kinds as declared.
|
|
20
|
+
const candidateKindHint = (kinds, rejected, sourceAspect, targetAspect) => {
|
|
21
|
+
const candidates = [...kinds]
|
|
22
|
+
.filter(([id, kind]) => id !== rejected &&
|
|
23
|
+
(kind.sourceAspects?.includes(sourceAspect) ?? true) &&
|
|
24
|
+
(kind.targetAspects?.includes(targetAspect) ?? true))
|
|
25
|
+
.map(([id]) => id);
|
|
26
|
+
if (candidates.length === 0) {
|
|
27
|
+
return '';
|
|
28
|
+
}
|
|
29
|
+
const observed = sourceAspect === targetAspect
|
|
30
|
+
? `both endpoints are ${describeAspect(sourceAspect)}`
|
|
31
|
+
: `source is ${describeAspect(sourceAspect)} and target is ${describeAspect(targetAspect)}`;
|
|
32
|
+
return `; ${observed}; valid candidates: ${candidates.join(', ')}`;
|
|
33
|
+
};
|
|
17
34
|
const diagnosticFailure = (diagnostics) => ({
|
|
18
35
|
ok: false,
|
|
19
36
|
diagnostics: [...diagnostics].sort((left, right) => left.path.localeCompare(right.path) ||
|
|
@@ -779,27 +796,35 @@ function compileWorkspaceResolved(sources) {
|
|
|
779
796
|
}
|
|
780
797
|
const policy = selectedProfile.relationshipKinds.get(relationship.kind);
|
|
781
798
|
if (policy !== undefined) {
|
|
782
|
-
|
|
783
|
-
const reference = endpoint === 'source' ? relationship.from : relationship.to;
|
|
799
|
+
const aspectOf = (reference) => {
|
|
784
800
|
const resolvedConcept = conceptByQualifiedId.get(qualifyReference(value.id, reference));
|
|
785
|
-
|
|
801
|
+
return resolvedConcept === undefined
|
|
786
802
|
? undefined
|
|
787
803
|
: profiles
|
|
788
804
|
.get(resolvedConcept.profile)
|
|
789
|
-
?.conceptKinds.get(resolvedConcept.concept.kind);
|
|
805
|
+
?.conceptKinds.get(resolvedConcept.concept.kind)?.aspect;
|
|
806
|
+
};
|
|
807
|
+
const sourceAspect = aspectOf(relationship.from);
|
|
808
|
+
const targetAspect = aspectOf(relationship.to);
|
|
809
|
+
const candidates = sourceAspect === undefined || targetAspect === undefined
|
|
810
|
+
? ''
|
|
811
|
+
: candidateKindHint(selectedProfile.relationshipKinds, relationship.kind, sourceAspect, targetAspect);
|
|
812
|
+
for (const endpoint of ['source', 'target']) {
|
|
813
|
+
const reference = endpoint === 'source' ? relationship.from : relationship.to;
|
|
814
|
+
const aspect = endpoint === 'source' ? sourceAspect : targetAspect;
|
|
790
815
|
const allowed = endpoint === 'source'
|
|
791
816
|
? policy.sourceAspects
|
|
792
817
|
: policy.targetAspects;
|
|
793
|
-
if (
|
|
818
|
+
if (aspect !== undefined &&
|
|
794
819
|
allowed !== undefined &&
|
|
795
|
-
!allowed.includes(
|
|
820
|
+
!allowed.includes(aspect)) {
|
|
796
821
|
const field = endpoint === 'source' ? 'from' : 'to';
|
|
797
822
|
const pointer = `/relationships/${index}/${field}`;
|
|
798
823
|
const source = location(['relationships', index, field], pointer);
|
|
799
824
|
diagnostics.push({
|
|
800
825
|
severity: 'error',
|
|
801
826
|
code: 'YM404',
|
|
802
|
-
message: `Relationship "${relationship.kind}" requires a ${endpoint} with aspect ${allowed.map((
|
|
827
|
+
message: `Relationship "${relationship.kind}" requires a ${endpoint} with aspect ${allowed.map((entry) => `"${entry}"`).join(' or ')}; "${reference}" has aspect "${aspect}"${policy.repair === undefined ? '' : `; ${policy.repair}`}${candidates}`,
|
|
803
828
|
path: input.path,
|
|
804
829
|
pointer,
|
|
805
830
|
line: source.line,
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { type CliResult } from './cli-support.js';
|
|
2
|
+
interface EvidenceCoverage {
|
|
3
|
+
readonly observations: number;
|
|
4
|
+
readonly confirmed: number;
|
|
5
|
+
readonly contradicted: number;
|
|
6
|
+
readonly unknown: number;
|
|
7
|
+
readonly notObserved: number;
|
|
8
|
+
}
|
|
9
|
+
export interface NextSubject {
|
|
10
|
+
readonly id: string;
|
|
11
|
+
readonly kind: string;
|
|
12
|
+
readonly name?: string;
|
|
13
|
+
readonly dependsOn: readonly string[];
|
|
14
|
+
readonly requiredBy: readonly string[];
|
|
15
|
+
readonly evidence: EvidenceCoverage;
|
|
16
|
+
readonly cycle?: true;
|
|
17
|
+
}
|
|
18
|
+
export interface NextResult {
|
|
19
|
+
readonly format: 'yarramate/next-result/v1';
|
|
20
|
+
readonly workspace: string;
|
|
21
|
+
readonly projection: string;
|
|
22
|
+
readonly subjects: readonly NextSubject[];
|
|
23
|
+
}
|
|
24
|
+
export declare function runNextCommand(options: readonly string[], cwd: string): CliResult;
|
|
25
|
+
export {};
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { resolve } from 'node:path';
|
|
3
|
+
import { parseDocument } from 'yaml';
|
|
4
|
+
import { diagnosticJson, humanDiagnostics, usage, } from './cli-support.js';
|
|
5
|
+
import { compileWorkspaceWithProfileContext, } from './compiler.js';
|
|
6
|
+
import { evaluateEvidenceWorkspace, loadEvidence, } from './evidence.js';
|
|
7
|
+
import { evaluateProjection, loadProjection } from './projection.js';
|
|
8
|
+
import { loadWorkspaceManifest } from './workspace.js';
|
|
9
|
+
// Which endpoint of a declared relationship must exist before the other,
|
|
10
|
+
// read off each core kind's declared intent (ADR 0048). Kinds without a
|
|
11
|
+
// build-order reading (association, assignment, influence) contribute no
|
|
12
|
+
// ordering edge rather than a guessed one.
|
|
13
|
+
const corePrerequisiteEndpoints = new Map([
|
|
14
|
+
['realization', 'source'],
|
|
15
|
+
['serving', 'source'],
|
|
16
|
+
['triggering', 'source'],
|
|
17
|
+
['flow', 'source'],
|
|
18
|
+
['composition', 'target'],
|
|
19
|
+
['aggregation', 'target'],
|
|
20
|
+
['access', 'target'],
|
|
21
|
+
['specialization', 'target'],
|
|
22
|
+
]);
|
|
23
|
+
const prerequisiteEndpoint = (kind, profileContext) => {
|
|
24
|
+
const candidates = [
|
|
25
|
+
kind,
|
|
26
|
+
...(profileContext?.relationshipKindLineages.get(kind) ?? []),
|
|
27
|
+
];
|
|
28
|
+
for (const candidate of candidates) {
|
|
29
|
+
const separator = candidate.indexOf('#');
|
|
30
|
+
if (separator === -1 || !candidate.startsWith('yarramate/core@')) {
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
const orientation = corePrerequisiteEndpoints.get(candidate.slice(separator + 1));
|
|
34
|
+
if (orientation !== undefined)
|
|
35
|
+
return orientation;
|
|
36
|
+
}
|
|
37
|
+
return undefined;
|
|
38
|
+
};
|
|
39
|
+
const claimValue = (claims, subject, predicate) => {
|
|
40
|
+
const object = claims.find((claim) => claim.subject === subject && claim.predicate === predicate)?.object;
|
|
41
|
+
return object !== undefined && 'value' in object ? object.value : undefined;
|
|
42
|
+
};
|
|
43
|
+
const plural = (count, singular) => `${count} ${count === 1 ? singular : `${singular}s`}`;
|
|
44
|
+
const coverageClause = (coverage) => {
|
|
45
|
+
if (coverage.observations === 0)
|
|
46
|
+
return 'no evidence';
|
|
47
|
+
const parts = [
|
|
48
|
+
...(coverage.confirmed > 0 ? [`${coverage.confirmed} confirmed`] : []),
|
|
49
|
+
...(coverage.contradicted > 0
|
|
50
|
+
? [`${coverage.contradicted} contradicted`]
|
|
51
|
+
: []),
|
|
52
|
+
...(coverage.unknown > 0 ? [`${coverage.unknown} unknown`] : []),
|
|
53
|
+
...(coverage.notObserved > 0
|
|
54
|
+
? [`${coverage.notObserved} not observed`]
|
|
55
|
+
: []),
|
|
56
|
+
];
|
|
57
|
+
return `${plural(coverage.observations, 'observation')} (${parts.join(', ')})`;
|
|
58
|
+
};
|
|
59
|
+
export function runNextCommand(options, cwd) {
|
|
60
|
+
const json = options.includes('--json');
|
|
61
|
+
const rest = options.filter((option) => option !== '--json');
|
|
62
|
+
const [projectionPath, workspacePath] = rest;
|
|
63
|
+
if (rest.length !== 2 ||
|
|
64
|
+
projectionPath === undefined ||
|
|
65
|
+
workspacePath === undefined ||
|
|
66
|
+
rest.some((option) => option.startsWith('-'))) {
|
|
67
|
+
return { exitCode: 2, stdout: '', stderr: usage };
|
|
68
|
+
}
|
|
69
|
+
try {
|
|
70
|
+
const manifestSource = readFileSync(resolve(cwd, workspacePath), 'utf8');
|
|
71
|
+
if (parseDocument(manifestSource).get('format') !== 'yarramate/workspace/v1') {
|
|
72
|
+
return {
|
|
73
|
+
exitCode: 2,
|
|
74
|
+
stdout: '',
|
|
75
|
+
stderr: 'next requires an explicit workspace manifest (yarramate/workspace/v1)\n',
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
const failed = (diagnostics) => ({
|
|
79
|
+
exitCode: 1,
|
|
80
|
+
stdout: json ? diagnosticJson(diagnostics) : humanDiagnostics(diagnostics),
|
|
81
|
+
stderr: '',
|
|
82
|
+
});
|
|
83
|
+
const loadedWorkspace = loadWorkspaceManifest({ path: workspacePath, source: manifestSource }, cwd);
|
|
84
|
+
if (!loadedWorkspace.ok)
|
|
85
|
+
return failed(loadedWorkspace.diagnostics);
|
|
86
|
+
const workspace = loadedWorkspace.workspace;
|
|
87
|
+
const loadedProjection = loadProjection({
|
|
88
|
+
path: projectionPath,
|
|
89
|
+
source: readFileSync(resolve(cwd, projectionPath), 'utf8'),
|
|
90
|
+
});
|
|
91
|
+
if (!loadedProjection.ok)
|
|
92
|
+
return failed(loadedProjection.diagnostics);
|
|
93
|
+
const compilation = compileWorkspaceWithProfileContext([...workspace.profiles, ...workspace.documents].map((path) => ({
|
|
94
|
+
path,
|
|
95
|
+
source: readFileSync(resolve(cwd, path), 'utf8'),
|
|
96
|
+
})));
|
|
97
|
+
if (!compilation.ok)
|
|
98
|
+
return failed(compilation.diagnostics);
|
|
99
|
+
const graph = compilation.graph;
|
|
100
|
+
const evidenceDocuments = [];
|
|
101
|
+
for (const path of workspace.evidence) {
|
|
102
|
+
const loaded = loadEvidence({
|
|
103
|
+
path,
|
|
104
|
+
source: readFileSync(resolve(cwd, path), 'utf8'),
|
|
105
|
+
});
|
|
106
|
+
if (!loaded.ok)
|
|
107
|
+
return failed(loaded.diagnostics);
|
|
108
|
+
evidenceDocuments.push(loaded.evidence);
|
|
109
|
+
}
|
|
110
|
+
const evaluation = evaluateEvidenceWorkspace(graph, evidenceDocuments);
|
|
111
|
+
if (!evaluation.ok)
|
|
112
|
+
return failed(evaluation.diagnostics);
|
|
113
|
+
const result = evaluateProjection(graph, loadedProjection.projection, compilation.profileContext);
|
|
114
|
+
const planned = result.subjects
|
|
115
|
+
.filter(({ id, type }) => type === 'concept' &&
|
|
116
|
+
claimValue(result.claims, id, 'yarramate/lifecycle/status') ===
|
|
117
|
+
'planned')
|
|
118
|
+
.map(({ id }) => id);
|
|
119
|
+
const plannedIds = new Set(planned);
|
|
120
|
+
const dependsOn = new Map();
|
|
121
|
+
const requiredBy = new Map();
|
|
122
|
+
for (const subject of result.subjects) {
|
|
123
|
+
if (subject.type !== 'relationship')
|
|
124
|
+
continue;
|
|
125
|
+
const claim = result.claims.find(({ id, object }) => id === subject.id && 'ref' in object);
|
|
126
|
+
if (claim === undefined || !('ref' in claim.object))
|
|
127
|
+
continue;
|
|
128
|
+
const orientation = prerequisiteEndpoint(claim.predicate, compilation.profileContext);
|
|
129
|
+
if (orientation === undefined)
|
|
130
|
+
continue;
|
|
131
|
+
const prerequisite = orientation === 'source' ? claim.subject : claim.object.ref;
|
|
132
|
+
const dependent = orientation === 'source' ? claim.object.ref : claim.subject;
|
|
133
|
+
if (prerequisite === dependent ||
|
|
134
|
+
!plannedIds.has(prerequisite) ||
|
|
135
|
+
!plannedIds.has(dependent)) {
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
dependsOn.set(dependent, (dependsOn.get(dependent) ?? new Set()).add(prerequisite));
|
|
139
|
+
requiredBy.set(prerequisite, (requiredBy.get(prerequisite) ?? new Set()).add(dependent));
|
|
140
|
+
}
|
|
141
|
+
// Deterministic Kahn ordering: everything whose prerequisites are
|
|
142
|
+
// already emitted goes next, lexicographic within a round; a cycle
|
|
143
|
+
// cannot be ordered, so its members are appended sorted and marked.
|
|
144
|
+
const ordered = [];
|
|
145
|
+
const cycles = new Set();
|
|
146
|
+
const emitted = new Set();
|
|
147
|
+
const remaining = new Set([...planned].sort());
|
|
148
|
+
while (remaining.size > 0) {
|
|
149
|
+
const ready = [...remaining]
|
|
150
|
+
.filter((id) => [...(dependsOn.get(id) ?? [])].every((dependency) => emitted.has(dependency)))
|
|
151
|
+
.sort();
|
|
152
|
+
if (ready.length === 0) {
|
|
153
|
+
for (const id of [...remaining].sort()) {
|
|
154
|
+
ordered.push(id);
|
|
155
|
+
cycles.add(id);
|
|
156
|
+
}
|
|
157
|
+
break;
|
|
158
|
+
}
|
|
159
|
+
for (const id of ready) {
|
|
160
|
+
ordered.push(id);
|
|
161
|
+
emitted.add(id);
|
|
162
|
+
remaining.delete(id);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
const relationshipIds = new Set(graph.subjects
|
|
166
|
+
.filter(({ type }) => type === 'relationship')
|
|
167
|
+
.map(({ id }) => id));
|
|
168
|
+
const relationshipEndpoints = new Map();
|
|
169
|
+
const claimOwners = new Map();
|
|
170
|
+
for (const claim of graph.claims) {
|
|
171
|
+
claimOwners.set(claim.id, claim.subject);
|
|
172
|
+
if (relationshipIds.has(claim.id) && 'ref' in claim.object) {
|
|
173
|
+
relationshipEndpoints.set(claim.id, [claim.subject, claim.object.ref]);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
const coverageTargets = (observation) => {
|
|
177
|
+
const target = 'subject' in observation ? observation.subject : observation.claim;
|
|
178
|
+
const endpoints = relationshipEndpoints.get(target);
|
|
179
|
+
if (endpoints !== undefined)
|
|
180
|
+
return endpoints;
|
|
181
|
+
if ('claim' in observation) {
|
|
182
|
+
const owner = claimOwners.get(target);
|
|
183
|
+
if (owner === undefined)
|
|
184
|
+
return [];
|
|
185
|
+
return relationshipEndpoints.get(owner) ?? [owner];
|
|
186
|
+
}
|
|
187
|
+
return [target];
|
|
188
|
+
};
|
|
189
|
+
const coverage = new Map();
|
|
190
|
+
for (const id of planned) {
|
|
191
|
+
coverage.set(id, {
|
|
192
|
+
observations: 0,
|
|
193
|
+
confirmed: 0,
|
|
194
|
+
contradicted: 0,
|
|
195
|
+
unknown: 0,
|
|
196
|
+
notObserved: 0,
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
for (const report of evaluation.reports) {
|
|
200
|
+
for (const observation of report.observations) {
|
|
201
|
+
for (const target of coverageTargets(observation)) {
|
|
202
|
+
const tally = coverage.get(target);
|
|
203
|
+
if (tally === undefined)
|
|
204
|
+
continue;
|
|
205
|
+
tally.observations += 1;
|
|
206
|
+
if (observation.result === 'not-observed') {
|
|
207
|
+
tally.notObserved += 1;
|
|
208
|
+
}
|
|
209
|
+
else {
|
|
210
|
+
tally[observation.result] += 1;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
const subjects = ordered.map((id) => {
|
|
216
|
+
const name = claimValue(result.claims, id, 'yarramate/concept/name');
|
|
217
|
+
return {
|
|
218
|
+
id,
|
|
219
|
+
kind: claimValue(result.claims, id, 'yarramate/concept/kind') ?? 'unknown',
|
|
220
|
+
...(name === undefined ? {} : { name }),
|
|
221
|
+
dependsOn: [...(dependsOn.get(id) ?? [])].sort(),
|
|
222
|
+
requiredBy: [...(requiredBy.get(id) ?? [])].sort(),
|
|
223
|
+
evidence: coverage.get(id),
|
|
224
|
+
...(cycles.has(id) ? { cycle: true } : {}),
|
|
225
|
+
};
|
|
226
|
+
});
|
|
227
|
+
const projectionLabel = `${loadedProjection.projection.id}@${loadedProjection.projection.version}`;
|
|
228
|
+
if (json) {
|
|
229
|
+
const payload = {
|
|
230
|
+
format: 'yarramate/next-result/v1',
|
|
231
|
+
workspace: workspace.id,
|
|
232
|
+
projection: projectionLabel,
|
|
233
|
+
subjects,
|
|
234
|
+
};
|
|
235
|
+
return {
|
|
236
|
+
exitCode: 0,
|
|
237
|
+
stdout: `${JSON.stringify(payload, null, 2)}\n`,
|
|
238
|
+
stderr: '',
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
if (subjects.length === 0) {
|
|
242
|
+
return {
|
|
243
|
+
exitCode: 0,
|
|
244
|
+
stdout: `No planned subjects in projection ${projectionLabel}.\n`,
|
|
245
|
+
stderr: '',
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
const width = Math.max(...subjects.map(({ id }) => id.length));
|
|
249
|
+
const lines = [
|
|
250
|
+
`Planned subjects in projection ${projectionLabel} (dependency order):`,
|
|
251
|
+
...subjects.map((subject) => {
|
|
252
|
+
const clauses = [
|
|
253
|
+
...(subject.requiredBy.length > 0
|
|
254
|
+
? [`<- required by ${subject.requiredBy.join(', ')}`]
|
|
255
|
+
: []),
|
|
256
|
+
coverageClause(subject.evidence),
|
|
257
|
+
...(subject.cycle === true ? ['dependency cycle'] : []),
|
|
258
|
+
];
|
|
259
|
+
return ` ${subject.id.padEnd(width)} ${clauses.join('; ')}`;
|
|
260
|
+
}),
|
|
261
|
+
];
|
|
262
|
+
return {
|
|
263
|
+
exitCode: 0,
|
|
264
|
+
stdout: `${lines.join('\n')}\n`,
|
|
265
|
+
stderr: '',
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
catch (error) {
|
|
269
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
270
|
+
return { exitCode: 2, stdout: '', stderr: `${message}\n` };
|
|
271
|
+
}
|
|
272
|
+
}
|
package/dist/reconciliation.d.ts
CHANGED
|
@@ -28,7 +28,9 @@ export interface ReconciliationReport {
|
|
|
28
28
|
readonly contradicted: number;
|
|
29
29
|
readonly unknown: number;
|
|
30
30
|
readonly notObserved: number;
|
|
31
|
+
readonly subjectsWithoutEvidence: number;
|
|
31
32
|
};
|
|
32
33
|
readonly findings: readonly ReconciliationFinding[];
|
|
34
|
+
readonly unobservedSubjects?: readonly string[];
|
|
33
35
|
}
|
|
34
36
|
export declare function reconcileEvidenceReports(workspace: string, reports: readonly EvidenceReport[], graph?: SemanticGraph): ReconciliationReport;
|
package/dist/reconciliation.js
CHANGED
|
@@ -25,8 +25,44 @@ const assertedRelationshipsByClaim = (graph) => {
|
|
|
25
25
|
}
|
|
26
26
|
return asserted;
|
|
27
27
|
};
|
|
28
|
+
const unobservedCurrentConcepts = (graph, reports) => {
|
|
29
|
+
if (graph === undefined)
|
|
30
|
+
return [];
|
|
31
|
+
const relationshipIds = new Set(graph.subjects
|
|
32
|
+
.filter(({ type }) => type === 'relationship')
|
|
33
|
+
.map(({ id }) => id));
|
|
34
|
+
const claimsById = new Map(graph.claims.map((claim) => [claim.id, claim]));
|
|
35
|
+
const observed = new Set();
|
|
36
|
+
for (const report of reports) {
|
|
37
|
+
for (const observation of report.observations) {
|
|
38
|
+
if ('subject' in observation) {
|
|
39
|
+
observed.add(observation.subject);
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
const claim = claimsById.get(observation.claim);
|
|
43
|
+
if (claim === undefined)
|
|
44
|
+
continue;
|
|
45
|
+
observed.add(claim.subject);
|
|
46
|
+
if (relationshipIds.has(claim.id) && 'ref' in claim.object) {
|
|
47
|
+
observed.add(claim.object.ref);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
const conceptIds = new Set(graph.subjects
|
|
52
|
+
.filter(({ type }) => type === 'concept')
|
|
53
|
+
.map(({ id }) => id));
|
|
54
|
+
return graph.claims
|
|
55
|
+
.filter((claim) => claim.predicate === 'yarramate/lifecycle/status' &&
|
|
56
|
+
'value' in claim.object &&
|
|
57
|
+
claim.object.value === 'current' &&
|
|
58
|
+
conceptIds.has(claim.subject) &&
|
|
59
|
+
!observed.has(claim.subject))
|
|
60
|
+
.map(({ subject }) => subject)
|
|
61
|
+
.sort((left, right) => left.localeCompare(right));
|
|
62
|
+
};
|
|
28
63
|
export function reconcileEvidenceReports(workspace, reports, graph) {
|
|
29
64
|
const assertedByClaim = assertedRelationshipsByClaim(graph);
|
|
65
|
+
const unobservedSubjects = unobservedCurrentConcepts(graph, reports);
|
|
30
66
|
const summary = {
|
|
31
67
|
evidenceDocuments: reports.length,
|
|
32
68
|
observations: 0,
|
|
@@ -35,6 +71,7 @@ export function reconcileEvidenceReports(workspace, reports, graph) {
|
|
|
35
71
|
contradicted: 0,
|
|
36
72
|
unknown: 0,
|
|
37
73
|
notObserved: 0,
|
|
74
|
+
subjectsWithoutEvidence: unobservedSubjects.length,
|
|
38
75
|
};
|
|
39
76
|
const findings = [];
|
|
40
77
|
for (const report of reports) {
|
|
@@ -74,5 +111,6 @@ export function reconcileEvidenceReports(workspace, reports, graph) {
|
|
|
74
111
|
workspace,
|
|
75
112
|
summary,
|
|
76
113
|
findings,
|
|
114
|
+
...(unobservedSubjects.length === 0 ? {} : { unobservedSubjects }),
|
|
77
115
|
};
|
|
78
116
|
}
|
package/dist/status-command.js
CHANGED
|
@@ -145,6 +145,9 @@ export function runStatusCommand(options, cwd) {
|
|
|
145
145
|
? ` (${result.reconciliation.contradicted} contradicted, ` +
|
|
146
146
|
`${result.reconciliation.unknown} unknown, ` +
|
|
147
147
|
`${result.reconciliation.notObserved} not observed)`
|
|
148
|
+
: '') +
|
|
149
|
+
(result.reconciliation.subjectsWithoutEvidence > 0
|
|
150
|
+
? `, ${plural(result.reconciliation.subjectsWithoutEvidence, 'current subject')} without evidence`
|
|
148
151
|
: ''));
|
|
149
152
|
}
|
|
150
153
|
lines.push(`Documents: ${documents.map(({ id }) => id).join(', ') || 'none'}`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "yarramate",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "Tool-neutral semantic architecture engine and guided methodology",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -59,6 +59,7 @@
|
|
|
59
59
|
"./schema/likec4-check-result": "./schema/yarramate-likec4-check-result.schema.json",
|
|
60
60
|
"./schema/state-comparison": "./schema/yarramate-state-comparison.schema.json",
|
|
61
61
|
"./schema/status-result": "./schema/yarramate-status-result.schema.json",
|
|
62
|
+
"./schema/next-result": "./schema/yarramate-next-result.schema.json",
|
|
62
63
|
"./schema/graph-v2": "./schema/yarramate-graph-v2.schema.json",
|
|
63
64
|
"./schema/workspace": "./schema/yarramate-workspace.schema.json",
|
|
64
65
|
"./schema/evidence": "./schema/yarramate-evidence.schema.json",
|
|
@@ -20,6 +20,9 @@
|
|
|
20
20
|
},
|
|
21
21
|
"counted": {
|
|
22
22
|
"$ref": "#/$defs/counted"
|
|
23
|
+
},
|
|
24
|
+
"strict": {
|
|
25
|
+
"$ref": "#/$defs/strict"
|
|
23
26
|
}
|
|
24
27
|
},
|
|
25
28
|
"allOf": [
|
|
@@ -50,6 +53,15 @@
|
|
|
50
53
|
}
|
|
51
54
|
],
|
|
52
55
|
"$defs": {
|
|
56
|
+
"strict": {
|
|
57
|
+
"type": "object",
|
|
58
|
+
"additionalProperties": false,
|
|
59
|
+
"required": ["observations", "contradicted"],
|
|
60
|
+
"properties": {
|
|
61
|
+
"observations": { "type": "integer", "minimum": 0 },
|
|
62
|
+
"contradicted": { "type": "integer", "minimum": 0 }
|
|
63
|
+
}
|
|
64
|
+
},
|
|
53
65
|
"counted": {
|
|
54
66
|
"type": "object",
|
|
55
67
|
"additionalProperties": false,
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://yarramate.org/schema/next-result/v1",
|
|
4
|
+
"title": "YarraMate next result",
|
|
5
|
+
"type": "object",
|
|
6
|
+
"additionalProperties": false,
|
|
7
|
+
"required": ["format", "workspace", "projection", "subjects"],
|
|
8
|
+
"properties": {
|
|
9
|
+
"format": {
|
|
10
|
+
"const": "yarramate/next-result/v1"
|
|
11
|
+
},
|
|
12
|
+
"workspace": {
|
|
13
|
+
"type": "string",
|
|
14
|
+
"minLength": 1
|
|
15
|
+
},
|
|
16
|
+
"projection": {
|
|
17
|
+
"type": "string",
|
|
18
|
+
"minLength": 1
|
|
19
|
+
},
|
|
20
|
+
"subjects": {
|
|
21
|
+
"type": "array",
|
|
22
|
+
"items": {
|
|
23
|
+
"$ref": "#/$defs/subject"
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
"$defs": {
|
|
28
|
+
"subject": {
|
|
29
|
+
"type": "object",
|
|
30
|
+
"additionalProperties": false,
|
|
31
|
+
"required": ["id", "kind", "dependsOn", "requiredBy", "evidence"],
|
|
32
|
+
"properties": {
|
|
33
|
+
"id": {
|
|
34
|
+
"type": "string",
|
|
35
|
+
"minLength": 1
|
|
36
|
+
},
|
|
37
|
+
"kind": {
|
|
38
|
+
"type": "string",
|
|
39
|
+
"minLength": 1
|
|
40
|
+
},
|
|
41
|
+
"name": {
|
|
42
|
+
"type": "string"
|
|
43
|
+
},
|
|
44
|
+
"dependsOn": {
|
|
45
|
+
"type": "array",
|
|
46
|
+
"items": {
|
|
47
|
+
"type": "string",
|
|
48
|
+
"minLength": 1
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
"requiredBy": {
|
|
52
|
+
"type": "array",
|
|
53
|
+
"items": {
|
|
54
|
+
"type": "string",
|
|
55
|
+
"minLength": 1
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
"evidence": {
|
|
59
|
+
"$ref": "#/$defs/coverage"
|
|
60
|
+
},
|
|
61
|
+
"cycle": {
|
|
62
|
+
"const": true
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
"coverage": {
|
|
67
|
+
"type": "object",
|
|
68
|
+
"additionalProperties": false,
|
|
69
|
+
"required": [
|
|
70
|
+
"observations",
|
|
71
|
+
"confirmed",
|
|
72
|
+
"contradicted",
|
|
73
|
+
"unknown",
|
|
74
|
+
"notObserved"
|
|
75
|
+
],
|
|
76
|
+
"properties": {
|
|
77
|
+
"observations": { "type": "integer", "minimum": 0 },
|
|
78
|
+
"confirmed": { "type": "integer", "minimum": 0 },
|
|
79
|
+
"contradicted": { "type": "integer", "minimum": 0 },
|
|
80
|
+
"unknown": { "type": "integer", "minimum": 0 },
|
|
81
|
+
"notObserved": { "type": "integer", "minimum": 0 }
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
@@ -18,7 +18,8 @@
|
|
|
18
18
|
"findings",
|
|
19
19
|
"contradicted",
|
|
20
20
|
"unknown",
|
|
21
|
-
"notObserved"
|
|
21
|
+
"notObserved",
|
|
22
|
+
"subjectsWithoutEvidence"
|
|
22
23
|
],
|
|
23
24
|
"properties": {
|
|
24
25
|
"evidenceDocuments": { "type": "integer", "minimum": 0 },
|
|
@@ -27,12 +28,17 @@
|
|
|
27
28
|
"findings": { "type": "integer", "minimum": 0 },
|
|
28
29
|
"contradicted": { "type": "integer", "minimum": 0 },
|
|
29
30
|
"unknown": { "type": "integer", "minimum": 0 },
|
|
30
|
-
"notObserved": { "type": "integer", "minimum": 0 }
|
|
31
|
+
"notObserved": { "type": "integer", "minimum": 0 },
|
|
32
|
+
"subjectsWithoutEvidence": { "type": "integer", "minimum": 0 }
|
|
31
33
|
}
|
|
32
34
|
},
|
|
33
35
|
"findings": {
|
|
34
36
|
"type": "array",
|
|
35
37
|
"items": { "$ref": "#/$defs/finding" }
|
|
38
|
+
},
|
|
39
|
+
"unobservedSubjects": {
|
|
40
|
+
"type": "array",
|
|
41
|
+
"items": { "$ref": "#/$defs/subjectIdentity" }
|
|
36
42
|
}
|
|
37
43
|
},
|
|
38
44
|
"$defs": {
|
|
@@ -79,7 +79,8 @@
|
|
|
79
79
|
"findings",
|
|
80
80
|
"contradicted",
|
|
81
81
|
"unknown",
|
|
82
|
-
"notObserved"
|
|
82
|
+
"notObserved",
|
|
83
|
+
"subjectsWithoutEvidence"
|
|
83
84
|
],
|
|
84
85
|
"properties": {
|
|
85
86
|
"evidenceDocuments": {
|
|
@@ -109,6 +110,10 @@
|
|
|
109
110
|
"notObserved": {
|
|
110
111
|
"type": "integer",
|
|
111
112
|
"minimum": 0
|
|
113
|
+
},
|
|
114
|
+
"subjectsWithoutEvidence": {
|
|
115
|
+
"type": "integer",
|
|
116
|
+
"minimum": 0
|
|
112
117
|
}
|
|
113
118
|
}
|
|
114
119
|
},
|