eidosmd 0.2.0 → 0.3.1
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/browser/dist/assets/index-D_Xs1hAc.css +1 -0
- package/browser/dist/assets/index-K_EgH2M8.js +46 -0
- package/browser/dist/index.html +2 -2
- package/dist/src/commands/check.js +9 -3
- package/dist/src/commands/configure.js +201 -0
- package/dist/src/commands/framework.js +15 -4
- package/dist/src/commands/init.js +4 -0
- package/dist/src/commands/property.js +125 -0
- package/dist/src/commands/setup.js +24 -12
- package/dist/src/commands/version.js +2 -2
- package/dist/src/core/canvas.js +59 -49
- package/dist/src/core/check.js +101 -26
- package/dist/src/core/edits.js +1381 -0
- package/dist/src/core/framework-markdown.js +9 -3
- package/dist/src/core/framework-model.js +43 -8
- package/dist/src/core/framework-structured.js +104 -28
- package/dist/src/core/frontmatter.js +61 -1
- package/dist/src/core/git.js +28 -3
- package/dist/src/core/links.js +87 -0
- package/dist/src/core/markdown.js +16 -9
- package/dist/src/core/migrate.js +91 -15
- package/dist/src/core/regions.js +117 -0
- package/dist/src/core/scaffold.js +15 -9
- package/dist/src/core/seed.js +56 -31
- package/dist/src/core/server.js +204 -31
- package/dist/src/core/settings.js +63 -12
- package/dist/src/core/store.js +73 -17
- package/dist/src/core/versions.js +10 -5
- package/dist/src/program.js +296 -11
- package/instructions/authoring.md +4 -2
- package/instructions/configuring.md +27 -11
- package/instructions/overview.md +6 -4
- package/instructions/validating.md +6 -4
- package/package.json +1 -1
- package/standard/EIDOS.md +135 -193
- package/standard/seeds/README.md +12 -16
- package/standard/seeds/book/Framework.yaml +30 -50
- package/standard/seeds/book/README.md +2 -1
- package/standard/seeds/book/_gitignore +7 -1
- package/standard/seeds/research/Framework.yaml +30 -50
- package/standard/seeds/research/README.md +2 -1
- package/standard/seeds/research/_gitignore +7 -1
- package/standard/seeds/software/Framework.yaml +31 -51
- package/standard/seeds/software/README.md +2 -1
- package/standard/seeds/software/_gitignore +7 -1
- package/browser/dist/assets/index-C2NMN_D4.css +0 -1
- package/browser/dist/assets/index-C65k1ihb.js +0 -46
package/dist/src/core/canvas.js
CHANGED
|
@@ -9,7 +9,7 @@ import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from
|
|
|
9
9
|
import path from 'node:path';
|
|
10
10
|
import { parse as parseYaml, Document } from 'yaml';
|
|
11
11
|
import { blueprintId, blueprintTitle, propertyString } from './blueprint.js';
|
|
12
|
-
import { FRAMEWORK_DIR, PLUGINS_DIR, TOOL_KEY } from './framework-model.js';
|
|
12
|
+
import { FRAMEWORK_DIR, PLUGINS_DIR, readValueStyle, TOOL_KEY } from './framework-model.js';
|
|
13
13
|
import { kebab } from './naming.js';
|
|
14
14
|
// Relative to `.eidos/`: the folder the standard reserves for this tool.
|
|
15
15
|
export const MAPS_DIR = `${PLUGINS_DIR}/${TOOL_KEY}/maps`;
|
|
@@ -119,22 +119,7 @@ export function readBox(value) {
|
|
|
119
119
|
return null;
|
|
120
120
|
return { w: Math.max(40, Math.round(value['w'])), h: Math.max(24, Math.round(value['h'])) };
|
|
121
121
|
}
|
|
122
|
-
export
|
|
123
|
-
if (!isPlain(value))
|
|
124
|
-
return null;
|
|
125
|
-
const style = {};
|
|
126
|
-
if (text(value['shape']) !== '')
|
|
127
|
-
style.shape = text(value['shape']);
|
|
128
|
-
if (text(value['color']) !== '')
|
|
129
|
-
style.color = text(value['color']);
|
|
130
|
-
if (value['fill'] === 'solid' || value['fill'] === 'tint' || value['fill'] === 'none')
|
|
131
|
-
style.fill = value['fill'];
|
|
132
|
-
if (text(value['fill_color']) !== '')
|
|
133
|
-
style.fill_color = text(value['fill_color']);
|
|
134
|
-
if (value['size'] === 'small' || value['size'] === 'medium' || value['size'] === 'large')
|
|
135
|
-
style.size = value['size'];
|
|
136
|
-
return Object.keys(style).length > 0 ? style : null;
|
|
137
|
-
}
|
|
122
|
+
export const readStyle = readValueStyle;
|
|
138
123
|
function readPage(value, index, problems) {
|
|
139
124
|
const where = `pages[${index}]`;
|
|
140
125
|
if (!isPlain(value) || text(value['id']) === '') {
|
|
@@ -393,44 +378,65 @@ export const NODE_WIDTH = 220;
|
|
|
393
378
|
export function canvasProperties(framework) {
|
|
394
379
|
return framework.schema.custom.filter((property) => property.canvas !== undefined);
|
|
395
380
|
}
|
|
381
|
+
// The style a property gives one value, matched without regard to case.
|
|
382
|
+
export function valueStyle(hint, value) {
|
|
383
|
+
const key = Object.keys(hint.styles ?? {}).find((candidate) => candidate.toLowerCase() === value.toLowerCase());
|
|
384
|
+
return key ? (hint.styles?.[key] ?? null) : null;
|
|
385
|
+
}
|
|
396
386
|
// The default look a node's property values give it. Properties are read in
|
|
397
|
-
// schema order and the top-most that
|
|
398
|
-
//
|
|
387
|
+
// schema order and, field by field, the top-most that sets it for the value
|
|
388
|
+
// wins, so the order of the custom block is the precedence.
|
|
399
389
|
export function styleFor(framework, values) {
|
|
400
|
-
|
|
401
|
-
let color = null;
|
|
390
|
+
const found = {};
|
|
402
391
|
const shown = [];
|
|
403
392
|
for (const property of canvasProperties(framework)) {
|
|
404
393
|
const value = values[property.name];
|
|
405
394
|
if (value === undefined || value === '')
|
|
406
395
|
continue;
|
|
407
|
-
const
|
|
408
|
-
if (
|
|
409
|
-
shape
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
396
|
+
const style = valueStyle(property.canvas, value);
|
|
397
|
+
if (style) {
|
|
398
|
+
if (found.shape === undefined && style.shape)
|
|
399
|
+
found.shape = style.shape;
|
|
400
|
+
if (found.color === undefined && style.color)
|
|
401
|
+
found.color = style.color;
|
|
402
|
+
if (found.fill === undefined && style.fill)
|
|
403
|
+
found.fill = style.fill;
|
|
404
|
+
if (found.fill_color === undefined && style.fill_color)
|
|
405
|
+
found.fill_color = style.fill_color;
|
|
406
|
+
if (found.size === undefined && style.size)
|
|
407
|
+
found.size = style.size;
|
|
408
|
+
}
|
|
413
409
|
if (property.canvas.show)
|
|
414
410
|
shown.push({ property: property.name, value });
|
|
415
411
|
}
|
|
416
|
-
return {
|
|
412
|
+
return { ...withLegacyShape(found), shown };
|
|
417
413
|
}
|
|
418
|
-
//
|
|
419
|
-
//
|
|
420
|
-
|
|
421
|
-
// the fill they meant.
|
|
422
|
-
function drawn(node, defaults) {
|
|
414
|
+
// Shapes that were really colors in the first canvases (dark, accent, text)
|
|
415
|
+
// read as a box with the fill they meant.
|
|
416
|
+
function withLegacyShape(style) {
|
|
423
417
|
const legacy = { dark: { fill: 'solid', fill_color: '#191b1f' }, accent: { fill: 'tint' }, text: { fill: 'none' } };
|
|
424
|
-
const
|
|
425
|
-
const meant = legacy[chosen];
|
|
418
|
+
const meant = style.shape ? legacy[style.shape] : undefined;
|
|
426
419
|
return {
|
|
427
|
-
shape: meant ? 'box' :
|
|
428
|
-
color:
|
|
429
|
-
fill:
|
|
430
|
-
fill_color:
|
|
431
|
-
size:
|
|
420
|
+
shape: meant ? 'box' : (style.shape ?? 'box'),
|
|
421
|
+
color: style.color ?? null,
|
|
422
|
+
fill: style.fill ?? meant?.fill ?? 'solid',
|
|
423
|
+
fill_color: style.fill_color ?? meant?.fill_color ?? null,
|
|
424
|
+
size: style.size ?? 'medium',
|
|
432
425
|
};
|
|
433
426
|
}
|
|
427
|
+
// The override laid over the default: each field the node sets itself, the
|
|
428
|
+
// rest from the properties.
|
|
429
|
+
function drawn(node, defaults) {
|
|
430
|
+
const style = node.style ?? {};
|
|
431
|
+
const own = withLegacyShape({
|
|
432
|
+
shape: style.shape ?? defaults.shape,
|
|
433
|
+
...(style.color ?? defaults.color ? { color: style.color ?? defaults.color ?? '' } : {}),
|
|
434
|
+
fill: style.fill ?? defaults.fill,
|
|
435
|
+
...(style.fill_color ?? defaults.fill_color ? { fill_color: style.fill_color ?? defaults.fill_color ?? '' } : {}),
|
|
436
|
+
size: style.size ?? defaults.size,
|
|
437
|
+
});
|
|
438
|
+
return { shape: own.shape, color: own.color, fill: own.fill, fill_color: own.fill_color, size: own.size };
|
|
439
|
+
}
|
|
434
440
|
// A simple layered layout from the edges: a node's column is the longest path
|
|
435
441
|
// from a source to it, its row its order within the column. Frames are drawn
|
|
436
442
|
// around their members by the page. Overrides win.
|
|
@@ -520,7 +526,7 @@ export function renderPage(framework, blueprints, page) {
|
|
|
520
526
|
style: drawn(node, style),
|
|
521
527
|
font: null,
|
|
522
528
|
box: node.box ?? null,
|
|
523
|
-
defaults: { shape: style.shape, color: style.color },
|
|
529
|
+
defaults: { shape: style.shape, color: style.color, fill: style.fill, fill_color: style.fill_color, size: style.size },
|
|
524
530
|
override: node.style ?? null,
|
|
525
531
|
styled: node.styled === true,
|
|
526
532
|
shown: style.shown,
|
|
@@ -543,7 +549,7 @@ export function renderPage(framework, blueprints, page) {
|
|
|
543
549
|
style: { shape: 'text', color: node.style?.color ?? null, fill: 'none', fill_color: null, size: node.text.size ?? 'medium' },
|
|
544
550
|
font: node.text.font ?? null,
|
|
545
551
|
box: node.box ?? null,
|
|
546
|
-
defaults: { shape: 'text', color: null },
|
|
552
|
+
defaults: { shape: 'text', color: null, fill: 'solid', fill_color: null, size: 'medium' },
|
|
547
553
|
override: node.style ?? null,
|
|
548
554
|
styled: node.styled === true,
|
|
549
555
|
shown: [],
|
|
@@ -566,7 +572,7 @@ export function renderPage(framework, blueprints, page) {
|
|
|
566
572
|
style: { shape: 'annotation', color: node.annotation.color ?? null, fill: 'solid', fill_color: null, size: node.style?.size ?? 'medium' },
|
|
567
573
|
font: null,
|
|
568
574
|
box: node.box ?? null,
|
|
569
|
-
defaults: { shape: 'annotation', color: null },
|
|
575
|
+
defaults: { shape: 'annotation', color: null, fill: 'solid', fill_color: null, size: 'medium' },
|
|
570
576
|
override: node.style ?? null,
|
|
571
577
|
styled: node.styled === true,
|
|
572
578
|
shown: [],
|
|
@@ -589,7 +595,7 @@ export function renderPage(framework, blueprints, page) {
|
|
|
589
595
|
style: { shape: 'sticky', color: node.sticky.color ?? null, fill: 'solid', fill_color: null, size: node.style?.size ?? 'medium' },
|
|
590
596
|
font: null,
|
|
591
597
|
box: node.box ?? null,
|
|
592
|
-
defaults: { shape: 'sticky', color: null },
|
|
598
|
+
defaults: { shape: 'sticky', color: null, fill: 'solid', fill_color: null, size: 'medium' },
|
|
593
599
|
override: node.style ?? null,
|
|
594
600
|
styled: node.styled === true,
|
|
595
601
|
shown: [],
|
|
@@ -614,7 +620,7 @@ export function renderPage(framework, blueprints, page) {
|
|
|
614
620
|
style: drawn(node, style),
|
|
615
621
|
font: null,
|
|
616
622
|
box: node.box ?? null,
|
|
617
|
-
defaults: { shape: style.shape, color: style.color },
|
|
623
|
+
defaults: { shape: style.shape, color: style.color, fill: style.fill, fill_color: style.fill_color, size: style.size },
|
|
618
624
|
override: node.style ?? null,
|
|
619
625
|
styled: node.styled === true,
|
|
620
626
|
shown: style.shown,
|
|
@@ -627,8 +633,9 @@ export function renderPage(framework, blueprints, page) {
|
|
|
627
633
|
// `kind` and `status` read as the first shape- and color-styled properties.
|
|
628
634
|
export function sketchValues(framework, sketch) {
|
|
629
635
|
const values = {};
|
|
630
|
-
const
|
|
631
|
-
const
|
|
636
|
+
const styles = (property) => Object.values(property.canvas.styles ?? {});
|
|
637
|
+
const kindProperty = canvasProperties(framework).find((property) => styles(property).some((style) => style.shape));
|
|
638
|
+
const statusProperty = canvasProperties(framework).find((property) => styles(property).some((style) => style.color));
|
|
632
639
|
if (sketch.kind && kindProperty)
|
|
633
640
|
values[kindProperty.name] = sketch.kind;
|
|
634
641
|
if (sketch.status && statusProperty)
|
|
@@ -702,14 +709,17 @@ export function toJsonCanvas(framework, blueprints, page) {
|
|
|
702
709
|
}
|
|
703
710
|
// --- the software seed's default palette, written by init --------------------
|
|
704
711
|
// The legend the owner already uses, as values of the seed's own properties.
|
|
712
|
+
const shaped = (shape) => ({ shape });
|
|
713
|
+
const colored = (color) => ({ color });
|
|
705
714
|
export const DEFAULT_CANVAS_HINTS = {
|
|
706
715
|
type: {
|
|
707
716
|
show: true,
|
|
708
|
-
|
|
717
|
+
styles: { page: shaped('pill'), feature: shaped('box'), capability: shaped('box'), integration: shaped('box'), background: shaped('box'), module: shaped('box'), decision: shaped('parallelogram'), note: shaped('box') },
|
|
709
718
|
},
|
|
710
719
|
status: {
|
|
711
720
|
show: true,
|
|
712
|
-
|
|
721
|
+
// the browser's standard colors (PALETTE in the canvas model), so a default and a hand-picked swatch never clash
|
|
722
|
+
styles: { Draft: colored('#8b9099'), Intake: colored('#d3a53c'), 'In Progress': colored('#5f80cf'), Done: colored('#7fae6c'), Archived: colored('#8b9099'), Deprecated: colored('#cf7b8f'), 'needs-clarification': colored('#5f80cf'), maybe: colored('#e08a63'), visionary: colored('#4aa39f') },
|
|
713
723
|
},
|
|
714
724
|
};
|
|
715
725
|
export function applyDefaultCanvasHints(properties) {
|
package/dist/src/core/check.js
CHANGED
|
@@ -5,13 +5,14 @@
|
|
|
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, resolveVariant } from './blueprint.js';
|
|
9
|
-
import { appliesTo, unitOf } from './framework.js';
|
|
8
|
+
import { blueprintId, blueprintTitle, groupDirs, propertyString, resolveVariant } from './blueprint.js';
|
|
9
|
+
import { appliesTo, isCollection, unitOf } from './framework.js';
|
|
10
10
|
import { renderEmbeddedEntries } from './index-leaf.js';
|
|
11
11
|
import { embeddedIndexOf } from './framework-structured.js';
|
|
12
12
|
import { FRAME_REF, isFrameRef, listCanvases } from './canvas.js';
|
|
13
13
|
import { normalizeNewlines } from './markdown.js';
|
|
14
14
|
import { kebab } from './naming.js';
|
|
15
|
+
import { parseRegions, stripRegions } from './regions.js';
|
|
15
16
|
import { loadTemplate } from './template.js';
|
|
16
17
|
const WORK_TRACKING_FIELDS = new Set(['sprint', 'estimate', 'assignee', 'story_points']);
|
|
17
18
|
const SKIPPED_DIRS = new Set(['node_modules', 'dist', 'build']);
|
|
@@ -50,6 +51,19 @@ function typeProblem(property, value) {
|
|
|
50
51
|
return null;
|
|
51
52
|
}
|
|
52
53
|
}
|
|
54
|
+
// Rule 9: a value off a property's declared options, exact, case included:
|
|
55
|
+
// the value (or the List's elements) not on the list, or null when every
|
|
56
|
+
// one is, or when the property declares none. The list rides along so the
|
|
57
|
+
// message can show it.
|
|
58
|
+
export function optionProblem(property, value) {
|
|
59
|
+
if (!property.options || isEmpty(value)) {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
const declared = new Set(property.options);
|
|
63
|
+
const held = Array.isArray(value) ? value : [value];
|
|
64
|
+
const off = held.filter((item) => !isEmpty(item) && (typeof item !== 'string' || !declared.has(item))).map((item) => (typeof item === 'string' ? item : JSON.stringify(item)));
|
|
65
|
+
return off.length > 0 ? { off, options: property.options } : null;
|
|
66
|
+
}
|
|
53
67
|
// Every markdown link target in a piece of text, outside code fences and
|
|
54
68
|
// inline code. Scheme-bearing targets and pure anchors are not files.
|
|
55
69
|
function linkTargets(text) {
|
|
@@ -110,6 +124,15 @@ class Collector {
|
|
|
110
124
|
function rel(root, file) {
|
|
111
125
|
return path.relative(root, file).split(path.sep).join('/');
|
|
112
126
|
}
|
|
127
|
+
// Rule 20: a region's contents are never faulted, and the one thing about a
|
|
128
|
+
// region that is, whichever tool it names, is an opener with no closer.
|
|
129
|
+
function checkRegions(text, file, out) {
|
|
130
|
+
for (const region of parseRegions(text)) {
|
|
131
|
+
if (region.close === null) {
|
|
132
|
+
out.error('region-unclosed', file, `\`<!-- ${region.tool}:${region.region} -->\` opens a region that never closes; add \`<!-- /${region.tool}:${region.region} -->\` after its contents`);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
113
136
|
function checkFramework(framework, out, options) {
|
|
114
137
|
const frameworkRel = rel(framework.root, framework.file);
|
|
115
138
|
for (const problem of framework.problems) {
|
|
@@ -136,12 +159,17 @@ function checkFramework(framework, out, options) {
|
|
|
136
159
|
if (/^versions:/m.test(readFileSync(framework.file, 'utf8'))) {
|
|
137
160
|
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');
|
|
138
161
|
}
|
|
139
|
-
if (!existsSync(path.join(framework.root, 'README.md'))) {
|
|
140
|
-
out.warn('readme-missing', null, 'no README.md at the root; the visible front door is missing');
|
|
141
|
-
}
|
|
142
162
|
for (const doc of framework.topLevel) {
|
|
143
|
-
if (isFileTarget(doc.path)
|
|
163
|
+
if (!isFileTarget(doc.path)) {
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
if (!linkExists(path.dirname(framework.file), doc.path)) {
|
|
144
167
|
out.warn('top-level-missing', frameworkRel, `## Top-Level lists ${doc.title} at ${doc.path}, which does not exist`);
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
const file = path.resolve(path.dirname(framework.file), doc.path.split('#')[0] ?? '');
|
|
171
|
+
if (file.startsWith(framework.root + path.sep) && /\.md$/i.test(file)) {
|
|
172
|
+
checkRegions(readFileSync(file, 'utf8'), rel(framework.root, file), out);
|
|
145
173
|
}
|
|
146
174
|
}
|
|
147
175
|
// A collection's templates are named for its unit, `<unit>.<variant>.md`;
|
|
@@ -155,6 +183,9 @@ function checkFramework(framework, out, options) {
|
|
|
155
183
|
if (template?.frontmatter) {
|
|
156
184
|
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
185
|
}
|
|
186
|
+
if (template) {
|
|
187
|
+
checkRegions(template.text, rel(framework.root, template.path), out);
|
|
188
|
+
}
|
|
158
189
|
if (!template) {
|
|
159
190
|
out.error('template-missing', frameworkRel, `${collection.name}/${variant.name} points at .eidos/${variant.template}, which does not exist`);
|
|
160
191
|
}
|
|
@@ -167,11 +198,28 @@ function checkFramework(framework, out, options) {
|
|
|
167
198
|
out.warn('collection-folder-missing', null, `collection ${collection.name} is declared but has no folder`);
|
|
168
199
|
}
|
|
169
200
|
}
|
|
201
|
+
for (const folder of framework.folders) {
|
|
202
|
+
if (!isCollection(folder) && !existsSync(path.join(framework.root, folder.name))) {
|
|
203
|
+
out.warn('folder-missing', null, `${folder.name} is declared as ${folder.type === 'assets' ? 'an assets' : 'an other'} folder but has no folder`);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
170
206
|
return templates;
|
|
171
207
|
}
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
208
|
+
// Rule 21: what sits at the root that the framework document does not
|
|
209
|
+
// declare. A folder is `.eidos/` or one under `folders`; a file is one under
|
|
210
|
+
// `top_level`. Hidden entries are the host's and exempt.
|
|
211
|
+
export function undeclaredAtRoot(framework) {
|
|
212
|
+
const declaredFolders = new Set(framework.folders.map((folder) => folder.name));
|
|
213
|
+
const declaredFiles = new Set();
|
|
214
|
+
for (const doc of framework.topLevel) {
|
|
215
|
+
if (!isFileTarget(doc.path))
|
|
216
|
+
continue;
|
|
217
|
+
const file = path.resolve(path.dirname(framework.file), doc.path.split('#')[0] ?? '');
|
|
218
|
+
if (path.dirname(file) === framework.root)
|
|
219
|
+
declaredFiles.add(path.basename(file));
|
|
220
|
+
}
|
|
221
|
+
const folders = [];
|
|
222
|
+
const files = [];
|
|
175
223
|
let entries = [];
|
|
176
224
|
try {
|
|
177
225
|
entries = readdirSync(framework.root);
|
|
@@ -180,19 +228,30 @@ function checkLayout(framework, blueprints, out) {
|
|
|
180
228
|
entries = [];
|
|
181
229
|
}
|
|
182
230
|
for (const entry of entries.sort()) {
|
|
183
|
-
if (entry.startsWith('.') ||
|
|
231
|
+
if (entry.startsWith('.') || SKIPPED_DIRS.has(entry))
|
|
184
232
|
continue;
|
|
185
|
-
}
|
|
186
233
|
let isDir = false;
|
|
187
234
|
try {
|
|
188
235
|
isDir = statSync(path.join(framework.root, entry)).isDirectory();
|
|
189
236
|
}
|
|
190
237
|
catch {
|
|
191
|
-
|
|
192
|
-
}
|
|
193
|
-
if (isDir) {
|
|
194
|
-
out.warn('folder-undeclared', entry, 'a folder at the root that no collection declares; declare it in the framework document or move it');
|
|
238
|
+
continue;
|
|
195
239
|
}
|
|
240
|
+
if (isDir && !declaredFolders.has(entry))
|
|
241
|
+
folders.push(entry);
|
|
242
|
+
else if (!isDir && !declaredFiles.has(entry))
|
|
243
|
+
files.push(entry);
|
|
244
|
+
}
|
|
245
|
+
return { folders, files };
|
|
246
|
+
}
|
|
247
|
+
function checkLayout(framework, blueprints, out) {
|
|
248
|
+
const embedded = embeddedIndexOf(readFileSync(framework.file, 'utf8'));
|
|
249
|
+
const undeclared = undeclaredAtRoot(framework);
|
|
250
|
+
for (const entry of undeclared.folders) {
|
|
251
|
+
out.warn('folder-undeclared', entry, 'a folder at the root the framework document does not declare; declare it under folders (a collection, assets, or other) or move it');
|
|
252
|
+
}
|
|
253
|
+
for (const entry of undeclared.files) {
|
|
254
|
+
out.warn('file-undeclared', entry, 'a file at the root that top_level does not list; declare it (`eidos configure:doc add`) or move it');
|
|
196
255
|
}
|
|
197
256
|
for (const collection of framework.collections) {
|
|
198
257
|
const folder = path.join(framework.root, collection.name);
|
|
@@ -201,13 +260,21 @@ function checkLayout(framework, blueprints, out) {
|
|
|
201
260
|
}
|
|
202
261
|
const own = blueprints.filter((blueprint) => blueprint.collection === collection);
|
|
203
262
|
const groupsOnDisk = [...new Set(own.map((blueprint) => blueprint.group).filter((group) => group !== null))];
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
}
|
|
263
|
+
// Rule 10: a collection is flat or grouped; every sub-folder is a declared
|
|
264
|
+
// group, and a group holds blueprints and nothing deeper.
|
|
265
|
+
const declaredGroups = new Map((collection.grouping?.groups ?? []).map((group) => [group.name.toLowerCase(), group.name]));
|
|
266
|
+
for (const group of groupDirs(folder)) {
|
|
267
|
+
if (!collection.grouping) {
|
|
268
|
+
out.warn('group-undeclared', `${collection.name}/${group}`, `a sub-folder of ${collection.name}, which declares no grouping; declare a grouping and the group, or move it`);
|
|
269
|
+
}
|
|
270
|
+
else if (!declaredGroups.has(group.toLowerCase())) {
|
|
271
|
+
out.warn('group-undeclared', `${collection.name}/${group}`, `not declared under ${collection.grouping.label} in the framework document`);
|
|
210
272
|
}
|
|
273
|
+
for (const nested of groupDirs(path.join(folder, group))) {
|
|
274
|
+
out.warn('folder-nested', `${collection.name}/${group}/${nested}`, 'a folder inside a group; a group holds blueprints and nothing deeper');
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
if (collection.grouping) {
|
|
211
278
|
for (const group of collection.grouping.groups) {
|
|
212
279
|
if (!existsSync(path.join(folder, group.name))) {
|
|
213
280
|
out.warn('group-folder-missing', `${collection.name}/${group.name}`, `declared under ${collection.grouping.label} but has no folder`);
|
|
@@ -239,7 +306,7 @@ function checkLayout(framework, blueprints, out) {
|
|
|
239
306
|
function checkVocabulary(framework, blueprint, out) {
|
|
240
307
|
if (framework.vocabulary.length === 0 || blueprint.body.trim() === '')
|
|
241
308
|
return;
|
|
242
|
-
const body = blueprint.body.toLowerCase();
|
|
309
|
+
const body = stripRegions(blueprint.body).toLowerCase();
|
|
243
310
|
for (const entry of framework.vocabulary) {
|
|
244
311
|
for (const clause of entry.not) {
|
|
245
312
|
const phrase = clause.split(/[,;:(]/)[0]?.trim().toLowerCase() ?? '';
|
|
@@ -287,12 +354,14 @@ function checkBlueprint(framework, blueprint, templates, out) {
|
|
|
287
354
|
const schemaProperties = [...framework.schema.custom, ...Object.values(framework.schema.tools).flat()];
|
|
288
355
|
const applicable = schemaProperties.filter((property) => appliesTo(property, collection.name));
|
|
289
356
|
const applicableNames = new Set(applicable.map((property) => property.name));
|
|
290
|
-
|
|
357
|
+
// Rule 4: a required property absent or blank is a gap to surface; an
|
|
358
|
+
// optional one absent is nothing, and present it is checked only by type.
|
|
359
|
+
for (const property of applicable.filter((candidate) => candidate.required)) {
|
|
291
360
|
if (!(property.name in properties)) {
|
|
292
|
-
out.warn('property-missing', file, `no \`${property.name}\` (${property.type}, applies to ${collection.name})`);
|
|
361
|
+
out.warn('property-missing', file, `no \`${property.name}\` (${property.type}, required, applies to ${collection.name})`);
|
|
293
362
|
}
|
|
294
363
|
else if (isEmpty(properties[property.name]) && property.type.toLowerCase() !== 'list') {
|
|
295
|
-
out.warn('property-empty', file, `\`${property.name}\` has no value`);
|
|
364
|
+
out.warn('property-empty', file, `\`${property.name}\` is required but has no value`);
|
|
296
365
|
}
|
|
297
366
|
}
|
|
298
367
|
for (const key of Object.keys(properties)) {
|
|
@@ -318,6 +387,11 @@ function checkBlueprint(framework, blueprint, templates, out) {
|
|
|
318
387
|
const problem = typeProblem(property, properties[property.name]);
|
|
319
388
|
if (problem) {
|
|
320
389
|
out.warn('property-type', file, `\`${property.name}\` (${property.type}): ${problem}`);
|
|
390
|
+
continue;
|
|
391
|
+
}
|
|
392
|
+
const offList = optionProblem(property, properties[property.name]);
|
|
393
|
+
if (offList) {
|
|
394
|
+
out.warn('property-option', file, `\`${property.name}: ${offList.off.join(', ')}\` is not one of its options (${offList.options.join(', ')})`);
|
|
321
395
|
}
|
|
322
396
|
}
|
|
323
397
|
if (collection.grouping?.property) {
|
|
@@ -337,8 +411,9 @@ function checkBlueprint(framework, blueprint, templates, out) {
|
|
|
337
411
|
}
|
|
338
412
|
}
|
|
339
413
|
}
|
|
414
|
+
checkRegions(blueprint.body, file, out);
|
|
340
415
|
const brokenInBody = new Map();
|
|
341
|
-
for (const target of linkTargets(blueprint.body)) {
|
|
416
|
+
for (const target of linkTargets(stripRegions(blueprint.body))) {
|
|
342
417
|
if (isFileTarget(target) && !linkExists(dir, target)) {
|
|
343
418
|
brokenInBody.set(target, (brokenInBody.get(target) ?? 0) + 1);
|
|
344
419
|
}
|