@chemx/create-chemx 26.10.7-1061 → 26.10.8-344

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/AGENTS.md CHANGED
@@ -5,6 +5,22 @@
5
5
 
6
6
  ---
7
7
 
8
+ ## ⚡ Quick Agent Reflex Table (Token-Bounded Commands)
9
+ | Standard Reflex | Chemical X (`cx`) Equivalent | Token & Architecture Advantage |
10
+ | :--- | :--- | :--- |
11
+ | `grep -rn "pattern" .` | `cx q -g "pattern"` or `cx q -g "pattern" -l` | Auto-ignores build/vendor; clamps lines to 60 chars or line-only (-l). |
12
+ | `git diff` | `cx d` | Zero-context (`-U0`), auto-collapses to `--stat` if > 80 lines, micro-syncs index. |
13
+ | `git log` | `cx log -n 5` | Single-line compact commit history (~8 tokens/commit). |
14
+ | `find . -name "*.vue"` | `cx f "*.vue"` | Strictly filters by `.gitignore` and ignored dirs. |
15
+ | `cat package.json` | `cx p -s` / `cx p <script>` | Instant single script or dep extraction (~3 tokens vs 300 lines). |
16
+ | `cat <data.json>` | `cx j <file.json>` | Structural schema shape only; collapses repeating arrays. |
17
+ | `cat <file>` / `head <file>` | `cx read <file> --outline` | AST signatures only (~50 tokens vs thousands). |
18
+ | `npm test` / `tsc` | `cx test` / `cx verify` | Silent on success; returns only failing diffs. |
19
+ | Multiple CLI actions | `cx do "<cmd1>" "<cmd2>"` | Executes sequentially in a single warm Node process. |
20
+ | Multiple MCP tool calls | `chemx({ commands: [...] })` | Executes multiple sub-operations in a single agent turn. |
21
+
22
+ ---
23
+
8
24
  ## 1. The Molecular Architecture Protocol
9
25
 
10
26
  ### A. Structural Weight, Cognitive Cohesion & Profile Architecture
@@ -38,12 +54,11 @@
38
54
  - Encapsulate shared feature state in dedicated domain composables/hooks, scoped stores, or typed provide/inject.
39
55
  - Child components consume state directly from the domain composable and emit minimal, intention-revealing semantic events.
40
56
 
41
- ### E. Same-Name Prop Shorthand (Vue 3.4+ & Svelte 5)
42
- - **Vue 3.4+**: Always use same-name `:prop` shorthand instead of redundant `:prop="prop"`:
43
- `<m-spark-kpi-strip :metrics :records :can-refresh />`
44
- - **Svelte 5**: Always use same-name `{prop}` shorthand instead of `prop={prop}`:
45
- `<SparkKpiStrip {metrics} {records} {canRefresh} />`
46
- - **React 19 (JSX)**: Explicitly bind `prop={prop}`. Never write `<Comp prop />` for variables (JSX evaluates bare attributes to boolean `true`).
57
+ ### E. Idiomatic Shorthand Binding
58
+ Use same-name shorthand across all languages; eliminate redundant `key: key` duplication:
59
+ - **JS / TS / Rust Data Structures**: `{ foo, bar }` over `{ foo: foo, bar: bar }`.
60
+ - **Vue 3.4+ & Svelte 5 Templates**: `<Comp :prop />` or `<Comp {prop} />` over `:prop="prop"`.
61
+ - **React / JSX Exception**: Explicit `prop={prop}` (bare `<Comp prop />` coerces to boolean `true`).
47
62
 
48
63
  ### F. Pre-Split Pattern Discovery & Harmonization Protocol
49
64
  - **Survey Before Slicing**: Before decomposing any monolithic file or collection of files, the AI must first conduct a cross-file pattern discovery audit across all candidate monoliths.
@@ -62,7 +77,7 @@
62
77
  - When repeated elements accumulate behavioral logic, accessibility handling, or styling variants across 3+ places (Rule of Three), extract into a canonical capsule (`m-tab-button`).
63
78
 
64
79
  ### H. AI Agent Codebase Query Machine Protocol
65
- - **Search First Rule**: AI agents MUST invoke `pnpm chemx q "<query>"` (or `npx chemx search "<query>"`) before running broad ripgrep, find, or file dumping.
80
+ - **AST & Literal Search First Rule**: AI agents should invoke `cx q "<query>"` for AST symbols or `cx q -g "<pattern>"` for literal text before running broad unthrottled grep. If AST search returns 0 results, check the suggested `cx q -g` fallback before escalating to raw ripgrep.
66
81
  - **AST Architecture Intelligence**: Always leverage `pnpm chemx q` to inspect component tiers, exported symbols, props, and hooks with minimal token burn.
67
82
  - **Mandatory Blast Radius Pre-Refactor Check**: Prior to modifying any foundational atom (`a-*`), shared molecule, or central composable (`use*`), agents MUST calculate the transitive blast radius (`pnpm chemx q <target> --blast-radius --json` or `chemx({ action: 'q', params: { query: '<target>', blastRadius: true } })`). Never perform blind refactors without mapping direct consumers, transitive dependents, and impacted tiers.
68
83
  - **Hybrid RRF Discovery Protocol**: When discovering components, controllers, or state machines without an exact symbol name, agents MUST use hybrid search (`pnpm chemx q "<concept>" --hybrid --json` or `chemx({ action: 'q', params: { query: '<concept>', hybrid: true } })`). This blends BM25 keyword matching and vector cosine similarity via Reciprocal Rank Fusion (RRF), eliminating keyword misses and semantic hallucinations.
@@ -105,6 +120,14 @@
105
120
  - **Command Forwarding Syntax**: Agents may invoke either structured action objects (`chemx({ action: 'test' })`, `chemx({ action: 'build' })`, `chemx({ action: 'audit', params: { path: 'src' } })`) or CLI command strings (`chemx({ command: 'test' })`, `chemx({ command: 'build' })`, `chemx({ command: 'audit src' })`).
106
121
  - **Master Action & Parameter Dispatch Matrix**:
107
122
  ```typescript
123
+ // Fast Token-Bounded Wrappers & Multi-Action Batching
124
+ chemx({ commands: ['d', 'p -s', 'test'] }); // Multi-command batch in single turn
125
+ chemx({ action: 'd' }); // Zero-context diff (-U0), auto-stat if > 80 lines
126
+ chemx({ action: 'log', params: { limit: 5 } }); // Compact single-line commit history
127
+ chemx({ action: 'p', params: { command: '-s' } }); // Read scripts from package.json
128
+ chemx({ action: 'j', params: { path: 'data.json' } }); // Structural JSON schema shape
129
+ chemx({ action: 'q', params: { query: 'theme', literal: true } }); // Literal ripgrep (-g)
130
+
108
131
  // Discovery & Impact Analysis
109
132
  chemx({ action: 'q', params: { query: 'a-button', blastRadius: true } });
110
133
  chemx({ action: 'q', params: { query: 'button state', semantic: true } });
@@ -146,7 +169,7 @@
146
169
  - **Read Window Auto-Outline**: Files longer than the chemx read window (100 lines by default), read without a target symbol or slice, automatically render an AST outline to prevent token exhaustion and host buffer spillovers (such as IDE `output.txt` dumps). The read window is a tool budget, not an architecture rule; file size is governed by 1.A.
147
170
  - **Enrich Mode (Component Capsules Only)**: When an agent needs both exported signatures AND logic flow for a component capsule, use `enrich: true` with `outline: true`: one response card holds the outline and a compacted logic skeleton, optionally with a forward trace or reverse caller chain (`traceSymbol`, `backtraceSymbol`). The skeleton generator is tuned for components; on procedural modules (services, CLI code, utilities) enrich saves only a few percent over reading the whole file, so use plain `outline: true` or `symbol: '<name>'` there.
148
171
  - **Symbol Connection Graph Over Multi-File Dumps**: Rather than reading multiple files to understand imports and consumers, agents MUST request symbol connections: `chemx({ action: 'read', params: { path, symbol: '<name>', connections: true } })` or `chemx({ action: 'q', params: { query, connections: true } })`. This instantly returns the definition, imported dependencies, and caller references in ~45 tokens.
149
- - **Prohibition on Native File Analyzers (`view_file` Ban)**: AI agents are strictly prohibited from using native IDE file-viewing tools (`view_file`, `read_file`, `cat`, `head`, `tail`, or full-file context dumps). Dumping raw files burns thousands of tokens, causes premature context exhaustion, and defeats the token economics of the molecular architecture. All inspections MUST flow through Chemical X AST readers.
172
+ - **Token-First File Inspection & Native Fallback**: AI agents should prioritize Chemical X AST readers (`cx read --outline` or `cx read --symbol`) to conserve tokens. Dumping raw files burns thousands of tokens and causes premature context exhaustion. If an agent requires exact raw verification or unformatted bytes, native tools (`view_file`, `grep`, `cat`) are permitted as an intentional fallback.
150
173
 
151
174
  ### O. Universal Programmatic File Jig & Closed-Loop Execution Protocol
152
175
  - **Deterministic Parameterized Synthesis**: When creating non-UI files (services, API route handlers, state stores, database repositories, utilities, or test specs), AI agents MUST NOT emit hundreds of lines of mechanical boilerplate via raw file writing tools. Agents MUST invoke `chemx generate --jig=<kind>` or `chemx({ action: 'generate', params: { jig: true, kind, name, ... } })`.
@@ -275,11 +298,7 @@
275
298
  - When branching performs uniform operations (e.g. mapping string keys to action handlers, CSS classes, prompt builders, or payload converters), extract into an O(1) keyed dictionary or method map (`const Registry = { ... }; Registry[key](...)`).
276
299
  - **Branch Chains**: An `if` / `else if` chain of 3 or more branches testing the same subject is a lookup, not a series of decisions. Use a keyed map; do not hoist a named boolean per branch (3.A does not apply to such chains).
277
300
  - **Prototype Pollution Guardrail**: Always guard dynamic object key access using `Object.hasOwn(Registry, key)` or `Object.prototype.hasOwnProperty.call(Registry, key)` before invoking mapped methods.
278
- - **Architectural Benefits**:
279
- - O(1) constant time lookup instead of O(N) linear string comparison branching.
280
- - Eliminates boilerplate syntax duplication, drastically slashing token burn and AI context footprint.
281
- - Complies with the Open-Closed Principle (OCP): new handlers can be registered dynamically without modifying the dispatcher AST.
282
- - Enables granular unit testing and mocking of individual handlers in isolation.
301
+ - **Architectural Benefits**: O(1) lookup over O(N) string branching, eliminates boilerplate syntax duplication, complies with OCP, and isolates unit testing.
283
302
 
284
303
  ### F. Name Quality, Assertion Prefixes & Adjacency
285
304
  - **Assertion Prefixes**: A condition name starts with `is`, `are`, `has`, `have`, `can`, `could`, `should`, `would`, `does`, `did`, `needs`, `must`, `allows`, `enables`, `contains`, `includes`, `supports`, `requires`, `exceeds`, `matches`, or `wants`, followed by its subject (`isEscapeChar`, `exceedsHookBudget`). A bare verb callee (`test`, `existsSync`, `includes`, `startsWith`) is not a name: bind its result to one first.
@@ -287,6 +306,11 @@
287
306
  - **Adjacency**: Declare a single-use named boolean on the statement immediately before its `if`. Hoist it only when it is referenced 2 or more times in the enclosing function, and then only to a point before its first use. A wall of `const is*` declarations at function top is a readability regression.
288
307
  - **No Double Negation**: `if (!isNotFound)` names the wrong side. Name the positive and negate at the point of use.
289
308
 
309
+ ### G. Explicit Guardrail Aborts (No Silent Failures)
310
+ - Guard clauses in event handlers, mutations, and async flows must never fail silently via bare `return;`.
311
+ - Halting execution requires an explicit contract: return a `ResultTuple` (`return [null, error]`), emit diagnostic logging (`logger.warn(...)`), update a user-facing error state (`state.error = '...'`), or throw a domain invariant.
312
+ - **Exemptions**: Benign lifecycle no-ops (`abortSignal.aborted`, `!isMounted`), optional prop callbacks (`!props.onClick`), debounce/throttle timers, and pure query predicates.
313
+
290
314
  ---
291
315
 
292
316
  ## 4. Reactivity, Composables & Hooks
@@ -469,15 +493,3 @@ Raw inline `style="..."` attributes are strictly prohibited. Visual styling flow
469
493
  ### C. "No Fake Doc-Sync" Anti-Pattern
470
494
  - Mirroring the "No Fake Green" testing rule in Section 10.D, superficial doc edits (whitespace tweaks, comment formatting, minor typo fixes) made merely to pass CI touch-checks without addressing substantive semantic changes are strictly prohibited.
471
495
  - Documentation reviews must verify substantive alignment between code behavior and documented contracts.
472
-
473
-
474
- ## Chemical X Codebase Query Machine Protocol
475
- - Search First Rule: AI agents MUST invoke 'pnpm chemx q "<query>"' (or 'npx chemx search "<query>"') before running broad ripgrep, find, or file dumping.
476
- - AST Architecture Intelligence: Always leverage 'pnpm chemx q' to inspect component tiers, exported symbols, props, and hooks with minimal token burn.
477
- - Inspect Mode: Use 'pnpm chemx q "<capsule-name>" --inspect' to examine props and hooks without reading entire source files.
478
- - JSON Mode: Use 'pnpm chemx q "<query>" --json' for zero-overhead, machine-readable agent lookups.
479
-
480
- ## Chemical X Verification-First Protocol & Zero-Token-Burn Pipeline
481
- - Verification First Rule: AI agents MUST NEVER run raw, unthrottled "npm test", "pnpm test", "vitest", "tsc --noEmit", or "npm run build" directly in a bash subshell.
482
- - MCP Verification Tools First: AI agents MUST invoke dedicated Chemical X MCP tools or CLI wrappers ('pnpm chemx verify', 'npx chemx verify', 'pnpm chemx build', 'npx chemx build') before running terminal commands.
483
-
@@ -5,7 +5,12 @@ import {
5
5
  isCustomHookFunction,
6
6
  resolveStartLine,
7
7
  isZeroDelayTimeout,
8
- isUnguardedConsoleCall
8
+ isUnguardedConsoleCall,
9
+ isSilentGuardClause,
10
+ isSwallowedCatch,
11
+ countOptionalChainingDepth,
12
+ isCombinatorCall,
13
+ isRawBooleanArg
9
14
  } from './rules-predicates.js';
10
15
  import { validateHookReturnShape } from './hook-shape-validator.js';
11
16
  import { evaluateComponentStructuralWeight } from './structural-weight-evaluator.js';
@@ -117,6 +122,22 @@ export const createAstVisitors = ({ relativePath, violations, hookRegistry, conf
117
122
  });
118
123
  }
119
124
  }
125
+
126
+ const silentGuard = isSilentGuardClause(astPath, t);
127
+ if (silentGuard) {
128
+ const line = astPath.node.loc?.start.line || 1;
129
+ const meta = RULE_REGISTRY.CONTROL_FLOW_SILENT_GUARD;
130
+ violations.push({
131
+ filePath: relativePath,
132
+ line,
133
+ column: astPath.node.loc?.start.column || 1,
134
+ hazard: `Silent guard abort detected in ${silentGuard.funcName}: bare return; swallows error condition without logging or error state. Return [null, error] or emit diagnostic per Directive 3.G.`,
135
+ rule: 'CONTROL_FLOW_SILENT_GUARD',
136
+ severity: meta.severity,
137
+ pillar: meta.pillar,
138
+ directive: meta.directive
139
+ });
140
+ }
120
141
  },
121
142
 
122
143
  ConditionalExpression(astPath) {
@@ -299,6 +320,100 @@ export const createAstVisitors = ({ relativePath, violations, hookRegistry, conf
299
320
  directive: meta.directive
300
321
  });
301
322
  }
323
+
324
+ // Pillar 6: Orphaned Event Listener
325
+ if (t.isMemberExpression(callee) && t.isIdentifier(callee.property, { name: 'addEventListener' })) {
326
+ const fnParent = astPath.getFunctionParent();
327
+ let hasCleanup = false;
328
+ if (fnParent) {
329
+ const fnName = fnParent.node.id?.name || '';
330
+ if (fnName === 'listen') {
331
+ hasCleanup = true;
332
+ } else {
333
+ fnParent.traverse({
334
+ CallExpression(innerCall) {
335
+ const innerCallee = innerCall.node.callee;
336
+ if (t.isMemberExpression(innerCallee) && t.isIdentifier(innerCallee.property, { name: 'removeEventListener' })) {
337
+ hasCleanup = true;
338
+ }
339
+ }
340
+ });
341
+ }
342
+ }
343
+ if (!hasCleanup) {
344
+ const line = astPath.node.loc?.start.line || 1;
345
+ const meta = RULE_REGISTRY.LIFECYCLE_ORPHANED_LISTENER;
346
+ violations.push({
347
+ filePath: relativePath,
348
+ line,
349
+ column: astPath.node.loc?.start.column || 1,
350
+ hazard: 'Raw addEventListener call lacking lifecycle disposer or removeEventListener teardown',
351
+ rule: 'LIFECYCLE_ORPHANED_LISTENER',
352
+ severity: meta.severity,
353
+ pillar: meta.pillar,
354
+ directive: meta.directive
355
+ });
356
+ }
357
+ }
358
+
359
+ // Pillar 2: Combinator Raw Boolean Arguments
360
+ if (isCombinatorCall(callee, t)) {
361
+ const hasRawArg = astPath.node.arguments.some((arg) => isRawBooleanArg(arg, t));
362
+ if (hasRawArg) {
363
+ const line = astPath.node.loc?.start.line || 1;
364
+ const meta = RULE_REGISTRY.COMBINATOR_RAW_BOOLEAN;
365
+ violations.push({
366
+ filePath: relativePath,
367
+ line,
368
+ column: astPath.node.loc?.start.column || 1,
369
+ hazard: `Raw boolean expression passed to ${callee.name}(). Decompose into named predicate or thunk.`,
370
+ rule: 'COMBINATOR_RAW_BOOLEAN',
371
+ severity: meta.severity,
372
+ pillar: meta.pillar,
373
+ directive: meta.directive
374
+ });
375
+ }
376
+ }
377
+ },
378
+
379
+ // Pillar 2: Swallowed Exceptions
380
+ CatchClause(astPath) {
381
+ if (isSwallowedCatch(astPath, t)) {
382
+ const line = astPath.node.loc?.start.line || 1;
383
+ const meta = RULE_REGISTRY.ERROR_SWALLOWED_EXCEPTION;
384
+ violations.push({
385
+ filePath: relativePath,
386
+ line,
387
+ column: astPath.node.loc?.start.column || 1,
388
+ hazard: 'Swallowed exception in catch block without active handling, logging, or ResultTuple',
389
+ rule: 'ERROR_SWALLOWED_EXCEPTION',
390
+ severity: meta.severity,
391
+ pillar: meta.pillar,
392
+ directive: meta.directive
393
+ });
394
+ }
395
+ },
396
+
397
+ // Pillar 4: Deep Optional Chaining Churn
398
+ OptionalMemberExpression(astPath) {
399
+ const isParentOptional = t.isOptionalMemberExpression(astPath.parent) || t.isOptionalCallExpression(astPath.parent);
400
+ if (!isParentOptional) {
401
+ const depth = countOptionalChainingDepth(astPath.node, t);
402
+ if (depth >= 3) {
403
+ const line = astPath.node.loc?.start.line || 1;
404
+ const meta = RULE_REGISTRY.DATA_FLOW_OPTIONAL_CHAINING_CHURN;
405
+ violations.push({
406
+ filePath: relativePath,
407
+ line,
408
+ column: astPath.node.loc?.start.column || 1,
409
+ hazard: `Deep optional chaining churn (${depth} chained operators >= 3 limit). Level data shapes line 1.`,
410
+ rule: 'DATA_FLOW_OPTIONAL_CHAINING_CHURN',
411
+ severity: meta.severity,
412
+ pillar: meta.pillar,
413
+ directive: meta.directive
414
+ });
415
+ }
416
+ }
302
417
  },
303
418
 
304
419
  // Pillar 4: Type Co-location
@@ -11,10 +11,43 @@ const GREEN = '\x1b[32m';
11
11
  const YELLOW = '\x1b[33m';
12
12
  const RED = '\x1b[31m';
13
13
 
14
+ export const findChemxDir = (startDir = process.cwd()) => {
15
+ const hasCustomRoot = Boolean(process.env.CHEMX_PROJECT_ROOT);
16
+ if (hasCustomRoot) {
17
+ return path.resolve(process.env.CHEMX_PROJECT_ROOT, '.chemx');
18
+ }
19
+
20
+ let current = path.resolve(startDir);
21
+ let gitRoot = null;
22
+ while (true) {
23
+ const candidate = path.join(current, '.chemx');
24
+ const hasExistingChemx = fs.existsSync(candidate);
25
+ if (hasExistingChemx) {
26
+ return candidate;
27
+ }
28
+ const hasGit = !gitRoot && fs.existsSync(path.join(current, '.git'));
29
+ if (hasGit) {
30
+ gitRoot = current;
31
+ }
32
+ const parent = path.dirname(current);
33
+ const isRootReached = parent === current;
34
+ if (isRootReached) break;
35
+ current = parent;
36
+ }
37
+
38
+ const hasDiscoveredGitRoot = Boolean(gitRoot);
39
+ if (hasDiscoveredGitRoot) {
40
+ return path.join(gitRoot, '.chemx');
41
+ }
42
+
43
+ return path.resolve(startDir, '.chemx');
44
+ };
45
+
14
46
  export const ensureChemxDir = (cwd = process.cwd()) => {
15
- const dir = path.resolve(cwd, '.chemx');
47
+ const dir = findChemxDir(cwd);
16
48
  try {
17
- if (!fs.existsSync(dir)) {
49
+ const isDirMissing = !fs.existsSync(dir);
50
+ if (isDirMissing) {
18
51
  fs.mkdirSync(dir, { recursive: true });
19
52
  }
20
53
  } catch {
@@ -22,17 +55,21 @@ export const ensureChemxDir = (cwd = process.cwd()) => {
22
55
  }
23
56
 
24
57
  // Ensure .chemx is gitignored if git repo exists
25
- const gitDir = path.resolve(cwd, '.git');
26
- if (fs.existsSync(gitDir)) {
27
- const gitignorePath = path.resolve(cwd, '.gitignore');
58
+ const targetParent = path.dirname(dir);
59
+ const gitDir = path.resolve(targetParent, '.git');
60
+ const hasGit = fs.existsSync(gitDir);
61
+ if (hasGit) {
62
+ const gitignorePath = path.resolve(targetParent, '.gitignore');
28
63
  try {
29
- let content = fs.existsSync(gitignorePath) ? fs.readFileSync(gitignorePath, 'utf-8') : '';
30
- if (!content.includes('.chemx')) {
31
- const trailingNewline = content.endsWith('\n') || content.length === 0 ? '' : '\n';
64
+ const hasGitignore = fs.existsSync(gitignorePath);
65
+ let content = hasGitignore ? fs.readFileSync(gitignorePath, 'utf-8') : '';
66
+ const hasChemxEntry = content.includes('.chemx');
67
+ if (!hasChemxEntry) {
68
+ const hasTrailingNewline = content.endsWith('\n') || content.length === 0;
69
+ const trailingNewline = hasTrailingNewline ? '' : '\n';
32
70
  fs.appendFileSync(gitignorePath, `${trailingNewline}# Chemical X local telemetry & audit history\n.chemx/\n`, 'utf-8');
33
71
  }
34
- } catch (err) {
35
- const error = err instanceof Error ? err : new Error(String(err));
72
+ } catch {
36
73
  return dir;
37
74
  }
38
75
  }
@@ -138,3 +138,203 @@ export const isNonInteractiveSession = (rawArgs, env = process.env) => {
138
138
  const isNotTTY = Boolean(process.stdout && process.stdout.isTTY === false);
139
139
  return [hasCliFlag, hasOutputFlag, hasFormatFlag, hasCiEnv, isNotTTY].some(Boolean);
140
140
  };
141
+
142
+ const EXEMPT_TEST_PATTERNS = [
143
+ /abort/i,
144
+ /signal/i,
145
+ /mounted/i,
146
+ /unmount/i,
147
+ /destroy/i,
148
+ /cleanup/i,
149
+ /timer/i,
150
+ /timeout/i,
151
+ /interval/i,
152
+ /debounce/i,
153
+ /throttle/i,
154
+ /\bon[A-Z]/,
155
+ /\bcall(?:back|backFn)?\b/i,
156
+ /\bcb\b/i,
157
+ /\bhandler\b/i
158
+ ];
159
+
160
+ const MUTATING_FUNCTION_PATTERNS = [
161
+ /^handle[A-Z]/,
162
+ /^on[A-Z]/,
163
+ /^(?:save|update|delete|remove|create|add|insert|post|send|process|submit|dispatch|execute|mutate|checkout|charge|sync|write)/i
164
+ ];
165
+
166
+ const PURE_PREDICATE_PATTERNS = [
167
+ /^(?:is|has|can|should|check)[A-Z]/
168
+ ];
169
+
170
+ const isDiagnosticOrErrorStatement = (stmt, t) => {
171
+ if (t.isThrowStatement(stmt)) return true;
172
+ if (t.isExpressionStatement(stmt)) {
173
+ const expr = stmt.expression;
174
+ if (t.isCallExpression(expr)) {
175
+ const callee = expr.callee;
176
+ if (t.isMemberExpression(callee)) {
177
+ const objName = t.isIdentifier(callee.object) ? callee.object.name : '';
178
+ const propName = t.isIdentifier(callee.property) ? callee.property.name : '';
179
+ if (['console', 'logger', 'log'].includes(objName)) return true;
180
+ if (/^(?:error|warn|info|trace|notify|alert)/i.test(propName)) return true;
181
+ }
182
+ if (t.isIdentifier(callee) && /^(?:report|notify|alert|set.*Error|handleError)/i.test(callee.name)) return true;
183
+ }
184
+ if (t.isAssignmentExpression(expr)) {
185
+ const left = expr.left;
186
+ if (t.isMemberExpression(left) && t.isIdentifier(left.property) && /error/i.test(left.property.name)) return true;
187
+ if (t.isIdentifier(left) && /error/i.test(left.name)) return true;
188
+ }
189
+ }
190
+ return false;
191
+ };
192
+
193
+ export const isSilentGuardClause = (ifPath, t) => {
194
+ const node = ifPath.node;
195
+ if (!node || node.alternate) return false;
196
+
197
+ let hasBareReturn = false;
198
+ let hasDiagnosticOrHandling = false;
199
+
200
+ const checkStatement = (stmt) => {
201
+ if (t.isReturnStatement(stmt)) {
202
+ const isBare = !stmt.argument || (t.isIdentifier(stmt.argument) && stmt.argument.name === 'undefined');
203
+ if (isBare) hasBareReturn = true;
204
+ } else if (isDiagnosticOrErrorStatement(stmt, t)) {
205
+ hasDiagnosticOrHandling = true;
206
+ }
207
+ };
208
+
209
+ if (t.isReturnStatement(node.consequent)) {
210
+ checkStatement(node.consequent);
211
+ } else if (t.isBlockStatement(node.consequent)) {
212
+ for (const stmt of node.consequent.body) {
213
+ checkStatement(stmt);
214
+ }
215
+ }
216
+
217
+ if (!hasBareReturn || hasDiagnosticOrHandling) return false;
218
+
219
+ const collectTestIdentifiers = (n) => {
220
+ const names = [];
221
+ const walk = (item) => {
222
+ if (!item) return;
223
+ if (t.isIdentifier(item)) {
224
+ names.push(item.name);
225
+ } else if (t.isMemberExpression(item)) {
226
+ walk(item.object);
227
+ walk(item.property);
228
+ } else if (t.isUnaryExpression(item)) {
229
+ walk(item.argument);
230
+ } else if (t.isLogicalExpression(item) || t.isBinaryExpression(item)) {
231
+ walk(item.left);
232
+ walk(item.right);
233
+ } else if (t.isCallExpression(item)) {
234
+ walk(item.callee);
235
+ for (const arg of item.arguments) walk(arg);
236
+ }
237
+ };
238
+ walk(n);
239
+ return names;
240
+ };
241
+
242
+ const testIdentifiers = collectTestIdentifiers(node.test);
243
+ const isExemptTest = testIdentifiers.some((name) => EXEMPT_TEST_PATTERNS.some((pat) => pat.test(name)));
244
+ if (isExemptTest) return false;
245
+
246
+ const funcParent = ifPath.getFunctionParent();
247
+ if (!funcParent) return false;
248
+
249
+ let funcName = '';
250
+ if (funcParent.node.id?.name) {
251
+ funcName = funcParent.node.id.name;
252
+ } else if (funcParent.parentPath?.isVariableDeclarator() && t.isIdentifier(funcParent.parentPath.node.id)) {
253
+ funcName = funcParent.parentPath.node.id.name;
254
+ } else if (t.isIdentifier(funcParent.node.key)) {
255
+ funcName = funcParent.node.key.name;
256
+ }
257
+
258
+ const isAsync = Boolean(funcParent.node.async);
259
+ const isMutatingName = MUTATING_FUNCTION_PATTERNS.some((pat) => pat.test(funcName));
260
+ const isPurePredicate = PURE_PREDICATE_PATTERNS.some((pat) => pat.test(funcName));
261
+
262
+ if (isPurePredicate && !isAsync && !isMutatingName) return false;
263
+
264
+ const isTargetFunction = isAsync || isMutatingName;
265
+ if (!isTargetFunction) return false;
266
+
267
+ return { funcName: funcName || (isAsync ? 'async operation' : 'command handler') };
268
+ };
269
+
270
+ export const isSwallowedCatch = (astPath, t) => {
271
+ const body = astPath.node.body?.body || [];
272
+ if (body.length === 0) return true;
273
+
274
+ const nonNoopStatements = body.filter((stmt) => !t.isEmptyStatement(stmt));
275
+ if (nonNoopStatements.length === 0) return true;
276
+
277
+ let hasActiveHandling = false;
278
+ astPath.traverse({
279
+ ThrowStatement() {
280
+ hasActiveHandling = true;
281
+ },
282
+ ReturnStatement(retPath) {
283
+ if (retPath.node.argument !== null) {
284
+ hasActiveHandling = true;
285
+ }
286
+ },
287
+ CallExpression(callPath) {
288
+ const callee = callPath.node.callee;
289
+ if (t.isMemberExpression(callee)) {
290
+ const objName = t.isIdentifier(callee.object) ? callee.object.name : '';
291
+ const propName = t.isIdentifier(callee.property) ? callee.property.name : '';
292
+ const isLogger = ['console', 'logger', 'log', 'telemetry', 'reportError'].includes(objName);
293
+ const isHandlingMethod = ['error', 'warn', 'info', 'captureException', 'track'].includes(propName);
294
+ const isProcessStderr = t.isMemberExpression(callee.object) &&
295
+ t.isIdentifier(callee.object.object, { name: 'process' }) &&
296
+ t.isIdentifier(callee.object.property, { name: 'stderr' });
297
+ if (isLogger || isHandlingMethod || isProcessStderr) {
298
+ hasActiveHandling = true;
299
+ }
300
+ } else if (t.isIdentifier(callee)) {
301
+ const isHandler = ['handleError', 'reportError', 'captureException', 'toResult'].includes(callee.name);
302
+ if (isHandler) {
303
+ hasActiveHandling = true;
304
+ }
305
+ }
306
+ }
307
+ });
308
+
309
+ return !hasActiveHandling;
310
+ };
311
+
312
+ export const countOptionalChainingDepth = (node, t) => {
313
+ let count = 0;
314
+ let curr = node;
315
+ while (curr && (t.isOptionalMemberExpression(curr) || t.isOptionalCallExpression(curr) || t.isMemberExpression(curr))) {
316
+ if (curr.optional) {
317
+ count++;
318
+ }
319
+ curr = curr.object || curr.callee;
320
+ }
321
+ return count;
322
+ };
323
+
324
+ const COMBINATOR_NAMES = new Set(['all', 'any', 'none', 'allPass', 'anyPass', 'nonePass']);
325
+
326
+ export const isCombinatorCall = (callee, t) => {
327
+ if (t.isIdentifier(callee)) {
328
+ return COMBINATOR_NAMES.has(callee.name);
329
+ }
330
+ return false;
331
+ };
332
+
333
+ export const isRawBooleanArg = (arg, t) => {
334
+ if (t.isBinaryExpression(arg)) return true;
335
+ if (t.isLogicalExpression(arg)) return true;
336
+ if (t.isBooleanLiteral(arg)) return true;
337
+ if (t.isUnaryExpression(arg, { operator: '!' })) return true;
338
+ return false;
339
+ };
340
+
@@ -111,6 +111,21 @@ export const RULE_REGISTRY = {
111
111
  severity: 'MEDIUM',
112
112
  directive: 'Replace repetitive dispatch switch with an O(1) keyed dictionary or method map (Directive 3.E)'
113
113
  },
114
+ CONTROL_FLOW_SILENT_GUARD: {
115
+ pillar: PILLARS.PILLAR_2,
116
+ severity: 'MEDIUM',
117
+ directive: 'Replace silent bare return in side-effecting logic with explicit ResultTuple, logger diagnostic, or error state (Directive 3.G)'
118
+ },
119
+ ERROR_SWALLOWED_EXCEPTION: {
120
+ pillar: PILLARS.PILLAR_2,
121
+ severity: 'CRITICAL',
122
+ directive: 'Do not swallow caught exceptions silently; log with error context or return explicit ResultTuple error (Directive 3.B)'
123
+ },
124
+ COMBINATOR_RAW_BOOLEAN: {
125
+ pillar: PILLARS.PILLAR_2,
126
+ severity: 'MEDIUM',
127
+ directive: 'Do not pass inline boolean expressions or raw booleans into logical combinators; use named predicates or thunks (Directive 3.A)'
128
+ },
114
129
  HOOK_SATURATION: {
115
130
  pillar: PILLARS.PILLAR_3,
116
131
  severity: 'HIGH',
@@ -141,6 +156,11 @@ export const RULE_REGISTRY = {
141
156
  severity: 'CRITICAL',
142
157
  directive: 'Replace render-hack setTimeout(0) with await nextTick() or RAF'
143
158
  },
159
+ LIFECYCLE_ORPHANED_LISTENER: {
160
+ pillar: PILLARS.PILLAR_6,
161
+ severity: 'HIGH',
162
+ directive: 'Wrap event listeners in self-cleaning disposers or register pairing removeEventListener (Directive 3.F)'
163
+ },
144
164
  TYPE_COLOCATION: {
145
165
  pillar: PILLARS.PILLAR_4,
146
166
  severity: 'MEDIUM',
@@ -156,6 +176,11 @@ export const RULE_REGISTRY = {
156
176
  severity: 'MEDIUM',
157
177
  directive: 'Use live API data or explicit empty states instead of mock placeholders'
158
178
  },
179
+ DATA_FLOW_OPTIONAL_CHAINING_CHURN: {
180
+ pillar: PILLARS.PILLAR_4,
181
+ severity: 'LOW',
182
+ directive: 'Level incoming data shapes line 1 using sentinels or normalizeArray instead of deep optional chaining churn (Directive 4.D)'
183
+ },
159
184
  RAW_INLINE_STYLE: {
160
185
  pillar: PILLARS.PILLAR_5,
161
186
  severity: 'MEDIUM',
@@ -75,6 +75,41 @@ export const dispatchCommand = async (firstArg, rawArgs, runAudit, getPackageVer
75
75
  await runSearch(rawArgs.slice(1), true);
76
76
  break;
77
77
  }
78
+ case 'd':
79
+ case 'diff': {
80
+ const { runDiff } = await import('./cmd-wrappers.js');
81
+ await runDiff(rawArgs, true);
82
+ break;
83
+ }
84
+ case 'log': {
85
+ const { runLog } = await import('./cmd-wrappers.js');
86
+ await runLog(rawArgs, true);
87
+ break;
88
+ }
89
+ case 'p':
90
+ case 'pkg': {
91
+ const { runPkg } = await import('./cmd-wrappers.js');
92
+ await runPkg(rawArgs, true);
93
+ break;
94
+ }
95
+ case 'f':
96
+ case 'ls': {
97
+ const { runFiles } = await import('./cmd-wrappers.js');
98
+ await runFiles(rawArgs, true);
99
+ break;
100
+ }
101
+ case 'j':
102
+ case 'json': {
103
+ const { runJsonShape } = await import('./cmd-wrappers.js');
104
+ await runJsonShape(rawArgs, true);
105
+ break;
106
+ }
107
+ case 'do':
108
+ case 'batch': {
109
+ const { runBatch } = await import('./cmd-wrappers.js');
110
+ await runBatch(rawArgs, true, (cmd, args) => dispatchCommand(cmd, args, runAudit, getPackageVersion, isCapsulePrefix));
111
+ break;
112
+ }
78
113
  case 'trace': {
79
114
  const { runTraceCli } = await import('../search-commands-graph.js');
80
115
  await runTraceCli(rawArgs.slice(1), true);