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.
Files changed (48) hide show
  1. package/README.md +39 -10
  2. package/catalogues/core-enrichment.yaml +700 -0
  3. package/dist/adapters/likec4-cli.js +179 -55
  4. package/dist/adapters/likec4-export.d.ts +1 -1
  5. package/dist/adapters/likec4-prepare.d.ts +6 -0
  6. package/dist/adapters/likec4-prepare.js +40 -2
  7. package/dist/adapters/mcp-cli.js +55 -49
  8. package/dist/apply-command.d.ts +2 -0
  9. package/dist/apply-command.js +199 -0
  10. package/dist/{new-command.d.ts → ask-command.d.ts} +1 -1
  11. package/dist/ask-command.js +729 -0
  12. package/dist/brief.d.ts +3 -0
  13. package/dist/brief.js +235 -0
  14. package/dist/check-command.js +70 -4
  15. package/dist/cli-support.d.ts +4 -1
  16. package/dist/cli-support.js +3 -2
  17. package/dist/cli.js +22 -586
  18. package/dist/compiler.js +45 -7
  19. package/dist/core-contract.d.ts +1 -1
  20. package/dist/{status-command.d.ts → design-command.d.ts} +1 -1
  21. package/dist/design-command.js +218 -0
  22. package/dist/export-command.d.ts +2 -0
  23. package/dist/export-command.js +228 -0
  24. package/dist/interrogate-command.d.ts +104 -0
  25. package/dist/interrogate-command.js +268 -0
  26. package/dist/next-command.d.ts +22 -0
  27. package/dist/next-command.js +167 -0
  28. package/dist/reconciliation.d.ts +2 -0
  29. package/dist/reconciliation.js +38 -0
  30. package/docs/CONSUMING-YARRAMATE.md +28 -21
  31. package/package.json +13 -10
  32. package/schema/yarramate-apply-result.schema.json +32 -0
  33. package/schema/yarramate-ask-result.schema.json +391 -0
  34. package/schema/yarramate-check-result.schema.json +12 -0
  35. package/schema/yarramate-core-contract.schema.json +5 -10
  36. package/schema/yarramate-design-step.schema.json +152 -0
  37. package/schema/yarramate-document.schema.json +76 -9
  38. package/schema/yarramate-interrogation-report.schema.json +89 -0
  39. package/schema/yarramate-likec4-generated-project-v2.schema.json +6 -0
  40. package/schema/yarramate-likec4-generated-project.schema.json +6 -0
  41. package/schema/yarramate-operations.schema.json +303 -0
  42. package/schema/yarramate-question-catalogue.schema.json +431 -0
  43. package/schema/yarramate-reconciliation-report.schema.json +8 -2
  44. package/skills/yarramate-architecture/SKILL.md +42 -17
  45. package/skills/yarramate-architecture/references/native-authoring.md +73 -17
  46. package/dist/new-command.js +0 -111
  47. package/dist/status-command.js +0 -171
  48. 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
+ }
@@ -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;
@@ -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
- After the repository is public, use the agent-skills installer:
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 context \
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 context \
115
- .yarramate/projections/<alternatives>.yaml \
116
- .yarramate/workspace.yaml
117
- yarramate context \
118
- .yarramate/projections/<target>.yaml \
119
- .yarramate/workspace.yaml
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 `yarramate_status`, `yarramate_check`, `yarramate_reconcile`,
162
- and `yarramate_context` (projection path or ad-hoc subjects, with an
163
- optional token budget). Every tool call executes the same stable CLI in
164
- the server's working directory; nothing mutates native documents, and
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.5.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/status-result": "./schema/yarramate-status-result.schema.json",
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:compile": "pnpm build && node dist/cli.js compile .yarramate/workspace.yaml",
88
- "self:context": "pnpm build && node dist/cli.js context .yarramate/projections/current-engine.yaml .yarramate/workspace.yaml",
89
- "self:view": "pnpm build && node dist/cli.js view .yarramate/projections/current-engine.yaml .yarramate/workspace.yaml",
90
- "self:compare": "pnpm build && node dist/cli.js compare yarramate-evolution#native-foundation yarramate-evolution#state-foundation .yarramate/workspace.yaml",
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",