yarramate 0.5.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +39 -10
- package/catalogues/core-enrichment.yaml +700 -0
- package/dist/adapters/likec4-cli.js +179 -55
- package/dist/adapters/likec4-export.d.ts +1 -1
- package/dist/adapters/likec4-prepare.d.ts +6 -0
- package/dist/adapters/likec4-prepare.js +40 -2
- package/dist/adapters/mcp-cli.js +55 -49
- package/dist/apply-command.d.ts +2 -0
- package/dist/apply-command.js +199 -0
- package/dist/{new-command.d.ts → ask-command.d.ts} +1 -1
- package/dist/ask-command.js +729 -0
- package/dist/brief.d.ts +3 -0
- package/dist/brief.js +235 -0
- package/dist/check-command.js +70 -4
- package/dist/cli-support.d.ts +4 -1
- package/dist/cli-support.js +3 -2
- package/dist/cli.js +22 -586
- package/dist/compiler.js +45 -7
- package/dist/core-contract.d.ts +1 -1
- package/dist/{status-command.d.ts → design-command.d.ts} +1 -1
- package/dist/design-command.js +218 -0
- package/dist/export-command.d.ts +2 -0
- package/dist/export-command.js +228 -0
- package/dist/interrogate-command.d.ts +104 -0
- package/dist/interrogate-command.js +268 -0
- package/dist/next-command.d.ts +22 -0
- package/dist/next-command.js +167 -0
- package/dist/reconciliation.d.ts +2 -0
- package/dist/reconciliation.js +38 -0
- package/docs/CONSUMING-YARRAMATE.md +28 -21
- package/package.json +13 -10
- package/schema/yarramate-apply-result.schema.json +32 -0
- package/schema/yarramate-ask-result.schema.json +391 -0
- package/schema/yarramate-check-result.schema.json +12 -0
- package/schema/yarramate-core-contract.schema.json +5 -10
- package/schema/yarramate-design-step.schema.json +152 -0
- package/schema/yarramate-document.schema.json +76 -9
- package/schema/yarramate-interrogation-report.schema.json +89 -0
- package/schema/yarramate-likec4-generated-project-v2.schema.json +6 -0
- package/schema/yarramate-likec4-generated-project.schema.json +6 -0
- package/schema/yarramate-operations.schema.json +303 -0
- package/schema/yarramate-question-catalogue.schema.json +431 -0
- package/schema/yarramate-reconciliation-report.schema.json +8 -2
- package/skills/yarramate-architecture/SKILL.md +42 -17
- package/skills/yarramate-architecture/references/native-authoring.md +73 -17
- package/dist/new-command.js +0 -111
- package/dist/status-command.js +0 -171
- package/schema/yarramate-status-result.schema.json +0 -273
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
import Ajv2020Module from 'ajv/dist/2020.js';
|
|
2
|
+
import { loadSourceDocument, locateSourcePath, } from './source-document.js';
|
|
3
|
+
import catalogueSchema from '../schema/yarramate-question-catalogue.schema.json' with {
|
|
4
|
+
type: 'json'
|
|
5
|
+
};
|
|
6
|
+
const Ajv2020 = Ajv2020Module.default;
|
|
7
|
+
const validateCatalogue = new Ajv2020({ allErrors: true }).compile(catalogueSchema);
|
|
8
|
+
const indexGraph = (graph) => {
|
|
9
|
+
const relationshipIds = new Set(graph.subjects
|
|
10
|
+
.filter(({ type }) => type === 'relationship')
|
|
11
|
+
.map(({ id }) => id));
|
|
12
|
+
const stateSubjects = new Set(graph.claims
|
|
13
|
+
.filter(({ predicate }) => predicate === 'yarramate/state/type')
|
|
14
|
+
.map(({ subject }) => subject));
|
|
15
|
+
// Architecture states carry concept subjects in the graph but are not
|
|
16
|
+
// enrichment targets; the catalogue interrogates the model, not the
|
|
17
|
+
// planning overlay.
|
|
18
|
+
const concepts = new Set(graph.subjects
|
|
19
|
+
.filter(({ id, type }) => type === 'concept' && !stateSubjects.has(id))
|
|
20
|
+
.map(({ id }) => id));
|
|
21
|
+
const claimsBySubject = new Map();
|
|
22
|
+
const kindOf = new Map();
|
|
23
|
+
const nameOf = new Map();
|
|
24
|
+
const statusOf = new Map();
|
|
25
|
+
const relationshipClaims = [];
|
|
26
|
+
const referenceClaims = [];
|
|
27
|
+
for (const claim of graph.claims) {
|
|
28
|
+
const forSubject = claimsBySubject.get(claim.subject);
|
|
29
|
+
if (forSubject === undefined) {
|
|
30
|
+
claimsBySubject.set(claim.subject, [claim]);
|
|
31
|
+
}
|
|
32
|
+
else {
|
|
33
|
+
forSubject.push(claim);
|
|
34
|
+
}
|
|
35
|
+
if (relationshipIds.has(claim.id) && 'ref' in claim.object) {
|
|
36
|
+
relationshipClaims.push(claim);
|
|
37
|
+
}
|
|
38
|
+
else if ('ref' in claim.object) {
|
|
39
|
+
referenceClaims.push(claim);
|
|
40
|
+
}
|
|
41
|
+
if ('value' in claim.object) {
|
|
42
|
+
if (claim.predicate === 'yarramate/concept/kind') {
|
|
43
|
+
kindOf.set(claim.subject, claim.object.value);
|
|
44
|
+
}
|
|
45
|
+
else if (claim.predicate === 'yarramate/concept/name') {
|
|
46
|
+
nameOf.set(claim.subject, claim.object.value);
|
|
47
|
+
}
|
|
48
|
+
else if (claim.predicate === 'yarramate/lifecycle/status') {
|
|
49
|
+
statusOf.set(claim.subject, claim.object.value);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return {
|
|
54
|
+
concepts,
|
|
55
|
+
claimsBySubject,
|
|
56
|
+
relationshipClaims,
|
|
57
|
+
referenceClaims,
|
|
58
|
+
kindOf,
|
|
59
|
+
nameOf,
|
|
60
|
+
statusOf,
|
|
61
|
+
hasStates: stateSubjects.size > 0,
|
|
62
|
+
};
|
|
63
|
+
};
|
|
64
|
+
const kindMatches = (subjectKind, selectedKinds, matching, profileContext) => {
|
|
65
|
+
if (subjectKind === undefined)
|
|
66
|
+
return false;
|
|
67
|
+
return selectedKinds.some((selected) => selected === subjectKind ||
|
|
68
|
+
(matching === 'descendants' &&
|
|
69
|
+
profileContext?.conceptKindLineages
|
|
70
|
+
.get(subjectKind)
|
|
71
|
+
?.includes(selected) === true));
|
|
72
|
+
};
|
|
73
|
+
const selectSubjects = (index, selector, profileContext) => {
|
|
74
|
+
// The schema's declared default for kindMatching is descendants, so a
|
|
75
|
+
// profile-derived kind satisfies a catalogue written against its parent.
|
|
76
|
+
const matching = selector.kindMatching ?? 'descendants';
|
|
77
|
+
let ids = [...index.concepts].filter((id) => kindMatches(index.kindOf.get(id), selector.kinds, matching, profileContext));
|
|
78
|
+
if (selector.statuses !== undefined) {
|
|
79
|
+
const statuses = new Set(selector.statuses);
|
|
80
|
+
ids = ids.filter((id) => {
|
|
81
|
+
const status = index.statusOf.get(id);
|
|
82
|
+
return status !== undefined && statuses.has(status);
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
if (selector.documents !== undefined) {
|
|
86
|
+
const documents = new Set(selector.documents);
|
|
87
|
+
ids = ids.filter((id) => documents.has(id.slice(0, id.indexOf('#'))));
|
|
88
|
+
}
|
|
89
|
+
return ids.sort((left, right) => left.localeCompare(right));
|
|
90
|
+
};
|
|
91
|
+
const relationshipKindMatches = (predicate, selectedKinds, matching, profileContext) => selectedKinds.some((selected) => selected === predicate ||
|
|
92
|
+
(matching === 'descendants' &&
|
|
93
|
+
profileContext?.relationshipKindLineages
|
|
94
|
+
.get(predicate)
|
|
95
|
+
?.includes(selected) === true));
|
|
96
|
+
const conditionHolds = (index, condition, subjectId, profileContext) => {
|
|
97
|
+
switch (condition.condition) {
|
|
98
|
+
case 'missing-claim':
|
|
99
|
+
return !(index.claimsBySubject.get(subjectId) ?? []).some(({ predicate }) => predicate === condition.predicate);
|
|
100
|
+
case 'missing-relationship': {
|
|
101
|
+
// Relationship kinds resolve through profile lineage by default, the
|
|
102
|
+
// same rule as selectors: a catalogue written against core kinds must
|
|
103
|
+
// see a profile-derived kind such as implements (realization child).
|
|
104
|
+
const matching = condition.kindMatching ?? 'descendants';
|
|
105
|
+
const touching = index.relationshipClaims.filter(({ predicate }) => relationshipKindMatches(predicate, condition.kinds, matching, profileContext));
|
|
106
|
+
const outgoing = touching.some(({ subject }) => subject === subjectId);
|
|
107
|
+
const incoming = touching.some(({ object }) => 'ref' in object && object.ref === subjectId);
|
|
108
|
+
if (condition.direction === 'outgoing')
|
|
109
|
+
return !outgoing;
|
|
110
|
+
if (condition.direction === 'incoming')
|
|
111
|
+
return !incoming;
|
|
112
|
+
return !outgoing && !incoming;
|
|
113
|
+
}
|
|
114
|
+
case 'isolated':
|
|
115
|
+
// Participation includes reference-bearing claims (ownership,
|
|
116
|
+
// constraints, identified references), not only relationships.
|
|
117
|
+
return (!index.relationshipClaims.some(({ subject, object }) => subject === subjectId ||
|
|
118
|
+
('ref' in object && object.ref === subjectId)) &&
|
|
119
|
+
!index.referenceClaims.some(({ object }) => 'ref' in object && object.ref === subjectId));
|
|
120
|
+
case 'no-subject-of-kind':
|
|
121
|
+
return ![...index.concepts].some((id) => condition.kinds.includes(index.kindOf.get(id) ?? ''));
|
|
122
|
+
case 'no-state-defined':
|
|
123
|
+
return !index.hasStates;
|
|
124
|
+
case 'missing-linkage': {
|
|
125
|
+
// The linkage-depth primitive: the subject lacks a relationship of
|
|
126
|
+
// these kinds, in this direction, whose counterpart is of one of
|
|
127
|
+
// these kinds. Both relationship and counterpart kinds resolve
|
|
128
|
+
// through profile lineage by default, matching the selector rule.
|
|
129
|
+
const matching = condition.kindMatching ?? 'descendants';
|
|
130
|
+
return !index.relationshipClaims.some((claim) => {
|
|
131
|
+
if (!('ref' in claim.object))
|
|
132
|
+
return false;
|
|
133
|
+
if (!relationshipKindMatches(claim.predicate, condition.kinds, matching, profileContext)) {
|
|
134
|
+
return false;
|
|
135
|
+
}
|
|
136
|
+
const counterpart = condition.direction === 'outgoing'
|
|
137
|
+
? claim.subject === subjectId
|
|
138
|
+
? claim.object.ref
|
|
139
|
+
: undefined
|
|
140
|
+
: claim.object.ref === subjectId
|
|
141
|
+
? claim.subject
|
|
142
|
+
: undefined;
|
|
143
|
+
return (counterpart !== undefined &&
|
|
144
|
+
kindMatches(index.kindOf.get(counterpart), condition.counterpartKinds, matching, profileContext));
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
case 'missing-reference':
|
|
148
|
+
return !index.referenceClaims.some((claim) => claim.predicate === condition.predicate &&
|
|
149
|
+
'ref' in claim.object &&
|
|
150
|
+
(condition.direction === 'outgoing'
|
|
151
|
+
? claim.subject === subjectId
|
|
152
|
+
: claim.object.ref === subjectId));
|
|
153
|
+
case 'missing-attestation':
|
|
154
|
+
return !(index.claimsBySubject.get(subjectId) ?? []).some(({ predicate }) => predicate === `yarramate/attestation/${condition.topic}`);
|
|
155
|
+
}
|
|
156
|
+
};
|
|
157
|
+
const renderQuestion = (template, subjectId, subjectName) => template
|
|
158
|
+
.trim()
|
|
159
|
+
.replaceAll('{subject.name}', subjectName ?? subjectId)
|
|
160
|
+
.replaceAll('{subject.id}', subjectId);
|
|
161
|
+
export function evaluateCatalogue(catalogue, graph, profileContext) {
|
|
162
|
+
const index = indexGraph(graph);
|
|
163
|
+
let open = 0;
|
|
164
|
+
let openQuestions = 0;
|
|
165
|
+
const waves = catalogue.waves.map((wave) => ({
|
|
166
|
+
id: wave.id,
|
|
167
|
+
name: wave.name,
|
|
168
|
+
questions: catalogue.questions
|
|
169
|
+
.filter((question) => question.wave === wave.id)
|
|
170
|
+
.map((question) => {
|
|
171
|
+
const base = {
|
|
172
|
+
id: question.id,
|
|
173
|
+
scope: question.scope,
|
|
174
|
+
authority: question.authority,
|
|
175
|
+
question: question.question.trim(),
|
|
176
|
+
materiality: question.materiality.trim(),
|
|
177
|
+
resolution: question.resolution.trim(),
|
|
178
|
+
};
|
|
179
|
+
if (question.scope === 'workspace') {
|
|
180
|
+
const isOpen = question.trigger.every((condition) => conditionHolds(index, condition, undefined, profileContext));
|
|
181
|
+
if (isOpen) {
|
|
182
|
+
open += 1;
|
|
183
|
+
openQuestions += 1;
|
|
184
|
+
}
|
|
185
|
+
return { ...base, open: isOpen };
|
|
186
|
+
}
|
|
187
|
+
const matches = selectSubjects(index, question.subjects, profileContext).filter((id) => question.trigger.every((condition) => conditionHolds(index, condition, id, profileContext)));
|
|
188
|
+
if (matches.length === 0) {
|
|
189
|
+
return { ...base, open: false };
|
|
190
|
+
}
|
|
191
|
+
open += matches.length;
|
|
192
|
+
openQuestions += 1;
|
|
193
|
+
return {
|
|
194
|
+
...base,
|
|
195
|
+
open: true,
|
|
196
|
+
subjects: matches.map((id) => {
|
|
197
|
+
const name = index.nameOf.get(id);
|
|
198
|
+
return {
|
|
199
|
+
id,
|
|
200
|
+
...(name === undefined ? {} : { name }),
|
|
201
|
+
question: renderQuestion(question.question, id, name),
|
|
202
|
+
};
|
|
203
|
+
}),
|
|
204
|
+
};
|
|
205
|
+
}),
|
|
206
|
+
}));
|
|
207
|
+
return {
|
|
208
|
+
format: 'yarramate/interrogation-report/v1',
|
|
209
|
+
catalogue: `${catalogue.id}@${catalogue.version}`,
|
|
210
|
+
summary: {
|
|
211
|
+
questions: catalogue.questions.length,
|
|
212
|
+
openQuestions,
|
|
213
|
+
open,
|
|
214
|
+
},
|
|
215
|
+
waves,
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
// Shared by interrogate and design: schema validation plus the YM911
|
|
219
|
+
// undeclared-wave check, both source-located against the catalogue file.
|
|
220
|
+
export function loadQuestionCatalogue(catalogueSource) {
|
|
221
|
+
const loadedCatalogue = loadSourceDocument(catalogueSource, validateCatalogue, 'Question catalogue');
|
|
222
|
+
if (!loadedCatalogue.ok) {
|
|
223
|
+
return { ok: false, diagnostics: loadedCatalogue.diagnostics };
|
|
224
|
+
}
|
|
225
|
+
const catalogue = loadedCatalogue.document.value;
|
|
226
|
+
const waveIds = new Set(catalogue.waves.map(({ id }) => id));
|
|
227
|
+
const waveDiagnostics = catalogue.questions.flatMap((question, questionIndex) => waveIds.has(question.wave)
|
|
228
|
+
? []
|
|
229
|
+
: [
|
|
230
|
+
{
|
|
231
|
+
severity: 'error',
|
|
232
|
+
code: 'YM911',
|
|
233
|
+
message: `Question "${question.id}" references undeclared wave "${question.wave}"`,
|
|
234
|
+
...locateSourcePath(catalogueSource.path, loadedCatalogue.document.yaml, loadedCatalogue.document.lineCounter, ['questions', questionIndex, 'wave'], `/questions/${questionIndex}/wave`),
|
|
235
|
+
},
|
|
236
|
+
]);
|
|
237
|
+
if (waveDiagnostics.length > 0) {
|
|
238
|
+
return { ok: false, diagnostics: waveDiagnostics };
|
|
239
|
+
}
|
|
240
|
+
return { ok: true, catalogue };
|
|
241
|
+
}
|
|
242
|
+
// Shared by interrogate and `ask --open`: the wave-by-wave human report.
|
|
243
|
+
export function renderInterrogationReport(report) {
|
|
244
|
+
const lines = [
|
|
245
|
+
`Catalogue ${report.catalogue} on workspace ${report.workspace}: ` +
|
|
246
|
+
`${report.summary.open} open ` +
|
|
247
|
+
`(${report.summary.openQuestions} of ${report.summary.questions} questions)`,
|
|
248
|
+
];
|
|
249
|
+
for (const wave of report.waves) {
|
|
250
|
+
lines.push('', `== ${wave.name} ==`);
|
|
251
|
+
for (const question of wave.questions) {
|
|
252
|
+
if (!question.open) {
|
|
253
|
+
lines.push(` closed ${question.id}`);
|
|
254
|
+
continue;
|
|
255
|
+
}
|
|
256
|
+
if (question.subjects === undefined) {
|
|
257
|
+
lines.push(` OPEN ${question.id} — ${question.question}`);
|
|
258
|
+
lines.push(` why: ${question.materiality}`);
|
|
259
|
+
continue;
|
|
260
|
+
}
|
|
261
|
+
lines.push(` OPEN ${question.id} (${question.subjects.length} ${question.subjects.length === 1 ? 'subject' : 'subjects'})`);
|
|
262
|
+
for (const subject of question.subjects) {
|
|
263
|
+
lines.push(` ask: "${subject.question}" [authority: ${question.authority}]`);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
return `${lines.join('\n')}\n`;
|
|
268
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { type ResolvedProfileContext, type SemanticGraph } from './compiler.js';
|
|
2
|
+
import { type EvidenceReport } from './evidence.js';
|
|
3
|
+
import { type ProjectionResult } from './projection.js';
|
|
4
|
+
interface EvidenceCoverage {
|
|
5
|
+
readonly observations: number;
|
|
6
|
+
readonly confirmed: number;
|
|
7
|
+
readonly contradicted: number;
|
|
8
|
+
readonly unknown: number;
|
|
9
|
+
readonly notObserved: number;
|
|
10
|
+
}
|
|
11
|
+
export interface NextSubject {
|
|
12
|
+
readonly id: string;
|
|
13
|
+
readonly kind: string;
|
|
14
|
+
readonly name?: string;
|
|
15
|
+
readonly dependsOn: readonly string[];
|
|
16
|
+
readonly requiredBy: readonly string[];
|
|
17
|
+
readonly evidence: EvidenceCoverage;
|
|
18
|
+
readonly cycle?: true;
|
|
19
|
+
}
|
|
20
|
+
export declare const coverageClause: (coverage: EvidenceCoverage) => string;
|
|
21
|
+
export declare function buildNextSubjects(result: ProjectionResult, graph: SemanticGraph, profileContext: ResolvedProfileContext | undefined, reports: readonly EvidenceReport[]): readonly NextSubject[];
|
|
22
|
+
export {};
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
// Which endpoint of a declared relationship must exist before the other,
|
|
2
|
+
// read off each core kind's declared intent (ADR 0048). Kinds without a
|
|
3
|
+
// build-order reading (association, assignment, influence) contribute no
|
|
4
|
+
// ordering edge rather than a guessed one.
|
|
5
|
+
const corePrerequisiteEndpoints = new Map([
|
|
6
|
+
['realization', 'source'],
|
|
7
|
+
['serving', 'source'],
|
|
8
|
+
['triggering', 'source'],
|
|
9
|
+
['flow', 'source'],
|
|
10
|
+
['composition', 'target'],
|
|
11
|
+
['aggregation', 'target'],
|
|
12
|
+
['access', 'target'],
|
|
13
|
+
['specialization', 'target'],
|
|
14
|
+
]);
|
|
15
|
+
const prerequisiteEndpoint = (kind, profileContext) => {
|
|
16
|
+
const candidates = [
|
|
17
|
+
kind,
|
|
18
|
+
...(profileContext?.relationshipKindLineages.get(kind) ?? []),
|
|
19
|
+
];
|
|
20
|
+
for (const candidate of candidates) {
|
|
21
|
+
const separator = candidate.indexOf('#');
|
|
22
|
+
if (separator === -1 || !candidate.startsWith('yarramate/core@')) {
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
const orientation = corePrerequisiteEndpoints.get(candidate.slice(separator + 1));
|
|
26
|
+
if (orientation !== undefined)
|
|
27
|
+
return orientation;
|
|
28
|
+
}
|
|
29
|
+
return undefined;
|
|
30
|
+
};
|
|
31
|
+
const claimValue = (claims, subject, predicate) => {
|
|
32
|
+
const object = claims.find((claim) => claim.subject === subject && claim.predicate === predicate)?.object;
|
|
33
|
+
return object !== undefined && 'value' in object ? object.value : undefined;
|
|
34
|
+
};
|
|
35
|
+
const plural = (count, singular) => `${count} ${count === 1 ? singular : `${singular}s`}`;
|
|
36
|
+
export const coverageClause = (coverage) => {
|
|
37
|
+
if (coverage.observations === 0)
|
|
38
|
+
return 'no evidence';
|
|
39
|
+
const parts = [
|
|
40
|
+
...(coverage.confirmed > 0 ? [`${coverage.confirmed} confirmed`] : []),
|
|
41
|
+
...(coverage.contradicted > 0
|
|
42
|
+
? [`${coverage.contradicted} contradicted`]
|
|
43
|
+
: []),
|
|
44
|
+
...(coverage.unknown > 0 ? [`${coverage.unknown} unknown`] : []),
|
|
45
|
+
...(coverage.notObserved > 0
|
|
46
|
+
? [`${coverage.notObserved} not observed`]
|
|
47
|
+
: []),
|
|
48
|
+
];
|
|
49
|
+
return `${plural(coverage.observations, 'observation')} (${parts.join(', ')})`;
|
|
50
|
+
};
|
|
51
|
+
// The build-ordering core, shared by `next` and `ask`: planned subjects in
|
|
52
|
+
// deterministic Kahn order with dependency links and evidence coverage.
|
|
53
|
+
export function buildNextSubjects(result, graph, profileContext, reports) {
|
|
54
|
+
const planned = result.subjects
|
|
55
|
+
.filter(({ id, type }) => type === 'concept' &&
|
|
56
|
+
claimValue(result.claims, id, 'yarramate/lifecycle/status') ===
|
|
57
|
+
'planned')
|
|
58
|
+
.map(({ id }) => id);
|
|
59
|
+
const plannedIds = new Set(planned);
|
|
60
|
+
const dependsOn = new Map();
|
|
61
|
+
const requiredBy = new Map();
|
|
62
|
+
for (const subject of result.subjects) {
|
|
63
|
+
if (subject.type !== 'relationship')
|
|
64
|
+
continue;
|
|
65
|
+
const claim = result.claims.find(({ id, object }) => id === subject.id && 'ref' in object);
|
|
66
|
+
if (claim === undefined || !('ref' in claim.object))
|
|
67
|
+
continue;
|
|
68
|
+
const orientation = prerequisiteEndpoint(claim.predicate, profileContext);
|
|
69
|
+
if (orientation === undefined)
|
|
70
|
+
continue;
|
|
71
|
+
const prerequisite = orientation === 'source' ? claim.subject : claim.object.ref;
|
|
72
|
+
const dependent = orientation === 'source' ? claim.object.ref : claim.subject;
|
|
73
|
+
if (prerequisite === dependent ||
|
|
74
|
+
!plannedIds.has(prerequisite) ||
|
|
75
|
+
!plannedIds.has(dependent)) {
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
dependsOn.set(dependent, (dependsOn.get(dependent) ?? new Set()).add(prerequisite));
|
|
79
|
+
requiredBy.set(prerequisite, (requiredBy.get(prerequisite) ?? new Set()).add(dependent));
|
|
80
|
+
}
|
|
81
|
+
// Deterministic Kahn ordering: everything whose prerequisites are
|
|
82
|
+
// already emitted goes next, lexicographic within a round; a cycle
|
|
83
|
+
// cannot be ordered, so its members are appended sorted and marked.
|
|
84
|
+
const ordered = [];
|
|
85
|
+
const cycles = new Set();
|
|
86
|
+
const emitted = new Set();
|
|
87
|
+
const remaining = new Set([...planned].sort());
|
|
88
|
+
while (remaining.size > 0) {
|
|
89
|
+
const ready = [...remaining]
|
|
90
|
+
.filter((id) => [...(dependsOn.get(id) ?? [])].every((dependency) => emitted.has(dependency)))
|
|
91
|
+
.sort();
|
|
92
|
+
if (ready.length === 0) {
|
|
93
|
+
for (const id of [...remaining].sort()) {
|
|
94
|
+
ordered.push(id);
|
|
95
|
+
cycles.add(id);
|
|
96
|
+
}
|
|
97
|
+
break;
|
|
98
|
+
}
|
|
99
|
+
for (const id of ready) {
|
|
100
|
+
ordered.push(id);
|
|
101
|
+
emitted.add(id);
|
|
102
|
+
remaining.delete(id);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
const relationshipIds = new Set(graph.subjects
|
|
106
|
+
.filter(({ type }) => type === 'relationship')
|
|
107
|
+
.map(({ id }) => id));
|
|
108
|
+
const relationshipEndpoints = new Map();
|
|
109
|
+
const claimOwners = new Map();
|
|
110
|
+
for (const claim of graph.claims) {
|
|
111
|
+
claimOwners.set(claim.id, claim.subject);
|
|
112
|
+
if (relationshipIds.has(claim.id) && 'ref' in claim.object) {
|
|
113
|
+
relationshipEndpoints.set(claim.id, [claim.subject, claim.object.ref]);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
const coverageTargets = (observation) => {
|
|
117
|
+
const target = 'subject' in observation ? observation.subject : observation.claim;
|
|
118
|
+
const endpoints = relationshipEndpoints.get(target);
|
|
119
|
+
if (endpoints !== undefined)
|
|
120
|
+
return endpoints;
|
|
121
|
+
if ('claim' in observation) {
|
|
122
|
+
const owner = claimOwners.get(target);
|
|
123
|
+
if (owner === undefined)
|
|
124
|
+
return [];
|
|
125
|
+
return relationshipEndpoints.get(owner) ?? [owner];
|
|
126
|
+
}
|
|
127
|
+
return [target];
|
|
128
|
+
};
|
|
129
|
+
const coverage = new Map();
|
|
130
|
+
for (const id of planned) {
|
|
131
|
+
coverage.set(id, {
|
|
132
|
+
observations: 0,
|
|
133
|
+
confirmed: 0,
|
|
134
|
+
contradicted: 0,
|
|
135
|
+
unknown: 0,
|
|
136
|
+
notObserved: 0,
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
for (const report of reports) {
|
|
140
|
+
for (const observation of report.observations) {
|
|
141
|
+
for (const target of coverageTargets(observation)) {
|
|
142
|
+
const tally = coverage.get(target);
|
|
143
|
+
if (tally === undefined)
|
|
144
|
+
continue;
|
|
145
|
+
tally.observations += 1;
|
|
146
|
+
if (observation.result === 'not-observed') {
|
|
147
|
+
tally.notObserved += 1;
|
|
148
|
+
}
|
|
149
|
+
else {
|
|
150
|
+
tally[observation.result] += 1;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return ordered.map((id) => {
|
|
156
|
+
const name = claimValue(result.claims, id, 'yarramate/concept/name');
|
|
157
|
+
return {
|
|
158
|
+
id,
|
|
159
|
+
kind: claimValue(result.claims, id, 'yarramate/concept/kind') ?? 'unknown',
|
|
160
|
+
...(name === undefined ? {} : { name }),
|
|
161
|
+
dependsOn: [...(dependsOn.get(id) ?? [])].sort(),
|
|
162
|
+
requiredBy: [...(requiredBy.get(id) ?? [])].sort(),
|
|
163
|
+
evidence: coverage.get(id),
|
|
164
|
+
...(cycles.has(id) ? { cycle: true } : {}),
|
|
165
|
+
};
|
|
166
|
+
});
|
|
167
|
+
}
|
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
|
}
|
|
@@ -63,7 +63,19 @@ self-model, source, tests, and fixtures.
|
|
|
63
63
|
|
|
64
64
|
## Install the agent skill
|
|
65
65
|
|
|
66
|
-
|
|
66
|
+
For Claude Code, the repository is its own plugin marketplace:
|
|
67
|
+
|
|
68
|
+
```sh
|
|
69
|
+
/plugin marketplace add yarrasys/yarramate
|
|
70
|
+
/plugin install yarramate-architecture@yarramate
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
The marketplace entry points at `skills/yarramate-architecture` in this
|
|
74
|
+
repository, so the installed plugin is the canonical skill rather than a
|
|
75
|
+
copy. It declares no version, taking its version from the commit it was
|
|
76
|
+
installed from.
|
|
77
|
+
|
|
78
|
+
For other harnesses, use the agent-skills installer:
|
|
67
79
|
|
|
68
80
|
```sh
|
|
69
81
|
npx skills add yarrasys/yarramate --skill yarramate-architecture
|
|
@@ -91,15 +103,13 @@ The skill will inspect repository evidence, propose native documents, and run:
|
|
|
91
103
|
|
|
92
104
|
```sh
|
|
93
105
|
yarramate check .yarramate/workspace.yaml --json
|
|
94
|
-
yarramate evidence \
|
|
95
|
-
.yarramate/evidence/<evidence>.yaml \
|
|
96
|
-
.yarramate/workspace.yaml
|
|
97
106
|
yarramate reconcile .yarramate/workspace.yaml
|
|
98
|
-
yarramate
|
|
99
|
-
.yarramate/projections/<projection>.yaml
|
|
100
|
-
.yarramate/workspace.yaml
|
|
107
|
+
yarramate ask .yarramate/workspace.yaml \
|
|
108
|
+
.yarramate/projections/<projection>.yaml
|
|
101
109
|
```
|
|
102
110
|
|
|
111
|
+
Evidence overlays declared in the manifest are evaluated by `reconcile`
|
|
112
|
+
(and gated by `check --strict`) rather than through a separate command.
|
|
103
113
|
Evidence remains distinct from declared intent. A generated proposal becomes
|
|
104
114
|
canonical only through the consuming repository's normal Git review.
|
|
105
115
|
|
|
@@ -111,15 +121,12 @@ implementation context, then runs:
|
|
|
111
121
|
|
|
112
122
|
```sh
|
|
113
123
|
yarramate check .yarramate/workspace.yaml --json
|
|
114
|
-
yarramate
|
|
115
|
-
.yarramate/projections/<alternatives>.yaml
|
|
116
|
-
|
|
117
|
-
yarramate
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
yarramate compare \
|
|
121
|
-
<baseline-state> <target-state> \
|
|
122
|
-
.yarramate/workspace.yaml
|
|
124
|
+
yarramate ask .yarramate/workspace.yaml \
|
|
125
|
+
.yarramate/projections/<alternatives>.yaml
|
|
126
|
+
yarramate ask .yarramate/workspace.yaml \
|
|
127
|
+
.yarramate/projections/<target>.yaml
|
|
128
|
+
yarramate ask .yarramate/workspace.yaml \
|
|
129
|
+
--compare <baseline-state> <target-state>
|
|
123
130
|
```
|
|
124
131
|
|
|
125
132
|
The CLI verifies deterministic correctness. It does not approve the design or
|
|
@@ -158,11 +165,11 @@ Harnesses that load MCP servers can connect the bundled read-only adapter:
|
|
|
158
165
|
}
|
|
159
166
|
```
|
|
160
167
|
|
|
161
|
-
It exposes `
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
the server's working directory; nothing mutates native
|
|
165
|
-
authoring stays with the CLI and Git review.
|
|
168
|
+
It exposes `yarramate_ask` (orientation, free text, subject ids, or a
|
|
169
|
+
projection path, with an optional token budget), `yarramate_design`,
|
|
170
|
+
`yarramate_check`, and `yarramate_reconcile`. Every tool call executes the
|
|
171
|
+
same stable CLI in the server's working directory; nothing mutates native
|
|
172
|
+
documents, and authoring stays with the CLI and Git review.
|
|
166
173
|
|
|
167
174
|
## Continuous drift signal in CI
|
|
168
175
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "yarramate",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "Tool-neutral semantic architecture engine and guided methodology",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
"dist",
|
|
26
26
|
"assets/likec4",
|
|
27
27
|
"schema",
|
|
28
|
+
"catalogues",
|
|
28
29
|
"skills/yarramate-architecture",
|
|
29
30
|
"docs/CONSUMING-YARRAMATE.md"
|
|
30
31
|
],
|
|
@@ -58,14 +59,19 @@
|
|
|
58
59
|
"./schema/likec4-kind-mapping": "./schema/yarramate-likec4-kind-mapping.schema.json",
|
|
59
60
|
"./schema/likec4-check-result": "./schema/yarramate-likec4-check-result.schema.json",
|
|
60
61
|
"./schema/state-comparison": "./schema/yarramate-state-comparison.schema.json",
|
|
61
|
-
"./schema/
|
|
62
|
+
"./schema/question-catalogue": "./schema/yarramate-question-catalogue.schema.json",
|
|
63
|
+
"./schema/interrogation-report": "./schema/yarramate-interrogation-report.schema.json",
|
|
62
64
|
"./schema/graph-v2": "./schema/yarramate-graph-v2.schema.json",
|
|
63
65
|
"./schema/workspace": "./schema/yarramate-workspace.schema.json",
|
|
64
66
|
"./schema/evidence": "./schema/yarramate-evidence.schema.json",
|
|
65
67
|
"./schema/evidence-report": "./schema/yarramate-evidence-report.schema.json",
|
|
66
68
|
"./schema/reconciliation-report": "./schema/yarramate-reconciliation-report.schema.json",
|
|
67
69
|
"./schema/core-contract": "./schema/yarramate-core-contract.schema.json",
|
|
68
|
-
"./skill/yarramate-architecture": "./skills/yarramate-architecture/SKILL.md"
|
|
70
|
+
"./skill/yarramate-architecture": "./skills/yarramate-architecture/SKILL.md",
|
|
71
|
+
"./schema/operations": "./schema/yarramate-operations.schema.json",
|
|
72
|
+
"./schema/apply-result": "./schema/yarramate-apply-result.schema.json",
|
|
73
|
+
"./schema/design-step": "./schema/yarramate-design-step.schema.json",
|
|
74
|
+
"./schema/ask-result": "./schema/yarramate-ask-result.schema.json"
|
|
69
75
|
},
|
|
70
76
|
"bin": {
|
|
71
77
|
"yarramate": "dist/cli.js",
|
|
@@ -84,13 +90,10 @@
|
|
|
84
90
|
"format": "likec4 format assets/likec4",
|
|
85
91
|
"self:check": "pnpm build && node dist/cli.js check .yarramate/workspace.yaml",
|
|
86
92
|
"self:check:json": "pnpm build && node dist/cli.js check .yarramate/workspace.yaml --json",
|
|
87
|
-
"self:
|
|
88
|
-
"self:
|
|
89
|
-
"self:
|
|
90
|
-
"self:compare": "pnpm build && node dist/cli.js compare yarramate-evolution#native-foundation yarramate-evolution#state-foundation
|
|
91
|
-
"self:compare:contract": "pnpm build && node dist/cli.js compare yarramate-evolution#state-foundation yarramate-evolution#core-contract-foundation .yarramate/workspace.yaml",
|
|
92
|
-
"self:contract": "pnpm build && node dist/cli.js context .yarramate/projections/core-contract-foundation.yaml .yarramate/workspace.yaml",
|
|
93
|
-
"self:evidence": "pnpm build && node dist/cli.js evidence .yarramate/evidence/repository.yaml .yarramate/workspace.yaml",
|
|
93
|
+
"self:ask": "pnpm build && node dist/cli.js ask .yarramate/workspace.yaml",
|
|
94
|
+
"self:export:graph": "pnpm build && node dist/cli.js export graph .yarramate/workspace.yaml",
|
|
95
|
+
"self:export:markdown": "pnpm build && node dist/cli.js export markdown .yarramate/projections/current-engine.yaml .yarramate/workspace.yaml",
|
|
96
|
+
"self:compare": "pnpm build && node dist/cli.js ask .yarramate/workspace.yaml --compare yarramate-evolution#native-foundation yarramate-evolution#state-foundation",
|
|
94
97
|
"self:reconcile": "pnpm build && node dist/cli.js reconcile .yarramate/workspace.yaml",
|
|
95
98
|
"self:check:likec4": "pnpm build && node dist/adapters/likec4-cli.js check .yarramate/likec4-project.yaml .yarramate/workspace.yaml",
|
|
96
99
|
"self:check:likec4:json": "pnpm build && node dist/adapters/likec4-cli.js check .yarramate/likec4-project.yaml --json .yarramate/workspace.yaml",
|