@turri-dev/cli 0.1.14-next.3e3ff53 → 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.
package/dist/host.d.ts CHANGED
@@ -1,8 +1,8 @@
1
1
  /**
2
2
  * turri for a host that runs it itself - a desktop app, an editor - imported as `@turri-dev/cli/host`: the
3
3
  * conversation as pi's extension, with the host told of its jobs and asking its questions (createTurriExtension), and
4
- * what a host shows beside it - a spec parsed, checked and coloured, how far each functional spec is built, the work a
5
- * spec change would plan. It is turri's own code, as the terminal runs it; a 0.x release may change any of it.
4
+ * what a host shows beside it - a spec parsed, checked and coloured, its concepts and links, the standard library, how
5
+ * far each functional spec is built, the work a spec change would plan. It is turri's own code, as the terminal runs it; a 0.x release may change any of it.
6
6
  */
7
7
  export { createTurriExtension, type TurriHost } from "./extension.ts";
8
8
  export type { AskUserIO } from "./ask.ts";
@@ -18,6 +18,10 @@ export { checkPlainFile, type CheckResult } from "./plain/check.ts";
18
18
  export { plainFileParser } from "./plain/parser.ts";
19
19
  export { FUNCTIONAL_REQUIREMENTS, iterateFunctionalSpecs } from "./plain/spec.ts";
20
20
  export { highlightLines, roleOf, type PlainRole } from "./plain/highlight.ts";
21
+ export { conceptsOf, type ModuleConcept } from "./plain/module-concepts.ts";
22
+ export { conceptBeingTyped, findConcepts } from "./plain/concepts.ts";
23
+ export { plainGraph, type PlainConceptNode, type PlainGraph, type PlainItemNode, type PlainLink, type PlainModuleNode, type PlainSection } from "./plain/graph.ts";
24
+ export { isStack, STDLIB_KINDS, stdlibTemplates, type StdlibKind, type StdlibTemplate } from "./stdlib.ts";
21
25
  export { createWorkLoop, describeModules, functionalityCompletion, promptOf, type DriftReport, type ModuleStatus, type WorkLoop } from "./regenerate/loop.ts";
22
26
  export { createJobRegistry, defaultParallelJobs, type Job, type JobRegistry } from "./regenerate/jobs.ts";
23
27
  export { describeTurnIncident, onModelEvent, type TranscriptEntry } from "./regenerate/activity.ts";
package/dist/host.js CHANGED
@@ -1,8 +1,8 @@
1
1
  /**
2
2
  * turri for a host that runs it itself - a desktop app, an editor - imported as `@turri-dev/cli/host`: the
3
3
  * conversation as pi's extension, with the host told of its jobs and asking its questions (createTurriExtension), and
4
- * what a host shows beside it - a spec parsed, checked and coloured, how far each functional spec is built, the work a
5
- * spec change would plan. It is turri's own code, as the terminal runs it; a 0.x release may change any of it.
4
+ * what a host shows beside it - a spec parsed, checked and coloured, its concepts and links, the standard library, how
5
+ * far each functional spec is built, the work a spec change would plan. It is turri's own code, as the terminal runs it; a 0.x release may change any of it.
6
6
  */
7
7
  // The conversation.
8
8
  export { createTurriExtension } from "./extension.js";
@@ -25,6 +25,14 @@ export { checkPlainFile } from "./plain/check.js";
25
25
  export { plainFileParser } from "./plain/parser.js";
26
26
  export { FUNCTIONAL_REQUIREMENTS, iterateFunctionalSpecs } from "./plain/spec.js";
27
27
  export { highlightLines, roleOf } from "./plain/highlight.js";
28
+ // Its concepts: those a module can name, with what each is and where it is defined, and those written in a piece of text.
29
+ export { conceptsOf } from "./plain/module-concepts.js";
30
+ export { conceptBeingTyped, findConcepts } from "./plain/concepts.js";
31
+ // A project's ***plain as a graph: its modules and their links both ways, each item of their sections, the concepts
32
+ // each names and where each is defined.
33
+ export { plainGraph } from "./plain/graph.js";
34
+ // The standard library: every template, what it is, what it imports, and its conformance template or stack.
35
+ export { isStack, STDLIB_KINDS, stdlibTemplates } from "./stdlib.js";
28
36
  // The work: what is built, what is left, and the jobs that build it.
29
37
  export { createWorkLoop, describeModules, functionalityCompletion, promptOf } from "./regenerate/loop.js";
30
38
  export { createJobRegistry, defaultParallelJobs } from "./regenerate/jobs.js";
@@ -1,8 +1,26 @@
1
1
  import { type AnyNode, type Section } from "./markdown.ts";
2
2
  import { type MarshalledPlainSource, type SpecificationItem } from "./spec.ts";
3
3
  /** Port of concept_utils.py. */
4
+ /**
5
+ * ***plain's own concepts, each with what it means, in a line: every module can name them, and none may define them.
6
+ * turri's, not the reference parser's - it has only the names.
7
+ */
8
+ export declare const DEFAULT_CONCEPT_DEFINITIONS: Readonly<Record<string, string>>;
4
9
  /** Reserved concept names that must not be redefined in the definitions section. */
5
10
  export declare const DEFAULT_CONCEPTS: Set<string>;
11
+ /** Every concept written in a piece of text - `:User:` - with where it is, as the parser reads concepts in a spec. */
12
+ export declare function findConcepts(text: string): Array<{
13
+ concept: string;
14
+ start: number;
15
+ end: number;
16
+ }>;
17
+ /** The concept being typed at the end of a piece of text - `:Us` - with where it starts, or undefined when there is none. */
18
+ export declare function conceptBeingTyped(text: string): {
19
+ typed: string;
20
+ start: number;
21
+ } | undefined;
22
+ /** A definition's text after the concepts it defines: `is a person who signs up` of `- :User: is a person who signs up`. */
23
+ export declare function definitionText(text: string): string;
6
24
  export declare function extractConceptsFromDefinition(text: string): [string[], string[]];
7
25
  export declare function extractConceptsFromSpecText(text: string): string[];
8
26
  export declare function collectConcepts(plainSource: MarshalledPlainSource): [string[], string[]];
@@ -2,23 +2,50 @@ import { PlainSyntaxError } from "./errors.js";
2
2
  import { renderToMarkdown } from "./markdown.js";
3
3
  import { ACCEPTANCE_TESTS, DEFINITIONS, FUNCTIONAL_REQUIREMENTS, NON_FUNCTIONAL_REQUIREMENTS, TEST_REQUIREMENTS, getSpecificationItems, } from "./spec.js";
4
4
  /** Port of concept_utils.py. */
5
+ /**
6
+ * ***plain's own concepts, each with what it means, in a line: every module can name them, and none may define them.
7
+ * turri's, not the reference parser's - it has only the names.
8
+ */
9
+ export const DEFAULT_CONCEPT_DEFINITIONS = {
10
+ ":ConformanceTests:": "the tests, written apart from the code, that check it conforms to the functional specs",
11
+ ":UnitTests:": "the unit tests written with the code of each functional spec",
12
+ ":AcceptanceTest:": "a test that checks one functional spec does what its acceptance tests say",
13
+ ":AcceptanceTests:": "the tests that check a functional spec does what its acceptance tests say",
14
+ ":Implementation:": "the software that implements the module's functional specs",
15
+ ":plainDefinitions:": "the content of the module's ***definitions*** section",
16
+ ":plainImplementationReqs:": "the content of the module's ***implementation reqs*** section",
17
+ ":plainFunctionality:": "the content of the module's ***functional specs*** section",
18
+ ":plainTestReqs:": "the content of the module's ***test reqs*** section",
19
+ ":plainImplementationCode:": "the code generated from the module",
20
+ };
5
21
  /** Reserved concept names that must not be redefined in the definitions section. */
6
- export const DEFAULT_CONCEPTS = new Set([
7
- ":ConformanceTests:",
8
- ":UnitTests:",
9
- ":AcceptanceTest:",
10
- ":AcceptanceTests:",
11
- ":Implementation:",
12
- ":plainDefinitions:",
13
- ":plainImplementationReqs:",
14
- ":plainFunctionality:",
15
- ":plainTestReqs:",
16
- ":plainImplementationCode:",
17
- ]);
22
+ export const DEFAULT_CONCEPTS = new Set(Object.keys(DEFAULT_CONCEPT_DEFINITIONS));
23
+ /** What a concept's name is made of, between its two colons. */
24
+ const CONCEPT_CHARACTERS = "+\\-.0-9A-Z_a-z";
18
25
  const DEFINITION_PATTERN = /-\s(:[^:]+:)(?:,\s*:[^:]+:)*/;
19
26
  const CONCEPT_CANDIDATE_PATTERN = /:[^:]+:/g;
20
- const VALID_CONCEPT_PATTERN = /^:[+\-.0-9A-Z_a-z]+:/;
21
- const SPEC_CONCEPT_PATTERN = /:[+\-.0-9A-Z_a-z]+:/g;
27
+ const VALID_CONCEPT_PATTERN = new RegExp(`^:[${CONCEPT_CHARACTERS}]+:`);
28
+ const SPEC_CONCEPT_PATTERN = new RegExp(`:[${CONCEPT_CHARACTERS}]+:`, "g");
29
+ const TYPED_CONCEPT_PATTERN = new RegExp(`:[${CONCEPT_CHARACTERS}]*$`);
30
+ /** Every concept written in a piece of text - `:User:` - with where it is, as the parser reads concepts in a spec. */
31
+ export function findConcepts(text) {
32
+ return [...text.matchAll(SPEC_CONCEPT_PATTERN)].map((match) => ({ concept: match[0], start: match.index, end: match.index + match[0].length }));
33
+ }
34
+ /** The concept being typed at the end of a piece of text - `:Us` - with where it starts, or undefined when there is none. */
35
+ export function conceptBeingTyped(text) {
36
+ const match = TYPED_CONCEPT_PATTERN.exec(text);
37
+ if (match === null)
38
+ return undefined;
39
+ // A colon that closes a concept is not one that opens the next.
40
+ if (findConcepts(text).some(({ end }) => end - 1 === match.index))
41
+ return undefined;
42
+ return { typed: match[0], start: match.index };
43
+ }
44
+ /** A definition's text after the concepts it defines: `is a person who signs up` of `- :User: is a person who signs up`. */
45
+ export function definitionText(text) {
46
+ const match = DEFINITION_PATTERN.exec(text);
47
+ return (match === null ? text : text.slice(match.index + match[0].length)).trim();
48
+ }
22
49
  export function extractConceptsFromDefinition(text) {
23
50
  const match = DEFINITION_PATTERN.exec(text);
24
51
  if (!match) {
@@ -1,5 +1,7 @@
1
1
  /** Ports of the Python reference implementation's file helpers this parser needs. */
2
2
  export declare function openFrom(dirs: string[], fileName: string): string | null;
3
+ /** The file `openFrom` reads: the first of the folders that has it. */
4
+ export declare function findIn(dirs: string[], fileName: string): string | null;
3
5
  /** First contiguous base64 blob at or above the threshold, or null. */
4
6
  export declare function findLargeBase64Blob(text: string): string | null;
5
7
  /**
@@ -3,16 +3,20 @@ import { join } from "node:path";
3
3
  import { createEnvironment } from "./liquid.js";
4
4
  /** Ports of the Python reference implementation's file helpers this parser needs. */
5
5
  export function openFrom(dirs, fileName) {
6
+ const path = findIn(dirs, fileName);
7
+ return path === null ? null : readFileSync(path, "utf8");
8
+ }
9
+ /** The file `openFrom` reads: the first of the folders that has it. */
10
+ export function findIn(dirs, fileName) {
6
11
  for (const dir of dirs) {
7
12
  const fullPath = join(dir, fileName);
8
13
  try {
9
- if (!statSync(fullPath).isFile())
10
- continue;
14
+ if (statSync(fullPath).isFile())
15
+ return fullPath;
11
16
  }
12
17
  catch {
13
18
  continue;
14
19
  }
15
- return readFileSync(fullPath, "utf8");
16
20
  }
17
21
  return null;
18
22
  }
@@ -0,0 +1,68 @@
1
+ import { DEFINITIONS, FUNCTIONAL_REQUIREMENTS, NON_FUNCTIONAL_REQUIREMENTS, TEST_REQUIREMENTS } from "./spec.ts";
2
+ /**
3
+ * A project's ***plain as a graph, to go from one place in it to another: its modules, each item of their sections,
4
+ * and the concepts - an item linked to its module, to the concepts it names and to where each is defined, a module to
5
+ * what it imports and requires and to what imports and requires it.
6
+ */
7
+ export interface PlainGraph {
8
+ modules: PlainModuleNode[];
9
+ items: PlainItemNode[];
10
+ concepts: PlainConceptNode[];
11
+ }
12
+ /** A module one .plain file names, and the file it is - null when it is not found. */
13
+ export interface PlainLink {
14
+ name: string;
15
+ file: string | null;
16
+ }
17
+ /** A .plain file: what it imports and requires, what imports and requires it, its items, and why it does not parse. */
18
+ export interface PlainModuleNode {
19
+ file: string;
20
+ module: string;
21
+ /** Resolved as the parser reads an import: a standard library module from turri's `stdlib/`. */
22
+ imports: PlainLink[];
23
+ /** Resolved as the requires chain finds a required module. */
24
+ requires: PlainLink[];
25
+ importedBy: PlainLink[];
26
+ requiredBy: PlainLink[];
27
+ /** Its own items, in the order they are written: their ids. */
28
+ items: string[];
29
+ /** The parser's message, when the file does not parse. Its links are then what its front matter says, its items what of its own text reads. */
30
+ error?: string;
31
+ }
32
+ export type PlainSection = typeof DEFINITIONS | typeof NON_FUNCTIONAL_REQUIREMENTS | typeof TEST_REQUIREMENTS | typeof FUNCTIONAL_REQUIREMENTS;
33
+ /** An item of a section, with what is nested under it - a functional spec with its acceptance tests. */
34
+ export interface PlainItemNode {
35
+ /** `<file>:<line>`. */
36
+ id: string;
37
+ file: string;
38
+ module: string;
39
+ section: PlainSection;
40
+ /** The line it starts on, from 1. */
41
+ line: number;
42
+ text: string;
43
+ /** A functional spec's functionality id: `1`, `2`, ... */
44
+ frid?: string;
45
+ /** The concepts a definition defines. */
46
+ defines: string[];
47
+ /** The concepts it names, each with the definition it is - an item's id - or null for ***plain's own and one defined nowhere it can see. */
48
+ uses: Array<{
49
+ concept: string;
50
+ definition: string | null;
51
+ }>;
52
+ }
53
+ /** A concept as defined in one place - or ***plain's own - and every item that names it there. */
54
+ export interface PlainConceptNode {
55
+ concept: string;
56
+ /** The item that defines it; null for ***plain's own. */
57
+ definition: string | null;
58
+ module: string | null;
59
+ /** The definition's text after its concepts. */
60
+ text: string;
61
+ usedBy: string[];
62
+ }
63
+ /**
64
+ * A project's ***plain as a graph: every .plain file in its folder and its `template/` folder, and every module they
65
+ * import or require, wherever the parser finds it, the standard library's too. Never throws: a file that does not
66
+ * parse is given with what its front matter says, what of its own text reads, and the error.
67
+ */
68
+ export declare function plainGraph(projectDir: string): PlainGraph;
@@ -0,0 +1,136 @@
1
+ import { readdirSync, readFileSync } from "node:fs";
2
+ import { basename, dirname, join, resolve } from "node:path";
3
+ import { resolveTemplateDirs } from "../regenerate/paths.js";
4
+ import { locateModule } from "../regenerate/requires.js";
5
+ import { DEFAULT_CONCEPT_DEFINITIONS, DEFAULT_CONCEPTS, definitionText, extractConceptsFromDefinition, findConcepts } from "./concepts.js";
6
+ import { lineNumber, renderToMarkdown } from "./markdown.js";
7
+ import { attempt, namesIn, readModule } from "./module-concepts.js";
8
+ import { IMPORT_DIRECTIVE, modulePlainFile, parsePlainFile, PLAIN_SOURCE_FILE_EXTENSION, plainFileParser, readPlainSourceMetadata, REQUIRES_DIRECTIVE, withParseCache, } from "./parser.js";
9
+ import { DEFINITIONS, FUNCTIONAL_REQUIREMENTS, NON_FUNCTIONAL_REQUIREMENTS, TEST_REQUIREMENTS } from "./spec.js";
10
+ import { isStdlibModule } from "./stdlib.js";
11
+ const SECTIONS = [DEFINITIONS, NON_FUNCTIONAL_REQUIREMENTS, TEST_REQUIREMENTS, FUNCTIONAL_REQUIREMENTS];
12
+ /**
13
+ * A project's ***plain as a graph: every .plain file in its folder and its `template/` folder, and every module they
14
+ * import or require, wherever the parser finds it, the standard library's too. Never throws: a file that does not
15
+ * parse is given with what its front matter says, what of its own text reads, and the error.
16
+ */
17
+ export function plainGraph(projectDir) {
18
+ const root = resolve(projectDir);
19
+ // The folders a .plain file of the project is read with: its own, its template/ folder, PLAIN_TEMPLATE_DIRS.
20
+ const dirs = resolveTemplateDirs(join(root, `project${PLAIN_SOURCE_FILE_EXTENSION}`));
21
+ const modules = new Map();
22
+ const queue = [];
23
+ const visit = (file, module) => {
24
+ if (modules.has(file))
25
+ return;
26
+ const node = { file, module, imports: [], requires: [], importedBy: [], requiredBy: [], items: [] };
27
+ modules.set(file, node);
28
+ queue.push(node);
29
+ };
30
+ for (const dir of [root, join(root, "template")]) {
31
+ const names = attempt(() => readdirSync(dir).filter((name) => name.endsWith(PLAIN_SOURCE_FILE_EXTENSION)).sort()) ?? [];
32
+ for (const name of names)
33
+ visit(join(dir, name), basename(name, PLAIN_SOURCE_FILE_EXTENSION));
34
+ }
35
+ for (let node = queue.shift(); node !== undefined; node = queue.shift()) {
36
+ let metadata = {};
37
+ try {
38
+ metadata = readPlainSourceMetadata(readFileSync(node.file, "utf8")).metadata;
39
+ }
40
+ catch (error) {
41
+ node.error = messageOf(error);
42
+ }
43
+ node.imports = namesIn(metadata[IMPORT_DIRECTIVE]).map((name) => ({ name, file: modulePlainFile(name, dirs) }));
44
+ node.requires = namesIn(metadata[REQUIRES_DIRECTIVE]).map((name) => ({ name, file: attempt(() => locateModule(name, dirs)) ?? null }));
45
+ for (const link of [...node.imports, ...node.requires])
46
+ if (link.file !== null)
47
+ visit(link.file, link.name);
48
+ }
49
+ for (const node of modules.values()) {
50
+ for (const link of node.imports)
51
+ if (link.file !== null)
52
+ modules.get(link.file).importedBy.push({ name: node.module, file: node.file });
53
+ for (const link of node.requires)
54
+ if (link.file !== null)
55
+ modules.get(link.file).requiredBy.push({ name: node.module, file: node.file });
56
+ }
57
+ return withParseCache(() => {
58
+ const items = [];
59
+ const concepts = new Map();
60
+ for (const [concept, text] of Object.entries(DEFAULT_CONCEPT_DEFINITIONS))
61
+ concepts.set(`${concept}\0`, { concept, definition: null, module: null, text, usedBy: [] });
62
+ const lines = new Map();
63
+ const idOf = (node, file) => `${file}:${lineOf(node, file, lines)}`;
64
+ const fileOf = (module, node) => (module === node.module ? node.file : modulePlainFile(module, dirs));
65
+ for (const node of modules.values()) {
66
+ if (node.error === undefined)
67
+ node.error = parseError(node, dirs);
68
+ const read = readModule(node.module, node.file, dirs);
69
+ // Where each concept it can name is defined, the first definition winning, as the parser's checks find one.
70
+ const visible = new Map();
71
+ for (const { node: definition, module } of read.definitions) {
72
+ const file = fileOf(module, node);
73
+ if (file === null)
74
+ continue;
75
+ const id = idOf(definition, file);
76
+ const text = renderToMarkdown(definition);
77
+ for (const concept of extractConceptsFromDefinition(text)[0]) {
78
+ if (!visible.has(concept))
79
+ visible.set(concept, id);
80
+ if (!concepts.has(`${concept}\0${id}`))
81
+ concepts.set(`${concept}\0${id}`, { concept, definition: id, module, text: definitionText(text), usedBy: [] });
82
+ }
83
+ }
84
+ for (const section of SECTIONS) {
85
+ const own = (read.parsed?.plainSource[section]?.children ?? []).filter((item) => !read.parsed.importedFrom.has(item));
86
+ own.forEach((item, index) => {
87
+ const text = renderToMarkdown(item).trim();
88
+ const defines = section === DEFINITIONS ? extractConceptsFromDefinition(text)[0] : [];
89
+ const named = [...new Set(findConcepts(text).map((found) => found.concept))].filter((concept) => !defines.includes(concept));
90
+ const id = idOf(item, node.file);
91
+ const uses = named.map((concept) => ({ concept, definition: DEFAULT_CONCEPTS.has(concept) ? null : (visible.get(concept) ?? null) }));
92
+ for (const use of uses)
93
+ concepts.get(`${use.concept}\0${use.definition ?? ""}`)?.usedBy.push(id);
94
+ items.push({ id, file: node.file, module: node.module, section, line: lineOf(item, node.file, lines), text, ...(section === FUNCTIONAL_REQUIREMENTS ? { frid: String(index + 1) } : {}), defines, uses });
95
+ node.items.push(id);
96
+ });
97
+ }
98
+ }
99
+ return { modules: [...modules.values()], items, concepts: [...concepts.values()] };
100
+ });
101
+ }
102
+ /** Why a module does not parse: an import module as an import is parsed, a module of its own as one - functional specs or not yet. */
103
+ function parseError(node, dirs) {
104
+ const imported = node.importedBy.length > 0 || isStdlibModule(node.module) || basename(dirname(node.file)) === "template";
105
+ try {
106
+ if (imported)
107
+ parsePlainFile(node.module, {}, dirs, [], []);
108
+ else
109
+ plainFileParser(`${node.module}${PLAIN_SOURCE_FILE_EXTENSION}`, dirs, { allowNoFunctionalities: true });
110
+ return undefined;
111
+ }
112
+ catch (error) {
113
+ return messageOf(error);
114
+ }
115
+ }
116
+ /**
117
+ * The line an item starts on in its file. The parser's is a line of the text after Liquid has run, which is the file's
118
+ * own unless an include or a tag changed the lines above it: then the first line of the file that has the item's
119
+ * first line is taken.
120
+ */
121
+ function lineOf(node, file, cache) {
122
+ let lines = cache.get(file);
123
+ if (lines === undefined) {
124
+ lines = (attempt(() => readFileSync(file, "utf8")) ?? "").split(/\r?\n/);
125
+ cache.set(file, lines);
126
+ }
127
+ const at = lineNumber(node);
128
+ const first = renderToMarkdown(node).trim().split("\n")[0].replace(/^[-*+]\s+/, "").trim();
129
+ if (first === "" || lines[at - 1]?.includes(first))
130
+ return at;
131
+ const found = lines.findIndex((line) => line.includes(first));
132
+ return found === -1 ? at : found + 1;
133
+ }
134
+ function messageOf(error) {
135
+ return error instanceof Error ? error.message : String(error);
136
+ }
@@ -0,0 +1,32 @@
1
+ import { type AnyNode } from "./markdown.ts";
2
+ import { type PlainFileParseResult } from "./parser.ts";
3
+ /** A concept a module can name, what it is, and the module that defines it - null for ***plain's own. */
4
+ export interface ModuleConcept {
5
+ concept: string;
6
+ /** The definition's text after its concepts: `is a person who signs up`. */
7
+ definition: string;
8
+ module: string | null;
9
+ }
10
+ /** A module as the parser reads it, for what it names: its own parse, and every definition it can name. */
11
+ export interface ReadModule {
12
+ /** Its parse, its imports merged in - or, when that fails, its own text read without them; undefined when neither reads. */
13
+ parsed?: PlainFileParseResult;
14
+ /** Its own definitions, those its imports bring and those its required modules export, each with the module it is written in. */
15
+ definitions: Array<{
16
+ node: AnyNode;
17
+ module: string;
18
+ }>;
19
+ }
20
+ /**
21
+ * A module read for what it can name, as the parser builds its definitions: its own, its imports' (`import`), and its
22
+ * required modules' `exported_concepts` with the definitions they use. Never throws: a module that does not parse
23
+ * gives its own definitions read without its imports, and what of those it names is there.
24
+ */
25
+ export declare function readModule(moduleName: string, plainFile: string, templateDirs: string[]): ReadModule;
26
+ /**
27
+ * Every concept a module can name: its own definitions, those its imports bring, those its required modules export,
28
+ * and ***plain's own - read as the parser reads them. A module that does not parse gives what can be read of it.
29
+ */
30
+ export declare function conceptsOf(plainFile: string, templateDirs?: string[]): ModuleConcept[];
31
+ export declare function attempt<T>(run: () => T): T | undefined;
32
+ export declare function namesIn(value: unknown): string[];
@@ -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/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
  }),
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.
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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@turri-dev/cli",
3
- "version": "0.1.14-next.3e3ff53",
3
+ "version": "0.1.14-next.678847f",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "repository": {