@turri-dev/cli 0.1.13 → 0.1.14-next.678847f

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 (42) hide show
  1. package/dist/extension.d.ts +5 -1
  2. package/dist/extension.js +28 -12
  3. package/dist/host.d.ts +6 -2
  4. package/dist/host.js +10 -2
  5. package/dist/new-project.d.ts +7 -0
  6. package/dist/new-project.js +37 -0
  7. package/dist/plain/concepts.d.ts +18 -0
  8. package/dist/plain/concepts.js +41 -14
  9. package/dist/plain/files.d.ts +2 -0
  10. package/dist/plain/files.js +7 -3
  11. package/dist/plain/graph.d.ts +68 -0
  12. package/dist/plain/graph.js +136 -0
  13. package/dist/plain/module-concepts.d.ts +32 -0
  14. package/dist/plain/module-concepts.js +79 -0
  15. package/dist/plain/parser.d.ts +9 -0
  16. package/dist/plain/parser.js +36 -22
  17. package/dist/plain/stdlib.d.ts +2 -0
  18. package/dist/plain/stdlib.js +5 -1
  19. package/dist/regenerate/requires.d.ts +2 -0
  20. package/dist/regenerate/requires.js +2 -1
  21. package/dist/setup.d.ts +1 -0
  22. package/dist/setup.js +1 -1
  23. package/dist/skills.d.ts +10 -0
  24. package/dist/skills.js +20 -3
  25. package/dist/stack.d.ts +10 -2
  26. package/dist/stack.js +17 -11
  27. package/dist/stdlib.d.ts +16 -0
  28. package/dist/stdlib.js +27 -9
  29. package/docs/authoring-plain.md +4 -4
  30. package/docs/hosting.md +24 -0
  31. package/docs/installation.md +1 -1
  32. package/docs/parser.md +1 -0
  33. package/docs/skills-and-gates.md +1 -1
  34. package/docs/using.md +3 -1
  35. package/genome/rules/import-modules.md +1 -0
  36. package/genome/rules/integration-embedded.md +1 -1
  37. package/genome/skills/break-down-func-spec/SKILL.md +1 -1
  38. package/genome/skills/refactor-module/SKILL.md +2 -2
  39. package/package.json +1 -1
  40. package/genome/skills/check-plain-env/SKILL.md +0 -285
  41. package/genome/skills/create-import-module/SKILL.md +0 -104
  42. package/genome/skills/create-requires-module/SKILL.md +0 -109
@@ -0,0 +1,79 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { basename } from "node:path";
3
+ import { resolveTemplateDirs } from "../regenerate/paths.js";
4
+ import { DEFAULT_CONCEPT_DEFINITIONS, definitionText, extractConceptsFromDefinition } from "./concepts.js";
5
+ import { renderToMarkdown } from "./markdown.js";
6
+ import { exportedDefinitionsOf, IMPORT_DIRECTIVE, parsePlainFile, parsePlainSource, PLAIN_SOURCE_FILE_EXTENSION, readPlainSourceMetadata, REQUIRES_DIRECTIVE, restoreStrippedLines, } from "./parser.js";
7
+ import { DEFINITIONS } from "./spec.js";
8
+ /**
9
+ * A module read for what it can name, as the parser builds its definitions: its own, its imports' (`import`), and its
10
+ * required modules' `exported_concepts` with the definitions they use. Never throws: a module that does not parse
11
+ * gives its own definitions read without its imports, and what of those it names is there.
12
+ */
13
+ export function readModule(moduleName, plainFile, templateDirs) {
14
+ const parse = (name) => attempt(() => parsePlainFile(name, {}, templateDirs, [], []));
15
+ const definitions = [];
16
+ const add = (result, nodes, module) => {
17
+ for (const node of nodes)
18
+ definitions.push({ node, module: result.importedFrom.get(node) ?? module });
19
+ };
20
+ const definitionsOf = (result) => result.plainSource[DEFINITIONS]?.children ?? [];
21
+ let requires;
22
+ let parsed = parse(moduleName);
23
+ if (parsed !== undefined) {
24
+ add(parsed, definitionsOf(parsed), moduleName);
25
+ requires = parsed.requiredModules;
26
+ }
27
+ else {
28
+ // What its front matter names, each read on its own, and its own text without them.
29
+ const text = attempt(() => readFileSync(plainFile, "utf8"));
30
+ const front = text === undefined ? undefined : attempt(() => readPlainSourceMetadata(text));
31
+ for (const name of namesIn(front?.metadata[IMPORT_DIRECTIVE])) {
32
+ const imported = parse(name);
33
+ if (imported !== undefined)
34
+ add(imported, definitionsOf(imported), name);
35
+ }
36
+ parsed = front && attempt(() => parsePlainSource(restoreStrippedLines(text, front.content), {}, templateDirs, [], [moduleName]));
37
+ if (parsed !== undefined)
38
+ add(parsed, definitionsOf(parsed), moduleName);
39
+ requires = namesIn(front?.metadata[REQUIRES_DIRECTIVE]);
40
+ }
41
+ for (const name of requires) {
42
+ const required = parse(name);
43
+ const exported = required && attempt(() => exportedDefinitionsOf(required));
44
+ if (required !== undefined && exported !== undefined)
45
+ add(required, exported, name);
46
+ }
47
+ return { ...(parsed !== undefined ? { parsed } : {}), definitions };
48
+ }
49
+ /**
50
+ * Every concept a module can name: its own definitions, those its imports bring, those its required modules export,
51
+ * and ***plain's own - read as the parser reads them. A module that does not parse gives what can be read of it.
52
+ */
53
+ export function conceptsOf(plainFile, templateDirs = resolveTemplateDirs(plainFile)) {
54
+ const found = [];
55
+ const seen = new Set();
56
+ for (const { node, module } of readModule(basename(plainFile, PLAIN_SOURCE_FILE_EXTENSION), plainFile, templateDirs).definitions) {
57
+ const text = renderToMarkdown(node);
58
+ for (const concept of extractConceptsFromDefinition(text)[0]) {
59
+ if (seen.has(`${concept}\0${module}`))
60
+ continue;
61
+ seen.add(`${concept}\0${module}`);
62
+ found.push({ concept, definition: definitionText(text), module });
63
+ }
64
+ }
65
+ for (const [concept, definition] of Object.entries(DEFAULT_CONCEPT_DEFINITIONS))
66
+ found.push({ concept, definition, module: null });
67
+ return found;
68
+ }
69
+ export function attempt(run) {
70
+ try {
71
+ return run();
72
+ }
73
+ catch {
74
+ return undefined;
75
+ }
76
+ }
77
+ export function namesIn(value) {
78
+ return Array.isArray(value) ? value.filter((name) => typeof name === "string") : [];
79
+ }
@@ -16,6 +16,8 @@ export interface PlainFileParseResult {
16
16
  plainSourceObj: FrontmatterPost;
17
17
  requiredModules: string[];
18
18
  requiredConcepts: string[];
19
+ /** The items its imports brought into its sections, each with the module it is written in. turri's: the Python parser does not keep it. */
20
+ importedFrom: Map<AnyNode, string>;
19
21
  }
20
22
  export declare function getFilenameFromModuleName(moduleName: string): string;
21
23
  export declare function getModuleNameFromFilename(filename: string): string;
@@ -57,9 +59,16 @@ export declare function normalizeLineEndings(plainSourceText: string): string;
57
59
  export declare function restoreStrippedLines(plainSourceText: string, content: string): string;
58
60
  export declare function readPlainSourceMetadata(plainSourceText: string): FrontmatterPost;
59
61
  export declare function parsePlainSource(plainSourceText: string, codeVariables: Record<string, Record<string, string>>, templateDirs: string[], importedModules: string[], modulesTrace: string[]): PlainFileParseResult;
62
+ /**
63
+ * The file a module name is read from, as `readModulePlainSource` reads it - a standard library module's in turri's
64
+ * `stdlib/`, any other's in the first template folder that has it - or null when there is none.
65
+ */
66
+ export declare function modulePlainFile(moduleName: string, templateDirs: string[]): string | null;
60
67
  export declare function readModulePlainSource(moduleName: string, templateDirs: string[]): string;
61
68
  export declare function parsePlainFile(moduleName: string, codeVariables: Record<string, Record<string, string>>, templateDirs: string[], importedModules: string[], modulesTrace: string[]): PlainFileParseResult;
62
69
  export declare function processRequiredModules(requiredModules: string[], codeVariables: Record<string, Record<string, string>>, templateDirs: string[], allRequiredModules: string[], modulesTrace: string[]): AnyNode[];
70
+ /** The definitions a required module exports: those of its `exported_concepts`, with the definitions they use. */
71
+ export declare function exportedDefinitionsOf(result: PlainFileParseResult): AnyNode[];
63
72
  export declare function processExportedDefinitions(plainSource: ParsedPlainSource, exportedDefinitions: AnyNode[]): void;
64
73
  /**
65
74
  * Parse a .plain file into the marshalled tree.
@@ -4,9 +4,9 @@ import { extname, isAbsolute, basename, resolve } from "node:path";
4
4
  import { load as loadYaml } from "js-yaml";
5
5
  import { extractConceptsFromDefinition, findConceptDefinitionsInPlainSource, sortDefinitions, validateConcepts, DEFAULT_CONCEPTS, } from "./concepts.js";
6
6
  import { ImportedModuleWithFunctionalitiesError, MissingFunctionalitiesError, ModuleDoesNotExistError, PlainSyntaxError, UnsupportedBase64Content, } from "./errors.js";
7
- import { findLargeBase64Blob, getLoadedTemplates, openFrom } from "./files.js";
7
+ import { findIn, findLargeBase64Blob, getLoadedTemplates, openFrom } from "./files.js";
8
8
  import { renderPlainSource } from "./liquid.js";
9
- import { isStdlibModule, readStdlibModule } from "./stdlib.js";
9
+ import { isStdlibModule, readStdlibModule, stdlibModuleFile } from "./stdlib.js";
10
10
  import { getRawText, lineNumber, parseMarkdown, removeQuotes, renderToMarkdown, traverse, } from "./markdown.js";
11
11
  import { ACCEPTANCE_TESTS, ACCEPTANCE_TEST_HEADING, ALLOWED_IMPORT_SPECIFICATION_HEADINGS, ALLOWED_SPECIFICATION_HEADINGS, DEFINITIONS, FUNCTIONAL_REQUIREMENTS, NON_FUNCTIONAL_REQUIREMENTS, TEST_REQUIREMENTS, getSpecificationItems, REQUIRED_FUNCTIONAL_SPECS, } from "./spec.js";
12
12
  export const IMPORT_DIRECTIVE = "import";
@@ -366,7 +366,7 @@ export function readPlainSourceMetadata(plainSourceText) {
366
366
  }
367
367
  return { metadata, content };
368
368
  }
369
- function processImports(plainSource, imports, codeVariables, templateDirs, importedModules, modulesTrace) {
369
+ function processImports(plainSource, imports, codeVariables, templateDirs, importedModules, modulesTrace, importedFrom) {
370
370
  const requiredConcepts = [];
371
371
  for (const moduleName of imports) {
372
372
  if (modulesTrace.includes(moduleName)) {
@@ -380,6 +380,10 @@ function processImports(plainSource, imports, codeVariables, templateDirs, impor
380
380
  "Imported modules may only provide definitions, implementation reqs, and test reqs — " +
381
381
  "use 'requires' instead of 'import' if this module's functionalities should be built on.");
382
382
  }
383
+ for (const section of Object.values(result.plainSource)) {
384
+ for (const item of section?.children ?? [])
385
+ importedFrom.set(item, result.importedFrom.get(item) ?? moduleName);
386
+ }
383
387
  for (const heading of Object.keys(result.plainSource)) {
384
388
  if (!ALLOWED_IMPORT_SPECIFICATION_HEADINGS.includes(heading)) {
385
389
  throw new PlainSyntaxError(`Plain syntax error: Invalid specification heading (\`${heading}\`). ` +
@@ -411,8 +415,9 @@ export function parsePlainSource(plainSourceText, codeVariables, templateDirs, i
411
415
  const plainSourceObj = readPlainSourceMetadata(plainSourceText);
412
416
  const plainSource = newPlainSource();
413
417
  let requiredConcepts = [];
418
+ const importedFrom = new Map();
414
419
  if (IMPORT_DIRECTIVE in plainSourceObj.metadata) {
415
- requiredConcepts = processImports(plainSource, plainSourceObj.metadata[IMPORT_DIRECTIVE], codeVariables, templateDirs, importedModules, modulesTrace);
420
+ requiredConcepts = processImports(plainSource, plainSourceObj.metadata[IMPORT_DIRECTIVE], codeVariables, templateDirs, importedModules, modulesTrace, importedFrom);
416
421
  }
417
422
  const [, loadedTemplates] = getLoadedTemplates(templateDirs, plainSourceText);
418
423
  const plainSourceContent = restoreStrippedLines(plainSourceText, plainSourceObj.content);
@@ -490,7 +495,14 @@ export function parsePlainSource(plainSourceText, codeVariables, templateDirs, i
490
495
  }
491
496
  }
492
497
  const requiredModules = plainSourceObj.metadata[REQUIRES_DIRECTIVE] ?? [];
493
- return { plainSource, plainSourceObj, requiredModules, requiredConcepts };
498
+ return { plainSource, plainSourceObj, requiredModules, requiredConcepts, importedFrom };
499
+ }
500
+ /**
501
+ * The file a module name is read from, as `readModulePlainSource` reads it - a standard library module's in turri's
502
+ * `stdlib/`, any other's in the first template folder that has it - or null when there is none.
503
+ */
504
+ export function modulePlainFile(moduleName, templateDirs) {
505
+ return isStdlibModule(moduleName) ? stdlibModuleFile(moduleName) : findIn(templateDirs, moduleName + PLAIN_SOURCE_FILE_EXTENSION);
494
506
  }
495
507
  export function readModulePlainSource(moduleName, templateDirs) {
496
508
  // `stdlib/...` is turri's standard library, and only that - see stdlib.ts.
@@ -536,27 +548,29 @@ export function processRequiredModules(requiredModules, codeVariables, templateD
536
548
  moduleName,
537
549
  ]);
538
550
  }
539
- if (EXPORTED_CONCEPTS_DIRECTIVE in result.plainSourceObj.metadata) {
540
- const exportedConcepts = [];
541
- for (const concept of result.plainSourceObj.metadata[EXPORTED_CONCEPTS_DIRECTIVE]) {
542
- if (typeof concept === "string" && DEFAULT_CONCEPTS.has(concept)) {
543
- throw new PlainSyntaxError(`Plain syntax error: Default concept cannot be exported: ${concept}. Only user-defined concepts can be exported.`);
544
- }
545
- if (typeof concept === "string") {
546
- exportedConcepts.push(...extractConceptsFromDefinition(concept)[0]);
547
- }
548
- else {
549
- throw new PlainSyntaxError(`Plain syntax error: Invalid exported concept: ${concept}.`);
550
- }
551
- }
552
- for (const exportedConcept of exportedConcepts) {
553
- exportedDefinitions.push(...findConceptDefinitionsInPlainSource(exportedConcept, result.plainSource[DEFINITIONS] ?? null));
554
- }
555
- }
551
+ exportedDefinitions.push(...exportedDefinitionsOf(result));
556
552
  allRequiredModules.push(moduleName);
557
553
  }
558
554
  return exportedDefinitions;
559
555
  }
556
+ /** The definitions a required module exports: those of its `exported_concepts`, with the definitions they use. */
557
+ export function exportedDefinitionsOf(result) {
558
+ if (!(EXPORTED_CONCEPTS_DIRECTIVE in result.plainSourceObj.metadata))
559
+ return [];
560
+ const exportedConcepts = [];
561
+ for (const concept of result.plainSourceObj.metadata[EXPORTED_CONCEPTS_DIRECTIVE]) {
562
+ if (typeof concept === "string" && DEFAULT_CONCEPTS.has(concept)) {
563
+ throw new PlainSyntaxError(`Plain syntax error: Default concept cannot be exported: ${concept}. Only user-defined concepts can be exported.`);
564
+ }
565
+ if (typeof concept === "string") {
566
+ exportedConcepts.push(...extractConceptsFromDefinition(concept)[0]);
567
+ }
568
+ else {
569
+ throw new PlainSyntaxError(`Plain syntax error: Invalid exported concept: ${concept}.`);
570
+ }
571
+ }
572
+ return exportedConcepts.flatMap((exportedConcept) => findConceptDefinitionsInPlainSource(exportedConcept, result.plainSource[DEFINITIONS] ?? null));
573
+ }
560
574
  export function processExportedDefinitions(plainSource, exportedDefinitions) {
561
575
  if (exportedDefinitions.length === 0)
562
576
  return;
@@ -10,5 +10,7 @@ export declare const STDLIB_PREFIX = "stdlib/";
10
10
  /** The folder `stdlib/` is in: turri's own. */
11
11
  export declare const STDLIB_ROOT: string;
12
12
  export declare function isStdlibModule(moduleName: string): boolean;
13
+ /** A standard library module's file, or null when the standard library has no such module. */
14
+ export declare function stdlibModuleFile(moduleName: string): string | null;
13
15
  /** A standard library module's text - refused when a folder the spec is resolved from has a module of that name too. */
14
16
  export declare function readStdlibModule(moduleName: string, templateDirs: string[]): string;
@@ -2,7 +2,7 @@ import { existsSync, realpathSync } from "node:fs";
2
2
  import { dirname, join } from "node:path";
3
3
  import { fileURLToPath } from "node:url";
4
4
  import { ModuleDoesNotExistError, PlainSyntaxError } from "./errors.js";
5
- import { openFrom } from "./files.js";
5
+ import { findIn, openFrom } from "./files.js";
6
6
  /**
7
7
  * The ***plain standard library's names. `stdlib/` is a reserved prefix: `stdlib/backend/python-fastapi`
8
8
  * is always turri's own template, in its `stdlib/` folder - never a module of the project's. A
@@ -25,6 +25,10 @@ function sameFile(a, b) {
25
25
  return false;
26
26
  }
27
27
  }
28
+ /** A standard library module's file, or null when the standard library has no such module. */
29
+ export function stdlibModuleFile(moduleName) {
30
+ return findIn([STDLIB_ROOT], `${moduleName.replace(/^\.\//, "")}.plain`);
31
+ }
28
32
  /** A standard library module's text - refused when a folder the spec is resolved from has a module of that name too. */
29
33
  export function readStdlibModule(moduleName, templateDirs) {
30
34
  const name = moduleName.replace(/^\.\//, "");
@@ -18,6 +18,8 @@ export interface ChainEntry {
18
18
  requires: string | null;
19
19
  frids: string[];
20
20
  }
21
+ /** A required module's file: the first template folder that has it. */
22
+ export declare function locateModule(moduleName: string, templateDirs: string[]): string;
21
23
  /**
22
24
  * The chain to build, bottom first.
23
25
  *
@@ -8,7 +8,8 @@ import { resolveTemplateDirs } from "./paths.js";
8
8
  import { inheritedRecord, readRecord, RECORD_FILE, writeCodeRecord } from "./records.js";
9
9
  import { head, historyLog, recordHistory } from "./lineage.js";
10
10
  import { listFiles } from "./testing.js";
11
- function locateModule(moduleName, templateDirs) {
11
+ /** A required module's file: the first template folder that has it. */
12
+ export function locateModule(moduleName, templateDirs) {
12
13
  for (const dir of templateDirs) {
13
14
  const candidate = join(dir, moduleName + PLAIN_SOURCE_FILE_EXTENSION);
14
15
  if (existsSync(candidate))
package/dist/setup.d.ts CHANGED
@@ -35,6 +35,7 @@ export declare function checkSandbox(wall?: Wall, platform?: NodeJS.Platform): C
35
35
  export declare function checkProject(project: string, machine?: {
36
36
  has?: (command: string) => boolean;
37
37
  exists?: (path: string) => boolean;
38
+ jdk?: () => boolean;
38
39
  }): Check[];
39
40
  /**
40
41
  * In a project: each model its config names - with the stack chosen, that stack's over it - tried with one short
package/dist/setup.js CHANGED
@@ -66,7 +66,7 @@ export function checkSandbox(wall = sandboxAvailable(), platform = process.platf
66
66
  */
67
67
  export function checkProject(project, machine = {}) {
68
68
  const checks = [];
69
- for (const { module, needs, missing } of stackOnMachine(project, machine.has)) {
69
+ for (const { module, needs, missing } of stackOnMachine(project, machine.has, machine.jdk)) {
70
70
  if (needs.length === 0)
71
71
  continue;
72
72
  checks.push(missing.length === 0
package/dist/skills.d.ts CHANGED
@@ -42,3 +42,13 @@ export declare function createCommandGate(): {
42
42
  };
43
43
  /** What cultivate says of a command-only skill the person did not start. */
44
44
  export declare function startedByCommandOnly(name: string): string;
45
+ /**
46
+ * What the model is told when the person picked a skill: start it now, with what they asked for - word for word, as the
47
+ * message that set off the question - so they never type its command after picking it.
48
+ */
49
+ export declare function startPicked(name: string, request: string): string;
50
+ /**
51
+ * The command-only skill an answer picks: an option offered to the person that starts with its command - `/brood - an
52
+ * interview` - is the person starting it, as if they typed it.
53
+ */
54
+ export declare function chosenCommand(answer: string, skills: ReadonlyArray<Pick<FoundSkill, "name" | "commandOnly">>): string | undefined;
package/dist/skills.js CHANGED
@@ -61,8 +61,8 @@ export function discoverSkills(packageRoot, projectDir) {
61
61
  /** A skill's line in cultivate's list: a project's says so, and where it is. */
62
62
  export function catalogueLine(skill) {
63
63
  const whose = skill.origin === "turri" ? "" : ` (this project's, ${skill.origin.project})`;
64
- // One the person starts themselves: the model points them to its command rather than running it.
65
- const byCommand = skill.commandOnly ? ` Started only by the person, with /${skill.name}: never run it unasked - when it fits, tell them to type /${skill.name}.` : "";
64
+ // One the person starts themselves - typed, or picked among the options the model offers: never run unasked.
65
+ const byCommand = skill.commandOnly ? ` Started only by the person, with /${skill.name} or by picking it when you offer it with ask_user: never run it unasked.` : "";
66
66
  return `- ${skill.name}${whose}: ${skill.description}${byCommand}`;
67
67
  }
68
68
  /** What is said under the list about the skills it leaves out. */
@@ -88,5 +88,22 @@ export function createCommandGate() {
88
88
  }
89
89
  /** What cultivate says of a command-only skill the person did not start. */
90
90
  export function startedByCommandOnly(name) {
91
- return `Not started: ${name} is started only by the person, with /${name}. If it fits what they want, tell them to type /${name} - with what they want after it, if they like.`;
91
+ return `Not started: ${name} is started only by the person, with /${name} or by picking an option starting /${name} that you offer them with ask_user. For a new project, ask them as <new-project> says; otherwise do what they asked yourself, in the conversation.`;
92
+ }
93
+ /**
94
+ * What the model is told when the person picked a skill: start it now, with what they asked for - word for word, as the
95
+ * message that set off the question - so they never type its command after picking it.
96
+ */
97
+ export function startPicked(name, request) {
98
+ const asked = request.trim();
99
+ return (`They picked ${name}, and it may start now: call cultivate with skill "${name}" at once, before anything else, and never ask them to type /${name}. ` +
100
+ `Pass as its context everything they have told you${asked !== "" ? `, starting with what they asked for, word for word: ${JSON.stringify(asked)}` : ""}.`);
101
+ }
102
+ /**
103
+ * The command-only skill an answer picks: an option offered to the person that starts with its command - `/brood - an
104
+ * interview` - is the person starting it, as if they typed it.
105
+ */
106
+ export function chosenCommand(answer, skills) {
107
+ const typed = /^\s*\/([\w-]+)/.exec(answer)?.[1];
108
+ return skills.find((skill) => skill.commandOnly && skill.name === typed)?.name;
92
109
  }
package/dist/stack.d.ts CHANGED
@@ -19,8 +19,11 @@ export declare function projectImplementationReqs(project: string): Array<{
19
19
  module: string;
20
20
  reqs: string[];
21
21
  }>;
22
- /** What a project's modules need of this machine, and which of it the machine lacks. */
23
- export declare function stackOnMachine(project: string, has?: (command: string) => boolean): Array<{
22
+ /**
23
+ * What a project's modules need of this machine, and which of it the machine lacks - `has` finding a command, and on
24
+ * macOS a JDK behind `java` (`toolchainCheck`), asked at most once.
25
+ */
26
+ export declare function stackOnMachine(project: string, has?: (command: string) => boolean, jdk?: () => boolean): Array<{
24
27
  module: string;
25
28
  needs: StackNeed[];
26
29
  missing: StackNeed[];
@@ -34,6 +37,11 @@ export declare const STDLIB_LANGUAGES: Array<{
34
37
  language: string;
35
38
  commands: string[];
36
39
  }>;
40
+ /**
41
+ * Whether a toolchain's command is really there: `has` finds it - and Java has a JDK behind it as well, for macOS keeps a
42
+ * `/usr/bin/java` that only asks to install one.
43
+ */
44
+ export declare function toolchainCheck(has: (command: string) => boolean, jdk?: () => boolean): (command: string) => boolean;
37
45
  /** Which of the standard library's languages this machine can build and test now, and which it lacks a command of. */
38
46
  export declare function languagesOnMachine(has?: (command: string) => boolean): {
39
47
  ready: typeof STDLIB_LANGUAGES;
package/dist/stack.js CHANGED
@@ -57,11 +57,16 @@ export function projectImplementationReqs(project) {
57
57
  }
58
58
  /** Whether a command is on this machine's PATH. */
59
59
  const onPath = (command) => onThisMachine(command) !== undefined;
60
- /** What a project's modules need of this machine, and which of it the machine lacks. */
61
- export function stackOnMachine(project, has = onPath) {
60
+ /**
61
+ * What a project's modules need of this machine, and which of it the machine lacks - `has` finding a command, and on
62
+ * macOS a JDK behind `java` (`toolchainCheck`), asked at most once.
63
+ */
64
+ export function stackOnMachine(project, has = onPath, jdk = jdkHere) {
65
+ let found;
66
+ const here = toolchainCheck(has, () => (found ??= jdk()));
62
67
  return projectImplementationReqs(project).map(({ module, reqs }) => {
63
68
  const needs = stackOf(reqs.join("\n"));
64
- return { module, needs, missing: needs.filter((need) => !need.commands.some(has)) };
69
+ return { module, needs, missing: needs.filter((need) => !need.commands.some(here)) };
65
70
  });
66
71
  }
67
72
  /**
@@ -76,17 +81,18 @@ export const STDLIB_LANGUAGES = [
76
81
  { language: "Rust", commands: ["cargo"] },
77
82
  { language: "Java", commands: ["java"] },
78
83
  ];
84
+ /** Whether this machine has a JDK behind `java`: anywhere but macOS, java found says so; on macOS, `java_home` finding one. */
85
+ function jdkHere() {
86
+ return process.platform !== "darwin" || spawnSync("/usr/libexec/java_home", [], { stdio: "ignore" }).status === 0;
87
+ }
79
88
  /**
80
- * Whether a toolchain's command is really there: on the PATH - and Java on macOS, which keeps a `/usr/bin/java` that only
81
- * asks to install a JDK, by `java_home` finding one.
89
+ * Whether a toolchain's command is really there: `has` finds it - and Java has a JDK behind it as well, for macOS keeps a
90
+ * `/usr/bin/java` that only asks to install one.
82
91
  */
83
- function toolchainHere(command) {
84
- if (!onPath(command))
85
- return false;
86
- if (command === "java" && process.platform === "darwin")
87
- return spawnSync("/usr/libexec/java_home", [], { stdio: "ignore" }).status === 0;
88
- return true;
92
+ export function toolchainCheck(has, jdk = jdkHere) {
93
+ return (command) => has(command) && (command !== "java" || jdk());
89
94
  }
95
+ const toolchainHere = toolchainCheck(onPath);
90
96
  /** Which of the standard library's languages this machine can build and test now, and which it lacks a command of. */
91
97
  export function languagesOnMachine(has = toolchainHere) {
92
98
  const ready = STDLIB_LANGUAGES.filter(({ commands }) => commands.every(has));
package/dist/stdlib.d.ts CHANGED
@@ -7,14 +7,30 @@
7
7
  export interface StdlibTemplate {
8
8
  /** What a spec imports it as: `stdlib/backend/python-fastapi`. */
9
9
  name: string;
10
+ /** Where it is: turri's `stdlib/backend/python-fastapi.plain`. */
11
+ file: string;
12
+ /** The kind of software it is for - `backend`, `frontend`, `cli`, `library` - or `lang`, a language's. */
13
+ kind: StdlibKind;
10
14
  description: string;
11
15
  text: string;
16
+ /** The modules its front matter imports, as the parser reads it. */
17
+ imports: string[];
18
+ /** A stack's conformance template, when it has one: `stdlib/backend/conformance/python-fastapi`. */
19
+ conformance?: string;
20
+ /** A conformance template's stack, when it is one's: `stdlib/backend/python-fastapi`. */
21
+ tests?: string;
12
22
  }
23
+ export type StdlibKind = "backend" | "frontend" | "cli" | "library" | "lang";
13
24
  export declare function stdlibTemplates(dir?: string): StdlibTemplate[];
14
25
  /** A stack to import - not a kind's `common`, not a conformance template, and not a language's. */
15
26
  export declare function isStack(template: StdlibTemplate): boolean;
16
27
  /** A stack's conformance template: `stdlib/backend/python-fastapi` has `stdlib/backend/conformance/python-fastapi`. */
17
28
  export declare function conformanceOf(stack: string): string;
29
+ /** The standard library's kinds, in the order they are listed, each with its title. Only the first four have stacks. */
30
+ export declare const STDLIB_KINDS: ReadonlyArray<{
31
+ kind: StdlibKind;
32
+ title: string;
33
+ }>;
18
34
  /**
19
35
  * The catalogue appended to the stdlib rule: every stack with its conformance template and what it is, the
20
36
  * shared templates, and the folder they are all in - an absolute path, so an agent reads the one it imports
package/dist/stdlib.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { readdirSync, readFileSync } from "node:fs";
2
2
  import { join, relative, sep } from "node:path";
3
+ import { IMPORT_DIRECTIVE, readPlainSourceMetadata } from "./plain/parser.js";
3
4
  import { STDLIB_DIR } from "./regenerate/paths.js";
4
5
  export function stdlibTemplates(dir = STDLIB_DIR) {
5
6
  const found = [];
@@ -19,16 +20,31 @@ export function stdlibTemplates(dir = STDLIB_DIR) {
19
20
  }
20
21
  };
21
22
  walk(dir);
22
- return found
23
+ const templates = found
23
24
  .map((file) => {
24
25
  const text = readFileSync(file, "utf8").trim();
25
- return {
26
- name: `stdlib/${relative(dir, file).split(sep).join("/").replace(/\.plain$/, "")}`,
27
- description: /^description:\s*(.+)$/m.exec(text)?.[1]?.trim() ?? "",
28
- text,
29
- };
26
+ const name = `stdlib/${relative(dir, file).split(sep).join("/").replace(/\.plain$/, "")}`;
27
+ return { name, file, kind: name.split("/")[1], description: /^description:\s*(.+)$/m.exec(text)?.[1]?.trim() ?? "", text, imports: importsOf(text) };
30
28
  })
31
29
  .sort((a, b) => a.name.localeCompare(b.name));
30
+ const names = new Set(templates.map((template) => template.name));
31
+ for (const template of templates) {
32
+ if (isStack(template) && names.has(conformanceOf(template.name)))
33
+ template.conformance = conformanceOf(template.name);
34
+ const stack = template.name.replace("/conformance/", "/");
35
+ if (template.name.includes("/conformance/") && templates.some((other) => other.name === stack && isStack(other)))
36
+ template.tests = stack;
37
+ }
38
+ return templates;
39
+ }
40
+ function importsOf(text) {
41
+ try {
42
+ const imports = readPlainSourceMetadata(text).metadata[IMPORT_DIRECTIVE];
43
+ return Array.isArray(imports) ? imports.filter((name) => typeof name === "string") : [];
44
+ }
45
+ catch {
46
+ return [];
47
+ }
32
48
  }
33
49
  /** A stack to import - not a kind's `common`, not a conformance template, and not a language's. */
34
50
  export function isStack(template) {
@@ -39,11 +55,13 @@ export function conformanceOf(stack) {
39
55
  const slash = stack.lastIndexOf("/");
40
56
  return `${stack.slice(0, slash)}/conformance/${stack.slice(slash + 1)}`;
41
57
  }
42
- const KINDS = [
58
+ /** The standard library's kinds, in the order they are listed, each with its title. Only the first four have stacks. */
59
+ export const STDLIB_KINDS = [
43
60
  { kind: "backend", title: "Backends" },
44
61
  { kind: "frontend", title: "Frontends" },
45
62
  { kind: "cli", title: "Command-line tools" },
46
63
  { kind: "library", title: "Libraries" },
64
+ { kind: "lang", title: "Languages" },
47
65
  ];
48
66
  /**
49
67
  * The catalogue appended to the stdlib rule: every stack with its conformance template and what it is, the
@@ -61,7 +79,7 @@ export function stdlibCatalogue(templates = stdlibTemplates(), dir = STDLIB_DIR)
61
79
  "",
62
80
  `Every template is a file in ${dir}: \`stdlib/backend/python-fastapi\` is \`${dir}/backend/python-fastapi.plain\`. Read the ones a module imports - the stack, its language, its kind's \`common\`, and their conformance templates - before writing its specs.`,
63
81
  "",
64
- ...KINDS.flatMap(({ kind, title }) => {
82
+ ...STDLIB_KINDS.flatMap(({ kind, title }) => {
65
83
  const stacks = templates.filter((template) => isStack(template) && template.name.startsWith(`stdlib/${kind}/`));
66
84
  return stacks.length === 0 ? [] : [`### ${title}`, "", "| import | its conformance tests | what it is |", "| --- | --- | --- |", ...stacks.map(row), ""];
67
85
  }),
@@ -85,7 +103,7 @@ export function describeStdlib(templates = stdlibTemplates(), dir = STDLIB_DIR)
85
103
  const width = Math.max(...templates.filter(isStack).map((template) => template.name.length));
86
104
  return [
87
105
  `The ***plain standard library - ${stdlibStackCount(templates)} stacks a spec can import instead of writing its implementation reqs:`,
88
- ...KINDS.flatMap(({ kind, title }) => {
106
+ ...STDLIB_KINDS.flatMap(({ kind, title }) => {
89
107
  const stacks = templates.filter((template) => isStack(template) && template.name.startsWith(`stdlib/${kind}/`));
90
108
  return stacks.length === 0 ? [] : ["", `${title}:`, ...stacks.map((template) => ` ${template.name.padEnd(width)} ${template.description}`)];
91
109
  }),
@@ -122,7 +122,7 @@ The ones that shape \*\*\*plain most:
122
122
 
123
123
  ## Writing \*\*\*plain with turri
124
124
 
125
- In a conversation (`turri`), say what you want and the matching skill runs - but the three that start a project or a feature you start yourself, by their commands: `/brood`, `/spawn` or `/bud`, with what you want after it. Two build a project from scratch, and write each \*\*\*plain change as a git diff that is built in the background the moment it is written. `brood` interviews you one question at a time and writes \*\*\*plain as you answer; `spawn`, its quick twin, gets something running as soon as it can - the smallest thing that runs first, then the product a slice at a time - asking only what your message leaves open:
125
+ In a conversation (`turri`), say what you want and the matching skill runs - but the three that start a project or a feature you start yourself, by their commands: `/brood`, `/spawn` or `/bud`, with what you want after it. Asked for new software without one - `create a small server in Java Spring Boot` - turri offers `/brood`, `/spawn` or `Just do it`, with no skill; a skill picked there starts at once with what you asked for, and Just do it starts the project in the conversation as `spawn` would, writing the module, its config and its unit testing script, then the smallest thing that runs, regenerated at once, and the first slices of what you described (see [using turri](using.md)). Two build a project from scratch, and write each \*\*\*plain change as a git diff that is built in the background the moment it is written. `brood` interviews you one question at a time and writes \*\*\*plain as you answer; `spawn`, its quick twin, gets something running as soon as it can - the smallest thing that runs first, then the product a slice at a time - asking only what your message leaves open:
126
126
 
127
127
  | skill | phases | how a \*\*\*plain change is written |
128
128
  | --- | --- | --- |
@@ -145,13 +145,13 @@ Every skill in `genome/skills/` can be run from a conversation; most are also us
145
145
 
146
146
  | to… | skills |
147
147
  | --- | --- |
148
- | start a project | `brood`, `spawn` |
148
+ | start a project | `brood`, `spawn` - or ask, and choose `Just do it` for no skill |
149
149
  | make rough \*\*\*plain build | `mature` |
150
150
  | add to \*\*\*plain | `bud`, `add-functional-spec`, `add-functional-specs`, `add-concept`, `add-implementation-requirement`, `add-test-requirement`, `add-acceptance-test`, `add-resource`, `add-template` |
151
151
  | check what is written | `analyze-if-func-spec-too-complex`, `analyze-2-func-specs`, `analyze-func-specs`, `resolve-section-ownership` |
152
152
  | fix what is written | `break-down-func-spec`, `resolve-spec-conflict`, `debug-specs` |
153
- | restructure modules | `create-import-module`, `create-requires-module`, `refactor-module`, `consolidate-concepts` |
154
- | set up the project to build | `implement-unit-testing-script`, `implement-conformance-testing-script`, `check-plain-env` |
153
+ | restructure modules | `refactor-module`, `consolidate-concepts` |
154
+ | set up the project to build | `implement-unit-testing-script`, `implement-conformance-testing-script` |
155
155
 
156
156
  `debug-specs` is the one for a running application that misbehaves: it reads the generated code to find the cause, traces it back to \*\*\*plain, and fixes only the `.plain` files.
157
157
 
package/docs/hosting.md CHANGED
@@ -18,6 +18,9 @@
18
18
  | `bedrockAwsRegion`, `bedrockNeedsAwsRegion` | the AWS region to set `AWS_REGION` to for a Bedrock geography, and whether it has to be set. |
19
19
  | `checkPlainFile`, `plainFileParser`, `resolveTemplateDirs`, `iterateFunctionalSpecs`, `FUNCTIONAL_REQUIREMENTS` | \*\*\*plain checked and parsed, its functional specs one by one - as `turri validate` reads it. |
20
20
  | `highlightLines`, `roleOf` | \*\*\*plain's lines coloured by turri's own [\*\*\*plain](https://plainlang.org/docs/) grammar. |
21
+ | `plainGraph(projectDir)` | a project's \*\*\*plain as a graph to move around in: its modules with what each imports and requires and what imports and requires it, each item of their sections with its line, and the concepts - an item linked to those it names and to where each is defined - see [\*\*\*plain for a host](#plain-for-a-host). |
22
+ | `conceptsOf(plainFile)`, `findConcepts(text)`, `conceptBeingTyped(text)` | the concepts a module can name, with what each is and the module that defines it; the concepts written in a piece of text; the one being typed at its end. |
23
+ | `stdlibTemplates()`, `isStack`, `STDLIB_KINDS` | turri's standard library: every template with its kind, file, text, imports and its conformance template or the stack it tests, and the kinds in order with their titles. |
21
24
  | `functionalityCompletion`, `createWorkLoop`, `createJobRegistry`, `promptOf`, `describeModules` | how far each functional spec is built, and - on a loop of the host's own with `dryRun` - the work a \*\*\*plain change would plan, as `turri glimpse` does. |
22
25
  | `onModelEvent`, `describeTurnIncident` | a provider failing and waited out, a turn turri ended itself. |
23
26
  | `onLedgerEvent`, `readLedger`, `ledgerDir`, `runViews`, `pulseView`, `describePulseView`, `describeRuns`, `ledgerProjects`, `earlierPulses`, `asJob` | what turri did, as facts - the [ledger](using.md#the-ledger): each event heard as it is recorded, and every run and pulse read back, earlier sessions' too - see [the ledger in a host](#the-ledger-in-a-host). |
@@ -108,6 +111,27 @@ the chat, and the toolbar's buttons send them too.
108
111
  - The Run button releases what is built and opens it.`;
109
112
  ```
110
113
 
114
+ ## \*\*\*plain for a host
115
+
116
+ ```js
117
+ import { conceptBeingTyped, conceptsOf, findConcepts, plainGraph, stdlibTemplates, STDLIB_KINDS } from "@turri-dev/cli/host";
118
+
119
+ const { modules, items, concepts } = plainGraph(projectDir);
120
+ const app = modules.find((module) => module.module === "app");
121
+ app.requires; // [{ name: "base", file: "/…/base.plain" }] - and imports, importedBy, requiredBy, the same way
122
+ app.items; // ["/…/app.plain:11", "/…/app.plain:15"] - its own items, in order
123
+ items.find((item) => item.id === app.items[1]);
124
+ // { id, file, module: "app", section: "functional specs", line: 15, frid: "1", text: "- :User: can fill :Cart:.",
125
+ // defines: [], uses: [{ concept: ":User:", definition: "/…/base.plain:8" }, { concept: ":Cart:", definition: "/…/app.plain:11" }] }
126
+ concepts.find((concept) => concept.definition === "/…/base.plain:8");
127
+ // { concept: ":User:", definition: "/…/base.plain:8", module: "base", text: "is a person who signs up.", usedBy: [...] }
128
+ ```
129
+
130
+ - **The graph.** `plainGraph` reads every .plain file in the project's folder and its `template/` folder, and every module they import or require, wherever the parser finds it - the standard library's templates too. A **module** is a file: its imports, resolved as the parser reads an import (a `stdlib/` module from turri's own folder), its required modules, resolved as the requires chain finds them - `file: null` when one is not found - and the reverse of both, `importedBy` and `requiredBy`, so a host goes up and down the requires chain without inverting anything. An **item** is one item of a section - a definition, an implementation req, a test req, a functional spec with its acceptance tests - with its file, the line it starts on, its text, a functional spec's `frid`, the concepts a definition defines, and each concept it names with the item that defines it: its own module's definitions, its imports', and those its required modules export, as the parser sees them from that module. `definition` is null for \*\*\*plain's own concepts, and for one defined nowhere the module can see. A **concept** is one definition of it - or \*\*\*plain's own, `definition: null` - with every item that names it there. An item's id is `<file>:<line>`: the file to open and the line to go to.
131
+ - **What does not parse.** Nothing throws. A file that does not parse has `error`, the parser's message; its links are what its front matter says, and its items what of its own text reads without its imports. A file whose items Liquid moves keeps the lines of its own text.
132
+ - **Concepts.** `conceptsOf(plainFile)` is what one module can name - `{ concept, definition, module }`, `definition` the text after its concepts, `module` null for \*\*\*plain's own - for suggestions as the person types. `findConcepts(text)` is every concept written in a piece of text, `{ concept, start, end }`, and `conceptBeingTyped(text)` the one at its end still open - `{ typed: ":Us", start }`, or undefined - both with the parser's own pattern.
133
+ - **The standard library.** `stdlibTemplates()` is every template in turri's `stdlib/`, sorted: `name` - what a module imports it as - `file`, `kind` (`backend`, `frontend`, `cli`, `library` or `lang`), `description`, `text`, `imports` as the parser reads its front matter, a stack's `conformance` template when it has one, and a conformance template's stack as `tests`. `isStack(template)` says whether it is a stack to import; `STDLIB_KINDS` is the kinds in order, each with its title.
134
+
111
135
  ## The ledger in a host
112
136
 
113
137
  The conversation's extension starts the ledger itself, in the folder the host made the session in - so a host that runs it there, the project's, needs nothing to have one: its pulses are recorded as `host` runs, and the conversation's `pulses` and `diagnose` already tell the model of earlier sessions' pulses. What the host can do with it is show it.
@@ -54,7 +54,7 @@ In a project - a folder with `config.turri.yaml` (or the old `config.yaml`) or `
54
54
 
55
55
  | what | |
56
56
  | --- | --- |
57
- | each module's toolchains | what its implementation reqs name - Python, Node, Java, Go, Rust, C/C++, Zig, ... - against this machine's PATH: the agents use this machine's toolchains and install none, so a missing one is yours to install |
57
+ | each module's toolchains | what its implementation reqs name - Python, Node, Java, Go, Rust, C/C++, Zig, ... - against this machine's PATH - and Java against a JDK, for macOS keeps a `/usr/bin/java` that only asks to install one: the agents use this machine's toolchains and install none, so a missing one is yours to install |
58
58
  | the testing scripts | what in them this machine does not have: a program named by its whole path that is not here (a macOS-only `/usr/libexec/java_home` on Linux), a toolchain they run that is not on the PATH |
59
59
  | the models | each model `config.turri.yaml` names - `harness-model`, `coding-model`, `judge-model`, every role under `models:` - sent one short request through [pi](https://pi.dev), with the credentials turri's agents will use, for any provider [pi](https://pi.dev) supports. One that answers is ready; for one that does not, it says what the provider said and what to do: the credentials [pi](https://pi.dev) reads for that provider (`AWS_PROFILE` and `AWS_REGION` for Amazon Bedrock, `OPENAI_API_KEY`, `/login`, ...), model access in the Bedrock console, the closest model ids [pi](https://pi.dev) knows for one it does not. `--harness-model`, `--coding-model` (`--model`) and `--judge-model` name models as the config's keys, as `turri regenerate` reads them - checked in a project or not. `--no-models` skips it |
60
60
 
package/docs/parser.md CHANGED
@@ -10,6 +10,7 @@
10
10
  | `src/plain/markdown.ts` | mistletoe's AST + `PlainRenderer` (mdast + a matching serializer) |
11
11
  | `src/plain/liquid.ts` | the reference implementation's Liquid nodes + the two filters in `plain_spec.py` (liquidjs) |
12
12
  | `src/plain/files.ts` | `file_utils.open_from` / `get_loaded_templates`, base64 blob detection |
13
+ | `src/plain/module-concepts.ts`, `src/plain/graph.ts` | nothing - turri's own, for a host: the concepts a module can name and where each is defined, and a project's \*\*\*plain as a graph of modules, items and concepts (see [hosting](hosting.md#plain-for-a-host)). The parser keeps, beside the reference's result, which module each item an import brought is written in (`importedFrom`) |
13
14
  | `src/plain/stdlib.ts` | nothing - turri's own: `stdlib/` is a reserved import prefix, read from turri's standard library only, and a project module at such a path is an error (see [the standard library](authoring-plain.md#the-standard-library)) |
14
15
 
15
16
  ## Checking \*\*\*plain
@@ -37,7 +37,7 @@ metadata:
37
37
 
38
38
  A skill declares its steps with a `## Steps` section whose `###` headings are the steps; without it the whole skill is one step, which is what most skills are. Steps are never inferred from `###` headings elsewhere in the body - prose skills are full of those. Each gate becomes a conditional edge after its step; a single-step skill is gated by naming the skill itself. None of the skills turri ships declares a gate today - `brood`'s phases end on the person's confirmation alone - but a project's own skill can.
39
39
 
40
- `command-only: true` under `metadata:` keeps a skill from the model: it is started only by the person, with its slash command - `/brood`, `/spawn` and `/bud` are. turri registers the command for every such skill, the project's own too. Typed, the command puts what was typed in the chat as the person's message, and lets `cultivate` start the skill once; the run starting uses that up, so the model cannot start it again later on its own. Without the command, `cultivate` refuses it and the model is told to name the command to the person instead - the skill's line in `cultivate`'s list says so too.
40
+ `command-only: true` under `metadata:` keeps a skill from the model: it is started only by the person, with its slash command - `/brood`, `/spawn` and `/bud` are. turri registers the command for every such skill, the project's own too. Typed, the command puts what was typed in the chat as the person's message, and lets `cultivate` start the skill once - and so does an option starting with the command, `/brood - an interview`, picked in the conversation's `ask_user`, which is how the chat offers `/brood`, `/spawn` or starting a project itself with no skill (`chosenCommand` in `src/skills.ts`, `src/new-project.ts`). The answer tells the model to call `cultivate` for it at once, with the person's request word for word as its context (`startPicked`), so they never type the command after picking it; the run starting uses that up, so the model cannot start it again later on its own. Without either, `cultivate` refuses it and the model is told how the person starts it - for a new project, by the choice it offers; the skill's line in `cultivate`'s list says so too.
41
41
 
42
42
  `paced: false` under `metadata:` lets a skill write \*\*\*plain changes one after another on its own: `change_plain` otherwise refuses a new change until the user has answered a question since the last one - one answer, one change, as an interview writes them. `spawn` is unpaced: the tool tells it so, and after each change to write the next at once, not to carry on with an interview it does not have.
43
43