eidosmd 0.1.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.
Files changed (95) hide show
  1. package/LICENSE +133 -0
  2. package/NOTICE +15 -0
  3. package/README.md +146 -0
  4. package/browser/index.html +268 -0
  5. package/dist/src/cli.js +5 -0
  6. package/dist/src/commands/agents.js +47 -0
  7. package/dist/src/commands/browser.js +40 -0
  8. package/dist/src/commands/check.js +61 -0
  9. package/dist/src/commands/convert.js +30 -0
  10. package/dist/src/commands/framework.js +76 -0
  11. package/dist/src/commands/index.js +64 -0
  12. package/dist/src/commands/init.js +66 -0
  13. package/dist/src/commands/instructions.js +40 -0
  14. package/dist/src/commands/list.js +73 -0
  15. package/dist/src/commands/new.js +68 -0
  16. package/dist/src/commands/seeds.js +40 -0
  17. package/dist/src/commands/show.js +24 -0
  18. package/dist/src/commands/whoami.js +84 -0
  19. package/dist/src/context.js +35 -0
  20. package/dist/src/core/blueprint.js +129 -0
  21. package/dist/src/core/check.js +407 -0
  22. package/dist/src/core/convert.js +37 -0
  23. package/dist/src/core/framework-markdown.js +233 -0
  24. package/dist/src/core/framework-model.js +48 -0
  25. package/dist/src/core/framework-structured.js +334 -0
  26. package/dist/src/core/framework.js +56 -0
  27. package/dist/src/core/frontmatter.js +116 -0
  28. package/dist/src/core/index-leaf.js +83 -0
  29. package/dist/src/core/markdown.js +100 -0
  30. package/dist/src/core/me.js +89 -0
  31. package/dist/src/core/naming.js +46 -0
  32. package/dist/src/core/root.js +88 -0
  33. package/dist/src/core/scaffold.js +118 -0
  34. package/dist/src/core/seed.js +223 -0
  35. package/dist/src/core/server.js +275 -0
  36. package/dist/src/core/shape.js +26 -0
  37. package/dist/src/output.js +38 -0
  38. package/dist/src/paths.js +28 -0
  39. package/dist/src/program.js +242 -0
  40. package/instructions/authoring.md +50 -0
  41. package/instructions/configuring.md +65 -0
  42. package/instructions/init-required.md +32 -0
  43. package/instructions/overview.md +37 -0
  44. package/instructions/validating.md +33 -0
  45. package/package.json +53 -0
  46. package/standard/EIDOS.md +324 -0
  47. package/standard/seeds/README.md +25 -0
  48. package/standard/seeds/book/Framework.md +87 -0
  49. package/standard/seeds/book/README.md +22 -0
  50. package/standard/seeds/book/_gitignore +4 -0
  51. package/standard/seeds/book/me.md +18 -0
  52. package/standard/seeds/book/roles/README.md +12 -0
  53. package/standard/seeds/book/roles/collaborator.md +16 -0
  54. package/standard/seeds/book/roles/editor.md +16 -0
  55. package/standard/seeds/book/roles/framework-owner.md +16 -0
  56. package/standard/seeds/book/roles/reader.md +16 -0
  57. package/standard/seeds/book/shapes/chapter.full.md +45 -0
  58. package/standard/seeds/book/shapes/chapter.sketch.md +26 -0
  59. package/standard/seeds/book/shapes/frame.market.md +23 -0
  60. package/standard/seeds/book/shapes/frame.premise.md +24 -0
  61. package/standard/seeds/book/shapes/frame.reader.md +23 -0
  62. package/standard/seeds/book/shapes/frame.voice.md +24 -0
  63. package/standard/seeds/research/Framework.md +88 -0
  64. package/standard/seeds/research/README.md +23 -0
  65. package/standard/seeds/research/_gitignore +4 -0
  66. package/standard/seeds/research/me.md +18 -0
  67. package/standard/seeds/research/roles/README.md +12 -0
  68. package/standard/seeds/research/roles/framework-owner.md +16 -0
  69. package/standard/seeds/research/roles/researcher.md +16 -0
  70. package/standard/seeds/research/roles/reviewer.md +16 -0
  71. package/standard/seeds/research/roles/sponsor.md +16 -0
  72. package/standard/seeds/research/shapes/frame.ethics.md +23 -0
  73. package/standard/seeds/research/shapes/frame.method.md +24 -0
  74. package/standard/seeds/research/shapes/frame.prior-work.md +23 -0
  75. package/standard/seeds/research/shapes/frame.question.md +24 -0
  76. package/standard/seeds/research/shapes/investigation.full.md +49 -0
  77. package/standard/seeds/research/shapes/investigation.note.md +26 -0
  78. package/standard/seeds/software/Framework.md +88 -0
  79. package/standard/seeds/software/README.md +24 -0
  80. package/standard/seeds/software/_gitignore +4 -0
  81. package/standard/seeds/software/me.md +18 -0
  82. package/standard/seeds/software/roles/.gitkeep +0 -0
  83. package/standard/seeds/software/roles/README.md +13 -0
  84. package/standard/seeds/software/roles/designer.md +16 -0
  85. package/standard/seeds/software/roles/developer.md +16 -0
  86. package/standard/seeds/software/roles/framework-owner.md +16 -0
  87. package/standard/seeds/software/roles/project-manager.md +18 -0
  88. package/standard/seeds/software/roles/stakeholder.md +16 -0
  89. package/standard/seeds/software/shapes/.gitkeep +0 -0
  90. package/standard/seeds/software/shapes/frame.architecture.md +26 -0
  91. package/standard/seeds/software/shapes/frame.audience.md +26 -0
  92. package/standard/seeds/software/shapes/frame.criteria.md +27 -0
  93. package/standard/seeds/software/shapes/frame.market.md +31 -0
  94. package/standard/seeds/software/shapes/spec.full.md +67 -0
  95. package/standard/seeds/software/shapes/spec.micro.md +32 -0
@@ -0,0 +1,37 @@
1
+ // Converting a markdown root to the form the CLI works in: the same framework
2
+ // written as `Framework.yaml` with the guidance as comments and the index
3
+ // inside it, `Framework.md` and each collection's `index.md` removed. The
4
+ // markdown form's prose (section intros, HTML comments) has no field to land
5
+ // in and is left behind; each collection's own description travels.
6
+ import { existsSync, rmSync, writeFileSync } from 'node:fs';
7
+ import path from 'node:path';
8
+ import { listBlueprints } from './blueprint.js';
9
+ import { loadFramework } from './framework.js';
10
+ import { frameworkToDocument, serializeDocument } from './framework-structured.js';
11
+ import { buildIndexes } from './index-leaf.js';
12
+ export function convertToYaml(framework, dryRun) {
13
+ const yamlFile = path.join(framework.root, '_eidos', 'Framework.yaml');
14
+ const actions = [];
15
+ const removed = [];
16
+ const text = serializeDocument(frameworkToDocument(framework));
17
+ actions.push(`write ${yamlFile}`);
18
+ actions.push(`rm ${framework.file}`);
19
+ removed.push(framework.file);
20
+ for (const collection of framework.collections) {
21
+ const leaf = path.join(framework.root, collection.name, 'index.md');
22
+ if (existsSync(leaf)) {
23
+ actions.push(`rm ${leaf}`);
24
+ removed.push(leaf);
25
+ }
26
+ }
27
+ actions.push(`index ${yamlFile}`);
28
+ if (!dryRun) {
29
+ writeFileSync(yamlFile, text, 'utf8');
30
+ for (const file of removed) {
31
+ rmSync(file, { force: true });
32
+ }
33
+ const converted = loadFramework(framework.root);
34
+ buildIndexes(converted, listBlueprints(converted), { check: false });
35
+ }
36
+ return { written: yamlFile, removed, actions, dryRun };
37
+ }
@@ -0,0 +1,233 @@
1
+ // The markdown framework document, `_eidos/Framework.md`: frontmatter for the
2
+ // version and naming convention, and a body indexing the Top-Level docs, the
3
+ // Collections (each a `###` with Leaf, Flavors, Canvas, and grouping bullets),
4
+ // and the property Schema as two tables.
5
+ import { parseFrontmatter } from './frontmatter.js';
6
+ import { groupingProperty, STANDARD_CORE } from './framework-model.js';
7
+ import { DEFAULT_NAMING, isNaming } from './naming.js';
8
+ import { section, sections, stripInlineCode } from './markdown.js';
9
+ import path from 'node:path';
10
+ const DECLARED_BULLETS = new Set(['leaf', 'flavors', 'canvas']);
11
+ function cleanText(text) {
12
+ return text.replace(/^_+|_+$/g, '').trim();
13
+ }
14
+ function firstParagraph(block) {
15
+ const lines = [];
16
+ for (const raw of block.split('\n')) {
17
+ const line = raw.trim();
18
+ if (line.startsWith('-') || line.startsWith('#')) {
19
+ break;
20
+ }
21
+ if (line === '') {
22
+ if (lines.length > 0) {
23
+ break;
24
+ }
25
+ continue;
26
+ }
27
+ lines.push(line);
28
+ }
29
+ return lines.join(' ');
30
+ }
31
+ function parseLink(text) {
32
+ const match = /\[([^\]]*)\]\(([^)]+)\)/.exec(text);
33
+ if (!match || match[1] === undefined || match[2] === undefined) {
34
+ return null;
35
+ }
36
+ return { title: match[1].trim(), target: match[2].trim() };
37
+ }
38
+ function parseCanvas(value) {
39
+ const plain = stripInlineCode(value).trim();
40
+ if (/^file\b/i.test(plain)) {
41
+ return { mode: 'file' };
42
+ }
43
+ const from = /card\s+from\s+#+\s*(.+?)\s*$/i.exec(plain);
44
+ if (from && from[1]) {
45
+ return { mode: 'card', section: from[1].trim() };
46
+ }
47
+ return { mode: 'card', section: null };
48
+ }
49
+ function flavorFromShape(label, shapeRel) {
50
+ const file = path.posix.basename(shapeRel.replace(/\\/g, '/')).replace(/\.md$/i, '');
51
+ const dot = file.indexOf('.');
52
+ const kind = dot === -1 ? file : file.slice(0, dot);
53
+ const fromFile = dot === -1 ? file : file.slice(dot + 1);
54
+ const name = /\.md$/i.test(label) ? fromFile : label;
55
+ return { name, kind };
56
+ }
57
+ function parseCollections(body) {
58
+ const block = section(body, 2, 'Collections');
59
+ if (block === null) {
60
+ return [];
61
+ }
62
+ const out = [];
63
+ for (const { heading, body: text } of sections(block, 3)) {
64
+ const collection = {
65
+ name: heading.text,
66
+ description: firstParagraph(text),
67
+ framing: out.length === 0,
68
+ leaf: null,
69
+ flavors: [],
70
+ canvas: null,
71
+ grouping: null,
72
+ };
73
+ let inFlavors = false;
74
+ let inGrouping = false;
75
+ for (const line of text.split('\n')) {
76
+ const bullet = /^-\s+\*\*(.+?):\*\*\s*(.*)$/.exec(line);
77
+ if (bullet && bullet[1] !== undefined) {
78
+ const label = bullet[1].trim();
79
+ const rest = (bullet[2] ?? '').trim();
80
+ inFlavors = label.toLowerCase() === 'flavors';
81
+ inGrouping = false;
82
+ if (label.toLowerCase() === 'leaf') {
83
+ collection.leaf = parseLink(rest)?.target ?? null;
84
+ }
85
+ else if (label.toLowerCase() === 'canvas') {
86
+ collection.canvas = parseCanvas(rest);
87
+ }
88
+ else if (!DECLARED_BULLETS.has(label.toLowerCase())) {
89
+ collection.grouping = { label, property: null, groups: [] };
90
+ inGrouping = true;
91
+ }
92
+ continue;
93
+ }
94
+ const nested = /^\s+-\s+(.*)$/.exec(line);
95
+ if (!nested || nested[1] === undefined) {
96
+ continue;
97
+ }
98
+ const item = nested[1].trim();
99
+ if (inFlavors) {
100
+ const flavorLine = /^\[(.+?)\]\((.+?)\)\s*(?:—\s*(.*))?$/.exec(item);
101
+ if (flavorLine && flavorLine[1] && flavorLine[2]) {
102
+ // `(default)` in the description marks the default flavor; it is a flag, not prose.
103
+ const described = (flavorLine[3] ?? '').trim();
104
+ const isDefault = /\(default\)/i.test(described);
105
+ const description = described.replace(/\s*\(default\)\.?$/i, '').replace(/\s*\(default\)/i, '').trim();
106
+ const { name, kind } = flavorFromShape(flavorLine[1].trim(), flavorLine[2].trim());
107
+ collection.flavors.push({
108
+ name,
109
+ kind,
110
+ shape: flavorLine[2].trim(),
111
+ description,
112
+ isDefault,
113
+ });
114
+ }
115
+ }
116
+ else if (inGrouping && collection.grouping) {
117
+ const groupLine = /^\*\*(.+?)\*\*\s*(?:—\s*(.*))?$/.exec(item);
118
+ if (groupLine && groupLine[1]) {
119
+ collection.grouping.groups.push({ name: groupLine[1].trim(), description: cleanText(groupLine[2] ?? '') });
120
+ }
121
+ }
122
+ }
123
+ if (collection.flavors.length > 0 && !collection.flavors.some((flavor) => flavor.isDefault)) {
124
+ const first = collection.flavors[0];
125
+ if (first) {
126
+ first.isDefault = true;
127
+ }
128
+ }
129
+ out.push(collection);
130
+ }
131
+ return out;
132
+ }
133
+ function parseTable(block) {
134
+ const rows = [];
135
+ for (const raw of block.split('\n')) {
136
+ const line = raw.trim();
137
+ if (!line.startsWith('|')) {
138
+ continue;
139
+ }
140
+ const cells = line.replace(/^\|/, '').replace(/\|$/, '').split('|').map((cell) => cell.trim());
141
+ if (cells.length === 0 || cells.every((cell) => /^:?-+:?$/.test(cell) || cell === '')) {
142
+ continue;
143
+ }
144
+ rows.push(cells);
145
+ }
146
+ return rows;
147
+ }
148
+ function parseAppliesTo(value) {
149
+ const plain = stripInlineCode(value).trim();
150
+ if (plain === '' || plain.toLowerCase() === 'all') {
151
+ return 'all';
152
+ }
153
+ return plain.split(/[,/]/).map((name) => name.trim()).filter((name) => name !== '');
154
+ }
155
+ function parseProperties(block, core) {
156
+ if (block === null) {
157
+ return [];
158
+ }
159
+ const out = [];
160
+ for (const cells of parseTable(block)) {
161
+ const name = stripInlineCode(cells[0] ?? '').trim();
162
+ if (name === '' || name.toLowerCase() === 'name') {
163
+ continue;
164
+ }
165
+ const type = (cells[1] ?? 'Text').trim();
166
+ if (core || cells.length < 4) {
167
+ out.push({ name, type, appliesTo: 'all', meaning: (cells[cells.length - 1] ?? '').trim(), core });
168
+ }
169
+ else {
170
+ out.push({ name, type, appliesTo: parseAppliesTo(cells[2] ?? ''), meaning: (cells[3] ?? '').trim(), core });
171
+ }
172
+ }
173
+ return out;
174
+ }
175
+ function parseTopLevel(body) {
176
+ const block = section(body, 2, 'Top-Level');
177
+ if (block === null) {
178
+ return [];
179
+ }
180
+ const out = [];
181
+ for (const raw of block.split('\n')) {
182
+ const bullet = /^-\s+(.*)$/.exec(raw.trim());
183
+ if (!bullet || bullet[1] === undefined) {
184
+ continue;
185
+ }
186
+ const link = parseLink(bullet[1]);
187
+ if (!link) {
188
+ continue;
189
+ }
190
+ const after = bullet[1].slice(bullet[1].indexOf(')') + 1);
191
+ const description = cleanText(after.replace(/^\s*—\s*/, ''));
192
+ out.push({ title: link.title, path: link.target, description });
193
+ }
194
+ return out;
195
+ }
196
+ export function parseFrameworkMarkdown(text, root, file) {
197
+ const { properties, body } = parseFrontmatter(text);
198
+ const declaredNaming = properties?.['naming'];
199
+ let naming = DEFAULT_NAMING;
200
+ let namingError = null;
201
+ if (declaredNaming !== undefined && declaredNaming !== null) {
202
+ if (isNaming(declaredNaming)) {
203
+ naming = declaredNaming;
204
+ }
205
+ else {
206
+ namingError = `naming: ${String(declaredNaming)} is not one of kebab-case, TitleCase, Title Case`;
207
+ }
208
+ }
209
+ 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');
213
+ const core = parseProperties(coreBlock, true);
214
+ const custom = parseProperties(customBlock, false);
215
+ const collections = parseCollections(body);
216
+ for (const collection of collections) {
217
+ if (collection.grouping) {
218
+ collection.grouping.property = groupingProperty(collection, custom);
219
+ }
220
+ }
221
+ return {
222
+ root,
223
+ file,
224
+ format: 'md',
225
+ eidosVersion: version === undefined || version === null ? null : String(version),
226
+ naming,
227
+ namingError,
228
+ topLevel: parseTopLevel(body),
229
+ collections,
230
+ schema: { core: core.length > 0 ? core : STANDARD_CORE, custom },
231
+ problems: [],
232
+ };
233
+ }
@@ -0,0 +1,48 @@
1
+ // The framework model: everything a framework document declares, whichever
2
+ // of `_eidos/Framework.md`, `.yaml`, or `.json` it is. The version and naming
3
+ // convention, the Top-Level docs, the Collections (each with flavors, canvas
4
+ // style, and grouping), and the property Schema.
5
+ import { kebab } from './naming.js';
6
+ export const FRAMEWORK_DIR = '_eidos';
7
+ export function isFrameworkFormat(value) {
8
+ return value === 'md' || value === 'yaml';
9
+ }
10
+ export const FRAMEWORK_FILES = [
11
+ { name: 'Framework.md', format: 'md' },
12
+ { name: 'Framework.yaml', format: 'yaml' },
13
+ { name: 'Framework.yml', format: 'yaml' },
14
+ ];
15
+ export const PROPERTY_TYPES = ['Text', 'List', 'Number', 'Checkbox', 'Date', 'Date & time'];
16
+ export class FrameworkError extends Error {
17
+ constructor(message) {
18
+ super(message);
19
+ this.name = 'FrameworkError';
20
+ }
21
+ }
22
+ // The standard's own five, used when a Framework.md carries no core block.
23
+ export const STANDARD_CORE = [
24
+ { name: 'id', type: 'Text', appliesTo: 'all', meaning: 'Stable, unique, kebab-case identity. Assigned once, never renamed.', core: true },
25
+ { name: 'title', type: 'Text', appliesTo: 'all', meaning: 'Human-readable name.', core: true },
26
+ { name: 'summary', type: 'Text', appliesTo: 'all', meaning: 'One plain line: what this blueprint is. The source for the collection index.', core: true },
27
+ { name: 'flavor', type: 'Text', appliesTo: 'all', meaning: 'Which flavor this blueprint follows. Absent = the collection default.', core: true },
28
+ { name: 'connects_to', type: 'List', appliesTo: 'all', meaning: 'Blueprints this one connects to on the canvas, each a link.', core: true },
29
+ ];
30
+ // `Domains` -> `domain`: the custom property that carries a collection's group,
31
+ // matched by the grouping label's singular against the properties scoped to it.
32
+ export function groupingProperty(collection, custom) {
33
+ if (!collection.grouping) {
34
+ return null;
35
+ }
36
+ const label = kebab(collection.grouping.label);
37
+ const candidates = new Set([label, label.replace(/s$/, ''), label.replace(/ies$/, 'y'), label.replace(/es$/, '')]);
38
+ const scoped = custom.filter((property) => appliesTo(property, collection.name));
39
+ const match = scoped.find((property) => candidates.has(kebab(property.name)));
40
+ return match ? match.name : null;
41
+ }
42
+ export function appliesTo(property, collectionName) {
43
+ if (property.appliesTo === 'all') {
44
+ return true;
45
+ }
46
+ const wanted = collectionName.toLowerCase();
47
+ return property.appliesTo.some((name) => name.toLowerCase() === wanted || kebab(name) === kebab(collectionName));
48
+ }
@@ -0,0 +1,334 @@
1
+ // The YAML framework document, `_eidos/Framework.yaml`: the same model as the
2
+ // markdown form, as fields in the frontmatter's snake_case, for a root that
3
+ // scripts and agents read.
4
+ // Paths are relative to `_eidos/`, as the markdown links are. A structured
5
+ // document also carries the generated index under `index`, one list per
6
+ // collection, which `eidos index` rewrites in place without disturbing
7
+ // anything a person wrote around it.
8
+ import { Document, isMap, isPair, isScalar, parse as parseYaml, parseDocument } from 'yaml';
9
+ import { groupingProperty, STANDARD_CORE, } from './framework-model.js';
10
+ import { DEFAULT_NAMING, isNaming } from './naming.js';
11
+ function isPlain(value) {
12
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
13
+ }
14
+ function text(value) {
15
+ if (value === undefined || value === null) {
16
+ return '';
17
+ }
18
+ return typeof value === 'string' ? value : String(value);
19
+ }
20
+ function list(value) {
21
+ return Array.isArray(value) ? value : [];
22
+ }
23
+ function parseDocumentText(source) {
24
+ try {
25
+ return { value: parseYaml(source), error: null };
26
+ }
27
+ catch (cause) {
28
+ const message = cause instanceof Error ? (cause.message.split('\n')[0] ?? cause.message) : String(cause);
29
+ return { value: null, error: message };
30
+ }
31
+ }
32
+ function readCanvas(value, where, problems) {
33
+ if (value === undefined || value === null) {
34
+ return null;
35
+ }
36
+ if (value === 'file') {
37
+ return { mode: 'file' };
38
+ }
39
+ if (value === 'card') {
40
+ return { mode: 'card', section: null };
41
+ }
42
+ if (isPlain(value)) {
43
+ if (value['mode'] === 'file') {
44
+ return { mode: 'file' };
45
+ }
46
+ if (value['mode'] === 'card') {
47
+ const section = value['section'];
48
+ return { mode: 'card', section: section === undefined || section === null ? null : text(section) };
49
+ }
50
+ }
51
+ problems.push(`${where}: canvas must be file, card, or { mode: card, section: <Section> }`);
52
+ return null;
53
+ }
54
+ function readFlavor(value, where, problems) {
55
+ if (!isPlain(value) || text(value['name']) === '' || text(value['shape']) === '') {
56
+ problems.push(`${where}: every flavor needs a name and a shape`);
57
+ return null;
58
+ }
59
+ const shape = text(value['shape']).replace(/\\/g, '/');
60
+ const file = shape.split('/').pop() ?? shape;
61
+ const stem = file.replace(/\.md$/i, '');
62
+ const dot = stem.indexOf('.');
63
+ return {
64
+ name: text(value['name']),
65
+ kind: dot === -1 ? stem : stem.slice(0, dot),
66
+ shape,
67
+ description: text(value['description']),
68
+ isDefault: value['default'] === true,
69
+ };
70
+ }
71
+ function readCollection(value, index, problems) {
72
+ const where = `collections[${index}]`;
73
+ if (!isPlain(value) || text(value['name']) === '') {
74
+ problems.push(`${where}: a collection needs a name`);
75
+ return null;
76
+ }
77
+ const name = text(value['name']);
78
+ const flavors = list(value['flavors'])
79
+ .map((flavor, position) => readFlavor(flavor, `${where} (${name}) flavors[${position}]`, problems))
80
+ .filter((flavor) => flavor !== null);
81
+ if (flavors.length > 0 && !flavors.some((flavor) => flavor.isDefault)) {
82
+ const first = flavors[0];
83
+ if (first) {
84
+ first.isDefault = true;
85
+ }
86
+ }
87
+ const collection = {
88
+ name,
89
+ description: text(value['description']),
90
+ framing: index === 0,
91
+ leaf: null,
92
+ flavors,
93
+ canvas: readCanvas(value['canvas'], `${where} (${name})`, problems),
94
+ grouping: null,
95
+ };
96
+ const grouping = value['grouping'];
97
+ if (grouping !== undefined && grouping !== null) {
98
+ if (!isPlain(grouping) || text(grouping['label']) === '') {
99
+ problems.push(`${where} (${name}): grouping needs a label`);
100
+ }
101
+ else {
102
+ const property = grouping['property'];
103
+ collection.grouping = {
104
+ label: text(grouping['label']),
105
+ property: property === undefined || property === null || text(property) === '' ? null : text(property),
106
+ groups: list(grouping['groups'])
107
+ .filter((group) => isPlain(group) && text(group['name']) !== '')
108
+ .map((group) => ({ name: text(group['name']), description: text(group['description']) })),
109
+ };
110
+ }
111
+ }
112
+ return collection;
113
+ }
114
+ function readProperty(value, where, core, problems) {
115
+ if (!isPlain(value) || text(value['name']) === '') {
116
+ problems.push(`${where}: a property needs a name`);
117
+ return null;
118
+ }
119
+ let applies = 'all';
120
+ const declared = value['applies_to'];
121
+ if (!core && declared !== undefined && declared !== null && declared !== 'all') {
122
+ if (Array.isArray(declared)) {
123
+ applies = declared.map(text).filter((name) => name !== '');
124
+ }
125
+ else if (typeof declared === 'string') {
126
+ applies = declared
127
+ .split(/[,/]/)
128
+ .map((name) => name.trim())
129
+ .filter((name) => name !== '');
130
+ }
131
+ else {
132
+ problems.push(`${where}: applies_to must be all or a list of collections`);
133
+ }
134
+ }
135
+ return { name: text(value['name']), type: text(value['type']) || 'Text', appliesTo: applies, meaning: text(value['meaning']), core };
136
+ }
137
+ export function readEmbeddedIndex(value) {
138
+ if (!isPlain(value)) {
139
+ return null;
140
+ }
141
+ const out = {};
142
+ for (const [collection, entries] of Object.entries(value)) {
143
+ out[collection] = list(entries)
144
+ .filter(isPlain)
145
+ .map((entry) => {
146
+ const item = {
147
+ id: entry['id'] === undefined || entry['id'] === null ? null : text(entry['id']),
148
+ title: text(entry['title']),
149
+ summary: entry['summary'] === undefined || entry['summary'] === null || text(entry['summary']) === '' ? null : text(entry['summary']),
150
+ path: text(entry['path']),
151
+ };
152
+ if (entry['group'] !== undefined && entry['group'] !== null) {
153
+ item.group = text(entry['group']);
154
+ }
155
+ return item;
156
+ });
157
+ }
158
+ return out;
159
+ }
160
+ export function parseFrameworkStructured(source, root, file) {
161
+ const problems = [];
162
+ const { value, error } = parseDocumentText(source);
163
+ const document = isPlain(value) ? value : {};
164
+ if (error) {
165
+ problems.push(`not valid YAML: ${error}`);
166
+ }
167
+ else if (!isPlain(value)) {
168
+ problems.push('the document must be a mapping of fields');
169
+ }
170
+ let naming = DEFAULT_NAMING;
171
+ let namingError = null;
172
+ const declaredNaming = document['naming'];
173
+ if (declaredNaming !== undefined && declaredNaming !== null) {
174
+ if (isNaming(declaredNaming)) {
175
+ naming = declaredNaming;
176
+ }
177
+ else {
178
+ namingError = `naming: ${text(declaredNaming)} is not one of kebab-case, TitleCase, Title Case`;
179
+ }
180
+ }
181
+ const collections = list(document['collections'])
182
+ .map((collection, index) => readCollection(collection, index, problems))
183
+ .filter((collection) => collection !== null);
184
+ if (!Array.isArray(document['collections']) && !error && isPlain(value)) {
185
+ problems.push('collections must be a list; the first is the framing collection');
186
+ }
187
+ const schema = isPlain(document['schema']) ? document['schema'] : {};
188
+ const core = list(schema['core'])
189
+ .map((property, index) => readProperty(property, `schema.core[${index}]`, true, problems))
190
+ .filter((property) => property !== null);
191
+ const custom = list(schema['custom'])
192
+ .map((property, index) => readProperty(property, `schema.custom[${index}]`, false, problems))
193
+ .filter((property) => property !== null);
194
+ for (const collection of collections) {
195
+ if (collection.grouping && collection.grouping.property === null) {
196
+ collection.grouping.property = groupingProperty(collection, custom);
197
+ }
198
+ }
199
+ const topLevel = list(document['top_level'])
200
+ .filter((doc) => isPlain(doc) && text(doc['path']) !== '')
201
+ .map((doc) => ({ title: text(doc['title']) || text(doc['path']), path: text(doc['path']), description: text(doc['description']) }));
202
+ const version = document['eidos_version'];
203
+ return {
204
+ root,
205
+ file,
206
+ format: 'yaml',
207
+ eidosVersion: version === undefined || version === null ? null : text(version),
208
+ naming,
209
+ namingError,
210
+ topLevel,
211
+ collections,
212
+ schema: { core: core.length > 0 ? core : STANDARD_CORE, custom },
213
+ problems,
214
+ };
215
+ }
216
+ // The document form of a framework: what `eidos framework --json` prints and
217
+ // what a YAML file holds, without the generated index.
218
+ // Keys land in the order a reader expects: version and naming, then the
219
+ // top-level docs, the collections, and the schema; `index` is added last by
220
+ // `eidos index`.
221
+ export function frameworkToDocument(framework) {
222
+ const document = {};
223
+ if (framework.eidosVersion !== null) {
224
+ document.eidos_version = framework.eidosVersion;
225
+ }
226
+ document.naming = framework.naming;
227
+ if (framework.topLevel.length > 0) {
228
+ document.top_level = framework.topLevel.map((doc) => {
229
+ const entry = { title: doc.title, path: doc.path };
230
+ if (doc.description) {
231
+ entry.description = doc.description;
232
+ }
233
+ return entry;
234
+ });
235
+ }
236
+ document.collections = framework.collections.map((collection) => {
237
+ const entry = { name: collection.name };
238
+ if (collection.description) {
239
+ entry.description = collection.description;
240
+ }
241
+ if (collection.canvas) {
242
+ if (collection.canvas.mode === 'file') {
243
+ entry.canvas = 'file';
244
+ }
245
+ else if (collection.canvas.section === null) {
246
+ entry.canvas = 'card';
247
+ }
248
+ else {
249
+ entry.canvas = { mode: 'card', section: collection.canvas.section };
250
+ }
251
+ }
252
+ entry.flavors = collection.flavors.map((flavor) => {
253
+ const item = { name: flavor.name, shape: flavor.shape };
254
+ if (flavor.description) {
255
+ item.description = flavor.description;
256
+ }
257
+ if (flavor.isDefault) {
258
+ item.default = true;
259
+ }
260
+ return item;
261
+ });
262
+ if (collection.grouping) {
263
+ entry.grouping = { label: collection.grouping.label };
264
+ if (collection.grouping.property) {
265
+ entry.grouping.property = collection.grouping.property;
266
+ }
267
+ if (collection.grouping.groups.length > 0) {
268
+ entry.grouping.groups = collection.grouping.groups.map((group) => {
269
+ const item = { name: group.name };
270
+ if (group.description) {
271
+ item.description = group.description;
272
+ }
273
+ return item;
274
+ });
275
+ }
276
+ }
277
+ return entry;
278
+ });
279
+ document.schema = {
280
+ core: framework.schema.core.map(({ name, type, meaning }) => ({ name, type, meaning })),
281
+ custom: framework.schema.custom.map(({ name, type, appliesTo: applies, meaning }) => ({ name, type, applies_to: applies, meaning })),
282
+ };
283
+ return document;
284
+ }
285
+ const YAML_GUIDANCE = {
286
+ eidos_version: ' The Eidos version this framework targets; migrate reads and bumps it.',
287
+ naming: ' How files, folders, and links are named: kebab-case | TitleCase | Title Case. Settled once.',
288
+ top_level: ' The top-level documents, README first: one entry per one-of-a-kind doc (a Roadmap, a Vision).',
289
+ collections: " The collections, the first of them the framing one. Each declares its flavors (one default), how a canvas draws it, and its grouping.\n Add a group under `grouping.groups` with a one-line description; add a flavor with a shape file in shapes/.",
290
+ schema: ' The property contract. `core` is the standard\'s and moves with eidos_version; `custom` is yours: name, type (Text | List | Number | Checkbox | Date | Date & time), applies_to (all or a list), meaning.',
291
+ index: ' Generated by `eidos index`; never hand-edited. One list per collection, in the order the markdown index would use.',
292
+ };
293
+ // A YAML document a person will edit gets the guidance the markdown seed
294
+ // keeps as prose, as comments above each top-level key.
295
+ function annotateYaml(document) {
296
+ if (!isMap(document.contents)) {
297
+ return;
298
+ }
299
+ document.commentBefore = ' The framework document: the structure this root is written in. Fields are documented in `eidos standard`.';
300
+ document.contents.items.forEach((pair, index) => {
301
+ if (!isPair(pair) || !isScalar(pair.key)) {
302
+ return;
303
+ }
304
+ const guidance = YAML_GUIDANCE[String(pair.key.value)];
305
+ if (guidance !== undefined) {
306
+ pair.key.commentBefore = guidance;
307
+ pair.key.spaceBefore = index > 0;
308
+ }
309
+ });
310
+ }
311
+ export function serializeDocument(document) {
312
+ const yamlDocument = new Document(document);
313
+ annotateYaml(yamlDocument);
314
+ return yamlDocument.toString({ lineWidth: 0 });
315
+ }
316
+ // Rewrite only the `index` key of a YAML document, keeping every other line,
317
+ // comment, and style a person chose. Returns the new text.
318
+ export function withEmbeddedIndex(source, index) {
319
+ const document = parseDocument(source);
320
+ // A scalar key node carries the guidance comment; on an existing key `set`
321
+ // keeps the pair (and whatever comment it already has) and swaps the value.
322
+ const key = document.createNode('index');
323
+ if (!document.has('index') && isScalar(key)) {
324
+ key.commentBefore = YAML_GUIDANCE['index'] ?? null;
325
+ key.spaceBefore = true;
326
+ }
327
+ document.set(key, index);
328
+ return document.toString({ lineWidth: 0 });
329
+ }
330
+ // The `index` a YAML document currently holds, or null.
331
+ export function embeddedIndexOf(source) {
332
+ const { value } = parseDocumentText(source);
333
+ return isPlain(value) ? readEmbeddedIndex(value['index']) : null;
334
+ }