Skip to content
Merged
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
10 changes: 10 additions & 0 deletions crates/trodden-store/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ use std::{
};

use anyhow::{Context, Result, bail};
use jiff::Timestamp;
use rusqlite::{Connection, ErrorCode, OpenFlags, OptionalExtension, params};

pub use ingest::{ExtractionRecord, Progress};
Expand Down Expand Up @@ -125,6 +126,15 @@ impl Store {
self.set_setting("verify_reminder", if on { "1" } else { "0" })
}

pub fn forgotten_before(&self) -> Result<Option<Timestamp>> {
self.setting("forgotten_before")?
.map(|at| {
at.parse()
.context("parse the time everything was forgotten")
})
.transpose()
}

pub fn embedding_scheme(&self) -> Result<Option<String>> {
self.setting("embedding_scheme")
}
Expand Down
26 changes: 26 additions & 0 deletions crates/trodden-store/src/procedures.rs
Original file line number Diff line number Diff line change
Expand Up @@ -686,6 +686,12 @@ impl Store {
if matches!(target, Forget::All) {
tx.execute_batch("DELETE FROM extractions; DELETE FROM sessions; DELETE FROM repos;")
.context("delete ingest history")?;
tx.execute(
"INSERT INTO settings (key, value) VALUES ('forgotten_before', ?1)
ON CONFLICT (key) DO UPDATE SET value = excluded.value",
[Timestamp::now().to_string()],
)
.context("remember when everything was forgotten")?;
}
tx.commit().context("commit forgetting")?;
Ok(deleted)
Expand Down Expand Up @@ -1071,6 +1077,26 @@ mod tests {
);
}

#[test]
fn forgetting_everything_remembers_when() {
let mut store = Store::open_in_memory().expect("store opens");
assert_eq!(store.forgotten_before().expect("setting reads"), None);
let before = Timestamp::now();

store.forget(Forget::All).expect("forgotten");

let forgotten = store
.forgotten_before()
.expect("setting reads")
.expect("the forget time is recorded");
assert!(before <= forgotten && forgotten <= Timestamp::now());
store.forget(Forget::Repo(REPO)).expect("forgotten again");
assert_eq!(
store.forgotten_before().expect("setting reads"),
Some(forgotten)
);
}

#[test]
fn relearning_a_quarantined_procedure_keeps_it_quarantined() {
for (check, expected) in [
Expand Down
77 changes: 74 additions & 3 deletions crates/trodden/src/ingest.rs
Original file line number Diff line number Diff line change
Expand Up @@ -386,19 +386,24 @@ impl Ingest {
};
let mut extracted_through = already;
let mut tasks = Vec::new();
let forgotten = self.store.forgotten_before()?;
let done = |seq: u32, at: Timestamp| {
already.is_some_and(|through| seq <= through)
|| forgotten.is_some_and(|forgot| at <= forgot)
};

let checks = ProjectChecks::discover(&workspace.root)
.redacted(|command| self.redactor.redact(command).into_owned());
for extraction in Extractor::new(checks).extract(&trace, &workspace.repo, ended) {
if already.is_some_and(|done| extraction.last_seq <= done) {
if done(extraction.last_seq, extraction.ended_at) {
continue;
}
tasks.push(TaskSpan {
first_seq: extraction.first_seq,
last_seq: extraction.last_seq,
outcome: extraction.outcome,
});
if already.is_some_and(|done| extraction.first_seq <= done) {
if done(extraction.first_seq, extraction.started_at) {
continue;
}
report.tasks += 1;
Expand Down Expand Up @@ -552,7 +557,7 @@ impl Ingest {
#[cfg(test)]
mod tests {
use serde_json::{Value, json};
use trodden_store::{Cue, Injection};
use trodden_store::{Cue, Forget, Injection};

use super::*;

Expand Down Expand Up @@ -806,6 +811,21 @@ mod tests {
]
}

fn tomorrow(lines: &[Value]) -> Vec<Value> {
let tomorrow = Timestamp::now()
.checked_add(jiff::SignedDuration::from_hours(24))
.expect("tomorrow is representable")
.strftime("%Y-%m-%d")
.to_string();
lines
.iter()
.map(|line| {
serde_json::from_str(&line.to_string().replace("2026-10-01", &tomorrow))
.expect("moved line is JSON")
})
.collect()
}

fn inject(ingest: &Ingest, minute: u32) {
let row = ingest
.store
Expand Down Expand Up @@ -949,6 +969,57 @@ mod tests {
);
}

#[test]
fn live_sessions_do_not_relearn_what_was_forgotten() {
let scratch = Scratch::new("forget-live");
let mut ingest = scratch.ingest();
scratch.append(&Scratch::task(
0,
"Page 2 in src/paginate.js repeats the last product from page 1",
"src/paginate.js",
));
scratch.append(&Scratch::task(
5,
"Checkout in src/checkout.js charges shipping twice",
"src/checkout.js",
));
let learned = ingest
.transcript(Harness::ClaudeCode, &scratch.transcript, false)
.expect("ingest before forgetting");
assert_eq!((learned.tasks, learned.created), (1, 1));

ingest
.store
.forget(Forget::All)
.expect("everything is forgotten");
scratch.append(&Scratch::tomorrow(&Scratch::task(
10,
"The cart total in src/cart.js ignores the discount code",
"src/cart.js",
)));
scratch.append(&Scratch::tomorrow(&Scratch::task(
15,
"The search in src/search.js drops accented names",
"src/search.js",
)));
let next = ingest
.transcript(Harness::ClaudeCode, &scratch.transcript, false)
.expect("ingest after forgetting");

assert_eq!((next.tasks, next.created), (1, 1));
let titles: Vec<_> = ingest
.store
.list(None, true)
.expect("procedures list")
.into_iter()
.map(|row| row.procedure.title)
.collect();
assert_eq!(
titles,
["The cart total in src/cart.js ignores the discount code"]
);
}

#[test]
fn sessions_spanning_a_pause_learn_only_what_followed_it() {
let scratch = Scratch::new("spanning-pause");
Expand Down
Loading