yarramate 1.5.0 → 1.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/dist/adapters/visual/session-server.js +27 -1
- package/dist/adapters/visual/workspace-model.d.ts +16 -3
- package/dist/adapters/visual/workspace-model.js +12 -5
- package/dist/ask-command.js +25 -16
- package/dist/catalogue-sources.d.ts +20 -0
- package/dist/catalogue-sources.js +39 -0
- package/dist/check-command.js +38 -9
- package/dist/cli-support.d.ts +8 -1
- package/dist/cli-support.js +5 -1
- package/dist/cli.d.ts +10 -0
- package/dist/cli.js +17 -1
- package/dist/design-command.js +21 -9
- package/dist/export-command.js +41 -5
- package/dist/import-command.d.ts +16 -0
- package/dist/import-command.js +156 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/interrogate-command.d.ts +82 -1
- package/dist/interrogate-command.js +195 -29
- package/dist/interrogation-entry.d.ts +1 -1
- package/dist/interrogation-entry.js +1 -1
- package/dist/projection.d.ts +47 -0
- package/dist/projection.js +126 -0
- package/dist/visual-app-lib/editor.js +26771 -26713
- package/dist/visual-app-lib/types/adapters/visual/workspace-model.d.ts +16 -3
- package/dist/visual-app-lib/types/interrogate-command.d.ts +82 -1
- package/dist/visual-app-lib/types/projection.d.ts +47 -0
- package/dist/visual-app-lib/types/workspace.d.ts +15 -0
- package/dist/workbook-entry.d.ts +16 -0
- package/dist/workbook-entry.js +16 -0
- package/dist/workbook-import-entry.d.ts +30 -0
- package/dist/workbook-import-entry.js +30 -0
- package/dist/workbook-merge.d.ts +77 -0
- package/dist/workbook-merge.js +108 -0
- package/dist/workbook-operations.d.ts +9 -0
- package/dist/workbook-operations.js +139 -0
- package/dist/workbook-read.d.ts +43 -0
- package/dist/workbook-read.js +314 -0
- package/dist/workbook-xlsx.d.ts +54 -0
- package/dist/workbook-xlsx.js +228 -0
- package/dist/workbook.d.ts +32 -0
- package/dist/workbook.js +308 -0
- package/dist/workspace.d.ts +15 -0
- package/dist/workspace.js +1 -0
- package/docs/CONSUMING-YARRAMATE.md +127 -0
- package/package.json +9 -1
- package/schema/yarramate-interrogation-report.schema.json +9 -0
- package/schema/yarramate-question-catalogue.schema.json +2 -3
- package/schema/yarramate-workspace.schema.json +4 -0
package/dist/workbook.js
ADDED
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
import { writeXlsx } from './workbook-xlsx.js';
|
|
2
|
+
/**
|
|
3
|
+
* The model as a workbook an architect can work in (#355).
|
|
4
|
+
*
|
|
5
|
+
* The shape is borrowed from a real consulting workbook rather than invented:
|
|
6
|
+
* identity is columnar and readability is derived. A stable id in column A,
|
|
7
|
+
* foreign keys inline, and an adjacent `(auto)` column carrying the referenced
|
|
8
|
+
* subject's NAME so a sheet reads without hunting across tabs. The id is what
|
|
9
|
+
* round-trips; the `(auto)` column is a convenience and is ignored on import.
|
|
10
|
+
*
|
|
11
|
+
* Losslessness does not depend on this file enumerating every predicate
|
|
12
|
+
* correctly. Well-known predicates become columns because a person needs them
|
|
13
|
+
* to be columns; **everything else lands in `07 Other Facts` verbatim**, so a
|
|
14
|
+
* predicate added to the compiler after this was written still survives a
|
|
15
|
+
* round trip. A mapping that silently dropped what it did not recognise is the
|
|
16
|
+
* failure this design exists to avoid.
|
|
17
|
+
*/
|
|
18
|
+
export const WORKBOOK_FORMAT = 'yarramate/workbook/v1';
|
|
19
|
+
/** Consumed into a column on `01 Concepts`, in this order. */
|
|
20
|
+
const CONCEPT_COLUMNS = [
|
|
21
|
+
['yarramate/concept/kind', 'Kind'],
|
|
22
|
+
['yarramate/concept/name', 'Name'],
|
|
23
|
+
['yarramate/concept/description', 'Description'],
|
|
24
|
+
['yarramate/lifecycle/status', 'Status'],
|
|
25
|
+
['yarramate/ownership/owner', 'Owner'],
|
|
26
|
+
['yarramate/organisation/folder', 'Folder'],
|
|
27
|
+
];
|
|
28
|
+
/** Consumed into a column on `02 Relationships`, in this order. */
|
|
29
|
+
const RELATIONSHIP_COLUMNS = [
|
|
30
|
+
['yarramate/relationship/name', 'Name'],
|
|
31
|
+
['yarramate/relationship/description', 'Description'],
|
|
32
|
+
['yarramate/access/mode', 'Mode'],
|
|
33
|
+
['yarramate/flow/content', 'Content'],
|
|
34
|
+
['yarramate/lifecycle/status', 'Status'],
|
|
35
|
+
];
|
|
36
|
+
/**
|
|
37
|
+
* Consumed into a column on `03 States`.
|
|
38
|
+
*
|
|
39
|
+
* `concept/kind` is here as well as on `01 Concepts` because a state IS a
|
|
40
|
+
* concept: it carries an ordinary kind (`plateau`) alongside its state type
|
|
41
|
+
* (`baseline`, `transition`, `target`). Omitting it dropped four kind claims
|
|
42
|
+
* into the overflow sheet, which round-tripped correctly and read as though
|
|
43
|
+
* the mapping had not recognised them.
|
|
44
|
+
*/
|
|
45
|
+
const STATE_COLUMNS = [
|
|
46
|
+
['yarramate/concept/kind', 'Kind'],
|
|
47
|
+
['yarramate/state/type', 'State kind'],
|
|
48
|
+
['yarramate/concept/name', 'Name'],
|
|
49
|
+
['yarramate/concept/description', 'Description'],
|
|
50
|
+
];
|
|
51
|
+
/** Ref-valued predicates that get their own sheet rather than the overflow. */
|
|
52
|
+
const REFERENCE_PREDICATES = new Set([
|
|
53
|
+
'yarramate/reference/refers-to',
|
|
54
|
+
'yarramate/lineage/supersedes',
|
|
55
|
+
'yarramate/lineage/supersedes-respect',
|
|
56
|
+
'yarramate/identity/distinct-from',
|
|
57
|
+
'yarramate/constraint/requires',
|
|
58
|
+
'yarramate/constraint/expects',
|
|
59
|
+
]);
|
|
60
|
+
const ALIAS_PREDICATE = 'yarramate/concept/alias';
|
|
61
|
+
const PRESENT_IN_PREDICATE = 'yarramate/state/present-in';
|
|
62
|
+
const STATE_TYPE_PREDICATE = 'yarramate/state/type';
|
|
63
|
+
const STATE_AFTER_PREDICATE = 'yarramate/state/after';
|
|
64
|
+
const KIND_PREDICATE = 'yarramate/concept/kind';
|
|
65
|
+
const NAME_PREDICATE = 'yarramate/concept/name';
|
|
66
|
+
const valueOf = (claim) => 'value' in claim.object ? claim.object.value : claim.object.ref;
|
|
67
|
+
const isRef = (claim) => 'ref' in claim.object;
|
|
68
|
+
/**
|
|
69
|
+
* A relationship IS a claim: its `id` is the relationship id, its `subject` is
|
|
70
|
+
* the source, its `predicate` is the kind and its object refs the target.
|
|
71
|
+
* Claims ABOUT a relationship carry that id as their own subject.
|
|
72
|
+
*/
|
|
73
|
+
const relationshipClaims = (result) => {
|
|
74
|
+
const relationshipIds = new Set(result.subjects
|
|
75
|
+
.filter(({ type }) => type === 'relationship')
|
|
76
|
+
.map(({ id }) => id));
|
|
77
|
+
return result.claims.filter((claim) => relationshipIds.has(claim.id));
|
|
78
|
+
};
|
|
79
|
+
export const buildWorkbookSheets = (result, provenance) => {
|
|
80
|
+
const conceptIds = new Set(result.subjects.filter(({ type }) => type === 'concept').map(({ id }) => id));
|
|
81
|
+
const relationshipIds = new Set(result.subjects
|
|
82
|
+
.filter(({ type }) => type === 'relationship')
|
|
83
|
+
.map(({ id }) => id));
|
|
84
|
+
const bySubject = new Map();
|
|
85
|
+
for (const claim of result.claims) {
|
|
86
|
+
const held = bySubject.get(claim.subject);
|
|
87
|
+
if (held === undefined)
|
|
88
|
+
bySubject.set(claim.subject, [claim]);
|
|
89
|
+
else
|
|
90
|
+
held.push(claim);
|
|
91
|
+
}
|
|
92
|
+
const nameOf = (id) => {
|
|
93
|
+
const claim = bySubject
|
|
94
|
+
.get(id)
|
|
95
|
+
?.find(({ predicate }) => predicate === NAME_PREDICATE);
|
|
96
|
+
return claim === undefined ? '' : valueOf(claim);
|
|
97
|
+
};
|
|
98
|
+
const first = (id, predicate) => {
|
|
99
|
+
const claim = bySubject.get(id)?.find((held) => held.predicate === predicate);
|
|
100
|
+
return claim === undefined ? '' : valueOf(claim);
|
|
101
|
+
};
|
|
102
|
+
/**
|
|
103
|
+
* The document a subject is declared in, as a PATH.
|
|
104
|
+
*
|
|
105
|
+
* Carried because an `apply` operation targets a document path, so without
|
|
106
|
+
* it a workbook can say what a subject is and not where it lives, and
|
|
107
|
+
* nothing can be written back. Every claim knows its own source, so this is
|
|
108
|
+
* recovered rather than guessed.
|
|
109
|
+
*/
|
|
110
|
+
const documentOf = (id) => bySubject.get(id)?.[0]?.source.path ?? '';
|
|
111
|
+
const documentOfClaim = (claim) => claim.source.path;
|
|
112
|
+
// A state is an ordinary concept carrying state predicates, so it is lifted
|
|
113
|
+
// onto its own sheet rather than being a second kind of thing.
|
|
114
|
+
const stateIds = new Set([...conceptIds].filter((id) => bySubject
|
|
115
|
+
.get(id)
|
|
116
|
+
?.some(({ predicate }) => predicate === STATE_TYPE_PREDICATE) === true));
|
|
117
|
+
/** Every claim a column or a named sheet has already carried. */
|
|
118
|
+
const consumed = new Set();
|
|
119
|
+
const consume = (id, predicate) => {
|
|
120
|
+
const claim = bySubject.get(id)?.find((held) => held.predicate === predicate);
|
|
121
|
+
if (claim !== undefined)
|
|
122
|
+
consumed.add(claim.id);
|
|
123
|
+
};
|
|
124
|
+
const conceptRows = [
|
|
125
|
+
['Concept ID', ...CONCEPT_COLUMNS.map(([, label]) => label), 'Document'],
|
|
126
|
+
];
|
|
127
|
+
for (const id of [...conceptIds].filter((one) => !stateIds.has(one)).sort()) {
|
|
128
|
+
for (const [predicate] of CONCEPT_COLUMNS)
|
|
129
|
+
consume(id, predicate);
|
|
130
|
+
conceptRows.push([
|
|
131
|
+
id,
|
|
132
|
+
...CONCEPT_COLUMNS.map(([predicate]) => first(id, predicate)),
|
|
133
|
+
documentOf(id),
|
|
134
|
+
]);
|
|
135
|
+
}
|
|
136
|
+
const relationshipRows = [
|
|
137
|
+
[
|
|
138
|
+
'Relationship ID',
|
|
139
|
+
'Kind',
|
|
140
|
+
'From',
|
|
141
|
+
'↳ From name (auto)',
|
|
142
|
+
'To',
|
|
143
|
+
'↳ To name (auto)',
|
|
144
|
+
...RELATIONSHIP_COLUMNS.map(([, label]) => label),
|
|
145
|
+
'Document',
|
|
146
|
+
],
|
|
147
|
+
];
|
|
148
|
+
for (const claim of [...relationshipClaims(result)].sort((left, right) => left.id < right.id ? -1 : left.id > right.id ? 1 : 0)) {
|
|
149
|
+
consumed.add(claim.id);
|
|
150
|
+
for (const [predicate] of RELATIONSHIP_COLUMNS)
|
|
151
|
+
consume(claim.id, predicate);
|
|
152
|
+
const target = 'ref' in claim.object ? claim.object.ref : '';
|
|
153
|
+
relationshipRows.push([
|
|
154
|
+
claim.id,
|
|
155
|
+
claim.predicate,
|
|
156
|
+
claim.subject,
|
|
157
|
+
nameOf(claim.subject),
|
|
158
|
+
target,
|
|
159
|
+
nameOf(target),
|
|
160
|
+
...RELATIONSHIP_COLUMNS.map(([predicate]) => first(claim.id, predicate)),
|
|
161
|
+
documentOfClaim(claim),
|
|
162
|
+
]);
|
|
163
|
+
}
|
|
164
|
+
const stateRows = [
|
|
165
|
+
[
|
|
166
|
+
'State ID',
|
|
167
|
+
...STATE_COLUMNS.map(([, label]) => label),
|
|
168
|
+
'After',
|
|
169
|
+
'↳ After name (auto)',
|
|
170
|
+
'Document',
|
|
171
|
+
],
|
|
172
|
+
];
|
|
173
|
+
for (const id of [...stateIds].sort()) {
|
|
174
|
+
for (const [predicate] of STATE_COLUMNS)
|
|
175
|
+
consume(id, predicate);
|
|
176
|
+
consume(id, STATE_AFTER_PREDICATE);
|
|
177
|
+
const after = first(id, STATE_AFTER_PREDICATE);
|
|
178
|
+
stateRows.push([
|
|
179
|
+
id,
|
|
180
|
+
...STATE_COLUMNS.map(([predicate]) => first(id, predicate)),
|
|
181
|
+
after,
|
|
182
|
+
nameOf(after),
|
|
183
|
+
documentOf(id),
|
|
184
|
+
]);
|
|
185
|
+
}
|
|
186
|
+
const presentInRows = [
|
|
187
|
+
['Subject ID', '↳ Subject name (auto)', 'State ID', '↳ State name (auto)'],
|
|
188
|
+
];
|
|
189
|
+
const referenceRows = [
|
|
190
|
+
['Subject ID', '↳ Subject name (auto)', 'Predicate', 'Target ID', '↳ Target name (auto)'],
|
|
191
|
+
];
|
|
192
|
+
const aliasRows = [['Subject ID', '↳ Subject name (auto)', 'Also known as']];
|
|
193
|
+
const otherRows = [['Subject ID', 'Predicate', 'Value', 'Target ID']];
|
|
194
|
+
for (const claim of result.claims) {
|
|
195
|
+
if (consumed.has(claim.id))
|
|
196
|
+
continue;
|
|
197
|
+
if (relationshipIds.has(claim.id))
|
|
198
|
+
continue;
|
|
199
|
+
if (claim.predicate === PRESENT_IN_PREDICATE) {
|
|
200
|
+
const state = valueOf(claim);
|
|
201
|
+
presentInRows.push([claim.subject, nameOf(claim.subject), state, nameOf(state)]);
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
if (claim.predicate === ALIAS_PREDICATE) {
|
|
205
|
+
aliasRows.push([claim.subject, nameOf(claim.subject), valueOf(claim)]);
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
if (REFERENCE_PREDICATES.has(claim.predicate)) {
|
|
209
|
+
const target = valueOf(claim);
|
|
210
|
+
referenceRows.push([
|
|
211
|
+
claim.subject,
|
|
212
|
+
nameOf(claim.subject),
|
|
213
|
+
claim.predicate,
|
|
214
|
+
target,
|
|
215
|
+
nameOf(target),
|
|
216
|
+
]);
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
// The overflow, and the reason a round trip stays lossless when the
|
|
220
|
+
// compiler grows a predicate this file has never heard of.
|
|
221
|
+
otherRows.push([
|
|
222
|
+
claim.subject,
|
|
223
|
+
claim.predicate,
|
|
224
|
+
isRef(claim) ? '' : valueOf(claim),
|
|
225
|
+
isRef(claim) ? valueOf(claim) : '',
|
|
226
|
+
]);
|
|
227
|
+
}
|
|
228
|
+
const readMe = [
|
|
229
|
+
['YarraMate workbook'],
|
|
230
|
+
[],
|
|
231
|
+
['Workspace', provenance.workspace],
|
|
232
|
+
['Projection', result.projection],
|
|
233
|
+
['Built by', `yarramate ${provenance.yarramateVersion}`],
|
|
234
|
+
[],
|
|
235
|
+
['How to use this'],
|
|
236
|
+
['1.', 'Edit the numbered sheets. Column A is the identity of the row and is what the model is keyed on.'],
|
|
237
|
+
['2.', 'A column marked (auto) is derived for readability. Edits to it are ignored.'],
|
|
238
|
+
['3.', 'Add a row with a new ID to add a subject. Kind and Status are dropdowns, and Document says which file it belongs to.'],
|
|
239
|
+
['4.', 'Deleting a row does NOT delete anything. Removals are reported, never applied.'],
|
|
240
|
+
['5.', 'Send the file back and import it. Your edits are merged; only a field changed on both sides is refused.'],
|
|
241
|
+
[],
|
|
242
|
+
[],
|
|
243
|
+
['What this holds, and what it does not'],
|
|
244
|
+
[
|
|
245
|
+
'',
|
|
246
|
+
`A projection is a QUERY, so this workbook is the slice it names: ${conceptRows.length - 1} concepts and ${relationshipRows.length - 1} relationships.`,
|
|
247
|
+
],
|
|
248
|
+
[
|
|
249
|
+
'',
|
|
250
|
+
'Everything the model knows about a subject that IS here is here, including anything these columns do not model, on 07 Other Facts.',
|
|
251
|
+
],
|
|
252
|
+
[
|
|
253
|
+
'',
|
|
254
|
+
'A subject outside the slice has no row, and importing this file cannot change it. A narrow workbook is safe against a wide model.',
|
|
255
|
+
],
|
|
256
|
+
[],
|
|
257
|
+
['Sheets beginning ~ are machinery. Do not edit them.'],
|
|
258
|
+
];
|
|
259
|
+
const listColumns = [
|
|
260
|
+
['Concept kind', ...provenance.conceptKinds],
|
|
261
|
+
['Relationship kind', ...provenance.relationshipKinds],
|
|
262
|
+
['Status', ...provenance.statuses],
|
|
263
|
+
];
|
|
264
|
+
const listHeight = Math.max(...listColumns.map((column) => column.length));
|
|
265
|
+
const listRows = [];
|
|
266
|
+
for (let row = 0; row < listHeight; row += 1) {
|
|
267
|
+
listRows.push(listColumns.map((column) => column[row] ?? ''));
|
|
268
|
+
}
|
|
269
|
+
const metaRows = [
|
|
270
|
+
['Key', 'Value'],
|
|
271
|
+
['Format', WORKBOOK_FORMAT],
|
|
272
|
+
['Workspace', provenance.workspace],
|
|
273
|
+
['Projection', result.projection],
|
|
274
|
+
['YarraMate version', provenance.yarramateVersion],
|
|
275
|
+
...Object.entries(provenance.sourceDigests)
|
|
276
|
+
.sort(([left], [right]) => (left < right ? -1 : 1))
|
|
277
|
+
.map(([path, digest]) => [`Digest ${path}`, digest]),
|
|
278
|
+
];
|
|
279
|
+
const working = [
|
|
280
|
+
{ name: '00 Read Me', rows: readMe },
|
|
281
|
+
{ name: '01 Concepts', rows: conceptRows, frozenRows: 1 },
|
|
282
|
+
{ name: '02 Relationships', rows: relationshipRows, frozenRows: 1 },
|
|
283
|
+
{ name: '03 States', rows: stateRows, frozenRows: 1 },
|
|
284
|
+
{ name: '04 Present In', rows: presentInRows, frozenRows: 1 },
|
|
285
|
+
{ name: '05 References', rows: referenceRows, frozenRows: 1 },
|
|
286
|
+
{ name: '06 Aliases', rows: aliasRows, frozenRows: 1 },
|
|
287
|
+
{ name: '07 Other Facts', rows: otherRows, frozenRows: 1 },
|
|
288
|
+
];
|
|
289
|
+
// The merge ancestor. Never edited, never updated: it records what the model
|
|
290
|
+
// looked like when this workbook was made, so import can tell an FDE's edit
|
|
291
|
+
// from a change the repository made underneath since. Each row carries its
|
|
292
|
+
// sheet name so one sheet can hold them all.
|
|
293
|
+
const baselineRows = [['Sheet', 'Cells...']];
|
|
294
|
+
for (const sheet of working) {
|
|
295
|
+
if (sheet.name === '00 Read Me')
|
|
296
|
+
continue;
|
|
297
|
+
for (const row of sheet.rows)
|
|
298
|
+
baselineRows.push([sheet.name, ...row]);
|
|
299
|
+
}
|
|
300
|
+
return [
|
|
301
|
+
...working,
|
|
302
|
+
{ name: '~Lists', state: 'hidden', rows: listRows },
|
|
303
|
+
{ name: '~Meta', state: 'hidden', rows: metaRows },
|
|
304
|
+
{ name: '~Baseline', state: 'veryHidden', rows: baselineRows },
|
|
305
|
+
];
|
|
306
|
+
};
|
|
307
|
+
/** The workbook, as bytes. Synchronous and deterministic. */
|
|
308
|
+
export const workbookFrom = (result, provenance) => writeXlsx(buildWorkbookSheets(result, provenance));
|
package/dist/workspace.d.ts
CHANGED
|
@@ -7,6 +7,7 @@ export interface WorkspaceManifest {
|
|
|
7
7
|
readonly projections: readonly string[];
|
|
8
8
|
readonly adapterMappings: readonly string[];
|
|
9
9
|
readonly patterns?: readonly string[];
|
|
10
|
+
readonly questions?: readonly string[];
|
|
10
11
|
readonly evidence?: readonly string[];
|
|
11
12
|
readonly contracts?: readonly string[];
|
|
12
13
|
}
|
|
@@ -17,6 +18,20 @@ export interface ResolvedWorkspace {
|
|
|
17
18
|
readonly projections: readonly string[];
|
|
18
19
|
readonly adapterMappings: readonly string[];
|
|
19
20
|
readonly patterns: readonly string[];
|
|
21
|
+
/**
|
|
22
|
+
* Question catalogues the workspace itself carries (#345, ADR 0129).
|
|
23
|
+
* ADDITIVE to the shipped catalogue: `--catalogue` replaces the base, this
|
|
24
|
+
* adds to it, which is what lets a consultant author a question mid
|
|
25
|
+
* engagement without a product release.
|
|
26
|
+
*
|
|
27
|
+
* OPTIONAL in the type although `loadWorkspaceManifest` always populates it,
|
|
28
|
+
* and that is deliberate. `ResolvedWorkspace` is published, and adding
|
|
29
|
+
* `patterns` to it as a required field broke a consumer's production module:
|
|
30
|
+
* a required field is free for readers and a break for CONSTRUCTORS. Six
|
|
31
|
+
* fixtures in this repository construct one, which is the same signal from
|
|
32
|
+
* inside. Read it as `workspace.questions ?? []`.
|
|
33
|
+
*/
|
|
34
|
+
readonly questions?: readonly string[];
|
|
20
35
|
readonly evidence: readonly string[];
|
|
21
36
|
readonly contracts: readonly string[];
|
|
22
37
|
}
|
package/dist/workspace.js
CHANGED
|
@@ -107,6 +107,7 @@ export function loadWorkspaceManifest(source, cwd) {
|
|
|
107
107
|
projections: expand('projections', 'projection', value.projections),
|
|
108
108
|
adapterMappings: expand('adapterMappings', 'adapter mapping', value.adapterMappings),
|
|
109
109
|
patterns: expand('patterns', 'pattern', value.patterns ?? []),
|
|
110
|
+
questions: expand('questions', 'question catalogue', value.questions ?? []),
|
|
110
111
|
evidence: expand('evidence', 'evidence', value.evidence ?? []),
|
|
111
112
|
contracts: expand('contracts', 'Core contract', value.contracts ?? []),
|
|
112
113
|
};
|
|
@@ -408,6 +408,133 @@ socket/session host.
|
|
|
408
408
|
|
|
409
409
|
[...]
|
|
410
410
|
|
|
411
|
+
## The model as an Excel workbook
|
|
412
|
+
|
|
413
|
+
`yarramate export xlsx <projection.yaml> <workspace.yaml> --out <file>` writes
|
|
414
|
+
a workbook an architect or FDE can work in, and `yarramate/workbook` publishes
|
|
415
|
+
the writer for a host that has no Node:
|
|
416
|
+
|
|
417
|
+
```ts
|
|
418
|
+
import { workbookFrom } from 'yarramate/workbook'
|
|
419
|
+
|
|
420
|
+
const bytes = workbookFrom(projectionResult, {
|
|
421
|
+
workspace: 'acme',
|
|
422
|
+
yarramateVersion: '1.5.0',
|
|
423
|
+
sourceDigests,
|
|
424
|
+
conceptKinds,
|
|
425
|
+
relationshipKinds,
|
|
426
|
+
statuses: ['planned', 'current', 'retired'],
|
|
427
|
+
})
|
|
428
|
+
```
|
|
429
|
+
|
|
430
|
+
Synchronous, dependency-free and deterministic: the same input always produces
|
|
431
|
+
the same bytes. The import graph is held free of Node builtins and of the
|
|
432
|
+
compiler at runtime, so it fits a Cloudflare Worker or a Durable Object. A
|
|
433
|
+
caller hands over an already-evaluated `ProjectionResult`, which is what keeps
|
|
434
|
+
schema validation out of that graph.
|
|
435
|
+
|
|
436
|
+
**It takes a projection**, so it inherits every facet a projection query has.
|
|
437
|
+
That is how a caller chooses which *version* to export: a query naming
|
|
438
|
+
`states` produces a workbook of that architecture state, with no separate
|
|
439
|
+
flag.
|
|
440
|
+
|
|
441
|
+
```yaml
|
|
442
|
+
query:
|
|
443
|
+
states:
|
|
444
|
+
- target-state
|
|
445
|
+
```
|
|
446
|
+
|
|
447
|
+
**Which makes a workbook a SLICE, and it is worth being plain about that.** An
|
|
448
|
+
omitted facet imposes no constraint, so `query: {}` exports the whole model,
|
|
449
|
+
and every facet you name narrows it. For a subject the query selected, nothing
|
|
450
|
+
is dropped: the projection filters claims by subject membership rather than by
|
|
451
|
+
predicate, so every fact about an included subject arrives, and `07 Other
|
|
452
|
+
Facts` catches whatever the named columns do not model. For a subject it did
|
|
453
|
+
not select, there is simply no row, and a missing row is never a deletion, so
|
|
454
|
+
**a narrow workbook cannot damage a wide model on the way back in**. The
|
|
455
|
+
`00 Read Me` sheet says all of this and reports the counts the slice actually
|
|
456
|
+
has, because an FDE handed a filtered workbook with no note will read it as
|
|
457
|
+
the model.
|
|
458
|
+
|
|
459
|
+
**Reading the sheets.** Column A is always the id and is what the model is
|
|
460
|
+
keyed on. A column headed `↳ … (auto)` is derived for readability and is
|
|
461
|
+
ignored on the way back. Kind and status columns are drawn from the compiled
|
|
462
|
+
profile, so they carry the vocabulary that workspace actually has.
|
|
463
|
+
|
|
464
|
+
Sheets beginning `~` are machinery. `~Meta` records the format, the versions
|
|
465
|
+
and the source digests. `~Baseline` is a hidden copy of the working rows
|
|
466
|
+
exactly as exported, and exists so a later import can tell an author's edit
|
|
467
|
+
from a change the repository made underneath. Neither is edited.
|
|
468
|
+
|
|
469
|
+
Anything the mapping does not recognise is carried verbatim on `07 Other
|
|
470
|
+
Facts` rather than dropped, so a workbook stays lossless across a compiler
|
|
471
|
+
that grows new predicates.
|
|
472
|
+
|
|
473
|
+
**Reading one back.** `yarramate import xlsx <workbook.xlsx> <workspace.yaml>`
|
|
474
|
+
merges an edited workbook into the model. An unedited round trip changes
|
|
475
|
+
nothing, byte for byte.
|
|
476
|
+
|
|
477
|
+
Edits land as `yarramate/operations/v1` through `apply`, so untouched YAML
|
|
478
|
+
keeps its comments, key order and formatting, and the import passes the same
|
|
479
|
+
atomic gate every other write does.
|
|
480
|
+
|
|
481
|
+
It is a three-way merge. `~Baseline` is the ancestor, so an edit the workspace
|
|
482
|
+
did not touch merges even if the repository moved on; only a field changed on
|
|
483
|
+
**both** sides is refused, and a refusal writes nothing anywhere. A missing row
|
|
484
|
+
is reported and never treated as a deletion, and a new row needs its `Document`
|
|
485
|
+
column filled in.
|
|
486
|
+
|
|
487
|
+
**On a host with no Node**, `yarramate/workbook/import` publishes the same
|
|
488
|
+
three steps the CLI runs, so ingesting a workbook is not CLI-only:
|
|
489
|
+
|
|
490
|
+
```ts
|
|
491
|
+
import {
|
|
492
|
+
readWorkbook,
|
|
493
|
+
baselineSheets,
|
|
494
|
+
mergeWorkbook,
|
|
495
|
+
operationsFrom,
|
|
496
|
+
operationsDocument,
|
|
497
|
+
} from 'yarramate/workbook/import'
|
|
498
|
+
|
|
499
|
+
const read = await readWorkbook(bytes)
|
|
500
|
+
if (!read.ok) return refuse(read.reason)
|
|
501
|
+
|
|
502
|
+
const ancestor = read.sheets.get('~Baseline')
|
|
503
|
+
if (ancestor === undefined) return refuse('not produced by yarramate export xlsx')
|
|
504
|
+
|
|
505
|
+
const report = mergeWorkbook(
|
|
506
|
+
read.sheets,
|
|
507
|
+
baselineSheets(ancestor),
|
|
508
|
+
buildWorkbookSheets(currentResult, provenance),
|
|
509
|
+
)
|
|
510
|
+
if (report.conflicts.length > 0) return refuse(report.conflicts)
|
|
511
|
+
|
|
512
|
+
const { operations, refusals } = operationsFrom(report, read.sheets)
|
|
513
|
+
await apply(operationsDocument(operations))
|
|
514
|
+
```
|
|
515
|
+
|
|
516
|
+
`readWorkbook` is **async** while the writer is synchronous, and deliberately
|
|
517
|
+
so: a workbook this package wrote has stored entries and inline strings, but
|
|
518
|
+
one a person saved from Excel comes back deflated and shared-stringed, and
|
|
519
|
+
inflation is only offered as a stream (`DecompressionStream('deflate-raw')`,
|
|
520
|
+
present on Workers, in browsers and in Node 18+). Keeping the writer
|
|
521
|
+
synchronous is what stops it infecting a synchronous store on the host side.
|
|
522
|
+
|
|
523
|
+
It is a **separate subpath** from `yarramate/workbook` on purpose. The package
|
|
524
|
+
declares no `sideEffects` field, so a bundler must assume every module might
|
|
525
|
+
have one and cannot shake an unused re-export away; a host that only generates
|
|
526
|
+
workbooks would otherwise carry the reader, the merge and the operations
|
|
527
|
+
emitter it never calls. `test/export-purity.test.ts` holds the two entries
|
|
528
|
+
disjoint as well as pure.
|
|
529
|
+
|
|
530
|
+
The host keeps three jobs: reading the bytes, evaluating the projection that
|
|
531
|
+
says what the model holds **now** (the third argument to `mergeWorkbook`, which
|
|
532
|
+
is what measures repo drift in the same terms the author edited in), and
|
|
533
|
+
applying the operations. Refusal wording is the host's too. The `Conflict`
|
|
534
|
+
values name the sheet, row, column, what the author wrote, what the workspace
|
|
535
|
+
now holds and their common ancestor, which reads like a merge tool rather than
|
|
536
|
+
like something you hand a consultant.
|
|
537
|
+
|
|
411
538
|
## MCP server for agent harnesses
|
|
412
539
|
|
|
413
540
|
Harnesses that load MCP servers can connect the bundled read-only adapter:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "yarramate",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.7.0",
|
|
4
4
|
"description": "Tool-neutral semantic architecture engine and guided methodology",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -58,6 +58,14 @@
|
|
|
58
58
|
"types": "./dist/interrogation-entry.d.ts",
|
|
59
59
|
"import": "./dist/interrogation-entry.js"
|
|
60
60
|
},
|
|
61
|
+
"./workbook": {
|
|
62
|
+
"types": "./dist/workbook-entry.d.ts",
|
|
63
|
+
"import": "./dist/workbook-entry.js"
|
|
64
|
+
},
|
|
65
|
+
"./workbook/import": {
|
|
66
|
+
"types": "./dist/workbook-import-entry.d.ts",
|
|
67
|
+
"import": "./dist/workbook-import-entry.js"
|
|
68
|
+
},
|
|
61
69
|
"./visual-app": {
|
|
62
70
|
"types": "./dist/visual-app-lib/editor.d.ts",
|
|
63
71
|
"import": "./dist/visual-app-lib/editor.js"
|
|
@@ -24,6 +24,15 @@
|
|
|
24
24
|
"type": "string",
|
|
25
25
|
"minLength": 1
|
|
26
26
|
},
|
|
27
|
+
"catalogues": {
|
|
28
|
+
"description": "Every catalogue that contributed, as id@version, base first (#345, ADR 0129). Present only when more than one did. OPTIONAL, and `catalogue` keeps naming the base with its existing value shape, because turning that field into an array would break every reader: an optional additive field is free for readers and does not force constructors. Question ids in this report are qualified as catalogue#question and carry NO version, so a catalogue version bump strands no stored judgment keyed on them; the versions live here instead.",
|
|
29
|
+
"type": "array",
|
|
30
|
+
"minItems": 1,
|
|
31
|
+
"items": {
|
|
32
|
+
"type": "string",
|
|
33
|
+
"minLength": 1
|
|
34
|
+
}
|
|
35
|
+
},
|
|
27
36
|
"semantics": {
|
|
28
37
|
"type": "string",
|
|
29
38
|
"minLength": 1,
|
|
@@ -44,9 +44,9 @@
|
|
|
44
44
|
}
|
|
45
45
|
},
|
|
46
46
|
"waves": {
|
|
47
|
-
"description": "Ordered interview phases. Array order is interview order. The wave taxonomy is catalogue opinion, not engine semantics.",
|
|
47
|
+
"description": "Ordered interview phases. Array order is interview order. The wave taxonomy is catalogue opinion, not engine semantics. MAY BE EMPTY (#345, ADR 0129): a catalogue that only contributes questions to waves ANOTHER catalogue declared is the ordinary shape of a project catalogue, and requiring a wave here would force it to declare one it does not want - which is refused as a duplicate the moment two catalogues do it. Evaluated alone, a catalogue with no waves and a question naming one is YM911, correctly: nothing declares it.",
|
|
48
48
|
"type": "array",
|
|
49
|
-
"minItems":
|
|
49
|
+
"minItems": 0,
|
|
50
50
|
"items": {
|
|
51
51
|
"type": "object",
|
|
52
52
|
"additionalProperties": false,
|
|
@@ -112,7 +112,6 @@
|
|
|
112
112
|
"description": "Which subjects a subject-scope question applies to. Field vocabulary mirrors the projection query selector (ADR 0017).",
|
|
113
113
|
"type": "object",
|
|
114
114
|
"additionalProperties": false,
|
|
115
|
-
|
|
116
115
|
"properties": {
|
|
117
116
|
"kinds": {
|
|
118
117
|
"type": "array",
|
|
@@ -32,6 +32,10 @@
|
|
|
32
32
|
"description": "yarramate/pattern/v1 documents: the shape a concept kind promises, and the wiring the compiler mints between an instance and the parts it binds (ADR 0123). Structure, where profiles declare vocabulary. Optional, like evidence and contracts.",
|
|
33
33
|
"$ref": "#/$defs/patterns"
|
|
34
34
|
},
|
|
35
|
+
"questions": {
|
|
36
|
+
"description": "yarramate/question-catalogue/v1 documents the WORKSPACE carries: questions true of this engagement and nowhere else (#345, ADR 0129). ADDITIVE to the shipped catalogue rather than replacing it, which is what --catalogue and MountOptions.catalogue do. Question ids are qualified as catalogue#question on the way out, so two catalogues may carry the same local id. A wave is declared exactly once across the resolved set, and any catalogue may contribute questions to a wave it did not declare. Optional, like patterns, evidence and contracts.",
|
|
37
|
+
"$ref": "#/$defs/patterns"
|
|
38
|
+
},
|
|
35
39
|
"adapterMappings": {
|
|
36
40
|
"$ref": "#/$defs/patterns"
|
|
37
41
|
},
|