@horka/app-forge 0.1.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.
Files changed (141) hide show
  1. package/LICENSE +32 -0
  2. package/README.md +99 -0
  3. package/bin/cli.js +371 -0
  4. package/bin/cli.test.js +91 -0
  5. package/package.json +43 -0
  6. package/templates/core/CLAUDE.md +36 -0
  7. package/templates/core/claude/memory/ARCHITECTURE.md +20 -0
  8. package/templates/core/claude/memory/COMMANDS.md +13 -0
  9. package/templates/core/claude/memory/DECISIONS.md +5 -0
  10. package/templates/core/claude/memory/NEXT_STEPS.md +11 -0
  11. package/templates/core/claude/memory/PROJECT_STATE.md +24 -0
  12. package/templates/core/claude/skills/kickoff/SKILL.md +84 -0
  13. package/templates/core/claude/skills/product-owner/SKILL.md +58 -0
  14. package/templates/core/claude/skills/restore-context/SKILL.md +29 -0
  15. package/templates/core/claude/skills/save-context/SKILL.md +35 -0
  16. package/templates/core/docs-architecture/ANTI_PATTERNS.md +180 -0
  17. package/templates/core/docs-architecture/ARCHITECTURE_PRINCIPLES.md +134 -0
  18. package/templates/core/docs-architecture/DELIVERY.md +68 -0
  19. package/templates/core/docs-architecture/DOCS_PLACEMENT.md +151 -0
  20. package/templates/core/docs-architecture/MULTI_REPO_CONTRACT.md +158 -0
  21. package/templates/core/docs-architecture/SDK_CONTRACT.md +214 -0
  22. package/templates/core/docs-architecture/SECURITY_USER_URLS.md +152 -0
  23. package/templates/core/gitignore +15 -0
  24. package/templates/core/mcp.json +8 -0
  25. package/templates/packs/nuxt-web/CLAUDE.md +74 -0
  26. package/templates/packs/nuxt-web/app/app.vue +5 -0
  27. package/templates/packs/nuxt-web/app/assets/css/main.css +18 -0
  28. package/templates/packs/nuxt-web/app/assets/css/tokens.css +41 -0
  29. package/templates/packs/nuxt-web/app/designSystem/DSButton/components/DSButton.vue +70 -0
  30. package/templates/packs/nuxt-web/app/designSystem/DSButton/index.ts +4 -0
  31. package/templates/packs/nuxt-web/app/designSystem/DSButton/tests/DSButton.spec.ts +34 -0
  32. package/templates/packs/nuxt-web/app/designSystem/DSButton/types/dsButton.ts +5 -0
  33. package/templates/packs/nuxt-web/app/domain/.gitkeep +0 -0
  34. package/templates/packs/nuxt-web/app/features/.gitkeep +0 -0
  35. package/templates/packs/nuxt-web/app/pages/index.vue +36 -0
  36. package/templates/packs/nuxt-web/app/utils/.gitkeep +0 -0
  37. package/templates/packs/nuxt-web/claude/memory/COMMANDS.md +21 -0
  38. package/templates/packs/nuxt-web/docs-architecture/ARCHITECTURE.md +169 -0
  39. package/templates/packs/nuxt-web/docs-architecture/CONVENTIONS.md +140 -0
  40. package/templates/packs/nuxt-web/docs-architecture/I18N.md +102 -0
  41. package/templates/packs/nuxt-web/docs-architecture/OPS_WEB.md +176 -0
  42. package/templates/packs/nuxt-web/docs-architecture/SEO_AND_ROUTING.md +118 -0
  43. package/templates/packs/nuxt-web/gitignore +18 -0
  44. package/templates/packs/nuxt-web/nuxt.config.ts +49 -0
  45. package/templates/packs/nuxt-web/pack.json +11 -0
  46. package/templates/packs/nuxt-web/package.json +31 -0
  47. package/templates/packs/nuxt-web/playwright.config.ts +39 -0
  48. package/templates/packs/nuxt-web/server/api/health.get.ts +7 -0
  49. package/templates/packs/nuxt-web/tests/e2e/home.spec.ts +19 -0
  50. package/templates/packs/nuxt-web/tsconfig.json +4 -0
  51. package/templates/packs/nuxt-web/vitest.config.ts +23 -0
  52. package/templates/packs/swift-ios/CLAUDE.md +64 -0
  53. package/templates/packs/swift-ios/Packages/DataLayer/Package.swift +21 -0
  54. package/templates/packs/swift-ios/Packages/DataLayer/Sources/DataLayer/DataLayer.swift +11 -0
  55. package/templates/packs/swift-ios/Packages/{{PROJECT_NAME}}Core/Package.swift +20 -0
  56. package/templates/packs/swift-ios/Packages/{{PROJECT_NAME}}Core/Sources/{{PROJECT_NAME}}Core/Domain/SampleItem.swift +15 -0
  57. package/templates/packs/swift-ios/Packages/{{PROJECT_NAME}}Core/Sources/{{PROJECT_NAME}}Core/Engine/SampleEngine.swift +14 -0
  58. package/templates/packs/swift-ios/Packages/{{PROJECT_NAME}}Core/Sources/{{PROJECT_NAME}}Core/Repository/SampleItemRepository.swift +27 -0
  59. package/templates/packs/swift-ios/Packages/{{PROJECT_NAME}}Core/Tests/{{PROJECT_NAME}}CoreTests/SampleEngineTests.swift +32 -0
  60. package/templates/packs/swift-ios/Packages/{{PROJECT_NAME}}DS/Package.swift +17 -0
  61. package/templates/packs/swift-ios/Packages/{{PROJECT_NAME}}DS/Sources/{{PROJECT_NAME}}DS/Color+DS.swift +18 -0
  62. package/templates/packs/swift-ios/Packages/{{PROJECT_NAME}}DS/Sources/{{PROJECT_NAME}}DS/Components/DSCard.swift +22 -0
  63. package/templates/packs/swift-ios/Packages/{{PROJECT_NAME}}DS/Sources/{{PROJECT_NAME}}DS/DS.swift +36 -0
  64. package/templates/packs/swift-ios/Packages/{{PROJECT_NAME}}DS/Sources/{{PROJECT_NAME}}DS/DSFont.swift +26 -0
  65. package/templates/packs/swift-ios/claude/memory/COMMANDS.md +18 -0
  66. package/templates/packs/swift-ios/docs-architecture/ARCHITECTURE.md +246 -0
  67. package/templates/packs/swift-ios/docs-architecture/CLOUDKIT_GUIDE.md +224 -0
  68. package/templates/packs/swift-ios/docs-architecture/CONVENTIONS.md +246 -0
  69. package/templates/packs/swift-ios/docs-architecture/DESIGN_SYSTEM.md +272 -0
  70. package/templates/packs/swift-ios/docs-architecture/NAVIGATION.md +241 -0
  71. package/templates/packs/swift-ios/docs-architecture/TESTING.md +176 -0
  72. package/templates/packs/swift-ios/docs-architecture/WORKFLOW.md +165 -0
  73. package/templates/packs/swift-ios/github/workflows/ci.yml +48 -0
  74. package/templates/packs/swift-ios/gitignore +5 -0
  75. package/templates/packs/swift-ios/mcp.json +8 -0
  76. package/templates/packs/swift-ios/pack.json +11 -0
  77. package/templates/packs/swift-ios/project.yml +33 -0
  78. package/templates/packs/swift-ios/{{PROJECT_NAME}}/App/App.swift +32 -0
  79. package/templates/packs/swift-ios/{{PROJECT_NAME}}/App/AppNamespace.swift +4 -0
  80. package/templates/packs/swift-ios/{{PROJECT_NAME}}/Module/.gitkeep +0 -0
  81. package/templates/packs/swift-ios/{{PROJECT_NAME}}/Store/.gitkeep +0 -0
  82. package/templates/packs/swift-ios/{{PROJECT_NAME}}/Tools/.gitkeep +0 -0
  83. package/templates/packs/ts-sdk/CHANGELOG.md +9 -0
  84. package/templates/packs/ts-sdk/CLAUDE.md +72 -0
  85. package/templates/packs/ts-sdk/MIGRATION.md +28 -0
  86. package/templates/packs/ts-sdk/claude/memory/COMMANDS.md +21 -0
  87. package/templates/packs/ts-sdk/docs-architecture/ARCHITECTURE.md +132 -0
  88. package/templates/packs/ts-sdk/docs-architecture/CONVENTIONS_TS.md +152 -0
  89. package/templates/packs/ts-sdk/gitignore +6 -0
  90. package/templates/packs/ts-sdk/pack.json +11 -0
  91. package/templates/packs/ts-sdk/package.json +55 -0
  92. package/templates/packs/ts-sdk/scripts/verify-dist.mjs +67 -0
  93. package/templates/packs/ts-sdk/src/clients/AuthClient.ts +168 -0
  94. package/templates/packs/ts-sdk/src/core/HttpClient.ts +85 -0
  95. package/templates/packs/ts-sdk/src/core/Logger.ts +27 -0
  96. package/templates/packs/ts-sdk/src/core/SDKContext.ts +40 -0
  97. package/templates/packs/ts-sdk/src/core/withTimeout.ts +19 -0
  98. package/templates/packs/ts-sdk/src/errors/ApiError.ts +93 -0
  99. package/templates/packs/ts-sdk/src/index.ts +62 -0
  100. package/templates/packs/ts-sdk/src/types/index.ts +33 -0
  101. package/templates/packs/ts-sdk/tests/apiError.test.ts +58 -0
  102. package/templates/packs/ts-sdk/tests/httpClient.test.ts +60 -0
  103. package/templates/packs/ts-sdk/tests/singleFlight.test.ts +191 -0
  104. package/templates/packs/ts-sdk/tsconfig.json +15 -0
  105. package/templates/packs/ts-sdk/tsup.config.ts +22 -0
  106. package/templates/packs/ts-sdk/vitest.config.ts +8 -0
  107. package/templates/packs/vapor-api/CLAUDE.md +73 -0
  108. package/templates/packs/vapor-api/Dockerfile +80 -0
  109. package/templates/packs/vapor-api/Package.swift +68 -0
  110. package/templates/packs/vapor-api/Sources/App/App.swift +5 -0
  111. package/templates/packs/vapor-api/Sources/App/Configure/AppConfig.swift +108 -0
  112. package/templates/packs/vapor-api/Sources/App/Configure/configure.swift +74 -0
  113. package/templates/packs/vapor-api/Sources/App/Configure/entrypoint.swift +47 -0
  114. package/templates/packs/vapor-api/Sources/App/Configure/routes.swift +21 -0
  115. package/templates/packs/vapor-api/Sources/App/Error/Failed.swift +73 -0
  116. package/templates/packs/vapor-api/Sources/App/Error/FailedMiddleware.swift +56 -0
  117. package/templates/packs/vapor-api/Sources/App/Features/Item/AppItem.swift +38 -0
  118. package/templates/packs/vapor-api/Sources/App/Features/Item/Controllers/ItemControllersCrud.swift +41 -0
  119. package/templates/packs/vapor-api/Sources/App/Features/Item/DTO/ItemDTO.swift +22 -0
  120. package/templates/packs/vapor-api/Sources/App/Features/Item/Entities/ItemEntity.swift +30 -0
  121. package/templates/packs/vapor-api/Sources/App/Features/Item/Migrations/ItemMigrationCreate.swift +25 -0
  122. package/templates/packs/vapor-api/Sources/App/Features/Item/Repositories/ItemRepository.swift +32 -0
  123. package/templates/packs/vapor-api/Sources/App/Features/Item/Services/ItemService.swift +57 -0
  124. package/templates/packs/vapor-api/Sources/App/Registry/ControllersRegister.swift +17 -0
  125. package/templates/packs/vapor-api/Sources/App/Registry/MiddlewaresRegister.swift +15 -0
  126. package/templates/packs/vapor-api/Sources/App/Registry/MigrationsRegister.swift +18 -0
  127. package/templates/packs/vapor-api/Sources/Monitoring/Logging/JSONLogHandler.swift +59 -0
  128. package/templates/packs/vapor-api/Sources/Monitoring/Middleware/HTTPLoggingMiddleware.swift +50 -0
  129. package/templates/packs/vapor-api/Sources/Monitoring/Monitoring.swift +110 -0
  130. package/templates/packs/vapor-api/Sources/{{PROJECT_NAME}}Foundation/String+Trimmed.swift +15 -0
  131. package/templates/packs/vapor-api/Tests/AppTests/AppTests.swift +155 -0
  132. package/templates/packs/vapor-api/claude/memory/COMMANDS.md +30 -0
  133. package/templates/packs/vapor-api/docs-architecture/ARCHITECTURE.md +144 -0
  134. package/templates/packs/vapor-api/docs-architecture/CONVENTIONS.md +121 -0
  135. package/templates/packs/vapor-api/docs-architecture/GOTCHAS_LINUX_SWIFT.md +109 -0
  136. package/templates/packs/vapor-api/docs-architecture/OPS.md +102 -0
  137. package/templates/packs/vapor-api/env_dist +29 -0
  138. package/templates/packs/vapor-api/gitignore +7 -0
  139. package/templates/packs/vapor-api/pack.json +11 -0
  140. package/templates/packs/vapor-api/scripts/generate-error-codes.sh +73 -0
  141. package/templates/packs/vapor-api/scripts/validate-env-vars.sh +72 -0
@@ -0,0 +1,169 @@
1
+ # ARCHITECTURE — Layered Nuxt Web App
2
+
3
+ Maps the universal L0–L5 contract (`ARCHITECTURE_PRINCIPLES.md` — read it first) onto
4
+ Nuxt 4 / Vue 3 / TypeScript. One repo, one `app/` source dir. Nuxt has no module-system
5
+ walls, so **the import graph IS the architecture** — every rule below must stay
6
+ grep-visible (Physical mapping rule 2).
7
+
8
+ ## 1. Layer model — Nuxt instantiation
9
+
10
+ ```
11
+ L5 COMPLETE FEATURES app/pages/ file-based routes — THIN assemblies only
12
+ app/layouts/ shells (default, app, …)
13
+ app/stores/ shared app state (Pinia) — explicit barrel imports
14
+ app/middleware/ route policy (auth from route meta — SEO_AND_ROUTING.md)
15
+ app/plugins/sdk.ts composition root: wires the SDK with runtime config
16
+ app.vue · error.vue · server/ (health endpoint, BFF routes)
17
+ L4 SHARED FEATURES app/features/<Feature>/ domain-AWARE bricks reused by ≥2 pages
18
+ (components/ + composables/ + types/ + index.ts barrel)
19
+ L3 CORE LOGIC app/domain/ pure TS: models, engines, validation —
20
+ no Vue, no Nuxt, no IO, no auto-imports
21
+ CORE UI app/designSystem/<DSModule>/ domain-BLIND components
22
+ (components/ + types/ + composables/ + index.ts barrel)
23
+ L2 DATA the typed SDK (external package — SDK_CONTRACT.md)
24
+ app/composables/api/ repository composables: call SDK,
25
+ map wire types → domain models
26
+ L1 OPS app/plugins/ analytics.client.ts · logger.{client,server}.ts · flags
27
+ app/constants/analytics.ts the typed event catalog (OPS_WEB.md)
28
+ L0 FOUNDATION app/assets/css/tokens.css design tokens (CSS custom properties)
29
+ app/utils/ pure helpers/formatters — no Vue imports
30
+ ```
31
+
32
+ > **Target shape vs. what ships.** The map above is where each kind of file *goes* — not an
33
+ > inventory of the skeleton. The scaffold ships the always-needed floor only:
34
+ > `app/assets/css/{tokens,main}.css`, `app/utils/`, `app/domain/`, `app/designSystem/DSButton`,
35
+ > `app/features/`, `app/pages/index.vue`, `app/app.vue`, and `server/api/health.get.ts`.
36
+ > Everything else named here — `app/plugins/` (analytics/logger/flags), `app/constants/analytics.ts`,
37
+ > `app/composables/api/`, `app/stores/` (Pinia — add `@pinia/nuxt` the day a store lands),
38
+ > `app/middleware/auth.global.ts`, `app/layouts/`, `error.vue` — is created **the slice it earns
39
+ > its first real file**. Don't pre-scaffold empty directories or add Pinia before a store exists;
40
+ > the rules below say where things belong when they arrive, and the greps in §5 assume the target
41
+ > layout (they no-op on directories that don't exist yet).
42
+
43
+ ## 2. Dependency direction rules
44
+
45
+ | Layer | May use | Must NEVER use | Why |
46
+ |---|---|---|---|
47
+ | L0 tokens + utils | nothing | Vue, Nuxt, the DOM | testable in node, reusable anywhere |
48
+ | L1 ops | L0, runtime config | domain types, SDK | plumbing is product-blind |
49
+ | L2 api composables | SDK public surface, L3 domain models, L1, L0 | components, stores | they implement L3 needs (the sanctioned upward arrow: models/contracts only) |
50
+ | L3 `domain/` | L0 utils only | Vue, Nuxt, SDK, fetch | pure — vitest runs it in milliseconds |
51
+ | L3 `designSystem/` | L0 tokens, Vue | domain types, SDK, stores, i18n keys of features | a DS button doesn't know what an "Order" is |
52
+ | L4 `features/` | L3, L2, L1, L0 | sibling features, pages, stores in props | bricks take values + emit events |
53
+ | L5 pages/layouts/stores | everything below | sibling pages | final assembly, throwaway by design |
54
+
55
+ - **L4/L5 communicate by props down / events up** (`@save`, `@delete`) — a brick never reaches
56
+ into a store on its own; the page wires store ↔ brick.
57
+ - **Auto-imports hide the import graph.** A composable used without an import line is still a
58
+ dependency. Compensate with placement + naming discipline (CONVENTIONS.md) and the greps in §5.
59
+
60
+ > ⚠️ **Gotcha:** Symptom — refactoring the API SDK breaks the *design system*. Cause — a
61
+ > production team kept domain row components (`ProjectRow`, `ReleaseRow`…) inside the DS folder,
62
+ > their props typed against deep SDK internals (`sdk/src/types/*`). Fix — `designSystem/` is
63
+ > domain-blind by contract: primitives and UI types in props only. Domain-aware composites are
64
+ > **features (L4)**; SDK types enter through L2 mapping, never through component props' imports.
65
+
66
+ > ⚠️ **Gotcha:** Symptom — a "component" added under `app/pages/` becomes a navigable route.
67
+ > Cause — Nuxt routes every `.vue` file under `pages/`. Fix — components never live in `pages/`;
68
+ > page-specific pieces go to the matching `features/<Feature>/components/`.
69
+
70
+ ## 3. Module anatomy + registration
71
+
72
+ DS modules (L3) and feature modules (L4) share one anatomy:
73
+
74
+ ```
75
+ app/designSystem/DSButton/ app/features/Checkout/
76
+ components/DSButton.vue components/CheckoutSummary.vue
77
+ types/dsButton.ts composables/useCheckout.ts
78
+ index.ts ← barrel types/checkout.ts
79
+ index.ts
80
+ ```
81
+
82
+ `nuxt.config.ts` registers them once, by glob — adding a module requires zero config:
83
+
84
+ ```ts
85
+ components: [
86
+ { path: '~/designSystem', pattern: '**/components/**', pathPrefix: false },
87
+ { path: '~/features', pattern: '**/components/**', pathPrefix: false },
88
+ ],
89
+ imports: { dirs: ['~/designSystem/**/composables', '~/features/**/composables'] },
90
+ ```
91
+
92
+ > ⚠️ **Gotcha:** Symptom — every DS component renders as an empty comment node, no error
93
+ > anywhere. Cause — the glob was put in `path` (`path: '~/designSystem/**/components'`);
94
+ > the scanner ignores glob-in-path **silently** and registers nothing. Fix — directory in
95
+ > `path`, glob in `pattern` (as above). Verify after config changes:
96
+ > `grep DSButton .nuxt/components.d.ts`.
97
+
98
+ - Components auto-register by file name (`<DSButton />` in any template).
99
+ - **Everything else crosses module boundaries through the barrel**:
100
+ `import { DS_BUTTON_VARIANTS } from '~/designSystem/DSButton'` — never a deep file path.
101
+ The barrel is the module's public surface; internals are refactorable.
102
+
103
+ ## 4. Layer contracts
104
+
105
+ ### L3 `domain/` — the pure heart
106
+ Plain TS modules: entities, engines (pure functions, injected `now`/randomness), validation,
107
+ repository *contracts* (interfaces) when the app owns orchestration. Zero imports from Vue/Nuxt.
108
+ This is where DELIVERY.md's "domain heart" slice lives; every rule ships with a vitest test that
109
+ runs without booting Nuxt.
110
+
111
+ ### L2 — the SDK and its composables
112
+ The SDK is wired ONCE in `app/plugins/sdk.ts` (runtime config → client instance → `provide`).
113
+ Repository composables (`app/composables/api/useProjectsApi.ts`) are the only callers; they map
114
+ wire DTOs to `domain/` models. **No component or store calls `$fetch` to the backend directly**
115
+ (MULTI_REPO_CONTRACT.md: never bypass the SDK).
116
+
117
+ ### L1 — ops plugins
118
+ Analytics catalog + `trackEvent`, logger plugins (client/server variants), feature-flag reader
119
+ (fail closed). All specified in OPS_WEB.md. Nothing here knows the domain.
120
+
121
+ ### L5 — pages, layouts, stores, middleware
122
+ Pages assemble bricks, declare their own route policy (`definePageMeta({ auth: false })`), own
123
+ SEO meta, and fetch via `useAsyncData` in setup (CONVENTIONS.md §4). Stores hold cross-page
124
+ state only; module-local state belongs in the module's composable.
125
+
126
+ ## 5. Enforcement greps — run before claiming a slice done
127
+
128
+ ```bash
129
+ grep -rn "from '~/designSystem" app/domain app/utils # L3-pure importing UI → bug
130
+ grep -rn "sdk/src\|/dist/" app --include='*.ts' --include='*.vue' # deep SDK imports → bug
131
+ grep -rnE "#[0-9a-fA-F]{3,8}\b" app --include='*.vue' # hex color outside tokens → bug
132
+ grep -rn "\$fetch(" app/pages app/features app/stores # raw API calls bypassing L2
133
+ grep -rn "onMounted" app/pages # each hit needs a CLIENT-ONLY rationale
134
+ awk 'END{ if (NR>300) exit 1 }' <component>.vue # size cap (CONVENTIONS.md §3)
135
+ ```
136
+
137
+ ## 6. Where does a new file go?
138
+
139
+ | You are adding… | It lives in… |
140
+ |---|---|
141
+ | A color/spacing/font/radius value | `app/assets/css/tokens.css` (L0) |
142
+ | A pure formatter/helper | `app/utils/` (L0) |
143
+ | An analytics event, logger, feature flag | `app/constants/` + `app/plugins/` (L1) |
144
+ | An API call / DTO mapping | `app/composables/api/` (L2) |
145
+ | A business rule, entity, engine | `app/domain/` (L3) — with its test |
146
+ | A domain-blind component (button, card, modal) | `app/designSystem/<DSModule>/` (L3) |
147
+ | A domain-aware brick used by ≥2 pages | `app/features/<Feature>/` (L4) |
148
+ | A route, layout, cross-page state, route policy | `app/pages/`, `app/layouts/`, `app/stores/`, `app/middleware/` (L5) |
149
+ | A server-only endpoint (health, BFF) | `server/api/` (L5) |
150
+ | A user-facing string | `i18n/<locale>/<feature>.json` (I18N.md) |
151
+
152
+ When in doubt between L4 and L5: start in the page, promote to `features/` on second use.
153
+
154
+ ## 7. Why this works with AI agents
155
+
156
+ - **Fast ground truth without the browser**: `npm run test` exercises `domain/`, `utils/`,
157
+ DS components and composables in seconds — agents iterate there before paying for
158
+ `nuxt build` or a browser session.
159
+ - **Grep-visible violations** (§5): every architectural rule is one search away.
160
+ - **Deterministic components**: DS modules mount standalone in vitest (no Nuxt context),
161
+ so UI bricks are provable without e2e.
162
+ - **Predictable placement** (§6): an agent knows the 3–5 files a feature needs; diffs stay small.
163
+
164
+ Build matrix before claiming "done":
165
+ ```bash
166
+ npm run test # unit layer — seconds
167
+ npm run build # full SSR build
168
+ npm run dev # then eyes-on: open the page, check empty/error states
169
+ ```
@@ -0,0 +1,140 @@
1
+ # CONVENTIONS — Vue 3 / Nuxt 4 / TypeScript
2
+
3
+ Prescriptive — follow as written. Layer placement is ARCHITECTURE.md; this file is how the
4
+ code inside each brick looks.
5
+
6
+ ## 1. DS modules (L3 Core UI)
7
+
8
+ One folder per component family, fixed anatomy, one barrel:
9
+
10
+ ```
11
+ app/designSystem/DSButton/
12
+ components/DSButton.vue # auto-registered globally (glob in nuxt.config)
13
+ types/dsButton.ts # variant consts + prop types — plain .ts, NEVER .d.ts
14
+ composables/ # optional: UI-only logic (focus trap, dismiss…)
15
+ tests/DSButton.spec.ts # vitest, mounts standalone — colocated with the module
16
+ index.ts # barrel = the module's ONLY public surface
17
+ ```
18
+
19
+ Rules:
20
+ - Name = `DS<Thing>` (folder, component, file). The prefix prevents collisions with feature
21
+ components and makes DS usage grep-able.
22
+ - **Domain-blind**: props are primitives, UI enums, slots, callbacks. A domain model in a DS
23
+ prop means the component belongs in `features/` (L4) — move it.
24
+ - Variants are a `const` array + derived union type (`DS_BUTTON_VARIANTS` →
25
+ `DSButtonVariant`), exported from `types/`, so tests can iterate every variant.
26
+ - Cross-module imports go through the barrel: `import { DSButton } from '~/designSystem/DSButton'`.
27
+
28
+ > ⚠️ **Gotcha:** Symptom — `import { MY_CONST } from './types/foo'` is `undefined` at runtime.
29
+ > Cause — consts were authored in a `.d.ts` file; declaration files emit no JavaScript and most
30
+ > pipelines silently skip them. Fix — module types live in **plain `.ts`** files; reserve `.d.ts`
31
+ > for ambient declarations only (e.g. `window.yourProvider` — OPS_WEB.md §1).
32
+
33
+ ## 2. Naming
34
+
35
+ | Thing | Convention | Example |
36
+ |---|---|---|
37
+ | DS component | `DS<Thing>.vue` | `DSButton.vue`, `DSModal.vue` |
38
+ | Feature component | `<Feature><Role>.vue` | `CheckoutSummary.vue` |
39
+ | Composable | `use<Thing>.ts`, one main export | `useApiErrorHandler.ts` |
40
+ | Domain module | noun, intent not tech | `order.ts`, `pricingEngine.ts` |
41
+ | Store | `use<Thing>Store` in `app/stores/` | `useSessionStore` |
42
+ | Types file | camelCase, plain `.ts` | `dsButton.ts` |
43
+ | Events emitted | past/imperative verb | `@save`, `@dismiss`, `@page-change` |
44
+ | Booleans | assertions | `isLoading`, `hasErrors`, `canSubmit` |
45
+
46
+ Comments explain **why** (decision, trap, side effect) — never what the code already says.
47
+
48
+ ## 3. Component size cap — 200 target, 300 hard
49
+
50
+ A `.vue` file (template + script + style) over **300 lines is a review blocker**; over 200,
51
+ plan the split. Splitting recipe, in order:
52
+ 1. **Pure logic out** → `app/domain/` (testable rules) or a module composable (UI orchestration).
53
+ 2. **Repeated/markable template chunks out** → child components in the same module's
54
+ `components/`, or a DS module if domain-blind.
55
+ 3. **Types and consts out** → the module's `types/` file.
56
+
57
+ > 📖 **War story:** a production homepage hero grew to 1,353 lines — animation logic, three
58
+ > sub-layouts, inline SVG and tracking calls in one SFC. Every change risked every behavior, and
59
+ > no part was testable alone. The cap exists so the split happens at 250 lines, not at 1,353.
60
+
61
+ ## 4. Data fetching — the SSR stance (read before any fetch)
62
+
63
+ This app runs `ssr: true`. **Default: fetch in setup** with `useAsyncData`/`useFetch` —
64
+ runs on the server, renders real content, hydrates without refetch:
65
+
66
+ ```ts
67
+ const { data: projects, error, pending } = await useAsyncData(
68
+ 'projects', // explicit, stable key
69
+ () => useProjectsApi().list(), // L2 repository composable — never raw $fetch
70
+ )
71
+ ```
72
+
73
+ **Client-only fetching is the exception** and requires a written rationale at the call site:
74
+
75
+ ```ts
76
+ // CLIENT-ONLY: depends on viewport size measured after mount; below the fold,
77
+ // not SEO-relevant, gated behind user interaction.
78
+ onMounted(async () => { … })
79
+ ```
80
+
81
+ `grep -rn onMounted app/pages` with no adjacent `CLIENT-ONLY:` comment = violation.
82
+
83
+ > 📖 **War story:** a production app ran `ssr: true` while ~45 pages fetched everything in
84
+ > `onMounted`. The server rendered empty shells, crawlers indexed spinners, users got a content
85
+ > flash on every navigation — all of SSR's cost, none of its value. The stance above is the fix.
86
+
87
+ SSR safety rules:
88
+ - No `window`/`document` at setup top level — guard with `import.meta.client` or move to
89
+ lifecycle hooks. `if (import.meta.server) return` is the idiom for client-only ops code.
90
+ - State shared between server render and client must use `useState`, not module-level `ref`s
91
+ (module state leaks **between requests** on the server).
92
+ - Never patch globals (`window.fetch`, `console`) — see OPS_WEB.md §Logging.
93
+
94
+ ## 5. Props / emits / boundaries
95
+
96
+ - `defineProps<{…}>()` + `withDefaults`, `defineEmits<{ save: [item: Item] }>()` — always typed.
97
+ - Bricks receive **values**, emit **events**. They never import a store, never navigate, never
98
+ call the SDK. The page (L5) wires store ↔ brick and owns navigation.
99
+ - Optional callbacks/slots change the UI (e.g. no `@delete` listener → hide the delete button):
100
+ use `defineSlots`/optional emits deliberately, document on the brick.
101
+
102
+ ## 6. SDK consumption (L2)
103
+
104
+ - Wired ONCE in `app/plugins/sdk.ts` from `runtimeConfig` — components never instantiate it.
105
+ - All calls go through repository composables in `app/composables/api/`, which map wire DTOs to
106
+ `domain/` models. Pages and stores call composables, never the SDK directly.
107
+ - **Import the SDK from its package root (or `…/types` subpath) only.** Deep `…/src/…` imports
108
+ bypass the public contract and break on any internal refactor (ANTI_PATTERNS.md #3 — found in
109
+ 22 files of one consumer). Enforce with `no-restricted-imports` once ESLint lands.
110
+ - Every `catch` around an SDK call delegates to `useApiErrorHandler` (OPS_WEB.md §2).
111
+
112
+ ## 7. Stores (L5)
113
+
114
+ - Pinia, only for state shared across pages (session, cart…). Module-local state lives in the
115
+ module's composable instead.
116
+ - Stores are imported **explicitly from the `~/stores` barrel** — no directory auto-scan magic;
117
+ the import graph must stay visible.
118
+ - Stores call L2 composables and `domain/` engines; they never touch components or routes.
119
+
120
+ ## 8. Strings, styles, sizes
121
+
122
+ - **Every user-facing string goes through i18n** — including `aria-label`, `placeholder`,
123
+ `title`. Hardcoded copy in a template is a violation (I18N.md has the detector script spec).
124
+ - **Visual values come from tokens**: `var(--ds-color-*)`, `var(--ds-space-*)`,
125
+ `var(--ds-radius-*)`, `var(--ds-font-*)`. A hex color or magic px in a component is a bug;
126
+ add a token instead.
127
+ - Scoped styles per component; global styles only in `app/assets/css/` base files.
128
+
129
+ ## 9. Testing — real or absent
130
+
131
+ - **Unit (vitest)**: `domain/` engines (exhaustive, table-driven), DS components (mounted with
132
+ `@vue/test-utils`, standalone — no Nuxt context), composables with logic. Colocated in each
133
+ module's `tests/`. Must pass from a clean checkout: `npm install && npm run test`.
134
+ - **E2E (playwright)**: user journeys only, in `tests/e2e/`. `@playwright/test` is a real
135
+ devDependency with a real config; specs boot the dev server themselves.
136
+ - A `package.json` script that cannot run from a clean checkout **is testing theater**
137
+ (ANTI_PATTERNS.md #10): either install the dependency and keep the test green, or delete the
138
+ script. Generated reports (`playwright-report/`, `coverage/`) are gitignored — a committed
139
+ report is a claim, a green run is proof (DELIVERY.md).
140
+ - Every new domain rule ships with its test in the same change. No test, no rule.
@@ -0,0 +1,102 @@
1
+ # I18N — module-per-feature translations, enforced parity
2
+
3
+ Discipline for `@nuxtjs/i18n` (add the module the day the first user-facing string appears —
4
+ which is usually day one). The unit of organization is the **feature module**, mirroring the
5
+ code layout: a feature ships its strings the way it ships its components.
6
+
7
+ ## 1. Layout
8
+
9
+ ```
10
+ i18n/
11
+ i18n.config.ts # the ONE registration point: LOCALES + MODULES + static imports
12
+ GLOSSARY.md # canonical translation of domain terms per locale
13
+ en/
14
+ common.json # genuinely shared strings ONLY (generic buttons, generic errors)
15
+ errors.json # API error names → messages (OPS_WEB.md §2 reads these)
16
+ <feature>.json # one file per feature/page module (checkout.json, settings.json…)
17
+ fr/
18
+ …same files, same keys
19
+ ```
20
+
21
+ Rules:
22
+ - **One JSON module per feature, created WITH the feature.** A new L4/L5 module's definition of
23
+ done includes its `<feature>.json` in every locale.
24
+ - A key used by exactly one feature lives in that feature's file. `common.json` is for strings
25
+ shared by ≥2 features — moving a key there is a deliberate promotion, like L5→L4 for code.
26
+ - Keys are namespaced inside the module: `checkout.summary.title`, max 3 nesting levels.
27
+ Never reuse another feature's keys — duplication across features is correct here (copy
28
+ diverges per context; shared keys create accidental coupling).
29
+ - Components consume strings only via `t('feature.…')` — including `aria-label`, `placeholder`,
30
+ `title` attributes and toast messages.
31
+
32
+ ## 2. Registration — static imports, one config
33
+
34
+ Lazy per-file loading does not compose with module-per-feature splitting in a reliable way;
35
+ register statically and let the bundler tree-shake:
36
+
37
+ ```ts
38
+ // i18n/i18n.config.ts
39
+ const LOCALES = ['en', 'fr'] as const
40
+ const MODULES = ['common', 'errors', 'checkout'] as const // ← add new modules HERE
41
+
42
+ import en_common from './en/common.json'
43
+ import en_errors from './en/errors.json'
44
+ // … one import per (locale × module); the analyzer (§3) fails CI if one is missing
45
+
46
+ export default defineI18nConfig(() => ({
47
+ legacy: false,
48
+ fallbackLocale: 'en',
49
+ messages: { en: { …merge en_* }, fr: { …merge fr_* } },
50
+ }))
51
+ ```
52
+
53
+ Adding a feature module = 1 file per locale + 1 entry in `MODULES` + 1 import line per locale.
54
+ Adding a locale = 1 folder + 1 entry in `LOCALES` (copy `en/` as the starting point so the
55
+ parity check drives the translation work).
56
+
57
+ ## 3. Parity enforcement — tooling, not vigilance
58
+
59
+ Translations drift silently; humans don't notice a missing key until a user does. A small
60
+ analyzer script (`scripts/check-i18n.*`, any language) runs locally and in CI with three checks:
61
+
62
+ 1. **Key parity (CI gate — fails the build):** every key present in one locale exists in all
63
+ locales, per module file. Missing file = every key missing.
64
+ 2. **Suspected untranslated (report):** keys whose values are byte-identical across locales.
65
+ Usually a pasted-but-never-translated copy.
66
+ 3. **Hardcoded strings (report → gate once clean):** scan `.vue` templates for literal
67
+ user-facing text outside `t()` calls (text nodes, `aria-label`, `placeholder`, `alt`).
68
+
69
+ Wire it as `npm run i18n:check` and run it in the same CI job as the unit tests — translation
70
+ breakage is a build failure, not a backlog ticket.
71
+
72
+ > ⚠️ **Gotcha:** Symptom — the identical-values check drowns in false positives. Cause — brand
73
+ > names, locale-invariant terms ("Premium", "API", product name) are *legitimately* identical.
74
+ > Fix — the script maintains an explicit ignore list of keys/values; every addition to the list
75
+ > is reviewed, so the report stays actionable instead of muted.
76
+
77
+ > ⚠️ **Gotcha:** Symptom — a locale renders raw `{name}` braces to the user. Cause —
78
+ > interpolation parameters present in one locale's string but missing/renamed in another;
79
+ > key parity can't see it. Fix — the analyzer also compares the **set of `{param}` placeholders**
80
+ > per key across locales and fails on mismatch.
81
+
82
+ > ⚠️ **Gotcha:** Symptom — translated rich text renders as escaped HTML or, worse, injects
83
+ > markup. Cause — HTML inside translation strings rendered with `v-html`. Fix — prefer
84
+ > component interpolation (`<i18n-t>`); if HTML in translations is unavoidable, sanitize at
85
+ > render time and say so next to the call site.
86
+
87
+ ## 4. GLOSSARY.md — domain terms decided once
88
+
89
+ Every domain term gets one canonical translation per locale, recorded in `i18n/GLOSSARY.md`
90
+ (term, per-locale translation, one-line definition, terms that must NOT be translated).
91
+ Translators — human or agent — consult it before inventing a synonym. Inconsistent domain
92
+ vocabulary across pages reads as three different products.
93
+
94
+ ## 5. Locale strategy
95
+
96
+ - Detection: cookie-persisted choice + browser-language first visit. Expose a visible switcher.
97
+ - URL strategy is an SEO decision, not an i18n one: `no_prefix` (locale invisible in URLs) is
98
+ fine for an app behind auth, but **indexable multilingual marketing pages need per-locale
99
+ URLs** (`prefix_except_default`) + `hreflang` — see SEO_AND_ROUTING.md. Decide deliberately
100
+ and record it in DECISIONS.md.
101
+ - Dates, numbers and currencies go through `Intl` formatters in `app/utils/` (L0) — never
102
+ hand-formatted strings in templates.
@@ -0,0 +1,176 @@
1
+ # OPS_WEB — analytics, errors, logging, flags, deploy
2
+
3
+ The L1 ops layer plus the ship pipeline. Everything here is product-blind plumbing; if a
4
+ snippet needs a domain type, it's in the wrong file.
5
+
6
+ ## 1. Analytics as code — the typed event catalog
7
+
8
+ Event names scattered as inline strings make the tracking plan unauditable and typo-prone.
9
+ The catalog **is** the tracking plan:
10
+
11
+ ```ts
12
+ // app/constants/analytics.ts (L1)
13
+ export const ANALYTICS_EVENTS = {
14
+ // Naming: [Context] [Action] [Element] — document expected props inline
15
+ SCROLL_DEPTH: 'Scroll Depth', // props: { percent: 25|50|75|100, page }
16
+ CTA_HERO_PRIMARY: 'CTA Hero Primary', // props: { page }
17
+ SIGNUP_COMPLETED: 'Signup Completed',
18
+ } as const
19
+
20
+ export type AnalyticsEvent = (typeof ANALYTICS_EVENTS)[keyof typeof ANALYTICS_EVENTS]
21
+
22
+ export function trackEvent(
23
+ name: AnalyticsEvent,
24
+ props?: Record<string, string | number | boolean>,
25
+ ): void {
26
+ if (import.meta.server) return // SSR-safe by construction
27
+ window.yourProvider?.(name, props ? { props } : undefined)
28
+ }
29
+ ```
30
+
31
+ Rules:
32
+ - **If an event name isn't in the catalog, the event doesn't exist.** A provider call with an
33
+ inline string anywhere else is a violation — grep for the provider symbol outside this file.
34
+ - Props are documented next to each event; changing an event's name or props is reviewed like
35
+ an API change (dashboards are consumers).
36
+ - Renamed events keep the old entry in a `// LEGACY` section until dashboards migrate, then die.
37
+ - Behavioral tracking (scroll depth, section visibility) lives in dedicated composables that
38
+ import the catalog — components call `useScrollDepthTracking('pricing')`, never the provider.
39
+ - Use a privacy-respecting, cookieless provider when possible; anything cookie-based goes
40
+ behind the consent manager.
41
+
42
+ ## 2. API errors — one handler, statusCode → severity
43
+
44
+ UI code never inspects response bodies or invents error copy. The SDK normalizes every non-2xx
45
+ into a typed `ApiError` (SDK_CONTRACT.md §2); the app maps it to UX exactly once:
46
+
47
+ ```ts
48
+ // app/composables/useApiErrorHandler.ts
49
+ const SEVERITY: Record<number, 'warning' | 'error'> = {
50
+ 409: 'warning', // conflict: "already exists" — recoverable, don't alarm
51
+ 423: 'warning', // locked / temporarily unavailable
52
+ }
53
+
54
+ export function useApiErrorHandler() {
55
+ const { t, te } = useI18n()
56
+ const toast = useToast()
57
+
58
+ function handleError(error: unknown, overrides?: Record<number, string>): void {
59
+ if (isApiError(error)) { // type guard exported by the SDK
60
+ const key = `errors.api.${error.errorName}` // wire contract → i18n/errors.json
61
+ const message = overrides?.[error.statusCode] ?? (te(key) ? t(key) : t('errors.generic'))
62
+ toast[SEVERITY[error.statusCode] ?? 'error'](message)
63
+ return
64
+ }
65
+ toast.error(t('errors.generic')) // never leak raw Error.message to users
66
+ }
67
+ return { handleError }
68
+ }
69
+ ```
70
+
71
+ Usage — per-call overrides for context-specific copy:
72
+
73
+ ```ts
74
+ try { await useTeamsApi().create(input) }
75
+ catch (e) { handleError(e, { 409: t('teams.alreadyExists') }) }
76
+ ```
77
+
78
+ Rules: every `catch` around an L2 call delegates here · 401 is NOT handled here (the SDK's
79
+ session layer owns refresh/logout — SDK_CONTRACT.md §3) · raw `error.message` never reaches
80
+ a toast (it's developer text, often English-only, sometimes sensitive).
81
+
82
+ ## 3. Logging — injected, never monkey-patched
83
+
84
+ One logger module in `app/utils/logger.ts` (level from runtime config, structured meta), exposed
85
+ to app code via plugins (`logger.client.ts`, `logger.server.ts`). Production builds drop stray
86
+ `console.*` (esbuild `drop`), so the logger is the only voice that survives.
87
+
88
+ > ⚠️ **Gotcha:** Symptom — every HTTP request in the app gets logged twice, aborts and streamed
89
+ > responses behave oddly, and nobody can find *where* the logging comes from. Cause — a plugin
90
+ > reassigned `window.fetch` to a logging wrapper. Patched globals are invisible at call sites,
91
+ > stack with every other patcher, double-instrument SDK traffic that already has its own logging
92
+ > port, and subtly break fetch semantics. Fix — instrument the boundaries you own: the SDK's
93
+ > injected logger (SDK_CONTRACT.md §5) and, for app-local calls, a created client —
94
+ > `$fetch.create({ onRequest, onResponse, onResponseError })` — that callers import knowingly.
95
+
96
+ ## 4. Feature flags — fail closed
97
+
98
+ ```ts
99
+ // app/utils/flags.ts (L0 pure) — read via useRuntimeConfig in a composable (L1)
100
+ const TRUTHY = new Set(['true', '1'])
101
+ export function flagEnabled(value: string | boolean | undefined | null): boolean {
102
+ if (typeof value === 'boolean') return value
103
+ if (value == null) return false // missing means OFF
104
+ return TRUTHY.has(String(value).trim().toLowerCase()) // unknown means OFF
105
+ }
106
+ ```
107
+
108
+ Misconfiguration must disable the feature, never enable it. Parsers like `value !== 'false'`
109
+ turn every typo into a silent activation (ANTI_PATTERNS.md #8 — found in production). When a
110
+ flag evaluates to off because the value was unrecognized, log a warning naming the variable.
111
+
112
+ ## 5. Runtime config — one shape, runtime overrides
113
+
114
+ - Declare every key in `runtimeConfig` (server) / `runtimeConfig.public` (client-visible) with
115
+ a safe default. Override at **runtime** via `NUXT_<KEY>` / `NUXT_PUBLIC_<KEY>` env vars.
116
+ - Secrets live server-side only. A secret in `public` ships to every browser.
117
+ - Because public values are runtime-overridable, **one image serves all environments** — don't
118
+ bake per-environment URLs at build time (see §6).
119
+
120
+ ## 6. Docker — multi-stage, BuildKit secrets, tiny runtime
121
+
122
+ ```dockerfile
123
+ # syntax=docker/dockerfile:1
124
+ FROM node:22-alpine AS builder
125
+ WORKDIR /app
126
+ COPY package.json package-lock.json ./
127
+ # Private registry/git auth: BuildKit secret — NEVER a build ARG.
128
+ RUN --mount=type=secret,id=npmrc,target=/root/.npmrc npm ci
129
+ COPY . .
130
+ RUN npm run build # → .output/ (self-contained Nitro server)
131
+
132
+ FROM node:22-alpine AS production
133
+ WORKDIR /app
134
+ ENV NODE_ENV=production
135
+ COPY --from=builder /app/.output ./.output
136
+ EXPOSE 3000
137
+ HEALTHCHECK --interval=30s --timeout=5s --retries=3 \
138
+ CMD wget -qO- http://127.0.0.1:3000/api/health || exit 1
139
+ CMD ["node", ".output/server/index.mjs"]
140
+ ```
141
+
142
+ Build: `docker build --secret id=npmrc,src=$HOME/.npmrc -t app .`
143
+ Ship a `.dockerignore` (node_modules, .nuxt, .output, .env*, test artifacts) or the builder
144
+ context uploads your laptop.
145
+
146
+ > ⚠️ **Gotcha:** Symptom — a repo access token readable by anyone who can pull the image.
147
+ > Cause — the token entered the build as an `ARG` and was written into git config in a `RUN`
148
+ > layer; `docker history` and layer caches preserve both. Fix — BuildKit secret mounts: the
149
+ > secret exists only during the single `RUN` it's mounted into and never lands in a layer.
150
+
151
+ > ⚠️ **Gotcha:** Symptom — every new config variable must be edited in four places (workflow
152
+ > secrets, Dockerfile ARGs, Dockerfile ENVs, compose file) and someone always misses one.
153
+ > Cause — public config baked at build time forces the whole env through build args. Fix — §5:
154
+ > runtime `NUXT_*` overrides; build args only for values that genuinely change the build output.
155
+
156
+ ## 7. Health & deploy gate — one endpoint, every consumer
157
+
158
+ ```ts
159
+ // server/api/health.get.ts — already in the skeleton
160
+ export default defineEventHandler(() => ({ status: 'ok', service: '…', time: … }))
161
+ ```
162
+
163
+ - **Exactly one health endpoint.** The Docker HEALTHCHECK, the orchestrator/reverse-proxy
164
+ check, the deploy gate and uptime monitoring all hit `/api/health`. Two "health" URLs WILL
165
+ diverge, and the one your alerting watches will be the stale one.
166
+ - The deploy pipeline's last step curls it on the live host and fails the deploy on non-200 —
167
+ proof over claims (DELIVERY.md), applied to infrastructure.
168
+ - Keep it dependency-free (no DB call) unless a dependency genuinely gates "alive"; if you add
169
+ readiness checks later, extend the same endpoint's payload, don't add a second URL.
170
+
171
+ ## 8. Error monitoring
172
+
173
+ Add a crash/error reporter (client + server) before the first real user, not after the first
174
+ real incident. Requirements: source maps uploaded at build (hidden from the public bundle),
175
+ environment tag from runtime config, and the `useApiErrorHandler` path reports unexpected
176
+ (5xx/unknown) errors — expected 4xx noise stays out of the alert channel.