@chemx/create-chemx 26.9.25-943 → 26.9.28-276
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/cli/audit/ast-visitors.js +7 -1
- package/cli/audit/extended-text-patterns.js +6 -3
- package/cli/audit/prompts.js +1 -1
- package/cli/audit/rules-helpers.js +7 -1
- package/cli/audit/rules.js +1 -1
- package/cli/commands/cmd-router.js +20 -2
- package/cli/commands-schema.js +65 -6
- package/cli/config/loader.js +1 -0
- package/cli/config/profiles.js +3 -0
- package/cli/friction-fixes.spec.js +96 -0
- package/cli/generator-help.js +26 -6
- package/cli/generator-jig-cli.js +67 -0
- package/cli/generator-jig.js +479 -0
- package/cli/generator-jig.spec.js +148 -0
- package/cli/generator.js +15 -2
- package/cli/index.js +4 -1
- package/cli/installer-templates.js +8 -3
- package/cli/mcp/manifests.js +29 -6
- package/cli/mcp/tools-generate.js +22 -2
- package/cli/mcp/tools-patch.js +4 -3
- package/cli/mcp/tools-read.js +53 -9
- package/cli/mcp/tools-team-locks.js +10 -3
- package/cli/mcp/tools-team-tasks.js +5 -2
- package/cli/mcp/tools-verify.js +2 -0
- package/cli/mcp/tools-write.js +2 -1
- package/cli/mcp/tools.js +96 -4
- package/cli/patcher.js +3 -2
- package/cli/path-scope.js +63 -0
- package/cli/path-traversal.spec.js +242 -0
- package/cli/project-detector.js +105 -2
- package/cli/reader-cli.js +146 -0
- package/cli/reader-logic.js +392 -0
- package/cli/reader-logic.spec.js +160 -0
- package/cli/reader.js +81 -172
- package/cli/search-commands-graph.js +125 -1
- package/cli/search-commands.js +7 -1
- package/cli/search-db.js +2 -0
- package/cli/search-queries-graph.js +294 -0
- package/cli/search-queries.js +6 -1
- package/cli/search.js +28 -30
- package/cli/team/team-commands-lock.js +17 -5
- package/cli/team/team-commands.js +2 -2
- package/cli/team/team-db-locks.js +50 -15
- package/cli/team/team-memory.js +30 -7
- package/cli/team/team-memory.spec.js +24 -0
- package/cli/tesseract.spec.js +2 -2
- package/cli/trace.spec.js +92 -0
- package/cli/verify-helpers.js +46 -3
- package/cli/verify-lint.js +224 -0
- package/cli/verify-lint.spec.js +44 -0
- package/cli/verify.js +8 -1
- package/cli/verify.spec.js +17 -0
- package/package.json +1 -1
- package/scripts/pre-commit.sh +8 -3
|
@@ -23,12 +23,18 @@ export const createAstVisitors = ({ relativePath, violations, hookRegistry, conf
|
|
|
23
23
|
const isCustomHook = isCustomHookFunction(astPath);
|
|
24
24
|
|
|
25
25
|
// Pillar 3: Hook Saturation
|
|
26
|
+
const CONTROLLER_WRAPPER_EXCLUSIONS = new Set([
|
|
27
|
+
'useSharedStatusController',
|
|
28
|
+
'useSharedController',
|
|
29
|
+
'useController'
|
|
30
|
+
]);
|
|
26
31
|
let hookCount = 0;
|
|
27
32
|
astPath.traverse({
|
|
28
33
|
CallExpression(callPath) {
|
|
29
34
|
const callee = callPath.node.callee;
|
|
30
35
|
if (t.isIdentifier(callee) && /^use[A-Z0-9]/.test(callee.name)) {
|
|
31
|
-
|
|
36
|
+
const isExcluded = CONTROLLER_WRAPPER_EXCLUSIONS.has(callee.name);
|
|
37
|
+
if (!isExcluded && callPath.getFunctionParent() === astPath) {
|
|
32
38
|
hookCount += 1;
|
|
33
39
|
}
|
|
34
40
|
}
|
|
@@ -32,7 +32,10 @@ const JAVASCRIPT_URL_TEMPLATE_PATTERN = /\b(?:href|src|action|formaction)\s*=\s*
|
|
|
32
32
|
/**
|
|
33
33
|
* Pillar 10: Check for co-located test files for molecule capsules.
|
|
34
34
|
*/
|
|
35
|
-
export const checkMoleculeCoLocatedTest = (filePath, relativePath, violations) => {
|
|
35
|
+
export const checkMoleculeCoLocatedTest = (filePath, relativePath, violations, config = {}) => {
|
|
36
|
+
const enforceColocated = config.enforceColocatedTests ?? (config.profile === 'atomic-strict');
|
|
37
|
+
if (!enforceColocated) return;
|
|
38
|
+
|
|
36
39
|
const baseName = path.basename(filePath);
|
|
37
40
|
const ext = path.extname(filePath);
|
|
38
41
|
|
|
@@ -194,8 +197,8 @@ export const checkControllerViewContract = (filePath, relativePath, content, vio
|
|
|
194
197
|
}
|
|
195
198
|
};
|
|
196
199
|
|
|
197
|
-
export const checkExtendedTextPatterns = (content, lines, relativePath, filePath, violations) => {
|
|
198
|
-
checkMoleculeCoLocatedTest(filePath, relativePath, violations);
|
|
200
|
+
export const checkExtendedTextPatterns = (content, lines, relativePath, filePath, violations, config = {}) => {
|
|
201
|
+
checkMoleculeCoLocatedTest(filePath, relativePath, violations, config);
|
|
199
202
|
checkControllerViewContract(filePath, relativePath, content, violations);
|
|
200
203
|
|
|
201
204
|
const isTestFile = /\.(test|spec)\.[jt]sx?$/.test(filePath)
|
package/cli/audit/prompts.js
CHANGED
|
@@ -66,7 +66,7 @@ export const formatGroupedPromptViolations = (violations = [], options = {}) =>
|
|
|
66
66
|
export const buildAgentCommandsSection = () => [
|
|
67
67
|
'### AI AGENT DISCOVERY & REFACTORING COMMANDS:',
|
|
68
68
|
'- Claim Work: Run `chemx team task list --status=queued` and claim via `chemx team task claim <id> --as=@coder`.',
|
|
69
|
-
'- Inspect Target: Run `chemx read <target_path> --outline`
|
|
69
|
+
'- Inspect Target: Run `chemx read <target_path> --outline` for AST signatures or `--outline --enrich` for signatures + logic flow without token bloat.',
|
|
70
70
|
'- Verify & Auto-Resolve: Run `chemx verify`. Clean files auto-reconcile tasks to done in SQLite.',
|
|
71
71
|
'- MCP Invocations: If invoking via MCP, pass command string directly via `chemx({ command: "..." })`.'
|
|
72
72
|
].join('\n');
|
|
@@ -17,7 +17,13 @@ export const countLogicalOperators = (node) => {
|
|
|
17
17
|
export const extractParseableCode = (content, ext) => {
|
|
18
18
|
if (ext === '.vue') {
|
|
19
19
|
const scriptMatch = content.match(/<script\b[^>]*>([\s\S]*?)<\/script>/i);
|
|
20
|
-
|
|
20
|
+
if (!scriptMatch) return '';
|
|
21
|
+
const scriptStartIndex = scriptMatch.index || 0;
|
|
22
|
+
const preScript = content.slice(0, scriptStartIndex);
|
|
23
|
+
const openTag = scriptMatch[0].match(/<script\b[^>]*>/i)?.[0] || '';
|
|
24
|
+
const preContent = preScript + openTag;
|
|
25
|
+
const leadingNewlines = preContent.split('\n').length - 1;
|
|
26
|
+
return '\n'.repeat(leadingNewlines) + scriptMatch[1];
|
|
21
27
|
}
|
|
22
28
|
return content;
|
|
23
29
|
};
|
package/cli/audit/rules.js
CHANGED
|
@@ -107,7 +107,7 @@ export const auditCode = (content, filePath, relativePath, options = {}) => {
|
|
|
107
107
|
checkSlopTextPatterns(content, lines, relativePath, violations);
|
|
108
108
|
|
|
109
109
|
// Pillars 8-11: Extended Fast Checks (A11y, Security secrets, Fake tests, Co-located specs)
|
|
110
|
-
checkExtendedTextPatterns(content, lines, relativePath, filePath, violations);
|
|
110
|
+
checkExtendedTextPatterns(content, lines, relativePath, filePath, violations, config);
|
|
111
111
|
|
|
112
112
|
const lang = getLanguageForFile(filePath);
|
|
113
113
|
if (lang?.id === 'csharp') {
|
|
@@ -75,6 +75,16 @@ export const dispatchCommand = async (firstArg, rawArgs, runAudit, getPackageVer
|
|
|
75
75
|
await runSearch(rawArgs.slice(1), true);
|
|
76
76
|
break;
|
|
77
77
|
}
|
|
78
|
+
case 'trace': {
|
|
79
|
+
const { runTraceCli } = await import('../search-commands-graph.js');
|
|
80
|
+
await runTraceCli(rawArgs.slice(1), true);
|
|
81
|
+
break;
|
|
82
|
+
}
|
|
83
|
+
case 'backtrace': {
|
|
84
|
+
const { runBacktraceCli } = await import('../search-commands-graph.js');
|
|
85
|
+
await runBacktraceCli(rawArgs.slice(1), true);
|
|
86
|
+
break;
|
|
87
|
+
}
|
|
78
88
|
case 'build':
|
|
79
89
|
case 'run':
|
|
80
90
|
case 'wrap': {
|
|
@@ -166,9 +176,10 @@ export const dispatchCommand = async (firstArg, rawArgs, runAudit, getPackageVer
|
|
|
166
176
|
case 'g':
|
|
167
177
|
case 'gen':
|
|
168
178
|
case 'generate':
|
|
169
|
-
case 'capsule':
|
|
179
|
+
case 'capsule':
|
|
180
|
+
case 'jig': {
|
|
170
181
|
const { runGenerateWizard } = await import('../scaffold.js');
|
|
171
|
-
await runGenerateWizard(rawArgs
|
|
182
|
+
await runGenerateWizard(rawArgs);
|
|
172
183
|
break;
|
|
173
184
|
}
|
|
174
185
|
case 'badge':
|
|
@@ -201,6 +212,13 @@ export const dispatchCommand = async (firstArg, rawArgs, runAudit, getPackageVer
|
|
|
201
212
|
await runTypecheckAudit(rawArgs.slice(1), true);
|
|
202
213
|
break;
|
|
203
214
|
}
|
|
215
|
+
case 'lint':
|
|
216
|
+
case 'check:lint':
|
|
217
|
+
case 'eslint': {
|
|
218
|
+
const { runLintAudit } = await import('../verify-lint.js');
|
|
219
|
+
await runLintAudit(rawArgs.slice(1), true);
|
|
220
|
+
break;
|
|
221
|
+
}
|
|
204
222
|
case 'test':
|
|
205
223
|
case 'tests':
|
|
206
224
|
case 'check:test': {
|
package/cli/commands-schema.js
CHANGED
|
@@ -16,7 +16,9 @@ export const COMMANDS_SCHEMA = [
|
|
|
16
16
|
{ flag: '-i, --inspect', desc: 'Inspect props and hooks without full source' },
|
|
17
17
|
{ flag: '--tier=<tier>', desc: 'Filter by tier: atom, molecule, organism, hook, view' },
|
|
18
18
|
{ flag: '--blast-radius', desc: 'Map direct consumers, transitive dependents & impacted tiers (aliases: --blast, --impact)' },
|
|
19
|
-
{ flag: '--
|
|
19
|
+
{ flag: '--trace', desc: 'Forward call trace: downstream functions invoked by target' },
|
|
20
|
+
{ flag: '--backtrace', desc: 'Reverse backtrace: upstream callers and causal paths leading to target' },
|
|
21
|
+
{ flag: '--max-depth=<N>', desc: 'Max depth for blast radius / trace traversal (default: 5)' },
|
|
20
22
|
{ flag: '--semantic', desc: 'Concept search via vector cosine similarity' },
|
|
21
23
|
{ flag: '--hybrid', desc: 'Blended BM25 keyword + Vector RRF ranking' },
|
|
22
24
|
{ flag: '--hazards', desc: 'Query architectural rule violations directly (flags: --rule=<id>, --critical)' },
|
|
@@ -26,6 +28,8 @@ export const COMMANDS_SCHEMA = [
|
|
|
26
28
|
examples: [
|
|
27
29
|
'npx chemx search "badge"',
|
|
28
30
|
'pnpm q a-button --blast-radius --json',
|
|
31
|
+
'pnpm q useUserCardController --trace',
|
|
32
|
+
'pnpm q handleAction --backtrace',
|
|
29
33
|
'pnpm q "button click handler" --semantic',
|
|
30
34
|
'pnpm q "useAttentionCardController" --hybrid --json',
|
|
31
35
|
'pnpm q "useTheme" --inspect',
|
|
@@ -34,13 +38,18 @@ export const COMMANDS_SCHEMA = [
|
|
|
34
38
|
},
|
|
35
39
|
{
|
|
36
40
|
name: 'read',
|
|
37
|
-
aliases: ['view'],
|
|
41
|
+
aliases: ['view', 'r'],
|
|
38
42
|
usage: 'npx chemx read <file> [options]',
|
|
39
|
-
summary: 'Token-minified file reader with AST outline extraction.',
|
|
40
|
-
description: 'Extracts structural AST outlines, stripped comments, line ranges, or targeted symbol declarations to minimize LLM context token consumption.',
|
|
43
|
+
summary: 'Token-minified file reader with AST outline and logic extraction.',
|
|
44
|
+
description: 'Extracts structural AST outlines, logic skeletons, stripped comments, line ranges, or targeted symbol declarations to minimize LLM context token consumption.',
|
|
41
45
|
flags: [
|
|
42
|
-
{ flag: '--outline', desc: 'Signatures only (80%+ token savings)' },
|
|
43
|
-
{ flag: '--
|
|
46
|
+
{ flag: '--outline, -o', desc: 'Signatures only (80%+ token savings)' },
|
|
47
|
+
{ flag: '--logic, -l', desc: 'AST logic skeleton: control flow, guards, and mutations without decorative syntax' },
|
|
48
|
+
{ flag: '--template, -t', desc: 'Extract template markup only (Vue/Svelte/JSX)' },
|
|
49
|
+
{ flag: '--enrich', desc: 'Append compacted logic skeleton after outline (free-lunch overlay). Compose with --outline for signatures + logic in one call.' },
|
|
50
|
+
{ flag: '--trace=<name>', desc: 'Append forward call trace card inline (requires --enrich)' },
|
|
51
|
+
{ flag: '--backtrace=<name>', desc: 'Append reverse caller chain card inline (requires --enrich)' },
|
|
52
|
+
{ flag: '--symbol=<name>, -s', desc: 'Target a specific symbol definition' },
|
|
44
53
|
{ flag: '--connections', desc: 'Include caller graph and dependent references alongside symbol' },
|
|
45
54
|
{ flag: '--strip-comments', desc: 'Remove all code comments' },
|
|
46
55
|
{ flag: '--compact', desc: 'Remove blank lines and indentation' },
|
|
@@ -50,10 +59,42 @@ export const COMMANDS_SCHEMA = [
|
|
|
50
59
|
],
|
|
51
60
|
examples: [
|
|
52
61
|
'npx chemx read src/store.ts --outline',
|
|
62
|
+
'npx chemx read src/controller.ts --outline --enrich',
|
|
63
|
+
'npx chemx read src/card.vue --outline --enrich --trace=handleSubmit',
|
|
64
|
+
'npx chemx read src/controller.ts --logic',
|
|
65
|
+
'npx chemx read src/card.vue --template',
|
|
53
66
|
'npx chemx read api.ts --symbol=login --connections',
|
|
54
67
|
'npx chemx read src/ui/atoms/a-button/types.d.ts --symbol=ButtonVariant'
|
|
55
68
|
]
|
|
56
69
|
},
|
|
70
|
+
{
|
|
71
|
+
name: 'trace',
|
|
72
|
+
usage: 'npx chemx trace <symbol> [options]',
|
|
73
|
+
summary: 'Forward call trace: inspects downstream function invocations.',
|
|
74
|
+
description: 'Maps the execution tree of functions, services, and external APIs called by the target symbol.',
|
|
75
|
+
flags: [
|
|
76
|
+
{ flag: '--max-depth=<N>', desc: 'Max depth for call trace traversal (default: 3)' },
|
|
77
|
+
{ flag: '--json', desc: 'Output call tree as minified JSON' }
|
|
78
|
+
],
|
|
79
|
+
examples: [
|
|
80
|
+
'npx chemx trace useCartController',
|
|
81
|
+
'npx chemx trace handleCheckout --json'
|
|
82
|
+
]
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
name: 'backtrace',
|
|
86
|
+
usage: 'npx chemx backtrace <symbol> [options]',
|
|
87
|
+
summary: 'Reverse call backtrace: maps upstream caller chains leading to target.',
|
|
88
|
+
description: 'Traces the causal path of how user interactions, components, and views trigger the target symbol.',
|
|
89
|
+
flags: [
|
|
90
|
+
{ flag: '--max-depth=<N>', desc: 'Max depth for backtrace traversal (default: 5)' },
|
|
91
|
+
{ flag: '--json', desc: 'Output causal path as minified JSON' }
|
|
92
|
+
],
|
|
93
|
+
examples: [
|
|
94
|
+
'npx chemx backtrace handleCheckout',
|
|
95
|
+
'npx chemx backtrace postOrder --json'
|
|
96
|
+
]
|
|
97
|
+
},
|
|
57
98
|
{
|
|
58
99
|
name: 'patch',
|
|
59
100
|
aliases: ['edit'],
|
|
@@ -178,6 +219,24 @@ export const COMMANDS_SCHEMA = [
|
|
|
178
219
|
'pnpm chemx verify --json'
|
|
179
220
|
]
|
|
180
221
|
},
|
|
222
|
+
{
|
|
223
|
+
name: 'lint',
|
|
224
|
+
aliases: ['check:lint', 'eslint'],
|
|
225
|
+
usage: 'npx chemx lint [path] [options]',
|
|
226
|
+
summary: 'Token-conserving linter runner and tree-formatter with automatic fix.',
|
|
227
|
+
description: 'Executes ESLint with token-efficient diagnostics, target scoping, and automatic --fix support.',
|
|
228
|
+
flags: [
|
|
229
|
+
{ flag: '--fix', desc: 'Automatically fix fixable lint and tree-formatting errors' },
|
|
230
|
+
{ flag: '--json', desc: 'Output structured diagnostics as JSON for AI agents' },
|
|
231
|
+
{ flag: '--raw', desc: 'Do not capture or format output' }
|
|
232
|
+
],
|
|
233
|
+
examples: [
|
|
234
|
+
'npx chemx lint',
|
|
235
|
+
'npx chemx lint --fix',
|
|
236
|
+
'npx chemx lint apps/youmeos/components/blueprints/desktop/u-desktop.vue --fix',
|
|
237
|
+
'pnpm chemx lint --json'
|
|
238
|
+
]
|
|
239
|
+
},
|
|
181
240
|
{
|
|
182
241
|
name: 'pillars',
|
|
183
242
|
aliases: ['config:pillars', 'rules'],
|
package/cli/config/loader.js
CHANGED
|
@@ -25,6 +25,7 @@ const normalizeRuleKeys = (rules = {}) => {
|
|
|
25
25
|
'max-prop-count': 'maxPropCount',
|
|
26
26
|
'prefer-design-tokens': 'preferDesignTokens',
|
|
27
27
|
'enforce-file-length': 'enforceFileLength',
|
|
28
|
+
'enforce-colocated-tests': 'enforceColocatedTests',
|
|
28
29
|
'rule-of-three-abstractions': 'ruleOfThreeAbstractions',
|
|
29
30
|
'ai-slop-detection': 'aiSlopDetection',
|
|
30
31
|
'max-line-count-warning': 'maxLineCountWarning'
|
package/cli/config/profiles.js
CHANGED
|
@@ -14,6 +14,7 @@ export const PROFILES = {
|
|
|
14
14
|
maxPropCount: 7,
|
|
15
15
|
preferDesignTokens: 'warning',
|
|
16
16
|
enforceFileLength: false,
|
|
17
|
+
enforceColocatedTests: false,
|
|
17
18
|
ruleOfThreeAbstractions: true,
|
|
18
19
|
aiSlopDetection: 'critical',
|
|
19
20
|
maxLineCountWarning: 250
|
|
@@ -28,6 +29,7 @@ export const PROFILES = {
|
|
|
28
29
|
maxPropCount: 5,
|
|
29
30
|
preferDesignTokens: 'error',
|
|
30
31
|
enforceFileLength: true,
|
|
32
|
+
enforceColocatedTests: true,
|
|
31
33
|
ruleOfThreeAbstractions: false,
|
|
32
34
|
aiSlopDetection: 'critical',
|
|
33
35
|
maxLineCountWarning: 100
|
|
@@ -42,6 +44,7 @@ export const PROFILES = {
|
|
|
42
44
|
maxPropCount: 12,
|
|
43
45
|
preferDesignTokens: 'off',
|
|
44
46
|
enforceFileLength: false,
|
|
47
|
+
enforceColocatedTests: false,
|
|
45
48
|
ruleOfThreeAbstractions: true,
|
|
46
49
|
aiSlopDetection: 'medium',
|
|
47
50
|
maxLineCountWarning: 500
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert';
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { handleChemx } from './mcp/tools.js';
|
|
6
|
+
import { auditCode } from './audit/rules.js';
|
|
7
|
+
|
|
8
|
+
test('Friction 1: team_task add and team task add persist task rows', async () => {
|
|
9
|
+
const resCmd = await handleChemx({ command: 'team task add CLI Task Test' });
|
|
10
|
+
assert.ok(resCmd && resCmd.id, 'Expected task object with id from team task add');
|
|
11
|
+
assert.strictEqual(resCmd.title, 'CLI Task Test');
|
|
12
|
+
|
|
13
|
+
const resCmd2 = await handleChemx({ command: 'team_task add Direct SubCmd Task' });
|
|
14
|
+
assert.ok(resCmd2 && resCmd2.id, 'Expected task object with id from team_task add');
|
|
15
|
+
assert.strictEqual(resCmd2.title, 'Direct SubCmd Task');
|
|
16
|
+
|
|
17
|
+
const resInfer = await handleChemx({ action: 'team_task', params: { title: 'Inferred Add Task' } });
|
|
18
|
+
assert.ok(resInfer && resInfer.id, 'Expected task object with id when title is given');
|
|
19
|
+
assert.strictEqual(resInfer.title, 'Inferred Add Task');
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test('Friction 2: patch handles all parameter aliases and command strings', async () => {
|
|
23
|
+
const testFile = path.resolve('test-patch-spec.txt');
|
|
24
|
+
fs.writeFileSync(testFile, 'line 1\nOLD_STRING\nline 3\n');
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
await handleChemx({ action: 'patch', params: { path: 'test-patch-spec.txt', target: 'OLD_STRING', replace: 'NEW_STRING_1' } });
|
|
28
|
+
assert.strictEqual(fs.readFileSync(testFile, 'utf8'), 'line 1\nNEW_STRING_1\nline 3\n');
|
|
29
|
+
|
|
30
|
+
await handleChemx({ action: 'patch', params: { path: 'test-patch-spec.txt', search: 'NEW_STRING_1', replacement: 'NEW_STRING_2' } });
|
|
31
|
+
assert.strictEqual(fs.readFileSync(testFile, 'utf8'), 'line 1\nNEW_STRING_2\nline 3\n');
|
|
32
|
+
|
|
33
|
+
await handleChemx({ action: 'patch', params: { path: 'test-patch-spec.txt', targetContent: 'NEW_STRING_2', replacementContent: 'FINAL_STRING' } });
|
|
34
|
+
assert.strictEqual(fs.readFileSync(testFile, 'utf8'), 'line 1\nFINAL_STRING\nline 3\n');
|
|
35
|
+
} finally {
|
|
36
|
+
if (fs.existsSync(testFile)) fs.unlinkSync(testFile);
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test('Friction 3: Vue file AST line numbers match file line numbers exactly', () => {
|
|
41
|
+
const vueContent = [
|
|
42
|
+
'<template>',
|
|
43
|
+
' <div>',
|
|
44
|
+
' <p>Line 3</p>',
|
|
45
|
+
' <p>Line 4</p>',
|
|
46
|
+
' <p>Line 5</p>',
|
|
47
|
+
' <p>Line 6</p>',
|
|
48
|
+
' <p>Line 7</p>',
|
|
49
|
+
' <p>Line 8</p>',
|
|
50
|
+
' <p>Line 9</p>',
|
|
51
|
+
' <p>Line 10</p>',
|
|
52
|
+
' </div>',
|
|
53
|
+
'</template>',
|
|
54
|
+
'',
|
|
55
|
+
'<script setup lang="ts">',
|
|
56
|
+
'const cond = a && b && c;',
|
|
57
|
+
'</script>'
|
|
58
|
+
].join('\n');
|
|
59
|
+
|
|
60
|
+
const recorded = [];
|
|
61
|
+
const fakeReg = { record: (type, sig, loc) => recorded.push({ type, sig, loc }) };
|
|
62
|
+
auditCode(vueContent, 'SampleVue.vue', 'SampleVue.vue', { patternRegistry: fakeReg });
|
|
63
|
+
|
|
64
|
+
const condRec = recorded.find((r) => r.type === 'PREDICATE_LOGIC');
|
|
65
|
+
assert.ok(condRec, 'Expected PREDICATE_LOGIC recorded');
|
|
66
|
+
assert.strictEqual(condRec.loc.line, 15, 'Expected condition on line 15');
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test('Friction 4: HOOK_SATURATION excludes canonical controller return wrappers', () => {
|
|
70
|
+
const hookCode = [
|
|
71
|
+
'export function useMyComposable() {',
|
|
72
|
+
' const a = useA();',
|
|
73
|
+
' const b = useB();',
|
|
74
|
+
' const c = useC();',
|
|
75
|
+
' const d = useD();',
|
|
76
|
+
' const e = useE();',
|
|
77
|
+
' return useSharedStatusController({ a, b, c, d, e });',
|
|
78
|
+
'}'
|
|
79
|
+
].join('\n');
|
|
80
|
+
|
|
81
|
+
const hookViolations = auditCode(hookCode, 'useMyComposable.ts', 'src/useMyComposable.ts');
|
|
82
|
+
const hookSat = hookViolations.filter((v) => v.rule === 'HOOK_SATURATION');
|
|
83
|
+
assert.strictEqual(hookSat.length, 0, 'useSharedStatusController should not trigger HOOK_SATURATION');
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test('Friction 5: TEST_MISSING_COLOCATED is gated by profile', () => {
|
|
87
|
+
const compCode = '<template><button>Click</button></template>';
|
|
88
|
+
|
|
89
|
+
const defaultViolations = auditCode(compCode, 'src/components/molecules/m-button.vue', 'src/components/molecules/m-button.vue', { config: { profile: 'pragmatic' } });
|
|
90
|
+
const testMissingDefault = defaultViolations.filter((v) => v.rule === 'TEST_MISSING_COLOCATED');
|
|
91
|
+
assert.strictEqual(testMissingDefault.length, 0, 'Pragmatic profile should not flag TEST_MISSING_COLOCATED');
|
|
92
|
+
|
|
93
|
+
const strictViolations = auditCode(compCode, 'src/components/molecules/m-button.vue', 'src/components/molecules/m-button.vue', { config: { profile: 'atomic-strict' } });
|
|
94
|
+
const testMissingStrict = strictViolations.filter((v) => v.rule === 'TEST_MISSING_COLOCATED');
|
|
95
|
+
assert.strictEqual(testMissingStrict.length, 1, 'Atomic-strict profile should flag TEST_MISSING_COLOCATED');
|
|
96
|
+
});
|
package/cli/generator-help.js
CHANGED
|
@@ -12,18 +12,29 @@ export const printGenerateHelp = () => {
|
|
|
12
12
|
` ${CYAN}npx chemx generate${RESET} <name> [options]`,
|
|
13
13
|
` ${CYAN}npx chemx generate${RESET} <tier> <name> [options]`,
|
|
14
14
|
'',
|
|
15
|
-
`${BOLD}TIERS${RESET}`,
|
|
16
|
-
` ${CYAN}atom${RESET} (a-) Foundational UI elements (raw HTML permitted)`,
|
|
15
|
+
`${BOLD}TIERS & JIG KINDS${RESET}`,
|
|
17
16
|
` ${CYAN}molecule${RESET} (m-) Groups of atoms (no raw HTML, co-located controller)`,
|
|
17
|
+
` ${CYAN}atom${RESET} (a-) Foundational UI elements (raw HTML permitted)`,
|
|
18
18
|
` ${CYAN}organism${RESET} (o-) Complex feature modules`,
|
|
19
19
|
` ${CYAN}hook${RESET} (use-) Domain state / headless composables (< 5 return properties)`,
|
|
20
20
|
` ${CYAN}view${RESET} (v-) Declarative Table-of-Contents views`,
|
|
21
|
+
` ${CYAN}service${RESET} Domain service / API client with Result tuples and spec`,
|
|
22
|
+
` ${CYAN}route${RESET} HTTP API route handler with schema validation and test`,
|
|
23
|
+
` ${CYAN}store${RESET} State store / reactive machine with discriminated unions`,
|
|
24
|
+
` ${CYAN}repo${RESET} Database repository with CRUD methods and Result tuples`,
|
|
25
|
+
` ${CYAN}util${RESET} Pure stateless utility functions with test spec`,
|
|
26
|
+
` ${CYAN}spec${RESET} Isolated unit test spec for an existing target`,
|
|
21
27
|
'',
|
|
22
28
|
`${BOLD}OPTIONS${RESET}`,
|
|
23
|
-
` ${CYAN}--tier=<tier>${RESET} Specify component tier`,
|
|
29
|
+
` ${CYAN}--tier=<tier>${RESET} Specify component tier (atom, molecule, organism, hook, view)`,
|
|
30
|
+
` ${CYAN}--jig=<kind>${RESET} Specify programmatic file kind (service, route, store, repo, util, spec)`,
|
|
24
31
|
` ${CYAN}--framework=<react|vue|svelte>${RESET} Framework flavor (default: auto-detected or react)`,
|
|
25
|
-
` ${CYAN}--dir=<path>${RESET} Target directory (default: src/components/<tier>s)`,
|
|
32
|
+
` ${CYAN}--dir=<path>${RESET} Target directory (default: src/components/<tier>s or src/<kind>s)`,
|
|
26
33
|
` ${CYAN}--desc="<text>"${RESET} Describe functionality to tailor archetype and state`,
|
|
34
|
+
` ${CYAN}--methods="<m1,m2>"${RESET} Method names or signatures for service/repo/store`,
|
|
35
|
+
` ${CYAN}--state="<s1,s2>"${RESET} State property definitions for store or controller`,
|
|
36
|
+
` ${CYAN}--routes="<r1,r2>"${RESET} Route definitions for API route jig (e.g. GET /items, POST /items)`,
|
|
37
|
+
` ${CYAN}--schema="<json>"${RESET} Structured JSON schema definition payload`,
|
|
27
38
|
` ${CYAN}--dry-run${RESET} Preview planned files and lines without touching disk`,
|
|
28
39
|
` ${CYAN}--lean${RESET} Generate minimal capsule without controller/spec`,
|
|
29
40
|
` ${CYAN}--json${RESET} Output result as minified JSON`,
|
|
@@ -32,14 +43,23 @@ export const printGenerateHelp = () => {
|
|
|
32
43
|
'',
|
|
33
44
|
`${BOLD}EXAMPLES${RESET}`,
|
|
34
45
|
` ${CYAN}npx chemx generate m-task-list --framework=react${RESET}`,
|
|
35
|
-
` ${CYAN}npx chemx generate
|
|
36
|
-
` ${CYAN}npx chemx
|
|
46
|
+
` ${CYAN}npx chemx generate --jig=service payment-gateway --methods="processCharge,refund"${RESET}`,
|
|
47
|
+
` ${CYAN}npx chemx jig route api-orders --routes="GET /orders, POST /orders"${RESET}`,
|
|
48
|
+
` ${CYAN}npx chemx jig store session-machine --desc="user authentication lifecycle"${RESET}`,
|
|
37
49
|
` ${CYAN}npx chemx generate hook use-task-filter${RESET}`,
|
|
38
50
|
` ${CYAN}npx chemx generate view v-user-profile${RESET}`,
|
|
39
51
|
'',
|
|
52
|
+
`${BOLD}TOKEN ECONOMICS & AGENT JIG${RESET}`,
|
|
53
|
+
` ${DIM}Thesis: Passing a 30-50 token parameter payload to chemx generate / jig replaces${RESET}`,
|
|
54
|
+
` ${DIM} 1,000-2,500 output tokens of repetitive TypeScript, SCSS, and spec boilerplate.${RESET}`,
|
|
55
|
+
` ${DIM}Reduction: 90%+ fewer LLM output tokens consumed per component, service, or route.${RESET}`,
|
|
56
|
+
` ${DIM}Invariants: Guarantees <100 LOC limits, Result tuples, 2-stage booleans, zero synthetic data,${RESET}`,
|
|
57
|
+
` ${DIM} and co-located unit test specs without LLM syntax drift or hallucination.${RESET}`,
|
|
58
|
+
'',
|
|
40
59
|
`${BOLD}LINE LIMITS${RESET}`,
|
|
41
60
|
` ${DIM}Molecule capsule template: < 100 lines (outer bound)${RESET}`,
|
|
42
61
|
` ${DIM}Co-located controller hook: < 100 lines (pure reactive state)${RESET}`,
|
|
62
|
+
` ${DIM}Domain service / repo: < 100 lines (stateless or cohesive atomic class)${RESET}`,
|
|
43
63
|
` ${DIM}Table of Contents view: 10-20 lines (declarative slot assembly)${RESET}`,
|
|
44
64
|
''
|
|
45
65
|
];
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { createJigFiles, normalizeJigKind, JIG_KINDS } from './generator-jig.js';
|
|
2
|
+
|
|
3
|
+
export const handleJigCli = ({ rawArgs, positional, rawName, dirArg, descArg, isDryRun, isJson }) => {
|
|
4
|
+
const jigFlag = (rawArgs.find((a) => a.startsWith('--jig=')) || '').split('=')[1];
|
|
5
|
+
const kindFlag = (rawArgs.find((a) => a.startsWith('--kind=')) || '').split('=')[1];
|
|
6
|
+
|
|
7
|
+
let resolvedKind = jigFlag || kindFlag;
|
|
8
|
+
let resolvedName = rawName;
|
|
9
|
+
|
|
10
|
+
if (!resolvedKind && positional[0] && JIG_KINDS.has(positional[0].toLowerCase())) {
|
|
11
|
+
resolvedKind = positional[0].toLowerCase();
|
|
12
|
+
resolvedName = positional[1] || rawName;
|
|
13
|
+
} else if (positional[0] && positional[0].toLowerCase() === 'jig') {
|
|
14
|
+
resolvedKind = positional[1];
|
|
15
|
+
resolvedName = positional[2] || rawName;
|
|
16
|
+
}
|
|
17
|
+
resolvedKind = normalizeJigKind(resolvedKind || 'service');
|
|
18
|
+
|
|
19
|
+
const methodsArg = (rawArgs.find((a) => a.startsWith('--methods=')) || '').replace(/^--methods=/, '');
|
|
20
|
+
const stateArg = (rawArgs.find((a) => a.startsWith('--state=')) || '').replace(/^--state=/, '');
|
|
21
|
+
const routesArg = (rawArgs.find((a) => a.startsWith('--routes=')) || '').replace(/^--routes=/, '');
|
|
22
|
+
const presetArg = (rawArgs.find((a) => a.startsWith('--preset=')) || '').replace(/^--preset=/, '');
|
|
23
|
+
const schemaArg = (rawArgs.find((a) => a.startsWith('--schema=')) || '').replace(/^--schema=/, '');
|
|
24
|
+
|
|
25
|
+
let schemaData = {};
|
|
26
|
+
if (schemaArg) {
|
|
27
|
+
try {
|
|
28
|
+
schemaData = JSON.parse(schemaArg);
|
|
29
|
+
} catch {
|
|
30
|
+
// ignore JSON parse error
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const jigRes = createJigFiles({
|
|
35
|
+
kind: schemaData.kind || resolvedKind,
|
|
36
|
+
name: schemaData.name || resolvedName || `sample-${resolvedKind}`,
|
|
37
|
+
dir: dirArg,
|
|
38
|
+
preset: presetArg,
|
|
39
|
+
methods: schemaData.methods || methodsArg,
|
|
40
|
+
state: schemaData.state || stateArg,
|
|
41
|
+
routes: schemaData.routes || routesArg,
|
|
42
|
+
desc: descArg || schemaData.desc,
|
|
43
|
+
dryRun: isDryRun,
|
|
44
|
+
cwd: process.cwd()
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
if (isJson) {
|
|
48
|
+
process.stdout.write(JSON.stringify(jigRes) + '\n');
|
|
49
|
+
return jigRes;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (jigRes.dryRun) {
|
|
53
|
+
process.stdout.write(`\n\x1b[1m\x1b[33m[DRY RUN]\x1b[0m Would generate programmatic jig (${jigRes.kind}) at \x1b[36m${jigRes.relativeDir}/\x1b[0m:\n`);
|
|
54
|
+
for (const f of jigRes.previews || []) {
|
|
55
|
+
process.stdout.write(` \x1b[33m•\x1b[0m ${f.file} (${f.lines} lines)\n`);
|
|
56
|
+
}
|
|
57
|
+
process.stdout.write('\n\x1b[2mDry run complete. No files were written to disk.\x1b[0m\n\n');
|
|
58
|
+
return jigRes;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
process.stdout.write(`\n\x1b[1m\x1b[32m✔ Successfully generated programmatic jig (${jigRes.kind}):\x1b[0m \x1b[36m${jigRes.relativeDir}/\x1b[0m\n`);
|
|
62
|
+
for (const f of jigRes.filesCreated) {
|
|
63
|
+
process.stdout.write(` \x1b[32m✔\x1b[0m ${f}\n`);
|
|
64
|
+
}
|
|
65
|
+
process.stdout.write('\n\x1b[2mChemical X Standards verified: < 100 lines per file, Result tuples, co-located spec tests.\x1b[0m\n\n');
|
|
66
|
+
return jigRes;
|
|
67
|
+
};
|