ndomo 0.1.0 → 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.
Files changed (65) hide show
  1. package/.env.example +4 -0
  2. package/README.es.md +29 -23
  3. package/README.md +64 -24
  4. package/bun.lock +447 -0
  5. package/docs/configuration.md +4 -4
  6. package/docs/installation.md +53 -34
  7. package/docs/installer.md +164 -0
  8. package/docs/integrations.md +1 -1
  9. package/docs/web-ui.md +124 -0
  10. package/package.json +43 -4
  11. package/scripts/install.sh +28 -0
  12. package/scripts/smoke-install.sh +47 -0
  13. package/scripts/smoke-web.sh +335 -0
  14. package/src/cli/__tests__/install.test.ts +733 -0
  15. package/src/cli/index.ts +8 -0
  16. package/src/cli/install.ts +1292 -0
  17. package/src/config/__tests__/schema.test.ts +223 -0
  18. package/src/config/schema.ts +129 -16
  19. package/src/http/__tests__/auth.test.ts +10 -10
  20. package/src/http/__tests__/spa.test.ts +296 -0
  21. package/src/http/auth.ts +8 -1
  22. package/src/http/server.ts +71 -2
  23. package/.bun-version +0 -1
  24. package/.dockerignore +0 -79
  25. package/.editorconfig +0 -18
  26. package/.github/CODEOWNERS +0 -8
  27. package/.github/ISSUE_TEMPLATE/bug_report.yml +0 -62
  28. package/.github/ISSUE_TEMPLATE/config.yml +0 -2
  29. package/.github/ISSUE_TEMPLATE/feature_request.yml +0 -34
  30. package/.github/dependabot.yml +0 -36
  31. package/.github/pull_request_template.md +0 -24
  32. package/.github/release.yml +0 -30
  33. package/.github/workflows/gitleaks.yml +0 -28
  34. package/.github/workflows/release-please.yml +0 -27
  35. package/.github/workflows/smoke.yml +0 -29
  36. package/.husky/commit-msg +0 -1
  37. package/CHANGELOG.md +0 -114
  38. package/Dockerfile +0 -32
  39. package/bin/ndomo-analyses.ts +0 -4
  40. package/bin/ndomo-status.ts +0 -4
  41. package/biome.json +0 -57
  42. package/commitlint.config.js +0 -3
  43. package/opencode.json +0 -5
  44. package/release-please-config.json +0 -11
  45. package/scripts/dev-bust-cache.sh +0 -164
  46. package/scripts/smoke-e2e.ts +0 -704
  47. package/scripts/smoke-hot.ts +0 -417
  48. package/scripts/smoke-v4.ts +0 -256
  49. package/scripts/smoke-v5.ts +0 -397
  50. package/scripts/uninstall.sh +0 -224
  51. package/src/index.ts +0 -37
  52. package/src/lib.ts +0 -65
  53. package/src/mem/scoped.ts +0 -65
  54. package/src/orchestrator/background.test.ts +0 -268
  55. package/src/orchestrator/background.ts +0 -293
  56. package/src/orchestrator/memory-hook.ts +0 -182
  57. package/src/orchestrator/reconciler.ts +0 -123
  58. package/src/orchestrator/scheduler.test.ts +0 -300
  59. package/src/orchestrator/scheduler.ts +0 -243
  60. package/src/plugin.test.ts +0 -2574
  61. package/src/plugin.ts +0 -1690
  62. package/src/worktrees/manager.ts +0 -236
  63. package/src/worktrees/state.ts +0 -87
  64. package/tests/integration/ranger-flow.test.ts +0 -257
  65. package/tsconfig.json +0 -31
@@ -1,236 +0,0 @@
1
- /**
2
- * Git worktree manager for isolated coding lanes.
3
- *
4
- * Provides CRUD operations for git worktrees with persistent state tracking.
5
- * All git operations use async exec to avoid blocking the event loop.
6
- *
7
- * @module worktrees/manager
8
- */
9
-
10
- import { exec as execCb } from "node:child_process";
11
- import { existsSync } from "node:fs";
12
- import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
13
- import { join } from "node:path";
14
- import { promisify } from "node:util";
15
-
16
- const exec = promisify(execCb);
17
-
18
- // ─── Types ───────────────────────────────────────────────────────────────────
19
-
20
- export interface Worktree {
21
- slug: string;
22
- branch: string;
23
- path: string;
24
- createdAt: number;
25
- status: "active" | "merged" | "abandoned";
26
- agent?: string | undefined;
27
- description?: string | undefined;
28
- }
29
-
30
- export interface WorktreeState {
31
- worktrees: Worktree[];
32
- updatedAt: number;
33
- }
34
-
35
- // ─── Constants ───────────────────────────────────────────────────────────────
36
-
37
- const STATE_FILE = ".slim/worktrees.json";
38
- const WORKTREE_DIR = ".slim/worktrees";
39
-
40
- /** Regex for valid slug/branch names — alphanumeric, hyphens, underscores, slashes */
41
- const SAFE_NAME_RE = /^[a-zA-Z0-9_\-/]+$/;
42
-
43
- // ─── Validation ──────────────────────────────────────────────────────────────
44
-
45
- /**
46
- * Validate that a string is safe for use in git commands.
47
- * Prevents shell injection via slug or branch parameters.
48
- */
49
- function assertSafeName(value: string, label: string): void {
50
- if (!value || typeof value !== "string") {
51
- throw new Error(`${label} must be a non-empty string`);
52
- }
53
- if (!SAFE_NAME_RE.test(value)) {
54
- throw new Error(
55
- `${label} contains invalid characters: "${value}". Only alphanumeric, hyphens, underscores, and slashes allowed.`,
56
- );
57
- }
58
- if (value.startsWith("-")) {
59
- throw new Error(`${label} must not start with a hyphen`);
60
- }
61
- }
62
-
63
- // ─── State Persistence ───────────────────────────────────────────────────────
64
-
65
- /**
66
- * Load worktree state from disk.
67
- * Returns empty state if file doesn't exist or is malformed.
68
- */
69
- export async function loadState(rootDir: string): Promise<WorktreeState> {
70
- const statePath = join(rootDir, STATE_FILE);
71
- try {
72
- const raw = await readFile(statePath, "utf-8");
73
- const parsed = JSON.parse(raw) as WorktreeState;
74
- // Defensive: ensure worktrees array exists
75
- if (!Array.isArray(parsed.worktrees)) {
76
- return { worktrees: [], updatedAt: Date.now() };
77
- }
78
- return parsed;
79
- } catch {
80
- return { worktrees: [], updatedAt: Date.now() };
81
- }
82
- }
83
-
84
- /**
85
- * Save worktree state to disk.
86
- * Creates .slim/ directory if needed.
87
- */
88
- export async function saveState(rootDir: string, state: WorktreeState): Promise<void> {
89
- const slimDir = join(rootDir, ".slim");
90
- if (!existsSync(slimDir)) {
91
- await mkdir(slimDir, { recursive: true });
92
- }
93
- state.updatedAt = Date.now();
94
- await writeFile(join(rootDir, STATE_FILE), JSON.stringify(state, null, 2));
95
- }
96
-
97
- // ─── Worktree Operations ─────────────────────────────────────────────────────
98
-
99
- /**
100
- * Create a new git worktree.
101
- *
102
- * @param rootDir - repository root directory
103
- * @param slug - short identifier (e.g., "auth-refactor", "fix-login")
104
- * @param branch - git branch name (created from current HEAD if doesn't exist)
105
- * @param agent - which agent will work in this worktree (optional)
106
- * @param description - human-readable description (optional)
107
- * @returns path to the new worktree
108
- * @throws if slug contains unsafe characters or worktree already exists
109
- */
110
- export async function createWorktree(
111
- rootDir: string,
112
- slug: string,
113
- branch: string,
114
- agent?: string,
115
- description?: string,
116
- ): Promise<string> {
117
- assertSafeName(slug, "slug");
118
- assertSafeName(branch, "branch");
119
-
120
- const worktreePath = join(rootDir, WORKTREE_DIR, slug);
121
-
122
- // Check if already exists
123
- const state = await loadState(rootDir);
124
- const existing = state.worktrees.find((w) => w.slug === slug && w.status === "active");
125
- if (existing) {
126
- throw new Error(`Worktree '${slug}' already exists at ${existing.path}`);
127
- }
128
-
129
- // Create parent directory
130
- await mkdir(join(rootDir, WORKTREE_DIR), { recursive: true });
131
-
132
- // Git worktree add — try creating new branch first, fall back to existing branch
133
- try {
134
- await exec(`git worktree add -b ${branch} ${worktreePath}`, { cwd: rootDir });
135
- } catch {
136
- // Branch might already exist
137
- await exec(`git worktree add ${worktreePath} ${branch}`, { cwd: rootDir });
138
- }
139
-
140
- // Update state
141
- state.worktrees.push({
142
- slug,
143
- branch,
144
- path: worktreePath,
145
- createdAt: Date.now(),
146
- status: "active",
147
- agent,
148
- description,
149
- });
150
- await saveState(rootDir, state);
151
-
152
- return worktreePath;
153
- }
154
-
155
- /**
156
- * Remove a worktree.
157
- *
158
- * @param rootDir - repository root directory
159
- * @param slug - worktree identifier
160
- * @param abandon - if true, marks as abandoned instead of merged
161
- * @throws if no active worktree found with the given slug
162
- */
163
- export async function removeWorktree(
164
- rootDir: string,
165
- slug: string,
166
- abandon = false,
167
- ): Promise<void> {
168
- assertSafeName(slug, "slug");
169
-
170
- const state = await loadState(rootDir);
171
- const wt = state.worktrees.find((w) => w.slug === slug && w.status === "active");
172
- if (!wt) {
173
- throw new Error(`No active worktree found with slug '${slug}'`);
174
- }
175
-
176
- // Remove git worktree, force if regular remove fails
177
- try {
178
- await exec(`git worktree remove ${wt.path}`, { cwd: rootDir });
179
- } catch {
180
- await exec(`git worktree remove --force ${wt.path}`, { cwd: rootDir });
181
- }
182
-
183
- wt.status = abandon ? "abandoned" : "merged";
184
- await saveState(rootDir, state);
185
- }
186
-
187
- /**
188
- * List all active worktrees.
189
- */
190
- export async function listActive(rootDir: string): Promise<Worktree[]> {
191
- const state = await loadState(rootDir);
192
- return state.worktrees.filter((w) => w.status === "active");
193
- }
194
-
195
- /**
196
- * Get a specific worktree by slug.
197
- * Returns undefined if not found.
198
- */
199
- export async function getWorktree(rootDir: string, slug: string): Promise<Worktree | undefined> {
200
- assertSafeName(slug, "slug");
201
-
202
- const state = await loadState(rootDir);
203
- return state.worktrees.find((w) => w.slug === slug);
204
- }
205
-
206
- /**
207
- * Cleanup all abandoned/merged worktrees older than maxAge (ms).
208
- *
209
- * @param rootDir - repository root directory
210
- * @param maxAge - max age in milliseconds (default: 7 days)
211
- * @returns list of removed worktree slugs
212
- */
213
- export async function cleanup(
214
- rootDir: string,
215
- maxAge: number = 7 * 24 * 60 * 60 * 1000,
216
- ): Promise<string[]> {
217
- const state = await loadState(rootDir);
218
- const now = Date.now();
219
- const removed: string[] = [];
220
-
221
- for (const wt of state.worktrees) {
222
- if (wt.status !== "active" && now - wt.createdAt > maxAge) {
223
- // Remove directory if it still exists
224
- if (existsSync(wt.path)) {
225
- await rm(wt.path, { recursive: true, force: true });
226
- }
227
- removed.push(wt.slug);
228
- }
229
- }
230
-
231
- // Filter out removed entries
232
- state.worktrees = state.worktrees.filter((w) => !removed.includes(w.slug));
233
- await saveState(rootDir, state);
234
-
235
- return removed;
236
- }
@@ -1,87 +0,0 @@
1
- /**
2
- * Worktree integrity verification.
3
- *
4
- * Checks that active worktrees have valid directories, branches,
5
- * and are properly registered with git.
6
- *
7
- * @module worktrees/state
8
- */
9
-
10
- import { exec as execCb } from "node:child_process";
11
- import { existsSync } from "node:fs";
12
- import { promisify } from "node:util";
13
- import type { Worktree, WorktreeState } from "./manager.ts";
14
- import { loadState } from "./manager.ts";
15
-
16
- const exec = promisify(execCb);
17
-
18
- export type { Worktree, WorktreeState };
19
-
20
- // ─── Types ───────────────────────────────────────────────────────────────────
21
-
22
- export interface IntegrityReport {
23
- slug: string;
24
- pathExists: boolean;
25
- branchValid: boolean;
26
- gitWorktreeValid: boolean;
27
- issues: string[];
28
- }
29
-
30
- // ─── Integrity Checks ────────────────────────────────────────────────────────
31
-
32
- /**
33
- * Verify integrity of all active worktrees.
34
- *
35
- * Checks per worktree:
36
- * 1. Directory exists on disk
37
- * 2. Branch exists in git
38
- * 3. Worktree is registered in `git worktree list`
39
- *
40
- * @param rootDir - repository root directory
41
- * @returns array of integrity reports (only for active worktrees)
42
- */
43
- export async function verifyIntegrity(rootDir: string): Promise<IntegrityReport[]> {
44
- const state = await loadState(rootDir);
45
- const reports: IntegrityReport[] = [];
46
-
47
- // Get git worktree list once (not per-worktree)
48
- let gitWorktreeOutput = "";
49
- try {
50
- const { stdout } = await exec("git worktree list", { cwd: rootDir });
51
- gitWorktreeOutput = stdout;
52
- } catch {
53
- // If this fails, all worktrees will report the issue
54
- }
55
-
56
- for (const wt of state.worktrees) {
57
- if (wt.status !== "active") continue;
58
-
59
- const issues: string[] = [];
60
- const pathExists = existsSync(wt.path);
61
-
62
- // Check if worktree path appears in git worktree list
63
- const gitWorktreeValid = gitWorktreeOutput.includes(wt.path);
64
-
65
- // Check branch exists
66
- let branchValid = false;
67
- try {
68
- await exec(`git rev-parse --verify ${wt.branch}`, { cwd: rootDir });
69
- branchValid = true;
70
- } catch {
71
- issues.push(`branch '${wt.branch}' not found`);
72
- }
73
-
74
- if (!pathExists) issues.push(`directory '${wt.path}' missing`);
75
- if (!gitWorktreeValid) issues.push("worktree not registered with git");
76
-
77
- reports.push({
78
- slug: wt.slug,
79
- pathExists,
80
- branchValid,
81
- gitWorktreeValid,
82
- issues,
83
- });
84
- }
85
-
86
- return reports;
87
- }
@@ -1,257 +0,0 @@
1
- /**
2
- * Integration test — analyses (ranger) full flow end-to-end.
3
- *
4
- * Exercises create/get/list/search/update/archive/link/unlink
5
- * using direct DB + CRUD calls (no tool runtime, no mocks).
6
- * Each test gets a fresh in-memory DB with full schema via runMigrations.
7
- */
8
-
9
- import { Database } from "bun:sqlite";
10
- import { beforeEach, describe, expect, test } from "bun:test";
11
- import {
12
- archiveAnalysis,
13
- createAnalysis,
14
- getAnalysis,
15
- getAnalysisBySlug,
16
- linkAnalysisToPlan,
17
- listAnalyses,
18
- searchAnalyses,
19
- unlinkAnalysisFromPlan,
20
- updateAnalysis,
21
- } from "../../src/db/analyses.ts";
22
- import { createPlan } from "../../src/db/plans.ts";
23
- import { runMigrations } from "../../src/db/migrations.ts";
24
-
25
- let db: Database;
26
-
27
- /** Minimal plan stub for FK-dependent tests. */
28
- function makePlan(id = "plan-1", slug = "test-plan") {
29
- return createPlan(db, {
30
- id,
31
- slug,
32
- title: "Test Plan",
33
- status: "draft",
34
- priority: 1,
35
- overview: "test overview",
36
- complexity: 3,
37
- createdBy: "test",
38
- updatedBy: "test",
39
- sessionId: null,
40
- approvedAt: null,
41
- completedAt: null,
42
- approach: null,
43
- sourceSessionId: null,
44
- sourceMessageId: null,
45
- category: null,
46
- metadata: {},
47
- archivedAt: null,
48
- });
49
- }
50
-
51
- beforeEach(() => {
52
- db = new Database(":memory:");
53
- db.exec("PRAGMA foreign_keys = ON");
54
- runMigrations(db);
55
- });
56
-
57
- describe("analyses integration — ranger flow", () => {
58
- test("create + get roundtrips all fields", () => {
59
- const created = createAnalysis(db, {
60
- slug: "auth-review",
61
- title: "Auth Module Review",
62
- projectPath: "/home/user/project",
63
- summary: "Reviewed auth module for vulnerabilities",
64
- findingsJson: JSON.stringify([{ severity: "high", desc: "weak hash" }]),
65
- agent: "ranger",
66
- });
67
-
68
- expect(created.id).toBeTruthy();
69
- expect(created.slug).toBe("auth-review");
70
- expect(created.title).toBe("Auth Module Review");
71
- expect(created.projectPath).toBe("/home/user/project");
72
- expect(created.summary).toBe("Reviewed auth module for vulnerabilities");
73
- expect(created.agent).toBe("ranger");
74
- expect(created.archivedAt).toBeNull();
75
-
76
- const findings = JSON.parse(created.findingsJson);
77
- expect(findings).toHaveLength(1);
78
- expect(findings[0].severity).toBe("high");
79
-
80
- // get by id returns same data
81
- const fetched = getAnalysis(db, created.id);
82
- expect(fetched).not.toBeNull();
83
- expect(fetched!.id).toBe(created.id);
84
- expect(fetched!.slug).toBe(created.slug);
85
- expect(fetched!.title).toBe(created.title);
86
- });
87
-
88
- test("agent defaults to 'ranger' when omitted", () => {
89
- const a = createAnalysis(db, {
90
- slug: "default-agent",
91
- title: "Default Agent Test",
92
- projectPath: "/p",
93
- });
94
- expect(a.agent).toBe("ranger");
95
- });
96
-
97
- test("slug uniqueness — same slug + projectPath throws", () => {
98
- createAnalysis(db, {
99
- slug: "dup-slug",
100
- title: "First",
101
- projectPath: "/p",
102
- });
103
-
104
- expect(() =>
105
- createAnalysis(db, {
106
- slug: "dup-slug",
107
- title: "Second",
108
- projectPath: "/p",
109
- }),
110
- ).toThrow(/already exists/);
111
- });
112
-
113
- test("same slug different projectPath is OK", () => {
114
- createAnalysis(db, { slug: "shared-slug", title: "A", projectPath: "/p1" });
115
- const b = createAnalysis(db, { slug: "shared-slug", title: "B", projectPath: "/p2" });
116
- expect(b.slug).toBe("shared-slug");
117
- expect(b.projectPath).toBe("/p2");
118
- });
119
-
120
- test("list filters by agent, sourcePlanId, archived", () => {
121
- const plan = makePlan();
122
- createAnalysis(db, { slug: "a1", title: "Alpha", projectPath: "/p", agent: "ranger" });
123
- createAnalysis(db, { slug: "a2", title: "Beta", projectPath: "/p", agent: "sage" });
124
- createAnalysis(db, { slug: "a3", title: "Gamma", projectPath: "/p", agent: "craftsman" });
125
- const linked = getAnalysisBySlug(db, "a3", "/p")!;
126
- linkAnalysisToPlan(db, linked.id, plan.id);
127
-
128
- // filter by agent
129
- const rangers = listAnalyses(db, { agent: "ranger" });
130
- expect(rangers).toHaveLength(1);
131
- expect(rangers[0].slug).toBe("a1");
132
-
133
- // filter by sourcePlanId
134
- const byPlan = listAnalyses(db, { sourcePlanId: plan.id });
135
- expect(byPlan).toHaveLength(1);
136
- expect(byPlan[0].slug).toBe("a3");
137
-
138
- // archived filter
139
- const all = listAnalyses(db, { archived: false });
140
- expect(all.length).toBeGreaterThanOrEqual(3);
141
-
142
- archiveAnalysis(db, linked.id);
143
- const active = listAnalyses(db, { archived: false });
144
- expect(active.find((a) => a.id === linked.id)).toBeUndefined();
145
-
146
- const archived = listAnalyses(db, { archived: true });
147
- expect(archived.find((a) => a.id === linked.id)).toBeDefined();
148
- });
149
-
150
- test("search FTS — finds matching analysis by title", () => {
151
- createAnalysis(db, { slug: "s1", title: "Database Security Audit", projectPath: "/p" });
152
- createAnalysis(db, { slug: "s2", title: "Frontend Performance Review", projectPath: "/p" });
153
- createAnalysis(db, { slug: "s3", title: "API Authentication Analysis", projectPath: "/p" });
154
-
155
- const results = searchAnalyses(db, "security");
156
- expect(results).toHaveLength(1);
157
- expect(results[0].slug).toBe("s1");
158
- });
159
-
160
- test("search FTS — finds matching analysis by summary", () => {
161
- createAnalysis(db, {
162
- slug: "s-summary",
163
- title: "Generic Title",
164
- projectPath: "/p",
165
- summary: "Found critical XSS vulnerabilities in form handlers",
166
- });
167
-
168
- const results = searchAnalyses(db, "XSS vulnerabilities");
169
- expect(results).toHaveLength(1);
170
- expect(results[0].slug).toBe("s-summary");
171
- });
172
-
173
- test("update partial — only changed fields update + updated_at bumps", () => {
174
- const a = createAnalysis(db, {
175
- slug: "upd",
176
- title: "Original Title",
177
- projectPath: "/p",
178
- summary: "original summary",
179
- });
180
-
181
- const updated = updateAnalysis(db, a.id, { title: "New Title" });
182
- expect(updated.title).toBe("New Title");
183
- expect(updated.summary).toBe("original summary"); // unchanged
184
- expect(updated.slug).toBe("upd"); // unchanged
185
- // updated_at is datetime('now') — same-second updates may have same value;
186
- // just verify the field exists and is a string
187
- expect(typeof updated.updatedAt).toBe("string");
188
- });
189
-
190
- test("archive + list excludes archived", () => {
191
- const a = createAnalysis(db, { slug: "to-archive", title: "Archive Me", projectPath: "/p" });
192
- expect(a.archivedAt).toBeNull();
193
-
194
- const archived = archiveAnalysis(db, a.id);
195
- expect(archived.archivedAt).toBeTruthy();
196
-
197
- // default list excludes archived
198
- const active = listAnalyses(db);
199
- expect(active.find((x) => x.id === a.id)).toBeUndefined();
200
-
201
- // getAnalysis excludes archived by default
202
- expect(getAnalysis(db, a.id)).toBeNull();
203
-
204
- // getAnalysis with includeArchived returns it
205
- const found = getAnalysis(db, a.id, { includeArchived: true });
206
- expect(found).not.toBeNull();
207
- expect(found!.id).toBe(a.id);
208
-
209
- // idempotent — archiving again is a no-op
210
- const again = archiveAnalysis(db, a.id);
211
- expect(again.archivedAt).toBe(archived.archivedAt);
212
- });
213
-
214
- test("linkAnalysisToPlan — sets sourcePlanId", () => {
215
- const plan = makePlan();
216
- const a = createAnalysis(db, { slug: "link-me", title: "Link Me", projectPath: "/p" });
217
- expect(a.sourcePlanId).toBeNull();
218
-
219
- const linked = linkAnalysisToPlan(db, a.id, plan.id);
220
- expect(linked.sourcePlanId).toBe(plan.id);
221
-
222
- // getAnalysisBySlug reflects the link
223
- const bySlug = getAnalysisBySlug(db, "link-me", "/p");
224
- expect(bySlug!.sourcePlanId).toBe(plan.id);
225
- });
226
-
227
- test("linkAnalysisToPlan — FK validation: non-existent plan throws", () => {
228
- const a = createAnalysis(db, { slug: "no-fk", title: "No FK", projectPath: "/p" });
229
- expect(() => linkAnalysisToPlan(db, a.id, "non-existent-plan-id")).toThrow(/not found/);
230
- });
231
-
232
- test("unlinkAnalysisFromPlan — sets sourcePlanId to null", () => {
233
- const plan = makePlan();
234
- const a = createAnalysis(db, { slug: "unlink", title: "Unlink", projectPath: "/p" });
235
- linkAnalysisToPlan(db, a.id, plan.id);
236
-
237
- const unlinked = unlinkAnalysisFromPlan(db, a.id);
238
- expect(unlinked.sourcePlanId).toBeNull();
239
-
240
- // idempotent — unlinking again is a no-op
241
- const again = unlinkAnalysisFromPlan(db, a.id);
242
- expect(again.sourcePlanId).toBeNull();
243
- });
244
-
245
- test("CASCADE plan deletion — sourcePlanId becomes NULL (ON DELETE SET NULL)", () => {
246
- const plan = makePlan("plan-cascade", "cascade-plan");
247
- const a = createAnalysis(db, { slug: "cascade", title: "Cascade", projectPath: "/p" });
248
- linkAnalysisToPlan(db, a.id, plan.id);
249
-
250
- // delete plan directly via SQL (simulates external deletion)
251
- db.query("DELETE FROM plans WHERE id = ?").run("plan-cascade");
252
-
253
- const after = getAnalysis(db, a.id);
254
- expect(after).not.toBeNull();
255
- expect(after!.sourcePlanId).toBeNull();
256
- });
257
- });
package/tsconfig.json DELETED
@@ -1,31 +0,0 @@
1
- {
2
- "compilerOptions": {
3
- "target": "ES2022",
4
- "module": "ESNext",
5
- "moduleResolution": "bundler",
6
- "lib": ["ES2022"],
7
- "types": ["bun-types"],
8
- "allowImportingTsExtensions": true,
9
- "verbatimModuleSyntax": true,
10
- "noEmit": true,
11
- "strict": true,
12
- "noImplicitAny": true,
13
- "strictNullChecks": true,
14
- "strictFunctionTypes": true,
15
- "noUnusedLocals": true,
16
- "noUnusedParameters": true,
17
- "noFallthroughCasesInSwitch": true,
18
- "noUncheckedIndexedAccess": true,
19
- "exactOptionalPropertyTypes": true,
20
- "esModuleInterop": true,
21
- "skipLibCheck": true,
22
- "forceConsistentCasingInFileNames": true,
23
- "resolveJsonModule": true,
24
- "isolatedModules": true,
25
- "paths": {
26
- "@/*": ["./src/*"]
27
- }
28
- },
29
- "include": ["src/**/*", "scripts/**/*", "build.ts"],
30
- "exclude": ["node_modules", "dist", ".slim"]
31
- }