@techninja/clearstack 0.3.15 → 0.3.17

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
@@ -50,8 +50,9 @@ npm install -D @techninja/clearstack # add to your project
50
50
  npx clearstack init # scaffold (interactive)
51
51
  npx clearstack init -y # scaffold (fullstack defaults)
52
52
  npx clearstack init --static # scaffold (static, no server)
53
+ npx clearstack update # sync docs (skip existing configs)
54
+ npx clearstack update --force # sync docs + overwrite configs
53
55
  npm run spec # check compliance
54
- npm run spec:update # sync docs + configs on upgrade
55
56
  ```
56
57
 
57
58
  Two modes: **fullstack** (Express + WebSocket + JSON DB + SSE) or **static** (localStorage, no server).
@@ -65,7 +66,7 @@ See [QUICKSTART.md](./docs/QUICKSTART.md) for the full walkthrough.
65
66
  - **Light DOM by default.** Shared styles just work.
66
67
  - **JSDoc over TypeScript.** Types without a compile step — validated by `tsc --checkJs`.
67
68
  - **Test at the boundary.** Each phase passes before the next begins.
68
- - **The spec checks itself.** `npm run spec:code` and `npm run spec:docs`.
69
+ - **The spec checks itself.** `npm run spec code` and `npm run spec docs`.
69
70
  - **Lint and format.** ESLint + Prettier, semicolons, 2-space indent.
70
71
 
71
72
  ## Scripts
@@ -78,10 +79,11 @@ npm run lint # ESLint check
78
79
  npm run lint:fix # ESLint auto-fix
79
80
  npm run format # Prettier auto-format
80
81
  npm run typecheck # JSDoc type validation via tsc
81
- npm run spec # Spec compliance check
82
- npm run spec:code # Check code files ≤150 lines
83
- npm run spec:docs # Check doc files ≤500 lines
84
- npm run spec:update # Sync docs + configs from upstream
82
+ npm run spec # Spec compliance (interactive)
83
+ npm run spec all # Full spec check
84
+ npm run spec code # Check code files ≤150 lines
85
+ npm run spec docs # Check doc files ≤500 lines
86
+ npm run spec update # Sync docs from upstream
85
87
  ```
86
88
 
87
89
  ## License
package/bin/cli.js CHANGED
@@ -4,7 +4,7 @@
4
4
  * clearstack CLI — scaffold, update, and check spec-compliant projects.
5
5
  * Usage:
6
6
  * clearstack init [-y] [--static|--fullstack] [--port 3000]
7
- * clearstack update
7
+ * clearstack update [--force]
8
8
  * clearstack check [code|docs|imports|lint|lint es|format|types|audit|all]
9
9
  * clearstack → interactive menu
10
10
  */
@@ -54,7 +54,7 @@ async function run(action) {
54
54
  await init(PKG_ROOT, { yes, ...flags });
55
55
  } else if (action === 'update') {
56
56
  const { update } = await import('../lib/update.js');
57
- await update(PKG_ROOT);
57
+ await update(PKG_ROOT, { force: !!flags.force });
58
58
  } else if (action === 'check') {
59
59
  const subs = args.filter((a) => a !== cmd && !a.startsWith('-'));
60
60
  const { check } = await import('../lib/check.js');
package/docs/BUILD_LOG.md CHANGED
@@ -250,3 +250,100 @@ These are the significant corrections:
250
250
  5. **The spec improves through implementation.** 9 significant corrections were
251
251
  made to the spec based on what we learned building the proof. The spec is
252
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.
@@ -298,7 +298,7 @@ multiple keys.
298
298
 
299
299
  - **One script per domain.** `test`, `spec`, `lint` — not `test:node`,
300
300
  `test:browser`, `lint:fix`, `spec:code`, `spec:docs`.
301
- - **Arguments over aliases.** `pnpm spec check code` instead of
301
+ - **Arguments over aliases.** `pnpm spec code` instead of
302
302
  `pnpm spec:code`. The CLI handles routing.
303
303
  - **Interactive by default.** Running `pnpm spec` with no arguments shows
304
304
  a menu of available actions. Users discover commands by using the tool.
@@ -370,7 +370,8 @@ npm run spec all
370
370
  npm run spec code # line counts (code files ≤150)
371
371
  npm run spec docs # line counts (doc files ≤500)
372
372
  npm run spec imports # import map aliases (no ../)
373
- npm run spec types # JSDoc types (tsc --checkJs)
373
+ npm run spec types # all jsconfigs (auto-discovered)
374
+ npm run spec types frontend # just .configs/jsconfig.json
374
375
  npm run spec audit # security audit
375
376
 
376
377
  # Parent keys run all children
@@ -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 the project root enables `checkJs: true`. Running
17
- `npm run typecheck` invokes `tsc --project jsconfig.json` which:
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 typecheck` before committing. Zero errors required.
175
+ - Run `npm run spec types` before committing. Zero errors required.
@@ -43,7 +43,7 @@ If a `package.json` already exists, Clearstack merges into it — your existing
43
43
 
44
44
  ```
45
45
  your-project/
46
- ├── .configs/ # Managedsynced on update
46
+ ├── .configs/ # ✏️ Scaffolded once yours to customize
47
47
  ├── .github/ # CI workflow, PR + issue templates
48
48
  ├── docs/
49
49
  │ ├── clearstack/ # ⟳ Managed — spec docs, synced on update
@@ -108,14 +108,16 @@ When Clearstack releases a new version:
108
108
 
109
109
  ```bash
110
110
  npm update @techninja/clearstack # bump the package
111
- npm run spec:update # sync docs + configs
111
+ npm run spec update # sync docs, skip existing configs
112
+ npm run spec update --force # sync docs + overwrite configs
112
113
  git diff docs/ .configs/ # review what changed
113
114
  ```
114
115
 
115
116
  This updates:
116
117
 
117
- - `docs/clearstack/*.md` — spec documentation
118
- - `.configs/*` — linter, formatter, type checker, test runner configs
118
+ - `docs/clearstack/*.md` — spec documentation (always overwritten)
119
+ - `.configs/*` — skipped if already exists (your customizations are safe)
120
+ - `.configs/*` with `--force` — overwritten with latest defaults
119
121
 
120
122
  This never touches:
121
123
 
@@ -127,12 +129,16 @@ This never touches:
127
129
  ## 8. Spec Compliance
128
130
 
129
131
  ```bash
130
- npm run spec # full check via clearstack binary
131
- npm run spec:code # code files ≤150 lines
132
- npm run spec:docs # doc files ≤500 lines
133
- npm run lint:fix # ESLint auto-fix
134
- npm run format # Prettier auto-format
135
- npm run typecheck # JSDoc type validation
132
+ npm run spec # interactive menu
133
+ npm run spec all # full check
134
+ npm run spec code # code files ≤150 lines
135
+ npm run spec docs # doc files ≤500 lines
136
+ npm run spec lint # ESLint + Stylelint + Markdown
137
+ npm run spec lint es # ESLint only
138
+ npm run spec format # Prettier
139
+ npm run spec imports # import map aliases
140
+ npm run spec types # JSDoc types (all jsconfigs)
141
+ npm run spec audit # security audit
136
142
  npm test # Node + browser tests
137
143
  ```
138
144
 
@@ -153,21 +159,21 @@ SPEC_IGNORE_DIRS=node_modules,src/vendor,.git,.configs
153
159
  The scaffolded `.github/workflows/spec.yml` runs all checks on every PR:
154
160
 
155
161
  ```
156
- spec:code spec:docs → lint format typecheck test
162
+ spec all → lint + format + code + docs + imports + types + audit
157
163
  ```
158
164
 
159
165
  ## Summary
160
166
 
161
- | Task | Command |
162
- | --------------------- | -------------------------------------- |
163
- | Install Clearstack | `npm install -D @techninja/clearstack` |
164
- | Scaffold (fullstack) | `npx clearstack init` |
165
- | Scaffold (static) | `npx clearstack init --static` |
166
- | Install dependencies | `npm install` |
167
- | Start dev server | `npm run dev` |
168
- | Lint + format | `npm run lint:fix && npm run format` |
169
- | Type check | `npm run typecheck` |
170
- | Run tests | `npm test` |
171
- | Full spec check | `npm run spec` |
172
- | Update spec + configs | `npm run spec:update` |
173
- | Review spec changes | `git diff docs/ .configs/` |
167
+ | Task | Command |
168
+ | --------------------- | ------------------------------------------ |
169
+ | Install Clearstack | `npm install -D @techninja/clearstack` |
170
+ | Scaffold (fullstack) | `npx clearstack init` |
171
+ | Scaffold (static) | `npx clearstack init --static` |
172
+ | Install dependencies | `npm install` |
173
+ | Start dev server | `npm run dev` |
174
+ | Lint + format | `npm run spec lint && npm run spec format` |
175
+ | Type check | `npm run spec types` |
176
+ | Run tests | `npm test` |
177
+ | Full spec check | `npm run spec` |
178
+ | Update spec + configs | `npm run spec update` |
179
+ | Review spec changes | `git diff docs/ .configs/` |
package/docs/TESTING.md CHANGED
@@ -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, html, expect } from '@open-wc/testing';
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(html`<app-button label="Click me"></app-button>`);
182
- const button = el.shadowRoot.querySelector('button');
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
- it('reflects disabled attribute', async () => {
187
- const el = await fixture(html`<app-button disabled></app-button>`);
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
- it('dispatches click event', async () => {
193
- const el = await fixture(html`<app-button label="Go"></app-button>`);
194
- let clicked = false;
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
package/lib/check.js CHANGED
@@ -3,7 +3,7 @@
3
3
  * @module lib/check
4
4
  */
5
5
 
6
- import { readFileSync, existsSync } from 'node:fs';
6
+ import { readFileSync, existsSync, readdirSync } from 'node:fs';
7
7
  import { resolve } from 'node:path';
8
8
  import { checkFileLines, runCmd, countFiles, checkImports } from './spec-utils.js';
9
9
 
@@ -51,7 +51,6 @@ export function buildCmds(projectDir) {
51
51
  stylelint: `${runner} stylelint --config .configs/.stylelintrc.json "src/**/*.css" --fix`,
52
52
  prettier: `${runner} prettier --config .configs/.prettierrc --write src scripts`,
53
53
  mdlint: `${runner} markdownlint-cli2 --config .configs/.markdownlint.jsonc --fix "docs/**/*.md" "*.md"`,
54
- types: `${runner} tsc --project .configs/jsconfig.json`,
55
54
  audit,
56
55
  };
57
56
  }
@@ -59,11 +58,29 @@ export function buildCmds(projectDir) {
59
58
 
60
59
  /** @typedef {{ key: string, name: string, parent?: string, run: () => boolean }} Check */
61
60
 
62
- /** Build the unified checks array. Children have a `parent` key. */
61
+ /** Find all jsconfig.json files main config + any in subdirectories. */
62
+ function findTypeConfigs(dir, runner) {
63
+ const main = resolve(dir, '.configs/jsconfig.json');
64
+ const configs = [];
65
+ if (existsSync(main)) configs.push({ key: 'frontend', label: 'Frontend', path: main });
66
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
67
+ if (!entry.isDirectory() || entry.name.startsWith('.') || entry.name === 'node_modules') continue;
68
+ const p = resolve(dir, entry.name, 'jsconfig.json');
69
+ if (existsSync(p)) configs.push({ key: entry.name, label: entry.name, path: p });
70
+ }
71
+ return configs.map((c) => ({
72
+ key: c.key, name: `JSDoc types — ${c.label}`, parent: 'types',
73
+ run: () => runCmd(`Types (${c.label})`, `${runner} tsc --project ${c.path}`, dir),
74
+ }));
75
+ }
76
+
77
+ /** Build the unified checks array. Children have a `parent` key.
78
+ * @returns {Check[]} */
63
79
  export function buildChecks(dir, cfg, cmds) {
64
80
  const js = () => countFiles(dir, ['.js'], cfg.ignore);
65
81
  const css = () => countFiles(dir, ['.css'], cfg.ignore);
66
82
  const md = () => countFiles(dir, ['.md'], cfg.ignore);
83
+ const runner = detectRunner(dir);
67
84
  return [
68
85
  { key: 'es', name: 'ESLint', parent: 'lint',
69
86
  run: () => runCmd('ESLint', cmds.lint, dir, `${js()} files`) },
@@ -79,8 +96,7 @@ export function buildChecks(dir, cfg, cmds) {
79
96
  run: () => checkFileLines(dir, cfg.docsExt, cfg.docsMax, cfg.ignore, `Docs (max ${cfg.docsMax} lines)`) },
80
97
  { key: 'imports', name: 'Import map aliases (no ../ imports)',
81
98
  run: () => checkImports(dir, cfg.ignore, 'Import map aliases (no ../ imports)') },
82
- { key: 'types', name: 'JSDoc types (tsc --checkJs)',
83
- run: () => runCmd('JSDoc types', cmds.types, dir, `${js()} files`) },
99
+ ...findTypeConfigs(dir, runner),
84
100
  { key: 'audit', name: 'Security audit',
85
101
  run: () => runCmd('Security audit', cmds.audit, dir) },
86
102
  ];
package/lib/update.js CHANGED
@@ -1,6 +1,7 @@
1
1
  /**
2
2
  * Spec updater — syncs docs and configs from the clearstack package.
3
- * Copies new versions so the user can review the git diff.
3
+ * Docs are overwritten (clearstack-owned). Configs skip existing files
4
+ * to preserve project customizations — use --force to overwrite.
4
5
  * @module lib/update
5
6
  */
6
7
 
@@ -8,24 +9,27 @@ import { readdirSync, readFileSync, writeFileSync, existsSync, mkdirSync } from
8
9
  import { resolve, join } from 'node:path';
9
10
 
10
11
  /**
11
- * Sync a source directory to a destination, comparing file contents.
12
+ * Sync a source directory to a destination.
12
13
  * @param {string} src
13
14
  * @param {string} dest
14
15
  * @param {string} label
16
+ * @param {{ skipExisting?: boolean }} [opts]
15
17
  * @returns {number} count of updated files
16
18
  */
17
- function syncDir(src, dest, label) {
19
+ function syncDir(src, dest, label, opts = {}) {
18
20
  if (!existsSync(src)) return 0;
19
21
  mkdirSync(dest, { recursive: true });
20
-
21
22
  const files = readdirSync(src).filter((f) => !f.startsWith('.spec'));
22
23
  let updated = 0;
23
24
 
24
25
  for (const file of files) {
25
- const srcContent = readFileSync(join(src, file), 'utf-8');
26
26
  const destPath = join(dest, file);
27
+ if (opts.skipExisting && existsSync(destPath)) {
28
+ console.log(` ⏭ Skipped: ${label}/${file} (exists, use --force to overwrite)`);
29
+ continue;
30
+ }
31
+ const srcContent = readFileSync(join(src, file), 'utf-8');
27
32
  const destContent = existsSync(destPath) ? readFileSync(destPath, 'utf-8') : '';
28
-
29
33
  if (srcContent !== destContent) {
30
34
  writeFileSync(destPath, srcContent);
31
35
  console.log(` ✓ Updated: ${label}/${file}`);
@@ -36,25 +40,29 @@ function syncDir(src, dest, label) {
36
40
  }
37
41
 
38
42
  /**
39
- * Update spec docs and configs from the clearstack package to the local project.
43
+ * Update spec docs and configs from the clearstack package.
40
44
  * @param {string} pkgRoot - Root of the clearstack package
45
+ * @param {{ force?: boolean }} [opts]
41
46
  */
42
- export async function update(pkgRoot) {
47
+ export async function update(pkgRoot, opts = {}) {
43
48
  const templateShared = resolve(pkgRoot, 'templates/shared');
44
49
  let total = 0;
45
50
 
46
51
  console.log('');
47
52
 
53
+ // Docs: always overwrite (clearstack-owned content)
48
54
  total += syncDir(
49
55
  resolve(templateShared, 'docs/clearstack'),
50
56
  resolve(process.cwd(), 'docs/clearstack'),
51
57
  'docs/clearstack',
52
58
  );
53
59
 
60
+ // Configs: skip existing unless --force (project may have customized)
54
61
  total += syncDir(
55
62
  resolve(templateShared, '.configs'),
56
63
  resolve(process.cwd(), '.configs'),
57
64
  '.configs',
65
+ { skipExisting: !opts.force },
58
66
  );
59
67
 
60
68
  // Merge .gitignore (append missing lines, never remove user entries)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@techninja/clearstack",
3
- "version": "0.3.15",
3
+ "version": "0.3.17",
4
4
  "type": "module",
5
5
  "description": "A no-build web component framework specification — scaffold, validate, and evolve spec-compliant projects",
6
6
  "bin": {
@@ -1,8 +1,35 @@
1
1
  import { playwrightLauncher } from '@web/test-runner-playwright';
2
+ import { dirname, resolve } from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+
5
+ const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..');
6
+
7
+ const aliases = {
8
+ '#store/': '/src/store/',
9
+ '#utils/': '/src/utils/',
10
+ '#atoms/': '/src/components/atoms/',
11
+ '#molecules/': '/src/components/molecules/',
12
+ '#organisms/': '/src/components/organisms/',
13
+ '#templates/': '/src/components/templates/',
14
+ '#pages/': '/src/pages/',
15
+ };
16
+
17
+ /** Resolve #prefix/ imports for @web/test-runner. */
18
+ function importMapPlugin() {
19
+ return {
20
+ name: 'import-map-aliases',
21
+ resolveImport({ source }) {
22
+ for (const [prefix, target] of Object.entries(aliases)) {
23
+ if (source.startsWith(prefix)) return source.replace(prefix, target);
24
+ }
25
+ },
26
+ };
27
+ }
2
28
 
3
29
  export default {
4
30
  files: 'src/components/**/*.test.js',
5
31
  nodeResolve: true,
6
- rootDir: '..',
32
+ rootDir: ROOT,
7
33
  browsers: [playwrightLauncher({ product: 'chromium' })],
34
+ plugins: [importMapPlugin()],
8
35
  };
@@ -250,3 +250,100 @@ These are the significant corrections:
250
250
  5. **The spec improves through implementation.** 9 significant corrections were
251
251
  made to the spec based on what we learned building the proof. The spec is
252
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.
@@ -298,7 +298,7 @@ multiple keys.
298
298
 
299
299
  - **One script per domain.** `test`, `spec`, `lint` — not `test:node`,
300
300
  `test:browser`, `lint:fix`, `spec:code`, `spec:docs`.
301
- - **Arguments over aliases.** `pnpm spec check code` instead of
301
+ - **Arguments over aliases.** `pnpm spec code` instead of
302
302
  `pnpm spec:code`. The CLI handles routing.
303
303
  - **Interactive by default.** Running `pnpm spec` with no arguments shows
304
304
  a menu of available actions. Users discover commands by using the tool.
@@ -370,7 +370,8 @@ npm run spec all
370
370
  npm run spec code # line counts (code files ≤150)
371
371
  npm run spec docs # line counts (doc files ≤500)
372
372
  npm run spec imports # import map aliases (no ../)
373
- npm run spec types # JSDoc types (tsc --checkJs)
373
+ npm run spec types # all jsconfigs (auto-discovered)
374
+ npm run spec types frontend # just .configs/jsconfig.json
374
375
  npm run spec audit # security audit
375
376
 
376
377
  # Parent keys run all children
@@ -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 the project root enables `checkJs: true`. Running
17
- `npm run typecheck` invokes `tsc --project jsconfig.json` which:
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 typecheck` before committing. Zero errors required.
175
+ - Run `npm run spec types` before committing. Zero errors required.
@@ -43,7 +43,7 @@ If a `package.json` already exists, Clearstack merges into it — your existing
43
43
 
44
44
  ```
45
45
  your-project/
46
- ├── .configs/ # Managedsynced on update
46
+ ├── .configs/ # ✏️ Scaffolded once yours to customize
47
47
  ├── .github/ # CI workflow, PR + issue templates
48
48
  ├── docs/
49
49
  │ ├── clearstack/ # ⟳ Managed — spec docs, synced on update
@@ -108,14 +108,16 @@ When Clearstack releases a new version:
108
108
 
109
109
  ```bash
110
110
  npm update @techninja/clearstack # bump the package
111
- npm run spec:update # sync docs + configs
111
+ npm run spec update # sync docs, skip existing configs
112
+ npm run spec update --force # sync docs + overwrite configs
112
113
  git diff docs/ .configs/ # review what changed
113
114
  ```
114
115
 
115
116
  This updates:
116
117
 
117
- - `docs/clearstack/*.md` — spec documentation
118
- - `.configs/*` — linter, formatter, type checker, test runner configs
118
+ - `docs/clearstack/*.md` — spec documentation (always overwritten)
119
+ - `.configs/*` — skipped if already exists (your customizations are safe)
120
+ - `.configs/*` with `--force` — overwritten with latest defaults
119
121
 
120
122
  This never touches:
121
123
 
@@ -127,12 +129,16 @@ This never touches:
127
129
  ## 8. Spec Compliance
128
130
 
129
131
  ```bash
130
- npm run spec # full check via clearstack binary
131
- npm run spec:code # code files ≤150 lines
132
- npm run spec:docs # doc files ≤500 lines
133
- npm run lint:fix # ESLint auto-fix
134
- npm run format # Prettier auto-format
135
- npm run typecheck # JSDoc type validation
132
+ npm run spec # interactive menu
133
+ npm run spec all # full check
134
+ npm run spec code # code files ≤150 lines
135
+ npm run spec docs # doc files ≤500 lines
136
+ npm run spec lint # ESLint + Stylelint + Markdown
137
+ npm run spec lint es # ESLint only
138
+ npm run spec format # Prettier
139
+ npm run spec imports # import map aliases
140
+ npm run spec types # JSDoc types (all jsconfigs)
141
+ npm run spec audit # security audit
136
142
  npm test # Node + browser tests
137
143
  ```
138
144
 
@@ -153,21 +159,21 @@ SPEC_IGNORE_DIRS=node_modules,src/vendor,.git,.configs
153
159
  The scaffolded `.github/workflows/spec.yml` runs all checks on every PR:
154
160
 
155
161
  ```
156
- spec:code spec:docs → lint format typecheck test
162
+ spec all → lint + format + code + docs + imports + types + audit
157
163
  ```
158
164
 
159
165
  ## Summary
160
166
 
161
- | Task | Command |
162
- | --------------------- | -------------------------------------- |
163
- | Install Clearstack | `npm install -D @techninja/clearstack` |
164
- | Scaffold (fullstack) | `npx clearstack init` |
165
- | Scaffold (static) | `npx clearstack init --static` |
166
- | Install dependencies | `npm install` |
167
- | Start dev server | `npm run dev` |
168
- | Lint + format | `npm run lint:fix && npm run format` |
169
- | Type check | `npm run typecheck` |
170
- | Run tests | `npm test` |
171
- | Full spec check | `npm run spec` |
172
- | Update spec + configs | `npm run spec:update` |
173
- | Review spec changes | `git diff docs/ .configs/` |
167
+ | Task | Command |
168
+ | --------------------- | ------------------------------------------ |
169
+ | Install Clearstack | `npm install -D @techninja/clearstack` |
170
+ | Scaffold (fullstack) | `npx clearstack init` |
171
+ | Scaffold (static) | `npx clearstack init --static` |
172
+ | Install dependencies | `npm install` |
173
+ | Start dev server | `npm run dev` |
174
+ | Lint + format | `npm run spec lint && npm run spec format` |
175
+ | Type check | `npm run spec types` |
176
+ | Run tests | `npm test` |
177
+ | Full spec check | `npm run spec` |
178
+ | Update spec + configs | `npm run spec update` |
179
+ | Review spec changes | `git diff docs/ .configs/` |
@@ -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, html, expect } from '@open-wc/testing';
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(html`<app-button label="Click me"></app-button>`);
182
- const button = el.shadowRoot.querySelector('button');
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
- it('reflects disabled attribute', async () => {
187
- const el = await fixture(html`<app-button disabled></app-button>`);
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
- it('dispatches click event', async () => {
193
- const el = await fixture(html`<app-button label="Go"></app-button>`);
194
- let clicked = false;
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