create-project-arch 1.3.1 → 1.5.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 CHANGED
@@ -5,6 +5,37 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [1.5.0] - 2026-03-12
9
+
10
+ ### Added
11
+
12
+ - Scaffolded projects now receive 8 bootstrap architecture tasks on `pa init` (previously 5)
13
+ - New bootstrap tasks 006 `define-system-boundaries`, 007 `define-module-model`, and 008 `define-runtime-architecture` included in generated milestone
14
+ - Bootstrap tasks 006–008 carry `discover` and `greenfield` tags for dual-mode authoring guidance
15
+ - `pa milestone status` command available in all scaffolded projects for dependency-aware task blocking visibility
16
+
17
+ ### Changed
18
+
19
+ - Updated templates to use `project-arch@1.5.0`
20
+ - Bootstrap task 005 slug updated to `finalize-architecture-foundation` in all scaffolded projects
21
+ - task 005 in generated projects now declares `dependsOn` on tasks 001–004 and 006–008, enforcing correct completion order
22
+
23
+ ---
24
+
25
+ ## [1.4.0] - 2026-03-10
26
+
27
+ ### Added
28
+
29
+ - Feedback system integration in scaffolded projects (`.arch/feedback/` structure auto-created)
30
+ - Enhanced validation-hook scaffolds with feedback collection support
31
+ - Feedback system documentation in generated architecture governance section
32
+
33
+ ### Changed
34
+
35
+ - Updated templates to include latest `project-arch@1.4.0` features
36
+ - Template validation now includes feedback CLI command testing
37
+ - Improved scaffolding documentation to cover feedback workflow
38
+
8
39
  ## [1.3.0] - 2026-03-08
9
40
 
10
41
  ### Added
@@ -51,6 +82,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
51
82
  - Template customization options
52
83
  - Force mode for non-empty directories
53
84
 
85
+ [1.4.0]: https://github.com/MissTitanK3/project-arch-system/compare/v1.3.0...v1.4.0
86
+ [1.3.0]: https://github.com/MissTitanK3/project-arch-system/compare/v1.1.0...v1.3.0
54
87
  [1.1.0]: https://github.com/MissTitanK3/project-arch-system/compare/v1.0.0...v1.1.0
55
88
  [1.0.0]: https://github.com/MissTitanK3/project-arch-system/releases/tag/v1.0.0
56
- [1.3.0]: https://github.com/MissTitanK3/project-arch-system/compare/v1.1.0...v1.3.0
package/README.md CHANGED
@@ -223,9 +223,11 @@ Scaffolded projects include closure report artifacts under `architecture/referen
223
223
  Recommended workflow:
224
224
 
225
225
  1. Complete milestone tasks and decision updates.
226
- 2. Run `pa check` and `pa report`.
227
- 3. Record closure outcomes in milestone closure report.
228
- 4. Track remaining gaps as follow-on tasks/decisions.
226
+ 2. Run `pa lint frontmatter --fix`.
227
+ 3. Run `pnpm lint:md`.
228
+ 4. Run `pa check` and `pa report`.
229
+ 5. Record closure outcomes in milestone closure report.
230
+ 6. Track remaining gaps as follow-on tasks/decisions.
229
231
 
230
232
  ### Local Validation Hook Scaffold
231
233
 
package/dist/cli.js CHANGED
@@ -8,6 +8,7 @@ const path_1 = __importDefault(require("path"));
8
8
  const fs_extra_1 = __importDefault(require("fs-extra"));
9
9
  const child_process_1 = require("child_process");
10
10
  const commander_1 = require("commander");
11
+ const projectNameValidation_1 = require("./projectNameValidation");
11
12
  function getTemplatesRoot() {
12
13
  return path_1.default.resolve(__dirname, "../templates");
13
14
  }
@@ -104,6 +105,67 @@ async function wireProjectArchUsage(targetDir) {
104
105
  await fs_extra_1.default.writeJSON(rootPackageJsonPath, nextPkg, { spaces: 2 });
105
106
  await fs_extra_1.default.appendFile(rootPackageJsonPath, "\n");
106
107
  }
108
+ async function normalizeTurboSchema(targetDir) {
109
+ const turboConfigPath = path_1.default.join(targetDir, "turbo.json");
110
+ if (!(await fs_extra_1.default.pathExists(turboConfigPath))) {
111
+ return;
112
+ }
113
+ const turboConfig = (await fs_extra_1.default.readJSON(turboConfigPath));
114
+ const existingGlobalEnv = Array.isArray(turboConfig.globalEnv)
115
+ ? turboConfig.globalEnv.filter((entry) => typeof entry === "string")
116
+ : [];
117
+ const globalEnv = existingGlobalEnv.includes("PROJECT_ROOT")
118
+ ? existingGlobalEnv
119
+ : [...existingGlobalEnv, "PROJECT_ROOT"];
120
+ await fs_extra_1.default.writeJSON(turboConfigPath, {
121
+ ...turboConfig,
122
+ $schema: "./node_modules/turbo/schema.json",
123
+ globalEnv,
124
+ }, { spaces: 2 });
125
+ await fs_extra_1.default.appendFile(turboConfigPath, "\n");
126
+ }
127
+ function getAppReadmeContent(title, appDir) {
128
+ return [
129
+ `# ${title}`,
130
+ "",
131
+ `This app lives in \`${appDir}\` and is part of the generated workspace.`,
132
+ "",
133
+ "## Getting Started",
134
+ "",
135
+ "Run from the repository root:",
136
+ "",
137
+ "```bash",
138
+ `pnpm --filter ${appDir.split("/").pop()} dev`,
139
+ "```",
140
+ "",
141
+ "Or run all apps in dev mode:",
142
+ "",
143
+ "```bash",
144
+ "pnpm dev",
145
+ "```",
146
+ "",
147
+ ].join("\n");
148
+ }
149
+ async function normalizeGeneratedAppReadmes(targetDir) {
150
+ const readmeTargets = [
151
+ {
152
+ readmePath: path_1.default.join(targetDir, "apps", "web", "README.md"),
153
+ title: "Web App",
154
+ appDir: "apps/web",
155
+ },
156
+ {
157
+ readmePath: path_1.default.join(targetDir, "apps", "docs", "README.md"),
158
+ title: "Docs App",
159
+ appDir: "apps/docs",
160
+ },
161
+ ];
162
+ for (const target of readmeTargets) {
163
+ if (!(await fs_extra_1.default.pathExists(target.readmePath))) {
164
+ continue;
165
+ }
166
+ await fs_extra_1.default.writeFile(target.readmePath, getAppReadmeContent(target.title, target.appDir), "utf8");
167
+ }
168
+ }
107
169
  async function scaffoldArchitectureApps(targetDir) {
108
170
  const templatesRoot = getTemplatesRoot();
109
171
  const archUiTemplate = path_1.default.join(templatesRoot, "arch-ui");
@@ -339,9 +401,7 @@ async function main() {
339
401
  if (projectName === "." || projectName === "./") {
340
402
  throw new Error("Refusing to scaffold into current directory. Provide a new project directory name.");
341
403
  }
342
- if (!/^[a-zA-Z0-9-_]+$/.test(projectName)) {
343
- throw new Error("Invalid project name. Only alphanumeric characters, dashes, and underscores are allowed.");
344
- }
404
+ (0, projectNameValidation_1.validateProjectName)(projectName);
345
405
  const targetDir = path_1.default.resolve(process.cwd(), projectName);
346
406
  await ensureTargetDir(targetDir, options.force);
347
407
  runCreateTurbo(projectName, options);
@@ -367,6 +427,8 @@ async function main() {
367
427
  await scaffoldArchitectureApps(targetDir);
368
428
  await upsertArchModulesInMap(targetDir);
369
429
  await wireProjectArchUsage(targetDir);
430
+ await normalizeTurboSchema(targetDir);
431
+ await normalizeGeneratedAppReadmes(targetDir);
370
432
  const relativeTarget = path_1.default.relative(process.cwd(), targetDir) || ".";
371
433
  console.log(`\nCreated project-arch repo at ${relativeTarget}`);
372
434
  console.log("\nNext steps:");
@@ -0,0 +1,25 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.validateProjectName = validateProjectName;
4
+ const npmPackageNamePattern = /^[a-z0-9-~][a-z0-9-._~]*$/;
5
+ const npmReservedNames = new Set(["node_modules", "favicon.ico"]);
6
+ function toNpmNameSuggestion(projectName) {
7
+ return projectName
8
+ .trim()
9
+ .toLowerCase()
10
+ .replace(/[^a-z0-9-_]+/g, "-");
11
+ }
12
+ function validateProjectName(projectName) {
13
+ if (!/^[a-zA-Z0-9-_]+$/.test(projectName)) {
14
+ throw new Error("Invalid project name. Only alphanumeric characters, dashes, and underscores are allowed.");
15
+ }
16
+ if (projectName !== projectName.toLowerCase()) {
17
+ throw new Error(`Invalid project name '${projectName}'. Use lowercase to produce a valid npm package name (for example: '${toNpmNameSuggestion(projectName)}').`);
18
+ }
19
+ if (projectName.length > 214) {
20
+ throw new Error("Invalid project name. npm package names must be 214 characters or fewer.");
21
+ }
22
+ if (!npmPackageNamePattern.test(projectName) || npmReservedNames.has(projectName)) {
23
+ throw new Error(`Invalid project name '${projectName}'. It must be a valid npm package name.`);
24
+ }
25
+ }
@@ -0,0 +1,19 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const vitest_1 = require("vitest");
4
+ const projectNameValidation_1 = require("./projectNameValidation");
5
+ (0, vitest_1.describe)("validateProjectName", () => {
6
+ (0, vitest_1.it)("accepts valid lowercase project names", () => {
7
+ (0, vitest_1.expect)(() => (0, projectNameValidation_1.validateProjectName)("project-arch")).not.toThrow();
8
+ (0, vitest_1.expect)(() => (0, projectNameValidation_1.validateProjectName)("project_arch_2")).not.toThrow();
9
+ });
10
+ (0, vitest_1.it)("rejects uppercase names with actionable guidance", () => {
11
+ (0, vitest_1.expect)(() => (0, projectNameValidation_1.validateProjectName)("testProject")).toThrow("Use lowercase to produce a valid npm package name");
12
+ });
13
+ (0, vitest_1.it)("rejects invalid characters", () => {
14
+ (0, vitest_1.expect)(() => (0, projectNameValidation_1.validateProjectName)("my project")).toThrow("Only alphanumeric characters, dashes, and underscores are allowed.");
15
+ });
16
+ (0, vitest_1.it)("rejects npm reserved names", () => {
17
+ (0, vitest_1.expect)(() => (0, projectNameValidation_1.validateProjectName)("node_modules")).toThrow("It must be a valid npm package name.");
18
+ });
19
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-project-arch",
3
- "version": "1.3.1",
3
+ "version": "1.5.0",
4
4
  "description": "Scaffold new projects with Project Arch templates including Next.js apps and component libraries",
5
5
  "keywords": [
6
6
  "create",
@@ -37,7 +37,7 @@
37
37
  "dependencies": {
38
38
  "commander": "^12.1.0",
39
39
  "fs-extra": "^11.3.0",
40
- "project-arch": "^1.3.0"
40
+ "project-arch": "^1.5.0"
41
41
  },
42
42
  "devDependencies": {
43
43
  "@types/fs-extra": "^11.0.4",
@@ -118,7 +118,7 @@ export default function WorkPage() {
118
118
  (Object.entries(filters.edgeTypes) as Array<[keyof typeof filters.edgeTypes, boolean]>)
119
119
  .filter(([, enabled]) => enabled)
120
120
  .map(([filter]) => filter as GraphEdgeFilter),
121
- [filters.edgeTypes],
121
+ [filters],
122
122
  );
123
123
  const activeAuthorityFilters = useMemo(
124
124
  () =>
@@ -129,7 +129,7 @@ export default function WorkPage() {
129
129
  )
130
130
  .filter(([, enabled]) => enabled)
131
131
  .map(([filter]) => filter),
132
- [filters.authorityTypes],
132
+ [filters],
133
133
  );
134
134
 
135
135
  return (
@@ -59,6 +59,12 @@ type GraphCanvasProps = {
59
59
 
60
60
  export { type GraphFilter, type InspectorNode } from "./graph/graph-types";
61
61
 
62
+ const VIEW_CANONICAL_TYPES: Record<GraphCanvasViewMode, string[]> = {
63
+ "architecture-map": ["arch_folder", "domain_doc", "architecture_doc", "architecture_model"],
64
+ tasks: ["roadmap_folder", "roadmap_epic", "roadmap_story", "roadmap_task"],
65
+ project: ["project_folder", "app", "package", "module", "component"],
66
+ };
67
+
62
68
  export function GraphCanvas({
63
69
  data,
64
70
  viewMode,
@@ -124,11 +130,6 @@ export function GraphCanvas({
124
130
  }, [nodes]);
125
131
 
126
132
  const behavior = GRAPH_VIEW_BEHAVIOR[viewMode];
127
- const viewCanonicalTypes: Record<GraphCanvasViewMode, string[]> = {
128
- "architecture-map": ["arch_folder", "domain_doc", "architecture_doc", "architecture_model"],
129
- tasks: ["roadmap_folder", "roadmap_epic", "roadmap_story", "roadmap_task"],
130
- project: ["project_folder", "app", "package", "module", "component"],
131
- };
132
133
  const effectiveHopDepth = useMemo(() => {
133
134
  const min = Math.min(...behavior.allowedHopDepths);
134
135
  const max = Math.max(...behavior.allowedHopDepths);
@@ -507,7 +508,7 @@ export function GraphCanvas({
507
508
  if (!stored) return;
508
509
  if (selectedNodeId) return;
509
510
  const selected = nodes.find((node) => node.id === stored);
510
- if (selected && viewCanonicalTypes[viewMode].includes(selected.data.canonicalType ?? "")) {
511
+ if (selected && VIEW_CANONICAL_TYPES[viewMode].includes(selected.data.canonicalType ?? "")) {
511
512
  setSelectedNodeId(stored);
512
513
  return;
513
514
  }
@@ -516,7 +517,7 @@ export function GraphCanvas({
516
517
  const candidateId = mapped.source === stored ? mapped.target : mapped.source;
517
518
  const candidateNode = nodes.find((node) => node.id === candidateId);
518
519
  if (!candidateNode) return;
519
- if (!viewCanonicalTypes[viewMode].includes(candidateNode.data.canonicalType ?? "")) return;
520
+ if (!VIEW_CANONICAL_TYPES[viewMode].includes(candidateNode.data.canonicalType ?? "")) return;
520
521
  setSelectedNodeId(candidateId);
521
522
  }, [edges, nodes, selectedNodeId, viewMode]);
522
523
 
@@ -18,15 +18,6 @@ type WorkspaceFilters = {
18
18
  hopDepth: number;
19
19
  };
20
20
 
21
- type WorkspacePersistedState = {
22
- splitPane: boolean;
23
- leftCollapsed: boolean;
24
- rightCollapsed: boolean;
25
- leftWidth: number;
26
- rightWidth: number;
27
- filtersByView: Record<GraphViewMode, WorkspaceFilters>;
28
- };
29
-
30
21
  type WorkspaceContextValue = {
31
22
  splitPane: boolean;
32
23
  leftCollapsed: boolean;
@@ -127,36 +118,38 @@ export function WorkspaceProvider({ children }: { children: ReactNode }) {
127
118
  if (typeof parsed.rightWidth === "number") setRightWidth(clamp(parsed.rightWidth, 320, 720));
128
119
  if (parsed.filtersByView) {
129
120
  const views: GraphViewMode[] = ["architecture-map", "tasks", "project"];
130
- const nextByView = { ...filtersByView };
131
- for (const view of views) {
132
- const incoming = parsed.filtersByView[view];
133
- if (!incoming) continue;
134
- const base = defaultFilters();
135
- nextByView[view] = {
136
- nodeTypes: {
137
- domains: incoming.nodeTypes?.domains ?? base.nodeTypes.domains,
138
- modules: incoming.nodeTypes?.modules ?? base.nodeTypes.modules,
139
- tasks: incoming.nodeTypes?.tasks ?? base.nodeTypes.tasks,
140
- decisions: incoming.nodeTypes?.decisions ?? base.nodeTypes.decisions,
141
- },
142
- edgeTypes: {
143
- dependency: incoming.edgeTypes?.dependency ?? base.edgeTypes.dependency,
144
- "data-flow": incoming.edgeTypes?.["data-flow"] ?? base.edgeTypes["data-flow"],
145
- blocking: incoming.edgeTypes?.blocking ?? base.edgeTypes.blocking,
146
- },
147
- authorityTypes: {
148
- authoritative:
149
- incoming.authorityTypes?.authoritative ?? base.authorityTypes.authoritative,
150
- manual: incoming.authorityTypes?.manual ?? base.authorityTypes.manual,
151
- inferred: incoming.authorityTypes?.inferred ?? base.authorityTypes.inferred,
152
- },
153
- hideCompletedTasks: incoming.hideCompletedTasks ?? base.hideCompletedTasks,
154
- showExternalDependencies:
155
- incoming.showExternalDependencies ?? base.showExternalDependencies,
156
- hopDepth: clamp(incoming.hopDepth ?? base.hopDepth, 0, 3),
157
- };
158
- }
159
- setFiltersByView(nextByView);
121
+ setFiltersByView((prev) => {
122
+ const nextByView = { ...prev };
123
+ for (const view of views) {
124
+ const incoming = parsed.filtersByView?.[view];
125
+ if (!incoming) continue;
126
+ const base = defaultFilters();
127
+ nextByView[view] = {
128
+ nodeTypes: {
129
+ domains: incoming.nodeTypes?.domains ?? base.nodeTypes.domains,
130
+ modules: incoming.nodeTypes?.modules ?? base.nodeTypes.modules,
131
+ tasks: incoming.nodeTypes?.tasks ?? base.nodeTypes.tasks,
132
+ decisions: incoming.nodeTypes?.decisions ?? base.nodeTypes.decisions,
133
+ },
134
+ edgeTypes: {
135
+ dependency: incoming.edgeTypes?.dependency ?? base.edgeTypes.dependency,
136
+ "data-flow": incoming.edgeTypes?.["data-flow"] ?? base.edgeTypes["data-flow"],
137
+ blocking: incoming.edgeTypes?.blocking ?? base.edgeTypes.blocking,
138
+ },
139
+ authorityTypes: {
140
+ authoritative:
141
+ incoming.authorityTypes?.authoritative ?? base.authorityTypes.authoritative,
142
+ manual: incoming.authorityTypes?.manual ?? base.authorityTypes.manual,
143
+ inferred: incoming.authorityTypes?.inferred ?? base.authorityTypes.inferred,
144
+ },
145
+ hideCompletedTasks: incoming.hideCompletedTasks ?? base.hideCompletedTasks,
146
+ showExternalDependencies:
147
+ incoming.showExternalDependencies ?? base.showExternalDependencies,
148
+ hopDepth: clamp(incoming.hopDepth ?? base.hopDepth, 0, 3),
149
+ };
150
+ }
151
+ return nextByView;
152
+ });
160
153
  }
161
154
  } catch {
162
155
  // Ignore invalid persisted state.
@@ -1,2 +1,4 @@
1
- import { nextJsConfig } from '@repo/eslint-config/next-js';
1
+ import { nextJsConfig } from "@repo/eslint-config/next-js";
2
+
3
+ /** @type {import("eslint").Linter.Config[]} */
2
4
  export default nextJsConfig;
@@ -67,8 +67,6 @@ async function walkDirectories(
67
67
  onDirectory: (relativePath: string, parentRelativePath: string | null) => Promise<void> | void,
68
68
  maxDepth = 6,
69
69
  ): Promise<void> {
70
- const start = path.join(root, relativeDir);
71
-
72
70
  async function walk(currentRelative: string, parentRelative: string | null, depth: number) {
73
71
  if (depth > maxDepth) return;
74
72
  await onDirectory(currentRelative, parentRelative);
@@ -138,7 +136,7 @@ async function filterGitIgnoredPaths(root: string, paths: string[]): Promise<Set
138
136
  }
139
137
  }
140
138
  return ignored;
141
- } catch (error) {
139
+ } catch {
142
140
  // If git is unavailable or command fails unexpectedly, do not block graph generation.
143
141
  return new Set();
144
142
  }
@@ -1,6 +1,6 @@
1
1
  /// <reference types="next" />
2
2
  /// <reference types="next/image-types/global" />
3
- import "./.next/dev/types/routes.d.ts";
3
+ import "./.next/types/routes.d.ts";
4
4
 
5
5
  // NOTE: This file should not be edited
6
6
  // see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
@@ -35,3 +35,4 @@
35
35
  "typescript": "5.9.2"
36
36
  }
37
37
  }
38
+
@@ -23,6 +23,8 @@
23
23
 
24
24
  - [ ] Architecture docs updated
25
25
  - [ ] Roadmap tasks/decisions synchronized
26
+ - [ ] Frontmatter preflight lint passes (`pa lint frontmatter --fix`)
27
+ - [ ] Markdown lint passes (`pnpm lint:md`)
26
28
  - [ ] Graph/parity checks pass (`pa check`)
27
29
  - [ ] Report diagnostics reviewed (`pa report`)
28
30
 
@@ -10,8 +10,10 @@ Create/update a closure report when a milestone reaches completion readiness.
10
10
 
11
11
  1. Copy `GAP_CLOSURE_TEMPLATE.md` into the milestone or reference location.
12
12
  2. Fill each section with concrete findings and evidence.
13
- 3. Run validation commands (`pa check`, `pa report`) and record outcomes.
14
- 4. Track unresolved items as follow-on tasks/decisions.
13
+ 3. Run `pa lint frontmatter --fix`.
14
+ 4. Run `pnpm lint:md`.
15
+ 5. Run `pa check` and `pa report`, then record outcomes.
16
+ 6. Track unresolved items as follow-on tasks/decisions.
15
17
 
16
18
  ## Suggested Locations
17
19
 
@@ -19,6 +19,8 @@ Milestone scope is complete, critical architecture artifacts are synchronized, a
19
19
 
20
20
  - [x] Architecture docs updated
21
21
  - [x] Roadmap tasks/decisions synchronized
22
+ - [x] Frontmatter preflight lint passes (`pa lint frontmatter --fix`)
23
+ - [x] Markdown lint passes (`pnpm lint:md`)
22
24
  - [x] Graph/parity checks pass (`pa check`)
23
25
  - [x] Report diagnostics reviewed (`pa report`)
24
26