create-project-arch 1.1.0 → 1.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.
- package/CHANGELOG.md +43 -0
- package/LICENSE +21 -0
- package/README.md +536 -43
- package/package.json +27 -3
- package/templates/arch-ui/.arch/edges/decision_to_domain.json +0 -1
- package/templates/arch-ui/.arch/edges/milestone_to_task.json +0 -1
- package/templates/arch-ui/.arch/edges/task_to_decision.json +0 -1
- package/templates/arch-ui/.arch/edges/task_to_module.json +0 -1
- package/templates/arch-ui/.arch/graph.json +0 -1
- package/templates/arch-ui/.arch/nodes/decisions.json +0 -1
- package/templates/arch-ui/.arch/nodes/domains.json +0 -1
- package/templates/arch-ui/.arch/nodes/milestones.json +0 -1
- package/templates/arch-ui/.arch/nodes/modules.json +0 -1
- package/templates/arch-ui/.arch/nodes/tasks.json +0 -1
- package/templates/arch-ui/app/api/health/route.ts +5 -4
- package/templates/arch-ui/app/api/node-files/route.ts +6 -1
- package/templates/arch-ui/app/api/search/route.ts +0 -1
- package/templates/arch-ui/app/work/page.tsx +94 -64
- package/templates/arch-ui/components/app-shell.tsx +1 -7
- package/templates/arch-ui/components/graph/arch-node.tsx +13 -3
- package/templates/arch-ui/components/graph/build-graph-from-dataset.ts +6 -2
- package/templates/arch-ui/components/graph/build-initial-graph.ts +215 -221
- package/templates/arch-ui/components/graph/graph-types.ts +49 -49
- package/templates/arch-ui/components/graph/use-auto-layout.ts +51 -51
- package/templates/arch-ui/components/graph/use-connection-validation.ts +48 -48
- package/templates/arch-ui/components/graph/use-flow-persistence.ts +38 -38
- package/templates/arch-ui/components/graph-canvas.tsx +90 -74
- package/templates/arch-ui/components/inspector.tsx +56 -22
- package/templates/arch-ui/components/sidebar.tsx +18 -8
- package/templates/arch-ui/components/topbar.tsx +8 -11
- package/templates/arch-ui/components/work-table.tsx +1 -5
- package/templates/arch-ui/components/workspace-context.tsx +2 -1
- package/templates/arch-ui/lib/graph-dataset.ts +4 -8
- package/templates/arch-ui/lib/graph-schema.ts +1 -4
- package/templates/arch-ui/package.json +0 -1
- package/templates/arch-ui/tsconfig.json +3 -11
|
@@ -9,12 +9,13 @@ export const runtime = "nodejs";
|
|
|
9
9
|
export async function GET() {
|
|
10
10
|
const root = getProjectRoot();
|
|
11
11
|
process.env.PROJECT_ROOT = root;
|
|
12
|
-
const [checkResult, architectureMap] = await Promise.all([
|
|
12
|
+
const [checkResult, architectureMap] = await Promise.all([
|
|
13
|
+
check.checkRun(),
|
|
14
|
+
readArchitectureMap(root),
|
|
15
|
+
]);
|
|
13
16
|
const { validation } = await buildValidatedGraphDataset(root, architectureMap);
|
|
14
17
|
|
|
15
|
-
const graphErrors = validation.errors.map(
|
|
16
|
-
(issue) => `[graph:${issue.ruleId}] ${issue.message}`,
|
|
17
|
-
);
|
|
18
|
+
const graphErrors = validation.errors.map((issue) => `[graph:${issue.ruleId}] ${issue.message}`);
|
|
18
19
|
const graphWarnings = validation.warnings.map(
|
|
19
20
|
(issue) => `[graph:${issue.ruleId}] ${issue.message}`,
|
|
20
21
|
);
|
|
@@ -132,7 +132,12 @@ async function resolveFilesForNode(root: string, type: NodeType, id: string): Pr
|
|
|
132
132
|
const [phase] = id.split("/");
|
|
133
133
|
if (!phase || !isSafeSegment(phase)) return [];
|
|
134
134
|
const phaseDir = path.join(root, "roadmap", "phases", phase);
|
|
135
|
-
return collectFiles(
|
|
135
|
+
return collectFiles(
|
|
136
|
+
phaseDir,
|
|
137
|
+
root,
|
|
138
|
+
new Set(["milestones", "tasks", "node_modules", ".git"]),
|
|
139
|
+
2,
|
|
140
|
+
);
|
|
136
141
|
}
|
|
137
142
|
|
|
138
143
|
const [phase, milestone] = id.split("/");
|
|
@@ -44,14 +44,18 @@ export default function WorkPage() {
|
|
|
44
44
|
setPhases(phaseResult.value);
|
|
45
45
|
} else {
|
|
46
46
|
setPhases([]);
|
|
47
|
-
errors.push(
|
|
47
|
+
errors.push(
|
|
48
|
+
`Phases unavailable: ${phaseResult.reason instanceof Error ? phaseResult.reason.message : "Unknown error"}`,
|
|
49
|
+
);
|
|
48
50
|
}
|
|
49
51
|
|
|
50
52
|
if (taskResult.status === "fulfilled") {
|
|
51
53
|
setTasks(taskResult.value.tasks);
|
|
52
54
|
} else {
|
|
53
55
|
setTasks([]);
|
|
54
|
-
errors.push(
|
|
56
|
+
errors.push(
|
|
57
|
+
`Tasks unavailable: ${taskResult.reason instanceof Error ? taskResult.reason.message : "Unknown error"}`,
|
|
58
|
+
);
|
|
55
59
|
}
|
|
56
60
|
|
|
57
61
|
if (graphResult.status === "fulfilled") {
|
|
@@ -59,7 +63,9 @@ export default function WorkPage() {
|
|
|
59
63
|
setGraphValidation(graphResult.value.validation);
|
|
60
64
|
if (!graphResult.value.validation.valid) {
|
|
61
65
|
errors.push(
|
|
62
|
-
...graphResult.value.validation.errors.map(
|
|
66
|
+
...graphResult.value.validation.errors.map(
|
|
67
|
+
(issue) => `Graph schema error: ${issue.message}`,
|
|
68
|
+
),
|
|
63
69
|
);
|
|
64
70
|
}
|
|
65
71
|
} else {
|
|
@@ -81,7 +87,8 @@ export default function WorkPage() {
|
|
|
81
87
|
}, []);
|
|
82
88
|
|
|
83
89
|
const viewParam = searchParams.get("view");
|
|
84
|
-
const view =
|
|
90
|
+
const view =
|
|
91
|
+
viewParam === "project" ? "project" : viewParam === "architecture" ? "architecture" : "tasks";
|
|
85
92
|
|
|
86
93
|
const milestones = useMemo(() => {
|
|
87
94
|
const milestoneSet = new Set<string>();
|
|
@@ -115,9 +122,11 @@ export default function WorkPage() {
|
|
|
115
122
|
);
|
|
116
123
|
const activeAuthorityFilters = useMemo(
|
|
117
124
|
() =>
|
|
118
|
-
(
|
|
119
|
-
|
|
120
|
-
|
|
125
|
+
(
|
|
126
|
+
Object.entries(filters.authorityTypes) as Array<
|
|
127
|
+
[keyof typeof filters.authorityTypes, boolean]
|
|
128
|
+
>
|
|
129
|
+
)
|
|
121
130
|
.filter(([, enabled]) => enabled)
|
|
122
131
|
.map(([filter]) => filter),
|
|
123
132
|
[filters.authorityTypes],
|
|
@@ -140,19 +149,27 @@ export default function WorkPage() {
|
|
|
140
149
|
<Tabs>
|
|
141
150
|
<TabsList>
|
|
142
151
|
<TabsTrigger
|
|
143
|
-
className={
|
|
152
|
+
className={
|
|
153
|
+
view === "architecture"
|
|
154
|
+
? "border border-blue-700 bg-slate-800 text-slate-100"
|
|
155
|
+
: ""
|
|
156
|
+
}
|
|
144
157
|
onClick={() => router.push("/work?view=architecture")}
|
|
145
158
|
>
|
|
146
159
|
Architecture
|
|
147
160
|
</TabsTrigger>
|
|
148
161
|
<TabsTrigger
|
|
149
|
-
className={
|
|
162
|
+
className={
|
|
163
|
+
view === "tasks" ? "border border-blue-700 bg-slate-800 text-slate-100" : ""
|
|
164
|
+
}
|
|
150
165
|
onClick={() => router.push("/work?view=tasks")}
|
|
151
166
|
>
|
|
152
167
|
Tasks
|
|
153
168
|
</TabsTrigger>
|
|
154
169
|
<TabsTrigger
|
|
155
|
-
className={
|
|
170
|
+
className={
|
|
171
|
+
view === "project" ? "border border-blue-700 bg-slate-800 text-slate-100" : ""
|
|
172
|
+
}
|
|
156
173
|
onClick={() => router.push("/work?view=project")}
|
|
157
174
|
>
|
|
158
175
|
Project
|
|
@@ -166,13 +183,17 @@ export default function WorkPage() {
|
|
|
166
183
|
<div className="rounded-xl border border-red-700 bg-red-950/60 p-3 text-sm text-red-200">
|
|
167
184
|
<p className="font-medium">Graph activation blocked by schema validation errors.</p>
|
|
168
185
|
{graphValidation.errors.map((issue, index) => (
|
|
169
|
-
<p key={`${issue.ruleId}:${index}`}>
|
|
186
|
+
<p key={`${issue.ruleId}:${index}`}>
|
|
187
|
+
[{issue.ruleId}] {issue.message}
|
|
188
|
+
</p>
|
|
170
189
|
))}
|
|
171
190
|
</div>
|
|
172
191
|
) : graphData ? (
|
|
173
192
|
<GraphCanvas
|
|
174
193
|
data={graphData}
|
|
175
|
-
viewMode={
|
|
194
|
+
viewMode={
|
|
195
|
+
view === "project" ? "project" : view === "architecture" ? "architecture-map" : "tasks"
|
|
196
|
+
}
|
|
176
197
|
enabledFilters={activeFilters}
|
|
177
198
|
enabledEdgeFilters={activeEdgeFilters}
|
|
178
199
|
enabledAuthorityFilters={activeAuthorityFilters}
|
|
@@ -182,7 +203,14 @@ export default function WorkPage() {
|
|
|
182
203
|
hideCompletedTasks={filters.hideCompletedTasks}
|
|
183
204
|
onNodeSelect={(node) =>
|
|
184
205
|
setSelection({
|
|
185
|
-
type: node.type as
|
|
206
|
+
type: node.type as
|
|
207
|
+
| "domain"
|
|
208
|
+
| "decision"
|
|
209
|
+
| "phase"
|
|
210
|
+
| "milestone"
|
|
211
|
+
| "task"
|
|
212
|
+
| "file"
|
|
213
|
+
| "health",
|
|
186
214
|
title: node.title,
|
|
187
215
|
id: node.id,
|
|
188
216
|
metadata: node.metadata,
|
|
@@ -199,66 +227,68 @@ export default function WorkPage() {
|
|
|
199
227
|
<div className="rounded-xl border border-amber-700 bg-amber-950/60 p-3 text-sm text-amber-200">
|
|
200
228
|
<p className="font-medium">Graph warnings</p>
|
|
201
229
|
{graphValidation.warnings.map((issue, index) => (
|
|
202
|
-
<p key={`${issue.ruleId}:${index}`}>
|
|
230
|
+
<p key={`${issue.ruleId}:${index}`}>
|
|
231
|
+
[{issue.ruleId}] {issue.message}
|
|
232
|
+
</p>
|
|
203
233
|
))}
|
|
204
234
|
</div>
|
|
205
235
|
) : null}
|
|
206
236
|
|
|
207
237
|
{view !== "architecture" ? (
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
238
|
+
<Card>
|
|
239
|
+
<CardHeader>
|
|
240
|
+
<CardTitle>Roadmap Work</CardTitle>
|
|
241
|
+
</CardHeader>
|
|
242
|
+
<CardContent className="grid gap-2">
|
|
243
|
+
<div className="flex flex-wrap gap-2">
|
|
244
|
+
<Select value={phase} onChange={(event) => setPhase(event.target.value)}>
|
|
245
|
+
<option value="all">All phases</option>
|
|
246
|
+
{phases.map((item) => (
|
|
247
|
+
<option key={item.id} value={item.id}>
|
|
248
|
+
{item.id} {item.active ? "(active)" : ""}
|
|
249
|
+
</option>
|
|
250
|
+
))}
|
|
251
|
+
</Select>
|
|
252
|
+
<Input
|
|
253
|
+
placeholder="Search task..."
|
|
254
|
+
value={query}
|
|
255
|
+
onChange={(event) => setQuery(event.target.value)}
|
|
256
|
+
/>
|
|
257
|
+
</div>
|
|
228
258
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
259
|
+
<div className="grid gap-1 text-slate-400">
|
|
260
|
+
{milestones.map((milestone) => (
|
|
261
|
+
<span key={milestone}>{milestone}</span>
|
|
262
|
+
))}
|
|
263
|
+
</div>
|
|
264
|
+
</CardContent>
|
|
265
|
+
</Card>
|
|
236
266
|
) : null}
|
|
237
267
|
|
|
238
268
|
{view !== "architecture" ? (
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
269
|
+
<Card>
|
|
270
|
+
<CardHeader>
|
|
271
|
+
<CardTitle>Tasks</CardTitle>
|
|
272
|
+
</CardHeader>
|
|
273
|
+
<CardContent>
|
|
274
|
+
<WorkTable
|
|
275
|
+
tasks={filteredTasks}
|
|
276
|
+
onSelectTask={(task) =>
|
|
277
|
+
setSelection({
|
|
278
|
+
type: "task",
|
|
279
|
+
title: task.title,
|
|
280
|
+
id: task.id,
|
|
281
|
+
metadata: [
|
|
282
|
+
{ label: "Phase", value: task.milestone.split("/")[0] ?? "unknown" },
|
|
283
|
+
{ label: "Milestone", value: task.milestone },
|
|
284
|
+
{ label: "Status", value: task.lane },
|
|
285
|
+
{ label: "Domain", value: task.domain ?? "foundation" },
|
|
286
|
+
],
|
|
287
|
+
})
|
|
288
|
+
}
|
|
289
|
+
/>
|
|
290
|
+
</CardContent>
|
|
291
|
+
</Card>
|
|
262
292
|
) : null}
|
|
263
293
|
</div>
|
|
264
294
|
);
|
|
@@ -57,13 +57,7 @@ function AppShellContent({ children }: { children: ReactNode }) {
|
|
|
57
57
|
}
|
|
58
58
|
window.addEventListener("keydown", onKeyDown);
|
|
59
59
|
return () => window.removeEventListener("keydown", onKeyDown);
|
|
60
|
-
}, [
|
|
61
|
-
leftCollapsed,
|
|
62
|
-
resetLayout,
|
|
63
|
-
rightCollapsed,
|
|
64
|
-
setLeftCollapsed,
|
|
65
|
-
setRightCollapsed,
|
|
66
|
-
]);
|
|
60
|
+
}, [leftCollapsed, resetLayout, rightCollapsed, setLeftCollapsed, setRightCollapsed]);
|
|
67
61
|
|
|
68
62
|
const shellStyle = useMemo(() => {
|
|
69
63
|
const leftResizer = leftCollapsed ? 0 : resizerWidth;
|
|
@@ -40,14 +40,20 @@ export function ArchNode({ data, selected }: NodeProps<ArchNodeData>) {
|
|
|
40
40
|
const metadataPreviewCount = detailLevel === 0 ? 1 : detailLevel === 1 ? 2 : 4;
|
|
41
41
|
|
|
42
42
|
return (
|
|
43
|
-
<div
|
|
43
|
+
<div
|
|
44
|
+
className={`w-[280px] rounded-xl border border-slate-600 px-2.5 py-2 text-slate-100 shadow ${toneClass}`}
|
|
45
|
+
>
|
|
44
46
|
<NodeToolbar isVisible={selected} position={Position.Top}>
|
|
45
47
|
<div className="flex items-center gap-2 rounded-md border border-slate-600 bg-slate-950 px-2 py-1">
|
|
46
48
|
<Badge variant="secondary">{data.kind}</Badge>
|
|
47
49
|
<span className="text-xs text-slate-400">drag, connect</span>
|
|
48
50
|
</div>
|
|
49
51
|
</NodeToolbar>
|
|
50
|
-
<Handle
|
|
52
|
+
<Handle
|
|
53
|
+
type="target"
|
|
54
|
+
position={Position.Left}
|
|
55
|
+
className="!h-2 !w-2 !border !border-slate-900 !bg-slate-200"
|
|
56
|
+
/>
|
|
51
57
|
<div className="mb-1 text-[13px] font-semibold" style={titleStyle}>
|
|
52
58
|
{data.label}
|
|
53
59
|
</div>
|
|
@@ -69,7 +75,11 @@ export function ArchNode({ data, selected }: NodeProps<ArchNodeData>) {
|
|
|
69
75
|
.join(" · ")}
|
|
70
76
|
</div>
|
|
71
77
|
) : null}
|
|
72
|
-
<Handle
|
|
78
|
+
<Handle
|
|
79
|
+
type="source"
|
|
80
|
+
position={Position.Right}
|
|
81
|
+
className="!h-2 !w-2 !border !border-slate-900 !bg-slate-200"
|
|
82
|
+
/>
|
|
73
83
|
</div>
|
|
74
84
|
);
|
|
75
85
|
}
|
|
@@ -103,7 +103,10 @@ function estimateNodeWidth(node: GraphDataset["nodes"][number]): number {
|
|
|
103
103
|
const normalized = Array.isArray(value) ? value.join(", ") : String(value);
|
|
104
104
|
return `${key}: ${normalized}`;
|
|
105
105
|
});
|
|
106
|
-
const metadataWidth = metadataStrings.reduce(
|
|
106
|
+
const metadataWidth = metadataStrings.reduce(
|
|
107
|
+
(max, value) => Math.max(max, value.length * 6.8),
|
|
108
|
+
0,
|
|
109
|
+
);
|
|
107
110
|
const estimated = base + Math.max(titleWidth, subtitleWidth, metadataWidth) * 0.58;
|
|
108
111
|
return Math.max(240, Math.min(estimated, 760));
|
|
109
112
|
}
|
|
@@ -136,7 +139,8 @@ export function buildGraphFromDataset(
|
|
|
136
139
|
columnX.set(column, cursorX);
|
|
137
140
|
const currentWidth = columnMaxWidth.get(column) ?? 260;
|
|
138
141
|
const nextColumn = columns[index + 1];
|
|
139
|
-
const nextWidth =
|
|
142
|
+
const nextWidth =
|
|
143
|
+
nextColumn === undefined ? currentWidth : (columnMaxWidth.get(nextColumn) ?? 260);
|
|
140
144
|
const adaptiveGap = Math.max(
|
|
141
145
|
MIN_HORIZONTAL_GAP,
|
|
142
146
|
Math.round(Math.max(currentWidth, nextWidth) * 0.24),
|