@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/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
@@ -1,69 +1,44 @@
1
1
  /**
2
- * Spec compliance checker — config, commands, and orchestration.
2
+ * Spec compliance checker — check orchestration and resolution.
3
+ * Config lives in spec-config.js, utilities in spec-utils.js.
3
4
  * @module lib/check
4
5
  */
5
6
 
6
- import { readFileSync, existsSync } from 'node:fs';
7
+ import { existsSync, readdirSync } from 'node:fs';
7
8
  import { resolve } from 'node:path';
8
9
  import { checkFileLines, runCmd, countFiles, checkImports } from './spec-utils.js';
10
+ import { loadConfig, buildCmds, detectRunner } from './spec-config.js';
9
11
 
10
12
  export { checkFileLines, runCmd, countFiles, findFiles, elapsed, checkImports } from './spec-utils.js';
13
+ export { loadConfig, buildCmds, detectRunner } from './spec-config.js';
11
14
 
12
- /** Detect the project's package manager runner (npx, pnpm exec, yarn). */
13
- function detectRunner(projectDir) {
14
- if (existsSync(resolve(projectDir, 'pnpm-lock.yaml'))) return 'pnpm exec';
15
- if (existsSync(resolve(projectDir, 'yarn.lock'))) return 'yarn';
16
- return 'npx';
17
- }
15
+ /** @typedef {{ key: string, name: string, parent?: string, run: () => boolean }} Check */
18
16
 
19
- /** @param {string} src */
20
- function parseEnv(src) {
21
- const env = {};
22
- for (const line of src.split('\n')) {
23
- const m = line.match(/^([^#=]+)=(.*)$/);
24
- if (m) env[m[1].trim()] = m[2].trim();
17
+ /** Find all jsconfig.json files — main config + subdirectories. */
18
+ function findTypeConfigs(dir, runner) {
19
+ const main = resolve(dir, '.configs/jsconfig.json');
20
+ const configs = [];
21
+ if (existsSync(main)) configs.push({ key: 'frontend', label: 'Frontend', path: main });
22
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
23
+ if (!entry.isDirectory() || entry.name.startsWith('.') || entry.name === 'node_modules') continue;
24
+ const p = resolve(dir, entry.name, 'jsconfig.json');
25
+ if (existsSync(p)) configs.push({ key: entry.name, label: entry.name, path: p });
25
26
  }
26
- return env;
27
+ return configs.map((c) => ({
28
+ key: c.key, name: `JSDoc types — ${c.label}`, parent: 'types',
29
+ run: () => runCmd(`Types (${c.label})`, `${runner} tsc --project ${c.path}`, dir),
30
+ }));
27
31
  }
28
32
 
29
33
  /**
30
- * Load spec config from project .env.
31
- * @param {string} projectDir
34
+ * Build the unified checks array. Children have a `parent` key.
35
+ * @returns {Check[]}
32
36
  */
33
- export function loadConfig(projectDir) {
34
- const envPath = resolve(projectDir, '.env');
35
- const env = existsSync(envPath) ? parseEnv(readFileSync(envPath, 'utf-8')) : {};
36
- return {
37
- codeMax: parseInt(env.SPEC_CODE_MAX_LINES) || 150,
38
- docsMax: parseInt(env.SPEC_DOCS_MAX_LINES) || 500,
39
- codeExt: (env.SPEC_CODE_EXTENSIONS || '.js,.css').split(','),
40
- docsExt: (env.SPEC_DOCS_EXTENSIONS || '.md').split(','),
41
- ignore: (env.SPEC_IGNORE_DIRS || 'node_modules,src/vendor,.git,.configs').split(','),
42
- };
43
- }
44
-
45
- /** Build check commands for the detected package manager. */
46
- export function buildCmds(projectDir) {
47
- const runner = detectRunner(projectDir);
48
- const audit = runner === 'pnpm exec' ? 'pnpm audit --prod' : 'npm audit --omit=dev';
49
- return {
50
- lint: `${runner} eslint --config .configs/eslint.config.js . --fix`,
51
- stylelint: `${runner} stylelint --config .configs/.stylelintrc.json "src/**/*.css" --fix`,
52
- prettier: `${runner} prettier --config .configs/.prettierrc --write src scripts`,
53
- mdlint: `${runner} markdownlint-cli2 --config .configs/.markdownlint.jsonc --fix "docs/**/*.md" "*.md"`,
54
- types: `${runner} tsc --project .configs/jsconfig.json`,
55
- audit,
56
- };
57
- }
58
-
59
-
60
- /** @typedef {{ key: string, name: string, parent?: string, run: () => boolean }} Check */
61
-
62
- /** Build the unified checks array. Children have a `parent` key. */
63
37
  export function buildChecks(dir, cfg, cmds) {
64
38
  const js = () => countFiles(dir, ['.js'], cfg.ignore);
65
39
  const css = () => countFiles(dir, ['.css'], cfg.ignore);
66
40
  const md = () => countFiles(dir, ['.md'], cfg.ignore);
41
+ const runner = detectRunner(dir);
67
42
  return [
68
43
  { key: 'es', name: 'ESLint', parent: 'lint',
69
44
  run: () => runCmd('ESLint', cmds.lint, dir, `${js()} files`) },
@@ -74,13 +49,14 @@ export function buildChecks(dir, cfg, cmds) {
74
49
  { key: 'prettier', name: 'Prettier', parent: 'format',
75
50
  run: () => runCmd('Prettier', cmds.prettier, dir, `${js()} files`) },
76
51
  { key: 'code', name: `Code (max ${cfg.codeMax} lines)`,
77
- run: () => checkFileLines(dir, cfg.codeExt, cfg.codeMax, cfg.ignore, `Code (max ${cfg.codeMax} lines)`) },
52
+ run: () => checkFileLines(dir, cfg.codeExt, cfg.codeMax, cfg.ignore, `Code (max ${cfg.codeMax} lines)`, { exclude: cfg.testPattern }) },
53
+ { key: 'tests', name: `Tests (max ${cfg.testMax} lines)`,
54
+ run: () => checkFileLines(dir, cfg.codeExt, cfg.testMax, cfg.ignore, `Tests (max ${cfg.testMax} lines)`, { include: cfg.testPattern }) },
78
55
  { key: 'docs', name: `Docs (max ${cfg.docsMax} lines)`,
79
56
  run: () => checkFileLines(dir, cfg.docsExt, cfg.docsMax, cfg.ignore, `Docs (max ${cfg.docsMax} lines)`) },
80
57
  { key: 'imports', name: 'Import map aliases (no ../ imports)',
81
58
  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`) },
59
+ ...findTypeConfigs(dir, runner),
84
60
  { key: 'audit', name: 'Security audit',
85
61
  run: () => runCmd('Security audit', cmds.audit, dir) },
86
62
  ];
package/lib/init.js CHANGED
@@ -7,6 +7,7 @@ import { copyFileSync, existsSync, readFileSync } from 'node:fs';
7
7
  import { basename, resolve } from 'node:path';
8
8
  import { copyTemplates } from './copy.js';
9
9
  import { writePackageJson } from './package-gen.js';
10
+ import { detectPlatforms, initPlatform } from './platform.js';
10
11
 
11
12
  /**
12
13
  * @typedef {Object} InitOptions
@@ -75,6 +76,10 @@ export async function init(pkgRoot, opts = {}) {
75
76
  console.log(' ✓ .env.local (edit this, .env has defaults)');
76
77
  }
77
78
 
79
+ // Platform stacking: detect and scaffold platform layers
80
+ const platforms = detectPlatforms(dest);
81
+ for (const platform of platforms) initPlatform(platform, dest);
82
+
78
83
  console.log(`\n✅ Project scaffolded at ${dest}`);
79
84
  console.log(' npm install');
80
85
  console.log(' npm run dev\n');
@@ -0,0 +1,113 @@
1
+ /**
2
+ * Platform file operations — vendor, sync, init, update.
3
+ * Detection lives in platform.js.
4
+ * @module lib/platform-files
5
+ */
6
+
7
+ import { cpSync, existsSync, mkdirSync, readdirSync } from 'node:fs';
8
+ import { resolve, join } from 'node:path';
9
+
10
+ /**
11
+ * Copy src → dest recursively (always overwrite).
12
+ * @param {string} src
13
+ * @param {string} dest
14
+ * @param {string} label
15
+ */
16
+ function copyDir(src, dest, label) {
17
+ if (!existsSync(src)) return 0;
18
+ mkdirSync(dest, { recursive: true });
19
+ cpSync(src, dest, { recursive: true });
20
+ console.log(` ✓ Synced: ${label}`);
21
+ return 1;
22
+ }
23
+
24
+ /**
25
+ * Copy entries from src → dest, skipping files that already exist.
26
+ * @param {string} src
27
+ * @param {string} dest
28
+ * @param {string} label
29
+ */
30
+ function copySkipExisting(src, dest, label) {
31
+ if (!existsSync(src)) return 0;
32
+ mkdirSync(dest, { recursive: true });
33
+ let count = 0;
34
+ for (const entry of readdirSync(src, { withFileTypes: true })) {
35
+ const destPath = join(dest, entry.name);
36
+ if (existsSync(destPath)) {
37
+ console.log(` ⏭ Skipped: ${label}/${entry.name} (exists)`);
38
+ continue;
39
+ }
40
+ cpSync(join(src, entry.name), destPath, { recursive: entry.isDirectory() });
41
+ console.log(` ✓ Created: ${label}/${entry.name}`);
42
+ count++;
43
+ }
44
+ return count;
45
+ }
46
+
47
+ /**
48
+ * Copy src → dest recursively, overwriting files but merging directories.
49
+ * @param {string} src
50
+ * @param {string} dest
51
+ * @param {string} label
52
+ */
53
+ function copyMerge(src, dest, label) {
54
+ if (!existsSync(src)) return 0;
55
+ mkdirSync(dest, { recursive: true });
56
+ let count = 0;
57
+ for (const entry of readdirSync(src, { withFileTypes: true })) {
58
+ const srcPath = join(src, entry.name);
59
+ const destPath = join(dest, entry.name);
60
+ if (entry.isDirectory()) {
61
+ count += copyMerge(srcPath, destPath, `${label}/${entry.name}`);
62
+ } else {
63
+ cpSync(srcPath, destPath);
64
+ console.log(` ✓ ${label}/${entry.name}`);
65
+ count++;
66
+ }
67
+ }
68
+ return count;
69
+ }
70
+
71
+ /** Vendor platform files → src/vendor/<prefix>/ (always overwrite). */
72
+ export function vendorPlatform(platform, projectDir) {
73
+ const { manifest, pkgDir } = platform;
74
+ return copyDir(
75
+ resolve(pkgDir, manifest.vendor),
76
+ resolve(projectDir, manifest.vendorDir),
77
+ `${manifest.prefix} → ${manifest.vendorDir}/`,
78
+ );
79
+ }
80
+
81
+ /** Sync platform docs → docs/<prefix>/ (always overwrite). */
82
+ export function syncPlatformDocs(platform, projectDir) {
83
+ const { manifest, pkgDir } = platform;
84
+ if (!manifest.docs) return 0;
85
+ return copyDir(
86
+ resolve(pkgDir, manifest.docs),
87
+ resolve(projectDir, `docs/${manifest.prefix}`),
88
+ `docs/${manifest.prefix}/`,
89
+ );
90
+ }
91
+
92
+ /** Run full platform init: templates, vendor, docs, scripts, api. */
93
+ export function initPlatform(platform, projectDir) {
94
+ const { manifest, pkgDir, name } = platform;
95
+ console.log(`\n📦 Platform detected: ${name} (${manifest.prefix})\n`);
96
+ copyMerge(resolve(pkgDir, manifest.templates), projectDir, 'templates');
97
+ vendorPlatform(platform, projectDir);
98
+ syncPlatformDocs(platform, projectDir);
99
+ if (manifest.scripts) {
100
+ copySkipExisting(resolve(pkgDir, manifest.scripts), resolve(projectDir, 'scripts'), 'scripts');
101
+ }
102
+ if (manifest.api) {
103
+ copySkipExisting(resolve(pkgDir, manifest.api), resolve(projectDir, 'api'), 'api');
104
+ }
105
+ }
106
+
107
+ /** Run platform update: re-vendor + sync docs only. */
108
+ export function updatePlatform(platform, projectDir) {
109
+ const { name, manifest } = platform;
110
+ console.log(`\n📦 Platform: ${name} (${manifest.prefix})`);
111
+ vendorPlatform(platform, projectDir);
112
+ syncPlatformDocs(platform, projectDir);
113
+ }
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Platform stacking — detect platforms in project dependencies.
3
+ * File operations live in platform-files.js.
4
+ * @module lib/platform
5
+ */
6
+
7
+ import { existsSync, readFileSync } from 'node:fs';
8
+ import { resolve } from 'node:path';
9
+
10
+ export {
11
+ vendorPlatform, syncPlatformDocs, initPlatform, updatePlatform,
12
+ } from './platform-files.js';
13
+
14
+ /**
15
+ * @typedef {Object} PlatformManifest
16
+ * @property {string} prefix
17
+ * @property {string} vendorDir
18
+ * @property {string} configFile
19
+ * @property {string} templates
20
+ * @property {string} vendor
21
+ * @property {string} [docs]
22
+ * @property {string} [scripts]
23
+ * @property {string} [api]
24
+ */
25
+
26
+ /** @typedef {{ name: string, pkgDir: string, manifest: PlatformManifest }} DetectedPlatform */
27
+
28
+ /**
29
+ * Detect platforms in the project's dependencies.
30
+ * @param {string} projectDir
31
+ * @returns {DetectedPlatform[]}
32
+ */
33
+ export function detectPlatforms(projectDir) {
34
+ const pkgPath = resolve(projectDir, 'package.json');
35
+ if (!existsSync(pkgPath)) return [];
36
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
37
+ const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
38
+ /** @type {DetectedPlatform[]} */
39
+ const platforms = [];
40
+ for (const name of Object.keys(allDeps)) {
41
+ const depPkg = resolve(projectDir, 'node_modules', name, 'package.json');
42
+ if (!existsSync(depPkg)) continue;
43
+ const dep = JSON.parse(readFileSync(depPkg, 'utf-8'));
44
+ if (!dep.clearstack?.platform) continue;
45
+ platforms.push({
46
+ name,
47
+ pkgDir: resolve(projectDir, 'node_modules', name),
48
+ manifest: dep.clearstack.platform,
49
+ });
50
+ }
51
+ return platforms;
52
+ }
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Spec config — environment detection, .env parsing, command building.
3
+ * @module lib/spec-config
4
+ */
5
+
6
+ import { readFileSync, existsSync } from 'node:fs';
7
+ import { resolve } from 'node:path';
8
+
9
+ /** Detect the project's package manager runner (npx, pnpm exec, yarn). */
10
+ export function detectRunner(projectDir) {
11
+ if (existsSync(resolve(projectDir, 'pnpm-lock.yaml'))) return 'pnpm exec';
12
+ if (existsSync(resolve(projectDir, 'yarn.lock'))) return 'yarn';
13
+ return 'npx';
14
+ }
15
+
16
+ /** @param {string} src */
17
+ function parseEnv(src) {
18
+ const env = {};
19
+ for (const line of src.split('\n')) {
20
+ const m = line.match(/^([^#=]+)=(.*)$/);
21
+ if (m) env[m[1].trim()] = m[2].trim();
22
+ }
23
+ return env;
24
+ }
25
+
26
+ /**
27
+ * Load spec config from project .env.
28
+ * @param {string} projectDir
29
+ */
30
+ export function loadConfig(projectDir) {
31
+ const envPath = resolve(projectDir, '.env');
32
+ const env = existsSync(envPath) ? parseEnv(readFileSync(envPath, 'utf-8')) : {};
33
+ return {
34
+ codeMax: parseInt(env.SPEC_CODE_MAX_LINES) || 150,
35
+ testMax: parseInt(env.SPEC_TEST_MAX_LINES) || 300,
36
+ docsMax: parseInt(env.SPEC_DOCS_MAX_LINES) || 500,
37
+ codeExt: (env.SPEC_CODE_EXTENSIONS || '.js,.css').split(','),
38
+ docsExt: (env.SPEC_DOCS_EXTENSIONS || '.md').split(','),
39
+ testPattern: env.SPEC_TEST_PATTERN || '.test.js',
40
+ ignore: (env.SPEC_IGNORE_DIRS || 'node_modules,src/vendor,.git,.configs').split(','),
41
+ };
42
+ }
43
+
44
+ /** Build check commands for the detected package manager. */
45
+ export function buildCmds(projectDir) {
46
+ const runner = detectRunner(projectDir);
47
+ const audit = runner === 'pnpm exec' ? 'pnpm audit --prod' : 'npm audit --omit=dev';
48
+ return {
49
+ lint: `${runner} eslint --config .configs/eslint.config.js . --fix`,
50
+ stylelint: `${runner} stylelint --config .configs/.stylelintrc.json "src/**/*.css" --fix`,
51
+ prettier: `${runner} prettier --config .configs/.prettierrc --write src scripts`,
52
+ mdlint: `${runner} markdownlint-cli2 --config .configs/.markdownlint.jsonc --fix "docs/**/*.md" "*.md"`,
53
+ audit,
54
+ };
55
+ }
package/lib/spec-utils.js CHANGED
@@ -59,11 +59,14 @@ export function countFiles(root, extensions, ignoreDirs, dirs, filter) {
59
59
  * @param {number} max
60
60
  * @param {string[]} ignoreDirs
61
61
  * @param {string} label
62
+ * @param {{ exclude?: string, include?: string }} [filter]
62
63
  * @returns {boolean}
63
64
  */
64
- export function checkFileLines(root, extensions, max, ignoreDirs, label) {
65
+ export function checkFileLines(root, extensions, max, ignoreDirs, label, filter) {
65
66
  const start = performance.now();
66
- const files = findFiles(root, extensions, ignoreDirs, root);
67
+ let files = findFiles(root, extensions, ignoreDirs, root);
68
+ if (filter?.exclude) files = files.filter((f) => !f.endsWith(filter.exclude));
69
+ if (filter?.include) files = files.filter((f) => f.endsWith(filter.include));
67
70
  const violations = [];
68
71
  for (const file of files) {
69
72
  const lines = readFileSync(resolve(root, file), 'utf-8').trimEnd().split('\n').length;
package/lib/update.js CHANGED
@@ -7,6 +7,7 @@
7
7
 
8
8
  import { readdirSync, readFileSync, writeFileSync, existsSync, mkdirSync } from 'node:fs';
9
9
  import { resolve, join } from 'node:path';
10
+ import { detectPlatforms, updatePlatform } from './platform.js';
10
11
 
11
12
  /**
12
13
  * Sync a source directory to a destination.
@@ -87,6 +88,13 @@ export async function update(pkgRoot, opts = {}) {
87
88
  mkdirSync(resolve(process.cwd(), 'docs/clearstack'), { recursive: true });
88
89
  writeFileSync(versionPath, pkg.version + '\n');
89
90
 
91
+ // Platform stacking: re-vendor + sync platform docs
92
+ const platforms = detectPlatforms(process.cwd());
93
+ for (const platform of platforms) {
94
+ updatePlatform(platform, process.cwd());
95
+ total++;
96
+ }
97
+
90
98
  console.log(`\n docs/app-spec/ — untouched (your project specs are safe)`);
91
99
 
92
100
  if (total === 0) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@techninja/clearstack",
3
- "version": "0.3.16",
3
+ "version": "0.3.19",
4
4
  "type": "module",
5
5
  "description": "A no-build web component framework specification — scaffold, validate, and evolve spec-compliant projects",
6
6
  "bin": {
@@ -33,13 +33,13 @@ export function connectRealtime(url, modelMap) {
33
33
  console.log(`[SSE] ${type} ${action} — clearing store cache`);
34
34
  try {
35
35
  store.clear([Model]);
36
- } catch {
37
- /* list may not exist */
36
+ } catch (e) {
37
+ console.warn(`[SSE] clear list failed for ${type}:`, e.message);
38
38
  }
39
39
  try {
40
40
  store.clear(Model);
41
- } catch {
42
- /* singular may not exist */
41
+ } catch (e) {
42
+ console.warn(`[SSE] clear singular failed for ${type}:`, e.message);
43
43
  }
44
44
  }, 300);
45
45
  });
@@ -39,6 +39,7 @@ export default [
39
39
  'unused-imports/no-unused-imports': 'error',
40
40
  'unused-imports/no-unused-vars': ['warn', { argsIgnorePattern: '^_' }],
41
41
  'no-console': 'off',
42
+ 'no-empty': ['error', { allowEmptyCatch: false }],
42
43
 
43
44
  // JSDoc enforcement
44
45
  'jsdoc/require-jsdoc': [
@@ -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
  };
@@ -197,6 +197,20 @@ function toggle(host) {
197
197
  }
198
198
  ```
199
199
 
200
+ ```javascript
201
+ // BAD — silent catch hides the failure completely
202
+ try {
203
+ store.clear([Model]);
204
+ } catch {
205
+ /* list may not exist */
206
+ }
207
+ ```
208
+
209
+ **Never use empty `catch` blocks.** If you catch to prevent crashing,
210
+ you must `console.warn` or `console.error` so the failure is visible.
211
+ Silent catches turn bugs into ghosts — things "just don't work" with
212
+ zero console output to trace.
213
+
200
214
  ### ✅ Do
201
215
 
202
216
  ```javascript
@@ -215,6 +229,15 @@ function toggle(host) {
215
229
  }
216
230
  ```
217
231
 
232
+ ```javascript
233
+ // GOOD — catch to prevent crash, but log so failures are visible
234
+ try {
235
+ store.clear([Model]);
236
+ } catch (e) {
237
+ console.warn('[store] clear failed:', e.message);
238
+ }
239
+ ```
240
+
218
241
  ---
219
242
 
220
243
  ### ✅ Always Do This
@@ -251,10 +274,24 @@ export const fullName = (user) => `${user.firstName} ${user.lastName}`;
251
274
 
252
275
  ---
253
276
 
254
- ## File Size: Soft Warnings Before Hard Limits
277
+ ## File Size: Why 150 Lines
278
+
279
+ The 150-line limit isn't about the number — it's about **context cost**.
280
+ A 400-line file isn't hard to scroll through, but it's expensive to
281
+ _hold in mind_. Humans can only reason about a limited scope at once.
282
+ LLMs face the same constraint differently: every token spent reading a
283
+ bloated file is a token not spent on the actual task. Small files mean
284
+ context is spent on _building_, not on _understanding what you're
285
+ looking at_.
286
+
287
+ When every file is small enough to comprehend in one pass, both humans
288
+ and LLMs can generate, review, and modify code without re-reading
289
+ unrelated sections. Refactors become safe because the blast radius of
290
+ any change is one small file. The limit forces decomposition into
291
+ concept-sized units — not arbitrary chunks, but files where each one
292
+ answers a single question.
255
293
 
256
- The 150-line limit is a hard gate in CI, but treat **~120 lines as a yellow
257
- light.** When a file passes 120 lines:
294
+ Treat **~120 lines as a yellow light.** When a file passes 120 lines:
258
295
 
259
296
  1. Add a `// SPLIT CANDIDATE:` comment noting where a logical split could happen
260
297
  2. Continue working — don't split mid-feature
@@ -270,7 +307,9 @@ function moveObj(o, dx, dy) { ... }
270
307
  ### When a File Exceeds 150 Lines
271
308
 
272
309
  The **only correct response** is to split the file into two or more files.
273
- Never do any of the following to reduce line count:
310
+ The concepts have outgrown the container find the seam between them
311
+ and give each its own file. Never do any of the following to reduce
312
+ line count:
274
313
 
275
314
  - Remove or shorten JSDoc comments
276
315
  - Collapse multi-line expressions onto one line
@@ -370,7 +409,8 @@ npm run spec all
370
409
  npm run spec code # line counts (code files ≤150)
371
410
  npm run spec docs # line counts (doc files ≤500)
372
411
  npm run spec imports # import map aliases (no ../)
373
- npm run spec types # JSDoc types (tsc --checkJs)
412
+ npm run spec types # all jsconfigs (auto-discovered)
413
+ npm run spec types frontend # just .configs/jsconfig.json
374
414
  npm run spec audit # security audit
375
415
 
376
416
  # Parent keys run all children