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
package/dist/cli.js
CHANGED
|
@@ -135,6 +135,150 @@ async function scaffoldArchitectureApps(targetDir) {
|
|
|
135
135
|
await fs_extra_1.default.writeJSON(archUiPkgPath, nextArchUiPkg, { spaces: 2 });
|
|
136
136
|
await fs_extra_1.default.appendFile(archUiPkgPath, "\n");
|
|
137
137
|
}
|
|
138
|
+
async function scaffoldFoundationDocs(targetDir) {
|
|
139
|
+
const templatesRoot = getTemplatesRoot();
|
|
140
|
+
const foundationTemplateRoot = path_1.default.join(templatesRoot, "foundation");
|
|
141
|
+
const foundationTargetRoot = path_1.default.join(targetDir, "architecture", "foundation");
|
|
142
|
+
if (!(await fs_extra_1.default.pathExists(foundationTemplateRoot))) {
|
|
143
|
+
throw new Error(`Missing foundation templates at ${foundationTemplateRoot}`);
|
|
144
|
+
}
|
|
145
|
+
await fs_extra_1.default.ensureDir(foundationTargetRoot);
|
|
146
|
+
const foundationEntries = await fs_extra_1.default.readdir(foundationTemplateRoot);
|
|
147
|
+
for (const entry of foundationEntries) {
|
|
148
|
+
const sourcePath = path_1.default.join(foundationTemplateRoot, entry);
|
|
149
|
+
const targetPath = path_1.default.join(foundationTargetRoot, entry);
|
|
150
|
+
if (await fs_extra_1.default.pathExists(targetPath)) {
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
await fs_extra_1.default.copy(sourcePath, targetPath, { overwrite: false });
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
async function scaffoldDomainSpecs(targetDir) {
|
|
157
|
+
const templatesRoot = getTemplatesRoot();
|
|
158
|
+
const domainsTemplateRoot = path_1.default.join(templatesRoot, "domains");
|
|
159
|
+
const domainsTargetRoot = path_1.default.join(targetDir, "arch-domains");
|
|
160
|
+
if (!(await fs_extra_1.default.pathExists(domainsTemplateRoot))) {
|
|
161
|
+
throw new Error(`Missing domain templates at ${domainsTemplateRoot}`);
|
|
162
|
+
}
|
|
163
|
+
await fs_extra_1.default.ensureDir(domainsTargetRoot);
|
|
164
|
+
const domainEntries = await fs_extra_1.default.readdir(domainsTemplateRoot);
|
|
165
|
+
for (const entry of domainEntries) {
|
|
166
|
+
const sourcePath = path_1.default.join(domainsTemplateRoot, entry);
|
|
167
|
+
const targetPath = path_1.default.join(domainsTargetRoot, entry);
|
|
168
|
+
if (entry === "DOMAIN_TEMPLATE.md" || entry === "README.md") {
|
|
169
|
+
await fs_extra_1.default.copy(sourcePath, targetPath, { overwrite: true });
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
if (entry === "domains.json") {
|
|
173
|
+
if (await fs_extra_1.default.pathExists(targetPath)) {
|
|
174
|
+
const existing = (await fs_extra_1.default.readJSON(targetPath));
|
|
175
|
+
if (Array.isArray(existing.domains) && existing.domains.length > 0) {
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
await fs_extra_1.default.copy(sourcePath, targetPath, { overwrite: true });
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
if (await fs_extra_1.default.pathExists(targetPath)) {
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
await fs_extra_1.default.copy(sourcePath, targetPath, { overwrite: false });
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
async function scaffoldArchitectureSpecs(targetDir) {
|
|
189
|
+
const templatesRoot = getTemplatesRoot();
|
|
190
|
+
const architectureSpecsTemplateRoot = path_1.default.join(templatesRoot, "architecture-specs");
|
|
191
|
+
const architectureSpecsTargetRoot = path_1.default.join(targetDir, "architecture", "architecture");
|
|
192
|
+
if (!(await fs_extra_1.default.pathExists(architectureSpecsTemplateRoot))) {
|
|
193
|
+
throw new Error(`Missing architecture spec templates at ${architectureSpecsTemplateRoot}`);
|
|
194
|
+
}
|
|
195
|
+
await fs_extra_1.default.ensureDir(architectureSpecsTargetRoot);
|
|
196
|
+
const specEntries = await fs_extra_1.default.readdir(architectureSpecsTemplateRoot);
|
|
197
|
+
for (const entry of specEntries) {
|
|
198
|
+
const sourcePath = path_1.default.join(architectureSpecsTemplateRoot, entry);
|
|
199
|
+
const targetPath = path_1.default.join(architectureSpecsTargetRoot, entry);
|
|
200
|
+
if (await fs_extra_1.default.pathExists(targetPath)) {
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
await fs_extra_1.default.copy(sourcePath, targetPath, { overwrite: false });
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
async function scaffoldConceptMap(targetDir) {
|
|
207
|
+
const templatesRoot = getTemplatesRoot();
|
|
208
|
+
const conceptMapTemplatePath = path_1.default.join(templatesRoot, "concept-map", "concept-map.json");
|
|
209
|
+
const conceptMapTargetPath = path_1.default.join(targetDir, "arch-model", "concept-map.json");
|
|
210
|
+
if (!(await fs_extra_1.default.pathExists(conceptMapTemplatePath))) {
|
|
211
|
+
throw new Error(`Missing concept-map template at ${conceptMapTemplatePath}`);
|
|
212
|
+
}
|
|
213
|
+
await fs_extra_1.default.ensureDir(path_1.default.dirname(conceptMapTargetPath));
|
|
214
|
+
if (await fs_extra_1.default.pathExists(conceptMapTargetPath)) {
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
await fs_extra_1.default.copy(conceptMapTemplatePath, conceptMapTargetPath, { overwrite: false });
|
|
218
|
+
}
|
|
219
|
+
async function scaffoldDecisionRecords(targetDir) {
|
|
220
|
+
const templatesRoot = getTemplatesRoot();
|
|
221
|
+
const decisionsTemplateRoot = path_1.default.join(templatesRoot, "decisions");
|
|
222
|
+
const decisionsTargetRoot = path_1.default.join(targetDir, "architecture", "decisions");
|
|
223
|
+
if (!(await fs_extra_1.default.pathExists(decisionsTemplateRoot))) {
|
|
224
|
+
throw new Error(`Missing decision templates at ${decisionsTemplateRoot}`);
|
|
225
|
+
}
|
|
226
|
+
await fs_extra_1.default.ensureDir(decisionsTargetRoot);
|
|
227
|
+
const entries = await fs_extra_1.default.readdir(decisionsTemplateRoot);
|
|
228
|
+
for (const entry of entries) {
|
|
229
|
+
const sourcePath = path_1.default.join(decisionsTemplateRoot, entry);
|
|
230
|
+
const targetPath = path_1.default.join(decisionsTargetRoot, entry);
|
|
231
|
+
if (await fs_extra_1.default.pathExists(targetPath)) {
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
234
|
+
await fs_extra_1.default.copy(sourcePath, targetPath, { overwrite: false });
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
async function scaffoldGapClosureTemplates(targetDir) {
|
|
238
|
+
const templatesRoot = getTemplatesRoot();
|
|
239
|
+
const gapClosureTemplateRoot = path_1.default.join(templatesRoot, "gap-closure");
|
|
240
|
+
const gapClosureTargetRoot = path_1.default.join(targetDir, "architecture", "reference");
|
|
241
|
+
if (!(await fs_extra_1.default.pathExists(gapClosureTemplateRoot))) {
|
|
242
|
+
throw new Error(`Missing gap-closure templates at ${gapClosureTemplateRoot}`);
|
|
243
|
+
}
|
|
244
|
+
await fs_extra_1.default.ensureDir(gapClosureTargetRoot);
|
|
245
|
+
const entries = await fs_extra_1.default.readdir(gapClosureTemplateRoot);
|
|
246
|
+
for (const entry of entries) {
|
|
247
|
+
const sourcePath = path_1.default.join(gapClosureTemplateRoot, entry);
|
|
248
|
+
const targetPath = path_1.default.join(gapClosureTargetRoot, entry);
|
|
249
|
+
if (await fs_extra_1.default.pathExists(targetPath)) {
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
await fs_extra_1.default.copy(sourcePath, targetPath, { overwrite: false });
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
async function scaffoldValidationHooks(targetDir) {
|
|
256
|
+
const templatesRoot = getTemplatesRoot();
|
|
257
|
+
const validationTemplateRoot = path_1.default.join(templatesRoot, "validation-hooks");
|
|
258
|
+
if (!(await fs_extra_1.default.pathExists(validationTemplateRoot))) {
|
|
259
|
+
throw new Error(`Missing validation hook templates at ${validationTemplateRoot}`);
|
|
260
|
+
}
|
|
261
|
+
const entries = await fs_extra_1.default.readdir(validationTemplateRoot);
|
|
262
|
+
for (const entry of entries) {
|
|
263
|
+
const sourcePath = path_1.default.join(validationTemplateRoot, entry);
|
|
264
|
+
const targetPath = path_1.default.join(targetDir, entry);
|
|
265
|
+
await copyMissingEntries(sourcePath, targetPath);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
async function copyMissingEntries(sourcePath, targetPath) {
|
|
269
|
+
const sourceStats = await fs_extra_1.default.stat(sourcePath);
|
|
270
|
+
if (!sourceStats.isDirectory()) {
|
|
271
|
+
if (!(await fs_extra_1.default.pathExists(targetPath))) {
|
|
272
|
+
await fs_extra_1.default.copy(sourcePath, targetPath, { overwrite: false });
|
|
273
|
+
}
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
await fs_extra_1.default.ensureDir(targetPath);
|
|
277
|
+
const children = await fs_extra_1.default.readdir(sourcePath);
|
|
278
|
+
for (const child of children) {
|
|
279
|
+
await copyMissingEntries(path_1.default.join(sourcePath, child), path_1.default.join(targetPath, child));
|
|
280
|
+
}
|
|
281
|
+
}
|
|
138
282
|
async function upsertArchModulesInMap(targetDir) {
|
|
139
283
|
const modulesPath = path_1.default.join(targetDir, "arch-model", "modules.json");
|
|
140
284
|
if (!(await fs_extra_1.default.pathExists(modulesPath))) {
|
|
@@ -213,6 +357,13 @@ async function main() {
|
|
|
213
357
|
}
|
|
214
358
|
}
|
|
215
359
|
await runPaInit(targetDir, options);
|
|
360
|
+
await scaffoldFoundationDocs(targetDir);
|
|
361
|
+
await scaffoldDomainSpecs(targetDir);
|
|
362
|
+
await scaffoldArchitectureSpecs(targetDir);
|
|
363
|
+
await scaffoldConceptMap(targetDir);
|
|
364
|
+
await scaffoldDecisionRecords(targetDir);
|
|
365
|
+
await scaffoldGapClosureTemplates(targetDir);
|
|
366
|
+
await scaffoldValidationHooks(targetDir);
|
|
216
367
|
await scaffoldArchitectureApps(targetDir);
|
|
217
368
|
await upsertArchModulesInMap(targetDir);
|
|
218
369
|
await wireProjectArchUsage(targetDir);
|
package/dist/cli.test.js
CHANGED
|
@@ -1,8 +1,199 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
2
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
7
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
3
8
|
const vitest_1 = require("vitest");
|
|
9
|
+
const currentDir = __dirname;
|
|
10
|
+
const archUiEslintConfigPath = node_path_1.default.resolve(currentDir, "../templates/arch-ui/eslint.config.js");
|
|
11
|
+
const foundationTemplateDir = node_path_1.default.resolve(currentDir, "../templates/foundation");
|
|
12
|
+
const domainTemplateDir = node_path_1.default.resolve(currentDir, "../templates/domains");
|
|
13
|
+
const architectureSpecTemplateDir = node_path_1.default.resolve(currentDir, "../templates/architecture-specs");
|
|
14
|
+
const conceptMapTemplatePath = node_path_1.default.resolve(currentDir, "../templates/concept-map/concept-map.json");
|
|
15
|
+
const decisionTemplateDir = node_path_1.default.resolve(currentDir, "../templates/decisions");
|
|
16
|
+
const gapClosureTemplateDir = node_path_1.default.resolve(currentDir, "../templates/gap-closure");
|
|
17
|
+
const validationHookTemplateDir = node_path_1.default.resolve(currentDir, "../templates/validation-hooks");
|
|
18
|
+
const foundationTemplateExpectations = [
|
|
19
|
+
{ fileName: "prompt.md", sectionHeader: "## Source Prompt" },
|
|
20
|
+
{ fileName: "project-overview.md", sectionHeader: "## Problem Statement" },
|
|
21
|
+
{ fileName: "goals.md", sectionHeader: "## Primary Goals" },
|
|
22
|
+
{ fileName: "user-journey.md", sectionHeader: "## Journey Steps" },
|
|
23
|
+
{ fileName: "scope.md", sectionHeader: "## In Scope" },
|
|
24
|
+
];
|
|
25
|
+
const domainTemplateExpectations = [
|
|
26
|
+
{ fileName: "core.md", sectionHeader: "## Responsibilities" },
|
|
27
|
+
{ fileName: "ui.md", sectionHeader: "## Responsibilities" },
|
|
28
|
+
{ fileName: "api.md", sectionHeader: "## Responsibilities" },
|
|
29
|
+
];
|
|
30
|
+
const architectureSpecTemplateSections = [
|
|
31
|
+
"## Purpose",
|
|
32
|
+
"## Scope",
|
|
33
|
+
"## Key Definitions",
|
|
34
|
+
"## Design",
|
|
35
|
+
"## Data Model",
|
|
36
|
+
"## Owning Domain",
|
|
37
|
+
"## MVP Constraints",
|
|
38
|
+
];
|
|
39
|
+
const decisionTemplateSections = [
|
|
40
|
+
"## Context",
|
|
41
|
+
"## Decision",
|
|
42
|
+
"## Rationale",
|
|
43
|
+
"## Alternatives Considered",
|
|
44
|
+
"## Affected Artifacts",
|
|
45
|
+
"## Implementation Status Checklist",
|
|
46
|
+
];
|
|
47
|
+
const gapClosureTemplateSections = [
|
|
48
|
+
"## Executive Summary",
|
|
49
|
+
"## Gap Categories And Resolutions",
|
|
50
|
+
"## Layer Synchronization Check",
|
|
51
|
+
"## Coverage Audit",
|
|
52
|
+
"## Remaining Gaps And Follow-On Items",
|
|
53
|
+
"## Template Improvement Feedback",
|
|
54
|
+
];
|
|
55
|
+
function getImportedNextJsSymbol(source) {
|
|
56
|
+
const match = source.match(/import\s+\{\s*([A-Za-z_$][A-Za-z0-9_$]*)\s*\}\s+from\s+["']@repo\/eslint-config\/next-js["']/);
|
|
57
|
+
return match?.[1] ?? null;
|
|
58
|
+
}
|
|
59
|
+
function getDefaultExportedSymbol(source) {
|
|
60
|
+
const match = source.match(/export\s+default\s+([A-Za-z_$][A-Za-z0-9_$]*)\s*;?/);
|
|
61
|
+
return match?.[1] ?? null;
|
|
62
|
+
}
|
|
4
63
|
(0, vitest_1.describe)("create-project-arch CLI", () => {
|
|
5
64
|
(0, vitest_1.it)("runs test suite successfully", () => {
|
|
6
65
|
(0, vitest_1.expect)(true).toBe(true);
|
|
7
66
|
});
|
|
67
|
+
(0, vitest_1.it)("keeps arch-ui eslint template wired to @repo/eslint-config/next-js export", () => {
|
|
68
|
+
const archUiConfigSource = node_fs_1.default.readFileSync(archUiEslintConfigPath, "utf8");
|
|
69
|
+
const importedSymbol = getImportedNextJsSymbol(archUiConfigSource);
|
|
70
|
+
const defaultExportedSymbol = getDefaultExportedSymbol(archUiConfigSource);
|
|
71
|
+
(0, vitest_1.expect)(importedSymbol).toBeTruthy();
|
|
72
|
+
(0, vitest_1.expect)(defaultExportedSymbol).toBeTruthy();
|
|
73
|
+
(0, vitest_1.expect)(defaultExportedSymbol).toBe(importedSymbol);
|
|
74
|
+
(0, vitest_1.expect)(importedSymbol).toBe("nextJsConfig");
|
|
75
|
+
});
|
|
76
|
+
(0, vitest_1.it)("includes all foundation document templates with structured guidance", () => {
|
|
77
|
+
for (const expectation of foundationTemplateExpectations) {
|
|
78
|
+
const templatePath = node_path_1.default.join(foundationTemplateDir, expectation.fileName);
|
|
79
|
+
(0, vitest_1.expect)(node_fs_1.default.existsSync(templatePath)).toBe(true);
|
|
80
|
+
const templateSource = node_fs_1.default.readFileSync(templatePath, "utf8");
|
|
81
|
+
(0, vitest_1.expect)(templateSource).toContain(expectation.sectionHeader);
|
|
82
|
+
(0, vitest_1.expect)(templateSource).toContain("<!-- Guidance:");
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
(0, vitest_1.it)("includes domain spec templates with ownership and milestone mapping guidance", () => {
|
|
86
|
+
const domainsJsonPath = node_path_1.default.join(domainTemplateDir, "domains.json");
|
|
87
|
+
const domainTemplatePath = node_path_1.default.join(domainTemplateDir, "DOMAIN_TEMPLATE.md");
|
|
88
|
+
const domainReadmePath = node_path_1.default.join(domainTemplateDir, "README.md");
|
|
89
|
+
(0, vitest_1.expect)(node_fs_1.default.existsSync(domainReadmePath)).toBe(true);
|
|
90
|
+
(0, vitest_1.expect)(node_fs_1.default.readFileSync(domainReadmePath, "utf8")).toContain("domain boundaries");
|
|
91
|
+
(0, vitest_1.expect)(node_fs_1.default.existsSync(domainsJsonPath)).toBe(true);
|
|
92
|
+
const domainsJson = JSON.parse(node_fs_1.default.readFileSync(domainsJsonPath, "utf8"));
|
|
93
|
+
(0, vitest_1.expect)(Array.isArray(domainsJson.domains)).toBe(true);
|
|
94
|
+
(0, vitest_1.expect)(domainsJson.domains?.map((domain) => domain.name)).toEqual(["core", "ui", "api"]);
|
|
95
|
+
(0, vitest_1.expect)(node_fs_1.default.existsSync(domainTemplatePath)).toBe(true);
|
|
96
|
+
const domainTemplateSource = node_fs_1.default.readFileSync(domainTemplatePath, "utf8");
|
|
97
|
+
(0, vitest_1.expect)(domainTemplateSource).toContain("## Responsibilities");
|
|
98
|
+
(0, vitest_1.expect)(domainTemplateSource).toContain("## Primary Data Ownership");
|
|
99
|
+
(0, vitest_1.expect)(domainTemplateSource).toContain("## Interface Contracts");
|
|
100
|
+
(0, vitest_1.expect)(domainTemplateSource).toContain("## Non-Goals");
|
|
101
|
+
(0, vitest_1.expect)(domainTemplateSource).toContain("## Milestone Mapping");
|
|
102
|
+
for (const expectation of domainTemplateExpectations) {
|
|
103
|
+
const templatePath = node_path_1.default.join(domainTemplateDir, expectation.fileName);
|
|
104
|
+
(0, vitest_1.expect)(node_fs_1.default.existsSync(templatePath)).toBe(true);
|
|
105
|
+
const templateSource = node_fs_1.default.readFileSync(templatePath, "utf8");
|
|
106
|
+
(0, vitest_1.expect)(templateSource).toContain(expectation.sectionHeader);
|
|
107
|
+
(0, vitest_1.expect)(templateSource).toContain("## Milestone Mapping");
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
(0, vitest_1.it)("includes reusable architecture spec template and example", () => {
|
|
111
|
+
const specTemplatePath = node_path_1.default.join(architectureSpecTemplateDir, "SPEC_TEMPLATE.md");
|
|
112
|
+
const exampleSpecPath = node_path_1.default.join(architectureSpecTemplateDir, "example-system.md");
|
|
113
|
+
(0, vitest_1.expect)(node_fs_1.default.existsSync(specTemplatePath)).toBe(true);
|
|
114
|
+
const specTemplateSource = node_fs_1.default.readFileSync(specTemplatePath, "utf8");
|
|
115
|
+
for (const section of architectureSpecTemplateSections) {
|
|
116
|
+
(0, vitest_1.expect)(specTemplateSource).toContain(section);
|
|
117
|
+
}
|
|
118
|
+
(0, vitest_1.expect)(specTemplateSource).toContain("<!-- Guidance:");
|
|
119
|
+
(0, vitest_1.expect)(node_fs_1.default.existsSync(exampleSpecPath)).toBe(true);
|
|
120
|
+
const exampleSpecSource = node_fs_1.default.readFileSync(exampleSpecPath, "utf8");
|
|
121
|
+
(0, vitest_1.expect)(exampleSpecSource).toContain("# Example System Specification");
|
|
122
|
+
(0, vitest_1.expect)(exampleSpecSource).toContain("## Purpose");
|
|
123
|
+
(0, vitest_1.expect)(exampleSpecSource).toContain("## MVP Constraints");
|
|
124
|
+
});
|
|
125
|
+
(0, vitest_1.it)("includes concept-map template with traceability schema placeholders", () => {
|
|
126
|
+
(0, vitest_1.expect)(node_fs_1.default.existsSync(conceptMapTemplatePath)).toBe(true);
|
|
127
|
+
const conceptMap = JSON.parse(node_fs_1.default.readFileSync(conceptMapTemplatePath, "utf8"));
|
|
128
|
+
(0, vitest_1.expect)(conceptMap.schemaVersion).toBe("1.0");
|
|
129
|
+
(0, vitest_1.expect)(Array.isArray(conceptMap.concepts)).toBe(true);
|
|
130
|
+
(0, vitest_1.expect)((conceptMap.concepts ?? []).length).toBeGreaterThanOrEqual(2);
|
|
131
|
+
const firstConcept = conceptMap.concepts?.[0] ?? {};
|
|
132
|
+
(0, vitest_1.expect)(firstConcept).toHaveProperty("id");
|
|
133
|
+
(0, vitest_1.expect)(firstConcept).toHaveProperty("name");
|
|
134
|
+
(0, vitest_1.expect)(firstConcept).toHaveProperty("description");
|
|
135
|
+
(0, vitest_1.expect)(firstConcept).toHaveProperty("owningDomain");
|
|
136
|
+
(0, vitest_1.expect)(firstConcept).toHaveProperty("moduleResponsibilities");
|
|
137
|
+
(0, vitest_1.expect)(firstConcept).toHaveProperty("implementationSurfaces");
|
|
138
|
+
(0, vitest_1.expect)(firstConcept).toHaveProperty("dependencies");
|
|
139
|
+
(0, vitest_1.expect)(Array.isArray(conceptMap.domainModuleMapping)).toBe(true);
|
|
140
|
+
(0, vitest_1.expect)(Array.isArray(conceptMap.implementationChecklist)).toBe(true);
|
|
141
|
+
});
|
|
142
|
+
(0, vitest_1.it)("includes decision record template and example with structured frontmatter", () => {
|
|
143
|
+
const decisionTemplatePath = node_path_1.default.join(decisionTemplateDir, "DECISION_TEMPLATE.md");
|
|
144
|
+
const decisionExamplePath = node_path_1.default.join(decisionTemplateDir, "example-decision.md");
|
|
145
|
+
const decisionReadmePath = node_path_1.default.join(decisionTemplateDir, "README.md");
|
|
146
|
+
(0, vitest_1.expect)(node_fs_1.default.existsSync(decisionReadmePath)).toBe(true);
|
|
147
|
+
(0, vitest_1.expect)(node_fs_1.default.readFileSync(decisionReadmePath, "utf8")).toContain("pa decision new");
|
|
148
|
+
(0, vitest_1.expect)(node_fs_1.default.existsSync(decisionTemplatePath)).toBe(true);
|
|
149
|
+
const decisionTemplateSource = node_fs_1.default.readFileSync(decisionTemplatePath, "utf8");
|
|
150
|
+
(0, vitest_1.expect)(decisionTemplateSource).toContain("---");
|
|
151
|
+
(0, vitest_1.expect)(decisionTemplateSource).toContain("id:");
|
|
152
|
+
(0, vitest_1.expect)(decisionTemplateSource).toContain("title:");
|
|
153
|
+
(0, vitest_1.expect)(decisionTemplateSource).toContain("slug:");
|
|
154
|
+
(0, vitest_1.expect)(decisionTemplateSource).toContain("status:");
|
|
155
|
+
(0, vitest_1.expect)(decisionTemplateSource).toContain("createdAt:");
|
|
156
|
+
(0, vitest_1.expect)(decisionTemplateSource).toContain("updatedAt:");
|
|
157
|
+
(0, vitest_1.expect)(decisionTemplateSource).toContain("relatedTasks:");
|
|
158
|
+
(0, vitest_1.expect)(decisionTemplateSource).toContain("relatedDocs:");
|
|
159
|
+
for (const section of decisionTemplateSections) {
|
|
160
|
+
(0, vitest_1.expect)(decisionTemplateSource).toContain(section);
|
|
161
|
+
}
|
|
162
|
+
(0, vitest_1.expect)(node_fs_1.default.existsSync(decisionExamplePath)).toBe(true);
|
|
163
|
+
const decisionExampleSource = node_fs_1.default.readFileSync(decisionExamplePath, "utf8");
|
|
164
|
+
(0, vitest_1.expect)(decisionExampleSource).toContain('status: "accepted"');
|
|
165
|
+
(0, vitest_1.expect)(decisionExampleSource).toContain("## Decision");
|
|
166
|
+
(0, vitest_1.expect)(decisionExampleSource).toContain("## Alternatives Considered");
|
|
167
|
+
});
|
|
168
|
+
(0, vitest_1.it)("includes milestone gap-closure report template and example", () => {
|
|
169
|
+
const gapClosureTemplatePath = node_path_1.default.join(gapClosureTemplateDir, "GAP_CLOSURE_TEMPLATE.md");
|
|
170
|
+
const gapClosureExamplePath = node_path_1.default.join(gapClosureTemplateDir, "example-gap-closure.md");
|
|
171
|
+
const gapClosureReadmePath = node_path_1.default.join(gapClosureTemplateDir, "README.md");
|
|
172
|
+
(0, vitest_1.expect)(node_fs_1.default.existsSync(gapClosureReadmePath)).toBe(true);
|
|
173
|
+
(0, vitest_1.expect)(node_fs_1.default.readFileSync(gapClosureReadmePath, "utf8")).toContain("pa check");
|
|
174
|
+
(0, vitest_1.expect)(node_fs_1.default.existsSync(gapClosureTemplatePath)).toBe(true);
|
|
175
|
+
const gapClosureTemplateSource = node_fs_1.default.readFileSync(gapClosureTemplatePath, "utf8");
|
|
176
|
+
for (const section of gapClosureTemplateSections) {
|
|
177
|
+
(0, vitest_1.expect)(gapClosureTemplateSource).toContain(section);
|
|
178
|
+
}
|
|
179
|
+
(0, vitest_1.expect)(gapClosureTemplateSource).toContain("- [ ]");
|
|
180
|
+
(0, vitest_1.expect)(node_fs_1.default.existsSync(gapClosureExamplePath)).toBe(true);
|
|
181
|
+
const gapClosureExampleSource = node_fs_1.default.readFileSync(gapClosureExamplePath, "utf8");
|
|
182
|
+
(0, vitest_1.expect)(gapClosureExampleSource).toContain("# Milestone Gap-Closure Report - Example");
|
|
183
|
+
(0, vitest_1.expect)(gapClosureExampleSource).toContain("## Layer Synchronization Check");
|
|
184
|
+
(0, vitest_1.expect)(gapClosureExampleSource).toContain("- [x]");
|
|
185
|
+
});
|
|
186
|
+
(0, vitest_1.it)("includes local validation hook script and pre-commit example", () => {
|
|
187
|
+
const validateScriptPath = node_path_1.default.join(validationHookTemplateDir, "scripts", "validate.sh");
|
|
188
|
+
const preCommitPath = node_path_1.default.join(validationHookTemplateDir, ".githooks", "pre-commit");
|
|
189
|
+
const readmePath = node_path_1.default.join(validationHookTemplateDir, "README.md");
|
|
190
|
+
(0, vitest_1.expect)(node_fs_1.default.existsSync(validateScriptPath)).toBe(true);
|
|
191
|
+
const validateScriptSource = node_fs_1.default.readFileSync(validateScriptPath, "utf8");
|
|
192
|
+
(0, vitest_1.expect)(validateScriptSource).toContain("pnpm arch:check");
|
|
193
|
+
(0, vitest_1.expect)(validateScriptSource).toContain("pnpm arch:report");
|
|
194
|
+
(0, vitest_1.expect)(node_fs_1.default.existsSync(preCommitPath)).toBe(true);
|
|
195
|
+
(0, vitest_1.expect)(node_fs_1.default.readFileSync(preCommitPath, "utf8")).toContain("sh scripts/validate.sh");
|
|
196
|
+
(0, vitest_1.expect)(node_fs_1.default.existsSync(readmePath)).toBe(true);
|
|
197
|
+
(0, vitest_1.expect)(node_fs_1.default.readFileSync(readmePath, "utf8")).toContain("Task Verification Guidance");
|
|
198
|
+
});
|
|
8
199
|
});
|
package/package.json
CHANGED
|
@@ -1,19 +1,43 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-project-arch",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "Scaffold
|
|
3
|
+
"version": "1.3.0",
|
|
4
|
+
"description": "Scaffold new projects with Project Arch templates including Next.js apps and component libraries",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"create",
|
|
7
|
+
"scaffold",
|
|
8
|
+
"template",
|
|
9
|
+
"project-arch",
|
|
10
|
+
"nextjs",
|
|
11
|
+
"react",
|
|
12
|
+
"turborepo",
|
|
13
|
+
"monorepo"
|
|
14
|
+
],
|
|
15
|
+
"homepage": "https://github.com/project-arch/project-arch-system#readme",
|
|
16
|
+
"bugs": {
|
|
17
|
+
"url": "https://github.com/project-arch/project-arch-system/issues"
|
|
18
|
+
},
|
|
19
|
+
"repository": {
|
|
20
|
+
"type": "git",
|
|
21
|
+
"url": "git+https://github.com/project-arch/project-arch-system.git",
|
|
22
|
+
"directory": "packages/create-project-arch"
|
|
23
|
+
},
|
|
24
|
+
"license": "MIT",
|
|
25
|
+
"author": "Project Arch Contributors",
|
|
5
26
|
"bin": {
|
|
6
27
|
"create-project-arch": "dist/cli.js"
|
|
7
28
|
},
|
|
8
29
|
"main": "dist/cli.js",
|
|
9
30
|
"files": [
|
|
10
31
|
"dist",
|
|
11
|
-
"templates"
|
|
32
|
+
"templates",
|
|
33
|
+
"README.md",
|
|
34
|
+
"LICENSE",
|
|
35
|
+
"CHANGELOG.md"
|
|
12
36
|
],
|
|
13
37
|
"dependencies": {
|
|
14
38
|
"commander": "^12.1.0",
|
|
15
39
|
"fs-extra": "^11.3.0",
|
|
16
|
-
"project-arch": "^1.
|
|
40
|
+
"project-arch": "^1.3.0"
|
|
17
41
|
},
|
|
18
42
|
"devDependencies": {
|
|
19
43
|
"@types/fs-extra": "^11.0.4",
|
|
@@ -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("/");
|