yarramate 0.8.0 → 0.9.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/catalogues/core-enrichment.yaml +9 -4
- package/dist/apply-command.js +35 -1
- package/dist/ask-command.js +93 -3
- package/dist/changed.d.ts +17 -0
- package/dist/changed.js +111 -0
- package/dist/cli-support.d.ts +1 -1
- package/dist/cli-support.js +1 -1
- package/dist/export-command.js +60 -10
- package/dist/interrogate-command.js +10 -6
- package/package.json +1 -1
- package/schema/yarramate-ask-result.schema.json +483 -115
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
format: yarramate/question-catalogue/v1
|
|
2
2
|
id: core-enrichment
|
|
3
|
-
version: "0.
|
|
3
|
+
version: "0.5"
|
|
4
4
|
profile: yarramate/core@0.1
|
|
5
5
|
presentation:
|
|
6
6
|
title: Core enrichment interview
|
|
@@ -122,7 +122,9 @@ questions:
|
|
|
122
122
|
authority: either
|
|
123
123
|
resolution: >-
|
|
124
124
|
Add realization relationships from the fulfilling capability or
|
|
125
|
-
service,
|
|
125
|
+
service, record the aspirational status in the goal's description,
|
|
126
|
+
or retire the goal (status: retired) to record that it no longer
|
|
127
|
+
steers design.
|
|
126
128
|
|
|
127
129
|
|
|
128
130
|
- id: goal-no-driver
|
|
@@ -219,8 +221,11 @@ questions:
|
|
|
219
221
|
roadmap.
|
|
220
222
|
authority: either
|
|
221
223
|
resolution: >-
|
|
222
|
-
Add realization from the fulfilling service, component, or
|
|
223
|
-
|
|
224
|
+
Add realization from the fulfilling service, component, or
|
|
225
|
+
behavior; to descope instead, retire the requirement (status:
|
|
226
|
+
retired) — retirement preserves the decision on record and closes
|
|
227
|
+
the question (ADR 0064). Delete only when the history itself is
|
|
228
|
+
noise.
|
|
224
229
|
|
|
225
230
|
- id: principle-unapplied
|
|
226
231
|
wave: motivation
|
package/dist/apply-command.js
CHANGED
|
@@ -123,7 +123,24 @@ const itemMap = (source, collection, id) => {
|
|
|
123
123
|
};
|
|
124
124
|
// The indent item fields sit at, read off the item's own first field.
|
|
125
125
|
const fieldIndentOf = (source, map) => indentAt(source, nodeRange(map.items[0].key)[0]);
|
|
126
|
+
// A flow-style item (`- { id: x, ... }`) cannot take block field lines;
|
|
127
|
+
// splicing them after it corrupts the sequence (Codex dogfood finding,
|
|
128
|
+
// 2026-08-01). The whole item is rewritten as a block mapping with the
|
|
129
|
+
// mutation applied — churn confined to the item being edited.
|
|
130
|
+
const rewriteFlowItem = (source, map, mutate) => {
|
|
131
|
+
const mutated = mutate(map.toJSON());
|
|
132
|
+
const [start, valueEnd] = nodeRange(map);
|
|
133
|
+
const fieldIndent = indentAt(source, start);
|
|
134
|
+
const rendered = reindent(stringify(mutated, { lineWidth: 0 }).trimEnd(), fieldIndent);
|
|
135
|
+
return splice(source, start, valueEnd, rendered);
|
|
136
|
+
};
|
|
126
137
|
const setScalarField = (source, map, key, value) => {
|
|
138
|
+
if (map.flow) {
|
|
139
|
+
return rewriteFlowItem(source, map, (fields) => ({
|
|
140
|
+
...fields,
|
|
141
|
+
[key]: value,
|
|
142
|
+
}));
|
|
143
|
+
}
|
|
127
144
|
const indent = fieldIndentOf(source, map);
|
|
128
145
|
const rendered = reindent(valueText(value), indent + 2);
|
|
129
146
|
const pair = pairFor(map, key);
|
|
@@ -134,6 +151,15 @@ const setScalarField = (source, map, key, value) => {
|
|
|
134
151
|
return splice(source, start, valueEnd, rendered);
|
|
135
152
|
};
|
|
136
153
|
const appendListField = (source, map, key, additions) => {
|
|
154
|
+
if (map.flow) {
|
|
155
|
+
return rewriteFlowItem(source, map, (fields) => ({
|
|
156
|
+
...fields,
|
|
157
|
+
[key]: [
|
|
158
|
+
...(fields[key] ?? []),
|
|
159
|
+
...additions,
|
|
160
|
+
],
|
|
161
|
+
}));
|
|
162
|
+
}
|
|
137
163
|
const indent = fieldIndentOf(source, map);
|
|
138
164
|
const pair = pairFor(map, key);
|
|
139
165
|
if (pair === undefined) {
|
|
@@ -163,11 +189,19 @@ const appendListField = (source, map, key, additions) => {
|
|
|
163
189
|
return splice(source, start, valueEnd, `\n${sequenceEntries(merged, indent + 2)}`);
|
|
164
190
|
};
|
|
165
191
|
// Retraction (#115): delete the field's whole entry, from the start of its
|
|
166
|
-
// key line through the end of its value's last line.
|
|
192
|
+
// key line through the end of its value's last line. A flow item is
|
|
193
|
+
// rewritten instead — line-based deletion there would take the whole item
|
|
194
|
+
// with it.
|
|
167
195
|
const removeField = (source, map, key) => {
|
|
168
196
|
const pair = pairFor(map, key);
|
|
169
197
|
if (pair === undefined)
|
|
170
198
|
return undefined;
|
|
199
|
+
if (map.flow) {
|
|
200
|
+
return rewriteFlowItem(source, map, (fields) => {
|
|
201
|
+
const { [key]: _removed, ...rest } = fields;
|
|
202
|
+
return rest;
|
|
203
|
+
});
|
|
204
|
+
}
|
|
171
205
|
const start = lineStartOf(source, nodeRange(pair.key)[0]);
|
|
172
206
|
const valueEnd = pair.value === null || pair.value === undefined
|
|
173
207
|
? nodeRange(pair.key)[2]
|
package/dist/ask-command.js
CHANGED
|
@@ -4,6 +4,7 @@ import { fileURLToPath } from 'node:url';
|
|
|
4
4
|
import { parseDocument } from 'yaml';
|
|
5
5
|
import { compareArchitectureStates, } from './architecture-state.js';
|
|
6
6
|
import { renderBrief } from './brief.js';
|
|
7
|
+
import { deriveChangedSubjects } from './changed.js';
|
|
7
8
|
import { runCheckCommand } from './check-command.js';
|
|
8
9
|
import { diagnosticJson, humanDiagnostics, usage, } from './cli-support.js';
|
|
9
10
|
import { compileWorkspaceWithProfileContext, } from './compiler.js';
|
|
@@ -124,6 +125,7 @@ export function runAskCommand(options, cwd) {
|
|
|
124
125
|
let kinds = false;
|
|
125
126
|
let advise = false;
|
|
126
127
|
let compare;
|
|
128
|
+
let changed;
|
|
127
129
|
let budget;
|
|
128
130
|
let kindFilter;
|
|
129
131
|
let statusFilter;
|
|
@@ -172,7 +174,8 @@ export function runAskCommand(options, cwd) {
|
|
|
172
174
|
if (option === '--budget' ||
|
|
173
175
|
option === '--kind' ||
|
|
174
176
|
option === '--status' ||
|
|
175
|
-
option === '--catalogue'
|
|
177
|
+
option === '--catalogue' ||
|
|
178
|
+
option === '--changed') {
|
|
176
179
|
const value = options[index + 1];
|
|
177
180
|
if (value === undefined || value.startsWith('-')) {
|
|
178
181
|
return { exitCode: 2, stdout: '', stderr: usage };
|
|
@@ -183,6 +186,12 @@ export function runAskCommand(options, cwd) {
|
|
|
183
186
|
}
|
|
184
187
|
budget = Number(value);
|
|
185
188
|
}
|
|
189
|
+
else if (option === '--changed') {
|
|
190
|
+
if (changed !== undefined) {
|
|
191
|
+
return { exitCode: 2, stdout: '', stderr: usage };
|
|
192
|
+
}
|
|
193
|
+
changed = value;
|
|
194
|
+
}
|
|
186
195
|
else if (option === '--kind') {
|
|
187
196
|
if (kindFilter !== undefined) {
|
|
188
197
|
return { exitCode: 2, stdout: '', stderr: usage };
|
|
@@ -223,9 +232,12 @@ export function runAskCommand(options, cwd) {
|
|
|
223
232
|
(advise && exclusiveModes > 0) ||
|
|
224
233
|
(advise && query.length === 0) ||
|
|
225
234
|
(query.length > 0 && exclusiveModes > 0) ||
|
|
235
|
+
(changed !== undefined &&
|
|
236
|
+
(query.length > 0 || exclusiveModes > 0 || advise)) ||
|
|
226
237
|
((kindFilter !== undefined || statusFilter !== undefined) && !subjects) ||
|
|
227
238
|
(cataloguePath !== undefined && !open && !advise) ||
|
|
228
|
-
(budget !== undefined &&
|
|
239
|
+
(budget !== undefined &&
|
|
240
|
+
(json || (query.length === 0 && !advise && changed === undefined)))) {
|
|
229
241
|
return { exitCode: 2, stdout: '', stderr: usage };
|
|
230
242
|
}
|
|
231
243
|
try {
|
|
@@ -263,7 +275,8 @@ export function runAskCommand(options, cwd) {
|
|
|
263
275
|
!open &&
|
|
264
276
|
!kinds &&
|
|
265
277
|
!advise &&
|
|
266
|
-
compare === undefined
|
|
278
|
+
compare === undefined &&
|
|
279
|
+
changed === undefined) {
|
|
267
280
|
const checked = runCheckCommand([workspacePath, '--json'], cwd);
|
|
268
281
|
const checkPayload = JSON.parse(checked.stdout);
|
|
269
282
|
if (!checkPayload.ok) {
|
|
@@ -544,6 +557,83 @@ export function runAskCommand(options, cwd) {
|
|
|
544
557
|
};
|
|
545
558
|
return emit(result, renderInterrogationReport(ordered));
|
|
546
559
|
}
|
|
560
|
+
// --changed: the review slice (ADR 0065). Git says what changed; the
|
|
561
|
+
// engine maps changed lines to subjects and renders their connected
|
|
562
|
+
// neighbourhood, plus a coverage note when a changed subject appears
|
|
563
|
+
// in no authored projection.
|
|
564
|
+
if (changed !== undefined) {
|
|
565
|
+
const documentIdByPath = new Map(graph.documents.map(({ id, source }) => [source, id]));
|
|
566
|
+
const derived = deriveChangedSubjects(cwd, changed, workspace.documents.map((path) => ({
|
|
567
|
+
path,
|
|
568
|
+
source: readFileSync(resolve(cwd, path), 'utf8'),
|
|
569
|
+
documentId: documentIdByPath.get(path) ?? path,
|
|
570
|
+
})));
|
|
571
|
+
if (!derived.ok) {
|
|
572
|
+
return { exitCode: 2, stdout: '', stderr: `${derived.message}\n` };
|
|
573
|
+
}
|
|
574
|
+
const endpoints = new Set();
|
|
575
|
+
for (const relationshipId of derived.changed.relationships) {
|
|
576
|
+
const claim = graph.claims.find((candidate) => candidate.id === relationshipId && 'ref' in candidate.object);
|
|
577
|
+
if (claim !== undefined && 'ref' in claim.object) {
|
|
578
|
+
endpoints.add(claim.subject);
|
|
579
|
+
endpoints.add(claim.object.ref);
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
const seeds = [
|
|
583
|
+
...new Set([...derived.changed.concepts, ...endpoints]),
|
|
584
|
+
].sort();
|
|
585
|
+
const changedIds = [
|
|
586
|
+
...derived.changed.concepts,
|
|
587
|
+
...derived.changed.relationships,
|
|
588
|
+
];
|
|
589
|
+
const covered = new Set();
|
|
590
|
+
for (const projectionPath of workspace.projections) {
|
|
591
|
+
const loaded = loadProjection({
|
|
592
|
+
path: projectionPath,
|
|
593
|
+
source: readFileSync(resolve(cwd, projectionPath), 'utf8'),
|
|
594
|
+
});
|
|
595
|
+
if (!loaded.ok)
|
|
596
|
+
continue;
|
|
597
|
+
const membership = evaluateProjection(graph, loaded.projection, compilation.profileContext);
|
|
598
|
+
for (const subject of membership.subjects) {
|
|
599
|
+
covered.add(subject.id);
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
const uncovered = changedIds.filter((id) => !covered.has(id));
|
|
603
|
+
const coverage = {
|
|
604
|
+
projections: workspace.projections.length,
|
|
605
|
+
uncovered,
|
|
606
|
+
};
|
|
607
|
+
const evaluated = sliceProjection(graph, seeds, `Review slice ${changed}`, compilation.profileContext);
|
|
608
|
+
const result = {
|
|
609
|
+
format: 'yarramate/ask-result/v1',
|
|
610
|
+
workspace: workspace.id,
|
|
611
|
+
mode: 'slice',
|
|
612
|
+
addressing: 'changed',
|
|
613
|
+
seeds,
|
|
614
|
+
changed: derived.changed,
|
|
615
|
+
coverage,
|
|
616
|
+
result: evaluated,
|
|
617
|
+
};
|
|
618
|
+
if (changedIds.length === 0) {
|
|
619
|
+
return emit(result, `No model subjects changed in ${changed}.\n`);
|
|
620
|
+
}
|
|
621
|
+
const rendered = budget === undefined
|
|
622
|
+
? renderBrief(evaluated, compilation.profileContext)
|
|
623
|
+
: renderBudgetedContext(evaluated, budget);
|
|
624
|
+
const lines = [
|
|
625
|
+
`Review slice ${changed} — ${plural(derived.changed.concepts.length, 'concept')}, ` +
|
|
626
|
+
`${plural(derived.changed.relationships.length, 'relationship')} changed (workspace ${workspace.id})`,
|
|
627
|
+
'',
|
|
628
|
+
rendered.trimEnd(),
|
|
629
|
+
'',
|
|
630
|
+
uncovered.length === 0
|
|
631
|
+
? `Review coverage: every changed subject appears in at least one of the ${coverage.projections} authored projections.`
|
|
632
|
+
: `Review coverage: ${uncovered.length} of ${changedIds.length} changed subjects appear in no authored projection:` +
|
|
633
|
+
`\n${uncovered.map((id) => ` ${id}`).join('\n')}`,
|
|
634
|
+
];
|
|
635
|
+
return emit(result, `${lines.join('\n')}\n`);
|
|
636
|
+
}
|
|
547
637
|
// Slice and advice both start from seeds. A single query term that
|
|
548
638
|
// names a projection file is precise addressing; anything else runs
|
|
549
639
|
// through free-text seeding, where exact subject ids win.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export interface ChangedSubjects {
|
|
2
|
+
readonly range: string;
|
|
3
|
+
readonly concepts: readonly string[];
|
|
4
|
+
readonly relationships: readonly string[];
|
|
5
|
+
}
|
|
6
|
+
export type ChangedResult = {
|
|
7
|
+
readonly ok: true;
|
|
8
|
+
readonly changed: ChangedSubjects;
|
|
9
|
+
} | {
|
|
10
|
+
readonly ok: false;
|
|
11
|
+
readonly message: string;
|
|
12
|
+
};
|
|
13
|
+
export declare function deriveChangedSubjects(cwd: string, range: string, documents: ReadonlyArray<{
|
|
14
|
+
readonly path: string;
|
|
15
|
+
readonly source: string;
|
|
16
|
+
readonly documentId: string;
|
|
17
|
+
}>): ChangedResult;
|
package/dist/changed.js
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
import { isMap, isScalar, isSeq, parseDocument } from 'yaml';
|
|
3
|
+
const lineOfOffset = (lineStarts, offset) => {
|
|
4
|
+
let low = 0;
|
|
5
|
+
let high = lineStarts.length - 1;
|
|
6
|
+
while (low < high) {
|
|
7
|
+
const mid = (low + high + 1) >> 1;
|
|
8
|
+
if (lineStarts[mid] <= offset)
|
|
9
|
+
low = mid;
|
|
10
|
+
else
|
|
11
|
+
high = mid - 1;
|
|
12
|
+
}
|
|
13
|
+
return low + 1;
|
|
14
|
+
};
|
|
15
|
+
const itemSpans = (source) => {
|
|
16
|
+
const lineStarts = [0];
|
|
17
|
+
for (let index = 0; index < source.length; index += 1) {
|
|
18
|
+
if (source[index] === '\n')
|
|
19
|
+
lineStarts.push(index + 1);
|
|
20
|
+
}
|
|
21
|
+
const document = parseDocument(source);
|
|
22
|
+
const root = document.contents;
|
|
23
|
+
if (!isMap(root))
|
|
24
|
+
return [];
|
|
25
|
+
const spans = [];
|
|
26
|
+
for (const collection of ['concepts', 'relationships', 'states']) {
|
|
27
|
+
const pair = root.items.find((candidate) => isScalar(candidate.key) && candidate.key.value === collection);
|
|
28
|
+
if (pair === undefined || !isSeq(pair.value))
|
|
29
|
+
continue;
|
|
30
|
+
for (const item of pair.value.items) {
|
|
31
|
+
if (!isMap(item))
|
|
32
|
+
continue;
|
|
33
|
+
const idPair = item.items.find((field) => isScalar(field.key) && field.key.value === 'id');
|
|
34
|
+
if (idPair === undefined ||
|
|
35
|
+
!isScalar(idPair.value) ||
|
|
36
|
+
typeof idPair.value.value !== 'string') {
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
const range = item
|
|
40
|
+
.range;
|
|
41
|
+
if (range === undefined)
|
|
42
|
+
continue;
|
|
43
|
+
spans.push({
|
|
44
|
+
id: idPair.value.value,
|
|
45
|
+
collection,
|
|
46
|
+
startLine: lineOfOffset(lineStarts, range[0]),
|
|
47
|
+
// range[1] can extend past the trailing newline; anchor the end
|
|
48
|
+
// on the last content character instead.
|
|
49
|
+
endLine: lineOfOffset(lineStarts, Math.max(range[0], range[1] - 1)),
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return spans;
|
|
54
|
+
};
|
|
55
|
+
// New-side line ranges from `git diff --unified=0`. A pure deletion has a
|
|
56
|
+
// zero count; it still touches the position it collapsed onto.
|
|
57
|
+
const changedLineRanges = (diff) => {
|
|
58
|
+
const ranges = [];
|
|
59
|
+
for (const match of diff.matchAll(/^@@ -\d+(?:,\d+)? \+(\d+)(?:,(\d+))? @@/gm)) {
|
|
60
|
+
const start = Number(match[1]);
|
|
61
|
+
const count = match[2] === undefined ? 1 : Number(match[2]);
|
|
62
|
+
ranges.push(count === 0 ? [Math.max(start, 1), Math.max(start, 1)] : [start, start + count - 1]);
|
|
63
|
+
}
|
|
64
|
+
return ranges;
|
|
65
|
+
};
|
|
66
|
+
export function deriveChangedSubjects(cwd, range, documents) {
|
|
67
|
+
const probe = spawnSync('git', ['-C', cwd, 'rev-parse', '--git-dir'], {
|
|
68
|
+
encoding: 'utf8',
|
|
69
|
+
});
|
|
70
|
+
if (probe.status !== 0) {
|
|
71
|
+
return {
|
|
72
|
+
ok: false,
|
|
73
|
+
message: '--changed requires the workspace to live in a git repository',
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
const concepts = new Set();
|
|
77
|
+
const relationships = new Set();
|
|
78
|
+
for (const document of documents) {
|
|
79
|
+
const diffed = spawnSync('git', ['-C', cwd, 'diff', '--unified=0', range, '--', document.path], { encoding: 'utf8' });
|
|
80
|
+
if (diffed.status !== 0) {
|
|
81
|
+
return {
|
|
82
|
+
ok: false,
|
|
83
|
+
message: `git diff failed for range "${range}": ${(diffed.stderr ?? '').trim()}`,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
const ranges = changedLineRanges(diffed.stdout ?? '');
|
|
87
|
+
if (ranges.length === 0)
|
|
88
|
+
continue;
|
|
89
|
+
const spans = itemSpans(document.source);
|
|
90
|
+
for (const span of spans) {
|
|
91
|
+
if (span.collection === 'states')
|
|
92
|
+
continue;
|
|
93
|
+
const touched = ranges.some(([from, to]) => from <= span.endLine && to >= span.startLine);
|
|
94
|
+
if (!touched)
|
|
95
|
+
continue;
|
|
96
|
+
const qualified = `${document.documentId}#${span.id}`;
|
|
97
|
+
if (span.collection === 'concepts')
|
|
98
|
+
concepts.add(qualified);
|
|
99
|
+
else
|
|
100
|
+
relationships.add(qualified);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return {
|
|
104
|
+
ok: true,
|
|
105
|
+
changed: {
|
|
106
|
+
range,
|
|
107
|
+
concepts: [...concepts].sort(),
|
|
108
|
+
relationships: [...relationships].sort(),
|
|
109
|
+
},
|
|
110
|
+
};
|
|
111
|
+
}
|
package/dist/cli-support.d.ts
CHANGED
|
@@ -7,7 +7,7 @@ export interface CliResult {
|
|
|
7
7
|
export declare const isMainModule: (moduleUrl: string, entrypoint: string | undefined) => boolean;
|
|
8
8
|
export declare const packageVersion: string;
|
|
9
9
|
export declare const versionResult: (binary: string) => CliResult;
|
|
10
|
-
export declare const usage = "Usage:\n yarramate init <directory> [--no-pointer]\n yarramate design <workspace.yaml> [--subject <document-id>#<local-id>] [--catalogue <catalogue.yaml>] [--json]\n yarramate apply <operations.yaml> <workspace.yaml> [--json]\n yarramate ask <workspace.yaml> [--json]\n yarramate ask <workspace.yaml> \"<free text>\" | <document-id>#<local-id> ... | <projection.yaml> [--budget <tokens>] [--json]\n yarramate ask <workspace.yaml> --subjects [--kind <term>] [--status <status>] [--json]\n yarramate ask <workspace.yaml> --kinds [--json]\n yarramate ask <workspace.yaml> --advise \"<topic>\" [--budget <tokens>] [--catalogue <catalogue.yaml>] [--json]\n yarramate ask <workspace.yaml> --next [--json]\n yarramate ask <workspace.yaml> --open [--catalogue <catalogue.yaml>] [--json]\n yarramate ask <workspace.yaml> --compare <from-state> <to-state> [--json]\n yarramate check <source.yaml> [source.yaml ...] [--json] [--strict]\n yarramate reconcile <workspace.yaml>\n yarramate export graph <workspace.yaml> [--out <file>]\n yarramate export markdown <projection.yaml> <workspace.yaml> [--out <file>]\n yarramate export briefs <projection.yaml> <workspace.yaml> --out <directory> [--budget <tokens>]\n yarramate export likec4 <likec4-project.yaml> <output-dir> <workspace.yaml>\n";
|
|
10
|
+
export declare const usage = "Usage:\n yarramate init <directory> [--no-pointer]\n yarramate design <workspace.yaml> [--subject <document-id>#<local-id>] [--catalogue <catalogue.yaml>] [--json]\n yarramate apply <operations.yaml> <workspace.yaml> [--json]\n yarramate ask <workspace.yaml> [--json]\n yarramate ask <workspace.yaml> \"<free text>\" | <document-id>#<local-id> ... | <projection.yaml> [--budget <tokens>] [--json]\n yarramate ask <workspace.yaml> --subjects [--kind <term>] [--status <status>] [--json]\n yarramate ask <workspace.yaml> --kinds [--json]\n yarramate ask <workspace.yaml> --advise \"<topic>\" [--budget <tokens>] [--catalogue <catalogue.yaml>] [--json]\n yarramate ask <workspace.yaml> --next [--json]\n yarramate ask <workspace.yaml> --open [--catalogue <catalogue.yaml>] [--json]\n yarramate ask <workspace.yaml> --compare <from-state> <to-state> [--json]\n yarramate ask <workspace.yaml> --changed <git-range> [--budget <tokens>] [--json]\n yarramate check <source.yaml> [source.yaml ...] [--json] [--strict]\n yarramate reconcile <workspace.yaml>\n yarramate export graph <workspace.yaml> [--out <file>]\n yarramate export markdown <projection.yaml> <workspace.yaml> [--out <file>]\n yarramate export markdown --changed <git-range> <workspace.yaml> [--out <file>]\n yarramate export briefs <projection.yaml> <workspace.yaml> --out <directory> [--budget <tokens>]\n yarramate export briefs --changed <git-range> <workspace.yaml> --out <directory> [--budget <tokens>]\n yarramate export likec4 <likec4-project.yaml> <output-dir> <workspace.yaml>\n";
|
|
11
11
|
export declare const diagnosticJson: (diagnostics: unknown) => string;
|
|
12
12
|
export declare const checkResultJson: (ok: boolean, diagnostics: unknown, counted?: {
|
|
13
13
|
readonly documents: number;
|
package/dist/cli-support.js
CHANGED
|
@@ -23,7 +23,7 @@ export const versionResult = (binary) => ({
|
|
|
23
23
|
stdout: `${binary} ${packageVersion}\n`,
|
|
24
24
|
stderr: '',
|
|
25
25
|
});
|
|
26
|
-
export const usage = 'Usage:\n yarramate init <directory> [--no-pointer]\n yarramate design <workspace.yaml> [--subject <document-id>#<local-id>] [--catalogue <catalogue.yaml>] [--json]\n yarramate apply <operations.yaml> <workspace.yaml> [--json]\n yarramate ask <workspace.yaml> [--json]\n yarramate ask <workspace.yaml> "<free text>" | <document-id>#<local-id> ... | <projection.yaml> [--budget <tokens>] [--json]\n yarramate ask <workspace.yaml> --subjects [--kind <term>] [--status <status>] [--json]\n yarramate ask <workspace.yaml> --kinds [--json]\n yarramate ask <workspace.yaml> --advise "<topic>" [--budget <tokens>] [--catalogue <catalogue.yaml>] [--json]\n yarramate ask <workspace.yaml> --next [--json]\n yarramate ask <workspace.yaml> --open [--catalogue <catalogue.yaml>] [--json]\n yarramate ask <workspace.yaml> --compare <from-state> <to-state> [--json]\n yarramate check <source.yaml> [source.yaml ...] [--json] [--strict]\n yarramate reconcile <workspace.yaml>\n yarramate export graph <workspace.yaml> [--out <file>]\n yarramate export markdown <projection.yaml> <workspace.yaml> [--out <file>]\n yarramate export briefs <projection.yaml> <workspace.yaml> --out <directory> [--budget <tokens>]\n yarramate export likec4 <likec4-project.yaml> <output-dir> <workspace.yaml>\n';
|
|
26
|
+
export const usage = 'Usage:\n yarramate init <directory> [--no-pointer]\n yarramate design <workspace.yaml> [--subject <document-id>#<local-id>] [--catalogue <catalogue.yaml>] [--json]\n yarramate apply <operations.yaml> <workspace.yaml> [--json]\n yarramate ask <workspace.yaml> [--json]\n yarramate ask <workspace.yaml> "<free text>" | <document-id>#<local-id> ... | <projection.yaml> [--budget <tokens>] [--json]\n yarramate ask <workspace.yaml> --subjects [--kind <term>] [--status <status>] [--json]\n yarramate ask <workspace.yaml> --kinds [--json]\n yarramate ask <workspace.yaml> --advise "<topic>" [--budget <tokens>] [--catalogue <catalogue.yaml>] [--json]\n yarramate ask <workspace.yaml> --next [--json]\n yarramate ask <workspace.yaml> --open [--catalogue <catalogue.yaml>] [--json]\n yarramate ask <workspace.yaml> --compare <from-state> <to-state> [--json]\n yarramate ask <workspace.yaml> --changed <git-range> [--budget <tokens>] [--json]\n yarramate check <source.yaml> [source.yaml ...] [--json] [--strict]\n yarramate reconcile <workspace.yaml>\n yarramate export graph <workspace.yaml> [--out <file>]\n yarramate export markdown <projection.yaml> <workspace.yaml> [--out <file>]\n yarramate export markdown --changed <git-range> <workspace.yaml> [--out <file>]\n yarramate export briefs <projection.yaml> <workspace.yaml> --out <directory> [--budget <tokens>]\n yarramate export briefs --changed <git-range> <workspace.yaml> --out <directory> [--budget <tokens>]\n yarramate export likec4 <likec4-project.yaml> <output-dir> <workspace.yaml>\n';
|
|
27
27
|
export const diagnosticJson = (diagnostics) => `${JSON.stringify({
|
|
28
28
|
format: 'yarramate/diagnostic-result/v1',
|
|
29
29
|
diagnostics,
|
package/dist/export-command.js
CHANGED
|
@@ -4,6 +4,7 @@ import { dirname, join, resolve } from 'node:path';
|
|
|
4
4
|
import { fileURLToPath } from 'node:url';
|
|
5
5
|
import { parseDocument } from 'yaml';
|
|
6
6
|
import { renderBrief } from './brief.js';
|
|
7
|
+
import { deriveChangedSubjects } from './changed.js';
|
|
7
8
|
import { humanDiagnostics, usage } from './cli-support.js';
|
|
8
9
|
import { compileWorkspaceWithProfileContext, } from './compiler.js';
|
|
9
10
|
import { serializeSemanticGraph } from './graph.js';
|
|
@@ -23,6 +24,7 @@ const parseExportOptions = (options) => {
|
|
|
23
24
|
const positionals = [];
|
|
24
25
|
let out;
|
|
25
26
|
let budget;
|
|
27
|
+
let changed;
|
|
26
28
|
let json = false;
|
|
27
29
|
for (let index = 0; index < options.length; index += 1) {
|
|
28
30
|
const option = options[index];
|
|
@@ -30,7 +32,9 @@ const parseExportOptions = (options) => {
|
|
|
30
32
|
json = true;
|
|
31
33
|
continue;
|
|
32
34
|
}
|
|
33
|
-
if (option === '--out' ||
|
|
35
|
+
if (option === '--out' ||
|
|
36
|
+
option === '--budget' ||
|
|
37
|
+
option === '--changed') {
|
|
34
38
|
const value = options[index + 1];
|
|
35
39
|
if (value === undefined || value.startsWith('-'))
|
|
36
40
|
return undefined;
|
|
@@ -39,6 +43,11 @@ const parseExportOptions = (options) => {
|
|
|
39
43
|
return undefined;
|
|
40
44
|
out = value;
|
|
41
45
|
}
|
|
46
|
+
else if (option === '--changed') {
|
|
47
|
+
if (changed !== undefined)
|
|
48
|
+
return undefined;
|
|
49
|
+
changed = value;
|
|
50
|
+
}
|
|
42
51
|
else {
|
|
43
52
|
if (budget !== undefined || !/^[1-9][0-9]*$/.test(value)) {
|
|
44
53
|
return undefined;
|
|
@@ -56,6 +65,7 @@ const parseExportOptions = (options) => {
|
|
|
56
65
|
positionals,
|
|
57
66
|
...(out === undefined ? {} : { out }),
|
|
58
67
|
...(budget === undefined ? {} : { budget }),
|
|
68
|
+
...(changed === undefined ? {} : { changed }),
|
|
59
69
|
json,
|
|
60
70
|
};
|
|
61
71
|
};
|
|
@@ -103,12 +113,14 @@ export function runExportCommand(options, cwd) {
|
|
|
103
113
|
stderr: delegated.stderr ?? '',
|
|
104
114
|
};
|
|
105
115
|
}
|
|
106
|
-
const
|
|
116
|
+
const usesChanged = parsed.changed !== undefined;
|
|
117
|
+
const expectedPositionals = kind === 'graph' || usesChanged ? 1 : 2;
|
|
107
118
|
const workspacePath = parsed.positionals[expectedPositionals - 1];
|
|
108
|
-
const projectionPath = kind === 'graph' ? undefined : parsed.positionals[0];
|
|
119
|
+
const projectionPath = kind === 'graph' || usesChanged ? undefined : parsed.positionals[0];
|
|
109
120
|
if (parsed.positionals.length !== expectedPositionals ||
|
|
110
121
|
workspacePath === undefined ||
|
|
111
122
|
parsed.json ||
|
|
123
|
+
(usesChanged && kind === 'graph') ||
|
|
112
124
|
(parsed.budget !== undefined && kind !== 'briefs') ||
|
|
113
125
|
(kind === 'briefs' && parsed.out === undefined)) {
|
|
114
126
|
return { exitCode: 2, stdout: '', stderr: usage };
|
|
@@ -151,13 +163,51 @@ export function runExportCommand(options, cwd) {
|
|
|
151
163
|
stderr: '',
|
|
152
164
|
};
|
|
153
165
|
}
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
166
|
+
let result;
|
|
167
|
+
if (parsed.changed !== undefined) {
|
|
168
|
+
// Review slices derive from git (ADR 0065): changed subjects seed
|
|
169
|
+
// the connected neighbourhood the reviewer inspects.
|
|
170
|
+
const documentIdByPath = new Map(compilation.graph.documents.map(({ id, source }) => [source, id]));
|
|
171
|
+
const derived = deriveChangedSubjects(cwd, parsed.changed, workspace.documents.map((path) => ({
|
|
172
|
+
path,
|
|
173
|
+
source: readFileSync(resolve(cwd, path), 'utf8'),
|
|
174
|
+
documentId: documentIdByPath.get(path) ?? path,
|
|
175
|
+
})));
|
|
176
|
+
if (!derived.ok) {
|
|
177
|
+
return { exitCode: 2, stdout: '', stderr: `${derived.message}\n` };
|
|
178
|
+
}
|
|
179
|
+
const endpoints = new Set();
|
|
180
|
+
for (const relationshipId of derived.changed.relationships) {
|
|
181
|
+
const claim = compilation.graph.claims.find((candidate) => candidate.id === relationshipId && 'ref' in candidate.object);
|
|
182
|
+
if (claim !== undefined && 'ref' in claim.object) {
|
|
183
|
+
endpoints.add(claim.subject);
|
|
184
|
+
endpoints.add(claim.object.ref);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
const seeds = [
|
|
188
|
+
...new Set([...derived.changed.concepts, ...endpoints]),
|
|
189
|
+
].sort();
|
|
190
|
+
result = evaluateProjection(compilation.graph, {
|
|
191
|
+
format: 'yarramate/projection/v1',
|
|
192
|
+
id: 'review-slice',
|
|
193
|
+
version: '0.0',
|
|
194
|
+
query: { subjects: seeds, relationships: 'connected' },
|
|
195
|
+
presentation: {
|
|
196
|
+
title: `Review slice ${parsed.changed}`,
|
|
197
|
+
description: `Connected neighbourhood of the subjects changed in ` +
|
|
198
|
+
`${parsed.changed}.`,
|
|
199
|
+
},
|
|
200
|
+
}, compilation.profileContext);
|
|
201
|
+
}
|
|
202
|
+
else {
|
|
203
|
+
const loadedProjection = loadProjection({
|
|
204
|
+
path: projectionPath,
|
|
205
|
+
source: readFileSync(resolve(cwd, projectionPath), 'utf8'),
|
|
206
|
+
});
|
|
207
|
+
if (!loadedProjection.ok)
|
|
208
|
+
return failed(loadedProjection.diagnostics);
|
|
209
|
+
result = evaluateProjection(compilation.graph, loadedProjection.projection, compilation.profileContext);
|
|
210
|
+
}
|
|
161
211
|
if (kind === 'markdown') {
|
|
162
212
|
const rendered = renderProjectionMarkdown(result);
|
|
163
213
|
if (parsed.out === undefined) {
|
|
@@ -12,12 +12,6 @@ const indexGraph = (graph) => {
|
|
|
12
12
|
const stateSubjects = new Set(graph.claims
|
|
13
13
|
.filter(({ predicate }) => predicate === 'yarramate/state/type')
|
|
14
14
|
.map(({ subject }) => subject));
|
|
15
|
-
// Architecture states carry concept subjects in the graph but are not
|
|
16
|
-
// enrichment targets; the catalogue interrogates the model, not the
|
|
17
|
-
// planning overlay.
|
|
18
|
-
const concepts = new Set(graph.subjects
|
|
19
|
-
.filter(({ id, type }) => type === 'concept' && !stateSubjects.has(id))
|
|
20
|
-
.map(({ id }) => id));
|
|
21
15
|
const claimsBySubject = new Map();
|
|
22
16
|
const kindOf = new Map();
|
|
23
17
|
const nameOf = new Map();
|
|
@@ -50,6 +44,16 @@ const indexGraph = (graph) => {
|
|
|
50
44
|
}
|
|
51
45
|
}
|
|
52
46
|
}
|
|
47
|
+
// Architecture states carry concept subjects in the graph but are not
|
|
48
|
+
// enrichment targets; the catalogue interrogates the model, not the
|
|
49
|
+
// planning overlay. Retired concepts are excluded for the same reason
|
|
50
|
+
// (ADR 0064): retirement is the recorded decision that a subject left
|
|
51
|
+
// the design conversation, so no question stays open against it.
|
|
52
|
+
const concepts = new Set(graph.subjects
|
|
53
|
+
.filter(({ id, type }) => type === 'concept' &&
|
|
54
|
+
!stateSubjects.has(id) &&
|
|
55
|
+
statusOf.get(id) !== 'retired')
|
|
56
|
+
.map(({ id }) => id));
|
|
53
57
|
return {
|
|
54
58
|
concepts,
|
|
55
59
|
claimsBySubject,
|