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
|
@@ -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/profile.d.ts
CHANGED
|
@@ -21,6 +21,11 @@ export interface RelationshipPolicy {
|
|
|
21
21
|
readonly intent: string;
|
|
22
22
|
readonly sourceAspects?: readonly Aspect[];
|
|
23
23
|
readonly targetAspects?: readonly Aspect[];
|
|
24
|
+
/**
|
|
25
|
+
* Repair hint appended to endpoint-aspect diagnostics (YM404). A remedy,
|
|
26
|
+
* never a correction: the compiler still rejects the input.
|
|
27
|
+
*/
|
|
28
|
+
readonly repair?: string;
|
|
24
29
|
}
|
|
25
30
|
/**
|
|
26
31
|
* Safe, intentionally broad semantic constraints. A future licensed
|
package/dist/profile.js
CHANGED
|
@@ -137,6 +137,7 @@ export const relationshipPolicies = [
|
|
|
137
137
|
id: 'assignment',
|
|
138
138
|
intent: 'Allocate an active structure to behavior or responsibility',
|
|
139
139
|
sourceAspects: ['active-structure'],
|
|
140
|
+
repair: 'assign from an active-structure element (an actor, component, or node), or use "association"',
|
|
140
141
|
},
|
|
141
142
|
{ id: 'realization', intent: 'Fulfil a more abstract concept' },
|
|
142
143
|
{ id: 'serving', intent: 'Make behavior or an interface available' },
|
|
@@ -144,11 +145,13 @@ export const relationshipPolicies = [
|
|
|
144
145
|
id: 'access',
|
|
145
146
|
intent: 'Read, write, create, or use passive structure',
|
|
146
147
|
targetAspects: ['passive-structure'],
|
|
148
|
+
repair: 'point "access" at passive structure (a business object, data object, or artifact), or use "association"',
|
|
147
149
|
},
|
|
148
150
|
{
|
|
149
151
|
id: 'influence',
|
|
150
152
|
intent: 'Affect a motivation concept',
|
|
151
153
|
targetAspects: ['motivation'],
|
|
154
|
+
repair: 'point "influence" at a motivation concept (a goal, requirement, or principle), or use "association"',
|
|
152
155
|
},
|
|
153
156
|
{ id: 'association', intent: 'Relevant connection with no stronger meaning' },
|
|
154
157
|
{
|
|
@@ -156,6 +159,7 @@ export const relationshipPolicies = [
|
|
|
156
159
|
intent: 'Express temporal or causal precedence',
|
|
157
160
|
sourceAspects: ['behavior'],
|
|
158
161
|
targetAspects: ['behavior'],
|
|
162
|
+
repair: 'use "flow" between active-structure elements, or introduce a behavior concept and "assignment"',
|
|
159
163
|
},
|
|
160
164
|
{ id: 'flow', intent: 'Transfer information, value, goods, or material' },
|
|
161
165
|
{ id: 'specialization', intent: 'Express a more specific form' },
|
package/dist/reconciliation.d.ts
CHANGED
|
@@ -1,9 +1,17 @@
|
|
|
1
|
+
import type { SemanticGraph } from './compiler.js';
|
|
1
2
|
import type { EvidenceLocator, EvidenceReport, EvidenceResult } from './evidence.js';
|
|
3
|
+
export interface AssertedRelationship {
|
|
4
|
+
readonly from: string;
|
|
5
|
+
readonly to: string;
|
|
6
|
+
readonly kind: string;
|
|
7
|
+
readonly name?: string;
|
|
8
|
+
}
|
|
2
9
|
export interface ReconciliationFinding {
|
|
3
10
|
readonly target: {
|
|
4
11
|
readonly type: 'subject' | 'claim';
|
|
5
12
|
readonly id: string;
|
|
6
13
|
};
|
|
14
|
+
readonly asserted?: AssertedRelationship;
|
|
7
15
|
readonly result: Exclude<EvidenceResult, 'confirmed'>;
|
|
8
16
|
readonly provider: string;
|
|
9
17
|
readonly evidenceDocument: string;
|
|
@@ -20,7 +28,9 @@ export interface ReconciliationReport {
|
|
|
20
28
|
readonly contradicted: number;
|
|
21
29
|
readonly unknown: number;
|
|
22
30
|
readonly notObserved: number;
|
|
31
|
+
readonly subjectsWithoutEvidence: number;
|
|
23
32
|
};
|
|
24
33
|
readonly findings: readonly ReconciliationFinding[];
|
|
34
|
+
readonly unobservedSubjects?: readonly string[];
|
|
25
35
|
}
|
|
26
|
-
export declare function reconcileEvidenceReports(workspace: string, reports: readonly EvidenceReport[]): ReconciliationReport;
|
|
36
|
+
export declare function reconcileEvidenceReports(workspace: string, reports: readonly EvidenceReport[], graph?: SemanticGraph): ReconciliationReport;
|
package/dist/reconciliation.js
CHANGED
|
@@ -1,4 +1,68 @@
|
|
|
1
|
-
|
|
1
|
+
const assertedRelationshipsByClaim = (graph) => {
|
|
2
|
+
const asserted = new Map();
|
|
3
|
+
if (graph === undefined)
|
|
4
|
+
return asserted;
|
|
5
|
+
const relationshipIds = new Set(graph.subjects
|
|
6
|
+
.filter(({ type }) => type === 'relationship')
|
|
7
|
+
.map(({ id }) => id));
|
|
8
|
+
const names = new Map();
|
|
9
|
+
for (const claim of graph.claims) {
|
|
10
|
+
if (claim.predicate === 'yarramate/relationship/name' &&
|
|
11
|
+
'value' in claim.object) {
|
|
12
|
+
names.set(claim.subject, claim.object.value);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
for (const claim of graph.claims) {
|
|
16
|
+
if (!relationshipIds.has(claim.id) || !('ref' in claim.object))
|
|
17
|
+
continue;
|
|
18
|
+
const name = names.get(claim.id);
|
|
19
|
+
asserted.set(claim.id, {
|
|
20
|
+
from: claim.subject,
|
|
21
|
+
to: claim.object.ref,
|
|
22
|
+
kind: claim.predicate,
|
|
23
|
+
...(name === undefined ? {} : { name }),
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
return asserted;
|
|
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
|
+
};
|
|
63
|
+
export function reconcileEvidenceReports(workspace, reports, graph) {
|
|
64
|
+
const assertedByClaim = assertedRelationshipsByClaim(graph);
|
|
65
|
+
const unobservedSubjects = unobservedCurrentConcepts(graph, reports);
|
|
2
66
|
const summary = {
|
|
3
67
|
evidenceDocuments: reports.length,
|
|
4
68
|
observations: 0,
|
|
@@ -7,6 +71,7 @@ export function reconcileEvidenceReports(workspace, reports) {
|
|
|
7
71
|
contradicted: 0,
|
|
8
72
|
unknown: 0,
|
|
9
73
|
notObserved: 0,
|
|
74
|
+
subjectsWithoutEvidence: unobservedSubjects.length,
|
|
10
75
|
};
|
|
11
76
|
const findings = [];
|
|
12
77
|
for (const report of reports) {
|
|
@@ -22,10 +87,13 @@ export function reconcileEvidenceReports(workspace, reports) {
|
|
|
22
87
|
else {
|
|
23
88
|
summary[observation.result] += 1;
|
|
24
89
|
}
|
|
90
|
+
const target = 'subject' in observation
|
|
91
|
+
? { type: 'subject', id: observation.subject }
|
|
92
|
+
: { type: 'claim', id: observation.claim };
|
|
93
|
+
const asserted = target.type === 'claim' ? assertedByClaim.get(target.id) : undefined;
|
|
25
94
|
findings.push({
|
|
26
|
-
target
|
|
27
|
-
|
|
28
|
-
: { type: 'claim', id: observation.claim },
|
|
95
|
+
target,
|
|
96
|
+
...(asserted === undefined ? {} : { asserted }),
|
|
29
97
|
result: observation.result,
|
|
30
98
|
provider: report.provider,
|
|
31
99
|
evidenceDocument: report.evidence,
|
|
@@ -43,5 +111,6 @@ export function reconcileEvidenceReports(workspace, reports) {
|
|
|
43
111
|
workspace,
|
|
44
112
|
summary,
|
|
45
113
|
findings,
|
|
114
|
+
...(unobservedSubjects.length === 0 ? {} : { unobservedSubjects }),
|
|
46
115
|
};
|
|
47
116
|
}
|
package/dist/status-command.js
CHANGED
|
@@ -91,7 +91,7 @@ export function runStatusCommand(options, cwd) {
|
|
|
91
91
|
});
|
|
92
92
|
const evaluation = evaluateEvidenceWorkspace(compilation.graph, evidenceDocuments);
|
|
93
93
|
if (evaluation.ok) {
|
|
94
|
-
reconciliation = reconcileEvidenceReports(workspace.id, evaluation.reports).summary;
|
|
94
|
+
reconciliation = reconcileEvidenceReports(workspace.id, evaluation.reports, compilation.graph).summary;
|
|
95
95
|
}
|
|
96
96
|
}
|
|
97
97
|
}
|
|
@@ -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",
|
|
@@ -92,11 +93,11 @@
|
|
|
92
93
|
"self:contract": "pnpm build && node dist/cli.js context .yarramate/projections/core-contract-foundation.yaml .yarramate/workspace.yaml",
|
|
93
94
|
"self:evidence": "pnpm build && node dist/cli.js evidence .yarramate/evidence/repository.yaml .yarramate/workspace.yaml",
|
|
94
95
|
"self:reconcile": "pnpm build && node dist/cli.js reconcile .yarramate/workspace.yaml",
|
|
95
|
-
"self:check:likec4": "pnpm build && node dist/adapters/likec4-cli.js check .yarramate/
|
|
96
|
-
"self:check:likec4:json": "pnpm build && node dist/adapters/likec4-cli.js check .yarramate/
|
|
97
|
-
"self:export:likec4": "pnpm build && node dist/adapters/likec4-cli.js export-project .yarramate/
|
|
96
|
+
"self:check:likec4": "pnpm build && node dist/adapters/likec4-cli.js check .yarramate/likec4-project.yaml .yarramate/workspace.yaml",
|
|
97
|
+
"self:check:likec4:json": "pnpm build && node dist/adapters/likec4-cli.js check .yarramate/likec4-project.yaml --json .yarramate/workspace.yaml",
|
|
98
|
+
"self:export:likec4": "pnpm build && node dist/adapters/likec4-cli.js export-project .yarramate/likec4-project.yaml .yarramate-out/likec4 .yarramate/workspace.yaml",
|
|
98
99
|
"validate": "pnpm self:export:likec4 && likec4 validate --no-layout .yarramate-out/likec4",
|
|
99
|
-
"verify": "pnpm typecheck && pnpm test && pnpm self:check && pnpm validate",
|
|
100
|
+
"verify": "pnpm typecheck && pnpm build && pnpm test && pnpm self:check && pnpm validate",
|
|
100
101
|
"test": "vitest run",
|
|
101
102
|
"typecheck": "tsc --noEmit"
|
|
102
103
|
},
|
|
@@ -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,
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
},
|
|
42
42
|
"path": {
|
|
43
43
|
"type": "string",
|
|
44
|
-
"description": "
|
|
44
|
+
"description": "Path resolved from the project-definition document's directory, matching workspace-manifest semantics. Parent traversal, absolute paths, and backslashes are rejected, so references stay beneath the project document.",
|
|
45
45
|
"pattern": "^(?!/)(?!.*(?:^|/)\\.\\.(?:/|$))(?!.*\\\\).+$"
|
|
46
46
|
},
|
|
47
47
|
"subjectIdentity": {
|
|
@@ -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
|
+
}
|