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
15 changes: 15 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,21 @@ Versioning: [Semantic Versioning 2.0](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

### Changed

- **Incremental TS gate — tsc `--incremental` + eslint `--cache`** (`F-bfe14aac`) — the
TypeScript type and lint gates re-ran from scratch every time. They now reuse a build
cache: `tsc --noEmit --incremental` (build-info file) and `eslint --cache`, both written
under `.cladding/cache/` (already gitignored, so the managed project's tree stays clean).
On an unchanged re-run — the local pre-commit/pre-push loop — measured on cladding's own
repo: **tsc 2.7s → 1.1s, eslint 2.5s → 0.6s (~3.4s saved)**. **Sound, not a shortcut:** a
newly-introduced type error is still caught with a stale build-info present (verified —
`tsc --incremental` rebuilds the affected program slice; `eslint --cache` keys on
file+config hash). Cold runs (fresh CI checkout) just rebuild the cache — no regression.
Test execution is deliberately **not** scoped — a gate must run the whole suite, so
changed-files/test-selection (unsound for a gate) was intentionally avoided.


## [0.7.0] — 2026-07-01 — Knowledge Graph

### Knowledge graph (spec↔code↔doc)
Expand Down
2 changes: 1 addition & 1 deletion plugins/claude-code/dist/clad.js

Large diffs are not rendered by default.

6 changes: 3 additions & 3 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"
test_files: 171
last_synced: "2026-07-02"
23 changes: 12 additions & 11 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: 830ae324d89c3fce
F-002: c116a9d32f862ee9
F-003: c9578f9a6e70dcdf
F-004: 791125e674b98fb5
F-004: b6e68a1ab2d8fe81
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: c347678282dafd98
F-024: f67a86816b06f8ee
F-025: 187339b684896b8e
F-026: bb35faf43ba582cb
Expand All @@ -50,7 +50,7 @@ attested:
F-041: cddb50fc41e49066
F-042: d1f661281bb9fb6e
F-043: 2c5f4a94e3e57e9b
F-044: 2f5ee0e3cee5f762
F-044: 95085e0995009f41
F-045: b494c9a80442ac20
F-046: 4b563fce74b6bb4b
F-047: baf5a2dbb9bb5a4b
Expand Down Expand Up @@ -85,7 +85,7 @@ attested:
F-076: 0061ab2d4b4991a8
F-077: 7aac44757146b695
F-078: c6af3d128b98f4e6
F-079: eaa08d9aeeb2a263
F-079: f3188948e53e942f
F-080: 1c19da74d32894e6
F-081: 248f9660cfb1b02b
F-098d3b: 42d61bf806ce462b
Expand All @@ -107,10 +107,10 @@ attested:
F-2de65d: 84ad71574d306c81
F-315fd7: c3b042c80fa7c187
F-31eeb8: d88a9880d29ae411
F-32b1e0: 9b29e21313d121b1
F-32b1e0: 4cc7240778eaae75
F-3788c2: af9778dea8687b29
F-37b4a8: e067655bad681488
F-3a5339: b2b2ea8775f99267
F-3a5339: 7d790460b97b35a7
F-3b3690: 6a36aad282d36f3a
F-40327b: 8295358f7b813c8a
F-417ff0: 0cc5eeefc5e08377
Expand All @@ -125,7 +125,7 @@ attested:
F-570a3f: 3f60012b22c9b715
F-59f093: 26735424fba6308c
F-5b188856: 92b72281c248eba3
F-5b9f9f: 0b972209be8b642f
F-5b9f9f: cad717b88efe592a
F-5d3ed2: 9452eac28760fb99
F-5f6b45: 15323c4f5b619de7
F-64a5c159: adedb516a257c7ec
Expand Down Expand Up @@ -163,7 +163,7 @@ attested:
F-aee61f: e009b9eb07addd30
F-af45042a: 9c61ca116a28cbb6
F-af96b1: e75ca2cb3412a7a5
F-b2094740: f379bf4feef6771a
F-b2094740: 2ea89b77e7c8e740
F-b43066: 9402b630adcf1eae
F-b61449: 7095ce00662e987d
F-b84c38: 61a41c3f765e8a92
Expand All @@ -173,6 +173,7 @@ attested:
F-bb15e6: 9b629bd8910007fa
F-bd07d7: 4bf7e1baddf5d754
F-bdcd90: 826853f7885a5a08
F-bfe14aac: 2ea89b77e7c8e740
F-c037ae: 6a58cdcfd0474e5f
F-c2c996: 5c73fa010b1502fa
F-c48eb2: 6a581a63e255c279
Expand All @@ -184,11 +185,11 @@ attested:
F-d2c806: b3d8668905855a6c
F-d3bde4: 915d13b33258d3fc
F-d49585: 11e3ac2dce796fc6
F-d6b93648: f755a47c66e07635
F-d6b93648: 2acbd4024d0f37ca
F-d7312b: 000237d094145b6a
F-d8223c: 0501e9564231899b
F-d980359c: 8f1559276afc5c03
F-dd51b42c: 496eeffa2641169d
F-dd51b42c: 30b7e2a656892648
F-dddb89: f5625354e55eba9b
F-e0f6c7: fe68521cda464f23
F-eb732f: d8abb536ff850a7a
Expand Down
33 changes: 33 additions & 0 deletions spec/features/incremental-ts-gate-bfe14aac.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
id: F-bfe14aac
slug: incremental-ts-gate
title: "Incremental TS gate — tsc --incremental + eslint --cache (sound; ~3.4s faster on warm re-runs)"
status: done
modules:
- src/stages/toolchain/detect.ts
acceptance_criteria:
- id: AC-41107118
ears: ubiquitous
action: "run the TypeScript type gate as tsc --noEmit --incremental with a tsBuildInfoFile under .cladding/cache, so an unchanged re-check reuses the build cache"
response: "detectToolchain's typescript gates.type carries --incremental + --tsBuildInfoFile .cladding/cache/tsc.tsbuildinfo"
text: "The system shall run the TS type gate with tsc --incremental and a .cladding/cache build-info file."
test_refs: ["tests/stages/incremental-gate.test.ts"]
- id: AC-1f2116fa
ears: ubiquitous
action: "run the TypeScript lint gate as eslint --cache with a cache-location under .cladding/cache, so unchanged files reuse cached lint results"
response: "detectToolchain's typescript gates.lint carries --cache + --cache-location .cladding/cache/eslint"
text: "The system shall run the TS lint gate with eslint --cache and a .cladding/cache cache-location."
test_refs: ["tests/stages/incremental-gate.test.ts"]
- id: AC-f253b9fa
ears: state
condition: "while a build cache from a prior run exists and the tree is unchanged"
action: "still catch a newly-introduced type/lint error (incremental is sound, not a correctness shortcut)"
response: "tsc --incremental rebuilds the affected program slice and eslint --cache keys on file+config hash, so a real error is never masked — the cache only skips proven-unchanged work"
text: "While a cache exists, the system shall still catch any real type/lint error (sound incrementality)."
test_refs: ["tests/stages/incremental-gate.test.ts"]
- id: AC-dd80d585
ears: unwanted
condition: "if the gate writes its incremental caches"
action: "keep them under the .cladding/ namespace so they never pollute the managed project's git status"
response: "both cache paths live under .cladding/cache (already gitignored), not the project root"
text: "If the gate caches, then it shall write under .cladding/ so the project tree stays clean."
test_refs: ["tests/stages/incremental-gate.test.ts"]
1 change: 1 addition & 0 deletions spec/index.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,7 @@ features:
F-bb15e6: {slug: clad-doctor, status: done, modules: 3}
F-bd07d7: {slug: greenfield-seeds, status: done, modules: 5}
F-bdcd90: {slug: oracle-policy-risk-weighted, status: done, modules: 8}
F-bfe14aac: {slug: incremental-ts-gate, status: done, modules: 1}
F-c037ae: {slug: test-refs-repair, status: done, modules: 4}
F-c2c996: {slug: checkpoint-events, status: done, modules: 3}
F-c48eb2: {slug: scan-source-roots, status: done, modules: 5}
Expand Down
11 changes: 9 additions & 2 deletions src/stages/toolchain/detect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,8 +111,15 @@ const CHAIN: readonly Entry[] = [
// "not found" → the stage's missing-tool classification → skip (exit 2),
// which the strict demand table (F-67d2e9) escalates when the spec
// relies on the stage.
type: {cmd: 'npx', args: ['--no-install', 'tsc', '--noEmit']},
lint: {cmd: 'npx', args: ['--no-install', 'eslint', '.']},
// Incremental caches (F-bfe14aac): tsc --incremental reuses a build-info
// file and eslint --cache reuses per-file results, so an unchanged re-run
// (the local pre-commit/pre-push loop) skips proven-unchanged work —
// ~3.4s faster on cladding's own tree. SOUND, not a shortcut: tsc rebuilds
// the affected program slice (a new type error is still caught) and eslint
// keys the cache on file+config hash. Caches live under .cladding/ (already
// gitignored) so they never pollute the managed project's git status.
type: {cmd: 'npx', args: ['--no-install', 'tsc', '--noEmit', '--incremental', '--tsBuildInfoFile', '.cladding/cache/tsc.tsbuildinfo']},
lint: {cmd: 'npx', args: ['--no-install', 'eslint', '.', '--cache', '--cache-location', '.cladding/cache/eslint']},
test: {cmd: 'npx', args: ['--no-install', 'vitest', 'run']},
coverage: {cmd: 'npx', args: ['--no-install', 'vitest', 'run', '--coverage']},
secret: {cmd: 'npx', args: ['--no-install', 'secretlint', '**/*']},
Expand Down
77 changes: 77 additions & 0 deletions tests/stages/incremental-gate.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import {afterEach, beforeEach, describe, expect, it} from 'vitest';
import {mkdtempSync, rmSync, writeFileSync} from 'node:fs';
import {tmpdir} from 'node:os';
import {join} from 'node:path';

import {detectToolchain} from '../../src/stages/toolchain/detect.js';

let dir: string;
beforeEach(() => {
dir = mkdtempSync(join(tmpdir(), 'incr-gate-'));
});
afterEach(() => {
rmSync(dir, {recursive: true, force: true});
});

const ts = () => writeFileSync(join(dir, 'package.json'), '{}');
const py = () => writeFileSync(join(dir, 'pyproject.toml'), '');

const after = (args: readonly string[], flag: string) =>
args[args.indexOf(flag) + 1];

describe('incremental TS gate (F-bfe14aac)', () => {
it('type gate uses incremental with cladding tsbuildinfo', () => {
ts();
const {gates} = detectToolchain(dir);
expect(gates.type).toBeDefined();
const {cmd, args} = gates.type!;
expect(cmd).toBe('npx');
expect(args.slice(0, 3)).toEqual(['--no-install', 'tsc', '--noEmit']);
expect(args).toContain('--incremental');
expect(args).toContain('--tsBuildInfoFile');
expect(after(args, '--tsBuildInfoFile')).toBe(
'.cladding/cache/tsc.tsbuildinfo',
);
});

it('lint gate uses eslint cache under cladding', () => {
ts();
const {gates} = detectToolchain(dir);
expect(gates.lint).toBeDefined();
const {args} = gates.lint!;
expect(args.slice(0, 3)).toEqual(['--no-install', 'eslint', '.']);
expect(args).toContain('--cache');
expect(args).toContain('--cache-location');
expect(after(args, '--cache-location')).toBe('.cladding/cache/eslint');
});

it('all cache paths stay under .cladding/ (no project-root pollution)', () => {
ts();
const {gates} = detectToolchain(dir);
const tsCache = after(gates.type!.args, '--tsBuildInfoFile');
const lintCache = after(gates.lint!.args, '--cache-location');
expect(tsCache.startsWith('.cladding/')).toBe(true);
expect(lintCache.startsWith('.cladding/')).toBe(true);
});

it('test and coverage gates are unchanged', () => {
ts();
const {gates} = detectToolchain(dir);
expect(gates.test).toBeDefined();
expect(gates.coverage).toBeDefined();
expect(gates.test!.args).toEqual(['--no-install', 'vitest', 'run']);
expect(gates.coverage!.args).toEqual([
'--no-install',
'vitest',
'run',
'--coverage',
]);
});

it('non-TS (python) project is unaffected by incremental flags', () => {
py();
const {gates} = detectToolchain(dir);
expect(gates.type?.args).not.toContain('--incremental');
expect(gates.lint?.args).not.toContain('--cache');
});
});
6 changes: 3 additions & 3 deletions tests/stages/toolchain.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ describe('detectToolchain', () => {
// State-transition: proves resolveTsLint actually reads the filesystem each call,
// not a hard-coded return (defeats the one-way-test critique).
writeFileSync(join(dir, 'package.json'), '{}');
const eslintGate = {cmd: 'npx', args: ['--no-install', 'eslint', '.']};
const eslintGate = {cmd: 'npx', args: ['--no-install', 'eslint', '.', '--cache', '--cache-location', '.cladding/cache/eslint']};
expect(detectToolchain(dir).gates.lint).toEqual(eslintGate);
writeFileSync(join(dir, 'biome.json'), '{}');
expect(detectToolchain(dir).gates.lint).toEqual({cmd: 'npx', args: ['--no-install', 'biome', 'lint', '.']});
Expand All @@ -156,7 +156,7 @@ describe('detectToolchain', () => {

test('typescript with no linter config → lint gate stays eslint (default preserved)', () => {
writeFileSync(join(dir, 'package.json'), '{}');
expect(detectToolchain(dir).gates.lint).toEqual({cmd: 'npx', args: ['--no-install', 'eslint', '.']});
expect(detectToolchain(dir).gates.lint).toEqual({cmd: 'npx', args: ['--no-install', 'eslint', '.', '--cache', '--cache-location', '.cladding/cache/eslint']});
});

test('biome takes precedence over oxlint when both configs present', () => {
Expand All @@ -170,7 +170,7 @@ describe('detectToolchain', () => {
writeFileSync(join(dir, 'package.json'), '{}');
writeFileSync(join(dir, 'biome.json'), '{}');
const tc = detectToolchain(dir);
expect(tc.gates.type).toEqual({cmd: 'npx', args: ['--no-install', 'tsc', '--noEmit']});
expect(tc.gates.type).toEqual({cmd: 'npx', args: ['--no-install', 'tsc', '--noEmit', '--incremental', '--tsBuildInfoFile', '.cladding/cache/tsc.tsbuildinfo']});
expect(tc.gates.test).toEqual({cmd: 'npx', args: ['--no-install', 'vitest', 'run']});
});

Expand Down
Loading