@techninja/clearstack 0.4.7 → 0.4.8

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/lib/check.js CHANGED
@@ -4,24 +4,23 @@
4
4
  * @module lib/check
5
5
  */
6
6
 
7
- import { existsSync, readdirSync } from 'node:fs';
7
+ import { existsSync } from 'node:fs';
8
8
  import { resolve } from 'node:path';
9
9
  import { runCmd } from './spec-utils.js';
10
10
  import { countFiles, checkFileLines, checkImports } from './spec-scan.js';
11
11
  import { checkI18n } from './spec-i18n.js';
12
12
  import { loadConfig, buildCmds, detectRunner } from './spec-config.js';
13
+ import { findTypeConfigs } from './spec-types.js';
13
14
 
14
15
  export { runCmd, elapsed } from './spec-utils.js';
15
16
  export { findFiles, countFiles, checkFileLines, checkImports } from './spec-scan.js';
16
17
  export { checkI18n } from './spec-i18n.js';
17
18
  export { loadConfig, buildCmds, detectRunner } from './spec-config.js';
18
19
 
20
+ /** @typedef {{ key: string, name: string, parent?: string, watchExts?: string[], fix?: () => void, run: (opts?: object) => (boolean | import('./spec-utils.js').CheckResult | Promise<import('./spec-utils.js').CheckResult>) }} Check */
21
+
19
22
  /**
20
- * Load project-level spec extensions from `clearstack.spec.js` in the project root.
21
- * The file may export a default array of Check objects. Any check whose key matches
22
- * a built-in check replaces it; new keys are appended.
23
- * @param {string} projectDir
24
- * @returns {Promise<Check[]>}
23
+ *
25
24
  */
26
25
  async function loadExtensions(projectDir) {
27
26
  const extPath = resolve(projectDir, 'clearstack.spec.js');
@@ -29,10 +28,7 @@ async function loadExtensions(projectDir) {
29
28
  try {
30
29
  const mod = await import(extPath);
31
30
  const exts = mod.default ?? [];
32
- if (!Array.isArray(exts)) {
33
- console.warn('⚠ clearstack.spec.js default export must be an array — extensions ignored');
34
- return [];
35
- }
31
+ if (!Array.isArray(exts)) { console.warn('⚠ clearstack.spec.js default export must be an array — extensions ignored'); return []; }
36
32
  return exts;
37
33
  } catch (e) {
38
34
  console.warn(`⚠ Failed to load clearstack.spec.js: ${e.message}`);
@@ -40,56 +36,28 @@ async function loadExtensions(projectDir) {
40
36
  }
41
37
  }
42
38
 
43
- /** @typedef {{ key: string, name: string, parent?: string, watchExts?: string[], run: (opts?: object) => (boolean | import('./spec-utils.js').CheckResult | Promise<import('./spec-utils.js').CheckResult>) }} Check */
44
-
45
- /** Find all jsconfig.json files — main config + subdirectories. */
46
- function findTypeConfigs(dir, runner) {
47
- const main = resolve(dir, '.configs/jsconfig.json');
48
- const configs = [];
49
- if (existsSync(main)) configs.push({ key: 'frontend', label: 'Frontend', path: main });
50
- for (const entry of readdirSync(dir, { withFileTypes: true })) {
51
- if (!entry.isDirectory() || entry.name.startsWith('.') || entry.name === 'node_modules') continue;
52
- const p = resolve(dir, entry.name, 'jsconfig.json');
53
- if (existsSync(p)) configs.push({ key: entry.name, label: entry.name, path: p });
54
- }
55
- return configs.map((c) => ({
56
- key: c.key, name: `JSDoc types — ${c.label}`, parent: 'types',
57
- run: (o) => runCmd(`Types (${c.label})`, `${runner} tsc --project ${c.path} --noEmit`, dir, undefined, o),
58
- }));
59
- }
60
-
61
- /**
62
- * Build the unified checks array. Children have a `parent` key.
63
- * Extensions from clearstack.spec.js are merged in: same key = replace, new key = append.
64
- * @returns {Promise<Check[]>}
65
- */
39
+ /** Build the unified checks array. Extensions from clearstack.spec.js are merged in. */
66
40
  export async function buildChecks(dir, cfg, cmds) {
67
41
  const js = () => countFiles(dir, ['.js'], cfg.ignore);
68
42
  const css = () => countFiles(dir, ['.css'], cfg.ignore);
69
43
  const md = () => countFiles(dir, ['.md'], cfg.ignore);
70
44
  const runner = detectRunner(dir);
71
45
  const builtin = [
72
- { key: 'es', name: 'ESLint', parent: 'lint',
73
- run: (o) => runCmd('ESLint', cmds.lint, dir, `${js()} files`, o) },
74
- { key: 'css', name: 'Stylelint', parent: 'lint',
75
- run: (o) => runCmd('Stylelint', cmds.stylelint, dir, `${css()} files`, o) },
76
- { key: 'md', name: 'Markdown lint', parent: 'lint',
77
- run: (o) => runCmd('Markdown', cmds.mdlint, dir, `${md()} files`, o) },
78
- { key: 'prettier', name: 'Prettier', parent: 'format',
79
- run: (o) => runCmd('Prettier', cmds.prettier, dir, `${js()} files`, o) },
80
- { key: 'lines', name: `Code (max ${cfg.codeMax} lines)`, parent: 'code',
46
+ { key: 'es', name: 'ESLint', parent: 'lint', run: (o) => runCmd('ESLint', cmds.lint, dir, `${js()} files`, o) },
47
+ { key: 'css', name: 'Stylelint', parent: 'lint', run: (o) => runCmd('Stylelint', cmds.stylelint, dir, `${css()} files`, o) },
48
+ { key: 'md', name: 'Markdown lint', parent: 'lint', run: (o) => runCmd('Markdown', cmds.mdlint, dir, `${md()} files`, o) },
49
+ { key: 'prettier', name: 'Prettier', parent: 'format', run: (o) => runCmd('Prettier', cmds.prettier, dir, `${js()} files`, o) },
50
+ { key: 'lines', name: `Code (max ${cfg.codeMax} lines)`, parent: 'code',
81
51
  run: (o) => checkFileLines(dir, cfg.codeExt, cfg.codeMax, cfg.ignore, `Code (max ${cfg.codeMax} lines)`, { exclude: cfg.testPattern, ...o }) },
82
- { key: 'i18n', name: 'i18n readiness', parent: 'code',
83
- run: (o) => checkI18n(dir, cfg.ignore, 'i18n readiness', o) },
84
- { key: 'tests', name: `Tests (max ${cfg.testMax} lines)`,
52
+ { key: 'i18n', name: 'i18n readiness', parent: 'code', run: (o) => checkI18n(dir, cfg.ignore, 'i18n readiness', o) },
53
+ { key: 'tests', name: `Tests (max ${cfg.testMax} lines)`,
85
54
  run: (o) => checkFileLines(dir, cfg.codeExt, cfg.testMax, cfg.ignore, `Tests (max ${cfg.testMax} lines)`, { include: cfg.testPattern, ...o }) },
86
- { key: 'docs', name: `Docs (max ${cfg.docsMax} lines)`,
55
+ { key: 'docs', name: `Docs (max ${cfg.docsMax} lines)`,
87
56
  run: (o) => checkFileLines(dir, cfg.docsExt, cfg.docsMax, cfg.ignore, `Docs (max ${cfg.docsMax} lines)`, o) },
88
57
  { key: 'imports', name: 'Import map aliases (no ../ imports)',
89
58
  run: (o) => checkImports(dir, cfg.ignore, 'Import map aliases (no ../ imports)', o) },
90
- ...findTypeConfigs(dir, runner),
91
- { key: 'audit', name: 'Security audit',
92
- run: (o) => runCmd('Security audit', cmds.audit, dir, undefined, o) },
59
+ ...findTypeConfigs(dir, runner, cfg.ignore),
60
+ { key: 'audit', name: 'Security audit', run: (o) => runCmd('Security audit', cmds.audit, dir, undefined, o) },
93
61
  ];
94
62
  const extensions = await loadExtensions(dir);
95
63
  if (!extensions.length) return builtin;
@@ -100,10 +68,7 @@ export async function buildChecks(dir, cfg, cmds) {
100
68
  /** Resolve a scope like 'lint', 'lint es', or 'code' to runnable check(s). */
101
69
  export function resolveChecks(checks, scope) {
102
70
  const [first, second] = scope.split(/\s+/);
103
- if (second) {
104
- const match = checks.find((c) => c.parent === first && c.key === second);
105
- return match ? [match] : null;
106
- }
71
+ if (second) { const match = checks.find((c) => c.parent === first && c.key === second); return match ? [match] : null; }
107
72
  const children = checks.filter((c) => c.parent === first);
108
73
  if (children.length) return children;
109
74
  const exact = checks.find((c) => c.key === first && !c.parent);
@@ -115,12 +80,11 @@ export function parentKeys(checks) {
115
80
  return [...new Set(checks.filter((c) => c.parent).map((c) => c.parent))];
116
81
  }
117
82
 
118
- /** Run the full spec compliance check (used by clearstack CLI and scripts/spec.js). */
83
+ /** Run the full spec compliance check. */
119
84
  export async function check(projectDir, scope, opts) {
120
85
  const cfg = loadConfig(projectDir);
121
86
  const cmds = buildCmds(projectDir);
122
87
  const checks = await buildChecks(projectDir, cfg, cmds);
123
-
124
88
  if (scope && scope !== 'all') {
125
89
  const matched = resolveChecks(checks, scope);
126
90
  if (!matched) {
@@ -130,15 +94,12 @@ export async function check(projectDir, scope, opts) {
130
94
  process.exit(1);
131
95
  }
132
96
  const results = await Promise.all(matched.map((c) => c.run(opts)));
133
- const ok = results.every((r) => typeof r === 'boolean' ? r : r.pass);
134
- if (!ok) process.exit(1);
97
+ if (!results.every((r) => typeof r === 'boolean' ? r : r.pass)) process.exit(1);
135
98
  return;
136
99
  }
137
-
138
100
  console.log('🔍 Clearstack compliance checking now... 💙\n');
139
101
  const results = await Promise.all(checks.map((c) => c.run(opts)));
140
- const passes = results.map((r) => typeof r === 'boolean' ? r : r.pass);
141
- const passed = passes.filter(Boolean).length;
102
+ const passed = results.filter((r) => typeof r === 'boolean' ? r : r.pass).length;
142
103
  console.log(`\n${'='.repeat(40)}`);
143
104
  console.log(`${passed}/${results.length} checks passed.`);
144
105
  if (passed < results.length) process.exit(1);
@@ -0,0 +1,54 @@
1
+ /**
2
+ * JSDoc type check helpers — jsconfig discovery and SPEC_IGNORE_DIRS merging.
3
+ * @module lib/spec-types
4
+ */
5
+
6
+ import { existsSync, readdirSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
7
+ import { resolve, dirname } from 'node:path';
8
+ import { tmpdir } from 'node:os';
9
+ import { runCmd } from './spec-utils.js';
10
+
11
+ /**
12
+ * Merge cfg.ignore dirs into a jsconfig's exclude list and write a temp file.
13
+ * Ensures SPEC_IGNORE_DIRS is the single source of truth for all checks.
14
+ * @param {string} configPath
15
+ * @param {string[]} ignore
16
+ * @returns {string} path to temp jsconfig
17
+ */
18
+ function mergedTypeConfig(configPath, ignore) {
19
+ const base = JSON.parse(readFileSync(configPath, 'utf-8'));
20
+ const root = dirname(configPath);
21
+ const existing = base.exclude ?? [];
22
+ const extra = ignore.map((d) => resolve(root, '..', d) + '/**');
23
+ const out = { ...base, exclude: [...new Set([...existing, ...extra])] };
24
+ const tmpDir = resolve(tmpdir(), 'clearstack-types');
25
+ mkdirSync(tmpDir, { recursive: true });
26
+ const tmpPath = resolve(tmpDir, configPath.replace(/[/\\:]/g, '_') + '.json');
27
+ writeFileSync(tmpPath, JSON.stringify(out));
28
+ return tmpPath;
29
+ }
30
+
31
+ /**
32
+ * Find all jsconfig.json files and return type check descriptors.
33
+ * @param {string} dir
34
+ * @param {string} runner
35
+ * @param {string[]} ignore
36
+ * @returns {object[]}
37
+ */
38
+ export function findTypeConfigs(dir, runner, ignore) {
39
+ const main = resolve(dir, '.configs/jsconfig.json');
40
+ const configs = [];
41
+ if (existsSync(main)) configs.push({ key: 'frontend', label: 'Frontend', path: main });
42
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
43
+ if (!entry.isDirectory() || entry.name.startsWith('.') || entry.name === 'node_modules') continue;
44
+ const p = resolve(dir, entry.name, 'jsconfig.json');
45
+ if (existsSync(p)) configs.push({ key: entry.name, label: entry.name, path: p });
46
+ }
47
+ return configs.map((c) => {
48
+ const merged = mergedTypeConfig(c.path, ignore);
49
+ return {
50
+ key: c.key, name: `JSDoc types — ${c.label}`, parent: 'types',
51
+ run: (o) => runCmd(`Types (${c.label})`, `${runner} tsc --project ${merged} --noEmit`, dir, undefined, o),
52
+ };
53
+ });
54
+ }
@@ -0,0 +1,97 @@
1
+ /**
2
+ * Watch runner — check execution, scheduling, and fix dispatch.
3
+ * @module lib/watch-runner
4
+ */
5
+
6
+ import { execSync } from 'node:child_process';
7
+ import { FAST_CHECKS } from './spec-config.js';
8
+ import { render, extractViolations } from './watch-ui.js';
9
+ import { setOnFix } from './watch-widgets.js';
10
+
11
+ /**
12
+ * @param {object[]} specRows
13
+ * @param {Set<string>} keys
14
+ * @param {{ rows: object[], lastCheck: { value: string }, watchDirs: string[], currentViolations: { value: string[][] }, projectDir: string }} ctx
15
+ */
16
+ export function runKeys(specRows, keys, ctx) {
17
+ const { rows, lastCheck, watchDirs, currentViolations, projectDir } = ctx;
18
+ const toRun = specRows.filter((r) => keys.has(r.key));
19
+ if (!toRun.length) return;
20
+ let i = 0;
21
+ /**
22
+ *
23
+ */
24
+ async function next() {
25
+ if (i >= toRun.length) {
26
+ const now = new Date();
27
+ lastCheck.value = `${now.getHours()}:${String(now.getMinutes()).padStart(2, '0')}:${String(now.getSeconds()).padStart(2, '0')}`;
28
+ render(rows, lastCheck.value, watchDirs, currentViolations.value);
29
+ return;
30
+ }
31
+ const row = toRun[i++];
32
+ row.pass = null;
33
+ row.detail = '';
34
+ render(rows, lastCheck.value, watchDirs, currentViolations.value);
35
+ const t0 = Date.now();
36
+ const raw = await row.run({ quiet: true });
37
+ const elapsed = ((Date.now() - t0) / 1000).toFixed(1) + 's';
38
+ row.result = typeof raw === 'boolean' ? { pass: raw } : raw;
39
+ row.pass = row.result.pass;
40
+ row.detail = row.pass
41
+ ? `${row.result.detail ?? (row.result.files ? `${row.result.files} files` : '')} (${elapsed})`
42
+ : (row.result.violations?.length
43
+ ? `${row.result.violations.length} violation(s) (${elapsed})`
44
+ : (() => { const errs = row.result.errors ?? []; const n = errs.filter((l) => /\.(js|ts|css|md)/.test(l)).length || errs.length; return `${n} error(s) (${elapsed})`; })());
45
+ currentViolations.value = specRows.flatMap((r) => extractViolations(r, projectDir));
46
+ render(rows, lastCheck.value, watchDirs, currentViolations.value);
47
+ setImmediate(next);
48
+ }
49
+ setImmediate(next);
50
+ }
51
+
52
+ /**
53
+ * @param {object[]} specRows
54
+ * @param {object} ctx
55
+ * @param {{ fast: Set<string>, slow: Set<string> }} pending
56
+ * @param {{ fast: ReturnType<typeof setTimeout>|null, slow: ReturnType<typeof setTimeout>|null }} timers
57
+ * @param {(keys: Set<string>) => void} run
58
+ */
59
+ export function schedule(specRows, ctx, pending, timers, run, checksForExt, ext) {
60
+ for (const k of checksForExt(ext)) {
61
+ const row = specRows.find((r) => r.key === k);
62
+ if (row) row.pass = null;
63
+ if (FAST_CHECKS.has(k)) pending.fast.add(k);
64
+ else pending.slow.add(k);
65
+ }
66
+ render(ctx.rows, ctx.lastCheck.value, ctx.watchDirs, ctx.currentViolations.value);
67
+ if (pending.fast.size) { clearTimeout(timers.fast); timers.fast = setTimeout(() => { const keys = new Set(pending.fast); pending.fast.clear(); run(keys); }, 50); }
68
+ if (pending.slow.size) { clearTimeout(timers.slow); timers.slow = setTimeout(() => { const keys = new Set(pending.slow); pending.slow.clear(); run(keys); }, 1500); }
69
+ }
70
+
71
+ /**
72
+ * @param {object[]} specRows
73
+ * @param {object} cmds
74
+ * @param {object} ctx
75
+ * @param {{ slow: Set<string> }} pending
76
+ * @param {{ slow: ReturnType<typeof setTimeout>|null }} timers
77
+ * @param {(keys: Set<string>) => void} run
78
+ */
79
+ export function setupFix(specRows, cmds, ctx, pending, timers, run) {
80
+ setOnFix(() => {
81
+ const fixable = specRows.filter((r) => !r.pass && (cmds.fix?.[r.key] || r.fix));
82
+ if (!fixable.length) return;
83
+ for (const row of fixable) {
84
+ row.pass = null;
85
+ pending.slow.add(row.key);
86
+ }
87
+ render(ctx.rows, ctx.lastCheck.value, ctx.watchDirs, ctx.currentViolations.value);
88
+ for (const row of fixable) {
89
+ try {
90
+ if (row.fix) row.fix();
91
+ else execSync(cmds.fix[row.key], { cwd: ctx.projectDir, stdio: 'pipe' });
92
+ } catch { /* fixer may exit 1 even after fixing */ }
93
+ }
94
+ clearTimeout(timers.slow);
95
+ timers.slow = setTimeout(() => { const keys = new Set(pending.slow); pending.slow.clear(); run(keys); }, 100);
96
+ });
97
+ }
package/lib/watch.js CHANGED
@@ -1,28 +1,25 @@
1
1
  /**
2
2
  * Spec watch dashboard — continuous compliance monitoring.
3
- * Runs affected checks on file change with 500ms debounce.
4
- * UI rendering lives in watch-ui.js.
3
+ * Execution logic lives in watch-runner.js, UI in watch-ui.js.
5
4
  * @module lib/watch
6
5
  */
7
6
 
8
7
  import { existsSync } from 'node:fs';
9
8
  import { resolve } from 'node:path';
10
- import { execSync } from 'node:child_process';
11
- import { loadConfig, buildCmds, makeExtMap, FAST_CHECKS } from './spec-config.js';
9
+ import { loadConfig, buildCmds, makeExtMap } from './spec-config.js';
12
10
  import { buildChecks } from './check.js';
13
- import { render, extractViolations, renderNote } from './watch-ui.js';
11
+ import { render, renderNote } from './watch-ui.js';
14
12
  import { spawnServer, detectServerCmd } from './server-proc.js';
15
13
  import { setupLifecycle } from './watch-lifecycle.js';
16
- import { setOnFix } from './watch-widgets.js';
14
+ import { runKeys, schedule, setupFix } from './watch-runner.js';
17
15
 
18
- /** Adapt a Check from buildChecks into a watch row (adds pass/detail/result state). */
19
- function toRow(check) {
20
- return { ...check, pass: null, detail: '', result: null };
21
- }
16
+ /**
17
+ *
18
+ */
19
+ function toRow(check) { return { ...check, pass: null, detail: '', result: null }; }
22
20
 
23
21
  /**
24
- * Start the spec watch dashboard.
25
- * @param {string} projectDir
22
+ *
26
23
  */
27
24
  export async function startWatch(projectDir) {
28
25
  const cfg = loadConfig(projectDir);
@@ -31,131 +28,41 @@ export async function startWatch(projectDir) {
31
28
  const specRows = checks.map(toRow);
32
29
  const checksForExt = makeExtMap(checks);
33
30
 
34
- const watchDirs = ['src', 'scripts', 'docs'].filter((d) =>
35
- existsSync(resolve(projectDir, d)),
36
- );
37
-
38
- let lastCheck = 'never';
39
- let fastTimer = null;
40
- let slowTimer = null;
41
- /** @type {Set<string>} */
42
- const fastPending = new Set();
43
- /** @type {Set<string>} */
44
- const slowPending = new Set();
31
+ const watchDirs = ['src', 'scripts', 'docs'].filter((d) => existsSync(resolve(projectDir, d)));
45
32
 
46
- // Render immediately — server spawns and checks run async behind it
47
33
  const serverCmd = detectServerCmd(projectDir, cfg);
48
-
49
- /** @type {import('./server-proc.js').ServerRow|null} */
50
- const serverRow = serverCmd ? { key: 'server', label: 'server', status: 'starting', pass: null, detail: 'starting…', kill: () => {} } : null;
34
+ const serverRow = serverCmd
35
+ ? { key: 'server', label: 'server', status: 'starting', pass: null, detail: 'starting…', kill: () => {} }
36
+ : null;
51
37
  const rows = serverRow ? [serverRow, ...specRows] : specRows;
52
38
 
53
- /**
54
- *
55
- */
56
- function spawnAndWatch() {
39
+ // Shared mutable state passed by reference into runner helpers
40
+ const lastCheck = { value: 'never' };
41
+ const currentViolations = { value: [] };
42
+ const pending = { fast: new Set(), slow: new Set() };
43
+ const timers = { fast: null, slow: null };
44
+ const ctx = { rows, lastCheck, watchDirs, currentViolations, projectDir };
45
+
46
+ const run = (keys) => runKeys(specRows, keys, ctx);
47
+
48
+ if (serverCmd) {
57
49
  const proc = spawnServer(serverCmd, projectDir, cfg.rawEnv, () => {
58
50
  serverRow.status = proc.status;
59
51
  serverRow.pass = proc.pass;
60
52
  serverRow.detail = proc.detail;
61
53
  serverRow.kill = proc.kill;
62
- render(rows, lastCheck, watchDirs, currentViolations);
54
+ render(rows, lastCheck.value, watchDirs, currentViolations.value);
63
55
  });
64
56
  }
65
- if (serverCmd) spawnAndWatch();
66
57
 
67
- /** @type {string[][]} */
68
- let currentViolations = [];
69
- render(rows, lastCheck, watchDirs, currentViolations);
58
+ render(rows, lastCheck.value, watchDirs, currentViolations.value);
59
+ run(new Set(specRows.map((r) => r.key)));
70
60
 
71
- /**
72
- *
73
- */
74
- function runKeys(keys) {
75
- const toRun = specRows.filter((r) => keys.has(r.key));
76
- if (!toRun.length) return;
77
- let i = 0;
78
- /**
79
- *
80
- */
81
- async function next() {
82
- if (i >= toRun.length) {
83
- const now = new Date();
84
- lastCheck = `${now.getHours()}:${String(now.getMinutes()).padStart(2, '0')}:${String(now.getSeconds()).padStart(2, '0')}`;
85
- render(rows, lastCheck, watchDirs, currentViolations);
86
- return;
87
- }
88
- const row = toRun[i++];
89
- row.pass = null;
90
- row.detail = '';
91
- render(rows, lastCheck, watchDirs, currentViolations);
92
- const t0 = Date.now();
93
- const raw = await row.run({ quiet: true });
94
- const elapsed = ((Date.now() - t0) / 1000).toFixed(1) + 's';
95
- row.result = typeof raw === 'boolean' ? { pass: raw } : raw;
96
- row.pass = row.result.pass;
97
- row.detail = row.pass
98
- ? `${row.result.detail ?? (row.result.files ? `${row.result.files} files` : '')} (${elapsed})`
99
- : (row.result.violations?.length
100
- ? `${row.result.violations.length} violation(s) (${elapsed})`
101
- : (() => { const errs = row.result.errors ?? []; const n = errs.filter((l) => /\.(js|ts|css|md)/.test(l)).length || errs.length; return `${n} error(s) (${elapsed})`; })());
102
- // Rebuild violations from all currently-failed rows
103
- currentViolations = specRows.flatMap((r) => extractViolations(r, projectDir));
104
- render(rows, lastCheck, watchDirs, currentViolations);
105
- setImmediate(next);
106
- }
107
- setImmediate(next);
108
- }
109
-
110
- /**
111
- *
112
- */
113
- function runFast() { const keys = new Set(fastPending); fastPending.clear(); runKeys(keys); }
114
- /**
115
- *
116
- */
117
- function runSlow() { const keys = new Set(slowPending); slowPending.clear(); runKeys(keys); }
118
-
119
- /**
120
- *
121
- */
122
- function schedule(ext) {
123
- for (const k of checksForExt(ext)) {
124
- const row = specRows.find((r) => r.key === k);
125
- if (row) row.pass = null; // show spinner while queued
126
- if (FAST_CHECKS.has(k)) fastPending.add(k);
127
- else slowPending.add(k);
128
- }
129
- render(rows, lastCheck, watchDirs, currentViolations);
130
- if (fastPending.size) { clearTimeout(fastTimer); fastTimer = setTimeout(runFast, 50); }
131
- if (slowPending.size) { clearTimeout(slowTimer); slowTimer = setTimeout(runSlow, 1500); }
132
- }
133
-
134
- runKeys(new Set(specRows.map((r) => r.key)));
135
-
136
- // f key — run fix commands for all currently-failing fixable checks
137
- // Checks can provide a fix via cmds.fix[key] (built-ins) or row.fix() (extensions)
138
- setOnFix(() => {
139
- const fixable = specRows.filter((r) => !r.pass && (cmds.fix?.[r.key] || r.fix));
140
- if (!fixable.length) return;
141
- for (const row of fixable) {
142
- row.pass = null; // show spinner immediately
143
- slowPending.add(row.key);
144
- }
145
- render(rows, lastCheck, watchDirs, currentViolations);
146
- for (const row of fixable) {
147
- try {
148
- if (row.fix) row.fix();
149
- else execSync(cmds.fix[row.key], { cwd: projectDir, stdio: 'pipe' });
150
- } catch { /* fixer may exit 1 even after fixing */ }
151
- }
152
- clearTimeout(slowTimer);
153
- slowTimer = setTimeout(runSlow, 100);
154
- });
61
+ setupFix(specRows, cmds, ctx, pending, timers, run);
155
62
 
156
63
  setupLifecycle({
157
64
  serverRow, specRows, projectDir, watchDirs,
158
- schedule,
159
- renderNote: () => renderNote(rows, lastCheck, watchDirs),
65
+ schedule: (ext) => schedule(specRows, ctx, pending, timers, run, checksForExt, ext),
66
+ renderNote: () => renderNote(rows, lastCheck.value, watchDirs),
160
67
  });
161
68
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@techninja/clearstack",
3
- "version": "0.4.7",
3
+ "version": "0.4.8",
4
4
  "type": "module",
5
5
  "description": "A no-build web component framework specification — scaffold, validate, and evolve spec-compliant projects",
6
6
  "bin": {