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
package/dist/src/core/check.js
CHANGED
|
@@ -1,17 +1,19 @@
|
|
|
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, 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
|
+
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 { parseRegions, stripRegions } from './regions.js';
|
|
16
|
+
import { loadTemplate } from './template.js';
|
|
15
17
|
const WORK_TRACKING_FIELDS = new Set(['sprint', 'estimate', 'assignee', 'story_points']);
|
|
16
18
|
const SKIPPED_DIRS = new Set(['node_modules', 'dist', 'build']);
|
|
17
19
|
function compareVersions(a, b) {
|
|
@@ -49,6 +51,19 @@ function typeProblem(property, value) {
|
|
|
49
51
|
return null;
|
|
50
52
|
}
|
|
51
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
|
+
}
|
|
52
67
|
// Every markdown link target in a piece of text, outside code fences and
|
|
53
68
|
// inline code. Scheme-bearing targets and pure anchors are not files.
|
|
54
69
|
function linkTargets(text) {
|
|
@@ -99,8 +114,8 @@ function propertyLinks(value) {
|
|
|
99
114
|
}
|
|
100
115
|
class Collector {
|
|
101
116
|
findings = [];
|
|
102
|
-
error(code, file, message) {
|
|
103
|
-
this.findings.push({ level: 'error', code, path: file, message });
|
|
117
|
+
error(code, file, message, fix) {
|
|
118
|
+
this.findings.push({ level: 'error', code, path: file, message, ...(fix ? { fix } : {}) });
|
|
104
119
|
}
|
|
105
120
|
warn(code, file, message) {
|
|
106
121
|
this.findings.push({ level: 'warning', code, path: file, message });
|
|
@@ -109,6 +124,15 @@ class Collector {
|
|
|
109
124
|
function rel(root, file) {
|
|
110
125
|
return path.relative(root, file).split(path.sep).join('/');
|
|
111
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
|
+
}
|
|
112
136
|
function checkFramework(framework, out, options) {
|
|
113
137
|
const frameworkRel = rel(framework.root, framework.file);
|
|
114
138
|
for (const problem of framework.problems) {
|
|
@@ -123,44 +147,79 @@ function checkFramework(framework, out, options) {
|
|
|
123
147
|
else {
|
|
124
148
|
const diff = compareVersions(framework.eidosVersion, options.standardVersion);
|
|
125
149
|
if (diff < 0) {
|
|
126
|
-
out.warn('version-behind', frameworkRel, `targets Eidos ${framework.eidosVersion}; this CLI carries ${options.standardVersion}
|
|
150
|
+
out.warn('version-behind', frameworkRel, `targets Eidos ${framework.eidosVersion}; this CLI carries ${options.standardVersion}: run \`eidos migrate\` when ready`);
|
|
127
151
|
}
|
|
128
152
|
else if (diff > 0) {
|
|
129
153
|
out.warn('version-ahead', frameworkRel, `targets Eidos ${framework.eidosVersion}, newer than the ${options.standardVersion} this CLI carries`);
|
|
130
154
|
}
|
|
131
155
|
}
|
|
132
156
|
if (framework.collections.length === 0) {
|
|
133
|
-
out.error('no-collections', frameworkRel, 'declares no collections
|
|
157
|
+
out.error('no-collections', frameworkRel, 'declares no collections; a framework needs at least one');
|
|
134
158
|
}
|
|
135
|
-
if (
|
|
136
|
-
out.warn('
|
|
159
|
+
if (/^versions:/m.test(readFileSync(framework.file, 'utf8'))) {
|
|
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');
|
|
137
161
|
}
|
|
138
162
|
for (const doc of framework.topLevel) {
|
|
139
|
-
if (isFileTarget(doc.path)
|
|
163
|
+
if (!isFileTarget(doc.path)) {
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
if (!linkExists(path.dirname(framework.file), doc.path)) {
|
|
140
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);
|
|
141
173
|
}
|
|
142
174
|
}
|
|
143
|
-
|
|
175
|
+
// A collection's templates are named for its unit, `<unit>.<variant>.md`;
|
|
176
|
+
// a file named otherwise is faulted, as is one that opens with frontmatter.
|
|
177
|
+
const templates = new Map();
|
|
144
178
|
for (const collection of framework.collections) {
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
179
|
+
const unit = unitOf(collection);
|
|
180
|
+
for (const variant of collection.variants) {
|
|
181
|
+
const template = loadTemplate(framework, variant);
|
|
182
|
+
templates.set(`${collection.name}/${variant.name}`, template);
|
|
183
|
+
if (template?.frontmatter) {
|
|
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');
|
|
185
|
+
}
|
|
186
|
+
if (template) {
|
|
187
|
+
checkRegions(template.text, rel(framework.root, template.path), out);
|
|
188
|
+
}
|
|
189
|
+
if (!template) {
|
|
190
|
+
out.error('template-missing', frameworkRel, `${collection.name}/${variant.name} points at .eidos/${variant.template}, which does not exist`);
|
|
191
|
+
}
|
|
192
|
+
const expected = unit ? `${unit}.${variant.name}.md` : null;
|
|
193
|
+
if (expected && !variant.template.endsWith(`/${expected}`) && variant.template !== expected) {
|
|
194
|
+
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
195
|
}
|
|
154
196
|
}
|
|
155
197
|
if (!existsSync(path.join(framework.root, collection.name))) {
|
|
156
198
|
out.warn('collection-folder-missing', null, `collection ${collection.name} is declared but has no folder`);
|
|
157
199
|
}
|
|
158
200
|
}
|
|
159
|
-
|
|
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
|
+
}
|
|
206
|
+
return templates;
|
|
160
207
|
}
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
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 = [];
|
|
164
223
|
let entries = [];
|
|
165
224
|
try {
|
|
166
225
|
entries = readdirSync(framework.root);
|
|
@@ -169,19 +228,30 @@ function checkLayout(framework, blueprints, out) {
|
|
|
169
228
|
entries = [];
|
|
170
229
|
}
|
|
171
230
|
for (const entry of entries.sort()) {
|
|
172
|
-
if (entry.startsWith('.') ||
|
|
231
|
+
if (entry.startsWith('.') || SKIPPED_DIRS.has(entry))
|
|
173
232
|
continue;
|
|
174
|
-
}
|
|
175
233
|
let isDir = false;
|
|
176
234
|
try {
|
|
177
235
|
isDir = statSync(path.join(framework.root, entry)).isDirectory();
|
|
178
236
|
}
|
|
179
237
|
catch {
|
|
180
|
-
|
|
181
|
-
}
|
|
182
|
-
if (isDir) {
|
|
183
|
-
out.warn('folder-undeclared', entry, 'a folder at the root that no collection declares; declare it in Framework.md or move it');
|
|
238
|
+
continue;
|
|
184
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');
|
|
185
255
|
}
|
|
186
256
|
for (const collection of framework.collections) {
|
|
187
257
|
const folder = path.join(framework.root, collection.name);
|
|
@@ -190,13 +260,21 @@ function checkLayout(framework, blueprints, out) {
|
|
|
190
260
|
}
|
|
191
261
|
const own = blueprints.filter((blueprint) => blueprint.collection === collection);
|
|
192
262
|
const groupsOnDisk = [...new Set(own.map((blueprint) => blueprint.group).filter((group) => group !== null))];
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
}
|
|
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`);
|
|
199
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`);
|
|
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) {
|
|
200
278
|
for (const group of collection.grouping.groups) {
|
|
201
279
|
if (!existsSync(path.join(folder, group.name))) {
|
|
202
280
|
out.warn('group-folder-missing', `${collection.name}/${group.name}`, `declared under ${collection.grouping.label} but has no folder`);
|
|
@@ -209,10 +287,10 @@ function checkLayout(framework, blueprints, out) {
|
|
|
209
287
|
}
|
|
210
288
|
}
|
|
211
289
|
if (collection.framing) {
|
|
212
|
-
for (const
|
|
213
|
-
const written = own.some((blueprint) =>
|
|
290
|
+
for (const variant of collection.variants) {
|
|
291
|
+
const written = own.some((blueprint) => resolveVariant(blueprint).variant === variant);
|
|
214
292
|
if (!written) {
|
|
215
|
-
out.warn('frame-unwritten', collection.name, `no ${
|
|
293
|
+
out.warn('frame-unwritten', collection.name, `no ${variant.name} frame yet; a gap to surface, not a failure`);
|
|
216
294
|
}
|
|
217
295
|
}
|
|
218
296
|
}
|
|
@@ -223,7 +301,25 @@ function checkLayout(framework, blueprints, out) {
|
|
|
223
301
|
}
|
|
224
302
|
}
|
|
225
303
|
}
|
|
226
|
-
|
|
304
|
+
// Rule 19: a near-miss the Vocabulary names is flagged with the declared term
|
|
305
|
+
// beside it, never refused. The near-miss is the clause's opening phrase.
|
|
306
|
+
function checkVocabulary(framework, blueprint, out) {
|
|
307
|
+
if (framework.vocabulary.length === 0 || blueprint.body.trim() === '')
|
|
308
|
+
return;
|
|
309
|
+
const body = stripRegions(blueprint.body).toLowerCase();
|
|
310
|
+
for (const entry of framework.vocabulary) {
|
|
311
|
+
for (const clause of entry.not) {
|
|
312
|
+
const phrase = clause.split(/[,;:(]/)[0]?.trim().toLowerCase() ?? '';
|
|
313
|
+
if (phrase === '' || phrase === entry.term.toLowerCase())
|
|
314
|
+
continue;
|
|
315
|
+
const pattern = new RegExp(`(^|[^a-z0-9])${phrase.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}(?![a-z0-9])`, 'i');
|
|
316
|
+
if (pattern.test(body)) {
|
|
317
|
+
out.warn('term-near-miss', blueprint.rel, `uses "${phrase}"; the Vocabulary declares "${entry.term}"${entry.means ? ` (${entry.means})` : ''}`);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
function checkBlueprint(framework, blueprint, templates, out) {
|
|
227
323
|
const file = blueprint.rel;
|
|
228
324
|
if (blueprint.properties === null) {
|
|
229
325
|
if (blueprint.frontmatterError) {
|
|
@@ -241,13 +337,7 @@ function checkBlueprint(framework, blueprint, shapes, out) {
|
|
|
241
337
|
const collection = blueprint.collection;
|
|
242
338
|
const id = propertyString(blueprint, 'id');
|
|
243
339
|
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`);
|
|
340
|
+
out.error('id-missing', file, 'no `id`; add a permanent identity (a slug, a number, a GUID)');
|
|
251
341
|
}
|
|
252
342
|
if (propertyString(blueprint, 'title') === null) {
|
|
253
343
|
out.error('title-missing', file, 'no `title`');
|
|
@@ -255,27 +345,30 @@ function checkBlueprint(framework, blueprint, shapes, out) {
|
|
|
255
345
|
if (propertyString(blueprint, 'summary') === null) {
|
|
256
346
|
out.warn('summary-missing', file, 'no `summary`; the collection index will flag it');
|
|
257
347
|
}
|
|
258
|
-
const {
|
|
348
|
+
const { variant, declared, known } = resolveVariant(blueprint);
|
|
259
349
|
if (!known) {
|
|
260
|
-
const names = collection.
|
|
261
|
-
out.error('
|
|
350
|
+
const names = collection.variants.map((candidate) => candidate.name).join(', ');
|
|
351
|
+
out.error('variant-unknown', file, `variant '${declared ?? ''}' is not one of ${collection.name}'s declared variants (${names})`);
|
|
262
352
|
}
|
|
263
353
|
const core = new Set(framework.schema.core.map((property) => property.name));
|
|
264
|
-
const
|
|
354
|
+
const schemaProperties = [...framework.schema.custom, ...Object.values(framework.schema.tools).flat()];
|
|
355
|
+
const applicable = schemaProperties.filter((property) => appliesTo(property, collection.name));
|
|
265
356
|
const applicableNames = new Set(applicable.map((property) => property.name));
|
|
266
|
-
|
|
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)) {
|
|
267
360
|
if (!(property.name in properties)) {
|
|
268
|
-
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})`);
|
|
269
362
|
}
|
|
270
363
|
else if (isEmpty(properties[property.name]) && property.type.toLowerCase() !== 'list') {
|
|
271
|
-
out.warn('property-empty', file, `\`${property.name}\` has no value`);
|
|
364
|
+
out.warn('property-empty', file, `\`${property.name}\` is required but has no value`);
|
|
272
365
|
}
|
|
273
366
|
}
|
|
274
367
|
for (const key of Object.keys(properties)) {
|
|
275
368
|
if (core.has(key) || applicableNames.has(key)) {
|
|
276
369
|
continue;
|
|
277
370
|
}
|
|
278
|
-
const elsewhere =
|
|
371
|
+
const elsewhere = schemaProperties.find((property) => property.name === key);
|
|
279
372
|
if (elsewhere) {
|
|
280
373
|
const scope = elsewhere.appliesTo === 'all' ? 'all' : elsewhere.appliesTo.join(', ');
|
|
281
374
|
out.warn('property-scope', file, `\`${key}\` applies to ${scope}, not ${collection.name}`);
|
|
@@ -284,7 +377,7 @@ function checkBlueprint(framework, blueprint, shapes, out) {
|
|
|
284
377
|
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
378
|
}
|
|
286
379
|
else {
|
|
287
|
-
out.warn('property-unknown', file, `\`${key}\` is
|
|
380
|
+
out.warn('property-unknown', file, `\`${key}\` is in no block of the Properties table; declare it among the custom properties or drop it`);
|
|
288
381
|
}
|
|
289
382
|
}
|
|
290
383
|
for (const property of [...framework.schema.core, ...applicable]) {
|
|
@@ -294,6 +387,11 @@ function checkBlueprint(framework, blueprint, shapes, out) {
|
|
|
294
387
|
const problem = typeProblem(property, properties[property.name]);
|
|
295
388
|
if (problem) {
|
|
296
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(', ')})`);
|
|
297
395
|
}
|
|
298
396
|
}
|
|
299
397
|
if (collection.grouping?.property) {
|
|
@@ -313,8 +411,9 @@ function checkBlueprint(framework, blueprint, shapes, out) {
|
|
|
313
411
|
}
|
|
314
412
|
}
|
|
315
413
|
}
|
|
414
|
+
checkRegions(blueprint.body, file, out);
|
|
316
415
|
const brokenInBody = new Map();
|
|
317
|
-
for (const target of linkTargets(blueprint.body)) {
|
|
416
|
+
for (const target of linkTargets(stripRegions(blueprint.body))) {
|
|
318
417
|
if (isFileTarget(target) && !linkExists(dir, target)) {
|
|
319
418
|
brokenInBody.set(target, (brokenInBody.get(target) ?? 0) + 1);
|
|
320
419
|
}
|
|
@@ -327,38 +426,89 @@ function checkBlueprint(framework, blueprint, shapes, out) {
|
|
|
327
426
|
if (h1 && h1.text !== title && kebab(h1.text) !== kebab(title)) {
|
|
328
427
|
out.warn('title-mismatch', file, `the heading '${h1.text}' differs from title '${title}'`);
|
|
329
428
|
}
|
|
330
|
-
const
|
|
331
|
-
if (!
|
|
429
|
+
const template = variant ? (templates.get(`${collection.name}/${variant.name}`) ?? loadTemplate(framework, variant)) : null;
|
|
430
|
+
if (!template || !variant) {
|
|
332
431
|
return;
|
|
333
432
|
}
|
|
334
433
|
const present = blueprint.sections.map((section) => section.heading.text);
|
|
335
434
|
const presentLower = present.map((text) => text.toLowerCase());
|
|
336
|
-
const
|
|
337
|
-
for (const name of
|
|
435
|
+
const templateLower = template.sections.map((text) => text.toLowerCase());
|
|
436
|
+
for (const name of template.sections) {
|
|
338
437
|
if (!presentLower.includes(name.toLowerCase())) {
|
|
339
|
-
out.warn('section-missing', file, `no \`## ${name}\` (${
|
|
438
|
+
out.warn('section-missing', file, `no \`## ${name}\` (${variant.unit}.${variant.name} declares it; leave it out only if it genuinely does not apply)`);
|
|
340
439
|
}
|
|
341
440
|
}
|
|
342
441
|
if (!collection.framing) {
|
|
343
442
|
for (const name of present) {
|
|
344
|
-
if (!
|
|
345
|
-
out.warn('section-unknown', file, `\`## ${name}\` is not a section of ${
|
|
443
|
+
if (!templateLower.includes(name.toLowerCase())) {
|
|
444
|
+
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
445
|
}
|
|
347
446
|
}
|
|
348
447
|
}
|
|
349
|
-
const order = presentLower.filter((name) =>
|
|
448
|
+
const order = presentLower.filter((name) => templateLower.includes(name)).map((name) => templateLower.indexOf(name));
|
|
350
449
|
for (let index = 1; index < order.length; index += 1) {
|
|
351
450
|
if ((order[index] ?? 0) < (order[index - 1] ?? 0)) {
|
|
352
|
-
out.warn('section-order', file, `sections are not in ${
|
|
451
|
+
out.warn('section-order', file, `sections are not in ${variant.unit}.${variant.name}'s order (${template.sections.join(' → ')})`);
|
|
353
452
|
break;
|
|
354
453
|
}
|
|
355
454
|
}
|
|
356
455
|
}
|
|
456
|
+
function checkCanvases(framework, blueprints, out) {
|
|
457
|
+
const ids = new Set(blueprints.map((blueprint) => blueprintId(blueprint)).filter((id) => id !== null));
|
|
458
|
+
const canvases = listCanvases(framework);
|
|
459
|
+
const canvasIds = new Set(canvases.map((canvas) => canvas.id));
|
|
460
|
+
for (const canvas of canvases) {
|
|
461
|
+
const file = rel(framework.root, canvas.file);
|
|
462
|
+
for (const problem of canvas.problems) {
|
|
463
|
+
out.error('canvas-invalid', file, problem);
|
|
464
|
+
}
|
|
465
|
+
for (const page of canvas.pages) {
|
|
466
|
+
const nodeIds = new Set(page.nodes.map((node) => node.id));
|
|
467
|
+
const frameIds = new Set(page.frames.map((frame) => frame.id));
|
|
468
|
+
for (const node of page.nodes) {
|
|
469
|
+
if (node.blueprint && !ids.has(node.blueprint)) {
|
|
470
|
+
out.error('canvas-node-missing', file, `page ${page.id}, node ${node.id}: no blueprint with id '${node.blueprint}'`);
|
|
471
|
+
}
|
|
472
|
+
if (node.frame && !frameIds.has(node.frame)) {
|
|
473
|
+
out.warn('canvas-frame-missing', file, `page ${page.id}, node ${node.id}: no frame '${node.frame}' on this page`);
|
|
474
|
+
}
|
|
475
|
+
if (node.link?.page && !canvas.pages.some((candidate) => candidate.id === node.link?.page)) {
|
|
476
|
+
out.error('canvas-link-missing', file, `page ${page.id}, node ${node.id}: links to page '${node.link.page}', which this canvas does not have`);
|
|
477
|
+
}
|
|
478
|
+
if (node.link?.canvas && !canvasIds.has(node.link.canvas)) {
|
|
479
|
+
out.error('canvas-link-missing', file, `page ${page.id}, node ${node.id}: links to canvas '${node.link.canvas}', which does not exist`);
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
for (const comment of page.comments) {
|
|
483
|
+
if (comment.node && !page.nodes.some((node) => node.id === comment.node)) {
|
|
484
|
+
out.warn('canvas-comment-orphan', file, `page ${page.id}, comment ${comment.id}: pinned to node '${comment.node}', which the page no longer has`);
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
const endpoint = (id) => (isFrameRef(id) ? frameIds.has(id.slice(FRAME_REF.length)) : nodeIds.has(id));
|
|
488
|
+
for (const edge of page.edges) {
|
|
489
|
+
if (!endpoint(edge.from) || !endpoint(edge.to)) {
|
|
490
|
+
out.error('canvas-edge-missing', file, `page ${page.id}: edge ${edge.from} → ${edge.to} names a node or frame the page does not have`);
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
for (const node of page.nodes) {
|
|
494
|
+
if (node.annotation && !page.edges.some((edge) => edge.from === node.id)) {
|
|
495
|
+
out.warn('canvas-annotation-unpointed', file, `page ${page.id}, annotation ${node.id}: points at nothing; an annotation exists to point`);
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
for (const frame of page.frames) {
|
|
499
|
+
if (frame.frame && !frameIds.has(frame.frame)) {
|
|
500
|
+
out.warn('canvas-frame-missing', file, `page ${page.id}, frame ${frame.id}: attached to frame '${frame.frame}', which the page does not have`);
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
}
|
|
357
506
|
export function checkRoot(framework, blueprints, options) {
|
|
358
507
|
const out = new Collector();
|
|
359
|
-
const
|
|
508
|
+
const templates = checkFramework(framework, out, options);
|
|
360
509
|
if (!options.only) {
|
|
361
510
|
checkLayout(framework, blueprints, out);
|
|
511
|
+
checkCanvases(framework, blueprints, out);
|
|
362
512
|
}
|
|
363
513
|
const seen = new Map();
|
|
364
514
|
for (const blueprint of blueprints) {
|
|
@@ -381,7 +531,8 @@ export function checkRoot(framework, blueprints, options) {
|
|
|
381
531
|
}
|
|
382
532
|
const targets = options.only ? blueprints.filter((blueprint) => options.only?.has(blueprint.path)) : blueprints;
|
|
383
533
|
for (const blueprint of targets) {
|
|
384
|
-
checkBlueprint(framework, blueprint,
|
|
534
|
+
checkBlueprint(framework, blueprint, templates, out);
|
|
535
|
+
checkVocabulary(framework, blueprint, out);
|
|
385
536
|
}
|
|
386
537
|
const findings = out.findings.sort((a, b) => {
|
|
387
538
|
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));
|