Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
508 changes: 254 additions & 254 deletions plugins/claude-code/dist/clad.js

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions spec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,8 @@ project:

# Auto-maintained by `clad sync` (F-5b9f9f). Do not edit by hand.
inventory:
features: 199
features: 200
scenarios: 2
capabilities: 6
test_files: 170
last_synced: "2026-07-01"
last_synced: "2026-07-02"
19 changes: 10 additions & 9 deletions spec/attestation.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@
# --tier=pre-push --strict` GREEN refreshes. Content-anchored: survives
# fresh clones and squash/rebase (suggested .gitattributes: merge=union).
attested:
F-001: 1eb56cee6b065fbe
F-001: 057fb7e929de3dd4
F-002: c116a9d32f862ee9
F-003: c9578f9a6e70dcdf
F-004: 791125e674b98fb5
F-004: 02679764caa40808
F-005: 70c3b7166f297bff
F-006: 488cb6f2d452a286
F-007: d1c159c46d5a454f
Expand All @@ -29,7 +29,7 @@ attested:
F-021: 8a1a82a59a1c45c7
F-022: 8f596a1c737f6d42
F-02343cd1: 77875ee09a7ea3bf
F-023: 0c14948e5a91bb0f
F-023: 293a0b1bef86e3cb
F-024: f67a86816b06f8ee
F-025: 187339b684896b8e
F-026: bb35faf43ba582cb
Expand Down Expand Up @@ -107,16 +107,17 @@ attested:
F-2de65d: 84ad71574d306c81
F-315fd7: c3b042c80fa7c187
F-31eeb8: d88a9880d29ae411
F-32b1e0: 9b29e21313d121b1
F-32b1e0: bb59e85a10beb3ff
F-3788c2: af9778dea8687b29
F-37b4a8: e067655bad681488
F-3a5339: b2b2ea8775f99267
F-3a5339: 4cb8321a8dbc3b94
F-3b3690: 6a36aad282d36f3a
F-40327b: 8295358f7b813c8a
F-417ff0: 0cc5eeefc5e08377
F-42af48: 7702447a407758a1
F-43d8e3: bbea25941e2b675d
F-4747ef: c255a18b6849d002
F-47b8bee5: f84e93fcfd514394
F-4db939: b2c386ca4e18c117
F-50ff43: fac674314685a912
F-551a1c: 305488fada044107
Expand All @@ -125,7 +126,7 @@ attested:
F-570a3f: 3f60012b22c9b715
F-59f093: 26735424fba6308c
F-5b188856: 92b72281c248eba3
F-5b9f9f: 0b972209be8b642f
F-5b9f9f: 12ee3c3a3ff49fb8
F-5d3ed2: 9452eac28760fb99
F-5f6b45: 15323c4f5b619de7
F-64a5c159: adedb516a257c7ec
Expand Down Expand Up @@ -163,7 +164,7 @@ attested:
F-aee61f: e009b9eb07addd30
F-af45042a: 9c61ca116a28cbb6
F-af96b1: e75ca2cb3412a7a5
F-b2094740: f379bf4feef6771a
F-b2094740: f84e93fcfd514394
F-b43066: 9402b630adcf1eae
F-b61449: 7095ce00662e987d
F-b84c38: 61a41c3f765e8a92
Expand All @@ -184,11 +185,11 @@ attested:
F-d2c806: b3d8668905855a6c
F-d3bde4: 915d13b33258d3fc
F-d49585: 11e3ac2dce796fc6
F-d6b93648: f755a47c66e07635
F-d6b93648: db3b10624db7a089
F-d7312b: 000237d094145b6a
F-d8223c: 0501e9564231899b
F-d980359c: 8f1559276afc5c03
F-dd51b42c: 496eeffa2641169d
F-dd51b42c: cbad1c9c2f3fe9e6
F-dddb89: f5625354e55eba9b
F-e0f6c7: fe68521cda464f23
F-eb732f: d8abb536ff850a7a
Expand Down
27 changes: 27 additions & 0 deletions spec/features/ts-toolchain-jest-and-multiext-arch-47b8bee5.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
id: F-47b8bee5
slug: ts-toolchain-jest-and-multiext-arch
title: "TS/JS toolchain fidelity — jest test-gate auto-detection + multi-extension arch scan"
status: done
modules:
- src/stages/toolchain/detect.ts
acceptance_criteria:
- id: AC-0d51828d
ears: event
condition: "when a jest config file (jest.config.js/.ts/.mjs/.cjs/.json) is present in a detected TypeScript/JavaScript project"
action: "resolve the test gate to `npx --no-install jest` and the coverage gate to `npx --no-install jest --coverage`"
response: "a Jest-based project (CRA, React Native, classic React) gates natively instead of silently skipping on a missing vitest"
text: "When a jest config file is present in a detected TypeScript/JavaScript project, the system shall resolve the test gate to `npx --no-install jest` and the coverage gate to `npx --no-install jest --coverage`."
test_refs: ["tests/stages/toolchain.test.ts"]
- id: AC-a3be7a76
ears: event
condition: "when no jest config file is present in a detected TypeScript/JavaScript project"
action: "keep the vitest test and coverage gate defaults unchanged"
response: "config-less and vitest projects behave exactly as before (backward compatible)"
text: "When no jest config file is present in a detected TypeScript/JavaScript project, the system shall keep the vitest `run` test gate and `run --coverage` coverage gate defaults unchanged."
test_refs: ["tests/stages/toolchain.test.ts"]
- id: AC-3a899053
ears: ubiquitous
action: "configure the architecture (madge --circular) gate to scan .ts, .tsx, .js, and .jsx source files"
response: "circular-dependency detection covers React/JSX component files, not only .ts"
text: "The system shall configure the TypeScript/JavaScript architecture gate to scan .ts, .tsx, .js, and .jsx source files so circular-dependency detection covers React/JSX component files."
test_refs: ["tests/stages/toolchain.test.ts"]
1 change: 1 addition & 0 deletions spec/index.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ features:
F-42af48: {slug: architecture-from-spec, status: done, modules: 2}
F-43d8e3: {slug: smoke-probe-token-pass, status: done, modules: 3}
F-4747ef: {slug: ssot-lifecycle-tests, status: done, modules: 9}
F-47b8bee5: {slug: ts-toolchain-jest-and-multiext-arch, status: done, modules: 1}
F-4db939: {slug: ab-evaluation, status: done, modules: 9}
F-50ff43: {slug: meta-integrity-skip-when-absent, status: done, modules: 2}
F-551a1c: {slug: oracle-cost-lever, status: done, modules: 4}
Expand Down
71 changes: 62 additions & 9 deletions src/stages/toolchain/detect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
// This is the polyglot adapter: cladding itself stays language-agnostic;
// the *user project* decides which language tools run.

import {existsSync, readdirSync} from 'node:fs';
import {existsSync, readFileSync, readdirSync} from 'node:fs';
import type {Dirent} from 'node:fs';
import {join} from 'node:path';

Expand Down Expand Up @@ -116,7 +116,11 @@ const CHAIN: readonly Entry[] = [
test: {cmd: 'npx', args: ['--no-install', 'vitest', 'run']},
coverage: {cmd: 'npx', args: ['--no-install', 'vitest', 'run', '--coverage']},
secret: {cmd: 'npx', args: ['--no-install', 'secretlint', '**/*']},
arch: {cmd: 'npx', args: ['--no-install', 'madge', '--circular', '--extensions', 'ts', '.']},
// .tsx/.jsx/.js alongside .ts so circular-dependency detection covers
// React/JSX component trees, not only plain .ts (F-47b8bee5). madge
// excludes node_modules by default, so widening extensions does not pull
// the dependency tree into the scan.
arch: {cmd: 'npx', args: ['--no-install', 'madge', '--circular', '--extensions', 'ts,tsx,js,jsx', '.']},
smoke: {cmd: 'npm', args: ['run', '--silent', 'smoke']},
perf: {cmd: 'npm', args: ['run', '--silent', 'perf']},
visual: {cmd: 'npm', args: ['run', '--silent', 'visual']},
Expand Down Expand Up @@ -282,6 +286,58 @@ function resolveTsLint(cwd: string, fallback: ToolSpec): ToolSpec {
return fallback;
}

/**
* TypeScript/JavaScript test-runner resolution by config-file presence
* (F-47b8bee5) — the test-gate analogue of `resolveTsLint`.
*
* `package.json` maps to one language, but the test gate defaulted to vitest
* unconditionally — so a Jest project (CRA, React Native, classic React) hit
* `npx --no-install vitest`, found nothing, and SILENTLY SKIPPED stage_2.1 /
* stage_2.2. Detect the Jest the project actually configured and gate with
* THAT. Precedence: jest config present → jest; else vitest (the default, also
* used config-less, so vitest and config-less projects behave exactly as
* before).
*
* `--no-install` is kept: detection only decides WHICH runner to invoke, never
* installs one. A configured-but-absent jest still resolves to skip via the
* stage's missing-tool path, which `--strict`'s skip-policy escalates.
*
* CAVEAT — by config PRESENCE, not content (mirrors `resolveTsLint`). A project
* carrying both a jest and a vitest config resolves to jest; one that tests with
* a different runner overrides via `.cladding/config.yaml::gate.commands`.
*/
const JEST_CONFIGS: readonly string[] = [
'jest.config.js', 'jest.config.ts', 'jest.config.mjs', 'jest.config.cjs', 'jest.config.json',
];

/** True when the project configures Jest — a jest.config.* file or a `jest` key in package.json. */
function hasJestConfig(cwd: string): boolean {
if (JEST_CONFIGS.some((c) => existsSync(join(cwd, c)))) return true;
try {
const pkg = JSON.parse(readFileSync(join(cwd, 'package.json'), 'utf8')) as {jest?: unknown};
return pkg.jest !== undefined;
} catch {
return false;
}
}

/**
* Applies TS/JS config-presence detection to the curated TS gates: linter
* (biome/oxlint/eslint via `resolveTsLint`) and test runner (jest/vitest via
* `hasJestConfig`). Other gates pass through unchanged.
*/
function resolveTsGates(cwd: string, base: ToolchainGates): ToolchainGates {
const out: ToolchainGates = base.lint ? {...base, lint: resolveTsLint(cwd, base.lint)} : {...base};
if (base.test && base.coverage && hasJestConfig(cwd)) {
return {
...out,
test: {cmd: 'npx', args: ['--no-install', 'jest']},
coverage: {cmd: 'npx', args: ['--no-install', 'jest', '--coverage']},
};
}
return out;
}

/**
* Detects the project's toolchain by walking a priority chain of manifests.
*
Expand Down Expand Up @@ -312,13 +368,10 @@ export function detectToolchain(cwd: string = '.'): Toolchain {
if (entry.requiresSource && !hasSourceFile(cwd, entry.requiresSource)) continue;
// Kotlin gates are a function of cwd (gradlew vs gradle); resolve first.
const baseGates = typeof entry.gates === 'function' ? entry.gates(cwd) : entry.gates;
// TS/JS: pick the linter the project configured (biome/oxlint) over the
// eslint default, so a non-eslint project gates natively. Other languages
// keep their single curated default.
const gates =
entry.language === 'typescript' && baseGates.lint
? {...baseGates, lint: resolveTsLint(cwd, baseGates.lint)}
: baseGates;
// TS/JS: pick the linter (biome/oxlint) and test runner (jest) the project
// configured over the eslint/vitest defaults, so a non-eslint / Jest project
// gates natively. Other languages keep their single curated default.
const gates = entry.language === 'typescript' ? resolveTsGates(cwd, baseGates) : baseGates;
return {language: entry.language, manifest, gates};
}
return UNKNOWN;
Expand Down
56 changes: 56 additions & 0 deletions tests/stages/toolchain.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,62 @@ describe('detectToolchain', () => {
expect(tc.language).toBe('python');
expect(tc.gates.lint).toEqual({cmd: 'ruff', args: ['check', '.']});
});

// ─── TS/JS test runner + arch extensions (F-47b8bee5) ───

test('typescript + jest.config.js → test gate is jest, coverage is jest --coverage', () => {
writeFileSync(join(dir, 'package.json'), '{}');
writeFileSync(join(dir, 'jest.config.js'), 'module.exports = {}');
const tc = detectToolchain(dir);
expect(tc.gates.test).toEqual({cmd: 'npx', args: ['--no-install', 'jest']});
expect(tc.gates.coverage).toEqual({cmd: 'npx', args: ['--no-install', 'jest', '--coverage']});
});

for (const cfg of ['jest.config.ts', 'jest.config.mjs', 'jest.config.cjs', 'jest.config.json']) {
test(`typescript + ${cfg} → test gate is jest`, () => {
writeFileSync(join(dir, 'package.json'), '{}');
writeFileSync(join(dir, cfg), '{}');
expect(detectToolchain(dir).gates.test?.args).toContain('jest');
});
}

test('package.json with a top-level "jest" key and no jest.config.* → test gate is jest', () => {
writeFileSync(join(dir, 'package.json'), '{"jest":{}}');
expect(detectToolchain(dir).gates.test).toEqual({cmd: 'npx', args: ['--no-install', 'jest']});
});

test('typescript with no jest config → test/coverage stay vitest (default preserved)', () => {
writeFileSync(join(dir, 'package.json'), '{}');
const tc = detectToolchain(dir);
expect(tc.gates.test).toEqual({cmd: 'npx', args: ['--no-install', 'vitest', 'run']});
expect(tc.gates.coverage).toEqual({cmd: 'npx', args: ['--no-install', 'vitest', 'run', '--coverage']});
});

test('test runner selection follows config presence — add jest.config.js swaps to jest, remove it falls back to vitest', () => {
// State-transition: proves the test-runner resolution reads the filesystem each call,
// not a hard-coded return.
writeFileSync(join(dir, 'package.json'), '{}');
const vitestGate = {cmd: 'npx', args: ['--no-install', 'vitest', 'run']};
expect(detectToolchain(dir).gates.test).toEqual(vitestGate);
writeFileSync(join(dir, 'jest.config.js'), 'module.exports = {}');
expect(detectToolchain(dir).gates.test).toEqual({cmd: 'npx', args: ['--no-install', 'jest']});
rmSync(join(dir, 'jest.config.js'));
expect(detectToolchain(dir).gates.test).toEqual(vitestGate);
});

test('jest.config.ts + biome.json compose — test gate is jest AND lint gate is biome (independent detections)', () => {
writeFileSync(join(dir, 'package.json'), '{}');
writeFileSync(join(dir, 'jest.config.ts'), 'export default {}');
writeFileSync(join(dir, 'biome.json'), '{}');
const tc = detectToolchain(dir);
expect(tc.gates.test?.args).toContain('jest');
expect(tc.gates.lint?.args).toContain('biome');
});

test('typescript arch gate scans ts,tsx,js,jsx extensions', () => {
writeFileSync(join(dir, 'package.json'), '{}');
expect(detectToolchain(dir).gates.arch).toEqual({cmd: 'npx', args: ['--no-install', 'madge', '--circular', '--extensions', 'ts,tsx,js,jsx', '.']});
});
});

describe('gradleCmd', () => {
Expand Down
Loading