eidosmd 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (100) hide show
  1. package/README.md +50 -29
  2. package/browser/dist/assets/index-C2NMN_D4.css +1 -0
  3. package/browser/dist/assets/index-C65k1ihb.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 +1 -1
  11. package/dist/src/commands/framework.js +24 -5
  12. package/dist/src/commands/index.js +4 -4
  13. package/dist/src/commands/init.js +1 -0
  14. package/dist/src/commands/instructions.js +1 -1
  15. package/dist/src/commands/list.js +8 -8
  16. package/dist/src/commands/migrate.js +32 -0
  17. package/dist/src/commands/new.js +3 -3
  18. package/dist/src/commands/seeds.js +5 -5
  19. package/dist/src/commands/setup.js +119 -0
  20. package/dist/src/commands/version.js +44 -0
  21. package/dist/src/commands/whoami.js +4 -4
  22. package/dist/src/context.js +5 -5
  23. package/dist/src/core/blueprint.js +16 -11
  24. package/dist/src/core/canvas-schema.js +148 -0
  25. package/dist/src/core/canvas.js +722 -0
  26. package/dist/src/core/check.js +127 -51
  27. package/dist/src/core/convert.js +7 -6
  28. package/dist/src/core/framework-markdown.js +113 -34
  29. package/dist/src/core/framework-model.js +27 -10
  30. package/dist/src/core/framework-structured.js +132 -33
  31. package/dist/src/core/framework.js +13 -13
  32. package/dist/src/core/git.js +59 -0
  33. package/dist/src/core/index-leaf.js +2 -2
  34. package/dist/src/core/me.js +16 -8
  35. package/dist/src/core/migrate.js +243 -0
  36. package/dist/src/core/naming.js +1 -1
  37. package/dist/src/core/root.js +2 -2
  38. package/dist/src/core/scaffold.js +19 -19
  39. package/dist/src/core/seed.js +151 -60
  40. package/dist/src/core/server.js +1236 -52
  41. package/dist/src/core/settings.js +181 -0
  42. package/dist/src/core/store.js +259 -0
  43. package/dist/src/core/template.js +32 -0
  44. package/dist/src/core/versions.js +79 -0
  45. package/dist/src/output.js +4 -1
  46. package/dist/src/program.js +134 -39
  47. package/instructions/authoring.md +13 -12
  48. package/instructions/configuring.md +65 -34
  49. package/instructions/init-required.md +4 -4
  50. package/instructions/overview.md +17 -7
  51. package/instructions/validating.md +5 -4
  52. package/package.json +21 -12
  53. package/standard/EIDOS.md +129 -188
  54. package/standard/seeds/README.md +6 -6
  55. package/standard/seeds/book/Framework.yaml +81 -0
  56. package/standard/seeds/book/README.md +9 -5
  57. package/standard/seeds/book/_gitignore +3 -3
  58. package/standard/seeds/book/me.md +1 -1
  59. package/standard/seeds/book/roles/README.md +3 -3
  60. package/standard/seeds/book/roles/framework-owner.md +2 -2
  61. package/standard/seeds/book/{shapes → templates}/chapter.full.md +0 -8
  62. package/standard/seeds/book/{shapes → templates}/chapter.sketch.md +0 -7
  63. package/standard/seeds/book/{shapes → templates}/frame.market.md +0 -6
  64. package/standard/seeds/book/templates/frame.premise.md +17 -0
  65. package/standard/seeds/book/{shapes → templates}/frame.reader.md +0 -6
  66. package/standard/seeds/book/{shapes → templates}/frame.voice.md +0 -7
  67. package/standard/seeds/research/Framework.yaml +81 -0
  68. package/standard/seeds/research/README.md +9 -5
  69. package/standard/seeds/research/_gitignore +3 -3
  70. package/standard/seeds/research/me.md +1 -1
  71. package/standard/seeds/research/roles/README.md +3 -3
  72. package/standard/seeds/research/roles/framework-owner.md +2 -2
  73. package/standard/seeds/research/{shapes → templates}/frame.ethics.md +0 -6
  74. package/standard/seeds/research/{shapes → templates}/frame.method.md +0 -7
  75. package/standard/seeds/research/{shapes → templates}/frame.prior-work.md +0 -6
  76. package/standard/seeds/research/{shapes → templates}/frame.question.md +0 -7
  77. package/standard/seeds/research/{shapes → templates}/investigation.full.md +0 -8
  78. package/standard/seeds/research/{shapes → templates}/investigation.note.md +0 -7
  79. package/standard/seeds/software/Framework.yaml +82 -0
  80. package/standard/seeds/software/README.md +5 -5
  81. package/standard/seeds/software/_gitignore +3 -3
  82. package/standard/seeds/software/me.md +1 -1
  83. package/standard/seeds/software/roles/README.md +3 -3
  84. package/standard/seeds/software/roles/framework-owner.md +2 -2
  85. package/standard/seeds/software/roles/project-manager.md +2 -2
  86. package/standard/seeds/software/roles/stakeholder.md +1 -1
  87. package/standard/seeds/software/{shapes → templates}/frame.architecture.md +0 -7
  88. package/standard/seeds/software/{shapes → templates}/frame.audience.md +1 -8
  89. package/standard/seeds/software/{shapes → templates}/frame.criteria.md +0 -8
  90. package/standard/seeds/software/{shapes → templates}/frame.market.md +0 -8
  91. package/standard/seeds/software/{shapes → templates}/spec.full.md +0 -8
  92. package/standard/seeds/software/{shapes → templates}/spec.micro.md +0 -9
  93. package/browser/index.html +0 -268
  94. package/dist/src/commands/convert.js +0 -30
  95. package/dist/src/core/shape.js +0 -26
  96. package/standard/seeds/book/Framework.md +0 -87
  97. package/standard/seeds/book/shapes/frame.premise.md +0 -24
  98. package/standard/seeds/research/Framework.md +0 -88
  99. package/standard/seeds/software/Framework.md +0 -88
  100. /package/standard/seeds/software/{shapes → templates}/.gitkeep +0 -0
@@ -1,19 +1,20 @@
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 collection names in
3
+ // its Framework.yaml (the seed's own comments kept), scaffold a folder per
4
+ // collection, and drop one blank blueprint per framing variant.
5
5
  // It writes no prose: the README's one-liner, each group's description, and
6
6
  // every scaffolded blueprint's summary and body stay the owner's.
7
7
  import { cpSync, existsSync, mkdirSync, readdirSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs';
8
8
  import path from 'node:path';
9
9
  import { formatFrontmatter } from './frontmatter.js';
10
10
  import { parseFramework, propertiesFor } from './framework.js';
11
- import { frameworkToDocument, serializeDocument } from './framework-structured.js';
12
11
  import { buildIndexes } from './index-leaf.js';
13
12
  import { listBlueprints } from './blueprint.js';
14
13
  import { convert, kebab, titleize } from './naming.js';
15
- import { renderShape } from './shape.js';
16
- const GROUP_PROMPT = '_(one line on what belongs here)_';
14
+ import { renderTemplate } from './template.js';
15
+ import { TOOL_KEY } from './framework-model.js';
16
+ import { DEFAULT_CANVAS_HINTS } from './canvas.js';
17
+ import { isMap, isScalar, isSeq, parseDocument } from 'yaml';
17
18
  const GITIGNORE_SHIPPED = '_gitignore';
18
19
  export function listSeeds(seedsDir) {
19
20
  if (!existsSync(seedsDir)) {
@@ -22,12 +23,12 @@ export function listSeeds(seedsDir) {
22
23
  return readdirSync(seedsDir)
23
24
  .sort()
24
25
  .map((name) => path.join(seedsDir, name))
25
- .filter((dir) => existsSync(path.join(dir, 'Framework.md')))
26
+ .filter((dir) => existsSync(path.join(dir, 'Framework.yaml')))
26
27
  .map((dir) => ({ name: path.basename(dir), dir, framework: readSeedFramework(dir) }));
27
28
  }
28
29
  function readSeedFramework(dir) {
29
- const file = path.join(dir, 'Framework.md');
30
- return parseFramework(readFileSync(file, 'utf8'), 'md', dir, file);
30
+ const file = path.join(dir, 'Framework.yaml');
31
+ return parseFramework(readFileSync(file, 'utf8'), 'yaml', dir, file);
31
32
  }
32
33
  export class InstallError extends Error {
33
34
  constructor(message) {
@@ -73,26 +74,57 @@ class Writer {
73
74
  }
74
75
  }
75
76
  }
76
- function setNaming(text, naming) {
77
- if (/^naming:.*$/m.test(text)) {
78
- return text.replace(/^naming:.*$/m, `naming: ${naming}`);
77
+ const mapsIn = (value) => (isSeq(value) ? value.items.filter((item) => isMap(item)) : []);
78
+ const nameOf = (map) => String(map.get('name') ?? '');
79
+ // The seed's collections and the properties scoped to them, renamed into the
80
+ // root's convention; every other key, and every comment, stays as the seed wrote it.
81
+ function renameCollections(document, renames) {
82
+ for (const collection of mapsIn(document.get('collections'))) {
83
+ const to = renames.get(nameOf(collection));
84
+ if (to !== undefined)
85
+ collection.set('name', to);
86
+ }
87
+ const properties = document.get('properties');
88
+ if (!isMap(properties))
89
+ return;
90
+ const blocks = [properties.get('custom'), ...(isMap(properties.get('tools')) ? properties.get('tools').items.map((pair) => pair.value) : [])];
91
+ for (const block of blocks) {
92
+ for (const property of mapsIn(block)) {
93
+ const applies = property.get('applies_to');
94
+ if (!isSeq(applies))
95
+ continue;
96
+ for (const item of applies.items) {
97
+ if (isScalar(item)) {
98
+ const to = renames.get(String(item.value));
99
+ if (to !== undefined)
100
+ item.value = to;
101
+ }
102
+ }
103
+ }
79
104
  }
80
- return text.replace(/^---\n/, `---\nnaming: ${naming}\n`);
81
105
  }
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}`);
106
+ // The canvas hint is the one field the CLI prescribes; a fresh root gets the
107
+ // legend as its default palette on the seed's own status and type.
108
+ function applyDefaultCanvasHints(document) {
109
+ const properties = document.get('properties');
110
+ if (!isMap(properties))
111
+ return;
112
+ for (const property of mapsIn(properties.get('custom'))) {
113
+ const hint = DEFAULT_CANVAS_HINTS[nameOf(property)];
114
+ if (!hint || property.has(TOOL_KEY))
115
+ continue;
116
+ // a hint is a map of maps; a seed writes its rows in flow style, which would put all of it on one line
117
+ property.flow = false;
118
+ property.set(TOOL_KEY, document.createNode({ canvas: hint }));
119
+ }
89
120
  }
90
- function renameCollection(text, from, to) {
121
+ // Comments name the seed's collections too; a word-bounded rename follows them.
122
+ function renameInText(text, from, to) {
91
123
  const escaped = from.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
92
124
  return text.replace(new RegExp(`(?<![\\w-])${escaped}(?!\\w)`, 'g'), to);
93
125
  }
94
126
  // 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.
127
+ // values given and `[]` for an empty list, then the variant's template as the body.
96
128
  export function blankBlueprint(framework, collectionName, values, shapeTemplate, title) {
97
129
  const entries = propertiesFor(framework, collectionName).map((property) => {
98
130
  const value = values[property.name];
@@ -101,77 +133,136 @@ export function blankBlueprint(framework, collectionName, values, shapeTemplate,
101
133
  }
102
134
  return [property.name, value ?? ''];
103
135
  });
104
- return formatFrontmatter(entries) + '\n' + renderShape(shapeTemplate, title);
136
+ return formatFrontmatter(entries) + '\n' + renderTemplate(shapeTemplate, title);
105
137
  }
106
138
  export function installSeed(options) {
107
139
  const seedDir = path.join(options.seedsDir, options.seed);
108
- const frameworkSrc = path.join(seedDir, 'Framework.md');
140
+ const frameworkSrc = path.join(seedDir, 'Framework.yaml');
109
141
  if (!existsSync(frameworkSrc)) {
110
- throw new InstallError(`no seed '${options.seed}' under ${options.seedsDir} (needs a Framework.md)`);
142
+ throw new InstallError(`no seed '${options.seed}' under ${options.seedsDir} (needs a Framework.yaml)`);
111
143
  }
112
144
  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`);
145
+ if (existsSync(path.join(root, '.eidos'))) {
146
+ throw new InstallError(`${root}/.eidos already exists: that is a root; use migrate`);
115
147
  }
116
- let text = readFileSync(frameworkSrc, 'utf8');
117
- const seedFramework = parseFramework(text, 'md', seedDir, frameworkSrc);
148
+ const text = readFileSync(frameworkSrc, 'utf8');
149
+ const seedFramework = parseFramework(text, 'yaml', seedDir, frameworkSrc);
150
+ const document = parseDocument(text);
118
151
  if (seedFramework.collections.length === 0) {
119
152
  throw new InstallError(`${frameworkSrc} declares no collections`);
120
153
  }
121
154
  const writer = new Writer(options.dryRun);
122
155
  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'));
156
+ // The framing collection is the recommendation, not the standard's rule: an
157
+ // owner who keeps the framing elsewhere leaves it out, templates and all.
158
+ const leftOut = options.framing === false ? seedFramework.collections.filter((collection) => collection.framing) : [];
159
+ const installing = seedFramework.collections.filter((collection) => !leftOut.includes(collection));
160
+ if (installing.length === 0) {
161
+ throw new InstallError(`'${options.seed}' has only its framing collection; nothing would be left to install`);
162
+ }
163
+ // 1. the framework itself. The seed's README is the root's visible door, and so
164
+ // is any other top-level doc the seed declares (the software seed's
165
+ // Vocabulary), so those move to the root; roles/, templates/, me.md, and
166
+ // .gitignore travel as they are.
167
+ const topLevelDocs = seedFramework.topLevel.filter((doc) => /^\.\.\/[^/]+\.md$/.test(doc.path) && doc.path !== '../README.md').map((doc) => ({ title: doc.title, file: doc.path.slice(3) }));
168
+ writer.copyTree(seedDir, path.join(root, '.eidos'));
169
+ writer.remove(path.join(root, '.eidos', 'README.md'));
170
+ for (const doc of topLevelDocs) {
171
+ writer.remove(path.join(root, '.eidos', doc.file));
172
+ }
127
173
  // npm never packs a file named .gitignore, so a seed ships it as _gitignore.
128
174
  if (existsSync(path.join(seedDir, GITIGNORE_SHIPPED))) {
129
- writer.rename(path.join(root, '_eidos', GITIGNORE_SHIPPED), path.join(root, '_eidos', '.gitignore'));
175
+ writer.rename(path.join(root, '.eidos', GITIGNORE_SHIPPED), path.join(root, '.eidos', '.gitignore'));
176
+ }
177
+ // 2. the framework document: naming, starting groups, and each collection in
178
+ // convention, edited in place so the seed's comments travel with it.
179
+ document.set('naming', naming);
180
+ for (const collection of leftOut) {
181
+ const collections = document.get('collections');
182
+ if (isSeq(collections))
183
+ collections.items = collections.items.filter((item) => !(isMap(item) && nameOf(item) === collection.name));
184
+ for (const variant of collection.variants)
185
+ writer.remove(path.join(root, '.eidos', variant.template));
130
186
  }
131
- // 2. the Framework: naming, starting groups, and each collection in convention.
132
- text = setNaming(text, naming);
133
187
  const grouped = seedFramework.collections.find((collection) => collection.grouping && !collection.framing) ?? null;
134
188
  if (options.groups.length > 0) {
135
189
  if (!grouped || !grouped.grouping) {
136
190
  throw new InstallError(`no collection in '${options.seed}' declares a grouping`);
137
191
  }
138
- text = setGroups(text, grouped.grouping.label, options.groups.map((group) => convert(group, naming)));
192
+ const groupedNode = mapsIn(document.get('collections')).find((collection) => nameOf(collection) === grouped.name);
193
+ const grouping = groupedNode?.get('grouping');
194
+ if (!isMap(grouping)) {
195
+ throw new InstallError(`${frameworkSrc}: ${grouped.name} declares a grouping the document does not carry`);
196
+ }
197
+ grouping.set('groups', document.createNode(options.groups.map((group) => ({ name: convert(group, naming), description: '' }))));
139
198
  }
140
199
  const readmeSrc = path.join(seedDir, 'README.md');
141
200
  let readme = existsSync(readmeSrc) ? readFileSync(readmeSrc, 'utf8') : null;
142
201
  if (readme !== null && options.product !== null) {
143
202
  readme = readme.replace(/\{\{Product\}\}/g, options.product);
144
203
  }
145
- if (readme !== null) {
146
- readme = readme.replace(/Framework\.md/g, 'Framework.yaml');
204
+ // A declared top-level doc is a file a person reads, so it takes the naming
205
+ // convention (README.md alone keeps its name), and the links to it in the
206
+ // framework and the README move with it.
207
+ const docs = [];
208
+ for (const doc of topLevelDocs) {
209
+ const source = path.join(seedDir, doc.file);
210
+ if (!existsSync(source)) {
211
+ continue;
212
+ }
213
+ let body = readFileSync(source, 'utf8');
214
+ if (options.product !== null) {
215
+ body = body.replace(/\{\{Product\}\}/g, options.product);
216
+ }
217
+ const target = `${convert(doc.title, naming)}.md`;
218
+ if (target !== doc.file) {
219
+ for (const entry of mapsIn(document.get('top_level'))) {
220
+ if (entry.get('path') === `../${doc.file}`)
221
+ entry.set('path', `../${target}`);
222
+ }
223
+ if (readme !== null) {
224
+ readme = readme.split(`(${doc.file})`).join(`(${target})`);
225
+ }
226
+ }
227
+ docs.push({ target, body });
147
228
  }
148
229
  const folders = new Map();
149
- for (const collection of seedFramework.collections) {
230
+ const renames = new Map();
231
+ if (readme !== null && leftOut.length > 0) {
232
+ readme = readme
233
+ .split('\n')
234
+ .filter((line) => !leftOut.some((collection) => line.includes(`[${collection.name}]`)))
235
+ .join('\n');
236
+ }
237
+ for (const collection of installing) {
150
238
  const folder = convert(collection.name, naming);
151
239
  folders.set(collection.name, folder);
152
240
  if (folder !== collection.name) {
153
- text = renameCollection(text, collection.name, folder);
241
+ renames.set(collection.name, folder);
154
242
  if (readme !== null) {
155
- readme = renameCollection(readme, collection.name, folder);
243
+ readme = renameInText(readme, collection.name, folder);
156
244
  }
157
245
  }
158
246
  }
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));
247
+ renameCollections(document, renames);
248
+ applyDefaultCanvasHints(document);
249
+ let serialized = document.toString({ lineWidth: 0 });
250
+ for (const [from, to] of renames) {
251
+ serialized = renameInText(serialized, from, to);
252
+ }
253
+ const frameworkFile = path.join(root, '.eidos', 'Framework.yaml');
166
254
  writer.write(frameworkFile, serialized);
167
255
  const installed = parseFramework(serialized, 'yaml', root, frameworkFile);
168
256
  if (readme !== null) {
169
257
  writer.write(path.join(root, 'README.md'), readme);
170
258
  }
259
+ for (const doc of docs) {
260
+ writer.write(path.join(root, doc.target), doc.body);
261
+ }
171
262
  // 3. a folder per collection; blank blueprints for the framing one.
172
263
  const scaffolded = [];
173
264
  const groupsInConvention = options.groups.map((group) => convert(group, naming));
174
- for (const collection of seedFramework.collections) {
265
+ for (const collection of installing) {
175
266
  const folderName = folders.get(collection.name) ?? collection.name;
176
267
  const folder = path.join(root, folderName);
177
268
  writer.mkdir(folder);
@@ -183,22 +274,22 @@ export function installSeed(options) {
183
274
  if (!collection.framing) {
184
275
  continue;
185
276
  }
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}`);
277
+ for (const variant of collection.variants) {
278
+ const templateFile = path.join(seedDir, variant.template);
279
+ if (!existsSync(templateFile)) {
280
+ writer.actions.push(`skip ${collection.name}/${variant.name}: no template at ${variant.template}`);
190
281
  continue;
191
282
  }
192
- const title = titleize(flavor.name);
283
+ const title = titleize(variant.name);
193
284
  const values = {
194
- id: kebab(flavor.name),
285
+ id: kebab(variant.name),
195
286
  title,
196
- flavor: flavor.name,
287
+ variant: variant.name,
197
288
  status: 'Draft',
198
289
  date_created: options.date,
199
290
  date_modified: options.date,
200
291
  };
201
- const body = blankBlueprint(installed, folderName, values, readFileSync(shapeFile, 'utf8'), title);
292
+ const body = blankBlueprint(installed, folderName, values, readFileSync(templateFile, 'utf8'), title);
202
293
  const file = path.join(folder, `${convert(title, naming)}.md`);
203
294
  writer.write(file, body);
204
295
  scaffolded.push(`${folderName}/${convert(title, naming)}.md`);
@@ -214,7 +305,7 @@ export function installSeed(options) {
214
305
  eidosVersion: installed.eidosVersion,
215
306
  naming,
216
307
  frameworkFile,
217
- collections: seedFramework.collections.map((collection) => folders.get(collection.name) ?? collection.name),
308
+ collections: installing.map((collection) => folders.get(collection.name) ?? collection.name),
218
309
  groupedCollection: grouped ? (folders.get(grouped.name) ?? grouped.name) : null,
219
310
  groups: groupsInConvention,
220
311
  scaffolded,