@reqord/web 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 (138) hide show
  1. package/LICENSE +661 -0
  2. package/next-env.d.ts +6 -0
  3. package/next.config.ts +7 -0
  4. package/package.json +59 -0
  5. package/postcss.config.mjs +7 -0
  6. package/src/__tests__/components/dashboard/critical-path-display.test.tsx +129 -0
  7. package/src/__tests__/components/dashboard/progress-bar.test.tsx +87 -0
  8. package/src/__tests__/components/dashboard/project-health.test.tsx +57 -0
  9. package/src/__tests__/components/dashboard/warning-alert.test.tsx +75 -0
  10. package/src/__tests__/components/feedback/feedback-client-view.test.tsx +84 -0
  11. package/src/__tests__/components/feedback/feedback-filters.test.tsx +51 -0
  12. package/src/__tests__/components/feedback/feedback-linked-items.test.tsx +131 -0
  13. package/src/__tests__/components/feedback/feedback-list.test.tsx +49 -0
  14. package/src/__tests__/components/feedback/feedback-table.test.tsx +165 -0
  15. package/src/__tests__/components/flags/flag-badge.test.tsx +41 -0
  16. package/src/__tests__/components/flags/flag-list.test.tsx +51 -0
  17. package/src/__tests__/components/gantt/gantt-bar.test.tsx +190 -0
  18. package/src/__tests__/components/gantt/gantt-chart.test.tsx +141 -0
  19. package/src/__tests__/components/gantt/gantt-header.test.tsx +84 -0
  20. package/src/__tests__/components/gantt/gantt-legend.test.tsx +52 -0
  21. package/src/__tests__/components/graph/dag-layout.test.ts +129 -0
  22. package/src/__tests__/components/graph/dependency-graph.test.tsx +94 -0
  23. package/src/__tests__/components/graph/drilldown-breadcrumb.test.tsx +70 -0
  24. package/src/__tests__/components/graph/drilldown-graph.test.tsx +108 -0
  25. package/src/__tests__/components/graph/edge-styles.test.ts +27 -0
  26. package/src/__tests__/components/graph/graph-page-client.test.tsx +124 -0
  27. package/src/__tests__/components/graph/issue-node.test.tsx +173 -0
  28. package/src/__tests__/components/graph/requirement-node.test.tsx +151 -0
  29. package/src/__tests__/components/graph/specification-node.test.tsx +140 -0
  30. package/src/__tests__/components/specification/spec-tabs.test.tsx +153 -0
  31. package/src/__tests__/components/specification/tab-coverage.test.tsx +70 -0
  32. package/src/__tests__/components/specification/tab-design.test.tsx +42 -0
  33. package/src/__tests__/components/specification/tab-history.test.tsx +118 -0
  34. package/src/__tests__/components/specification/tab-issues.test.tsx +126 -0
  35. package/src/__tests__/components/specification/tab-research.test.tsx +42 -0
  36. package/src/__tests__/lib/dashboard-data.test.ts +334 -0
  37. package/src/__tests__/lib/drilldown-graph-data.test.ts +267 -0
  38. package/src/__tests__/lib/gantt-data.test.ts +299 -0
  39. package/src/__tests__/lib/graph-data.test.ts +309 -0
  40. package/src/__tests__/lib/local-feedback-repository.test.ts +74 -0
  41. package/src/__tests__/lib/local-specification-repository.test.ts +194 -0
  42. package/src/__tests__/lib/reqord-root.test.ts +31 -0
  43. package/src/__tests__/lib/specification-file.test.ts +63 -0
  44. package/src/__tests__/lib/tasks-data.test.ts +104 -0
  45. package/src/app/dashboard/loading.tsx +21 -0
  46. package/src/app/dashboard/page.tsx +50 -0
  47. package/src/app/error.tsx +22 -0
  48. package/src/app/feedback/loading.tsx +13 -0
  49. package/src/app/feedback/page.tsx +48 -0
  50. package/src/app/globals.css +2 -0
  51. package/src/app/graph/page.tsx +32 -0
  52. package/src/app/layout.tsx +25 -0
  53. package/src/app/page.tsx +5 -0
  54. package/src/app/requirements/[id]/edit/page.tsx +40 -0
  55. package/src/app/requirements/[id]/loading.tsx +14 -0
  56. package/src/app/requirements/[id]/not-found.tsx +18 -0
  57. package/src/app/requirements/[id]/page.tsx +43 -0
  58. package/src/app/requirements/loading.tsx +13 -0
  59. package/src/app/requirements/new/page.tsx +14 -0
  60. package/src/app/requirements/page.tsx +35 -0
  61. package/src/app/specifications/[id]/loading.tsx +14 -0
  62. package/src/app/specifications/[id]/not-found.tsx +18 -0
  63. package/src/app/specifications/[id]/page.tsx +52 -0
  64. package/src/app/specifications/loading.tsx +13 -0
  65. package/src/app/specifications/page.tsx +42 -0
  66. package/src/components/dashboard/critical-path-display.tsx +76 -0
  67. package/src/components/dashboard/progress-bar.tsx +45 -0
  68. package/src/components/dashboard/progress-section.tsx +57 -0
  69. package/src/components/dashboard/project-health.tsx +35 -0
  70. package/src/components/dashboard/status-card.tsx +27 -0
  71. package/src/components/dashboard/status-cards.tsx +28 -0
  72. package/src/components/dashboard/warning-alert.tsx +33 -0
  73. package/src/components/dashboard/warning-alerts.tsx +24 -0
  74. package/src/components/feedback/feedback-badge.tsx +48 -0
  75. package/src/components/feedback/feedback-client-view.tsx +38 -0
  76. package/src/components/feedback/feedback-filters.tsx +86 -0
  77. package/src/components/feedback/feedback-linked-items.tsx +93 -0
  78. package/src/components/feedback/feedback-list.tsx +40 -0
  79. package/src/components/feedback/feedback-table.tsx +115 -0
  80. package/src/components/gantt/gantt-bar.tsx +65 -0
  81. package/src/components/gantt/gantt-chart.tsx +88 -0
  82. package/src/components/gantt/gantt-constants.ts +15 -0
  83. package/src/components/gantt/gantt-critical-path.tsx +38 -0
  84. package/src/components/gantt/gantt-group.tsx +25 -0
  85. package/src/components/gantt/gantt-header.tsx +47 -0
  86. package/src/components/gantt/gantt-legend.tsx +26 -0
  87. package/src/components/graph/dag-layout.ts +131 -0
  88. package/src/components/graph/dependency-graph.tsx +88 -0
  89. package/src/components/graph/drilldown-breadcrumb.tsx +35 -0
  90. package/src/components/graph/drilldown-graph.tsx +45 -0
  91. package/src/components/graph/edge-styles.ts +16 -0
  92. package/src/components/graph/graph-loader.tsx +25 -0
  93. package/src/components/graph/graph-page-client.tsx +98 -0
  94. package/src/components/graph/issue-node.tsx +46 -0
  95. package/src/components/graph/multi-level-graph.tsx +91 -0
  96. package/src/components/graph/requirement-node.tsx +69 -0
  97. package/src/components/graph/specification-node.tsx +39 -0
  98. package/src/components/requirement/delete-button.tsx +46 -0
  99. package/src/components/requirement/dependency-editor.tsx +79 -0
  100. package/src/components/requirement/markdown-editor.tsx +47 -0
  101. package/src/components/requirement/markdown-renderer.tsx +12 -0
  102. package/src/components/requirement/requirement-detail.tsx +228 -0
  103. package/src/components/requirement/requirement-form.tsx +390 -0
  104. package/src/components/requirement/requirement-table.tsx +203 -0
  105. package/src/components/requirement/requirement-tabs.tsx +65 -0
  106. package/src/components/requirement/success-criteria-editor.tsx +53 -0
  107. package/src/components/specification/spec-detail.tsx +103 -0
  108. package/src/components/specification/spec-tabs.tsx +66 -0
  109. package/src/components/specification/specification-table.tsx +193 -0
  110. package/src/components/specification/tab-coverage.tsx +52 -0
  111. package/src/components/specification/tab-design.tsx +16 -0
  112. package/src/components/specification/tab-history.tsx +61 -0
  113. package/src/components/specification/tab-issues.tsx +111 -0
  114. package/src/components/specification/tab-research.tsx +16 -0
  115. package/src/components/ui/badge.tsx +64 -0
  116. package/src/components/ui/nav.tsx +49 -0
  117. package/src/components/ui/tabs.tsx +39 -0
  118. package/src/lib/actions.ts +222 -0
  119. package/src/lib/dashboard-data.ts +224 -0
  120. package/src/lib/data.ts +21 -0
  121. package/src/lib/drilldown-graph-data.ts +98 -0
  122. package/src/lib/feedback-data.ts +33 -0
  123. package/src/lib/feedback-repository.ts +6 -0
  124. package/src/lib/file-system.ts +167 -0
  125. package/src/lib/gantt-data.ts +168 -0
  126. package/src/lib/get-repository.ts +43 -0
  127. package/src/lib/graph-data.ts +161 -0
  128. package/src/lib/id-generator.ts +23 -0
  129. package/src/lib/local-feedback-repository.ts +36 -0
  130. package/src/lib/local-repository.ts +78 -0
  131. package/src/lib/local-specification-repository.ts +61 -0
  132. package/src/lib/repository.ts +11 -0
  133. package/src/lib/reqord-root.ts +33 -0
  134. package/src/lib/specification-data.ts +28 -0
  135. package/src/lib/specification-file.ts +12 -0
  136. package/src/lib/specification-repository.ts +8 -0
  137. package/src/lib/tasks-data.ts +32 -0
  138. package/tsconfig.json +27 -0
@@ -0,0 +1,309 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import type { Requirement, Specification, TaskEntry } from "@reqord/shared";
3
+ import { buildMultiLevelGraphData } from "../../lib/graph-data";
4
+
5
+ function makeReq(
6
+ id: string,
7
+ overrides: Partial<Requirement> = {},
8
+ ): Requirement {
9
+ return {
10
+ id,
11
+ version: "1.0.0",
12
+ title: `Requirement ${id}`,
13
+ status: "draft",
14
+ priority: "medium",
15
+ createdAt: "2026-01-01T00:00:00Z",
16
+ updatedAt: "2026-01-01T00:00:00Z",
17
+ versionHistory: [],
18
+ files: {
19
+ description: `requirements/${id}/description.md`,
20
+ supplementary: [],
21
+ },
22
+ successCriteria: [],
23
+ format: { type: "free-form" },
24
+ dependencies: { blockedBy: [], blocks: [], relatedTo: [] },
25
+ ...overrides,
26
+ } as Requirement;
27
+ }
28
+
29
+ function makeSpec(
30
+ id: string,
31
+ reqId: string,
32
+ overrides: Partial<Specification> = {},
33
+ ): Specification {
34
+ return {
35
+ id,
36
+ requirementId: reqId,
37
+ version: "1.0.0",
38
+ status: "draft",
39
+ createdAt: "2026-01-01T00:00:00Z",
40
+ updatedAt: "2026-01-01T00:00:00Z",
41
+ versionHistory: [],
42
+ files: { design: `specifications/${id}/design.md`, supplementary: [] },
43
+ ...overrides,
44
+ } as Specification;
45
+ }
46
+
47
+ function makeTask(
48
+ number: number,
49
+ specIds: string[],
50
+ overrides: Partial<TaskEntry> = {},
51
+ ): TaskEntry {
52
+ return {
53
+ number,
54
+ title: `Task #${number}`,
55
+ url: `https://github.com/test/repo/issues/${number}`,
56
+ status: "open",
57
+ linkedTo: { specifications: specIds },
58
+ syncedAt: "2026-01-01T00:00:00Z",
59
+ priority: "P1",
60
+ ...overrides,
61
+ };
62
+ }
63
+
64
+ describe("buildMultiLevelGraphData", () => {
65
+ describe("empty inputs", () => {
66
+ it("returns empty nodes and edges when both arrays are empty", () => {
67
+ const result = buildMultiLevelGraphData([], [], []);
68
+ expect(result.nodes).toEqual([]);
69
+ expect(result.edges).toEqual([]);
70
+ });
71
+ });
72
+
73
+ describe("requirements only", () => {
74
+ it("creates requirement nodes without specification or issue nodes", () => {
75
+ const req1 = makeReq("req-000001");
76
+ const req2 = makeReq("req-000002");
77
+ const result = buildMultiLevelGraphData([req1, req2], [], []);
78
+ expect(result.nodes).toHaveLength(2);
79
+ expect(result.nodes[0]).toMatchObject({
80
+ id: "req-000001",
81
+ type: "requirement",
82
+ data: { label: "Requirement req-000001", status: "draft", priority: "medium" },
83
+ position: { x: 0, y: 0 },
84
+ });
85
+ expect(result.nodes[1]).toMatchObject({
86
+ id: "req-000002",
87
+ type: "requirement",
88
+ position: { x: 0, y: 120 },
89
+ });
90
+ expect(result.edges).toEqual([]);
91
+ });
92
+
93
+ it("creates dependency edges from blockedBy relationships", () => {
94
+ const req1 = makeReq("req-000001");
95
+ const req2 = makeReq("req-000002", {
96
+ dependencies: { blockedBy: ["req-000001"], blocks: [], relatedTo: [] },
97
+ });
98
+ const result = buildMultiLevelGraphData([req1, req2], [], []);
99
+ expect(result.edges).toHaveLength(1);
100
+ expect(result.edges[0]).toMatchObject({
101
+ id: "dep-req-000001-req-000002",
102
+ source: "req-000001",
103
+ target: "req-000002",
104
+ type: "dependency",
105
+ });
106
+ });
107
+ });
108
+
109
+ describe("requirements and specifications", () => {
110
+ it("creates specification nodes at center column", () => {
111
+ const req1 = makeReq("req-000001");
112
+ const spec1 = makeSpec("spec-000001", "req-000001");
113
+ const result = buildMultiLevelGraphData([req1], [spec1], []);
114
+ const specNode = result.nodes.find((n) => n.type === "specification");
115
+ expect(specNode).toMatchObject({
116
+ id: "spec-000001",
117
+ type: "specification",
118
+ data: { label: "spec-000001", status: "draft" },
119
+ position: { x: 400, y: 0 },
120
+ });
121
+ });
122
+
123
+ it("creates implements edge from specification to requirement", () => {
124
+ const req1 = makeReq("req-000001");
125
+ const spec1 = makeSpec("spec-000001", "req-000001");
126
+ const result = buildMultiLevelGraphData([req1], [spec1], []);
127
+ const implementsEdge = result.edges.find((e) => e.type === "implements");
128
+ expect(implementsEdge).toMatchObject({
129
+ id: "impl-spec-000001-req-000001",
130
+ source: "spec-000001",
131
+ target: "req-000001",
132
+ type: "implements",
133
+ });
134
+ });
135
+
136
+ it("creates implements edge even when requirementId does not exist", () => {
137
+ const spec1 = makeSpec("spec-000001", "req-999999");
138
+ const result = buildMultiLevelGraphData([], [spec1], []);
139
+ expect(result.edges).toHaveLength(1);
140
+ expect(result.edges[0]).toMatchObject({
141
+ source: "spec-000001",
142
+ target: "req-999999",
143
+ type: "implements",
144
+ });
145
+ });
146
+
147
+ it("creates multiple implements edges when multiple specs reference same requirement", () => {
148
+ const req1 = makeReq("req-000001");
149
+ const spec1 = makeSpec("spec-000001", "req-000001");
150
+ const spec2 = makeSpec("spec-000002", "req-000001");
151
+ const result = buildMultiLevelGraphData([req1], [spec1, spec2], []);
152
+ const implementsEdges = result.edges.filter((e) => e.type === "implements");
153
+ expect(implementsEdges).toHaveLength(2);
154
+ expect(implementsEdges[0].target).toBe("req-000001");
155
+ expect(implementsEdges[1].target).toBe("req-000001");
156
+ });
157
+ });
158
+
159
+ describe("requirements, specifications, and tasks", () => {
160
+ it("creates issue nodes at right column", () => {
161
+ const req1 = makeReq("req-000001");
162
+ const spec1 = makeSpec("spec-000001", "req-000001");
163
+ const task = makeTask(123, ["spec-000001"], {
164
+ title: "Implement feature X",
165
+ url: "https://github.com/test/repo/issues/123",
166
+ priority: "P1",
167
+ status: "open",
168
+ });
169
+ const result = buildMultiLevelGraphData([req1], [spec1], [task]);
170
+ const issueNode = result.nodes.find((n) => n.type === "issue");
171
+ expect(issueNode).toMatchObject({
172
+ id: "issue-123",
173
+ type: "issue",
174
+ data: {
175
+ label: "Issue #123",
176
+ status: "open",
177
+ priority: "P1",
178
+ issueNumber: 123,
179
+ issueUrl: "https://github.com/test/repo/issues/123",
180
+ },
181
+ position: { x: 800, y: 0 },
182
+ });
183
+ });
184
+
185
+ it("creates tracks edge from issue to specification", () => {
186
+ const req1 = makeReq("req-000001");
187
+ const spec1 = makeSpec("spec-000001", "req-000001");
188
+ const task = makeTask(123, ["spec-000001"], {
189
+ title: "Implement feature X",
190
+ url: "https://github.com/test/repo/issues/123",
191
+ priority: "P1",
192
+ status: "open",
193
+ });
194
+ const result = buildMultiLevelGraphData([req1], [spec1], [task]);
195
+ const tracksEdge = result.edges.find((e) => e.type === "tracks");
196
+ expect(tracksEdge).toMatchObject({
197
+ id: "track-issue-123-spec-000001",
198
+ source: "issue-123",
199
+ target: "spec-000001",
200
+ type: "tracks",
201
+ });
202
+ });
203
+
204
+ it("creates all three node types and three edge types correctly", () => {
205
+ const req1 = makeReq("req-000001");
206
+ const req2 = makeReq("req-000002", {
207
+ dependencies: { blockedBy: ["req-000001"], blocks: [], relatedTo: [] },
208
+ });
209
+ const spec1 = makeSpec("spec-000001", "req-000002");
210
+ const task = makeTask(123, ["spec-000001"], {
211
+ title: "Implement feature X",
212
+ url: "https://github.com/test/repo/issues/123",
213
+ priority: "P1",
214
+ status: "open",
215
+ });
216
+ const result = buildMultiLevelGraphData([req1, req2], [spec1], [task]);
217
+ const reqNodes = result.nodes.filter((n) => n.type === "requirement");
218
+ const specNodes = result.nodes.filter((n) => n.type === "specification");
219
+ const issueNodes = result.nodes.filter((n) => n.type === "issue");
220
+ expect(reqNodes).toHaveLength(2);
221
+ expect(specNodes).toHaveLength(1);
222
+ expect(issueNodes).toHaveLength(1);
223
+ const depEdges = result.edges.filter((e) => e.type === "dependency");
224
+ const implEdges = result.edges.filter((e) => e.type === "implements");
225
+ const trackEdges = result.edges.filter((e) => e.type === "tracks");
226
+ expect(depEdges).toHaveLength(1);
227
+ expect(implEdges).toHaveLength(1);
228
+ expect(trackEdges).toHaveLength(1);
229
+ });
230
+
231
+ it("does not create issue nodes or tracks edges when no tasks match spec", () => {
232
+ const req1 = makeReq("req-000001");
233
+ const spec1 = makeSpec("spec-000001", "req-000001");
234
+ const result = buildMultiLevelGraphData([req1], [spec1], []);
235
+ const issueNodes = result.nodes.filter((n) => n.type === "issue");
236
+ const trackEdges = result.edges.filter((e) => e.type === "tracks");
237
+ expect(issueNodes).toHaveLength(0);
238
+ expect(trackEdges).toHaveLength(0);
239
+ });
240
+
241
+ it("only includes tasks linked to the matching spec", () => {
242
+ const req1 = makeReq("req-000001");
243
+ const spec1 = makeSpec("spec-000001", "req-000001");
244
+ const task1 = makeTask(123, ["spec-000001"]);
245
+ const task2 = makeTask(124, ["spec-000002"]);
246
+ const result = buildMultiLevelGraphData([req1], [spec1], [task1, task2]);
247
+ const issueNodes = result.nodes.filter((n) => n.type === "issue");
248
+ expect(issueNodes).toHaveLength(1);
249
+ expect(issueNodes[0].id).toBe("issue-123");
250
+ });
251
+
252
+ it("positions multiple issues vertically with offset", () => {
253
+ const req1 = makeReq("req-000001");
254
+ const spec1 = makeSpec("spec-000001", "req-000001");
255
+ const task1 = makeTask(123, ["spec-000001"]);
256
+ const task2 = makeTask(124, ["spec-000001"]);
257
+ const result = buildMultiLevelGraphData([req1], [spec1], [task1, task2]);
258
+ const issueNodes = result.nodes.filter((n) => n.type === "issue");
259
+ expect(issueNodes).toHaveLength(2);
260
+ expect(issueNodes[0].position).toEqual({ x: 800, y: 0 });
261
+ expect(issueNodes[1].position).toEqual({ x: 800, y: 80 });
262
+ });
263
+
264
+ it("creates only one issue node when a task is linked to multiple specs", () => {
265
+ const req1 = makeReq("req-000001");
266
+ const spec1 = makeSpec("spec-000001", "req-000001");
267
+ const spec2 = makeSpec("spec-000002", "req-000001");
268
+ const task = makeTask(123, ["spec-000001", "spec-000002"]);
269
+ const result = buildMultiLevelGraphData([req1], [spec1, spec2], [task]);
270
+ const issueNodes = result.nodes.filter((n) => n.type === "issue");
271
+ expect(issueNodes).toHaveLength(1);
272
+ expect(issueNodes[0].id).toBe("issue-123");
273
+ const tracksEdges = result.edges.filter((e) => e.type === "tracks");
274
+ expect(tracksEdges).toHaveLength(2);
275
+ expect(tracksEdges.map((e) => e.target)).toEqual(
276
+ expect.arrayContaining(["spec-000001", "spec-000002"]),
277
+ );
278
+ });
279
+ });
280
+
281
+ describe("node positioning", () => {
282
+ it("positions requirement nodes at x=0", () => {
283
+ const req1 = makeReq("req-000001");
284
+ const req2 = makeReq("req-000002");
285
+ const result = buildMultiLevelGraphData([req1, req2], [], []);
286
+ expect(result.nodes[0].position.x).toBe(0);
287
+ expect(result.nodes[1].position.x).toBe(0);
288
+ });
289
+
290
+ it("positions specification nodes at x=400", () => {
291
+ const req1 = makeReq("req-000001");
292
+ const spec1 = makeSpec("spec-000001", "req-000001");
293
+ const spec2 = makeSpec("spec-000002", "req-000001");
294
+ const result = buildMultiLevelGraphData([req1], [spec1, spec2], []);
295
+ const specNodes = result.nodes.filter((n) => n.type === "specification");
296
+ expect(specNodes[0].position.x).toBe(400);
297
+ expect(specNodes[1].position.x).toBe(400);
298
+ });
299
+
300
+ it("positions issue nodes at x=800", () => {
301
+ const req1 = makeReq("req-000001");
302
+ const spec1 = makeSpec("spec-000001", "req-000001");
303
+ const task = makeTask(123, ["spec-000001"]);
304
+ const result = buildMultiLevelGraphData([req1], [spec1], [task]);
305
+ const issueNode = result.nodes.find((n) => n.type === "issue");
306
+ expect(issueNode?.position.x).toBe(800);
307
+ });
308
+ });
309
+ });
@@ -0,0 +1,74 @@
1
+ import { describe, it, expect, vi, beforeEach } from "vitest";
2
+
3
+ vi.mock("../../lib/file-system", () => ({
4
+ readYAML: vi.fn(),
5
+ joinPath: vi.fn((...parts: string[]) => parts.join("/")),
6
+ }));
7
+
8
+ vi.mock("../../lib/reqord-root", () => ({
9
+ getIssuesDir: vi.fn(() => "/test/.reqord/issues"),
10
+ }));
11
+
12
+ import { LocalFeedbackRepository } from "../../lib/local-feedback-repository";
13
+ import { readYAML } from "../../lib/file-system";
14
+
15
+ const mockReadYAML = vi.mocked(readYAML);
16
+
17
+ describe("LocalFeedbackRepository", () => {
18
+ let repo: LocalFeedbackRepository;
19
+
20
+ beforeEach(() => {
21
+ vi.clearAllMocks();
22
+ repo = new LocalFeedbackRepository();
23
+ });
24
+
25
+ it("feedbacks.yaml存在時にFeedbackEntry配列を返す", async () => {
26
+ mockReadYAML.mockResolvedValue({
27
+ feedbacks: [
28
+ {
29
+ githubIssue: 42,
30
+ type: "bug",
31
+ severity: "high",
32
+ linkedTo: {
33
+ requirements: ["req-000001"],
34
+ createdRequirements: [],
35
+ specifications: [],
36
+ createdSpecifications: [],
37
+ },
38
+ syncedAt: "2026-01-01T00:00:00Z",
39
+ status: "open",
40
+ },
41
+ ],
42
+ });
43
+
44
+ const result = await repo.findAll();
45
+
46
+ expect(result).toHaveLength(1);
47
+ expect(result[0].githubIssue).toBe(42);
48
+ expect(result[0].type).toBe("bug");
49
+ });
50
+
51
+ it("feedbacks.yaml不在時に空配列を返す", async () => {
52
+ mockReadYAML.mockRejectedValue(new Error("ENOENT"));
53
+
54
+ const result = await repo.findAll();
55
+
56
+ expect(result).toEqual([]);
57
+ });
58
+
59
+ it("不正なYAML時に空配列を返す", async () => {
60
+ mockReadYAML.mockResolvedValue({ invalid: "data" });
61
+
62
+ const result = await repo.findAll();
63
+
64
+ expect(result).toEqual([]);
65
+ });
66
+
67
+ it("feedbacks配列が空の場合に空配列を返す", async () => {
68
+ mockReadYAML.mockResolvedValue({ feedbacks: [] });
69
+
70
+ const result = await repo.findAll();
71
+
72
+ expect(result).toEqual([]);
73
+ });
74
+ });
@@ -0,0 +1,194 @@
1
+ import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
2
+ import { mkdtemp, writeFile, mkdir, rm } from "node:fs/promises";
3
+ import { join } from "node:path";
4
+ import { tmpdir } from "node:os";
5
+ import { dump as yamlDump, JSON_SCHEMA } from "js-yaml";
6
+
7
+ function makeSpecYaml(overrides: Record<string, unknown> = {}) {
8
+ return yamlDump(
9
+ {
10
+ id: "spec-000001",
11
+ requirementId: "req-000001",
12
+ version: "1.0.0",
13
+ status: "draft",
14
+ createdAt: "2026-02-08T14:07:39.737Z",
15
+ updatedAt: "2026-02-08T14:07:54.055Z",
16
+ versionHistory: [],
17
+ files: {
18
+ design: "specifications/spec-000001/design.md",
19
+ supplementary: [],
20
+ },
21
+ ...overrides,
22
+ },
23
+ { schema: JSON_SCHEMA },
24
+ );
25
+ }
26
+
27
+ describe("LocalSpecificationRepository", () => {
28
+ let tmpDir: string;
29
+ let specDir: string;
30
+
31
+ beforeEach(async () => {
32
+ tmpDir = await mkdtemp(join(tmpdir(), "reqord-spec-test-"));
33
+ specDir = join(tmpDir, ".reqord", "specifications");
34
+ await mkdir(specDir, { recursive: true });
35
+ process.env.REQORD_ROOT = tmpDir;
36
+ vi.resetModules();
37
+ });
38
+
39
+ afterEach(async () => {
40
+ delete process.env.REQORD_ROOT;
41
+ await rm(tmpDir, { recursive: true, force: true });
42
+ });
43
+
44
+ async function getRepo() {
45
+ const { LocalSpecificationRepository } = await import(
46
+ "../../lib/local-specification-repository"
47
+ );
48
+ return new LocalSpecificationRepository();
49
+ }
50
+
51
+ describe("findAll", () => {
52
+ it("returns all valid specification files sorted by ID", async () => {
53
+ await writeFile(
54
+ join(specDir, "spec-000002.yaml"),
55
+ makeSpecYaml({ id: "spec-000002", requirementId: "req-000002" }),
56
+ );
57
+ await writeFile(
58
+ join(specDir, "spec-000001.yaml"),
59
+ makeSpecYaml({ id: "spec-000001", requirementId: "req-000001" }),
60
+ );
61
+
62
+ const repo = await getRepo();
63
+ const specs = await repo.findAll();
64
+
65
+ expect(specs).toHaveLength(2);
66
+ expect(specs[0].id).toBe("spec-000001");
67
+ expect(specs[1].id).toBe("spec-000002");
68
+ });
69
+
70
+ it("skips invalid YAML files", async () => {
71
+ await writeFile(
72
+ join(specDir, "spec-000001.yaml"),
73
+ makeSpecYaml({ id: "spec-000001" }),
74
+ );
75
+ await writeFile(join(specDir, "spec-000002.yaml"), "invalid: yaml: content:");
76
+
77
+ const repo = await getRepo();
78
+ const specs = await repo.findAll();
79
+
80
+ expect(specs).toHaveLength(1);
81
+ expect(specs[0].id).toBe("spec-000001");
82
+ });
83
+
84
+ it("skips files not matching spec-NNNNNN.yaml pattern", async () => {
85
+ await writeFile(
86
+ join(specDir, "spec-000001.yaml"),
87
+ makeSpecYaml({ id: "spec-000001" }),
88
+ );
89
+ await writeFile(join(specDir, "notes.txt"), "not a spec");
90
+
91
+ const repo = await getRepo();
92
+ const specs = await repo.findAll();
93
+
94
+ expect(specs).toHaveLength(1);
95
+ });
96
+
97
+ it("returns empty array when directory has no files", async () => {
98
+ const repo = await getRepo();
99
+ const specs = await repo.findAll();
100
+
101
+ expect(specs).toEqual([]);
102
+ });
103
+ });
104
+
105
+ describe("findById", () => {
106
+ it("returns a specification when it exists", async () => {
107
+ await writeFile(
108
+ join(specDir, "spec-000001.yaml"),
109
+ makeSpecYaml({ id: "spec-000001" }),
110
+ );
111
+
112
+ const repo = await getRepo();
113
+ const spec = await repo.findById("spec-000001");
114
+
115
+ expect(spec).not.toBeNull();
116
+ expect(spec!.id).toBe("spec-000001");
117
+ expect(spec!.requirementId).toBe("req-000001");
118
+ });
119
+
120
+ it("returns null when specification does not exist", async () => {
121
+ const repo = await getRepo();
122
+ const spec = await repo.findById("spec-999999");
123
+
124
+ expect(spec).toBeNull();
125
+ });
126
+
127
+ it("throws on invalid specification data", async () => {
128
+ await writeFile(
129
+ join(specDir, "spec-000001.yaml"),
130
+ yamlDump({ id: "spec-000001" }, { schema: JSON_SCHEMA }),
131
+ );
132
+
133
+ const repo = await getRepo();
134
+ await expect(repo.findById("spec-000001")).rejects.toThrow(
135
+ "Invalid specification",
136
+ );
137
+ });
138
+ });
139
+
140
+ describe("loadDesign", () => {
141
+ it("returns design content when design.md exists", async () => {
142
+ const designDir = join(specDir, "spec-000001");
143
+ await mkdir(designDir, { recursive: true });
144
+ await writeFile(join(designDir, "design.md"), "# Design\n\nSome content");
145
+
146
+ const repo = await getRepo();
147
+ const design = await repo.loadDesign("spec-000001");
148
+
149
+ expect(design).toBe("# Design\n\nSome content");
150
+ });
151
+
152
+ it("returns null when design.md does not exist", async () => {
153
+ const repo = await getRepo();
154
+ const design = await repo.loadDesign("spec-999999");
155
+
156
+ expect(design).toBeNull();
157
+ });
158
+ });
159
+
160
+ describe("findByRequirementId", () => {
161
+ it("returns specifications matching the requirement ID", async () => {
162
+ await writeFile(
163
+ join(specDir, "spec-000001.yaml"),
164
+ makeSpecYaml({ id: "spec-000001", requirementId: "req-000001" }),
165
+ );
166
+ await writeFile(
167
+ join(specDir, "spec-000002.yaml"),
168
+ makeSpecYaml({ id: "spec-000002", requirementId: "req-000002" }),
169
+ );
170
+ await writeFile(
171
+ join(specDir, "spec-000003.yaml"),
172
+ makeSpecYaml({ id: "spec-000003", requirementId: "req-000001" }),
173
+ );
174
+
175
+ const repo = await getRepo();
176
+ const specs = await repo.findByRequirementId("req-000001");
177
+
178
+ expect(specs).toHaveLength(2);
179
+ expect(specs.map((s) => s.id)).toEqual(["spec-000001", "spec-000003"]);
180
+ });
181
+
182
+ it("returns empty array when no specifications match", async () => {
183
+ await writeFile(
184
+ join(specDir, "spec-000001.yaml"),
185
+ makeSpecYaml({ id: "spec-000001", requirementId: "req-000001" }),
186
+ );
187
+
188
+ const repo = await getRepo();
189
+ const specs = await repo.findByRequirementId("req-999999");
190
+
191
+ expect(specs).toEqual([]);
192
+ });
193
+ });
194
+ });
@@ -0,0 +1,31 @@
1
+ import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
2
+
3
+ describe("getReqordRoot", () => {
4
+ let originalEnv: string | undefined;
5
+
6
+ beforeEach(() => {
7
+ originalEnv = process.env.REQORD_ROOT;
8
+ // Clear module cache to reset the cached root
9
+ vi.resetModules();
10
+ });
11
+
12
+ afterEach(() => {
13
+ if (originalEnv !== undefined) {
14
+ process.env.REQORD_ROOT = originalEnv;
15
+ } else {
16
+ delete process.env.REQORD_ROOT;
17
+ }
18
+ });
19
+
20
+ it("returns the REQORD_ROOT env value", async () => {
21
+ process.env.REQORD_ROOT = "/test/path";
22
+ const { getReqordRoot } = await import("../../lib/reqord-root");
23
+ expect(getReqordRoot()).toBe("/test/path");
24
+ });
25
+
26
+ it("throws when REQORD_ROOT is not set", async () => {
27
+ delete process.env.REQORD_ROOT;
28
+ const { getReqordRoot } = await import("../../lib/reqord-root");
29
+ expect(() => getReqordRoot()).toThrow("REQORD_ROOT");
30
+ });
31
+ });
@@ -0,0 +1,63 @@
1
+ import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
2
+ import { mkdtemp, writeFile, mkdir, rm } from "node:fs/promises";
3
+ import { join } from "node:path";
4
+ import { tmpdir } from "node:os";
5
+
6
+ describe("loadSpecFile", () => {
7
+ let tmpDir: string;
8
+ let specDir: string;
9
+
10
+ beforeEach(async () => {
11
+ tmpDir = await mkdtemp(join(tmpdir(), "reqord-spec-file-test-"));
12
+ specDir = join(tmpDir, ".reqord", "specifications");
13
+ await mkdir(specDir, { recursive: true });
14
+ process.env.REQORD_ROOT = tmpDir;
15
+ vi.resetModules();
16
+ });
17
+
18
+ afterEach(async () => {
19
+ delete process.env.REQORD_ROOT;
20
+ await rm(tmpDir, { recursive: true, force: true });
21
+ });
22
+
23
+ async function getLoadSpecFile() {
24
+ const { loadSpecFile } = await import("../../lib/specification-file");
25
+ return loadSpecFile;
26
+ }
27
+
28
+ it("returns file content when file exists", async () => {
29
+ const specSubDir = join(specDir, "spec-000001");
30
+ await mkdir(specSubDir, { recursive: true });
31
+ await writeFile(join(specSubDir, "design.md"), "# Design\n\nContent here");
32
+
33
+ const loadSpecFile = await getLoadSpecFile();
34
+ const content = await loadSpecFile("spec-000001", "design.md");
35
+
36
+ expect(content).toBe("# Design\n\nContent here");
37
+ });
38
+
39
+ it("returns null when file does not exist", async () => {
40
+ const loadSpecFile = await getLoadSpecFile();
41
+ const content = await loadSpecFile("spec-999999", "nonexistent.md");
42
+
43
+ expect(content).toBeNull();
44
+ });
45
+
46
+ it("returns null when spec directory does not exist", async () => {
47
+ const loadSpecFile = await getLoadSpecFile();
48
+ const content = await loadSpecFile("spec-999999", "design.md");
49
+
50
+ expect(content).toBeNull();
51
+ });
52
+
53
+ it("loads research.md correctly", async () => {
54
+ const specSubDir = join(specDir, "spec-000001");
55
+ await mkdir(specSubDir, { recursive: true });
56
+ await writeFile(join(specSubDir, "research.md"), "# Research\n\nFindings");
57
+
58
+ const loadSpecFile = await getLoadSpecFile();
59
+ const content = await loadSpecFile("spec-000001", "research.md");
60
+
61
+ expect(content).toBe("# Research\n\nFindings");
62
+ });
63
+ });