@techninja/clearstack 0.3.12 → 0.3.14

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/README.md CHANGED
@@ -29,17 +29,17 @@ A project/task tracker that exercises every pattern in the spec: API-backed enti
29
29
 
30
30
  ## Specification
31
31
 
32
- | Document | What It Covers |
33
- |---|---|
32
+ | Document | What It Covers |
33
+ | --------------------------------------------------------------------------- | -------------------------------------------------------------- |
34
34
  | [FRONTEND_IMPLEMENTATION_RULES.md](./docs/FRONTEND_IMPLEMENTATION_RULES.md) | Philosophy, framework choice, project structure, atomic design |
35
- | [COMPONENT_PATTERNS.md](./docs/COMPONENT_PATTERNS.md) | Authoring, light DOM, styling, layout engine, JSDoc typing |
36
- | [STATE_AND_ROUTING.md](./docs/STATE_AND_ROUTING.md) | Store, routing, unified app state, realtime SSE sync |
37
- | [CONVENTIONS.md](./docs/CONVENTIONS.md) | Naming rules, anti-patterns |
38
- | [SERVER_AND_DEPS.md](./docs/SERVER_AND_DEPS.md) | Express server, import maps, vendor dependency loading |
39
- | [BACKEND_API_SPEC.md](./docs/BACKEND_API_SPEC.md) | REST CRUD, JSON Schema via HEAD, entity management |
40
- | [TESTING.md](./docs/TESTING.md) | Testing philosophy, tools, patterns, phase checkpoints |
41
- | [BUILD_LOG.md](./docs/BUILD_LOG.md) | How this project was built — LLM-human collaboration proof |
42
- | [QUICKSTART.md](./docs/QUICKSTART.md) | Scaffolder setup, development workflow, updating, compliance |
35
+ | [COMPONENT_PATTERNS.md](./docs/COMPONENT_PATTERNS.md) | Authoring, light DOM, styling, layout engine, JSDoc typing |
36
+ | [STATE_AND_ROUTING.md](./docs/STATE_AND_ROUTING.md) | Store, routing, unified app state, realtime SSE sync |
37
+ | [CONVENTIONS.md](./docs/CONVENTIONS.md) | Naming rules, anti-patterns |
38
+ | [SERVER_AND_DEPS.md](./docs/SERVER_AND_DEPS.md) | Express server, import maps, vendor dependency loading |
39
+ | [BACKEND_API_SPEC.md](./docs/BACKEND_API_SPEC.md) | REST CRUD, JSON Schema via HEAD, entity management |
40
+ | [TESTING.md](./docs/TESTING.md) | Testing philosophy, tools, patterns, phase checkpoints |
41
+ | [BUILD_LOG.md](./docs/BUILD_LOG.md) | How this project was built — LLM-human collaboration proof |
42
+ | [QUICKSTART.md](./docs/QUICKSTART.md) | Scaffolder setup, development workflow, updating, compliance |
43
43
 
44
44
  ## Using Clearstack
45
45
 
@@ -48,7 +48,8 @@ Install as a dev dependency, scaffold, and keep in sync:
48
48
  ```bash
49
49
  npm install -D @techninja/clearstack # add to your project
50
50
  npx clearstack init # scaffold (interactive)
51
- npx clearstack init -y # scaffold (defaults)
51
+ npx clearstack init -y # scaffold (fullstack defaults)
52
+ npx clearstack init --static # scaffold (static, no server)
52
53
  npm run spec # check compliance
53
54
  npm run spec:update # sync docs + configs on upgrade
54
55
  ```
package/bin/cli.js CHANGED
@@ -3,7 +3,7 @@
3
3
  /**
4
4
  * clearstack CLI — scaffold, update, and check spec-compliant projects.
5
5
  * Usage:
6
- * clearstack init [-y] [--mode fullstack|static] [--port 3000]
6
+ * clearstack init [-y] [--static|--fullstack] [--port 3000]
7
7
  * clearstack update
8
8
  * clearstack check [code|docs|imports|lint|lint es|format|types|audit|all]
9
9
  * clearstack → interactive menu
@@ -22,6 +22,8 @@ const flags = Object.fromEntries(
22
22
  }),
23
23
  );
24
24
  const yes = args.includes('-y') || args.includes('--yes');
25
+ if (flags.static) flags.mode = 'static';
26
+ if (flags.fullstack) flags.mode = 'fullstack';
25
27
 
26
28
  /** Show interactive menu. */
27
29
  async function interactive() {
package/docs/BUILD_LOG.md CHANGED
@@ -168,6 +168,42 @@ These are the significant corrections:
168
168
  - **Actual:** Coordinates calculated from SVG rect, not accounting for pan
169
169
  - **Fix:** Shared `canvasPos()` utility subtracts pan offset from all tools
170
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
+
171
207
  ---
172
208
 
173
209
  ## Metrics
@@ -212,6 +212,29 @@ Pages → Templates → Organisms → Molecules → Atoms
212
212
  Store / Utils
213
213
  ```
214
214
 
215
+ #### Organisms Must Not Import Pages
216
+
217
+ Organisms sit below pages in the import hierarchy. If an organism needs
218
+ to generate a URL to a page (e.g. a product grid linking to a product
219
+ detail page), **use a string URL, not `router.url(PageView)`**.
220
+
221
+ `router.url()` requires importing the page component, which creates a
222
+ circular dependency: page → organism → page. Even if the bundler or
223
+ browser resolves it, it makes the dependency graph untraceable.
224
+
225
+ ```javascript
226
+ // ❌ BAD — organism imports a page, circular dependency
227
+ import ProductDetailView from '#pages/product-detail/product-detail-view.js';
228
+ html`<a href="${router.url(ProductDetailView, { sku })}">View</a>`;
229
+
230
+ // ✅ GOOD — string URL, no import needed
231
+ html`<a href="${`/product/${sku}`}">View</a>`;
232
+ ```
233
+
234
+ If the URL pattern changes, update it in one place. For complex URL
235
+ generation, extract a `buildUrl(view, params)` utility in `src/utils/`
236
+ that returns strings — no component imports.
237
+
215
238
  - Atoms import **nothing** from other component tiers.
216
239
  - Molecules import only from **atoms**.
217
240
  - Organisms import from **molecules** and **atoms**.
@@ -76,6 +76,73 @@ function handleClick(host, event) {
76
76
  }
77
77
  ```
78
78
 
79
+ ## Enumerable Models Need `@type {any}`
80
+
81
+ Hybrids' TypeScript definitions don't support `id: true` on `Model<T>` —
82
+ `tsc` rejects it because `id` isn't in the typedef. The fix is to type
83
+ the entire model as `any` with a comment explaining why:
84
+
85
+ ```javascript
86
+ /** @type {any} — hybrids Model with id:true; cast to bypass tsc limitations */
87
+ const Product = {
88
+ id: true,
89
+ name: '',
90
+ // ...
91
+ [store.connect]: { get: ..., list: ... },
92
+ };
93
+ ```
94
+
95
+ Do **not** add `id` to the `@typedef` — it's a hybrids directive, not a
96
+ data field. The `@typedef` should describe the shape of the data.
97
+
98
+ ## List Store Properties Need Casts
99
+
100
+ `store([Model])` returns a descriptor that `tsc` can't reconcile with
101
+ array-typed host properties. Both the descriptor and `store.ready()` calls
102
+ need `any` casts:
103
+
104
+ ```javascript
105
+ /**
106
+ * @typedef {Object} MyGridHost
107
+ * @property {any} items — list store, cast for tsc
108
+ */
109
+
110
+ /** @type {import('hybrids').Component<MyGridHost>} */
111
+ export default define({
112
+ tag: 'my-grid',
113
+ items: /** @type {any} */ (store([Product], { id: () => ({}) })),
114
+ render: {
115
+ value: ({ items }) => html`
116
+ ${
117
+ /** @type {any} */ (store).ready(items)
118
+ ? /** @type {any[]} */ (items).map((i) => html`<span>${i.name}</span>`)
119
+ : html`<p>Loading…</p>`
120
+ }
121
+ `,
122
+ shadow: false,
123
+ },
124
+ });
125
+ ```
126
+
127
+ Three casts are needed:
128
+
129
+ 1. `/** @type {any} */` on the `store([Model])` descriptor assignment
130
+ 2. `/** @type {any} */ (store).ready(items)` — `store.ready()` overloads
131
+ reject array-typed arguments
132
+ 3. `/** @type {any[]} */ (items)` before calling `.map()` / `.filter()`
133
+
134
+ ## `list` Connector Params Are `ModelIdentifier`
135
+
136
+ The `list` connector receives a `ModelIdentifier`, not a plain object.
137
+ Accessing custom filter properties (like `.category`) requires a cast:
138
+
139
+ ```javascript
140
+ list: async (params) => {
141
+ const category = /** @type {any} */ (params)?.category;
142
+ // ...
143
+ },
144
+ ```
145
+
79
146
  ## Rules
80
147
 
81
148
  - Every exported component gets a `@typedef` for its host interface.
@@ -22,8 +22,10 @@ pnpm add -D @techninja/clearstack
22
22
  From your project root:
23
23
 
24
24
  ```bash
25
- npx clearstack init # interactive
26
- npx clearstack init -y # non-interactive (defaults)
25
+ npx clearstack init # interactive
26
+ npx clearstack init -y # non-interactive (fullstack defaults)
27
+ npx clearstack init --static # non-interactive, static mode
28
+ npx clearstack init -y --static # same as above
27
29
  ```
28
30
 
29
31
  The interactive prompt asks for:
@@ -159,7 +161,8 @@ spec:code → spec:docs → lint → format → typecheck → test
159
161
  | Task | Command |
160
162
  | --------------------- | -------------------------------------- |
161
163
  | Install Clearstack | `npm install -D @techninja/clearstack` |
162
- | Scaffold a project | `npx clearstack init` |
164
+ | Scaffold (fullstack) | `npx clearstack init` |
165
+ | Scaffold (static) | `npx clearstack init --static` |
163
166
  | Install dependencies | `npm install` |
164
167
  | Start dev server | `npm run dev` |
165
168
  | Lint + format | `npm run lint:fix && npm run format` |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@techninja/clearstack",
3
- "version": "0.3.12",
3
+ "version": "0.3.14",
4
4
  "type": "module",
5
5
  "description": "A no-build web component framework specification — scaffold, validate, and evolve spec-compliant projects",
6
6
  "bin": {
@@ -168,6 +168,42 @@ These are the significant corrections:
168
168
  - **Actual:** Coordinates calculated from SVG rect, not accounting for pan
169
169
  - **Fix:** Shared `canvasPos()` utility subtracts pan offset from all tools
170
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
+
171
207
  ---
172
208
 
173
209
  ## Metrics
@@ -212,6 +212,29 @@ Pages → Templates → Organisms → Molecules → Atoms
212
212
  Store / Utils
213
213
  ```
214
214
 
215
+ #### Organisms Must Not Import Pages
216
+
217
+ Organisms sit below pages in the import hierarchy. If an organism needs
218
+ to generate a URL to a page (e.g. a product grid linking to a product
219
+ detail page), **use a string URL, not `router.url(PageView)`**.
220
+
221
+ `router.url()` requires importing the page component, which creates a
222
+ circular dependency: page → organism → page. Even if the bundler or
223
+ browser resolves it, it makes the dependency graph untraceable.
224
+
225
+ ```javascript
226
+ // ❌ BAD — organism imports a page, circular dependency
227
+ import ProductDetailView from '#pages/product-detail/product-detail-view.js';
228
+ html`<a href="${router.url(ProductDetailView, { sku })}">View</a>`;
229
+
230
+ // ✅ GOOD — string URL, no import needed
231
+ html`<a href="${`/product/${sku}`}">View</a>`;
232
+ ```
233
+
234
+ If the URL pattern changes, update it in one place. For complex URL
235
+ generation, extract a `buildUrl(view, params)` utility in `src/utils/`
236
+ that returns strings — no component imports.
237
+
215
238
  - Atoms import **nothing** from other component tiers.
216
239
  - Molecules import only from **atoms**.
217
240
  - Organisms import from **molecules** and **atoms**.
@@ -76,6 +76,73 @@ function handleClick(host, event) {
76
76
  }
77
77
  ```
78
78
 
79
+ ## Enumerable Models Need `@type {any}`
80
+
81
+ Hybrids' TypeScript definitions don't support `id: true` on `Model<T>` —
82
+ `tsc` rejects it because `id` isn't in the typedef. The fix is to type
83
+ the entire model as `any` with a comment explaining why:
84
+
85
+ ```javascript
86
+ /** @type {any} — hybrids Model with id:true; cast to bypass tsc limitations */
87
+ const Product = {
88
+ id: true,
89
+ name: '',
90
+ // ...
91
+ [store.connect]: { get: ..., list: ... },
92
+ };
93
+ ```
94
+
95
+ Do **not** add `id` to the `@typedef` — it's a hybrids directive, not a
96
+ data field. The `@typedef` should describe the shape of the data.
97
+
98
+ ## List Store Properties Need Casts
99
+
100
+ `store([Model])` returns a descriptor that `tsc` can't reconcile with
101
+ array-typed host properties. Both the descriptor and `store.ready()` calls
102
+ need `any` casts:
103
+
104
+ ```javascript
105
+ /**
106
+ * @typedef {Object} MyGridHost
107
+ * @property {any} items — list store, cast for tsc
108
+ */
109
+
110
+ /** @type {import('hybrids').Component<MyGridHost>} */
111
+ export default define({
112
+ tag: 'my-grid',
113
+ items: /** @type {any} */ (store([Product], { id: () => ({}) })),
114
+ render: {
115
+ value: ({ items }) => html`
116
+ ${
117
+ /** @type {any} */ (store).ready(items)
118
+ ? /** @type {any[]} */ (items).map((i) => html`<span>${i.name}</span>`)
119
+ : html`<p>Loading…</p>`
120
+ }
121
+ `,
122
+ shadow: false,
123
+ },
124
+ });
125
+ ```
126
+
127
+ Three casts are needed:
128
+
129
+ 1. `/** @type {any} */` on the `store([Model])` descriptor assignment
130
+ 2. `/** @type {any} */ (store).ready(items)` — `store.ready()` overloads
131
+ reject array-typed arguments
132
+ 3. `/** @type {any[]} */ (items)` before calling `.map()` / `.filter()`
133
+
134
+ ## `list` Connector Params Are `ModelIdentifier`
135
+
136
+ The `list` connector receives a `ModelIdentifier`, not a plain object.
137
+ Accessing custom filter properties (like `.category`) requires a cast:
138
+
139
+ ```javascript
140
+ list: async (params) => {
141
+ const category = /** @type {any} */ (params)?.category;
142
+ // ...
143
+ },
144
+ ```
145
+
79
146
  ## Rules
80
147
 
81
148
  - Every exported component gets a `@typedef` for its host interface.
@@ -22,8 +22,10 @@ pnpm add -D @techninja/clearstack
22
22
  From your project root:
23
23
 
24
24
  ```bash
25
- npx clearstack init # interactive
26
- npx clearstack init -y # non-interactive (defaults)
25
+ npx clearstack init # interactive
26
+ npx clearstack init -y # non-interactive (fullstack defaults)
27
+ npx clearstack init --static # non-interactive, static mode
28
+ npx clearstack init -y --static # same as above
27
29
  ```
28
30
 
29
31
  The interactive prompt asks for:
@@ -159,7 +161,8 @@ spec:code → spec:docs → lint → format → typecheck → test
159
161
  | Task | Command |
160
162
  | --------------------- | -------------------------------------- |
161
163
  | Install Clearstack | `npm install -D @techninja/clearstack` |
162
- | Scaffold a project | `npx clearstack init` |
164
+ | Scaffold (fullstack) | `npx clearstack init` |
165
+ | Scaffold (static) | `npx clearstack init --static` |
163
166
  | Install dependencies | `npm install` |
164
167
  | Start dev server | `npm run dev` |
165
168
  | Lint + format | `npm run lint:fix && npm run format` |