create-project-arch 1.1.0 → 1.3.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 +56 -0
- package/LICENSE +21 -0
- package/README.md +662 -43
- package/dist/cli.js +151 -0
- package/dist/cli.test.js +191 -0
- package/package.json +28 -4
- 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/eslint.config.js +2 -2
- 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
- package/templates/architecture-specs/SPEC_TEMPLATE.md +49 -0
- package/templates/architecture-specs/example-system.md +42 -0
- package/templates/concept-map/concept-map.json +67 -0
- package/templates/decisions/DECISION_TEMPLATE.md +53 -0
- package/templates/decisions/README.md +19 -0
- package/templates/decisions/example-decision.md +45 -0
- package/templates/domains/DOMAIN_TEMPLATE.md +43 -0
- package/templates/domains/README.md +18 -0
- package/templates/domains/api.md +33 -0
- package/templates/domains/core.md +33 -0
- package/templates/domains/domains.json +19 -0
- package/templates/domains/ui.md +34 -0
- package/templates/foundation/goals.md +35 -0
- package/templates/foundation/project-overview.md +35 -0
- package/templates/foundation/prompt.md +23 -0
- package/templates/foundation/scope.md +35 -0
- package/templates/foundation/user-journey.md +37 -0
- package/templates/gap-closure/GAP_CLOSURE_TEMPLATE.md +50 -0
- package/templates/gap-closure/README.md +19 -0
- package/templates/gap-closure/example-gap-closure.md +43 -0
- package/templates/validation-hooks/.githooks/pre-commit +4 -0
- package/templates/validation-hooks/README.md +20 -0
- package/templates/validation-hooks/scripts/validate.sh +9 -0
|
@@ -119,7 +119,9 @@ function isTaskLikeType(type: string): type is "task" | "milestone" | "phase" {
|
|
|
119
119
|
return type === "task" || type === "milestone" || type === "phase";
|
|
120
120
|
}
|
|
121
121
|
|
|
122
|
-
function isFileBackedType(
|
|
122
|
+
function isFileBackedType(
|
|
123
|
+
type: string,
|
|
124
|
+
): type is "task" | "milestone" | "phase" | "domain" | "file" {
|
|
123
125
|
return (
|
|
124
126
|
type === "task" ||
|
|
125
127
|
type === "milestone" ||
|
|
@@ -190,7 +192,11 @@ function humanizeValue(label: string, value: string): string {
|
|
|
190
192
|
return value;
|
|
191
193
|
}
|
|
192
194
|
|
|
193
|
-
function parseTaskId(taskId: string): {
|
|
195
|
+
function parseTaskId(taskId: string): {
|
|
196
|
+
phaseId?: string;
|
|
197
|
+
milestoneId?: string;
|
|
198
|
+
taskNumber?: string;
|
|
199
|
+
} {
|
|
194
200
|
const [phaseId, milestoneId, taskNumber] = taskId.split("/");
|
|
195
201
|
return {
|
|
196
202
|
phaseId: phaseId || undefined,
|
|
@@ -393,23 +399,21 @@ function MetadataDetails({
|
|
|
393
399
|
className="rounded-lg border border-slate-700/70 bg-slate-900/30 p-2 [&_summary::-webkit-details-marker]:hidden"
|
|
394
400
|
open={defaultOpen}
|
|
395
401
|
>
|
|
396
|
-
<summary className="cursor-pointer list-none text-sm font-medium text-slate-200">
|
|
402
|
+
<summary className="cursor-pointer list-none text-sm font-medium text-slate-200">
|
|
403
|
+
{title}
|
|
404
|
+
</summary>
|
|
397
405
|
<div className="mt-2 grid gap-2">{children}</div>
|
|
398
406
|
</details>
|
|
399
407
|
);
|
|
400
408
|
}
|
|
401
409
|
|
|
402
|
-
function MetadataValue({
|
|
403
|
-
label,
|
|
404
|
-
value,
|
|
405
|
-
}: {
|
|
406
|
-
label: string;
|
|
407
|
-
value: FrontmatterValue;
|
|
408
|
-
}) {
|
|
410
|
+
function MetadataValue({ label, value }: { label: string; value: FrontmatterValue }) {
|
|
409
411
|
if (value === null) {
|
|
410
412
|
return (
|
|
411
413
|
<div className="grid gap-1">
|
|
412
|
-
<p className="m-0 text-xs uppercase tracking-[0.06em] text-slate-400">
|
|
414
|
+
<p className="m-0 text-xs uppercase tracking-[0.06em] text-slate-400">
|
|
415
|
+
{humanizeLabel(label)}
|
|
416
|
+
</p>
|
|
413
417
|
<p className="m-0 text-sm text-slate-500">None</p>
|
|
414
418
|
</div>
|
|
415
419
|
);
|
|
@@ -419,7 +423,9 @@ function MetadataValue({
|
|
|
419
423
|
if (value.length === 0) {
|
|
420
424
|
return (
|
|
421
425
|
<div className="grid gap-1">
|
|
422
|
-
<p className="m-0 text-xs uppercase tracking-[0.06em] text-slate-400">
|
|
426
|
+
<p className="m-0 text-xs uppercase tracking-[0.06em] text-slate-400">
|
|
427
|
+
{humanizeLabel(label)}
|
|
428
|
+
</p>
|
|
423
429
|
<p className="m-0 text-sm text-slate-500">None</p>
|
|
424
430
|
</div>
|
|
425
431
|
);
|
|
@@ -428,7 +434,9 @@ function MetadataValue({
|
|
|
428
434
|
const listLike = normalizeKey(label).includes("criteria");
|
|
429
435
|
return (
|
|
430
436
|
<div className="grid gap-1.5">
|
|
431
|
-
<p className="m-0 text-xs uppercase tracking-[0.06em] text-slate-400">
|
|
437
|
+
<p className="m-0 text-xs uppercase tracking-[0.06em] text-slate-400">
|
|
438
|
+
{humanizeLabel(label)}
|
|
439
|
+
</p>
|
|
432
440
|
{listLike ? (
|
|
433
441
|
<ol className="m-0 grid list-decimal gap-1 pl-5 text-sm text-slate-200">
|
|
434
442
|
{value.map((item) => (
|
|
@@ -485,7 +493,11 @@ export function InspectorPanel() {
|
|
|
485
493
|
keys
|
|
486
494
|
.filter((key) => sourceEntries.has(key))
|
|
487
495
|
.map((key) => [key, sourceEntries.get(key) ?? null] as const)
|
|
488
|
-
.filter(
|
|
496
|
+
.filter(
|
|
497
|
+
([, value]) =>
|
|
498
|
+
showEmptyFileMetadata ||
|
|
499
|
+
!(value === null || (Array.isArray(value) && value.length === 0)),
|
|
500
|
+
);
|
|
489
501
|
|
|
490
502
|
return {
|
|
491
503
|
identity: pick(keyOrder.identity),
|
|
@@ -651,9 +663,15 @@ export function InspectorPanel() {
|
|
|
651
663
|
<aside className="h-full min-h-0 overflow-y-auto overflow-x-visible border-l border-slate-800 bg-slate-950/85 p-4">
|
|
652
664
|
<div className="grid gap-3">
|
|
653
665
|
<div className="flex flex-wrap items-center gap-2">
|
|
654
|
-
<Badge variant="secondary">
|
|
655
|
-
|
|
656
|
-
|
|
666
|
+
<Badge variant="secondary">
|
|
667
|
+
{humanizeLabel(hasFileDocument ? "file" : selection.type)}
|
|
668
|
+
</Badge>
|
|
669
|
+
{typeof fileLaneValue === "string" ? (
|
|
670
|
+
<Badge variant="secondary">{humanizeValue("lane", fileLaneValue)}</Badge>
|
|
671
|
+
) : null}
|
|
672
|
+
{typeof fileStatusValue === "string" ? (
|
|
673
|
+
<Badge variant="secondary">{humanizeValue("status", fileStatusValue)}</Badge>
|
|
674
|
+
) : null}
|
|
657
675
|
{selectedLevelFile?.path ? (
|
|
658
676
|
<Button
|
|
659
677
|
variant="outline"
|
|
@@ -687,7 +705,9 @@ export function InspectorPanel() {
|
|
|
687
705
|
{graphWarnings.length > 0 ? (
|
|
688
706
|
<Section title="Graph Diagnostics">
|
|
689
707
|
{graphWarnings.slice(0, 8).map((issue, index) => (
|
|
690
|
-
<Code key={`${issue.ruleId}:${index}`}>
|
|
708
|
+
<Code key={`${issue.ruleId}:${index}`}>
|
|
709
|
+
[{issue.ruleId}] {issue.message}
|
|
710
|
+
</Code>
|
|
691
711
|
))}
|
|
692
712
|
</Section>
|
|
693
713
|
) : null}
|
|
@@ -774,7 +794,11 @@ export function InspectorPanel() {
|
|
|
774
794
|
{fileMetadataGroups.references.length > 0 ? (
|
|
775
795
|
<MetadataDetails title="References">
|
|
776
796
|
{fileMetadataGroups.references.map(([label, value]) => (
|
|
777
|
-
<MetadataValue
|
|
797
|
+
<MetadataValue
|
|
798
|
+
key={`file-references:${label}`}
|
|
799
|
+
label={label}
|
|
800
|
+
value={value}
|
|
801
|
+
/>
|
|
778
802
|
))}
|
|
779
803
|
</MetadataDetails>
|
|
780
804
|
) : null}
|
|
@@ -806,7 +830,11 @@ export function InspectorPanel() {
|
|
|
806
830
|
<div className="grid gap-1.5">
|
|
807
831
|
<p className="text-slate-400">Task Metadata</p>
|
|
808
832
|
{rawTaskMetadata.map((item) => (
|
|
809
|
-
<Field
|
|
833
|
+
<Field
|
|
834
|
+
key={`${item.label}:${item.value}`}
|
|
835
|
+
label={item.label}
|
|
836
|
+
value={item.value}
|
|
837
|
+
/>
|
|
810
838
|
))}
|
|
811
839
|
</div>
|
|
812
840
|
) : null}
|
|
@@ -836,7 +864,9 @@ export function InspectorPanel() {
|
|
|
836
864
|
{trace?.files?.map((file) => (
|
|
837
865
|
<Code key={file}>{file}</Code>
|
|
838
866
|
))}
|
|
839
|
-
{!trace?.decisionRefs?.length &&
|
|
867
|
+
{!trace?.decisionRefs?.length &&
|
|
868
|
+
!trace?.moduleRefs?.length &&
|
|
869
|
+
!trace?.files?.length ? (
|
|
840
870
|
<p className="text-slate-400">No trace links found.</p>
|
|
841
871
|
) : null}
|
|
842
872
|
</div>
|
|
@@ -854,7 +884,11 @@ export function InspectorPanel() {
|
|
|
854
884
|
<div className="grid gap-2">
|
|
855
885
|
<p className="text-slate-400">Entity Data</p>
|
|
856
886
|
{entityDetails.summary.map((item) => (
|
|
857
|
-
<Field
|
|
887
|
+
<Field
|
|
888
|
+
key={`${item.label}:${item.value}`}
|
|
889
|
+
label={item.label}
|
|
890
|
+
value={item.value}
|
|
891
|
+
/>
|
|
858
892
|
))}
|
|
859
893
|
<RelatedList label="Tasks" values={entityDetails.relatedTasks} />
|
|
860
894
|
<RelatedList label="Milestones" values={entityDetails.relatedMilestones} />
|
|
@@ -81,12 +81,17 @@ function sortTree(nodes: TreeNode[]): TreeNode[] {
|
|
|
81
81
|
}));
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
-
function buildDocNodes(
|
|
84
|
+
function buildDocNodes(
|
|
85
|
+
scope: DomainDocsData["docs"][number]["scope"],
|
|
86
|
+
docs: DomainDocsData["docs"],
|
|
87
|
+
): TreeNode[] {
|
|
85
88
|
const scopedDocs = docs.filter((doc) => doc.scope === scope);
|
|
86
89
|
const roots: TreeNode[] = [];
|
|
87
90
|
|
|
88
91
|
for (const doc of scopedDocs) {
|
|
89
|
-
const relativePath = doc.path.startsWith(`${scope}/`)
|
|
92
|
+
const relativePath = doc.path.startsWith(`${scope}/`)
|
|
93
|
+
? doc.path.slice(scope.length + 1)
|
|
94
|
+
: doc.path;
|
|
90
95
|
const parts = relativePath.split("/").filter(Boolean);
|
|
91
96
|
if (parts.length === 0) continue;
|
|
92
97
|
|
|
@@ -162,7 +167,8 @@ export function Sidebar() {
|
|
|
162
167
|
}, []);
|
|
163
168
|
|
|
164
169
|
const activeView = useMemo<WorkspaceView>(() => {
|
|
165
|
-
if (pathname === "/work" && searchParams.get("view") === "architecture")
|
|
170
|
+
if (pathname === "/work" && searchParams.get("view") === "architecture")
|
|
171
|
+
return "architecture-map";
|
|
166
172
|
if (pathname === "/work" && searchParams.get("view") === "project") return "project-map";
|
|
167
173
|
if (pathname === "/work") return "tasks-roadmap";
|
|
168
174
|
const view = searchParams.get("view");
|
|
@@ -290,9 +296,7 @@ export function Sidebar() {
|
|
|
290
296
|
) : (
|
|
291
297
|
<span className="h-4 w-4" />
|
|
292
298
|
)}
|
|
293
|
-
<span className="text-slate-300">
|
|
294
|
-
{iconForNode(node, expandedKeys.has(node.key))}
|
|
295
|
-
</span>
|
|
299
|
+
<span className="text-slate-300">{iconForNode(node, expandedKeys.has(node.key))}</span>
|
|
296
300
|
<button
|
|
297
301
|
type="button"
|
|
298
302
|
className="min-w-0 flex-1 truncate text-left text-[12px] text-slate-200"
|
|
@@ -405,8 +409,14 @@ export function Sidebar() {
|
|
|
405
409
|
|
|
406
410
|
<NavigationMenu>
|
|
407
411
|
<section className="mb-4">
|
|
408
|
-
<p className="mb-2 text-[11px] uppercase tracking-[0.1em] text-slate-400">
|
|
409
|
-
|
|
412
|
+
<p className="mb-2 text-[11px] uppercase tracking-[0.1em] text-slate-400">
|
|
413
|
+
Unified Explorer
|
|
414
|
+
</p>
|
|
415
|
+
{tree.length > 0 ? (
|
|
416
|
+
renderTree(tree)
|
|
417
|
+
) : (
|
|
418
|
+
<p className="text-sm text-slate-400">Loading explorer...</p>
|
|
419
|
+
)}
|
|
410
420
|
</section>
|
|
411
421
|
<section className="mb-4">
|
|
412
422
|
<p className="mb-2 text-[11px] uppercase tracking-[0.1em] text-slate-400">Node Filters</p>
|
|
@@ -32,7 +32,8 @@ export function Topbar({ projectName }: { projectName: string }) {
|
|
|
32
32
|
const pathname = usePathname();
|
|
33
33
|
const searchParams = useSearchParams();
|
|
34
34
|
const { selection } = useInspector();
|
|
35
|
-
const { splitPane, setSplitPane, rightCollapsed, setRightCollapsed, resetLayout } =
|
|
35
|
+
const { splitPane, setSplitPane, rightCollapsed, setRightCollapsed, resetLayout } =
|
|
36
|
+
useWorkspace();
|
|
36
37
|
|
|
37
38
|
useEffect(() => {
|
|
38
39
|
function onKeyDown(event: KeyboardEvent) {
|
|
@@ -79,11 +80,7 @@ export function Topbar({ projectName }: { projectName: string }) {
|
|
|
79
80
|
|
|
80
81
|
const breadcrumbs = useMemo(() => {
|
|
81
82
|
const pathLabel =
|
|
82
|
-
pathname === "/work"
|
|
83
|
-
? "Work Graph"
|
|
84
|
-
: pathname === "/health"
|
|
85
|
-
? "Health"
|
|
86
|
-
: "Workspace";
|
|
83
|
+
pathname === "/work" ? "Work Graph" : pathname === "/health" ? "Health" : "Workspace";
|
|
87
84
|
const view = searchParams.get("view");
|
|
88
85
|
const secondary =
|
|
89
86
|
view === "docs"
|
|
@@ -92,11 +89,11 @@ export function Topbar({ projectName }: { projectName: string }) {
|
|
|
92
89
|
? "Decisions"
|
|
93
90
|
: view === "architecture"
|
|
94
91
|
? "Architecture"
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
92
|
+
: view === "project"
|
|
93
|
+
? "Project"
|
|
94
|
+
: view === "tasks"
|
|
95
|
+
? "Tasks"
|
|
96
|
+
: "Map";
|
|
100
97
|
|
|
101
98
|
const parts = [pathLabel];
|
|
102
99
|
if (pathname === "/work") parts.push(secondary);
|
|
@@ -31,11 +31,7 @@ export function WorkTable({ tasks, onSelectTask }: WorkTableProps) {
|
|
|
31
31
|
</TableHeader>
|
|
32
32
|
<TableBody>
|
|
33
33
|
{tasks.map((task) => (
|
|
34
|
-
<TableRow
|
|
35
|
-
className="cursor-pointer"
|
|
36
|
-
key={task.id}
|
|
37
|
-
onClick={() => onSelectTask(task)}
|
|
38
|
-
>
|
|
34
|
+
<TableRow className="cursor-pointer" key={task.id} onClick={() => onSelectTask(task)}>
|
|
39
35
|
<TableCell>
|
|
40
36
|
<Code>{task.id}</Code>
|
|
41
37
|
</TableCell>
|
|
@@ -84,7 +84,8 @@ function defaultFilters(): WorkspaceFilters {
|
|
|
84
84
|
|
|
85
85
|
function resolveActiveGraphView(pathname: string, searchParams: URLSearchParams): GraphViewMode {
|
|
86
86
|
if (pathname === "/work" && searchParams.get("view") === "project") return "project";
|
|
87
|
-
if (pathname === "/work" && searchParams.get("view") === "architecture")
|
|
87
|
+
if (pathname === "/work" && searchParams.get("view") === "architecture")
|
|
88
|
+
return "architecture-map";
|
|
88
89
|
if (pathname === "/work") return "tasks";
|
|
89
90
|
return "architecture-map";
|
|
90
91
|
}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import {
|
|
2
|
-
export default
|
|
1
|
+
import { nextJsConfig } from '@repo/eslint-config/next-js';
|
|
2
|
+
export default nextJsConfig;
|
|
@@ -114,14 +114,10 @@ async function filterGitIgnoredPaths(root: string, paths: string[]): Promise<Set
|
|
|
114
114
|
if (chunk.length === 0) continue;
|
|
115
115
|
|
|
116
116
|
try {
|
|
117
|
-
const { stdout } = (await execFile(
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
cwd: root,
|
|
122
|
-
encoding: "utf8",
|
|
123
|
-
} as Parameters<typeof execFile>[2],
|
|
124
|
-
)) as { stdout: string };
|
|
117
|
+
const { stdout } = (await execFile("git", ["check-ignore", ...chunk], {
|
|
118
|
+
cwd: root,
|
|
119
|
+
encoding: "utf8",
|
|
120
|
+
} as Parameters<typeof execFile>[2])) as { stdout: string };
|
|
125
121
|
stdout
|
|
126
122
|
.split("\n")
|
|
127
123
|
.map((line: string) => line.trim())
|
|
@@ -229,10 +229,7 @@ export const GRAPH_VALIDATION_RULES = [
|
|
|
229
229
|
},
|
|
230
230
|
] as const;
|
|
231
231
|
|
|
232
|
-
function pushIssue(
|
|
233
|
-
output: GraphValidationResult,
|
|
234
|
-
issue: GraphValidationIssue,
|
|
235
|
-
) {
|
|
232
|
+
function pushIssue(output: GraphValidationResult, issue: GraphValidationIssue) {
|
|
236
233
|
if (issue.severity === "error") output.errors.push(issue);
|
|
237
234
|
else output.warnings.push(issue);
|
|
238
235
|
}
|
|
@@ -8,14 +8,6 @@
|
|
|
8
8
|
],
|
|
9
9
|
"strictNullChecks": true
|
|
10
10
|
},
|
|
11
|
-
"include": [
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
"**/*.d.ts",
|
|
15
|
-
"next-env.d.ts",
|
|
16
|
-
"next.config.js"
|
|
17
|
-
],
|
|
18
|
-
"exclude": [
|
|
19
|
-
"node_modules"
|
|
20
|
-
]
|
|
21
|
-
}
|
|
11
|
+
"include": ["**/*.ts", "**/*.tsx", "**/*.d.ts", "next-env.d.ts", "next.config.js"],
|
|
12
|
+
"exclude": ["node_modules"]
|
|
13
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# System Architecture Specification
|
|
2
|
+
|
|
3
|
+
<!-- Guidance: Use this template for new architecture specs in architecture/architecture/. -->
|
|
4
|
+
|
|
5
|
+
## Purpose
|
|
6
|
+
|
|
7
|
+
<!-- Guidance: Explain why this system/component exists and the problem it solves. -->
|
|
8
|
+
|
|
9
|
+
...
|
|
10
|
+
|
|
11
|
+
## Scope
|
|
12
|
+
|
|
13
|
+
### In Scope
|
|
14
|
+
|
|
15
|
+
- ...
|
|
16
|
+
|
|
17
|
+
### Out Of Scope
|
|
18
|
+
|
|
19
|
+
- ...
|
|
20
|
+
|
|
21
|
+
## Key Definitions
|
|
22
|
+
|
|
23
|
+
<!-- Guidance: Define domain terms and technical vocabulary used in this spec. -->
|
|
24
|
+
|
|
25
|
+
- ...
|
|
26
|
+
|
|
27
|
+
## Design
|
|
28
|
+
|
|
29
|
+
<!-- Guidance: Describe architecture approach, constraints, and major patterns. -->
|
|
30
|
+
|
|
31
|
+
...
|
|
32
|
+
|
|
33
|
+
## Data Model
|
|
34
|
+
|
|
35
|
+
<!-- Guidance: List core entities, relationships, and ownership boundaries. -->
|
|
36
|
+
|
|
37
|
+
- ...
|
|
38
|
+
|
|
39
|
+
## Owning Domain
|
|
40
|
+
|
|
41
|
+
<!-- Guidance: Specify the domain accountable for this architecture area. -->
|
|
42
|
+
|
|
43
|
+
- ...
|
|
44
|
+
|
|
45
|
+
## MVP Constraints
|
|
46
|
+
|
|
47
|
+
<!-- Guidance: List minimum viable boundaries and explicit non-goals for initial delivery. -->
|
|
48
|
+
|
|
49
|
+
- ...
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# Example System Specification
|
|
2
|
+
|
|
3
|
+
<!-- Guidance: Reference implementation of SPEC_TEMPLATE.md for new projects. -->
|
|
4
|
+
|
|
5
|
+
## Purpose
|
|
6
|
+
|
|
7
|
+
Provide a canonical architecture spec format for teams documenting a system surface.
|
|
8
|
+
|
|
9
|
+
## Scope
|
|
10
|
+
|
|
11
|
+
### In Scope
|
|
12
|
+
|
|
13
|
+
- Define required architecture sections and ownership expectations.
|
|
14
|
+
- Establish traceable spec structure for implementation planning.
|
|
15
|
+
|
|
16
|
+
### Out Of Scope
|
|
17
|
+
|
|
18
|
+
- Detailed API contract definitions.
|
|
19
|
+
- Final production implementation decisions.
|
|
20
|
+
|
|
21
|
+
## Key Definitions
|
|
22
|
+
|
|
23
|
+
- System surface: a bounded capability area implemented across one or more modules.
|
|
24
|
+
- Ownership boundary: the domain responsible for maintaining this surface.
|
|
25
|
+
|
|
26
|
+
## Design
|
|
27
|
+
|
|
28
|
+
Use a layered design where interface boundaries, data ownership, and dependency direction are explicit.
|
|
29
|
+
|
|
30
|
+
## Data Model
|
|
31
|
+
|
|
32
|
+
- `SpecSection`: named architecture section with required content.
|
|
33
|
+
- `OwnershipLink`: maps the system surface to an owning domain.
|
|
34
|
+
|
|
35
|
+
## Owning Domain
|
|
36
|
+
|
|
37
|
+
- `core`
|
|
38
|
+
|
|
39
|
+
## MVP Constraints
|
|
40
|
+
|
|
41
|
+
- Keep scope focused on structure and traceability.
|
|
42
|
+
- Defer advanced runtime behavior until downstream milestones.
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schemaVersion": "1.0",
|
|
3
|
+
"concepts": [
|
|
4
|
+
{
|
|
5
|
+
"id": "concept-identity-and-access",
|
|
6
|
+
"name": "Identity And Access",
|
|
7
|
+
"description": "Authentication, authorization, and role enforcement boundaries.",
|
|
8
|
+
"owningDomain": "core",
|
|
9
|
+
"moduleResponsibilities": ["packages/api", "packages/types"],
|
|
10
|
+
"implementationSurfaces": [
|
|
11
|
+
{
|
|
12
|
+
"type": "api",
|
|
13
|
+
"path": "packages/api/src/auth"
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
"type": "ui",
|
|
17
|
+
"path": "apps/web/app/(auth)"
|
|
18
|
+
}
|
|
19
|
+
],
|
|
20
|
+
"dependencies": ["concept-user-profile"]
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
"id": "concept-user-profile",
|
|
24
|
+
"name": "User Profile",
|
|
25
|
+
"description": "Canonical user metadata and preference lifecycle.",
|
|
26
|
+
"owningDomain": "ui",
|
|
27
|
+
"moduleResponsibilities": ["apps/web", "packages/ui"],
|
|
28
|
+
"implementationSurfaces": [
|
|
29
|
+
{
|
|
30
|
+
"type": "ui",
|
|
31
|
+
"path": "apps/web/app/profile"
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
"type": "component",
|
|
35
|
+
"path": "packages/ui/src/profile"
|
|
36
|
+
}
|
|
37
|
+
],
|
|
38
|
+
"dependencies": []
|
|
39
|
+
}
|
|
40
|
+
],
|
|
41
|
+
"domainModuleMapping": [
|
|
42
|
+
{
|
|
43
|
+
"domain": "core",
|
|
44
|
+
"module": "packages/api",
|
|
45
|
+
"responsibility": "Policy enforcement and access control services"
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
"domain": "ui",
|
|
49
|
+
"module": "apps/web",
|
|
50
|
+
"responsibility": "User-facing workflows and interaction handling"
|
|
51
|
+
}
|
|
52
|
+
],
|
|
53
|
+
"implementationChecklist": [
|
|
54
|
+
{
|
|
55
|
+
"conceptId": "concept-identity-and-access",
|
|
56
|
+
"checks": [
|
|
57
|
+
"Decision linkage recorded",
|
|
58
|
+
"Primary code targets identified",
|
|
59
|
+
"Verification criteria defined"
|
|
60
|
+
]
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
"conceptId": "concept-user-profile",
|
|
64
|
+
"checks": ["Domain ownership confirmed", "Surface mapping completed", "Dependencies reviewed"]
|
|
65
|
+
}
|
|
66
|
+
]
|
|
67
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: "project:YYYYMMDD:decision-slug"
|
|
3
|
+
title: "Decision Title"
|
|
4
|
+
slug: "decision-slug"
|
|
5
|
+
status: "proposed"
|
|
6
|
+
createdAt: "YYYY-MM-DD"
|
|
7
|
+
updatedAt: "YYYY-MM-DD"
|
|
8
|
+
relatedTasks: []
|
|
9
|
+
relatedDocs: []
|
|
10
|
+
supersedes: []
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Decision Title
|
|
14
|
+
|
|
15
|
+
<!-- Guidance: Capture one architecture decision per file. Keep scope explicit and testable. -->
|
|
16
|
+
|
|
17
|
+
## Context
|
|
18
|
+
|
|
19
|
+
<!-- Guidance: What problem/constraint requires a decision now? -->
|
|
20
|
+
|
|
21
|
+
...
|
|
22
|
+
|
|
23
|
+
## Decision
|
|
24
|
+
|
|
25
|
+
<!-- Guidance: State the chosen approach unambiguously. -->
|
|
26
|
+
|
|
27
|
+
...
|
|
28
|
+
|
|
29
|
+
## Rationale
|
|
30
|
+
|
|
31
|
+
<!-- Guidance: Why this option was selected versus alternatives. -->
|
|
32
|
+
|
|
33
|
+
...
|
|
34
|
+
|
|
35
|
+
## Alternatives Considered
|
|
36
|
+
|
|
37
|
+
- Option A: ...
|
|
38
|
+
- Option B: ...
|
|
39
|
+
|
|
40
|
+
## Affected Artifacts
|
|
41
|
+
|
|
42
|
+
<!-- Guidance: List code/doc surfaces impacted by this decision. -->
|
|
43
|
+
|
|
44
|
+
- `packages/...`
|
|
45
|
+
- `apps/...`
|
|
46
|
+
- `architecture/...`
|
|
47
|
+
|
|
48
|
+
## Implementation Status Checklist
|
|
49
|
+
|
|
50
|
+
- [ ] Task links added
|
|
51
|
+
- [ ] Code targets updated
|
|
52
|
+
- [ ] Public docs updated
|
|
53
|
+
- [ ] Validation checks pass
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# Decision Records
|
|
2
|
+
|
|
3
|
+
<!-- Guidance: Use decision records for meaningful architecture choices and tradeoffs. -->
|
|
4
|
+
|
|
5
|
+
## Workflow
|
|
6
|
+
|
|
7
|
+
1. Copy `DECISION_TEMPLATE.md` for each significant decision.
|
|
8
|
+
2. Set frontmatter (`id`, `title`, `slug`, `status`, timestamps, links).
|
|
9
|
+
3. Fill Context, Decision, Rationale, Alternatives, and Affected Artifacts.
|
|
10
|
+
4. Track implementation progress with the checklist.
|
|
11
|
+
|
|
12
|
+
## Decision Locations
|
|
13
|
+
|
|
14
|
+
- `architecture/decisions/` for architecture-level decision records.
|
|
15
|
+
- `roadmap/decisions/` and milestone decision indexes can reference decision IDs for execution traceability.
|
|
16
|
+
|
|
17
|
+
## CLI Support
|
|
18
|
+
|
|
19
|
+
Use `pa decision new` to create operational decision records linked into roadmap indexes.
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: "project:20260308:adopt-typed-service-contracts"
|
|
3
|
+
title: "Adopt Typed Service Contracts"
|
|
4
|
+
slug: "adopt-typed-service-contracts"
|
|
5
|
+
status: "accepted"
|
|
6
|
+
createdAt: "2026-03-08"
|
|
7
|
+
updatedAt: "2026-03-08"
|
|
8
|
+
relatedTasks:
|
|
9
|
+
- "phase-1/milestone-1-setup/005"
|
|
10
|
+
relatedDocs:
|
|
11
|
+
- "architecture/architecture/example-system.md"
|
|
12
|
+
supersedes: []
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## Adopt Typed Service Contracts
|
|
16
|
+
|
|
17
|
+
## Context
|
|
18
|
+
|
|
19
|
+
Service integration points were drifting across modules due to implicit payload assumptions.
|
|
20
|
+
|
|
21
|
+
## Decision
|
|
22
|
+
|
|
23
|
+
All cross-module service boundaries must use explicit shared types in `packages/types` with runtime validation in API adapters.
|
|
24
|
+
|
|
25
|
+
## Rationale
|
|
26
|
+
|
|
27
|
+
Typed contracts reduce integration regressions, improve agent traceability, and make architectural intent machine-verifiable.
|
|
28
|
+
|
|
29
|
+
## Alternatives Considered
|
|
30
|
+
|
|
31
|
+
- Keep implicit JSON contracts and rely on integration tests only.
|
|
32
|
+
- Use per-module ad hoc type aliases without shared contracts.
|
|
33
|
+
|
|
34
|
+
## Affected Artifacts
|
|
35
|
+
|
|
36
|
+
- `packages/types/src/`
|
|
37
|
+
- `packages/api/src/`
|
|
38
|
+
- `architecture/architecture/example-system.md`
|
|
39
|
+
|
|
40
|
+
## Implementation Status Checklist
|
|
41
|
+
|
|
42
|
+
- [x] Task links added
|
|
43
|
+
- [x] Code targets updated
|
|
44
|
+
- [x] Public docs updated
|
|
45
|
+
- [x] Validation checks pass
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# Domain Template
|
|
2
|
+
|
|
3
|
+
<!-- Guidance: Copy this file to `<domain-name>.md` when introducing a new domain. -->
|
|
4
|
+
|
|
5
|
+
## Domain Name
|
|
6
|
+
|
|
7
|
+
<domain-name>
|
|
8
|
+
|
|
9
|
+
## Responsibilities
|
|
10
|
+
|
|
11
|
+
<!-- Guidance: What this domain owns and manages. -->
|
|
12
|
+
|
|
13
|
+
- ...
|
|
14
|
+
|
|
15
|
+
## Primary Data Ownership
|
|
16
|
+
|
|
17
|
+
<!-- Guidance: Entities this domain is the source of truth for. -->
|
|
18
|
+
|
|
19
|
+
- ...
|
|
20
|
+
|
|
21
|
+
## Interface Contracts
|
|
22
|
+
|
|
23
|
+
<!-- Guidance: APIs/events this domain exposes and consumes. -->
|
|
24
|
+
|
|
25
|
+
- ...
|
|
26
|
+
|
|
27
|
+
## Non-Goals
|
|
28
|
+
|
|
29
|
+
<!-- Guidance: Explicit boundaries for what this domain does NOT do. -->
|
|
30
|
+
|
|
31
|
+
- ...
|
|
32
|
+
|
|
33
|
+
## Milestone Mapping
|
|
34
|
+
|
|
35
|
+
<!-- Guidance: Map milestones to domain delivery outcomes. -->
|
|
36
|
+
|
|
37
|
+
- milestone-1: ...
|
|
38
|
+
- milestone-2: ...
|
|
39
|
+
|
|
40
|
+
## Implementation Surfaces
|
|
41
|
+
|
|
42
|
+
- `apps/...`
|
|
43
|
+
- `packages/...`
|