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
572 changes: 286 additions & 286 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: 201
scenarios: 2
capabilities: 6
test_files: 170
last_synced: "2026-07-01"
last_synced: "2026-07-02"
20 changes: 11 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: df89117572f607aa
F-002: c116a9d32f862ee9
F-003: c9578f9a6e70dcdf
F-004: 791125e674b98fb5
F-004: aa293c7a7ea024db
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: 27460334a1f95de6
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: 8a0afb73f19d7a3a
F-3788c2: af9778dea8687b29
F-37b4a8: e067655bad681488
F-3a5339: b2b2ea8775f99267
F-3a5339: 719d2a090d4d2002
F-3b3690: 6a36aad282d36f3a
F-40327b: 8295358f7b813c8a
F-417ff0: 0cc5eeefc5e08377
F-42af48: 7702447a407758a1
F-43d8e3: bbea25941e2b675d
F-4747ef: c255a18b6849d002
F-47b8bee5: 049cc9b4f672e4bc
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: 69d68c610cad4da8
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: 049cc9b4f672e4bc
F-b43066: 9402b630adcf1eae
F-b61449: 7095ce00662e987d
F-b84c38: 61a41c3f765e8a92
Expand All @@ -184,13 +185,14 @@ attested:
F-d2c806: b3d8668905855a6c
F-d3bde4: 915d13b33258d3fc
F-d49585: 11e3ac2dce796fc6
F-d6b93648: f755a47c66e07635
F-d6b93648: 29b1d6f135853969
F-d7312b: 000237d094145b6a
F-d8223c: 0501e9564231899b
F-d980359c: 8f1559276afc5c03
F-dd51b42c: 496eeffa2641169d
F-dd51b42c: 6a7081918b0ab6c2
F-dddb89: f5625354e55eba9b
F-e0f6c7: fe68521cda464f23
F-e4159959: aa293c7a7ea024db
F-eb732f: d8abb536ff850a7a
F-ee47fc2b: adb87c97b8ccf6e1
F-ee5f643e: 7c61f35852f093ca
Expand Down
35 changes: 35 additions & 0 deletions spec/features/swift-flutter-dart-toolchain-e4159959.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
id: F-e4159959
slug: swift-flutter-dart-toolchain
title: "Polyglot toolchain — Swift (SPM) and Flutter/Dart gate detection"
status: done
modules:
- src/stages/toolchain/detect.ts
- src/stages/toolchain/types.ts
acceptance_criteria:
- id: AC-aa3d5503
ears: event
condition: "when a Package.swift manifest is present in the project root"
action: "detect language 'swift' and resolve the gates to `swift build` (type), `swiftlint lint` (lint), `swift test` (test) and `swift test --enable-code-coverage` (coverage)"
response: "an SPM Swift package gates natively instead of resolving to 'unknown' and skipping every command stage"
text: "When a Package.swift manifest is present, the system shall detect language 'swift' and resolve the type/lint/test/coverage gates to swift build / swiftlint lint / swift test / swift test --enable-code-coverage respectively."
test_refs: ["tests/stages/toolchain.test.ts"]
- id: AC-61dd5a8e
ears: event
condition: "when a pubspec.yaml that declares a flutter dependency is present"
action: "detect language 'dart' and resolve the gates to `flutter analyze` (type), `flutter test` (test) and `flutter test --coverage` (coverage)"
response: "a Flutter app gates with the flutter toolchain rather than skipping"
text: "When a pubspec.yaml declaring a flutter dependency is present, the system shall detect language 'dart' and resolve the type/test/coverage gates to flutter analyze / flutter test / flutter test --coverage."
test_refs: ["tests/stages/toolchain.test.ts"]
- id: AC-4cb02211
ears: event
condition: "when a pubspec.yaml that does NOT declare a flutter dependency is present"
action: "detect language 'dart' and resolve the gates to `dart analyze` (type), `dart test` (test) and `dart test --coverage=coverage` (coverage)"
response: "a pure-Dart package gates with the dart CLI rather than the flutter wrapper"
text: "When a pubspec.yaml without a flutter dependency is present, the system shall detect language 'dart' and resolve the type/test/coverage gates to dart analyze / dart test / dart test --coverage=coverage."
test_refs: ["tests/stages/toolchain.test.ts"]
- id: AC-dca37b0a
ears: ubiquitous
action: "map the secret gate of the swift and dart toolchains to `gitleaks detect --no-banner` and omit the arch gate"
response: "secret scanning is consistent with the other compiled languages, and arch is intentionally absent because SPM/Dart enforce acyclic module imports at build time"
text: "The system shall map the swift and dart secret gate to gitleaks detect --no-banner and intentionally omit the architecture gate, consistent with the other compiled-language entries."
test_refs: ["tests/stages/toolchain.test.ts"]
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"]
2 changes: 2 additions & 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 Expand Up @@ -193,6 +194,7 @@ features:
F-dd51b42c: {slug: kotlin-first-class-toolchain, status: done, modules: 11}
F-dddb89: {slug: ears-validation-at-creation, status: done, modules: 4}
F-e0f6c7: {slug: smoke-disposition-spine, status: done, modules: 3}
F-e4159959: {slug: swift-flutter-dart-toolchain, status: done, modules: 2}
F-eb732f: {slug: spec-authoring-anti-hollow, status: done, modules: 2}
F-ee47fc2b: {slug: reverse-index-core, status: done, modules: 1}
F-ee5f643e: {slug: doc-graph-links, status: done, modules: 4}
Expand Down
129 changes: 120 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 @@ -63,6 +63,42 @@ function kotlinGates(cwd: string): ToolchainGates {
};
}

/**
* Dart vs Flutter share the `pubspec.yaml` manifest; the SDK in use is what
* tells them apart. A Flutter package declares the flutter SDK (a `flutter:`
* stanza or a `sdk: flutter` dependency), so its gates run through the
* `flutter` wrapper (which bundles the Flutter-aware analyzer + test harness);
* a pure-Dart package gates with the bare `dart` CLI. Gates are a thunk so the
* pubspec is read once per detection.
*/
function dartGates(cwd: string): ToolchainGates {
let isFlutter = false;
try {
isFlutter = /(^|\n)\s*flutter\s*:|sdk:\s*flutter/.test(readFileSync(join(cwd, 'pubspec.yaml'), 'utf8'));
} catch {
/* unreadable pubspec → treat as pure Dart */
}
const lint: ToolSpec = {cmd: 'dart', args: ['format', '--output=none', '--set-exit-if-changed', '.']};
const secret: ToolSpec = {cmd: 'gitleaks', args: ['detect', '--no-banner']};
return isFlutter
? {
type: {cmd: 'flutter', args: ['analyze']},
lint,
test: {cmd: 'flutter', args: ['test']},
coverage: {cmd: 'flutter', args: ['test', '--coverage']},
secret,
}
: {
type: {cmd: 'dart', args: ['analyze']},
lint,
test: {cmd: 'dart', args: ['test']},
coverage: {cmd: 'dart', args: ['test', '--coverage=coverage']},
secret,
};
// No `arch` gate: Dart/Flutter package imports are resolved acyclically by
// the SDK build, mirroring the rust/go/kotlin "compiler enforces it" stance.
}

/** Directories never worth descending into when probing for source files. */
const SOURCE_PROBE_IGNORE = new Set([
'node_modules', '.git', '.gradle', '.idea', 'build', 'target', 'dist', 'out', '.cladding',
Expand Down Expand Up @@ -116,7 +152,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 @@ -222,6 +262,28 @@ const CHAIN: readonly Entry[] = [
secret: {cmd: 'gitleaks', args: ['detect', '--no-banner']},
},
},
{
// Swift Package Manager. Xcode-only apps (no Package.swift) gate via a
// `.cladding/config.yaml::gate.commands` xcodebuild override — matching
// `.xcodeproj` here would wrongly point `swift build` at a project SPM
// cannot drive. No `arch` gate: SPM resolves module imports acyclically.
language: 'swift',
manifests: ['Package.swift'],
gates: {
type: {cmd: 'swift', args: ['build']},
lint: {cmd: 'swiftlint', args: ['lint']},
test: {cmd: 'swift', args: ['test']},
coverage: {cmd: 'swift', args: ['test', '--enable-code-coverage']},
secret: {cmd: 'gitleaks', args: ['detect', '--no-banner']},
},
},
{
// Dart + Flutter share pubspec.yaml; the gates thunk reads it to pick the
// `flutter` wrapper vs the bare `dart` CLI. @see dartGates.
language: 'dart',
manifests: ['pubspec.yaml'],
gates: dartGates,
},
];

/** Empty toolchain returned when no manifest matches. */
Expand Down Expand Up @@ -282,6 +344,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 +426,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
2 changes: 2 additions & 0 deletions src/stages/toolchain/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ export type Language =
| 'ruby'
| 'elixir'
| 'dotnet'
| 'swift'
| 'dart'
| 'unknown';

/** A concrete command (cmd + args) used to run one gate. */
Expand Down
Loading
Loading