diff --git a/CHANGELOG.md b/CHANGELOG.md deleted file mode 100644 index a1d0450..0000000 --- a/CHANGELOG.md +++ /dev/null @@ -1,11 +0,0 @@ -# Changelog - -All notable changes to Tempyr will be documented here. - -This project follows human-readable release notes. Until the first tagged -release, changes are tracked through pull requests and the `master` -branch history. - -## Unreleased - -- Initial public-readiness documentation and CI setup. diff --git a/Cargo.lock b/Cargo.lock index dae51aa..2aaef91 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3548,9 +3548,9 @@ dependencies = [ [[package]] name = "unicode-segmentation" -version = "1.13.0" +version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a559e63b5d8004e12f9bce88af5c6d939c58de839b7532cfe9653846cedd2a9e" +checksum = "9629274872b2bfaf8d66f5f15725007f635594914870f65218920345aa11aa8c" [[package]] name = "unicode-truncate" diff --git a/Cargo.toml b/Cargo.toml index f884e0e..24d91f6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,7 +15,14 @@ members = [ [workspace.package] version = "0.1.0" edition = "2024" +rust-version = "1.89" license = "MIT OR Apache-2.0" +authors = ["Caleb Leak "] +repository = "https://github.com/cleak/tempyr" +homepage = "https://github.com/cleak/tempyr" +readme = "README.md" +keywords = ["knowledge-graph", "ai", "markdown", "graph", "agents"] +categories = ["development-tools"] [workspace.dependencies] serde = { version = "1", features = ["derive"] } diff --git a/README.md b/README.md index 6b375ab..8af4c35 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,15 @@ # Tempyr +[![CI](https://github.com/cleak/tempyr/actions/workflows/ci.yml/badge.svg)](https://github.com/cleak/tempyr/actions/workflows/ci.yml) +[![License: MIT OR Apache-2.0](https://img.shields.io/badge/license-MIT%20OR%20Apache--2.0-blue.svg)](#license) +[![Rust](https://img.shields.io/badge/rust-2024_edition-orange.svg)](https://www.rust-lang.org) +[![Status: experimental](https://img.shields.io/badge/status-experimental-yellow.svg)](#status) + +## Status + +> Early, experimental, solo side project. APIs, schemas, and the on-disk +> graph layout may change without notice. Use at your own risk. + Tempyr is a file-based knowledge graph for AI-assisted product and technical design. It sits between a PRD helper, a project management system, and an AI-centric task system: graph files are the source of truth, while PRDs, TDDs, @@ -67,7 +77,7 @@ tempyr init --no-wizard Add a node: ```sh -tempyr add feature --id feat-session-replay --status draft --owner caleb --body "Capture and replay user sessions." +tempyr add feature --id feat-session-replay --status draft --owner alice --body "Capture and replay user sessions." ``` Validate the graph: diff --git a/crates/tempyr-cli/Cargo.toml b/crates/tempyr-cli/Cargo.toml index d991bb4..2052783 100644 --- a/crates/tempyr-cli/Cargo.toml +++ b/crates/tempyr-cli/Cargo.toml @@ -1,22 +1,30 @@ [package] name = "tempyr-cli" +description = "Tempyr command-line interface: file-based knowledge graph for AI-assisted product and technical design." version.workspace = true edition.workspace = true +rust-version.workspace = true license.workspace = true +authors.workspace = true +repository.workspace = true +homepage.workspace = true +readme.workspace = true +keywords = ["tempyr", "knowledge-graph", "ai", "agents", "cli"] +categories = ["command-line-utilities", "development-tools"] [[bin]] name = "tempyr" path = "src/main.rs" [dependencies] -tempyr-core = { path = "../tempyr-core" } -tempyr-index = { path = "../tempyr-index" } -tempyr-interview = { path = "../tempyr-interview" } -tempyr-render = { path = "../tempyr-render" } -tempyr-linear = { path = "../tempyr-linear" } -tempyr-mcp = { path = "../tempyr-mcp" } -tempyr-journal = { path = "../tempyr-journal" } -tempyr-journal-index = { path = "../tempyr-journal-index" } +tempyr-core = { path = "../tempyr-core", version = "0.1.0" } +tempyr-index = { path = "../tempyr-index", version = "0.1.0" } +tempyr-interview = { path = "../tempyr-interview", version = "0.1.0" } +tempyr-render = { path = "../tempyr-render", version = "0.1.0" } +tempyr-linear = { path = "../tempyr-linear", version = "0.1.0" } +tempyr-mcp = { path = "../tempyr-mcp", version = "0.1.0" } +tempyr-journal = { path = "../tempyr-journal", version = "0.1.0" } +tempyr-journal-index = { path = "../tempyr-journal-index", version = "0.1.0" } anyhow = "1" blake3 = { workspace = true } clap = { workspace = true } diff --git a/crates/tempyr-cli/src/commands/dispatch.rs b/crates/tempyr-cli/src/commands/dispatch.rs index 3fdbdb5..e7e3a0f 100644 --- a/crates/tempyr-cli/src/commands/dispatch.rs +++ b/crates/tempyr-cli/src/commands/dispatch.rs @@ -506,7 +506,7 @@ mod tests { id: epic-vs type: epic status: active -owner: caleb +owner: alice edges: - target: feat-chain type: parent_of @@ -520,7 +520,7 @@ Build the core caravan escort loop. id: feat-chain type: feature status: active -owner: caleb +owner: alice edges: - target: epic-vs type: child_of @@ -743,7 +743,7 @@ Tune spring/damper parameters for stability. id: feat-done type: feature status: completed -owner: caleb +owner: alice edges: - target: task-after type: decomposes_to diff --git a/crates/tempyr-cli/src/commands/migrate.rs b/crates/tempyr-cli/src/commands/migrate.rs index 1a43961..0aab898 100644 --- a/crates/tempyr-cli/src/commands/migrate.rs +++ b/crates/tempyr-cli/src/commands/migrate.rs @@ -329,7 +329,7 @@ mod tests { fs::create_dir_all(&dir).unwrap(); fs::write( dir.join(format!("{id}.md")), - format!("---\nid: {id}\ntype: {node_type}\nstatus: draft\nowner: caleb\n---\n# {id}\n"), + format!("---\nid: {id}\ntype: {node_type}\nstatus: draft\nowner: alice\n---\n# {id}\n"), ) .unwrap(); } diff --git a/crates/tempyr-cli/tests/integration.rs b/crates/tempyr-cli/tests/integration.rs index f7c97fc..cc36071 100644 --- a/crates/tempyr-cli/tests/integration.rs +++ b/crates/tempyr-cli/tests/integration.rs @@ -667,7 +667,7 @@ fn test_mcp_relative_project_root_arg_prefers_client_roots_over_launch_project() fs::write( client_root.join("graph/features/client-only.md"), - "---\nid: client-only\ntype: feature\nstatus: draft\nowner: caleb\nedges: []\n---\n# Client Only\n", + "---\nid: client-only\ntype: feature\nstatus: draft\nowner: alice\nedges: []\n---\n# Client Only\n", ) .unwrap(); @@ -798,7 +798,7 @@ fn test_add_node() { "--status", "draft", "--owner", - "caleb", + "alice", ]) .assert() .success() @@ -816,13 +816,13 @@ fn test_validate_with_valid_nodes() { &tmp, "features", "feat-a", - "---\nid: feat-a\ntype: feature\nstatus: draft\nowner: caleb\nedges:\n - target: epic-a\n type: child_of\n---\n# Feat A\n", + "---\nid: feat-a\ntype: feature\nstatus: draft\nowner: alice\nedges:\n - target: epic-a\n type: child_of\n---\n# Feat A\n", ); write_node( &tmp, "epics", "epic-a", - "---\nid: epic-a\ntype: epic\nstatus: draft\nowner: caleb\nedges:\n - target: feat-a\n type: parent_of\n---\n# Epic A\n", + "---\nid: epic-a\ntype: epic\nstatus: draft\nowner: alice\nedges:\n - target: feat-a\n type: parent_of\n---\n# Epic A\n", ); tempyr() @@ -842,7 +842,7 @@ fn test_validate_catches_dangling_edge() { &tmp, "features", "feat-a", - "---\nid: feat-a\ntype: feature\nstatus: draft\nowner: caleb\nedges:\n - target: nonexistent\n type: depends_on\n---\n# A\n", + "---\nid: feat-a\ntype: feature\nstatus: draft\nowner: alice\nedges:\n - target: nonexistent\n type: depends_on\n---\n# A\n", ); tempyr() @@ -861,13 +861,13 @@ fn test_add_edge_and_validate() { &tmp, "features", "feat-a", - "---\nid: feat-a\ntype: feature\nstatus: draft\nowner: caleb\n---\n# A\n", + "---\nid: feat-a\ntype: feature\nstatus: draft\nowner: alice\n---\n# A\n", ); write_node( &tmp, "epics", "epic-a", - "---\nid: epic-a\ntype: epic\nstatus: draft\nowner: caleb\n---\n# Epic\n", + "---\nid: epic-a\ntype: epic\nstatus: draft\nowner: alice\n---\n# Epic\n", ); tempyr() @@ -894,13 +894,13 @@ fn test_remove_edge() { &tmp, "features", "feat-a", - "---\nid: feat-a\ntype: feature\nstatus: draft\nowner: caleb\n---\n# A\n", + "---\nid: feat-a\ntype: feature\nstatus: draft\nowner: alice\n---\n# A\n", ); write_node( &tmp, "epics", "epic-a", - "---\nid: epic-a\ntype: epic\nstatus: draft\nowner: caleb\n---\n# Epic\n", + "---\nid: epic-a\ntype: epic\nstatus: draft\nowner: alice\n---\n# Epic\n", ); tempyr() @@ -926,7 +926,7 @@ fn test_rename_node() { &tmp, "features", "feat-old", - "---\nid: feat-old\ntype: feature\nstatus: draft\nowner: caleb\n---\n# Old\n", + "---\nid: feat-old\ntype: feature\nstatus: draft\nowner: alice\n---\n# Old\n", ); tempyr() @@ -949,7 +949,7 @@ fn test_status_update() { &tmp, "features", "feat-a", - "---\nid: feat-a\ntype: feature\nstatus: draft\nowner: caleb\n---\n# A\n", + "---\nid: feat-a\ntype: feature\nstatus: draft\nowner: alice\n---\n# A\n", ); tempyr() @@ -1659,7 +1659,7 @@ fn test_status_does_not_emit_for_non_task_nodes() { &tmp, "features", "feat-a", - "---\nid: feat-a\ntype: feature\nstatus: draft\nowner: caleb\n---\n# A\n", + "---\nid: feat-a\ntype: feature\nstatus: draft\nowner: alice\n---\n# A\n", ); tempyr() @@ -1722,13 +1722,13 @@ fn test_traverse() { &tmp, "features", "feat-a", - "---\nid: feat-a\ntype: feature\nstatus: draft\nowner: caleb\nedges:\n - target: epic-a\n type: child_of\n---\n# Feat A\n", + "---\nid: feat-a\ntype: feature\nstatus: draft\nowner: alice\nedges:\n - target: epic-a\n type: child_of\n---\n# Feat A\n", ); write_node( &tmp, "epics", "epic-a", - "---\nid: epic-a\ntype: epic\nstatus: draft\nowner: caleb\nedges:\n - target: feat-a\n type: parent_of\n---\n# Epic A\n", + "---\nid: epic-a\ntype: epic\nstatus: draft\nowner: alice\nedges:\n - target: feat-a\n type: parent_of\n---\n# Epic A\n", ); tempyr() @@ -1749,7 +1749,7 @@ fn test_index_rebuild_and_search() { &tmp, "features", "feat-replay", - "---\nid: feat-replay\ntype: feature\nstatus: draft\nowner: caleb\n---\n# Session Replay\n\nCapture and replay user sessions.\n", + "---\nid: feat-replay\ntype: feature\nstatus: draft\nowner: alice\n---\n# Session Replay\n\nCapture and replay user sessions.\n", ); tempyr() @@ -1776,7 +1776,7 @@ fn test_search_builds_structural_index_when_missing() { &tmp, "features", "feat-replay", - "---\nid: feat-replay\ntype: feature\nstatus: draft\nowner: caleb\n---\n# Session Replay\n\nCapture and replay user sessions.\n", + "---\nid: feat-replay\ntype: feature\nstatus: draft\nowner: alice\n---\n# Session Replay\n\nCapture and replay user sessions.\n", ); tempyr() @@ -1796,7 +1796,7 @@ fn test_add_refreshes_index_for_follow_up_search() { &tmp, "features", "feat-existing", - "---\nid: feat-existing\ntype: feature\nstatus: draft\nowner: caleb\n---\n# Existing\n\nAlready indexed.\n", + "---\nid: feat-existing\ntype: feature\nstatus: draft\nowner: alice\n---\n# Existing\n\nAlready indexed.\n", ); tempyr() @@ -1815,7 +1815,7 @@ fn test_add_refreshes_index_for_follow_up_search() { "--status", "draft", "--owner", - "caleb", + "alice", "--body", "# Terrain Streaming\n\nLOD terrain streaming for large worlds.\n", ]) @@ -1845,7 +1845,7 @@ fn test_add_builds_index_when_missing() { "--status", "draft", "--owner", - "caleb", + "alice", "--body", "# Terrain Streaming\n\nLOD terrain streaming for large worlds.\n", ]) @@ -1869,7 +1869,7 @@ fn test_status_refreshes_index_for_filtered_search() { &tmp, "features", "feat-terrain", - "---\nid: feat-terrain\ntype: feature\nstatus: draft\nowner: caleb\n---\n# Terrain Streaming\n\nLOD terrain streaming for large worlds.\n", + "---\nid: feat-terrain\ntype: feature\nstatus: draft\nowner: alice\n---\n# Terrain Streaming\n\nLOD terrain streaming for large worlds.\n", ); tempyr() @@ -1901,13 +1901,13 @@ fn test_add_edge_refreshes_index_for_follow_up_search() { &tmp, "features", "feat-terrain", - "---\nid: feat-terrain\ntype: feature\nstatus: draft\nowner: caleb\n---\n# Terrain Streaming\n\nLOD terrain streaming for large worlds.\n", + "---\nid: feat-terrain\ntype: feature\nstatus: draft\nowner: alice\n---\n# Terrain Streaming\n\nLOD terrain streaming for large worlds.\n", ); write_node( &tmp, "epics", "epic-world", - "---\nid: epic-world\ntype: epic\nstatus: draft\nowner: caleb\n---\n# World Streaming\n\nParent epic.\n", + "---\nid: epic-world\ntype: epic\nstatus: draft\nowner: alice\n---\n# World Streaming\n\nParent epic.\n", ); tempyr() @@ -1939,7 +1939,7 @@ fn test_render_prd() { &tmp, "features", "feat-replay", - "---\nid: feat-replay\ntype: feature\nstatus: draft\nowner: caleb\n---\n# Session Replay\n\nCapture user sessions.\n", + "---\nid: feat-replay\ntype: feature\nstatus: draft\nowner: alice\n---\n# Session Replay\n\nCapture user sessions.\n", ); tempyr() @@ -1961,7 +1961,7 @@ fn test_render_to_file() { &tmp, "features", "feat-a", - "---\nid: feat-a\ntype: feature\nstatus: draft\nowner: caleb\n---\n# Feature A\n\nBody text.\n", + "---\nid: feat-a\ntype: feature\nstatus: draft\nowner: alice\n---\n# Feature A\n\nBody text.\n", ); let output_path = tmp.path().join("output.md"); @@ -1992,7 +1992,7 @@ fn test_render_to_file_creates_parent_directories() { &tmp, "features", "feat-a", - "---\nid: feat-a\ntype: feature\nstatus: draft\nowner: caleb\n---\n# Feature A\n\nBody text.\n", + "---\nid: feat-a\ntype: feature\nstatus: draft\nowner: alice\n---\n# Feature A\n\nBody text.\n", ); let output_path = tmp.path().join("renders").join("feature-a.md"); @@ -2026,7 +2026,7 @@ fn test_json_output() { &tmp, "features", "feat-a", - "---\nid: feat-a\ntype: feature\nstatus: draft\nowner: caleb\n---\n# A\n", + "---\nid: feat-a\ntype: feature\nstatus: draft\nowner: alice\n---\n# A\n", ); tempyr() @@ -2046,7 +2046,7 @@ fn test_index_stats() { &tmp, "features", "feat-a", - "---\nid: feat-a\ntype: feature\nstatus: draft\nowner: caleb\n---\n# A\n", + "---\nid: feat-a\ntype: feature\nstatus: draft\nowner: alice\n---\n# A\n", ); tempyr() @@ -2072,19 +2072,19 @@ fn test_list_by_status() { &tmp, "tasks", "task-a", - "---\nid: task-a\ntype: task\nstatus: backlog\nowner: caleb\n---\n# Task A\n\nDo stuff.\n", + "---\nid: task-a\ntype: task\nstatus: backlog\nowner: alice\n---\n# Task A\n\nDo stuff.\n", ); write_node( &tmp, "tasks", "task-b", - "---\nid: task-b\ntype: task\nstatus: in_progress\nowner: alice\n---\n# Task B\n\nDo other stuff.\n", + "---\nid: task-b\ntype: task\nstatus: in_progress\nowner: bob\n---\n# Task B\n\nDo other stuff.\n", ); write_node( &tmp, "features", "feat-x", - "---\nid: feat-x\ntype: feature\nstatus: draft\nowner: caleb\n---\n# Feature X\n\nA feature.\n", + "---\nid: feat-x\ntype: feature\nstatus: draft\nowner: alice\n---\n# Feature X\n\nA feature.\n", ); tempyr() @@ -2114,7 +2114,7 @@ fn test_list_by_status() { // List by owner tempyr() .current_dir(tmp.path()) - .args(["list", "--owner", "caleb"]) + .args(["list", "--owner", "alice"]) .assert() .success() .stdout(predicate::str::contains("task-a")) diff --git a/crates/tempyr-core/Cargo.toml b/crates/tempyr-core/Cargo.toml index 66a4f9e..5075e46 100644 --- a/crates/tempyr-core/Cargo.toml +++ b/crates/tempyr-core/Cargo.toml @@ -1,8 +1,16 @@ [package] name = "tempyr-core" +description = "Tempyr graph data model: node and edge parsing, schema validation, traversal, and temporal filtering." version.workspace = true edition.workspace = true +rust-version.workspace = true license.workspace = true +authors.workspace = true +repository.workspace = true +homepage.workspace = true +readme.workspace = true +keywords.workspace = true +categories.workspace = true [dependencies] serde = { workspace = true } diff --git a/crates/tempyr-core/src/graph.rs b/crates/tempyr-core/src/graph.rs index 5eb46b9..3cb180e 100644 --- a/crates/tempyr-core/src/graph.rs +++ b/crates/tempyr-core/src/graph.rs @@ -143,7 +143,7 @@ mod tests { fn make_feature_node(id: &str, edges: &str) -> Node { let content = format!( - "---\nid: {id}\ntype: feature\nstatus: draft\nowner: caleb\nedges:\n{edges}---\n# {id}\n\nBody of {id}.\n" + "---\nid: {id}\ntype: feature\nstatus: draft\nowner: alice\nedges:\n{edges}---\n# {id}\n\nBody of {id}.\n" ); parse_node(&content, PathBuf::from(format!("graph/features/{id}.md"))).unwrap() } diff --git a/crates/tempyr-core/src/lib.rs b/crates/tempyr-core/src/lib.rs index ea73719..75a458c 100644 --- a/crates/tempyr-core/src/lib.rs +++ b/crates/tempyr-core/src/lib.rs @@ -1,3 +1,9 @@ +//! Core graph data model for Tempyr. +//! +//! Provides node and edge parsing from Markdown + YAML frontmatter, schema +//! validation, an in-memory graph with traversal, bidirectional edge +//! consistency, and temporal filtering via `valid_from` / `valid_until`. + pub mod edge; pub mod graph; pub mod id; diff --git a/crates/tempyr-core/src/node.rs b/crates/tempyr-core/src/node.rs index 1f7becb..ae620ee 100644 --- a/crates/tempyr-core/src/node.rs +++ b/crates/tempyr-core/src/node.rs @@ -128,7 +128,7 @@ type: feature status: draft created: 2026-03-20T14:30:00Z updated: 2026-03-23T09:15:00Z -owner: caleb +owner: alice tags: [replay, observability, q2-2026] edges: - target: epic-observability-v2 @@ -158,7 +158,7 @@ Platform engineers currently debug funnel drop-offs by reading logs. assert_eq!(node.id(), "feat-session-replay"); assert_eq!(node.node_type(), "feature"); assert_eq!(node.status(), Some("draft")); - assert_eq!(node.frontmatter.owner.as_deref(), Some("caleb")); + assert_eq!(node.frontmatter.owner.as_deref(), Some("alice")); assert_eq!( node.frontmatter.tags.as_ref().unwrap(), &["replay", "observability", "q2-2026"] diff --git a/crates/tempyr-core/src/ops.rs b/crates/tempyr-core/src/ops.rs index b48ea7e..7ca6304 100644 --- a/crates/tempyr-core/src/ops.rs +++ b/crates/tempyr-core/src/ops.rs @@ -609,7 +609,7 @@ mod tests { "feat-test", "feature", Some("draft"), - Some("caleb"), + Some("alice"), Some(&["test".to_string()]), "# Test Feature\n\nA test.\n", ) @@ -622,7 +622,7 @@ mod tests { assert_eq!(node.id(), "feat-test"); assert_eq!(node.node_type(), "feature"); assert_eq!(node.status(), Some("draft")); - assert_eq!(node.frontmatter.owner.as_deref(), Some("caleb")); + assert_eq!(node.frontmatter.owner.as_deref(), Some("alice")); } #[test] @@ -662,13 +662,13 @@ mod tests { &graph_dir, "features", "feat-a", - "---\nid: feat-a\ntype: feature\nstatus: draft\nowner: caleb\n---\n# Feat A\n", + "---\nid: feat-a\ntype: feature\nstatus: draft\nowner: alice\n---\n# Feat A\n", ); write_node( &graph_dir, "epics", "epic-a", - "---\nid: epic-a\ntype: epic\nstatus: draft\nowner: caleb\n---\n# Epic A\n", + "---\nid: epic-a\ntype: epic\nstatus: draft\nowner: alice\n---\n# Epic A\n", ); add_edge(&graph_dir, "feat-a", "epic-a", "child_of", &schema).unwrap(); @@ -704,19 +704,19 @@ mod tests { &graph_dir, "features", "feat-a", - "---\nid: feat-a\ntype: feature\nstatus: draft\nowner: caleb\n---\n# A\n", + "---\nid: feat-a\ntype: feature\nstatus: draft\nowner: alice\n---\n# A\n", ); write_node( &graph_dir, "epics", "epic-z", - "---\nid: epic-z\ntype: epic\nstatus: draft\nowner: caleb\n---\n# Z\n", + "---\nid: epic-z\ntype: epic\nstatus: draft\nowner: alice\n---\n# Z\n", ); write_node( &graph_dir, "epics", "epic-a", - "---\nid: epic-a\ntype: epic\nstatus: draft\nowner: caleb\n---\n# A\n", + "---\nid: epic-a\ntype: epic\nstatus: draft\nowner: alice\n---\n# A\n", ); add_edge(&graph_dir, "feat-a", "epic-z", "child_of", &schema).unwrap(); @@ -738,13 +738,13 @@ mod tests { &graph_dir, "features", "feat-a", - "---\nid: feat-a\ntype: feature\nstatus: draft\nowner: caleb\n---\n# A\n", + "---\nid: feat-a\ntype: feature\nstatus: draft\nowner: alice\n---\n# A\n", ); write_node( &graph_dir, "epics", "epic-a", - "---\nid: epic-a\ntype: epic\nstatus: draft\nowner: caleb\n---\n# A\n", + "---\nid: epic-a\ntype: epic\nstatus: draft\nowner: alice\n---\n# A\n", ); add_edge(&graph_dir, "feat-a", "epic-a", "child_of", &schema).unwrap(); @@ -762,13 +762,13 @@ mod tests { &graph_dir, "features", "feat-a", - "---\nid: feat-a\ntype: feature\nstatus: draft\nowner: caleb\n---\n# A\n", + "---\nid: feat-a\ntype: feature\nstatus: draft\nowner: alice\n---\n# A\n", ); write_node( &graph_dir, "epics", "epic-a", - "---\nid: epic-a\ntype: epic\nstatus: draft\nowner: caleb\n---\n# A\n", + "---\nid: epic-a\ntype: epic\nstatus: draft\nowner: alice\n---\n# A\n", ); add_edge(&graph_dir, "feat-a", "epic-a", "child_of", &schema).unwrap(); @@ -792,7 +792,7 @@ mod tests { &graph_dir, "features", "feat-old", - "---\nid: feat-old\ntype: feature\nstatus: draft\nowner: caleb\n---\n# Old\n", + "---\nid: feat-old\ntype: feature\nstatus: draft\nowner: alice\n---\n# Old\n", ); let modified = rename_node(&graph_dir, "feat-old", "feat-new").unwrap(); @@ -818,13 +818,13 @@ mod tests { &graph_dir, "features", "feat-a", - "---\nid: feat-a\ntype: feature\nstatus: draft\nowner: caleb\n---\n# A\n", + "---\nid: feat-a\ntype: feature\nstatus: draft\nowner: alice\n---\n# A\n", ); write_node( &graph_dir, "epics", "epic-a", - "---\nid: epic-a\ntype: epic\nstatus: draft\nowner: caleb\n---\n# Epic\n", + "---\nid: epic-a\ntype: epic\nstatus: draft\nowner: alice\n---\n# Epic\n", ); add_edge(&graph_dir, "feat-a", "epic-a", "child_of", &schema).unwrap(); @@ -848,13 +848,13 @@ mod tests { &graph_dir, "features", "feat-a", - "---\nid: feat-a\ntype: feature\nstatus: draft\nowner: caleb\n---\n# A\n", + "---\nid: feat-a\ntype: feature\nstatus: draft\nowner: alice\n---\n# A\n", ); write_node( &graph_dir, "features", "feat-b", - "---\nid: feat-b\ntype: feature\nstatus: draft\nowner: caleb\n---\n# B\n", + "---\nid: feat-b\ntype: feature\nstatus: draft\nowner: alice\n---\n# B\n", ); let err = rename_node(&graph_dir, "feat-a", "feat-b").unwrap_err(); @@ -873,7 +873,7 @@ mod tests { &graph_dir, "features", "feat-a", - "---\nid: feat-a\ntype: feature\nstatus: draft\nowner: caleb\n---\n# A\n", + "---\nid: feat-a\ntype: feature\nstatus: draft\nowner: alice\n---\n# A\n", ); update_status(&graph_dir, "feat-a", "active", &schema).unwrap(); @@ -893,7 +893,7 @@ mod tests { &graph_dir, "features", "feat-a", - "---\nid: feat-a\ntype: feature\nstatus: draft\nowner: caleb\n---\n# A\n", + "---\nid: feat-a\ntype: feature\nstatus: draft\nowner: alice\n---\n# A\n", ); let result = update_status(&graph_dir, "feat-a", "banana", &schema); @@ -910,7 +910,7 @@ mod tests { &graph_dir, "features", "feat-a", - "---\nid: feat-a\ntype: feature\nstatus: draft\nowner: caleb\n---\n# A\n\nOld body.\n", + "---\nid: feat-a\ntype: feature\nstatus: draft\nowner: alice\n---\n# A\n\nOld body.\n", ); update_node( @@ -930,7 +930,7 @@ mod tests { assert!(node.body.contains("New body.")); assert!(!node.body.contains("Old body.")); // Owner should be preserved - assert_eq!(node.frontmatter.owner.as_deref(), Some("caleb")); + assert_eq!(node.frontmatter.owner.as_deref(), Some("alice")); } #[test] @@ -943,13 +943,13 @@ mod tests { &graph_dir, "features", "feat-a", - "---\nid: feat-a\ntype: feature\nstatus: draft\nowner: caleb\n---\n# A\n", + "---\nid: feat-a\ntype: feature\nstatus: draft\nowner: alice\n---\n# A\n", ); write_node( &graph_dir, "epics", "epic-a", - "---\nid: epic-a\ntype: epic\nstatus: draft\nowner: caleb\n---\n# Epic\n", + "---\nid: epic-a\ntype: epic\nstatus: draft\nowner: alice\n---\n# Epic\n", ); add_edge(&graph_dir, "feat-a", "epic-a", "child_of", &schema).unwrap(); @@ -986,7 +986,7 @@ mod tests { "session-replay", "feature", Some("draft"), - Some("caleb"), + Some("alice"), None, "# Session Replay\n\nA feature.\n", ) @@ -1073,7 +1073,7 @@ mod tests { &graph_dir, "features", "feat-old", - "---\nid: feat-old\ntype: feature\nstatus: draft\nowner: caleb\n---\n# Old\n", + "---\nid: feat-old\ntype: feature\nstatus: draft\nowner: alice\n---\n# Old\n", ); let result = rename_node_slug(&graph_dir, "feat-old", "new-name"); diff --git a/crates/tempyr-core/src/validate.rs b/crates/tempyr-core/src/validate.rs index c74e65e..28f855b 100644 --- a/crates/tempyr-core/src/validate.rs +++ b/crates/tempyr-core/src/validate.rs @@ -216,8 +216,8 @@ mod tests { fn test_validate_clean_graph() { let mut graph = Graph::new(make_schema()); - let epic = "---\nid: epic-a\ntype: epic\nstatus: draft\nowner: caleb\nedges:\n - target: feat-a\n type: parent_of\n---\n# Epic A\n"; - let feat = "---\nid: feat-a\ntype: feature\nstatus: draft\nowner: caleb\nedges:\n - target: epic-a\n type: child_of\n---\n# Feat A\n"; + let epic = "---\nid: epic-a\ntype: epic\nstatus: draft\nowner: alice\nedges:\n - target: feat-a\n type: parent_of\n---\n# Epic A\n"; + let feat = "---\nid: feat-a\ntype: feature\nstatus: draft\nowner: alice\nedges:\n - target: epic-a\n type: child_of\n---\n# Feat A\n"; graph.add_node(parse_node(epic, PathBuf::from("epic.md")).unwrap()); graph.add_node(parse_node(feat, PathBuf::from("feat.md")).unwrap()); @@ -234,7 +234,7 @@ mod tests { fn test_validate_dangling_edge() { let mut graph = Graph::new(make_schema()); - let node = "---\nid: feat-a\ntype: feature\nstatus: draft\nowner: caleb\nedges:\n - target: nonexistent-node\n type: depends_on\n---\n# Feat A\n"; + let node = "---\nid: feat-a\ntype: feature\nstatus: draft\nowner: alice\nedges:\n - target: nonexistent-node\n type: depends_on\n---\n# Feat A\n"; graph.add_node(parse_node(node, PathBuf::from("feat.md")).unwrap()); let issues = validate_graph(&graph); @@ -250,8 +250,8 @@ mod tests { let mut graph = Graph::new(make_schema()); // feat-a has child_of -> epic-a, but epic-a does NOT have parent_of -> feat-a - let feat = "---\nid: feat-a\ntype: feature\nstatus: draft\nowner: caleb\nedges:\n - target: epic-a\n type: child_of\n---\n# Feat A\n"; - let epic = "---\nid: epic-a\ntype: epic\nstatus: draft\nowner: caleb\n---\n# Epic A\n"; + let feat = "---\nid: feat-a\ntype: feature\nstatus: draft\nowner: alice\nedges:\n - target: epic-a\n type: child_of\n---\n# Feat A\n"; + let epic = "---\nid: epic-a\ntype: epic\nstatus: draft\nowner: alice\n---\n# Epic A\n"; graph.add_node(parse_node(feat, PathBuf::from("feat.md")).unwrap()); graph.add_node(parse_node(epic, PathBuf::from("epic.md")).unwrap()); @@ -282,7 +282,7 @@ mod tests { fn test_validate_invalid_status() { let mut graph = Graph::new(make_schema()); - let node = "---\nid: feat-a\ntype: feature\nstatus: banana\nowner: caleb\n---\n# Feat\n"; + let node = "---\nid: feat-a\ntype: feature\nstatus: banana\nowner: alice\n---\n# Feat\n"; graph.add_node(parse_node(node, PathBuf::from("feat.md")).unwrap()); let issues = validate_graph(&graph); diff --git a/crates/tempyr-index/Cargo.toml b/crates/tempyr-index/Cargo.toml index 549de0d..0d0d9b4 100644 --- a/crates/tempyr-index/Cargo.toml +++ b/crates/tempyr-index/Cargo.toml @@ -1,12 +1,20 @@ [package] name = "tempyr-index" +description = "SQLite-backed indexing for Tempyr: FTS5 full-text search, sqlite-vec embeddings, and hybrid retrieval." version.workspace = true edition.workspace = true +rust-version.workspace = true license.workspace = true +authors.workspace = true +repository.workspace = true +homepage.workspace = true +readme.workspace = true +keywords.workspace = true +categories.workspace = true [dependencies] -tempyr-core = { path = "../tempyr-core" } -tempyr-journal = { path = "../tempyr-journal" } +tempyr-core = { path = "../tempyr-core", version = "0.1.0" } +tempyr-journal = { path = "../tempyr-journal", version = "0.1.0" } rusqlite = { workspace = true } chrono = { workspace = true } serde = { workspace = true } diff --git a/crates/tempyr-index/src/embeddings.rs b/crates/tempyr-index/src/embeddings.rs index 1157320..ed04b25 100644 --- a/crates/tempyr-index/src/embeddings.rs +++ b/crates/tempyr-index/src/embeddings.rs @@ -887,7 +887,7 @@ reverse = "dependency_of" fn make_graph() -> Graph { let mut graph = Graph::new(make_schema()); let node = parse_node( - "---\nid: feat-replay\ntype: feature\nstatus: draft\nowner: caleb\n---\n# Session Replay\n\nCapture sessions.\n", + "---\nid: feat-replay\ntype: feature\nstatus: draft\nowner: alice\n---\n# Session Replay\n\nCapture sessions.\n", PathBuf::from("graph/features/feat-replay.md"), ) .unwrap(); @@ -988,12 +988,12 @@ reverse = "dependency_of" let schema = make_schema(); let mut graph = Graph::new(schema); let node_a = parse_node( - "---\nid: feat-a\ntype: feature\nstatus: draft\nowner: caleb\n---\n# Shared Title\n\nSame body.\n", + "---\nid: feat-a\ntype: feature\nstatus: draft\nowner: alice\n---\n# Shared Title\n\nSame body.\n", PathBuf::from("graph/features/feat-a.md"), ) .unwrap(); let node_b = parse_node( - "---\nid: feat-b\ntype: feature\nstatus: draft\nowner: caleb\n---\n# Shared Title\n\nSame body.\n", + "---\nid: feat-b\ntype: feature\nstatus: draft\nowner: alice\n---\n# Shared Title\n\nSame body.\n", PathBuf::from("graph/features/feat-b.md"), ) .unwrap(); diff --git a/crates/tempyr-index/src/fts.rs b/crates/tempyr-index/src/fts.rs index 2f1f889..40a1045 100644 --- a/crates/tempyr-index/src/fts.rs +++ b/crates/tempyr-index/src/fts.rs @@ -230,7 +230,7 @@ mod tests { fn build_test_index() -> Index { let mut graph = Graph::new(make_schema()); - let feat = "---\nid: feat-replay\ntype: feature\nstatus: draft\nowner: caleb\ntags: [replay, observability]\n---\n# Session Replay\n\nCapture and replay user sessions for debugging funnel drop-offs.\n"; + let feat = "---\nid: feat-replay\ntype: feature\nstatus: draft\nowner: alice\ntags: [replay, observability]\n---\n# Session Replay\n\nCapture and replay user sessions for debugging funnel drop-offs.\n"; let decision = "---\nid: decision-storage\ntype: decision\nstatus: decided\n---\n# Storage Backend Decision\n\nWe decided to use ClickHouse for replay event storage due to high write throughput.\n"; let task = "---\nid: task-ingestion\ntype: task\nstatus: backlog\n---\n# Build Ingestion Pipeline\n\nImplement the event ingestion pipeline for session replay data.\n"; @@ -320,7 +320,7 @@ mod tests { fn test_fts_with_owner_filter() { let index = build_test_index(); let filter = MetadataFilter { - owner: Some("caleb"), + owner: Some("alice"), ..Default::default() }; let results = index @@ -395,13 +395,13 @@ mod tests { fn test_query_by_metadata_owner() { let index = build_test_index(); let filter = MetadataFilter { - owner: Some("caleb"), + owner: Some("alice"), ..Default::default() }; let results = index.query_by_metadata(&filter, 100).unwrap(); assert_eq!(results.len(), 1); assert_eq!(results[0].node_id, "feat-replay"); - assert_eq!(results[0].owner.as_deref(), Some("caleb")); + assert_eq!(results[0].owner.as_deref(), Some("alice")); } #[test] @@ -420,7 +420,7 @@ mod tests { let index = build_test_index(); let filter = MetadataFilter { node_type: Some("feature"), - owner: Some("caleb"), + owner: Some("alice"), ..Default::default() }; let results = index.query_by_metadata(&filter, 100).unwrap(); diff --git a/crates/tempyr-index/src/hybrid.rs b/crates/tempyr-index/src/hybrid.rs index c248cd4..1996341 100644 --- a/crates/tempyr-index/src/hybrid.rs +++ b/crates/tempyr-index/src/hybrid.rs @@ -249,7 +249,7 @@ mod tests { fn build_graph_and_index() -> (Graph, Index) { let mut graph = Graph::new(make_schema()); - let feat = "---\nid: feat-replay\ntype: feature\nstatus: draft\nowner: caleb\nupdated: 2026-03-23T10:00:00Z\nedges:\n - target: decision-storage\n type: depends_on\n---\n# Session Replay\n\nCapture and replay user sessions.\n"; + let feat = "---\nid: feat-replay\ntype: feature\nstatus: draft\nowner: alice\nupdated: 2026-03-23T10:00:00Z\nedges:\n - target: decision-storage\n type: depends_on\n---\n# Session Replay\n\nCapture and replay user sessions.\n"; let decision = "---\nid: decision-storage\ntype: decision\nstatus: decided\nupdated: 2026-03-23T10:00:00Z\nedges:\n - target: feat-replay\n type: decision_for\n---\n# Storage Backend\n\nUse ClickHouse for replay storage.\n"; let task = "---\nid: task-ingestion\ntype: task\nstatus: backlog\nupdated: 2026-01-01T00:00:00Z\n---\n# Ingestion Pipeline\n\nBuild the session replay ingestion pipeline.\n"; diff --git a/crates/tempyr-index/src/incremental.rs b/crates/tempyr-index/src/incremental.rs index f5bb6e3..e354e18 100644 --- a/crates/tempyr-index/src/incremental.rs +++ b/crates/tempyr-index/src/incremental.rs @@ -123,7 +123,7 @@ mod tests { #[test] fn test_incremental_add_new() { let mut graph = Graph::new(make_schema()); - let feat = "---\nid: feat-a\ntype: feature\nstatus: draft\nowner: caleb\n---\n# A\n"; + let feat = "---\nid: feat-a\ntype: feature\nstatus: draft\nowner: alice\n---\n# A\n"; graph.add_node(parse_node(feat, PathBuf::from("f.md")).unwrap()); let index = Index::create_in_memory().unwrap(); @@ -140,14 +140,14 @@ mod tests { #[test] fn test_incremental_update_changed() { let mut graph = Graph::new(make_schema()); - let feat = "---\nid: feat-a\ntype: feature\nstatus: draft\nowner: caleb\n---\n# A\n\nOriginal body.\n"; + let feat = "---\nid: feat-a\ntype: feature\nstatus: draft\nowner: alice\n---\n# A\n\nOriginal body.\n"; graph.add_node(parse_node(feat, PathBuf::from("f.md")).unwrap()); let index = Index::create_in_memory().unwrap(); index.rebuild(&graph).unwrap(); // Update the body (changes content hash) - let updated = "---\nid: feat-a\ntype: feature\nstatus: draft\nowner: caleb\n---\n# A\n\nUpdated body content.\n"; + let updated = "---\nid: feat-a\ntype: feature\nstatus: draft\nowner: alice\n---\n# A\n\nUpdated body content.\n"; graph.add_node(parse_node(updated, PathBuf::from("f.md")).unwrap()); let stats = index.incremental_update(&graph).unwrap(); @@ -161,7 +161,7 @@ mod tests { #[test] fn test_incremental_remove_deleted() { let mut graph = Graph::new(make_schema()); - let feat = "---\nid: feat-a\ntype: feature\nstatus: draft\nowner: caleb\n---\n# A\n"; + let feat = "---\nid: feat-a\ntype: feature\nstatus: draft\nowner: alice\n---\n# A\n"; let task = "---\nid: task-b\ntype: task\nstatus: backlog\n---\n# B\n"; graph.add_node(parse_node(feat, PathBuf::from("f.md")).unwrap()); graph.add_node(parse_node(task, PathBuf::from("t.md")).unwrap()); @@ -180,7 +180,7 @@ mod tests { #[test] fn test_incremental_no_changes() { let mut graph = Graph::new(make_schema()); - let feat = "---\nid: feat-a\ntype: feature\nstatus: draft\nowner: caleb\n---\n# A\n"; + let feat = "---\nid: feat-a\ntype: feature\nstatus: draft\nowner: alice\n---\n# A\n"; graph.add_node(parse_node(feat, PathBuf::from("f.md")).unwrap()); let index = Index::create_in_memory().unwrap(); diff --git a/crates/tempyr-index/src/indexer.rs b/crates/tempyr-index/src/indexer.rs index 56c0498..38a7850 100644 --- a/crates/tempyr-index/src/indexer.rs +++ b/crates/tempyr-index/src/indexer.rs @@ -324,8 +324,8 @@ mod tests { fn make_test_graph() -> Graph { let mut graph = Graph::new(make_schema()); - let feat = "---\nid: feat-a\ntype: feature\nstatus: draft\nowner: caleb\ntags: [replay, test]\nedges:\n - target: epic-a\n type: child_of\n---\n# Feature A\n\nThis feature handles session replay.\n"; - let epic = "---\nid: epic-a\ntype: epic\nstatus: active\nowner: caleb\nedges:\n - target: feat-a\n type: parent_of\n---\n# Epic A\n\nThe observability epic.\n"; + let feat = "---\nid: feat-a\ntype: feature\nstatus: draft\nowner: alice\ntags: [replay, test]\nedges:\n - target: epic-a\n type: child_of\n---\n# Feature A\n\nThis feature handles session replay.\n"; + let epic = "---\nid: epic-a\ntype: epic\nstatus: active\nowner: alice\nedges:\n - target: feat-a\n type: parent_of\n---\n# Epic A\n\nThe observability epic.\n"; let task = "---\nid: task-a\ntype: task\nstatus: backlog\n---\n# Task A\n\nImplement ingestion pipeline.\n"; graph.add_node(parse_node(feat, PathBuf::from("feat.md")).unwrap()); diff --git a/crates/tempyr-index/src/lib.rs b/crates/tempyr-index/src/lib.rs index 77ca1b9..5477087 100644 --- a/crates/tempyr-index/src/lib.rs +++ b/crates/tempyr-index/src/lib.rs @@ -1,3 +1,11 @@ +//! Derived SQLite index for the Tempyr graph. +//! +//! Combines structural data, FTS5 full-text search, and sqlite-vec embeddings +//! into a single index file (`.tempyr/index.db`). The hybrid retrieval +//! pipeline blends graph traversal, BM25, and vector similarity, then fills +//! a token budget by combined score. The index is derived and rebuildable +//! from the source Markdown files. + pub mod embeddings; pub mod fts; pub mod health; diff --git a/crates/tempyr-interview/Cargo.toml b/crates/tempyr-interview/Cargo.toml index 060b426..5db3c6f 100644 --- a/crates/tempyr-interview/Cargo.toml +++ b/crates/tempyr-interview/Cargo.toml @@ -1,12 +1,20 @@ [package] name = "tempyr-interview" +description = "AI-assisted interview state machine for Tempyr: gap detection, phase transitions, and structured extraction." version.workspace = true edition.workspace = true +rust-version.workspace = true license.workspace = true +authors.workspace = true +repository.workspace = true +homepage.workspace = true +readme.workspace = true +keywords.workspace = true +categories.workspace = true [dependencies] -tempyr-core = { path = "../tempyr-core" } -tempyr-index = { path = "../tempyr-index" } +tempyr-core = { path = "../tempyr-core", version = "0.1.0" } +tempyr-index = { path = "../tempyr-index", version = "0.1.0" } serde = { workspace = true } serde_json = { workspace = true } chrono = { workspace = true } diff --git a/crates/tempyr-interview/src/lib.rs b/crates/tempyr-interview/src/lib.rs index df10571..720249c 100644 --- a/crates/tempyr-interview/src/lib.rs +++ b/crates/tempyr-interview/src/lib.rs @@ -1,3 +1,10 @@ +//! AI-assisted interview engine for Tempyr. +//! +//! Drives the five-phase interview state machine (Discovery → Product → +//! Technical → Decomposition → Review). Phase transitions and gap detection +//! are deterministic Rust; the LLM only extracts structured data from +//! natural-language answers. Proposals are tentative until the user commits. + pub mod gaps; pub mod llm; pub mod phases; diff --git a/crates/tempyr-interview/src/session.rs b/crates/tempyr-interview/src/session.rs index ffb19f6..7655556 100644 --- a/crates/tempyr-interview/src/session.rs +++ b/crates/tempyr-interview/src/session.rs @@ -706,7 +706,7 @@ mod tests { session .root_node .fields - .insert("owner".to_string(), "caleb".to_string()); + .insert("owner".to_string(), "alice".to_string()); session.add_tentative_node(TentativeNode { id: "persona-dev".to_string(), diff --git a/crates/tempyr-journal-index/Cargo.toml b/crates/tempyr-journal-index/Cargo.toml index cfd4fe8..127d18e 100644 --- a/crates/tempyr-journal-index/Cargo.toml +++ b/crates/tempyr-journal-index/Cargo.toml @@ -1,11 +1,19 @@ [package] name = "tempyr-journal-index" +description = "Hybrid search index for the Tempyr journal: FTS5 + vec0 RRF with optional cross-encoder reranking." version.workspace = true edition.workspace = true +rust-version.workspace = true license.workspace = true +authors.workspace = true +repository.workspace = true +homepage.workspace = true +readme.workspace = true +keywords.workspace = true +categories.workspace = true [dependencies] -tempyr-journal = { path = "../tempyr-journal" } +tempyr-journal = { path = "../tempyr-journal", version = "0.1.0" } serde = { workspace = true } serde_json = { workspace = true } chrono = { workspace = true } diff --git a/crates/tempyr-journal/Cargo.toml b/crates/tempyr-journal/Cargo.toml index f1ebd8a..8f90a95 100644 --- a/crates/tempyr-journal/Cargo.toml +++ b/crates/tempyr-journal/Cargo.toml @@ -1,8 +1,16 @@ [package] name = "tempyr-journal" +description = "Append-only agent reasoning journal for Tempyr: cross-platform locking, JSONL storage, and secret redaction." version.workspace = true edition.workspace = true +rust-version.workspace = true license.workspace = true +authors.workspace = true +repository.workspace = true +homepage.workspace = true +readme.workspace = true +keywords.workspace = true +categories.workspace = true [dependencies] serde = { workspace = true } diff --git a/crates/tempyr-linear/Cargo.toml b/crates/tempyr-linear/Cargo.toml index 313d44a..159ae62 100644 --- a/crates/tempyr-linear/Cargo.toml +++ b/crates/tempyr-linear/Cargo.toml @@ -1,12 +1,20 @@ [package] name = "tempyr-linear" +description = "Linear integration for Tempyr: push/pull sync, status mapping, and context generation." version.workspace = true edition.workspace = true +rust-version.workspace = true license.workspace = true +authors.workspace = true +repository.workspace = true +homepage.workspace = true +readme.workspace = true +keywords.workspace = true +categories.workspace = true [dependencies] -tempyr-core = { path = "../tempyr-core" } -tempyr-index = { path = "../tempyr-index" } +tempyr-core = { path = "../tempyr-core", version = "0.1.0" } +tempyr-index = { path = "../tempyr-index", version = "0.1.0" } serde = { workspace = true } serde_json = { workspace = true } chrono = { workspace = true } diff --git a/crates/tempyr-linear/src/lib.rs b/crates/tempyr-linear/src/lib.rs index 7e7f4ad..af3d5eb 100644 --- a/crates/tempyr-linear/src/lib.rs +++ b/crates/tempyr-linear/src/lib.rs @@ -1,3 +1,8 @@ +//! Linear integration for Tempyr. +//! +//! Push and pull task nodes between the Tempyr graph and a Linear workspace, +//! including status mapping and rendering of context payloads for assignees. + #![allow(clippy::too_many_arguments)] pub mod client; diff --git a/crates/tempyr-mcp/Cargo.toml b/crates/tempyr-mcp/Cargo.toml index 573af68..94d244a 100644 --- a/crates/tempyr-mcp/Cargo.toml +++ b/crates/tempyr-mcp/Cargo.toml @@ -1,18 +1,26 @@ [package] name = "tempyr-mcp" +description = "MCP server library for Tempyr: exposes graph operations as tools for Claude Code and other MCP clients." version.workspace = true edition.workspace = true +rust-version.workspace = true license.workspace = true +authors.workspace = true +repository.workspace = true +homepage.workspace = true +readme.workspace = true +keywords.workspace = true +categories.workspace = true [dependencies] anyhow = "1" -tempyr-core = { path = "../tempyr-core" } -tempyr-index = { path = "../tempyr-index" } -tempyr-interview = { path = "../tempyr-interview" } -tempyr-render = { path = "../tempyr-render" } -tempyr-linear = { path = "../tempyr-linear" } -tempyr-journal = { path = "../tempyr-journal" } -tempyr-journal-index = { path = "../tempyr-journal-index" } +tempyr-core = { path = "../tempyr-core", version = "0.1.0" } +tempyr-index = { path = "../tempyr-index", version = "0.1.0" } +tempyr-interview = { path = "../tempyr-interview", version = "0.1.0" } +tempyr-render = { path = "../tempyr-render", version = "0.1.0" } +tempyr-linear = { path = "../tempyr-linear", version = "0.1.0" } +tempyr-journal = { path = "../tempyr-journal", version = "0.1.0" } +tempyr-journal-index = { path = "../tempyr-journal-index", version = "0.1.0" } chrono = { workspace = true } rmcp = { workspace = true } schemars = { workspace = true } diff --git a/crates/tempyr-mcp/src/lib.rs b/crates/tempyr-mcp/src/lib.rs index 39a0c78..6fd05e0 100644 --- a/crates/tempyr-mcp/src/lib.rs +++ b/crates/tempyr-mcp/src/lib.rs @@ -1,3 +1,9 @@ +//! MCP server for Tempyr. +//! +//! Exposes graph, search, render, journal, and interview operations as +//! Model Context Protocol tools so AI clients (e.g. Claude Code) can read +//! and mutate a Tempyr project conversationally. Launched via `tempyr --mcp`. + pub mod handler; mod journal_ticker; mod shutdown; diff --git a/crates/tempyr-render/Cargo.toml b/crates/tempyr-render/Cargo.toml index 9e8f538..4988309 100644 --- a/crates/tempyr-render/Cargo.toml +++ b/crates/tempyr-render/Cargo.toml @@ -1,11 +1,19 @@ [package] name = "tempyr-render" +description = "Document rendering for Tempyr: TOML template parsing, graph collection, and Markdown output." version.workspace = true edition.workspace = true +rust-version.workspace = true license.workspace = true +authors.workspace = true +repository.workspace = true +homepage.workspace = true +readme.workspace = true +keywords.workspace = true +categories.workspace = true [dependencies] -tempyr-core = { path = "../tempyr-core" } +tempyr-core = { path = "../tempyr-core", version = "0.1.0" } toml = { workspace = true } serde = { workspace = true } chrono = { workspace = true } diff --git a/crates/tempyr-render/src/collector.rs b/crates/tempyr-render/src/collector.rs index 0001758..cc11197 100644 --- a/crates/tempyr-render/src/collector.rs +++ b/crates/tempyr-render/src/collector.rs @@ -313,7 +313,7 @@ mod tests { id: feat-replay type: feature status: draft -owner: caleb +owner: alice created: 2026-03-20T14:30:00Z updated: 2026-03-23T09:15:00Z edges: diff --git a/crates/tempyr-render/src/formatter.rs b/crates/tempyr-render/src/formatter.rs index e0295b6..fa4d4bf 100644 --- a/crates/tempyr-render/src/formatter.rs +++ b/crates/tempyr-render/src/formatter.rs @@ -80,7 +80,7 @@ mod tests { use tempyr_core::node::parse_node; fn make_root() -> Node { - let content = "---\nid: feat-test\ntype: feature\nstatus: draft\nowner: caleb\n---\n# Test Feature\n\nA test feature body.\n"; + let content = "---\nid: feat-test\ntype: feature\nstatus: draft\nowner: alice\n---\n# Test Feature\n\nA test feature body.\n"; parse_node(content, PathBuf::from("test.md")).unwrap() } diff --git a/crates/tempyr-render/src/lib.rs b/crates/tempyr-render/src/lib.rs index 7683afc..b75dc3c 100644 --- a/crates/tempyr-render/src/lib.rs +++ b/crates/tempyr-render/src/lib.rs @@ -1,3 +1,9 @@ +//! Document rendering for Tempyr. +//! +//! PRDs, TDDs, and similar documents are rendered views over the graph: a +//! TOML template selects a root node and walks specified edges, then a +//! formatter assembles the collected nodes into a Markdown document. + pub mod collector; pub mod formatter; pub mod template; @@ -115,7 +121,7 @@ mod tests { id: feat-replay type: feature status: draft -owner: caleb +owner: alice edges: - target: decision-storage type: depends_on diff --git a/docs/graphspec.md b/docs/graphspec.md index f501800..ad6eb5f 100644 --- a/docs/graphspec.md +++ b/docs/graphspec.md @@ -212,7 +212,7 @@ type: feature status: draft # draft | active | completed | superseded | archived created: 2026-03-20T14:30:00Z updated: 2026-03-23T09:15:00Z -owner: caleb +owner: alice tags: [replay, observability, q2-2026] edges: - target: epic-observability-v2