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,729 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { dirname, join, resolve } from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import { parseDocument } from 'yaml';
|
|
5
|
+
import { compareArchitectureStates, } from './architecture-state.js';
|
|
6
|
+
import { renderBrief } from './brief.js';
|
|
7
|
+
import { runCheckCommand } from './check-command.js';
|
|
8
|
+
import { diagnosticJson, humanDiagnostics, usage, } from './cli-support.js';
|
|
9
|
+
import { compileWorkspaceWithProfileContext, } from './compiler.js';
|
|
10
|
+
import { evaluateEvidenceWorkspace, loadEvidence } from './evidence.js';
|
|
11
|
+
import { evaluateCatalogue, loadQuestionCatalogue, renderInterrogationReport, } from './interrogate-command.js';
|
|
12
|
+
import { buildNextSubjects, coverageClause, } from './next-command.js';
|
|
13
|
+
import { conceptKinds, relationshipPolicies, } from './profile.js';
|
|
14
|
+
import { evaluateProjection, loadProjection, renderBudgetedContext, } from './projection.js';
|
|
15
|
+
import { reconcileEvidenceReports, } from './reconciliation.js';
|
|
16
|
+
import { loadWorkspaceManifest } from './workspace.js';
|
|
17
|
+
// The same internal catalogue design interviews from: ask reads what
|
|
18
|
+
// design asks, so both must see identical open questions.
|
|
19
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
20
|
+
const shippedCataloguePath = join(here, '..', 'catalogues', 'core-enrichment.yaml');
|
|
21
|
+
const claimValue = (claims, subject, predicate) => {
|
|
22
|
+
const object = claims.find((claim) => claim.subject === subject && claim.predicate === predicate)?.object;
|
|
23
|
+
return object !== undefined && 'value' in object ? object.value : undefined;
|
|
24
|
+
};
|
|
25
|
+
// The roster index: every model concept (never the planning states) with
|
|
26
|
+
// the fields free-text seeding matches against.
|
|
27
|
+
const conceptEntries = (graph) => {
|
|
28
|
+
const stateIds = new Set(graph.claims
|
|
29
|
+
.filter(({ predicate }) => predicate === 'yarramate/state/type')
|
|
30
|
+
.map(({ subject }) => subject));
|
|
31
|
+
return graph.subjects
|
|
32
|
+
.filter(({ id, type }) => type === 'concept' && !stateIds.has(id))
|
|
33
|
+
.map(({ id }) => {
|
|
34
|
+
const name = claimValue(graph.claims, id, 'yarramate/concept/name');
|
|
35
|
+
const status = claimValue(graph.claims, id, 'yarramate/lifecycle/status');
|
|
36
|
+
const description = claimValue(graph.claims, id, 'yarramate/concept/description');
|
|
37
|
+
return {
|
|
38
|
+
id,
|
|
39
|
+
kind: claimValue(graph.claims, id, 'yarramate/concept/kind') ?? 'unknown',
|
|
40
|
+
...(name === undefined ? {} : { name }),
|
|
41
|
+
...(status === undefined ? {} : { status }),
|
|
42
|
+
...(description === undefined ? {} : { description }),
|
|
43
|
+
};
|
|
44
|
+
})
|
|
45
|
+
.sort((left, right) => left.id.localeCompare(right.id));
|
|
46
|
+
};
|
|
47
|
+
const seedLimit = 5;
|
|
48
|
+
// Free text is the default addressing mode: terms match concept ids,
|
|
49
|
+
// names, and descriptions; matching concepts seed the slice. Exact
|
|
50
|
+
// subject ids short-circuit to precise addressing — the seeding finds
|
|
51
|
+
// what an explicit --subject flag would have named.
|
|
52
|
+
const resolveSeeds = (terms, entries) => {
|
|
53
|
+
const known = new Set(entries.map(({ id }) => id));
|
|
54
|
+
const unique = [...new Set(terms)];
|
|
55
|
+
if (unique.every((term) => known.has(term))) {
|
|
56
|
+
return { addressing: 'subjects', seeds: unique, matched: unique.length };
|
|
57
|
+
}
|
|
58
|
+
const lowered = [
|
|
59
|
+
...new Set(terms
|
|
60
|
+
.flatMap((term) => term.split(/\s+/))
|
|
61
|
+
.filter((term) => term.length > 0)
|
|
62
|
+
.map((term) => term.toLowerCase())),
|
|
63
|
+
];
|
|
64
|
+
const scored = entries
|
|
65
|
+
.map((entry) => {
|
|
66
|
+
const text = `${entry.id} ${entry.name ?? ''} ${entry.description ?? ''}`.toLowerCase();
|
|
67
|
+
return {
|
|
68
|
+
id: entry.id,
|
|
69
|
+
score: lowered.filter((term) => text.includes(term)).length,
|
|
70
|
+
};
|
|
71
|
+
})
|
|
72
|
+
.filter(({ score }) => score > 0)
|
|
73
|
+
.sort((left, right) => right.score - left.score || left.id.localeCompare(right.id));
|
|
74
|
+
return {
|
|
75
|
+
addressing: 'free-text',
|
|
76
|
+
seeds: scored.slice(0, seedLimit).map(({ id }) => id),
|
|
77
|
+
matched: scored.length,
|
|
78
|
+
};
|
|
79
|
+
};
|
|
80
|
+
const plural = (count, singular) => `${count} ${count === 1 ? singular : `${singular}s`}`;
|
|
81
|
+
const reconciliationLine = (summary) => `Reconciliation: ${plural(summary.observations, 'observation')}, ` +
|
|
82
|
+
`${summary.confirmed} confirmed, ` +
|
|
83
|
+
`${plural(summary.findings, 'finding')}` +
|
|
84
|
+
(summary.findings > 0
|
|
85
|
+
? ` (${summary.contradicted} contradicted, ` +
|
|
86
|
+
`${summary.unknown} unknown, ` +
|
|
87
|
+
`${summary.notObserved} not observed)`
|
|
88
|
+
: '') +
|
|
89
|
+
(summary.subjectsWithoutEvidence > 0
|
|
90
|
+
? `, ${plural(summary.subjectsWithoutEvidence, 'current subject')} without evidence`
|
|
91
|
+
: '');
|
|
92
|
+
const plannedLines = (subjects) => {
|
|
93
|
+
if (subjects.length === 0)
|
|
94
|
+
return [' none'];
|
|
95
|
+
const width = Math.max(...subjects.map(({ id }) => id.length));
|
|
96
|
+
return subjects.map((subject) => {
|
|
97
|
+
const clauses = [
|
|
98
|
+
...(subject.requiredBy.length > 0
|
|
99
|
+
? [`<- required by ${subject.requiredBy.join(', ')}`]
|
|
100
|
+
: []),
|
|
101
|
+
coverageClause(subject.evidence),
|
|
102
|
+
...(subject.cycle === true ? ['dependency cycle'] : []),
|
|
103
|
+
];
|
|
104
|
+
return ` ${subject.id.padEnd(width)} ${clauses.join('; ')}`;
|
|
105
|
+
});
|
|
106
|
+
};
|
|
107
|
+
// The one-hop connected neighbourhood every slice and advice mode uses:
|
|
108
|
+
// the same machinery context --subject exposed, now seeded by matching.
|
|
109
|
+
const sliceProjection = (graph, seeds, title, profileContext) => evaluateProjection(graph, {
|
|
110
|
+
format: 'yarramate/projection/v1',
|
|
111
|
+
id: 'ask-slice',
|
|
112
|
+
version: '0.0',
|
|
113
|
+
query: { subjects: [...seeds], relationships: 'connected' },
|
|
114
|
+
presentation: {
|
|
115
|
+
title,
|
|
116
|
+
description: `Connected neighbourhood of ${seeds.join(', ')}`,
|
|
117
|
+
},
|
|
118
|
+
}, profileContext);
|
|
119
|
+
export function runAskCommand(options, cwd) {
|
|
120
|
+
let json = false;
|
|
121
|
+
let subjects = false;
|
|
122
|
+
let next = false;
|
|
123
|
+
let open = false;
|
|
124
|
+
let kinds = false;
|
|
125
|
+
let advise = false;
|
|
126
|
+
let compare;
|
|
127
|
+
let budget;
|
|
128
|
+
let kindFilter;
|
|
129
|
+
let statusFilter;
|
|
130
|
+
let cataloguePath;
|
|
131
|
+
const positionals = [];
|
|
132
|
+
for (let index = 0; index < options.length; index += 1) {
|
|
133
|
+
const option = options[index];
|
|
134
|
+
if (option === '--json') {
|
|
135
|
+
json = true;
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
if (option === '--subjects') {
|
|
139
|
+
subjects = true;
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
if (option === '--next') {
|
|
143
|
+
next = true;
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
if (option === '--open') {
|
|
147
|
+
open = true;
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
if (option === '--kinds') {
|
|
151
|
+
kinds = true;
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
if (option === '--advise') {
|
|
155
|
+
advise = true;
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
if (option === '--compare') {
|
|
159
|
+
const from = options[index + 1];
|
|
160
|
+
const to = options[index + 2];
|
|
161
|
+
if (compare !== undefined ||
|
|
162
|
+
from === undefined ||
|
|
163
|
+
to === undefined ||
|
|
164
|
+
from.startsWith('-') ||
|
|
165
|
+
to.startsWith('-')) {
|
|
166
|
+
return { exitCode: 2, stdout: '', stderr: usage };
|
|
167
|
+
}
|
|
168
|
+
compare = [from, to];
|
|
169
|
+
index += 2;
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
if (option === '--budget' ||
|
|
173
|
+
option === '--kind' ||
|
|
174
|
+
option === '--status' ||
|
|
175
|
+
option === '--catalogue') {
|
|
176
|
+
const value = options[index + 1];
|
|
177
|
+
if (value === undefined || value.startsWith('-')) {
|
|
178
|
+
return { exitCode: 2, stdout: '', stderr: usage };
|
|
179
|
+
}
|
|
180
|
+
if (option === '--budget') {
|
|
181
|
+
if (budget !== undefined || !/^[1-9][0-9]*$/.test(value)) {
|
|
182
|
+
return { exitCode: 2, stdout: '', stderr: usage };
|
|
183
|
+
}
|
|
184
|
+
budget = Number(value);
|
|
185
|
+
}
|
|
186
|
+
else if (option === '--kind') {
|
|
187
|
+
if (kindFilter !== undefined) {
|
|
188
|
+
return { exitCode: 2, stdout: '', stderr: usage };
|
|
189
|
+
}
|
|
190
|
+
kindFilter = value;
|
|
191
|
+
}
|
|
192
|
+
else if (option === '--status') {
|
|
193
|
+
if (statusFilter !== undefined ||
|
|
194
|
+
!['planned', 'current', 'retired'].includes(value)) {
|
|
195
|
+
return { exitCode: 2, stdout: '', stderr: usage };
|
|
196
|
+
}
|
|
197
|
+
statusFilter = value;
|
|
198
|
+
}
|
|
199
|
+
else {
|
|
200
|
+
if (cataloguePath !== undefined) {
|
|
201
|
+
return { exitCode: 2, stdout: '', stderr: usage };
|
|
202
|
+
}
|
|
203
|
+
cataloguePath = value;
|
|
204
|
+
}
|
|
205
|
+
index += 1;
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
if (option === undefined || option.startsWith('-')) {
|
|
209
|
+
return { exitCode: 2, stdout: '', stderr: usage };
|
|
210
|
+
}
|
|
211
|
+
positionals.push(option);
|
|
212
|
+
}
|
|
213
|
+
const [workspacePath, ...query] = positionals;
|
|
214
|
+
const exclusiveModes = [
|
|
215
|
+
subjects,
|
|
216
|
+
next,
|
|
217
|
+
open,
|
|
218
|
+
kinds,
|
|
219
|
+
compare !== undefined,
|
|
220
|
+
].filter(Boolean).length;
|
|
221
|
+
if (workspacePath === undefined ||
|
|
222
|
+
exclusiveModes > 1 ||
|
|
223
|
+
(advise && exclusiveModes > 0) ||
|
|
224
|
+
(advise && query.length === 0) ||
|
|
225
|
+
(query.length > 0 && exclusiveModes > 0) ||
|
|
226
|
+
((kindFilter !== undefined || statusFilter !== undefined) && !subjects) ||
|
|
227
|
+
(cataloguePath !== undefined && !open && !advise) ||
|
|
228
|
+
(budget !== undefined && (json || (query.length === 0 && !advise)))) {
|
|
229
|
+
return { exitCode: 2, stdout: '', stderr: usage };
|
|
230
|
+
}
|
|
231
|
+
try {
|
|
232
|
+
const manifestSource = readFileSync(resolve(cwd, workspacePath), 'utf8');
|
|
233
|
+
if (parseDocument(manifestSource).get('format') !== 'yarramate/workspace/v1') {
|
|
234
|
+
return {
|
|
235
|
+
exitCode: 2,
|
|
236
|
+
stdout: '',
|
|
237
|
+
stderr: 'ask requires an explicit workspace manifest (yarramate/workspace/v1)\n',
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
const failed = (diagnostics) => ({
|
|
241
|
+
exitCode: 1,
|
|
242
|
+
stdout: json
|
|
243
|
+
? diagnosticJson(diagnostics)
|
|
244
|
+
: humanDiagnostics(diagnostics),
|
|
245
|
+
stderr: '',
|
|
246
|
+
});
|
|
247
|
+
const loadedWorkspace = loadWorkspaceManifest({ path: workspacePath, source: manifestSource }, cwd);
|
|
248
|
+
if (!loadedWorkspace.ok)
|
|
249
|
+
return failed(loadedWorkspace.diagnostics);
|
|
250
|
+
const workspace = loadedWorkspace.workspace;
|
|
251
|
+
const emit = (result, human, exitCode = 0) => json
|
|
252
|
+
? {
|
|
253
|
+
exitCode,
|
|
254
|
+
stdout: `${JSON.stringify(result, null, 2)}\n`,
|
|
255
|
+
stderr: '',
|
|
256
|
+
}
|
|
257
|
+
: { exitCode, stdout: human, stderr: '' };
|
|
258
|
+
// Orientation is the only mode that reports on a failing model rather
|
|
259
|
+
// than failing with it: the verdict is the content.
|
|
260
|
+
if (query.length === 0 &&
|
|
261
|
+
!subjects &&
|
|
262
|
+
!next &&
|
|
263
|
+
!open &&
|
|
264
|
+
!kinds &&
|
|
265
|
+
!advise &&
|
|
266
|
+
compare === undefined) {
|
|
267
|
+
const checked = runCheckCommand([workspacePath, '--json'], cwd);
|
|
268
|
+
const checkPayload = JSON.parse(checked.stdout);
|
|
269
|
+
if (!checkPayload.ok) {
|
|
270
|
+
const result = {
|
|
271
|
+
format: 'yarramate/ask-result/v1',
|
|
272
|
+
workspace: workspace.id,
|
|
273
|
+
mode: 'orientation',
|
|
274
|
+
ok: false,
|
|
275
|
+
check: { ok: false, diagnostics: checkPayload.diagnostics },
|
|
276
|
+
backlog: { planned: [], current: [], retired: [] },
|
|
277
|
+
};
|
|
278
|
+
return emit(result, `Workspace ${workspace.id}: check failing\n` +
|
|
279
|
+
`Diagnostics: ${plural(checkPayload.diagnostics.length, 'error')}; run \`yarramate check ${workspacePath}\` for details\n`, 1);
|
|
280
|
+
}
|
|
281
|
+
const compilation = compileWorkspaceWithProfileContext([...workspace.profiles, ...workspace.documents].map((path) => ({
|
|
282
|
+
path,
|
|
283
|
+
source: readFileSync(resolve(cwd, path), 'utf8'),
|
|
284
|
+
})));
|
|
285
|
+
if (!compilation.ok)
|
|
286
|
+
return failed(compilation.diagnostics);
|
|
287
|
+
const entries = conceptEntries(compilation.graph);
|
|
288
|
+
const evidenceDocuments = [];
|
|
289
|
+
for (const path of workspace.evidence) {
|
|
290
|
+
const loaded = loadEvidence({
|
|
291
|
+
path,
|
|
292
|
+
source: readFileSync(resolve(cwd, path), 'utf8'),
|
|
293
|
+
});
|
|
294
|
+
if (!loaded.ok)
|
|
295
|
+
return failed(loaded.diagnostics);
|
|
296
|
+
evidenceDocuments.push(loaded.evidence);
|
|
297
|
+
}
|
|
298
|
+
const evaluation = evaluateEvidenceWorkspace(compilation.graph, evidenceDocuments);
|
|
299
|
+
if (!evaluation.ok)
|
|
300
|
+
return failed(evaluation.diagnostics);
|
|
301
|
+
const reconciliation = workspace.evidence.length > 0
|
|
302
|
+
? reconcileEvidenceReports(workspace.id, evaluation.reports, compilation.graph).summary
|
|
303
|
+
: undefined;
|
|
304
|
+
const wholeWorkspace = evaluateProjection(compilation.graph, {
|
|
305
|
+
format: 'yarramate/projection/v1',
|
|
306
|
+
id: 'ask-orientation',
|
|
307
|
+
version: '0.0',
|
|
308
|
+
query: {},
|
|
309
|
+
}, compilation.profileContext);
|
|
310
|
+
const planned = buildNextSubjects(wholeWorkspace, compilation.graph, compilation.profileContext, evaluation.reports);
|
|
311
|
+
const current = entries.filter(({ status }) => status === 'current');
|
|
312
|
+
const retired = entries.filter(({ status }) => status === 'retired');
|
|
313
|
+
const loadedCatalogue = loadQuestionCatalogue({
|
|
314
|
+
path: shippedCataloguePath,
|
|
315
|
+
source: readFileSync(shippedCataloguePath, 'utf8'),
|
|
316
|
+
});
|
|
317
|
+
if (!loadedCatalogue.ok)
|
|
318
|
+
return failed(loadedCatalogue.diagnostics);
|
|
319
|
+
const report = evaluateCatalogue(loadedCatalogue.catalogue, compilation.graph, compilation.profileContext);
|
|
320
|
+
const result = {
|
|
321
|
+
format: 'yarramate/ask-result/v1',
|
|
322
|
+
workspace: workspace.id,
|
|
323
|
+
mode: 'orientation',
|
|
324
|
+
ok: true,
|
|
325
|
+
check: {
|
|
326
|
+
ok: true,
|
|
327
|
+
diagnostics: [],
|
|
328
|
+
...(checkPayload.counted === undefined
|
|
329
|
+
? {}
|
|
330
|
+
: { counted: checkPayload.counted }),
|
|
331
|
+
},
|
|
332
|
+
...(reconciliation === undefined ? {} : { reconciliation }),
|
|
333
|
+
design: { catalogue: report.catalogue, open: report.summary.open },
|
|
334
|
+
backlog: { planned, current, retired },
|
|
335
|
+
};
|
|
336
|
+
const counted = checkPayload.counted;
|
|
337
|
+
const lines = [
|
|
338
|
+
`Workspace ${workspace.id}: check ok` +
|
|
339
|
+
(counted === undefined
|
|
340
|
+
? ''
|
|
341
|
+
: ` (${plural(counted.concepts, 'concept')}, ` +
|
|
342
|
+
`${plural(counted.relationships, 'relationship')}, ` +
|
|
343
|
+
`${plural(counted.states, 'state')}, ` +
|
|
344
|
+
`${plural(counted.documents, 'document')})`),
|
|
345
|
+
];
|
|
346
|
+
if (reconciliation !== undefined) {
|
|
347
|
+
lines.push(reconciliationLine(reconciliation));
|
|
348
|
+
}
|
|
349
|
+
lines.push(report.summary.open === 0
|
|
350
|
+
? `Design interview complete (catalogue ${report.catalogue}): no open questions.`
|
|
351
|
+
: `Design interview: ${plural(report.summary.open, 'open question')} (catalogue ${report.catalogue}) — continue: yarramate design ${workspacePath}`, '', 'Backlog — planned, dependency order:', ...plannedLines(planned), '', `Current: ${plural(current.length, 'subject')} · Retired: ${retired.length} ` +
|
|
352
|
+
`(roster: yarramate ask ${workspacePath} --subjects)`);
|
|
353
|
+
return emit(result, `${lines.join('\n')}\n`);
|
|
354
|
+
}
|
|
355
|
+
// Every other mode reads the compiled model directly.
|
|
356
|
+
const compilation = compileWorkspaceWithProfileContext([...workspace.profiles, ...workspace.documents].map((path) => ({
|
|
357
|
+
path,
|
|
358
|
+
source: readFileSync(resolve(cwd, path), 'utf8'),
|
|
359
|
+
})));
|
|
360
|
+
if (!compilation.ok)
|
|
361
|
+
return failed(compilation.diagnostics);
|
|
362
|
+
const graph = compilation.graph;
|
|
363
|
+
const entries = conceptEntries(graph);
|
|
364
|
+
if (subjects) {
|
|
365
|
+
const filtered = entries.filter((entry) => (kindFilter === undefined ||
|
|
366
|
+
entry.kind.toLowerCase().includes(kindFilter.toLowerCase())) &&
|
|
367
|
+
(statusFilter === undefined || entry.status === statusFilter));
|
|
368
|
+
const result = {
|
|
369
|
+
format: 'yarramate/ask-result/v1',
|
|
370
|
+
workspace: workspace.id,
|
|
371
|
+
mode: 'roster',
|
|
372
|
+
total: entries.length,
|
|
373
|
+
subjects: filtered,
|
|
374
|
+
};
|
|
375
|
+
const lines = [
|
|
376
|
+
`Subjects in workspace ${workspace.id}: ${filtered.length} of ${entries.length}`,
|
|
377
|
+
];
|
|
378
|
+
if (filtered.length > 0) {
|
|
379
|
+
const width = Math.max(...filtered.map(({ id }) => id.length));
|
|
380
|
+
const kindWidth = Math.max(...filtered.map(({ kind }) => (kind.split('#')[1] ?? kind).length));
|
|
381
|
+
for (const entry of filtered) {
|
|
382
|
+
const kind = entry.kind.split('#')[1] ?? entry.kind;
|
|
383
|
+
const description = entry.description === undefined
|
|
384
|
+
? ''
|
|
385
|
+
: ` — ${entry.description.length > 100
|
|
386
|
+
? `${entry.description.slice(0, 100)}…`
|
|
387
|
+
: entry.description}`;
|
|
388
|
+
lines.push(` ${entry.id.padEnd(width)} ${kind.padEnd(kindWidth)} ` +
|
|
389
|
+
`${entry.name ?? entry.id}` +
|
|
390
|
+
`${entry.status === undefined ? '' : ` (${entry.status})`}` +
|
|
391
|
+
description);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
return emit(result, `${lines.join('\n')}\n`);
|
|
395
|
+
}
|
|
396
|
+
// --kinds: the declarable vocabulary (#89) — what agents previously
|
|
397
|
+
// learned by reading src/profile.ts. Core kinds ship with the engine;
|
|
398
|
+
// extensions come from the workspace's resolved profiles.
|
|
399
|
+
if (kinds) {
|
|
400
|
+
const coreConceptIds = new Set(conceptKinds.map(({ id }) => id));
|
|
401
|
+
const coreRelationshipIds = new Set(relationshipPolicies.map(({ id }) => id));
|
|
402
|
+
const extensions = [];
|
|
403
|
+
const lineagePairs = [
|
|
404
|
+
['concept', compilation.profileContext.conceptKindLineages],
|
|
405
|
+
['relationship', compilation.profileContext.relationshipKindLineages],
|
|
406
|
+
];
|
|
407
|
+
for (const [type, lineages] of lineagePairs) {
|
|
408
|
+
for (const [id, lineage] of [...lineages.entries()].sort(([a], [b]) => a.localeCompare(b))) {
|
|
409
|
+
const local = id.slice(id.indexOf('#') + 1);
|
|
410
|
+
const isCore = id.startsWith('yarramate/core@') &&
|
|
411
|
+
(type === 'concept'
|
|
412
|
+
? coreConceptIds.has(local)
|
|
413
|
+
: coreRelationshipIds.has(local));
|
|
414
|
+
if (!isCore)
|
|
415
|
+
extensions.push({ id, type, lineage });
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
const result = {
|
|
419
|
+
format: 'yarramate/ask-result/v1',
|
|
420
|
+
workspace: workspace.id,
|
|
421
|
+
mode: 'kinds',
|
|
422
|
+
conceptKinds,
|
|
423
|
+
relationshipKinds: relationshipPolicies,
|
|
424
|
+
extensions,
|
|
425
|
+
};
|
|
426
|
+
const lines = [
|
|
427
|
+
`Declarable kinds — core profile yarramate/core@0.1`,
|
|
428
|
+
'',
|
|
429
|
+
'Concept kinds (by layer):',
|
|
430
|
+
];
|
|
431
|
+
const layers = [...new Set(conceptKinds.map(({ layer }) => layer))];
|
|
432
|
+
for (const layer of layers) {
|
|
433
|
+
const inLayer = conceptKinds.filter((candidate) => candidate.layer === layer);
|
|
434
|
+
lines.push(` ${layer}: ${inLayer.map(({ id }) => id).join(', ')}`);
|
|
435
|
+
}
|
|
436
|
+
lines.push('', 'Relationship kinds:');
|
|
437
|
+
for (const policy of relationshipPolicies) {
|
|
438
|
+
const constraint = policy.sourceAspects !== undefined ||
|
|
439
|
+
policy.targetAspects !== undefined
|
|
440
|
+
? ` [${policy.sourceAspects?.join('|') ?? 'any'} -> ${policy.targetAspects?.join('|') ?? 'any'}]`
|
|
441
|
+
: '';
|
|
442
|
+
lines.push(` ${policy.id} — ${policy.intent}${constraint}`);
|
|
443
|
+
}
|
|
444
|
+
if (extensions.length > 0) {
|
|
445
|
+
lines.push('', 'Profile extensions in this workspace:');
|
|
446
|
+
for (const extension of extensions) {
|
|
447
|
+
lines.push(` ${extension.id} (${extension.type})` +
|
|
448
|
+
(extension.lineage.length > 0
|
|
449
|
+
? ` -> ${extension.lineage.join(' -> ')}`
|
|
450
|
+
: ''));
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
return emit(result, `${lines.join('\n')}\n`);
|
|
454
|
+
}
|
|
455
|
+
if (compare !== undefined) {
|
|
456
|
+
const comparison = compareArchitectureStates(graph, ...compare);
|
|
457
|
+
if (!comparison.ok) {
|
|
458
|
+
return {
|
|
459
|
+
exitCode: 2,
|
|
460
|
+
stdout: '',
|
|
461
|
+
stderr: `${comparison.issues.map(({ message }) => message).join('\n')}\n`,
|
|
462
|
+
};
|
|
463
|
+
}
|
|
464
|
+
const result = {
|
|
465
|
+
format: 'yarramate/ask-result/v1',
|
|
466
|
+
workspace: workspace.id,
|
|
467
|
+
mode: 'compare',
|
|
468
|
+
comparison: comparison.comparison,
|
|
469
|
+
};
|
|
470
|
+
const { added, removed, retained } = comparison.comparison;
|
|
471
|
+
const lines = [
|
|
472
|
+
`States ${compare[0]} -> ${compare[1]}: ` +
|
|
473
|
+
`${added.length} added, ${removed.length} removed, ${retained.length} retained`,
|
|
474
|
+
];
|
|
475
|
+
if (added.length > 0) {
|
|
476
|
+
lines.push('Added:', ...added.map(({ id, type }) => ` ${id} (${type})`));
|
|
477
|
+
}
|
|
478
|
+
if (removed.length > 0) {
|
|
479
|
+
lines.push('Removed:', ...removed.map(({ id, type }) => ` ${id} (${type})`));
|
|
480
|
+
}
|
|
481
|
+
return emit(result, `${lines.join('\n')}\n`);
|
|
482
|
+
}
|
|
483
|
+
if (next) {
|
|
484
|
+
const evidenceDocuments = [];
|
|
485
|
+
for (const path of workspace.evidence) {
|
|
486
|
+
const loaded = loadEvidence({
|
|
487
|
+
path,
|
|
488
|
+
source: readFileSync(resolve(cwd, path), 'utf8'),
|
|
489
|
+
});
|
|
490
|
+
if (!loaded.ok)
|
|
491
|
+
return failed(loaded.diagnostics);
|
|
492
|
+
evidenceDocuments.push(loaded.evidence);
|
|
493
|
+
}
|
|
494
|
+
const evaluation = evaluateEvidenceWorkspace(graph, evidenceDocuments);
|
|
495
|
+
if (!evaluation.ok)
|
|
496
|
+
return failed(evaluation.diagnostics);
|
|
497
|
+
const wholeWorkspace = evaluateProjection(graph, {
|
|
498
|
+
format: 'yarramate/projection/v1',
|
|
499
|
+
id: 'ask-next',
|
|
500
|
+
version: '0.0',
|
|
501
|
+
query: {},
|
|
502
|
+
}, compilation.profileContext);
|
|
503
|
+
const ordered = buildNextSubjects(wholeWorkspace, graph, compilation.profileContext, evaluation.reports);
|
|
504
|
+
const result = {
|
|
505
|
+
format: 'yarramate/ask-result/v1',
|
|
506
|
+
workspace: workspace.id,
|
|
507
|
+
mode: 'next',
|
|
508
|
+
subjects: ordered,
|
|
509
|
+
};
|
|
510
|
+
const human = ordered.length === 0
|
|
511
|
+
? `No planned subjects in workspace ${workspace.id}.\n`
|
|
512
|
+
: `${[
|
|
513
|
+
`Planned subjects in workspace ${workspace.id} (dependency order):`,
|
|
514
|
+
...plannedLines(ordered),
|
|
515
|
+
].join('\n')}\n`;
|
|
516
|
+
return emit(result, human);
|
|
517
|
+
}
|
|
518
|
+
if (open) {
|
|
519
|
+
const resolvedCataloguePath = cataloguePath === undefined
|
|
520
|
+
? shippedCataloguePath
|
|
521
|
+
: resolve(cwd, cataloguePath);
|
|
522
|
+
const loadedCatalogue = loadQuestionCatalogue({
|
|
523
|
+
path: cataloguePath ?? resolvedCataloguePath,
|
|
524
|
+
source: readFileSync(resolvedCataloguePath, 'utf8'),
|
|
525
|
+
});
|
|
526
|
+
if (!loadedCatalogue.ok)
|
|
527
|
+
return failed(loadedCatalogue.diagnostics);
|
|
528
|
+
const report = {
|
|
529
|
+
...evaluateCatalogue(loadedCatalogue.catalogue, graph, compilation.profileContext),
|
|
530
|
+
workspace: workspace.id,
|
|
531
|
+
};
|
|
532
|
+
const ordered = {
|
|
533
|
+
format: report.format,
|
|
534
|
+
workspace: report.workspace,
|
|
535
|
+
catalogue: report.catalogue,
|
|
536
|
+
summary: report.summary,
|
|
537
|
+
waves: report.waves,
|
|
538
|
+
};
|
|
539
|
+
const result = {
|
|
540
|
+
format: 'yarramate/ask-result/v1',
|
|
541
|
+
workspace: workspace.id,
|
|
542
|
+
mode: 'open',
|
|
543
|
+
report: ordered,
|
|
544
|
+
};
|
|
545
|
+
return emit(result, renderInterrogationReport(ordered));
|
|
546
|
+
}
|
|
547
|
+
// Slice and advice both start from seeds. A single query term that
|
|
548
|
+
// names a projection file is precise addressing; anything else runs
|
|
549
|
+
// through free-text seeding, where exact subject ids win.
|
|
550
|
+
const soleTerm = query.length === 1 ? query[0] : undefined;
|
|
551
|
+
const projectionCandidate = soleTerm !== undefined && !advise && existsSync(resolve(cwd, soleTerm))
|
|
552
|
+
? resolve(cwd, soleTerm)
|
|
553
|
+
: undefined;
|
|
554
|
+
if (projectionCandidate !== undefined &&
|
|
555
|
+
parseDocument(readFileSync(projectionCandidate, 'utf8')).get('format') === 'yarramate/projection/v1') {
|
|
556
|
+
const loaded = loadProjection({
|
|
557
|
+
path: soleTerm,
|
|
558
|
+
source: readFileSync(projectionCandidate, 'utf8'),
|
|
559
|
+
});
|
|
560
|
+
if (!loaded.ok)
|
|
561
|
+
return failed(loaded.diagnostics);
|
|
562
|
+
const evaluated = evaluateProjection(graph, loaded.projection, compilation.profileContext);
|
|
563
|
+
const result = {
|
|
564
|
+
format: 'yarramate/ask-result/v1',
|
|
565
|
+
workspace: workspace.id,
|
|
566
|
+
mode: 'slice',
|
|
567
|
+
addressing: 'projection',
|
|
568
|
+
result: evaluated,
|
|
569
|
+
};
|
|
570
|
+
return emit(result, budget === undefined
|
|
571
|
+
? renderBrief(evaluated, compilation.profileContext)
|
|
572
|
+
: renderBudgetedContext(evaluated, budget));
|
|
573
|
+
}
|
|
574
|
+
const topic = query.join(' ');
|
|
575
|
+
const resolution = resolveSeeds(query, entries);
|
|
576
|
+
if (resolution.seeds.length === 0) {
|
|
577
|
+
return {
|
|
578
|
+
exitCode: 1,
|
|
579
|
+
stdout: '',
|
|
580
|
+
stderr: `No concepts match "${topic}" ` +
|
|
581
|
+
`(searched ${plural(entries.length, 'concept')} by id, name, and description). ` +
|
|
582
|
+
`List the roster: yarramate ask ${workspacePath} --subjects\n`,
|
|
583
|
+
};
|
|
584
|
+
}
|
|
585
|
+
const evaluated = sliceProjection(graph, resolution.seeds, topic, compilation.profileContext);
|
|
586
|
+
if (!advise) {
|
|
587
|
+
const result = {
|
|
588
|
+
format: 'yarramate/ask-result/v1',
|
|
589
|
+
workspace: workspace.id,
|
|
590
|
+
mode: 'slice',
|
|
591
|
+
addressing: resolution.addressing,
|
|
592
|
+
topic,
|
|
593
|
+
seeds: resolution.seeds,
|
|
594
|
+
matched: resolution.matched,
|
|
595
|
+
result: evaluated,
|
|
596
|
+
};
|
|
597
|
+
const rendered = budget === undefined
|
|
598
|
+
? renderBrief(evaluated, compilation.profileContext)
|
|
599
|
+
: renderBudgetedContext(evaluated, budget);
|
|
600
|
+
const header = resolution.addressing === 'free-text'
|
|
601
|
+
? `Slice for "${topic}" — ${plural(resolution.matched, 'concept')} matched` +
|
|
602
|
+
(resolution.matched > resolution.seeds.length
|
|
603
|
+
? `, seeded from the top ${resolution.seeds.length}`
|
|
604
|
+
: '') +
|
|
605
|
+
`: ${resolution.seeds.join(', ')}\n\n`
|
|
606
|
+
: '';
|
|
607
|
+
return emit(result, `${header}${rendered}`);
|
|
608
|
+
}
|
|
609
|
+
// --advise: the expert composition. The engine assembles ground
|
|
610
|
+
// truth — slice, open questions, drift — and stops; the reading and
|
|
611
|
+
// the advice belong to the LLM on top (ADR 0054).
|
|
612
|
+
const brief = renderBrief(evaluated, compilation.profileContext, budget);
|
|
613
|
+
const sliceIds = new Set(evaluated.subjects
|
|
614
|
+
.filter(({ type }) => type === 'concept')
|
|
615
|
+
.map(({ id }) => id));
|
|
616
|
+
const resolvedCataloguePath = cataloguePath === undefined
|
|
617
|
+
? shippedCataloguePath
|
|
618
|
+
: resolve(cwd, cataloguePath);
|
|
619
|
+
const loadedCatalogue = loadQuestionCatalogue({
|
|
620
|
+
path: cataloguePath ?? resolvedCataloguePath,
|
|
621
|
+
source: readFileSync(resolvedCataloguePath, 'utf8'),
|
|
622
|
+
});
|
|
623
|
+
if (!loadedCatalogue.ok)
|
|
624
|
+
return failed(loadedCatalogue.diagnostics);
|
|
625
|
+
const report = evaluateCatalogue(loadedCatalogue.catalogue, graph, compilation.profileContext);
|
|
626
|
+
const openQuestions = [];
|
|
627
|
+
for (const wave of report.waves) {
|
|
628
|
+
for (const question of wave.questions) {
|
|
629
|
+
if (!question.open)
|
|
630
|
+
continue;
|
|
631
|
+
if (question.subjects === undefined) {
|
|
632
|
+
openQuestions.push({
|
|
633
|
+
wave: wave.id,
|
|
634
|
+
id: question.id,
|
|
635
|
+
authority: question.authority,
|
|
636
|
+
question: question.question,
|
|
637
|
+
materiality: question.materiality,
|
|
638
|
+
});
|
|
639
|
+
continue;
|
|
640
|
+
}
|
|
641
|
+
for (const subject of question.subjects) {
|
|
642
|
+
if (!sliceIds.has(subject.id))
|
|
643
|
+
continue;
|
|
644
|
+
openQuestions.push({
|
|
645
|
+
wave: wave.id,
|
|
646
|
+
id: question.id,
|
|
647
|
+
authority: question.authority,
|
|
648
|
+
question: subject.question,
|
|
649
|
+
materiality: question.materiality,
|
|
650
|
+
subject: subject.id,
|
|
651
|
+
});
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
let reconciliation;
|
|
656
|
+
if (workspace.evidence.length > 0) {
|
|
657
|
+
const evidenceDocuments = [];
|
|
658
|
+
for (const path of workspace.evidence) {
|
|
659
|
+
const loaded = loadEvidence({
|
|
660
|
+
path,
|
|
661
|
+
source: readFileSync(resolve(cwd, path), 'utf8'),
|
|
662
|
+
});
|
|
663
|
+
if (!loaded.ok)
|
|
664
|
+
return failed(loaded.diagnostics);
|
|
665
|
+
evidenceDocuments.push(loaded.evidence);
|
|
666
|
+
}
|
|
667
|
+
const evaluation = evaluateEvidenceWorkspace(graph, evidenceDocuments);
|
|
668
|
+
if (!evaluation.ok)
|
|
669
|
+
return failed(evaluation.diagnostics);
|
|
670
|
+
const reconciled = reconcileEvidenceReports(workspace.id, evaluation.reports, graph);
|
|
671
|
+
reconciliation = {
|
|
672
|
+
summary: reconciled.summary,
|
|
673
|
+
findings: reconciled.findings.filter((finding) => sliceIds.has(finding.target.id) ||
|
|
674
|
+
(finding.asserted !== undefined &&
|
|
675
|
+
(sliceIds.has(finding.asserted.from) ||
|
|
676
|
+
sliceIds.has(finding.asserted.to)))),
|
|
677
|
+
};
|
|
678
|
+
}
|
|
679
|
+
const result = {
|
|
680
|
+
format: 'yarramate/ask-result/v1',
|
|
681
|
+
workspace: workspace.id,
|
|
682
|
+
mode: 'advice',
|
|
683
|
+
topic,
|
|
684
|
+
seeds: resolution.seeds,
|
|
685
|
+
matched: resolution.matched,
|
|
686
|
+
slice: brief,
|
|
687
|
+
openQuestions,
|
|
688
|
+
...(reconciliation === undefined ? {} : { reconciliation }),
|
|
689
|
+
};
|
|
690
|
+
const lines = [
|
|
691
|
+
`Advise on: ${topic} — workspace ${workspace.id}`,
|
|
692
|
+
'The engine composed the ground truth below from the model; the reading and the advice are yours.',
|
|
693
|
+
'',
|
|
694
|
+
'== Model slice ==',
|
|
695
|
+
'',
|
|
696
|
+
brief.trimEnd(),
|
|
697
|
+
'',
|
|
698
|
+
'== Open questions touching this slice ==',
|
|
699
|
+
];
|
|
700
|
+
if (openQuestions.length === 0) {
|
|
701
|
+
lines.push(' none — the catalogue is satisfied here');
|
|
702
|
+
}
|
|
703
|
+
else {
|
|
704
|
+
for (const question of openQuestions) {
|
|
705
|
+
lines.push(` [${question.wave} · ${question.id}] ${question.question}`, ` why: ${question.materiality}`);
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
lines.push('', '== Evidence drift ==');
|
|
709
|
+
if (reconciliation === undefined) {
|
|
710
|
+
lines.push(' no evidence declared in this workspace');
|
|
711
|
+
}
|
|
712
|
+
else {
|
|
713
|
+
lines.push(` ${reconciliationLine(reconciliation.summary)}`);
|
|
714
|
+
if (reconciliation.findings.length === 0) {
|
|
715
|
+
lines.push(' no findings touch this slice');
|
|
716
|
+
}
|
|
717
|
+
else {
|
|
718
|
+
for (const finding of reconciliation.findings) {
|
|
719
|
+
lines.push(` ${finding.target.id}: ${finding.result} (${finding.provider})`);
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
return emit(result, `${lines.join('\n')}\n`);
|
|
724
|
+
}
|
|
725
|
+
catch (error) {
|
|
726
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
727
|
+
return { exitCode: 2, stdout: '', stderr: `${message}\n` };
|
|
728
|
+
}
|
|
729
|
+
}
|