@techninja/clearstack 0.3.16 → 0.3.19
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/docs/BUILD_LOG.md +107 -0
- package/docs/CONVENTIONS.md +45 -5
- package/docs/JSDOC_TYPING.md +24 -3
- package/docs/PLATFORM_STACKING.md +486 -0
- package/docs/QUICKSTART.md +2 -2
- package/docs/STATE_AND_ROUTING.md +54 -49
- package/docs/TESTING.md +52 -19
- package/lib/check.js +26 -50
- package/lib/init.js +5 -0
- package/lib/platform-files.js +113 -0
- package/lib/platform.js +52 -0
- package/lib/spec-config.js +55 -0
- package/lib/spec-utils.js +5 -2
- package/lib/update.js +8 -0
- package/package.json +1 -1
- package/templates/fullstack/src/store/realtimeSync.js +4 -4
- package/templates/shared/.configs/eslint.config.js +1 -0
- package/templates/shared/.configs/web-test-runner.config.js +28 -1
- package/templates/shared/docs/clearstack/CONVENTIONS.md +45 -5
- package/templates/shared/docs/clearstack/JSDOC_TYPING.md +24 -3
- package/templates/shared/docs/clearstack/QUICKSTART.md +2 -2
- package/templates/shared/docs/clearstack/STATE_AND_ROUTING.md +54 -49
- package/templates/shared/docs/clearstack/TESTING.md +52 -19
- package/templates/shared/docs/clearstack/BUILD_LOG.md +0 -252
|
@@ -13,8 +13,8 @@
|
|
|
13
13
|
TypeScript's `tsc` compiler validates JSDoc annotations via `--checkJs`,
|
|
14
14
|
giving us compile-time type checking without a build step.
|
|
15
15
|
|
|
16
|
-
A `jsconfig.json` at
|
|
17
|
-
`npm run
|
|
16
|
+
A `jsconfig.json` at `.configs/jsconfig.json` enables `checkJs: true` for
|
|
17
|
+
frontend code. Running `npm run spec types` invokes `tsc` against it.
|
|
18
18
|
|
|
19
19
|
1. Reads all `.js` files in `src/` and `scripts/`
|
|
20
20
|
2. Parses JSDoc annotations as type information
|
|
@@ -24,6 +24,27 @@ A `jsconfig.json` at the project root enables `checkJs: true`. Running
|
|
|
24
24
|
This means `@typedef`, `@type`, `@param`, and `@returns` are not just
|
|
25
25
|
documentation — they are **enforced types**.
|
|
26
26
|
|
|
27
|
+
### Multiple Type Configs
|
|
28
|
+
|
|
29
|
+
The spec checker auto-discovers `jsconfig.json` files in subdirectories.
|
|
30
|
+
If your project has a separate backend (e.g. `api/`) with its own
|
|
31
|
+
dependencies and module resolution, add `api/jsconfig.json`:
|
|
32
|
+
|
|
33
|
+
```
|
|
34
|
+
your-project/
|
|
35
|
+
├── .configs/jsconfig.json ← frontend (src/, scripts/)
|
|
36
|
+
└── api/jsconfig.json ← backend (api/, auto-discovered)
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
npm run spec types # runs all discovered jsconfigs
|
|
41
|
+
npm run spec types frontend # just .configs/jsconfig.json
|
|
42
|
+
npm run spec types api # just api/jsconfig.json
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
The child key matches the directory name. Both must pass for
|
|
46
|
+
`npm run spec all` to succeed.
|
|
47
|
+
|
|
27
48
|
---
|
|
28
49
|
|
|
29
50
|
## Component Properties
|
|
@@ -151,4 +172,4 @@ list: async (params) => {
|
|
|
151
172
|
- Keep JSDoc blocks to 3–5 lines. No novels.
|
|
152
173
|
- Use `/** @type {any} */ (expr)` for framework type limitations (e.g.
|
|
153
174
|
`store.pending()` on array results) — document why with a comment.
|
|
154
|
-
- Run `npm run
|
|
175
|
+
- Run `npm run spec types` before committing. Zero errors required.
|
|
@@ -137,7 +137,7 @@ npm run spec lint # ESLint + Stylelint + Markdown
|
|
|
137
137
|
npm run spec lint es # ESLint only
|
|
138
138
|
npm run spec format # Prettier
|
|
139
139
|
npm run spec imports # import map aliases
|
|
140
|
-
npm run spec types # JSDoc
|
|
140
|
+
npm run spec types # JSDoc types (all jsconfigs)
|
|
141
141
|
npm run spec audit # security audit
|
|
142
142
|
npm test # Node + browser tests
|
|
143
143
|
```
|
|
@@ -172,7 +172,7 @@ spec all → lint + format + code + docs + imports + types + audit
|
|
|
172
172
|
| Install dependencies | `npm install` |
|
|
173
173
|
| Start dev server | `npm run dev` |
|
|
174
174
|
| Lint + format | `npm run spec lint && npm run spec format` |
|
|
175
|
-
| Type check | `npm run
|
|
175
|
+
| Type check | `npm run spec types` |
|
|
176
176
|
| Run tests | `npm test` |
|
|
177
177
|
| Full spec check | `npm run spec` |
|
|
178
178
|
| Update spec + configs | `npm run spec update` |
|
|
@@ -253,6 +253,43 @@ This applies to any property with `value: []` and an async `connect`.
|
|
|
253
253
|
The `connect` callback sets the value and calls `invalidate()`, but the
|
|
254
254
|
first render happens before that resolves.
|
|
255
255
|
|
|
256
|
+
#### Async Init with Child Component Props
|
|
257
|
+
|
|
258
|
+
When a parent loads data during `connect` and passes it to a child via
|
|
259
|
+
template props, the child may never see the change. Hybrids' change
|
|
260
|
+
detection requires observing old → new. If the prop is set before the
|
|
261
|
+
child mounts, there's no "old" value to compare against.
|
|
262
|
+
|
|
263
|
+
```javascript
|
|
264
|
+
// ❌ BAD — resultCount set during connect, before child mounts
|
|
265
|
+
_init: {
|
|
266
|
+
value: false,
|
|
267
|
+
connect: (host, _key, invalidate) => {
|
|
268
|
+
loadData(host).then(() => {
|
|
269
|
+
host.resultCount = 14;
|
|
270
|
+
invalidate(); // child mounts with 14, never sees a change
|
|
271
|
+
});
|
|
272
|
+
},
|
|
273
|
+
},
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
The fix: defer the prop update to after the first render frame so the
|
|
277
|
+
child is mounted and can observe the transition:
|
|
278
|
+
|
|
279
|
+
```javascript
|
|
280
|
+
// ✅ GOOD — child mounts with default (0), then sees 0 → 14
|
|
281
|
+
connect: (host, _key, invalidate) => {
|
|
282
|
+
loadData(host).then(() => {
|
|
283
|
+
invalidate();
|
|
284
|
+
requestAnimationFrame(() => { host.resultCount = loadedCount; });
|
|
285
|
+
});
|
|
286
|
+
},
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
Pre-populate external caches during `loadData` so the child has data
|
|
290
|
+
ready when the deferred update triggers. The 0 → 14 transition happens
|
|
291
|
+
within a single frame — no flicker.
|
|
292
|
+
|
|
256
293
|
---
|
|
257
294
|
|
|
258
295
|
## Routing
|
|
@@ -410,37 +447,16 @@ For live data updates, the backend pushes events via **Server-Sent Events
|
|
|
410
447
|
|
|
411
448
|
### Frontend: SSE Listener
|
|
412
449
|
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
import { store } from 'hybrids';
|
|
417
|
-
|
|
418
|
-
export function connectRealtime(url, modelMap) {
|
|
419
|
-
const source = new EventSource(url);
|
|
420
|
-
|
|
421
|
-
source.addEventListener('update', (event) => {
|
|
422
|
-
const { type } = JSON.parse(event.data);
|
|
423
|
-
const Model = modelMap[type];
|
|
424
|
-
if (Model) store.clear(Model); // full clear triggers re-fetch
|
|
425
|
-
});
|
|
426
|
-
|
|
427
|
-
source.addEventListener('error', () => {
|
|
428
|
-
source.close();
|
|
429
|
-
setTimeout(() => connectRealtime(url, modelMap), 5000);
|
|
430
|
-
});
|
|
431
|
-
|
|
432
|
-
return () => source.close();
|
|
433
|
-
}
|
|
434
|
-
```
|
|
450
|
+
`src/utils/realtimeSync.js` connects to the SSE endpoint, listens for
|
|
451
|
+
`update` events, and calls `store.clear(Model)` to invalidate the cache.
|
|
452
|
+
Any component bound via `store()` re-fetches automatically.
|
|
435
453
|
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
from the API. This is the mechanism that makes multi-user realtime work —
|
|
439
|
-
when user A creates a task, user B's task list updates automatically.
|
|
454
|
+
The listener reconnects on error (5s delay) and returns a disconnect
|
|
455
|
+
function for cleanup in the router shell's `connect` descriptor.
|
|
440
456
|
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
457
|
+
`store.clear(Model)` is also called by form submit handlers after a
|
|
458
|
+
successful save. The SSE event that follows is redundant for the local
|
|
459
|
+
user but ensures other connected clients update.
|
|
444
460
|
|
|
445
461
|
### Backend: SSE Endpoint
|
|
446
462
|
|
|
@@ -449,31 +465,20 @@ contract.
|
|
|
449
465
|
|
|
450
466
|
### Wiring It Up
|
|
451
467
|
|
|
452
|
-
In the router shell
|
|
468
|
+
In the router shell, use a `connect` descriptor to start SSE and return
|
|
469
|
+
the disconnect function. Map entity types to store models:
|
|
453
470
|
|
|
454
471
|
```javascript
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
tag: 'app-router',
|
|
460
|
-
stack: router(HomeView, { url: '/' }),
|
|
461
|
-
connection: {
|
|
462
|
-
value: undefined,
|
|
463
|
-
connect(host) {
|
|
464
|
-
const disconnect = connectRealtime('/api/events', {
|
|
465
|
-
user: UserModel,
|
|
466
|
-
});
|
|
467
|
-
return disconnect;
|
|
468
|
-
},
|
|
472
|
+
connection: {
|
|
473
|
+
value: undefined,
|
|
474
|
+
connect(host) {
|
|
475
|
+
return connectRealtime('/api/events', { user: UserModel });
|
|
469
476
|
},
|
|
470
|
-
|
|
471
|
-
});
|
|
477
|
+
},
|
|
472
478
|
```
|
|
473
479
|
|
|
474
|
-
When the backend sends `{ type: "user"
|
|
475
|
-
|
|
476
|
-
re-fetches automatically.
|
|
480
|
+
When the backend sends `{ type: "user" }` over SSE, the store cache for
|
|
481
|
+
`UserModel` is cleared and any bound component re-fetches automatically.
|
|
477
482
|
|
|
478
483
|
### Debouncing Batch Operations
|
|
479
484
|
|
|
@@ -169,36 +169,31 @@ describe('GET /api/users?schema=true', () => {
|
|
|
169
169
|
|
|
170
170
|
### Component Tests (browser)
|
|
171
171
|
|
|
172
|
-
Using `@web/test-runner` with `@open-wc/testing
|
|
172
|
+
Using `@web/test-runner` with `@open-wc/testing`. Since all components
|
|
173
|
+
use light DOM (`shadow: false`), query the host element directly — not
|
|
174
|
+
`shadowRoot`:
|
|
173
175
|
|
|
174
176
|
```javascript
|
|
175
177
|
// src/components/atoms/app-button/app-button.test.js
|
|
176
|
-
import { fixture,
|
|
178
|
+
import { fixture, expect } from '@open-wc/testing';
|
|
177
179
|
import './app-button.js';
|
|
178
180
|
|
|
179
181
|
describe('app-button', () => {
|
|
180
182
|
it('renders with label', async () => {
|
|
181
|
-
const el = await fixture(
|
|
182
|
-
|
|
183
|
+
const el = await fixture(`<app-button label="Click me"></app-button>`);
|
|
184
|
+
await new Promise((r) => requestAnimationFrame(r));
|
|
185
|
+
const button = el.querySelector('button');
|
|
183
186
|
expect(button.textContent).to.contain('Click me');
|
|
184
187
|
});
|
|
188
|
+
});
|
|
189
|
+
```
|
|
185
190
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
const button = el.shadowRoot.querySelector('button');
|
|
189
|
-
expect(button.hasAttribute('disabled')).to.be.true;
|
|
190
|
-
});
|
|
191
|
+
The `requestAnimationFrame` wait gives hybrids time to complete its
|
|
192
|
+
render cycle. For components with async store data, use a longer timeout:
|
|
191
193
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
el.addEventListener('click', () => {
|
|
196
|
-
clicked = true;
|
|
197
|
-
});
|
|
198
|
-
el.shadowRoot.querySelector('button').click();
|
|
199
|
-
expect(clicked).to.be.true;
|
|
200
|
-
});
|
|
201
|
-
});
|
|
194
|
+
```javascript
|
|
195
|
+
const tick = () => new Promise((r) => setTimeout(r, 100));
|
|
196
|
+
await tick(); // after fixture, before assertions
|
|
202
197
|
```
|
|
203
198
|
|
|
204
199
|
### Store Integration Tests (browser)
|
|
@@ -227,16 +222,54 @@ describe('task-list', () => {
|
|
|
227
222
|
|
|
228
223
|
### web-test-runner.config.js
|
|
229
224
|
|
|
225
|
+
`@web/test-runner` uses `nodeResolve` for bare specifiers like `hybrids`,
|
|
226
|
+
but it does **not** support browser import maps. The `#prefix/` aliases
|
|
227
|
+
from `index.html` won't resolve in tests without a custom plugin:
|
|
228
|
+
|
|
230
229
|
```javascript
|
|
231
230
|
import { playwrightLauncher } from '@web/test-runner-playwright';
|
|
231
|
+
import { dirname, resolve } from 'node:path';
|
|
232
|
+
import { fileURLToPath } from 'node:url';
|
|
233
|
+
|
|
234
|
+
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
|
235
|
+
|
|
236
|
+
const aliases = {
|
|
237
|
+
'#store/': '/src/store/',
|
|
238
|
+
'#utils/': '/src/utils/',
|
|
239
|
+
'#atoms/': '/src/components/atoms/',
|
|
240
|
+
'#molecules/': '/src/components/molecules/',
|
|
241
|
+
'#organisms/': '/src/components/organisms/',
|
|
242
|
+
'#templates/': '/src/components/templates/',
|
|
243
|
+
'#pages/': '/src/pages/',
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
function importMapPlugin() {
|
|
247
|
+
return {
|
|
248
|
+
name: 'import-map-aliases',
|
|
249
|
+
resolveImport({ source }) {
|
|
250
|
+
for (const [prefix, target] of Object.entries(aliases)) {
|
|
251
|
+
if (source.startsWith(prefix)) return source.replace(prefix, target);
|
|
252
|
+
}
|
|
253
|
+
},
|
|
254
|
+
};
|
|
255
|
+
}
|
|
232
256
|
|
|
233
257
|
export default {
|
|
234
258
|
files: 'src/components/**/*.test.js',
|
|
235
259
|
nodeResolve: true,
|
|
260
|
+
rootDir: ROOT,
|
|
236
261
|
browsers: [playwrightLauncher({ product: 'chromium' })],
|
|
262
|
+
plugins: [importMapPlugin()],
|
|
237
263
|
};
|
|
238
264
|
```
|
|
239
265
|
|
|
266
|
+
Key points:
|
|
267
|
+
|
|
268
|
+
- **`rootDir`** must be the project root so `/src/store/` resolves correctly.
|
|
269
|
+
- **Aliases must match the import map** in `index.html`. If you add a prefix
|
|
270
|
+
there, add it here too.
|
|
271
|
+
- Test files can use `#prefix/` imports just like app code.
|
|
272
|
+
|
|
240
273
|
### package.json Scripts
|
|
241
274
|
|
|
242
275
|
```json
|
|
@@ -1,252 +0,0 @@
|
|
|
1
|
-
# Build Log
|
|
2
|
-
|
|
3
|
-
## How This Project Was Built
|
|
4
|
-
|
|
5
|
-
> This entire repository — specification, implementation, tests, and this
|
|
6
|
-
> document — was authored in a single continuous conversation between a human
|
|
7
|
-
> and an LLM (Amazon Q). No code was written outside this collaboration.
|
|
8
|
-
|
|
9
|
-
---
|
|
10
|
-
|
|
11
|
-
## Timeline
|
|
12
|
-
|
|
13
|
-
The project was built over ~1.5 days in a single LLM context window.
|
|
14
|
-
Each phase was implemented, tested, and verified before proceeding.
|
|
15
|
-
|
|
16
|
-
### Phase 1: Specification
|
|
17
|
-
|
|
18
|
-
- Wrote the initial spec as one file, hit ~500 lines
|
|
19
|
-
- Split into 7 topic-specific documents (the spec eating its own dogfood)
|
|
20
|
-
- Chose Hybrids.js after reading its source and type definitions from node_modules
|
|
21
|
-
|
|
22
|
-
### Phase 2: Infrastructure
|
|
23
|
-
|
|
24
|
-
- Express server, vendor-deps script, import maps, HTML shell
|
|
25
|
-
- JSON Schema registry with seed data
|
|
26
|
-
- Generic CRUD router — one router handles all entity types
|
|
27
|
-
- 15 server tests passing before writing any frontend code
|
|
28
|
-
|
|
29
|
-
### Phase 3: Store Models & Utils
|
|
30
|
-
|
|
31
|
-
- Singleton models (AppState, UserPrefs) with localStorage connectors
|
|
32
|
-
- Enumerable models (ProjectModel, TaskModel) with API connectors
|
|
33
|
-
- Utility functions (formatDate, timeAgo, statusColors)
|
|
34
|
-
- 35 tests passing
|
|
35
|
-
|
|
36
|
-
### Phase 4: Components (Atoms → Molecules → Organisms)
|
|
37
|
-
|
|
38
|
-
- Built bottom-up: app-button, app-badge, app-icon → task-card, project-card → task-list, project-header
|
|
39
|
-
- Browser tests via @web/test-runner + Playwright
|
|
40
|
-
- 63 tests passing
|
|
41
|
-
|
|
42
|
-
### Phase 5: Pages, Router & Integration
|
|
43
|
-
|
|
44
|
-
- page-layout template, home-view, project-view, app-router
|
|
45
|
-
- SSE realtime sync wired at the router level
|
|
46
|
-
- SPA fallback for direct URL navigation
|
|
47
|
-
|
|
48
|
-
### Phase 6: Schema-Driven Forms
|
|
49
|
-
|
|
50
|
-
- OPTIONS endpoint returns JSON Schema + form layout
|
|
51
|
-
- schema-form organism auto-generates fields from schema
|
|
52
|
-
- Native HTML validation from schema constraints
|
|
53
|
-
- Server-side validation returns 422 with per-field errors
|
|
54
|
-
- Form layout system with column grouping and action alignment
|
|
55
|
-
|
|
56
|
-
### Phase 7: Polish & Tooling
|
|
57
|
-
|
|
58
|
-
- Dark mode via CSS custom property overrides
|
|
59
|
-
- Spec checker CLI with interactive menu
|
|
60
|
-
- ESLint + Prettier + tsc --checkJs for JSDoc type validation
|
|
61
|
-
- GitHub Actions CI pipeline
|
|
62
|
-
- File reorganization (docs/, .configs/, tests/)
|
|
63
|
-
|
|
64
|
-
### Phase 8: Drag Reorder & Whiteboard
|
|
65
|
-
|
|
66
|
-
- Drag-to-reorder with visual gap indicator and backend persistence
|
|
67
|
-
- WebSocket server for real-time canvas collaboration
|
|
68
|
-
- SVG whiteboard with drawing tools (in progress)
|
|
69
|
-
|
|
70
|
-
---
|
|
71
|
-
|
|
72
|
-
## Discoveries & Corrections
|
|
73
|
-
|
|
74
|
-
The spec was written first, then corrected as implementation revealed gaps.
|
|
75
|
-
These are the significant corrections:
|
|
76
|
-
|
|
77
|
-
### Light DOM breaks slots
|
|
78
|
-
|
|
79
|
-
- **Expected:** `<slot>` for content composition
|
|
80
|
-
- **Actual:** Hybrids throws on `<slot>` in light DOM
|
|
81
|
-
- **Fix:** Template functions instead of template components
|
|
82
|
-
- **Documented in:** COMPONENT_PATTERNS.md → Content Composition
|
|
83
|
-
|
|
84
|
-
### Template components break host context
|
|
85
|
-
|
|
86
|
-
- **Expected:** Event handlers in content templates resolve to the page
|
|
87
|
-
- **Actual:** `host` resolves to the nearest hybrids component (the template)
|
|
88
|
-
- **Fix:** Use plain functions that return `html`, not `define()`'d components
|
|
89
|
-
- **Documented in:** COMPONENT_PATTERNS.md → Event Handler Host Context
|
|
90
|
-
|
|
91
|
-
### Custom events don't bubble by default
|
|
92
|
-
|
|
93
|
-
- **Expected:** `dispatch(host, 'press')` reaches parent listeners
|
|
94
|
-
- **Actual:** Without `bubbles: true`, events stop at the dispatching element
|
|
95
|
-
- **Fix:** Always `dispatch(host, 'event', { bubbles: true })`
|
|
96
|
-
- **Documented in:** COMPONENT_PATTERNS.md → Custom Events Must Bubble
|
|
97
|
-
|
|
98
|
-
### store.clear(Model) vs store.clear([Model])
|
|
99
|
-
|
|
100
|
-
- **Expected:** `store.clear(TaskModel)` refreshes task lists
|
|
101
|
-
- **Actual:** Only clears singular cache, not list cache
|
|
102
|
-
- **Fix:** Use `store.clear([TaskModel])` for list stores
|
|
103
|
-
- **Documented in:** STATE_AND_ROUTING.md → Store API Quick Reference
|
|
104
|
-
|
|
105
|
-
### store.ready(list) doesn't guarantee item readiness
|
|
106
|
-
|
|
107
|
-
- **Expected:** If the list is ready, items are ready
|
|
108
|
-
- **Actual:** Individual items can be pending after cache clear
|
|
109
|
-
- **Fix:** Guard each item with `store.ready(task)` in map()
|
|
110
|
-
- **Documented in:** STATE_AND_ROUTING.md → Guarding List Item Access
|
|
111
|
-
|
|
112
|
-
### localStorage connector must return {}, not undefined
|
|
113
|
-
|
|
114
|
-
- **Expected:** Returning undefined uses model defaults
|
|
115
|
-
- **Actual:** Hybrids treats undefined as a failed get → error state
|
|
116
|
-
- **Fix:** Return `{}` so hybrids merges with defaults
|
|
117
|
-
- **Documented in:** STATE_AND_ROUTING.md → localStorage Connector
|
|
118
|
-
|
|
119
|
-
### Batch operations cause SSE storms
|
|
120
|
-
|
|
121
|
-
- **Expected:** Reordering N tasks sends N updates, UI handles it
|
|
122
|
-
- **Actual:** N SSE events → N store.clear() calls → cascading errors
|
|
123
|
-
- **Fix:** Debounce SSE handler per entity type (300ms)
|
|
124
|
-
- **Documented in:** STATE_AND_ROUTING.md → Debouncing Batch Operations
|
|
125
|
-
|
|
126
|
-
### Server sort used string comparison for numbers
|
|
127
|
-
|
|
128
|
-
- **Expected:** sortOrder field sorts numerically
|
|
129
|
-
- **Actual:** `localeCompare` on numbers gives wrong order
|
|
130
|
-
- **Fix:** Detect numeric values and use `a - b` comparison
|
|
131
|
-
|
|
132
|
-
### SVG transforms: two-group rotation approach
|
|
133
|
-
|
|
134
|
-
- **Expected:** Embed rotation in shapeTransform string
|
|
135
|
-
- **Actual:** Rotation center in local coords doesn't match screen position
|
|
136
|
-
- **Fix:** Outer `<g>` for rotation (screen-space center), inner `<g>` for translate+scale
|
|
137
|
-
- **Documented in:** COMPONENT_PATTERNS.md → Coordinate Transforms
|
|
138
|
-
|
|
139
|
-
### Move after rotation: unrotate is wrong for translate
|
|
140
|
-
|
|
141
|
-
- **Expected:** Unrotate screen delta for the inner translate
|
|
142
|
-
- **Actual:** Causes magnified/skewed movement
|
|
143
|
-
- **Fix:** Move both rotation center AND inner translate by raw screen delta
|
|
144
|
-
- **Key insight:** `rotate(deg, cx, cy)` = `translate(cx,cy) rotate(deg) translate(-cx,-cy)`. Shifting cx,cy and translate by the same delta cancels out.
|
|
145
|
-
|
|
146
|
-
### Resize after rotation: unrotate IS needed
|
|
147
|
-
|
|
148
|
-
- **Expected:** Same approach as move
|
|
149
|
-
- **Actual:** Handles are visually rotated, so screen drag doesn't align with object axes
|
|
150
|
-
- **Fix:** Unrotate resize deltas only, not move deltas
|
|
151
|
-
|
|
152
|
-
### SVG innerHTML destroys event listeners
|
|
153
|
-
|
|
154
|
-
- **Expected:** Event listeners persist across renders
|
|
155
|
-
- **Actual:** `innerHTML` replaces the DOM, losing all listeners
|
|
156
|
-
- **Fix:** Re-bind mouse listeners in `observe`, attach keyboard to host element
|
|
157
|
-
- **Documented in:** COMPONENT_PATTERNS.md → SVG Content via innerHTML
|
|
158
|
-
|
|
159
|
-
### Path d-string manipulation breaks arc commands
|
|
160
|
-
|
|
161
|
-
- **Expected:** Regex replace on coordinate pairs works for all paths
|
|
162
|
-
- **Actual:** Arc commands have flags (0/1) that get mangled
|
|
163
|
-
- **Fix:** Use `shapeTransform` for complex shapes, only rewrite d for M/L paths
|
|
164
|
-
|
|
165
|
-
### Canvas pan offset not applied to drawing coordinates
|
|
166
|
-
|
|
167
|
-
- **Expected:** Drawing at the visual position works after panning
|
|
168
|
-
- **Actual:** Coordinates calculated from SVG rect, not accounting for pan
|
|
169
|
-
- **Fix:** Shared `canvasPos()` utility subtracts pan offset from all tools
|
|
170
|
-
|
|
171
|
-
### Enumerable models (`id: true`) break tsc
|
|
172
|
-
|
|
173
|
-
- **Expected:** `@type {import('hybrids').Model<Product>}` works with `id: true`
|
|
174
|
-
- **Actual:** tsc rejects `id: true` — it's not in the Model type definition
|
|
175
|
-
- **Fix:** Type the model as `@type {any}` with a comment explaining the cast
|
|
176
|
-
- **Documented in:** JSDOC_TYPING.md → Enumerable Models Need `@type {any}`
|
|
177
|
-
|
|
178
|
-
### Empty arrays in store models throw at runtime
|
|
179
|
-
|
|
180
|
-
- **Expected:** `items: []` on a singleton model works as an empty default
|
|
181
|
-
- **Actual:** Hybrids throws `The first item of the 'items' array must be defined`
|
|
182
|
-
- **Why:** Hybrids infers array item type from the first element at model setup
|
|
183
|
-
- **Fix:** Provide a prototype item: `items: [{ sku: '', quantity: 0 }]`
|
|
184
|
-
- **Documented in:** STATE_AND_ROUTING.md → Store Array Properties
|
|
185
|
-
|
|
186
|
-
### List store descriptors don't match array types in tsc
|
|
187
|
-
|
|
188
|
-
- **Expected:** `store([Model])` assignable to an array-typed host property
|
|
189
|
-
- **Actual:** tsc sees `EnumerableInstance`, not `any[]` — three separate casts needed
|
|
190
|
-
- **Fix:** Cast the descriptor, `store.ready()` call, and the array before `.map()`
|
|
191
|
-
- **Documented in:** JSDOC_TYPING.md → List Store Properties Need Casts
|
|
192
|
-
|
|
193
|
-
### Organisms importing pages creates circular dependencies
|
|
194
|
-
|
|
195
|
-
- **Expected:** `router.url(PageView)` works from an organism for link generation
|
|
196
|
-
- **Actual:** Page imports organism, organism imports page → circular
|
|
197
|
-
- **Fix:** Use string URLs (`/product/${sku}`) in organisms instead of `router.url()`
|
|
198
|
-
- **Documented in:** FRONTEND_IMPLEMENTATION_RULES.md → Organisms Must Not Import Pages
|
|
199
|
-
|
|
200
|
-
### `list` connector params need cast for custom filter properties
|
|
201
|
-
|
|
202
|
-
- **Expected:** `list: async ({ category }) => ...` destructures cleanly
|
|
203
|
-
- **Actual:** Param is `ModelIdentifier`, tsc rejects `.category` access
|
|
204
|
-
- **Fix:** Cast params to `any` before accessing custom properties
|
|
205
|
-
- **Documented in:** JSDOC_TYPING.md → `list` Connector Params
|
|
206
|
-
|
|
207
|
-
---
|
|
208
|
-
|
|
209
|
-
## Metrics
|
|
210
|
-
|
|
211
|
-
| Metric | Value |
|
|
212
|
-
| ---------------------------- | ------------------------------------------------ |
|
|
213
|
-
| Total source files | 108 |
|
|
214
|
-
| Utility modules | 25 |
|
|
215
|
-
| Component files | 13 |
|
|
216
|
-
| Style sheets | 6 |
|
|
217
|
-
| API modules | 6 |
|
|
218
|
-
| Page views | 2 |
|
|
219
|
-
| Test files | 14 |
|
|
220
|
-
| Node tests | 65 |
|
|
221
|
-
| Browser tests | 41 |
|
|
222
|
-
| Spec documents | 10 |
|
|
223
|
-
| Max lines per file | 150 (enforced) |
|
|
224
|
-
| Max lines per doc | 500 (enforced) |
|
|
225
|
-
| Automated checks | 7 (line counts, lint, format, types, tests) |
|
|
226
|
-
| Build phases | 8 + whiteboard |
|
|
227
|
-
| Bugs found & fixed | ~25 significant |
|
|
228
|
-
| Bugs requiring >4 iterations | 3 (drag reorder, event bubbling, SVG transforms) |
|
|
229
|
-
| External dependencies | 4 runtime (hybrids, express, ws, lucide-static) |
|
|
230
|
-
| Build tools | 0 |
|
|
231
|
-
|
|
232
|
-
---
|
|
233
|
-
|
|
234
|
-
## What This Proves
|
|
235
|
-
|
|
236
|
-
1. **Small files work for LLMs.** Every file fits in a single read. No scrolling,
|
|
237
|
-
no "show me lines 200-300", no losing context.
|
|
238
|
-
|
|
239
|
-
2. **The spec catches drift.** When implementation diverged from the spec
|
|
240
|
-
(e.g. using slots in light DOM), the spec checker or manual review caught
|
|
241
|
-
it, and both the code and spec were corrected.
|
|
242
|
-
|
|
243
|
-
3. **Test-at-the-boundary works.** Each phase was tested before the next began.
|
|
244
|
-
The two hardest bugs (drag reorder, event bubbling) were in the last phases
|
|
245
|
-
where components composed deeply — exactly where integration tests matter.
|
|
246
|
-
|
|
247
|
-
4. **No-build is viable.** The app loads in 0.17s LCP, handles real-time sync
|
|
248
|
-
across browsers, and every file is debuggable as-written in devtools.
|
|
249
|
-
|
|
250
|
-
5. **The spec improves through implementation.** 9 significant corrections were
|
|
251
|
-
made to the spec based on what we learned building the proof. The spec is
|
|
252
|
-
now more accurate than if it had been written in isolation.
|