create-lumen 2.0.0-alpha.0 → 2.0.0-alpha.1
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/README.md +20 -5
- package/package.json +2 -1
- package/schema/fixtures/v2/invalid/architecture-preset-type-mismatch.json +35 -0
- package/schema/fixtures/v2/invalid/harness-empty-commands.json +37 -0
- package/schema/fixtures/v2/invalid/harness-missing-command.json +41 -0
- package/schema/fixtures/v2/invalid/invalid-validation-mode.json +34 -0
- package/schema/fixtures/v2/valid/harness-full.json +44 -0
- package/schema/fixtures/v2/valid/validation-none.json +41 -0
- package/schema/fixtures/v2/valid/validation-relaxed.json +41 -0
- package/schema/lumen.config.v2.json +54 -1
- package/skills/create-lumen/SKILL.md +110 -0
- package/skills/create-lumen/references/manifest-v2.md +9 -0
- package/src/agent-context.js +114 -0
- package/src/main.js +14 -0
- package/src/manifest/emit.js +49 -3
- package/src/manifest/schema.js +64 -6
package/README.md
CHANGED
|
@@ -72,7 +72,7 @@ create-lumen my-app # after npm link
|
|
|
72
72
|
- Optional testing: Vitest or Jest
|
|
73
73
|
- Linting: ESLint (default), Oxlint, or Biome
|
|
74
74
|
- Code formatter: Prettier or Oxfmt (chosen after the linter; `eslint-config-prettier` is auto-wired for the ESLint + Prettier combo)
|
|
75
|
-
- Manifest v2: Emits a deterministic `lumen.config.json` validated against JSON Schema Draft 2020-12
|
|
75
|
+
- Manifest v2: Emits a deterministic `lumen.config.json` (optional but recommended) validated against JSON Schema Draft 2020-12, including architecture validation modes and harness command configuration.
|
|
76
76
|
- Explicit path mapping: Target directories declared for `lumen-cli` code generation
|
|
77
77
|
- Optional Axios setup and icon libraries
|
|
78
78
|
- Auto `git init`, generated `README` (with a project description reflecting the chosen tooling) and `LICENSE`
|
|
@@ -187,15 +187,30 @@ src/
|
|
|
187
187
|
|
|
188
188
|
`create-lumen` is part of the Lumen ecosystem alongside **`lumen-cli`**:
|
|
189
189
|
|
|
190
|
-
- **`create-lumen` (this repo):** The bootstrap scaffolder that initializes project repositories and writes an explicit, strictly validated `lumen.config.json` manifest (`manifestVersion: 2`) into the project root.
|
|
191
|
-
- **`lumen-cli`:** The companion developer CLI that reads `lumen.config.json` to generate components, features, hooks, and
|
|
190
|
+
- **`create-lumen` (this repo):** The bootstrap scaffolder that initializes project repositories and writes an explicit, strictly validated `lumen.config.json` manifest (`manifestVersion: 2`) into the project root. The manifest is **optional but highly recommended** for full editor integration and agent interoperability. If omitted, `lumen-cli` will attempt to infer a basic configuration, but advanced features (like configurable harness and architecture validation) require the manifest.
|
|
191
|
+
- **`lumen-cli`:** The companion developer CLI that reads `lumen.config.json` to generate components, features, hooks, services, and run diagnostics (harness and doctor) at the paths declared in `manifest.paths`. It also provides commands to inspect and update the manifest.
|
|
192
|
+
|
|
193
|
+
### Manifest v2 Schema (v2.0.0-alpha.1+)
|
|
194
|
+
|
|
195
|
+
The manifest now includes:
|
|
196
|
+
- **`architecture.preset`**: The base architecture (`feature-based`, `type-based`, `hybrid`, `none`).
|
|
197
|
+
- **`architecture.validation`**: Validation mode for `lumen doctor` (`strict`, `relaxed`, `none`).
|
|
198
|
+
- **`harness.commands`**: An array of commands to run with `lumen harness` (e.g., lint, format, types, test, build), each with a name, shell command, required flag, and description.
|
|
192
199
|
|
|
193
200
|
### Ecosystem Documentation & Contracts
|
|
194
201
|
|
|
195
|
-
- **[Manifest v2 Overview](./docs/manifest-v2.md):** Specification of the nested schema and validation
|
|
196
|
-
- **[Shared Contract: `create-lumen` ↔ `lumen-cli`](./docs/contracts/manifest-v2-contract.md):** The agreed contract for path mapping, barrel conventions, and
|
|
202
|
+
- **[Manifest v2 Overview](./docs/manifest-v2.md):** Specification of the nested schema, validation behavior, and the new harness and architecture validation fields.
|
|
203
|
+
- **[Shared Contract: `create-lumen` ↔ `lumen-cli`](./docs/contracts/manifest-v2-contract.md):** The agreed contract for path mapping, barrel conventions, version negotiation, and the harness/configuration interface.
|
|
197
204
|
- **[v1 to v2 Migration Guide](./docs/migration/v1-to-v2.md):** Complete guide for migrating generated projects and legacy flat configs to v2.
|
|
198
205
|
|
|
206
|
+
### Agent Interoperability
|
|
207
|
+
|
|
208
|
+
Lumen is designed for both people and agents. When `agentDocs: true` is set in the manifest (or via the `--agent-docs` flag), `create-lumen` generates a `.lumen/` directory in the project root containing:
|
|
209
|
+
- `project.json`: The resolved manifest.
|
|
210
|
+
- `architecture.json`: The effective architecture preset and validation mode.
|
|
211
|
+
- `conventions.md`: A markdown file documenting project-specific conventions (e.g., component naming, file organization).
|
|
212
|
+
These files enable agents to understand the project without parsing the source code.
|
|
213
|
+
|
|
199
214
|
## Roadmap & Planning
|
|
200
215
|
|
|
201
216
|
- **Changelog:** Notable changes are documented in [`docs/CHANGELOG.md`](./docs/CHANGELOG.md) (Keep a Changelog format).
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-lumen",
|
|
3
|
-
"version": "2.0.0-alpha.
|
|
3
|
+
"version": "2.0.0-alpha.1",
|
|
4
4
|
"description": "Scaffold a React + Vite project with architecture choice: feature-based or type-based.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
"register.js",
|
|
16
16
|
"src/",
|
|
17
17
|
"templates/",
|
|
18
|
+
"skills/",
|
|
18
19
|
"schema/",
|
|
19
20
|
"README.md",
|
|
20
21
|
"LICENSE"
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://lumen-sh.github.io/create-lumen/schema/lumen.config.v2.json",
|
|
3
|
+
"manifestVersion": 2,
|
|
4
|
+
"framework": {
|
|
5
|
+
"name": "react",
|
|
6
|
+
"variant": "vite"
|
|
7
|
+
},
|
|
8
|
+
"styling": {
|
|
9
|
+
"engine": "tailwind"
|
|
10
|
+
},
|
|
11
|
+
"architecture": {
|
|
12
|
+
"preset": "feature-based",
|
|
13
|
+
"type": "type-based",
|
|
14
|
+
"validation": "strict"
|
|
15
|
+
},
|
|
16
|
+
"ui": {
|
|
17
|
+
"kit": "none"
|
|
18
|
+
},
|
|
19
|
+
"docs": {
|
|
20
|
+
"language": "en"
|
|
21
|
+
},
|
|
22
|
+
"paths": {
|
|
23
|
+
"features": "src/features",
|
|
24
|
+
"components": "src/shared/components",
|
|
25
|
+
"services": "src/shared/services",
|
|
26
|
+
"hooks": "src/shared/hooks",
|
|
27
|
+
"pages": "src/app/router",
|
|
28
|
+
"ui": "src/shared/components/ui"
|
|
29
|
+
},
|
|
30
|
+
"tooling": {
|
|
31
|
+
"language": "ts",
|
|
32
|
+
"linter": "eslint",
|
|
33
|
+
"formatter": "prettier"
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://lumen-sh.github.io/create-lumen/schema/lumen.config.v2.json",
|
|
3
|
+
"manifestVersion": 2,
|
|
4
|
+
"framework": {
|
|
5
|
+
"name": "react",
|
|
6
|
+
"variant": "vite"
|
|
7
|
+
},
|
|
8
|
+
"styling": {
|
|
9
|
+
"engine": "tailwind"
|
|
10
|
+
},
|
|
11
|
+
"architecture": {
|
|
12
|
+
"preset": "feature-based",
|
|
13
|
+
"validation": "strict"
|
|
14
|
+
},
|
|
15
|
+
"harness": {
|
|
16
|
+
"commands": []
|
|
17
|
+
},
|
|
18
|
+
"ui": {
|
|
19
|
+
"kit": "none"
|
|
20
|
+
},
|
|
21
|
+
"docs": {
|
|
22
|
+
"language": "en"
|
|
23
|
+
},
|
|
24
|
+
"paths": {
|
|
25
|
+
"features": "src/features",
|
|
26
|
+
"components": "src/shared/components",
|
|
27
|
+
"services": "src/shared/services",
|
|
28
|
+
"hooks": "src/shared/hooks",
|
|
29
|
+
"pages": "src/app/router",
|
|
30
|
+
"ui": "src/shared/components/ui"
|
|
31
|
+
},
|
|
32
|
+
"tooling": {
|
|
33
|
+
"language": "ts",
|
|
34
|
+
"linter": "eslint",
|
|
35
|
+
"formatter": "prettier"
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://lumen-sh.github.io/create-lumen/schema/lumen.config.v2.json",
|
|
3
|
+
"manifestVersion": 2,
|
|
4
|
+
"framework": {
|
|
5
|
+
"name": "react",
|
|
6
|
+
"variant": "vite"
|
|
7
|
+
},
|
|
8
|
+
"styling": {
|
|
9
|
+
"engine": "tailwind"
|
|
10
|
+
},
|
|
11
|
+
"architecture": {
|
|
12
|
+
"preset": "feature-based",
|
|
13
|
+
"validation": "strict"
|
|
14
|
+
},
|
|
15
|
+
"harness": {
|
|
16
|
+
"commands": [
|
|
17
|
+
{ "name": "lint", "required": true },
|
|
18
|
+
{ "name": "test", "command": "vitest run", "required": true },
|
|
19
|
+
{ "name": "build", "command": "npm run build", "required": true }
|
|
20
|
+
]
|
|
21
|
+
},
|
|
22
|
+
"ui": {
|
|
23
|
+
"kit": "none"
|
|
24
|
+
},
|
|
25
|
+
"docs": {
|
|
26
|
+
"language": "en"
|
|
27
|
+
},
|
|
28
|
+
"paths": {
|
|
29
|
+
"features": "src/features",
|
|
30
|
+
"components": "src/shared/components",
|
|
31
|
+
"services": "src/shared/services",
|
|
32
|
+
"hooks": "src/shared/hooks",
|
|
33
|
+
"pages": "src/app/router",
|
|
34
|
+
"ui": "src/shared/components/ui"
|
|
35
|
+
},
|
|
36
|
+
"tooling": {
|
|
37
|
+
"language": "ts",
|
|
38
|
+
"linter": "eslint",
|
|
39
|
+
"formatter": "prettier"
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://lumen-sh.github.io/create-lumen/schema/lumen.config.v2.json",
|
|
3
|
+
"manifestVersion": 2,
|
|
4
|
+
"framework": {
|
|
5
|
+
"name": "react",
|
|
6
|
+
"variant": "vite"
|
|
7
|
+
},
|
|
8
|
+
"styling": {
|
|
9
|
+
"engine": "tailwind"
|
|
10
|
+
},
|
|
11
|
+
"architecture": {
|
|
12
|
+
"preset": "feature-based",
|
|
13
|
+
"validation": "super-strict"
|
|
14
|
+
},
|
|
15
|
+
"ui": {
|
|
16
|
+
"kit": "none"
|
|
17
|
+
},
|
|
18
|
+
"docs": {
|
|
19
|
+
"language": "en"
|
|
20
|
+
},
|
|
21
|
+
"paths": {
|
|
22
|
+
"features": "src/features",
|
|
23
|
+
"components": "src/shared/components",
|
|
24
|
+
"services": "src/shared/services",
|
|
25
|
+
"hooks": "src/shared/hooks",
|
|
26
|
+
"pages": "src/app/router",
|
|
27
|
+
"ui": "src/shared/components/ui"
|
|
28
|
+
},
|
|
29
|
+
"tooling": {
|
|
30
|
+
"language": "ts",
|
|
31
|
+
"linter": "eslint",
|
|
32
|
+
"formatter": "prettier"
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://lumen-sh.github.io/create-lumen/schema/lumen.config.v2.json",
|
|
3
|
+
"manifestVersion": 2,
|
|
4
|
+
"framework": {
|
|
5
|
+
"name": "react",
|
|
6
|
+
"variant": "vite"
|
|
7
|
+
},
|
|
8
|
+
"styling": {
|
|
9
|
+
"engine": "tailwind"
|
|
10
|
+
},
|
|
11
|
+
"architecture": {
|
|
12
|
+
"preset": "feature-based",
|
|
13
|
+
"type": "feature-based",
|
|
14
|
+
"validation": "strict"
|
|
15
|
+
},
|
|
16
|
+
"harness": {
|
|
17
|
+
"commands": [
|
|
18
|
+
{ "name": "lint", "command": "oxlint .", "required": true },
|
|
19
|
+
{ "name": "format", "command": "oxfmt --check .", "required": true },
|
|
20
|
+
{ "name": "types", "command": "tsc -b", "required": true },
|
|
21
|
+
{ "name": "test", "command": "vitest run", "required": true },
|
|
22
|
+
{ "name": "build", "command": "npm run build", "required": true }
|
|
23
|
+
]
|
|
24
|
+
},
|
|
25
|
+
"ui": {
|
|
26
|
+
"kit": "none"
|
|
27
|
+
},
|
|
28
|
+
"docs": {
|
|
29
|
+
"language": "en"
|
|
30
|
+
},
|
|
31
|
+
"paths": {
|
|
32
|
+
"features": "src/features",
|
|
33
|
+
"components": "src/shared/components",
|
|
34
|
+
"services": "src/shared/services",
|
|
35
|
+
"hooks": "src/shared/hooks",
|
|
36
|
+
"pages": "src/app/router",
|
|
37
|
+
"ui": "src/shared/components/ui"
|
|
38
|
+
},
|
|
39
|
+
"tooling": {
|
|
40
|
+
"language": "ts",
|
|
41
|
+
"linter": "eslint",
|
|
42
|
+
"formatter": "prettier"
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://lumen-sh.github.io/create-lumen/schema/lumen.config.v2.json",
|
|
3
|
+
"manifestVersion": 2,
|
|
4
|
+
"framework": {
|
|
5
|
+
"name": "react",
|
|
6
|
+
"variant": "vite"
|
|
7
|
+
},
|
|
8
|
+
"styling": {
|
|
9
|
+
"engine": "tailwind"
|
|
10
|
+
},
|
|
11
|
+
"architecture": {
|
|
12
|
+
"preset": "feature-based",
|
|
13
|
+
"validation": "none"
|
|
14
|
+
},
|
|
15
|
+
"harness": {
|
|
16
|
+
"commands": [
|
|
17
|
+
{ "name": "lint", "command": "oxlint .", "required": true },
|
|
18
|
+
{ "name": "test", "command": "vitest run", "required": true },
|
|
19
|
+
{ "name": "build", "command": "npm run build", "required": true }
|
|
20
|
+
]
|
|
21
|
+
},
|
|
22
|
+
"ui": {
|
|
23
|
+
"kit": "none"
|
|
24
|
+
},
|
|
25
|
+
"docs": {
|
|
26
|
+
"language": "en"
|
|
27
|
+
},
|
|
28
|
+
"paths": {
|
|
29
|
+
"features": "src/features",
|
|
30
|
+
"components": "src/shared/components",
|
|
31
|
+
"services": "src/shared/services",
|
|
32
|
+
"hooks": "src/shared/hooks",
|
|
33
|
+
"pages": "src/app/router",
|
|
34
|
+
"ui": "src/shared/components/ui"
|
|
35
|
+
},
|
|
36
|
+
"tooling": {
|
|
37
|
+
"language": "ts",
|
|
38
|
+
"linter": "eslint",
|
|
39
|
+
"formatter": "prettier"
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://lumen-sh.github.io/create-lumen/schema/lumen.config.v2.json",
|
|
3
|
+
"manifestVersion": 2,
|
|
4
|
+
"framework": {
|
|
5
|
+
"name": "react",
|
|
6
|
+
"variant": "vite"
|
|
7
|
+
},
|
|
8
|
+
"styling": {
|
|
9
|
+
"engine": "tailwind"
|
|
10
|
+
},
|
|
11
|
+
"architecture": {
|
|
12
|
+
"preset": "feature-based",
|
|
13
|
+
"validation": "relaxed"
|
|
14
|
+
},
|
|
15
|
+
"harness": {
|
|
16
|
+
"commands": [
|
|
17
|
+
{ "name": "lint", "command": "oxlint .", "required": true },
|
|
18
|
+
{ "name": "test", "command": "vitest run", "required": true },
|
|
19
|
+
{ "name": "build", "command": "npm run build", "required": true }
|
|
20
|
+
]
|
|
21
|
+
},
|
|
22
|
+
"ui": {
|
|
23
|
+
"kit": "none"
|
|
24
|
+
},
|
|
25
|
+
"docs": {
|
|
26
|
+
"language": "en"
|
|
27
|
+
},
|
|
28
|
+
"paths": {
|
|
29
|
+
"features": "src/features",
|
|
30
|
+
"components": "src/shared/components",
|
|
31
|
+
"services": "src/shared/services",
|
|
32
|
+
"hooks": "src/shared/hooks",
|
|
33
|
+
"pages": "src/app/router",
|
|
34
|
+
"ui": "src/shared/components/ui"
|
|
35
|
+
},
|
|
36
|
+
"tooling": {
|
|
37
|
+
"language": "ts",
|
|
38
|
+
"linter": "eslint",
|
|
39
|
+
"formatter": "prettier"
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -75,6 +75,15 @@
|
|
|
75
75
|
"architecture": {
|
|
76
76
|
"type": "object",
|
|
77
77
|
"properties": {
|
|
78
|
+
"preset": {
|
|
79
|
+
"type": "string",
|
|
80
|
+
"enum": [
|
|
81
|
+
"feature-based",
|
|
82
|
+
"type-based",
|
|
83
|
+
"hybrid",
|
|
84
|
+
"none"
|
|
85
|
+
]
|
|
86
|
+
},
|
|
78
87
|
"type": {
|
|
79
88
|
"type": "string",
|
|
80
89
|
"enum": [
|
|
@@ -83,10 +92,54 @@
|
|
|
83
92
|
"hybrid",
|
|
84
93
|
"none"
|
|
85
94
|
]
|
|
95
|
+
},
|
|
96
|
+
"validation": {
|
|
97
|
+
"default": "strict",
|
|
98
|
+
"type": "string",
|
|
99
|
+
"enum": [
|
|
100
|
+
"strict",
|
|
101
|
+
"relaxed",
|
|
102
|
+
"none"
|
|
103
|
+
]
|
|
104
|
+
}
|
|
105
|
+
},
|
|
106
|
+
"additionalProperties": false
|
|
107
|
+
},
|
|
108
|
+
"harness": {
|
|
109
|
+
"type": "object",
|
|
110
|
+
"properties": {
|
|
111
|
+
"commands": {
|
|
112
|
+
"minItems": 1,
|
|
113
|
+
"type": "array",
|
|
114
|
+
"items": {
|
|
115
|
+
"type": "object",
|
|
116
|
+
"properties": {
|
|
117
|
+
"name": {
|
|
118
|
+
"type": "string",
|
|
119
|
+
"minLength": 1
|
|
120
|
+
},
|
|
121
|
+
"command": {
|
|
122
|
+
"type": "string",
|
|
123
|
+
"minLength": 1
|
|
124
|
+
},
|
|
125
|
+
"required": {
|
|
126
|
+
"default": true,
|
|
127
|
+
"type": "boolean"
|
|
128
|
+
},
|
|
129
|
+
"description": {
|
|
130
|
+
"type": "string"
|
|
131
|
+
}
|
|
132
|
+
},
|
|
133
|
+
"required": [
|
|
134
|
+
"name",
|
|
135
|
+
"command"
|
|
136
|
+
],
|
|
137
|
+
"additionalProperties": false
|
|
138
|
+
}
|
|
86
139
|
}
|
|
87
140
|
},
|
|
88
141
|
"required": [
|
|
89
|
-
"
|
|
142
|
+
"commands"
|
|
90
143
|
],
|
|
91
144
|
"additionalProperties": false
|
|
92
145
|
},
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: create-lumen
|
|
3
|
+
description: Scaffold and extend a lumen v2 project (manifestVersion 2). Read lumen.config.json, resolve generator paths via manifest.paths, respect feature barrels, @/* alias, harness commands, and validation modes.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# create-lumen
|
|
7
|
+
|
|
8
|
+
## When to use
|
|
9
|
+
|
|
10
|
+
Use this skill when the working directory contains (or should contain) a
|
|
11
|
+
`lumen.config.json` with `manifestVersion: 2`: scaffolding follow-ups,
|
|
12
|
+
`lumen g` generation, `lumen doctor` / `lumen harness` diagnostics.
|
|
13
|
+
|
|
14
|
+
Do NOT guess layout. Every target directory comes from the manifest.
|
|
15
|
+
|
|
16
|
+
## 1. Read `lumen.config.json`
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
cat lumen.config.json
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Required top-level keys: `manifestVersion: 2`, `framework`, `styling`,
|
|
23
|
+
`architecture`, `ui`, `docs`, `paths`, `tooling`, `harness`.
|
|
24
|
+
Source of truth: `src/manifest/schema.js` (`parseManifest`, Zod strict).
|
|
25
|
+
Full spec: `docs/contracts/manifest-v2-contract.md`, `docs/manifest-v2.md`.
|
|
26
|
+
|
|
27
|
+
If `manifestVersion` is missing / `framework` is a string → **v1 manifest**:
|
|
28
|
+
fail fast (exit 1), generate nothing, point to
|
|
29
|
+
`docs/migration/v1-to-v2.md`.
|
|
30
|
+
|
|
31
|
+
## 2. Resolve paths via `manifest.paths` (never hardcode)
|
|
32
|
+
|
|
33
|
+
Generators MUST do `<root>/<manifest.paths.<key>>`. POSIX `/` separators.
|
|
34
|
+
|
|
35
|
+
| Framework | Preset | features | components | services | hooks | pages | ui |
|
|
36
|
+
|---|---|---|---|---|---|---|---|
|
|
37
|
+
| React+Vite | `feature-based` | `src/features` | `src/shared/components` | `src/shared/services` | `src/shared/hooks` | `src/app/router` | `src/shared/components/ui` |
|
|
38
|
+
| React+Vite | `type-based` | `src/features` | `src/components` | `src/services` | `src/hooks` | `src/pages` | `src/ui` |
|
|
39
|
+
| Next App | `feature-based`/`hybrid` | `src/features` | `src/shared/components` | `src/shared/services` | `src/shared/hooks` | `app` | `src/shared/components/ui` |
|
|
40
|
+
| Next Pages | `feature-based` | `src/features` | `src/shared/components` | `src/shared/services` | `src/shared/hooks` | `pages` | `src/shared/components/ui` |
|
|
41
|
+
| Any | `none` | `src` | `src` (`components/` for Next) | `src/services` | `src/hooks` | per framework | `src…/ui` |
|
|
42
|
+
|
|
43
|
+
Resolver: `src/manifest/paths.js` (`resolvePaths`). Fixtures:
|
|
44
|
+
`schema/fixtures/v2/valid/react-vite-feature-based.json`,
|
|
45
|
+
`full-kitchen-sink.json`.
|
|
46
|
+
|
|
47
|
+
## 3. Feature structure + barrels
|
|
48
|
+
|
|
49
|
+
New feature at `<paths.features>/<name>/` (e.g. `src/features/billing`):
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
lumen g feature billing
|
|
53
|
+
lumen g component billing/InvoiceCard --feature billing
|
|
54
|
+
lumen g hook useBilling --feature billing
|
|
55
|
+
lumen g service billing-api --feature billing
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Rules:
|
|
59
|
+
|
|
60
|
+
- Private subfolders: `components/ hooks/ layouts/ pages/ services/ store/ types/`.
|
|
61
|
+
- Public barrel required: `<feature>/index.ts` (or `.js` when
|
|
62
|
+
`tooling.language === "js"`). External code imports only the barrel:
|
|
63
|
+
`import { InvoiceCard } from "@/features/billing"`.
|
|
64
|
+
- Extensions: `.tsx`/`.jsx` for JSX components, `.ts`/`.js` for pure logic,
|
|
65
|
+
per `tooling.language`.
|
|
66
|
+
|
|
67
|
+
## 4. Path alias `@/*`
|
|
68
|
+
|
|
69
|
+
`@/*` → `<root>/src/*` (guaranteed by `tsconfig` + Vite/Next config).
|
|
70
|
+
Feature-based: shared imports via `@/shared/...`.
|
|
71
|
+
Type-based: `@/components/...`, `@/hooks/...`, `@/services/...`.
|
|
72
|
+
|
|
73
|
+
```ts
|
|
74
|
+
import { Button } from "@/shared/components/ui/button";
|
|
75
|
+
import { useBilling } from "@/features/billing";
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## 5. `harness.commands` execution
|
|
79
|
+
|
|
80
|
+
`lumen harness` runs each entry in order; `required: true` (default) fails
|
|
81
|
+
the run on error, `required: false` warns and continues.
|
|
82
|
+
|
|
83
|
+
```json
|
|
84
|
+
{ "name": "lint", "command": "oxlint .", "required": true }
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Defaults: `lint (oxlint .)`, `format (oxfmt --check .)`, `types (tsc -b)`,
|
|
88
|
+
`test (vitest run)`, `build (npm run build)`.
|
|
89
|
+
|
|
90
|
+
## 6. `architecture.validation` modes
|
|
91
|
+
|
|
92
|
+
| Mode | Behavior |
|
|
93
|
+
|---|---|
|
|
94
|
+
| `strict` | Enforce barrels, encapsulation, path rules; fail on violation. |
|
|
95
|
+
| `relaxed` | Warn on violation, still generate. |
|
|
96
|
+
| `none` | Skip architecture checks. |
|
|
97
|
+
|
|
98
|
+
Note: `architecture.preset` is canonical; legacy `type` is accepted as an
|
|
99
|
+
alias (`parseManifest` normalizes both). Valid presets: React →
|
|
100
|
+
`feature-based|type-based|none`; Next → `feature-based|hybrid|none`.
|
|
101
|
+
|
|
102
|
+
## 7. Common errors — reject, don't patch around
|
|
103
|
+
|
|
104
|
+
- **v1 rejection:** flat config (`css`, string `framework`/`architecture`) →
|
|
105
|
+
exit 1 + migration pointer. Never partially generate.
|
|
106
|
+
- **`shadcn` → `tailwind`:** `ui.kit: "shadcn"` requires
|
|
107
|
+
`styling.engine: "tailwind"` (v4 CSS-first, no `tailwind.config.js`).
|
|
108
|
+
- **`bundler`/`adapter` Next-only:** `framework.bundler` (`turbopack|webpack`)
|
|
109
|
+
and `adapter` (`node|vercel|cloudflare|static`) are prohibited on `react`.
|
|
110
|
+
`react` variant MUST be `vite`; `next` MUST be `app-router|pages-router`.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# manifest-v2 pointer
|
|
2
|
+
|
|
3
|
+
Canonical spec lives in `docs/` — do not duplicate it here.
|
|
4
|
+
|
|
5
|
+
- Contract: `docs/contracts/manifest-v2-contract.md`
|
|
6
|
+
- Shape / v1→v2 diff: `docs/manifest-v2.md`
|
|
7
|
+
- Zod source of truth: `src/manifest/schema.js`
|
|
8
|
+
- Path resolver: `src/manifest/paths.js`
|
|
9
|
+
- Valid fixtures: `schema/fixtures/v2/valid/`
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { promises as fsp } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { buildManifest } from "./manifest/emit.js";
|
|
4
|
+
|
|
5
|
+
const VALIDATION_NOTES = {
|
|
6
|
+
strict: "strict: architecture boundaries are enforced — do not import across features except via barrels or shared paths; violations must be fixed.",
|
|
7
|
+
relaxed: "relaxed: architecture boundaries are advisory — prefer barrels and shared paths, but cross-imports only warn.",
|
|
8
|
+
none: "none: no architecture validation — paths are still the canonical locations.",
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
function resolveManifest(manifestOrResponses) {
|
|
12
|
+
if (manifestOrResponses && typeof manifestOrResponses.manifestVersion !== "undefined") {
|
|
13
|
+
return manifestOrResponses;
|
|
14
|
+
}
|
|
15
|
+
if (manifestOrResponses && manifestOrResponses._manifestSource) {
|
|
16
|
+
return manifestOrResponses._manifestSource;
|
|
17
|
+
}
|
|
18
|
+
return buildManifest(manifestOrResponses);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Render the short agent conventions markdown from a manifest.
|
|
23
|
+
* Deterministic: derived only from stable manifest fields.
|
|
24
|
+
* @param {object} manifest - validated manifest v2
|
|
25
|
+
* @returns {string}
|
|
26
|
+
*/
|
|
27
|
+
export function renderConventions(manifest) {
|
|
28
|
+
const preset = manifest.architecture?.preset ?? manifest.architecture?.type ?? "feature-based";
|
|
29
|
+
const validation = manifest.architecture?.validation ?? "strict";
|
|
30
|
+
const paths = manifest.paths ?? {};
|
|
31
|
+
const harness = manifest.harness ?? { commands: [] };
|
|
32
|
+
const barrelFile = manifest.tooling?.language === "js" ? "index.js" : "index.ts";
|
|
33
|
+
|
|
34
|
+
const pathLines = Object.entries(paths)
|
|
35
|
+
.map(([key, value]) => `- ${key}: \`${value}\``)
|
|
36
|
+
.join("\n");
|
|
37
|
+
|
|
38
|
+
const harnessLines = (harness.commands ?? [])
|
|
39
|
+
.map((c) => `- \`${c.name}\`: \`${c.command}\` (${c.required === false ? "optional" : "required"})`)
|
|
40
|
+
.join("\n");
|
|
41
|
+
|
|
42
|
+
const harnessSummary = (harness.commands ?? []).map((c) => `\`${c.command}\``).join(", ");
|
|
43
|
+
|
|
44
|
+
return (
|
|
45
|
+
`# Lumen Agent Conventions\n` +
|
|
46
|
+
`\n` +
|
|
47
|
+
`Generated from \`lumen.config.json\`. Resolve every file location via \`paths.*\` in \`.lumen/architecture.json\` (preset: \`${preset}\`, validation: \`${validation}\`).\n` +
|
|
48
|
+
`\n` +
|
|
49
|
+
`## Rules\n` +
|
|
50
|
+
`\n` +
|
|
51
|
+
`- Resolve all paths via \`paths.*\` — never hardcode alternative \`src/...\` locations.\n` +
|
|
52
|
+
`- Feature barrel: every \`src/features/<feature>/\` must re-export its public API via \`${barrelFile}\`.\n` +
|
|
53
|
+
`- Shared code: import shared utilities via \`@/shared\` (plus the \`@/...\` aliases from tsconfig/jsconfig).\n` +
|
|
54
|
+
`- Harness: before finishing, run each command in \`.lumen/project.json\` \`harness.commands\` in order (${harnessSummary}).\n` +
|
|
55
|
+
`- Validation mode \`${validation}\`: ${VALIDATION_NOTES[validation] ?? VALIDATION_NOTES.strict}\n` +
|
|
56
|
+
`\n` +
|
|
57
|
+
`## Paths\n` +
|
|
58
|
+
`\n` +
|
|
59
|
+
`${pathLines}\n` +
|
|
60
|
+
`\n` +
|
|
61
|
+
`## Harness\n` +
|
|
62
|
+
`\n` +
|
|
63
|
+
`${harnessLines}\n`
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Build the deterministic agent-context payload from a validated manifest v2.
|
|
69
|
+
* No timestamps — output must be byte-stable.
|
|
70
|
+
* @param {object} manifest - validated manifest v2
|
|
71
|
+
* @returns {{ project: object, architecture: object, conventionsMd: string }}
|
|
72
|
+
*/
|
|
73
|
+
export function buildAgentContext(manifest) {
|
|
74
|
+
const project = {
|
|
75
|
+
manifestVersion: manifest.manifestVersion,
|
|
76
|
+
framework: manifest.framework,
|
|
77
|
+
styling: manifest.styling,
|
|
78
|
+
tooling: manifest.tooling,
|
|
79
|
+
harness: manifest.harness,
|
|
80
|
+
docs: manifest.docs,
|
|
81
|
+
};
|
|
82
|
+
const architecture = {
|
|
83
|
+
preset: manifest.architecture?.preset ?? manifest.architecture?.type,
|
|
84
|
+
validation: manifest.architecture?.validation ?? "strict",
|
|
85
|
+
paths: manifest.paths,
|
|
86
|
+
};
|
|
87
|
+
const conventionsMd = renderConventions(manifest);
|
|
88
|
+
return { project, architecture, conventionsMd };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function writeJson(filePath, value) {
|
|
92
|
+
return fsp.writeFile(filePath, JSON.stringify(value, null, 2) + "\n", "utf8");
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Emit the .lumen agent context dir into a scaffolded project.
|
|
97
|
+
* Accepts either a validated manifest v2 or scaffolder responses
|
|
98
|
+
* (uses responses._manifestSource when present, else buildManifest).
|
|
99
|
+
* Writes .lumen/project.json, .lumen/architecture.json, .lumen/conventions.md
|
|
100
|
+
* with 2-space indent + trailing newline. Byte-deterministic (no timestamps).
|
|
101
|
+
* @param {string} projectPath - absolute path to scaffolded project
|
|
102
|
+
* @param {object} manifestOrResponses
|
|
103
|
+
* @returns {Promise<string>} absolute path to the .lumen dir
|
|
104
|
+
*/
|
|
105
|
+
export async function emitAgentContext(projectPath, manifestOrResponses) {
|
|
106
|
+
const manifest = resolveManifest(manifestOrResponses);
|
|
107
|
+
const { project, architecture, conventionsMd } = buildAgentContext(manifest);
|
|
108
|
+
const dir = path.join(projectPath, ".lumen");
|
|
109
|
+
await fsp.mkdir(dir, { recursive: true });
|
|
110
|
+
await writeJson(path.join(dir, "project.json"), project);
|
|
111
|
+
await writeJson(path.join(dir, "architecture.json"), architecture);
|
|
112
|
+
await fsp.writeFile(path.join(dir, "conventions.md"), conventionsMd, "utf8");
|
|
113
|
+
return dir;
|
|
114
|
+
}
|
package/src/main.js
CHANGED
|
@@ -17,6 +17,7 @@ import { installAllDeps } from "@/dependencies.js";
|
|
|
17
17
|
import { generateReadme } from "@/readme.js";
|
|
18
18
|
import { runProjectFormat } from "@/format.js";
|
|
19
19
|
import { emitManifest } from "@/manifest/emit.js";
|
|
20
|
+
import { emitAgentContext } from "@/agent-context.js";
|
|
20
21
|
|
|
21
22
|
import { parseCliArgs, loadManifestSource, manifestToResponses, PRESETS } from "@/cli-args.js";
|
|
22
23
|
|
|
@@ -247,6 +248,19 @@ export async function main(options = {}) {
|
|
|
247
248
|
throw err;
|
|
248
249
|
}
|
|
249
250
|
|
|
251
|
+
// 12.7 Emit .lumen agent context dir (only when agentDocs is enabled)
|
|
252
|
+
if (responses.agentDocs === true || responses._manifestSource?.agentDocs === true) {
|
|
253
|
+
const agentSpin = spinner();
|
|
254
|
+
agentSpin.start("Writing .lumen agent context...");
|
|
255
|
+
try {
|
|
256
|
+
await emitAgentContext(projectPath, responses._manifestSource ?? responses);
|
|
257
|
+
agentSpin.stop(chalk.green(".lumen agent context written."));
|
|
258
|
+
} catch (err) {
|
|
259
|
+
agentSpin.stop(chalk.red("Failed to write .lumen agent context."));
|
|
260
|
+
throw err;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
250
264
|
// 13. Generate README.md + LICENSE (if requested)
|
|
251
265
|
if (responses.readme) {
|
|
252
266
|
const readmeSpin = spinner();
|
package/src/manifest/emit.js
CHANGED
|
@@ -1,8 +1,43 @@
|
|
|
1
1
|
import { promises as fsp } from "fs";
|
|
2
2
|
import path from "path";
|
|
3
|
-
import { parseManifest, SCHEMA_URL, MANIFEST_VERSION } from "./schema.js";
|
|
3
|
+
import { parseManifest, SCHEMA_URL, MANIFEST_VERSION, HARNESS_DEFAULT_COMMANDS } from "./schema.js";
|
|
4
4
|
import { resolvePaths } from "./paths.js";
|
|
5
5
|
|
|
6
|
+
const LINT_COMMANDS = {
|
|
7
|
+
eslint: "eslint .",
|
|
8
|
+
oxlint: "oxlint .",
|
|
9
|
+
biome: "biome check .",
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
const FORMAT_COMMANDS = {
|
|
13
|
+
prettier: "prettier --check .",
|
|
14
|
+
oxfmt: "oxfmt --check .",
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Build default harness commands from tooling choices.
|
|
19
|
+
* Skips lint/format when tooling is "none"; skips types when language is not "ts".
|
|
20
|
+
* Test + build are always included so commands.length >= 1.
|
|
21
|
+
* @param {{ linter?: string, formatter?: string, language?: string }} tooling
|
|
22
|
+
* @returns {Array<{ name: string, command: string, required: boolean }>}
|
|
23
|
+
*/
|
|
24
|
+
export function defaultHarnessCommands(tooling = {}) {
|
|
25
|
+
const { linter = "none", formatter = "none", language = "ts" } = tooling;
|
|
26
|
+
const commands = [];
|
|
27
|
+
if (linter && linter !== "none" && LINT_COMMANDS[linter]) {
|
|
28
|
+
commands.push({ name: "lint", command: LINT_COMMANDS[linter], required: true });
|
|
29
|
+
}
|
|
30
|
+
if (formatter && formatter !== "none" && FORMAT_COMMANDS[formatter]) {
|
|
31
|
+
commands.push({ name: "format", command: FORMAT_COMMANDS[formatter], required: true });
|
|
32
|
+
}
|
|
33
|
+
if (language === "ts") {
|
|
34
|
+
commands.push({ name: "types", command: "tsc -b", required: true });
|
|
35
|
+
}
|
|
36
|
+
commands.push({ name: "test", command: "vitest run", required: true });
|
|
37
|
+
commands.push({ name: "build", command: "npm run build", required: true });
|
|
38
|
+
return commands.length > 0 ? commands : [...HARNESS_DEFAULT_COMMANDS];
|
|
39
|
+
}
|
|
40
|
+
|
|
6
41
|
/**
|
|
7
42
|
* Build an ordered manifest v2 object from scaffolder responses.
|
|
8
43
|
* Stable key ordering for byte-determinism.
|
|
@@ -35,9 +70,14 @@ export function buildManifest(responses, opts = {}) {
|
|
|
35
70
|
engine: responses.cssFramework || "tailwind",
|
|
36
71
|
};
|
|
37
72
|
|
|
38
|
-
const rawArch =
|
|
73
|
+
const rawArch =
|
|
74
|
+
responses.architecturePreset || responses.architecture || "feature-based";
|
|
75
|
+
const normalizedArch = rawArch === "component-based" ? "type-based" : rawArch;
|
|
39
76
|
const architecture = {
|
|
40
|
-
|
|
77
|
+
preset: normalizedArch,
|
|
78
|
+
type: normalizedArch,
|
|
79
|
+
validation:
|
|
80
|
+
responses.architectureValidation || responses.validation || "strict",
|
|
41
81
|
};
|
|
42
82
|
|
|
43
83
|
const ui = {
|
|
@@ -59,12 +99,17 @@ export function buildManifest(responses, opts = {}) {
|
|
|
59
99
|
formatter: responses.formatter || "none",
|
|
60
100
|
};
|
|
61
101
|
|
|
102
|
+
const harness = responses.harness ?? {
|
|
103
|
+
commands: defaultHarnessCommands(tooling),
|
|
104
|
+
};
|
|
105
|
+
|
|
62
106
|
const manifest = {
|
|
63
107
|
$schema: SCHEMA_URL,
|
|
64
108
|
manifestVersion: MANIFEST_VERSION,
|
|
65
109
|
framework,
|
|
66
110
|
styling,
|
|
67
111
|
architecture,
|
|
112
|
+
harness,
|
|
68
113
|
ui,
|
|
69
114
|
docs,
|
|
70
115
|
paths,
|
|
@@ -83,6 +128,7 @@ export function buildManifest(responses, opts = {}) {
|
|
|
83
128
|
"framework",
|
|
84
129
|
"styling",
|
|
85
130
|
"architecture",
|
|
131
|
+
"harness",
|
|
86
132
|
"ui",
|
|
87
133
|
"docs",
|
|
88
134
|
"paths",
|
package/src/manifest/schema.js
CHANGED
|
@@ -13,7 +13,11 @@ export const FRAMEWORK_BUNDLERS = ["turbopack", "webpack"];
|
|
|
13
13
|
export const FRAMEWORK_ADAPTERS = ["node", "vercel", "cloudflare", "static"];
|
|
14
14
|
|
|
15
15
|
export const STYLING_ENGINES = ["tailwind", "bootstrap", "none"];
|
|
16
|
-
export const
|
|
16
|
+
export const ARCHITECTURE_PRESETS = ["feature-based", "type-based", "hybrid", "none"];
|
|
17
|
+
// Legacy alias: docs used architecture.type, canonical is architecture.preset.
|
|
18
|
+
// Keep both in sync; parseManifest normalizes to expose both keys.
|
|
19
|
+
export const ARCHITECTURE_TYPES = ARCHITECTURE_PRESETS;
|
|
20
|
+
export const ARCHITECTURE_VALIDATIONS = ["strict", "relaxed", "none"];
|
|
17
21
|
export const UI_KITS = ["shadcn", "none"];
|
|
18
22
|
export const DOCS_LANGUAGES = ["en", "es"];
|
|
19
23
|
export const TOOLING_LANGUAGES = ["ts", "js"];
|
|
@@ -40,7 +44,49 @@ const stylingSchema = z
|
|
|
40
44
|
|
|
41
45
|
const architectureSchema = z
|
|
42
46
|
.object({
|
|
43
|
-
type
|
|
47
|
+
// Canonical key per docs/manifest-v2.md; legacy `type` kept as alias.
|
|
48
|
+
preset: z.enum(ARCHITECTURE_PRESETS).optional(),
|
|
49
|
+
type: z.enum(ARCHITECTURE_TYPES).optional(),
|
|
50
|
+
validation: z.enum(ARCHITECTURE_VALIDATIONS).optional().default("strict"),
|
|
51
|
+
})
|
|
52
|
+
.strict()
|
|
53
|
+
.superRefine((val, ctx) => {
|
|
54
|
+
if (!val.preset && !val.type) {
|
|
55
|
+
ctx.addIssue({
|
|
56
|
+
code: "custom",
|
|
57
|
+
path: ["preset"],
|
|
58
|
+
message: 'architecture must define "preset" or legacy "type"',
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
if (val.preset && val.type && val.preset !== val.type) {
|
|
62
|
+
ctx.addIssue({
|
|
63
|
+
code: "custom",
|
|
64
|
+
path: ["preset"],
|
|
65
|
+
message: `architecture.preset "${val.preset}" must match architecture.type "${val.type}"`,
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
export const HARNESS_DEFAULT_COMMANDS = [
|
|
71
|
+
{ name: "lint", command: "oxlint .", required: true },
|
|
72
|
+
{ name: "format", command: "oxfmt --check .", required: true },
|
|
73
|
+
{ name: "types", command: "tsc -b", required: true },
|
|
74
|
+
{ name: "test", command: "vitest run", required: true },
|
|
75
|
+
{ name: "build", command: "npm run build", required: true },
|
|
76
|
+
];
|
|
77
|
+
|
|
78
|
+
const harnessCommandSchema = z
|
|
79
|
+
.object({
|
|
80
|
+
name: z.string().min(1),
|
|
81
|
+
command: z.string().min(1),
|
|
82
|
+
required: z.boolean().optional().default(true),
|
|
83
|
+
description: z.string().optional(),
|
|
84
|
+
})
|
|
85
|
+
.strict();
|
|
86
|
+
|
|
87
|
+
const harnessSchema = z
|
|
88
|
+
.object({
|
|
89
|
+
commands: z.array(harnessCommandSchema).min(1),
|
|
44
90
|
})
|
|
45
91
|
.strict();
|
|
46
92
|
|
|
@@ -85,6 +131,7 @@ export const manifestSchemaV2 = z
|
|
|
85
131
|
framework: frameworkSchema,
|
|
86
132
|
styling: stylingSchema,
|
|
87
133
|
architecture: architectureSchema,
|
|
134
|
+
harness: harnessSchema.optional(),
|
|
88
135
|
ui: uiSchema,
|
|
89
136
|
docs: docsSchema,
|
|
90
137
|
paths: pathsSchema,
|
|
@@ -130,16 +177,17 @@ export const manifestSchemaV2 = z
|
|
|
130
177
|
});
|
|
131
178
|
}
|
|
132
179
|
|
|
133
|
-
// architecture scoping per framework (#34)
|
|
180
|
+
// architecture scoping per framework (#34) — canonical `preset`, legacy `type` fallback
|
|
181
|
+
const archValue = architecture.preset ?? architecture.type;
|
|
134
182
|
const allowedByFramework =
|
|
135
183
|
framework.name === "react"
|
|
136
184
|
? ["feature-based", "type-based", "none"]
|
|
137
185
|
: ["feature-based", "hybrid", "none"];
|
|
138
|
-
if (!allowedByFramework.includes(
|
|
186
|
+
if (!allowedByFramework.includes(archValue)) {
|
|
139
187
|
ctx.addIssue({
|
|
140
188
|
code: "custom",
|
|
141
189
|
path: ["architecture", "type"],
|
|
142
|
-
message: `architecture.type "${
|
|
190
|
+
message: `architecture.type "${archValue}" is not valid for framework "${framework.name}" (allowed: ${allowedByFramework.join(", ")})`,
|
|
143
191
|
});
|
|
144
192
|
}
|
|
145
193
|
|
|
@@ -205,7 +253,17 @@ export function parseManifest(raw) {
|
|
|
205
253
|
err.cause = result.error;
|
|
206
254
|
throw err;
|
|
207
255
|
}
|
|
208
|
-
|
|
256
|
+
// Normalize: always expose architecture.preset + architecture.type (synced)
|
|
257
|
+
// + architecture.validation default, for backward/forward compat.
|
|
258
|
+
const data = result.data;
|
|
259
|
+
const preset = data.architecture.preset ?? data.architecture.type;
|
|
260
|
+
const type = data.architecture.type ?? data.architecture.preset;
|
|
261
|
+
data.architecture = {
|
|
262
|
+
preset,
|
|
263
|
+
type,
|
|
264
|
+
validation: data.architecture.validation ?? "strict",
|
|
265
|
+
};
|
|
266
|
+
return data;
|
|
209
267
|
}
|
|
210
268
|
|
|
211
269
|
// Re-export type helper for JSDoc (z.infer)
|