create-win-project 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/LICENSE +21 -0
- package/README.md +187 -0
- package/ci/expo.yml +28 -0
- package/ci/laravel.yml +49 -0
- package/ci/nextjs.yml +29 -0
- package/ci/springboot.yml +33 -0
- package/ci/vite.yml +36 -0
- package/compatibility/profiles.json +237 -0
- package/docs/ARCHITECTURE.md +118 -0
- package/docs/CONTENT_MODEL.md +36 -0
- package/docs/DEPENDENCY_MAINTENANCE.md +31 -0
- package/index.js +431 -0
- package/lib/application-shapes.js +95 -0
- package/lib/banner.js +45 -0
- package/lib/catalog.js +343 -0
- package/lib/compatibility.js +116 -0
- package/lib/constants.js +3 -0
- package/lib/doctor.js +45 -0
- package/lib/files.js +158 -0
- package/lib/generator.js +366 -0
- package/lib/interview.js +77 -0
- package/lib/laravel-scaffold.js +3 -0
- package/lib/playbooks.js +172 -0
- package/lib/project-location.js +18 -0
- package/lib/scaffold.js +467 -0
- package/lib/stacks/context.js +27 -0
- package/lib/stacks/contract.js +142 -0
- package/lib/stacks/index.js +8 -0
- package/lib/stacks/laravel/architecture.js +27 -0
- package/lib/stacks/laravel/auth/oidc.js +13 -0
- package/lib/stacks/laravel/auth/public.js +28 -0
- package/lib/stacks/laravel/auth/sanctum.js +14 -0
- package/lib/stacks/laravel/auth/session.js +17 -0
- package/lib/stacks/laravel/composer.js +50 -0
- package/lib/stacks/laravel/generate.js +362 -0
- package/lib/stacks/laravel/index.js +18 -0
- package/lib/stacks/laravel/ui/blade.js +15 -0
- package/lib/stacks/laravel/ui/index.js +29 -0
- package/lib/stacks/laravel/ui/inertia-react.js +35 -0
- package/lib/stacks/laravel/ui/livewire.js +39 -0
- package/lib/stacks/laravel/ui/shared.js +14 -0
- package/lib/stacks/registry.js +42 -0
- package/lib/stacks/shared/contributions.js +25 -0
- package/lib/template.js +62 -0
- package/package.json +49 -0
- package/playbooks/INDEX.md +58 -0
- package/playbooks/capabilities/auth/oidc-resource-server.md +15 -0
- package/playbooks/capabilities/auth/spring-session.md +15 -0
- package/playbooks/capabilities/ci/github-actions.manifest.json +10 -0
- package/playbooks/capabilities/ci/github-actions.md +61 -0
- package/playbooks/capabilities/docker/docker.manifest.json +11 -0
- package/playbooks/capabilities/docker/overview.md +55 -0
- package/playbooks/capabilities/flyway/environments.md +5 -0
- package/playbooks/capabilities/flyway/flyway.manifest.json +12 -0
- package/playbooks/capabilities/flyway/migrations.md +7 -0
- package/playbooks/capabilities/flyway/testing.md +5 -0
- package/playbooks/capabilities/laravel/database.md +8 -0
- package/playbooks/capabilities/laravel/migrations.md +15 -0
- package/playbooks/capabilities/laravel/observability.md +8 -0
- package/playbooks/capabilities/laravel/oidc-resource-server.md +10 -0
- package/playbooks/capabilities/laravel/queues.md +8 -0
- package/playbooks/capabilities/laravel/sanctum-spa.md +8 -0
- package/playbooks/capabilities/laravel/scheduler.md +7 -0
- package/playbooks/capabilities/laravel/session-auth.md +8 -0
- package/playbooks/capabilities/laravel/storage-uploads.md +7 -0
- package/playbooks/capabilities/postgresql/architecture.md +5 -0
- package/playbooks/capabilities/postgresql/migrations.md +5 -0
- package/playbooks/capabilities/postgresql/postgresql.manifest.json +15 -0
- package/playbooks/capabilities/postgresql/schema-design.md +7 -0
- package/playbooks/capabilities/postgresql/security.md +5 -0
- package/playbooks/capabilities/postgresql/testing.md +5 -0
- package/playbooks/capabilities/prisma/architecture.md +5 -0
- package/playbooks/capabilities/prisma/migrations.md +11 -0
- package/playbooks/capabilities/prisma/prisma.manifest.json +14 -0
- package/playbooks/capabilities/prisma/runtime.md +5 -0
- package/playbooks/capabilities/prisma/schema.md +5 -0
- package/playbooks/capabilities/prisma/testing.md +5 -0
- package/playbooks/capabilities/supabase/architecture.md +5 -0
- package/playbooks/capabilities/supabase/authentication.md +5 -0
- package/playbooks/capabilities/supabase/expo.md +16 -0
- package/playbooks/capabilities/supabase/migrations.md +7 -0
- package/playbooks/capabilities/supabase/nextjs.md +24 -0
- package/playbooks/capabilities/supabase/rls.md +7 -0
- package/playbooks/capabilities/supabase/supabase.manifest.json +32 -0
- package/playbooks/capabilities/supabase/testing.md +5 -0
- package/playbooks/capabilities/supabase/vite.md +14 -0
- package/playbooks/concerns/axios.md +123 -0
- package/playbooks/concerns/next-safe-action.md +66 -0
- package/playbooks/concerns/next-themes.md +87 -0
- package/playbooks/concerns/nuqs.md +59 -0
- package/playbooks/concerns/t3-env.md +48 -0
- package/playbooks/concerns/tanstack-query.md +145 -0
- package/playbooks/concerns/zod.md +174 -0
- package/playbooks/concerns/zustand.md +93 -0
- package/playbooks/devops/makefile.manifest.json +10 -0
- package/playbooks/devops/makefile.md +556 -0
- package/playbooks/devops/pr-template.manifest.json +10 -0
- package/playbooks/devops/pr-template.md +106 -0
- package/playbooks/platform/laravel-ui/blade/architecture.md +5 -0
- package/playbooks/platform/laravel-ui/blade/runtime.md +5 -0
- package/playbooks/platform/laravel-ui/blade/security.md +5 -0
- package/playbooks/platform/laravel-ui/blade/structure.md +5 -0
- package/playbooks/platform/laravel-ui/blade/testing.md +5 -0
- package/playbooks/platform/laravel-ui/inertia-react/architecture.md +5 -0
- package/playbooks/platform/laravel-ui/inertia-react/runtime.md +5 -0
- package/playbooks/platform/laravel-ui/inertia-react/security.md +5 -0
- package/playbooks/platform/laravel-ui/inertia-react/structure.md +5 -0
- package/playbooks/platform/laravel-ui/inertia-react/testing.md +5 -0
- package/playbooks/platform/laravel-ui/livewire/architecture.md +5 -0
- package/playbooks/platform/laravel-ui/livewire/runtime.md +5 -0
- package/playbooks/platform/laravel-ui/livewire/security.md +5 -0
- package/playbooks/platform/laravel-ui/livewire/structure.md +5 -0
- package/playbooks/platform/laravel-ui/livewire/testing.md +5 -0
- package/playbooks/platform/mobile.manifest.json +17 -0
- package/playbooks/platform/mobile.md +15 -0
- package/playbooks/platform/web.manifest.json +17 -0
- package/playbooks/platform/web.md +17 -0
- package/playbooks/stack/expo/architecture.md +27 -0
- package/playbooks/stack/expo/runtime.md +14 -0
- package/playbooks/stack/expo/security.md +18 -0
- package/playbooks/stack/expo/structure.md +26 -0
- package/playbooks/stack/expo/testing.md +13 -0
- package/playbooks/stack/laravel/architecture.md +34 -0
- package/playbooks/stack/laravel/runtime.md +27 -0
- package/playbooks/stack/laravel/security.md +23 -0
- package/playbooks/stack/laravel/structure.md +29 -0
- package/playbooks/stack/laravel/testing.md +22 -0
- package/playbooks/stack/laravel-ui.manifest.json +42 -0
- package/playbooks/stack/laravel.manifest.json +35 -0
- package/playbooks/stack/nextjs/architecture.md +62 -0
- package/playbooks/stack/nextjs/runtime.md +31 -0
- package/playbooks/stack/nextjs/security.md +32 -0
- package/playbooks/stack/nextjs/structure.md +50 -0
- package/playbooks/stack/nextjs/testing.md +20 -0
- package/playbooks/stack/nextjs.manifest.json +49 -0
- package/playbooks/stack/no-frontend.manifest.json +25 -0
- package/playbooks/stack/none.manifest.json +21 -0
- package/playbooks/stack/react-native.manifest.json +49 -0
- package/playbooks/stack/react-vite/architecture.md +33 -0
- package/playbooks/stack/react-vite/runtime.md +13 -0
- package/playbooks/stack/react-vite/security.md +16 -0
- package/playbooks/stack/react-vite/structure.md +23 -0
- package/playbooks/stack/react-vite/testing.md +12 -0
- package/playbooks/stack/react-vite.manifest.json +46 -0
- package/playbooks/stack/springboot/architecture.md +40 -0
- package/playbooks/stack/springboot/runtime.md +28 -0
- package/playbooks/stack/springboot/security.md +28 -0
- package/playbooks/stack/springboot/structure.md +30 -0
- package/playbooks/stack/springboot/testing.md +22 -0
- package/playbooks/stack/springboot.manifest.json +38 -0
- package/playbooks/styling/css-modules-extensions.md +267 -0
- package/playbooks/styling/css-modules.manifest.json +11 -0
- package/playbooks/styling/native-styles.manifest.json +9 -0
- package/playbooks/styling/native-styles.md +29 -0
- package/playbooks/styling/tailwind-extensions.md +182 -0
- package/playbooks/styling/tailwind.manifest.json +11 -0
- package/playbooks/universal/accessibility.manifest.json +11 -0
- package/playbooks/universal/accessibility.md +45 -0
- package/playbooks/universal/coding-rules.manifest.json +12 -0
- package/playbooks/universal/coding-rules.md +281 -0
- package/playbooks/universal/error-handling.manifest.json +10 -0
- package/playbooks/universal/error-handling.md +21 -0
- package/playbooks/universal/git-conventions.manifest.json +10 -0
- package/playbooks/universal/git-conventions.md +186 -0
- package/playbooks/universal/observability.manifest.json +10 -0
- package/playbooks/universal/observability.md +29 -0
- package/playbooks/universal/security.manifest.json +11 -0
- package/playbooks/universal/security.md +30 -0
- package/playbooks/universal/typescript.manifest.json +11 -0
- package/playbooks/universal/typescript.md +272 -0
- package/scripts/compatibility-matrix.mjs +48 -0
- package/scripts/validate-content.mjs +76 -0
- package/scripts/verify-generated.mjs +153 -0
- package/templates/agents/nextjs.md +47 -0
- package/templates/agents/react-native.md +48 -0
- package/templates/agents/react-vite.md +48 -0
- package/templates/docker/compose/postgres.yml +29 -0
- package/templates/docker/compose/springboot.yml +69 -0
- package/templates/docker/compose/supabase.yml +18 -0
- package/templates/docker/compose-prod/springboot.yml +64 -0
- package/templates/docker/dockerfile/laravel-inertia.dev.dockerfile +21 -0
- package/templates/docker/dockerfile/laravel.dev.dockerfile +12 -0
- package/templates/docker/dockerfile/laravel.prod.dockerfile +13 -0
- package/templates/docker/dockerfile/nextjs.dev.dockerfile +6 -0
- package/templates/docker/dockerfile/nextjs.prod.dockerfile +18 -0
- package/templates/docker/dockerfile/springboot.dev.dockerfile +5 -0
- package/templates/docker/dockerfile/springboot.prod.dockerfile +10 -0
- package/templates/docker/dockerfile/vite.dev.dockerfile +6 -0
- package/templates/docker/dockerfile/vite.prod.dockerfile +12 -0
- package/templates/gitignore/base.gitignore +32 -0
- package/templates/gitignore/nextjs.gitignore +40 -0
- package/templates/gitignore/react-native.gitignore +43 -0
- package/templates/gitignore/react-vite.gitignore +39 -0
- package/templates/makefile/frontend.mk +31 -0
- package/templates/makefile/laravel.mk +47 -0
- package/templates/makefile/postgres.mk +40 -0
- package/templates/makefile/springboot.mk +125 -0
- package/templates/makefile/supabase.mk +42 -0
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
# TypeScript Rules (Universal)
|
|
2
|
+
|
|
3
|
+
Applies to every TypeScript project.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Strict Mode — Always On
|
|
8
|
+
```json
|
|
9
|
+
// tsconfig.json
|
|
10
|
+
{
|
|
11
|
+
"compilerOptions": {
|
|
12
|
+
"strict": true,
|
|
13
|
+
"noUncheckedIndexedAccess": true,
|
|
14
|
+
"noImplicitReturns": true,
|
|
15
|
+
"noFallthroughCasesInSwitch": true,
|
|
16
|
+
"exactOptionalPropertyTypes": true
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
These flags catch real bugs. Never disable them.
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## No `any`
|
|
26
|
+
- Never use `any` unless absolutely unavoidable
|
|
27
|
+
- When unavoidable: add `// reason:` comment explaining why
|
|
28
|
+
|
|
29
|
+
```typescript
|
|
30
|
+
// ❌
|
|
31
|
+
const data: any = fetchUser()
|
|
32
|
+
function process(input: any) { ... }
|
|
33
|
+
|
|
34
|
+
// ✅
|
|
35
|
+
const data: unknown = fetchUser()
|
|
36
|
+
function process(input: UserInput) { ... }
|
|
37
|
+
|
|
38
|
+
// ✅ justified any with comment
|
|
39
|
+
// reason: third-party library has no types and @types package doesn't exist
|
|
40
|
+
const chart = new (window as any).ChartLib()
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### Alternatives to `any`
|
|
44
|
+
| Instead of `any` | Use |
|
|
45
|
+
|------------------|-------------------------|
|
|
46
|
+
| Unknown data | `unknown` |
|
|
47
|
+
| Flexible object | `Record<string, unknown>` |
|
|
48
|
+
| Multiple types | Union `string \| number` |
|
|
49
|
+
| Optional | `T \| undefined` |
|
|
50
|
+
| Nullable | `T \| null` |
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
## Type vs Interface
|
|
55
|
+
```typescript
|
|
56
|
+
// Interface — for objects that may be extended or implemented
|
|
57
|
+
interface User {
|
|
58
|
+
id: string
|
|
59
|
+
name: string
|
|
60
|
+
email: string
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Type — for unions, intersections, primitives, tuples
|
|
64
|
+
type UserId = string
|
|
65
|
+
type UserRole = 'ADMIN' | 'USER' | 'MODERATOR'
|
|
66
|
+
type Result<T, E> = { ok: true; value: T } | { ok: false; error: E }
|
|
67
|
+
type UserWithRole = User & { role: UserRole }
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### Rule
|
|
71
|
+
- Objects → `interface` (extends naturally, better error messages)
|
|
72
|
+
- Everything else → `type`
|
|
73
|
+
- Never use `I` prefix for interfaces (`IUser` → `User`)
|
|
74
|
+
|
|
75
|
+
---
|
|
76
|
+
|
|
77
|
+
## Discriminated Unions for State
|
|
78
|
+
```typescript
|
|
79
|
+
// ❌ scattered boolean flags
|
|
80
|
+
type State = {
|
|
81
|
+
isLoading: boolean
|
|
82
|
+
isError: boolean
|
|
83
|
+
isSuccess: boolean
|
|
84
|
+
data?: User
|
|
85
|
+
error?: Error
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// ✅ discriminated union — mutually exclusive states
|
|
89
|
+
type State =
|
|
90
|
+
| { status: 'idle' }
|
|
91
|
+
| { status: 'loading' }
|
|
92
|
+
| { status: 'success'; data: User }
|
|
93
|
+
| { status: 'error'; error: Error }
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
---
|
|
97
|
+
|
|
98
|
+
## Unknown Over Any for Errors
|
|
99
|
+
```typescript
|
|
100
|
+
// ❌
|
|
101
|
+
} catch (error: any) {
|
|
102
|
+
console.log(error.message)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// ✅
|
|
106
|
+
} catch (error: unknown) {
|
|
107
|
+
const message = error instanceof Error ? error.message : 'Unknown error'
|
|
108
|
+
logger.error(message)
|
|
109
|
+
}
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
---
|
|
113
|
+
|
|
114
|
+
## Generics
|
|
115
|
+
Use when genuinely reusable — not for ceremony.
|
|
116
|
+
|
|
117
|
+
```typescript
|
|
118
|
+
// ✅ genuinely reusable
|
|
119
|
+
function first<T>(array: T[]): T | undefined {
|
|
120
|
+
return array[0]
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// ✅ reusable operation result inside the application
|
|
124
|
+
type Result<T, E> = { ok: true; value: T } | { ok: false; error: E }
|
|
125
|
+
|
|
126
|
+
// ❌ unnecessary generic
|
|
127
|
+
function getUserName<T extends User>(user: T): string {
|
|
128
|
+
return user.name
|
|
129
|
+
// just use: function getUserName(user: User): string
|
|
130
|
+
}
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
---
|
|
134
|
+
|
|
135
|
+
## Zod for Runtime Validation
|
|
136
|
+
|
|
137
|
+
Use Zod for runtime validation **when** this project has runtime validation needs (form input, API responses, external data). If the project has no such need, this section does not apply — the rules are optional.
|
|
138
|
+
|
|
139
|
+
TypeScript types are compile-time only. Use Zod at runtime boundaries.
|
|
140
|
+
|
|
141
|
+
```typescript
|
|
142
|
+
// Define schema
|
|
143
|
+
const UserSchema = z.object({
|
|
144
|
+
id: z.string().uuid(),
|
|
145
|
+
name: z.string().min(1),
|
|
146
|
+
email: z.string().email(),
|
|
147
|
+
role: z.enum(['ADMIN', 'USER']),
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
// Infer type from schema — single source of truth
|
|
151
|
+
type User = z.infer<typeof UserSchema>
|
|
152
|
+
|
|
153
|
+
// Validate at runtime
|
|
154
|
+
const user = UserSchema.parse(apiResponse.data)
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
### When to use Zod
|
|
158
|
+
- Form validation (via React Hook Form resolver)
|
|
159
|
+
- API response parsing
|
|
160
|
+
- Environment variable validation (t3-env)
|
|
161
|
+
- Any external data entering the system
|
|
162
|
+
|
|
163
|
+
---
|
|
164
|
+
|
|
165
|
+
## Where Types Live
|
|
166
|
+
|
|
167
|
+
### Decision tree
|
|
168
|
+
```
|
|
169
|
+
Is this type used across multiple features?
|
|
170
|
+
→ src/types/[name].ts
|
|
171
|
+
|
|
172
|
+
Is this type specific to one feature?
|
|
173
|
+
→ src/features/[name]/types/index.ts
|
|
174
|
+
|
|
175
|
+
Is this type a Zod schema + inferred type?
|
|
176
|
+
→ src/features/[name]/schemas/[name].schema.ts
|
|
177
|
+
|
|
178
|
+
Is this type shared by several transport clients?
|
|
179
|
+
→ src/types/transport.ts
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
### Global types (src/types/)
|
|
183
|
+
```typescript
|
|
184
|
+
// src/types/transport.ts
|
|
185
|
+
// Successful endpoints return their normal DTO. Model only genuinely shared shapes.
|
|
186
|
+
export type ProblemDetails = { type: string; title: string; status: number; detail?: string; traceId?: string }
|
|
187
|
+
|
|
188
|
+
// src/types/pagination.ts
|
|
189
|
+
export type PaginatedResponse<T> = {
|
|
190
|
+
data: T[]
|
|
191
|
+
total: number
|
|
192
|
+
page: number
|
|
193
|
+
limit: number
|
|
194
|
+
}
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
---
|
|
198
|
+
|
|
199
|
+
## Type Assertions
|
|
200
|
+
Avoid `as` unless justified.
|
|
201
|
+
|
|
202
|
+
```typescript
|
|
203
|
+
// ❌ lying to TypeScript
|
|
204
|
+
const user = data as User
|
|
205
|
+
|
|
206
|
+
// ✅ validate first, then trust
|
|
207
|
+
const user = UserSchema.parse(data)
|
|
208
|
+
|
|
209
|
+
// ✅ justified assertion with comment
|
|
210
|
+
// reason: DOM ref is always set before this handler fires
|
|
211
|
+
const input = inputRef.current as HTMLInputElement
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
---
|
|
215
|
+
|
|
216
|
+
## Enums vs Union Types
|
|
217
|
+
Prefer union types over enums for most cases.
|
|
218
|
+
|
|
219
|
+
```typescript
|
|
220
|
+
// ❌ enum (compiles to weird JS, harder to iterate)
|
|
221
|
+
enum UserRole {
|
|
222
|
+
ADMIN = 'ADMIN',
|
|
223
|
+
USER = 'USER',
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// ✅ const object + union type (tree-shakeable, readable compiled output)
|
|
227
|
+
export const ROLES = {
|
|
228
|
+
ADMIN: 'ADMIN',
|
|
229
|
+
USER: 'USER',
|
|
230
|
+
} as const
|
|
231
|
+
|
|
232
|
+
export type UserRole = typeof ROLES[keyof typeof ROLES]
|
|
233
|
+
// → 'ADMIN' | 'USER'
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
---
|
|
237
|
+
|
|
238
|
+
## Null vs Undefined
|
|
239
|
+
```typescript
|
|
240
|
+
// Use null: explicitly absent (API returns null, user cleared a field)
|
|
241
|
+
type User = { avatar: string | null }
|
|
242
|
+
|
|
243
|
+
// Use undefined: optionally present (optional function param, optional object key)
|
|
244
|
+
type Options = { timeout?: number }
|
|
245
|
+
|
|
246
|
+
// Never mix without reason — pick one for each case and be consistent
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
---
|
|
250
|
+
|
|
251
|
+
## Agent Rules
|
|
252
|
+
```
|
|
253
|
+
strict: true — never disable, never add ts-ignore without // reason:
|
|
254
|
+
|
|
255
|
+
Type a new variable?
|
|
256
|
+
→ Use the most specific type possible
|
|
257
|
+
→ Infer from Zod schema if validating external data
|
|
258
|
+
→ Use unknown for error catch blocks
|
|
259
|
+
|
|
260
|
+
New shared type?
|
|
261
|
+
→ If used in 2+ features → src/types/
|
|
262
|
+
→ If feature-specific → features/[name]/types/
|
|
263
|
+
|
|
264
|
+
New form?
|
|
265
|
+
→ Zod schema first → infer type → pass to React Hook Form
|
|
266
|
+
|
|
267
|
+
Avoid:
|
|
268
|
+
→ any (use unknown)
|
|
269
|
+
→ type assertions without reason comment
|
|
270
|
+
→ enums (use const object + union type)
|
|
271
|
+
→ I prefix on interfaces
|
|
272
|
+
```
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import fs from 'fs-extra'
|
|
2
|
+
|
|
3
|
+
const catalog = await fs.readJson(new URL('../compatibility/profiles.json', import.meta.url))
|
|
4
|
+
const scope = process.argv.find((arg) => arg.startsWith('--scope='))?.split('=')[1] || 'smoke'
|
|
5
|
+
if (!['smoke', 'full'].includes(scope)) throw new Error('--scope must be smoke or full')
|
|
6
|
+
|
|
7
|
+
const cases = [
|
|
8
|
+
'nextjs-none', 'nextjs-supabase', 'nextjs-springboot', 'nextjs-postgres', 'nextjs-laravel',
|
|
9
|
+
'react-none', 'react-supabase', 'react-springboot', 'react-laravel',
|
|
10
|
+
'react-native-none', 'react-native-supabase', 'react-native-springboot', 'react-native-laravel',
|
|
11
|
+
'laravel-api', 'laravel-blade', 'laravel-livewire', 'laravel-inertia-react',
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
function authChoices(caseName) {
|
|
15
|
+
const mobile = caseName.startsWith('react-native')
|
|
16
|
+
const nonBrowser = mobile || caseName === 'laravel-api'
|
|
17
|
+
const base = ['not-yet', 'none'].map((authentication) => ({ authentication, audience: nonBrowser ? 'multi-client' : 'website' }))
|
|
18
|
+
if (caseName.includes('supabase')) base.push({ authentication: 'yes', audience: mobile ? 'multi-client' : 'website' })
|
|
19
|
+
if (caseName.includes('springboot') || caseName.includes('laravel')) {
|
|
20
|
+
if (caseName.startsWith('laravel-') && caseName !== 'laravel-api') base.push({ authentication: 'yes', audience: 'website' })
|
|
21
|
+
else if (mobile || caseName === 'laravel-api') base.push({ authentication: 'yes', audience: 'multi-client' })
|
|
22
|
+
else base.push({ authentication: 'yes', audience: 'website' }, { authentication: 'yes', audience: 'multi-client' })
|
|
23
|
+
}
|
|
24
|
+
return base
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const current = catalog.defaultProfile
|
|
28
|
+
const full = Object.entries(catalog.profiles).filter(([profile]) => profile === current).flatMap(([profile, versions]) =>
|
|
29
|
+
cases.flatMap((caseName) => ['small', 'medium', 'large'].flatMap((architecture) =>
|
|
30
|
+
authChoices(caseName).map((auth) => ({ profile, case: caseName, architecture, ...auth, node: versions.runtimes.node, java: versions.runtimes.java, php: versions.runtimes.php })))),
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
const smokeSelections = [
|
|
34
|
+
['nextjs-none', 'small', 'not-yet', 'website'],
|
|
35
|
+
['nextjs-supabase', 'medium', 'yes', 'website'],
|
|
36
|
+
['react-springboot', 'large', 'yes', 'website'],
|
|
37
|
+
['react-native-supabase', 'medium', 'yes', 'multi-client'],
|
|
38
|
+
['react-native-springboot', 'small', 'yes', 'multi-client'],
|
|
39
|
+
['laravel-api', 'medium', 'not-yet', 'multi-client'],
|
|
40
|
+
['laravel-blade', 'small', 'yes', 'website'],
|
|
41
|
+
['laravel-livewire', 'medium', 'none', 'website'],
|
|
42
|
+
['laravel-inertia-react', 'large', 'yes', 'website'],
|
|
43
|
+
['react-laravel', 'medium', 'yes', 'website'],
|
|
44
|
+
]
|
|
45
|
+
const smoke = full.filter((entry) => entry.profile === current && smokeSelections.some(([caseName, architecture, authentication, audience]) =>
|
|
46
|
+
entry.case === caseName && entry.architecture === architecture && entry.authentication === authentication && entry.audience === audience))
|
|
47
|
+
|
|
48
|
+
process.stdout.write(JSON.stringify(scope === 'full' ? full : smoke))
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import fs from 'fs-extra'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import { fileURLToPath } from 'node:url'
|
|
4
|
+
import { loadCatalog, resolveStack } from '../lib/catalog.js'
|
|
5
|
+
import { buildRulesIndex } from '../lib/playbooks.js'
|
|
6
|
+
|
|
7
|
+
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
|
|
8
|
+
const playbooksDir = path.join(root, 'playbooks')
|
|
9
|
+
const catalog = await loadCatalog(playbooksDir)
|
|
10
|
+
const errors = []
|
|
11
|
+
|
|
12
|
+
for (const manifest of catalog.manifests) {
|
|
13
|
+
for (const entry of manifest._playbooks || []) {
|
|
14
|
+
const relative = typeof entry === 'string' ? entry : entry.file
|
|
15
|
+
if (!await fs.pathExists(path.join(playbooksDir, relative))) {
|
|
16
|
+
errors.push(`${manifest.id}: missing playbook ${relative}`)
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const architectures = ['small', 'medium', 'large']
|
|
22
|
+
const authIntents = ['not-yet', 'none']
|
|
23
|
+
for (const frontend of catalog.frontends) {
|
|
24
|
+
for (const backend of frontend.appliesTo?.backend || []) {
|
|
25
|
+
const authCases = [...authIntents]
|
|
26
|
+
if (['supabase', 'springboot'].includes(backend)) authCases.push('yes')
|
|
27
|
+
for (const architecture of architectures) {
|
|
28
|
+
for (const authentication of authCases) {
|
|
29
|
+
const audiences = authentication === 'yes' && backend === 'springboot'
|
|
30
|
+
? ['website', 'multi-client']
|
|
31
|
+
: [frontend.platform === 'mobile' ? 'multi-client' : 'website']
|
|
32
|
+
for (const authAudience of audiences) {
|
|
33
|
+
const stack = resolveStack({
|
|
34
|
+
frontend: frontend.id, backend, architecture, authentication, authAudience,
|
|
35
|
+
styling: frontend.stylingOptions?.[0], testing: 'basic', githubActions: true,
|
|
36
|
+
}, catalog)
|
|
37
|
+
try { await buildRulesIndex(stack, catalog, playbooksDir) }
|
|
38
|
+
catch (error) { errors.push(`${frontend.id}/${backend}/${architecture}/${authentication}/${authAudience}: ${error.message}`) }
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const markdownFiles = (await fs.readdir(playbooksDir, { recursive: true, withFileTypes: true }))
|
|
46
|
+
.filter((entry) => entry.isFile() && entry.name.endsWith('.md'))
|
|
47
|
+
.map((entry) => path.join(entry.parentPath, entry.name))
|
|
48
|
+
for (const file of markdownFiles) {
|
|
49
|
+
const content = await fs.readFile(file, 'utf8')
|
|
50
|
+
const relative = path.relative(playbooksDir, file)
|
|
51
|
+
const limit = relative.startsWith('stack/') || relative.startsWith('platform/') || relative.startsWith('capabilities/') ? 250 : 650
|
|
52
|
+
if (content.split('\n').length > limit) errors.push(`${relative}: exceeds ${limit} lines`)
|
|
53
|
+
for (const match of content.matchAll(/\[[^\]]*\]\(([^)]+)\)/g)) {
|
|
54
|
+
const target = match[1].split('#')[0]
|
|
55
|
+
if (!target || /^(?:https?:|mailto:)/.test(target)) continue
|
|
56
|
+
if (!await fs.pathExists(path.resolve(path.dirname(file), target))) errors.push(`${relative}: broken link ${match[1]}`)
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const staleClaims = ['stack/nextjs.md', 'stack/react-vite.md', 'stack/react-native.md', 'stack/springboot.md', 'database/supabase.md', 'folder-structure.md']
|
|
61
|
+
for (const directory of [path.join(root, 'README.md'), path.join(root, 'docs'), playbooksDir]) {
|
|
62
|
+
const stats = await fs.stat(directory)
|
|
63
|
+
const files = stats.isDirectory()
|
|
64
|
+
? (await fs.readdir(directory, { recursive: true, withFileTypes: true })).filter((entry) => entry.isFile() && entry.name.endsWith('.md')).map((entry) => path.join(entry.parentPath, entry.name))
|
|
65
|
+
: [directory]
|
|
66
|
+
for (const file of files) {
|
|
67
|
+
const content = await fs.readFile(file, 'utf8')
|
|
68
|
+
for (const stale of staleClaims) if (content.includes(stale)) errors.push(`${path.relative(root, file)}: stale reference ${stale}`)
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (errors.length) {
|
|
73
|
+
console.error(errors.join('\n'))
|
|
74
|
+
process.exit(1)
|
|
75
|
+
}
|
|
76
|
+
console.log(`Validated ${catalog.manifests.length} manifests and ${markdownFiles.length} playbooks across the architecture/authentication matrix.`)
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import fs from 'fs-extra'
|
|
2
|
+
import os from 'node:os'
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
import { spawnSync } from 'node:child_process'
|
|
5
|
+
import { fileURLToPath } from 'node:url'
|
|
6
|
+
import { generateProject } from '../lib/generator.js'
|
|
7
|
+
|
|
8
|
+
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
|
|
9
|
+
const args = Object.fromEntries(process.argv.slice(2).map((arg) => {
|
|
10
|
+
const [key, value = 'true'] = arg.replace(/^--/, '').split('=')
|
|
11
|
+
return [key, value]
|
|
12
|
+
}))
|
|
13
|
+
|
|
14
|
+
const cases = {
|
|
15
|
+
'nextjs-none': { frontend: 'nextjs', backend: 'none', styling: 'tailwind' },
|
|
16
|
+
'nextjs-supabase': { frontend: 'nextjs', backend: 'supabase', styling: 'tailwind' },
|
|
17
|
+
'nextjs-springboot': { frontend: 'nextjs', backend: 'springboot', styling: 'css-modules', packageName: 'com.example' },
|
|
18
|
+
'nextjs-postgres': { frontend: 'nextjs', backend: 'postgres', styling: 'tailwind' },
|
|
19
|
+
'nextjs-laravel': { frontend: 'nextjs', backend: 'laravel', styling: 'tailwind', applicationShape: 'separate' },
|
|
20
|
+
'react-supabase': { frontend: 'react', backend: 'supabase', styling: 'tailwind' },
|
|
21
|
+
'react-none': { frontend: 'react', backend: 'none', styling: 'css-modules' },
|
|
22
|
+
'react-springboot': { frontend: 'react', backend: 'springboot', styling: 'css-modules', packageName: 'com.example' },
|
|
23
|
+
'react-laravel': { frontend: 'react', backend: 'laravel', styling: 'tailwind', applicationShape: 'separate' },
|
|
24
|
+
'react-native-supabase': { frontend: 'react-native', backend: 'supabase' },
|
|
25
|
+
'react-native-springboot': { frontend: 'react-native', backend: 'springboot', packageName: 'com.example' },
|
|
26
|
+
'react-native-none': { frontend: 'react-native', backend: 'none' },
|
|
27
|
+
'react-native-laravel': { frontend: 'react-native', backend: 'laravel', applicationShape: 'mobile' },
|
|
28
|
+
'laravel-api': { frontend: 'no-frontend', backend: 'laravel', applicationShape: 'api' },
|
|
29
|
+
'laravel-blade': { frontend: 'laravel-ui', backend: 'laravel', applicationShape: 'fullstack', laravelUi: 'blade', styling: 'tailwind' },
|
|
30
|
+
'laravel-livewire': { frontend: 'laravel-ui', backend: 'laravel', applicationShape: 'fullstack', laravelUi: 'livewire', styling: 'tailwind' },
|
|
31
|
+
'laravel-inertia-react': { frontend: 'laravel-ui', backend: 'laravel', applicationShape: 'fullstack', laravelUi: 'inertia-react', styling: 'tailwind' },
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (!args.profile || !cases[args.case]) {
|
|
35
|
+
throw new Error(`Usage: node scripts/verify-generated.mjs --profile=<id> --case=<${Object.keys(cases).join('|')}> [--architecture=small|medium|large] [--authentication=yes|not-yet|none] [--auth-audience=website|multi-client]`)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const architecture = args.architecture || 'medium'
|
|
39
|
+
const authentication = args.authentication || 'not-yet'
|
|
40
|
+
const authAudience = args['auth-audience'] || (cases[args.case].frontend === 'react-native' ? 'multi-client' : 'website')
|
|
41
|
+
if (!['small', 'medium', 'large'].includes(architecture)) throw new Error(`Invalid architecture: ${architecture}`)
|
|
42
|
+
if (!['yes', 'not-yet', 'none'].includes(authentication)) throw new Error(`Invalid authentication: ${authentication}`)
|
|
43
|
+
if (!['website', 'multi-client'].includes(authAudience)) throw new Error(`Invalid auth audience: ${authAudience}`)
|
|
44
|
+
|
|
45
|
+
function run(command, commandArgs, cwd, extraEnv = {}) {
|
|
46
|
+
const result = spawnSync(command, commandArgs, {
|
|
47
|
+
cwd,
|
|
48
|
+
env: { ...process.env, ...extraEnv },
|
|
49
|
+
encoding: 'utf8',
|
|
50
|
+
stdio: 'inherit',
|
|
51
|
+
})
|
|
52
|
+
if (result.error) throw result.error
|
|
53
|
+
if (result.status !== 0) throw new Error(`${command} ${commandArgs.join(' ')} failed with status ${result.status}`)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const fixtureRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'create-win-compat-'))
|
|
57
|
+
const selected = cases[args.case]
|
|
58
|
+
const projectName = `${args.case.replaceAll('react-native', 'expo')}-${architecture}-${authentication.replaceAll('-', '')}-${authAudience}`
|
|
59
|
+
|
|
60
|
+
try {
|
|
61
|
+
process.chdir(fixtureRoot)
|
|
62
|
+
await generateProject({
|
|
63
|
+
projectName,
|
|
64
|
+
projectDescription: `Compatibility verification for ${args.case}`,
|
|
65
|
+
compatibilityProfile: args.profile,
|
|
66
|
+
architecture,
|
|
67
|
+
authentication,
|
|
68
|
+
authAudience,
|
|
69
|
+
testing: selected.frontend === 'react-native' ? 'basic' : 'full',
|
|
70
|
+
docker: selected.frontend !== 'react-native' || selected.backend === 'laravel',
|
|
71
|
+
makefile: false,
|
|
72
|
+
githubActions: true,
|
|
73
|
+
expectedConcerns: [],
|
|
74
|
+
...selected,
|
|
75
|
+
}, root)
|
|
76
|
+
|
|
77
|
+
const projectRoot = path.join(fixtureRoot, projectName)
|
|
78
|
+
const metadata = await fs.readJson(path.join(projectRoot, 'create-win-project.profile.json'))
|
|
79
|
+
if (metadata.compatibilityProfile.id !== args.profile || metadata.architectureProfile !== architecture ||
|
|
80
|
+
metadata.authentication.intent !== authentication || metadata.authentication.audience !== authAudience) {
|
|
81
|
+
throw new Error('Generated profile metadata does not match the requested matrix entry')
|
|
82
|
+
}
|
|
83
|
+
const packageRoot = selected.frontend === 'react' ? path.join(projectRoot, 'frontend') : projectRoot
|
|
84
|
+
const publicEnv = {
|
|
85
|
+
NEXT_PUBLIC_SUPABASE_URL: 'https://example.supabase.co',
|
|
86
|
+
NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY: 'test-publishable-key',
|
|
87
|
+
VITE_SUPABASE_URL: 'https://example.supabase.co',
|
|
88
|
+
VITE_SUPABASE_PUBLISHABLE_KEY: 'test-publishable-key',
|
|
89
|
+
EXPO_PUBLIC_SUPABASE_URL: 'https://example.supabase.co',
|
|
90
|
+
EXPO_PUBLIC_SUPABASE_PUBLISHABLE_KEY: 'test-publishable-key',
|
|
91
|
+
NEXT_PUBLIC_API_URL: 'http://localhost:8080',
|
|
92
|
+
VITE_API_URL: 'http://localhost:8080',
|
|
93
|
+
EXPO_PUBLIC_API_URL: 'http://localhost:8080',
|
|
94
|
+
POSTGRES_USER: 'postgres',
|
|
95
|
+
POSTGRES_PASSWORD: 'compatibility-test',
|
|
96
|
+
POSTGRES_DB: projectName.replaceAll('-', '_'),
|
|
97
|
+
DATABASE_URL: `postgresql://postgres:compatibility-test@localhost:5432/${projectName.replaceAll('-', '_')}`,
|
|
98
|
+
SPRING_PROFILES_ACTIVE: 'test',
|
|
99
|
+
OIDC_ISSUER_URI: 'http://localhost:9090/realms/app',
|
|
100
|
+
OIDC_AUDIENCE: 'api',
|
|
101
|
+
AUTH0_DOMAIN: 'example.auth0.com',
|
|
102
|
+
AUTH0_AUDIENCE: 'https://api.example.test',
|
|
103
|
+
SPRING_SECURITY_USER_NAME: 'compat-user',
|
|
104
|
+
SPRING_SECURITY_USER_PASSWORD: 'compat-password',
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (await fs.pathExists(path.join(packageRoot, 'package.json'))) {
|
|
108
|
+
run('npm', ['install'], packageRoot)
|
|
109
|
+
const packageJson = await fs.readJson(path.join(packageRoot, 'package.json'))
|
|
110
|
+
if (packageJson.scripts.lint) run('npm', ['run', 'lint'], packageRoot, publicEnv)
|
|
111
|
+
if (packageJson.scripts.typecheck) run('npm', ['run', 'typecheck'], packageRoot, publicEnv)
|
|
112
|
+
if (packageJson.scripts.test) run('npm', ['test', '--', ...(selected.frontend === 'react-native' ? ['--runInBand'] : [])], packageRoot, publicEnv)
|
|
113
|
+
|
|
114
|
+
if (selected.frontend === 'react-native') {
|
|
115
|
+
run('npx', ['expo', 'install', '--check'], packageRoot, { ...publicEnv, CI: '1' })
|
|
116
|
+
run('npm', ['run', 'build', '--', '--platform', 'web'], packageRoot, publicEnv)
|
|
117
|
+
} else {
|
|
118
|
+
run('npm', ['run', 'build'], packageRoot, publicEnv)
|
|
119
|
+
if (packageJson.scripts['test:e2e']) {
|
|
120
|
+
run('npx', ['playwright', 'install', ...(process.env.CI ? ['--with-deps'] : []), 'chromium'], packageRoot)
|
|
121
|
+
run('npm', ['run', 'test:e2e'], packageRoot, publicEnv)
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (selected.backend === 'springboot') {
|
|
127
|
+
const backendRoot = path.join(projectRoot, 'backend')
|
|
128
|
+
run('./mvnw', ['--batch-mode', 'test'], backendRoot)
|
|
129
|
+
run('./mvnw', ['--batch-mode', 'package', '-DskipTests'], backendRoot)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (selected.backend === 'laravel') {
|
|
133
|
+
const backendRoot = ['laravel-ui', 'no-frontend'].includes(selected.frontend) ? projectRoot : path.join(projectRoot, 'backend')
|
|
134
|
+
run('composer', ['install', '--no-interaction', '--prefer-dist'], backendRoot)
|
|
135
|
+
run('docker', ['compose', 'up', '-d', '--wait', 'db'], projectRoot, publicEnv)
|
|
136
|
+
try {
|
|
137
|
+
run('composer', ['check'], backendRoot, { ...publicEnv, DB_HOST: '127.0.0.1', DB_PORT: '5432', DB_DATABASE: publicEnv.POSTGRES_DB, DB_USERNAME: publicEnv.POSTGRES_USER, DB_PASSWORD: publicEnv.POSTGRES_PASSWORD, APP_KEY: 'base64:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=' })
|
|
138
|
+
} finally {
|
|
139
|
+
run('docker', ['compose', 'down', '--volumes'], projectRoot, publicEnv)
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (selected.frontend !== 'react-native') {
|
|
144
|
+
run('docker', ['compose', 'config'], projectRoot, publicEnv)
|
|
145
|
+
if (args.containers === 'true') {
|
|
146
|
+
run('docker', ['compose', 'build'], projectRoot, publicEnv)
|
|
147
|
+
if (selected.backend === 'postgres') run('docker', ['build', '-t', `${projectName}:compat`, '.'], projectRoot, publicEnv)
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
} finally {
|
|
151
|
+
process.chdir(root)
|
|
152
|
+
await fs.remove(fixtureRoot)
|
|
153
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# AGENTS.md
|
|
2
|
+
|
|
3
|
+
> Small operating contract for agents. Project intent lives in `CONTEXT.md`; task guidance is routed through `RULES.md`.
|
|
4
|
+
|
|
5
|
+
## Project
|
|
6
|
+
|
|
7
|
+
{{PROJECT_NAME}} — {{PROJECT_DESCRIPTION}}
|
|
8
|
+
|
|
9
|
+
- Stack: `{{STACK}}`
|
|
10
|
+
- Platform: `{{PLATFORM}}`
|
|
11
|
+
- Architecture profile: `{{ARCHITECTURE}}`
|
|
12
|
+
|
|
13
|
+
## Commands
|
|
14
|
+
|
|
15
|
+
- Install: `npm install` initially; use `npm ci` after committing the lockfile.
|
|
16
|
+
- Develop: `npm run dev`
|
|
17
|
+
- Validate: `npm run lint && npm run typecheck && npm run test --if-present && npm run build`
|
|
18
|
+
- End-to-end: `npm run test:e2e --if-present`
|
|
19
|
+
- Spring backend, when present: `cd backend && ./mvnw --batch-mode test` (`mvnw.cmd` on Windows)
|
|
20
|
+
|
|
21
|
+
## Required workflow
|
|
22
|
+
|
|
23
|
+
1. Read `CONTEXT.md` and inspect neighboring implementation and tests.
|
|
24
|
+
2. Find the concern in `RULES.md`; open only its linked playbook section.
|
|
25
|
+
3. State assumptions when product behavior is ambiguous.
|
|
26
|
+
4. Make the smallest coherent change and add risk-appropriate tests.
|
|
27
|
+
5. Run lint, typecheck, relevant tests, and a production build before completion.
|
|
28
|
+
|
|
29
|
+
## Always-on constraints
|
|
30
|
+
|
|
31
|
+
{{CONSTRAINTS}}
|
|
32
|
+
|
|
33
|
+
- Validate untrusted input at the server boundary.
|
|
34
|
+
- Authenticate and authorize separately; enforce authorization near data and side effects.
|
|
35
|
+
- Never expose server secrets through public environment prefixes or client modules.
|
|
36
|
+
|
|
37
|
+
## Authority boundaries
|
|
38
|
+
|
|
39
|
+
- Do not deploy, publish, merge, push, send messages, or modify production data unless explicitly asked.
|
|
40
|
+
- Do not delete user work or weaken tests/security controls to make a check pass.
|
|
41
|
+
|
|
42
|
+
## Definition of done
|
|
43
|
+
|
|
44
|
+
- Acceptance behavior works and has appropriate coverage.
|
|
45
|
+
- Validation commands pass, or the exact blocker is reported.
|
|
46
|
+
- Errors do not leak secrets, personal data, or internal details.
|
|
47
|
+
- Relevant architecture, API, and environment documentation is updated.
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# AGENTS.md
|
|
2
|
+
|
|
3
|
+
> Small operating contract for agents. Project intent lives in `CONTEXT.md`; task guidance is routed through `RULES.md`.
|
|
4
|
+
|
|
5
|
+
## Project
|
|
6
|
+
|
|
7
|
+
{{PROJECT_NAME}} — {{PROJECT_DESCRIPTION}}
|
|
8
|
+
|
|
9
|
+
- Stack: `{{STACK}}`
|
|
10
|
+
- Platform: `{{PLATFORM}}`
|
|
11
|
+
|
|
12
|
+
## Commands
|
|
13
|
+
|
|
14
|
+
- Install: `npm install` initially; use `npm ci` after committing the lockfile.
|
|
15
|
+
- Develop: `npm run dev`
|
|
16
|
+
- Typecheck: `npm run typecheck`
|
|
17
|
+
- Tests: `npm test -- --runInBand` when configured.
|
|
18
|
+
- Export check: `npm run build -- --platform web`
|
|
19
|
+
- Spring backend, when present: `cd backend && ./mvnw --batch-mode test` (`mvnw.cmd` on Windows)
|
|
20
|
+
|
|
21
|
+
## Required workflow
|
|
22
|
+
|
|
23
|
+
1. Read `CONTEXT.md` and inspect neighboring screens, hooks, services, and tests.
|
|
24
|
+
2. Find the concern in `RULES.md`; open only its linked playbook section.
|
|
25
|
+
3. State assumptions when behavior is ambiguous.
|
|
26
|
+
4. Make the smallest coherent change and test it on the affected platform.
|
|
27
|
+
5. Run typecheck, relevant tests, and an export/build check.
|
|
28
|
+
|
|
29
|
+
## Always-on constraints
|
|
30
|
+
|
|
31
|
+
{{CONSTRAINTS}}
|
|
32
|
+
|
|
33
|
+
- Keep screens thin; side effects belong in hooks and services.
|
|
34
|
+
- Store sensitive credentials with platform secure storage, never AsyncStorage.
|
|
35
|
+
- Public-prefixed Expo values are bundled into the application and are never secrets.
|
|
36
|
+
- Client navigation guards are user experience, not server authorization.
|
|
37
|
+
|
|
38
|
+
## Authority boundaries
|
|
39
|
+
|
|
40
|
+
- Do not publish builds, deploy, merge, push, or modify production data unless explicitly asked.
|
|
41
|
+
- Do not add native modules or eject without explaining the impact.
|
|
42
|
+
|
|
43
|
+
## Definition of done
|
|
44
|
+
|
|
45
|
+
- Acceptance behavior works on the affected platform with appropriate tests.
|
|
46
|
+
- Typecheck, tests, and export/build pass, or the exact blocker is reported.
|
|
47
|
+
- Loading, offline, error, safe-area, keyboard, and accessibility behavior are considered.
|
|
48
|
+
- Relevant docs are updated.
|