@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 +37 -25
- package/cli/audit/ast-visitors.js +116 -1
- package/cli/audit/history.js +47 -10
- package/cli/audit/rules-predicates.js +200 -0
- package/cli/audit/rules-registry.js +25 -0
- package/cli/commands/cmd-router.js +35 -0
- package/cli/commands/cmd-wrappers.js +322 -0
- package/cli/commands-schema.js +98 -8
- package/cli/errors/catcher.js +35 -2
- package/cli/help.js +41 -0
- package/cli/host-shims.js +8 -5
- package/cli/index.js +6 -0
- package/cli/mcp/antigravity.js +8 -0
- package/cli/mcp/call-scope.js +1 -1
- package/cli/mcp/manifests.js +19 -25
- package/cli/mcp/tools.js +38 -4
- package/cli/reader.js +58 -6
- package/cli/search-commands.js +96 -0
- package/cli/search-db.js +11 -2
- package/cli/search-schema.js +22 -9
- package/cli/search.js +51 -50
- package/package.json +3 -2
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.
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
- **Svelte 5**:
|
|
45
|
-
|
|
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
|
|
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
|
-
- **
|
|
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
|
package/cli/audit/history.js
CHANGED
|
@@ -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 =
|
|
47
|
+
const dir = findChemxDir(cwd);
|
|
16
48
|
try {
|
|
17
|
-
|
|
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
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
|
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);
|