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,9 +1,15 @@
1
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.
2
+ // of `.eidos/Framework.md` or `.yaml` it is. The version and naming
3
+ // convention, the Top-Level docs, the Collections (each with variants and
4
+ // grouping), the Properties table (`schema` here, `properties` on disk), the
5
+ // Vocabulary, and the root's own Versions.
5
6
  import { kebab } from './naming.js';
6
- export const FRAMEWORK_DIR = '_eidos';
7
+ export const FRAMEWORK_DIR = '.eidos';
8
+ // This tool's name, as the standard has a tool sign what it keeps in a
9
+ // framework: its folder under `.eidos/plugins/<name>/`, and its fields on a
10
+ // Properties row (a key on the YAML entry, a column in the markdown table).
11
+ export const TOOL_KEY = 'eidosmd';
12
+ export const PLUGINS_DIR = 'plugins';
7
13
  export function isFrameworkFormat(value) {
8
14
  return value === 'md' || value === 'yaml';
9
15
  }
@@ -13,20 +19,31 @@ export const FRAMEWORK_FILES = [
13
19
  { name: 'Framework.yml', format: 'yaml' },
14
20
  ];
15
21
  export const PROPERTY_TYPES = ['Text', 'List', 'Number', 'Checkbox', 'Date', 'Date & time'];
22
+ // The unit a collection speaks in, the word for one of its blueprints: what
23
+ // its template files are named for (`<unit>.<variant>.md`), read off the
24
+ // default variant. A collection whose unit is `frame` is the framing one, this
25
+ // CLI's recommendation for every product; the standard names no collection.
26
+ export function unitOf(collection) {
27
+ const variant = collection.variants.find((entry) => entry.isDefault) ?? collection.variants[0];
28
+ return variant && variant.unit !== '' ? variant.unit : null;
29
+ }
16
30
  export class FrameworkError extends Error {
17
31
  constructor(message) {
18
32
  super(message);
19
33
  this.name = 'FrameworkError';
20
34
  }
21
35
  }
22
- // The standard's own five, used when a Framework.md carries no core block.
36
+ // The standard's own four (Eidos 5.0.0), used when a Framework.md carries no core block.
23
37
  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 },
38
+ { name: 'id', type: 'Text', appliesTo: 'all', meaning: 'Stable, unique identity, in any form: a slug, a number, a GUID. Assigned once, never changed.', core: true, owner: 'eidos' },
39
+ { name: 'title', type: 'Text', appliesTo: 'all', meaning: 'Human-readable name.', core: true, owner: 'eidos' },
40
+ { name: 'summary', type: 'Text', appliesTo: 'all', meaning: 'One plain line: what this blueprint is. The source for the collection index.', core: true, owner: 'eidos' },
41
+ { name: 'variant', type: 'Text', appliesTo: 'all', meaning: 'Which variant this blueprint follows. Absent = the collection default.', core: true, owner: 'eidos' },
29
42
  ];
43
+ // Every property of the Properties table, whichever block owns it, core first.
44
+ export function allProperties(schema) {
45
+ return [...schema.core, ...schema.custom, ...Object.values(schema.tools).flat()];
46
+ }
30
47
  // `Domains` -> `domain`: the custom property that carries a collection's group,
31
48
  // matched by the grouping label's singular against the properties scoped to it.
32
49
  export function groupingProperty(collection, custom) {
@@ -1,13 +1,14 @@
1
- // The YAML framework document, `_eidos/Framework.yaml`: the same model as the
1
+ // The YAML framework document, `.eidos/Framework.yaml`: the same model as the
2
2
  // markdown form, as fields in the frontmatter's snake_case, for a root that
3
3
  // scripts and agents read.
4
- // Paths are relative to `_eidos/`, as the markdown links are. A structured
4
+ // Paths are relative to `.eidos/`, as the markdown links are. A structured
5
5
  // document also carries the generated index under `index`, one list per
6
6
  // collection, which `eidos index` rewrites in place without disturbing
7
7
  // anything a person wrote around it.
8
8
  import { Document, isMap, isPair, isScalar, parse as parseYaml, parseDocument } from 'yaml';
9
- import { groupingProperty, STANDARD_CORE, } from './framework-model.js';
9
+ import { groupingProperty, STANDARD_CORE, TOOL_KEY, unitOf } from './framework-model.js';
10
10
  import { DEFAULT_NAMING, isNaming } from './naming.js';
11
+ const STANDARD_PROPERTY_KEYS = new Set(['name', 'type', 'applies_to', 'meaning']);
11
12
  function isPlain(value) {
12
13
  return typeof value === 'object' && value !== null && !Array.isArray(value);
13
14
  }
@@ -51,23 +52,43 @@ function readCanvas(value, where, problems) {
51
52
  problems.push(`${where}: canvas must be file, card, or { mode: card, section: <Section> }`);
52
53
  return null;
53
54
  }
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`);
55
+ // `template` (`shape` before 5.0.0) is the file's path relative to `.eidos/`.
56
+ function readVariant(value, where, problems) {
57
+ const given = isPlain(value) ? text(value['template']) || text(value['shape']) : '';
58
+ if (!isPlain(value) || text(value['name']) === '' || given === '') {
59
+ problems.push(`${where}: every variant needs a name and a template`);
57
60
  return null;
58
61
  }
59
- const shape = text(value['shape']).replace(/\\/g, '/');
60
- const file = shape.split('/').pop() ?? shape;
62
+ const template = given.replace(/\\/g, '/');
63
+ const file = template.split('/').pop() ?? template;
61
64
  const stem = file.replace(/\.md$/i, '');
62
65
  const dot = stem.indexOf('.');
63
66
  return {
64
67
  name: text(value['name']),
65
- kind: dot === -1 ? stem : stem.slice(0, dot),
66
- shape,
68
+ unit: dot === -1 ? stem : stem.slice(0, dot),
69
+ template,
67
70
  description: text(value['description']),
68
71
  isDefault: value['default'] === true,
69
72
  };
70
73
  }
74
+ // `vocabulary`: one entry per term; `not` is a list (a single string reads as one entry).
75
+ function readVocabulary(value, problems) {
76
+ const out = [];
77
+ list(value).forEach((entry, index) => {
78
+ if (!isPlain(entry) || text(entry['term']) === '') {
79
+ problems.push(`vocabulary[${index}]: a term needs a term`);
80
+ return;
81
+ }
82
+ const nots = Array.isArray(entry['not']) ? entry['not'] : entry['not'] === undefined || entry['not'] === null ? [] : [entry['not']];
83
+ out.push({
84
+ term: text(entry['term']),
85
+ means: text(entry['means']),
86
+ not: nots.map((item) => text(item)).filter((item) => item !== ''),
87
+ see: text(entry['see']) === '' ? null : text(entry['see']),
88
+ });
89
+ });
90
+ return out;
91
+ }
71
92
  function readCollection(value, index, problems) {
72
93
  const where = `collections[${index}]`;
73
94
  if (!isPlain(value) || text(value['name']) === '') {
@@ -75,11 +96,11 @@ function readCollection(value, index, problems) {
75
96
  return null;
76
97
  }
77
98
  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];
99
+ const variants = list(value['variants'] ?? value['flavors'])
100
+ .map((variant, position) => readVariant(variant, `${where} (${name}) variants[${position}]`, problems))
101
+ .filter((variant) => variant !== null);
102
+ if (variants.length > 0 && !variants.some((variant) => variant.isDefault)) {
103
+ const first = variants[0];
83
104
  if (first) {
84
105
  first.isDefault = true;
85
106
  }
@@ -87,12 +108,13 @@ function readCollection(value, index, problems) {
87
108
  const collection = {
88
109
  name,
89
110
  description: text(value['description']),
90
- framing: index === 0,
111
+ framing: false,
91
112
  leaf: null,
92
- flavors,
113
+ variants,
93
114
  canvas: readCanvas(value['canvas'], `${where} (${name})`, problems),
94
115
  grouping: null,
95
116
  };
117
+ collection.framing = unitOf(collection) === 'frame';
96
118
  const grouping = value['grouping'];
97
119
  if (grouping !== undefined && grouping !== null) {
98
120
  if (!isPlain(grouping) || text(grouping['label']) === '') {
@@ -111,7 +133,7 @@ function readCollection(value, index, problems) {
111
133
  }
112
134
  return collection;
113
135
  }
114
- function readProperty(value, where, core, problems) {
136
+ function readProperty(value, where, core, problems, owner = core ? 'eidos' : 'custom') {
115
137
  if (!isPlain(value) || text(value['name']) === '') {
116
138
  problems.push(`${where}: a property needs a name`);
117
139
  return null;
@@ -132,7 +154,54 @@ function readProperty(value, where, core, problems) {
132
154
  problems.push(`${where}: applies_to must be all or a list of collections`);
133
155
  }
134
156
  }
135
- return { name: text(value['name']), type: text(value['type']) || 'Text', appliesTo: applies, meaning: text(value['meaning']), core };
157
+ const property = { name: text(value['name']), type: text(value['type']) || 'Text', appliesTo: applies, meaning: text(value['meaning']), core, owner };
158
+ const tools = {};
159
+ for (const [key, field] of Object.entries(value)) {
160
+ if (!STANDARD_PROPERTY_KEYS.has(key) && key !== 'canvas' && field !== undefined && field !== null) {
161
+ tools[key] = field;
162
+ }
163
+ }
164
+ if (Object.keys(tools).length > 0) {
165
+ property.tools = tools;
166
+ }
167
+ // This tool's hint sits under its own key; a bare `canvas` (how the first
168
+ // canvases were written) still reads.
169
+ const own = isPlain(tools[TOOL_KEY]) ? tools[TOOL_KEY]['canvas'] : undefined;
170
+ const hint = readCanvasHint(own) ?? readCanvasHint(value['canvas']);
171
+ if (hint) {
172
+ property.canvas = hint;
173
+ }
174
+ return property;
175
+ }
176
+ function readStringMap(value) {
177
+ if (!isPlain(value)) {
178
+ return undefined;
179
+ }
180
+ const out = {};
181
+ for (const [key, item] of Object.entries(value)) {
182
+ if (typeof item === 'string' || typeof item === 'number') {
183
+ out[key] = String(item);
184
+ }
185
+ }
186
+ return Object.keys(out).length > 0 ? out : undefined;
187
+ }
188
+ export function readCanvasHint(value) {
189
+ if (!isPlain(value)) {
190
+ return null;
191
+ }
192
+ const hint = {};
193
+ if (value['show'] === true) {
194
+ hint.show = true;
195
+ }
196
+ const shape = readStringMap(value['shape']);
197
+ if (shape) {
198
+ hint.shape = shape;
199
+ }
200
+ const color = readStringMap(value['color']);
201
+ if (color) {
202
+ hint.color = color;
203
+ }
204
+ return Object.keys(hint).length > 0 ? hint : null;
136
205
  }
137
206
  export function readEmbeddedIndex(value) {
138
207
  if (!isPlain(value)) {
@@ -182,15 +251,23 @@ export function parseFrameworkStructured(source, root, file) {
182
251
  .map((collection, index) => readCollection(collection, index, problems))
183
252
  .filter((collection) => collection !== null);
184
253
  if (!Array.isArray(document['collections']) && !error && isPlain(value)) {
185
- problems.push('collections must be a list; the first is the framing collection');
254
+ problems.push('collections must be a list');
186
255
  }
187
- const schema = isPlain(document['schema']) ? document['schema'] : {};
256
+ const schema = isPlain(document['properties']) ? document['properties'] : isPlain(document['schema']) ? document['schema'] : {};
188
257
  const core = list(schema['core'])
189
258
  .map((property, index) => readProperty(property, `schema.core[${index}]`, true, problems))
190
259
  .filter((property) => property !== null);
191
260
  const custom = list(schema['custom'])
192
261
  .map((property, index) => readProperty(property, `schema.custom[${index}]`, false, problems))
193
262
  .filter((property) => property !== null);
263
+ const tools = {};
264
+ if (isPlain(schema['tools'])) {
265
+ for (const [tool, block] of Object.entries(schema['tools'])) {
266
+ tools[tool] = list(block)
267
+ .map((property, index) => readProperty(property, `schema.tools.${tool}[${index}]`, false, problems, tool))
268
+ .filter((property) => property !== null);
269
+ }
270
+ }
194
271
  for (const collection of collections) {
195
272
  if (collection.grouping && collection.grouping.property === null) {
196
273
  collection.grouping.property = groupingProperty(collection, custom);
@@ -209,7 +286,8 @@ export function parseFrameworkStructured(source, root, file) {
209
286
  namingError,
210
287
  topLevel,
211
288
  collections,
212
- schema: { core: core.length > 0 ? core : STANDARD_CORE, custom },
289
+ schema: { core: core.length > 0 ? core : STANDARD_CORE, custom, tools },
290
+ vocabulary: readVocabulary(document['vocabulary'], problems),
213
291
  problems,
214
292
  };
215
293
  }
@@ -249,12 +327,12 @@ export function frameworkToDocument(framework) {
249
327
  entry.canvas = { mode: 'card', section: collection.canvas.section };
250
328
  }
251
329
  }
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;
330
+ entry.variants = collection.variants.map((variant) => {
331
+ const item = { name: variant.name, template: variant.template };
332
+ if (variant.description) {
333
+ item.description = variant.description;
256
334
  }
257
- if (flavor.isDefault) {
335
+ if (variant.isDefault) {
258
336
  item.default = true;
259
337
  }
260
338
  return item;
@@ -276,23 +354,44 @@ export function frameworkToDocument(framework) {
276
354
  }
277
355
  return entry;
278
356
  });
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 })),
357
+ const entry = ({ name, type, appliesTo: applies, meaning, canvas, tools }) => {
358
+ const fields = { ...(tools ?? {}) };
359
+ if (canvas) {
360
+ const own = isPlain(fields[TOOL_KEY]) ? fields[TOOL_KEY] : {};
361
+ fields[TOOL_KEY] = { ...own, canvas };
362
+ }
363
+ return { name, type, applies_to: applies, meaning, ...fields };
282
364
  };
365
+ document.properties = {
366
+ core: framework.schema.core.map(({ name, type, meaning, tools }) => ({ name, type, meaning, ...(tools ?? {}) })),
367
+ custom: framework.schema.custom.map(entry),
368
+ };
369
+ if (Object.keys(framework.schema.tools).length > 0) {
370
+ document.properties.tools = Object.fromEntries(Object.entries(framework.schema.tools).map(([tool, block]) => [tool, block.map(entry)]));
371
+ }
372
+ if (framework.vocabulary.length > 0) {
373
+ document.vocabulary = framework.vocabulary.map((term) => {
374
+ const entry = { term: term.term, means: term.means, not: term.not };
375
+ if (term.see)
376
+ entry.see = term.see;
377
+ return entry;
378
+ });
379
+ }
283
380
  return document;
284
381
  }
285
382
  const YAML_GUIDANCE = {
286
383
  eidos_version: ' The Eidos version this framework targets; migrate reads and bumps it.',
287
384
  naming: ' How files, folders, and links are named: kebab-case | TitleCase | Title Case. Settled once.',
288
385
  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.',
386
+ collections: " The collections. Each declares its variants (one default; each a template file named <unit>.<variant>.md under templates/, the unit being the word for one of its blueprints) and its grouping.\n Add a group under `grouping.groups` with a one-line description; add a variant with a template file in templates/.",
387
+ properties: " The Properties table, one block per owner. `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; `tools.<tool>` is a block a tool declared and alone writes.\n A key past those four on a row is a tool's, named for the tool; `eidosmd.canvas` is how this CLI styles a canvas node by the property's value.",
388
+ vocabulary: " The root's own terms, one entry each: term, means (one line), not (the near-misses, each with why it is a different thing), and see (the blueprint that defines it in full) when one does. Absent = none declared.",
389
+ versions: " Snapshots of the root, taken on purpose, newest first: version (the root's own number), commit (the sha that is the snapshot), tag (blueprints/<version>, when made). Absent = none recorded, the normal state.",
291
390
  index: ' Generated by `eidos index`; never hand-edited. One list per collection, in the order the markdown index would use.',
292
391
  };
293
392
  // A YAML document a person will edit gets the guidance the markdown seed
294
393
  // keeps as prose, as comments above each top-level key.
295
- function annotateYaml(document) {
394
+ export function annotateYaml(document) {
296
395
  if (!isMap(document.contents)) {
297
396
  return;
298
397
  }
@@ -1,6 +1,6 @@
1
- // Finding and loading the root's framework document: exactly one of
2
- // `_eidos/Framework.md` or `Framework.yaml`, parsed into
3
- // the one model every command reads.
1
+ // Finding and loading the root's framework document, `.eidos/Framework.yaml`,
2
+ // parsed into the one model every command reads. A 4.x root's Framework.md
3
+ // is found and parsed too, only so `migrate` can move it.
4
4
  import { existsSync, readFileSync } from 'node:fs';
5
5
  import path from 'node:path';
6
6
  import { parseFrameworkMarkdown } from './framework-markdown.js';
@@ -8,7 +8,7 @@ import { parseFrameworkStructured } from './framework-structured.js';
8
8
  import { appliesTo, FRAMEWORK_DIR, FRAMEWORK_FILES, FrameworkError } from './framework-model.js';
9
9
  import { kebab } from './naming.js';
10
10
  export * from './framework-model.js';
11
- // The one framework document in a folder's `_eidos/`, or null; two is an error.
11
+ // The one framework document in a folder's `.eidos/`, or null; two is an error.
12
12
  export function findFrameworkFile(dir) {
13
13
  const present = FRAMEWORK_FILES.map((candidate) => ({ file: path.join(dir, FRAMEWORK_DIR, candidate.name), format: candidate.format })).filter((candidate) => existsSync(candidate.file));
14
14
  if (present.length > 1) {
@@ -28,7 +28,7 @@ export function parseFramework(text, format, root, file) {
28
28
  export function loadFramework(root) {
29
29
  const found = findFrameworkFile(root);
30
30
  if (!found) {
31
- throw new FrameworkError(`${path.join(root, FRAMEWORK_DIR)} holds no framework document (Framework.md or Framework.yaml)`);
31
+ throw new FrameworkError(`${path.join(root, FRAMEWORK_DIR)} holds no framework document (Framework.yaml)`);
32
32
  }
33
33
  return parseFramework(readFileSync(found.file, 'utf8'), found.format, root, found.file);
34
34
  }
@@ -38,19 +38,19 @@ export function findCollection(framework, name) {
38
38
  framework.collections.find((collection) => kebab(collection.name) === kebab(name)) ??
39
39
  null);
40
40
  }
41
- export function findFlavor(collection, name) {
41
+ export function findVariant(collection, name) {
42
42
  const wanted = name.trim().toLowerCase();
43
- return (collection.flavors.find((flavor) => flavor.name.toLowerCase() === wanted) ??
44
- collection.flavors.find((flavor) => kebab(flavor.name) === kebab(name)) ??
43
+ return (collection.variants.find((variant) => variant.name.toLowerCase() === wanted) ??
44
+ collection.variants.find((variant) => kebab(variant.name) === kebab(name)) ??
45
45
  null);
46
46
  }
47
- export function defaultFlavor(collection) {
48
- return collection.flavors.find((flavor) => flavor.isDefault) ?? collection.flavors[0] ?? null;
47
+ export function defaultVariant(collection) {
48
+ return collection.variants.find((variant) => variant.isDefault) ?? collection.variants[0] ?? null;
49
49
  }
50
50
  // The properties a blueprint in this collection carries, core first.
51
51
  export function propertiesFor(framework, collectionName) {
52
- return [...framework.schema.core, ...framework.schema.custom.filter((property) => appliesTo(property, collectionName))];
52
+ return [...framework.schema.core, ...[...framework.schema.custom, ...Object.values(framework.schema.tools).flat()].filter((property) => appliesTo(property, collectionName))];
53
53
  }
54
- export function shapePath(framework, flavor) {
55
- return path.join(framework.root, FRAMEWORK_DIR, flavor.shape);
54
+ export function templatePath(framework, variant) {
55
+ return path.join(framework.root, FRAMEWORK_DIR, variant.template);
56
56
  }
@@ -0,0 +1,59 @@
1
+ // The little git the CLI needs: the commit a root sits at, whether a commit
2
+ // exists, and a tag on one. Every call is read-only except `gitTag`, and
3
+ // every one answers null or false rather than throwing where git is absent
4
+ // or the root is not in a repository.
5
+ import { execFileSync } from 'node:child_process';
6
+ import path from 'node:path';
7
+ function git(root, args) {
8
+ try {
9
+ return execFileSync('git', args, { cwd: root, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim() || null;
10
+ }
11
+ catch {
12
+ return null;
13
+ }
14
+ }
15
+ export function gitHead(root) {
16
+ return git(root, ['rev-parse', 'HEAD']);
17
+ }
18
+ // The full sha of a commit named any way git accepts (a short sha, HEAD, a
19
+ // tag), or null when nothing by that name is a commit here.
20
+ export function gitResolve(root, ref) {
21
+ return git(root, ['rev-parse', '--verify', '--quiet', `${ref}^{commit}`]);
22
+ }
23
+ export function gitTagExists(root, tag) {
24
+ return git(root, ['rev-parse', '--verify', '--quiet', `refs/tags/${tag}`]) !== null;
25
+ }
26
+ export function gitTag(root, tag, sha) {
27
+ return git(root, ['tag', tag, sha]) !== null || gitTagExists(root, tag);
28
+ }
29
+ // Who git says is committing here: the repository's configured user, falling
30
+ // back to the global one the way git itself does. Null outside a repository
31
+ // or where no name is set.
32
+ export function gitUser(root) {
33
+ if (gitHead(root) === null && git(root, ['rev-parse', '--git-dir']) === null)
34
+ return null;
35
+ const name = git(root, ['config', 'user.name']);
36
+ if (name === null)
37
+ return null;
38
+ return { name, email: git(root, ['config', 'user.email']) };
39
+ }
40
+ // The commits that touched one file, newest first, following renames.
41
+ export function gitLog(root, file, limit = 50) {
42
+ const out = git(root, ['log', `--max-count=${limit}`, '--follow', '--format=%H%x1f%h%x1f%an%x1f%ae%x1f%aI%x1f%s', '--', file]);
43
+ if (out === null)
44
+ return [];
45
+ return out
46
+ .split('\n')
47
+ .map((line) => line.split('\x1f'))
48
+ .filter((parts) => parts.length === 6)
49
+ .map(([sha, short, author, email, date, subject]) => ({ sha: sha ?? '', short: short ?? '', author: author ?? '', email: email ?? '', date: date ?? '', subject: subject ?? '' }));
50
+ }
51
+ // The file as one commit held it, or null when that commit has no such file.
52
+ export function gitShowFile(root, sha, file) {
53
+ try {
54
+ return execFileSync('git', ['show', `${sha}:${file.split(path.sep).join('/')}`], { cwd: root, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] });
55
+ }
56
+ catch {
57
+ return null;
58
+ }
59
+ }
@@ -2,8 +2,8 @@
2
2
  // `summary`, kept inside the YAML framework document under `index`, one list
3
3
  // per collection, regenerated wholesale. `eidos index` rewrites that key and
4
4
  // nothing else in the document, so the comments and layout a person chose
5
- // survive. (A markdown root keeps a per-collection `index.md` instead; the
6
- // CLI converts such a root rather than maintaining that form.)
5
+ // survive. (A 4.x markdown root kept a per-collection `index.md` instead;
6
+ // `migrate` converts such a root.)
7
7
  import { existsSync, readFileSync, writeFileSync } from 'node:fs';
8
8
  import path from 'node:path';
9
9
  import { blueprintId, blueprintTitle, propertyString } from './blueprint.js';
@@ -1,14 +1,14 @@
1
- // The actor: `_eidos/me.md`, personal and gitignored, naming a role from
2
- // `_eidos/roles/` and calibrating it on three axes.
1
+ // The actor: `.eidos/me.md`, personal and gitignored, naming a role from
2
+ // `.eidos/roles/` and calibrating it on three axes.
3
3
  import { existsSync, readdirSync, readFileSync, writeFileSync } from 'node:fs';
4
4
  import path from 'node:path';
5
5
  import { headings, section } from './markdown.js';
6
6
  import { kebab } from './naming.js';
7
7
  export function rolesDir(root) {
8
- return path.join(root, '_eidos', 'roles');
8
+ return path.join(root, '.eidos', 'roles');
9
9
  }
10
10
  export function meFile(root) {
11
- return path.join(root, '_eidos', 'me.md');
11
+ return path.join(root, '.eidos', 'me.md');
12
12
  }
13
13
  export function listRoles(root) {
14
14
  const dir = rolesDir(root);
@@ -27,6 +27,11 @@ export function listRoles(root) {
27
27
  return { name: entry.replace(/\.md$/i, ''), file, title, summary };
28
28
  });
29
29
  }
30
+ // A role's file name is its kebab-case name, one level under roles/.
31
+ export const isRoleName = (name) => /^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(name);
32
+ export function roleFile(root, name) {
33
+ return path.join(rolesDir(root), `${name}.md`);
34
+ }
30
35
  export function findRole(root, name) {
31
36
  const wanted = kebab(name);
32
37
  return listRoles(root).find((role) => role.name === wanted || kebab(role.title) === wanted) ?? null;
@@ -65,13 +70,16 @@ export function parseActor(text) {
65
70
  actor.capacity = axis('Technical capacity');
66
71
  return actor;
67
72
  }
68
- export function renderActor(role, custom, axes) {
69
- const lines = ['# Me', '', 'Personal and per-actor — gitignored, never shared. The agent reads it before acting.', ''];
73
+ // `note` is anything the person adds under their role, in their own words.
74
+ export function renderActor(role, custom, axes, note = null) {
75
+ const lines = ['# Me', '', 'Personal and per-actor, gitignored, never shared. The agent reads it before acting.', ''];
70
76
  if (role) {
71
- lines.push(`## You are: ${role.title}`, '', `Role: [_eidos/roles/${role.name}.md](roles/${role.name}.md).`, '');
77
+ lines.push(`## You are: ${role.title}`, '', `Role: [.eidos/roles/${role.name}.md](roles/${role.name}.md).`, '');
78
+ if (note)
79
+ lines.push(note, '');
72
80
  }
73
81
  else if (custom) {
74
- lines.push(`## You are: ${custom}`, '', 'A custom role: describe how you want to be helped here.', '');
82
+ lines.push(`## You are: ${custom}`, '', note ?? 'A custom role: describe how you want to be helped here.', '');
75
83
  }
76
84
  else {
77
85
  lines.push('## You are:', '', '<!-- Name your role, or pick one with `eidos whoami --role <role>`. -->', '');