@turri-dev/cli 0.1.27 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -6
- package/dist/advisor-chat.d.ts +63 -0
- package/dist/advisor-chat.js +290 -0
- package/dist/background-skills.d.ts +53 -0
- package/dist/background-skills.js +121 -0
- package/dist/bedrock-region.d.ts +66 -0
- package/dist/bedrock-region.js +144 -0
- package/dist/btw.d.ts +1 -1
- package/dist/btw.js +1 -1
- package/dist/change-plain-cli.js +20 -13
- package/dist/chat-idle.d.ts +7 -0
- package/dist/chat-idle.js +20 -0
- package/dist/chats.d.ts +10 -0
- package/dist/chats.js +23 -0
- package/dist/cli-options.d.ts +1 -2
- package/dist/cli-options.js +2 -7
- package/dist/cli.js +11 -3
- package/dist/config-command.d.ts +14 -0
- package/dist/config-command.js +35 -15
- package/dist/config-edit.d.ts +3 -3
- package/dist/config-edit.js +16 -12
- package/dist/config.d.ts +54 -34
- package/dist/config.js +40 -44
- package/dist/conform-cli.js +12 -10
- package/dist/conformance-switch.d.ts +34 -0
- package/dist/conformance-switch.js +108 -0
- package/dist/cultivate-cli.js +15 -5
- package/dist/drift-cli.js +2 -0
- package/dist/extension.d.ts +15 -0
- package/dist/extension.js +260 -83
- package/dist/host-session.d.ts +26 -0
- package/dist/host-session.js +30 -2
- package/dist/host.d.ts +5 -2
- package/dist/host.js +7 -1
- package/dist/init-config.d.ts +1 -1
- package/dist/init-config.js +20 -11
- package/dist/loop-cli.d.ts +3 -3
- package/dist/loop-cli.js +13 -16
- package/dist/loop-tools.js +18 -10
- package/dist/model-check.d.ts +8 -3
- package/dist/model-check.js +13 -8
- package/dist/new-project.js +12 -8
- package/dist/{validate-cli.js → parse-cli.js} +11 -5
- package/dist/parse-tool.d.ts +9 -0
- package/dist/parse-tool.js +33 -0
- package/dist/pi-launch.js +1 -1
- package/dist/plain/check.d.ts +53 -6
- package/dist/plain/check.js +217 -24
- package/dist/plain/concepts.d.ts +8 -0
- package/dist/plain/concepts.js +66 -23
- package/dist/plain/cycles.d.ts +21 -0
- package/dist/plain/cycles.js +190 -0
- package/dist/plain/errors.d.ts +4 -0
- package/dist/plain/errors.js +7 -0
- package/dist/plain/files.d.ts +18 -1
- package/dist/plain/files.js +39 -2
- package/dist/plain/graph.d.ts +33 -4
- package/dist/plain/graph.js +60 -12
- package/dist/plain/highlight.d.ts +3 -2
- package/dist/plain/highlight.js +4 -3
- package/dist/plain/index.d.ts +8 -5
- package/dist/plain/index.js +8 -5
- package/dist/plain/liquid.js +1 -6
- package/dist/plain/markdown.d.ts +32 -3
- package/dist/plain/markdown.js +146 -24
- package/dist/plain/module-concepts.js +2 -2
- package/dist/plain/parser.d.ts +83 -28
- package/dist/plain/parser.js +381 -260
- package/dist/plain/plain.tmLanguage.json +29 -3
- package/dist/plain/project.d.ts +48 -0
- package/dist/plain/project.js +155 -0
- package/dist/plain/spec.d.ts +90 -28
- package/dist/plain/spec.js +107 -133
- package/dist/plain/test-plain-file.d.ts +59 -0
- package/dist/plain/test-plain-file.js +232 -0
- package/dist/regenerate/activity.d.ts +20 -9
- package/dist/regenerate/activity.js +11 -4
- package/dist/regenerate/advisor-records.d.ts +93 -0
- package/dist/regenerate/advisor-records.js +110 -0
- package/dist/regenerate/advisor-runs.d.ts +87 -0
- package/dist/regenerate/advisor-runs.js +327 -0
- package/dist/regenerate/base-folder.d.ts +68 -0
- package/dist/regenerate/base-folder.js +168 -0
- package/dist/regenerate/cases.d.ts +9 -6
- package/dist/regenerate/cases.js +70 -14
- package/dist/regenerate/change-view.d.ts +8 -3
- package/dist/regenerate/change-view.js +71 -22
- package/dist/regenerate/chunk-plan.d.ts +36 -15
- package/dist/regenerate/chunk-plan.js +165 -59
- package/dist/regenerate/conform.d.ts +3 -0
- package/dist/regenerate/conform.js +6 -5
- package/dist/regenerate/conformance-agents.d.ts +18 -39
- package/dist/regenerate/conformance-agents.js +60 -88
- package/dist/regenerate/conformance.d.ts +23 -13
- package/dist/regenerate/conformance.js +119 -82
- package/dist/regenerate/diff-view.d.ts +1 -1
- package/dist/regenerate/diff-view.js +20 -4
- package/dist/regenerate/drift.d.ts +47 -15
- package/dist/regenerate/drift.js +101 -30
- package/dist/regenerate/jobs.d.ts +2 -2
- package/dist/regenerate/jobs.js +1 -1
- package/dist/regenerate/ledger.d.ts +19 -0
- package/dist/regenerate/loop.d.ts +62 -43
- package/dist/regenerate/loop.js +472 -341
- package/dist/regenerate/paths.d.ts +4 -0
- package/dist/regenerate/paths.js +7 -1
- package/dist/regenerate/plain-palette.js +2 -0
- package/dist/regenerate/prompt.d.ts +57 -19
- package/dist/regenerate/prompt.js +157 -122
- package/dist/regenerate/records.d.ts +43 -12
- package/dist/regenerate/records.js +102 -88
- package/dist/regenerate/regenerate.js +29 -11
- package/dist/regenerate/regression.d.ts +4 -4
- package/dist/regenerate/regression.js +14 -10
- package/dist/regenerate/renames.d.ts +42 -0
- package/dist/regenerate/renames.js +185 -0
- package/dist/regenerate/requires.d.ts +1 -1
- package/dist/regenerate/requires.js +10 -3
- package/dist/regenerate/resources.d.ts +5 -3
- package/dist/regenerate/resources.js +4 -4
- package/dist/regenerate/scripts-being-written.d.ts +19 -0
- package/dist/regenerate/scripts-being-written.js +30 -0
- package/dist/regenerate/spec-map-view.d.ts +2 -2
- package/dist/regenerate/spec-map-view.js +46 -15
- package/dist/regenerate/spec-map.d.ts +27 -5
- package/dist/regenerate/spec-map.js +92 -60
- package/dist/regenerate/spec-steps.d.ts +48 -5
- package/dist/regenerate/spec-steps.js +213 -28
- package/dist/regenerate/state.d.ts +4 -3
- package/dist/regenerate/stop-report.d.ts +1 -1
- package/dist/regenerate/stop-report.js +10 -10
- package/dist/regenerate/test-advisor.d.ts +101 -0
- package/dist/regenerate/test-advisor.js +379 -0
- package/dist/regenerate/test-kinds.d.ts +71 -28
- package/dist/regenerate/test-kinds.js +110 -63
- package/dist/regenerate/test-scripts.d.ts +2 -0
- package/dist/regenerate/test-scripts.js +4 -0
- package/dist/regenerate/testing-steps.d.ts +41 -0
- package/dist/regenerate/testing-steps.js +110 -0
- package/dist/regenerate/testing.d.ts +1 -1
- package/dist/regenerate/testing.js +5 -4
- package/dist/regenerate/unit-review.d.ts +1 -1
- package/dist/regenerate/unit-review.js +3 -3
- package/dist/regenerate-cli.js +11 -12
- package/dist/release-agent.js +4 -0
- package/dist/release-cli.js +2 -0
- package/dist/release-tools.js +2 -0
- package/dist/run-report.d.ts +2 -2
- package/dist/run-report.js +4 -3
- package/dist/self-invocation.d.ts +1 -1
- package/dist/self-invocation.js +1 -1
- package/dist/sentinel.js +1 -1
- package/dist/setup.d.ts +10 -0
- package/dist/setup.js +52 -4
- package/dist/skill.d.ts +10 -0
- package/dist/skill.js +6 -0
- package/dist/skills.js +3 -1
- package/dist/spec-tools.js +21 -31
- package/dist/test-plain-files.d.ts +41 -0
- package/dist/test-plain-files.js +177 -0
- package/docs/README.md +3 -1
- package/docs/authoring-plain.md +46 -13
- package/docs/chats.md +3 -1
- package/docs/config.md +20 -15
- package/docs/deploy.md +1 -1
- package/docs/development.md +2 -1
- package/docs/headless.md +15 -18
- package/docs/hosting.md +28 -12
- package/docs/installation.md +1 -0
- package/docs/parser.md +88 -27
- package/docs/regenerating.md +87 -52
- package/docs/sentinel.md +1 -1
- package/docs/skills-and-gates.md +3 -1
- package/docs/test-advisor.md +110 -0
- package/docs/theming.md +1 -1
- package/docs/using.md +17 -15
- package/genome/rules/bullet-continuation.md +8 -8
- package/genome/rules/concise-specs-examples.md +111 -80
- package/genome/rules/concise-specs-integration-examples.md +56 -41
- package/genome/rules/concise-specs.md +17 -16
- package/genome/rules/definitions.md +5 -5
- package/genome/rules/examples/integration-embedded-testing/run_conformance_tests_java.sh +1 -1
- package/genome/rules/func-specs.md +69 -74
- package/genome/rules/impl-reqs.md +4 -4
- package/genome/rules/integration-embedded-testing.md +20 -20
- package/genome/rules/integration-embedded.md +31 -31
- package/genome/rules/integration-standalone.md +9 -9
- package/genome/rules/integrations.md +18 -18
- package/genome/rules/linked-resources.md +19 -16
- package/genome/rules/module-structure.md +21 -14
- package/genome/rules/project-model.md +18 -17
- package/genome/rules/required-concepts.md +1 -1
- package/genome/rules/requires-modules.md +6 -5
- package/genome/rules/semantic-ids.md +49 -0
- package/genome/rules/stdlib.md +4 -5
- package/genome/rules/test-plain-files.md +130 -0
- package/genome/rules/test-reqs.md +8 -8
- package/genome/rules/turri-regeneration.md +23 -8
- package/genome/skills/analyze-2-func-specs/SKILL.md +36 -21
- package/genome/skills/analyze-func-specs/SKILL.md +46 -32
- package/genome/skills/break-down-func-spec/SKILL.md +91 -67
- package/genome/skills/consolidate-concepts/SKILL.md +10 -10
- package/genome/skills/convert-old-syntax/SKILL.md +68 -0
- package/genome/skills/convert-old-syntax/references/rubric.md +11 -0
- package/genome/skills/debug-specs/SKILL.md +62 -56
- package/genome/skills/implement-conformance-testing-script/SKILL.md +19 -15
- package/genome/skills/implement-conformance-testing-script/assets/run_conformance_tests_java.sh +2 -2
- package/genome/skills/implement-conformance-testing-script/assets/run_conformance_tests_python.sh +2 -2
- package/genome/skills/implement-unit-testing-script/SKILL.md +14 -10
- package/genome/skills/refactor-module/SKILL.md +35 -35
- package/genome/skills/resolve-section-ownership/SKILL.md +9 -9
- package/genome/skills/resolve-spec-conflict/SKILL.md +35 -29
- package/package.json +7 -6
- package/dist/regenerate/choose-conformance.d.ts +0 -32
- package/dist/regenerate/choose-conformance.js +0 -117
- /package/dist/{validate-cli.d.ts → parse-cli.d.ts} +0 -0
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { resolve } from "node:path";
|
|
2
|
+
import { defineTool } from "@earendil-works/pi-coding-agent";
|
|
3
|
+
import { Type } from "typebox";
|
|
4
|
+
import { checkPlainTargets, formatCheckResults } from "./plain/check.js";
|
|
5
|
+
/** parse, for the conversation and for the skills that write specs alike: it checks the ***plain, never the code. */
|
|
6
|
+
export function checkPlainTool(cwd) {
|
|
7
|
+
return defineTool({
|
|
8
|
+
name: "parse",
|
|
9
|
+
label: "Parse ***plain",
|
|
10
|
+
description: "Parse one or more .plain modules - their syntax and structure - without regenerating: each with its test plain files, its " +
|
|
11
|
+
"imports and required modules, reporting every syntax, concept and structure error with the line it is on, " +
|
|
12
|
+
"and every test plain file outside its module's folder. A module that passes lists each functional spec with its " +
|
|
13
|
+
"test plain file and its scenarios - one with no test plain file is not tested. Pass a directory to " +
|
|
14
|
+
"check every .plain module in it; a test plain file or test folder is refused - name its module. Fast, free, and offline. " +
|
|
15
|
+
"It reads the ***plain alone: it runs no code and no test - whether the code does what the ***plain says is the conformance tests', with conform.",
|
|
16
|
+
promptSnippet: "parse ***plain - its syntax and structure, never its code or tests - regenerating nothing",
|
|
17
|
+
promptGuidelines: [
|
|
18
|
+
"Run parse after editing a .plain file, and before telling the user it is ready. Never shell out to another renderer to check one.",
|
|
19
|
+
],
|
|
20
|
+
parameters: Type.Object({
|
|
21
|
+
target: Type.String({ description: "A .plain file, or a directory holding .plain files" }),
|
|
22
|
+
template_dir: Type.Optional(Type.Array(Type.String(), { description: "Extra template directories" })),
|
|
23
|
+
}),
|
|
24
|
+
async execute(_id, params) {
|
|
25
|
+
const results = checkPlainTargets(resolve(cwd, params.target), (params.template_dir ?? []).map((dir) => resolve(cwd, dir)));
|
|
26
|
+
const failed = results.filter((result) => !result.ok).length;
|
|
27
|
+
const text = results.length === 0
|
|
28
|
+
? `No .plain files found in ${params.target}.`
|
|
29
|
+
: `${formatCheckResults(results)}\n\n${failed === 0 ? "All specs valid." : `${failed} of ${results.length} failed.`}`;
|
|
30
|
+
return { content: [{ type: "text", text }], details: { failed, checked: results.length } };
|
|
31
|
+
},
|
|
32
|
+
});
|
|
33
|
+
}
|
package/dist/pi-launch.js
CHANGED
|
@@ -70,7 +70,7 @@ if (interactive) {
|
|
|
70
70
|
args.splice(asked, 1);
|
|
71
71
|
restore = saved;
|
|
72
72
|
}
|
|
73
|
-
driveTerminal(processSession(() => createTurriCore({}, { persist: true })),
|
|
73
|
+
driveTerminal(processSession(() => createTurriCore({}, { persist: true, advisor: true })),
|
|
74
74
|
// With none of the person's chats saved - only the sentinel's - pi starts a fresh one, and every saved chat is opened beside it.
|
|
75
75
|
restore !== undefined ? { restore, skipFirst: restore.chats.some((chat) => chat.role === undefined) } : {});
|
|
76
76
|
}
|
package/dist/plain/check.d.ts
CHANGED
|
@@ -1,23 +1,70 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Static validation of
|
|
2
|
+
* Static validation of ***plain, using this repo's own parser - `turri parse`, the chat's `parse`, and the check
|
|
3
|
+
* `change_plain` and `turri change-plain` run on what a change touched.
|
|
3
4
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
5
|
+
* It parses each module with its test plain files, resolves its imports and required modules, and reports every error
|
|
6
|
+
* the parser can see without generating anything. The parser's errors come one at a time, the first it meets, as the
|
|
7
|
+
* reference gives them. turri's own checks list everything they find at once: every functional spec without a test
|
|
8
|
+
* plain file, and every test plain file that is not where it belongs. A module whose functional specs have scenarios
|
|
9
|
+
* but which has no test reqs, or no conformance testing script for its stack, passes with a warning.
|
|
7
10
|
*/
|
|
11
|
+
export interface CheckedFunctionalSpec {
|
|
12
|
+
funcId: string;
|
|
13
|
+
/** Its test plain files, from the project's folder. */
|
|
14
|
+
testPlainFiles: string[];
|
|
15
|
+
conformance: number;
|
|
16
|
+
acceptance: number;
|
|
17
|
+
}
|
|
8
18
|
export interface CheckResult {
|
|
19
|
+
/** The module's file - or, for the test folder's check, the test folder. */
|
|
9
20
|
file: string;
|
|
10
21
|
ok: boolean;
|
|
11
22
|
moduleName?: string;
|
|
12
|
-
|
|
23
|
+
/** The module's own functional specs, each with its test plain files and how many scenarios they have. */
|
|
24
|
+
functionalSpecs?: CheckedFunctionalSpec[];
|
|
13
25
|
requiredModules?: string[];
|
|
14
26
|
/** The parser's own message, verbatim - it names the line and the construct. */
|
|
15
27
|
error?: string;
|
|
16
28
|
errorType?: string;
|
|
29
|
+
/** turri's next step for the error, when it has one: old syntax, say. */
|
|
30
|
+
nextStep?: string;
|
|
31
|
+
/** turri's own findings, all of them: what is wrong, and what to do about each. */
|
|
32
|
+
problems?: string[];
|
|
33
|
+
/** What does not fail it but leaves something undone: a step of adding tests skipped, and the next step. */
|
|
34
|
+
warnings?: string[];
|
|
17
35
|
}
|
|
36
|
+
/** Whether a parser error is one the previous ***plain syntax makes. */
|
|
37
|
+
export declare function isPreviousSyntaxError(message: string): boolean;
|
|
38
|
+
/** turri's next step for a module in the previous ***plain syntax. */
|
|
39
|
+
export declare function previousSyntaxStep(file: string): string;
|
|
40
|
+
/**
|
|
41
|
+
* The modules of a project whose requires chains a module is in and that nothing requires: the tips its conformance
|
|
42
|
+
* tests are written in, by name with their files. None when nothing requires it - it is a tip itself.
|
|
43
|
+
*/
|
|
44
|
+
export declare function tipsAbove(moduleName: string, projectDir: string): Array<{
|
|
45
|
+
name: string;
|
|
46
|
+
file: string;
|
|
47
|
+
}>;
|
|
18
48
|
export declare function checkPlainFile(file: string, templateDirs?: string[]): CheckResult;
|
|
49
|
+
/** The check of a project's test folder: every test plain file not where it belongs. Undefined when there is none. */
|
|
50
|
+
export declare function checkTestFolder(projectDir: string): CheckResult | undefined;
|
|
19
51
|
/** Every `.plain` file directly in a directory, or the single file given. */
|
|
20
52
|
export declare function resolvePlainTargets(target: string): string[];
|
|
53
|
+
/**
|
|
54
|
+
* Check a file, or every .plain file directly in a folder - never a test folder, which holds no module - and then the
|
|
55
|
+
* project's test folder, once, at the end.
|
|
56
|
+
*/
|
|
21
57
|
export declare function checkPlainTargets(target: string, templateDirs?: string[]): CheckResult[];
|
|
22
|
-
/**
|
|
58
|
+
/**
|
|
59
|
+
* The check of what a ***plain change touched, shared by `change_plain` and `turri change-plain`: each module it
|
|
60
|
+
* touched, an import module as the modules that import it read it, and each test plain file as its module - by its
|
|
61
|
+
* folder - is parsed with its test plain files; a test plain file the change put where it does not belong is refused
|
|
62
|
+
* as `parse` refuses it. A file the change deleted is checked through the module it belonged to.
|
|
63
|
+
*/
|
|
64
|
+
export declare function checkTouchedPlain(paths: string[]): CheckResult[];
|
|
65
|
+
/**
|
|
66
|
+
* One line per file, plus the parser's message under any that failed and turri's next step, or every problem turri
|
|
67
|
+
* found; a module that passed, its functional specs under it, each with its test plain file and its scenarios - and
|
|
68
|
+
* a WARN line for each warning, a step of adding tests it skipped.
|
|
69
|
+
*/
|
|
23
70
|
export declare function formatCheckResults(results: CheckResult[]): string;
|
package/dist/plain/check.js
CHANGED
|
@@ -1,43 +1,166 @@
|
|
|
1
|
-
import { existsSync, readdirSync, statSync } from "node:fs";
|
|
2
|
-
import { basename, extname, join, resolve } from "node:path";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
1
|
+
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
|
|
2
|
+
import { basename, extname, join, relative, resolve, sep } from "node:path";
|
|
3
|
+
import { testDirOf } from "../config.js";
|
|
4
|
+
import { projectDirOf, resolveTemplateDirs } from "../regenerate/paths.js";
|
|
5
|
+
import { importingModules, isImportModule } from "../regenerate/requires.js";
|
|
6
|
+
import { testingGap } from "../regenerate/testing-steps.js";
|
|
7
|
+
import { missingBaseFolder } from "../regenerate/base-folder.js";
|
|
8
|
+
import { conformanceTestsOn } from "../conformance-switch.js";
|
|
9
|
+
import { testedFuncIds } from "../regenerate/records.js";
|
|
10
|
+
import { moduleOfTestPlainFile, projectModules, strayTestPlainFiles, testPlaceOf, testPlainFileRefusal } from "../test-plain-files.js";
|
|
11
|
+
import { plainFileParser, PLAIN_SOURCE_FILE_EXTENSION, PREVIOUS_SYNTAX_HINT, readPlainSourceMetadata, REQUIRES_DIRECTIVE } from "./parser.js";
|
|
12
|
+
import { ACCEPTANCE_TESTS, CONFORMANCE_TESTS, getFunctionalSpecs, TEST_PLAIN_FILES, } from "./spec.js";
|
|
13
|
+
/** Whether a parser error is one the previous ***plain syntax makes. */
|
|
14
|
+
export function isPreviousSyntaxError(message) {
|
|
15
|
+
return message.includes(PREVIOUS_SYNTAX_HINT) || message.includes("is not allowed in a spec plain file; test scenarios live in test plain files");
|
|
16
|
+
}
|
|
17
|
+
/** turri's next step for a module in the previous ***plain syntax. */
|
|
18
|
+
export function previousSyntaxStep(file) {
|
|
19
|
+
return (`${basename(file)} is in the old ***plain syntax. Ask turri to convert it: it goes one module at a time, ` +
|
|
20
|
+
"proposing func-ids and test plain files, and asks you before writing anything.");
|
|
21
|
+
}
|
|
22
|
+
/** A path from the project's folder, as a person reads it. */
|
|
23
|
+
function shown(path, projectDir) {
|
|
24
|
+
return relative(projectDir, resolve(path)).split(sep).join("/");
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* The modules of a project whose requires chains a module is in and that nothing requires: the tips its conformance
|
|
28
|
+
* tests are written in, by name with their files. None when nothing requires it - it is a tip itself.
|
|
29
|
+
*/
|
|
30
|
+
export function tipsAbove(moduleName, projectDir) {
|
|
31
|
+
const modules = projectModules(projectDir);
|
|
32
|
+
const requiredBy = new Map();
|
|
33
|
+
for (const [name, file] of modules) {
|
|
34
|
+
let requires = [];
|
|
35
|
+
try {
|
|
36
|
+
requires = readPlainSourceMetadata(readFileSync(file, "utf8")).metadata[REQUIRES_DIRECTIVE] ?? [];
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
for (const required of requires) {
|
|
42
|
+
const below = basename(required.replace(/^\.\//, ""), PLAIN_SOURCE_FILE_EXTENSION);
|
|
43
|
+
requiredBy.set(below, [...(requiredBy.get(below) ?? []), name]);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
const tips = new Set();
|
|
47
|
+
const seen = new Set();
|
|
48
|
+
const up = (name) => {
|
|
49
|
+
if (seen.has(name))
|
|
50
|
+
return;
|
|
51
|
+
seen.add(name);
|
|
52
|
+
const above = requiredBy.get(name) ?? [];
|
|
53
|
+
if (above.length === 0 && name !== moduleName)
|
|
54
|
+
tips.add(name);
|
|
55
|
+
for (const next of above)
|
|
56
|
+
up(next);
|
|
57
|
+
};
|
|
58
|
+
up(moduleName);
|
|
59
|
+
return [...tips].sort().flatMap((name) => (modules.has(name) ? [{ name, file: modules.get(name) }] : []));
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* The steps of adding tests a module skipped, as warnings. A module another requires has its conformance tests
|
|
63
|
+
* written in the tips above it, from their test reqs and with their script: it is judged by each of them, which says so.
|
|
64
|
+
*/
|
|
65
|
+
function testingGapsOf(moduleName, path, tree, projectDir, templateDirs) {
|
|
66
|
+
// Conformance tests off: no step of adding them is skipped - and scenarios written are said not to be tested.
|
|
67
|
+
if (!conformanceTestsOn(path)) {
|
|
68
|
+
const tested = testedFuncIds(tree);
|
|
69
|
+
return tested.length > 0
|
|
70
|
+
? [`its scenarios are not tested - ${tested.map((funcId) => `${moduleName}:${funcId}`).join(", ")}: conformance tests are off (conformance-tests: false, or unset). Asking for them in the chat turns them on, or conformance-tests: true in config.turri.yaml.`]
|
|
71
|
+
: [];
|
|
72
|
+
}
|
|
73
|
+
const tips = tipsAbove(moduleName, projectDir);
|
|
74
|
+
if (tips.length === 0) {
|
|
75
|
+
const gap = testingGap({ moduleName, plainFile: path, tree });
|
|
76
|
+
return gap !== undefined ? [gap.text] : [];
|
|
77
|
+
}
|
|
78
|
+
return tips.flatMap((tip) => {
|
|
79
|
+
let tipTree;
|
|
80
|
+
try {
|
|
81
|
+
[, tipTree] = plainFileParser(basename(tip.file), resolveTemplateDirs(tip.file, templateDirs), {
|
|
82
|
+
allowNoFunctionalities: true,
|
|
83
|
+
testDir: testDirOf(tip.file),
|
|
84
|
+
requireTestPlainFiles: false,
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
// A tip that does not parse is reported when it is checked itself.
|
|
89
|
+
return [];
|
|
90
|
+
}
|
|
91
|
+
const gap = testingGap({ moduleName: tip.name, plainFile: tip.file, tree: tipTree });
|
|
92
|
+
return gap !== undefined ? [`${moduleName}'s conformance tests are written in ${tip.name}, which requires it: ${gap.text}`] : [];
|
|
93
|
+
});
|
|
94
|
+
}
|
|
6
95
|
export function checkPlainFile(file, templateDirs = []) {
|
|
7
96
|
const path = resolve(file);
|
|
8
97
|
if (extname(path) !== PLAIN_SOURCE_FILE_EXTENSION) {
|
|
9
|
-
return {
|
|
10
|
-
file,
|
|
11
|
-
ok: false,
|
|
12
|
-
error: `Not a ${PLAIN_SOURCE_FILE_EXTENSION} file.`,
|
|
13
|
-
errorType: "UsageError",
|
|
14
|
-
};
|
|
98
|
+
return { file, ok: false, error: `Not a ${PLAIN_SOURCE_FILE_EXTENSION} file.`, errorType: "UsageError" };
|
|
15
99
|
}
|
|
16
100
|
if (!existsSync(path)) {
|
|
17
101
|
return { file, ok: false, error: "File does not exist.", errorType: "UsageError" };
|
|
18
102
|
}
|
|
103
|
+
const refusal = testPlainFileRefusal(path);
|
|
104
|
+
if (refusal !== undefined)
|
|
105
|
+
return { file, ok: false, error: refusal, errorType: "UsageError" };
|
|
106
|
+
const projectDir = projectDirOf(path);
|
|
107
|
+
const testDir = testDirOf(path);
|
|
19
108
|
try {
|
|
20
109
|
const dirs = resolveTemplateDirs(path, templateDirs);
|
|
21
110
|
// A module with no functional specs yet is valid - it is how every module starts, and how
|
|
22
111
|
// an import module always is. It is reported, not failed.
|
|
23
|
-
const [moduleName, tree, requiredModules] = plainFileParser(basename(path), dirs, {
|
|
112
|
+
const [moduleName, tree, requiredModules] = plainFileParser(basename(path), dirs, {
|
|
113
|
+
allowNoFunctionalities: true,
|
|
114
|
+
testDir,
|
|
115
|
+
requireTestPlainFiles: false,
|
|
116
|
+
});
|
|
117
|
+
// Scenarios with a step of adding tests skipped - the test reqs, or the conformance testing script: a warning, never a failure.
|
|
118
|
+
const gaps = testingGapsOf(moduleName, path, tree, projectDir, templateDirs);
|
|
119
|
+
// A codebase that requires nothing starts from its base folder: one named and not there stops its first pulse.
|
|
120
|
+
const base = requiredModules.length === 0 && !isImportModule(path) ? missingBaseFolder(path) : undefined;
|
|
121
|
+
if (base !== undefined)
|
|
122
|
+
gaps.push(base);
|
|
123
|
+
const bindings = Object.entries(tree[TEST_PLAIN_FILES] ?? {});
|
|
24
124
|
return {
|
|
25
125
|
file,
|
|
26
126
|
ok: true,
|
|
27
127
|
moduleName,
|
|
28
|
-
|
|
128
|
+
...(gaps.length > 0 ? { warnings: gaps } : {}),
|
|
129
|
+
functionalSpecs: getFunctionalSpecs(tree).map((item) => ({
|
|
130
|
+
funcId: item.id,
|
|
131
|
+
testPlainFiles: bindings
|
|
132
|
+
.filter(([, binding]) => binding.module === moduleName && binding.func_id === item.id)
|
|
133
|
+
.map(([name]) => shown(join(dirs[0], name), projectDir)),
|
|
134
|
+
conformance: item[CONFORMANCE_TESTS].length,
|
|
135
|
+
acceptance: item[ACCEPTANCE_TESTS].length,
|
|
136
|
+
})),
|
|
29
137
|
requiredModules,
|
|
30
138
|
};
|
|
31
139
|
}
|
|
32
140
|
catch (error) {
|
|
141
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
33
142
|
return {
|
|
34
143
|
file,
|
|
35
144
|
ok: false,
|
|
36
|
-
error:
|
|
145
|
+
error: message,
|
|
37
146
|
errorType: error instanceof Error ? error.name : "Error",
|
|
147
|
+
...(isPreviousSyntaxError(message) ? { nextStep: previousSyntaxStep(path) } : {}),
|
|
38
148
|
};
|
|
39
149
|
}
|
|
40
150
|
}
|
|
151
|
+
/** The check of a project's test folder: every test plain file not where it belongs. Undefined when there is none. */
|
|
152
|
+
export function checkTestFolder(projectDir) {
|
|
153
|
+
const strays = strayTestPlainFiles(projectDir);
|
|
154
|
+
if (strays.length === 0)
|
|
155
|
+
return undefined;
|
|
156
|
+
const testDir = testDirOf(join(projectDir, `x${PLAIN_SOURCE_FILE_EXTENSION}`));
|
|
157
|
+
return {
|
|
158
|
+
file: `${shown(testDir, projectDir)}/`,
|
|
159
|
+
ok: false,
|
|
160
|
+
errorType: `${strays.length} test plain file${strays.length === 1 ? "" : "s"} outside a module's folder`,
|
|
161
|
+
problems: strays.map((stray) => `${stray.file} - ${stray.problem}`),
|
|
162
|
+
};
|
|
163
|
+
}
|
|
41
164
|
/** Every `.plain` file directly in a directory, or the single file given. */
|
|
42
165
|
export function resolvePlainTargets(target) {
|
|
43
166
|
const path = resolve(target);
|
|
@@ -49,27 +172,97 @@ export function resolvePlainTargets(target) {
|
|
|
49
172
|
}
|
|
50
173
|
return [path];
|
|
51
174
|
}
|
|
175
|
+
/**
|
|
176
|
+
* Check a file, or every .plain file directly in a folder - never a test folder, which holds no module - and then the
|
|
177
|
+
* project's test folder, once, at the end.
|
|
178
|
+
*/
|
|
52
179
|
export function checkPlainTargets(target, templateDirs = []) {
|
|
53
|
-
|
|
180
|
+
const path = resolve(target);
|
|
181
|
+
if (existsSync(path) && statSync(path).isDirectory()) {
|
|
182
|
+
const refusal = testPlainFileRefusal(path);
|
|
183
|
+
if (refusal !== undefined)
|
|
184
|
+
return [{ file: target, ok: false, error: refusal, errorType: "UsageError" }];
|
|
185
|
+
}
|
|
186
|
+
const results = resolvePlainTargets(target).map((file) => checkPlainFile(file, templateDirs));
|
|
187
|
+
const projects = [...new Set(resolvePlainTargets(target).map((file) => projectDirOf(file)))];
|
|
188
|
+
for (const project of projects) {
|
|
189
|
+
const folder = checkTestFolder(project);
|
|
190
|
+
if (folder !== undefined)
|
|
191
|
+
results.push(folder);
|
|
192
|
+
}
|
|
193
|
+
return results;
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* The check of what a ***plain change touched, shared by `change_plain` and `turri change-plain`: each module it
|
|
197
|
+
* touched, an import module as the modules that import it read it, and each test plain file as its module - by its
|
|
198
|
+
* folder - is parsed with its test plain files; a test plain file the change put where it does not belong is refused
|
|
199
|
+
* as `parse` refuses it. A file the change deleted is checked through the module it belonged to.
|
|
200
|
+
*/
|
|
201
|
+
export function checkTouchedPlain(paths) {
|
|
202
|
+
const modules = new Set();
|
|
203
|
+
const results = [];
|
|
204
|
+
for (const given of paths) {
|
|
205
|
+
const path = resolve(given);
|
|
206
|
+
const place = testPlaceOf(path);
|
|
207
|
+
if (place !== undefined && !place.directory) {
|
|
208
|
+
const stray = strayTestPlainFiles(place.projectDir).find((found) => resolve(place.projectDir, found.file) === path);
|
|
209
|
+
if (stray !== undefined) {
|
|
210
|
+
results.push({ file: path, ok: false, errorType: "a test plain file outside its module's folder", problems: [`${stray.file} - ${stray.problem}`] });
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
const owner = moduleOfTestPlainFile(path);
|
|
214
|
+
if (owner?.plainFile !== undefined)
|
|
215
|
+
modules.add(owner.plainFile);
|
|
216
|
+
else if (owner !== undefined)
|
|
217
|
+
results.push({ file: path, ok: false, errorType: "a test plain file outside a module's folder", problems: [`${shown(path, place.projectDir)} - no module ${owner.module}`] });
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
if (!existsSync(path))
|
|
221
|
+
continue;
|
|
222
|
+
const importing = isImportModule(path) ? importingModules(path) : [];
|
|
223
|
+
for (const module of importing.length > 0 ? importing : [path])
|
|
224
|
+
modules.add(resolve(module));
|
|
225
|
+
}
|
|
226
|
+
for (const module of modules)
|
|
227
|
+
results.push(checkPlainFile(module));
|
|
228
|
+
return results;
|
|
229
|
+
}
|
|
230
|
+
/** Columns padded to their widest cell, each line indented under the module's. */
|
|
231
|
+
function columns(rows) {
|
|
232
|
+
const widths = rows.reduce((acc, row) => row.map((cell, index) => Math.max(acc[index] ?? 0, cell.length)), []);
|
|
233
|
+
return rows.map((row) => ` ${row.map((cell, index) => (index === row.length - 1 ? cell : cell.padEnd(widths[index]))).join(" ")}`);
|
|
234
|
+
}
|
|
235
|
+
/** How many scenarios a functional spec has, or that it has none and so is not tested. */
|
|
236
|
+
function scenarioCounts(spec) {
|
|
237
|
+
const counts = [spec.conformance > 0 ? `${spec.conformance} conformance` : "", spec.acceptance > 0 ? `${spec.acceptance} acceptance` : ""].filter(Boolean);
|
|
238
|
+
return counts.length === 0 ? "no scenarios - not tested" : counts.join(", ");
|
|
54
239
|
}
|
|
55
|
-
/**
|
|
240
|
+
/**
|
|
241
|
+
* One line per file, plus the parser's message under any that failed and turri's next step, or every problem turri
|
|
242
|
+
* found; a module that passed, its functional specs under it, each with its test plain file and its scenarios - and
|
|
243
|
+
* a WARN line for each warning, a step of adding tests it skipped.
|
|
244
|
+
*/
|
|
56
245
|
export function formatCheckResults(results) {
|
|
57
246
|
const lines = [];
|
|
58
247
|
for (const result of results) {
|
|
59
|
-
const name = basename(result.file);
|
|
248
|
+
const name = result.file.endsWith("/") ? result.file : basename(result.file);
|
|
60
249
|
if (result.ok) {
|
|
61
|
-
const
|
|
62
|
-
const requires = result.requiredModules && result.requiredModules.length > 0
|
|
63
|
-
|
|
64
|
-
: "";
|
|
65
|
-
lines.push(frids.length === 0
|
|
250
|
+
const specs = result.functionalSpecs ?? [];
|
|
251
|
+
const requires = result.requiredModules && result.requiredModules.length > 0 ? `, requires ${result.requiredModules.join(", ")}` : "";
|
|
252
|
+
lines.push(specs.length === 0
|
|
66
253
|
? `PASS ${name} no functional specs yet${requires}`
|
|
67
|
-
: `PASS ${name} ${
|
|
254
|
+
: `PASS ${name} ${specs.length} functional spec${specs.length === 1 ? "" : "s"}${requires}`);
|
|
255
|
+
// No test plain file is a functional spec not tested - the one way to say so: an empty one is a syntax error.
|
|
256
|
+
lines.push(...columns(specs.map((spec) => [spec.funcId, spec.testPlainFiles.length > 0 ? spec.testPlainFiles.join(", ") : "no test plain file", scenarioCounts(spec)])));
|
|
257
|
+
for (const warning of result.warnings ?? [])
|
|
258
|
+
lines.push(`WARN ${name} ${warning}`);
|
|
68
259
|
}
|
|
69
260
|
else {
|
|
70
261
|
lines.push(`FAIL ${name} ${result.errorType ?? "Error"}`);
|
|
71
|
-
for (const line of (result.error ?? "").split("\n"))
|
|
262
|
+
for (const line of [...(result.error ?? "").split("\n").filter((line, index) => index > 0 || line !== ""), ...(result.problems ?? [])])
|
|
72
263
|
lines.push(` ${line}`);
|
|
264
|
+
if (result.nextStep !== undefined)
|
|
265
|
+
lines.push(` ${result.nextStep}`);
|
|
73
266
|
}
|
|
74
267
|
}
|
|
75
268
|
return lines.join("\n");
|
package/dist/plain/concepts.d.ts
CHANGED
|
@@ -26,4 +26,12 @@ export declare function extractConceptsFromSpecText(text: string): string[];
|
|
|
26
26
|
export declare function collectConcepts(plainSource: MarshalledPlainSource): [string[], string[]];
|
|
27
27
|
export declare function validateConcepts(marshalled: MarshalledPlainSource): string[];
|
|
28
28
|
export declare function findConceptDefinitionsInPlainSource(concept: string, definitions: Section | null): AnyNode[];
|
|
29
|
+
export declare const MAX_REPORTED_CYCLES = 10;
|
|
30
|
+
/**
|
|
31
|
+
* The elementary cycles in the graph, each as a "Cycle N: :A: -> :B: -> :A:" line followed by the definition of
|
|
32
|
+
* every concept in the cycle. Arrows point from a definition to a concept it mentions, each cycle starts at its
|
|
33
|
+
* earliest-defined concept, and cycles are ordered by definition position, so the report reads top to bottom like
|
|
34
|
+
* the file. At most MAX_REPORTED_CYCLES are listed; the search stops once one more than that is found.
|
|
35
|
+
*/
|
|
36
|
+
export declare function formatCyclesError(adjacency: Map<string, string[]>, conceptDefinitions: Map<string, SpecificationItem>): string;
|
|
29
37
|
export declare function sortDefinitions(definitions: SpecificationItem[]): void;
|
package/dist/plain/concepts.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
import { simpleCycles } from "./cycles.js";
|
|
1
2
|
import { PlainSyntaxError } from "./errors.js";
|
|
2
3
|
import { renderToMarkdown } from "./markdown.js";
|
|
3
|
-
import {
|
|
4
|
+
import { DEFINITIONS, FUNCTIONAL_REQUIREMENTS, NON_FUNCTIONAL_REQUIREMENTS, TEST_REQUIREMENTS, getSpecificationItems, } from "./spec.js";
|
|
4
5
|
/** Port of concept_utils.py. */
|
|
5
6
|
/**
|
|
6
7
|
* ***plain's own concepts, each with what it means, in a line: every module can name them, and none may define them.
|
|
@@ -123,11 +124,6 @@ export function validateConcepts(marshalled) {
|
|
|
123
124
|
]) {
|
|
124
125
|
errors.push(...validateConceptsInSpecGroup(specGroup, marshalled, treeBasedConcepts));
|
|
125
126
|
}
|
|
126
|
-
for (const funcSpec of getSpecificationItems(marshalled, FUNCTIONAL_REQUIREMENTS) ?? []) {
|
|
127
|
-
for (const test of funcSpec[ACCEPTANCE_TESTS] ?? []) {
|
|
128
|
-
errors.push(...validateConceptsInSpec(test, treeBasedConcepts, ACCEPTANCE_TESTS));
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
127
|
return errors;
|
|
132
128
|
}
|
|
133
129
|
export function findConceptDefinitionsInPlainSource(concept, definitions) {
|
|
@@ -170,13 +166,13 @@ function buildAdjacencyList(definitions) {
|
|
|
170
166
|
return [adjacency, conceptDefinitions];
|
|
171
167
|
}
|
|
172
168
|
/**
|
|
173
|
-
*
|
|
169
|
+
* The order graphlib's `TopologicalSorter.static_order` gives, reproduced.
|
|
174
170
|
*
|
|
175
|
-
* It
|
|
176
|
-
*
|
|
177
|
-
*
|
|
178
|
-
* assert that exact order - a generic topological
|
|
179
|
-
* constraints but produce a different sequence.
|
|
171
|
+
* It hands out every node that is ready at once, in the order the nodes were first
|
|
172
|
+
* added, then the nodes each one frees, in the order it frees them - Kahn's algorithm
|
|
173
|
+
* one generation at a time. The ordering of independent concepts therefore depends on
|
|
174
|
+
* insertion order, and the Python tests assert that exact order - a generic topological
|
|
175
|
+
* sort would pass the dependency constraints but produce a different sequence.
|
|
180
176
|
*/
|
|
181
177
|
function topologicalSort(successors) {
|
|
182
178
|
const inDegree = new Map();
|
|
@@ -210,22 +206,69 @@ function topologicalSort(successors) {
|
|
|
210
206
|
return { order, cycle: [...remaining.keys()] };
|
|
211
207
|
return { order, cycle: null };
|
|
212
208
|
}
|
|
209
|
+
export const MAX_REPORTED_CYCLES = 10;
|
|
210
|
+
/**
|
|
211
|
+
* The elementary cycles in the graph, each as a "Cycle N: :A: -> :B: -> :A:" line followed by the definition of
|
|
212
|
+
* every concept in the cycle. Arrows point from a definition to a concept it mentions, each cycle starts at its
|
|
213
|
+
* earliest-defined concept, and cycles are ordered by definition position, so the report reads top to bottom like
|
|
214
|
+
* the file. At most MAX_REPORTED_CYCLES are listed; the search stops once one more than that is found.
|
|
215
|
+
*/
|
|
216
|
+
export function formatCyclesError(adjacency, conceptDefinitions) {
|
|
217
|
+
const position = new Map([...conceptDefinitions.keys()].map((concept, index) => [concept, index]));
|
|
218
|
+
const at = (concept) => position.get(concept);
|
|
219
|
+
// Keyed in definition order, so a search cut short by the limit has found the earliest cycles.
|
|
220
|
+
const byPosition = new Map();
|
|
221
|
+
for (const concept of [...conceptDefinitions.keys(), ...adjacency.keys()]) {
|
|
222
|
+
if (!byPosition.has(concept))
|
|
223
|
+
byPosition.set(concept, adjacency.get(concept));
|
|
224
|
+
}
|
|
225
|
+
let cycles = [];
|
|
226
|
+
for (const found of simpleCycles(byPosition, MAX_REPORTED_CYCLES + 1)) {
|
|
227
|
+
// Reversed, since the edges point from a used concept to the one defined with it.
|
|
228
|
+
const cycle = [...found].reverse();
|
|
229
|
+
let first = 0;
|
|
230
|
+
cycle.forEach((concept, index) => {
|
|
231
|
+
if (at(concept) < at(cycle[first]))
|
|
232
|
+
first = index;
|
|
233
|
+
});
|
|
234
|
+
cycles.push([...cycle.slice(first), ...cycle.slice(0, first)]);
|
|
235
|
+
}
|
|
236
|
+
cycles.sort((a, b) => {
|
|
237
|
+
if (at(a[0]) !== at(b[0]))
|
|
238
|
+
return at(a[0]) - at(b[0]);
|
|
239
|
+
if (a.length !== b.length)
|
|
240
|
+
return a.length - b.length;
|
|
241
|
+
for (let i = 0; i < a.length; i += 1)
|
|
242
|
+
if (at(a[i]) !== at(b[i]))
|
|
243
|
+
return at(a[i]) - at(b[i]);
|
|
244
|
+
return 0;
|
|
245
|
+
});
|
|
246
|
+
let lines;
|
|
247
|
+
if (cycles.length > MAX_REPORTED_CYCLES) {
|
|
248
|
+
cycles = cycles.slice(0, MAX_REPORTED_CYCLES);
|
|
249
|
+
lines = [
|
|
250
|
+
`Plain syntax error: Found more than ${MAX_REPORTED_CYCLES} cycles in concept definitions. Cycles are not allowed.`,
|
|
251
|
+
`Showing the first ${MAX_REPORTED_CYCLES}.`,
|
|
252
|
+
];
|
|
253
|
+
}
|
|
254
|
+
else {
|
|
255
|
+
lines = [`Plain syntax error: Found ${cycles.length} cycle(s) in concept definitions. Cycles are not allowed.`];
|
|
256
|
+
}
|
|
257
|
+
cycles.forEach((cycle, index) => {
|
|
258
|
+
lines.push("");
|
|
259
|
+
lines.push(`Cycle ${index + 1}: ` + [...cycle, cycle[0]].join(" -> "));
|
|
260
|
+
// Concepts defined on one line can share a cycle; that line is printed once.
|
|
261
|
+
lines.push(...new Set(cycle.map((concept) => conceptDefinitions.get(concept).markdown)));
|
|
262
|
+
});
|
|
263
|
+
return lines.join("\n");
|
|
264
|
+
}
|
|
213
265
|
export function sortDefinitions(definitions) {
|
|
214
266
|
if (definitions.length <= 1)
|
|
215
267
|
return;
|
|
216
268
|
const [adjacency, conceptDefinitions] = buildAdjacencyList(definitions);
|
|
217
269
|
const { order, cycle } = topologicalSort(adjacency);
|
|
218
|
-
if (cycle !== null)
|
|
219
|
-
|
|
220
|
-
const cyclicDefinitions = cycle
|
|
221
|
-
.slice(0, 2)
|
|
222
|
-
.map((concept) => conceptDefinitions.get(concept)?.markdown)
|
|
223
|
-
.filter((markdown) => markdown !== undefined);
|
|
224
|
-
msg += "Cyclic definitons:\n";
|
|
225
|
-
msg += cyclicDefinitions.join("\n");
|
|
226
|
-
msg += "\n";
|
|
227
|
-
throw new PlainSyntaxError(`Plain syntax error: ${msg}`);
|
|
228
|
-
}
|
|
270
|
+
if (cycle !== null)
|
|
271
|
+
throw new PlainSyntaxError(formatCyclesError(adjacency, conceptDefinitions));
|
|
229
272
|
if (order.length === 0)
|
|
230
273
|
return;
|
|
231
274
|
const definitionOrder = new Map();
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Port of graph.py: the cycle search over a directed graph given as a mapping from a node to its successors.
|
|
3
|
+
*
|
|
4
|
+
* Nodes that appear only as successors are treated as having no outgoing edges. The search is Johnson's algorithm,
|
|
5
|
+
* with the strongly connected components found by Tarjan's. Both keep their own stack of frames instead of
|
|
6
|
+
* recursing, so the depth of a tangle is not limited by the call stack.
|
|
7
|
+
*
|
|
8
|
+
* - D. B. Johnson, "Finding all the elementary circuits of a directed graph", SIAM J. Comput. 4(1):77-84, 1975
|
|
9
|
+
* - R. Tarjan, "Depth-first search and linear graph algorithms", SIAM J. Comput. 1(2):146-160, 1972
|
|
10
|
+
*/
|
|
11
|
+
export type Graph<T> = Map<T, Iterable<T>>;
|
|
12
|
+
/**
|
|
13
|
+
* Tarjan: one depth-first walk, each node stamped with its visit index and the lowest index it can reach; a node
|
|
14
|
+
* whose lowest reachable index is its own closes a component.
|
|
15
|
+
*/
|
|
16
|
+
export declare function stronglyConnectedComponents<T>(graph: Graph<T>): Array<Set<T>>;
|
|
17
|
+
/**
|
|
18
|
+
* The elementary cycles of `graph`, each as its nodes in edge order, stopping after `limit` cycles when one is given.
|
|
19
|
+
* Self-loops come first; longer cycles follow in order of their earliest node in the graph's iteration order.
|
|
20
|
+
*/
|
|
21
|
+
export declare function simpleCycles<T>(graph: Graph<T>, limit?: number): Generator<T[]>;
|