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.
Files changed (48) hide show
  1. package/README.md +39 -10
  2. package/catalogues/core-enrichment.yaml +700 -0
  3. package/dist/adapters/likec4-cli.js +179 -55
  4. package/dist/adapters/likec4-export.d.ts +1 -1
  5. package/dist/adapters/likec4-prepare.d.ts +6 -0
  6. package/dist/adapters/likec4-prepare.js +40 -2
  7. package/dist/adapters/mcp-cli.js +55 -49
  8. package/dist/apply-command.d.ts +2 -0
  9. package/dist/apply-command.js +199 -0
  10. package/dist/{new-command.d.ts → ask-command.d.ts} +1 -1
  11. package/dist/ask-command.js +729 -0
  12. package/dist/brief.d.ts +3 -0
  13. package/dist/brief.js +235 -0
  14. package/dist/check-command.js +70 -4
  15. package/dist/cli-support.d.ts +4 -1
  16. package/dist/cli-support.js +3 -2
  17. package/dist/cli.js +22 -586
  18. package/dist/compiler.js +45 -7
  19. package/dist/core-contract.d.ts +1 -1
  20. package/dist/{status-command.d.ts → design-command.d.ts} +1 -1
  21. package/dist/design-command.js +218 -0
  22. package/dist/export-command.d.ts +2 -0
  23. package/dist/export-command.js +228 -0
  24. package/dist/interrogate-command.d.ts +104 -0
  25. package/dist/interrogate-command.js +268 -0
  26. package/dist/next-command.d.ts +22 -0
  27. package/dist/next-command.js +167 -0
  28. package/dist/reconciliation.d.ts +2 -0
  29. package/dist/reconciliation.js +38 -0
  30. package/docs/CONSUMING-YARRAMATE.md +28 -21
  31. package/package.json +13 -10
  32. package/schema/yarramate-apply-result.schema.json +32 -0
  33. package/schema/yarramate-ask-result.schema.json +391 -0
  34. package/schema/yarramate-check-result.schema.json +12 -0
  35. package/schema/yarramate-core-contract.schema.json +5 -10
  36. package/schema/yarramate-design-step.schema.json +152 -0
  37. package/schema/yarramate-document.schema.json +76 -9
  38. package/schema/yarramate-interrogation-report.schema.json +89 -0
  39. package/schema/yarramate-likec4-generated-project-v2.schema.json +6 -0
  40. package/schema/yarramate-likec4-generated-project.schema.json +6 -0
  41. package/schema/yarramate-operations.schema.json +303 -0
  42. package/schema/yarramate-question-catalogue.schema.json +431 -0
  43. package/schema/yarramate-reconciliation-report.schema.json +8 -2
  44. package/skills/yarramate-architecture/SKILL.md +42 -17
  45. package/skills/yarramate-architecture/references/native-authoring.md +73 -17
  46. package/dist/new-command.js +0 -111
  47. package/dist/status-command.js +0 -171
  48. package/schema/yarramate-status-result.schema.json +0 -273
@@ -0,0 +1,199 @@
1
+ import { readFileSync, writeFileSync } from 'node:fs';
2
+ import { resolve } from 'node:path';
3
+ import { isSeq, parseDocument } from 'yaml';
4
+ import Ajv2020Module from 'ajv/dist/2020.js';
5
+ import { diagnosticJson, humanDiagnostics, usage, } from './cli-support.js';
6
+ import { compileWorkspace } from './compiler.js';
7
+ import { loadSourceDocument, locateSourcePath, } from './source-document.js';
8
+ import { loadWorkspaceManifest } from './workspace.js';
9
+ import operationsSchema from '../schema/yarramate-operations.schema.json' with {
10
+ type: 'json'
11
+ };
12
+ const Ajv2020 = Ajv2020Module.default;
13
+ const validateOperations = new Ajv2020({ allErrors: true }).compile(operationsSchema);
14
+ // Scalar fields replace; list fields append. An answer enriches what is
15
+ // there — it never silently shrinks it (removals stay Git edits).
16
+ const SCALAR_CONCEPT_FIELDS = ['kind', 'name', 'description', 'status', 'owner'];
17
+ const LIST_CONCEPT_FIELDS = ['constraints', 'references', 'presentIn', 'attestations'];
18
+ const SCALAR_RELATIONSHIP_FIELDS = ['kind', 'from', 'to', 'name', 'description', 'status', 'mode', 'content'];
19
+ const LIST_RELATIONSHIP_FIELDS = ['references', 'presentIn'];
20
+ const appendBlockItem = (document, collection, item) => {
21
+ document.addIn([collection], item);
22
+ const sequence = document.getIn([collection], true);
23
+ if (isSeq(sequence)) {
24
+ sequence.flow = false;
25
+ }
26
+ };
27
+ const findItem = (document, collection, id) => {
28
+ const sequence = document.get(collection, true);
29
+ if (!isSeq(sequence))
30
+ return undefined;
31
+ return sequence.items.find((item) => typeof item === 'object' &&
32
+ item !== null &&
33
+ 'get' in item &&
34
+ item.get('id') === id);
35
+ };
36
+ const applyFields = (document, item, fields, scalars, lists) => {
37
+ for (const key of scalars) {
38
+ if (fields[key] !== undefined)
39
+ item.set(key, fields[key]);
40
+ }
41
+ for (const key of lists) {
42
+ const additions = fields[key];
43
+ if (additions === undefined || additions.length === 0)
44
+ continue;
45
+ const existing = item.get(key);
46
+ if (existing === undefined) {
47
+ item.set(key, additions);
48
+ }
49
+ else if (isSeq(existing)) {
50
+ for (const entry of additions) {
51
+ existing.items.push(document.createNode(entry));
52
+ }
53
+ }
54
+ }
55
+ };
56
+ export function runApplyCommand(options, cwd) {
57
+ const json = options.includes('--json');
58
+ const rest = options.filter((option) => option !== '--json');
59
+ const [operationsPath, workspacePath] = rest;
60
+ if (rest.length !== 2 ||
61
+ operationsPath === undefined ||
62
+ workspacePath === undefined ||
63
+ rest.some((option) => option.startsWith('-'))) {
64
+ return { exitCode: 2, stdout: '', stderr: usage };
65
+ }
66
+ try {
67
+ const manifestSource = readFileSync(resolve(cwd, workspacePath), 'utf8');
68
+ if (parseDocument(manifestSource).get('format') !== 'yarramate/workspace/v1') {
69
+ return {
70
+ exitCode: 2,
71
+ stdout: '',
72
+ stderr: 'apply requires an explicit workspace manifest (yarramate/workspace/v1)\n',
73
+ };
74
+ }
75
+ const failed = (diagnostics) => ({
76
+ exitCode: 1,
77
+ stdout: json ? diagnosticJson(diagnostics) : humanDiagnostics(diagnostics),
78
+ stderr: '',
79
+ });
80
+ const loadedWorkspace = loadWorkspaceManifest({ path: workspacePath, source: manifestSource }, cwd);
81
+ if (!loadedWorkspace.ok)
82
+ return failed(loadedWorkspace.diagnostics);
83
+ const workspace = loadedWorkspace.workspace;
84
+ const operationsSource = readFileSync(resolve(cwd, operationsPath), 'utf8');
85
+ const loadedOperations = loadSourceDocument({ path: operationsPath, source: operationsSource }, validateOperations, 'Operations');
86
+ if (!loadedOperations.ok)
87
+ return failed(loadedOperations.diagnostics);
88
+ const operations = loadedOperations.document.value.operations;
89
+ const yaml = loadedOperations.document.yaml;
90
+ const lineCounter = loadedOperations.document.lineCounter;
91
+ // Documents are addressed by their manifest paths; an operation aimed
92
+ // anywhere else is rejected before anything is touched.
93
+ const workspaceDocuments = new Map(workspace.documents.map((path) => [resolve(cwd, path), path]));
94
+ const parsed = new Map();
95
+ const counts = {
96
+ addedConcepts: 0,
97
+ addedRelationships: 0,
98
+ updatedConcepts: 0,
99
+ updatedRelationships: 0,
100
+ };
101
+ for (const [index, operation] of operations.entries()) {
102
+ const absolute = resolve(cwd, operation.document);
103
+ const manifestPath = workspaceDocuments.get(absolute);
104
+ const locate = (message) => ({
105
+ severity: 'error',
106
+ code: 'YM912',
107
+ message,
108
+ ...locateSourcePath(operationsPath, yaml, lineCounter, ['operations', index, 'document'], `/operations/${index}/document`),
109
+ });
110
+ if (manifestPath === undefined) {
111
+ return failed([
112
+ locate(`Operation ${index} targets "${operation.document}", which is not a document of workspace "${workspace.id}"`),
113
+ ]);
114
+ }
115
+ let document = parsed.get(absolute);
116
+ if (document === undefined) {
117
+ document = parseDocument(readFileSync(absolute, 'utf8'));
118
+ parsed.set(absolute, document);
119
+ }
120
+ if (operation.op === 'add-concept') {
121
+ appendBlockItem(document, 'concepts', operation.concept);
122
+ counts.addedConcepts += 1;
123
+ }
124
+ else if (operation.op === 'add-relationship') {
125
+ appendBlockItem(document, 'relationships', operation.relationship);
126
+ counts.addedRelationships += 1;
127
+ }
128
+ else {
129
+ const collection = operation.op === 'update-concept' ? 'concepts' : 'relationships';
130
+ const payload = operation.op === 'update-concept'
131
+ ? operation.concept
132
+ : operation.relationship;
133
+ const item = findItem(document, collection, payload.id);
134
+ if (item === undefined) {
135
+ return failed([
136
+ locate(`Operation ${index} updates "${payload.id}", which does not exist in ${operation.document}`),
137
+ ]);
138
+ }
139
+ applyFields(document, item, payload, operation.op === 'update-concept'
140
+ ? SCALAR_CONCEPT_FIELDS
141
+ : SCALAR_RELATIONSHIP_FIELDS, operation.op === 'update-concept'
142
+ ? LIST_CONCEPT_FIELDS
143
+ : LIST_RELATIONSHIP_FIELDS);
144
+ if (operation.op === 'update-concept') {
145
+ counts.updatedConcepts += 1;
146
+ }
147
+ else {
148
+ counts.updatedRelationships += 1;
149
+ }
150
+ }
151
+ }
152
+ // The atomic gate: the whole candidate workspace must compile before a
153
+ // single byte is written; any diagnostic rejects the entire batch.
154
+ const candidates = new Map([...parsed.entries()].map(([absolute, document]) => [
155
+ absolute,
156
+ document.toString({ lineWidth: 0 }),
157
+ ]));
158
+ const compilation = compileWorkspace([...workspace.profiles, ...workspace.documents].map((path) => {
159
+ const absolute = resolve(cwd, path);
160
+ return {
161
+ path,
162
+ source: candidates.get(absolute) ?? readFileSync(absolute, 'utf8'),
163
+ };
164
+ }));
165
+ if (!compilation.ok)
166
+ return failed(compilation.diagnostics);
167
+ for (const [absolute, source] of candidates) {
168
+ writeFileSync(absolute, source, 'utf8');
169
+ }
170
+ const touched = [...parsed.keys()]
171
+ .map((absolute) => workspaceDocuments.get(absolute))
172
+ .sort();
173
+ if (json) {
174
+ return {
175
+ exitCode: 0,
176
+ stdout: `${JSON.stringify({
177
+ format: 'yarramate/apply-result/v1',
178
+ workspace: workspace.id,
179
+ applied: counts,
180
+ documents: touched,
181
+ }, null, 2)}\n`,
182
+ stderr: '',
183
+ };
184
+ }
185
+ const applied = counts.addedConcepts +
186
+ counts.addedRelationships +
187
+ counts.updatedConcepts +
188
+ counts.updatedRelationships;
189
+ return {
190
+ exitCode: 0,
191
+ stdout: `Applied ${applied} operation${applied === 1 ? '' : 's'} to ${touched.join(', ')}\n`,
192
+ stderr: '',
193
+ };
194
+ }
195
+ catch (error) {
196
+ const message = error instanceof Error ? error.message : String(error);
197
+ return { exitCode: 2, stdout: '', stderr: `${message}\n` };
198
+ }
199
+ }
@@ -1,2 +1,2 @@
1
1
  import { type CliResult } from './cli-support.js';
2
- export declare function runNewCommand(options: readonly string[], cwd: string): CliResult;
2
+ export declare function runAskCommand(options: readonly string[], cwd: string): CliResult;