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,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
+ }