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,56 @@
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.
4
+ import { existsSync, readFileSync } from 'node:fs';
5
+ import path from 'node:path';
6
+ import { parseFrameworkMarkdown } from './framework-markdown.js';
7
+ import { parseFrameworkStructured } from './framework-structured.js';
8
+ import { appliesTo, FRAMEWORK_DIR, FRAMEWORK_FILES, FrameworkError } from './framework-model.js';
9
+ import { kebab } from './naming.js';
10
+ export * from './framework-model.js';
11
+ // The one framework document in a folder's `_eidos/`, or null; two is an error.
12
+ export function findFrameworkFile(dir) {
13
+ const present = FRAMEWORK_FILES.map((candidate) => ({ file: path.join(dir, FRAMEWORK_DIR, candidate.name), format: candidate.format })).filter((candidate) => existsSync(candidate.file));
14
+ if (present.length > 1) {
15
+ throw new FrameworkError(`${path.join(dir, FRAMEWORK_DIR)} holds more than one framework document (${present.map((candidate) => path.basename(candidate.file)).join(', ')}); keep exactly one`);
16
+ }
17
+ return present[0] ?? null;
18
+ }
19
+ export function isRoot(dir) {
20
+ return FRAMEWORK_FILES.some((candidate) => existsSync(path.join(dir, FRAMEWORK_DIR, candidate.name)));
21
+ }
22
+ export function parseFramework(text, format, root, file) {
23
+ if (format === 'md') {
24
+ return parseFrameworkMarkdown(text, root, file);
25
+ }
26
+ return parseFrameworkStructured(text, root, file);
27
+ }
28
+ export function loadFramework(root) {
29
+ const found = findFrameworkFile(root);
30
+ if (!found) {
31
+ throw new FrameworkError(`${path.join(root, FRAMEWORK_DIR)} holds no framework document (Framework.md or Framework.yaml)`);
32
+ }
33
+ return parseFramework(readFileSync(found.file, 'utf8'), found.format, root, found.file);
34
+ }
35
+ export function findCollection(framework, name) {
36
+ const wanted = name.toLowerCase();
37
+ return (framework.collections.find((collection) => collection.name.toLowerCase() === wanted) ??
38
+ framework.collections.find((collection) => kebab(collection.name) === kebab(name)) ??
39
+ null);
40
+ }
41
+ export function findFlavor(collection, name) {
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)) ??
45
+ null);
46
+ }
47
+ export function defaultFlavor(collection) {
48
+ return collection.flavors.find((flavor) => flavor.isDefault) ?? collection.flavors[0] ?? null;
49
+ }
50
+ // The properties a blueprint in this collection carries, core first.
51
+ export function propertiesFor(framework, collectionName) {
52
+ return [...framework.schema.core, ...framework.schema.custom.filter((property) => appliesTo(property, collectionName))];
53
+ }
54
+ export function shapePath(framework, flavor) {
55
+ return path.join(framework.root, FRAMEWORK_DIR, flavor.shape);
56
+ }
@@ -0,0 +1,116 @@
1
+ // Reading and writing a blueprint's frontmatter. Parsing is real YAML, because
2
+ // list properties hold quoted markdown links; writing uses the flat house style
3
+ // every seed and scaffold already uses, so a generated file diffs cleanly
4
+ // against a hand-written one.
5
+ import { parse as parseYaml } from 'yaml';
6
+ import { splitFrontmatter } from './markdown.js';
7
+ // One `key: value` per line, read without YAML: what an unquoted summary with
8
+ // a colon in it still means to a person, and what Obsidian will refuse to show.
9
+ function parseLeniently(frontmatter) {
10
+ const out = {};
11
+ let found = false;
12
+ for (const raw of frontmatter.split('\n')) {
13
+ const line = raw.replace(/\s+$/, '');
14
+ if (line === '' || /^[\s#-]/.test(line)) {
15
+ continue;
16
+ }
17
+ const colon = line.indexOf(':');
18
+ if (colon === -1) {
19
+ continue;
20
+ }
21
+ const key = line.slice(0, colon).trim();
22
+ const value = line.slice(colon + 1).trim();
23
+ if (key === '') {
24
+ continue;
25
+ }
26
+ found = true;
27
+ let parsed = value;
28
+ try {
29
+ parsed = value === '' ? null : parseYaml(value);
30
+ }
31
+ catch {
32
+ parsed = value;
33
+ }
34
+ if (typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed)) {
35
+ parsed = value;
36
+ }
37
+ out[key] = parsed;
38
+ }
39
+ return found ? out : null;
40
+ }
41
+ export function parseFrontmatter(text) {
42
+ const { frontmatter, body } = splitFrontmatter(text);
43
+ if (frontmatter === null) {
44
+ return { properties: null, body, error: null };
45
+ }
46
+ let problem;
47
+ try {
48
+ const parsed = parseYaml(frontmatter);
49
+ if (parsed === null || parsed === undefined) {
50
+ return { properties: {}, body, error: null };
51
+ }
52
+ if (typeof parsed === 'object' && !Array.isArray(parsed)) {
53
+ return { properties: parsed, body, error: null };
54
+ }
55
+ problem = 'frontmatter is not a mapping of properties';
56
+ }
57
+ catch (cause) {
58
+ problem = describeYamlError(cause, frontmatter);
59
+ }
60
+ return { properties: parseLeniently(frontmatter), body, error: problem };
61
+ }
62
+ // The parser's first line, with the offending property named when the error
63
+ // points at a line, so the fix ("quote the summary") is obvious.
64
+ function describeYamlError(cause, frontmatter) {
65
+ const raw = cause instanceof Error ? cause.message : String(cause);
66
+ const firstLine = (raw.split('\n')[0] ?? raw).replace(/\s+at line \d+, column \d+:?$/, '').replace(/:$/, '');
67
+ const at = /at line (\d+)/.exec(raw);
68
+ let key = null;
69
+ if (at && at[1]) {
70
+ const line = frontmatter.split('\n')[Number.parseInt(at[1], 10) - 1] ?? '';
71
+ const match = /^([^\s:#][^:]*):/.exec(line);
72
+ key = match && match[1] ? match[1].trim() : null;
73
+ }
74
+ const subject = key ? `\`${key}\` is not valid YAML` : 'the frontmatter is not valid YAML';
75
+ return `${subject} (${firstLine}); read leniently here, but Obsidian will not show these properties. Quote the value`;
76
+ }
77
+ function needsQuotes(value) {
78
+ if (value === '') {
79
+ return false;
80
+ }
81
+ if (/^[\s"'[\]{}#&*!|>%@`,?:-]/.test(value) || /[:#]\s/.test(value) || /\s$/.test(value)) {
82
+ return true;
83
+ }
84
+ return /^(true|false|null|yes|no|on|off|~)$/i.test(value) || /^[-+]?(\d[\d_]*\.?\d*|\.\d+)([eE][-+]?\d+)?$/.test(value);
85
+ }
86
+ function quote(value) {
87
+ return '"' + value.replace(/\\/g, '\\\\').replace(/"/g, '\\"') + '"';
88
+ }
89
+ export function formatScalar(value) {
90
+ if (value === null || value === undefined) {
91
+ return '';
92
+ }
93
+ if (typeof value === 'string') {
94
+ return needsQuotes(value) ? quote(value) : value;
95
+ }
96
+ if (typeof value === 'number' || typeof value === 'boolean') {
97
+ return String(value);
98
+ }
99
+ return quote(JSON.stringify(value));
100
+ }
101
+ // `key: value` per line; lists in flow style (`[]`, `["a", "b"]`); an empty
102
+ // value leaves the key alone with nothing after the colon.
103
+ export function formatFrontmatter(entries) {
104
+ const lines = ['---'];
105
+ for (const [key, value] of entries) {
106
+ if (Array.isArray(value)) {
107
+ const items = value.map((item) => (typeof item === 'string' ? quote(item) : formatScalar(item)));
108
+ lines.push(`${key}: [${items.join(', ')}]`);
109
+ continue;
110
+ }
111
+ const scalar = formatScalar(value);
112
+ lines.push(scalar === '' ? `${key}:` : `${key}: ${scalar}`);
113
+ }
114
+ lines.push('---');
115
+ return lines.join('\n') + '\n';
116
+ }
@@ -0,0 +1,83 @@
1
+ // The generated index: every collection's blueprints with their one-line
2
+ // `summary`, kept inside the YAML framework document under `index`, one list
3
+ // per collection, regenerated wholesale. `eidos index` rewrites that key and
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.)
7
+ import { existsSync, readFileSync, writeFileSync } from 'node:fs';
8
+ import path from 'node:path';
9
+ import { blueprintId, blueprintTitle, propertyString } from './blueprint.js';
10
+ import { embeddedIndexOf, withEmbeddedIndex } from './framework-structured.js';
11
+ import { compareLower } from './naming.js';
12
+ // One collection's entries: flat blueprints first, then each group, titles
13
+ // sorted the way a person would scan them. A missing summary is null, never invented.
14
+ export function renderEmbeddedEntries(collection, blueprints) {
15
+ const own = blueprints.filter((blueprint) => blueprint.collection === collection);
16
+ const toEntry = (blueprint) => {
17
+ const summary = (propertyString(blueprint, 'summary') ?? '').trim();
18
+ const entry = {
19
+ id: blueprintId(blueprint),
20
+ title: blueprintTitle(blueprint),
21
+ summary: summary === '' ? null : summary,
22
+ path: blueprint.group === null ? blueprint.file : `${blueprint.group}/${blueprint.file}`,
23
+ };
24
+ if (blueprint.group !== null) {
25
+ entry.group = blueprint.group;
26
+ }
27
+ return entry;
28
+ };
29
+ const flat = own.filter((blueprint) => blueprint.group === null).sort((a, b) => compareLower(blueprintTitle(a), blueprintTitle(b)));
30
+ const grouped = own
31
+ .filter((blueprint) => blueprint.group !== null)
32
+ .sort((a, b) => compareLower(a.group ?? '', b.group ?? '') || compareLower(blueprintTitle(a), blueprintTitle(b)));
33
+ return [...flat, ...grouped].map(toEntry);
34
+ }
35
+ export function renderEmbeddedIndex(framework, blueprints, collections = framework.collections) {
36
+ const index = {};
37
+ for (const collection of collections) {
38
+ index[collection.name] = renderEmbeddedEntries(collection, blueprints);
39
+ }
40
+ return index;
41
+ }
42
+ function sameEntries(a, b) {
43
+ return JSON.stringify(a ?? null) === JSON.stringify(b);
44
+ }
45
+ // Compare each collection's entries with what the document holds, and rewrite
46
+ // the document's `index` once if anything moved. Collections outside
47
+ // `collections` keep the entries they had.
48
+ export function buildIndexes(framework, blueprints, options) {
49
+ const targets = options.collections ?? framework.collections;
50
+ const source = readFileSync(framework.file, 'utf8');
51
+ const current = embeddedIndexOf(source) ?? {};
52
+ const wanted = renderEmbeddedIndex(framework, blueprints, targets);
53
+ const results = [];
54
+ let changed = false;
55
+ for (const collection of targets) {
56
+ const own = blueprints.filter((blueprint) => blueprint.collection === collection);
57
+ const result = {
58
+ collection,
59
+ file: framework.file,
60
+ count: own.length,
61
+ missingSummary: own.filter((blueprint) => (propertyString(blueprint, 'summary') ?? '').trim() === '').map((blueprint) => blueprint.rel),
62
+ status: 'current',
63
+ };
64
+ if (!existsSync(path.join(framework.root, collection.name))) {
65
+ result.status = 'folder-missing';
66
+ }
67
+ else if (!sameEntries(current[collection.name], wanted[collection.name] ?? [])) {
68
+ result.status = options.check ? 'stale' : 'written';
69
+ changed = true;
70
+ }
71
+ results.push(result);
72
+ }
73
+ if (changed && !options.check) {
74
+ const merged = { ...current };
75
+ for (const collection of targets) {
76
+ if (existsSync(path.join(framework.root, collection.name))) {
77
+ merged[collection.name] = wanted[collection.name] ?? [];
78
+ }
79
+ }
80
+ writeFileSync(framework.file, withEmbeddedIndex(source, merged), 'utf8');
81
+ }
82
+ return results;
83
+ }
@@ -0,0 +1,100 @@
1
+ // Small markdown readers shared by the framework, shape, and blueprint parsers:
2
+ // splitting frontmatter from body, and finding headings and sections outside
3
+ // code fences and HTML comments.
4
+ export function normalizeNewlines(text) {
5
+ return text.replace(/\r\n?/g, '\n');
6
+ }
7
+ // A leading `---` block. Returns null frontmatter when the file has none; the
8
+ // body is everything after the closing fence (or the whole text).
9
+ export function splitFrontmatter(text) {
10
+ const lines = normalizeNewlines(text).split('\n');
11
+ if (lines[0]?.trim() !== '---') {
12
+ return { frontmatter: null, body: lines.join('\n') };
13
+ }
14
+ for (let index = 1; index < lines.length; index += 1) {
15
+ if (lines[index]?.trim() === '---') {
16
+ return {
17
+ frontmatter: lines.slice(1, index).join('\n'),
18
+ body: lines.slice(index + 1).join('\n'),
19
+ };
20
+ }
21
+ }
22
+ return { frontmatter: null, body: lines.join('\n') };
23
+ }
24
+ // Every line with a flag saying whether it sits inside a fenced code block or
25
+ // an HTML comment, so a `## heading` quoted in guidance is not read as one.
26
+ function visibleLines(text) {
27
+ const out = [];
28
+ let inFence = false;
29
+ let inComment = false;
30
+ for (const line of normalizeNewlines(text).split('\n')) {
31
+ const trimmed = line.trim();
32
+ if (!inComment && /^(```|~~~)/.test(trimmed)) {
33
+ inFence = !inFence;
34
+ out.push({ line, visible: false });
35
+ continue;
36
+ }
37
+ if (inFence) {
38
+ out.push({ line, visible: false });
39
+ continue;
40
+ }
41
+ if (!inComment && trimmed.startsWith('<!--')) {
42
+ inComment = !trimmed.includes('-->');
43
+ out.push({ line, visible: false });
44
+ continue;
45
+ }
46
+ if (inComment) {
47
+ if (trimmed.includes('-->')) {
48
+ inComment = false;
49
+ }
50
+ out.push({ line, visible: false });
51
+ continue;
52
+ }
53
+ out.push({ line, visible: true });
54
+ }
55
+ return out;
56
+ }
57
+ export function headings(text) {
58
+ const found = [];
59
+ visibleLines(text).forEach(({ line, visible }, index) => {
60
+ if (!visible) {
61
+ return;
62
+ }
63
+ const match = /^(#{1,6})\s+(.+?)\s*#*\s*$/.exec(line);
64
+ if (match && match[1] && match[2]) {
65
+ found.push({ level: match[1].length, text: match[2].trim(), line: index + 1 });
66
+ }
67
+ });
68
+ return found;
69
+ }
70
+ // The sections at one heading level: each heading with the text beneath it, up
71
+ // to the next heading of the same or a higher level.
72
+ export function sections(text, level) {
73
+ const lines = normalizeNewlines(text).split('\n');
74
+ const all = headings(text);
75
+ const out = [];
76
+ all.forEach((heading, index) => {
77
+ if (heading.level !== level) {
78
+ return;
79
+ }
80
+ let end = lines.length;
81
+ for (let next = index + 1; next < all.length; next += 1) {
82
+ const candidate = all[next];
83
+ if (candidate && candidate.level <= level) {
84
+ end = candidate.line - 1;
85
+ break;
86
+ }
87
+ }
88
+ out.push({ heading, body: lines.slice(heading.line, end).join('\n').trim() });
89
+ });
90
+ return out;
91
+ }
92
+ // The body of the first section with this heading text at this level.
93
+ export function section(text, level, headingText) {
94
+ const wanted = headingText.trim().toLowerCase();
95
+ const found = sections(text, level).find((candidate) => candidate.heading.text.toLowerCase() === wanted);
96
+ return found ? found.body : null;
97
+ }
98
+ export function stripInlineCode(text) {
99
+ return text.replace(/`([^`]*)`/g, '$1');
100
+ }
@@ -0,0 +1,89 @@
1
+ // The actor: `_eidos/me.md`, personal and gitignored, naming a role from
2
+ // `_eidos/roles/` and calibrating it on three axes.
3
+ import { existsSync, readdirSync, readFileSync, writeFileSync } from 'node:fs';
4
+ import path from 'node:path';
5
+ import { headings, section } from './markdown.js';
6
+ import { kebab } from './naming.js';
7
+ export function rolesDir(root) {
8
+ return path.join(root, '_eidos', 'roles');
9
+ }
10
+ export function meFile(root) {
11
+ return path.join(root, '_eidos', 'me.md');
12
+ }
13
+ export function listRoles(root) {
14
+ const dir = rolesDir(root);
15
+ if (!existsSync(dir)) {
16
+ return [];
17
+ }
18
+ return readdirSync(dir)
19
+ .filter((entry) => /\.md$/i.test(entry) && entry.toLowerCase() !== 'readme.md' && !entry.startsWith('.'))
20
+ .sort()
21
+ .map((entry) => {
22
+ const file = path.join(dir, entry);
23
+ const text = readFileSync(file, 'utf8');
24
+ const title = headings(text).find((heading) => heading.level === 1)?.text ?? entry.replace(/\.md$/i, '');
25
+ const who = section(text, 2, 'Who they are');
26
+ const summary = (who ?? '').split('\n').find((line) => line.trim() !== '')?.trim() ?? '';
27
+ return { name: entry.replace(/\.md$/i, ''), file, title, summary };
28
+ });
29
+ }
30
+ export function findRole(root, name) {
31
+ const wanted = kebab(name);
32
+ return listRoles(root).find((role) => role.name === wanted || kebab(role.title) === wanted) ?? null;
33
+ }
34
+ export function readActor(root) {
35
+ const file = meFile(root);
36
+ if (!existsSync(file)) {
37
+ return { exists: false, text: '', actor: emptyActor() };
38
+ }
39
+ const text = readFileSync(file, 'utf8');
40
+ return { exists: true, text, actor: parseActor(text) };
41
+ }
42
+ function emptyActor() {
43
+ return { role: null, roleFile: null, note: null, ownership: null, experience: null, capacity: null };
44
+ }
45
+ export function parseActor(text) {
46
+ const actor = emptyActor();
47
+ const youAre = headings(text).find((heading) => heading.level === 2 && /^you are\b/i.test(heading.text));
48
+ if (youAre) {
49
+ const declared = youAre.text.replace(/^you are\s*:?\s*/i, '').trim();
50
+ actor.role = declared === '' ? null : declared;
51
+ const body = section(text, 2, youAre.text) ?? '';
52
+ const link = /\[[^\]]*\]\(([^)]+)\)/.exec(body);
53
+ actor.roleFile = link && link[1] ? link[1] : null;
54
+ const note = body.replace(/^Role:.*$/m, '').trim();
55
+ actor.note = note === '' ? null : note;
56
+ }
57
+ const calibration = section(text, 2, 'Calibration') ?? '';
58
+ const axis = (label) => {
59
+ const match = new RegExp(`^-[ \\t]+\\*\\*${label}[^*\\n]*\\*\\*[ \\t]*:?[ \\t]*(.*)$`, 'im').exec(calibration);
60
+ const value = match && match[1] ? match[1].trim() : '';
61
+ return value === '' ? null : value;
62
+ };
63
+ actor.ownership = axis('Ownership');
64
+ actor.experience = axis('Experience');
65
+ actor.capacity = axis('Technical capacity');
66
+ return actor;
67
+ }
68
+ export function renderActor(role, custom, axes) {
69
+ const lines = ['# Me', '', 'Personal and per-actor — gitignored, never shared. The agent reads it before acting.', ''];
70
+ if (role) {
71
+ lines.push(`## You are: ${role.title}`, '', `Role: [_eidos/roles/${role.name}.md](roles/${role.name}.md).`, '');
72
+ }
73
+ else if (custom) {
74
+ lines.push(`## You are: ${custom}`, '', 'A custom role: describe how you want to be helped here.', '');
75
+ }
76
+ else {
77
+ lines.push('## You are:', '', '<!-- Name your role, or pick one with `eidos whoami --role <role>`. -->', '');
78
+ }
79
+ lines.push('## Calibration', '');
80
+ lines.push(`- **Ownership:** ${axes.ownership ?? ''}`.trimEnd());
81
+ lines.push(`- **Experience with the scope:** ${axes.experience ?? ''}`.trimEnd());
82
+ lines.push(`- **Technical capacity:** ${axes.capacity ?? ''}`.trimEnd());
83
+ return lines.join('\n') + '\n';
84
+ }
85
+ export function writeActor(root, text) {
86
+ const file = meFile(root);
87
+ writeFileSync(file, text, 'utf8');
88
+ return file;
89
+ }
@@ -0,0 +1,46 @@
1
+ // The framework's naming convention, applied to everything a human reads in the
2
+ // tree: collection folders, group folders, blueprint filenames, and the links
3
+ // that reach them. The id is always kebab-case regardless.
4
+ export const NAMINGS = ['kebab-case', 'TitleCase', 'Title Case'];
5
+ export const DEFAULT_NAMING = 'kebab-case';
6
+ export function isNaming(value) {
7
+ return typeof value === 'string' && NAMINGS.includes(value);
8
+ }
9
+ export function kebab(label) {
10
+ return label
11
+ .toLowerCase()
12
+ .replace(/[^a-z0-9]+/g, '-')
13
+ .replace(/^-+|-+$/g, '');
14
+ }
15
+ export function isKebab(value) {
16
+ return /^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(value);
17
+ }
18
+ // `prior work` -> `Prior Work`; a declared flavor label as a human title.
19
+ export function titleize(label) {
20
+ return label
21
+ .split(' ')
22
+ .map((word) => word.slice(0, 1).toUpperCase() + word.slice(1))
23
+ .join(' ');
24
+ }
25
+ // A Title Case display name in the framework's convention.
26
+ export function convert(display, naming) {
27
+ if (naming === 'TitleCase') {
28
+ return display.split(/\s+/).join('');
29
+ }
30
+ if (naming === 'kebab-case') {
31
+ return kebab(display);
32
+ }
33
+ return display;
34
+ }
35
+ // Compare the way Python sorts lowercased strings: by code point, not locale.
36
+ export function compareLower(a, b) {
37
+ const left = a.toLowerCase();
38
+ const right = b.toLowerCase();
39
+ if (left < right) {
40
+ return -1;
41
+ }
42
+ if (left > right) {
43
+ return 1;
44
+ }
45
+ return 0;
46
+ }
@@ -0,0 +1,88 @@
1
+ // Finding the root: the one folder with `_eidos/` inside it, never a folder
2
+ // with a particular name. Looks at the starting folder, then its ancestors,
3
+ // then one and two levels beneath it, so `eidos check` works from a repo root
4
+ // that keeps its blueprints in `Blueprints/`.
5
+ import { readdirSync, statSync } from 'node:fs';
6
+ import path from 'node:path';
7
+ import { FRAMEWORK_DIR, isRoot } from './framework.js';
8
+ const SKIPPED_DIRS = new Set(['node_modules', 'dist', 'build', 'out', 'target', 'vendor']);
9
+ export class RootError extends Error {
10
+ candidates;
11
+ constructor(message, candidates = []) {
12
+ super(message);
13
+ this.name = 'RootError';
14
+ this.candidates = candidates;
15
+ }
16
+ }
17
+ function childDirs(dir) {
18
+ let entries;
19
+ try {
20
+ entries = readdirSync(dir);
21
+ }
22
+ catch {
23
+ return [];
24
+ }
25
+ const out = [];
26
+ for (const entry of entries) {
27
+ if (entry.startsWith('.') || entry === FRAMEWORK_DIR || SKIPPED_DIRS.has(entry)) {
28
+ continue;
29
+ }
30
+ const full = path.join(dir, entry);
31
+ try {
32
+ if (statSync(full).isDirectory()) {
33
+ out.push(full);
34
+ }
35
+ }
36
+ catch {
37
+ // unreadable entries are simply not candidates
38
+ }
39
+ }
40
+ return out.sort();
41
+ }
42
+ // Every root at or beneath `dir`, at most two levels down.
43
+ export function rootsBeneath(dir) {
44
+ const found = [];
45
+ if (isRoot(dir)) {
46
+ found.push(dir);
47
+ }
48
+ for (const child of childDirs(dir)) {
49
+ if (isRoot(child)) {
50
+ found.push(child);
51
+ continue;
52
+ }
53
+ for (const grandchild of childDirs(child)) {
54
+ if (isRoot(grandchild)) {
55
+ found.push(grandchild);
56
+ }
57
+ }
58
+ }
59
+ return found;
60
+ }
61
+ export function findRoot(start, explicit) {
62
+ if (explicit !== undefined) {
63
+ const resolved = path.resolve(start, explicit);
64
+ if (!isRoot(resolved)) {
65
+ throw new RootError(`${resolved} is not a root: no ${FRAMEWORK_DIR}/Framework.md inside it`);
66
+ }
67
+ return resolved;
68
+ }
69
+ let current = path.resolve(start);
70
+ for (;;) {
71
+ if (isRoot(current)) {
72
+ return current;
73
+ }
74
+ const parent = path.dirname(current);
75
+ if (parent === current) {
76
+ break;
77
+ }
78
+ current = parent;
79
+ }
80
+ const beneath = rootsBeneath(path.resolve(start));
81
+ if (beneath.length === 1 && beneath[0]) {
82
+ return beneath[0];
83
+ }
84
+ if (beneath.length > 1) {
85
+ throw new RootError(`several roots beneath ${path.resolve(start)}; pass --root to pick one:\n` + beneath.map((candidate) => ` ${candidate}`).join('\n'), beneath);
86
+ }
87
+ throw new RootError(`no root found from ${path.resolve(start)}: nothing here has a ${FRAMEWORK_DIR}/ folder. Run \`eidos init\` to create one, or pass --root.`);
88
+ }