@techninja/clearstack 0.3.17 → 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.
@@ -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
- Lives in `src/utils/realtimeSync.js`:
414
-
415
- ```javascript
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
- `store.clear(Model)` fully invalidates the cache for that model type.
437
- Any component bound to the model via `store()` will automatically re-fetch
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
- For the local user, form submit handlers also call `store.clear(Model)`
442
- immediately after a successful save. The SSE event that follows is
443
- redundant for the local user but ensures other connected clients update.
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's `connect` descriptor:
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
- import { connectRealtime } from '../utils/realtimeSync.js';
456
- import UserModel from '../store/UserModel.js';
457
-
458
- export default define({
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
- render: ({ stack }) => html` <template layout="column height::100vh">${stack}</template> `,
471
- });
477
+ },
472
478
  ```
473
479
 
474
- When the backend sends `{ type: "user", id: "42" }` over SSE, the store
475
- cache for `UserModel` is cleared, and any component displaying that user
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
 
package/lib/check.js CHANGED
@@ -1,64 +1,20 @@
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, readdirSync } 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';
11
-
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
- }
18
-
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();
25
- }
26
- return env;
27
- }
28
-
29
- /**
30
- * Load spec config from project .env.
31
- * @param {string} projectDir
32
- */
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
- audit,
55
- };
56
- }
57
-
13
+ export { loadConfig, buildCmds, detectRunner } from './spec-config.js';
58
14
 
59
15
  /** @typedef {{ key: string, name: string, parent?: string, run: () => boolean }} Check */
60
16
 
61
- /** Find all jsconfig.json files — main config + any in subdirectories. */
17
+ /** Find all jsconfig.json files — main config + subdirectories. */
62
18
  function findTypeConfigs(dir, runner) {
63
19
  const main = resolve(dir, '.configs/jsconfig.json');
64
20
  const configs = [];
@@ -74,8 +30,10 @@ function findTypeConfigs(dir, runner) {
74
30
  }));
75
31
  }
76
32
 
77
- /** Build the unified checks array. Children have a `parent` key.
78
- * @returns {Check[]} */
33
+ /**
34
+ * Build the unified checks array. Children have a `parent` key.
35
+ * @returns {Check[]}
36
+ */
79
37
  export function buildChecks(dir, cfg, cmds) {
80
38
  const js = () => countFiles(dir, ['.js'], cfg.ignore);
81
39
  const css = () => countFiles(dir, ['.css'], cfg.ignore);
@@ -91,7 +49,9 @@ export function buildChecks(dir, cfg, cmds) {
91
49
  { key: 'prettier', name: 'Prettier', parent: 'format',
92
50
  run: () => runCmd('Prettier', cmds.prettier, dir, `${js()} files`) },
93
51
  { key: 'code', name: `Code (max ${cfg.codeMax} lines)`,
94
- 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 }) },
95
55
  { key: 'docs', name: `Docs (max ${cfg.docsMax} lines)`,
96
56
  run: () => checkFileLines(dir, cfg.docsExt, cfg.docsMax, cfg.ignore, `Docs (max ${cfg.docsMax} lines)`) },
97
57
  { key: 'imports', name: 'Import map aliases (no ../ imports)',
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.17",
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': [
@@ -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