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
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,9 +110,11 @@ Ephemeral ──promote──▶ Stable ──gc──▶ Deprecated
| `memora branch [NAME]` | List or create branches. |
| `memora switch NAME` | Move HEAD to an existing branch. |
| `memora rollback --to <commit>` | Reset HEAD to a previous commit (auto-checkpoint first). |
| `memora promote --id <NODE> \| --type <KIND> \| --all-confirmed [T]` | Promote ephemeral nodes to stable. |
| `memora diff [FROM] [TO] [--working] [--semantic]` | Show belief changes between two revisions. |

Future phases add `diff`, `merge`, `replay`, `export`, `import`, `gc`,
`promote`, `push`, `pull`. See `SPEC.md` for the full roadmap.
Future phases add `merge`, `replay`, `export`, `import`, `gc`, `push`,
`pull`. See `SPEC.md` for the full roadmap.

---

Expand Down
20 changes: 19 additions & 1 deletion SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,23 @@ CREATE TABLE commit_nodes (
PRIMARY KEY (commit_id, node_id)
);

CREATE TABLE node_versions (
commit_id TEXT NOT NULL REFERENCES commits(id) ON DELETE CASCADE,
node_id TEXT NOT NULL,
kind TEXT NOT NULL,
content TEXT NOT NULL,
confidence REAL NOT NULL,
status TEXT NOT NULL,
source TEXT NOT NULL,
evidence TEXT,
tags_json TEXT NOT NULL DEFAULT '[]',
related_to_json TEXT NOT NULL DEFAULT '[]',
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL,
expires_at INTEGER,
PRIMARY KEY (commit_id, node_id)
);

CREATE TABLE sessions (
id TEXT PRIMARY KEY,
started_at INTEGER NOT NULL,
Expand All @@ -118,7 +135,8 @@ CREATE TABLE session_events (

Indexes are `idx_nodes_kind`, `idx_nodes_status`, `idx_nodes_updated`,
`idx_commits_parent`, `idx_commits_ts`, `idx_commit_nodes_node`,
`idx_session_events_session`. They are advisory: any tool may rebuild them.
`idx_node_versions_node`, `idx_session_events_session`. They are advisory:
any tool may rebuild them.

`PRAGMA foreign_keys = ON` and `PRAGMA journal_mode = WAL` are required for
correctness and concurrency.
Expand Down
53 changes: 53 additions & 0 deletions crates/memora-cli/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,12 @@ pub enum Command {

/// Roll HEAD back to a specified commit, with auto checkpoint.
Rollback(RollbackArgs),

/// Promote ephemeral nodes to stable.
Promote(PromoteArgs),

/// Show what changed between two commits (or commit vs working set).
Diff(DiffArgs),
}

/// Arguments for `memora init`.
Expand Down Expand Up @@ -145,3 +151,50 @@ pub struct RollbackArgs {
#[arg(long, default_value = "human")]
pub author: String,
}

/// Arguments for `memora promote`.
///
/// Exactly one of `--id`, `--type`, or `--all-confirmed` must be provided.
#[derive(Debug, clap::Args)]
pub struct PromoteArgs {
/// Promote a single node by id (full or short hex).
#[arg(long = "id", value_name = "NODE")]
pub id: Option<String>,

/// Promote every ephemeral node of the given memory type.
#[arg(long = "type", value_name = "KIND", conflicts_with = "id")]
pub kind: Option<String>,

/// Promote every ephemeral node whose confidence is >= the threshold
/// (default 0.8 when the flag is given without a value).
#[arg(
long = "all-confirmed",
value_name = "THRESHOLD",
num_args = 0..=1,
default_missing_value = "0.8",
conflicts_with_all = ["id", "kind"],
)]
pub all_confirmed: Option<f32>,
}

/// Arguments for `memora diff`.
#[derive(Debug, clap::Args)]
pub struct DiffArgs {
/// `from` revision (commit, branch, HEAD, HEAD~N). Defaults to HEAD~1.
#[arg(value_name = "FROM", default_value = "HEAD~1")]
pub from: String,

/// `to` revision. Defaults to HEAD; pass `--working` to compare
/// against the uncommitted working set instead.
#[arg(value_name = "TO", default_value = "HEAD")]
pub to: String,

/// Compare `from` to the current uncommitted working set rather than
/// to a second commit.
#[arg(long)]
pub working: bool,

/// Include a natural-language summary of belief changes.
#[arg(long)]
pub semantic: bool,
}
7 changes: 3 additions & 4 deletions crates/memora-cli/src/commands/add.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,12 @@ use std::env;
use std::str::FromStr;

use anyhow::{Context, Result};
use owo_colors::OwoColorize;

use memora_core::node::{MemoryKind, MemorySource, NewNode};
use memora_core::Repository;

use crate::cli::AddArgs;
use crate::ui::short_id;
use crate::ui::{bold, green, short_id};

/// Entry point for the `add` subcommand.
pub fn run(args: AddArgs) -> Result<()> {
Expand Down Expand Up @@ -42,9 +41,9 @@ pub fn run(args: AddArgs) -> Result<()> {

println!(
"{} [{}] node {} (confidence {:.2}, status {})",
"Added".green().bold(),
bold(green("Added")),
node.kind,
short_id(&node.id).bold(),
bold(short_id(&node.id)),
node.confidence,
node.status,
);
Expand Down
8 changes: 4 additions & 4 deletions crates/memora-cli/src/commands/branch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@
use std::env;

use anyhow::Result;
use owo_colors::OwoColorize;

use memora_core::Repository;

use crate::cli::BranchArgs;
use crate::ui::{bold, dim, green};

/// Entry point for the `branch` subcommand.
pub fn run(args: BranchArgs) -> Result<()> {
Expand All @@ -20,12 +20,12 @@ pub fn run(args: BranchArgs) -> Result<()> {
let current = head.branch();
let branches = repo.list_branches()?;
if branches.is_empty() {
println!("{}", "no branches".dimmed());
println!("{}", dim("no branches"));
return Ok(());
}
for b in branches {
if Some(b.as_str()) == current {
println!("* {}", b.green().bold());
println!("* {}", bold(green(&b)));
} else {
println!(" {b}");
}
Expand All @@ -36,6 +36,6 @@ pub fn run(args: BranchArgs) -> Result<()> {
// Otherwise create a new branch from HEAD.
let name = args.name.unwrap();
repo.create_branch(&name)?;
println!("{} branch {}", "Created".green().bold(), name.bold());
println!("{} branch {}", bold(green("Created")), bold(&name));
Ok(())
}
19 changes: 9 additions & 10 deletions crates/memora-cli/src/commands/commit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,11 @@
use std::env;

use anyhow::Result;
use owo_colors::OwoColorize;

use memora_core::Repository;

use crate::cli::CommitArgs;
use crate::ui::short_id;
use crate::ui::{bold, cyan, green, red, short_id, yellow};

/// Entry point for the `commit` subcommand.
pub fn run(args: CommitArgs) -> Result<()> {
Expand All @@ -21,24 +20,24 @@ pub fn run(args: CommitArgs) -> Result<()> {
None => {
println!(
"{} no changes since the last commit.",
"Nothing to commit:".yellow().bold()
bold(yellow("Nothing to commit:"))
);
}
Some(c) => {
let branch = outcome.branch.as_deref().unwrap_or("(detached)");
println!(
"[{} {}] {}",
branch.bold(),
short_id(&c.id).yellow(),
bold(branch),
yellow(short_id(&c.id)),
c.message
);
println!(
" +{} added · ~{} modified · -{} removed · {} promoted · {} conflicted",
c.stats.added.green(),
c.stats.modified.cyan(),
c.stats.removed.red(),
c.stats.promoted.green(),
c.stats.conflicted.yellow(),
green(c.stats.added.to_string()),
cyan(c.stats.modified.to_string()),
red(c.stats.removed.to_string()),
green(c.stats.promoted.to_string()),
yellow(c.stats.conflicted.to_string()),
);
}
}
Expand Down
105 changes: 105 additions & 0 deletions crates/memora-cli/src/commands/diff.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
//! `memora diff` — show belief changes between two commits or vs working set.

use std::env;

use anyhow::Result;

use memora_core::{NodeChange, Repository};

use crate::cli::DiffArgs;
use crate::ui::{bold, cyan, dim, green, red, short_id, yellow};

/// Entry point for the `diff` subcommand.
pub fn run(args: DiffArgs) -> Result<()> {
let cwd = env::current_dir()?;
let repo = Repository::open_from(&cwd)?;

let to = if args.working { None } else { Some(args.to.as_str()) };
let report = repo.diff(&args.from, to)?;

let to_label = if to.is_none() {
dim("(working set)")
} else {
yellow(short_id(&report.to_label))
};
println!(
"{} {} → {}",
bold("diff"),
yellow(short_id(&report.from_id)),
to_label,
);

if report.is_empty() {
println!("{}", dim("no belief changes between the two states"));
return Ok(());
}

if !report.added.is_empty() {
println!("\n{}", bold(green("Added:")));
for n in &report.added {
println!(
" + {} [{}] {}",
dim(short_id(&n.id)),
bold(n.kind.to_string()),
truncate(&n.content, 96),
);
}
}

if !report.modified.is_empty() {
println!("\n{}", bold(cyan("Changed:")));
for m in &report.modified {
println!(
" ~ {} [{}] {}",
dim(short_id(&m.after.id)),
bold(m.after.kind.to_string()),
truncate(&m.after.content, 96),
);
for ch in &m.changes {
let label = match ch {
NodeChange::Status { from, to } => format!("status: {from} → {to}"),
NodeChange::Content => "content updated".to_string(),
NodeChange::Confidence { from, to } => {
format!("confidence: {from:.2} → {to:.2}")
}
NodeChange::Source => "source updated".to_string(),
NodeChange::Evidence => "evidence updated".to_string(),
};
println!(" {}", dim(&label));
}
}
}

if !report.removed.is_empty() {
println!("\n{}", bold(red("Removed:")));
for n in &report.removed {
println!(
" - {} [{}] {}",
dim(short_id(&n.id)),
bold(n.kind.to_string()),
truncate(&n.content, 96),
);
}
}

if args.semantic {
let lines = report.semantic_lines();
if !lines.is_empty() {
println!("\n{}", bold("Semantic summary:"));
for l in lines {
println!(" {l}");
}
}
}
Ok(())
}

fn truncate(s: &str, max: usize) -> String {
if s.chars().count() <= max {
s.to_string()
} else {
let mut out: String = s.chars().take(max - 1).collect();
out.push('…');
out
}
}
10 changes: 5 additions & 5 deletions crates/memora-cli/src/commands/init.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@
use std::env;

use anyhow::Result;
use owo_colors::OwoColorize;

use memora_core::Repository;

use crate::cli::InitArgs;
use crate::ui::{bold, dim, green};

/// Entry point for the `init` subcommand.
pub fn run(args: InitArgs) -> Result<()> {
Expand All @@ -18,12 +18,12 @@ pub fn run(args: InitArgs) -> Result<()> {
let repo = Repository::init(&target)?;
println!(
"{} memora store at {}",
"Initialised".green().bold(),
bold(green("Initialised")),
repo.memora_dir().display()
);
println!("HEAD now points at branch {}.", "main".bold());
println!("HEAD now points at branch {}.", bold("main"));
println!("Next steps:");
println!(" {}", "memora add --type=semantic --content=\"...\"".dimmed());
println!(" {}", "memora commit -m \"first memory\"".dimmed());
println!(" {}", dim("memora add --type=semantic --content=\"...\""));
println!(" {}", dim("memora commit -m \"first memory\""));
Ok(())
}
9 changes: 4 additions & 5 deletions crates/memora-cli/src/commands/log.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,27 +3,26 @@
use std::env;

use anyhow::Result;
use owo_colors::OwoColorize;

use memora_core::Repository;

use crate::cli::LogArgs;
use crate::ui::{fmt_timestamp, short_id};
use crate::ui::{dim, fmt_timestamp, short_id, yellow};

/// Entry point for the `log` subcommand.
pub fn run(args: LogArgs) -> Result<()> {
let cwd = env::current_dir()?;
let repo = Repository::open_from(&cwd)?;
let commits = repo.log(args.limit)?;
if commits.is_empty() {
println!("{}", "no commits yet".dimmed());
println!("{}", dim("no commits yet"));
return Ok(());
}
for c in commits {
if args.oneline {
println!("{} {}", short_id(&c.id).yellow(), c.message);
println!("{} {}", yellow(short_id(&c.id)), c.message);
} else {
println!("{} {}", "commit".yellow(), c.id.yellow());
println!("{} {}", yellow("commit"), yellow(&c.id));
if let Some(p) = &c.parent {
println!("Parent: {}", short_id(p));
}
Expand Down
Loading
Loading