yarramate 0.6.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 (38) hide show
  1. package/README.md +39 -12
  2. package/catalogues/core-enrichment.yaml +700 -0
  3. package/dist/adapters/mcp-cli.js +55 -49
  4. package/dist/apply-command.d.ts +2 -0
  5. package/dist/apply-command.js +199 -0
  6. package/dist/{new-command.d.ts → ask-command.d.ts} +1 -1
  7. package/dist/ask-command.js +729 -0
  8. package/dist/brief.d.ts +3 -0
  9. package/dist/brief.js +235 -0
  10. package/dist/cli-support.d.ts +1 -1
  11. package/dist/cli-support.js +1 -1
  12. package/dist/cli.js +22 -590
  13. package/dist/compiler.js +13 -0
  14. package/dist/core-contract.d.ts +1 -1
  15. package/dist/{status-command.d.ts → design-command.d.ts} +1 -1
  16. package/dist/design-command.js +218 -0
  17. package/dist/export-command.d.ts +2 -0
  18. package/dist/export-command.js +228 -0
  19. package/dist/interrogate-command.d.ts +104 -0
  20. package/dist/interrogate-command.js +268 -0
  21. package/dist/next-command.d.ts +5 -8
  22. package/dist/next-command.js +106 -211
  23. package/docs/CONSUMING-YARRAMATE.md +28 -21
  24. package/package.json +13 -11
  25. package/schema/yarramate-apply-result.schema.json +32 -0
  26. package/schema/yarramate-ask-result.schema.json +391 -0
  27. package/schema/yarramate-core-contract.schema.json +5 -11
  28. package/schema/yarramate-design-step.schema.json +152 -0
  29. package/schema/yarramate-document.schema.json +76 -9
  30. package/schema/yarramate-interrogation-report.schema.json +89 -0
  31. package/schema/yarramate-operations.schema.json +303 -0
  32. package/schema/yarramate-question-catalogue.schema.json +431 -0
  33. package/skills/yarramate-architecture/SKILL.md +42 -17
  34. package/skills/yarramate-architecture/references/native-authoring.md +73 -17
  35. package/dist/new-command.js +0 -111
  36. package/dist/status-command.js +0 -174
  37. package/schema/yarramate-next-result.schema.json +0 -85
  38. package/schema/yarramate-status-result.schema.json +0 -278
@@ -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
+ }
@@ -1,4 +1,6 @@
1
- import { type CliResult } from './cli-support.js';
1
+ import { type ResolvedProfileContext, type SemanticGraph } from './compiler.js';
2
+ import { type EvidenceReport } from './evidence.js';
3
+ import { type ProjectionResult } from './projection.js';
2
4
  interface EvidenceCoverage {
3
5
  readonly observations: number;
4
6
  readonly confirmed: number;
@@ -15,11 +17,6 @@ export interface NextSubject {
15
17
  readonly evidence: EvidenceCoverage;
16
18
  readonly cycle?: true;
17
19
  }
18
- export interface NextResult {
19
- readonly format: 'yarramate/next-result/v1';
20
- readonly workspace: string;
21
- readonly projection: string;
22
- readonly subjects: readonly NextSubject[];
23
- }
24
- export declare function runNextCommand(options: readonly string[], cwd: string): CliResult;
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[];
25
22
  export {};
@@ -1,11 +1,3 @@
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
1
  // Which endpoint of a declared relationship must exist before the other,
10
2
  // read off each core kind's declared intent (ADR 0048). Kinds without a
11
3
  // build-order reading (association, assignment, influence) contribute no
@@ -41,7 +33,7 @@ const claimValue = (claims, subject, predicate) => {
41
33
  return object !== undefined && 'value' in object ? object.value : undefined;
42
34
  };
43
35
  const plural = (count, singular) => `${count} ${count === 1 ? singular : `${singular}s`}`;
44
- const coverageClause = (coverage) => {
36
+ export const coverageClause = (coverage) => {
45
37
  if (coverage.observations === 0)
46
38
  return 'no evidence';
47
39
  const parts = [
@@ -56,217 +48,120 @@ const coverageClause = (coverage) => {
56
48
  ];
57
49
  return `${plural(coverage.observations, 'observation')} (${parts.join(', ')})`;
58
50
  };
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));
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;
140
77
  }
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) {
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()) {
160
94
  ordered.push(id);
161
- emitted.add(id);
162
- remaining.delete(id);
95
+ cycles.add(id);
163
96
  }
97
+ break;
164
98
  }
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
- }
99
+ for (const id of ready) {
100
+ ordered.push(id);
101
+ emitted.add(id);
102
+ remaining.delete(id);
175
103
  }
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
- });
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]);
198
114
  }
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
- }
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];
214
126
  }
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
- };
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,
226
137
  });
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
- };
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
+ }
247
153
  }
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
- ];
154
+ }
155
+ return ordered.map((id) => {
156
+ const name = claimValue(result.claims, id, 'yarramate/concept/name');
262
157
  return {
263
- exitCode: 0,
264
- stdout: `${lines.join('\n')}\n`,
265
- stderr: '',
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 } : {}),
266
165
  };
267
- }
268
- catch (error) {
269
- const message = error instanceof Error ? error.message : String(error);
270
- return { exitCode: 2, stdout: '', stderr: `${message}\n` };
271
- }
166
+ });
272
167
  }