@techninja/clearstack 0.3.17 → 0.3.20
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 +11 -1
- package/docs/CONVENTIONS.md +43 -4
- package/docs/PLATFORM_STACKING.md +486 -0
- package/docs/STATE_AND_ROUTING.md +54 -49
- package/lib/check.js +13 -53
- package/lib/init.js +5 -0
- package/lib/package-gen.js +1 -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/.github/workflows/release.yml +53 -0
- package/templates/shared/docs/clearstack/CONVENTIONS.md +43 -4
- package/templates/shared/docs/clearstack/STATE_AND_ROUTING.md +54 -49
- package/templates/shared/scripts/release.js +81 -0
- package/templates/shared/docs/clearstack/BUILD_LOG.md +0 -349
|
@@ -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
|
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Release helper — bumps version, updates changelog, commits, and tags.
|
|
5
|
+
* Platform-aware: runs sync-vendor.js if it exists.
|
|
6
|
+
* Usage: node scripts/release.js [patch|minor|major]
|
|
7
|
+
* @module scripts/release
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { execSync } from 'node:child_process';
|
|
11
|
+
import { readFileSync, writeFileSync, existsSync } from 'node:fs';
|
|
12
|
+
import { resolve, dirname } from 'node:path';
|
|
13
|
+
import { fileURLToPath } from 'node:url';
|
|
14
|
+
|
|
15
|
+
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
|
16
|
+
const bump = process.argv[2] || 'patch';
|
|
17
|
+
|
|
18
|
+
if (!['patch', 'minor', 'major'].includes(bump)) {
|
|
19
|
+
console.error('Usage: node scripts/release.js [patch|minor|major]');
|
|
20
|
+
process.exit(1);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const run = (cmd) => execSync(cmd, { cwd: ROOT, stdio: 'inherit' });
|
|
24
|
+
const out = (cmd) => execSync(cmd, { cwd: ROOT, encoding: 'utf-8' }).trim();
|
|
25
|
+
|
|
26
|
+
// Gate: clean working tree (allow staged changes)
|
|
27
|
+
const dirty = out('git diff --name-only HEAD').split('\n').filter(Boolean);
|
|
28
|
+
if (dirty.length > 0) {
|
|
29
|
+
console.error('\n❌ Uncommitted changes — commit or stash first:\n');
|
|
30
|
+
dirty.forEach((f) => console.error(` ${f}`));
|
|
31
|
+
process.exit(1);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Bump version
|
|
35
|
+
const pkgPath = resolve(ROOT, 'package.json');
|
|
36
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
|
|
37
|
+
const [ma, mi, pa] = pkg.version.split('.').map(Number);
|
|
38
|
+
const next =
|
|
39
|
+
bump === 'major' ? `${ma + 1}.0.0` : bump === 'minor' ? `${ma}.${mi + 1}.0` : `${ma}.${mi}.${pa + 1}`;
|
|
40
|
+
pkg.version = next;
|
|
41
|
+
writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n');
|
|
42
|
+
console.log(`\n📦 ${pkg.name} → v${next}\n`);
|
|
43
|
+
|
|
44
|
+
// Platform: sync vendor if applicable
|
|
45
|
+
const syncScript = resolve(ROOT, 'scripts/sync-vendor.js');
|
|
46
|
+
if (existsSync(syncScript)) {
|
|
47
|
+
console.log('🔄 Syncing vendor...');
|
|
48
|
+
run('node scripts/sync-vendor.js');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Spec check
|
|
52
|
+
console.log('\n🔍 Running spec checks...\n');
|
|
53
|
+
run('node scripts/spec.js check all');
|
|
54
|
+
|
|
55
|
+
// Changelog
|
|
56
|
+
const changelogPath = resolve(ROOT, 'CHANGELOG.md');
|
|
57
|
+
const lastTag = out('git tag --sort=-v:refname').split('\n')[0] || '';
|
|
58
|
+
const range = lastTag ? `${lastTag}..HEAD` : 'HEAD';
|
|
59
|
+
const log = out(`git log ${range} --pretty=format:"- %s"`);
|
|
60
|
+
const date = new Date().toISOString().split('T')[0];
|
|
61
|
+
const entry = `## [${next}] - ${date}\n\n${log || '- Initial release'}\n`;
|
|
62
|
+
|
|
63
|
+
if (existsSync(changelogPath)) {
|
|
64
|
+
const cl = readFileSync(changelogPath, 'utf-8');
|
|
65
|
+
const marker = '## [Unreleased]';
|
|
66
|
+
const updated = cl.includes(marker)
|
|
67
|
+
? cl.replace(marker, `${marker}\n\n${entry}`)
|
|
68
|
+
: `${entry}\n\n${cl}`;
|
|
69
|
+
writeFileSync(changelogPath, updated);
|
|
70
|
+
} else {
|
|
71
|
+
const header = '# Changelog\n\n## [Unreleased]\n\n';
|
|
72
|
+
writeFileSync(changelogPath, header + entry);
|
|
73
|
+
}
|
|
74
|
+
console.log('📝 Updated CHANGELOG.md');
|
|
75
|
+
|
|
76
|
+
// Commit + tag
|
|
77
|
+
run('git add -A');
|
|
78
|
+
run(`git commit -m "release: v${next}"`);
|
|
79
|
+
run(`git tag v${next}`);
|
|
80
|
+
console.log(`\n🏷️ Tagged v${next}`);
|
|
81
|
+
console.log(`\n git push && git push --tags\n`);
|
|
@@ -1,349 +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.
|
|
253
|
-
|
|
254
|
-
---
|
|
255
|
-
|
|
256
|
-
## StatiCart: Second Project Validation
|
|
257
|
-
|
|
258
|
-
StatiCart is a full e-commerce platform built on Clearstack — the second
|
|
259
|
-
project built against the spec, validating that the patterns hold beyond
|
|
260
|
-
the original POC. Three phases (catalog, cart, polish) were completed in
|
|
261
|
-
a single LLM session.
|
|
262
|
-
|
|
263
|
-
### Velocity
|
|
264
|
-
|
|
265
|
-
| Metric | Value |
|
|
266
|
-
| --- | --- |
|
|
267
|
-
| Phases completed | 3 of 8 |
|
|
268
|
-
| Components created | 7 (2 atoms, 2 molecules, 2 organisms, 0 templates) |
|
|
269
|
-
| Pages created | 5 (catalog, product-detail, cart, order-success, order-cancelled) |
|
|
270
|
-
| Store models | 2 (Product enumerable, CartState singleton) |
|
|
271
|
-
| Utilities | 4 (formatPrice, setPageMeta, productVariants, statusColors) |
|
|
272
|
-
| Tests | 41 (10 node, 31 browser) |
|
|
273
|
-
| Spec checks | 9/9 passing |
|
|
274
|
-
| Files over 150 lines | 0 (2 hit the limit, both split successfully) |
|
|
275
|
-
|
|
276
|
-
Three phases of a real app in one session. No blocked-on-framework moments.
|
|
277
|
-
Every blocker was a hybrids runtime behavior, not a Clearstack constraint.
|
|
278
|
-
|
|
279
|
-
### What the 150-Line Limit Actually Did
|
|
280
|
-
|
|
281
|
-
The limit forced two splits during this build:
|
|
282
|
-
|
|
283
|
-
1. `shared.css` hit 203 lines after adding page layout styles → split into
|
|
284
|
-
`pages.css`. Later `pages.css` hit 163 → split into `pages-cart.css`.
|
|
285
|
-
2. `product-detail-view.js` hit 154 after adding the image gallery →
|
|
286
|
-
extracted `productVariants.js` utility.
|
|
287
|
-
|
|
288
|
-
Both splits improved the code. The page styles split by concern (catalog
|
|
289
|
-
vs cart) which made each file scannable. The utility extraction made
|
|
290
|
-
`effectivePrice`/`effectiveStock` reusable and independently testable.
|
|
291
|
-
|
|
292
|
-
Without the limit, these would have stayed as 300-line monoliths. The
|
|
293
|
-
LLM would have needed to re-read them on every edit, and the utility
|
|
294
|
-
functions would never have gotten their own test file.
|
|
295
|
-
|
|
296
|
-
### What the Spec Documentation Prevented
|
|
297
|
-
|
|
298
|
-
The Clearstack spec docs prevented several classes of bugs entirely:
|
|
299
|
-
|
|
300
|
-
- **No `../` imports.** The import map alias rule (`#store/`, `#atoms/`)
|
|
301
|
-
was enforced from the first file. Zero refactoring of import paths.
|
|
302
|
-
- **`shadow: false` everywhere.** No time wasted on shadow DOM styling
|
|
303
|
-
issues. Global CSS just worked.
|
|
304
|
-
- **Named event handlers.** No "button doesn't work" debugging from
|
|
305
|
-
inline arrows in nested templates.
|
|
306
|
-
- **`store.connect.get` returns `{}`.** The localStorage pattern was
|
|
307
|
-
correct on first write for CartState.
|
|
308
|
-
- **Template functions over template components.** No host context bugs.
|
|
309
|
-
|
|
310
|
-
### What the Spec Didn't Prevent (New Discoveries)
|
|
311
|
-
|
|
312
|
-
Five new hybrids gotchas were found and documented:
|
|
313
|
-
|
|
314
|
-
1. **Empty arrays throw** — `items: []` needs a prototype item
|
|
315
|
-
2. **`id` is reserved in nested models** — even inside array prototypes
|
|
316
|
-
3. **Enumerable models need `@type {any}`** — tsc can't type `id: true`
|
|
317
|
-
4. **List store descriptors need 3 casts** — descriptor, ready(), and map()
|
|
318
|
-
5. **`list` connector params need cast** — `ModelIdentifier` not plain object
|
|
319
|
-
|
|
320
|
-
All five are hybrids type system friction, not architectural issues. The
|
|
321
|
-
runtime behavior is correct — it's the JSDoc/tsc layer that struggles.
|
|
322
|
-
Each was fixed in under 2 minutes once identified.
|
|
323
|
-
|
|
324
|
-
### Test Runner Discovery
|
|
325
|
-
|
|
326
|
-
`@web/test-runner` doesn't support browser import maps. A custom
|
|
327
|
-
`resolveImport` plugin was needed to map `#prefix/` aliases to absolute
|
|
328
|
-
paths. This is now documented in TESTING.md with the full config pattern.
|
|
329
|
-
|
|
330
|
-
### Framework Reuse Validation
|
|
331
|
-
|
|
332
|
-
The Clearstack scaffolder (`npm run spec update`) worked correctly for
|
|
333
|
-
doc syncing. The config overwrite issue (jsconfig.json, eslint.config.js)
|
|
334
|
-
was found and fixed upstream — `update` now skips existing configs unless
|
|
335
|
-
`--force` is passed. This validates the "dependency, not template" model.
|
|
336
|
-
|
|
337
|
-
### Key Insight
|
|
338
|
-
|
|
339
|
-
The speed came from **not having to make architectural decisions**. The
|
|
340
|
-
spec answered every "where does this go" question before it was asked:
|
|
341
|
-
store models in `src/store/`, atoms vs molecules vs organisms, CSS in
|
|
342
|
-
per-component files registered in `components.css`, pages in `src/pages/`.
|
|
343
|
-
The LLM never had to ask "how should I structure this?" — it read the
|
|
344
|
-
spec and built.
|
|
345
|
-
|
|
346
|
-
This is the core value proposition: **Clearstack trades flexibility for
|
|
347
|
-
velocity.** A team (or LLM) that knows the spec can build without
|
|
348
|
-
discussion. The constraints aren't limitations — they're decisions that
|
|
349
|
-
have already been made.
|