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.
Files changed (39) hide show
  1. package/.project/adrs/001-use-bun-typescript.md +52 -0
  2. package/.project/guardrails.md +65 -0
  3. package/.project/kanban/backlog.md +7 -0
  4. package/.project/kanban/done.md +240 -0
  5. package/.project/kanban/in-progress.md +11 -0
  6. package/.project/kickstart.md +63 -0
  7. package/.project/protocol.md +134 -0
  8. package/.project/specs/requirements.md +152 -0
  9. package/.project/testing/strategy.md +123 -0
  10. package/.project/theology.md +125 -0
  11. package/CLAUDE.md +119 -0
  12. package/README.md +143 -0
  13. package/package.json +31 -0
  14. package/src/args.ts +104 -0
  15. package/src/commands/add.ts +78 -0
  16. package/src/commands/init.ts +128 -0
  17. package/src/commands/praise.ts +19 -0
  18. package/src/commands/regenerate.ts +122 -0
  19. package/src/commands/status.ts +163 -0
  20. package/src/help.ts +52 -0
  21. package/src/index.ts +102 -0
  22. package/src/kanban.ts +83 -0
  23. package/src/manifest.ts +67 -0
  24. package/src/presets/base.ts +195 -0
  25. package/src/presets/elixir.ts +118 -0
  26. package/src/presets/github.ts +194 -0
  27. package/src/presets/index.ts +154 -0
  28. package/src/presets/typescript.ts +589 -0
  29. package/src/presets/zig.ts +494 -0
  30. package/tests/integration/add.test.ts +104 -0
  31. package/tests/integration/init.test.ts +197 -0
  32. package/tests/integration/praise.test.ts +36 -0
  33. package/tests/integration/regenerate.test.ts +154 -0
  34. package/tests/integration/status.test.ts +165 -0
  35. package/tests/unit/args.test.ts +144 -0
  36. package/tests/unit/kanban.test.ts +162 -0
  37. package/tests/unit/manifest.test.ts +155 -0
  38. package/tests/unit/presets.test.ts +295 -0
  39. 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
+ };
@@ -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
+ };