eidosmd 0.1.0 → 0.2.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-C2NMN_D4.css +1 -0
- package/browser/dist/assets/index-C65k1ihb.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 +1 -1
- package/dist/src/commands/framework.js +24 -5
- package/dist/src/commands/index.js +4 -4
- package/dist/src/commands/init.js +1 -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/seeds.js +5 -5
- package/dist/src/commands/setup.js +119 -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 +722 -0
- package/dist/src/core/check.js +127 -51
- package/dist/src/core/convert.js +7 -6
- package/dist/src/core/framework-markdown.js +113 -34
- package/dist/src/core/framework-model.js +27 -10
- package/dist/src/core/framework-structured.js +132 -33
- package/dist/src/core/framework.js +13 -13
- package/dist/src/core/git.js +59 -0
- package/dist/src/core/index-leaf.js +2 -2
- package/dist/src/core/me.js +16 -8
- package/dist/src/core/migrate.js +243 -0
- package/dist/src/core/naming.js +1 -1
- package/dist/src/core/root.js +2 -2
- package/dist/src/core/scaffold.js +19 -19
- package/dist/src/core/seed.js +151 -60
- package/dist/src/core/server.js +1236 -52
- package/dist/src/core/settings.js +181 -0
- package/dist/src/core/store.js +259 -0
- package/dist/src/core/template.js +32 -0
- package/dist/src/core/versions.js +79 -0
- package/dist/src/output.js +4 -1
- package/dist/src/program.js +134 -39
- package/instructions/authoring.md +13 -12
- package/instructions/configuring.md +65 -34
- package/instructions/init-required.md +4 -4
- package/instructions/overview.md +17 -7
- package/instructions/validating.md +5 -4
- package/package.json +21 -12
- package/standard/EIDOS.md +129 -188
- package/standard/seeds/README.md +6 -6
- package/standard/seeds/book/Framework.yaml +81 -0
- package/standard/seeds/book/README.md +9 -5
- package/standard/seeds/book/_gitignore +3 -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 +81 -0
- package/standard/seeds/research/README.md +9 -5
- package/standard/seeds/research/_gitignore +3 -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 +82 -0
- package/standard/seeds/software/README.md +5 -5
- package/standard/seeds/software/_gitignore +3 -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
package/dist/src/core/check.js
CHANGED
|
@@ -1,17 +1,18 @@
|
|
|
1
|
-
// Validation, framework-defined: every rule reads the root's own Framework.
|
|
2
|
-
// and
|
|
1
|
+
// Validation, framework-defined: every rule reads the root's own Framework.yaml
|
|
2
|
+
// and templates, never a contract hardcoded here. Findings surface; a human acts
|
|
3
3
|
// on them. Errors are things that are wrong on any reading (a malformed file,
|
|
4
4
|
// an id used twice, a link to nowhere); warnings are gaps the standard says to
|
|
5
5
|
// note and offer, never refuse.
|
|
6
6
|
import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
|
|
7
7
|
import path from 'node:path';
|
|
8
|
-
import { blueprintId, blueprintTitle, propertyString,
|
|
9
|
-
import { appliesTo } from './framework.js';
|
|
8
|
+
import { blueprintId, blueprintTitle, propertyString, resolveVariant } from './blueprint.js';
|
|
9
|
+
import { appliesTo, unitOf } from './framework.js';
|
|
10
10
|
import { renderEmbeddedEntries } from './index-leaf.js';
|
|
11
11
|
import { embeddedIndexOf } from './framework-structured.js';
|
|
12
|
+
import { FRAME_REF, isFrameRef, listCanvases } from './canvas.js';
|
|
12
13
|
import { normalizeNewlines } from './markdown.js';
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
14
|
+
import { kebab } from './naming.js';
|
|
15
|
+
import { loadTemplate } from './template.js';
|
|
15
16
|
const WORK_TRACKING_FIELDS = new Set(['sprint', 'estimate', 'assignee', 'story_points']);
|
|
16
17
|
const SKIPPED_DIRS = new Set(['node_modules', 'dist', 'build']);
|
|
17
18
|
function compareVersions(a, b) {
|
|
@@ -99,8 +100,8 @@ function propertyLinks(value) {
|
|
|
99
100
|
}
|
|
100
101
|
class Collector {
|
|
101
102
|
findings = [];
|
|
102
|
-
error(code, file, message) {
|
|
103
|
-
this.findings.push({ level: 'error', code, path: file, message });
|
|
103
|
+
error(code, file, message, fix) {
|
|
104
|
+
this.findings.push({ level: 'error', code, path: file, message, ...(fix ? { fix } : {}) });
|
|
104
105
|
}
|
|
105
106
|
warn(code, file, message) {
|
|
106
107
|
this.findings.push({ level: 'warning', code, path: file, message });
|
|
@@ -123,14 +124,17 @@ function checkFramework(framework, out, options) {
|
|
|
123
124
|
else {
|
|
124
125
|
const diff = compareVersions(framework.eidosVersion, options.standardVersion);
|
|
125
126
|
if (diff < 0) {
|
|
126
|
-
out.warn('version-behind', frameworkRel, `targets Eidos ${framework.eidosVersion}; this CLI carries ${options.standardVersion}
|
|
127
|
+
out.warn('version-behind', frameworkRel, `targets Eidos ${framework.eidosVersion}; this CLI carries ${options.standardVersion}: run \`eidos migrate\` when ready`);
|
|
127
128
|
}
|
|
128
129
|
else if (diff > 0) {
|
|
129
130
|
out.warn('version-ahead', frameworkRel, `targets Eidos ${framework.eidosVersion}, newer than the ${options.standardVersion} this CLI carries`);
|
|
130
131
|
}
|
|
131
132
|
}
|
|
132
133
|
if (framework.collections.length === 0) {
|
|
133
|
-
out.error('no-collections', frameworkRel, 'declares no collections
|
|
134
|
+
out.error('no-collections', frameworkRel, 'declares no collections; a framework needs at least one');
|
|
135
|
+
}
|
|
136
|
+
if (/^versions:/m.test(readFileSync(framework.file, 'utf8'))) {
|
|
137
|
+
out.warn('versions-legacy', frameworkRel, 'carries a versions key; since Eidos 5.0.0 a root\'s versions are a tool\'s own, and `eidos migrate` moves them to .eidos/plugins/eidosmd/versions.yaml');
|
|
134
138
|
}
|
|
135
139
|
if (!existsSync(path.join(framework.root, 'README.md'))) {
|
|
136
140
|
out.warn('readme-missing', null, 'no README.md at the root; the visible front door is missing');
|
|
@@ -140,23 +144,30 @@ function checkFramework(framework, out, options) {
|
|
|
140
144
|
out.warn('top-level-missing', frameworkRel, `## Top-Level lists ${doc.title} at ${doc.path}, which does not exist`);
|
|
141
145
|
}
|
|
142
146
|
}
|
|
143
|
-
|
|
147
|
+
// A collection's templates are named for its unit, `<unit>.<variant>.md`;
|
|
148
|
+
// a file named otherwise is faulted, as is one that opens with frontmatter.
|
|
149
|
+
const templates = new Map();
|
|
144
150
|
for (const collection of framework.collections) {
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
151
|
+
const unit = unitOf(collection);
|
|
152
|
+
for (const variant of collection.variants) {
|
|
153
|
+
const template = loadTemplate(framework, variant);
|
|
154
|
+
templates.set(`${collection.name}/${variant.name}`, template);
|
|
155
|
+
if (template?.frontmatter) {
|
|
156
|
+
out.error('template-frontmatter', rel(framework.root, template.path), 'carries a frontmatter block; a template is body only, its frontmatter is generated from the Properties table', 'Remove the block');
|
|
157
|
+
}
|
|
158
|
+
if (!template) {
|
|
159
|
+
out.error('template-missing', frameworkRel, `${collection.name}/${variant.name} points at .eidos/${variant.template}, which does not exist`);
|
|
160
|
+
}
|
|
161
|
+
const expected = unit ? `${unit}.${variant.name}.md` : null;
|
|
162
|
+
if (expected && !variant.template.endsWith(`/${expected}`) && variant.template !== expected) {
|
|
163
|
+
out.error('template-name', frameworkRel, `${collection.name}/${variant.name} is at .eidos/${variant.template}; a template file is templates/${expected} (its unit, then its variant)`);
|
|
153
164
|
}
|
|
154
165
|
}
|
|
155
166
|
if (!existsSync(path.join(framework.root, collection.name))) {
|
|
156
167
|
out.warn('collection-folder-missing', null, `collection ${collection.name} is declared but has no folder`);
|
|
157
168
|
}
|
|
158
169
|
}
|
|
159
|
-
return
|
|
170
|
+
return templates;
|
|
160
171
|
}
|
|
161
172
|
function checkLayout(framework, blueprints, out) {
|
|
162
173
|
const declared = new Set(framework.collections.map((collection) => collection.name));
|
|
@@ -169,7 +180,7 @@ function checkLayout(framework, blueprints, out) {
|
|
|
169
180
|
entries = [];
|
|
170
181
|
}
|
|
171
182
|
for (const entry of entries.sort()) {
|
|
172
|
-
if (entry.startsWith('.') || entry === '
|
|
183
|
+
if (entry.startsWith('.') || entry === '.eidos' || SKIPPED_DIRS.has(entry) || declared.has(entry)) {
|
|
173
184
|
continue;
|
|
174
185
|
}
|
|
175
186
|
let isDir = false;
|
|
@@ -180,7 +191,7 @@ function checkLayout(framework, blueprints, out) {
|
|
|
180
191
|
isDir = false;
|
|
181
192
|
}
|
|
182
193
|
if (isDir) {
|
|
183
|
-
out.warn('folder-undeclared', entry, 'a folder at the root that no collection declares; declare it in
|
|
194
|
+
out.warn('folder-undeclared', entry, 'a folder at the root that no collection declares; declare it in the framework document or move it');
|
|
184
195
|
}
|
|
185
196
|
}
|
|
186
197
|
for (const collection of framework.collections) {
|
|
@@ -194,7 +205,7 @@ function checkLayout(framework, blueprints, out) {
|
|
|
194
205
|
const declaredGroups = new Map(collection.grouping.groups.map((group) => [group.name.toLowerCase(), group.name]));
|
|
195
206
|
for (const group of groupsOnDisk) {
|
|
196
207
|
if (!declaredGroups.has(group.toLowerCase())) {
|
|
197
|
-
out.warn('group-undeclared', `${collection.name}/${group}`, `not declared under ${collection.grouping.label} in
|
|
208
|
+
out.warn('group-undeclared', `${collection.name}/${group}`, `not declared under ${collection.grouping.label} in the framework document`);
|
|
198
209
|
}
|
|
199
210
|
}
|
|
200
211
|
for (const group of collection.grouping.groups) {
|
|
@@ -209,10 +220,10 @@ function checkLayout(framework, blueprints, out) {
|
|
|
209
220
|
}
|
|
210
221
|
}
|
|
211
222
|
if (collection.framing) {
|
|
212
|
-
for (const
|
|
213
|
-
const written = own.some((blueprint) =>
|
|
223
|
+
for (const variant of collection.variants) {
|
|
224
|
+
const written = own.some((blueprint) => resolveVariant(blueprint).variant === variant);
|
|
214
225
|
if (!written) {
|
|
215
|
-
out.warn('frame-unwritten', collection.name, `no ${
|
|
226
|
+
out.warn('frame-unwritten', collection.name, `no ${variant.name} frame yet; a gap to surface, not a failure`);
|
|
216
227
|
}
|
|
217
228
|
}
|
|
218
229
|
}
|
|
@@ -223,7 +234,25 @@ function checkLayout(framework, blueprints, out) {
|
|
|
223
234
|
}
|
|
224
235
|
}
|
|
225
236
|
}
|
|
226
|
-
|
|
237
|
+
// Rule 19: a near-miss the Vocabulary names is flagged with the declared term
|
|
238
|
+
// beside it, never refused. The near-miss is the clause's opening phrase.
|
|
239
|
+
function checkVocabulary(framework, blueprint, out) {
|
|
240
|
+
if (framework.vocabulary.length === 0 || blueprint.body.trim() === '')
|
|
241
|
+
return;
|
|
242
|
+
const body = blueprint.body.toLowerCase();
|
|
243
|
+
for (const entry of framework.vocabulary) {
|
|
244
|
+
for (const clause of entry.not) {
|
|
245
|
+
const phrase = clause.split(/[,;:(]/)[0]?.trim().toLowerCase() ?? '';
|
|
246
|
+
if (phrase === '' || phrase === entry.term.toLowerCase())
|
|
247
|
+
continue;
|
|
248
|
+
const pattern = new RegExp(`(^|[^a-z0-9])${phrase.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}(?![a-z0-9])`, 'i');
|
|
249
|
+
if (pattern.test(body)) {
|
|
250
|
+
out.warn('term-near-miss', blueprint.rel, `uses "${phrase}"; the Vocabulary declares "${entry.term}"${entry.means ? ` (${entry.means})` : ''}`);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
function checkBlueprint(framework, blueprint, templates, out) {
|
|
227
256
|
const file = blueprint.rel;
|
|
228
257
|
if (blueprint.properties === null) {
|
|
229
258
|
if (blueprint.frontmatterError) {
|
|
@@ -241,13 +270,7 @@ function checkBlueprint(framework, blueprint, shapes, out) {
|
|
|
241
270
|
const collection = blueprint.collection;
|
|
242
271
|
const id = propertyString(blueprint, 'id');
|
|
243
272
|
if (id === null) {
|
|
244
|
-
out.error('id-missing', file, 'no `id`; add a permanent
|
|
245
|
-
}
|
|
246
|
-
else if (!isKebab(id)) {
|
|
247
|
-
out.error('id-format', file, `id '${id}' is not kebab-case`);
|
|
248
|
-
}
|
|
249
|
-
else if (framework.naming === 'kebab-case' && blueprint.stem !== id) {
|
|
250
|
-
out.warn('id-filename', file, `in a kebab-case root the filename is the id; expected ${id}.md`);
|
|
273
|
+
out.error('id-missing', file, 'no `id`; add a permanent identity (a slug, a number, a GUID)');
|
|
251
274
|
}
|
|
252
275
|
if (propertyString(blueprint, 'title') === null) {
|
|
253
276
|
out.error('title-missing', file, 'no `title`');
|
|
@@ -255,13 +278,14 @@ function checkBlueprint(framework, blueprint, shapes, out) {
|
|
|
255
278
|
if (propertyString(blueprint, 'summary') === null) {
|
|
256
279
|
out.warn('summary-missing', file, 'no `summary`; the collection index will flag it');
|
|
257
280
|
}
|
|
258
|
-
const {
|
|
281
|
+
const { variant, declared, known } = resolveVariant(blueprint);
|
|
259
282
|
if (!known) {
|
|
260
|
-
const names = collection.
|
|
261
|
-
out.error('
|
|
283
|
+
const names = collection.variants.map((candidate) => candidate.name).join(', ');
|
|
284
|
+
out.error('variant-unknown', file, `variant '${declared ?? ''}' is not one of ${collection.name}'s declared variants (${names})`);
|
|
262
285
|
}
|
|
263
286
|
const core = new Set(framework.schema.core.map((property) => property.name));
|
|
264
|
-
const
|
|
287
|
+
const schemaProperties = [...framework.schema.custom, ...Object.values(framework.schema.tools).flat()];
|
|
288
|
+
const applicable = schemaProperties.filter((property) => appliesTo(property, collection.name));
|
|
265
289
|
const applicableNames = new Set(applicable.map((property) => property.name));
|
|
266
290
|
for (const property of applicable) {
|
|
267
291
|
if (!(property.name in properties)) {
|
|
@@ -275,7 +299,7 @@ function checkBlueprint(framework, blueprint, shapes, out) {
|
|
|
275
299
|
if (core.has(key) || applicableNames.has(key)) {
|
|
276
300
|
continue;
|
|
277
301
|
}
|
|
278
|
-
const elsewhere =
|
|
302
|
+
const elsewhere = schemaProperties.find((property) => property.name === key);
|
|
279
303
|
if (elsewhere) {
|
|
280
304
|
const scope = elsewhere.appliesTo === 'all' ? 'all' : elsewhere.appliesTo.join(', ');
|
|
281
305
|
out.warn('property-scope', file, `\`${key}\` applies to ${scope}, not ${collection.name}`);
|
|
@@ -284,7 +308,7 @@ function checkBlueprint(framework, blueprint, shapes, out) {
|
|
|
284
308
|
out.warn('work-tracking-field', file, `\`${key}\` is a work-tracking field; a blueprint captures state and intent, not work (link to a tracker instead)`);
|
|
285
309
|
}
|
|
286
310
|
else {
|
|
287
|
-
out.warn('property-unknown', file, `\`${key}\` is
|
|
311
|
+
out.warn('property-unknown', file, `\`${key}\` is in no block of the Properties table; declare it among the custom properties or drop it`);
|
|
288
312
|
}
|
|
289
313
|
}
|
|
290
314
|
for (const property of [...framework.schema.core, ...applicable]) {
|
|
@@ -327,38 +351,89 @@ function checkBlueprint(framework, blueprint, shapes, out) {
|
|
|
327
351
|
if (h1 && h1.text !== title && kebab(h1.text) !== kebab(title)) {
|
|
328
352
|
out.warn('title-mismatch', file, `the heading '${h1.text}' differs from title '${title}'`);
|
|
329
353
|
}
|
|
330
|
-
const
|
|
331
|
-
if (!
|
|
354
|
+
const template = variant ? (templates.get(`${collection.name}/${variant.name}`) ?? loadTemplate(framework, variant)) : null;
|
|
355
|
+
if (!template || !variant) {
|
|
332
356
|
return;
|
|
333
357
|
}
|
|
334
358
|
const present = blueprint.sections.map((section) => section.heading.text);
|
|
335
359
|
const presentLower = present.map((text) => text.toLowerCase());
|
|
336
|
-
const
|
|
337
|
-
for (const name of
|
|
360
|
+
const templateLower = template.sections.map((text) => text.toLowerCase());
|
|
361
|
+
for (const name of template.sections) {
|
|
338
362
|
if (!presentLower.includes(name.toLowerCase())) {
|
|
339
|
-
out.warn('section-missing', file, `no \`## ${name}\` (${
|
|
363
|
+
out.warn('section-missing', file, `no \`## ${name}\` (${variant.unit}.${variant.name} declares it; leave it out only if it genuinely does not apply)`);
|
|
340
364
|
}
|
|
341
365
|
}
|
|
342
366
|
if (!collection.framing) {
|
|
343
367
|
for (const name of present) {
|
|
344
|
-
if (!
|
|
345
|
-
out.warn('section-unknown', file, `\`## ${name}\` is not a section of ${
|
|
368
|
+
if (!templateLower.includes(name.toLowerCase())) {
|
|
369
|
+
out.warn('section-unknown', file, `\`## ${name}\` is not a section of ${variant.unit}.${variant.name}; nest it under one that is, or add it to the template`);
|
|
346
370
|
}
|
|
347
371
|
}
|
|
348
372
|
}
|
|
349
|
-
const order = presentLower.filter((name) =>
|
|
373
|
+
const order = presentLower.filter((name) => templateLower.includes(name)).map((name) => templateLower.indexOf(name));
|
|
350
374
|
for (let index = 1; index < order.length; index += 1) {
|
|
351
375
|
if ((order[index] ?? 0) < (order[index - 1] ?? 0)) {
|
|
352
|
-
out.warn('section-order', file, `sections are not in ${
|
|
376
|
+
out.warn('section-order', file, `sections are not in ${variant.unit}.${variant.name}'s order (${template.sections.join(' → ')})`);
|
|
353
377
|
break;
|
|
354
378
|
}
|
|
355
379
|
}
|
|
356
380
|
}
|
|
381
|
+
function checkCanvases(framework, blueprints, out) {
|
|
382
|
+
const ids = new Set(blueprints.map((blueprint) => blueprintId(blueprint)).filter((id) => id !== null));
|
|
383
|
+
const canvases = listCanvases(framework);
|
|
384
|
+
const canvasIds = new Set(canvases.map((canvas) => canvas.id));
|
|
385
|
+
for (const canvas of canvases) {
|
|
386
|
+
const file = rel(framework.root, canvas.file);
|
|
387
|
+
for (const problem of canvas.problems) {
|
|
388
|
+
out.error('canvas-invalid', file, problem);
|
|
389
|
+
}
|
|
390
|
+
for (const page of canvas.pages) {
|
|
391
|
+
const nodeIds = new Set(page.nodes.map((node) => node.id));
|
|
392
|
+
const frameIds = new Set(page.frames.map((frame) => frame.id));
|
|
393
|
+
for (const node of page.nodes) {
|
|
394
|
+
if (node.blueprint && !ids.has(node.blueprint)) {
|
|
395
|
+
out.error('canvas-node-missing', file, `page ${page.id}, node ${node.id}: no blueprint with id '${node.blueprint}'`);
|
|
396
|
+
}
|
|
397
|
+
if (node.frame && !frameIds.has(node.frame)) {
|
|
398
|
+
out.warn('canvas-frame-missing', file, `page ${page.id}, node ${node.id}: no frame '${node.frame}' on this page`);
|
|
399
|
+
}
|
|
400
|
+
if (node.link?.page && !canvas.pages.some((candidate) => candidate.id === node.link?.page)) {
|
|
401
|
+
out.error('canvas-link-missing', file, `page ${page.id}, node ${node.id}: links to page '${node.link.page}', which this canvas does not have`);
|
|
402
|
+
}
|
|
403
|
+
if (node.link?.canvas && !canvasIds.has(node.link.canvas)) {
|
|
404
|
+
out.error('canvas-link-missing', file, `page ${page.id}, node ${node.id}: links to canvas '${node.link.canvas}', which does not exist`);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
for (const comment of page.comments) {
|
|
408
|
+
if (comment.node && !page.nodes.some((node) => node.id === comment.node)) {
|
|
409
|
+
out.warn('canvas-comment-orphan', file, `page ${page.id}, comment ${comment.id}: pinned to node '${comment.node}', which the page no longer has`);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
const endpoint = (id) => (isFrameRef(id) ? frameIds.has(id.slice(FRAME_REF.length)) : nodeIds.has(id));
|
|
413
|
+
for (const edge of page.edges) {
|
|
414
|
+
if (!endpoint(edge.from) || !endpoint(edge.to)) {
|
|
415
|
+
out.error('canvas-edge-missing', file, `page ${page.id}: edge ${edge.from} → ${edge.to} names a node or frame the page does not have`);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
for (const node of page.nodes) {
|
|
419
|
+
if (node.annotation && !page.edges.some((edge) => edge.from === node.id)) {
|
|
420
|
+
out.warn('canvas-annotation-unpointed', file, `page ${page.id}, annotation ${node.id}: points at nothing; an annotation exists to point`);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
for (const frame of page.frames) {
|
|
424
|
+
if (frame.frame && !frameIds.has(frame.frame)) {
|
|
425
|
+
out.warn('canvas-frame-missing', file, `page ${page.id}, frame ${frame.id}: attached to frame '${frame.frame}', which the page does not have`);
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
}
|
|
357
431
|
export function checkRoot(framework, blueprints, options) {
|
|
358
432
|
const out = new Collector();
|
|
359
|
-
const
|
|
433
|
+
const templates = checkFramework(framework, out, options);
|
|
360
434
|
if (!options.only) {
|
|
361
435
|
checkLayout(framework, blueprints, out);
|
|
436
|
+
checkCanvases(framework, blueprints, out);
|
|
362
437
|
}
|
|
363
438
|
const seen = new Map();
|
|
364
439
|
for (const blueprint of blueprints) {
|
|
@@ -381,7 +456,8 @@ export function checkRoot(framework, blueprints, options) {
|
|
|
381
456
|
}
|
|
382
457
|
const targets = options.only ? blueprints.filter((blueprint) => options.only?.has(blueprint.path)) : blueprints;
|
|
383
458
|
for (const blueprint of targets) {
|
|
384
|
-
checkBlueprint(framework, blueprint,
|
|
459
|
+
checkBlueprint(framework, blueprint, templates, out);
|
|
460
|
+
checkVocabulary(framework, blueprint, out);
|
|
385
461
|
}
|
|
386
462
|
const findings = out.findings.sort((a, b) => {
|
|
387
463
|
const left = a.path ?? '';
|
package/dist/src/core/convert.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
// Converting a markdown root to
|
|
2
|
-
//
|
|
3
|
-
// inside it, `Framework.md` and each collection's
|
|
4
|
-
//
|
|
5
|
-
//
|
|
1
|
+
// Converting a 4.x markdown root to Framework.yaml, the framework document
|
|
2
|
+
// since 5.0.0: the same framework written as data with this CLI's guidance
|
|
3
|
+
// as comments and the index inside it, `Framework.md` and each collection's
|
|
4
|
+
// `index.md` removed. `migrate` runs this on the way up. The markdown form's
|
|
5
|
+
// prose (section intros, HTML comments) has no field to land in and is left
|
|
6
|
+
// behind; each collection's own description travels.
|
|
6
7
|
import { existsSync, rmSync, writeFileSync } from 'node:fs';
|
|
7
8
|
import path from 'node:path';
|
|
8
9
|
import { listBlueprints } from './blueprint.js';
|
|
@@ -10,7 +11,7 @@ import { loadFramework } from './framework.js';
|
|
|
10
11
|
import { frameworkToDocument, serializeDocument } from './framework-structured.js';
|
|
11
12
|
import { buildIndexes } from './index-leaf.js';
|
|
12
13
|
export function convertToYaml(framework, dryRun) {
|
|
13
|
-
const yamlFile = path.join(framework.root, '
|
|
14
|
+
const yamlFile = path.join(framework.root, '.eidos', 'Framework.yaml');
|
|
14
15
|
const actions = [];
|
|
15
16
|
const removed = [];
|
|
16
17
|
const text = serializeDocument(frameworkToDocument(framework));
|
|
@@ -1,13 +1,15 @@
|
|
|
1
|
-
// The markdown framework document,
|
|
1
|
+
// The markdown framework document, `.eidos/Framework.md`: frontmatter for the
|
|
2
2
|
// version and naming convention, and a body indexing the Top-Level docs, the
|
|
3
|
-
// Collections (each a `###` with Leaf,
|
|
4
|
-
//
|
|
3
|
+
// Collections (each a `###` with Leaf, Variants, and grouping bullets), the
|
|
4
|
+
// Properties table per owner, the Vocabulary, and the Versions. The 4.x
|
|
5
|
+
// spellings (Schema, Flavors, shape) still read, so `migrate` can load a root
|
|
6
|
+
// it is about to move.
|
|
5
7
|
import { parseFrontmatter } from './frontmatter.js';
|
|
6
|
-
import { groupingProperty, STANDARD_CORE } from './framework-model.js';
|
|
8
|
+
import { groupingProperty, STANDARD_CORE, unitOf } from './framework-model.js';
|
|
7
9
|
import { DEFAULT_NAMING, isNaming } from './naming.js';
|
|
8
10
|
import { section, sections, stripInlineCode } from './markdown.js';
|
|
9
11
|
import path from 'node:path';
|
|
10
|
-
const DECLARED_BULLETS = new Set(['leaf', 'flavors', 'canvas']);
|
|
12
|
+
const DECLARED_BULLETS = new Set(['leaf', 'unit', 'variants', 'flavors', 'canvas']);
|
|
11
13
|
function cleanText(text) {
|
|
12
14
|
return text.replace(/^_+|_+$/g, '').trim();
|
|
13
15
|
}
|
|
@@ -46,13 +48,13 @@ function parseCanvas(value) {
|
|
|
46
48
|
}
|
|
47
49
|
return { mode: 'card', section: null };
|
|
48
50
|
}
|
|
49
|
-
function
|
|
50
|
-
const file = path.posix.basename(
|
|
51
|
+
function variantFromTemplate(label, templateRel) {
|
|
52
|
+
const file = path.posix.basename(templateRel.replace(/\\/g, '/')).replace(/\.md$/i, '');
|
|
51
53
|
const dot = file.indexOf('.');
|
|
52
|
-
const
|
|
54
|
+
const unit = dot === -1 ? file : file.slice(0, dot);
|
|
53
55
|
const fromFile = dot === -1 ? file : file.slice(dot + 1);
|
|
54
56
|
const name = /\.md$/i.test(label) ? fromFile : label;
|
|
55
|
-
return { name,
|
|
57
|
+
return { name, unit };
|
|
56
58
|
}
|
|
57
59
|
function parseCollections(body) {
|
|
58
60
|
const block = section(body, 2, 'Collections');
|
|
@@ -64,20 +66,20 @@ function parseCollections(body) {
|
|
|
64
66
|
const collection = {
|
|
65
67
|
name: heading.text,
|
|
66
68
|
description: firstParagraph(text),
|
|
67
|
-
framing:
|
|
69
|
+
framing: false,
|
|
68
70
|
leaf: null,
|
|
69
|
-
|
|
71
|
+
variants: [],
|
|
70
72
|
canvas: null,
|
|
71
73
|
grouping: null,
|
|
72
74
|
};
|
|
73
|
-
let
|
|
75
|
+
let inVariants = false;
|
|
74
76
|
let inGrouping = false;
|
|
75
77
|
for (const line of text.split('\n')) {
|
|
76
78
|
const bullet = /^-\s+\*\*(.+?):\*\*\s*(.*)$/.exec(line);
|
|
77
79
|
if (bullet && bullet[1] !== undefined) {
|
|
78
80
|
const label = bullet[1].trim();
|
|
79
81
|
const rest = (bullet[2] ?? '').trim();
|
|
80
|
-
|
|
82
|
+
inVariants = label.toLowerCase() === 'variants' || label.toLowerCase() === 'flavors';
|
|
81
83
|
inGrouping = false;
|
|
82
84
|
if (label.toLowerCase() === 'leaf') {
|
|
83
85
|
collection.leaf = parseLink(rest)?.target ?? null;
|
|
@@ -96,18 +98,18 @@ function parseCollections(body) {
|
|
|
96
98
|
continue;
|
|
97
99
|
}
|
|
98
100
|
const item = nested[1].trim();
|
|
99
|
-
if (
|
|
100
|
-
const
|
|
101
|
-
if (
|
|
102
|
-
// `(default)` in the description marks the default
|
|
103
|
-
const described = (
|
|
101
|
+
if (inVariants) {
|
|
102
|
+
const variantLine = /^\[(.+?)\]\((.+?)\)\s*(?:—\s*(.*))?$/.exec(item);
|
|
103
|
+
if (variantLine && variantLine[1] && variantLine[2]) {
|
|
104
|
+
// `(default)` in the description marks the default variant; it is a flag, not prose.
|
|
105
|
+
const described = (variantLine[3] ?? '').trim();
|
|
104
106
|
const isDefault = /\(default\)/i.test(described);
|
|
105
107
|
const description = described.replace(/\s*\(default\)\.?$/i, '').replace(/\s*\(default\)/i, '').trim();
|
|
106
|
-
const { name,
|
|
107
|
-
collection.
|
|
108
|
+
const { name, unit } = variantFromTemplate(variantLine[1].trim(), variantLine[2].trim());
|
|
109
|
+
collection.variants.push({
|
|
108
110
|
name,
|
|
109
|
-
|
|
110
|
-
|
|
111
|
+
unit,
|
|
112
|
+
template: variantLine[2].trim(),
|
|
111
113
|
description,
|
|
112
114
|
isDefault,
|
|
113
115
|
});
|
|
@@ -120,12 +122,13 @@ function parseCollections(body) {
|
|
|
120
122
|
}
|
|
121
123
|
}
|
|
122
124
|
}
|
|
123
|
-
if (collection.
|
|
124
|
-
const first = collection.
|
|
125
|
+
if (collection.variants.length > 0 && !collection.variants.some((variant) => variant.isDefault)) {
|
|
126
|
+
const first = collection.variants[0];
|
|
125
127
|
if (first) {
|
|
126
128
|
first.isDefault = true;
|
|
127
129
|
}
|
|
128
130
|
}
|
|
131
|
+
collection.framing = unitOf(collection) === 'frame';
|
|
129
132
|
out.push(collection);
|
|
130
133
|
}
|
|
131
134
|
return out;
|
|
@@ -152,23 +155,86 @@ function parseAppliesTo(value) {
|
|
|
152
155
|
}
|
|
153
156
|
return plain.split(/[,/]/).map((name) => name.trim()).filter((name) => name !== '');
|
|
154
157
|
}
|
|
155
|
-
|
|
158
|
+
// The standard's columns come first (Name, Type, Meaning for the core; Name,
|
|
159
|
+
// Type, Applies To, Meaning for custom properties); any column past them is a
|
|
160
|
+
// tool's, headed with the tool's name, and rides along as that tool's field.
|
|
161
|
+
function parseProperties(block, core, owner = core ? 'eidos' : 'custom') {
|
|
156
162
|
if (block === null) {
|
|
157
163
|
return [];
|
|
158
164
|
}
|
|
159
165
|
const out = [];
|
|
166
|
+
const standard = core ? 3 : 4;
|
|
167
|
+
let header = [];
|
|
160
168
|
for (const cells of parseTable(block)) {
|
|
161
169
|
const name = stripInlineCode(cells[0] ?? '').trim();
|
|
162
|
-
if (name === ''
|
|
170
|
+
if (name === '') {
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
if (name.toLowerCase() === 'name') {
|
|
174
|
+
header = cells.map((cell) => stripInlineCode(cell).trim());
|
|
163
175
|
continue;
|
|
164
176
|
}
|
|
165
177
|
const type = (cells[1] ?? 'Text').trim();
|
|
166
|
-
|
|
167
|
-
|
|
178
|
+
const property = core || cells.length < 4
|
|
179
|
+
? { name, type, appliesTo: 'all', meaning: (cells[Math.min(cells.length, standard) - 1] ?? '').trim(), core, owner }
|
|
180
|
+
: { name, type, appliesTo: parseAppliesTo(cells[2] ?? ''), meaning: (cells[3] ?? '').trim(), core, owner };
|
|
181
|
+
const tools = {};
|
|
182
|
+
for (let index = standard; index < cells.length; index += 1) {
|
|
183
|
+
const tool = header[index];
|
|
184
|
+
const cell = (cells[index] ?? '').trim();
|
|
185
|
+
if (tool && cell !== '') {
|
|
186
|
+
tools[tool] = cell;
|
|
187
|
+
}
|
|
168
188
|
}
|
|
169
|
-
|
|
170
|
-
|
|
189
|
+
if (Object.keys(tools).length > 0) {
|
|
190
|
+
property.tools = tools;
|
|
171
191
|
}
|
|
192
|
+
out.push(property);
|
|
193
|
+
}
|
|
194
|
+
return out;
|
|
195
|
+
}
|
|
196
|
+
// `## Vocabulary`: Term · Means · Not, one row per term. A Term cell may be a
|
|
197
|
+
// link to the blueprint that defines it; the Not cell lists near-misses, `;` apart.
|
|
198
|
+
function parseVocabulary(body) {
|
|
199
|
+
const block = section(body, 2, 'Vocabulary');
|
|
200
|
+
if (block === null)
|
|
201
|
+
return [];
|
|
202
|
+
const out = [];
|
|
203
|
+
for (const row of parseTable(block)) {
|
|
204
|
+
const [termCell = '', means = '', notCell = ''] = row;
|
|
205
|
+
const link = parseLink(termCell.trim());
|
|
206
|
+
const term = (link ? link.title : stripInlineCode(termCell.replace(/\*\*/g, ''))).trim();
|
|
207
|
+
// the header row, a placeholder row, and a blank row are not terms
|
|
208
|
+
if (term === '' || term.toLowerCase() === 'term' || /^<.*>$/.test(term) || /^-+$/.test(term))
|
|
209
|
+
continue;
|
|
210
|
+
out.push({
|
|
211
|
+
term,
|
|
212
|
+
means: cleanText(means),
|
|
213
|
+
not: notCell
|
|
214
|
+
.split(';')
|
|
215
|
+
.map((entry) => cleanText(entry))
|
|
216
|
+
.filter((entry) => entry !== ''),
|
|
217
|
+
see: link ? link.target : null,
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
return out;
|
|
221
|
+
}
|
|
222
|
+
// `## Versions`: Version · Commit · Tag, newest first, as written. The key
|
|
223
|
+
// left the standard in 5.0.0; `migrate` moves what a 4.x root recorded.
|
|
224
|
+
export function parseVersions(body) {
|
|
225
|
+
const block = section(body, 2, 'Versions');
|
|
226
|
+
if (block === null) {
|
|
227
|
+
return [];
|
|
228
|
+
}
|
|
229
|
+
const out = [];
|
|
230
|
+
for (const cells of parseTable(block)) {
|
|
231
|
+
const version = stripInlineCode(cells[0] ?? '').replace(/^\*\*|\*\*$/g, '').trim();
|
|
232
|
+
if (version === '' || version.toLowerCase() === 'version') {
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
const commit = stripInlineCode(cells[1] ?? '').trim();
|
|
236
|
+
const tag = stripInlineCode(cells[2] ?? '').trim();
|
|
237
|
+
out.push({ version, commit, tag: tag === '' || tag === '—' || tag === '-' ? null : tag });
|
|
172
238
|
}
|
|
173
239
|
return out;
|
|
174
240
|
}
|
|
@@ -207,11 +273,23 @@ export function parseFrameworkMarkdown(text, root, file) {
|
|
|
207
273
|
}
|
|
208
274
|
}
|
|
209
275
|
const version = properties?.['eidos_version'];
|
|
210
|
-
const schemaBlock = section(body, 2, 'Schema') ?? '';
|
|
211
|
-
const coreBlock = section(schemaBlock, 3, 'Eidos Core');
|
|
212
|
-
const customBlock = section(schemaBlock, 3, 'Custom Properties');
|
|
276
|
+
const schemaBlock = section(body, 2, 'Properties') ?? section(body, 2, 'Schema') ?? '';
|
|
277
|
+
const coreBlock = section(schemaBlock, 3, 'Eidos') ?? section(schemaBlock, 3, 'Eidos Core');
|
|
278
|
+
const customBlock = section(schemaBlock, 3, 'Custom') ?? section(schemaBlock, 3, 'Custom Properties');
|
|
213
279
|
const core = parseProperties(coreBlock, true);
|
|
214
280
|
const custom = parseProperties(customBlock, false);
|
|
281
|
+
// `### <tool>` (`### <tool> Properties` before 5.0.0): a block a tool declared and alone writes.
|
|
282
|
+
const tools = {};
|
|
283
|
+
for (const { heading, body: block } of sections(schemaBlock, 3)) {
|
|
284
|
+
const name = heading.text.trim();
|
|
285
|
+
if (/^(eidos( core)?|custom( properties)?)$/i.test(name))
|
|
286
|
+
continue;
|
|
287
|
+
const match = /^(.+?)(?:\s+Properties)?$/i.exec(name);
|
|
288
|
+
const tool = match?.[1]?.trim() ?? '';
|
|
289
|
+
if (!match || tool === '' || tool.toLowerCase() === 'custom' || /^<.*>$/.test(tool))
|
|
290
|
+
continue;
|
|
291
|
+
tools[tool] = parseProperties(block, false, tool);
|
|
292
|
+
}
|
|
215
293
|
const collections = parseCollections(body);
|
|
216
294
|
for (const collection of collections) {
|
|
217
295
|
if (collection.grouping) {
|
|
@@ -227,7 +305,8 @@ export function parseFrameworkMarkdown(text, root, file) {
|
|
|
227
305
|
namingError,
|
|
228
306
|
topLevel: parseTopLevel(body),
|
|
229
307
|
collections,
|
|
230
|
-
schema: { core: core.length > 0 ? core : STANDARD_CORE, custom },
|
|
308
|
+
schema: { core: core.length > 0 ? core : STANDARD_CORE, custom, tools },
|
|
309
|
+
vocabulary: parseVocabulary(body),
|
|
231
310
|
problems: [],
|
|
232
311
|
};
|
|
233
312
|
}
|