ndomo 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 (247) hide show
  1. package/.bun-version +1 -0
  2. package/.dockerignore +79 -0
  3. package/.editorconfig +18 -0
  4. package/.env.example +19 -0
  5. package/.github/CODEOWNERS +8 -0
  6. package/.github/ISSUE_TEMPLATE/bug_report.yml +62 -0
  7. package/.github/ISSUE_TEMPLATE/config.yml +2 -0
  8. package/.github/ISSUE_TEMPLATE/feature_request.yml +34 -0
  9. package/.github/dependabot.yml +36 -0
  10. package/.github/pull_request_template.md +24 -0
  11. package/.github/release.yml +30 -0
  12. package/.github/workflows/gitleaks.yml +28 -0
  13. package/.github/workflows/release-please.yml +27 -0
  14. package/.github/workflows/smoke.yml +29 -0
  15. package/.husky/commit-msg +1 -0
  16. package/CHANGELOG.md +114 -0
  17. package/Dockerfile +32 -0
  18. package/README.es.md +174 -0
  19. package/README.md +187 -0
  20. package/agents/chronicler.md +98 -0
  21. package/agents/ci-smith.md +136 -0
  22. package/agents/craftsman.md +341 -0
  23. package/agents/deploy-smith.md +138 -0
  24. package/agents/foreman.md +377 -0
  25. package/agents/go-smith.md +164 -0
  26. package/agents/guild.md +188 -0
  27. package/agents/inspector.md +83 -0
  28. package/agents/js-smith.md +127 -0
  29. package/agents/ops-scout.md +173 -0
  30. package/agents/painter.md +200 -0
  31. package/agents/python-smith.md +120 -0
  32. package/agents/ranger.md +307 -0
  33. package/agents/release-smith.md +165 -0
  34. package/agents/rust-smith.md +159 -0
  35. package/agents/sage.md +178 -0
  36. package/agents/scout.md +144 -0
  37. package/agents/scribe.md +156 -0
  38. package/agents/smith.md +201 -0
  39. package/agents/vue-smith.md +155 -0
  40. package/agents/warden.md +216 -0
  41. package/agents/zig-smith.md +156 -0
  42. package/bin/ndomo-analyses.ts +4 -0
  43. package/bin/ndomo-status.ts +4 -0
  44. package/biome.json +57 -0
  45. package/bun.lock +514 -0
  46. package/commitlint.config.js +3 -0
  47. package/config/ndomo.config.json +258 -0
  48. package/config/ndomo.schema.json +166 -0
  49. package/docs/agents.md +375 -0
  50. package/docs/bugs/plan-create-orphan-fk.md +131 -0
  51. package/docs/bugs/task_create_batch-order-index-collision.md +158 -0
  52. package/docs/configuration.md +276 -0
  53. package/docs/database.md +364 -0
  54. package/docs/features/feature-flexible-builder-v1.md +724 -0
  55. package/docs/features/feature-flexible-builder-v2.md +882 -0
  56. package/docs/features/feature-flexible-builder.md +974 -0
  57. package/docs/http-server.md +244 -0
  58. package/docs/installation.md +259 -0
  59. package/docs/integrations.md +129 -0
  60. package/docs/operations/anti-pattern-sub-agent-verify-2026-06-21.md +32 -0
  61. package/docs/operations/audit-v1.md +417 -0
  62. package/docs/operations/audit-v2.md +197 -0
  63. package/docs/operations/audit-v3.md +306 -0
  64. package/docs/operations/db-optimize-foundations.md +123 -0
  65. package/docs/operations/verify-gate-architecture.md +82 -0
  66. package/docs/workflows.md +448 -0
  67. package/opencode.json +5 -0
  68. package/package.json +65 -0
  69. package/release-please-config.json +11 -0
  70. package/scripts/dev-bust-cache.sh +164 -0
  71. package/scripts/install.sh +688 -0
  72. package/scripts/smoke-e2e.ts +704 -0
  73. package/scripts/smoke-hot.ts +417 -0
  74. package/scripts/smoke-http.sh +228 -0
  75. package/scripts/smoke-v4.ts +256 -0
  76. package/scripts/smoke-v5.ts +397 -0
  77. package/scripts/smoke.sh +9 -0
  78. package/scripts/uninstall.sh +224 -0
  79. package/skills/api-security-best-practices/SKILL.md +915 -0
  80. package/skills/bash-scripting/SKILL.md +201 -0
  81. package/skills/bun/SKILL.md +313 -0
  82. package/skills/cavecrew/SKILL.md +82 -0
  83. package/skills/caveman/SKILL.md +74 -0
  84. package/skills/caveman-review/README.md +33 -0
  85. package/skills/caveman-review/SKILL.md +55 -0
  86. package/skills/find-skills/SKILL.md +142 -0
  87. package/skills/frontend-design/LICENSE.txt +177 -0
  88. package/skills/frontend-design/SKILL.md +55 -0
  89. package/skills/golang-patterns/SKILL.md +674 -0
  90. package/skills/golang-security/SKILL.md +185 -0
  91. package/skills/golang-security/evals/evals.json +595 -0
  92. package/skills/golang-security/references/architecture.md +268 -0
  93. package/skills/golang-security/references/checklist.md +80 -0
  94. package/skills/golang-security/references/cookies.md +200 -0
  95. package/skills/golang-security/references/cryptography.md +424 -0
  96. package/skills/golang-security/references/filesystem.md +285 -0
  97. package/skills/golang-security/references/injection.md +315 -0
  98. package/skills/golang-security/references/logging.md +163 -0
  99. package/skills/golang-security/references/memory-safety.md +241 -0
  100. package/skills/golang-security/references/network.md +253 -0
  101. package/skills/golang-security/references/secrets.md +189 -0
  102. package/skills/golang-security/references/third-party.md +159 -0
  103. package/skills/golang-security/references/threat-modeling.md +189 -0
  104. package/skills/golang-testing/SKILL.md +720 -0
  105. package/skills/grill-me/SKILL.md +7 -0
  106. package/skills/javascript-testing-patterns/SKILL.md +537 -0
  107. package/skills/javascript-testing-patterns/references/advanced-testing-patterns.md +513 -0
  108. package/skills/modern-javascript-patterns/SKILL.md +43 -0
  109. package/skills/modern-javascript-patterns/references/advanced-patterns.md +487 -0
  110. package/skills/modern-javascript-patterns/references/details.md +457 -0
  111. package/skills/python-anti-patterns/SKILL.md +349 -0
  112. package/skills/python-design-patterns/SKILL.md +85 -0
  113. package/skills/python-design-patterns/references/details.md +353 -0
  114. package/skills/python-error-handling/SKILL.md +193 -0
  115. package/skills/python-error-handling/references/details.md +171 -0
  116. package/skills/python-testing-patterns/SKILL.md +278 -0
  117. package/skills/python-testing-patterns/references/advanced-patterns.md +411 -0
  118. package/skills/python-testing-patterns/references/details.md +349 -0
  119. package/skills/rust-patterns/SKILL.md +500 -0
  120. package/skills/rust-testing/SKILL.md +501 -0
  121. package/skills/security-review/SKILL.md +504 -0
  122. package/skills/security-review/cloud-infrastructure-security.md +361 -0
  123. package/skills/vue-best-practices/SKILL.md +154 -0
  124. package/skills/vue-best-practices/references/animation-class-based-technique.md +254 -0
  125. package/skills/vue-best-practices/references/animation-state-driven-technique.md +291 -0
  126. package/skills/vue-best-practices/references/component-async.md +97 -0
  127. package/skills/vue-best-practices/references/component-data-flow.md +307 -0
  128. package/skills/vue-best-practices/references/component-fallthrough-attrs.md +174 -0
  129. package/skills/vue-best-practices/references/component-keep-alive.md +137 -0
  130. package/skills/vue-best-practices/references/component-slots.md +216 -0
  131. package/skills/vue-best-practices/references/component-suspense.md +228 -0
  132. package/skills/vue-best-practices/references/component-teleport.md +108 -0
  133. package/skills/vue-best-practices/references/component-transition-group.md +128 -0
  134. package/skills/vue-best-practices/references/component-transition.md +125 -0
  135. package/skills/vue-best-practices/references/composables.md +290 -0
  136. package/skills/vue-best-practices/references/directives.md +162 -0
  137. package/skills/vue-best-practices/references/perf-avoid-component-abstraction-in-lists.md +159 -0
  138. package/skills/vue-best-practices/references/perf-v-once-v-memo-directives.md +182 -0
  139. package/skills/vue-best-practices/references/perf-virtualize-large-lists.md +187 -0
  140. package/skills/vue-best-practices/references/plugins.md +166 -0
  141. package/skills/vue-best-practices/references/reactivity.md +344 -0
  142. package/skills/vue-best-practices/references/render-functions.md +201 -0
  143. package/skills/vue-best-practices/references/sfc.md +310 -0
  144. package/skills/vue-best-practices/references/state-management.md +135 -0
  145. package/skills/vue-best-practices/references/updated-hook-performance.md +187 -0
  146. package/skills/vue-pinia-best-practices/SKILL.md +21 -0
  147. package/skills/vue-pinia-best-practices/reference/pinia-no-active-pinia-error.md +248 -0
  148. package/skills/vue-pinia-best-practices/reference/pinia-setup-store-return-all-state.md +227 -0
  149. package/skills/vue-pinia-best-practices/reference/pinia-store-destructuring-breaks-reactivity.md +193 -0
  150. package/skills/vue-pinia-best-practices/reference/state-url-for-ephemeral-filters.md +238 -0
  151. package/skills/vue-pinia-best-practices/reference/state-use-pinia-for-large-apps.md +262 -0
  152. package/skills/vue-pinia-best-practices/reference/store-method-binding-parentheses.md +191 -0
  153. package/skills/zig-0.16/SKILL.md +840 -0
  154. package/skills/zig-0.16/scripts/check-zig-version.sh +21 -0
  155. package/src/cli/analyses.ts +280 -0
  156. package/src/cli/index.ts +108 -0
  157. package/src/cli/serve.ts +192 -0
  158. package/src/cli/smoke.ts +131 -0
  159. package/src/cli/status.test.ts +204 -0
  160. package/src/cli/status.ts +263 -0
  161. package/src/cli/vacuum.test.ts +82 -0
  162. package/src/cli/vacuum.ts +96 -0
  163. package/src/config/schema.test.ts +88 -0
  164. package/src/config/schema.ts +64 -0
  165. package/src/db/analyses-migration.test.ts +210 -0
  166. package/src/db/analyses.test.ts +466 -0
  167. package/src/db/analyses.ts +375 -0
  168. package/src/db/auto-checkpoint.ts +131 -0
  169. package/src/db/client.test.ts +129 -0
  170. package/src/db/client.ts +55 -0
  171. package/src/db/fts-escape.ts +20 -0
  172. package/src/db/incidents.test.ts +201 -0
  173. package/src/db/incidents.ts +93 -0
  174. package/src/db/index.ts +86 -0
  175. package/src/db/migrations-v13.test.ts +141 -0
  176. package/src/db/migrations-v8.test.ts +301 -0
  177. package/src/db/migrations.ts +147 -0
  178. package/src/db/plan-archive.test.ts +180 -0
  179. package/src/db/plan-archive.ts +274 -0
  180. package/src/db/plan-create.test.ts +276 -0
  181. package/src/db/plan-create.ts +78 -0
  182. package/src/db/plan-files.test.ts +289 -0
  183. package/src/db/plan-update-status.ts +287 -0
  184. package/src/db/plans.test.ts +490 -0
  185. package/src/db/plans.ts +534 -0
  186. package/src/db/resolve-project-dir.test.ts +143 -0
  187. package/src/db/resolve-project-dir.ts +75 -0
  188. package/src/db/rollbacks.test.ts +150 -0
  189. package/src/db/rollbacks.ts +67 -0
  190. package/src/db/schema.ts +907 -0
  191. package/src/db/sessions.test.ts +80 -0
  192. package/src/db/sessions.ts +135 -0
  193. package/src/db/shutdown.test.ts +147 -0
  194. package/src/db/shutdown.ts +45 -0
  195. package/src/db/tasks.test.ts +921 -0
  196. package/src/db/tasks.ts +747 -0
  197. package/src/db/types.ts +619 -0
  198. package/src/http/__tests__/auth.test.ts +196 -0
  199. package/src/http/__tests__/routes.test.ts +465 -0
  200. package/src/http/__tests__/sse.test.ts +317 -0
  201. package/src/http/auth.ts +72 -0
  202. package/src/http/middleware/cors.ts +53 -0
  203. package/src/http/middleware/security-headers.ts +21 -0
  204. package/src/http/routes/events.ts +112 -0
  205. package/src/http/routes/health.ts +51 -0
  206. package/src/http/routes/plans.ts +66 -0
  207. package/src/http/routes/sessions.ts +50 -0
  208. package/src/http/routes/tasks.ts +60 -0
  209. package/src/http/server.ts +95 -0
  210. package/src/http/sse.ts +116 -0
  211. package/src/index.ts +37 -0
  212. package/src/lib.ts +65 -0
  213. package/src/mem/scoped.ts +65 -0
  214. package/src/orchestrator/background.test.ts +268 -0
  215. package/src/orchestrator/background.ts +293 -0
  216. package/src/orchestrator/memory-hook.ts +182 -0
  217. package/src/orchestrator/reconciler.ts +123 -0
  218. package/src/orchestrator/scheduler.test.ts +300 -0
  219. package/src/orchestrator/scheduler.ts +243 -0
  220. package/src/plugin.test.ts +2574 -0
  221. package/src/plugin.ts +1690 -0
  222. package/src/sdk/client.ts +66 -0
  223. package/src/worktrees/manager.ts +236 -0
  224. package/src/worktrees/state.ts +87 -0
  225. package/tests/integration/ranger-flow.test.ts +257 -0
  226. package/tools/analysis_archive.ts +28 -0
  227. package/tools/analysis_create.ts +55 -0
  228. package/tools/analysis_get.ts +33 -0
  229. package/tools/analysis_link_plan.ts +44 -0
  230. package/tools/analysis_list.ts +48 -0
  231. package/tools/analysis_search.ts +36 -0
  232. package/tools/analysis_update.ts +44 -0
  233. package/tools/plan_approve.ts +31 -0
  234. package/tools/plan_create.ts +58 -0
  235. package/tools/plan_get.ts +40 -0
  236. package/tools/plan_list.ts +37 -0
  237. package/tools/plan_search.ts +34 -0
  238. package/tools/plan_update_status.ts +71 -0
  239. package/tools/session_checkpoint.ts +31 -0
  240. package/tools/session_end.ts +26 -0
  241. package/tools/session_start.ts +43 -0
  242. package/tools/task_create_batch.ts +70 -0
  243. package/tools/task_list.ts +35 -0
  244. package/tools/task_next_for_agent.ts +30 -0
  245. package/tools/task_search.ts +34 -0
  246. package/tools/task_update_status.ts +37 -0
  247. package/tsconfig.json +31 -0
@@ -0,0 +1,466 @@
1
+ import { Database } from "bun:sqlite";
2
+ import { beforeEach, describe, expect, test } from "bun:test";
3
+ import {
4
+ archiveAnalysis,
5
+ createAnalysis,
6
+ getAnalysis,
7
+ getAnalysisBySlug,
8
+ linkAnalysisToPlan,
9
+ listAnalyses,
10
+ searchAnalyses,
11
+ unlinkAnalysisFromPlan,
12
+ updateAnalysis,
13
+ validateAnalysisFindings,
14
+ } from "./analyses.ts";
15
+ import { createPlan } from "./plans.ts";
16
+ import { runMigrations } from "./migrations.ts";
17
+
18
+
19
+ let db: Database;
20
+
21
+ function makePlan(overrides?: Partial<ReturnType<typeof createPlan>>): ReturnType<typeof createPlan> {
22
+ return createPlan(db, {
23
+ id: "plan-1",
24
+ slug: "test-plan",
25
+ title: "Test Plan",
26
+ status: "draft",
27
+ priority: 1,
28
+ overview: "test overview",
29
+ complexity: 3,
30
+ createdBy: "test",
31
+ updatedBy: "test",
32
+ sessionId: null,
33
+ approvedAt: null,
34
+ completedAt: null,
35
+ approach: null,
36
+ sourceSessionId: null,
37
+ sourceMessageId: null,
38
+ category: null,
39
+ metadata: {},
40
+ archivedAt: null,
41
+ ...overrides,
42
+ });
43
+ }
44
+
45
+ beforeEach(() => {
46
+ db = new Database(":memory:");
47
+ db.exec("PRAGMA foreign_keys = ON");
48
+ runMigrations(db);
49
+ });
50
+
51
+ describe("analyses.ts", () => {
52
+ // ── createAnalysis ─────────────────────────────────────────────────────────
53
+
54
+ describe("createAnalysis", () => {
55
+ test("happy path — creates analysis with defaults", () => {
56
+ const a = createAnalysis(db, {
57
+ slug: "my-analysis",
58
+ title: "My Analysis",
59
+ projectPath: "/home/project",
60
+ });
61
+ expect(a.id).toBeTruthy();
62
+ expect(a.slug).toBe("my-analysis");
63
+ expect(a.title).toBe("My Analysis");
64
+ expect(a.projectPath).toBe("/home/project");
65
+ expect(a.summary).toBe("");
66
+ expect(a.findingsJson).toBe("[]");
67
+ expect(a.agent).toBe("ranger");
68
+ expect(a.sourcePlanId).toBeNull();
69
+ expect(a.sessionId).toBeNull();
70
+ expect(a.createdBy).toBeNull();
71
+ expect(a.createdAt).toBeTruthy();
72
+ expect(a.updatedAt).toBeTruthy();
73
+ expect(a.archivedAt).toBeNull();
74
+ });
75
+
76
+ test("creates analysis with all optional fields", () => {
77
+ const a = createAnalysis(db, {
78
+ slug: "full-analysis",
79
+ title: "Full Analysis",
80
+ projectPath: "/opt/app",
81
+ summary: "A complete analysis",
82
+ findingsJson: '[{"issue":"foo"}]',
83
+ agent: "js-smith",
84
+ sessionId: "sess-1",
85
+ createdBy: "user-1",
86
+ });
87
+ expect(a.summary).toBe("A complete analysis");
88
+ expect(a.findingsJson).toBe('[{"issue":"foo"}]');
89
+ expect(a.agent).toBe("js-smith");
90
+ expect(a.sessionId).toBe("sess-1");
91
+ expect(a.createdBy).toBe("user-1");
92
+ });
93
+
94
+ test("throws on duplicate slug+project_path", () => {
95
+ createAnalysis(db, { slug: "dup", title: "A", projectPath: "/p" });
96
+ expect(() =>
97
+ createAnalysis(db, { slug: "dup", title: "B", projectPath: "/p" }),
98
+ ).toThrow("already exists");
99
+ });
100
+
101
+ test("same slug different project_path is OK", () => {
102
+ createAnalysis(db, { slug: "shared", title: "A", projectPath: "/p1" });
103
+ const b = createAnalysis(db, { slug: "shared", title: "B", projectPath: "/p2" });
104
+ expect(b.id).toBeTruthy();
105
+ });
106
+
107
+ test("throws on empty slug", () => {
108
+ expect(() =>
109
+ createAnalysis(db, { slug: "", title: "T", projectPath: "/p" }),
110
+ ).toThrow("slug cannot be empty");
111
+ });
112
+
113
+ test("throws on empty projectPath", () => {
114
+ expect(() =>
115
+ createAnalysis(db, { slug: "ok", title: "T", projectPath: "" }),
116
+ ).toThrow("projectPath cannot be empty");
117
+ });
118
+
119
+ test("trims slug and title", () => {
120
+ const a = createAnalysis(db, {
121
+ slug: " trimmed ",
122
+ title: " spaced ",
123
+ projectPath: "/p",
124
+ });
125
+ expect(a.slug).toBe("trimmed");
126
+ expect(a.title).toBe("spaced");
127
+ });
128
+ });
129
+
130
+ // ── getAnalysis / getAnalysisBySlug ────────────────────────────────────────
131
+
132
+ describe("getAnalysis", () => {
133
+ test("returns existing analysis", () => {
134
+ const created = createAnalysis(db, { slug: "find", title: "Find me", projectPath: "/p" });
135
+ const found = getAnalysis(db, created.id);
136
+ expect(found).not.toBeNull();
137
+ expect(found!.slug).toBe("find");
138
+ });
139
+
140
+ test("returns null for non-existent id", () => {
141
+ expect(getAnalysis(db, "nonexistent")).toBeNull();
142
+ });
143
+
144
+ test("excludes archived by default", () => {
145
+ const created = createAnalysis(db, { slug: "arch", title: "Arch", projectPath: "/p" });
146
+ archiveAnalysis(db, created.id);
147
+ expect(getAnalysis(db, created.id)).toBeNull();
148
+ });
149
+
150
+ test("includeArchived=true returns archived", () => {
151
+ const created = createAnalysis(db, { slug: "arch2", title: "Arch2", projectPath: "/p" });
152
+ archiveAnalysis(db, created.id);
153
+ const found = getAnalysis(db, created.id, { includeArchived: true });
154
+ expect(found).not.toBeNull();
155
+ expect(found!.archivedAt).toBeTruthy();
156
+ });
157
+ });
158
+
159
+ describe("getAnalysisBySlug", () => {
160
+ test("returns analysis by slug+project", () => {
161
+ createAnalysis(db, { slug: "by-slug", title: "BySlug", projectPath: "/proj" });
162
+ const found = getAnalysisBySlug(db, "by-slug", "/proj");
163
+ expect(found).not.toBeNull();
164
+ expect(found!.title).toBe("BySlug");
165
+ });
166
+
167
+ test("returns null for wrong project", () => {
168
+ createAnalysis(db, { slug: "x", title: "X", projectPath: "/p1" });
169
+ expect(getAnalysisBySlug(db, "x", "/p2")).toBeNull();
170
+ });
171
+ });
172
+
173
+ // ── listAnalyses ──────────────────────────────────────────────────────────
174
+
175
+ describe("listAnalyses", () => {
176
+ test("returns empty array when no analyses", () => {
177
+ expect(listAnalyses(db)).toEqual([]);
178
+ });
179
+
180
+ test("returns all active analyses", () => {
181
+ createAnalysis(db, { slug: "a", title: "A", projectPath: "/p" });
182
+ createAnalysis(db, { slug: "b", title: "B", projectPath: "/p" });
183
+ const all = listAnalyses(db);
184
+ expect(all.length).toBe(2);
185
+ });
186
+
187
+ test("excludes archived by default", () => {
188
+ const a = createAnalysis(db, { slug: "a", title: "A", projectPath: "/p" });
189
+ createAnalysis(db, { slug: "b", title: "B", projectPath: "/p" });
190
+ archiveAnalysis(db, a.id);
191
+ const active = listAnalyses(db);
192
+ expect(active.length).toBe(1);
193
+ expect(active[0]!.slug).toBe("b");
194
+ });
195
+
196
+ test("filters by agent", () => {
197
+ createAnalysis(db, { slug: "a", title: "A", projectPath: "/p", agent: "js-smith" });
198
+ createAnalysis(db, { slug: "b", title: "B", projectPath: "/p", agent: "go-smith" });
199
+ const js = listAnalyses(db, { agent: "js-smith" });
200
+ expect(js.length).toBe(1);
201
+ expect(js[0]!.agent).toBe("js-smith");
202
+ });
203
+
204
+ test("filters by projectPath", () => {
205
+ createAnalysis(db, { slug: "a", title: "A", projectPath: "/p1" });
206
+ createAnalysis(db, { slug: "b", title: "B", projectPath: "/p2" });
207
+ const p1 = listAnalyses(db, { projectPath: "/p1" });
208
+ expect(p1.length).toBe(1);
209
+ });
210
+
211
+ test("filters by sourcePlanId", () => {
212
+ makePlan();
213
+ createAnalysis(db, { slug: "a", title: "A", projectPath: "/p", sourcePlanId: "plan-1" });
214
+ createAnalysis(db, { slug: "b", title: "B", projectPath: "/p" });
215
+ const linked = listAnalyses(db, { sourcePlanId: "plan-1" });
216
+ expect(linked.length).toBe(1);
217
+ });
218
+
219
+ test("respects limit", () => {
220
+ for (let i = 0; i < 5; i++) {
221
+ createAnalysis(db, { slug: `a${i}`, title: `A${i}`, projectPath: "/p" });
222
+ }
223
+ const limited = listAnalyses(db, { limit: 2 });
224
+ expect(limited.length).toBe(2);
225
+ });
226
+ });
227
+
228
+ // ── searchAnalyses ────────────────────────────────────────────────────────
229
+
230
+ describe("searchAnalyses", () => {
231
+ test("matches by title", () => {
232
+ createAnalysis(db, { slug: "s1", title: "Security Audit", projectPath: "/p" });
233
+ createAnalysis(db, { slug: "s2", title: "Performance Review", projectPath: "/p" });
234
+ const results = searchAnalyses(db, "Security");
235
+ expect(results.length).toBe(1);
236
+ expect(results[0]!.title).toBe("Security Audit");
237
+ });
238
+
239
+ test("matches by summary", () => {
240
+ createAnalysis(db, {
241
+ slug: "s1",
242
+ title: "T",
243
+ projectPath: "/p",
244
+ summary: "Found critical SQL injection vulnerability",
245
+ });
246
+ createAnalysis(db, { slug: "s2", title: "T2", projectPath: "/p", summary: "No issues" });
247
+ const results = searchAnalyses(db, "injection");
248
+ expect(results.length).toBe(1);
249
+ });
250
+
251
+ test("matches by findings_json", () => {
252
+ createAnalysis(db, {
253
+ slug: "s1",
254
+ title: "T",
255
+ projectPath: "/p",
256
+ findingsJson: '[{"issue":"XSS in login form"}]',
257
+ });
258
+ const results = searchAnalyses(db, "XSS");
259
+ expect(results.length).toBe(1);
260
+ });
261
+
262
+ test("returns empty on no match", () => {
263
+ createAnalysis(db, { slug: "s1", title: "Foo", projectPath: "/p" });
264
+ expect(searchAnalyses(db, "nonexistent")).toEqual([]);
265
+ });
266
+
267
+ test("escapes FTS special chars", () => {
268
+ createAnalysis(db, { slug: "s1", title: "auth-bug fix", projectPath: "/p" });
269
+ const results = searchAnalyses(db, "auth-bug");
270
+ expect(results.length).toBe(1);
271
+ });
272
+
273
+ test("filters by agent", () => {
274
+ createAnalysis(db, { slug: "a", title: "Alpha", projectPath: "/p", agent: "js-smith" });
275
+ createAnalysis(db, { slug: "b", title: "Alpha2", projectPath: "/p", agent: "go-smith" });
276
+ const results = searchAnalyses(db, "Alpha", { agent: "js-smith" });
277
+ expect(results.length).toBe(1);
278
+ });
279
+ });
280
+
281
+ // ── updateAnalysis ────────────────────────────────────────────────────────
282
+
283
+ describe("updateAnalysis", () => {
284
+ test("partial update — updates only specified fields", () => {
285
+ const created = createAnalysis(db, { slug: "upd", title: "Original", projectPath: "/p" });
286
+ const updated = updateAnalysis(db, created.id, { title: "Changed" });
287
+ expect(updated.title).toBe("Changed");
288
+ expect(updated.slug).toBe("upd"); // unchanged
289
+ expect(updated.projectPath).toBe("/p"); // unchanged
290
+ });
291
+
292
+ test("bumps updated_at", () => {
293
+ const created = createAnalysis(db, { slug: "upd2", title: "T", projectPath: "/p" });
294
+ // Force a small delay by using a known different timestamp
295
+ const updated = updateAnalysis(db, created.id, { summary: "new" });
296
+ // updated_at should be a valid ISO string (datetime('now'))
297
+ expect(updated.updatedAt).toBeTruthy();
298
+ expect(updated.summary).toBe("new");
299
+ });
300
+
301
+ test("throws on non-existent analysis", () => {
302
+ expect(() => updateAnalysis(db, "nonexistent", { title: "X" })).toThrow("not found");
303
+ });
304
+
305
+ test("no-op when no fields changed", () => {
306
+ const created = createAnalysis(db, { slug: "noop", title: "T", projectPath: "/p" });
307
+ const same = updateAnalysis(db, created.id, {});
308
+ expect(same.id).toBe(created.id);
309
+ });
310
+ });
311
+
312
+ // ── archiveAnalysis ───────────────────────────────────────────────────────
313
+
314
+ describe("archiveAnalysis", () => {
315
+ test("sets archived_at", () => {
316
+ const created = createAnalysis(db, { slug: "arch", title: "T", projectPath: "/p" });
317
+ const archived = archiveAnalysis(db, created.id);
318
+ expect(archived.archivedAt).toBeTruthy();
319
+ });
320
+
321
+ test("idempotent — archive twice returns same result", () => {
322
+ const created = createAnalysis(db, { slug: "arch2", title: "T", projectPath: "/p" });
323
+ const first = archiveAnalysis(db, created.id);
324
+ const second = archiveAnalysis(db, created.id);
325
+ expect(first.archivedAt).toBe(second.archivedAt);
326
+ });
327
+
328
+ test("archived analysis excluded from default list", () => {
329
+ const created = createAnalysis(db, { slug: "arch3", title: "T", projectPath: "/p" });
330
+ archiveAnalysis(db, created.id);
331
+ const active = listAnalyses(db);
332
+ expect(active.length).toBe(0);
333
+ });
334
+
335
+ test("throws on non-existent analysis", () => {
336
+ expect(() => archiveAnalysis(db, "nonexistent")).toThrow("not found");
337
+ });
338
+ });
339
+
340
+ // ── linkAnalysisToPlan ────────────────────────────────────────────────────
341
+
342
+ describe("linkAnalysisToPlan", () => {
343
+ test("sets source_plan_id", () => {
344
+ makePlan();
345
+ const a = createAnalysis(db, { slug: "link", title: "T", projectPath: "/p" });
346
+ const linked = linkAnalysisToPlan(db, a.id, "plan-1");
347
+ expect(linked.sourcePlanId).toBe("plan-1");
348
+ });
349
+
350
+ test("throws on non-existent analysis", () => {
351
+ makePlan();
352
+ expect(() => linkAnalysisToPlan(db, "nonexistent", "plan-1")).toThrow("not found");
353
+ });
354
+
355
+ test("throws on non-existent plan (FK enforcement)", () => {
356
+ const a = createAnalysis(db, { slug: "link2", title: "T", projectPath: "/p" });
357
+ expect(() => linkAnalysisToPlan(db, a.id, "bad-plan")).toThrow("not found");
358
+ });
359
+ });
360
+
361
+ describe("unlinkAnalysisFromPlan", () => {
362
+ test("clears source_plan_id", () => {
363
+ makePlan();
364
+ const a = createAnalysis(db, { slug: "unlink", title: "T", projectPath: "/p" });
365
+ linkAnalysisToPlan(db, a.id, "plan-1");
366
+ const unlinked = unlinkAnalysisFromPlan(db, a.id);
367
+ expect(unlinked.sourcePlanId).toBeNull();
368
+ });
369
+
370
+ test("idempotent — unlink when already unlinked is a no-op", () => {
371
+ const a = createAnalysis(db, { slug: "unlink2", title: "T", projectPath: "/p" });
372
+ expect(a.sourcePlanId).toBeNull();
373
+ const unlinked = unlinkAnalysisFromPlan(db, a.id);
374
+ expect(unlinked.sourcePlanId).toBeNull();
375
+ });
376
+
377
+ test("throws on non-existent analysis", () => {
378
+ expect(() => unlinkAnalysisFromPlan(db, "nonexistent")).toThrow("not found");
379
+ });
380
+ });
381
+
382
+ // ── validateAnalysisFindings (v15 agent boundary contract) ──────────────
383
+
384
+ describe("validateAnalysisFindings", () => {
385
+ test("no-op when findingsJson is undefined", () => {
386
+ expect(() => validateAnalysisFindings(undefined, "ranger")).not.toThrow();
387
+ expect(() => validateAnalysisFindings(undefined, "foreman")).not.toThrow();
388
+ });
389
+
390
+ test("no-op for non-ranger agents even with proposedAction present", () => {
391
+ const findings = JSON.stringify([
392
+ { severity: "high", observation: "x", proposedAction: "y" },
393
+ ]);
394
+ expect(() => validateAnalysisFindings(findings, "foreman")).not.toThrow();
395
+ expect(() => validateAnalysisFindings(findings, "craftsman")).not.toThrow();
396
+ expect(() => validateAnalysisFindings(findings, "inspector")).not.toThrow();
397
+ expect(() => validateAnalysisFindings(findings, undefined)).not.toThrow();
398
+ });
399
+
400
+ test("no-op when findingsJson is not valid JSON (defers to tool layer)", () => {
401
+ expect(() => validateAnalysisFindings("not-json{{", "ranger")).not.toThrow();
402
+ });
403
+
404
+ test("no-op when JSON is empty array", () => {
405
+ expect(() => validateAnalysisFindings("[]", "ranger")).not.toThrow();
406
+ });
407
+
408
+ test("no-op when JSON is not an array", () => {
409
+ expect(() => validateAnalysisFindings("{}", "ranger")).not.toThrow();
410
+ expect(() => validateAnalysisFindings("\"hello\"", "ranger")).not.toThrow();
411
+ expect(() => validateAnalysisFindings("null", "ranger")).not.toThrow();
412
+ });
413
+
414
+ test("ranger with observation-only findings is allowed", () => {
415
+ const findings = JSON.stringify([
416
+ { severity: "high", observation: "auth missing on /admin" },
417
+ { severity: "medium", observation: "N+1 query in listUsers" },
418
+ ]);
419
+ expect(() => validateAnalysisFindings(findings, "ranger")).not.toThrow();
420
+ });
421
+
422
+ test("ranger with proposedAction key throws clear validation error", () => {
423
+ const findings = JSON.stringify([
424
+ { severity: "high", observation: "auth missing", proposedAction: "add guard" },
425
+ ]);
426
+ expect(() => validateAnalysisFindings(findings, "ranger")).toThrow(
427
+ /ranger cannot emit proposedAction/i,
428
+ );
429
+ });
430
+
431
+ test("ranger throws even if only one finding has proposedAction", () => {
432
+ const findings = JSON.stringify([
433
+ { severity: "high", observation: "ok" },
434
+ { severity: "low", observation: "fine", proposedAction: "do X" },
435
+ ]);
436
+ expect(() => validateAnalysisFindings(findings, "ranger")).toThrow(
437
+ /ranger cannot emit proposedAction/i,
438
+ );
439
+ });
440
+
441
+ test("ranger throwing preserves error message mentioning both agent roles", () => {
442
+ const findings = JSON.stringify([
443
+ { severity: "high", observation: "x", proposedAction: "y" },
444
+ ]);
445
+ let err: Error | null = null;
446
+ try {
447
+ validateAnalysisFindings(findings, "ranger");
448
+ } catch (e) {
449
+ err = e as Error;
450
+ }
451
+ expect(err).not.toBeNull();
452
+ expect(err!.message).toContain("ranger");
453
+ expect(err!.message).toContain("foreman");
454
+ });
455
+
456
+ test("non-object items in array are tolerated", () => {
457
+ const findings = JSON.stringify([
458
+ "string-finding",
459
+ null,
460
+ 42,
461
+ { severity: "high", observation: "real finding" },
462
+ ]);
463
+ expect(() => validateAnalysisFindings(findings, "ranger")).not.toThrow();
464
+ });
465
+ });
466
+ });