@educa-corp/sdd-framework 0.5.0 → 0.7.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 (243) hide show
  1. package/bin/build.js +113 -19
  2. package/bin/gate-trace.js +487 -0
  3. package/bin/index.js +445 -146
  4. package/bin/lint-trace.js +643 -0
  5. package/bin/self-check.js +804 -2
  6. package/bin/trace-schema.json +621 -10
  7. package/core/FRAMEWORK_VERSION +1 -1
  8. package/core/README.md +20 -0
  9. package/core/commands/amend-prd.md +518 -0
  10. package/core/commands/debug.md +123 -511
  11. package/core/commands/define-product.md +86 -510
  12. package/core/commands/dev-gen-test.md +86 -510
  13. package/core/commands/dev-run-test.md +133 -519
  14. package/core/commands/dev-smoke-test.md +86 -510
  15. package/core/commands/extend-prd.md +128 -522
  16. package/core/commands/fix-bug.md +118 -509
  17. package/core/commands/generate-architecture.md +94 -515
  18. package/core/commands/generate-bdd.md +128 -513
  19. package/core/commands/generate-code.md +119 -510
  20. package/core/commands/generate-design-spec.md +86 -510
  21. package/core/commands/generate-prd.md +89 -510
  22. package/core/commands/generate-spec-manifest.md +86 -510
  23. package/core/commands/generate-tech-docs.md +120 -512
  24. package/core/commands/learn.md +172 -496
  25. package/core/commands/map-testids.md +86 -510
  26. package/core/commands/propose-scenario.md +86 -510
  27. package/core/commands/qc-analyze.md +86 -510
  28. package/core/commands/qc-design-test.md +86 -510
  29. package/core/commands/qc-plan.md +86 -510
  30. package/core/commands/qc-report.md +86 -510
  31. package/core/commands/qc-review.md +86 -510
  32. package/core/commands/qc-run-test.md +115 -513
  33. package/core/commands/refine-prd.md +112 -522
  34. package/core/commands/report-bug.md +86 -510
  35. package/core/commands/review-code.md +123 -511
  36. package/core/commands/review-context.md +136 -522
  37. package/core/commands/review-tech-docs.md +90 -511
  38. package/core/commands/setup-ai-first.md +166 -138
  39. package/core/commands/sync.md +155 -107
  40. package/core/commands/update-framework.md +16 -103
  41. package/core/commands/validate-traces.md +426 -511
  42. package/core/hooks/data-guard.js +174 -83
  43. package/core/hooks/settings.json +2 -1
  44. package/core/rules/workflow.md +64 -4
  45. package/core/steps/capture-lesson.md +34 -1
  46. package/core/steps/context-loader.md +50 -8
  47. package/core/steps/gate.md +92 -35
  48. package/core/steps/report-footer.md +23 -0
  49. package/core/templates/README.md +24 -1
  50. package/core/templates/ci/trace-gate.yml +146 -0
  51. package/core/templates/feature.template +1 -1
  52. package/core/templates/hooks/pre-push +61 -0
  53. package/docs/02-concepts/architecture.md +61 -6
  54. package/docs/02-concepts/traceability.md +57 -0
  55. package/docs/03-guides/architect.md +63 -0
  56. package/docs/04-reference/commands.md +148 -134
  57. package/docs/04-reference/model-selection.md +32 -19
  58. package/docs/04-reference/trace-schema.md +39 -0
  59. package/docs/explain/02b-extend-prd.md +1 -1
  60. package/docs/explain/02c-amend-prd.md +152 -0
  61. package/docs/explain/21-validate-traces.md +2 -1
  62. package/docs/explain/27-learn.md +5 -3
  63. package/docs/explain/28-sync.md +25 -0
  64. package/docs/explain/README.md +136 -135
  65. package/package.json +5 -9
  66. package/commands/debug.md +0 -917
  67. package/commands/debug.tmpl +0 -257
  68. package/commands/define-product.md +0 -862
  69. package/commands/define-product.tmpl +0 -225
  70. package/commands/dev-gen-test.md +0 -1124
  71. package/commands/dev-gen-test.tmpl +0 -490
  72. package/commands/dev-run-test.md +0 -859
  73. package/commands/dev-run-test.tmpl +0 -225
  74. package/commands/dev-smoke-test.md +0 -798
  75. package/commands/dev-smoke-test.tmpl +0 -217
  76. package/commands/extend-prd.md +0 -907
  77. package/commands/extend-prd.tmpl +0 -270
  78. package/commands/fix-bug.md +0 -910
  79. package/commands/fix-bug.tmpl +0 -197
  80. package/commands/generate-architecture.md +0 -775
  81. package/commands/generate-architecture.tmpl +0 -194
  82. package/commands/generate-bdd.md +0 -1347
  83. package/commands/generate-bdd.tmpl +0 -590
  84. package/commands/generate-code.md +0 -1283
  85. package/commands/generate-code.tmpl +0 -649
  86. package/commands/generate-design-spec.md +0 -1161
  87. package/commands/generate-design-spec.tmpl +0 -524
  88. package/commands/generate-prd.md +0 -1143
  89. package/commands/generate-prd.tmpl +0 -223
  90. package/commands/generate-spec-manifest.md +0 -745
  91. package/commands/generate-spec-manifest.tmpl +0 -164
  92. package/commands/generate-tech-docs.md +0 -1344
  93. package/commands/generate-tech-docs.tmpl +0 -273
  94. package/commands/learn.md +0 -723
  95. package/commands/learn.tmpl +0 -63
  96. package/commands/map-testids.md +0 -662
  97. package/commands/map-testids.tmpl +0 -81
  98. package/commands/propose-scenario.md +0 -783
  99. package/commands/propose-scenario.tmpl +0 -202
  100. package/commands/qc-analyze.md +0 -693
  101. package/commands/qc-analyze.tmpl +0 -112
  102. package/commands/qc-design-test.md +0 -650
  103. package/commands/qc-design-test.tmpl +0 -69
  104. package/commands/qc-plan.md +0 -630
  105. package/commands/qc-plan.tmpl +0 -49
  106. package/commands/qc-report.md +0 -641
  107. package/commands/qc-report.tmpl +0 -60
  108. package/commands/qc-review.md +0 -634
  109. package/commands/qc-review.tmpl +0 -53
  110. package/commands/qc-run-test.md +0 -750
  111. package/commands/qc-run-test.tmpl +0 -116
  112. package/commands/refine-prd.md +0 -1074
  113. package/commands/refine-prd.tmpl +0 -278
  114. package/commands/report-bug.md +0 -729
  115. package/commands/report-bug.tmpl +0 -148
  116. package/commands/review-code.md +0 -803
  117. package/commands/review-code.tmpl +0 -143
  118. package/commands/review-context.md +0 -1323
  119. package/commands/review-context.tmpl +0 -527
  120. package/commands/review-tech-docs.md +0 -982
  121. package/commands/review-tech-docs.tmpl +0 -401
  122. package/commands/setup-ai-first.md +0 -574
  123. package/commands/setup-ai-first.tmpl +0 -378
  124. package/commands/sync.md +0 -486
  125. package/commands/sync.tmpl +0 -384
  126. package/commands/update-framework.md +0 -290
  127. package/commands/update-framework.tmpl +0 -188
  128. package/commands/validate-traces.md +0 -1435
  129. package/commands/validate-traces.tmpl +0 -854
  130. package/hooks/data-guard.js +0 -141
  131. package/hooks/settings.json +0 -18
  132. package/modules/android-compose/module.yaml +0 -13
  133. package/modules/android-compose/stack-profile.yaml +0 -57
  134. package/modules/angular/architecture-snippets/component-patterns.md +0 -187
  135. package/modules/angular/module.yaml +0 -6
  136. package/modules/angular/stack-profile.yaml +0 -38
  137. package/modules/context-engineering/architecture-snippets/context-design.md +0 -119
  138. package/modules/context-engineering/module.yaml +0 -9
  139. package/modules/context-engineering/stack-profile.yaml +0 -61
  140. package/modules/dotnet/architecture-snippets/clean-arch.md +0 -160
  141. package/modules/dotnet/module.yaml +0 -6
  142. package/modules/dotnet/stack-profile.yaml +0 -50
  143. package/modules/flutter/module.yaml +0 -14
  144. package/modules/flutter/stack-profile.yaml +0 -59
  145. package/modules/golang/architecture-snippets/domain-layout.md +0 -283
  146. package/modules/golang/module.yaml +0 -6
  147. package/modules/golang/stack-profile.yaml +0 -40
  148. package/modules/ios-swiftui/module.yaml +0 -13
  149. package/modules/ios-swiftui/stack-profile.yaml +0 -55
  150. package/modules/java-spring/architecture-snippets/layered-arch.md +0 -201
  151. package/modules/java-spring/module.yaml +0 -15
  152. package/modules/java-spring/stack-profile.yaml +0 -28
  153. package/modules/nextjs/architecture-snippets/app-router-patterns.md +0 -269
  154. package/modules/nextjs/module.yaml +0 -14
  155. package/modules/nextjs/stack-profile.yaml +0 -74
  156. package/modules/nuxt/module.yaml +0 -14
  157. package/modules/nuxt/stack-profile.yaml +0 -58
  158. package/modules/phaser-game/architecture-snippets/phaser-scene-patterns.md +0 -646
  159. package/modules/phaser-game/module.yaml +0 -15
  160. package/modules/phaser-game/stack-profile.yaml +0 -90
  161. package/modules/php-laravel/architecture-snippets/service-repository.md +0 -302
  162. package/modules/php-laravel/module.yaml +0 -15
  163. package/modules/php-laravel/stack-profile.yaml +0 -56
  164. package/modules/qc-playwright/stack-profile.yaml +0 -66
  165. package/modules/react/architecture-snippets/hooks-query-patterns.md +0 -254
  166. package/modules/react/module.yaml +0 -14
  167. package/modules/react/stack-profile.yaml +0 -63
  168. package/modules/react-native/module.yaml +0 -14
  169. package/modules/react-native/stack-profile.yaml +0 -56
  170. package/modules/vue/module.yaml +0 -14
  171. package/modules/vue/stack-profile.yaml +0 -65
  172. package/rules/data-protection.md +0 -80
  173. package/rules/workflow.md +0 -73
  174. package/scripts/init.sh +0 -49
  175. package/scripts/upgrade.sh +0 -94
  176. package/skills/code/SKILL.md +0 -19
  177. package/skills/code/SKILL.tmpl +0 -19
  178. package/skills/debug/SKILL.md +0 -19
  179. package/skills/debug/SKILL.tmpl +0 -19
  180. package/skills/design-spec/SKILL.md +0 -11
  181. package/skills/design-spec/SKILL.tmpl +0 -11
  182. package/skills/discovery/SKILL.md +0 -14
  183. package/skills/discovery/SKILL.tmpl +0 -14
  184. package/skills/prd/SKILL.md +0 -19
  185. package/skills/prd/SKILL.tmpl +0 -19
  186. package/skills/qc/qa-analyst/DOC_GAPS.template.md +0 -63
  187. package/skills/qc/qa-analyst/acceptance-criteria.md +0 -60
  188. package/skills/qc/qa-analyst/business-rules.md +0 -59
  189. package/skills/qc/qa-analyst/data-flow.md +0 -64
  190. package/skills/qc/qa-analyst/spec-breakdown.md +0 -61
  191. package/skills/qc/qa-designer/e2e/journey.md +0 -41
  192. package/skills/qc/qa-designer/exploratory/charter.md +0 -68
  193. package/skills/qc/qa-designer/exploratory/explore-to-functional.md +0 -43
  194. package/skills/qc/qa-designer/functional/api.md +0 -45
  195. package/skills/qc/qa-designer/functional/gui-feature.md +0 -46
  196. package/skills/qc/qa-designer/functional/gui-screen.md +0 -52
  197. package/skills/qc/qa-designer/integration/api.md +0 -42
  198. package/skills/qc/qa-designer/integration/db.md +0 -39
  199. package/skills/qc/qa-designer/integration/gui.md +0 -40
  200. package/skills/qc/qa-designer/integration/kafka.md +0 -40
  201. package/skills/qc/qa-designer/non-functional.md +0 -40
  202. package/skills/qc/qa-planner/test-plan.md +0 -120
  203. package/skills/qc/qa-reviewer/script/e2e.md +0 -87
  204. package/skills/qc/qa-reviewer/script/exploratory.md +0 -45
  205. package/skills/qc/qa-reviewer/script/functional.md +0 -101
  206. package/skills/qc/qa-reviewer/script/integration.md +0 -91
  207. package/skills/qc/qa-reviewer/script/non-functional.md +0 -126
  208. package/skills/qc/qa-reviewer/test-case/e2e.md +0 -73
  209. package/skills/qc/qa-reviewer/test-case/exploratory.md +0 -43
  210. package/skills/qc/qa-reviewer/test-case/functional.md +0 -76
  211. package/skills/qc/qa-reviewer/test-case/integration.md +0 -69
  212. package/skills/qc/qa-reviewer/test-case/non-functional.md +0 -73
  213. package/skills/qc/qa-runner/e2e.md +0 -49
  214. package/skills/qc/qa-runner/exploratory/session.md +0 -36
  215. package/skills/qc/qa-runner/functional/api.md +0 -35
  216. package/skills/qc/qa-runner/functional/gui-feature.md +0 -51
  217. package/skills/qc/qa-runner/functional/gui-screen.md +0 -55
  218. package/skills/qc/qa-runner/integration.md +0 -47
  219. package/skills/qc/qa-runner/non-functional.md +0 -49
  220. package/skills/qc/qa-runner/report/report.md +0 -37
  221. package/skills/setup-ai-first/SKILL.md +0 -19
  222. package/skills/setup-ai-first/SKILL.tmpl +0 -19
  223. package/skills/spec/SKILL.md +0 -19
  224. package/skills/spec/SKILL.tmpl +0 -19
  225. package/skills/test/SKILL.md +0 -18
  226. package/skills/test/SKILL.tmpl +0 -18
  227. package/steps/business-language.md +0 -56
  228. package/steps/capture-lesson.md +0 -79
  229. package/steps/context-loader.md +0 -385
  230. package/steps/gate.md +0 -94
  231. package/steps/report-footer.md +0 -102
  232. package/steps/review-fanout.md +0 -159
  233. package/steps/spawn-agent.md +0 -129
  234. package/steps/trace-mirror.md +0 -53
  235. package/templates/README.md +0 -47
  236. package/templates/architecture.template.md +0 -394
  237. package/templates/design-spec.template.md +0 -217
  238. package/templates/feature.template +0 -123
  239. package/templates/platform-guide.template.md +0 -145
  240. package/templates/prd.template.md +0 -283
  241. package/templates/product-definition.template.md +0 -188
  242. package/templates/project-context.yaml +0 -212
  243. package/templates/tech-design.template.md +0 -490
@@ -1,646 +0,0 @@
1
- # Phaser Game — Scenes, Prefabs, Domain Logic & WebView Bridge Patterns
2
-
3
- Stack: **Phaser 3 + TypeScript + Vite**, packaged to run from `file://` inside an **Android/iOS WebView**.
4
- Core ideas:
5
- - **Scenes orchestrate**, **domain modules do the math** (pure TS, no Phaser → unit-testable in Node).
6
- - **Integration = Approach A**: the game calls the **backend directly over HTTP**, using a token the
7
- host app (Flutter) injects via `window.GAME_CONFIG`. The native bridge is **thin** — only
8
- `onReady()` / `onClose()` (back/X → Flutter pops the WebView). No game data crosses the bridge.
9
- - `vite base: './'` so the built bundle loads over `file://`; `plugin-legacy` for old WebViews.
10
- - `eruda` debug console loads **only** with `?debug=1`.
11
-
12
- Examples use a **Gold Miner** game (swinging hook grabs gold; heavier gold retracts slower).
13
-
14
- ## Vite config — WebView-ready build
15
-
16
- ```typescript
17
- // vite.config.ts
18
- import { defineConfig } from 'vite';
19
- import legacy from '@vitejs/plugin-legacy';
20
-
21
- export default defineConfig({
22
- base: './', // REQUIRED: relative paths so dist/ works from file://
23
- build: {
24
- target: 'es2015',
25
- assetsInlineLimit: 4096,
26
- },
27
- plugins: [
28
- legacy({ // old Android WebView support (toggle per min-SDK)
29
- targets: ['Android >= 5', 'Chrome >= 40'],
30
- polyfills: true, // pulls core-js
31
- renderLegacyChunks: true,
32
- }),
33
- ],
34
- });
35
- ```
36
-
37
- ## Bootstrap — `index.html` + `main.ts`
38
-
39
- ```html
40
- <!-- index.html -->
41
- <!doctype html>
42
- <html lang="en">
43
- <head>
44
- <meta charset="utf-8" />
45
- <meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no, viewport-fit=cover" />
46
- <title>Gold Miner</title>
47
- <style>html,body{margin:0;height:100%;background:#1a1a2e;overflow:hidden}#game{width:100%;height:100%}</style>
48
- </head>
49
- <body>
50
- <div id="game"></div>
51
- <script type="module" src="/src/main.ts"></script>
52
- </body>
53
- </html>
54
- ```
55
-
56
- ```typescript
57
- // src/main.ts — mount debug console (if ?debug=1), then start Phaser
58
- import Phaser from 'phaser';
59
- import { gameConfig } from './config/game-config';
60
- import { maybeLoadDebugConsole } from './debug/eruda';
61
-
62
- async function bootstrap() {
63
- await maybeLoadDebugConsole(); // eruda only when ?debug=1
64
- new Phaser.Game(gameConfig);
65
- }
66
-
67
- bootstrap();
68
- ```
69
-
70
- ```typescript
71
- // src/config/game-config.ts
72
- import Phaser from 'phaser';
73
- import { BootScene } from '../scenes/BootScene';
74
- import { PreloadScene } from '../scenes/PreloadScene';
75
- import { MenuScene } from '../scenes/MenuScene';
76
- import { GameScene } from '../scenes/GameScene';
77
- import { GameOverScene } from '../scenes/GameOverScene';
78
- import { UIScene } from '../scenes/UIScene';
79
-
80
- export const gameConfig: Phaser.Types.Core.GameConfig = {
81
- type: Phaser.AUTO, // WebGL with Canvas fallback
82
- parent: 'game',
83
- backgroundColor: '#1a1a2e',
84
- scale: {
85
- mode: Phaser.Scale.FIT, // fit WebView, keep aspect
86
- autoCenter: Phaser.Scale.CENTER_BOTH,
87
- width: 960,
88
- height: 540,
89
- },
90
- physics: {
91
- default: 'arcade',
92
- arcade: { gravity: { x: 0, y: 0 }, debug: false },
93
- },
94
- scene: [BootScene, PreloadScene, MenuScene, GameScene, GameOverScene, UIScene],
95
- };
96
- ```
97
-
98
- ## Scenes — one responsibility each
99
-
100
- ```typescript
101
- // src/scenes/PreloadScene.ts — load every asset once, show a progress bar
102
- import Phaser from 'phaser';
103
-
104
- export class PreloadScene extends Phaser.Scene {
105
- constructor() { super('PreloadScene'); }
106
-
107
- preload() {
108
- const bar = this.add.rectangle(480, 270, 0, 20, 0xc9a227);
109
- this.load.on('progress', (p: number) => { bar.width = 400 * p; });
110
-
111
- this.load.image('bg', 'assets/bg.png');
112
- this.load.image('gold', 'assets/gold.png');
113
- this.load.atlas('miner', 'assets/miner.png', 'assets/miner.json');
114
- this.load.audio('grab', 'assets/grab.mp3');
115
- }
116
-
117
- create() { this.scene.start('MenuScene'); }
118
- }
119
- ```
120
-
121
- ```typescript
122
- // src/scenes/GameScene.ts — orchestrates; heavy math lives in src/domain/
123
- import Phaser from 'phaser';
124
- import { Hook } from '../objects/Hook';
125
- import { Gold } from '../objects/Gold';
126
- import { rollSpawn } from '../domain/spawn';
127
- import { calcScore } from '../domain/scoring';
128
- import { EventBus } from '../bridge/event-bus';
129
- import { SPAWN_TABLE } from '../config/constants';
130
-
131
- // @trace.implements=GAME-UC1-SC1
132
- // @trace.source=specs/game/swing-hook/bdd/GAME-UC1.feature
133
- export class GameScene extends Phaser.Scene {
134
- private hook!: Hook;
135
- private golds!: Phaser.Physics.Arcade.Group;
136
- private score = 0;
137
- private rng = new Phaser.Math.RandomDataGenerator(['gold-miner']);
138
-
139
- constructor() { super('GameScene'); }
140
-
141
- create() {
142
- this.add.image(480, 270, 'bg');
143
- this.scene.launch('UIScene'); // HUD overlay runs in parallel
144
-
145
- this.hook = new Hook(this, 480, 60);
146
- this.golds = this.physics.add.group();
147
- this.spawnField();
148
-
149
- // Arcade overlap does the collision — no hand-rolled loop
150
- this.physics.add.overlap(this.hook.tip, this.golds, (_tip, goldObj) => {
151
- this.hook.grab(goldObj as Gold);
152
- });
153
-
154
- this.input.on('pointerdown', () => this.hook.drop());
155
- this.input.keyboard?.on('keydown-SPACE', () => this.hook.drop());
156
- }
157
-
158
- update(_t: number, dtMs: number) {
159
- const dt = dtMs / 1000;
160
- const banked = this.hook.update(dt); // returns a Gold when fully retracted, else null
161
- if (banked) {
162
- this.score += calcScore(banked.value); // pure domain fn
163
- EventBus.emit('score-changed', this.score); // → UIScene + native bridge
164
- banked.destroy();
165
- }
166
- }
167
-
168
- private spawnField() {
169
- for (const spot of rollSpawn(SPAWN_TABLE, this.rng)) { // deterministic given rng
170
- this.golds.add(new Gold(this, spot.x, spot.y, spot.size));
171
- }
172
- }
173
- }
174
- ```
175
-
176
- ## Game Objects (prefabs) — self-contained Phaser subclasses
177
-
178
- ```typescript
179
- // src/objects/Gold.ts
180
- import Phaser from 'phaser';
181
-
182
- export class Gold extends Phaser.Physics.Arcade.Sprite {
183
- readonly value: number;
184
- readonly weight: number;
185
-
186
- constructor(scene: Phaser.Scene, x: number, y: number, size: number) {
187
- super(scene, x, y, 'gold');
188
- scene.add.existing(this);
189
- scene.physics.add.existing(this);
190
- this.setDisplaySize(size, size);
191
- this.value = size * 3;
192
- this.weight = size / 32; // heavier → slower retract (classic mechanic)
193
- }
194
- }
195
- ```
196
-
197
- ```typescript
198
- // src/objects/Hook.ts — swing → extend → retract state machine
199
- import Phaser from 'phaser';
200
- import { Gold } from './Gold';
201
- import { retractSpeed } from '../domain/hook-physics';
202
- import { SWING_SPEED, HOOK_SPEED } from '../config/constants';
203
-
204
- type HookState = 'swing' | 'extend' | 'retract';
205
-
206
- export class Hook extends Phaser.GameObjects.Container {
207
- readonly tip: Phaser.GameObjects.Zone; // arcade body used for overlap
208
- private angle = 0;
209
- private dir = 1;
210
- private length = 0;
211
- private state: HookState = 'swing';
212
- private grabbed: Gold | null = null;
213
-
214
- constructor(scene: Phaser.Scene, x: number, y: number) {
215
- super(scene, x, y);
216
- scene.add.existing(this);
217
- this.tip = scene.add.zone(x, y, 16, 16);
218
- scene.physics.add.existing(this.tip);
219
- }
220
-
221
- drop() { if (this.state === 'swing') this.state = 'extend'; }
222
-
223
- grab(gold: Gold) {
224
- if (this.state !== 'extend' || this.grabbed) return;
225
- this.grabbed = gold;
226
- this.state = 'retract';
227
- }
228
-
229
- /** @returns the banked Gold when fully retracted, else null */
230
- update(dt: number): Gold | null {
231
- if (this.state === 'swing') {
232
- this.angle += SWING_SPEED * this.dir * dt;
233
- if (Math.abs(this.angle) > 1.3) this.dir *= -1;
234
- } else if (this.state === 'extend') {
235
- this.length += HOOK_SPEED * dt;
236
- if (this.length > 520) this.state = 'retract';
237
- } else {
238
- this.length -= retractSpeed(HOOK_SPEED, this.grabbed?.weight ?? 1) * dt;
239
- if (this.length <= 0) {
240
- this.length = 0;
241
- this.state = 'swing';
242
- const banked = this.grabbed;
243
- this.grabbed = null;
244
- this.syncTip();
245
- return banked;
246
- }
247
- }
248
- this.syncTip();
249
- return null;
250
- }
251
-
252
- private syncTip() {
253
- this.tip.setPosition(this.x + Math.sin(this.angle) * this.length,
254
- this.y + Math.cos(this.angle) * this.length);
255
- this.grabbed?.setPosition(this.tip.x, this.tip.y);
256
- }
257
- }
258
- ```
259
-
260
- ## Domain logic — PURE TypeScript, zero Phaser (unit-testable)
261
-
262
- ```typescript
263
- // src/domain/hook-physics.ts
264
- // Heavier gold retracts slower. Pure math → trivially testable.
265
- export function retractSpeed(baseSpeed: number, weight: number): number {
266
- return baseSpeed / Math.max(weight, 0.1);
267
- }
268
- ```
269
-
270
- ```typescript
271
- // src/domain/scoring.ts
272
- export function calcScore(goldValue: number, comboMultiplier = 1): number {
273
- return Math.round(goldValue * comboMultiplier);
274
- }
275
- ```
276
-
277
- ```typescript
278
- // src/domain/spawn.ts — inject the RNG so spawns are deterministic in tests
279
- export interface SpawnSpot { x: number; y: number; size: number; }
280
- export interface SpawnRow { count: number; sizes: number[]; band: [number, number]; }
281
-
282
- export interface Rng { between(min: number, max: number): number; pick<T>(arr: T[]): T; }
283
-
284
- export function rollSpawn(table: SpawnRow[], rng: Rng): SpawnSpot[] {
285
- const spots: SpawnSpot[] = [];
286
- for (const row of table) {
287
- for (let i = 0; i < row.count; i++) {
288
- spots.push({
289
- x: rng.between(60, 900),
290
- y: rng.between(row.band[0], row.band[1]),
291
- size: rng.pick(row.sizes),
292
- });
293
- }
294
- }
295
- return spots;
296
- }
297
- ```
298
-
299
- ## Integration model — **Approach A** (game calls backend directly)
300
-
301
- ```
302
- Flutter app WebView (this game)
303
- ─────────── ───────────────────
304
- inject window.GAME_CONFIG ───────▶ host-config.ts (read + validate)
305
- { apiBase, token, userId } │
306
-
307
- api/client.ts ──HTTP+Bearer──▶ Backend
308
- loadData() / submitResult() ◀──────────
309
-
310
- back / X pressed ◀──onClose()── native-bridge.ts (window control only)
311
- Navigator.pop()
312
- ```
313
-
314
- - **Data** (load + submit result) goes over **HTTP straight from the game**, authorized by the token Flutter injected.
315
- - **The native bridge is thin**: it only tells Flutter *"I'm ready"* and *"close me"*. No game data crosses it.
316
-
317
- ## Host config — Flutter injects it before load
318
-
319
- ```typescript
320
- // src/contracts/game-contract.ts — the native window-control surface (control only, no data)
321
- export interface GameContract {
322
- onReady(): void; // boot succeeded — Flutter may hide its splash
323
- onClose(): void; // back / X pressed — Flutter pops the WebView route
324
- }
325
-
326
- /** Injected by Flutter into the WebView BEFORE the bundle loads. */
327
- export interface HostConfig {
328
- apiBase: string; // e.g. https://api.edupia.vn
329
- token: string; // Bearer token owned by the app
330
- userId: string;
331
- sessionId: string;
332
- }
333
-
334
- declare global {
335
- interface Window {
336
- // Android JS interface OR flutter_inappwebview handler — abstracted in native-bridge.ts
337
- Android?: { postMessage(json: string): void };
338
- flutter_inappwebview?: { callHandler(name: string, ...args: unknown[]): Promise<unknown> };
339
- GAME_CONFIG?: HostConfig;
340
- }
341
- }
342
- ```
343
-
344
- ```typescript
345
- // src/config/host-config.ts — read once, validate, fail loudly if missing
346
- import type { HostConfig } from '../contracts/game-contract';
347
- import { bridge } from '../bridge/native-bridge';
348
-
349
- export function readHostConfig(): HostConfig {
350
- const cfg = window.GAME_CONFIG;
351
- const ok = cfg && cfg.apiBase && cfg.token;
352
- if (!ok) {
353
- console.error('[host-config] missing GAME_CONFIG — cannot reach backend');
354
- bridge.onClose(); // bail back to the app instead of a broken game
355
- throw new Error('Missing HostConfig');
356
- }
357
- return cfg!;
358
- }
359
-
360
- // Dev fallback: run `vite` in a plain browser with a .env-driven config
361
- export function readHostConfigDev(): HostConfig {
362
- return window.GAME_CONFIG ?? {
363
- apiBase: import.meta.env.VITE_API_BASE ?? 'http://localhost:8080',
364
- token: import.meta.env.VITE_DEV_TOKEN ?? 'dev-token',
365
- userId: 'dev', sessionId: 'dev',
366
- };
367
- }
368
- ```
369
-
370
- ## API client — direct fetch with the injected token (Approach A)
371
-
372
- ```typescript
373
- // src/contracts/api.dto.ts — typed backend contracts
374
- export interface GameDataDto { level: number; spawnSeed: number; timeLimit: number; highScore: number; }
375
- export interface ResultDto { sessionId: string; finalScore: number; goldGrabbed: number; durationMs: number; }
376
- export interface SubmitAck { accepted: boolean; newHighScore: boolean; }
377
- ```
378
-
379
- ```typescript
380
- // src/api/client.ts — one fetch wrapper; token from HostConfig
381
- import type { HostConfig } from '../contracts/game-contract';
382
-
383
- export function createApiClient(cfg: HostConfig) {
384
- async function request<T>(path: string, init: RequestInit = {}): Promise<T> {
385
- const res = await fetch(`${cfg.apiBase}${path}`, {
386
- ...init,
387
- headers: {
388
- 'Content-Type': 'application/json',
389
- Authorization: `Bearer ${cfg.token}`, // token owned by the Flutter app
390
- ...(init.headers ?? {}),
391
- },
392
- });
393
- if (res.status === 401) throw new ApiError('unauthorized', 401);
394
- if (!res.ok) throw new ApiError(`http_${res.status}`, res.status);
395
- return res.json() as Promise<T>;
396
- }
397
- return { request };
398
- }
399
-
400
- export class ApiError extends Error {
401
- constructor(message: string, readonly status: number) { super(message); }
402
- }
403
- ```
404
-
405
- ```typescript
406
- // src/api/game.api.ts — the two calls this game needs
407
- import type { ApiClient } from './client'; // typeof createApiClient return
408
- import type { GameDataDto, ResultDto, SubmitAck } from '../contracts/api.dto';
409
-
410
- export function createGameApi(client: ReturnType<typeof import('./client').createApiClient>) {
411
- return {
412
- // @trace.implements=GAME-UC4-SC1 (load data for the game)
413
- // @trace.source=specs/game/load-data/bdd/GAME-UC4.feature
414
- loadData: (userId: string) =>
415
- client.request<GameDataDto>(`/v1/gold-miner/data?userId=${encodeURIComponent(userId)}`),
416
-
417
- // @trace.implements=GAME-UC5-SC1 (submit result to server)
418
- // @trace.source=specs/game/submit-result/bdd/GAME-UC5.feature
419
- submitResult: (dto: ResultDto) =>
420
- client.request<SubmitAck>('/v1/gold-miner/result', {
421
- method: 'POST',
422
- body: JSON.stringify(dto),
423
- }),
424
- };
425
- }
426
- ```
427
-
428
- ## Thin native bridge — window control only (ready + close)
429
-
430
- ```typescript
431
- // src/bridge/native-bridge.ts — abstracts flutter_inappwebview / Android JS interface
432
- import type { GameContract } from '../contracts/game-contract';
433
-
434
- class NativeBridge implements GameContract {
435
- private post(type: string, payload: unknown = {}) {
436
- // flutter_inappwebview (preferred): Flutter registers a JS handler
437
- if (window.flutter_inappwebview) {
438
- void window.flutter_inappwebview.callHandler(type, payload);
439
- } else if (window.Android) { // webview_flutter JavaScriptChannel
440
- window.Android.postMessage(JSON.stringify({ type, payload }));
441
- } else {
442
- console.debug('[bridge:no-native]', type, payload); // plain browser dev
443
- }
444
- }
445
- onReady() { this.post('onReady'); }
446
- onClose() { this.post('onClose'); } // Flutter side: Navigator.pop() → back to app screen
447
- }
448
-
449
- export const bridge: GameContract = new NativeBridge();
450
- ```
451
-
452
- > **Flutter side (reference, `flutter_inappwebview`)** — inject config + handle close:
453
- > ```dart
454
- > initialUserScripts: UnmodifiableListView([
455
- > UserScript(
456
- > source: "window.GAME_CONFIG = ${jsonEncode(config)};",
457
- > injectionTime: UserScriptInjectionTime.AT_DOCUMENT_START, // before bundle runs
458
- > ),
459
- > ]),
460
- > onWebViewCreated: (c) {
461
- > c.addJavaScriptHandler(handlerName: 'onClose', callback: (_) { Navigator.pop(context); });
462
- > c.addJavaScriptHandler(handlerName: 'onReady', callback: (_) => hideSplash());
463
- > },
464
- > ```
465
-
466
- ## Wiring it together — Boot & Preload scenes
467
-
468
- ```typescript
469
- // src/scenes/BootScene.ts — read host config, build API, then preload
470
- import Phaser from 'phaser';
471
- import { readHostConfig } from '../config/host-config';
472
- import { createApiClient } from '../api/client';
473
- import { createGameApi } from '../api/game.api';
474
- import { bridge } from '../bridge/native-bridge';
475
-
476
- export class BootScene extends Phaser.Scene {
477
- constructor() { super('BootScene'); }
478
- create() {
479
- const cfg = readHostConfig(); // throws + onClose() if invalid
480
- const api = createGameApi(createApiClient(cfg));
481
- this.registry.set('cfg', cfg); // share across scenes via registry
482
- this.registry.set('api', api);
483
- bridge.onReady();
484
- this.scene.start('PreloadScene');
485
- }
486
- }
487
- ```
488
-
489
- ```typescript
490
- // src/scenes/PreloadScene.ts (create) — load game data over HTTP, then start the game
491
- async create() {
492
- const api = this.registry.get('api');
493
- const cfg = this.registry.get('cfg');
494
- try {
495
- const data = await api.loadData(cfg.userId); // direct backend call (Approach A)
496
- this.scene.start('GameScene', { data });
497
- } catch (e) {
498
- console.error('loadData failed', e);
499
- this.scene.start('MenuScene', { offline: true }); // graceful fallback
500
- }
501
- }
502
- ```
503
-
504
- ```typescript
505
- // GameScene — on game over, submit the result straight to the backend
506
- private async endGame() {
507
- const api = this.registry.get('api');
508
- const cfg = this.registry.get('cfg');
509
- try {
510
- await api.submitResult({
511
- sessionId: cfg.sessionId,
512
- finalScore: this.score,
513
- goldGrabbed: this.grabbedCount,
514
- durationMs: this.time.now,
515
- });
516
- } catch (e) {
517
- console.error('submitResult failed', e); // optionally queue for retry
518
- }
519
- this.scene.start('GameOverScene', { score: this.score });
520
- }
521
- ```
522
-
523
- ## Event bus — game ↔ HUD without hard scene references
524
-
525
- ```typescript
526
- // src/bridge/event-bus.ts
527
- import Phaser from 'phaser';
528
- export const EventBus = new Phaser.Events.EventEmitter();
529
- // GameScene: EventBus.emit('score-changed', score)
530
- // UIScene: EventBus.on('score-changed', (s) => this.scoreText.setText(`Score: ${s}`))
531
- ```
532
-
533
- ## Debug console — eruda, only with `?debug=1`
534
-
535
- ```typescript
536
- // src/debug/eruda.ts — lazy import keeps eruda out of the normal bundle path
537
- export async function maybeLoadDebugConsole(): Promise<void> {
538
- if (new URLSearchParams(location.search).get('debug') !== '1') return;
539
- const eruda = (await import('eruda')).default; // dynamic import → its own chunk
540
- eruda.init();
541
- }
542
- ```
543
-
544
- ## Unit Test — domain logic under Vitest (no Phaser, no browser)
545
-
546
- ```typescript
547
- // tests/hook-physics.test.ts
548
- // @trace.verifies=GAME-UC1
549
- // @trace.test_type=unit
550
- import { describe, it, expect } from 'vitest';
551
- import { retractSpeed } from '../src/domain/hook-physics';
552
-
553
- describe('retractSpeed', () => {
554
- it('is slower for heavier gold', () => {
555
- expect(retractSpeed(320, 2)).toBeLessThan(retractSpeed(320, 1));
556
- });
557
- it('clamps tiny weights to avoid divide blow-up', () => {
558
- expect(Number.isFinite(retractSpeed(320, 0))).toBe(true);
559
- });
560
- });
561
- ```
562
-
563
- ```typescript
564
- // tests/spawn.test.ts — deterministic thanks to an injected RNG
565
- // @trace.verifies=GAME-UC0
566
- // @trace.test_type=unit
567
- import { describe, it, expect } from 'vitest';
568
- import { rollSpawn, type Rng } from '../src/domain/spawn';
569
-
570
- const fixedRng: Rng = { between: (min) => min, pick: (arr) => arr[0] };
571
-
572
- describe('rollSpawn', () => {
573
- it('produces one spot per count across all rows', () => {
574
- const spots = rollSpawn([{ count: 3, sizes: [32], band: [200, 400] }], fixedRng);
575
- expect(spots).toHaveLength(3);
576
- expect(spots[0]).toEqual({ x: 60, y: 200, size: 32 });
577
- });
578
- });
579
- ```
580
-
581
- ## Unit Test — API client (Approach A) with mocked fetch
582
-
583
- ```typescript
584
- // tests/game-api.test.ts
585
- // @trace.verifies=GAME-UC5
586
- // @trace.test_type=unit
587
- import { describe, it, expect, vi, beforeEach } from 'vitest';
588
- import { createApiClient } from '../src/api/client';
589
- import { createGameApi } from '../src/api/game.api';
590
-
591
- const cfg = { apiBase: 'https://api.test', token: 'tkn', userId: 'u1', sessionId: 's1' };
592
-
593
- beforeEach(() => vi.restoreAllMocks());
594
-
595
- describe('submitResult', () => {
596
- it('POSTs to the result endpoint with the Bearer token from HostConfig', async () => {
597
- const fetchMock = vi.fn().mockResolvedValue(
598
- new Response(JSON.stringify({ accepted: true, newHighScore: false }), { status: 200 }),
599
- );
600
- vi.stubGlobal('fetch', fetchMock);
601
-
602
- const api = createGameApi(createApiClient(cfg));
603
- const ack = await api.submitResult({ sessionId: 's1', finalScore: 120, goldGrabbed: 4, durationMs: 30000 });
604
-
605
- expect(ack.accepted).toBe(true);
606
- const [url, init] = fetchMock.mock.calls[0];
607
- expect(url).toBe('https://api.test/v1/gold-miner/result');
608
- expect(init.method).toBe('POST');
609
- expect(init.headers.Authorization).toBe('Bearer tkn');
610
- });
611
-
612
- it('throws ApiError(401) when the token is rejected', async () => {
613
- vi.stubGlobal('fetch', vi.fn().mockResolvedValue(new Response('', { status: 401 })));
614
- const api = createGameApi(createApiClient(cfg));
615
- await expect(api.loadData('u1')).rejects.toMatchObject({ status: 401 });
616
- });
617
- });
618
- ```
619
-
620
- ## Smoke Test — Playwright: stub host config + intercept backend
621
-
622
- ```typescript
623
- // tests/e2e/play.spec.ts
624
- // @trace.verifies=GAME-UC0
625
- // @trace.test_type=integration
626
- import { test, expect } from '@playwright/test';
627
-
628
- test('boots with injected config, loads data, closes on back', async ({ page }) => {
629
- // Emulate what Flutter injects BEFORE the bundle runs
630
- await page.addInitScript(() => {
631
- (window as any).GAME_CONFIG = {
632
- apiBase: 'https://api.test', token: 'tkn', userId: 'u1', sessionId: 's1',
633
- };
634
- });
635
- // No real server needed — intercept the direct backend calls (Approach A)
636
- await page.route('**/v1/gold-miner/data*', (r) =>
637
- r.fulfill({ json: { level: 1, spawnSeed: 42, timeLimit: 60, highScore: 0 } }));
638
- await page.route('**/v1/gold-miner/result', (r) =>
639
- r.fulfill({ json: { accepted: true, newHighScore: true } }));
640
-
641
- await page.goto('http://localhost:4173/'); // vite preview
642
- await expect(page.locator('#game canvas')).toBeVisible();
643
- await page.locator('#game').click(); // start / drop the hook
644
- await expect(page.locator('#hud-score')).toHaveText(/Score: \d+/);
645
- });
646
- ```
@@ -1,15 +0,0 @@
1
- name: "Phaser Game"
2
- version: "1.0.0"
3
- description: "HTML5 game with Phaser 3 + TypeScript + Vite, packaged for Android WebView (file://)"
4
- language: "TypeScript"
5
- framework: "Phaser 3"
6
- stack_type: "frontend"
7
- default_layer_order:
8
- - Contracts / types (GameContract, DTOs shared with the native host)
9
- - Config (game config, tuning constants, asset manifest)
10
- - Domain logic (pure TS: scoring, spawn tables, physics helpers — framework-free)
11
- - Scenes (Boot → Preload → Menu → Game → GameOver, UI overlay scene)
12
- - Game objects / prefabs (Phaser.GameObjects subclasses: Miner, Hook, Gold)
13
- - Bridge (WebView ↔ native messaging via GameContract)
14
- - Bootstrap (main.ts → new Phaser.Game)
15
- test_framework: "Vitest (pure domain logic) + Playwright (in-browser smoke)"