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

## [Unreleased]

### Changed

- **Gate ~47% faster — secretlint/madge now spawn once per gate, not twice** (`F-5a49899e`)
— `HARDCODED_SECRET` (secretlint, ~4.4s) and `ARCHITECTURE_VIOLATION` (madge, ~1.4s)
shell out to an external tool and together are ~97% of the drift stage. They ran
**twice** per gate: once inside the Drift stage (which sweeps every detector) and
again as their dedicated Secret/Arch stage. `secret.ts` even documented that the
layering "avoids spawning the scanner twice" — but nothing enforced it. A new
gate-scoped memo (`src/stages/scanner-cache.ts`, mirroring the run-scoped spec cache
F-cd0415) makes the second invocation a cache hit, so each tool spawns once. Measured
on cladding's own repo (isolated worktree A/B): `clad check --tier=pre-commit`
**~11.9s → ~6.3s (−47%)**, with **identical findings** (a pass-through when no gate
cache is primed, so standalone/MCP behavior is unchanged; cleared in a `finally` so
the long-lived MCP server never serves a stale scan).


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

### Knowledge graph (spec↔code↔doc)
Expand Down
542 changes: 271 additions & 271 deletions 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"
89 changes: 45 additions & 44 deletions spec/attestation.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,11 @@ attested:
F-002: c116a9d32f862ee9
F-003: c9578f9a6e70dcdf
F-004: 791125e674b98fb5
F-005: 70c3b7166f297bff
F-006: 488cb6f2d452a286
F-005: 3e4bf76de9589d84
F-006: adaccec4f53e75ba
F-007: d1c159c46d5a454f
F-008: 86cb22d725182847
F-009: 80831c8be1440e79
F-009: 20b0a33a31f10db9
F-00eb1a: 4dfd6110051f8854
F-010: 049a2581168f569f
F-011: efd71e966a937fab
Expand All @@ -28,7 +28,7 @@ attested:
F-020: 2c5f4a94e3e57e9b
F-021: 8a1a82a59a1c45c7
F-022: 8f596a1c737f6d42
F-02343cd1: 77875ee09a7ea3bf
F-02343cd1: 1efcec1a4983c0fc
F-023: 0c14948e5a91bb0f
F-024: f67a86816b06f8ee
F-025: 187339b684896b8e
Expand All @@ -46,17 +46,17 @@ attested:
F-037: 7f811c5c8bc0e8e3
F-038: 1338100beadb15a6
F-039: 2e60f3d899b72d7f
F-040: f678671ad6613fd8
F-040: b77ad87784b60fc7
F-041: cddb50fc41e49066
F-042: d1f661281bb9fb6e
F-043: 2c5f4a94e3e57e9b
F-044: 2f5ee0e3cee5f762
F-044: 8a5016801a0dce23
F-045: b494c9a80442ac20
F-046: 4b563fce74b6bb4b
F-047: baf5a2dbb9bb5a4b
F-048: 30dcf0786717f873
F-048: 151bb32ca5b1602a
F-049: 444a75986c1c3430
F-051: 1c6cf231cc139640
F-051: 663bc2c9c74194e3
F-052: 61e043371b9f7c71
F-053: 13a90cd08aec07ae
F-054: 5f5d30500bd8cd7f
Expand All @@ -69,7 +69,7 @@ attested:
F-061: c16123610e8fe7fc
F-062: 0ab83282a7f7b1ef
F-063: 76a719993cc71fa8
F-064: e176668e53f61c6e
F-064: f0a6ab15e479ddf4
F-065: e6ed3ef916201947
F-066: 512896be3294c6e4
F-067: 6e6dbd05bf314b56
Expand All @@ -79,84 +79,85 @@ attested:
F-070: e50bb5d3addc7720
F-071: 3183a483a8015d4c
F-072: 44e1d39139c816cc
F-073: 47be139281e7940a
F-073: 728fa28e1e1d2a76
F-074: a4cc2b9bf66c77ac
F-075: f8242e8d7fdc0488
F-075: 5e65af58cffb6ba3
F-076: 0061ab2d4b4991a8
F-077: 7aac44757146b695
F-078: c6af3d128b98f4e6
F-079: eaa08d9aeeb2a263
F-079: ff1fc868f49652f2
F-080: 1c19da74d32894e6
F-081: 248f9660cfb1b02b
F-098d3b: 42d61bf806ce462b
F-09d68b: dca7938bcf6fa47e
F-09d68b: 5108a9397eaed2d4
F-0ed2db: f94e2f45a16ff99c
F-0f2984d0: b852882d00177ef9
F-0f2984d0: 238b400d3792bcf5
F-12d740: 84ad71574d306c81
F-15999130: 894b484b7a93690c
F-16138071: c8b73555d47cfdd7
F-16138071: 0bf680acd8b8aa28
F-16746b: 2f98a1261b9b1fc2
F-17df0a: 915d13b33258d3fc
F-18e951: d907c170a230e052
F-1c9166: c72ece4c25cc4748
F-1d23a6: 8edadd4a7f136330
F-1d23a6: d372848de0e4986c
F-1edb38: 64283112a3ab96ce
F-24062d: 7d0f890c1b7c68d0
F-245bd5: a8372aeb83acc411
F-2be3e3bb: b852882d00177ef9
F-2be3e3bb: 238b400d3792bcf5
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
F-417ff0: 089ee52ebb0b26ff
F-42af48: 7702447a407758a1
F-43d8e3: bbea25941e2b675d
F-4747ef: c255a18b6849d002
F-4db939: b2c386ca4e18c117
F-50ff43: fac674314685a912
F-551a1c: 305488fada044107
F-569f4b37: 6ad74b9c3113817a
F-56abaa: a8863eff906b4d73
F-569f4b37: 7caf880481d36f07
F-56abaa: f8bc71b523ddaacb
F-570a3f: 3f60012b22c9b715
F-59f093: 26735424fba6308c
F-5a49899e: dcb9a2cb3702e118
F-5b188856: 92b72281c248eba3
F-5b9f9f: 0b972209be8b642f
F-5b9f9f: 0bdb9c83be1117a9
F-5d3ed2: 9452eac28760fb99
F-5f6b45: 15323c4f5b619de7
F-64a5c159: adedb516a257c7ec
F-64a5c159: eedb61d6380e18ca
F-65814a: 2136c8b8c94ef535
F-67d2e9: 03dc30dc995da728
F-67d2e9: eeff027b10d79716
F-67e33f: c8f84d2f7b3971aa
F-6d943d: e02e71ed007aded2
F-6f80e7: 0c0e5b71ae22cc26
F-7076f7: 7e819e0d440f3ffe
F-7794a6bc: 4d075a1a19bf4ba5
F-7794a6bc: c5149206d58ca94a
F-77f7ead0: 3f8cd4feeece1abd
F-78b50d: 688e6afe2352a034
F-7afbd4: 18ce48352bee0fce
F-7ce18e: 42fe8057bc8e250b
F-7ce18e: ff63a24536fcd5ae
F-7fa4a7: 19b7709a0b2202e3
F-80d19d: 6e03a098269ce4c3
F-80d19d: 6cc038373ff59549
F-836a90: a0d57b88aafbe6e8
F-8f419e: f3473746f4e252bc
F-904495a5: 56b5bb4bf3c35ddf
F-9064ff: b586e3aa6bc24aac
F-904495a5: bfd7d39d8504c322
F-9064ff: 07f2ec5a7b8873f2
F-94dda4: 8dfb0267c45534f9
F-95a096: 4c7b844669411617
F-96250595: 846ab496e40cdd48
F-96700032: 5ae97b4c82c14acc
F-99c6e5: 8a8724852f1d6059
F-9a3b61: 786eeefef2d25138
F-9b643e: 1c68e95890f2671e
F-9b643e: 811134f6d2da9417
F-9d168287: 6a3dc163c77b9e15
F-a04cd9: e65d87671306d305
F-a4b512: d136f2a1bbc4383a
F-a5228c: 4a9f641497c37b67
F-a5228c: 2d021707b1cbbf34
F-aa7197: 7f561e4f3c902716
F-ae61c1: 7c3c8622e5375754
F-aee1da: a6d7525a6c547877
Expand All @@ -166,34 +167,34 @@ attested:
F-b2094740: f379bf4feef6771a
F-b43066: 9402b630adcf1eae
F-b61449: 7095ce00662e987d
F-b84c38: 61a41c3f765e8a92
F-b99577: b3de7411ed1f21aa
F-b84c38: 44519404503b81ec
F-b99577: bfd9f7881498eb49
F-ba2e05: 158e77c8af32514a
F-ba4b7a: c282e0e915ed547c
F-bb15e6: 9b629bd8910007fa
F-bb15e6: 80ace8727a844830
F-bd07d7: 4bf7e1baddf5d754
F-bdcd90: 826853f7885a5a08
F-c037ae: 6a58cdcfd0474e5f
F-c2c996: 5c73fa010b1502fa
F-c48eb2: 6a581a63e255c279
F-bdcd90: 40b1a05831056a7f
F-c037ae: b034c5269a2dcf1d
F-c2c996: cfdf338c71557204
F-c48eb2: 3bb4a0c77281209a
F-c4c5ae: 18200d79542ae22e
F-c8aef8: 02e07f929a1d0ded
F-cd0415: 9cf6ce40e2a8b381
F-cfba0c: 077c03b8a96f562b
F-d12edf: c19aa5d1007e0b8f
F-d2c806: b3d8668905855a6c
F-d2c806: 992300366b5228c2
F-d3bde4: 915d13b33258d3fc
F-d49585: 11e3ac2dce796fc6
F-d6b93648: f755a47c66e07635
F-d49585: 9f243ecbfc695249
F-d6b93648: 2acbd4024d0f37ca
F-d7312b: 000237d094145b6a
F-d8223c: 0501e9564231899b
F-d980359c: 8f1559276afc5c03
F-dd51b42c: 496eeffa2641169d
F-dddb89: f5625354e55eba9b
F-e0f6c7: fe68521cda464f23
F-e0f6c7: 2d0c15ab3fe375e7
F-eb732f: d8abb536ff850a7a
F-ee47fc2b: adb87c97b8ccf6e1
F-ee5f643e: 7c61f35852f093ca
F-ee5f643e: 67e6187e3fcc09b5
F-ef2fd9: b3a1dcd1e750a714
F-f334fa: 5207f35968a0c9b2
F-f44d1b: 62e0779d9c0ef11f
Expand Down
38 changes: 38 additions & 0 deletions spec/features/gate-scanner-memo-5a49899e.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
id: F-5a49899e
slug: gate-scanner-memo
title: "Gate-scoped scanner memoization — spawn secretlint/madge ONCE per gate, not twice (the dedup secret.ts already claims)"
status: done
modules:
- src/stages/scanner-cache.ts
- src/stages/detectors/hardcoded-secret.ts
- src/stages/detectors/architecture-violation.ts
- src/cli/clad.ts
- src/stages/secret.ts
acceptance_criteria:
- id: AC-10a55b5a
ears: ubiquitous
action: "memoize an external-scanner detector's findings by (cwd, cmd, args) within a primed gate pass so the underlying tool runs at most once"
response: "scanner-cache.ts exposes primeScannerCache(on)/memoizeScan(key, run); HARDCODED_SECRET (secretlint) and ARCHITECTURE_VIOLATION (madge) route their execaSync through memoizeScan"
text: "The system shall memoize external-scanner detector results within a primed gate pass."
test_refs: ["tests/stages/scanner-cache.test.ts"]
- id: AC-762ad120
ears: event
condition: "when the Drift stage and the Secret/Arch stage both invoke the same scanner detector in one gate run"
action: "spawn the external tool only once (the second invocation is a cache hit)"
response: "clad check primes the cache around the stage loop, so stage_1.3's HARDCODED_SECRET/ARCHITECTURE_VIOLATION and stage_1.6/1.5's re-invocation share one spawn — the dedup secret.ts's comment already promises"
text: "When two stages invoke the same scanner in one gate, the system shall spawn the tool only once."
test_refs: ["tests/stages/scanner-cache.test.ts"]
- id: AC-17bf2e7c
ears: state
condition: "while no gate cache is primed (a standalone detector call, or the MCP read path)"
action: "run the tool directly with no caching"
response: "memoizeScan with a null cache calls run() verbatim — behavior outside a primed gate is byte-for-byte unchanged"
text: "While no cache is primed, the system shall run the scanner directly (unchanged behavior)."
test_refs: ["tests/stages/scanner-cache.test.ts"]
- id: AC-6e6daf3a
ears: unwanted
condition: "if the cache was primed for a gate run"
action: "clear it in a finally so a long-lived process never serves a stale scan across runs"
response: "clad check clears the cache in a finally after the stage loop; a fresh Map is created per gate run, so the MCP server's long-lived process cannot carry a scan across requests"
text: "If the cache was primed, then the system shall clear it in a finally to avoid stale cross-run scans."
test_refs: ["tests/stages/scanner-cache.test.ts"]
1 change: 1 addition & 0 deletions spec/index.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ features:
F-56abaa: {slug: intent-aware-init, status: done, modules: 5}
F-570a3f: {slug: mcp-structural-channel, status: done, modules: 1}
F-59f093: {slug: multidev-integration-test-and-scenario-regex, status: done, modules: 3}
F-5a49899e: {slug: gate-scanner-memo, status: done, modules: 5}
F-5b188856: {slug: graph-color-groups, status: done, modules: 4}
F-5b9f9f: {slug: spec-yaml-inventory-and-hints, status: done, modules: 6}
F-5d3ed2: {slug: postmortem-on-rollback, status: done, modules: 2}
Expand Down
11 changes: 11 additions & 0 deletions src/cli/clad.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import {runAudit} from '../stages/audit.js';
import {runCommit} from '../stages/commit.js';
import {runCov} from '../stages/cov.js';
import {runDrift} from '../stages/drift.js';
import {primeScannerCache} from '../stages/scanner-cache.js';
import {runLint} from '../stages/lint.js';
import {runPerf} from '../stages/perf.js';
import {runSecret} from '../stages/secret.js';
Expand Down Expand Up @@ -466,6 +467,13 @@ export function runCheckStages(opts: {internal?: boolean; strict?: boolean; tier
const pulseKindOf = (s: GateStatus): PulseKind =>
s === 'pass' ? 'pass' : s === 'liveness' ? 'note' : s === 'na' ? 'skip' : isBlocking(s) ? 'fail' : 'skip';
const collected: {stage: string; label: string; status: GateStatus; exitCode: number; stderr?: string; findings?: readonly DriftFinding[]}[] = [];
// Gate-scoped scanner memo (F-5a49899e): the Drift stage and the Secret/Arch
// stages each reach HARDCODED_SECRET / ARCHITECTURE_VIOLATION, which shell out
// to secretlint / madge (~4.4s / ~1.4s). Priming the memo here makes the second
// invocation a cache hit, so each tool spawns once per gate instead of twice.
// Cleared in finally — the long-lived MCP server must not carry a scan across runs.
primeScannerCache(true);
try {
for (const [name, run] of stages) {
const r = run({}) as {
pass: boolean;
Expand All @@ -492,6 +500,9 @@ export function runCheckStages(opts: {internal?: boolean; strict?: boolean; tier
if (isBlocking(status)) printStageDetails(r);
}
}
} finally {
primeScannerCache(false);
}
// STRICT SKIP-POLICY (F-67d2e9, generalizes the 0.5.x unit-only guard).
// Under --strict, a skipped stage the spec DEMANDS is a fail: 1.1 when a
// declared language ships done features, 2.1 when done features declare
Expand Down
47 changes: 25 additions & 22 deletions src/stages/detectors/architecture-violation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,14 @@
// imports (rust, go, java) do not register an arch gate — the detector
// emits a single `info` finding for them.

import {resolve} from 'node:path';

import {execaSync} from 'execa';

import {detectToolchain} from '../toolchain/detect.js';
import type {CommandStageOptions, DriftDetector, DriftFinding} from '../types.js';
import {classifyScannerExit, isMissingBinary} from '../util.js';
import {memoizeScan} from '../scanner-cache.js';

const NAME = 'ARCHITECTURE_VIOLATION';

Expand Down Expand Up @@ -40,28 +43,28 @@ function runArchitectureViolation(opts: CommandStageOptions): readonly DriftFind
},
];
}
const proc = execaSync(spec.cmd, [...spec.args], {cwd, reject: false});
// execaSync(reject:false) RETURNS (does not throw) on a missing binary, so
// ENOENT must be detected on the RESULT — a try/catch here would be dead code
// and let a registered-but-uninstalled validator fall through to a FALSE
// "architecture violations" error finding (a missing tool is a config gap).
if (isMissingBinary(proc)) {
return [
{
detector: NAME,
severity: 'info',
message: `architecture validator '${spec.cmd}' not installed`,
},
];
}
// The validator RAN but exited non-zero. A real cycle/boundary violation blocks
// (error); a config/setup gap (validator present but unconfigured) skips (info).
return classifyScannerExit(
proc,
NAME,
(detail) => `${spec.cmd} reported architecture violations: ${detail}`,
(detail) => `${spec.cmd} could not validate (config/setup gap, not a violation): ${detail}`,
);
// Gate-scoped memo (F-5a49899e): the Drift stage AND the Arch stage both reach
// this detector in one gate run — without memoization madge spawns twice
// (~1.4s each). Keyed by (cwd, cmd, args); a pass-through when no gate cache is
// primed, so behavior outside a gate is unchanged.
return memoizeScan(`arch:${resolve(cwd)}:${spec.cmd}:${spec.args.join(' ')}`, () => {
const proc = execaSync(spec.cmd, [...spec.args], {cwd, reject: false});
// execaSync(reject:false) RETURNS (does not throw) on a missing binary, so
// ENOENT must be detected on the RESULT — a try/catch here would be dead code
// and let a registered-but-uninstalled validator fall through to a FALSE
// "architecture violations" error finding (a missing tool is a config gap).
if (isMissingBinary(proc)) {
return [{detector: NAME, severity: 'info', message: `architecture validator '${spec.cmd}' not installed`}];
}
// The validator RAN but exited non-zero. A real cycle/boundary violation blocks
// (error); a config/setup gap (validator present but unconfigured) skips (info).
return classifyScannerExit(
proc,
NAME,
(detail) => `${spec.cmd} reported architecture violations: ${detail}`,
(detail) => `${spec.cmd} could not validate (config/setup gap, not a violation): ${detail}`,
);
});
}

export const architectureViolation: DriftDetector = {
Expand Down
Loading
Loading