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.
- package/README.md +39 -12
- package/catalogues/core-enrichment.yaml +700 -0
- 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/cli-support.d.ts +1 -1
- package/dist/cli-support.js +1 -1
- package/dist/cli.js +22 -590
- package/dist/compiler.js +13 -0
- 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 +5 -8
- package/dist/next-command.js +106 -211
- package/docs/CONSUMING-YARRAMATE.md +28 -21
- package/package.json +13 -11
- package/schema/yarramate-apply-result.schema.json +32 -0
- package/schema/yarramate-ask-result.schema.json +391 -0
- package/schema/yarramate-core-contract.schema.json +5 -11
- 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-operations.schema.json +303 -0
- package/schema/yarramate-question-catalogue.schema.json +431 -0
- 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 -174
- package/schema/yarramate-next-result.schema.json +0 -85
- 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
|
+
}
|
package/dist/next-command.d.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import { type
|
|
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
|
|
19
|
-
|
|
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 {};
|
package/dist/next-command.js
CHANGED
|
@@ -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
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
if (
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
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
|
-
|
|
162
|
-
remaining.delete(id);
|
|
95
|
+
cycles.add(id);
|
|
163
96
|
}
|
|
97
|
+
break;
|
|
164
98
|
}
|
|
165
|
-
const
|
|
166
|
-
.
|
|
167
|
-
.
|
|
168
|
-
|
|
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
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
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
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
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
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
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
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
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
|
-
|
|
249
|
-
|
|
250
|
-
|
|
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
|
-
|
|
264
|
-
|
|
265
|
-
|
|
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
|
}
|