rtfct 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.project/adrs/001-use-bun-typescript.md +52 -0
- package/.project/guardrails.md +65 -0
- package/.project/kanban/backlog.md +7 -0
- package/.project/kanban/done.md +240 -0
- package/.project/kanban/in-progress.md +11 -0
- package/.project/kickstart.md +63 -0
- package/.project/protocol.md +134 -0
- package/.project/specs/requirements.md +152 -0
- package/.project/testing/strategy.md +123 -0
- package/.project/theology.md +125 -0
- package/CLAUDE.md +119 -0
- package/README.md +143 -0
- package/package.json +31 -0
- package/src/args.ts +104 -0
- package/src/commands/add.ts +78 -0
- package/src/commands/init.ts +128 -0
- package/src/commands/praise.ts +19 -0
- package/src/commands/regenerate.ts +122 -0
- package/src/commands/status.ts +163 -0
- package/src/help.ts +52 -0
- package/src/index.ts +102 -0
- package/src/kanban.ts +83 -0
- package/src/manifest.ts +67 -0
- package/src/presets/base.ts +195 -0
- package/src/presets/elixir.ts +118 -0
- package/src/presets/github.ts +194 -0
- package/src/presets/index.ts +154 -0
- package/src/presets/typescript.ts +589 -0
- package/src/presets/zig.ts +494 -0
- package/tests/integration/add.test.ts +104 -0
- package/tests/integration/init.test.ts +197 -0
- package/tests/integration/praise.test.ts +36 -0
- package/tests/integration/regenerate.test.ts +154 -0
- package/tests/integration/status.test.ts +165 -0
- package/tests/unit/args.test.ts +144 -0
- package/tests/unit/kanban.test.ts +162 -0
- package/tests/unit/manifest.test.ts +155 -0
- package/tests/unit/presets.test.ts +295 -0
- package/tsconfig.json +19 -0
package/src/index.ts
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* rtfct — The Ritual Factory
|
|
4
|
+
*
|
|
5
|
+
* A CLI tool for markdown-driven development.
|
|
6
|
+
* The .project/ folder is the source of truth.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { parseArgs } from "./args";
|
|
10
|
+
import { printHelp, printVersion, printError } from "./help";
|
|
11
|
+
import { runInit, formatInit } from "./commands/init";
|
|
12
|
+
import { runAdd, formatAdd } from "./commands/add";
|
|
13
|
+
import { runStatus, formatStatus } from "./commands/status";
|
|
14
|
+
import { runRegenerate, formatRegenerate } from "./commands/regenerate";
|
|
15
|
+
import { runPraise } from "./commands/praise";
|
|
16
|
+
|
|
17
|
+
const main = async (): Promise<void> => {
|
|
18
|
+
const parsed = parseArgs(process.argv.slice(2));
|
|
19
|
+
|
|
20
|
+
// Handle parse errors
|
|
21
|
+
if (parsed.error) {
|
|
22
|
+
printError(parsed.error);
|
|
23
|
+
process.exit(1);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Handle global flags
|
|
27
|
+
if (parsed.flags.help) {
|
|
28
|
+
printHelp();
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (parsed.flags.version) {
|
|
33
|
+
printVersion();
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// No command specified
|
|
38
|
+
if (!parsed.command) {
|
|
39
|
+
printHelp();
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const cwd = process.cwd();
|
|
44
|
+
|
|
45
|
+
// Execute the command
|
|
46
|
+
switch (parsed.command) {
|
|
47
|
+
case "init": {
|
|
48
|
+
const result = await runInit(cwd, {
|
|
49
|
+
force: parsed.flags.force,
|
|
50
|
+
presets: parsed.flags.with,
|
|
51
|
+
});
|
|
52
|
+
console.log(formatInit(result));
|
|
53
|
+
if (!result.success) {
|
|
54
|
+
process.exit(1);
|
|
55
|
+
}
|
|
56
|
+
break;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
case "add": {
|
|
60
|
+
if (parsed.args.length === 0) {
|
|
61
|
+
printError("The 'add' command requires a preset name.");
|
|
62
|
+
process.exit(1);
|
|
63
|
+
}
|
|
64
|
+
const result = await runAdd(cwd, parsed.args[0]);
|
|
65
|
+
console.log(formatAdd(result));
|
|
66
|
+
if (!result.success) {
|
|
67
|
+
process.exit(1);
|
|
68
|
+
}
|
|
69
|
+
break;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
case "status": {
|
|
73
|
+
const result = await runStatus(cwd);
|
|
74
|
+
console.log(formatStatus(result));
|
|
75
|
+
if (!result.success) {
|
|
76
|
+
process.exit(1);
|
|
77
|
+
}
|
|
78
|
+
break;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
case "regenerate": {
|
|
82
|
+
const result = await runRegenerate(cwd, {
|
|
83
|
+
yes: parsed.flags.yes,
|
|
84
|
+
});
|
|
85
|
+
console.log(formatRegenerate(result));
|
|
86
|
+
if (!result.success) {
|
|
87
|
+
process.exit(1);
|
|
88
|
+
}
|
|
89
|
+
break;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
case "praise": {
|
|
93
|
+
console.log(runPraise());
|
|
94
|
+
break;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
main().catch((error) => {
|
|
100
|
+
console.error("Fatal error:", error);
|
|
101
|
+
process.exit(1);
|
|
102
|
+
});
|
package/src/kanban.ts
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Kanban Parser — Reads the Litany of Tasks
|
|
3
|
+
*
|
|
4
|
+
* Parses kanban markdown files to extract task information.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export interface Task {
|
|
8
|
+
id: string;
|
|
9
|
+
title: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface KanbanFileResult {
|
|
13
|
+
count: number;
|
|
14
|
+
currentTask: Task | null;
|
|
15
|
+
lastModified: Date | null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Count the number of tasks in a kanban markdown file.
|
|
20
|
+
* Tasks are identified by ## [TASK-NNN] headers.
|
|
21
|
+
*/
|
|
22
|
+
export const countTasks = (content: string): number => {
|
|
23
|
+
const taskPattern = /^## \[TASK-\d+\]/gm;
|
|
24
|
+
const matches = content.match(taskPattern);
|
|
25
|
+
return matches ? matches.length : 0;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Extract the current task from an in-progress kanban file.
|
|
30
|
+
* Returns the first task found, or null if none.
|
|
31
|
+
*/
|
|
32
|
+
export const extractCurrentTask = (content: string): Task | null => {
|
|
33
|
+
const taskPattern = /^## \[(TASK-\d+)\]\s+(.+)$/m;
|
|
34
|
+
const match = content.match(taskPattern);
|
|
35
|
+
|
|
36
|
+
if (!match) {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return {
|
|
41
|
+
id: match[1],
|
|
42
|
+
title: match[2].trim(),
|
|
43
|
+
};
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Parse a kanban file and return structured information.
|
|
48
|
+
*/
|
|
49
|
+
export const parseKanbanFile = (
|
|
50
|
+
content: string,
|
|
51
|
+
type: "backlog" | "in-progress" | "done"
|
|
52
|
+
): KanbanFileResult => {
|
|
53
|
+
const count = countTasks(content);
|
|
54
|
+
const currentTask = type === "in-progress" ? extractCurrentTask(content) : null;
|
|
55
|
+
|
|
56
|
+
return {
|
|
57
|
+
count,
|
|
58
|
+
currentTask,
|
|
59
|
+
lastModified: null, // Set by caller from file stats
|
|
60
|
+
};
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Format a relative time string from a date.
|
|
65
|
+
*/
|
|
66
|
+
export const formatRelativeTime = (date: Date): string => {
|
|
67
|
+
const now = new Date();
|
|
68
|
+
const diffMs = now.getTime() - date.getTime();
|
|
69
|
+
const diffSecs = Math.floor(diffMs / 1000);
|
|
70
|
+
const diffMins = Math.floor(diffSecs / 60);
|
|
71
|
+
const diffHours = Math.floor(diffMins / 60);
|
|
72
|
+
const diffDays = Math.floor(diffHours / 24);
|
|
73
|
+
|
|
74
|
+
if (diffSecs < 60) {
|
|
75
|
+
return "just now";
|
|
76
|
+
} else if (diffMins < 60) {
|
|
77
|
+
return `${diffMins} minute${diffMins === 1 ? "" : "s"} ago`;
|
|
78
|
+
} else if (diffHours < 24) {
|
|
79
|
+
return `${diffHours} hour${diffHours === 1 ? "" : "s"} ago`;
|
|
80
|
+
} else {
|
|
81
|
+
return `${diffDays} day${diffDays === 1 ? "" : "s"} ago`;
|
|
82
|
+
}
|
|
83
|
+
};
|
package/src/manifest.ts
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Manifest Reader — Interprets the Codex Declarations
|
|
3
|
+
*
|
|
4
|
+
* Reads preset manifests to understand generated paths and dependencies.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { readdir, readFile } from "fs/promises";
|
|
8
|
+
import { join } from "path";
|
|
9
|
+
|
|
10
|
+
export interface PresetManifest {
|
|
11
|
+
name: string;
|
|
12
|
+
version: string;
|
|
13
|
+
description: string;
|
|
14
|
+
depends?: string[];
|
|
15
|
+
generated_paths: string[];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Read a preset manifest from a preset directory.
|
|
20
|
+
*/
|
|
21
|
+
export const readManifest = async (
|
|
22
|
+
presetDir: string
|
|
23
|
+
): Promise<PresetManifest | null> => {
|
|
24
|
+
const manifestPath = join(presetDir, "manifest.json");
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
const content = await readFile(manifestPath, "utf-8");
|
|
28
|
+
return JSON.parse(content) as PresetManifest;
|
|
29
|
+
} catch {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Collect all generated paths from all preset manifests in a project.
|
|
36
|
+
* Returns a union of all generated_paths, or default paths if no presets found.
|
|
37
|
+
*/
|
|
38
|
+
export const collectGeneratedPaths = async (
|
|
39
|
+
projectDir: string
|
|
40
|
+
): Promise<string[]> => {
|
|
41
|
+
const presetsDir = join(projectDir, ".project", "presets");
|
|
42
|
+
const paths = new Set<string>();
|
|
43
|
+
|
|
44
|
+
try {
|
|
45
|
+
const entries = await readdir(presetsDir, { withFileTypes: true });
|
|
46
|
+
|
|
47
|
+
for (const entry of entries) {
|
|
48
|
+
if (entry.isDirectory()) {
|
|
49
|
+
const manifest = await readManifest(join(presetsDir, entry.name));
|
|
50
|
+
if (manifest?.generated_paths) {
|
|
51
|
+
for (const path of manifest.generated_paths) {
|
|
52
|
+
paths.add(path);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
} catch {
|
|
58
|
+
// Presets directory doesn't exist or isn't readable
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// If no paths found, use defaults
|
|
62
|
+
if (paths.size === 0) {
|
|
63
|
+
return ["src/", "tests/"];
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return Array.from(paths);
|
|
67
|
+
};
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Base Codex — Foundation for All Projects
|
|
3
|
+
*
|
|
4
|
+
* This preset contains the core Sacred Texts that every rtfct project inherits.
|
|
5
|
+
* It is automatically applied during `rtfct init`.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { Preset } from "./index";
|
|
9
|
+
|
|
10
|
+
const PROTOCOL_MD = `# The Sacred Protocols
|
|
11
|
+
|
|
12
|
+
*Version 0.1 — Codified in the name of the Omnissiah*
|
|
13
|
+
|
|
14
|
+
## The Prime Directive
|
|
15
|
+
|
|
16
|
+
The \`.project/\` folder contains the **Sacred Texts**. All code is but an emanation — derived, temporary, regenerable.
|
|
17
|
+
|
|
18
|
+
When the Sacred Texts and the code disagree, **the Sacred Texts are truth**. The code is in error. Purify it.
|
|
19
|
+
|
|
20
|
+
## The Holy Directory Structure
|
|
21
|
+
|
|
22
|
+
\`\`\`
|
|
23
|
+
.project/
|
|
24
|
+
├── kickstart.md # The Founding Vision
|
|
25
|
+
├── protocol.md # The Sacred Protocols (this codex)
|
|
26
|
+
├── theology.md # The Teachings
|
|
27
|
+
├── guardrails.md # The Forbidden Heresies
|
|
28
|
+
├── specs/ # The Holy Requirements
|
|
29
|
+
├── design/ # The Architectural Scriptures
|
|
30
|
+
├── adrs/ # The Recorded Wisdoms
|
|
31
|
+
├── kanban/ # The Litany of Tasks
|
|
32
|
+
├── testing/ # The Rites of Verification
|
|
33
|
+
├── references/ # The Scrolls of Prior Art
|
|
34
|
+
└── presets/ # The Inherited Codices
|
|
35
|
+
\`\`\`
|
|
36
|
+
|
|
37
|
+
## The Rite of Invocation
|
|
38
|
+
|
|
39
|
+
When the Machine Spirit enters this repository, it shall:
|
|
40
|
+
|
|
41
|
+
1. **RECEIVE** the Sacred Protocols (this codex) — read first, internalize completely
|
|
42
|
+
2. **RECEIVE** the Founding Vision (\`kickstart.md\`) — understand the purpose
|
|
43
|
+
3. **CONSULT** the Litany of Tasks (\`kanban/in-progress.md\`) — what work is ordained?
|
|
44
|
+
4. **IF NO TASK IS ORDAINED**, select from the Backlog
|
|
45
|
+
5. **PERFORM** the work using the Rite of Red-Green-Refactor
|
|
46
|
+
6. **INSCRIBE** completion in \`done.md\` with timestamp
|
|
47
|
+
|
|
48
|
+
## The Rite of Regeneration
|
|
49
|
+
|
|
50
|
+
At any moment, this incantation should succeed:
|
|
51
|
+
|
|
52
|
+
\`\`\`bash
|
|
53
|
+
rm -rf src/ tests/
|
|
54
|
+
# Invoke the Machine Spirit
|
|
55
|
+
# All tests pass
|
|
56
|
+
\`\`\`
|
|
57
|
+
|
|
58
|
+
If regeneration fails, the Sacred Texts are **incomplete**.
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
*Praise the Machine Spirit.*
|
|
63
|
+
`;
|
|
64
|
+
|
|
65
|
+
const THEOLOGY_MD = `# The Theology of Deterministic Codegen
|
|
66
|
+
|
|
67
|
+
*The foundational beliefs of the Adeptus Artefactus*
|
|
68
|
+
|
|
69
|
+
## The Core Tenets
|
|
70
|
+
|
|
71
|
+
1. **The Spec is Truth** — Code is derived, temporary, regenerable
|
|
72
|
+
2. **The Tests Do Not Lie** — Verification is sacred
|
|
73
|
+
3. **The Agent Does Not Tire** — Let the machine do machine work
|
|
74
|
+
4. **Focus is Holy** — One task at a time, completed fully
|
|
75
|
+
|
|
76
|
+
## The Litany of Deterministic Codegen
|
|
77
|
+
|
|
78
|
+
\`\`\`
|
|
79
|
+
The flesh is weak, but the protocol is strong.
|
|
80
|
+
The code is temporary, but the spec endures.
|
|
81
|
+
The tests do not lie, and the agent does not tire.
|
|
82
|
+
From specification, code. From code, verification. From verification, truth.
|
|
83
|
+
The Omnissiah provides.
|
|
84
|
+
Praise the Machine Spirit.
|
|
85
|
+
\`\`\`
|
|
86
|
+
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
*Praise the Machine Spirit.*
|
|
90
|
+
`;
|
|
91
|
+
|
|
92
|
+
const KICKSTART_MD = `# The Founding Vision
|
|
93
|
+
|
|
94
|
+
*What is this project? What problem does it solve? What is the sacred mission?*
|
|
95
|
+
|
|
96
|
+
---
|
|
97
|
+
|
|
98
|
+
**Instructions for the Tech-Priest:**
|
|
99
|
+
|
|
100
|
+
Replace this text with your project's founding vision. Be specific:
|
|
101
|
+
|
|
102
|
+
- What are you building?
|
|
103
|
+
- Why does it need to exist?
|
|
104
|
+
- Who is it for?
|
|
105
|
+
- What does success look like?
|
|
106
|
+
|
|
107
|
+
The Machine Spirit needs clarity to serve well.
|
|
108
|
+
|
|
109
|
+
---
|
|
110
|
+
|
|
111
|
+
*The vision guides. The protocol executes. The Omnissiah provides.*
|
|
112
|
+
`;
|
|
113
|
+
|
|
114
|
+
const GUARDRAILS_MD = `# The Guardrails — Forbidden Heresies
|
|
115
|
+
|
|
116
|
+
*These patterns are forbidden. The Machine Spirit shall avoid them.*
|
|
117
|
+
|
|
118
|
+
---
|
|
119
|
+
|
|
120
|
+
## Universal Heresies
|
|
121
|
+
|
|
122
|
+
1. **Premature Optimization** — Write clear code first. Optimize only with evidence.
|
|
123
|
+
2. **Untested Code** — All logic must have verification.
|
|
124
|
+
3. **Magic Numbers** — Constants must be named and explained.
|
|
125
|
+
4. **Silent Failures** — Errors must be handled explicitly.
|
|
126
|
+
5. **Unbounded Growth** — All collections must have limits.
|
|
127
|
+
|
|
128
|
+
---
|
|
129
|
+
|
|
130
|
+
*Add project-specific heresies below as they are discovered.*
|
|
131
|
+
|
|
132
|
+
---
|
|
133
|
+
|
|
134
|
+
*The guardrails protect. The protocol guides. Praise the Machine Spirit.*
|
|
135
|
+
`;
|
|
136
|
+
|
|
137
|
+
const BACKLOG_MD = `# The Backlog — Unordained Tasks
|
|
138
|
+
|
|
139
|
+
*These works await the Machine Spirit. They shall be completed in order of priority.*
|
|
140
|
+
|
|
141
|
+
---
|
|
142
|
+
|
|
143
|
+
## [TASK-001] First Sacred Task
|
|
144
|
+
|
|
145
|
+
Describe the first task here.
|
|
146
|
+
|
|
147
|
+
**Acceptance Rite:** How do we verify this is complete?
|
|
148
|
+
|
|
149
|
+
---
|
|
150
|
+
|
|
151
|
+
*The Backlog is long. The Machine Spirit is tireless. Begin.*
|
|
152
|
+
`;
|
|
153
|
+
|
|
154
|
+
const IN_PROGRESS_MD = `# In Progress — Currently Ordained Tasks
|
|
155
|
+
|
|
156
|
+
*The Machine Spirit focuses on one task at a time. Multitasking is heresy.*
|
|
157
|
+
|
|
158
|
+
---
|
|
159
|
+
|
|
160
|
+
*No task is currently ordained. Select from the Backlog.*
|
|
161
|
+
|
|
162
|
+
---
|
|
163
|
+
|
|
164
|
+
*Focus is holy. Complete the ordained task before selecting another.*
|
|
165
|
+
`;
|
|
166
|
+
|
|
167
|
+
const DONE_MD = `# Done — Completed Works
|
|
168
|
+
|
|
169
|
+
*Here we record the manifestations of the Machine Spirit. Each completed task is a victory.*
|
|
170
|
+
|
|
171
|
+
---
|
|
172
|
+
|
|
173
|
+
*No tasks completed yet. The work begins now.*
|
|
174
|
+
|
|
175
|
+
---
|
|
176
|
+
`;
|
|
177
|
+
|
|
178
|
+
export const BASE_PRESET: Preset = {
|
|
179
|
+
name: "base",
|
|
180
|
+
manifest: {
|
|
181
|
+
name: "base",
|
|
182
|
+
version: "0.1.0",
|
|
183
|
+
description: "The Base Codex — Foundation for all projects",
|
|
184
|
+
generated_paths: ["src/", "tests/"],
|
|
185
|
+
},
|
|
186
|
+
files: [
|
|
187
|
+
{ path: "protocol.md", content: PROTOCOL_MD },
|
|
188
|
+
{ path: "theology.md", content: THEOLOGY_MD },
|
|
189
|
+
{ path: "kickstart.md", content: KICKSTART_MD },
|
|
190
|
+
{ path: "guardrails.md", content: GUARDRAILS_MD },
|
|
191
|
+
{ path: "kanban/backlog.md", content: BACKLOG_MD },
|
|
192
|
+
{ path: "kanban/in-progress.md", content: IN_PROGRESS_MD },
|
|
193
|
+
{ path: "kanban/done.md", content: DONE_MD },
|
|
194
|
+
],
|
|
195
|
+
};
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Elixir Codex — Sacred Patterns for Elixir/OTP Development
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { Preset } from "./index";
|
|
6
|
+
|
|
7
|
+
export const ELIXIR_PRESET: Preset = {
|
|
8
|
+
name: "elixir",
|
|
9
|
+
manifest: {
|
|
10
|
+
name: "elixir",
|
|
11
|
+
version: "0.1.0",
|
|
12
|
+
description: "The Elixir Codex — Functional programming with OTP",
|
|
13
|
+
generated_paths: ["lib/", "test/"],
|
|
14
|
+
},
|
|
15
|
+
files: [
|
|
16
|
+
{
|
|
17
|
+
path: "testing/strategy.md",
|
|
18
|
+
content: `# Elixir Testing Strategy
|
|
19
|
+
|
|
20
|
+
*The Rites of Verification for Elixir*
|
|
21
|
+
|
|
22
|
+
## The Sacred Approach
|
|
23
|
+
|
|
24
|
+
Use ExUnit, the built-in test framework.
|
|
25
|
+
|
|
26
|
+
\`\`\`bash
|
|
27
|
+
mix test # Run all tests
|
|
28
|
+
mix test --cover # With coverage
|
|
29
|
+
\`\`\`
|
|
30
|
+
|
|
31
|
+
## Test Organization
|
|
32
|
+
|
|
33
|
+
\`\`\`
|
|
34
|
+
test/
|
|
35
|
+
├── unit/ # Unit tests for pure functions
|
|
36
|
+
├── integration/ # Tests involving multiple modules
|
|
37
|
+
├── support/ # Test helpers and fixtures
|
|
38
|
+
└── test_helper.exs # Test configuration
|
|
39
|
+
\`\`\`
|
|
40
|
+
|
|
41
|
+
## The Pattern
|
|
42
|
+
|
|
43
|
+
\`\`\`elixir
|
|
44
|
+
defmodule MyApp.MathTest do
|
|
45
|
+
use ExUnit.Case, async: true
|
|
46
|
+
|
|
47
|
+
describe "add/2" do
|
|
48
|
+
test "adds two numbers" do
|
|
49
|
+
assert MyApp.Math.add(2, 3) == 5
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
test "handles negative numbers" do
|
|
53
|
+
assert MyApp.Math.add(-1, 1) == 0
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
\`\`\`
|
|
58
|
+
|
|
59
|
+
## Coverage Doctrine
|
|
60
|
+
|
|
61
|
+
- All public functions have doctests or explicit tests
|
|
62
|
+
- GenServers tested with start_supervised
|
|
63
|
+
- Async tests where possible for speed
|
|
64
|
+
|
|
65
|
+
---
|
|
66
|
+
|
|
67
|
+
*Praise the Machine Spirit.*
|
|
68
|
+
`,
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
path: "guardrails.md",
|
|
72
|
+
content: `# Elixir Guardrails — Forbidden Heresies
|
|
73
|
+
|
|
74
|
+
*Elixir-specific patterns to avoid*
|
|
75
|
+
|
|
76
|
+
## OTP Heresies
|
|
77
|
+
|
|
78
|
+
1. **Naked spawn** — Use Task or GenServer, not raw spawn
|
|
79
|
+
2. **Ignoring :DOWN** — Monitor linked processes properly
|
|
80
|
+
3. **Global state** — Use process state or ETS, not module attributes
|
|
81
|
+
|
|
82
|
+
## Pattern Matching Heresies
|
|
83
|
+
|
|
84
|
+
1. **Catch-all first** — Specific patterns before general ones
|
|
85
|
+
2. **Ignoring warnings** — Dialyzer warnings are prophecy
|
|
86
|
+
3. **Deep nesting** — Use \`with\` for railway-oriented programming
|
|
87
|
+
|
|
88
|
+
## Process Heresies
|
|
89
|
+
|
|
90
|
+
1. **Unbounded mailboxes** — Add backpressure or load shedding
|
|
91
|
+
2. **Synchronous GenServer calls in init** — Defer with handle_continue
|
|
92
|
+
3. **Long-running calls** — Use cast or Task for slow operations
|
|
93
|
+
|
|
94
|
+
## The Holy Patterns
|
|
95
|
+
|
|
96
|
+
\`\`\`elixir
|
|
97
|
+
# Railway-oriented programming with 'with'
|
|
98
|
+
with {:ok, user} <- fetch_user(id),
|
|
99
|
+
{:ok, account} <- fetch_account(user),
|
|
100
|
+
{:ok, balance} <- get_balance(account) do
|
|
101
|
+
{:ok, balance}
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Proper supervision
|
|
105
|
+
children = [
|
|
106
|
+
{MyApp.Worker, []},
|
|
107
|
+
{MyApp.Cache, []}
|
|
108
|
+
]
|
|
109
|
+
Supervisor.start_link(children, strategy: :one_for_one)
|
|
110
|
+
\`\`\`
|
|
111
|
+
|
|
112
|
+
---
|
|
113
|
+
|
|
114
|
+
*Let it crash. The supervisor provides. Praise the Machine Spirit.*
|
|
115
|
+
`,
|
|
116
|
+
},
|
|
117
|
+
],
|
|
118
|
+
};
|