@chemx/starter-kit 26.9.21-8 → 26.9.24-393
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 +10 -17
- package/cli/audit/autofix.js +17 -2
- package/cli/audit/clone-detector.js +64 -0
- package/cli/audit/clone-detector.spec.js +46 -0
- package/cli/audit/history.js +22 -6
- package/cli/audit/hook-shape-validator.js +312 -0
- package/cli/audit/prompts.js +13 -10
- package/cli/audit/prompts.spec.js +52 -1
- package/cli/audit/reporter-clones.js +33 -0
- package/cli/audit/reporter-hotspot-graph.js +34 -0
- package/cli/audit/rules-registry.js +6 -1
- package/cli/audit/rules.js +10 -2
- package/cli/audit/rules.spec.js +129 -0
- package/cli/audit-stages.spec.js +30 -0
- package/cli/audit.js +12 -3
- package/cli/commands/cmd-audit.js +56 -12
- package/cli/commands/cmd-project.js +100 -0
- package/cli/commands/cmd-router.js +52 -3
- package/cli/commands-schema.js +28 -0
- package/cli/create.spec.js +2 -2
- package/cli/exploder.js +258 -0
- package/cli/exploder.spec.js +99 -0
- package/cli/generator-compact.spec.js +104 -0
- package/cli/generator-framework.spec.js +72 -0
- package/cli/generator-templates/archetypes/collection-archetypes.js +9 -9
- package/cli/generator-templates/archetypes/index.js +34 -4
- package/cli/generator-templates/archetypes/input-archetypes.js +2 -2
- package/cli/generator-templates/archetypes/layout-archetypes.js +2 -2
- package/cli/generator-templates/archetypes/minimal-archetypes.js +131 -0
- package/cli/generator-templates/archetypes/status-archetypes.js +4 -4
- package/cli/generator-templates/archetypes/vector-matcher.js +50 -0
- package/cli/generator-templates/archetypes/vector-matcher.spec.js +25 -0
- package/cli/generator-templates/compact.js +130 -0
- package/cli/generator-templates/controller-converters.js +93 -0
- package/cli/generator-templates/controller.js +2 -64
- package/cli/generator-templates/react.js +1 -1
- package/cli/generator-templates/svelte.js +1 -1
- package/cli/generator-templates/types.js +2 -2
- package/cli/generator-templates/vue.js +1 -1
- package/cli/generator-templates.js +1 -0
- package/cli/generator-templates.spec.js +6 -1
- package/cli/generator.js +94 -7
- package/cli/help.js +1 -1
- package/cli/index.js +23 -8
- package/cli/installer-templates.js +1 -1
- package/cli/mcp/antigravity.js +5 -2
- package/cli/mcp/installer.js +7 -7
- package/cli/mcp/installer.spec.js +3 -3
- package/cli/mcp/manifests.js +4 -2
- package/cli/mcp/prompts.js +1 -1
- package/cli/mcp/server.js +2 -0
- package/cli/mcp/tools-audit.js +97 -0
- package/cli/mcp/tools-generate.js +62 -0
- package/cli/mcp/tools-patch.js +72 -0
- package/cli/mcp/tools-patterns.js +71 -0
- package/cli/mcp/tools-project.js +75 -0
- package/cli/mcp/tools-project.spec.js +51 -0
- package/cli/mcp/tools-q-modes.js +69 -0
- package/cli/mcp/tools-q.js +58 -0
- package/cli/mcp/tools-read.js +69 -0
- package/cli/mcp/tools-search-util.js +35 -0
- package/cli/mcp/tools-search.js +15 -294
- package/cli/mcp/tools-team-feed.js +57 -0
- package/cli/mcp/tools-team-locks.js +63 -0
- package/cli/mcp/tools-team-mailbox.js +32 -0
- package/cli/mcp/tools-team-tasks.js +85 -0
- package/cli/mcp/tools-team.js +37 -183
- package/cli/mcp/tools-verify.js +47 -0
- package/cli/mcp/tools-write.js +33 -0
- package/cli/mcp/tools.js +79 -372
- package/cli/mutators.js +10 -23
- package/cli/pillars-schema.js +3 -3
- package/cli/project-detector.js +47 -0
- package/cli/reader-shorthand.spec.js +60 -0
- package/cli/reader.js +67 -24
- package/cli/search-bulletproof.spec.js +104 -0
- package/cli/search-db.js +33 -5
- package/cli/search-queries-hotspot-graph.js +65 -0
- package/cli/search-queries-hotspot-graph.spec.js +66 -0
- package/cli/search-queries-similar.js +84 -0
- package/cli/search-queries-similar.spec.js +43 -0
- package/cli/search-schema.js +79 -11
- package/cli/search.js +13 -2
- package/cli/team/index.js +9 -0
- package/cli/team/team-commands.js +106 -64
- package/cli/team/team-concurrency-stress.spec.js +95 -0
- package/cli/team/team-db-lock-promotion.js +15 -11
- package/cli/team/team-db-mailbox.js +86 -0
- package/cli/team/team-db-task-helpers.js +80 -28
- package/cli/team/team-db-tasks.js +13 -5
- package/cli/team/team-flags.js +72 -0
- package/cli/team/team-format.js +87 -46
- package/cli/team/team-mailbox-empirical.spec.js +191 -0
- package/cli/team/team-mailbox.spec.js +93 -0
- package/cli/team/team-memory.js +84 -0
- package/cli/team/team-memory.spec.js +74 -0
- package/cli/team/team-projects-coordinator.js +100 -0
- package/cli/team/team-projects-format.js +45 -0
- package/cli/team/team-projects-memory.js +70 -0
- package/cli/team/team-projects.js +94 -0
- package/cli/team/team-projects.spec.js +116 -0
- package/cli/team/team-schema-memory.js +25 -0
- package/cli/team/team-schema-projects.js +50 -0
- package/cli/team/team-schema.js +33 -14
- package/cli/team/team-tasks-hierarchy.spec.js +176 -0
- package/cli/team/team-tokens.js +95 -0
- package/cli/team/team-triage.js +274 -52
- package/cli/team/team-verification-gate.spec.js +253 -0
- package/cli/team/team.spec.js +1 -1
- package/cli/tesseract-hud.js +59 -0
- package/cli/tesseract-manifesto.js +61 -0
- package/cli/tesseract-state.js +58 -0
- package/cli/tesseract.js +81 -0
- package/cli/tesseract.spec.js +55 -0
- package/cli/trend.js +84 -0
- package/cli/trend.spec.js +51 -0
- package/cli/ui-actions-tasks.js +13 -5
- package/cli/ui-actions.js +10 -2
- package/cli/ui-client-api.js +2 -2
- package/cli/ui-client-script.js +12 -15
- package/cli/ui-e2e-verification.spec.js +13 -3
- package/cli/ui-filetree.spec.js +5 -5
- package/cli/ui-handlers.js +10 -2
- package/cli/ui-html.js +26 -34
- package/cli/ui-index.html +905 -0
- package/cli/ui-kanban.spec.js +12 -12
- package/cli/ui-server.js +19 -3
- package/cli/ui-sse.js +57 -0
- package/cli/ui-sse.spec.js +74 -0
- package/cli/ui-styles.js +195 -81
- package/cli/ui-template-kanban.js +15 -1
- package/cli/ui-vbulletin.spec.js +12 -13
- package/cli/ui.spec.js +12 -5
- package/cli/verify.js +19 -8
- package/package.json +3 -1
- package/scripts/check-framework-generation.mjs +195 -0
- package/scripts/pre-commit.sh +2 -7
- package/scripts/publish-both.mjs +4 -0
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { ANSI } from '../theme.js';
|
|
2
|
+
|
|
3
|
+
export const formatCloneReport = (cloneResult) => {
|
|
4
|
+
const { count, threshold, exactClones = [], nearClones = [], pairs = [] } = cloneResult;
|
|
5
|
+
const hasPairs = Array.isArray(pairs) && pairs.length > 0;
|
|
6
|
+
if (!hasPairs) {
|
|
7
|
+
return ` ${ANSI.LIME}✔ Zero duplicate clone hazards detected (threshold: ${threshold})${ANSI.RESET}\n`;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const lines = [
|
|
11
|
+
` ${ANSI.BOLD}${ANSI.PINK}Semantic Clone Detection (${count} candidate pairs >= ${threshold})${ANSI.RESET}`
|
|
12
|
+
];
|
|
13
|
+
|
|
14
|
+
const hasExact = exactClones.length > 0;
|
|
15
|
+
if (hasExact) {
|
|
16
|
+
lines.push(` ${ANSI.RED}${ANSI.BOLD}Hazard: ${exactClones.length} high-similarity duplicate(s) (>= 0.95)${ANSI.RESET}`);
|
|
17
|
+
for (const p of exactClones) {
|
|
18
|
+
const simPct = Math.round(p.similarity * 100);
|
|
19
|
+
lines.push(` ${ANSI.RED}[${simPct}%]${ANSI.RESET} ${p.fileA} <-> ${p.fileB}`);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const hasNear = nearClones.length > 0;
|
|
24
|
+
if (hasNear) {
|
|
25
|
+
lines.push(` ${ANSI.GOLD}Harmonization Candidates: ${nearClones.length} structural clone(s)${ANSI.RESET}`);
|
|
26
|
+
for (const p of nearClones) {
|
|
27
|
+
const simPct = Math.round(p.similarity * 100);
|
|
28
|
+
lines.push(` ${ANSI.GOLD}[${simPct}%]${ANSI.RESET} ${p.fileA} <-> ${p.fileB}`);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return lines.join('\n') + '\n';
|
|
33
|
+
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { ANSI } from '../theme.js';
|
|
2
|
+
|
|
3
|
+
export const formatHotspotGraphReport = (graphResult) => {
|
|
4
|
+
const { count, hotspots = [] } = graphResult;
|
|
5
|
+
const hasHotspots = Array.isArray(hotspots) && hotspots.length > 0;
|
|
6
|
+
if (!hasHotspots) {
|
|
7
|
+
return ` ${ANSI.LIME}✔ Zero cascading violation hotspots detected.${ANSI.RESET}\n`;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const lines = [
|
|
11
|
+
` ${ANSI.BOLD}${ANSI.PINK}Cascading Violation Hotspot Graph (${count} files with propagating debt)${ANSI.RESET}`
|
|
12
|
+
];
|
|
13
|
+
|
|
14
|
+
for (let idx = 0; idx < hotspots.length; idx++) {
|
|
15
|
+
const h = hotspots[idx];
|
|
16
|
+
const rank = idx + 1;
|
|
17
|
+
const blast = h.blastRadius;
|
|
18
|
+
const critStr = h.criticalCount > 0 ? `${ANSI.RED}${h.criticalCount} crit${ANSI.RESET}, ` : '';
|
|
19
|
+
const highStr = h.highCount > 0 ? `${ANSI.GOLD}${h.highCount} high${ANSI.RESET}, ` : '';
|
|
20
|
+
const medStr = `${h.medCount} med`;
|
|
21
|
+
|
|
22
|
+
lines.push(
|
|
23
|
+
` ${ANSI.BOLD}#${rank} ${ANSI.CYAN}${h.filePath}${ANSI.RESET} [${h.tier}, ${h.lines} LOC]`
|
|
24
|
+
);
|
|
25
|
+
lines.push(
|
|
26
|
+
` ${ANSI.RED}Cascading Risk: ${h.cascadingRisk} pts${ANSI.RESET} (Violations: ${critStr}${highStr}${medStr})`
|
|
27
|
+
);
|
|
28
|
+
lines.push(
|
|
29
|
+
` ${ANSI.DIM}Blast Radius: ${blast.totalImpact} consumers (${blast.directConsumers} direct, ${blast.impactedTests} tests across ${blast.depth} hops)${ANSI.RESET}`
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return lines.join('\n') + '\n';
|
|
34
|
+
};
|
|
@@ -117,9 +117,14 @@ export const RULE_REGISTRY = {
|
|
|
117
117
|
directive: 'Extract related state and effects into dedicated domain hooks'
|
|
118
118
|
},
|
|
119
119
|
HOOK_RETURN_OVERLOAD: {
|
|
120
|
+
pillar: PILLARS.PILLAR_3,
|
|
121
|
+
severity: 'MEDIUM',
|
|
122
|
+
directive: 'Limit hook return values to 3 to 5 properties (Deprecated: use HOOK_SHAPE_CONTRACT)'
|
|
123
|
+
},
|
|
124
|
+
HOOK_SHAPE_CONTRACT: {
|
|
120
125
|
pillar: PILLARS.PILLAR_3,
|
|
121
126
|
severity: 'HIGH',
|
|
122
|
-
directive: '
|
|
127
|
+
directive: 'Structure hook returns into flat State, Status, and verb-prefixed Actions buckets'
|
|
123
128
|
},
|
|
124
129
|
CONTROLLER_VIEW_MISMATCH: {
|
|
125
130
|
pillar: PILLARS.PILLAR_3,
|
package/cli/audit/rules.js
CHANGED
|
@@ -19,8 +19,9 @@ import {
|
|
|
19
19
|
createExtendedVisitors
|
|
20
20
|
} from './extended-visitors.js';
|
|
21
21
|
import { createPatternVisitors, recordTemplatePatterns } from './pattern-detector.js';
|
|
22
|
+
import { createHookShapeRegistry } from './hook-shape-validator.js';
|
|
22
23
|
|
|
23
|
-
export { PILLARS, RULE_REGISTRY };
|
|
24
|
+
export { PILLARS, RULE_REGISTRY, createHookShapeRegistry };
|
|
24
25
|
|
|
25
26
|
export const auditCode = (content, filePath, relativePath, options = {}) => {
|
|
26
27
|
const violations = [];
|
|
@@ -145,7 +146,8 @@ export const auditCode = (content, filePath, relativePath, options = {}) => {
|
|
|
145
146
|
}
|
|
146
147
|
|
|
147
148
|
const traverseFn = traverse.default || traverse;
|
|
148
|
-
const
|
|
149
|
+
const hookRegistry = options.hookRegistry || createHookShapeRegistry();
|
|
150
|
+
const visitors = createAstVisitors({ relativePath, violations, hookRegistry });
|
|
149
151
|
const slopVisitors = createAiSlopVisitors({ relativePath, violations });
|
|
150
152
|
const extendedVisitors = createExtendedVisitors({ relativePath, violations });
|
|
151
153
|
const patternVisitors = options.patternRegistry
|
|
@@ -169,5 +171,11 @@ export const auditCode = (content, filePath, relativePath, options = {}) => {
|
|
|
169
171
|
|
|
170
172
|
traverseFn(ast, mergedVisitors);
|
|
171
173
|
|
|
174
|
+
const shouldRunLocalConsistency = !options.hookRegistry;
|
|
175
|
+
if (shouldRunLocalConsistency) {
|
|
176
|
+
const consistencyViolations = hookRegistry.validateCrossHookConsistency();
|
|
177
|
+
violations.push(...consistencyViolations);
|
|
178
|
+
}
|
|
179
|
+
|
|
172
180
|
return violations;
|
|
173
181
|
};
|
package/cli/audit/rules.spec.js
CHANGED
|
@@ -203,3 +203,132 @@ export const debugCount = (itemCount: number) => {
|
|
|
203
203
|
assert.strictEqual(violation, undefined);
|
|
204
204
|
});
|
|
205
205
|
|
|
206
|
+
test("Audit Hook Shape Contract: passes flat hook with 4 actions, 2 state fields, 1 status field (7 total)", () => {
|
|
207
|
+
const code = `
|
|
208
|
+
export const useTodoList = () => {
|
|
209
|
+
const items = ref([]);
|
|
210
|
+
const filter = ref("all");
|
|
211
|
+
const isLoading = ref(false);
|
|
212
|
+
|
|
213
|
+
const addItem = (item) => { items.value.push(item); };
|
|
214
|
+
const toggleItem = (id) => { /* toggle */ };
|
|
215
|
+
const removeItem = (id) => { /* remove */ };
|
|
216
|
+
const setFilter = (next) => { filter.value = next; };
|
|
217
|
+
|
|
218
|
+
return {
|
|
219
|
+
items,
|
|
220
|
+
filter,
|
|
221
|
+
isLoading,
|
|
222
|
+
addItem,
|
|
223
|
+
toggleItem,
|
|
224
|
+
removeItem,
|
|
225
|
+
setFilter
|
|
226
|
+
};
|
|
227
|
+
};
|
|
228
|
+
`;
|
|
229
|
+
const violations = auditCode(code, "src/useTodoList.ts", "src/useTodoList.ts");
|
|
230
|
+
const shapeViolation = violations.find((v) => v.rule === "HOOK_SHAPE_CONTRACT");
|
|
231
|
+
assert.strictEqual(shapeViolation, undefined, "Expected HOOK_SHAPE_CONTRACT to pass for flat 7-property hook");
|
|
232
|
+
|
|
233
|
+
const overloadViolation = violations.find((v) => v.rule === "HOOK_RETURN_OVERLOAD");
|
|
234
|
+
assert.ok(overloadViolation, "Expected deprecated HOOK_RETURN_OVERLOAD warning");
|
|
235
|
+
assert.strictEqual(overloadViolation.severity, "MEDIUM", "Expected downgraded warning severity for HOOK_RETURN_OVERLOAD");
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
test("Audit Hook Shape Contract: fails hook with actions nested under actions wrapper", () => {
|
|
239
|
+
const code = `
|
|
240
|
+
export const useTodoList = () => {
|
|
241
|
+
const items = ref([]);
|
|
242
|
+
const filter = ref("all");
|
|
243
|
+
const isLoading = ref(false);
|
|
244
|
+
|
|
245
|
+
const addItem = (item) => { items.value.push(item); };
|
|
246
|
+
const toggleItem = (id) => { /* toggle */ };
|
|
247
|
+
const removeItem = (id) => { /* remove */ };
|
|
248
|
+
const setFilter = (next) => { filter.value = next; };
|
|
249
|
+
|
|
250
|
+
return {
|
|
251
|
+
items,
|
|
252
|
+
filter,
|
|
253
|
+
isLoading,
|
|
254
|
+
actions: {
|
|
255
|
+
addItem,
|
|
256
|
+
toggleItem,
|
|
257
|
+
removeItem,
|
|
258
|
+
setFilter
|
|
259
|
+
}
|
|
260
|
+
};
|
|
261
|
+
};
|
|
262
|
+
`;
|
|
263
|
+
const violations = auditCode(code, "src/useTodoList.ts", "src/useTodoList.ts");
|
|
264
|
+
const shapeViolation = violations.find((v) => v.rule === "HOOK_SHAPE_CONTRACT");
|
|
265
|
+
assert.ok(shapeViolation, "Expected HOOK_SHAPE_CONTRACT failure for nested action wrapper");
|
|
266
|
+
assert.strictEqual(shapeViolation.severity, "HIGH");
|
|
267
|
+
assert.ok(shapeViolation.hazard.includes("actions"));
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
test("Audit Hook Shape Contract: flags raw DOM ref in hook return", () => {
|
|
271
|
+
const code = `
|
|
272
|
+
export const useDropdown = () => {
|
|
273
|
+
const isOpen = ref(false);
|
|
274
|
+
const containerRef = ref(null);
|
|
275
|
+
const toggleDropdown = () => { isOpen.value = !isOpen.value; };
|
|
276
|
+
|
|
277
|
+
return {
|
|
278
|
+
isOpen,
|
|
279
|
+
containerRef,
|
|
280
|
+
toggleDropdown
|
|
281
|
+
};
|
|
282
|
+
};
|
|
283
|
+
`;
|
|
284
|
+
const violations = auditCode(code, "src/useDropdown.ts", "src/useDropdown.ts");
|
|
285
|
+
const shapeViolation = violations.find((v) => v.rule === "HOOK_SHAPE_CONTRACT");
|
|
286
|
+
assert.ok(shapeViolation, "Expected HOOK_SHAPE_CONTRACT failure for raw DOM ref");
|
|
287
|
+
assert.strictEqual(shapeViolation.severity, "HIGH");
|
|
288
|
+
assert.ok(shapeViolation.hazard.includes("containerRef"));
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
test("Audit Hook Shape Contract: flags non-verb action handler in hook return", () => {
|
|
292
|
+
const code = `
|
|
293
|
+
export const useCounter = () => {
|
|
294
|
+
const count = ref(0);
|
|
295
|
+
const onToggle = () => { count.value += 1; };
|
|
296
|
+
|
|
297
|
+
return {
|
|
298
|
+
count,
|
|
299
|
+
onToggle
|
|
300
|
+
};
|
|
301
|
+
};
|
|
302
|
+
`;
|
|
303
|
+
const violations = auditCode(code, "src/useCounter.ts", "src/useCounter.ts");
|
|
304
|
+
const shapeViolation = violations.find((v) => v.rule === "HOOK_SHAPE_CONTRACT");
|
|
305
|
+
assert.ok(shapeViolation, "Expected HOOK_SHAPE_CONTRACT failure for onToggle");
|
|
306
|
+
assert.strictEqual(shapeViolation.severity, "HIGH");
|
|
307
|
+
assert.ok(shapeViolation.hazard.includes("onToggle"));
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
test("Audit Hook Shape Contract: flags cross-hook naming inconsistency for loading state", () => {
|
|
311
|
+
const code = `
|
|
312
|
+
export const useFirst = () => {
|
|
313
|
+
const items = ref([]);
|
|
314
|
+
const isLoading = ref(false);
|
|
315
|
+
const fetchItems = () => {};
|
|
316
|
+
return { items, isLoading, fetchItems };
|
|
317
|
+
};
|
|
318
|
+
|
|
319
|
+
export const useSecond = () => {
|
|
320
|
+
const user = ref(null);
|
|
321
|
+
const loading = ref(false);
|
|
322
|
+
const fetchUser = () => {};
|
|
323
|
+
return { user, loading, fetchUser };
|
|
324
|
+
};
|
|
325
|
+
`;
|
|
326
|
+
const violations = auditCode(code, "src/useMulti.ts", "src/useMulti.ts");
|
|
327
|
+
const inconsistentViolation = violations.find(
|
|
328
|
+
(v) => v.rule === "HOOK_SHAPE_CONTRACT" && v.hazard.includes("loading")
|
|
329
|
+
);
|
|
330
|
+
assert.ok(inconsistentViolation, "Expected cross-hook naming inconsistency failure for loading vs isLoading");
|
|
331
|
+
assert.strictEqual(inconsistentViolation.severity, "HIGH");
|
|
332
|
+
assert.ok(inconsistentViolation.hazard.includes("isLoading"));
|
|
333
|
+
});
|
|
334
|
+
|
|
@@ -0,0 +1,30 @@
|
|
|
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 { runAudit } from './audit.js';
|
|
6
|
+
|
|
7
|
+
test('audit: options.stage tags the report as draft or strict', () => {
|
|
8
|
+
const tmpDir = path.resolve(process.cwd(), 'scratch/test-audit-stage');
|
|
9
|
+
if (fs.existsSync(tmpDir)) fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
10
|
+
fs.mkdirSync(tmpDir, { recursive: true });
|
|
11
|
+
|
|
12
|
+
// Sample file with an inline multi-clause boolean (MEDIUM severity)
|
|
13
|
+
const code = `export const check = (a: number, b: number, c: number) => {
|
|
14
|
+
if (a > 0 && b > 0 && c > 0) return true;
|
|
15
|
+
return false;
|
|
16
|
+
};`;
|
|
17
|
+
fs.writeFileSync(path.join(tmpDir, 'sample.ts'), code, 'utf-8');
|
|
18
|
+
|
|
19
|
+
const draftReport = runAudit(tmpDir, { stage: 'draft' });
|
|
20
|
+
assert.strictEqual(draftReport.stage, 'draft');
|
|
21
|
+
assert.ok(draftReport.totalViolations > 0);
|
|
22
|
+
|
|
23
|
+
const hasCritical = draftReport.violations.some((v) => v.severity === 'CRITICAL');
|
|
24
|
+
assert.strictEqual(hasCritical, false, 'Should have 0 critical violations');
|
|
25
|
+
|
|
26
|
+
const strictReport = runAudit(tmpDir, { stage: 'strict' });
|
|
27
|
+
assert.strictEqual(strictReport.stage, 'strict');
|
|
28
|
+
|
|
29
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
30
|
+
});
|
package/cli/audit.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
-
import { auditCode, PILLARS, RULE_REGISTRY } from './audit/rules.js';
|
|
3
|
+
import { auditCode, PILLARS, RULE_REGISTRY, createHookShapeRegistry } from './audit/rules.js';
|
|
4
4
|
import { createPatternRegistry } from './audit/pattern-detector.js';
|
|
5
5
|
import {
|
|
6
6
|
buildRemediationRoadmap,
|
|
@@ -116,6 +116,7 @@ const auditFileEntry = (fullPath, relPath, scanOptions) => {
|
|
|
116
116
|
const hookCount = hookMatches ? hookMatches.length : 0;
|
|
117
117
|
const fileViolations = auditCode(content, fullPath, relPath, {
|
|
118
118
|
patternRegistry: scanOptions.patternRegistry,
|
|
119
|
+
hookRegistry: scanOptions.hookRegistry,
|
|
119
120
|
fast: scanOptions.fast
|
|
120
121
|
});
|
|
121
122
|
|
|
@@ -174,15 +175,20 @@ export const scanDirectory = (targetDir, baseDir) => {
|
|
|
174
175
|
};
|
|
175
176
|
|
|
176
177
|
export const runAudit = (targetDir = 'src', options = {}) => {
|
|
177
|
-
const cwd = process.cwd();
|
|
178
|
-
const absoluteTarget = path.resolve(cwd, targetDir);
|
|
178
|
+
const cwd = options.cwd || process.cwd();
|
|
179
|
+
const absoluteTarget = path.isAbsolute(targetDir) ? targetDir : path.resolve(cwd, targetDir);
|
|
179
180
|
const patternRegistry = createPatternRegistry();
|
|
181
|
+
const hookRegistry = createHookShapeRegistry();
|
|
180
182
|
const { violations, fileStats, totalHooks } = scanTree(absoluteTarget, cwd, {
|
|
181
183
|
patternRegistry,
|
|
184
|
+
hookRegistry,
|
|
182
185
|
fast: Boolean(options.fast),
|
|
183
186
|
fileList: options.fileList || null
|
|
184
187
|
});
|
|
185
188
|
|
|
189
|
+
const crossHookViolations = hookRegistry.validateCrossHookConsistency();
|
|
190
|
+
violations.push(...crossHookViolations);
|
|
191
|
+
|
|
186
192
|
const scannedFiles = fileStats.length;
|
|
187
193
|
const totalLoc = fileStats.reduce((acc, f) => acc + f.lineCount, 0);
|
|
188
194
|
const avgLoc = scannedFiles > 0 ? Math.round(totalLoc / scannedFiles) : 0;
|
|
@@ -226,8 +232,11 @@ export const runAudit = (targetDir = 'src', options = {}) => {
|
|
|
226
232
|
const patterns = patternRegistry.resolveHarmonizationCandidates(hotspots);
|
|
227
233
|
const roadmap = buildRemediationRoadmap({ hotspots, violations, patterns });
|
|
228
234
|
|
|
235
|
+
const stage = options.stage || (options.relax ? 'draft' : 'strict');
|
|
236
|
+
|
|
229
237
|
const report = {
|
|
230
238
|
targetDir,
|
|
239
|
+
stage,
|
|
231
240
|
options,
|
|
232
241
|
scannedFiles,
|
|
233
242
|
totalViolations: violations.length,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* cmd-audit.js
|
|
2
|
+
* cmd-audit.js: Audit command orchestrator.
|
|
3
3
|
* Single responsibility: run the full AST audit pipeline given CLI args.
|
|
4
4
|
* Extracted from cli/index.js per Directive 1.A (Monolith Decomposition).
|
|
5
5
|
*/
|
|
@@ -47,6 +47,19 @@ export const runAudit = async (customDir, isCli, rawArgs, loadProjectConfig) =>
|
|
|
47
47
|
const isPromptOnFail = rawArgs.includes('--prompt-on-fail');
|
|
48
48
|
const isCopyPrompt = rawArgs.includes('--copy-prompt');
|
|
49
49
|
|
|
50
|
+
const stageFlag = rawArgs.find((arg) => arg.startsWith('--stage='));
|
|
51
|
+
let stage = 'strict';
|
|
52
|
+
if (stageFlag) {
|
|
53
|
+
stage = stageFlag.split('=')[1];
|
|
54
|
+
} else if (rawArgs.includes('--relax') || rawArgs.includes('--draft')) {
|
|
55
|
+
stage = 'draft';
|
|
56
|
+
} else if (isStrict) {
|
|
57
|
+
stage = 'strict';
|
|
58
|
+
} else if (projectConfig.stage) {
|
|
59
|
+
stage = projectConfig.stage;
|
|
60
|
+
}
|
|
61
|
+
const isDraft = stage === 'draft';
|
|
62
|
+
|
|
50
63
|
const dirFlag = rawArgs.find((arg) => arg.startsWith('--dir='));
|
|
51
64
|
const outputFlag = rawArgs.find((arg) => arg.startsWith('--output=') || arg.startsWith('-o='));
|
|
52
65
|
const outputFile = outputFlag ? outputFlag.split('=')[1] : null;
|
|
@@ -88,23 +101,53 @@ export const runAudit = async (customDir, isCli, rawArgs, loadProjectConfig) =>
|
|
|
88
101
|
fileList = preflight.fileList;
|
|
89
102
|
}
|
|
90
103
|
|
|
91
|
-
const auditOptions = { outputFile, model, costPerMillion, fast: isFast, fileList };
|
|
104
|
+
const auditOptions = { outputFile, model, costPerMillion, fast: isFast, fileList, stage };
|
|
92
105
|
const report = executeAstAudit(targetDir, auditOptions);
|
|
93
106
|
saveAuditSnapshot(report);
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
107
|
+
try {
|
|
108
|
+
const syncRes = syncSearchIndex(targetDir, process.cwd());
|
|
109
|
+
|
|
110
|
+
if (syncRes?.db) {
|
|
111
|
+
syncViolationsIndex(syncRes.db, report.violations);
|
|
112
|
+
const shouldTriage = !rawArgs.includes('--no-triage');
|
|
113
|
+
if (shouldTriage) {
|
|
114
|
+
const createdTasks = autoGenerateTasksFromAudit(syncRes.db, { cwd: process.cwd(), targetDir });
|
|
115
|
+
if (isCli && !isJson && createdTasks.length > 0) {
|
|
116
|
+
process.stdout.write(`\x1b[32m✔\x1b[0m Auto-triage synchronized ${createdTasks.length} team task(s) in SQLite backlog.\n`);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
if (rawArgs.includes('--clones')) {
|
|
120
|
+
const { detectSemanticClones } = await import('../audit/clone-detector.js');
|
|
121
|
+
const { formatCloneReport } = await import('../audit/reporter-clones.js');
|
|
122
|
+
const cloneThresholdArg = rawArgs.find((a) => a.startsWith('--clone-threshold='));
|
|
123
|
+
const threshold = cloneThresholdArg ? parseFloat(cloneThresholdArg.split('=')[1]) : 0.85;
|
|
124
|
+
const clones = detectSemanticClones(syncRes.db, { threshold });
|
|
125
|
+
if (isJson) {
|
|
126
|
+
report.clones = clones;
|
|
127
|
+
} else {
|
|
128
|
+
process.stdout.write(formatCloneReport(clones));
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
if (rawArgs.includes('--hotspot-graph')) {
|
|
132
|
+
const { calculateCascadingHotspotGraph } = await import('../search-queries-hotspot-graph.js');
|
|
133
|
+
const { formatHotspotGraphReport } = await import('../audit/reporter-hotspot-graph.js');
|
|
134
|
+
const limitArg = rawArgs.find((a) => a.startsWith('--limit='));
|
|
135
|
+
const limit = limitArg ? parseInt(limitArg.split('=')[1], 10) : 10;
|
|
136
|
+
const graph = calculateCascadingHotspotGraph(syncRes.db, { limit });
|
|
137
|
+
if (isJson) {
|
|
138
|
+
report.hotspotGraph = graph;
|
|
139
|
+
} else {
|
|
140
|
+
process.stdout.write(formatHotspotGraphReport(graph));
|
|
141
|
+
}
|
|
103
142
|
}
|
|
104
143
|
}
|
|
144
|
+
} catch {
|
|
145
|
+
// Gracefully bypass indexing when database is readonly in sandboxed environment
|
|
105
146
|
}
|
|
106
147
|
|
|
107
148
|
// Stage 1: Atomic failure predicates
|
|
149
|
+
const isCriticalViolation = (v) => v.severity === 'CRITICAL';
|
|
150
|
+
const hasCritical = report.violations.some(isCriticalViolation);
|
|
108
151
|
const isSevereViolation = (v) => v.severity === 'CRITICAL' || v.severity === 'HIGH';
|
|
109
152
|
const hasCriticalOrHigh = report.violations.some(isSevereViolation);
|
|
110
153
|
const hasViolations = report.violations.length > 0;
|
|
@@ -116,7 +159,8 @@ export const runAudit = async (customDir, isCli, rawArgs, loadProjectConfig) =>
|
|
|
116
159
|
const isDefaultFail = !hasThreshold && !isStrict && hasCriticalOrHigh;
|
|
117
160
|
|
|
118
161
|
// Stage 2: Unified failure decision
|
|
119
|
-
const
|
|
162
|
+
const hasStandardFailure = evaluateAuditFailure([isStrictFail, isDefaultFail, isGradeFail, isScoreFail]);
|
|
163
|
+
const hasFailingViolations = isDraft ? hasCritical : hasStandardFailure;
|
|
120
164
|
|
|
121
165
|
if (isJson) {
|
|
122
166
|
process.stdout.write(JSON.stringify(report, null, 2) + '\n');
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* cmd-project.js: Persistent Coordinator and Projects CLI Handler
|
|
3
|
+
* Dispatches init, status, step, chat, pause, resume, and learnings
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { openIndexDb } from '../search-db.js';
|
|
7
|
+
import {
|
|
8
|
+
initProjectSession, getActiveProjectSession, getProjectSession,
|
|
9
|
+
postProjectMessage, getProjectMessages, updateProjectSession
|
|
10
|
+
} from '../team/team-projects.js';
|
|
11
|
+
import { queryRelevantLearnings, formatLearningsCard } from '../team/team-projects-memory.js';
|
|
12
|
+
import { executeCoordinatorStep } from '../team/team-projects-coordinator.js';
|
|
13
|
+
import { formatProjectStatusCard } from '../team/team-projects-format.js';
|
|
14
|
+
import { listTasks } from '../team/team-db-tasks.js';
|
|
15
|
+
|
|
16
|
+
export const runProjectCli = async (rawArgs = [], isCli = false, cwd = process.cwd()) => {
|
|
17
|
+
const db = openIndexDb(cwd);
|
|
18
|
+
if (!db) {
|
|
19
|
+
if (isCli) process.stderr.write('\x1b[31m✕ SQLite database unavailable.\x1b[0m\n');
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const isJson = rawArgs.includes('--json');
|
|
24
|
+
const nonFlag = rawArgs.filter((a) => !a.startsWith('-'));
|
|
25
|
+
const subCommand = nonFlag[0] || 'status';
|
|
26
|
+
const restText = nonFlag.slice(1).join(' ');
|
|
27
|
+
|
|
28
|
+
if (subCommand === 'init' || subCommand === 'start') {
|
|
29
|
+
const goal = restText || 'Autonomous codebase optimization';
|
|
30
|
+
const session = initProjectSession(db, { goal, title: goal.slice(0, 40) });
|
|
31
|
+
if (isCli) {
|
|
32
|
+
if (isJson) process.stdout.write(`${JSON.stringify(session, null, 2)}\n`);
|
|
33
|
+
else process.stdout.write(`\x1b[32m✔\x1b[0m Initialized project session #${session.id}: "${session.title}"\n`);
|
|
34
|
+
}
|
|
35
|
+
return session;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (subCommand === 'step' || subCommand === 'turn') {
|
|
39
|
+
const res = executeCoordinatorStep(db, { cwd });
|
|
40
|
+
if (isCli) {
|
|
41
|
+
if (isJson) process.stdout.write(`${JSON.stringify(res, null, 2)}\n`);
|
|
42
|
+
else process.stdout.write(`\x1b[36m⚡ [Turn ${res.turn || 0}]\x1b[0m ${res.action || res.status} - Status: ${res.status}\n`);
|
|
43
|
+
}
|
|
44
|
+
return res;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (subCommand === 'chat' || subCommand === 'msg') {
|
|
48
|
+
const active = getActiveProjectSession(db);
|
|
49
|
+
if (!active) {
|
|
50
|
+
if (isCli) process.stderr.write('\x1b[31m✕ No active project. Run "chemx project init <goal>" first.\x1b[0m\n');
|
|
51
|
+
return { error: 'no_active_session' };
|
|
52
|
+
}
|
|
53
|
+
const msg = postProjectMessage(db, {
|
|
54
|
+
projectId: active.id,
|
|
55
|
+
turnIndex: active.current_turn,
|
|
56
|
+
authorId: '@user',
|
|
57
|
+
role: 'user',
|
|
58
|
+
message: restText || 'Status report'
|
|
59
|
+
});
|
|
60
|
+
if (isCli) {
|
|
61
|
+
if (isJson) process.stdout.write(`${JSON.stringify(msg, null, 2)}\n`);
|
|
62
|
+
else process.stdout.write(`\x1b[32m✔\x1b[0m Sent message to coordinator: "${msg.message}"\n`);
|
|
63
|
+
}
|
|
64
|
+
return msg;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (subCommand === 'learnings' || subCommand === 'memory') {
|
|
68
|
+
const learnings = queryRelevantLearnings(db, { limit: 10 });
|
|
69
|
+
if (isCli) {
|
|
70
|
+
if (isJson) process.stdout.write(`${JSON.stringify(learnings, null, 2)}\n`);
|
|
71
|
+
else process.stdout.write(formatLearningsCard(learnings));
|
|
72
|
+
}
|
|
73
|
+
return learnings;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (subCommand === 'pause' || subCommand === 'resume') {
|
|
77
|
+
const active = getActiveProjectSession(db);
|
|
78
|
+
if (!active) return null;
|
|
79
|
+
const newStatus = subCommand === 'pause' ? 'paused' : 'active';
|
|
80
|
+
const updated = updateProjectSession(db, active.id, { status: newStatus });
|
|
81
|
+
if (isCli) process.stdout.write(`\x1b[32m✔\x1b[0m Project #${active.id} status updated to: ${newStatus}\n`);
|
|
82
|
+
return updated;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Default: status
|
|
86
|
+
const session = getActiveProjectSession(db);
|
|
87
|
+
const messages = session ? getProjectMessages(db, session.id, 5) : [];
|
|
88
|
+
const activeTasks = session ? listTasks(db, { status: 'in_progress' }) : [];
|
|
89
|
+
const learnings = queryRelevantLearnings(db, { limit: 5 });
|
|
90
|
+
|
|
91
|
+
if (isJson) {
|
|
92
|
+
const payload = { session, messages, activeTasks, learnings };
|
|
93
|
+
if (isCli) process.stdout.write(`${JSON.stringify(payload, null, 2)}\n`);
|
|
94
|
+
return payload;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const card = formatProjectStatusCard({ session, messages, activeTasks, learnings });
|
|
98
|
+
if (isCli) process.stdout.write(card);
|
|
99
|
+
return { session, messages, activeTasks, learnings };
|
|
100
|
+
};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* cmd-router.js
|
|
2
|
+
* cmd-router.js: CLI command dispatch table.
|
|
3
3
|
* Single responsibility: map first-arg tokens to their lazy-loaded command handlers.
|
|
4
4
|
* Extracted from cli/index.js per Directive 1.A (Monolith Decomposition).
|
|
5
5
|
*/
|
|
@@ -23,6 +23,25 @@ export const dispatchCommand = async (firstArg, rawArgs, runAudit, getPackageVer
|
|
|
23
23
|
runTeamCli(rawArgs.slice(1), true);
|
|
24
24
|
break;
|
|
25
25
|
}
|
|
26
|
+
case 'project':
|
|
27
|
+
case 'coordinator': {
|
|
28
|
+
const { runProjectCli } = await import('./cmd-project.js');
|
|
29
|
+
await runProjectCli(rawArgs.slice(1), true);
|
|
30
|
+
break;
|
|
31
|
+
}
|
|
32
|
+
case 'tokens':
|
|
33
|
+
case 'telemetry': {
|
|
34
|
+
const { runTeamCli } = await import('../team/index.js');
|
|
35
|
+
runTeamCli(['tokens', ...rawArgs.slice(1)], true);
|
|
36
|
+
break;
|
|
37
|
+
}
|
|
38
|
+
case 'benchmark':
|
|
39
|
+
case 'ablation':
|
|
40
|
+
case 'memory': {
|
|
41
|
+
const { runTeamCli } = await import('../team/index.js');
|
|
42
|
+
runTeamCli(['benchmark', ...rawArgs.slice(1)], true);
|
|
43
|
+
break;
|
|
44
|
+
}
|
|
26
45
|
case 'mcp':
|
|
27
46
|
case 'mcp-server':
|
|
28
47
|
case 'server': {
|
|
@@ -31,7 +50,8 @@ export const dispatchCommand = async (firstArg, rawArgs, runAudit, getPackageVer
|
|
|
31
50
|
break;
|
|
32
51
|
}
|
|
33
52
|
case 'read':
|
|
34
|
-
case 'view':
|
|
53
|
+
case 'view':
|
|
54
|
+
case 'r': {
|
|
35
55
|
const { runReaderCli } = await import('../reader.js');
|
|
36
56
|
runReaderCli(rawArgs.slice(1), true);
|
|
37
57
|
break;
|
|
@@ -67,6 +87,12 @@ export const dispatchCommand = async (firstArg, rawArgs, runAudit, getPackageVer
|
|
|
67
87
|
await runAudit(posDir, true);
|
|
68
88
|
break;
|
|
69
89
|
}
|
|
90
|
+
case 'trend':
|
|
91
|
+
case 'trends': {
|
|
92
|
+
const { runTrend } = await import('../trend.js');
|
|
93
|
+
runTrend(rawArgs.slice(1), process.cwd());
|
|
94
|
+
break;
|
|
95
|
+
}
|
|
70
96
|
case 'init': {
|
|
71
97
|
const { runInit } = await import('../scaffold.js');
|
|
72
98
|
await runInit(rawArgs[1] || 'src/chemical-x', rawArgs, runAudit);
|
|
@@ -107,6 +133,12 @@ export const dispatchCommand = async (firstArg, rawArgs, runAudit, getPackageVer
|
|
|
107
133
|
await runMutatorCli(rawArgs, true);
|
|
108
134
|
break;
|
|
109
135
|
}
|
|
136
|
+
case 'explode':
|
|
137
|
+
case 'unpack': {
|
|
138
|
+
const { runExplodeCli } = await import('../exploder.js');
|
|
139
|
+
await runExplodeCli(rawArgs.slice(1), true);
|
|
140
|
+
break;
|
|
141
|
+
}
|
|
110
142
|
case 'add': {
|
|
111
143
|
if (['prop', 'state', 'action'].includes(rawArgs[1])) {
|
|
112
144
|
const { runMutatorCli } = await import('../mutators.js');
|
|
@@ -168,7 +200,24 @@ export const dispatchCommand = async (firstArg, rawArgs, runAudit, getPackageVer
|
|
|
168
200
|
const { startUiServer } = await import('../ui-server.js');
|
|
169
201
|
const portArg = rawArgs.find((a) => a.startsWith('--port='));
|
|
170
202
|
const port = portArg ? parseInt(portArg.split('=')[1], 10) : 4173;
|
|
171
|
-
|
|
203
|
+
const hostArg = rawArgs.find((a) => a.startsWith('--host='));
|
|
204
|
+
const host = hostArg ? hostArg.split('=')[1] : '0.0.0.0';
|
|
205
|
+
const { server } = await startUiServer({ port, host, isCli: true, cwd: process.cwd() });
|
|
206
|
+
await new Promise((resolve) => {
|
|
207
|
+
const shutdown = () => {
|
|
208
|
+
server.close(() => resolve());
|
|
209
|
+
};
|
|
210
|
+
process.on('SIGINT', shutdown);
|
|
211
|
+
process.on('SIGTERM', shutdown);
|
|
212
|
+
server.on('close', resolve);
|
|
213
|
+
});
|
|
214
|
+
break;
|
|
215
|
+
}
|
|
216
|
+
case 'tesseract':
|
|
217
|
+
case 'cube':
|
|
218
|
+
case 'matrix': {
|
|
219
|
+
const { runTesseract } = await import('../tesseract.js');
|
|
220
|
+
await runTesseract(rawArgs.slice(1), true);
|
|
172
221
|
break;
|
|
173
222
|
}
|
|
174
223
|
case 'help':
|
package/cli/commands-schema.js
CHANGED
|
@@ -194,5 +194,33 @@ export const COMMANDS_SCHEMA = [
|
|
|
194
194
|
'npx chemx pillars --preset=recommended',
|
|
195
195
|
'npx chemx pillars --preset=minimal --dry-run'
|
|
196
196
|
]
|
|
197
|
+
},
|
|
198
|
+
{
|
|
199
|
+
name: 'ui',
|
|
200
|
+
aliases: ['dashboard', 'preview'],
|
|
201
|
+
usage: 'npx chemx ui [options]',
|
|
202
|
+
summary: 'Launch the Chemical X interactive web dashboard.',
|
|
203
|
+
description: 'Provides real-time Kanban task boards, agent rails, live SQLite Database Studio, and codebase AST tree exploration.',
|
|
204
|
+
flags: [
|
|
205
|
+
{ flag: '--port=<N>', desc: 'Server port (default: 4173)' }
|
|
206
|
+
],
|
|
207
|
+
examples: [
|
|
208
|
+
'npx chemx ui',
|
|
209
|
+
'npx chemx dashboard --port=3000'
|
|
210
|
+
]
|
|
211
|
+
},
|
|
212
|
+
{
|
|
213
|
+
name: 'tesseract',
|
|
214
|
+
aliases: ['cube', 'matrix'],
|
|
215
|
+
usage: 'npx chemx tesseract [options]',
|
|
216
|
+
summary: 'Autonomous AI Cognitive Bridge and Human-AI Symbiosis Interface.',
|
|
217
|
+
description: 'Deploys the full Chemical X cognitive lattice, AST topology, swarm telemetry, and Jarvis operational directives for instant AI onboarding.',
|
|
218
|
+
flags: [
|
|
219
|
+
{ flag: '--json', desc: 'Output machine-readable JSON cognitive payload' }
|
|
220
|
+
],
|
|
221
|
+
examples: [
|
|
222
|
+
'npx chemx tesseract',
|
|
223
|
+
'npx chemx tesseract --json'
|
|
224
|
+
]
|
|
197
225
|
}
|
|
198
226
|
];
|