@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.
- package/LICENSE +661 -0
- package/next-env.d.ts +6 -0
- package/next.config.ts +7 -0
- package/package.json +59 -0
- package/postcss.config.mjs +7 -0
- package/src/__tests__/components/dashboard/critical-path-display.test.tsx +129 -0
- package/src/__tests__/components/dashboard/progress-bar.test.tsx +87 -0
- package/src/__tests__/components/dashboard/project-health.test.tsx +57 -0
- package/src/__tests__/components/dashboard/warning-alert.test.tsx +75 -0
- package/src/__tests__/components/feedback/feedback-client-view.test.tsx +84 -0
- package/src/__tests__/components/feedback/feedback-filters.test.tsx +51 -0
- package/src/__tests__/components/feedback/feedback-linked-items.test.tsx +131 -0
- package/src/__tests__/components/feedback/feedback-list.test.tsx +49 -0
- package/src/__tests__/components/feedback/feedback-table.test.tsx +165 -0
- package/src/__tests__/components/flags/flag-badge.test.tsx +41 -0
- package/src/__tests__/components/flags/flag-list.test.tsx +51 -0
- package/src/__tests__/components/gantt/gantt-bar.test.tsx +190 -0
- package/src/__tests__/components/gantt/gantt-chart.test.tsx +141 -0
- package/src/__tests__/components/gantt/gantt-header.test.tsx +84 -0
- package/src/__tests__/components/gantt/gantt-legend.test.tsx +52 -0
- package/src/__tests__/components/graph/dag-layout.test.ts +129 -0
- package/src/__tests__/components/graph/dependency-graph.test.tsx +94 -0
- package/src/__tests__/components/graph/drilldown-breadcrumb.test.tsx +70 -0
- package/src/__tests__/components/graph/drilldown-graph.test.tsx +108 -0
- package/src/__tests__/components/graph/edge-styles.test.ts +27 -0
- package/src/__tests__/components/graph/graph-page-client.test.tsx +124 -0
- package/src/__tests__/components/graph/issue-node.test.tsx +173 -0
- package/src/__tests__/components/graph/requirement-node.test.tsx +151 -0
- package/src/__tests__/components/graph/specification-node.test.tsx +140 -0
- package/src/__tests__/components/specification/spec-tabs.test.tsx +153 -0
- package/src/__tests__/components/specification/tab-coverage.test.tsx +70 -0
- package/src/__tests__/components/specification/tab-design.test.tsx +42 -0
- package/src/__tests__/components/specification/tab-history.test.tsx +118 -0
- package/src/__tests__/components/specification/tab-issues.test.tsx +126 -0
- package/src/__tests__/components/specification/tab-research.test.tsx +42 -0
- package/src/__tests__/lib/dashboard-data.test.ts +334 -0
- package/src/__tests__/lib/drilldown-graph-data.test.ts +267 -0
- package/src/__tests__/lib/gantt-data.test.ts +299 -0
- package/src/__tests__/lib/graph-data.test.ts +309 -0
- package/src/__tests__/lib/local-feedback-repository.test.ts +74 -0
- package/src/__tests__/lib/local-specification-repository.test.ts +194 -0
- package/src/__tests__/lib/reqord-root.test.ts +31 -0
- package/src/__tests__/lib/specification-file.test.ts +63 -0
- package/src/__tests__/lib/tasks-data.test.ts +104 -0
- package/src/app/dashboard/loading.tsx +21 -0
- package/src/app/dashboard/page.tsx +50 -0
- package/src/app/error.tsx +22 -0
- package/src/app/feedback/loading.tsx +13 -0
- package/src/app/feedback/page.tsx +48 -0
- package/src/app/globals.css +2 -0
- package/src/app/graph/page.tsx +32 -0
- package/src/app/layout.tsx +25 -0
- package/src/app/page.tsx +5 -0
- package/src/app/requirements/[id]/edit/page.tsx +40 -0
- package/src/app/requirements/[id]/loading.tsx +14 -0
- package/src/app/requirements/[id]/not-found.tsx +18 -0
- package/src/app/requirements/[id]/page.tsx +43 -0
- package/src/app/requirements/loading.tsx +13 -0
- package/src/app/requirements/new/page.tsx +14 -0
- package/src/app/requirements/page.tsx +35 -0
- package/src/app/specifications/[id]/loading.tsx +14 -0
- package/src/app/specifications/[id]/not-found.tsx +18 -0
- package/src/app/specifications/[id]/page.tsx +52 -0
- package/src/app/specifications/loading.tsx +13 -0
- package/src/app/specifications/page.tsx +42 -0
- package/src/components/dashboard/critical-path-display.tsx +76 -0
- package/src/components/dashboard/progress-bar.tsx +45 -0
- package/src/components/dashboard/progress-section.tsx +57 -0
- package/src/components/dashboard/project-health.tsx +35 -0
- package/src/components/dashboard/status-card.tsx +27 -0
- package/src/components/dashboard/status-cards.tsx +28 -0
- package/src/components/dashboard/warning-alert.tsx +33 -0
- package/src/components/dashboard/warning-alerts.tsx +24 -0
- package/src/components/feedback/feedback-badge.tsx +48 -0
- package/src/components/feedback/feedback-client-view.tsx +38 -0
- package/src/components/feedback/feedback-filters.tsx +86 -0
- package/src/components/feedback/feedback-linked-items.tsx +93 -0
- package/src/components/feedback/feedback-list.tsx +40 -0
- package/src/components/feedback/feedback-table.tsx +115 -0
- package/src/components/gantt/gantt-bar.tsx +65 -0
- package/src/components/gantt/gantt-chart.tsx +88 -0
- package/src/components/gantt/gantt-constants.ts +15 -0
- package/src/components/gantt/gantt-critical-path.tsx +38 -0
- package/src/components/gantt/gantt-group.tsx +25 -0
- package/src/components/gantt/gantt-header.tsx +47 -0
- package/src/components/gantt/gantt-legend.tsx +26 -0
- package/src/components/graph/dag-layout.ts +131 -0
- package/src/components/graph/dependency-graph.tsx +88 -0
- package/src/components/graph/drilldown-breadcrumb.tsx +35 -0
- package/src/components/graph/drilldown-graph.tsx +45 -0
- package/src/components/graph/edge-styles.ts +16 -0
- package/src/components/graph/graph-loader.tsx +25 -0
- package/src/components/graph/graph-page-client.tsx +98 -0
- package/src/components/graph/issue-node.tsx +46 -0
- package/src/components/graph/multi-level-graph.tsx +91 -0
- package/src/components/graph/requirement-node.tsx +69 -0
- package/src/components/graph/specification-node.tsx +39 -0
- package/src/components/requirement/delete-button.tsx +46 -0
- package/src/components/requirement/dependency-editor.tsx +79 -0
- package/src/components/requirement/markdown-editor.tsx +47 -0
- package/src/components/requirement/markdown-renderer.tsx +12 -0
- package/src/components/requirement/requirement-detail.tsx +228 -0
- package/src/components/requirement/requirement-form.tsx +390 -0
- package/src/components/requirement/requirement-table.tsx +203 -0
- package/src/components/requirement/requirement-tabs.tsx +65 -0
- package/src/components/requirement/success-criteria-editor.tsx +53 -0
- package/src/components/specification/spec-detail.tsx +103 -0
- package/src/components/specification/spec-tabs.tsx +66 -0
- package/src/components/specification/specification-table.tsx +193 -0
- package/src/components/specification/tab-coverage.tsx +52 -0
- package/src/components/specification/tab-design.tsx +16 -0
- package/src/components/specification/tab-history.tsx +61 -0
- package/src/components/specification/tab-issues.tsx +111 -0
- package/src/components/specification/tab-research.tsx +16 -0
- package/src/components/ui/badge.tsx +64 -0
- package/src/components/ui/nav.tsx +49 -0
- package/src/components/ui/tabs.tsx +39 -0
- package/src/lib/actions.ts +222 -0
- package/src/lib/dashboard-data.ts +224 -0
- package/src/lib/data.ts +21 -0
- package/src/lib/drilldown-graph-data.ts +98 -0
- package/src/lib/feedback-data.ts +33 -0
- package/src/lib/feedback-repository.ts +6 -0
- package/src/lib/file-system.ts +167 -0
- package/src/lib/gantt-data.ts +168 -0
- package/src/lib/get-repository.ts +43 -0
- package/src/lib/graph-data.ts +161 -0
- package/src/lib/id-generator.ts +23 -0
- package/src/lib/local-feedback-repository.ts +36 -0
- package/src/lib/local-repository.ts +78 -0
- package/src/lib/local-specification-repository.ts +61 -0
- package/src/lib/repository.ts +11 -0
- package/src/lib/reqord-root.ts +33 -0
- package/src/lib/specification-data.ts +28 -0
- package/src/lib/specification-file.ts +12 -0
- package/src/lib/specification-repository.ts +8 -0
- package/src/lib/tasks-data.ts +32 -0
- package/tsconfig.json +27 -0
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import type { Requirement, Specification, TaskEntry } from "@reqord/shared";
|
|
2
|
+
import { getAllRequirements } from "./data";
|
|
3
|
+
import { getAllSpecifications } from "./specification-data";
|
|
4
|
+
import { loadTasksYaml } from "./tasks-data";
|
|
5
|
+
import { getAllFeedbacks } from "./feedback-data";
|
|
6
|
+
|
|
7
|
+
export type Warning = {
|
|
8
|
+
type:
|
|
9
|
+
| "missing_specification"
|
|
10
|
+
| "unapproved_dependency"
|
|
11
|
+
| "design_verification_error";
|
|
12
|
+
message: string;
|
|
13
|
+
severity: "error" | "warning" | "info";
|
|
14
|
+
relatedId: string;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export type CriticalPathItem = {
|
|
18
|
+
issueNumber: number;
|
|
19
|
+
title: string;
|
|
20
|
+
url: string;
|
|
21
|
+
priority: string;
|
|
22
|
+
status: string;
|
|
23
|
+
estimatedHours: number;
|
|
24
|
+
specId: string;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export type StatusBreakdown = Record<string, number>;
|
|
28
|
+
|
|
29
|
+
export type CategorySummary = {
|
|
30
|
+
total: number;
|
|
31
|
+
breakdown: StatusBreakdown;
|
|
32
|
+
approvalRate: number;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export type IssueSummary = {
|
|
36
|
+
total: number;
|
|
37
|
+
completed: number;
|
|
38
|
+
completionRate: number;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export type DashboardData = {
|
|
42
|
+
requirements: CategorySummary;
|
|
43
|
+
specifications: CategorySummary;
|
|
44
|
+
issues: IssueSummary;
|
|
45
|
+
healthScore: number;
|
|
46
|
+
warnings: Warning[];
|
|
47
|
+
criticalPath: CriticalPathItem[] | null;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
// Health score weights
|
|
51
|
+
const HEALTH_WEIGHTS = {
|
|
52
|
+
requirements: 40,
|
|
53
|
+
specifications: 30,
|
|
54
|
+
issues: 30,
|
|
55
|
+
} as const;
|
|
56
|
+
|
|
57
|
+
function isApproved(status: string): boolean {
|
|
58
|
+
return status === "approved" || status === "implemented";
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function groupByStatus(
|
|
62
|
+
items: Array<{ status: string }>
|
|
63
|
+
): Record<string, number> {
|
|
64
|
+
const result: Record<string, number> = {};
|
|
65
|
+
|
|
66
|
+
for (const item of items) {
|
|
67
|
+
result[item.status] = (result[item.status] || 0) + 1;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return result;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function calculateApprovalRate(items: Array<{ status: string }>): number {
|
|
74
|
+
if (items.length === 0) {
|
|
75
|
+
return 0;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const approvedCount = items.filter((item) => isApproved(item.status)).length;
|
|
79
|
+
|
|
80
|
+
return approvedCount / items.length;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function calculateIssueSummary(tasks: TaskEntry[]): IssueSummary {
|
|
84
|
+
const totalIssues = tasks.length;
|
|
85
|
+
const completedIssues = tasks.filter((t) => t.status === "closed").length;
|
|
86
|
+
|
|
87
|
+
return {
|
|
88
|
+
total: totalIssues,
|
|
89
|
+
completed: completedIssues,
|
|
90
|
+
completionRate: totalIssues > 0 ? completedIssues / totalIssues : 0,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export async function detectWarnings(
|
|
95
|
+
requirements: Requirement[],
|
|
96
|
+
specifications: Specification[]
|
|
97
|
+
): Promise<Warning[]> {
|
|
98
|
+
const warnings: Warning[] = [];
|
|
99
|
+
const allFeedbacks = await getAllFeedbacks();
|
|
100
|
+
|
|
101
|
+
const specsByReqId = new Map<string, Specification[]>();
|
|
102
|
+
for (const spec of specifications) {
|
|
103
|
+
const specs = specsByReqId.get(spec.requirementId) || [];
|
|
104
|
+
specs.push(spec);
|
|
105
|
+
specsByReqId.set(spec.requirementId, specs);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const reqMap = new Map<string, Requirement>();
|
|
109
|
+
for (const req of requirements) {
|
|
110
|
+
reqMap.set(req.id, req);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
for (const req of requirements) {
|
|
114
|
+
if (req.status !== "draft") {
|
|
115
|
+
const specs = specsByReqId.get(req.id);
|
|
116
|
+
if (!specs || specs.length === 0) {
|
|
117
|
+
warnings.push({
|
|
118
|
+
type: "missing_specification",
|
|
119
|
+
message: `Requirement ${req.id} has no specification`,
|
|
120
|
+
severity: "warning",
|
|
121
|
+
relatedId: req.id,
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
for (const req of requirements) {
|
|
128
|
+
for (const depId of req.dependencies.blockedBy) {
|
|
129
|
+
const dep = reqMap.get(depId);
|
|
130
|
+
if (dep && !isApproved(dep.status)) {
|
|
131
|
+
warnings.push({
|
|
132
|
+
type: "unapproved_dependency",
|
|
133
|
+
message: `Requirement ${req.id} is blocked by unapproved requirement ${depId}`,
|
|
134
|
+
severity: "warning",
|
|
135
|
+
relatedId: req.id,
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
for (const spec of specifications) {
|
|
142
|
+
const unresolvedFeedbacks = allFeedbacks.filter((f) => {
|
|
143
|
+
const linked = f.linkedTo.specifications.includes(spec.id)
|
|
144
|
+
|| (f.linkedTo.createdSpecifications ?? []).includes(spec.id);
|
|
145
|
+
const resolved = f.linkedTo.resolved?.specifications?.includes(spec.id) ?? false;
|
|
146
|
+
return linked && !resolved;
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
const hasCriticalFeedback = unresolvedFeedbacks.some(
|
|
150
|
+
(f) => f.severity === "critical" || f.severity === "high"
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
if (hasCriticalFeedback) {
|
|
154
|
+
warnings.push({
|
|
155
|
+
type: "design_verification_error",
|
|
156
|
+
message: `Specification ${spec.id} has critical/high unresolved feedback requiring attention`,
|
|
157
|
+
severity: "error",
|
|
158
|
+
relatedId: spec.id,
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return warnings;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export function extractCriticalPath(
|
|
167
|
+
tasks: TaskEntry[]
|
|
168
|
+
): CriticalPathItem[] | null {
|
|
169
|
+
if (tasks.length === 0) {
|
|
170
|
+
return null;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const items: CriticalPathItem[] = tasks.map((task) => ({
|
|
174
|
+
issueNumber: task.number,
|
|
175
|
+
title: task.title,
|
|
176
|
+
url: task.url,
|
|
177
|
+
priority: task.priority ?? "",
|
|
178
|
+
status: task.status,
|
|
179
|
+
estimatedHours: task.estimatedHours ?? 0,
|
|
180
|
+
specId: task.linkedTo.specifications[0] ?? "",
|
|
181
|
+
}));
|
|
182
|
+
|
|
183
|
+
return items;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export async function getDashboardData(): Promise<DashboardData> {
|
|
187
|
+
const requirements = await getAllRequirements();
|
|
188
|
+
const specifications = await getAllSpecifications();
|
|
189
|
+
const tasksIndex = await loadTasksYaml();
|
|
190
|
+
const allTasks = tasksIndex.tasks;
|
|
191
|
+
|
|
192
|
+
const requirementsBreakdown = groupByStatus(requirements);
|
|
193
|
+
const requirementsApprovalRate = calculateApprovalRate(requirements);
|
|
194
|
+
|
|
195
|
+
const specificationsBreakdown = groupByStatus(specifications);
|
|
196
|
+
const specificationsApprovalRate = calculateApprovalRate(specifications);
|
|
197
|
+
|
|
198
|
+
const issues = calculateIssueSummary(allTasks);
|
|
199
|
+
|
|
200
|
+
const healthScore =
|
|
201
|
+
requirementsApprovalRate * HEALTH_WEIGHTS.requirements +
|
|
202
|
+
specificationsApprovalRate * HEALTH_WEIGHTS.specifications +
|
|
203
|
+
issues.completionRate * HEALTH_WEIGHTS.issues;
|
|
204
|
+
|
|
205
|
+
const warnings = await detectWarnings(requirements, specifications);
|
|
206
|
+
const criticalPath = extractCriticalPath(allTasks);
|
|
207
|
+
|
|
208
|
+
return {
|
|
209
|
+
requirements: {
|
|
210
|
+
total: requirements.length,
|
|
211
|
+
breakdown: requirementsBreakdown,
|
|
212
|
+
approvalRate: requirementsApprovalRate,
|
|
213
|
+
},
|
|
214
|
+
specifications: {
|
|
215
|
+
total: specifications.length,
|
|
216
|
+
breakdown: specificationsBreakdown,
|
|
217
|
+
approvalRate: specificationsApprovalRate,
|
|
218
|
+
},
|
|
219
|
+
issues,
|
|
220
|
+
healthScore,
|
|
221
|
+
warnings,
|
|
222
|
+
criticalPath,
|
|
223
|
+
};
|
|
224
|
+
}
|
package/src/lib/data.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { Requirement } from "@reqord/shared";
|
|
2
|
+
import { getRepository } from "./get-repository";
|
|
3
|
+
|
|
4
|
+
export async function getAllRequirements(): Promise<Requirement[]> {
|
|
5
|
+
const repo = getRepository();
|
|
6
|
+
return repo.findAll();
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export async function getRequirementById(
|
|
10
|
+
id: string,
|
|
11
|
+
): Promise<Requirement | null> {
|
|
12
|
+
const repo = getRepository();
|
|
13
|
+
return repo.findById(id);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export async function getRequirementDescription(
|
|
17
|
+
id: string,
|
|
18
|
+
): Promise<string | null> {
|
|
19
|
+
const repo = getRepository();
|
|
20
|
+
return repo.loadDescription(id);
|
|
21
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import type { Node, Edge } from "@xyflow/react";
|
|
2
|
+
import type { Requirement, Specification, TaskEntry } from "@reqord/shared";
|
|
3
|
+
import { EDGE_STYLES } from "@/components/graph/edge-styles";
|
|
4
|
+
|
|
5
|
+
export interface DrillDownGraphData {
|
|
6
|
+
nodes: Node[];
|
|
7
|
+
edges: Edge[];
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const LAYOUT = {
|
|
11
|
+
REQ_X: 0,
|
|
12
|
+
SPEC_X: 400,
|
|
13
|
+
ISSUE_X: 800,
|
|
14
|
+
VERTICAL_GAP: 120,
|
|
15
|
+
ISSUE_VERTICAL_GAP: 80,
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export function buildDrillDownGraphData(
|
|
19
|
+
requirement: Requirement,
|
|
20
|
+
specifications: Specification[],
|
|
21
|
+
tasks: TaskEntry[],
|
|
22
|
+
): DrillDownGraphData {
|
|
23
|
+
const nodes: Node[] = [];
|
|
24
|
+
const edges: Edge[] = [];
|
|
25
|
+
|
|
26
|
+
// 1. Requirement node (left)
|
|
27
|
+
nodes.push({
|
|
28
|
+
id: requirement.id,
|
|
29
|
+
type: "requirement",
|
|
30
|
+
position: { x: LAYOUT.REQ_X, y: 0 },
|
|
31
|
+
data: {
|
|
32
|
+
label: requirement.title,
|
|
33
|
+
status: requirement.status,
|
|
34
|
+
priority: requirement.priority,
|
|
35
|
+
specCount: specifications.length,
|
|
36
|
+
},
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
// 2. Specification nodes (middle column) + implements edges
|
|
40
|
+
const addedIssueIds = new Set<string>();
|
|
41
|
+
specifications.forEach((spec, i) => {
|
|
42
|
+
const specNodeId = spec.id;
|
|
43
|
+
nodes.push({
|
|
44
|
+
id: specNodeId,
|
|
45
|
+
type: "specification",
|
|
46
|
+
position: { x: LAYOUT.SPEC_X, y: i * LAYOUT.VERTICAL_GAP },
|
|
47
|
+
data: {
|
|
48
|
+
label: spec.id,
|
|
49
|
+
status: spec.status,
|
|
50
|
+
},
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
edges.push({
|
|
54
|
+
id: `impl-${requirement.id}-${specNodeId}`,
|
|
55
|
+
source: requirement.id,
|
|
56
|
+
target: specNodeId,
|
|
57
|
+
style: EDGE_STYLES.implements,
|
|
58
|
+
animated: false,
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
// 3. Issue nodes (right column) from tasks.yaml linked to this spec
|
|
62
|
+
const specTasks = tasks.filter((t) =>
|
|
63
|
+
t.linkedTo.specifications.includes(spec.id),
|
|
64
|
+
);
|
|
65
|
+
let issueOffset = 0;
|
|
66
|
+
specTasks.forEach((task) => {
|
|
67
|
+
const issueNodeId = `issue-${task.number}`;
|
|
68
|
+
if (!addedIssueIds.has(issueNodeId)) {
|
|
69
|
+
addedIssueIds.add(issueNodeId);
|
|
70
|
+
nodes.push({
|
|
71
|
+
id: issueNodeId,
|
|
72
|
+
type: "issue",
|
|
73
|
+
position: {
|
|
74
|
+
x: LAYOUT.ISSUE_X,
|
|
75
|
+
y: i * LAYOUT.VERTICAL_GAP + issueOffset * LAYOUT.ISSUE_VERTICAL_GAP,
|
|
76
|
+
},
|
|
77
|
+
data: {
|
|
78
|
+
label: task.title,
|
|
79
|
+
status: task.status,
|
|
80
|
+
issueNumber: task.number,
|
|
81
|
+
issueUrl: task.url,
|
|
82
|
+
},
|
|
83
|
+
});
|
|
84
|
+
issueOffset++;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
edges.push({
|
|
88
|
+
id: `track-${specNodeId}-${issueNodeId}`,
|
|
89
|
+
source: specNodeId,
|
|
90
|
+
target: issueNodeId,
|
|
91
|
+
style: EDGE_STYLES.tracks,
|
|
92
|
+
animated: false,
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
return { nodes, edges };
|
|
98
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { FeedbackEntry } from "@reqord/shared";
|
|
2
|
+
import type { FeedbackRepository } from "./feedback-repository";
|
|
3
|
+
import { LocalFeedbackRepository } from "./local-feedback-repository";
|
|
4
|
+
|
|
5
|
+
let instance: FeedbackRepository | null = null;
|
|
6
|
+
|
|
7
|
+
function getFeedbackRepository(): FeedbackRepository {
|
|
8
|
+
if (instance) {
|
|
9
|
+
return instance;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const dataSource = process.env.REQORD_DATA_SOURCE ?? "local";
|
|
13
|
+
|
|
14
|
+
switch (dataSource) {
|
|
15
|
+
case "local":
|
|
16
|
+
instance = new LocalFeedbackRepository();
|
|
17
|
+
break;
|
|
18
|
+
default:
|
|
19
|
+
throw new Error(`Unknown REQORD_DATA_SOURCE: ${dataSource}`);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return instance;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export async function getAllFeedbacks(): Promise<FeedbackEntry[]> {
|
|
26
|
+
const repo = getFeedbackRepository();
|
|
27
|
+
return repo.findAll();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export async function findUnresolvedByArtifactId(artifactId: string): Promise<FeedbackEntry[]> {
|
|
31
|
+
const repo = getFeedbackRepository();
|
|
32
|
+
return repo.findUnresolvedByArtifactId(artifactId);
|
|
33
|
+
}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import {
|
|
2
|
+
readFile,
|
|
3
|
+
writeFile,
|
|
4
|
+
mkdir,
|
|
5
|
+
readdir,
|
|
6
|
+
access,
|
|
7
|
+
rm,
|
|
8
|
+
} from "node:fs/promises";
|
|
9
|
+
import { join } from "node:path";
|
|
10
|
+
import { load as yamlLoad, dump as yamlDump, JSON_SCHEMA } from "js-yaml";
|
|
11
|
+
|
|
12
|
+
export async function exists(path: string): Promise<boolean> {
|
|
13
|
+
try {
|
|
14
|
+
await access(path);
|
|
15
|
+
return true;
|
|
16
|
+
} catch {
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export async function readText(path: string): Promise<string> {
|
|
22
|
+
return readFile(path, "utf-8");
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export async function writeText(
|
|
26
|
+
path: string,
|
|
27
|
+
content: string,
|
|
28
|
+
): Promise<void> {
|
|
29
|
+
await writeFile(path, content, "utf-8");
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export async function mkdirp(path: string): Promise<void> {
|
|
33
|
+
await mkdir(path, { recursive: true });
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export async function readdirFiles(
|
|
37
|
+
dirPath: string,
|
|
38
|
+
filter?: (name: string) => boolean,
|
|
39
|
+
): Promise<string[]> {
|
|
40
|
+
try {
|
|
41
|
+
const entries = await readdir(dirPath);
|
|
42
|
+
return filter ? entries.filter(filter) : entries;
|
|
43
|
+
} catch {
|
|
44
|
+
return [];
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export async function remove(path: string): Promise<void> {
|
|
49
|
+
await rm(path, { recursive: true, force: true });
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function joinPath(...segments: string[]): string {
|
|
53
|
+
return join(...segments);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function fixUnquotedHash(content: string, filePath: string): string {
|
|
57
|
+
const lines = content.split("\n");
|
|
58
|
+
const fixedLines: string[] = [];
|
|
59
|
+
let modified = false;
|
|
60
|
+
let inBlockScalar = false;
|
|
61
|
+
let blockIndent = -1;
|
|
62
|
+
|
|
63
|
+
for (const line of lines) {
|
|
64
|
+
if (line.trim() === "") {
|
|
65
|
+
fixedLines.push(line);
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (/^\s*#/.test(line)) {
|
|
70
|
+
fixedLines.push(line);
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (/:\s+[>|][-+]?\s*$/.test(line)) {
|
|
75
|
+
inBlockScalar = true;
|
|
76
|
+
blockIndent = -1;
|
|
77
|
+
fixedLines.push(line);
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (inBlockScalar) {
|
|
82
|
+
const currentIndent = line.length - line.trimStart().length;
|
|
83
|
+
if (blockIndent === -1) {
|
|
84
|
+
blockIndent = currentIndent;
|
|
85
|
+
fixedLines.push(line);
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
if (currentIndent >= blockIndent && blockIndent > 0) {
|
|
89
|
+
fixedLines.push(line);
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
inBlockScalar = false;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (!/ #\d/.test(line)) {
|
|
96
|
+
fixedLines.push(line);
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const kvMatch = line.match(/^(\s*[\w-]+:\s+)('.*'|".*")\s*$/);
|
|
101
|
+
if (kvMatch) {
|
|
102
|
+
fixedLines.push(line);
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const listMatch = line.match(/^(\s*-\s+)('.*'|".*")\s*$/);
|
|
107
|
+
if (listMatch) {
|
|
108
|
+
fixedLines.push(line);
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const kvUnquoted = line.match(/^(\s*[\w-]+:\s+)(.+)$/);
|
|
113
|
+
if (kvUnquoted) {
|
|
114
|
+
const [, prefix, value] = kvUnquoted;
|
|
115
|
+
if (/ #\d/.test(value) && !(/^'.*'$/.test(value) || /^".*"$/.test(value))) {
|
|
116
|
+
const escaped = value.replace(/'/g, "''");
|
|
117
|
+
fixedLines.push(`${prefix}'${escaped}'`);
|
|
118
|
+
modified = true;
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const listUnquoted = line.match(/^(\s*-\s+)(.+)$/);
|
|
124
|
+
if (listUnquoted) {
|
|
125
|
+
const [, prefix, value] = listUnquoted;
|
|
126
|
+
if (/ #\d/.test(value) && !(/^'.*'$/.test(value) || /^".*"$/.test(value))) {
|
|
127
|
+
const escaped = value.replace(/'/g, "''");
|
|
128
|
+
fixedLines.push(`${prefix}'${escaped}'`);
|
|
129
|
+
modified = true;
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
fixedLines.push(line);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (modified) {
|
|
138
|
+
console.warn(
|
|
139
|
+
`[reqord] Warning: Auto-fixed unquoted ' #' in YAML plain scalar (${filePath}). Data would be silently truncated by js-yaml.`,
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return fixedLines.join("\n");
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export async function readYAML<T>(filePath: string): Promise<T> {
|
|
147
|
+
const content = await readFile(filePath, "utf-8");
|
|
148
|
+
const fixed = fixUnquotedHash(content, filePath);
|
|
149
|
+
try {
|
|
150
|
+
return yamlLoad(fixed, { schema: JSON_SCHEMA }) as T;
|
|
151
|
+
} catch (error) {
|
|
152
|
+
throw new Error(
|
|
153
|
+
`YAML構文エラー (${filePath}): ${error instanceof Error ? error.message : String(error)}`,
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export async function writeYAML(filePath: string, data: unknown): Promise<void> {
|
|
159
|
+
const yamlContent = yamlDump(data, {
|
|
160
|
+
indent: 2,
|
|
161
|
+
lineWidth: 120,
|
|
162
|
+
noRefs: true,
|
|
163
|
+
sortKeys: false,
|
|
164
|
+
schema: JSON_SCHEMA,
|
|
165
|
+
});
|
|
166
|
+
await writeFile(filePath, yamlContent, "utf-8");
|
|
167
|
+
}
|