eidosmd 0.1.0 → 0.3.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 +50 -29
- package/browser/dist/assets/index-Cc3cNWHY.css +1 -0
- package/browser/dist/assets/index-DQgCQRa5.js +46 -0
- package/browser/dist/favicon.svg +5 -0
- package/browser/dist/index.html +15 -0
- package/browser/dist/mark.svg +4 -0
- package/dist/src/cli.js +7 -0
- package/dist/src/commands/agents.js +1 -1
- package/dist/src/commands/canvas.js +77 -0
- package/dist/src/commands/check.js +10 -4
- package/dist/src/commands/configure.js +201 -0
- package/dist/src/commands/framework.js +37 -7
- package/dist/src/commands/index.js +4 -4
- package/dist/src/commands/init.js +5 -0
- package/dist/src/commands/instructions.js +1 -1
- package/dist/src/commands/list.js +8 -8
- package/dist/src/commands/migrate.js +32 -0
- package/dist/src/commands/new.js +3 -3
- package/dist/src/commands/property.js +125 -0
- package/dist/src/commands/seeds.js +5 -5
- package/dist/src/commands/setup.js +131 -0
- package/dist/src/commands/version.js +44 -0
- package/dist/src/commands/whoami.js +4 -4
- package/dist/src/context.js +5 -5
- package/dist/src/core/blueprint.js +16 -11
- package/dist/src/core/canvas-schema.js +148 -0
- package/dist/src/core/canvas.js +732 -0
- package/dist/src/core/check.js +221 -70
- package/dist/src/core/convert.js +7 -6
- package/dist/src/core/edits.js +1381 -0
- package/dist/src/core/framework-markdown.js +119 -34
- package/dist/src/core/framework-model.js +62 -10
- package/dist/src/core/framework-structured.js +222 -47
- package/dist/src/core/framework.js +13 -13
- package/dist/src/core/frontmatter.js +61 -1
- package/dist/src/core/git.js +84 -0
- package/dist/src/core/index-leaf.js +2 -2
- package/dist/src/core/links.js +87 -0
- package/dist/src/core/markdown.js +16 -9
- package/dist/src/core/me.js +16 -8
- package/dist/src/core/migrate.js +319 -0
- package/dist/src/core/naming.js +1 -1
- package/dist/src/core/regions.js +117 -0
- package/dist/src/core/root.js +2 -2
- package/dist/src/core/scaffold.js +33 -27
- package/dist/src/core/seed.js +187 -71
- package/dist/src/core/server.js +1410 -53
- package/dist/src/core/settings.js +232 -0
- package/dist/src/core/store.js +315 -0
- package/dist/src/core/template.js +32 -0
- package/dist/src/core/versions.js +84 -0
- package/dist/src/output.js +4 -1
- package/dist/src/program.js +421 -41
- package/instructions/authoring.md +15 -12
- package/instructions/configuring.md +81 -34
- package/instructions/init-required.md +4 -4
- package/instructions/overview.md +21 -9
- package/instructions/validating.md +9 -6
- package/package.json +21 -12
- package/standard/EIDOS.md +142 -259
- package/standard/seeds/README.md +12 -16
- package/standard/seeds/book/Framework.yaml +61 -0
- package/standard/seeds/book/README.md +10 -5
- package/standard/seeds/book/_gitignore +9 -3
- package/standard/seeds/book/me.md +1 -1
- package/standard/seeds/book/roles/README.md +3 -3
- package/standard/seeds/book/roles/framework-owner.md +2 -2
- package/standard/seeds/book/{shapes → templates}/chapter.full.md +0 -8
- package/standard/seeds/book/{shapes → templates}/chapter.sketch.md +0 -7
- package/standard/seeds/book/{shapes → templates}/frame.market.md +0 -6
- package/standard/seeds/book/templates/frame.premise.md +17 -0
- package/standard/seeds/book/{shapes → templates}/frame.reader.md +0 -6
- package/standard/seeds/book/{shapes → templates}/frame.voice.md +0 -7
- package/standard/seeds/research/Framework.yaml +61 -0
- package/standard/seeds/research/README.md +10 -5
- package/standard/seeds/research/_gitignore +9 -3
- package/standard/seeds/research/me.md +1 -1
- package/standard/seeds/research/roles/README.md +3 -3
- package/standard/seeds/research/roles/framework-owner.md +2 -2
- package/standard/seeds/research/{shapes → templates}/frame.ethics.md +0 -6
- package/standard/seeds/research/{shapes → templates}/frame.method.md +0 -7
- package/standard/seeds/research/{shapes → templates}/frame.prior-work.md +0 -6
- package/standard/seeds/research/{shapes → templates}/frame.question.md +0 -7
- package/standard/seeds/research/{shapes → templates}/investigation.full.md +0 -8
- package/standard/seeds/research/{shapes → templates}/investigation.note.md +0 -7
- package/standard/seeds/software/Framework.yaml +62 -0
- package/standard/seeds/software/README.md +7 -6
- package/standard/seeds/software/_gitignore +9 -3
- package/standard/seeds/software/me.md +1 -1
- package/standard/seeds/software/roles/README.md +3 -3
- package/standard/seeds/software/roles/framework-owner.md +2 -2
- package/standard/seeds/software/roles/project-manager.md +2 -2
- package/standard/seeds/software/roles/stakeholder.md +1 -1
- package/standard/seeds/software/{shapes → templates}/frame.architecture.md +0 -7
- package/standard/seeds/software/{shapes → templates}/frame.audience.md +1 -8
- package/standard/seeds/software/{shapes → templates}/frame.criteria.md +0 -8
- package/standard/seeds/software/{shapes → templates}/frame.market.md +0 -8
- package/standard/seeds/software/{shapes → templates}/spec.full.md +0 -8
- package/standard/seeds/software/{shapes → templates}/spec.micro.md +0 -9
- package/browser/index.html +0 -268
- package/dist/src/commands/convert.js +0 -30
- package/dist/src/core/shape.js +0 -26
- package/standard/seeds/book/Framework.md +0 -87
- package/standard/seeds/book/shapes/frame.premise.md +0 -24
- package/standard/seeds/research/Framework.md +0 -88
- package/standard/seeds/software/Framework.md +0 -88
- /package/standard/seeds/software/{shapes → templates}/.gitkeep +0 -0
|
@@ -1,13 +1,16 @@
|
|
|
1
|
-
// The YAML framework document,
|
|
1
|
+
// The YAML framework document, `.eidos/Framework.yaml`: the same model as the
|
|
2
2
|
// markdown form, as fields in the frontmatter's snake_case, for a root that
|
|
3
3
|
// scripts and agents read.
|
|
4
|
-
// Paths are relative to
|
|
4
|
+
// Paths are relative to `.eidos/`, as the markdown links are. Every folder
|
|
5
|
+
// at the root is declared under `folders` with its type (a 5.2.x root's
|
|
6
|
+
// `collections` still reads, every entry a collection). A structured
|
|
5
7
|
// document also carries the generated index under `index`, one list per
|
|
6
8
|
// collection, which `eidos index` rewrites in place without disturbing
|
|
7
9
|
// anything a person wrote around it.
|
|
8
10
|
import { Document, isMap, isPair, isScalar, parse as parseYaml, parseDocument } from 'yaml';
|
|
9
|
-
import { groupingProperty, STANDARD_CORE, } from './framework-model.js';
|
|
11
|
+
import { readValueStyle, coreRequires, groupingProperty, isCollection, isFolderType, STANDARD_CORE, TOOL_KEY, unitOf } from './framework-model.js';
|
|
10
12
|
import { DEFAULT_NAMING, isNaming } from './naming.js';
|
|
13
|
+
const STANDARD_PROPERTY_KEYS = new Set(['name', 'type', 'applies_to', 'required', 'options', 'meaning']);
|
|
11
14
|
function isPlain(value) {
|
|
12
15
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
13
16
|
}
|
|
@@ -51,48 +54,89 @@ function readCanvas(value, where, problems) {
|
|
|
51
54
|
problems.push(`${where}: canvas must be file, card, or { mode: card, section: <Section> }`);
|
|
52
55
|
return null;
|
|
53
56
|
}
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
+
// `template` (`shape` before 5.0.0) is the file's path relative to `.eidos/`.
|
|
58
|
+
function readVariant(value, where, problems) {
|
|
59
|
+
const given = isPlain(value) ? text(value['template']) || text(value['shape']) : '';
|
|
60
|
+
if (!isPlain(value) || text(value['name']) === '' || given === '') {
|
|
61
|
+
problems.push(`${where}: every variant needs a name and a template`);
|
|
57
62
|
return null;
|
|
58
63
|
}
|
|
59
|
-
const
|
|
60
|
-
const file =
|
|
64
|
+
const template = given.replace(/\\/g, '/');
|
|
65
|
+
const file = template.split('/').pop() ?? template;
|
|
61
66
|
const stem = file.replace(/\.md$/i, '');
|
|
62
67
|
const dot = stem.indexOf('.');
|
|
63
68
|
return {
|
|
64
69
|
name: text(value['name']),
|
|
65
|
-
|
|
66
|
-
|
|
70
|
+
unit: dot === -1 ? stem : stem.slice(0, dot),
|
|
71
|
+
template,
|
|
67
72
|
description: text(value['description']),
|
|
68
73
|
isDefault: value['default'] === true,
|
|
69
74
|
};
|
|
70
75
|
}
|
|
71
|
-
|
|
72
|
-
|
|
76
|
+
// `vocabulary`: one entry per term; `not` is a list (a single string reads as one entry).
|
|
77
|
+
function readVocabulary(value, problems) {
|
|
78
|
+
const out = [];
|
|
79
|
+
list(value).forEach((entry, index) => {
|
|
80
|
+
if (!isPlain(entry) || text(entry['term']) === '') {
|
|
81
|
+
problems.push(`vocabulary[${index}]: a term needs a term`);
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
const nots = Array.isArray(entry['not']) ? entry['not'] : entry['not'] === undefined || entry['not'] === null ? [] : [entry['not']];
|
|
85
|
+
out.push({
|
|
86
|
+
term: text(entry['term']),
|
|
87
|
+
means: text(entry['means']),
|
|
88
|
+
not: nots.map((item) => text(item)).filter((item) => item !== ''),
|
|
89
|
+
see: text(entry['see']) === '' ? null : text(entry['see']),
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
return out;
|
|
93
|
+
}
|
|
94
|
+
// One entry of `folders` (or of a 5.2.x root's `collections`, every one a
|
|
95
|
+
// collection). A type outside the standard's three is a fault; the folder is
|
|
96
|
+
// still read as declared (as `other`) so the root is not also reported as
|
|
97
|
+
// undeclared.
|
|
98
|
+
function readFolder(value, key, index, problems) {
|
|
99
|
+
const where = `${key}[${index}]`;
|
|
73
100
|
if (!isPlain(value) || text(value['name']) === '') {
|
|
74
|
-
problems.push(`${where}: a
|
|
101
|
+
problems.push(`${where}: a folder needs a name`);
|
|
75
102
|
return null;
|
|
76
103
|
}
|
|
77
104
|
const name = text(value['name']);
|
|
78
|
-
const
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
105
|
+
const declared = value['type'];
|
|
106
|
+
let type = 'collection';
|
|
107
|
+
if (isFolderType(declared)) {
|
|
108
|
+
type = declared;
|
|
109
|
+
}
|
|
110
|
+
else if (key === 'folders') {
|
|
111
|
+
problems.push(`${where} (${name}): type must be one of collection, assets, other${declared === undefined || declared === null ? '' : `, not ${text(declared)}`}`);
|
|
112
|
+
type = 'other';
|
|
113
|
+
}
|
|
114
|
+
if (type !== 'collection') {
|
|
115
|
+
return { type, name, description: text(value['description']) };
|
|
116
|
+
}
|
|
117
|
+
return readCollection(value, where, name, problems);
|
|
118
|
+
}
|
|
119
|
+
function readCollection(value, where, name, problems) {
|
|
120
|
+
const variants = list(value['variants'] ?? value['flavors'])
|
|
121
|
+
.map((variant, position) => readVariant(variant, `${where} (${name}) variants[${position}]`, problems))
|
|
122
|
+
.filter((variant) => variant !== null);
|
|
123
|
+
if (variants.length > 0 && !variants.some((variant) => variant.isDefault)) {
|
|
124
|
+
const first = variants[0];
|
|
83
125
|
if (first) {
|
|
84
126
|
first.isDefault = true;
|
|
85
127
|
}
|
|
86
128
|
}
|
|
87
129
|
const collection = {
|
|
130
|
+
type: 'collection',
|
|
88
131
|
name,
|
|
89
132
|
description: text(value['description']),
|
|
90
|
-
framing:
|
|
133
|
+
framing: false,
|
|
91
134
|
leaf: null,
|
|
92
|
-
|
|
135
|
+
variants,
|
|
93
136
|
canvas: readCanvas(value['canvas'], `${where} (${name})`, problems),
|
|
94
137
|
grouping: null,
|
|
95
138
|
};
|
|
139
|
+
collection.framing = unitOf(collection) === 'frame';
|
|
96
140
|
const grouping = value['grouping'];
|
|
97
141
|
if (grouping !== undefined && grouping !== null) {
|
|
98
142
|
if (!isPlain(grouping) || text(grouping['label']) === '') {
|
|
@@ -111,7 +155,7 @@ function readCollection(value, index, problems) {
|
|
|
111
155
|
}
|
|
112
156
|
return collection;
|
|
113
157
|
}
|
|
114
|
-
function readProperty(value, where, core, problems) {
|
|
158
|
+
function readProperty(value, where, core, problems, owner = core ? 'eidos' : 'custom') {
|
|
115
159
|
if (!isPlain(value) || text(value['name']) === '') {
|
|
116
160
|
problems.push(`${where}: a property needs a name`);
|
|
117
161
|
return null;
|
|
@@ -132,7 +176,94 @@ function readProperty(value, where, core, problems) {
|
|
|
132
176
|
problems.push(`${where}: applies_to must be all or a list of collections`);
|
|
133
177
|
}
|
|
134
178
|
}
|
|
135
|
-
|
|
179
|
+
// `required` is true or false, absent meaning false; on the standard's own
|
|
180
|
+
// block an absent key means what the core for this version says, so a root
|
|
181
|
+
// written before the key existed still requires its id and title.
|
|
182
|
+
const declaredRequired = value['required'];
|
|
183
|
+
let required = declaredRequired === true;
|
|
184
|
+
if (declaredRequired === undefined || declaredRequired === null) {
|
|
185
|
+
required = core && coreRequires(text(value['name']));
|
|
186
|
+
}
|
|
187
|
+
else if (typeof declaredRequired !== 'boolean') {
|
|
188
|
+
problems.push(`${where}: required must be true or false`);
|
|
189
|
+
}
|
|
190
|
+
const property = { name: text(value['name']), type: text(value['type']) || 'Text', appliesTo: applies, required, meaning: text(value['meaning']), core, owner };
|
|
191
|
+
// `options` closes the value to a declared set: a non-empty list on a Text
|
|
192
|
+
// or List property. An empty list is a fault, since the absent key already
|
|
193
|
+
// says the set is open; on any other type the list means nothing.
|
|
194
|
+
const declaredOptions = value['options'];
|
|
195
|
+
if (declaredOptions !== undefined && declaredOptions !== null) {
|
|
196
|
+
const options = Array.isArray(declaredOptions) ? declaredOptions.map(text).filter((option) => option !== '') : [];
|
|
197
|
+
if (!Array.isArray(declaredOptions) || options.length === 0) {
|
|
198
|
+
problems.push(`${where}: options must be a non-empty list of values`);
|
|
199
|
+
}
|
|
200
|
+
else if (!['text', 'list'].includes(property.type.toLowerCase())) {
|
|
201
|
+
problems.push(`${where}: options apply to a Text or List property, not ${property.type}`);
|
|
202
|
+
}
|
|
203
|
+
else {
|
|
204
|
+
property.options = options;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
const tools = {};
|
|
208
|
+
for (const [key, field] of Object.entries(value)) {
|
|
209
|
+
if (!STANDARD_PROPERTY_KEYS.has(key) && key !== 'canvas' && field !== undefined && field !== null) {
|
|
210
|
+
tools[key] = field;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
if (Object.keys(tools).length > 0) {
|
|
214
|
+
property.tools = tools;
|
|
215
|
+
}
|
|
216
|
+
// This tool's hint sits under its own key; a bare `canvas` (how the first
|
|
217
|
+
// canvases were written) still reads.
|
|
218
|
+
const own = isPlain(tools[TOOL_KEY]) ? tools[TOOL_KEY]['canvas'] : undefined;
|
|
219
|
+
const hint = readCanvasHint(own) ?? readCanvasHint(value['canvas']);
|
|
220
|
+
if (hint) {
|
|
221
|
+
property.canvas = hint;
|
|
222
|
+
}
|
|
223
|
+
return property;
|
|
224
|
+
}
|
|
225
|
+
function readStringMap(value) {
|
|
226
|
+
if (!isPlain(value)) {
|
|
227
|
+
return undefined;
|
|
228
|
+
}
|
|
229
|
+
const out = {};
|
|
230
|
+
for (const [key, item] of Object.entries(value)) {
|
|
231
|
+
if (typeof item === 'string' || typeof item === 'number') {
|
|
232
|
+
out[key] = String(item);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
return Object.keys(out).length > 0 ? out : undefined;
|
|
236
|
+
}
|
|
237
|
+
// `styles` maps a value to a whole node style; the first canvases wrote a
|
|
238
|
+
// `shape` map and a `color` map instead, which still read, folded into it.
|
|
239
|
+
export function readCanvasHint(value) {
|
|
240
|
+
if (!isPlain(value)) {
|
|
241
|
+
return null;
|
|
242
|
+
}
|
|
243
|
+
const hint = {};
|
|
244
|
+
if (value['show'] === true) {
|
|
245
|
+
hint.show = true;
|
|
246
|
+
}
|
|
247
|
+
const styles = {};
|
|
248
|
+
if (isPlain(value['styles'])) {
|
|
249
|
+
for (const [key, item] of Object.entries(value['styles'])) {
|
|
250
|
+
const style = readValueStyle(item);
|
|
251
|
+
if (style)
|
|
252
|
+
styles[key] = style;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
const shape = readStringMap(value['shape']);
|
|
256
|
+
for (const [key, item] of Object.entries(shape ?? {})) {
|
|
257
|
+
styles[key] = { ...(styles[key] ?? {}), shape: styles[key]?.shape ?? item };
|
|
258
|
+
}
|
|
259
|
+
const color = readStringMap(value['color']);
|
|
260
|
+
for (const [key, item] of Object.entries(color ?? {})) {
|
|
261
|
+
styles[key] = { ...(styles[key] ?? {}), color: styles[key]?.color ?? item };
|
|
262
|
+
}
|
|
263
|
+
if (Object.keys(styles).length > 0) {
|
|
264
|
+
hint.styles = styles;
|
|
265
|
+
}
|
|
266
|
+
return Object.keys(hint).length > 0 ? hint : null;
|
|
136
267
|
}
|
|
137
268
|
export function readEmbeddedIndex(value) {
|
|
138
269
|
if (!isPlain(value)) {
|
|
@@ -178,19 +309,31 @@ export function parseFrameworkStructured(source, root, file) {
|
|
|
178
309
|
namingError = `naming: ${text(declaredNaming)} is not one of kebab-case, TitleCase, Title Case`;
|
|
179
310
|
}
|
|
180
311
|
}
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
312
|
+
// `folders` since 5.3.0; a root not yet migrated still declares `collections`,
|
|
313
|
+
// every one a collection, and reads so that `migrate` and `check` can see it.
|
|
314
|
+
const foldersKey = document['folders'] === undefined && document['collections'] !== undefined ? 'collections' : 'folders';
|
|
315
|
+
const folders = list(document[foldersKey])
|
|
316
|
+
.map((folder, index) => readFolder(folder, foldersKey, index, problems))
|
|
317
|
+
.filter((folder) => folder !== null);
|
|
318
|
+
if (!Array.isArray(document[foldersKey]) && !error && isPlain(value)) {
|
|
319
|
+
problems.push(`${foldersKey} must be a list`);
|
|
186
320
|
}
|
|
187
|
-
const
|
|
321
|
+
const collections = folders.filter(isCollection);
|
|
322
|
+
const schema = isPlain(document['properties']) ? document['properties'] : isPlain(document['schema']) ? document['schema'] : {};
|
|
188
323
|
const core = list(schema['core'])
|
|
189
324
|
.map((property, index) => readProperty(property, `schema.core[${index}]`, true, problems))
|
|
190
325
|
.filter((property) => property !== null);
|
|
191
326
|
const custom = list(schema['custom'])
|
|
192
327
|
.map((property, index) => readProperty(property, `schema.custom[${index}]`, false, problems))
|
|
193
328
|
.filter((property) => property !== null);
|
|
329
|
+
const tools = {};
|
|
330
|
+
if (isPlain(schema['tools'])) {
|
|
331
|
+
for (const [tool, block] of Object.entries(schema['tools'])) {
|
|
332
|
+
tools[tool] = list(block)
|
|
333
|
+
.map((property, index) => readProperty(property, `schema.tools.${tool}[${index}]`, false, problems, tool))
|
|
334
|
+
.filter((property) => property !== null);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
194
337
|
for (const collection of collections) {
|
|
195
338
|
if (collection.grouping && collection.grouping.property === null) {
|
|
196
339
|
collection.grouping.property = groupingProperty(collection, custom);
|
|
@@ -208,15 +351,22 @@ export function parseFrameworkStructured(source, root, file) {
|
|
|
208
351
|
naming,
|
|
209
352
|
namingError,
|
|
210
353
|
topLevel,
|
|
354
|
+
folders,
|
|
211
355
|
collections,
|
|
212
|
-
schema: { core: core.length > 0 ? core : STANDARD_CORE, custom },
|
|
356
|
+
schema: { core: core.length > 0 ? core : STANDARD_CORE, custom, tools },
|
|
357
|
+
vocabulary: readVocabulary(document['vocabulary'], problems),
|
|
213
358
|
problems,
|
|
214
359
|
};
|
|
215
360
|
}
|
|
361
|
+
// The standard's block as a document writes it: `required` only where it is
|
|
362
|
+
// true, since absent means false. `migrate` writes the same rows.
|
|
363
|
+
export function coreEntries(core) {
|
|
364
|
+
return core.map(({ name, type, required, options, meaning, tools }) => ({ name, type, ...(required ? { required: true } : {}), ...(options ? { options } : {}), meaning, ...(tools ?? {}) }));
|
|
365
|
+
}
|
|
216
366
|
// The document form of a framework: what `eidos framework --json` prints and
|
|
217
367
|
// what a YAML file holds, without the generated index.
|
|
218
368
|
// Keys land in the order a reader expects: version and naming, then the
|
|
219
|
-
// top-level docs, the
|
|
369
|
+
// top-level docs, the folders, and the schema; `index` is added last by
|
|
220
370
|
// `eidos index`.
|
|
221
371
|
export function frameworkToDocument(framework) {
|
|
222
372
|
const document = {};
|
|
@@ -233,11 +383,15 @@ export function frameworkToDocument(framework) {
|
|
|
233
383
|
return entry;
|
|
234
384
|
});
|
|
235
385
|
}
|
|
236
|
-
document.
|
|
237
|
-
const entry = { name:
|
|
238
|
-
if (
|
|
239
|
-
entry.description =
|
|
386
|
+
document.folders = framework.folders.map((folder) => {
|
|
387
|
+
const entry = { name: folder.name, type: folder.type };
|
|
388
|
+
if (folder.description) {
|
|
389
|
+
entry.description = folder.description;
|
|
390
|
+
}
|
|
391
|
+
if (!isCollection(folder)) {
|
|
392
|
+
return entry;
|
|
240
393
|
}
|
|
394
|
+
const collection = folder;
|
|
241
395
|
if (collection.canvas) {
|
|
242
396
|
if (collection.canvas.mode === 'file') {
|
|
243
397
|
entry.canvas = 'file';
|
|
@@ -249,12 +403,12 @@ export function frameworkToDocument(framework) {
|
|
|
249
403
|
entry.canvas = { mode: 'card', section: collection.canvas.section };
|
|
250
404
|
}
|
|
251
405
|
}
|
|
252
|
-
entry.
|
|
253
|
-
const item = { name:
|
|
254
|
-
if (
|
|
255
|
-
item.description =
|
|
406
|
+
entry.variants = collection.variants.map((variant) => {
|
|
407
|
+
const item = { name: variant.name, template: variant.template };
|
|
408
|
+
if (variant.description) {
|
|
409
|
+
item.description = variant.description;
|
|
256
410
|
}
|
|
257
|
-
if (
|
|
411
|
+
if (variant.isDefault) {
|
|
258
412
|
item.default = true;
|
|
259
413
|
}
|
|
260
414
|
return item;
|
|
@@ -276,23 +430,44 @@ export function frameworkToDocument(framework) {
|
|
|
276
430
|
}
|
|
277
431
|
return entry;
|
|
278
432
|
});
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
433
|
+
const entry = ({ name, type, appliesTo: applies, required, options, meaning, canvas, tools }) => {
|
|
434
|
+
const fields = { ...(tools ?? {}) };
|
|
435
|
+
if (canvas) {
|
|
436
|
+
const own = isPlain(fields[TOOL_KEY]) ? fields[TOOL_KEY] : {};
|
|
437
|
+
fields[TOOL_KEY] = { ...own, canvas };
|
|
438
|
+
}
|
|
439
|
+
return { name, type, applies_to: applies, ...(required ? { required: true } : {}), ...(options ? { options } : {}), meaning, ...fields };
|
|
440
|
+
};
|
|
441
|
+
document.properties = {
|
|
442
|
+
core: coreEntries(framework.schema.core),
|
|
443
|
+
custom: framework.schema.custom.map(entry),
|
|
282
444
|
};
|
|
445
|
+
if (Object.keys(framework.schema.tools).length > 0) {
|
|
446
|
+
document.properties.tools = Object.fromEntries(Object.entries(framework.schema.tools).map(([tool, block]) => [tool, block.map(entry)]));
|
|
447
|
+
}
|
|
448
|
+
if (framework.vocabulary.length > 0) {
|
|
449
|
+
document.vocabulary = framework.vocabulary.map((term) => {
|
|
450
|
+
const entry = { term: term.term, means: term.means, not: term.not };
|
|
451
|
+
if (term.see)
|
|
452
|
+
entry.see = term.see;
|
|
453
|
+
return entry;
|
|
454
|
+
});
|
|
455
|
+
}
|
|
283
456
|
return document;
|
|
284
457
|
}
|
|
285
|
-
const YAML_GUIDANCE = {
|
|
458
|
+
export const YAML_GUIDANCE = {
|
|
286
459
|
eidos_version: ' The Eidos version this framework targets; migrate reads and bumps it.',
|
|
287
460
|
naming: ' How files, folders, and links are named: kebab-case | TitleCase | Title Case. Settled once.',
|
|
288
|
-
top_level: '
|
|
289
|
-
|
|
290
|
-
|
|
461
|
+
top_level: ' Every file at the root, one entry each, in the order they are read (the browser lands on the first): a README, a Roadmap, a Vision.',
|
|
462
|
+
folders: " Every folder at the root, one entry each, with its type: collection | assets | other. A collection declares its variants (one default; each a template file named <unit>.<variant>.md under templates/, the unit being the word for one of its blueprints) and its grouping; an assets or other folder is a name, a type, and a description, and nothing inside it is read.\n Add a group under `grouping.groups` with a one-line description; add a variant with a template file in templates/.",
|
|
463
|
+
properties: " The Properties table, one block per owner. `core` is the standard's and moves with eidos_version; `custom` is yours: name, type (Text | List | Number | Checkbox | Date | Date & time), applies_to (all or a list), required (true puts it on every new blueprint and notes one that lacks it; absent = false, written when it has a value), options (the closed set a Text value is one of, or a List's elements are, in order; absent = any value), meaning; `tools.<tool>` is a block a tool declared and alone writes.\n A key past those six on a row is a tool's, named for the tool; `eidosmd.canvas` is how this CLI styles a canvas node by the property's value.",
|
|
464
|
+
vocabulary: " The root's own terms, one entry each: term, means (one line), not (the near-misses, each with why it is a different thing), and see (the blueprint that defines it in full) when one does. Absent = none declared.",
|
|
465
|
+
versions: " Snapshots of the root, taken on purpose, newest first: version (the root's own number), commit (the sha that is the snapshot), tag (blueprints/<version>, when made). Absent = none recorded, the normal state.",
|
|
291
466
|
index: ' Generated by `eidos index`; never hand-edited. One list per collection, in the order the markdown index would use.',
|
|
292
467
|
};
|
|
293
468
|
// A YAML document a person will edit gets the guidance the markdown seed
|
|
294
469
|
// keeps as prose, as comments above each top-level key.
|
|
295
|
-
function annotateYaml(document) {
|
|
470
|
+
export function annotateYaml(document) {
|
|
296
471
|
if (!isMap(document.contents)) {
|
|
297
472
|
return;
|
|
298
473
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
// Finding and loading the root's framework document
|
|
2
|
-
//
|
|
3
|
-
//
|
|
1
|
+
// Finding and loading the root's framework document, `.eidos/Framework.yaml`,
|
|
2
|
+
// parsed into the one model every command reads. A 4.x root's Framework.md
|
|
3
|
+
// is found and parsed too, only so `migrate` can move it.
|
|
4
4
|
import { existsSync, readFileSync } from 'node:fs';
|
|
5
5
|
import path from 'node:path';
|
|
6
6
|
import { parseFrameworkMarkdown } from './framework-markdown.js';
|
|
@@ -8,7 +8,7 @@ import { parseFrameworkStructured } from './framework-structured.js';
|
|
|
8
8
|
import { appliesTo, FRAMEWORK_DIR, FRAMEWORK_FILES, FrameworkError } from './framework-model.js';
|
|
9
9
|
import { kebab } from './naming.js';
|
|
10
10
|
export * from './framework-model.js';
|
|
11
|
-
// The one framework document in a folder's
|
|
11
|
+
// The one framework document in a folder's `.eidos/`, or null; two is an error.
|
|
12
12
|
export function findFrameworkFile(dir) {
|
|
13
13
|
const present = FRAMEWORK_FILES.map((candidate) => ({ file: path.join(dir, FRAMEWORK_DIR, candidate.name), format: candidate.format })).filter((candidate) => existsSync(candidate.file));
|
|
14
14
|
if (present.length > 1) {
|
|
@@ -28,7 +28,7 @@ export function parseFramework(text, format, root, file) {
|
|
|
28
28
|
export function loadFramework(root) {
|
|
29
29
|
const found = findFrameworkFile(root);
|
|
30
30
|
if (!found) {
|
|
31
|
-
throw new FrameworkError(`${path.join(root, FRAMEWORK_DIR)} holds no framework document (Framework.
|
|
31
|
+
throw new FrameworkError(`${path.join(root, FRAMEWORK_DIR)} holds no framework document (Framework.yaml)`);
|
|
32
32
|
}
|
|
33
33
|
return parseFramework(readFileSync(found.file, 'utf8'), found.format, root, found.file);
|
|
34
34
|
}
|
|
@@ -38,19 +38,19 @@ export function findCollection(framework, name) {
|
|
|
38
38
|
framework.collections.find((collection) => kebab(collection.name) === kebab(name)) ??
|
|
39
39
|
null);
|
|
40
40
|
}
|
|
41
|
-
export function
|
|
41
|
+
export function findVariant(collection, name) {
|
|
42
42
|
const wanted = name.trim().toLowerCase();
|
|
43
|
-
return (collection.
|
|
44
|
-
collection.
|
|
43
|
+
return (collection.variants.find((variant) => variant.name.toLowerCase() === wanted) ??
|
|
44
|
+
collection.variants.find((variant) => kebab(variant.name) === kebab(name)) ??
|
|
45
45
|
null);
|
|
46
46
|
}
|
|
47
|
-
export function
|
|
48
|
-
return collection.
|
|
47
|
+
export function defaultVariant(collection) {
|
|
48
|
+
return collection.variants.find((variant) => variant.isDefault) ?? collection.variants[0] ?? null;
|
|
49
49
|
}
|
|
50
50
|
// The properties a blueprint in this collection carries, core first.
|
|
51
51
|
export function propertiesFor(framework, collectionName) {
|
|
52
|
-
return [...framework.schema.core, ...framework.schema.custom.filter((property) => appliesTo(property, collectionName))];
|
|
52
|
+
return [...framework.schema.core, ...[...framework.schema.custom, ...Object.values(framework.schema.tools).flat()].filter((property) => appliesTo(property, collectionName))];
|
|
53
53
|
}
|
|
54
|
-
export function
|
|
55
|
-
return path.join(framework.root, FRAMEWORK_DIR,
|
|
54
|
+
export function templatePath(framework, variant) {
|
|
55
|
+
return path.join(framework.root, FRAMEWORK_DIR, variant.template);
|
|
56
56
|
}
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// every seed and scaffold already uses, so a generated file diffs cleanly
|
|
4
4
|
// against a hand-written one.
|
|
5
5
|
import { parse as parseYaml } from 'yaml';
|
|
6
|
-
import { splitFrontmatter } from './markdown.js';
|
|
6
|
+
import { normalizeNewlines, splitFrontmatter } from './markdown.js';
|
|
7
7
|
// One `key: value` per line, read without YAML: what an unquoted summary with
|
|
8
8
|
// a colon in it still means to a person, and what Obsidian will refuse to show.
|
|
9
9
|
function parseLeniently(frontmatter) {
|
|
@@ -114,3 +114,63 @@ export function formatFrontmatter(entries) {
|
|
|
114
114
|
lines.push('---');
|
|
115
115
|
return lines.join('\n') + '\n';
|
|
116
116
|
}
|
|
117
|
+
const escapeRegExp = (text) => text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
118
|
+
// The lines a top-level key occupies inside [from, to): its own line and
|
|
119
|
+
// every line beneath it that is indented or a `- ` item, so a block list
|
|
120
|
+
// or a folded scalar moves as one.
|
|
121
|
+
export function keySpan(lines, from, to, key) {
|
|
122
|
+
const head = new RegExp(`^${escapeRegExp(key)}\\s*:`);
|
|
123
|
+
for (let index = from; index < to; index += 1) {
|
|
124
|
+
if (!head.test(lines[index] ?? ''))
|
|
125
|
+
continue;
|
|
126
|
+
let end = index + 1;
|
|
127
|
+
while (end < to && /^(\s+\S|-\s)/.test(lines[end] ?? ''))
|
|
128
|
+
end += 1;
|
|
129
|
+
return { start: index, end };
|
|
130
|
+
}
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
// Whether a file's frontmatter carries the key at the top level.
|
|
134
|
+
export function hasKey(text, key) {
|
|
135
|
+
const lines = normalizeNewlines(text).split('\n');
|
|
136
|
+
const close = closingFence(lines);
|
|
137
|
+
return close !== null && keySpan(lines, 1, close, key) !== null;
|
|
138
|
+
}
|
|
139
|
+
function closingFence(lines) {
|
|
140
|
+
if (lines[0]?.trim() !== '---')
|
|
141
|
+
return null;
|
|
142
|
+
for (let index = 1; index < lines.length; index += 1) {
|
|
143
|
+
if (lines[index]?.trim() === '---')
|
|
144
|
+
return index;
|
|
145
|
+
}
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
// The file with one key edited in its frontmatter. A file with no block, or
|
|
149
|
+
// a rename or delete of a key it lacks, comes back unchanged; a set of a key
|
|
150
|
+
// it lacks appends the key at the end of the block.
|
|
151
|
+
export function editFrontmatterKey(text, edit) {
|
|
152
|
+
const lines = normalizeNewlines(text).split('\n');
|
|
153
|
+
const close = closingFence(lines);
|
|
154
|
+
if (close === null)
|
|
155
|
+
return text;
|
|
156
|
+
const span = keySpan(lines, 1, close, edit.key);
|
|
157
|
+
if (edit.kind === 'rename') {
|
|
158
|
+
if (!span)
|
|
159
|
+
return text;
|
|
160
|
+
const line = lines[span.start] ?? '';
|
|
161
|
+
lines[span.start] = line.replace(new RegExp(`^${escapeRegExp(edit.key)}(\\s*:)`), `${edit.to}$1`);
|
|
162
|
+
return lines.join('\n');
|
|
163
|
+
}
|
|
164
|
+
if (edit.kind === 'delete') {
|
|
165
|
+
if (!span)
|
|
166
|
+
return text;
|
|
167
|
+
lines.splice(span.start, span.end - span.start);
|
|
168
|
+
return lines.join('\n');
|
|
169
|
+
}
|
|
170
|
+
const rendered = formatFrontmatter([[edit.key, edit.value]]).split('\n').slice(1, -2);
|
|
171
|
+
if (span)
|
|
172
|
+
lines.splice(span.start, span.end - span.start, ...rendered);
|
|
173
|
+
else
|
|
174
|
+
lines.splice(close, 0, ...rendered);
|
|
175
|
+
return lines.join('\n');
|
|
176
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
// The little git the CLI needs: the commit a root sits at, whether a commit
|
|
2
|
+
// exists, a tag on one, and a move that keeps history. Every call is
|
|
3
|
+
// read-only except `gitTag` and `moveFile`, and every one answers null or
|
|
4
|
+
// false rather than throwing where git is absent or the root is not in a
|
|
5
|
+
// repository.
|
|
6
|
+
import { execFileSync } from 'node:child_process';
|
|
7
|
+
import { renameSync } from 'node:fs';
|
|
8
|
+
import path from 'node:path';
|
|
9
|
+
import { readSettings } from './settings.js';
|
|
10
|
+
function git(root, args) {
|
|
11
|
+
try {
|
|
12
|
+
return execFileSync('git', args, { cwd: root, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim() || null;
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
// A command run for its effect: true when git exits clean, whatever it prints.
|
|
19
|
+
function gitRun(root, args) {
|
|
20
|
+
try {
|
|
21
|
+
execFileSync('git', args, { cwd: root, encoding: 'utf8', stdio: ['ignore', 'ignore', 'ignore'] });
|
|
22
|
+
return true;
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
// Whether git tracks this path (a file, or a folder with tracked files in it).
|
|
29
|
+
export function gitTracks(root, absolute) {
|
|
30
|
+
return git(root, ['ls-files', '--error-unmatch', '--', absolute]) !== null;
|
|
31
|
+
}
|
|
32
|
+
// A file or folder moved the way the root wants: with git on for the root
|
|
33
|
+
// and the path tracked, `git mv`, so history follows the rename; otherwise,
|
|
34
|
+
// or when git refuses, a plain rename.
|
|
35
|
+
export function moveFile(root, from, to) {
|
|
36
|
+
if (readSettings(root).shared.features.git && gitTracks(root, from) && gitRun(root, ['mv', '--', from, to]))
|
|
37
|
+
return;
|
|
38
|
+
renameSync(from, to);
|
|
39
|
+
}
|
|
40
|
+
export function gitHead(root) {
|
|
41
|
+
return git(root, ['rev-parse', 'HEAD']);
|
|
42
|
+
}
|
|
43
|
+
// The full sha of a commit named any way git accepts (a short sha, HEAD, a
|
|
44
|
+
// tag), or null when nothing by that name is a commit here.
|
|
45
|
+
export function gitResolve(root, ref) {
|
|
46
|
+
return git(root, ['rev-parse', '--verify', '--quiet', `${ref}^{commit}`]);
|
|
47
|
+
}
|
|
48
|
+
export function gitTagExists(root, tag) {
|
|
49
|
+
return git(root, ['rev-parse', '--verify', '--quiet', `refs/tags/${tag}`]) !== null;
|
|
50
|
+
}
|
|
51
|
+
export function gitTag(root, tag, sha) {
|
|
52
|
+
return git(root, ['tag', tag, sha]) !== null || gitTagExists(root, tag);
|
|
53
|
+
}
|
|
54
|
+
// Who git says is committing here: the repository's configured user, falling
|
|
55
|
+
// back to the global one the way git itself does. Null outside a repository
|
|
56
|
+
// or where no name is set.
|
|
57
|
+
export function gitUser(root) {
|
|
58
|
+
if (gitHead(root) === null && git(root, ['rev-parse', '--git-dir']) === null)
|
|
59
|
+
return null;
|
|
60
|
+
const name = git(root, ['config', 'user.name']);
|
|
61
|
+
if (name === null)
|
|
62
|
+
return null;
|
|
63
|
+
return { name, email: git(root, ['config', 'user.email']) };
|
|
64
|
+
}
|
|
65
|
+
// The commits that touched one file, newest first, following renames.
|
|
66
|
+
export function gitLog(root, file, limit = 50) {
|
|
67
|
+
const out = git(root, ['log', `--max-count=${limit}`, '--follow', '--format=%H%x1f%h%x1f%an%x1f%ae%x1f%aI%x1f%s', '--', file]);
|
|
68
|
+
if (out === null)
|
|
69
|
+
return [];
|
|
70
|
+
return out
|
|
71
|
+
.split('\n')
|
|
72
|
+
.map((line) => line.split('\x1f'))
|
|
73
|
+
.filter((parts) => parts.length === 6)
|
|
74
|
+
.map(([sha, short, author, email, date, subject]) => ({ sha: sha ?? '', short: short ?? '', author: author ?? '', email: email ?? '', date: date ?? '', subject: subject ?? '' }));
|
|
75
|
+
}
|
|
76
|
+
// The file as one commit held it, or null when that commit has no such file.
|
|
77
|
+
export function gitShowFile(root, sha, file) {
|
|
78
|
+
try {
|
|
79
|
+
return execFileSync('git', ['show', `${sha}:${file.split(path.sep).join('/')}`], { cwd: root, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] });
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
// `summary`, kept inside the YAML framework document under `index`, one list
|
|
3
3
|
// per collection, regenerated wholesale. `eidos index` rewrites that key and
|
|
4
4
|
// nothing else in the document, so the comments and layout a person chose
|
|
5
|
-
// survive. (A markdown root
|
|
6
|
-
//
|
|
5
|
+
// survive. (A 4.x markdown root kept a per-collection `index.md` instead;
|
|
6
|
+
// `migrate` converts such a root.)
|
|
7
7
|
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
8
8
|
import path from 'node:path';
|
|
9
9
|
import { blueprintId, blueprintTitle, propertyString } from './blueprint.js';
|