Skip to content

Commit c4b8835

Browse files
authored
feat: add review-only mutation candidates (#9)
1 parent ff141af commit c4b8835

20 files changed

Lines changed: 1052 additions & 7 deletions

File tree

.mise.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,5 +30,5 @@ description = "Run the pull-request quality gate"
3030
depends = ["ci", "docs", "fmt", "lint", "test"]
3131

3232
[tasks.demo]
33-
description = "Run the offline Milestone 1 import-to-digest demonstration"
33+
description = "Run the offline import-to-mutation-candidate demonstration"
3434
run = "scripts/demo-milestone-1.sh"

Cargo.lock

Lines changed: 21 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ members = [
77
"crates/autophagy-cli",
88
"crates/autophagy-core",
99
"crates/autophagy-events",
10+
"crates/autophagy-mutations",
1011
"crates/autophagy-patterns",
1112
"crates/autophagy-redaction",
1213
"crates/autophagy-store",
@@ -24,6 +25,7 @@ clap = { version = "4.6.2", features = ["derive", "env"] }
2425
directories = "6.0"
2526
globset = "0.4"
2627
regex = "1.12"
28+
semver = "1.0"
2729
serde = { version = "1.0", features = ["derive"] }
2830
serde_json = "1.0"
2931
sha2 = "0.11"

README.md

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,9 @@ change because of what happened?”
1414
The local-only Milestone 1 engine is implemented: AEP v0.1, transactional
1515
SQLite storage, generic JSONL plus Claude Code and Codex adapters, deterministic
1616
evidence-linked findings, ingestion redaction, retention, export, and deletion.
17-
No daemon, mutation generation, replay, or background capture ships yet.
17+
Phase 2 now generates review-only, zero-permission mutation candidates. No
18+
daemon, replay, installation, autonomous execution, or background capture ships
19+
yet.
1820

1921
## Principles
2022

@@ -33,6 +35,7 @@ crates/autophagy-adapter-test-support/ Shared native-adapter conformance checks
3335
crates/autophagy-cli/ User-facing import, sessions, and search commands
3436
crates/autophagy-core/ Reusable streaming import application services
3537
crates/autophagy-events/ AEP Rust types, parsing, and validation
38+
crates/autophagy-mutations/ Versioned review-only mutation candidates
3639
crates/autophagy-patterns/ Model-free recurrence detectors and evidence packets
3740
crates/autophagy-redaction/ Secret rules and project/artifact path policy
3841
crates/autophagy-store/ SQLite migrations, idempotency, FTS, and deletion
@@ -42,6 +45,7 @@ docs/decisions/ Architecture decision records
4245
docs/roadmap/ Small pull-request delivery sequence
4346
docs/specs/aep/0.1/ Versioned AEP JSON Schema and examples
4447
docs/specs/evidence/0.1/ Versioned deterministic finding contract
48+
docs/specs/mutation/0.1/ Versioned mutation package contract
4549
```
4650

4751
The intended repository structure is documented in
@@ -95,16 +99,16 @@ The [deterministic findings guide](docs/guides/deterministic-findings.md)
9599
documents recurrence thresholds, signature normalization, counterexamples, and
96100
the versioned Evidence Packet contract.
97101

98-
## Run the offline milestone demo
102+
## Run the offline demo
99103

100104
```sh
101105
mise run demo
102106
```
103107

104108
The demo imports anonymized evidence, emits two deterministic patterns with
105109
exact evidence IDs, produces a digest that confirms no model or network was
106-
used, and previews retention deletion. Its temporary database is removed on
107-
exit.
110+
used, generates two zero-permission mutation candidates, and previews retention
111+
deletion. Its temporary database is removed on exit.
108112

109113
Useful privacy and lifecycle commands:
110114

@@ -120,6 +124,16 @@ autophagy delete all --confirm delete-all
120124
See the [privacy and lifecycle guide](docs/guides/privacy-and-lifecycle.md) and
121125
[threat model](docs/security/threat-model.md) for guarantees and limitations.
122126

127+
Inspect candidate packages directly:
128+
129+
```sh
130+
autophagy mutations
131+
autophagy --output json mutations --project /workspace/example
132+
```
133+
134+
See the [mutation candidate guide](docs/guides/mutation-candidates.md) for the
135+
contract, refusal behavior, and deliberately unavailable lifecycle actions.
136+
123137
## Try the contract
124138

125139
Install [mise](https://mise.jdx.dev/), then run:

crates/autophagy-cli/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ autophagy-adapter-claude-code = { path = "../../adapters/claude-code" }
1717
autophagy-adapter-codex = { path = "../../adapters/codex" }
1818
autophagy-core = { path = "../autophagy-core" }
1919
autophagy-events = { path = "../autophagy-events" }
20+
autophagy-mutations = { path = "../autophagy-mutations" }
2021
autophagy-patterns = { path = "../autophagy-patterns" }
2122
autophagy-store = { path = "../autophagy-store" }
2223
clap.workspace = true

crates/autophagy-cli/src/main.rs

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ use autophagy_adapter_codex::{
1616
};
1717
use autophagy_core::{ImportOptions, ImportSummary, import_jsonl};
1818
use autophagy_events::Event;
19+
use autophagy_mutations::{GenerationOutcome, generate_candidates};
1920
use autophagy_patterns::{DetectorConfig, EvidencePacket, detect};
2021
use autophagy_store::{
2122
DeleteAllSummary, DeleteSummary, EventStore, PruneSummary, SearchHit, SessionSummary,
@@ -150,6 +151,16 @@ enum Commands {
150151
thresholds: ThresholdArgs,
151152
},
152153

154+
/// Propose review-only, zero-permission mutation candidates from findings.
155+
Mutations {
156+
/// Limit candidate generation to one exact project path.
157+
#[arg(long, value_name = "PATH")]
158+
project: Option<String>,
159+
160+
#[command(flatten)]
161+
thresholds: ThresholdArgs,
162+
},
163+
153164
/// Export redacted canonical AEP events as JSONL to standard output.
154165
Export {
155166
/// Limit export to one exact project path.
@@ -228,6 +239,7 @@ enum CommandReport {
228239
Search(Vec<SearchHit>),
229240
Digest(DigestReport),
230241
Patterns(Vec<EvidencePacket>),
242+
Mutations(Vec<GenerationOutcome>),
231243
Export(Vec<Event>),
232244
Prune(PruneSummary),
233245
DeleteSession(DeleteSummary),
@@ -270,6 +282,7 @@ impl CommandReport {
270282
| Self::Search(_)
271283
| Self::Digest(_)
272284
| Self::Patterns(_)
285+
| Self::Mutations(_)
273286
| Self::Export(_)
274287
| Self::Prune(_)
275288
| Self::DeleteSession(_)
@@ -446,6 +459,16 @@ fn execute(cli: Cli) -> Result<CommandReport, CliError> {
446459
let events = store.list_events_for_detection(project.as_deref())?;
447460
Ok(CommandReport::Patterns(detect(&events, thresholds.into())))
448461
}
462+
Commands::Mutations {
463+
project,
464+
thresholds,
465+
} => {
466+
let database = resolve_database_path(cli.database)?;
467+
let store = open_store(&database)?;
468+
let events = store.list_events_for_detection(project.as_deref())?;
469+
let findings = detect(&events, thresholds.into());
470+
Ok(CommandReport::Mutations(generate_candidates(&findings)))
471+
}
449472
Commands::Export { project } => {
450473
let database = resolve_database_path(cli.database)?;
451474
let store = open_store(&database)?;
@@ -580,6 +603,7 @@ fn write_report(
580603
write_findings(&mut writer, &report.findings)?;
581604
}
582605
CommandReport::Patterns(findings) => write_findings(&mut writer, findings)?,
606+
CommandReport::Mutations(outcomes) => write_mutations(&mut writer, outcomes)?,
583607
CommandReport::Prune(summary) => writeln!(
584608
writer,
585609
"{} sessions · {} events · {} artifacts{}",
@@ -631,6 +655,27 @@ fn write_findings(writer: &mut impl Write, findings: &[EvidencePacket]) -> io::R
631655
Ok(())
632656
}
633657

658+
fn write_mutations(writer: &mut impl Write, outcomes: &[GenerationOutcome]) -> io::Result<()> {
659+
if outcomes.is_empty() {
660+
writeln!(writer, "no mutation candidates above evidence threshold")?;
661+
}
662+
for outcome in outcomes {
663+
match outcome {
664+
GenerationOutcome::Candidate { package } => writeln!(
665+
writer,
666+
"{}\t{}\t{} evidence\tzero permissions\tcandidate",
667+
package.mutation_id,
668+
package.title,
669+
package.hypothesis.supporting_event_ids.len()
670+
)?,
671+
GenerationOutcome::InsufficientEvidence { finding_id, reason } => {
672+
writeln!(writer, "{finding_id}\tinsufficient evidence\t{reason}")?;
673+
}
674+
}
675+
}
676+
Ok(())
677+
}
678+
634679
fn write_codex_import_summary(
635680
writer: &mut impl Write,
636681
summary: &CodexImportSummary,

crates/autophagy-cli/tests/cli.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,21 @@ fn milestone_demo_digests_exports_deletes_and_prunes_offline() {
207207
2
208208
);
209209

210+
let mutations = run_json(&database, ["mutations"]);
211+
let candidates = mutations["result"].as_array().expect("candidates");
212+
assert_eq!(candidates.len(), 2);
213+
for outcome in candidates {
214+
assert_eq!(outcome["status"], "candidate");
215+
assert_eq!(outcome["package"]["state"], "candidate");
216+
assert_eq!(outcome["package"]["permissions"]["network"], false);
217+
assert!(
218+
outcome["package"]["permissions"]["commands"]
219+
.as_array()
220+
.expect("commands")
221+
.is_empty()
222+
);
223+
}
224+
210225
let exported = command(&database).arg("export").output().expect("export");
211226
assert!(exported.status.success());
212227
let lines = String::from_utf8(exported.stdout).expect("UTF-8 export");
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
[package]
2+
name = "autophagy-mutations"
3+
description = "Versioned, permission-scoped mutation candidates for Autophagy"
4+
version.workspace = true
5+
edition.workspace = true
6+
rust-version.workspace = true
7+
license.workspace = true
8+
repository.workspace = true
9+
publish = false
10+
11+
[dependencies]
12+
autophagy-patterns = { path = "../autophagy-patterns" }
13+
semver.workspace = true
14+
serde.workspace = true
15+
sha2.workspace = true
16+
17+
[dev-dependencies]
18+
autophagy-core = { path = "../autophagy-core" }
19+
autophagy-store = { path = "../autophagy-store" }
20+
jsonschema = { version = "0.47", default-features = false }
21+
serde_json.workspace = true
22+
23+
[lints]
24+
workspace = true

0 commit comments

Comments
 (0)