@chem-x/starter-kit 26.9.20-1257 → 26.9.20-1298

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/README.md CHANGED
@@ -215,6 +215,11 @@ npx chemx --version
215
215
  # create-chemx v26.9.17-606
216
216
  ```
217
217
 
218
+ ### Package Pairing & npm Registry Propagation
219
+
220
+ - **Package Pairing**: `@chemx/create-chemx` and `chemx` publish together under synchronized minute-precision CalVer timestamps (`YY.M.D-minute`). For consistent behavior, install or pin the matching release versions.
221
+ - **npm Registry Propagation Note**: Newly published releases on npm can take 2–5 minutes to propagate across all edge CDN mirrors after `npm view` lists them. If `npm install` intermittently fails with a 404 on an exact newly published version, wait a few minutes and retry.
222
+
218
223
  Because autonomous coding agent models update frequently, this package is continuously integrated and deployed via automated CI whenever new agent directives, AST checks, or framework rules are tuned. Rapid version iterations reflect active, daily alignment rather than breaking SemVer shifts.
219
224
 
220
225
  > [!NOTE]
@@ -10,8 +10,10 @@
10
10
  "verify": "chemx verify"
11
11
  },
12
12
  "devDependencies": {
13
+ "@vue/test-utils": "^2.4.6",
13
14
  "create-chemx": "latest",
14
15
  "typescript": "^5.4.0",
15
- "vitest": "^1.6.0"
16
+ "vitest": "^1.6.0",
17
+ "vue": "^3.4.0"
16
18
  }
17
19
  }
@@ -7,7 +7,7 @@ export const COLLECTION_ARCHETYPES = [
7
7
  id: 'task-list',
8
8
  name: 'Collection & Task List',
9
9
  keywords: ['task', 'todo', 'tasklist', 'checklist', 'item', 'queue', 'list', 'add', 'toggle', 'remove', 'delete'],
10
- destructure: 'items, activeCount, setFilter, toggleItem, removeItem',
10
+ destructure: 'items, activeCount, setFilter, toggleItem, removeItem, addItem',
11
11
  buildState: (name, pascal) => `export interface ${pascal}Item {
12
12
  readonly id: string;
13
13
  readonly title: string;
@@ -25,22 +25,23 @@ export interface ${pascal}State {
25
25
  buildProps: (name, pascal) => `export interface ${pascal}Props {
26
26
  readonly initialItems?: readonly ${pascal}Item[];
27
27
  readonly onItemsChange?: (items: readonly ${pascal}Item[]) => void;
28
+ readonly className?: string;
28
29
  }
29
30
  `,
30
31
  buildController: (name, pascal) => `import { useState, useMemo } from 'react';
31
- import type { ${pascal}Item, ${pascal}Filter, ${pascal}State } from './types';
32
+ import type { ${pascal}Item, ${pascal}Filter } from './types';
32
33
 
33
34
  export const use${pascal}Controller = (options: { initialItems?: readonly ${pascal}Item[]; onItemsChange?: (items: readonly ${pascal}Item[]) => void } = {}) => {
34
35
  const [items, setItems] = useState<readonly ${pascal}Item[]>(options.initialItems || [
35
- { id: '1', title: 'Initialize Chemical X molecular structure', completed: true },
36
- { id: '2', title: 'Implement domain controller state', completed: false }
36
+ { id: '1', title: 'Review Chemical X molecular standards', completed: true },
37
+ { id: '2', title: 'Audit atomic and molecular tier boundaries', completed: false }
37
38
  ]);
38
39
  const [filter, setFilter] = useState<${pascal}Filter>('all');
39
40
 
40
- const activeCount = useMemo(() => items.filter((i) => !i.completed).length, [items]);
41
+ const activeCount = useMemo(() => items.filter((i: ${pascal}Item) => !i.completed).length, [items]);
41
42
  const visibleItems = useMemo(() => {
42
- if (filter === 'active') return items.filter((i) => !i.completed);
43
- if (filter === 'completed') return items.filter((i) => i.completed);
43
+ if (filter === 'active') return items.filter((i: ${pascal}Item) => !i.completed);
44
+ if (filter === 'completed') return items.filter((i: ${pascal}Item) => i.completed);
44
45
  return items;
45
46
  }, [items, filter]);
46
47
 
@@ -52,13 +53,13 @@ export const use${pascal}Controller = (options: { initialItems?: readonly ${pasc
52
53
  };
53
54
 
54
55
  const toggleItem = (id: string) => {
55
- const next = items.map((i) => (i.id === id ? { ...i, completed: !i.completed } : i));
56
+ const next = items.map((i: ${pascal}Item) => (i.id === id ? { ...i, completed: !i.completed } : i));
56
57
  setItems(next);
57
58
  options.onItemsChange?.(next);
58
59
  };
59
60
 
60
61
  const removeItem = (id: string) => {
61
- const next = items.filter((i) => i.id !== id);
62
+ const next = items.filter((i: ${pascal}Item) => i.id !== id);
62
63
  setItems(next);
63
64
  options.onItemsChange?.(next);
64
65
  };
@@ -66,17 +67,44 @@ export const use${pascal}Controller = (options: { initialItems?: readonly ${pasc
66
67
  return { items: visibleItems, allItems: items, filter, activeCount, setFilter, addItem, toggleItem, removeItem };
67
68
  };
68
69
  `,
69
- buildReactBody: (name, pascal) => ` <div className="${name}__controls">
70
- <span className="${name}__count">{activeCount} pending</span>
70
+ buildReactBody: (name, pascal) => ` <form
71
+ className="${name}__form"
72
+ onSubmit={(e) => {
73
+ e.preventDefault();
74
+ const target = e.currentTarget;
75
+ const input = target.elements.namedItem('taskTitle') as HTMLInputElement;
76
+ if (input && input.value.trim()) {
77
+ addItem(input.value.trim());
78
+ input.value = '';
79
+ }
80
+ }}
81
+ >
82
+ <input
83
+ name="taskTitle"
84
+ type="text"
85
+ placeholder="Add a new task..."
86
+ className="${name}__input"
87
+ />
88
+ <button type="submit" className="${name}__add-btn">Add</button>
89
+ </form>
90
+ <div className="${name}__controls">
91
+ <span className="${name}__counter">{activeCount} active</span>
71
92
  <div className="${name}__filters">
72
- <button type="button" onClick={() => setFilter('all')}>All</button>
73
- <button type="button" onClick={() => setFilter('active')}>Active</button>
74
- <button type="button" onClick={() => setFilter('completed')}>Done</button>
93
+ {(['all', 'active', 'completed'] as const).map((f) => (
94
+ <button
95
+ key={f}
96
+ type="button"
97
+ className={filter === f ? 'active' : ''}
98
+ onClick={() => setFilter(f)}
99
+ >
100
+ {f}
101
+ </button>
102
+ ))}
75
103
  </div>
76
104
  </div>
77
105
  <ul className="${name}__list">
78
106
  {items.map((item) => (
79
- <li key={item.id} className={item.completed ? '${name}__item ${name}__item--done' : '${name}__item'}>
107
+ <li key={item.id} className={item.completed ? 'completed' : ''}>
80
108
  <input type="checkbox" checked={item.completed} onChange={() => toggleItem(item.id)} />
81
109
  <span>{item.title}</span>
82
110
  <button type="button" onClick={() => removeItem(item.id)}>×</button>
@@ -253,7 +281,7 @@ export const use${pascal}Controller = (options: { cards?: readonly ${pascal}Card
253
281
  const [activeCardId, setActiveCardId] = useState<string | null>(null);
254
282
 
255
283
  const moveCard = (cardId: string, toColumnId: string) => {
256
- const next = cards.map((c) => (c.id === cardId ? { ...c, columnId: toColumnId } : c));
284
+ const next = cards.map((c: ${pascal}Card) => (c.id === cardId ? { ...c, columnId: toColumnId } : c));
257
285
  setCards(next);
258
286
  options.onCardMove?.(cardId, toColumnId);
259
287
  };
@@ -146,8 +146,8 @@ export const use${pascal}Controller = (options: { onCheckout?: (total: number) =
146
146
  const updateQuantity = (id: string, delta: number) => {
147
147
  setItems((prev) =>
148
148
  prev
149
- .map((i) => (i.id === id ? { ...i, quantity: Math.max(0, i.quantity + delta) } : i))
150
- .filter((i) => i.quantity > 0)
149
+ .map((i: ${pascal}CartItem) => (i.id === id ? { ...i, quantity: Math.max(0, i.quantity + delta) } : i))
150
+ .filter((i: ${pascal}CartItem) => i.quantity > 0)
151
151
  );
152
152
  };
153
153
 
@@ -48,13 +48,37 @@ export const buildHookIndex = (name, camelName) => `export { ${camelName} } from
48
48
  export type * from './types';
49
49
  `;
50
50
 
51
- export const buildHookSpec = (name, camelName) => `import { describe, it } from 'node:test';
51
+ export const buildHookSpec = (name, camelName, runner = 'node:test') => {
52
+ if (runner === 'vitest') {
53
+ return `import { describe, it, expect } from 'vitest';
54
+ import { ${camelName} } from './${name}';
55
+
56
+ describe('${camelName} Hook Composable', () => {
57
+ it('is defined as a hook function', () => {
58
+ expect(typeof ${camelName}).toBe('function');
59
+ });
60
+ });
61
+ `;
62
+ }
63
+ if (runner === 'jest') {
64
+ return `import { describe, it, expect } from '@jest/globals';
65
+ import { ${camelName} } from './${name}';
66
+
67
+ describe('${camelName} Hook Composable', () => {
68
+ it('is defined as a hook function', () => {
69
+ expect(typeof ${camelName}).toBe('function');
70
+ });
71
+ });
72
+ `;
73
+ }
74
+ return `import { describe, it } from 'node:test';
52
75
  import assert from 'node:assert/strict';
53
- import { ${camelName} } from './${name}.ts';
76
+ import { ${camelName} } from './${name}';
54
77
 
55
78
  describe('${camelName} Hook Composable', () => {
56
- it('exports pure hook function', () => {
79
+ it('is defined as a hook function', () => {
57
80
  assert.equal(typeof ${camelName}, 'function');
58
81
  });
59
82
  });
60
83
  `;
84
+ };
@@ -19,8 +19,8 @@ export const buildReactComponent = (name, pascalName, options = {}) => {
19
19
  }
20
20
 
21
21
  const stateBindings = hasController
22
- ? ` const { ${archetype.destructure} } = { ...props, ...use${pascalName}Controller(props as any) };`
23
- : ` const { ${archetype.destructure} } = props as any;`;
22
+ ? ` const { ${archetype.destructure} } = { ...props, ...use${pascalName}Controller(props) };`
23
+ : ` const { ${archetype.destructure} } = props;`;
24
24
 
25
25
  return `import React from 'react';
26
26
  ${atomImport}import type { ${pascalName}Props } from './types';
@@ -1,8 +1,54 @@
1
- export const buildComponentSpec = (name, pascalName, hasController = true) => {
1
+ export const buildComponentSpec = (name, pascalName, hasController = true, runner = 'node:test') => {
2
+ if (runner === 'vitest') {
3
+ if (hasController) {
4
+ return `import { describe, it, expect } from 'vitest';
5
+ import { use${pascalName}Controller } from './${name}.controller';
6
+
7
+ describe('${pascalName} Capsule Controller', () => {
8
+ it('exports pure controller hook', () => {
9
+ expect(typeof use${pascalName}Controller).toBe('function');
10
+ });
11
+ });
12
+ `;
13
+ }
14
+
15
+ return `import { describe, it, expect } from 'vitest';
16
+
17
+ describe('${pascalName} Atom Foundation', () => {
18
+ it('defines foundational UI atom contract', () => {
19
+ expect(true).toBe(true);
20
+ });
21
+ });
22
+ `;
23
+ }
24
+
25
+ if (runner === 'jest') {
26
+ if (hasController) {
27
+ return `import { describe, it, expect } from '@jest/globals';
28
+ import { use${pascalName}Controller } from './${name}.controller';
29
+
30
+ describe('${pascalName} Capsule Controller', () => {
31
+ it('exports pure controller hook', () => {
32
+ expect(typeof use${pascalName}Controller).toBe('function');
33
+ });
34
+ });
35
+ `;
36
+ }
37
+
38
+ return `import { describe, it, expect } from '@jest/globals';
39
+
40
+ describe('${pascalName} Atom Foundation', () => {
41
+ it('defines foundational UI atom contract', () => {
42
+ expect(true).toBe(true);
43
+ });
44
+ });
45
+ `;
46
+ }
47
+
2
48
  if (hasController) {
3
49
  return `import { describe, it } from 'node:test';
4
50
  import assert from 'node:assert/strict';
5
- import { use${pascalName}Controller } from './${name}.controller.ts';
51
+ import { use${pascalName}Controller } from './${name}.controller';
6
52
 
7
53
  describe('${pascalName} Capsule Controller', () => {
8
54
  it('exports pure controller hook', () => {
@@ -65,12 +65,37 @@ export type * from './types';
65
65
  `;
66
66
  };
67
67
 
68
- export const buildViewSpec = (name, pascalName) => `import { describe, it } from 'node:test';
68
+ export const buildViewSpec = (name, pascalName, runner = 'node:test') => {
69
+ if (runner === 'vitest') {
70
+ return `import { describe, it, expect } from 'vitest';
71
+ import { ${pascalName}View } from './${name}';
72
+
73
+ describe('${pascalName}View Table of Contents', () => {
74
+ it('is defined as a view component', () => {
75
+ expect(typeof ${pascalName}View).toBe('function');
76
+ });
77
+ });
78
+ `;
79
+ }
80
+ if (runner === 'jest') {
81
+ return `import { describe, it, expect } from '@jest/globals';
82
+ import { ${pascalName}View } from './${name}';
83
+
84
+ describe('${pascalName}View Table of Contents', () => {
85
+ it('is defined as a view component', () => {
86
+ expect(typeof ${pascalName}View).toBe('function');
87
+ });
88
+ });
89
+ `;
90
+ }
91
+ return `import { describe, it } from 'node:test';
69
92
  import assert from 'node:assert/strict';
93
+ import { ${pascalName}View } from './${name}';
70
94
 
71
95
  describe('${pascalName}View Table of Contents', () => {
72
- it('defines pure declarative blueprint layout', () => {
73
- assert.ok(true);
96
+ it('is defined as a view component', () => {
97
+ assert.equal(typeof ${pascalName}View, 'function');
74
98
  });
75
99
  });
76
100
  `;
101
+ };
package/cli/generator.js CHANGED
@@ -27,7 +27,7 @@ import {
27
27
  buildViewIndex,
28
28
  buildViewSpec
29
29
  } from './generator-templates.js';
30
- import { detectFramework, detectTierBaseDir, detectInstalledFamily } from './project-detector.js';
30
+ import { detectFramework, detectTierBaseDir, detectInstalledFamily, detectTestRunner } from './project-detector.js';
31
31
 
32
32
  const TIERS = [
33
33
  { prefix: 'm-', tier: 'molecule', label: '1. m- Molecule (Self-contained feature block < 100 lines - Recommended)' },
@@ -112,6 +112,7 @@ export const createCapsuleFiles = ({
112
112
  const parentDir = targetParent || detectedDir;
113
113
  const resolvedParent = path.resolve(cwd, parentDir || '.');
114
114
  const targetDir = path.resolve(resolvedParent, capsuleName);
115
+ const runner = detectTestRunner(targetDir || cwd);
115
116
 
116
117
  if (!dryRun && fs.existsSync(targetDir)) {
117
118
  throw new Error(`Directory ${capsuleName} already exists at ${targetDir}.`);
@@ -142,7 +143,7 @@ export const createCapsuleFiles = ({
142
143
 
143
144
  recordFile(hookFile, path.join(targetDir, hookFile), buildHook(capsuleName, camelName, pascalName));
144
145
  recordFile('index.ts', path.join(targetDir, 'index.ts'), buildHookIndex(capsuleName, camelName));
145
- recordFile(specFile, path.join(targetDir, specFile), buildHookSpec(capsuleName, camelName));
146
+ recordFile(specFile, path.join(targetDir, specFile), buildHookSpec(capsuleName, camelName, runner));
146
147
 
147
148
  recordFile('types/options.d.ts', path.join(typesDir, 'options.d.ts'), buildHookOptionsType(pascalName));
148
149
  recordFile('types/return.d.ts', path.join(typesDir, 'return.d.ts'), buildHookReturnType(pascalName));
@@ -154,7 +155,7 @@ export const createCapsuleFiles = ({
154
155
 
155
156
  recordFile(viewFile, path.join(targetDir, viewFile), selectedFramework.viewBuilder(capsuleName, pascalName));
156
157
  recordFile('index.ts', path.join(targetDir, 'index.ts'), buildViewIndex(capsuleName, pascalName, selectedFramework.ext));
157
- recordFile(specFile, path.join(targetDir, specFile), buildViewSpec(capsuleName, pascalName));
158
+ recordFile(specFile, path.join(targetDir, specFile), buildViewSpec(capsuleName, pascalName, runner));
158
159
 
159
160
  recordFile('types/params.d.ts', path.join(typesDir, 'params.d.ts'), buildViewParamsType(pascalName));
160
161
  recordFile('types/index.ts', path.join(typesDir, 'index.ts'), buildTypesIndex(['params']));
@@ -168,7 +169,7 @@ export const createCapsuleFiles = ({
168
169
 
169
170
  recordFile(compFile, path.join(targetDir, compFile), selectedFramework.compBuilder(capsuleName, pascalName, templateOpts));
170
171
  recordFile('index.ts', path.join(targetDir, 'index.ts'), buildIndex(capsuleName, pascalName, selectedFramework.ext, hasController));
171
- recordFile(specFile, path.join(targetDir, specFile), buildComponentSpec(capsuleName, pascalName, hasController));
172
+ recordFile(specFile, path.join(targetDir, specFile), buildComponentSpec(capsuleName, pascalName, hasController, runner));
172
173
 
173
174
  recordFile('types/props.d.ts', path.join(typesDir, 'props.d.ts'), buildPropsType(capsuleName, pascalName, { description: capsuleDesc }));
174
175
  recordFile('types/state.d.ts', path.join(typesDir, 'state.d.ts'), buildStateType(capsuleName, pascalName, { description: capsuleDesc }));
@@ -198,5 +198,62 @@ test('runGenerateWizard: --json with --help returns help object without mutation
198
198
  assert.strictEqual(res.help, true);
199
199
  });
200
200
 
201
+ test('runGenerateWizard: description-driven task list wires addItem, toggleItem, removeItem and avoids implicit any or as any', async () => {
202
+ const tmpDir = path.resolve(process.cwd(), 'scratch/test-gen-tasklist');
203
+ if (fs.existsSync(tmpDir)) fs.rmSync(tmpDir, { recursive: true, force: true });
204
+
205
+ const res = await runGenerateWizard([
206
+ 'molecule',
207
+ 'task-list',
208
+ '-y',
209
+ `--dir=${tmpDir}`,
210
+ '--framework=react',
211
+ '--desc=add, toggle, and remove tasks'
212
+ ]);
213
+
214
+ assert.strictEqual(res.success, true);
215
+ const capsuleDir = path.join(tmpDir, 'm-task-list');
216
+ const tsxContent = fs.readFileSync(path.join(capsuleDir, 'm-task-list.tsx'), 'utf8');
217
+ const controllerContent = fs.readFileSync(path.join(capsuleDir, 'm-task-list.controller.ts'), 'utf8');
218
+
219
+ // Verify all actions are destructured and wired in UI
220
+ assert.ok(tsxContent.includes('addItem'));
221
+ assert.ok(tsxContent.includes('toggleItem'));
222
+ assert.ok(tsxContent.includes('removeItem'));
223
+ assert.ok(tsxContent.includes('<form'));
224
+ assert.ok(!tsxContent.includes('as any'));
225
+
226
+ // Verify controller has explicit types and no implicit any
227
+ assert.ok(controllerContent.includes('(i: TaskListItem)'));
228
+ assert.ok(!controllerContent.includes('(i) =>'));
229
+
230
+ fs.rmSync(tmpDir, { recursive: true, force: true });
231
+ });
232
+
233
+ test('runGenerateWizard: spec uses Vitest imports when vitest is declared in package.json', async () => {
234
+ const tmpDir = path.resolve(process.cwd(), 'scratch/test-gen-vitest');
235
+ if (fs.existsSync(tmpDir)) fs.rmSync(tmpDir, { recursive: true, force: true });
236
+ fs.mkdirSync(tmpDir, { recursive: true });
237
+ fs.writeFileSync(path.join(tmpDir, 'package.json'), JSON.stringify({
238
+ name: 'test-project',
239
+ devDependencies: { vitest: '^1.6.0' }
240
+ }));
241
+
242
+ const res = await runGenerateWizard([
243
+ 'molecule',
244
+ 'demo-card',
245
+ '-y',
246
+ `--dir=${tmpDir}`,
247
+ '--framework=react'
248
+ ]);
249
+
250
+ assert.strictEqual(res.success, true);
251
+ const specContent = fs.readFileSync(path.join(tmpDir, 'm-demo-card/m-demo-card.spec.ts'), 'utf8');
252
+ assert.ok(specContent.includes("from 'vitest'"));
253
+ assert.ok(!specContent.includes("from 'node:test'"));
254
+
255
+ fs.rmSync(tmpDir, { recursive: true, force: true });
256
+ });
257
+
201
258
 
202
259
 
package/cli/help.spec.js CHANGED
@@ -1,5 +1,7 @@
1
1
  import test from 'node:test';
2
2
  import assert from 'node:assert';
3
+ import { spawnSync } from 'node:child_process';
4
+ import path from 'node:path';
3
5
  import { COMMANDS_SCHEMA } from './commands-schema.js';
4
6
  import { printHelp } from './help.js';
5
7
 
@@ -25,3 +27,13 @@ test('help: printHelp renders without throwing', () => {
25
27
  printHelp();
26
28
  });
27
29
  });
30
+
31
+ test('cli: unknown command exits immediately with code 1 and error message', () => {
32
+ const cliPath = path.resolve('cli/index.js');
33
+ const res = spawnSync(process.execPath, [cliPath, 'config'], {
34
+ encoding: 'utf8',
35
+ timeout: 10000
36
+ });
37
+ assert.strictEqual(res.status, 1);
38
+ assert.ok(res.stderr.includes('Unknown command "config"'));
39
+ });
package/cli/index.js CHANGED
@@ -420,12 +420,14 @@ const main = async () => {
420
420
  printHelp();
421
421
  break;
422
422
  default:
423
- if (firstArg && isCapsulePrefix(firstArg)) {
423
+ if (!firstArg) {
424
+ printHelp();
425
+ } else if (isCapsulePrefix(firstArg)) {
424
426
  await runGenerateWizard(rawArgs);
425
- } else if (firstArg && !firstArg.startsWith('-')) {
426
- await runScaffold(firstArg, rawArgs, runAudit);
427
427
  } else {
428
- printHelp();
428
+ process.stderr.write(`\x1b[31mError:\x1b[0m Unknown command "${firstArg}".\n`);
429
+ process.stderr.write(`Run "chemx --help" for a list of available commands.\n\n`);
430
+ process.exit(1);
429
431
  }
430
432
  break;
431
433
  }
package/cli/license.js CHANGED
@@ -117,9 +117,10 @@ export const obtainLicenseKey = async (rawArgs = [], onRunAudit = null) => {
117
117
  rawArgs.includes('--non-interactive') ||
118
118
  rawArgs.includes('--no-interactive') ||
119
119
  Boolean(process.env.CI) ||
120
- process.stdout?.isTTY === false ||
121
- process.stdin?.isTTY === false ||
122
- Boolean(process.argv?.some((arg) => arg === '--headless' || arg === '--ci' || arg === '--yes' || arg === '-y'));
120
+ !process.stdout?.isTTY ||
121
+ !process.stdin?.isTTY ||
122
+ process.env.TERM === 'dumb' ||
123
+ Boolean(process.argv?.some((arg) => arg === '--headless' || arg === '--ci' || arg === '--yes' || arg === '-y' || arg === '--non-interactive'));
123
124
 
124
125
  if (isHeadless) {
125
126
  return null;
@@ -35,6 +35,27 @@ export const detectFramework = (cwd = process.cwd()) => {
35
35
  return 'react';
36
36
  };
37
37
 
38
+ export const detectTestRunner = (startDir = process.cwd()) => {
39
+ let curr = path.resolve(startDir);
40
+ while (curr && curr !== path.dirname(curr)) {
41
+ const pkgPath = path.join(curr, 'package.json');
42
+ if (fs.existsSync(pkgPath)) {
43
+ try {
44
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
45
+ const deps = { ...(pkg.dependencies || {}), ...(pkg.devDependencies || {}) };
46
+ if (deps.vitest || (pkg.scripts?.test && pkg.scripts.test.includes('vitest'))) return 'vitest';
47
+ if (deps.jest || (pkg.scripts?.test && pkg.scripts.test.includes('jest'))) return 'jest';
48
+ } catch {
49
+ // ignore
50
+ }
51
+ break;
52
+ }
53
+ curr = path.dirname(curr);
54
+ }
55
+
56
+ return 'node:test';
57
+ };
58
+
38
59
  const ATOMS_PACKAGES = ['@chemx/x-atoms', '@chemx/atoms', 'x-atoms', '@chem-x/x-atoms'];
39
60
  const PALETTE_PACKAGES = ['@chemx/o-command-palette', '@chemx/command-palette', 'o-command-palette', '@chem-x/o-command-palette'];
40
61
 
@@ -139,7 +139,7 @@ export const completeTaskWithAudit = (db, taskId, agentId, options = {}) => {
139
139
  if (fs.existsSync(fullPath)) {
140
140
  try {
141
141
  const auditRes = auditFile(fullPath, task.target_path);
142
- const remainingHazards = auditRes?.fileViolations || [];
142
+ const remainingHazards = Array.isArray(auditRes) ? auditRes : (auditRes?.fileViolations || auditRes?.violations || []);
143
143
  hazardCount = remainingHazards.length;
144
144
  healthScore = Math.max(0, 100 - hazardCount * 15);
145
145
  verified = hazardCount === 0;
@@ -151,6 +151,7 @@ export const completeTaskWithAudit = (db, taskId, agentId, options = {}) => {
151
151
  if (hazardCount > 0 && options.force !== true) {
152
152
  return {
153
153
  refused: true,
154
+ verified: false,
154
155
  taskId: Number(taskId),
155
156
  taskTitle: task.title,
156
157
  hazardCount,
@@ -487,3 +487,31 @@ test('team-commands: runTeamCli handles task update and re-verifies AST upon com
487
487
  }
488
488
  });
489
489
 
490
+ test('team-triage: completeTaskWithAudit correctly detects real on-disk violations and refuses completion', () => {
491
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'chemx-task-audit-'));
492
+ const testFile = path.join(tmpDir, 'm-test-hazard.tsx');
493
+ fs.writeFileSync(
494
+ testFile,
495
+ `import React from 'react';\nexport const MTest = () => {\n return <div><button>Violating Raw Button</button></div>;\n};\nexport default MTest;\n`
496
+ );
497
+
498
+ const db = setupTestDb();
499
+ const task = createTask(db, {
500
+ title: 'Fix raw button in molecule',
501
+ target_path: testFile
502
+ });
503
+
504
+ const res = completeTaskWithAudit(db, task.id, '@auditor', { cwd: tmpDir });
505
+ assert.strictEqual(res.refused, true);
506
+ assert.ok(res.hazardCount > 0);
507
+ assert.strictEqual(res.verified, false);
508
+
509
+ // With force: true, completion succeeds with forced flag
510
+ const forced = completeTaskWithAudit(db, task.id, '@auditor', { cwd: tmpDir, force: true });
511
+ assert.ok(forced.id);
512
+ assert.strictEqual(forced.status, 'done');
513
+ assert.strictEqual(forced.result_payload.forced, true);
514
+
515
+ fs.rmSync(tmpDir, { recursive: true, force: true });
516
+ });
517
+
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chem-x/starter-kit",
3
- "version": "26.9.20-1257",
3
+ "version": "26.9.20-1298",
4
4
  "description": "Chemical X Protocol: Private drop-in architecture starter kit and capsule generator",
5
5
  "type": "module",
6
6
  "bin": {