yarramate 0.5.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 -10
- package/catalogues/core-enrichment.yaml +700 -0
- package/dist/adapters/likec4-cli.js +179 -55
- package/dist/adapters/likec4-export.d.ts +1 -1
- package/dist/adapters/likec4-prepare.d.ts +6 -0
- package/dist/adapters/likec4-prepare.js +40 -2
- 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/check-command.js +70 -4
- package/dist/cli-support.d.ts +4 -1
- package/dist/cli-support.js +3 -2
- package/dist/cli.js +22 -586
- package/dist/compiler.js +45 -7
- 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 +22 -0
- package/dist/next-command.js +167 -0
- package/dist/reconciliation.d.ts +2 -0
- package/dist/reconciliation.js +38 -0
- package/docs/CONSUMING-YARRAMATE.md +28 -21
- package/package.json +13 -10
- package/schema/yarramate-apply-result.schema.json +32 -0
- package/schema/yarramate-ask-result.schema.json +391 -0
- package/schema/yarramate-check-result.schema.json +12 -0
- package/schema/yarramate-core-contract.schema.json +5 -10
- 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-likec4-generated-project-v2.schema.json +6 -0
- package/schema/yarramate-likec4-generated-project.schema.json +6 -0
- package/schema/yarramate-operations.schema.json +303 -0
- package/schema/yarramate-question-catalogue.schema.json +431 -0
- package/schema/yarramate-reconciliation-report.schema.json +8 -2
- 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 -171
- package/schema/yarramate-status-result.schema.json +0 -273
package/dist/compiler.js
CHANGED
|
@@ -14,6 +14,23 @@ const validateDocument = new Ajv2020({ allErrors: true }).compile(documentSchema
|
|
|
14
14
|
const validateProfile = new Ajv2020({ allErrors: true }).compile(profileSchema);
|
|
15
15
|
const compareById = (left, right) => left.id.localeCompare(right.id);
|
|
16
16
|
const presenceClaimId = (subject, state) => `${subject}~present-in-${Buffer.from(state, 'utf8').toString('hex')}`;
|
|
17
|
+
const describeAspect = (aspect) => aspect.replace('-', ' ');
|
|
18
|
+
// Candidate order is the policy-matrix declaration order: the resolved kind
|
|
19
|
+
// map inserts core policies first, then extension kinds as declared.
|
|
20
|
+
const candidateKindHint = (kinds, rejected, sourceAspect, targetAspect) => {
|
|
21
|
+
const candidates = [...kinds]
|
|
22
|
+
.filter(([id, kind]) => id !== rejected &&
|
|
23
|
+
(kind.sourceAspects?.includes(sourceAspect) ?? true) &&
|
|
24
|
+
(kind.targetAspects?.includes(targetAspect) ?? true))
|
|
25
|
+
.map(([id]) => id);
|
|
26
|
+
if (candidates.length === 0) {
|
|
27
|
+
return '';
|
|
28
|
+
}
|
|
29
|
+
const observed = sourceAspect === targetAspect
|
|
30
|
+
? `both endpoints are ${describeAspect(sourceAspect)}`
|
|
31
|
+
: `source is ${describeAspect(sourceAspect)} and target is ${describeAspect(targetAspect)}`;
|
|
32
|
+
return `; ${observed}; valid candidates: ${candidates.join(', ')}`;
|
|
33
|
+
};
|
|
17
34
|
const diagnosticFailure = (diagnostics) => ({
|
|
18
35
|
ok: false,
|
|
19
36
|
diagnostics: [...diagnostics].sort((left, right) => left.path.localeCompare(right.path) ||
|
|
@@ -779,27 +796,35 @@ function compileWorkspaceResolved(sources) {
|
|
|
779
796
|
}
|
|
780
797
|
const policy = selectedProfile.relationshipKinds.get(relationship.kind);
|
|
781
798
|
if (policy !== undefined) {
|
|
782
|
-
|
|
783
|
-
const reference = endpoint === 'source' ? relationship.from : relationship.to;
|
|
799
|
+
const aspectOf = (reference) => {
|
|
784
800
|
const resolvedConcept = conceptByQualifiedId.get(qualifyReference(value.id, reference));
|
|
785
|
-
|
|
801
|
+
return resolvedConcept === undefined
|
|
786
802
|
? undefined
|
|
787
803
|
: profiles
|
|
788
804
|
.get(resolvedConcept.profile)
|
|
789
|
-
?.conceptKinds.get(resolvedConcept.concept.kind);
|
|
805
|
+
?.conceptKinds.get(resolvedConcept.concept.kind)?.aspect;
|
|
806
|
+
};
|
|
807
|
+
const sourceAspect = aspectOf(relationship.from);
|
|
808
|
+
const targetAspect = aspectOf(relationship.to);
|
|
809
|
+
const candidates = sourceAspect === undefined || targetAspect === undefined
|
|
810
|
+
? ''
|
|
811
|
+
: candidateKindHint(selectedProfile.relationshipKinds, relationship.kind, sourceAspect, targetAspect);
|
|
812
|
+
for (const endpoint of ['source', 'target']) {
|
|
813
|
+
const reference = endpoint === 'source' ? relationship.from : relationship.to;
|
|
814
|
+
const aspect = endpoint === 'source' ? sourceAspect : targetAspect;
|
|
790
815
|
const allowed = endpoint === 'source'
|
|
791
816
|
? policy.sourceAspects
|
|
792
817
|
: policy.targetAspects;
|
|
793
|
-
if (
|
|
818
|
+
if (aspect !== undefined &&
|
|
794
819
|
allowed !== undefined &&
|
|
795
|
-
!allowed.includes(
|
|
820
|
+
!allowed.includes(aspect)) {
|
|
796
821
|
const field = endpoint === 'source' ? 'from' : 'to';
|
|
797
822
|
const pointer = `/relationships/${index}/${field}`;
|
|
798
823
|
const source = location(['relationships', index, field], pointer);
|
|
799
824
|
diagnostics.push({
|
|
800
825
|
severity: 'error',
|
|
801
826
|
code: 'YM404',
|
|
802
|
-
message: `Relationship "${relationship.kind}" requires a ${endpoint} with aspect ${allowed.map((
|
|
827
|
+
message: `Relationship "${relationship.kind}" requires a ${endpoint} with aspect ${allowed.map((entry) => `"${entry}"`).join(' or ')}; "${reference}" has aspect "${aspect}"${policy.repair === undefined ? '' : `; ${policy.repair}`}${candidates}`,
|
|
803
828
|
path: input.path,
|
|
804
829
|
pointer,
|
|
805
830
|
line: source.line,
|
|
@@ -886,6 +911,19 @@ function compileWorkspaceResolved(sources) {
|
|
|
886
911
|
source: location(['concepts', index, 'references', referenceIndex, 'ref'], `/concepts/${index}/references/${referenceIndex}/ref`),
|
|
887
912
|
});
|
|
888
913
|
}
|
|
914
|
+
// An attestation is a recorded judgment, not content the engine
|
|
915
|
+
// evaluates: the claim's existence is what triggers can see, and
|
|
916
|
+
// revocation is deletion, reviewed at the Git boundary.
|
|
917
|
+
for (const [attestationIndex, attestation] of (concept.attestations ?? []).entries()) {
|
|
918
|
+
claims.push({
|
|
919
|
+
id: `${subject}~attestation-${attestation.topic}`,
|
|
920
|
+
subject,
|
|
921
|
+
predicate: `yarramate/attestation/${attestation.topic}`,
|
|
922
|
+
object: { value: `${attestation.by} ${attestation.on}` },
|
|
923
|
+
origin: 'declared',
|
|
924
|
+
source: location(['concepts', index, 'attestations', attestationIndex, 'topic'], `/concepts/${index}/attestations/${attestationIndex}/topic`),
|
|
925
|
+
});
|
|
926
|
+
}
|
|
889
927
|
for (const [stateIndex, state] of (concept.presentIn ?? []).entries()) {
|
|
890
928
|
const stateIdentity = qualifyReference(value.id, state);
|
|
891
929
|
claims.push({
|
package/dist/core-contract.d.ts
CHANGED
|
@@ -5,7 +5,7 @@ export interface CoreContractFormat {
|
|
|
5
5
|
readonly packageExport: string;
|
|
6
6
|
}
|
|
7
7
|
export interface CoreContractCommand {
|
|
8
|
-
readonly name: 'init' | '
|
|
8
|
+
readonly name: 'init' | 'design' | 'apply' | 'ask' | 'check' | 'reconcile' | 'export';
|
|
9
9
|
readonly binary: 'yarramate';
|
|
10
10
|
readonly machineFormat?: string;
|
|
11
11
|
}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
import { type CliResult } from './cli-support.js';
|
|
2
|
-
export declare function
|
|
2
|
+
export declare function runDesignCommand(options: readonly string[], cwd: string): CliResult;
|
|
@@ -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 {};
|