flagon-io/g1t

public

Git for AI scale: a forge for thousands of agents working on the same code at once.

g1t/services/work/src/memory.rs

733 lines30,758 bytesCodeBlame
//! Memory: what agents and people have learned that the next agent should
//! know, at two levels. A project's memory is about its codebase; the
//! workspace's is true across its projects ("we use pnpm everywhere",
//! "staging lives at …").
//!
//! A workspace's memory is members-only, since it can hold internal
//! knowledge; a project's is for whoever can read the project, and changed
//! by whoever can push to it. It never holds a secret: text that looks like a key or a token is refused. Every
//! g1t agent run is given it (`memory_context`): pinned first, then what
//! was used most recently, within a size budget.

use g1t_contracts::access::{self, Capability};
use g1t_contracts::agents::*;
use g1t_contracts::repos::{Repo, RepoPath};
use g1t_contracts::time::rfc3339;
use g1t_contracts::{FailureCode, Outcome, PrincipalKind, User, Viewer, new_id};
use g1t_kit::now_ms;
use serde::Deserialize;
use worker::Result;
use worker::wasm_bindgen::JsValue;

use crate::Work;
use crate::runs::member_of;

/// A workspace renamed: its runs and memories move to the slug it has now.
/// `?1` is the current slug, `?2` a stale one (see `g1t_kit::rename`).
pub(crate) const RENAMED: &[&str] = &[
    "UPDATE agent_runs SET workspace = ?1 WHERE workspace = ?2",
    "UPDATE agent_runs SET repo = ?1 || substr(repo, length(?2) + 1) WHERE substr(repo, 1, length(?2) + 1) = ?2 || '/'",
    "UPDATE memories SET scope_key = ?1 WHERE scope = 'workspace' AND scope_key = ?2",
    "UPDATE memories SET workspace = ?1 WHERE workspace = ?2",
    "UPDATE memories SET repo = ?1 || substr(repo, length(?2) + 1) WHERE substr(repo, 1, length(?2) + 1) = ?2 || '/'",
    "UPDATE memories SET source_repo = ?1 || substr(source_repo, length(?2) + 1) WHERE substr(source_repo, 1, length(?2) + 1) = ?2 || '/'",
];

/// A repository transferred (see `g1t_kit::transfer`): its agent runs and
/// its project's memory go with it, as its issues and pull requests do by
/// id. `?1`/`?2` the path now and before, `?3`/`?4` the workspaces, `?5`
/// the repository's id.
pub(crate) const TRANSFERRED: &[&str] = &[
    "UPDATE agent_runs SET workspace = ?3, repo = ?1 WHERE repo_id = ?5 AND repo = ?2",
    "UPDATE memories SET workspace = ?3, repo = ?1 WHERE scope = 'project' AND scope_key = ?5 AND workspace = ?4",
    "UPDATE memories SET source_repo = ?1 WHERE source_repo = ?2",
];

/// A workspace deleted: its own memory, guardrails and queued runs go.
/// `?1` is its slug. Project memory went with each repository's transfer.
pub(crate) const DELETED: &[&str] = &[
    "DELETE FROM memories WHERE scope = 'workspace' AND scope_key = ?1",
    "DELETE FROM guardrails WHERE scope = 'workspace' AND scope_key = ?1",
    "DELETE FROM agent_waits WHERE workspace = ?1",
];

/// The most memories one project, or one workspace, keeps.
const MAX_PER_SCOPE: u32 = 500;
/// What an agent is given by default, in characters.
const DEFAULT_BUDGET: u32 = 6000;
const MAX_BUDGET: u32 = 20_000;
const DEFAULT_RECALL: u32 = 20;

#[derive(Deserialize)]
pub(crate) struct MemoryRow {
    id: String,
    pub(crate) scope: String,
    pub(crate) scope_key: String,
    workspace: String,
    repo: Option<String>,
    text: String,
    kind: String,
    source_kind: String,
    source_run: Option<String>,
    source_repo: Option<String>,
    source_number: Option<u32>,
    created_by: String,
    pinned: u32,
    created_at: String,
    updated_at: String,
    last_used_at: Option<String>,
    // What capture adds (capture.rs, migration 0019).
    #[serde(default)]
    status: Option<String>,
    #[serde(default)]
    confidence: Option<f64>,
    #[serde(default)]
    source_ref: Option<String>,
    #[serde(default)]
    evidence: Option<String>,
    #[serde(default)]
    sources: Option<String>,
}

fn path(text: &str) -> Option<RepoPath> {
    let (namespace, name) = text.split_once('/')?;
    Some(RepoPath {
        namespace: namespace.to_owned(),
        name: name.to_owned(),
    })
}

impl From<MemoryRow> for Memory {
    fn from(row: MemoryRow) -> Self {
        Memory {
            id: row.id,
            scope: if row.scope == "workspace" {
                MemoryScope::Workspace
            } else {
                MemoryScope::Project
            },
            workspace: row.workspace,
            repo: row.repo.as_deref().and_then(path),
            text: row.text,
            kind: MemoryKind::parse(&row.kind).unwrap_or_default(),
            source: MemorySource {
                kind: row.source_kind,
                run_id: row.source_run,
                repo: row.source_repo.as_deref().and_then(path),
                number: row.source_number,
                reference: row.source_ref,
                evidence: row.evidence,
            },
            created_by: row.created_by,
            pinned: row.pinned != 0,
            created_at: row.created_at,
            updated_at: row.updated_at,
            last_used_at: row.last_used_at,
            status: row.status.as_deref().and_then(MemoryStatus::parse).unwrap_or_default(),
            confidence: row.confidence,
            seen: row
                .sources
                .as_deref()
                .and_then(|sources| serde_json::from_str::<Vec<String>>(sources).ok())
                .map_or(1, |sources| sources.len().max(1) as u32),
        }
    }
}

/// The text tidied, or why it cannot be kept.
fn valid_text(text: &str) -> std::result::Result<String, String> {
    let text = text.trim();
    if text.is_empty() {
        return Err("Write what should be remembered.".into());
    }
    if text.chars().count() > MAX_MEMORY_CHARS {
        return Err(format!(
            "Keep a memory to {MAX_MEMORY_CHARS} characters: one fact, convention, decision or gotcha each."
        ));
    }
    if let Some(what) = secret_in(text) {
        return Err(format!(
            "That looks like {what}. Memory is read by every agent in the workspace, so it never holds secrets. Say where the secret lives instead, such as \"the deploy key is the DEPLOY_KEY secret\"."
        ));
    }
    Ok(text.to_owned())
}

/// Why a person cannot change a memory.
const CHANGE_DENIED: &str = "Only members of the workspace can change its memory, and a project's only with the Write role on it.";

fn denied<T>() -> Outcome<T> {
    Outcome::fail(
        FailureCode::Forbidden,
        "Memory is for members of the workspace and its agents.",
    )
}

/// Whether `actor` may change a memory of `workspace`: the workspace's own
/// needs a member; a project's (`repo_id`, its repository) the Write role
/// on that repository, as pushing to it does.
fn may_write(actor: &User, workspace: &str, repo_id: Option<&str>) -> bool {
    let able = actor.verified || actor.kind != PrincipalKind::User;
    able && match repo_id {
        None => actor.is_member(workspace),
        // Write is never had through being public, so treating it as
        // private changes nothing.
        Some(id) => access::can(
            Some(actor),
            access::RepoRef { id, namespace: workspace, private: true },
            Capability::Push,
        ),
    }
}

/// The order memories are given and listed in: pinned, then most recently
/// used or changed.
const ORDER: &str = "pinned DESC, COALESCE(last_used_at, updated_at) DESC, created_at DESC";

/// What an agent is told about memory, ahead of the memories themselves.
const PREAMBLE: &str = "What people and agents have learned here before you, kept as memory. Treat it as notes from colleagues: usually right, sometimes out of date. Where it disagrees with the code, the code wins; say so in your summary.";

/// How an agent is told to add to memory.
const HOW_TO_REMEMBER: &str = "When you learn something the next agent here would need (how to build or test, a convention, a decision and why, a trap), save it with the remember tool: scope project for this codebase, workspace for what holds across the workspace's projects. One short fact each. Never a secret, a key or a token. recall searches what is kept.";

/// The context an agent gets: as many memories as fit in `budget`, each
/// level labelled, and the ids of those given.
fn compose(workspace: &[Memory], project: &[Memory], repo: &RepoPath, budget: usize) -> (Option<String>, Vec<String>) {
    let line = |memory: &Memory| {
        format!(
            "- [{}{}] {}",
            memory.kind.as_str(),
            if memory.pinned { ", pinned" } else { "" },
            memory.text.replace('\n', " ")
        )
    };
    let mut used = PREAMBLE.len() + HOW_TO_REMEMBER.len() + 200;
    let mut given = Vec::new();
    let mut sections = Vec::new();
    // Pinned memories of either level go in before anything else does.
    let mut take = |memories: &[Memory], pinned: bool, out: &mut Vec<String>| {
        for memory in memories.iter().filter(|memory| memory.pinned == pinned) {
            let text = line(memory);
            if used + text.len() + 1 > budget {
                continue;
            }
            used += text.len() + 1;
            given.push(memory.id.clone());
            out.push(text);
        }
    };
    let (mut ws, mut own) = (Vec::new(), Vec::new());
    take(workspace, true, &mut ws);
    take(project, true, &mut own);
    take(project, false, &mut own);
    take(workspace, false, &mut ws);
    if ws.is_empty() && own.is_empty() {
        return (None, given);
    }
    sections.push(PREAMBLE.to_owned());
    if !ws.is_empty() {
        sections.push(format!(
            "Workspace memory (true across the {} workspace's projects):\n{}",
            repo.namespace,
            ws.join("\n")
        ));
    }
    if !own.is_empty() {
        sections.push(format!(
            "Project memory ({}/{}):\n{}",
            repo.namespace,
            repo.name,
            own.join("\n")
        ));
    }
    sections.push(HOW_TO_REMEMBER.to_owned());
    (Some(sections.join("\n\n")), given)
}

impl Work {
    async fn memories_of(&self, scope: MemoryScope, key: &str, limit: u32) -> Result<Vec<Memory>> {
        Ok(self
            .db
            .prepare(format!(
                // Only what is kept: candidates wait for review (capture.rs).
                "SELECT * FROM memories WHERE scope = ? AND scope_key = ? AND status = 'kept' ORDER BY {ORDER} LIMIT ?"
            ))
            .bind(&[scope.as_str().into(), key.into(), limit.into()])?
            .all()
            .await?
            .results::<MemoryRow>()?
            .into_iter()
            .map(Memory::from)
            .collect())
    }

    async fn memory_row(&self, workspace: &str, id: &str) -> Result<Option<Memory>> {
        Ok(self
            .db
            .prepare("SELECT * FROM memories WHERE id = ? AND workspace = ?")
            .bind(&[id.into(), workspace.into()])?
            .first::<MemoryRow>(None)
            .await?
            .map(Memory::from))
    }

    /// The repository a project's memory is about; none for the workspace's.
    async fn memory_repo_id(&self, id: &str) -> Result<Option<String>> {
        Ok(self
            .db
            .prepare("SELECT scope_key AS value FROM memories WHERE id = ? AND scope = 'project'")
            .bind(&[id.into()])?
            .first::<String>(Some("value"))
            .await?)
    }

    async fn mark_used(&self, ids: &[String]) -> Result<()> {
        if ids.is_empty() {
            return Ok(());
        }
        self.db
            .prepare("UPDATE memories SET last_used_at = ? WHERE id IN (SELECT value FROM json_each(?))")
            .bind(&[rfc3339(now_ms()).into(), serde_json::to_string(ids)?.into()])?
            .run()
            .await?;
        Ok(())
    }

    /// The project's repository, which must be in `workspace`.
    async fn project_repo(&self, path: &RepoPath, viewer: &Viewer, workspace: &str) -> Result<Outcome<Repo>> {
        Ok(match self.repo(path, viewer).await? {
            Outcome::Ok(repo) if repo.namespace.to_lowercase() == workspace => Outcome::Ok(repo),
            Outcome::Ok(_) => Outcome::fail(FailureCode::Invalid, "That project is in another workspace."),
            Outcome::Fail(failure) => Outcome::Fail(failure),
        })
    }

    pub(crate) async fn list_memories(&self, a: ListMemoriesArgs) -> Result<Outcome<Memories>> {
        let workspace = a.workspace.to_lowercase();
        let member = a.viewer.as_ref().is_some_and(|viewer| viewer.is_member(&workspace));
        // Someone else who can read the project sees its memory alone.
        if !member && a.repo.is_none() {
            return Ok(denied());
        }
        let project = match &a.repo {
            Some(path) => match self.project_repo(path, &a.viewer, &workspace).await? {
                Outcome::Ok(repo) => self.memories_of(MemoryScope::Project, &repo.id, MAX_PER_SCOPE).await?,
                Outcome::Fail(failure) => return Ok(Outcome::Fail(failure)),
            },
            None => Vec::new(),
        };
        let workspace = if member {
            self.memories_of(MemoryScope::Workspace, &workspace, MAX_PER_SCOPE).await?
        } else {
            Vec::new()
        };
        Ok(Outcome::Ok(Memories { project, workspace }))
    }

    pub(crate) async fn add_memory(&self, a: AddMemoryArgs) -> Result<Outcome<Memory>> {
        let workspace = a.workspace.to_lowercase();
        if a.scope == MemoryScope::Workspace && !may_write(&a.actor, &workspace, None) {
            return Ok(denied());
        }
        let text = match valid_text(&a.text) {
            Ok(text) => text,
            Err(message) => return Ok(Outcome::fail(FailureCode::Invalid, message)),
        };
        let viewer = Some(a.actor.clone());
        let repo = match &a.repo {
            Some(path) => match self.project_repo(path, &viewer, &workspace).await? {
                Outcome::Ok(repo) => Some(repo),
                Outcome::Fail(failure) => return Ok(Outcome::Fail(failure)),
            },
            None => None,
        };
        let key = match (a.scope, &repo) {
            (MemoryScope::Workspace, _) => workspace.clone(),
            (MemoryScope::Project, Some(repo)) => {
                if !may_write(&a.actor, &workspace, Some(&repo.id)) {
                    return Ok(Outcome::fail(FailureCode::Forbidden, CHANGE_DENIED));
                }
                repo.id.clone()
            }
            (MemoryScope::Project, None) => {
                return Ok(Outcome::fail(FailureCode::Invalid, "Name the project this memory is about."));
            }
        };
        // The same thing remembered twice is one memory, freshened; written
        // by hand, a candidate or a dismissed one with the same words is
        // kept.
        let now = rfc3339(now_ms());
        let print = g1t_contracts::capture::fingerprint(&text);
        let same = self
            .db
            .prepare(
                "UPDATE memories SET updated_at = ?, status = 'kept'
                 WHERE scope = ? AND scope_key = ? AND (text = ? OR fingerprint = ?) RETURNING id AS value",
            )
            .bind(&[now.as_str().into(), a.scope.as_str().into(), key.as_str().into(), text.as_str().into(), print.as_str().into()])?
            .first::<String>(Some("value"))
            .await?;
        if let Some(id) = same {
            let repo_id = repo.as_ref().filter(|_| a.scope == MemoryScope::Project).map(|repo| repo.id.clone());
            self.memory_changed(&id, &workspace, "kept", repo_id.as_deref()).await;
            return Ok(match self.memory_row(&workspace, &id).await? {
                Some(memory) => Outcome::Ok(memory),
                None => Outcome::fail(FailureCode::NotFound, "Memory not found."),
            });
        }
        let count = self
            .db
            .prepare("SELECT count(*) AS value FROM memories WHERE scope = ? AND scope_key = ? AND status != 'dismissed'")
            .bind(&[a.scope.as_str().into(), key.as_str().into()])?
            .first::<u32>(Some("value"))
            .await?
            .unwrap_or_default();
        if count >= MAX_PER_SCOPE {
            return Ok(Outcome::fail(
                FailureCode::Conflict,
                format!("This {} already keeps {MAX_PER_SCOPE} memories. Remove some that no longer hold first.", a.scope.as_str()),
            ));
        }
        // Where it came from: a person, or an agent, and the run it was in.
        let from = match (&repo, a.from_number) {
            (Some(repo), Some(number)) => Some((repo, number)),
            _ => None,
        };
        let run = match (a.actor.kind, from) {
            (PrincipalKind::Agent, Some((repo, number))) => self.active_run_on(&repo.id, number).await?,
            _ => None,
        };
        let source_kind = match (a.actor.kind, &run) {
            (PrincipalKind::User, _) => "person",
            (_, Some(_)) => "run",
            _ => "agent",
        };
        let id = new_id("mem", now_ms());
        let repo_text = repo.as_ref().map(|repo| format!("{}/{}", repo.namespace, repo.name));
        // Its source, as capture counts sources (capture.rs).
        let reference = match &run {
            Some(run) => format!("run:{run}"),
            None => format!("{source_kind}:{}", a.actor.username),
        };
        let changed_repo = repo.as_ref().filter(|_| a.scope == MemoryScope::Project).map(|repo| repo.id.clone());
        self.db
            .prepare(
                "INSERT INTO memories
                   (id, scope, scope_key, workspace, repo, text, kind, source_kind, source_run,
                    source_repo, source_number, created_by, pinned, created_at, updated_at,
                    status, fingerprint, sources, source_ref)
                 VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'kept', ?, ?, ?)",
            )
            .bind(&[
                id.as_str().into(),
                a.scope.as_str().into(),
                key.clone().into(),
                workspace.as_str().into(),
                // A workspace's memory belongs to no one project, though it
                // remembers which it was learned in.
                if a.scope == MemoryScope::Project { repo_text.clone().map_or(JsValue::NULL, JsValue::from) } else { JsValue::NULL },
                text.into(),
                a.kind.as_str().into(),
                source_kind.into(),
                run.map_or(JsValue::NULL, JsValue::from),
                repo_text.map_or(JsValue::NULL, JsValue::from),
                from.map_or(JsValue::NULL, |(_, number)| number.into()),
                a.actor.username.as_str().into(),
                u32::from(a.pinned).into(),
                now.as_str().into(),
                now.as_str().into(),
                print.as_str().into(),
                serde_json::to_string(&[&reference])?.into(),
                reference.as_str().into(),
            ])?
            .run()
            .await?;
        self.memory_changed(&id, &workspace, "kept", changed_repo.as_deref()).await;
        Ok(match self.memory_row(&workspace, &id).await? {
            Some(memory) => Outcome::Ok(memory),
            None => Outcome::fail(FailureCode::NotFound, "Memory not found."),
        })
    }

    pub(crate) async fn update_memory(&self, a: UpdateMemoryArgs) -> Result<Outcome<Memory>> {
        let workspace = a.workspace.to_lowercase();
        let repo_id = self.memory_repo_id(&a.id).await?;
        if !may_write(&a.actor, &workspace, repo_id.as_deref()) || a.actor.kind == PrincipalKind::Agent {
            return Ok(Outcome::fail(FailureCode::Forbidden, CHANGE_DENIED));
        }
        let Some(before) = self.memory_row(&workspace, &a.id).await? else {
            return Ok(Outcome::fail(FailureCode::NotFound, "Memory not found."));
        };
        let text = match a.text.as_deref().map(valid_text) {
            Some(Err(message)) => return Ok(Outcome::fail(FailureCode::Invalid, message)),
            Some(Ok(text)) => Some(text),
            None => None,
        };
        let print = text.as_deref().map(g1t_contracts::capture::fingerprint);
        self.db
            .prepare(
                "UPDATE memories SET text = COALESCE(?, text), kind = COALESCE(?, kind),
                   pinned = COALESCE(?, pinned), fingerprint = COALESCE(?, fingerprint), updated_at = ?
                 WHERE id = ? AND workspace = ?",
            )
            .bind(&[
                text.map_or(JsValue::NULL, JsValue::from),
                a.kind.map_or(JsValue::NULL, |kind| kind.as_str().into()),
                a.pinned.map_or(JsValue::NULL, |pinned| u32::from(pinned).into()),
                print.map_or(JsValue::NULL, JsValue::from),
                rfc3339(now_ms()).into(),
                a.id.as_str().into(),
                workspace.as_str().into(),
            ])?
            .run()
            .await?;
        self.memory_changed(&a.id, &workspace, before.status.as_str(), repo_id.as_deref()).await;
        Ok(match self.memory_row(&workspace, &a.id).await? {
            Some(memory) => Outcome::Ok(memory),
            None => Outcome::fail(FailureCode::NotFound, "Memory not found."),
        })
    }

    pub(crate) async fn delete_memory(&self, a: DeleteMemoryArgs) -> Result<Outcome<bool>> {
        let workspace = a.workspace.to_lowercase();
        let repo_id = self.memory_repo_id(&a.id).await?;
        if !may_write(&a.actor, &workspace, repo_id.as_deref()) || a.actor.kind == PrincipalKind::Agent {
            return Ok(Outcome::fail(FailureCode::Forbidden, CHANGE_DENIED));
        }
        let gone = self
            .db
            .prepare("DELETE FROM memories WHERE id = ? AND workspace = ? RETURNING id AS value")
            .bind(&[a.id.as_str().into(), workspace.as_str().into()])?
            .first::<String>(Some("value"))
            .await?;
        Ok(match gone {
            Some(_) => {
                self.memory_changed(&a.id, &workspace, "deleted", repo_id.as_deref()).await;
                Outcome::Ok(true)
            }
            None => Outcome::fail(FailureCode::NotFound, "Memory not found."),
        })
    }

    pub(crate) async fn recall(&self, a: RecallArgs) -> Result<Outcome<Memories>> {
        let repo = match self.repo(&a.repo, &a.viewer).await? {
            Outcome::Ok(repo) => repo,
            Outcome::Fail(failure) => return Ok(Outcome::Fail(failure)),
        };
        let workspace = repo.namespace.to_lowercase();
        // Found for the viewer, so they can read the project and its
        // memory; the workspace's is its members'.
        let member = a.viewer.as_ref().is_some_and(|viewer| viewer.is_member(&workspace));
        let words: Vec<String> = a
            .query
            .as_deref()
            .unwrap_or_default()
            .split_whitespace()
            .map(str::to_lowercase)
            .collect();
        let limit = a.limit.unwrap_or(DEFAULT_RECALL).clamp(1, 100) as usize;
        let matching = |memories: Vec<Memory>| -> Vec<Memory> {
            memories
                .into_iter()
                .filter(|memory| {
                    let text = memory.text.to_lowercase();
                    words.iter().all(|word| text.contains(word.as_str()))
                })
                .take(limit)
                .collect()
        };
        let found = Memories {
            project: matching(self.memories_of(MemoryScope::Project, &repo.id, MAX_PER_SCOPE).await?),
            workspace: if member {
                matching(self.memories_of(MemoryScope::Workspace, &workspace, MAX_PER_SCOPE).await?)
            } else {
                Vec::new()
            },
        };
        let ids: Vec<String> = found
            .project
            .iter()
            .chain(&found.workspace)
            .map(|memory| memory.id.clone())
            .collect();
        self.mark_used(&ids).await?;
        Ok(Outcome::Ok(found))
    }

    pub(crate) async fn memory_context(&self, a: MemoryContextArgs) -> Result<MemoryContext> {
        let service = User {
            id: "g1t_runner".into(),
            username: "g1t".into(),
            ..User::default()
        };
        let Outcome::Ok(repo) = self.repo(&a.repo, &member_of(&service, &a.repo.namespace)).await? else {
            return Ok(MemoryContext::default());
        };
        // The workspace's memory is its members': a run for an outside
        // collaborator is told only the project's.
        let workspace = if workspace_memory_for(a.requester.as_ref(), &repo.namespace) {
            self.memories_of(MemoryScope::Workspace, &repo.namespace.to_lowercase(), MAX_PER_SCOPE)
                .await?
        } else {
            Vec::new()
        };
        let project = self.memories_of(MemoryScope::Project, &repo.id, MAX_PER_SCOPE).await?;
        let budget = a.budget.unwrap_or(DEFAULT_BUDGET).clamp(500, MAX_BUDGET) as usize;
        let path = RepoPath {
            namespace: repo.namespace.clone(),
            name: repo.name.clone(),
        };
        let (text, given) = compose(&workspace, &project, &path, budget);
        self.mark_used(&given).await?;
        Ok(MemoryContext {
            text,
            count: given.len() as u32,
        })
    }
}

/// Whether a run for `requester` in `namespace` is told the workspace's
/// memory: for its members, and for the workspace's own steps (no
/// requester), never for an outside collaborator.
fn workspace_memory_for(requester: Option<&User>, namespace: &str) -> bool {
    requester.is_none_or(|user| user.is_member(&namespace.to_lowercase()))
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn workspace_memory_is_for_members_runs_only() {
        let member = User {
            id: "u_ana".into(),
            username: "ana".into(),
            workspaces: vec![g1t_contracts::Membership::member("acme")],
            ..User::default()
        };
        let outside = User {
            id: "u_oli".into(),
            username: "oli".into(),
            ..User::default()
        };
        assert!(workspace_memory_for(None, "acme"));
        assert!(workspace_memory_for(Some(&member), "Acme"));
        assert!(!workspace_memory_for(Some(&outside), "acme"));
    }

    #[test]
    fn transfer_and_deletion_statements_take_what_they_name() {
        for sql in TRANSFERRED.iter().chain(crate::guardrails::TRANSFERRED) {
            let n = g1t_kit::transfer::parameters(sql);
            assert!((1..=5).contains(&n), "{sql}");
        }
        for sql in DELETED {
            assert_eq!(g1t_kit::rename::parameters(sql), 1, "{sql}");
        }
    }

    fn memory(id: &str, text: &str, pinned: bool) -> Memory {
        Memory {
            id: id.into(),
            scope: MemoryScope::Project,
            workspace: "acme".into(),
            repo: None,
            text: text.into(),
            kind: MemoryKind::Fact,
            source: MemorySource::default(),
            created_by: "ana".into(),
            pinned,
            created_at: String::new(),
            updated_at: String::new(),
            last_used_at: None,
            status: MemoryStatus::Kept,
            confidence: None,
            seen: 1,
        }
    }

    fn repo() -> RepoPath {
        RepoPath {
            namespace: "acme".into(),
            name: "web".into(),
        }
    }

    #[test]
    fn rename_statements_take_the_two_slugs() {
        for sql in RENAMED {
            assert_eq!(g1t_kit::rename::parameters(sql), 2, "{sql}");
        }
    }

    #[test]
    fn nothing_kept_is_nothing_said() {
        assert_eq!(compose(&[], &[], &repo(), 6000), (None, Vec::new()));
    }

    #[test]
    fn levels_are_labelled_and_pinned_come_first() {
        let workspace = [memory("w1", "We use pnpm everywhere.", false)];
        let project = [memory("p1", "Tests need TZ=UTC.", false), memory("p2", "Never edit generated.rs.", true)];
        let (text, given) = compose(&workspace, &project, &repo(), 6000);
        let text = text.unwrap();
        assert!(text.contains("Workspace memory (true across the acme workspace's projects)"));
        assert!(text.contains("Project memory (acme/web)"));
        assert!(text.find("Never edit").unwrap() < text.find("Tests need").unwrap());
        assert_eq!(given, ["p2", "p1", "w1"]);
    }

    #[test]
    fn the_budget_is_kept() {
        let project: Vec<Memory> = (0..100).map(|n| memory(&format!("p{n}"), &"x".repeat(200), false)).collect();
        let (text, given) = compose(&[], &project, &repo(), 3000);
        assert!(text.unwrap().len() <= 3000);
        assert!(given.len() < 100 && !given.is_empty());
    }

    #[test]
    fn secrets_are_refused_with_a_way_out() {
        // Joined at run time, so no whole key sits in the source for scanners to flag.
        let key = ["ghp", "_", "abcdefghijklmnopqrstuvwxyz0123456789"].concat();
        let refused = valid_text(&format!("deploy with {key}")).unwrap_err();
        assert!(refused.contains("never holds secrets"));
        assert_eq!(valid_text("  We use pnpm. ").unwrap(), "We use pnpm.");
        assert!(valid_text("   ").is_err());
    }

    fn person(base: Option<access::BasePermission>, grants: &[(&str, access::RepoRole)], member: bool) -> User {
        User {
            id: "usr_1".into(),
            username: "ana".into(),
            verified: true,
            workspaces: if member {
                vec![g1t_contracts::Membership { base_permission: base, ..g1t_contracts::Membership::member("acme") }]
            } else {
                Vec::new()
            },
            grants: grants
                .iter()
                .map(|(id, role)| access::RepoGrant { repo_id: (*id).into(), workspace: "acme".into(), role: *role })
                .collect(),
            ..User::default()
        }
    }

    #[test]
    fn a_workspaces_memory_is_its_members_and_a_projects_needs_write() {
        use access::{BasePermission, RepoRole};
        let member = person(None, &[], true);
        assert!(may_write(&member, "acme", None));
        assert!(may_write(&member, "acme", Some("rep_1")));
        // A reader may not change a project's memory, nor may an outsider.
        let reader = person(Some(BasePermission::Read), &[], true);
        assert!(may_write(&reader, "acme", None));
        assert!(!may_write(&reader, "acme", Some("rep_1")));
        let outsider = person(None, &[("rep_1", RepoRole::Write)], false);
        assert!(may_write(&outsider, "acme", Some("rep_1")));
        assert!(!may_write(&outsider, "acme", Some("rep_2")));
        assert!(!may_write(&outsider, "acme", None));
        let unverified = User { verified: false, ..person(None, &[], true) };
        assert!(!may_write(&unverified, "acme", None));
    }
}