Status: proposed
Issue: #562
CodeDecay should evolve from a PR regression-risk tool into a unified, local-first code safety harness.
The harness should answer one question before a PR is merged:
What could this PR break, is it secure, and do the tests actually prove it
will not?
The product direction is agent-first but not agent-dependent. CodeDecay should produce deterministic evidence that any user-owned coding agent, desktop app, MCP client, or local review workflow can consume. Optional AI investigation can add suggestions, but the free baseline must stay useful without a model call.
Positioning:
CodeDecay is an open-source PR red-team harness for AI-assisted development.
It maps impact, audits tests, checks security-sensitive code paths, runs
configured open-source tools, and packages evidence for the user's own agent.
This RFC studies Vercel Labs deepsec as a reference architecture. CodeDecay
should adapt the strongest ideas: staged pipelines, matcher registries,
per-file state, revalidation, enrichment, and plugin contracts. CodeDecay
should not copy an AI-required or cloud-gateway-required workflow.
The OSS repository must remain:
- local-first
- deterministic by default
- useful without telemetry
- useful without API keys
- useful without required LLM or model calls
- useful without CodeDecayCloud
- explicit about command execution
- clear about which evidence is trusted tool output versus untrusted AI suggestion
AI, external memory, and hosted services are optional adapters. They must never
be hidden prerequisites for codedecay analyze or deterministic
codedecay redteam.
Current package ownership already points in the right direction:
| Package | Current responsibility | Gap for unified harness |
|---|---|---|
packages/core |
Shared report, findings, impact, scoring, product-failure types | Needs security dimension, candidate/finding distinction, and unified evidence provenance |
packages/git |
Changed files, refs, repo roots, untracked/deleted/renamed files, worktrees | Needs whole-repo audit file discovery integration |
packages/analyzer-js |
JS/TS deterministic impact, routes, risky areas, decay, weak-test signals, runtime coverage | Needs pluggable language/parser boundary and security matcher inputs |
packages/report |
JSON, Markdown, SARIF rendering | Needs unified report sections for security candidates, revalidation, enrichment, and audit state |
packages/cli |
User-facing commands including analyze, redteam, agent, execute, differential, mcp, product | Needs new audit/investigate/revalidate flows once RFC is implemented |
packages/redteam |
Deterministic merge-safety report assembly | Needs to become the stage orchestrator for unified evidence, not just a wrapper around analyze |
packages/harness |
Harness interface, evidence, registry, failure modes | Good foundation; needs broader plugin coordination and stage metadata |
packages/memory |
Local repo memory and learn/import helpers | Needs feedback loop from confirmed findings and regressions |
packages/llm |
Disabled/Ollama/LiteLLM provider abstraction | Good foundation; needs explicit investigate/revalidate prompt contracts and refusal handling |
packages/mcp |
Local MCP tools for analysis, execution, product checks | Needs tools for audit, investigate, revalidate, and evidence retrieval |
packages/execution |
Safe configured command execution | Good foundation; must stay the only execution path for adapters |
packages/test-audit |
Test-proof classification and weak/missing-test audit | Needs deeper integration with mutation, coverage, and real behavior evidence |
packages/tool-adapters |
Agent process, coverage, Pact, Playwright, Schemathesis, Semgrep, Stryker adapters | Good foundation; should be registered through a plugin architecture |
packages/config |
.codedecay config, safety, commands, LLM, adapters |
Needs plugin config and audit storage config |
packages/agent |
Task bundles for user-owned agents | Needs to consume unified evidence and produce fix tasks without claiming proof |
packages/skills |
Local skill loading/summaries | Needs skill selection tied to impacted areas, security candidates, and weak tests |
packages/github-action |
Composite action wrapper | Should expose new deterministic safety harness modes as they land |
packages/github-app |
App server experiments | Should stay deterministic until sandboxing and permission design are ready |
The current deterministic scoring model has two scores: merge risk and decay risk. It does not yet have an explicit security score or a structured candidate/investigation/revalidation lifecycle.
deepsec uses a staged architecture:
scan -> process -> revalidate -> enrich -> export
Strong ideas worth adapting:
- Separate stages. Free scanning is separated from expensive processing and from later revalidation/enrichment.
- Per-file state. Each source file has a FileRecord containing candidates, findings, analysis history, git/ownership metadata, status, and lock state.
- Append-only history. Re-running analysis appends history and merges new findings instead of destructively replacing old ones.
- Matcher registry. Built-in and plugin matchers contribute candidate evidence. Matchers have slugs, descriptions, file patterns, noise tiers, optional gates, and examples.
- Plugin slots. Matchers, agents, notifiers, ownership, people, executor, and commands are extension points.
- Atomic claims. Long-running processing uses per-file locks so multiple workers do not clobber each other.
- Revalidation. Existing findings can be classified as true-positive, false-positive, fixed, or uncertain.
- Enrichment. Git history and ownership metadata can be attached after findings exist.
- Refusal and quota visibility. Model refusals, skipped files, and quota failures are recorded rather than hidden.
- Direct file-list scans. Diff-driven modes can write records for every listed file, including files with no matcher hit.
What CodeDecay should not copy:
- AI-required core. CodeDecay's deterministic baseline must work without a model.
- Hosted gateway dependency. No default hosted model or CodeDecayCloud dependency should be required.
- Candidate-as-gate behavior. Matcher hits must not be the only files that get analyzed in PR diff mode. Every changed source file in scope must be represented, even when no matcher fires.
- AI output as proof. AI suggestions must not raise deterministic scores by themselves.
- Silent skips. Skipped files, capped files, refusals, unavailable tools, and denied commands must appear as missing evidence or explicit limitations.
CLI / GitHub Action / MCP / future GitHub App
|
v
Unified safety harness
|
+--> git diff / file discovery
+--> deterministic analyzers
+--> security matchers
+--> impact map
+--> test audit
+--> local memory
+--> configured OSS tool adapters
+--> optional user-owned agent or LLM investigation
+--> optional revalidation
+--> optional ownership/enrichment
|
v
One merge-safety report
|
+--> regression risk
+--> maintainability decay
+--> weak/missing test proof
+--> security candidates/findings
+--> evidence and limitations
+--> tasks for the user's coding agent
All surfaces should use the same engine:
- CLI
- GitHub Action
- MCP server
- future GitHub App
CodeDecay needs a model that can represent:
- deterministic regression findings
- deterministic decay findings
- deterministic weak-test findings
- deterministic security candidates
- tool findings from adapters
- optional AI suggestions
- revalidation verdicts
- enrichment data
Proposed concepts:
type SafetyDimension =
| "regression"
| "decay"
| "test-proof"
| "security";
type EvidenceTrust =
| "deterministic"
| "tool"
| "memory"
| "ai-suggestion"
| "user";
type EvidenceConfidence =
| "direct"
| "heuristic"
| "structural"
| "unverified-suggestion";
type InvestigationStatus =
| "candidate"
| "confirmed"
| "false-positive"
| "fixed"
| "accepted-risk"
| "uncertain";Rules:
- Security matcher hits start as deterministic candidates.
- Deterministic direct findings can affect scores immediately.
- Tool findings can affect scores when the tool produced structured evidence.
- Memory and AI suggestions are context, not proof.
- Optional AI investigation can propose confirmation, but final reports must show that verdict as untrusted unless paired with deterministic/tool evidence.
- Reports should show limitations and skipped files beside findings.
Current scores:
mergeRiskScoredecayScore
Proposed addition:
securityScore
The top-level risk level can remain a rollup, but the report should expose separate breakdowns so a PR can be low regression risk and high security risk, or high decay risk but low security risk.
Proposed score breakdowns:
mergeRiskBreakdowndecayBreakdowntestProofBreakdownsecurityBreakdown
testProofBreakdown can start as report-only if adding a third/fourth score is
too disruptive for v1. The RFC phase should decide whether test proof is a
score or a supporting risk dimension.
Add a future package:
packages/matchers
Purpose:
- deterministic candidate generation for security-sensitive patterns
- framework entry-point coverage
- CWE-tagged rules
- plugin-contributed matchers
- reproducible examples and tests
Proposed public shape:
interface SecurityMatcher {
ruleId: string;
cwe?: string | undefined;
title: string;
description: string;
severity: "low" | "medium" | "high";
confidence: "direct" | "heuristic" | "entry-point";
languages: string[];
filePatterns: string[];
examples?: SecurityMatcherExample[] | undefined;
match(context: SecurityMatcherContext): SecurityCandidate[];
}
interface SecurityCandidate {
ruleId: string;
cwe?: string | undefined;
title: string;
severity: "low" | "medium" | "high";
file: string;
line?: number | undefined;
snippet?: string | undefined;
evidence: string;
confidence: "direct" | "heuristic" | "entry-point";
}Initial matcher classes:
- SQL injection candidates
- command execution candidates
- path traversal candidates
- SSRF candidates
- unsafe HTML rendering candidates
- hardcoded secret candidates
- missing auth entry-point candidates
- insecure cookie/session config candidates
- unsafe deserialization candidates
- prototype pollution candidates
Important coverage rule:
In PR diff mode, every changed source file in scope must be represented in the
analysis state. Matcher hits add evidence. Matcher misses must not remove the
file from analysis.
This avoids a candidate-gated false-negative path.
Current JS/TS analysis lives in packages/analyzer-js, with TypeScript/JS
specific parsing and route detection. The unified harness should add a
language/parser plugin boundary instead of hard-coding source extensions in one
package.
Proposed capabilities:
- classify language for a file
- parse symbols/routes/imports when supported
- normalize findings with line/snippet evidence
- report parser failures explicitly
- allow future Tree-sitter-backed parsing for non-JS languages
Initial implementation should keep JS/TS behavior intact and wrap it in a pluggable interface before adding new languages.
Default, free, CI-safe.
Inputs:
- cwd
- base/head or working tree diff
.codedecay/config.*.codedecay/memory.json- optional existing audit data
Work:
- git diff and changed-file discovery
- path/AST impact mapping
- risky-area detection
- decay checks
- weak-test audit
- security matcher candidates
- configured check planning
- local memory matching
- report generation
No commands execute unless explicitly configured and invoked through execution workflows. No model calls are made.
Explicit opt-in only.
Inputs:
- deterministic evidence bundle
- changed source file content
- selected memory context
- selected skills
- provider config for a user-owned local/BYOK model or agent
Work:
- ask targeted questions about candidates, weak tests, and missing edge cases
- request suggested reproductions or fix tasks
- ask whether any file or issue was skipped/refused
- store suggestions separately from deterministic evidence
Rules:
- no default cloud call
- provider must be configured explicitly
- failures become explicit evidence limitations
- AI suggestions never mutate deterministic scores alone
Normal PR analysis should not require persistent state. Whole-repo audit and incremental security review can use optional local state:
.codedecay/data/<project>/
|-- project.json
|-- config.json
|-- memory-snapshot.json
|-- files/
| `-- src/path/file.ts.json
|-- runs/
| `-- 20260629-abc123.json
`-- reports/
|-- latest.json
`-- latest.md
The directory should be gitignored by default. It is local cache and audit state, not required source code.
interface AuditProjectRecord {
projectId: string;
rootPath: string;
createdAt: string;
repoUrl?: string | undefined;
defaultBranch?: string | undefined;
configHash?: string | undefined;
}interface AuditRunRecord {
runId: string;
projectId: string;
rootPath: string;
type: "analyze" | "scan" | "investigate" | "revalidate" | "enrich";
scope: "pr-diff" | "whole-repo" | "file-list";
base?: string | undefined;
head?: string | undefined;
createdAt: string;
completedAt?: string | undefined;
status: "running" | "done" | "error";
stats: Record<string, number>;
}interface AuditFileRecord {
filePath: string;
projectId: string;
language?: string | undefined;
fileHash: string;
status: "pending" | "processing" | "analyzed" | "error" | "skipped";
lockedByRunId?: string | undefined;
candidates: UnifiedCandidate[];
findings: UnifiedFinding[];
analysisHistory: AuditAnalysisEntry[];
limitations: AuditLimitation[];
lastScannedAt?: string | undefined;
lastScannedRunId?: string | undefined;
}Rules:
- writes are append-only where possible
- re-runs are idempotent for unchanged files
- changed files are re-analyzed by content hash
- locks prevent clobbering concurrent runs
- skipped/capped files are recorded with reasons
- no state is required for normal PR diff mode
Revalidation should work in two layers.
Examples:
- candidate file no longer exists
- matching snippet no longer exists
- line range changed and matcher no longer fires
- configured tool now passes
- coverage artifact now includes the changed source lines
- weak-test pattern was removed
- missing nearby tests now exist
Examples:
- confirm whether an entry-point candidate is actually unauthenticated
- judge whether a test covers the real behavior path
- explain whether a fix likely addresses a regression
AI revalidation must be rendered as an untrusted suggestion unless paired with deterministic or tool evidence.
Verdicts:
confirmedfalse-positivefixedaccepted-riskuncertain
Current local memory is a good base. The unified harness should learn from confirmed evidence, not from arbitrary agent output.
Memory may store:
- important flows
- test commands
- invariants
- past regressions
- security-sensitive routes
- architecture notes
- known weak-test patterns
- required behavior probes
Loop-back rules:
- confirmed findings can become past-regression memory
- accepted risks can become review context
- user edits always win
- AI-generated memory suggestions require explicit review/apply
- memory is never executable authority
Default provider:
.codedecay/memory.json
Future optional providers:
- Supermemory
- Mem0
- CodeDecayCloud
CodeDecay should generalize current tool adapter configuration into a plugin registry. Plugins should be local JavaScript/TypeScript modules or package exports, loaded only from explicit config.
Proposed extension points:
| Slot | Purpose | Merge behavior |
|---|---|---|
matchers |
Deterministic security and impact candidates | additive, same id override must be explicit |
languages |
File classification and parser support | additive |
analyzers |
Deterministic repo/domain analyzers | additive |
agentProviders |
Optional user-owned agent or model providers | additive |
toolAdapters |
OSS tool harnesses | additive |
memoryProviders |
Local or external memory providers | explicit, local default |
ownership |
File/team/person ownership | single selected provider |
notifiers |
PR comments, issue creation, webhooks | additive, disabled by default |
Example config shape:
export default {
plugins: [
localSecurityPlugin(),
nextjsPlugin(),
codeownersPlugin()
],
safety: {
allowCommands: false,
commandTimeoutMs: 120000
}
};Safety:
- plugins cannot execute commands directly
- command execution must go through
packages/execution - plugins cannot call models unless registered as explicit agent providers and selected by the user
- plugin failures are report limitations, not silent skips
Enrichment should start deterministic:
- git blame or recent committers
- CODEOWNERS mapping
- package/workspace ownership if configured
Future optional enrichment:
- organization people directory
- team ownership service
- notifier routing
Triage should combine:
- severity
- confidence
- blast radius
- impacted routes/user flows
- security dimension
- weak/missing test proof
- ownership
- whether behavior changed between base/head
Triage should not hide lower-severity findings. It should prioritize review.
One report should include:
- Executive summary
- Merge risk score
- Decay score
- Security score or security section
- Test-proof status
- Impacted areas and routes
- Findings grouped by dimension and severity
- Security candidates and confidence
- Weak/missing tests
- Tool evidence
- Optional AI suggestions
- Revalidation verdicts
- Ownership/enrichment
- Limitations and skipped files
- Fix tasks for the user's coding agent
The report must keep this wall:
Deterministic/tool evidence != AI suggestion
Existing commands should remain stable.
Potential future commands:
codedecay audit --scope pr
codedecay audit --scope repo
codedecay investigate --input .codedecay/data/<project>/runs/<run>.json
codedecay revalidate --input .codedecay/data/<project>/reports/latest.json
codedecay enrich --ownership codeownersDo not add these commands before implementation issues are created.
Add a reproducible fixture repo with planted issues:
- SQL injection
- hardcoded secret
- missing auth
- path traversal
- SSRF
- command injection
- unsafe HTML rendering
- weak/fake tests
- missing real API test
- high-complexity change
- duplicate logic
- config/database regression
Benchmark modes:
| Mode | Expected result |
|---|---|
| deterministic diff | detects all changed-file regression/decay/test/security candidates with no keys and no model calls |
| deterministic whole-repo | records every source file, candidates, skips, and limitations |
| optional investigate | produces untrusted suggestions and records refusals/failures |
| revalidate | marks fixed/false-positive/uncertain without hiding prior history |
| incremental rerun | re-analyzes only changed files by hash |
Benchmark report should include:
- candidate recall
- confirmed finding precision after revalidation
- skipped/capped files
- duration
- cost, always
$0for deterministic mode - model/provider cost only for explicit optional investigate mode
Issue:
feat(matchers): add deterministic security matcher registry
Scope:
- add
packages/matchers - add security candidate type
- add matcher registry and tests
- add a minimal set of JS/TS security candidates
- render security candidates in reports
- do not call models
Acceptance:
- changed source files are represented even with zero matcher hits
- no silent skipped files
- deterministic scores remain stable
- CodeQL/security regex findings do not reintroduce unsafe regex patterns
Issue:
feat(language): add pluggable language and parser boundary
Scope:
- wrap existing JS/TS analyzer behind language interface
- keep current JS/TS behavior unchanged
- allow future Tree-sitter or compiler API plugins
Acceptance:
- current analyzer tests still pass
- unsupported files produce limitations, not crashes
Issue:
feat(investigate): add optional grounded investigation stage
Scope:
- explicit command or flag only
- uses existing
packages/llmprovider boundary - uses local memory and skills
- records suggestions separately
- records provider failure/refusal
Acceptance:
- disabled provider remains default
- deterministic report unchanged when investigate is not used
- AI suggestions cannot raise deterministic score alone
Issue:
feat(audit): add resumable local audit data model
Scope:
.codedecay/data/<project>/- project/run/file records
- content hash
- append-only history
- safe lock metadata
- whole-repo audit mode
Acceptance:
- normal PR mode does not require persistent state
- rerun skips unchanged files
- all skipped/capped files are recorded
Issue:
feat(revalidate): add deterministic revalidation and memory learning
Scope:
- deterministic revalidation checks first
- optional AI verdict second
- user-reviewed memory updates
Acceptance:
- fixed findings stay in history
- memory updates require explicit apply
- report shows true-positive/false-positive/fixed/uncertain state
Issue:
feat(plugins): add CodeDecay plugin registry
Scope:
- plugin config loading
- matchers/languages/analyzers/agent providers/tool adapters/memory/ownership/notifiers
- CODEOWNERS enrichment first
Acceptance:
- plugins are explicit
- plugin failures are visible
- adapters still execute only through
packages/execution
Issue:
test(benchmark): add unified harness planted-issue corpus
Scope:
- fixture repo or fixture workspace
- planted vulnerability/regression/weak-test cases
- deterministic run script
- output report
Acceptance:
- deterministic mode has no API/model calls
- benchmark records recall/limitations/duration
- CI can run the deterministic benchmark
| Risk | Mitigation |
|---|---|
| Regex matchers create false positives | mark candidates separately from confirmed findings, use severity/confidence, add examples |
| Matcher coverage creates false negatives | never gate changed-file analysis on matcher hits |
| Optional AI makes product look cloud-required | disabled provider remains default, docs show no-key path first |
| Plugin API becomes too broad too early | start with matchers and language boundary, then expand |
| Whole-repo audit state becomes confusing | keep it optional and local under .codedecay/data/ |
| Tool adapters run unsafe commands | only packages/execution can execute configured commands |
| Memory pollutes future analysis | memory is untrusted context and user-reviewable |
- Should
securityScorebe a first-class top-level score in the first matcher phase, or should security start as a report section with high-severity findings? - Should
.codedecay/data/live under.codedecay/local/data/to make the local-cache nature clearer? - Should plugin loading use ESM config only, or support JSON-declared package names first?
- What should be the first non-JS language target after the interface exists?
- Should optional investigate be a separate command or an explicit
redteam --investigatemode?
This RFC PR should be docs-only.
Required validation:
pnpm run lint
pnpm typecheck
pnpm test
pnpm buildNo feature code should be added until this RFC is reviewed and follow-up implementation issues are created.