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,218 @@
|
|
|
1
|
+
import { 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 { diagnosticJson, humanDiagnostics, usage, } from './cli-support.js';
|
|
6
|
+
import { compileWorkspaceWithProfileContext, } from './compiler.js';
|
|
7
|
+
import { evaluateCatalogue, loadQuestionCatalogue, } from './interrogate-command.js';
|
|
8
|
+
import { evaluateProjection } from './projection.js';
|
|
9
|
+
import { renderBrief } from './brief.js';
|
|
10
|
+
import { loadWorkspaceManifest } from './workspace.js';
|
|
11
|
+
// The catalogue is internal to design: it ships inside the package,
|
|
12
|
+
// versioned with it, and harnesses never pass catalogue paths. The
|
|
13
|
+
// relative hop works from both src/ (dev) and dist/ (shipped).
|
|
14
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
15
|
+
const shippedCataloguePath = join(here, '..', 'catalogues', 'core-enrichment.yaml');
|
|
16
|
+
// The top step is the first open question in wave order, then catalogue
|
|
17
|
+
// order within the wave; a subject-scoped question serves its first open
|
|
18
|
+
// subject and reports how many more share it. One question at a time is
|
|
19
|
+
// the discipline; everything else is a read (ask --open).
|
|
20
|
+
const selectStep = (report, subjectFilter) => {
|
|
21
|
+
for (const wave of report.waves) {
|
|
22
|
+
for (const question of wave.questions) {
|
|
23
|
+
if (!question.open)
|
|
24
|
+
continue;
|
|
25
|
+
if (question.subjects === undefined) {
|
|
26
|
+
if (subjectFilter !== undefined)
|
|
27
|
+
continue;
|
|
28
|
+
return {
|
|
29
|
+
questionId: question.id,
|
|
30
|
+
wave: wave.id,
|
|
31
|
+
scope: 'workspace',
|
|
32
|
+
authority: question.authority,
|
|
33
|
+
question: question.question,
|
|
34
|
+
materiality: question.materiality,
|
|
35
|
+
resolution: question.resolution,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
const subjects = subjectFilter === undefined
|
|
39
|
+
? question.subjects
|
|
40
|
+
: question.subjects.filter(({ id }) => id === subjectFilter);
|
|
41
|
+
const first = subjects[0];
|
|
42
|
+
if (first === undefined)
|
|
43
|
+
continue;
|
|
44
|
+
return {
|
|
45
|
+
questionId: question.id,
|
|
46
|
+
wave: wave.id,
|
|
47
|
+
scope: 'subject',
|
|
48
|
+
authority: question.authority,
|
|
49
|
+
question: first.question,
|
|
50
|
+
materiality: question.materiality,
|
|
51
|
+
resolution: question.resolution,
|
|
52
|
+
subject: {
|
|
53
|
+
id: first.id,
|
|
54
|
+
...(first.name === undefined ? {} : { name: first.name }),
|
|
55
|
+
},
|
|
56
|
+
...(subjects.length > 1
|
|
57
|
+
? { remainingSubjects: subjects.length - 1 }
|
|
58
|
+
: {}),
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return null;
|
|
63
|
+
};
|
|
64
|
+
export function runDesignCommand(options, cwd) {
|
|
65
|
+
const json = options.includes('--json');
|
|
66
|
+
let subjectFilter;
|
|
67
|
+
let cataloguePath;
|
|
68
|
+
const rest = [];
|
|
69
|
+
const withoutJson = options.filter((option) => option !== '--json');
|
|
70
|
+
for (let index = 0; index < withoutJson.length; index += 1) {
|
|
71
|
+
const option = withoutJson[index];
|
|
72
|
+
if (option === '--subject' || option === '--catalogue') {
|
|
73
|
+
const value = withoutJson[index + 1];
|
|
74
|
+
if (value === undefined || value.startsWith('-')) {
|
|
75
|
+
return { exitCode: 2, stdout: '', stderr: usage };
|
|
76
|
+
}
|
|
77
|
+
if (option === '--subject') {
|
|
78
|
+
if (subjectFilter !== undefined) {
|
|
79
|
+
return { exitCode: 2, stdout: '', stderr: usage };
|
|
80
|
+
}
|
|
81
|
+
subjectFilter = value;
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
if (cataloguePath !== undefined) {
|
|
85
|
+
return { exitCode: 2, stdout: '', stderr: usage };
|
|
86
|
+
}
|
|
87
|
+
cataloguePath = value;
|
|
88
|
+
}
|
|
89
|
+
index += 1;
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
if (option === undefined || option.startsWith('-')) {
|
|
93
|
+
return { exitCode: 2, stdout: '', stderr: usage };
|
|
94
|
+
}
|
|
95
|
+
rest.push(option);
|
|
96
|
+
}
|
|
97
|
+
const [workspacePath] = rest;
|
|
98
|
+
if (rest.length !== 1 || workspacePath === undefined) {
|
|
99
|
+
return { exitCode: 2, stdout: '', stderr: usage };
|
|
100
|
+
}
|
|
101
|
+
try {
|
|
102
|
+
const manifestSource = readFileSync(resolve(cwd, workspacePath), 'utf8');
|
|
103
|
+
if (parseDocument(manifestSource).get('format') !== 'yarramate/workspace/v1') {
|
|
104
|
+
return {
|
|
105
|
+
exitCode: 2,
|
|
106
|
+
stdout: '',
|
|
107
|
+
stderr: 'design requires an explicit workspace manifest (yarramate/workspace/v1)\n',
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
const failed = (diagnostics) => ({
|
|
111
|
+
exitCode: 1,
|
|
112
|
+
stdout: json
|
|
113
|
+
? diagnosticJson(diagnostics)
|
|
114
|
+
: humanDiagnostics(diagnostics),
|
|
115
|
+
stderr: '',
|
|
116
|
+
});
|
|
117
|
+
const loadedWorkspace = loadWorkspaceManifest({ path: workspacePath, source: manifestSource }, cwd);
|
|
118
|
+
if (!loadedWorkspace.ok)
|
|
119
|
+
return failed(loadedWorkspace.diagnostics);
|
|
120
|
+
const workspace = loadedWorkspace.workspace;
|
|
121
|
+
const resolvedCataloguePath = cataloguePath === undefined
|
|
122
|
+
? shippedCataloguePath
|
|
123
|
+
: resolve(cwd, cataloguePath);
|
|
124
|
+
const loadedCatalogue = loadQuestionCatalogue({
|
|
125
|
+
path: cataloguePath ?? resolvedCataloguePath,
|
|
126
|
+
source: readFileSync(resolvedCataloguePath, 'utf8'),
|
|
127
|
+
});
|
|
128
|
+
if (!loadedCatalogue.ok)
|
|
129
|
+
return failed(loadedCatalogue.diagnostics);
|
|
130
|
+
const compilation = compileWorkspaceWithProfileContext([...workspace.profiles, ...workspace.documents].map((path) => ({
|
|
131
|
+
path,
|
|
132
|
+
source: readFileSync(resolve(cwd, path), 'utf8'),
|
|
133
|
+
})));
|
|
134
|
+
if (!compilation.ok)
|
|
135
|
+
return failed(compilation.diagnostics);
|
|
136
|
+
if (subjectFilter !== undefined) {
|
|
137
|
+
const known = new Set(compilation.graph.subjects.map(({ id }) => id));
|
|
138
|
+
if (!known.has(subjectFilter)) {
|
|
139
|
+
return {
|
|
140
|
+
exitCode: 1,
|
|
141
|
+
stdout: '',
|
|
142
|
+
stderr: `Unknown subject identity: ${subjectFilter} (the compiled workspace declares ${known.size} subjects)\n`,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
const report = evaluateCatalogue(loadedCatalogue.catalogue, compilation.graph, compilation.profileContext);
|
|
147
|
+
const step = selectStep(report, subjectFilter);
|
|
148
|
+
let slice;
|
|
149
|
+
if (step?.subject !== undefined) {
|
|
150
|
+
const projection = evaluateProjection(compilation.graph, {
|
|
151
|
+
format: 'yarramate/projection/v1',
|
|
152
|
+
id: 'design-step',
|
|
153
|
+
version: '0.0',
|
|
154
|
+
query: {
|
|
155
|
+
subjects: [step.subject.id],
|
|
156
|
+
relationships: 'connected',
|
|
157
|
+
},
|
|
158
|
+
presentation: {
|
|
159
|
+
title: step.subject.name ?? step.subject.id,
|
|
160
|
+
description: `The neighbourhood of ${step.subject.id} as declared today.`,
|
|
161
|
+
},
|
|
162
|
+
}, compilation.profileContext);
|
|
163
|
+
slice = renderBrief(projection, compilation.profileContext);
|
|
164
|
+
}
|
|
165
|
+
const result = {
|
|
166
|
+
format: 'yarramate/design-step/v1',
|
|
167
|
+
workspace: workspace.id,
|
|
168
|
+
catalogue: report.catalogue,
|
|
169
|
+
progress: {
|
|
170
|
+
questions: report.summary.questions,
|
|
171
|
+
openQuestions: report.summary.openQuestions,
|
|
172
|
+
open: report.summary.open,
|
|
173
|
+
waves: report.waves.map((wave) => ({
|
|
174
|
+
id: wave.id,
|
|
175
|
+
open: wave.questions.reduce((total, question) => total +
|
|
176
|
+
(question.open ? (question.subjects?.length ?? 1) : 0), 0),
|
|
177
|
+
})),
|
|
178
|
+
},
|
|
179
|
+
step,
|
|
180
|
+
...(slice === undefined ? {} : { slice }),
|
|
181
|
+
};
|
|
182
|
+
if (json) {
|
|
183
|
+
return {
|
|
184
|
+
exitCode: 0,
|
|
185
|
+
stdout: `${JSON.stringify(result, null, 2)}\n`,
|
|
186
|
+
stderr: '',
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
const waveSummary = result.progress.waves
|
|
190
|
+
.map(({ id, open }) => `${id} ${open} open`)
|
|
191
|
+
.join(' · ');
|
|
192
|
+
const lines = [
|
|
193
|
+
`Design interview — workspace ${workspace.id} · catalogue ${report.catalogue}`,
|
|
194
|
+
`Waves: ${waveSummary}`,
|
|
195
|
+
'',
|
|
196
|
+
];
|
|
197
|
+
if (step === null) {
|
|
198
|
+
lines.push(subjectFilter === undefined
|
|
199
|
+
? 'Interview complete: no open questions. The model answers everything the catalogue asks.'
|
|
200
|
+
: `Interview complete for ${subjectFilter}: no open questions touch it.`);
|
|
201
|
+
}
|
|
202
|
+
else {
|
|
203
|
+
lines.push(`Q [${step.wave} · ${step.questionId}] (authority: ${step.authority})`, ` ${step.question}`, ` Why it matters: ${step.materiality}`, ` How to answer: ${step.resolution}`);
|
|
204
|
+
if (step.remainingSubjects !== undefined) {
|
|
205
|
+
lines.push(` (${step.remainingSubjects} more subject${step.remainingSubjects === 1 ? '' : 's'} share${step.remainingSubjects === 1 ? 's' : ''} this question)`);
|
|
206
|
+
}
|
|
207
|
+
if (slice !== undefined) {
|
|
208
|
+
lines.push('', 'Subject slice:', '', slice.trimEnd());
|
|
209
|
+
}
|
|
210
|
+
lines.push('', 'Answer by updating the model (one atomic batch):', ` yarramate apply <operations.yaml> ${workspacePath}`, `Then re-run: yarramate design ${workspacePath}`);
|
|
211
|
+
}
|
|
212
|
+
return { exitCode: 0, stdout: `${lines.join('\n')}\n`, stderr: '' };
|
|
213
|
+
}
|
|
214
|
+
catch (error) {
|
|
215
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
216
|
+
return { exitCode: 2, stdout: '', stderr: `${message}\n` };
|
|
217
|
+
}
|
|
218
|
+
}
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync, } from 'node:fs';
|
|
3
|
+
import { dirname, join, resolve } from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { parseDocument } from 'yaml';
|
|
6
|
+
import { renderBrief } from './brief.js';
|
|
7
|
+
import { humanDiagnostics, usage } from './cli-support.js';
|
|
8
|
+
import { compileWorkspaceWithProfileContext, } from './compiler.js';
|
|
9
|
+
import { serializeSemanticGraph } from './graph.js';
|
|
10
|
+
import { evaluateProjection, loadProjection, renderProjectionMarkdown, } from './projection.js';
|
|
11
|
+
import { loadWorkspaceManifest } from './workspace.js';
|
|
12
|
+
// The adapter stays a separate process behind the verb: the core never
|
|
13
|
+
// imports adapter code (the adapter-runtime-dependency exclusion), it
|
|
14
|
+
// hands the invocation to the sibling binary shipped in the same package.
|
|
15
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
16
|
+
const likec4AdapterEntry = join(here, 'adapters', 'likec4-cli.js');
|
|
17
|
+
const claimValue = (claims, subject, predicate) => {
|
|
18
|
+
const object = claims.find((claim) => claim.subject === subject && claim.predicate === predicate)?.object;
|
|
19
|
+
return object !== undefined && 'value' in object ? object.value : undefined;
|
|
20
|
+
};
|
|
21
|
+
const briefFileName = (id) => `${id.replaceAll('#', '--')}.md`;
|
|
22
|
+
const parseExportOptions = (options) => {
|
|
23
|
+
const positionals = [];
|
|
24
|
+
let out;
|
|
25
|
+
let budget;
|
|
26
|
+
let json = false;
|
|
27
|
+
for (let index = 0; index < options.length; index += 1) {
|
|
28
|
+
const option = options[index];
|
|
29
|
+
if (option === '--json') {
|
|
30
|
+
json = true;
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
if (option === '--out' || option === '--budget') {
|
|
34
|
+
const value = options[index + 1];
|
|
35
|
+
if (value === undefined || value.startsWith('-'))
|
|
36
|
+
return undefined;
|
|
37
|
+
if (option === '--out') {
|
|
38
|
+
if (out !== undefined)
|
|
39
|
+
return undefined;
|
|
40
|
+
out = value;
|
|
41
|
+
}
|
|
42
|
+
else {
|
|
43
|
+
if (budget !== undefined || !/^[1-9][0-9]*$/.test(value)) {
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
46
|
+
budget = Number(value);
|
|
47
|
+
}
|
|
48
|
+
index += 1;
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
if (option === undefined || option.startsWith('-'))
|
|
52
|
+
return undefined;
|
|
53
|
+
positionals.push(option);
|
|
54
|
+
}
|
|
55
|
+
return {
|
|
56
|
+
positionals,
|
|
57
|
+
...(out === undefined ? {} : { out }),
|
|
58
|
+
...(budget === undefined ? {} : { budget }),
|
|
59
|
+
json,
|
|
60
|
+
};
|
|
61
|
+
};
|
|
62
|
+
export function runExportCommand(options, cwd) {
|
|
63
|
+
const [kind, ...rest] = options;
|
|
64
|
+
if (kind === undefined ||
|
|
65
|
+
!['graph', 'markdown', 'briefs', 'likec4'].includes(kind)) {
|
|
66
|
+
return { exitCode: 2, stdout: '', stderr: usage };
|
|
67
|
+
}
|
|
68
|
+
const parsed = parseExportOptions(rest);
|
|
69
|
+
if (parsed === undefined) {
|
|
70
|
+
return { exitCode: 2, stdout: '', stderr: usage };
|
|
71
|
+
}
|
|
72
|
+
// likec4 delegates whole: <likec4-project.yaml> <output-dir> <workspace>.
|
|
73
|
+
if (kind === 'likec4') {
|
|
74
|
+
const [projectDefinition, outputDirectory, workspacePath] = parsed.positionals;
|
|
75
|
+
if (parsed.positionals.length !== 3 ||
|
|
76
|
+
projectDefinition === undefined ||
|
|
77
|
+
outputDirectory === undefined ||
|
|
78
|
+
workspacePath === undefined ||
|
|
79
|
+
parsed.out !== undefined ||
|
|
80
|
+
parsed.budget !== undefined ||
|
|
81
|
+
parsed.json) {
|
|
82
|
+
return { exitCode: 2, stdout: '', stderr: usage };
|
|
83
|
+
}
|
|
84
|
+
if (!existsSync(likec4AdapterEntry)) {
|
|
85
|
+
return {
|
|
86
|
+
exitCode: 2,
|
|
87
|
+
stdout: '',
|
|
88
|
+
stderr: `LikeC4 adapter entry not found at ${likec4AdapterEntry}; ` +
|
|
89
|
+
'run from the installed package or use the yarramate-likec4 binary directly\n',
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
const delegated = spawnSync(process.execPath, [
|
|
93
|
+
likec4AdapterEntry,
|
|
94
|
+
'export-project',
|
|
95
|
+
projectDefinition,
|
|
96
|
+
outputDirectory,
|
|
97
|
+
workspacePath,
|
|
98
|
+
], { cwd, encoding: 'utf8' });
|
|
99
|
+
const exitCode = delegated.status === 0 ? 0 : delegated.status === 1 ? 1 : 2;
|
|
100
|
+
return {
|
|
101
|
+
exitCode,
|
|
102
|
+
stdout: delegated.stdout ?? '',
|
|
103
|
+
stderr: delegated.stderr ?? '',
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
const expectedPositionals = kind === 'graph' ? 1 : 2;
|
|
107
|
+
const workspacePath = parsed.positionals[expectedPositionals - 1];
|
|
108
|
+
const projectionPath = kind === 'graph' ? undefined : parsed.positionals[0];
|
|
109
|
+
if (parsed.positionals.length !== expectedPositionals ||
|
|
110
|
+
workspacePath === undefined ||
|
|
111
|
+
parsed.json ||
|
|
112
|
+
(parsed.budget !== undefined && kind !== 'briefs') ||
|
|
113
|
+
(kind === 'briefs' && parsed.out === undefined)) {
|
|
114
|
+
return { exitCode: 2, stdout: '', stderr: usage };
|
|
115
|
+
}
|
|
116
|
+
try {
|
|
117
|
+
const manifestSource = readFileSync(resolve(cwd, workspacePath), 'utf8');
|
|
118
|
+
if (parseDocument(manifestSource).get('format') !== 'yarramate/workspace/v1') {
|
|
119
|
+
return {
|
|
120
|
+
exitCode: 2,
|
|
121
|
+
stdout: '',
|
|
122
|
+
stderr: 'export requires an explicit workspace manifest (yarramate/workspace/v1)\n',
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
const failed = (diagnostics) => ({
|
|
126
|
+
exitCode: 1,
|
|
127
|
+
stdout: humanDiagnostics(diagnostics),
|
|
128
|
+
stderr: '',
|
|
129
|
+
});
|
|
130
|
+
const loadedWorkspace = loadWorkspaceManifest({ path: workspacePath, source: manifestSource }, cwd);
|
|
131
|
+
if (!loadedWorkspace.ok)
|
|
132
|
+
return failed(loadedWorkspace.diagnostics);
|
|
133
|
+
const workspace = loadedWorkspace.workspace;
|
|
134
|
+
const compilation = compileWorkspaceWithProfileContext([...workspace.profiles, ...workspace.documents].map((path) => ({
|
|
135
|
+
path,
|
|
136
|
+
source: readFileSync(resolve(cwd, path), 'utf8'),
|
|
137
|
+
})));
|
|
138
|
+
if (!compilation.ok)
|
|
139
|
+
return failed(compilation.diagnostics);
|
|
140
|
+
if (kind === 'graph') {
|
|
141
|
+
const serialized = serializeSemanticGraph(compilation.graph);
|
|
142
|
+
if (parsed.out === undefined) {
|
|
143
|
+
return { exitCode: 0, stdout: serialized, stderr: '' };
|
|
144
|
+
}
|
|
145
|
+
const outPath = resolve(cwd, parsed.out);
|
|
146
|
+
mkdirSync(dirname(outPath), { recursive: true });
|
|
147
|
+
writeFileSync(outPath, serialized, 'utf8');
|
|
148
|
+
return {
|
|
149
|
+
exitCode: 0,
|
|
150
|
+
stdout: `Wrote graph to ${parsed.out}\n`,
|
|
151
|
+
stderr: '',
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
const loadedProjection = loadProjection({
|
|
155
|
+
path: projectionPath,
|
|
156
|
+
source: readFileSync(resolve(cwd, projectionPath), 'utf8'),
|
|
157
|
+
});
|
|
158
|
+
if (!loadedProjection.ok)
|
|
159
|
+
return failed(loadedProjection.diagnostics);
|
|
160
|
+
const result = evaluateProjection(compilation.graph, loadedProjection.projection, compilation.profileContext);
|
|
161
|
+
if (kind === 'markdown') {
|
|
162
|
+
const rendered = renderProjectionMarkdown(result);
|
|
163
|
+
if (parsed.out === undefined) {
|
|
164
|
+
return { exitCode: 0, stdout: rendered, stderr: '' };
|
|
165
|
+
}
|
|
166
|
+
const outPath = resolve(cwd, parsed.out);
|
|
167
|
+
mkdirSync(dirname(outPath), { recursive: true });
|
|
168
|
+
writeFileSync(outPath, rendered, 'utf8');
|
|
169
|
+
return {
|
|
170
|
+
exitCode: 0,
|
|
171
|
+
stdout: `Wrote markdown to ${parsed.out}\n`,
|
|
172
|
+
stderr: '',
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
// briefs: the handoff bundle — one brief per projected concept, each
|
|
176
|
+
// the concept's one-hop neighbourhood (ADR 0055), plus an index so N
|
|
177
|
+
// implementers can each pick up one slice.
|
|
178
|
+
const stateIds = new Set(result.claims
|
|
179
|
+
.filter(({ predicate }) => predicate === 'yarramate/state/type')
|
|
180
|
+
.map(({ subject }) => subject));
|
|
181
|
+
const concepts = result.subjects
|
|
182
|
+
.filter(({ id, type }) => type === 'concept' && !stateIds.has(id))
|
|
183
|
+
.map(({ id }) => id)
|
|
184
|
+
.sort((left, right) => left.localeCompare(right));
|
|
185
|
+
const outDirectory = resolve(cwd, parsed.out);
|
|
186
|
+
mkdirSync(outDirectory, { recursive: true });
|
|
187
|
+
const indexLines = [
|
|
188
|
+
`# Briefs — ${result.presentation?.title ?? result.projection}`,
|
|
189
|
+
'',
|
|
190
|
+
`Derived from projection ${result.projection}; one brief per concept,`,
|
|
191
|
+
'each the concept\'s connected neighbourhood as declared today.',
|
|
192
|
+
'',
|
|
193
|
+
];
|
|
194
|
+
for (const id of concepts) {
|
|
195
|
+
const slice = evaluateProjection(compilation.graph, {
|
|
196
|
+
format: 'yarramate/projection/v1',
|
|
197
|
+
id: 'export-brief',
|
|
198
|
+
version: '0.0',
|
|
199
|
+
query: { subjects: [id], relationships: 'connected' },
|
|
200
|
+
presentation: {
|
|
201
|
+
title: claimValue(result.claims, id, 'yarramate/concept/name') ?? id,
|
|
202
|
+
description: `The neighbourhood of ${id} as declared today.`,
|
|
203
|
+
},
|
|
204
|
+
}, compilation.profileContext);
|
|
205
|
+
const brief = renderBrief(slice, compilation.profileContext, parsed.budget);
|
|
206
|
+
writeFileSync(join(outDirectory, briefFileName(id)), brief, 'utf8');
|
|
207
|
+
const name = claimValue(result.claims, id, 'yarramate/concept/name');
|
|
208
|
+
const conceptKind = claimValue(result.claims, id, 'yarramate/concept/kind') ?? 'unknown';
|
|
209
|
+
const status = claimValue(result.claims, id, 'yarramate/lifecycle/status');
|
|
210
|
+
indexLines.push(`- [${name ?? id}](${briefFileName(id)}) — ` +
|
|
211
|
+
`${conceptKind.split('#')[1] ?? conceptKind}` +
|
|
212
|
+
`${status === undefined ? '' : ` (${status})`} — \`${id}\``);
|
|
213
|
+
}
|
|
214
|
+
if (concepts.length === 0) {
|
|
215
|
+
indexLines.push('No concepts selected by this projection.');
|
|
216
|
+
}
|
|
217
|
+
writeFileSync(join(outDirectory, 'INDEX.md'), `${indexLines.join('\n')}\n`, 'utf8');
|
|
218
|
+
return {
|
|
219
|
+
exitCode: 0,
|
|
220
|
+
stdout: `Wrote ${concepts.length} brief${concepts.length === 1 ? '' : 's'} and INDEX.md to ${parsed.out}\n`,
|
|
221
|
+
stderr: '',
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
catch (error) {
|
|
225
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
226
|
+
return { exitCode: 2, stdout: '', stderr: `${message}\n` };
|
|
227
|
+
}
|
|
228
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { type Diagnostic, type ResolvedProfileContext, type SemanticGraph, type WorkspaceSource } from './compiler.js';
|
|
2
|
+
interface CatalogueSelector {
|
|
3
|
+
readonly kinds: readonly string[];
|
|
4
|
+
readonly kindMatching?: 'exact' | 'descendants';
|
|
5
|
+
readonly statuses?: readonly string[];
|
|
6
|
+
readonly documents?: readonly string[];
|
|
7
|
+
}
|
|
8
|
+
type CatalogueCondition = {
|
|
9
|
+
readonly condition: 'missing-claim';
|
|
10
|
+
readonly predicate: string;
|
|
11
|
+
} | {
|
|
12
|
+
readonly condition: 'missing-relationship';
|
|
13
|
+
readonly kinds: readonly string[];
|
|
14
|
+
readonly direction: 'incoming' | 'outgoing' | 'any';
|
|
15
|
+
readonly kindMatching?: 'exact' | 'descendants';
|
|
16
|
+
} | {
|
|
17
|
+
readonly condition: 'isolated';
|
|
18
|
+
} | {
|
|
19
|
+
readonly condition: 'no-subject-of-kind';
|
|
20
|
+
readonly kinds: readonly string[];
|
|
21
|
+
} | {
|
|
22
|
+
readonly condition: 'no-state-defined';
|
|
23
|
+
} | {
|
|
24
|
+
readonly condition: 'missing-linkage';
|
|
25
|
+
readonly kinds: readonly string[];
|
|
26
|
+
readonly direction: 'incoming' | 'outgoing';
|
|
27
|
+
readonly counterpartKinds: readonly string[];
|
|
28
|
+
readonly kindMatching?: 'exact' | 'descendants';
|
|
29
|
+
} | {
|
|
30
|
+
readonly condition: 'missing-reference';
|
|
31
|
+
readonly predicate: string;
|
|
32
|
+
readonly direction: 'incoming' | 'outgoing';
|
|
33
|
+
} | {
|
|
34
|
+
readonly condition: 'missing-attestation';
|
|
35
|
+
readonly topic: string;
|
|
36
|
+
};
|
|
37
|
+
export interface CatalogueQuestion {
|
|
38
|
+
readonly id: string;
|
|
39
|
+
readonly wave: string;
|
|
40
|
+
readonly scope: 'workspace' | 'subject';
|
|
41
|
+
readonly subjects?: CatalogueSelector;
|
|
42
|
+
readonly trigger: readonly CatalogueCondition[];
|
|
43
|
+
readonly question: string;
|
|
44
|
+
readonly materiality: string;
|
|
45
|
+
readonly resolution: string;
|
|
46
|
+
readonly authority: 'human' | 'agent' | 'either';
|
|
47
|
+
}
|
|
48
|
+
export interface QuestionCatalogue {
|
|
49
|
+
readonly format: 'yarramate/question-catalogue/v1';
|
|
50
|
+
readonly id: string;
|
|
51
|
+
readonly version: string;
|
|
52
|
+
readonly profile: string;
|
|
53
|
+
readonly presentation?: {
|
|
54
|
+
readonly title?: string;
|
|
55
|
+
readonly description?: string;
|
|
56
|
+
};
|
|
57
|
+
readonly waves: readonly {
|
|
58
|
+
readonly id: string;
|
|
59
|
+
readonly name: string;
|
|
60
|
+
readonly description?: string;
|
|
61
|
+
}[];
|
|
62
|
+
readonly questions: readonly CatalogueQuestion[];
|
|
63
|
+
}
|
|
64
|
+
interface OpenSubject {
|
|
65
|
+
readonly id: string;
|
|
66
|
+
readonly name?: string;
|
|
67
|
+
readonly question: string;
|
|
68
|
+
}
|
|
69
|
+
interface ReportQuestion {
|
|
70
|
+
readonly id: string;
|
|
71
|
+
readonly scope: 'workspace' | 'subject';
|
|
72
|
+
readonly authority: 'human' | 'agent' | 'either';
|
|
73
|
+
readonly open: boolean;
|
|
74
|
+
readonly question: string;
|
|
75
|
+
readonly materiality: string;
|
|
76
|
+
readonly resolution: string;
|
|
77
|
+
readonly subjects?: readonly OpenSubject[];
|
|
78
|
+
}
|
|
79
|
+
export interface InterrogationReport {
|
|
80
|
+
readonly format: 'yarramate/interrogation-report/v1';
|
|
81
|
+
readonly workspace: string;
|
|
82
|
+
readonly catalogue: string;
|
|
83
|
+
readonly summary: {
|
|
84
|
+
readonly questions: number;
|
|
85
|
+
readonly openQuestions: number;
|
|
86
|
+
readonly open: number;
|
|
87
|
+
};
|
|
88
|
+
readonly waves: readonly {
|
|
89
|
+
readonly id: string;
|
|
90
|
+
readonly name: string;
|
|
91
|
+
readonly questions: readonly ReportQuestion[];
|
|
92
|
+
}[];
|
|
93
|
+
}
|
|
94
|
+
export declare function evaluateCatalogue(catalogue: QuestionCatalogue, graph: SemanticGraph, profileContext?: ResolvedProfileContext): Omit<InterrogationReport, 'workspace'>;
|
|
95
|
+
export type CatalogueLoadResult = {
|
|
96
|
+
readonly ok: true;
|
|
97
|
+
readonly catalogue: QuestionCatalogue;
|
|
98
|
+
} | {
|
|
99
|
+
readonly ok: false;
|
|
100
|
+
readonly diagnostics: readonly Diagnostic[];
|
|
101
|
+
};
|
|
102
|
+
export declare function loadQuestionCatalogue(catalogueSource: WorkspaceSource): CatalogueLoadResult;
|
|
103
|
+
export declare function renderInterrogationReport(report: InterrogationReport): string;
|
|
104
|
+
export {};
|