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.
Files changed (107) hide show
  1. package/README.md +50 -29
  2. package/browser/dist/assets/index-Cc3cNWHY.css +1 -0
  3. package/browser/dist/assets/index-DQgCQRa5.js +46 -0
  4. package/browser/dist/favicon.svg +5 -0
  5. package/browser/dist/index.html +15 -0
  6. package/browser/dist/mark.svg +4 -0
  7. package/dist/src/cli.js +7 -0
  8. package/dist/src/commands/agents.js +1 -1
  9. package/dist/src/commands/canvas.js +77 -0
  10. package/dist/src/commands/check.js +10 -4
  11. package/dist/src/commands/configure.js +201 -0
  12. package/dist/src/commands/framework.js +37 -7
  13. package/dist/src/commands/index.js +4 -4
  14. package/dist/src/commands/init.js +5 -0
  15. package/dist/src/commands/instructions.js +1 -1
  16. package/dist/src/commands/list.js +8 -8
  17. package/dist/src/commands/migrate.js +32 -0
  18. package/dist/src/commands/new.js +3 -3
  19. package/dist/src/commands/property.js +125 -0
  20. package/dist/src/commands/seeds.js +5 -5
  21. package/dist/src/commands/setup.js +131 -0
  22. package/dist/src/commands/version.js +44 -0
  23. package/dist/src/commands/whoami.js +4 -4
  24. package/dist/src/context.js +5 -5
  25. package/dist/src/core/blueprint.js +16 -11
  26. package/dist/src/core/canvas-schema.js +148 -0
  27. package/dist/src/core/canvas.js +732 -0
  28. package/dist/src/core/check.js +221 -70
  29. package/dist/src/core/convert.js +7 -6
  30. package/dist/src/core/edits.js +1381 -0
  31. package/dist/src/core/framework-markdown.js +119 -34
  32. package/dist/src/core/framework-model.js +62 -10
  33. package/dist/src/core/framework-structured.js +222 -47
  34. package/dist/src/core/framework.js +13 -13
  35. package/dist/src/core/frontmatter.js +61 -1
  36. package/dist/src/core/git.js +84 -0
  37. package/dist/src/core/index-leaf.js +2 -2
  38. package/dist/src/core/links.js +87 -0
  39. package/dist/src/core/markdown.js +16 -9
  40. package/dist/src/core/me.js +16 -8
  41. package/dist/src/core/migrate.js +319 -0
  42. package/dist/src/core/naming.js +1 -1
  43. package/dist/src/core/regions.js +117 -0
  44. package/dist/src/core/root.js +2 -2
  45. package/dist/src/core/scaffold.js +33 -27
  46. package/dist/src/core/seed.js +187 -71
  47. package/dist/src/core/server.js +1410 -53
  48. package/dist/src/core/settings.js +232 -0
  49. package/dist/src/core/store.js +315 -0
  50. package/dist/src/core/template.js +32 -0
  51. package/dist/src/core/versions.js +84 -0
  52. package/dist/src/output.js +4 -1
  53. package/dist/src/program.js +421 -41
  54. package/instructions/authoring.md +15 -12
  55. package/instructions/configuring.md +81 -34
  56. package/instructions/init-required.md +4 -4
  57. package/instructions/overview.md +21 -9
  58. package/instructions/validating.md +9 -6
  59. package/package.json +21 -12
  60. package/standard/EIDOS.md +142 -259
  61. package/standard/seeds/README.md +12 -16
  62. package/standard/seeds/book/Framework.yaml +61 -0
  63. package/standard/seeds/book/README.md +10 -5
  64. package/standard/seeds/book/_gitignore +9 -3
  65. package/standard/seeds/book/me.md +1 -1
  66. package/standard/seeds/book/roles/README.md +3 -3
  67. package/standard/seeds/book/roles/framework-owner.md +2 -2
  68. package/standard/seeds/book/{shapes → templates}/chapter.full.md +0 -8
  69. package/standard/seeds/book/{shapes → templates}/chapter.sketch.md +0 -7
  70. package/standard/seeds/book/{shapes → templates}/frame.market.md +0 -6
  71. package/standard/seeds/book/templates/frame.premise.md +17 -0
  72. package/standard/seeds/book/{shapes → templates}/frame.reader.md +0 -6
  73. package/standard/seeds/book/{shapes → templates}/frame.voice.md +0 -7
  74. package/standard/seeds/research/Framework.yaml +61 -0
  75. package/standard/seeds/research/README.md +10 -5
  76. package/standard/seeds/research/_gitignore +9 -3
  77. package/standard/seeds/research/me.md +1 -1
  78. package/standard/seeds/research/roles/README.md +3 -3
  79. package/standard/seeds/research/roles/framework-owner.md +2 -2
  80. package/standard/seeds/research/{shapes → templates}/frame.ethics.md +0 -6
  81. package/standard/seeds/research/{shapes → templates}/frame.method.md +0 -7
  82. package/standard/seeds/research/{shapes → templates}/frame.prior-work.md +0 -6
  83. package/standard/seeds/research/{shapes → templates}/frame.question.md +0 -7
  84. package/standard/seeds/research/{shapes → templates}/investigation.full.md +0 -8
  85. package/standard/seeds/research/{shapes → templates}/investigation.note.md +0 -7
  86. package/standard/seeds/software/Framework.yaml +62 -0
  87. package/standard/seeds/software/README.md +7 -6
  88. package/standard/seeds/software/_gitignore +9 -3
  89. package/standard/seeds/software/me.md +1 -1
  90. package/standard/seeds/software/roles/README.md +3 -3
  91. package/standard/seeds/software/roles/framework-owner.md +2 -2
  92. package/standard/seeds/software/roles/project-manager.md +2 -2
  93. package/standard/seeds/software/roles/stakeholder.md +1 -1
  94. package/standard/seeds/software/{shapes → templates}/frame.architecture.md +0 -7
  95. package/standard/seeds/software/{shapes → templates}/frame.audience.md +1 -8
  96. package/standard/seeds/software/{shapes → templates}/frame.criteria.md +0 -8
  97. package/standard/seeds/software/{shapes → templates}/frame.market.md +0 -8
  98. package/standard/seeds/software/{shapes → templates}/spec.full.md +0 -8
  99. package/standard/seeds/software/{shapes → templates}/spec.micro.md +0 -9
  100. package/browser/index.html +0 -268
  101. package/dist/src/commands/convert.js +0 -30
  102. package/dist/src/core/shape.js +0 -26
  103. package/standard/seeds/book/Framework.md +0 -87
  104. package/standard/seeds/book/shapes/frame.premise.md +0 -24
  105. package/standard/seeds/research/Framework.md +0 -88
  106. package/standard/seeds/software/Framework.md +0 -88
  107. /package/standard/seeds/software/{shapes → templates}/.gitkeep +0 -0
@@ -1,13 +1,15 @@
1
- // Scaffolding one blueprint: frontmatter generated from the properties that
2
- // apply to its collection, body rendered from its flavor's shape, filename in
3
- // the framework's convention with a permanent kebab-case id inside. Born
1
+ // Scaffolding one blueprint: frontmatter generated from the required
2
+ // properties that apply to its collection plus any optional one given a
3
+ // value, body rendered from its variant's template, filename in the
4
+ // framework's convention with a permanent kebab-case id inside. Born
4
5
  // conforming; the prose is still the owner's.
5
6
  import { existsSync } from 'node:fs';
6
7
  import path from 'node:path';
7
8
  import { blueprintId } from './blueprint.js';
8
- import { defaultFlavor, findCollection, findFlavor, propertiesFor } from './framework.js';
9
- import { convert, isKebab, kebab } from './naming.js';
10
- import { loadShape } from './shape.js';
9
+ import { optionProblem } from './check.js';
10
+ import { defaultVariant, findCollection, findVariant, propertiesFor } from './framework.js';
11
+ import { convert, kebab } from './naming.js';
12
+ import { loadTemplate } from './template.js';
11
13
  import { blankBlueprint } from './seed.js';
12
14
  export class ScaffoldError extends Error {
13
15
  constructor(message) {
@@ -15,7 +17,8 @@ export class ScaffoldError extends Error {
15
17
  this.name = 'ScaffoldError';
16
18
  }
17
19
  }
18
- function coerce(type, raw) {
20
+ // A value typed on a command line, as the property's declared type holds it.
21
+ export function coerce(type, raw) {
19
22
  switch (type.toLowerCase()) {
20
23
  case 'list':
21
24
  return raw
@@ -38,24 +41,24 @@ export function scaffoldBlueprint(framework, existing, options) {
38
41
  const known = framework.collections.map((candidate) => candidate.name).join(', ');
39
42
  throw new ScaffoldError(`no collection '${options.collection}' in this framework (declared: ${known})`);
40
43
  }
41
- const flavor = options.flavor === null ? defaultFlavor(collection) : findFlavor(collection, options.flavor);
42
- if (!flavor) {
43
- const known = collection.flavors.map((candidate) => candidate.name).join(', ');
44
- throw new ScaffoldError(options.flavor === null
45
- ? `collection '${collection.name}' declares no flavors`
46
- : `no flavor '${options.flavor}' in '${collection.name}' (declared: ${known})`);
44
+ const variant = options.variant === null ? defaultVariant(collection) : findVariant(collection, options.variant);
45
+ if (!variant) {
46
+ const known = collection.variants.map((candidate) => candidate.name).join(', ');
47
+ throw new ScaffoldError(options.variant === null
48
+ ? `collection '${collection.name}' declares no variants`
49
+ : `no variant '${options.variant}' in '${collection.name}' (declared: ${known})`);
47
50
  }
48
- const shape = loadShape(framework, flavor);
51
+ const shape = loadTemplate(framework, variant);
49
52
  if (!shape) {
50
- throw new ScaffoldError(`flavor '${flavor.name}' points at a shape that does not exist: _eidos/${flavor.shape}`);
53
+ throw new ScaffoldError(`variant '${variant.name}' points at a template that does not exist: .eidos/${variant.template}`);
51
54
  }
52
55
  const title = options.title.trim();
53
56
  if (title === '') {
54
57
  throw new ScaffoldError('a blueprint needs a title');
55
58
  }
56
59
  const id = options.id ?? kebab(title);
57
- if (!isKebab(id)) {
58
- throw new ScaffoldError(`id '${id}' is not kebab-case`);
60
+ if (/\s/.test(id)) {
61
+ throw new ScaffoldError(`id '${id}' has whitespace in it; an id is one token, in any form`);
59
62
  }
60
63
  const clash = existing.find((blueprint) => blueprintId(blueprint) === id);
61
64
  if (clash) {
@@ -67,7 +70,7 @@ export function scaffoldBlueprint(framework, existing, options) {
67
70
  const declared = collection.grouping?.groups.find((candidate) => candidate.name.toLowerCase() === options.group?.toLowerCase() || kebab(candidate.name) === kebab(options.group ?? ''));
68
71
  group = declared ? declared.name : convert(options.group, framework.naming);
69
72
  if (!declared && collection.grouping && collection.grouping.groups.length > 0) {
70
- notes.push(`group '${group}' is not declared under ${collection.grouping.label} in Framework.md; add a bullet for it`);
73
+ notes.push(`group '${group}' is not declared under ${collection.grouping.label} in the framework document; add it there`);
71
74
  }
72
75
  if (!collection.grouping) {
73
76
  notes.push(`collection '${collection.name}' declares no grouping; the sub-folder alone groups this blueprint`);
@@ -83,23 +86,26 @@ export function scaffoldBlueprint(framework, existing, options) {
83
86
  id,
84
87
  title,
85
88
  summary: options.summary ?? '',
86
- flavor: flavor.name,
87
- status: 'Draft',
89
+ variant: variant.name,
88
90
  date_created: options.date,
89
91
  date_modified: options.date,
90
92
  };
91
93
  if (group !== null && collection.grouping?.property) {
92
94
  values[collection.grouping.property] = group;
93
95
  }
94
- const types = new Map(propertiesFor(framework, collection.name).map((property) => [property.name, property.type]));
96
+ const declared = new Map(propertiesFor(framework, collection.name).map((property) => [property.name, property]));
97
+ const types = new Map([...declared].map(([name, property]) => [name, property.type]));
95
98
  for (const [key, raw] of Object.entries(options.set)) {
96
- const type = types.get(key);
97
- if (type === undefined) {
98
- notes.push(`property '${key}' is not in the Schema for ${collection.name}; written anyway`);
99
+ const property = declared.get(key);
100
+ if (property === undefined) {
101
+ notes.push(`property '${key}' is not in the Properties table for ${collection.name}; written anyway`);
99
102
  }
100
- values[key] = coerce(type ?? 'Text', raw);
103
+ values[key] = coerce(property?.type ?? 'Text', raw);
104
+ const offList = property ? optionProblem(property, values[key]) : null;
105
+ if (offList)
106
+ notes.push(`'${key}: ${offList.off.join(', ')}' is not one of its options (${offList.options.join(', ')}); written anyway, and check reports it`);
101
107
  }
102
- let content = blankBlueprint(framework, collection.name, values, shape.template, title);
108
+ let content = blankBlueprint(framework, collection.name, values, shape.text, title);
103
109
  const extra = Object.keys(options.set).filter((key) => !types.has(key));
104
110
  if (extra.length > 0) {
105
111
  const lines = extra.map((key) => `${key}: ${String(values[key])}`).join('\n');
@@ -110,7 +116,7 @@ export function scaffoldBlueprint(framework, existing, options) {
110
116
  rel: path.relative(framework.root, file).split(path.sep).join('/'),
111
117
  content,
112
118
  collection,
113
- flavor,
119
+ variant,
114
120
  group,
115
121
  id,
116
122
  notes,
@@ -1,19 +1,21 @@
1
- // Installing a seed: copy a starting framework into a new root's `_eidos/`,
2
- // write it as Framework.yaml with its naming convention and starting groups,
3
- // scaffold a folder per collection, and drop one blank blueprint per framing
4
- // flavor.
1
+ // Installing a seed: copy a starting framework into a new root's `.eidos/`,
2
+ // set the naming convention, the starting groups, and the folder names in
3
+ // its Framework.yaml (the seed's own comments kept), scaffold every declared
4
+ // folder (a collection's with its groups, an assets folder empty), and drop
5
+ // one blank blueprint per framing variant.
5
6
  // It writes no prose: the README's one-liner, each group's description, and
6
7
  // every scaffolded blueprint's summary and body stay the owner's.
7
8
  import { cpSync, existsSync, mkdirSync, readdirSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs';
8
9
  import path from 'node:path';
9
10
  import { formatFrontmatter } from './frontmatter.js';
10
11
  import { parseFramework, propertiesFor } from './framework.js';
11
- import { frameworkToDocument, serializeDocument } from './framework-structured.js';
12
12
  import { buildIndexes } from './index-leaf.js';
13
13
  import { listBlueprints } from './blueprint.js';
14
14
  import { convert, kebab, titleize } from './naming.js';
15
- import { renderShape } from './shape.js';
16
- const GROUP_PROMPT = '_(one line on what belongs here)_';
15
+ import { renderTemplate } from './template.js';
16
+ import { isCollection, TOOL_KEY } from './framework-model.js';
17
+ import { DEFAULT_CANVAS_HINTS } from './canvas.js';
18
+ import { isMap, isScalar, isSeq, parseDocument } from 'yaml';
17
19
  const GITIGNORE_SHIPPED = '_gitignore';
18
20
  export function listSeeds(seedsDir) {
19
21
  if (!existsSync(seedsDir)) {
@@ -22,12 +24,12 @@ export function listSeeds(seedsDir) {
22
24
  return readdirSync(seedsDir)
23
25
  .sort()
24
26
  .map((name) => path.join(seedsDir, name))
25
- .filter((dir) => existsSync(path.join(dir, 'Framework.md')))
27
+ .filter((dir) => existsSync(path.join(dir, 'Framework.yaml')))
26
28
  .map((dir) => ({ name: path.basename(dir), dir, framework: readSeedFramework(dir) }));
27
29
  }
28
30
  function readSeedFramework(dir) {
29
- const file = path.join(dir, 'Framework.md');
30
- return parseFramework(readFileSync(file, 'utf8'), 'md', dir, file);
31
+ const file = path.join(dir, 'Framework.yaml');
32
+ return parseFramework(readFileSync(file, 'utf8'), 'yaml', dir, file);
31
33
  }
32
34
  export class InstallError extends Error {
33
35
  constructor(message) {
@@ -73,105 +75,219 @@ class Writer {
73
75
  }
74
76
  }
75
77
  }
76
- function setNaming(text, naming) {
77
- if (/^naming:.*$/m.test(text)) {
78
- return text.replace(/^naming:.*$/m, `naming: ${naming}`);
78
+ const mapsIn = (value) => (isSeq(value) ? value.items.filter((item) => isMap(item)) : []);
79
+ const nameOf = (map) => String(map.get('name') ?? '');
80
+ // The `folders` list as the seed wrote it (`collections` in a seed older than 5.3.0).
81
+ const foldersOf = (document) => document.get('folders') ?? document.get('collections');
82
+ // The seed's folders and the properties scoped to its collections, renamed
83
+ // into the root's convention; every other key, and every comment, stays as
84
+ // the seed wrote it.
85
+ function renameFolders(document, renames) {
86
+ for (const folder of mapsIn(foldersOf(document))) {
87
+ const to = renames.get(nameOf(folder));
88
+ if (to !== undefined)
89
+ folder.set('name', to);
90
+ }
91
+ const properties = document.get('properties');
92
+ if (!isMap(properties))
93
+ return;
94
+ const blocks = [properties.get('custom'), ...(isMap(properties.get('tools')) ? properties.get('tools').items.map((pair) => pair.value) : [])];
95
+ for (const block of blocks) {
96
+ for (const property of mapsIn(block)) {
97
+ const applies = property.get('applies_to');
98
+ if (!isSeq(applies))
99
+ continue;
100
+ for (const item of applies.items) {
101
+ if (isScalar(item)) {
102
+ const to = renames.get(String(item.value));
103
+ if (to !== undefined)
104
+ item.value = to;
105
+ }
106
+ }
107
+ }
79
108
  }
80
- return text.replace(/^---\n/, `---\nnaming: ${naming}\n`);
81
109
  }
82
- // The grouping bullet's placeholder becomes one bullet per starting group, each
83
- // in the framework's convention so a grouping property's value can match its folder.
84
- function setGroups(text, label, groups) {
85
- const escaped = label.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
86
- const pattern = new RegExp(`^-\\s+\\*\\*${escaped}:\\*\\*.*$`, 'm');
87
- const bullets = groups.map((group) => ` - **${group}** — ${GROUP_PROMPT}`).join('\n');
88
- return text.replace(pattern, `- **${label}:**\n${bullets}`);
110
+ // The canvas hint is the one field the CLI prescribes; a fresh root gets the
111
+ // legend as its default palette on the seed's own status and type.
112
+ function applyDefaultCanvasHints(document) {
113
+ const properties = document.get('properties');
114
+ if (!isMap(properties))
115
+ return;
116
+ for (const property of mapsIn(properties.get('custom'))) {
117
+ const hint = DEFAULT_CANVAS_HINTS[nameOf(property)];
118
+ if (!hint || property.has(TOOL_KEY))
119
+ continue;
120
+ // a hint is a map of maps; a seed writes its rows in flow style, which would put all of it on one line
121
+ property.flow = false;
122
+ property.set(TOOL_KEY, document.createNode({ canvas: hint }));
123
+ }
89
124
  }
90
- function renameCollection(text, from, to) {
125
+ // Comments name the seed's collections too; a word-bounded rename follows them.
126
+ function renameInText(text, from, to) {
91
127
  const escaped = from.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
92
128
  return text.replace(new RegExp(`(?<![\\w-])${escaped}(?!\\w)`, 'g'), to);
93
129
  }
94
- // A blank blueprint: every property that applies to the collection, with the
95
- // values given and `[]` for an empty list, then the flavor's shape as the body.
130
+ // A value worth writing: anything but nothing, an empty string, or an empty list.
131
+ function hasValue(value) {
132
+ return value !== undefined && value !== null && value !== '' && !(Array.isArray(value) && value.length === 0);
133
+ }
134
+ // A blank blueprint: every required property that applies to the collection
135
+ // (with the value given, else blank, `[]` for a list), every optional one
136
+ // that was given a value, then the variant's template as the body. An
137
+ // optional property left out is not a gap, so the file carries nothing it
138
+ // doesn't need.
96
139
  export function blankBlueprint(framework, collectionName, values, shapeTemplate, title) {
97
- const entries = propertiesFor(framework, collectionName).map((property) => {
140
+ const entries = [];
141
+ for (const property of propertiesFor(framework, collectionName)) {
98
142
  const value = values[property.name];
99
- if ((value === undefined || value === null || value === '') && property.type.toLowerCase() === 'list') {
100
- return [property.name, []];
143
+ if (hasValue(value)) {
144
+ entries.push([property.name, value]);
101
145
  }
102
- return [property.name, value ?? ''];
103
- });
104
- return formatFrontmatter(entries) + '\n' + renderShape(shapeTemplate, title);
146
+ else if (property.required) {
147
+ entries.push([property.name, property.type.toLowerCase() === 'list' ? [] : '']);
148
+ }
149
+ }
150
+ return formatFrontmatter(entries) + '\n' + renderTemplate(shapeTemplate, title);
105
151
  }
106
152
  export function installSeed(options) {
107
153
  const seedDir = path.join(options.seedsDir, options.seed);
108
- const frameworkSrc = path.join(seedDir, 'Framework.md');
154
+ const frameworkSrc = path.join(seedDir, 'Framework.yaml');
109
155
  if (!existsSync(frameworkSrc)) {
110
- throw new InstallError(`no seed '${options.seed}' under ${options.seedsDir} (needs a Framework.md)`);
156
+ throw new InstallError(`no seed '${options.seed}' under ${options.seedsDir} (needs a Framework.yaml)`);
111
157
  }
112
158
  const root = path.resolve(options.root);
113
- if (existsSync(path.join(root, '_eidos'))) {
114
- throw new InstallError(`${root}/_eidos already exists: that is a root; use migrate`);
159
+ if (existsSync(path.join(root, '.eidos'))) {
160
+ throw new InstallError(`${root}/.eidos already exists: that is a root; use migrate`);
115
161
  }
116
- let text = readFileSync(frameworkSrc, 'utf8');
117
- const seedFramework = parseFramework(text, 'md', seedDir, frameworkSrc);
162
+ const text = readFileSync(frameworkSrc, 'utf8');
163
+ const seedFramework = parseFramework(text, 'yaml', seedDir, frameworkSrc);
164
+ const document = parseDocument(text);
118
165
  if (seedFramework.collections.length === 0) {
119
166
  throw new InstallError(`${frameworkSrc} declares no collections`);
120
167
  }
121
168
  const writer = new Writer(options.dryRun);
122
169
  const naming = options.naming;
123
- // 1. the framework itself. The seed's README is the root's visible door, so it
124
- // moves to the root; roles/, shapes/, me.md, and .gitignore travel as they are.
125
- writer.copyTree(seedDir, path.join(root, '_eidos'));
126
- writer.remove(path.join(root, '_eidos', 'README.md'));
170
+ // The framing collection is the recommendation, not the standard's rule: an
171
+ // owner who keeps the framing elsewhere leaves it out, templates and all.
172
+ const leftOut = options.framing === false ? seedFramework.collections.filter((collection) => collection.framing) : [];
173
+ const installing = seedFramework.collections.filter((collection) => !leftOut.includes(collection));
174
+ if (installing.length === 0) {
175
+ throw new InstallError(`'${options.seed}' has only its framing collection; nothing would be left to install`);
176
+ }
177
+ // 1. the framework itself. The seed's README is the root's visible door, and so
178
+ // is any other top-level doc the seed declares (the software seed's
179
+ // Vocabulary), so those move to the root; roles/, templates/, me.md, and
180
+ // .gitignore travel as they are.
181
+ const topLevelDocs = seedFramework.topLevel.filter((doc) => /^\.\.\/[^/]+\.md$/.test(doc.path) && doc.path !== '../README.md').map((doc) => ({ title: doc.title, file: doc.path.slice(3) }));
182
+ writer.copyTree(seedDir, path.join(root, '.eidos'));
183
+ writer.remove(path.join(root, '.eidos', 'README.md'));
184
+ for (const doc of topLevelDocs) {
185
+ writer.remove(path.join(root, '.eidos', doc.file));
186
+ }
127
187
  // npm never packs a file named .gitignore, so a seed ships it as _gitignore.
128
188
  if (existsSync(path.join(seedDir, GITIGNORE_SHIPPED))) {
129
- writer.rename(path.join(root, '_eidos', GITIGNORE_SHIPPED), path.join(root, '_eidos', '.gitignore'));
189
+ writer.rename(path.join(root, '.eidos', GITIGNORE_SHIPPED), path.join(root, '.eidos', '.gitignore'));
190
+ }
191
+ // 2. the framework document: naming, starting groups, and each collection in
192
+ // convention, edited in place so the seed's comments travel with it.
193
+ document.set('naming', naming);
194
+ for (const collection of leftOut) {
195
+ const folders = foldersOf(document);
196
+ if (isSeq(folders))
197
+ folders.items = folders.items.filter((item) => !(isMap(item) && nameOf(item) === collection.name));
198
+ for (const variant of collection.variants)
199
+ writer.remove(path.join(root, '.eidos', variant.template));
130
200
  }
131
- // 2. the Framework: naming, starting groups, and each collection in convention.
132
- text = setNaming(text, naming);
133
201
  const grouped = seedFramework.collections.find((collection) => collection.grouping && !collection.framing) ?? null;
134
202
  if (options.groups.length > 0) {
135
203
  if (!grouped || !grouped.grouping) {
136
204
  throw new InstallError(`no collection in '${options.seed}' declares a grouping`);
137
205
  }
138
- text = setGroups(text, grouped.grouping.label, options.groups.map((group) => convert(group, naming)));
206
+ const groupedNode = mapsIn(foldersOf(document)).find((collection) => nameOf(collection) === grouped.name);
207
+ const grouping = groupedNode?.get('grouping');
208
+ if (!isMap(grouping)) {
209
+ throw new InstallError(`${frameworkSrc}: ${grouped.name} declares a grouping the document does not carry`);
210
+ }
211
+ grouping.set('groups', document.createNode(options.groups.map((group) => ({ name: convert(group, naming), description: '' }))));
139
212
  }
140
213
  const readmeSrc = path.join(seedDir, 'README.md');
141
214
  let readme = existsSync(readmeSrc) ? readFileSync(readmeSrc, 'utf8') : null;
142
215
  if (readme !== null && options.product !== null) {
143
216
  readme = readme.replace(/\{\{Product\}\}/g, options.product);
144
217
  }
145
- if (readme !== null) {
146
- readme = readme.replace(/Framework\.md/g, 'Framework.yaml');
218
+ // A declared top-level doc is a file a person reads, so it takes the naming
219
+ // convention (README.md alone keeps its name), and the links to it in the
220
+ // framework and the README move with it.
221
+ const docs = [];
222
+ for (const doc of topLevelDocs) {
223
+ const source = path.join(seedDir, doc.file);
224
+ if (!existsSync(source)) {
225
+ continue;
226
+ }
227
+ let body = readFileSync(source, 'utf8');
228
+ if (options.product !== null) {
229
+ body = body.replace(/\{\{Product\}\}/g, options.product);
230
+ }
231
+ const target = `${convert(doc.title, naming)}.md`;
232
+ if (target !== doc.file) {
233
+ for (const entry of mapsIn(document.get('top_level'))) {
234
+ if (entry.get('path') === `../${doc.file}`)
235
+ entry.set('path', `../${target}`);
236
+ }
237
+ if (readme !== null) {
238
+ readme = readme.split(`(${doc.file})`).join(`(${target})`);
239
+ }
240
+ }
241
+ docs.push({ target, body });
147
242
  }
243
+ // Every declared folder takes the naming convention, a collection and an
244
+ // assets folder alike; the files inside an assets folder keep their names.
148
245
  const folders = new Map();
149
- for (const collection of seedFramework.collections) {
150
- const folder = convert(collection.name, naming);
151
- folders.set(collection.name, folder);
152
- if (folder !== collection.name) {
153
- text = renameCollection(text, collection.name, folder);
246
+ const renames = new Map();
247
+ if (readme !== null && leftOut.length > 0) {
248
+ readme = readme
249
+ .split('\n')
250
+ .filter((line) => !leftOut.some((collection) => line.includes(`[${collection.name}]`)))
251
+ .join('\n');
252
+ }
253
+ const plain = seedFramework.folders.filter((folder) => !isCollection(folder));
254
+ for (const declared of [...installing, ...plain]) {
255
+ // a seed writes a collection's name in Title Case and its assets folder in
256
+ // lowercase; the convention is applied to the title-cased word either way
257
+ const folder = convert(titleize(declared.name), naming);
258
+ folders.set(declared.name, folder);
259
+ if (folder !== declared.name) {
260
+ renames.set(declared.name, folder);
154
261
  if (readme !== null) {
155
- readme = renameCollection(readme, collection.name, folder);
262
+ readme = renameInText(readme, declared.name, folder);
156
263
  }
157
264
  }
158
265
  }
159
- // The seed's markdown is the source; the root gets the same framework as
160
- // YAML, with the guidance as comments. The seed's prose (section intros,
161
- // HTML comments) has no field to land in and stays in the seed.
162
- const fromSeed = parseFramework(text, 'md', root, path.join(root, '_eidos', 'Framework.md'));
163
- writer.remove(path.join(root, '_eidos', 'Framework.md'));
164
- const frameworkFile = path.join(root, '_eidos', 'Framework.yaml');
165
- const serialized = serializeDocument(frameworkToDocument(fromSeed));
266
+ renameFolders(document, renames);
267
+ applyDefaultCanvasHints(document);
268
+ let serialized = document.toString({ lineWidth: 0 });
269
+ // comments name the collections too, so the word follows; an assets folder's
270
+ // name is also a type (`type: assets`), so only its entry is renamed
271
+ for (const [from, to] of renames) {
272
+ if (installing.some((collection) => collection.name === from))
273
+ serialized = renameInText(serialized, from, to);
274
+ }
275
+ const frameworkFile = path.join(root, '.eidos', 'Framework.yaml');
166
276
  writer.write(frameworkFile, serialized);
167
277
  const installed = parseFramework(serialized, 'yaml', root, frameworkFile);
168
278
  if (readme !== null) {
169
279
  writer.write(path.join(root, 'README.md'), readme);
170
280
  }
171
- // 3. a folder per collection; blank blueprints for the framing one.
281
+ for (const doc of docs) {
282
+ writer.write(path.join(root, doc.target), doc.body);
283
+ }
284
+ // 3. a folder per declared folder; blank blueprints for the framing collection.
172
285
  const scaffolded = [];
173
286
  const groupsInConvention = options.groups.map((group) => convert(group, naming));
174
- for (const collection of seedFramework.collections) {
287
+ for (const folder of plain) {
288
+ writer.mkdir(path.join(root, folders.get(folder.name) ?? folder.name));
289
+ }
290
+ for (const collection of installing) {
175
291
  const folderName = folders.get(collection.name) ?? collection.name;
176
292
  const folder = path.join(root, folderName);
177
293
  writer.mkdir(folder);
@@ -183,22 +299,21 @@ export function installSeed(options) {
183
299
  if (!collection.framing) {
184
300
  continue;
185
301
  }
186
- for (const flavor of collection.flavors) {
187
- const shapeFile = path.join(seedDir, flavor.shape);
188
- if (!existsSync(shapeFile)) {
189
- writer.actions.push(`skip ${collection.name}/${flavor.name}: no shape at ${flavor.shape}`);
302
+ for (const variant of collection.variants) {
303
+ const templateFile = path.join(seedDir, variant.template);
304
+ if (!existsSync(templateFile)) {
305
+ writer.actions.push(`skip ${collection.name}/${variant.name}: no template at ${variant.template}`);
190
306
  continue;
191
307
  }
192
- const title = titleize(flavor.name);
308
+ const title = titleize(variant.name);
193
309
  const values = {
194
- id: kebab(flavor.name),
310
+ id: kebab(variant.name),
195
311
  title,
196
- flavor: flavor.name,
197
- status: 'Draft',
312
+ variant: variant.name,
198
313
  date_created: options.date,
199
314
  date_modified: options.date,
200
315
  };
201
- const body = blankBlueprint(installed, folderName, values, readFileSync(shapeFile, 'utf8'), title);
316
+ const body = blankBlueprint(installed, folderName, values, readFileSync(templateFile, 'utf8'), title);
202
317
  const file = path.join(folder, `${convert(title, naming)}.md`);
203
318
  writer.write(file, body);
204
319
  scaffolded.push(`${folderName}/${convert(title, naming)}.md`);
@@ -214,7 +329,8 @@ export function installSeed(options) {
214
329
  eidosVersion: installed.eidosVersion,
215
330
  naming,
216
331
  frameworkFile,
217
- collections: seedFramework.collections.map((collection) => folders.get(collection.name) ?? collection.name),
332
+ collections: installing.map((collection) => folders.get(collection.name) ?? collection.name),
333
+ folders: plain.map((folder) => folders.get(folder.name) ?? folder.name),
218
334
  groupedCollection: grouped ? (folders.get(grouped.name) ?? grouped.name) : null,
219
335
  groups: groupsInConvention,
220
336
  scaffolded,