memoryが.insomnia配下ではなくworkspace root直下を想定していた問題の修正

This commit is contained in:
2026-04-28 11:53:08 +09:00
parent dfa6213c18
commit 8cf1d6c9cf
11 changed files with 87 additions and 70 deletions
+16 -16
View File
@@ -322,7 +322,7 @@ mod tests {
#[test]
fn workflow_write_rejected() {
let (dir, linter) = workspace();
let path = dir.path().join("memory/workflow/wf.md");
let path = dir.path().join(".insomnia/memory/workflow/wf.md");
let content = format!(
"---\nupdated_at: {now}\ndescription: x\nauto_invoke: false\nuser_invocable: true\n---\nbody",
now = iso_now()
@@ -352,7 +352,7 @@ mod tests {
#[test]
fn decision_with_unknown_replaced_by_errors() {
let (dir, linter) = workspace();
let path = dir.path().join("memory/decisions/foo.md");
let path = dir.path().join(".insomnia/memory/decisions/foo.md");
let content = format!(
"---\ncreated_at: {now}\nupdated_at: {now}\nsources: []\nstatus: replaced\nreplaced_by: ghost\n---\nbody\n",
now = iso_now()
@@ -369,7 +369,7 @@ mod tests {
#[test]
fn decision_replaced_by_self_errors() {
let (dir, linter) = workspace();
let path = dir.path().join("memory/decisions/foo.md");
let path = dir.path().join(".insomnia/memory/decisions/foo.md");
let content = format!(
"---\ncreated_at: {now}\nupdated_at: {now}\nsources: []\nstatus: replaced\nreplaced_by: foo\n---\nbody\n",
now = iso_now()
@@ -387,7 +387,7 @@ mod tests {
fn decision_replaced_by_existing_ok() {
let (dir, linter) = workspace();
// Pre-create the target.
let target = dir.path().join("memory/decisions/bar.md");
let target = dir.path().join(".insomnia/memory/decisions/bar.md");
write(
&target,
&format!(
@@ -395,7 +395,7 @@ mod tests {
now = iso_now()
),
);
let path = dir.path().join("memory/decisions/foo.md");
let path = dir.path().join(".insomnia/memory/decisions/foo.md");
let content = format!(
"---\ncreated_at: {now}\nupdated_at: {now}\nsources: []\nstatus: replaced\nreplaced_by: bar\n---\nbody\n",
now = iso_now()
@@ -407,7 +407,7 @@ mod tests {
#[test]
fn missing_required_field_errors() {
let (dir, linter) = workspace();
let path = dir.path().join("memory/decisions/foo.md");
let path = dir.path().join(".insomnia/memory/decisions/foo.md");
// Missing `status`.
let content = format!(
"---\ncreated_at: {now}\nupdated_at: {now}\nsources: []\n---\nbody\n",
@@ -423,7 +423,7 @@ mod tests {
#[test]
fn knowledge_long_description_with_model_invokation_errors() {
let (dir, linter) = workspace();
let path = dir.path().join("knowledge/foo.md");
let path = dir.path().join(".insomnia/knowledge/foo.md");
let big_desc = "x".repeat(2000);
let content = format!(
"---\ncreated_at: {now}\nupdated_at: {now}\nkind: rule\ndescription: {big_desc}\nmodel_invokation: true\nuser_invocable: true\nlast_sources: []\n---\nbody\n",
@@ -441,7 +441,7 @@ mod tests {
#[test]
fn knowledge_long_description_without_model_invokation_ok() {
let (dir, linter) = workspace();
let path = dir.path().join("knowledge/foo.md");
let path = dir.path().join(".insomnia/knowledge/foo.md");
let big_desc = "x".repeat(2000);
let content = format!(
"---\ncreated_at: {now}\nupdated_at: {now}\nkind: rule\ndescription: {big_desc}\nmodel_invokation: false\nuser_invocable: true\nlast_sources: []\n---\nbody\n",
@@ -454,7 +454,7 @@ mod tests {
#[test]
fn summary_path_accepted() {
let (dir, linter) = workspace();
let path = dir.path().join("memory/summary.md");
let path = dir.path().join(".insomnia/memory/summary.md");
let content = format!(
"---\nupdated_at: {now}\n---\nsummary body\n",
now = iso_now()
@@ -466,7 +466,7 @@ mod tests {
#[test]
fn create_when_existing_errors() {
let (dir, linter) = workspace();
let path = dir.path().join("memory/decisions/foo.md");
let path = dir.path().join(".insomnia/memory/decisions/foo.md");
write(
&path,
&format!(
@@ -491,7 +491,7 @@ mod tests {
fn workflow_lint_accepts_valid_record() {
let (dir, linter) = workspace();
// Place a Knowledge record that the workflow will reference.
let kn = dir.path().join("knowledge/foo.md");
let kn = dir.path().join(".insomnia/knowledge/foo.md");
write(
&kn,
&format!(
@@ -548,14 +548,14 @@ mod tests {
// `db-pol` (1 deletion), `db-pools` (1 insertion).
for slug in ["db-pol", "db-pools"] {
write(
&dir.path().join(format!("memory/decisions/{slug}.md")),
&dir.path().join(format!(".insomnia/memory/decisions/{slug}.md")),
&format!(
"---\ncreated_at: {n}\nupdated_at: {n}\nsources: []\nstatus: open\n---\n",
n = iso_now()
),
);
}
let path = dir.path().join("memory/decisions/db-pool.md");
let path = dir.path().join(".insomnia/memory/decisions/db-pool.md");
let content = format!(
"---\ncreated_at: {n}\nupdated_at: {n}\nsources: []\nstatus: open\n---\nbody\n",
n = iso_now()
@@ -577,14 +577,14 @@ mod tests {
let (dir, linter) = workspace();
for slug in ["alpha", "bravo"] {
write(
&dir.path().join(format!("memory/decisions/{slug}.md")),
&dir.path().join(format!(".insomnia/memory/decisions/{slug}.md")),
&format!(
"---\ncreated_at: {n}\nupdated_at: {n}\nsources: []\nstatus: open\n---\n",
n = iso_now()
),
);
}
let path = dir.path().join("memory/decisions/charlie.md");
let path = dir.path().join(".insomnia/memory/decisions/charlie.md");
let content = format!(
"---\ncreated_at: {n}\nupdated_at: {n}\nsources: []\nstatus: open\n---\n",
n = iso_now()
@@ -603,7 +603,7 @@ mod tests {
#[test]
fn body_size_limit_errors() {
let (dir, linter) = workspace();
let path = dir.path().join("memory/decisions/foo.md");
let path = dir.path().join(".insomnia/memory/decisions/foo.md");
let big_body = "x".repeat(8001);
let content = format!(
"---\ncreated_at: {now}\nupdated_at: {now}\nsources: []\nstatus: open\n---\n{body}",
+4 -4
View File
@@ -84,7 +84,7 @@ mod tests {
model_invokation: bool,
body: &str,
) {
let path = dir.join("knowledge").join(format!("{slug}.md"));
let path = dir.join(".insomnia/knowledge").join(format!("{slug}.md"));
let content = format!(
"---\ncreated_at: {n}\nupdated_at: {n}\nkind: policy\ndescription: \"{description}\"\nmodel_invokation: {flag}\nuser_invocable: true\nlast_sources: []\n---\n{body}",
n = now(),
@@ -95,7 +95,7 @@ mod tests {
fn setup() -> (TempDir, WorkspaceLayout) {
let dir = TempDir::new().unwrap();
std::fs::create_dir_all(dir.path().join("knowledge")).unwrap();
std::fs::create_dir_all(dir.path().join(".insomnia/knowledge")).unwrap();
let layout = WorkspaceLayout::new(dir.path().to_path_buf());
(dir, layout)
}
@@ -141,7 +141,7 @@ mod tests {
write_knowledge(dir.path(), "good", "ok", true, "");
// Garbage in frontmatter — must be skipped, not panic.
std::fs::write(
dir.path().join("knowledge/bad.md"),
dir.path().join(".insomnia/knowledge/bad.md"),
"---\nthis is not yaml: : :\n---\nbody\n",
)
.unwrap();
@@ -155,7 +155,7 @@ mod tests {
fn non_md_files_ignored() {
let (dir, layout) = setup();
write_knowledge(dir.path(), "good", "ok", true, "");
std::fs::write(dir.path().join("knowledge/note.txt"), "not markdown\n").unwrap();
std::fs::write(dir.path().join(".insomnia/knowledge/note.txt"), "not markdown\n").unwrap();
let got = collect_resident_knowledge(&layout);
assert_eq!(got.len(), 1);
+2 -2
View File
@@ -41,9 +41,9 @@ mod tests {
let layout = WorkspaceLayout::new(PathBuf::from("/ws"));
let rules = deny_write_rules(&layout);
assert_eq!(rules.len(), 2);
assert_eq!(rules[0].target, PathBuf::from("/ws/memory"));
assert_eq!(rules[0].target, PathBuf::from("/ws/.insomnia/memory"));
assert_eq!(rules[0].permission, Permission::Write);
assert!(rules[0].recursive);
assert_eq!(rules[1].target, PathBuf::from("/ws/knowledge"));
assert_eq!(rules[1].target, PathBuf::from("/ws/.insomnia/knowledge"));
}
}
+1 -1
View File
@@ -174,7 +174,7 @@ mod tests {
fn setup() -> (TempDir, WorkspaceLayout, PathBuf) {
let dir = TempDir::new().unwrap();
let layout = WorkspaceLayout::new(dir.path().to_path_buf());
let path = dir.path().join("memory/decisions/foo.md");
let path = dir.path().join(".insomnia/memory/decisions/foo.md");
std::fs::create_dir_all(path.parent().unwrap()).unwrap();
let initial = format!(
"---\ncreated_at: {n}\nupdated_at: {n}\nsources: []\nstatus: open\n---\nbody body\n",
+16 -16
View File
@@ -6,11 +6,11 @@
//! omitted, returns one entry per file (no excerpt) so the agent can
//! enumerate what records exist without knowing what's inside them.
//!
//! - `MemoryQuery` walks `memory/summary.md`, `memory/decisions/`,
//! `memory/requests/`. `memory/workflow/` and `memory/_staging/`
//! are excluded by construction.
//! - `KnowledgeQuery` walks `knowledge/*.md` and supports a `kind`
//! filter against the Knowledge frontmatter's `kind` field.
//! - `MemoryQuery` walks `.insomnia/memory/{summary.md,decisions/,
//! requests/}`. `.insomnia/memory/workflow/` and
//! `.insomnia/memory/_staging/` are excluded by construction.
//! - `KnowledgeQuery` walks `.insomnia/knowledge/*.md` and supports a
//! `kind` filter against the Knowledge frontmatter's `kind` field.
//!
//! No derived index — the file tree is the source of truth and is
//! re-scanned per call. 出現順: within a file by line order, across
@@ -441,16 +441,16 @@ mod tests {
fn setup() -> (TempDir, WorkspaceLayout) {
let dir = TempDir::new().unwrap();
let layout = WorkspaceLayout::new(dir.path().to_path_buf());
std::fs::create_dir_all(dir.path().join("memory/decisions")).unwrap();
std::fs::create_dir_all(dir.path().join("memory/requests")).unwrap();
std::fs::create_dir_all(dir.path().join("memory/workflow")).unwrap();
std::fs::create_dir_all(dir.path().join("memory/_staging")).unwrap();
std::fs::create_dir_all(dir.path().join("knowledge")).unwrap();
std::fs::create_dir_all(dir.path().join(".insomnia/memory/decisions")).unwrap();
std::fs::create_dir_all(dir.path().join(".insomnia/memory/requests")).unwrap();
std::fs::create_dir_all(dir.path().join(".insomnia/memory/workflow")).unwrap();
std::fs::create_dir_all(dir.path().join(".insomnia/memory/_staging")).unwrap();
std::fs::create_dir_all(dir.path().join(".insomnia/knowledge")).unwrap();
(dir, layout)
}
fn write_decision(dir: &Path, slug: &str, body: &str) {
let path = dir.join("memory/decisions").join(format!("{slug}.md"));
let path = dir.join(".insomnia/memory/decisions").join(format!("{slug}.md"));
let content = format!(
"---\ncreated_at: {n}\nupdated_at: {n}\nsources: []\nstatus: open\n---\n{body}",
n = now()
@@ -459,7 +459,7 @@ mod tests {
}
fn write_knowledge(dir: &Path, slug: &str, kind: &str, description: &str, body: &str) {
let path = dir.join("knowledge").join(format!("{slug}.md"));
let path = dir.join(".insomnia/knowledge").join(format!("{slug}.md"));
let content = format!(
"---\ncreated_at: {n}\nupdated_at: {n}\nkind: {kind}\ndescription: \"{description}\"\nmodel_invokation: false\nuser_invocable: true\nlast_sources: []\n---\n{body}",
n = now()
@@ -514,7 +514,7 @@ mod tests {
let (dir, layout) = setup();
write_decision(dir.path(), "alpha", "body\n");
write_decision(dir.path(), "beta", "body\n");
let summary_path = dir.path().join("memory/summary.md");
let summary_path = dir.path().join(".insomnia/memory/summary.md");
std::fs::write(
&summary_path,
format!("---\nupdated_at: {n}\n---\nhello\n", n = now()),
@@ -534,7 +534,7 @@ mod tests {
#[tokio::test]
async fn memory_query_finds_summary() {
let (dir, layout) = setup();
let summary_path = dir.path().join("memory/summary.md");
let summary_path = dir.path().join(".insomnia/memory/summary.md");
std::fs::write(
&summary_path,
format!("---\nupdated_at: {n}\n---\nthe needle is here\n", n = now()),
@@ -552,9 +552,9 @@ mod tests {
#[tokio::test]
async fn memory_query_excludes_workflow_and_staging() {
let (dir, layout) = setup();
let wf = dir.path().join("memory/workflow/wf.md");
let wf = dir.path().join(".insomnia/memory/workflow/wf.md");
std::fs::write(&wf, "needle in workflow\n").unwrap();
let stg = dir.path().join("memory/_staging/abc.json");
let stg = dir.path().join(".insomnia/memory/_staging/abc.json");
std::fs::write(&stg, "needle in staging\n").unwrap();
let (_, tool) = memory_query_tool(layout, QueryConfig::default())();
+3 -3
View File
@@ -144,7 +144,7 @@ mod tests {
#[tokio::test]
async fn read_decision_by_slug() {
let (dir, layout) = setup();
let path = dir.path().join("memory/decisions/foo.md");
let path = dir.path().join(".insomnia/memory/decisions/foo.md");
std::fs::create_dir_all(path.parent().unwrap()).unwrap();
std::fs::write(&path, "alpha\nbeta\n").unwrap();
@@ -159,7 +159,7 @@ mod tests {
#[tokio::test]
async fn read_summary_without_slug() {
let (dir, layout) = setup();
let path = dir.path().join("memory/summary.md");
let path = dir.path().join(".insomnia/memory/summary.md");
std::fs::create_dir_all(path.parent().unwrap()).unwrap();
std::fs::write(&path, "summary body\n").unwrap();
@@ -199,7 +199,7 @@ mod tests {
#[tokio::test]
async fn knowledge_path_resolution() {
let (dir, layout) = setup();
let path = dir.path().join("knowledge/policy.md");
let path = dir.path().join(".insomnia/knowledge/policy.md");
std::fs::create_dir_all(path.parent().unwrap()).unwrap();
std::fs::write(&path, "k\n").unwrap();
+3 -3
View File
@@ -149,7 +149,7 @@ mod tests {
#[tokio::test]
async fn write_creates_summary() {
let (dir, layout) = setup();
let path = dir.path().join("memory/summary.md");
let path = dir.path().join(".insomnia/memory/summary.md");
let content = format!("---\nupdated_at: {n}\n---\nbody\n", n = now());
let (meta, tool) = write_tool(layout)();
@@ -187,7 +187,7 @@ mod tests {
#[tokio::test]
async fn write_update_existing() {
let (dir, layout) = setup();
let path = dir.path().join("memory/decisions/foo.md");
let path = dir.path().join(".insomnia/memory/decisions/foo.md");
std::fs::create_dir_all(path.parent().unwrap()).unwrap();
let initial = format!(
"---\ncreated_at: {n}\nupdated_at: {n}\nsources: []\nstatus: open\n---\nold\n",
@@ -220,7 +220,7 @@ mod tests {
#[tokio::test]
async fn write_does_not_persist_on_lint_failure() {
let (dir, layout) = setup();
let path = dir.path().join("memory/decisions/foo.md");
let path = dir.path().join(".insomnia/memory/decisions/foo.md");
let bad = "no frontmatter at all";
let (_, tool) = write_tool(layout)();
let inp = serde_json::json!({
+37 -23
View File
@@ -1,20 +1,28 @@
//! Workspace-level path layout for the memory subsystem.
//!
//! Resolves a workspace root into the concrete directories the linter
//! and tools operate on:
//! `WorkspaceLayout` carries the workspace root (typically the Pod's
//! pwd). All insomnia-managed content lives under the conventional
//! `<root>/.insomnia/` subdirectory — the same place that holds
//! `manifest.toml` and `prompts/`. The memory subsystem nests its
//! trees inside it:
//!
//! - `<root>/memory/summary.md`
//! - `<root>/memory/decisions/<slug>.md`
//! - `<root>/memory/requests/<slug>.md`
//! - `<root>/memory/workflow/<slug>.md`
//! - `<root>/memory/_staging/<id>.json`
//! - `<root>/knowledge/<slug>.md`
//! - `<root>/.insomnia/memory/summary.md`
//! - `<root>/.insomnia/memory/decisions/<slug>.md`
//! - `<root>/.insomnia/memory/requests/<slug>.md`
//! - `<root>/.insomnia/memory/workflow/<slug>.md`
//! - `<root>/.insomnia/memory/_staging/<id>.json`
//! - `<root>/.insomnia/knowledge/<slug>.md`
//!
//! Configuring `[memory]` with an empty body is therefore sufficient
//! for any workspace that already uses the `.insomnia/` convention; no
//! `workspace_root` override is needed.
use std::path::{Path, PathBuf};
use crate::error::LintError;
use crate::slug::Slug;
const INSOMNIA_DIR: &str = ".insomnia";
const MEMORY_DIR: &str = "memory";
const KNOWLEDGE_DIR: &str = "knowledge";
const SUMMARY_FILE: &str = "summary.md";
@@ -81,12 +89,17 @@ impl WorkspaceLayout {
&self.root
}
/// `<root>/.insomnia/`. The base of every other memory path.
pub fn insomnia_dir(&self) -> PathBuf {
self.root.join(INSOMNIA_DIR)
}
pub fn memory_dir(&self) -> PathBuf {
self.root.join(MEMORY_DIR)
self.insomnia_dir().join(MEMORY_DIR)
}
pub fn knowledge_dir(&self) -> PathBuf {
self.root.join(KNOWLEDGE_DIR)
self.insomnia_dir().join(KNOWLEDGE_DIR)
}
pub fn summary_path(&self) -> PathBuf {
@@ -126,13 +139,14 @@ impl WorkspaceLayout {
}
/// Classify a path under the memory tree. Returns `None` if the
/// path is not under `memory/` or `knowledge/` of this workspace,
/// or if it lives in `_staging/` (which is opaque to the linter).
/// path is not under `.insomnia/memory/` or `.insomnia/knowledge/`
/// of this workspace, or if it lives in `_staging/` (which is
/// opaque to the linter).
///
/// On a conventional path that's *almost* a record but malformed
/// (e.g. `decisions/Foo.md` with an invalid slug), returns
/// `Err(LintError::InvalidSlug | InvalidPath)` so the caller can
/// surface it as a write violation.
/// (e.g. `.insomnia/memory/decisions/Foo.md` with an invalid slug),
/// returns `Err(LintError::InvalidSlug | InvalidPath)` so the caller
/// can surface it as a write violation.
pub fn classify(&self, path: &Path) -> Result<Option<ClassifiedPath>, LintError> {
let memory = self.memory_dir();
let knowledge = self.knowledge_dir();
@@ -221,7 +235,7 @@ mod tests {
#[test]
fn classifies_summary() {
let cp = layout()
.classify(&PathBuf::from("/ws/memory/summary.md"))
.classify(&PathBuf::from("/ws/.insomnia/memory/summary.md"))
.unwrap()
.unwrap();
assert_eq!(cp.kind, RecordKind::Summary);
@@ -231,7 +245,7 @@ mod tests {
#[test]
fn classifies_decision_with_slug() {
let cp = layout()
.classify(&PathBuf::from("/ws/memory/decisions/foo-bar.md"))
.classify(&PathBuf::from("/ws/.insomnia/memory/decisions/foo-bar.md"))
.unwrap()
.unwrap();
assert_eq!(cp.kind, RecordKind::Decision);
@@ -241,7 +255,7 @@ mod tests {
#[test]
fn classifies_knowledge() {
let cp = layout()
.classify(&PathBuf::from("/ws/knowledge/x.md"))
.classify(&PathBuf::from("/ws/.insomnia/knowledge/x.md"))
.unwrap()
.unwrap();
assert_eq!(cp.kind, RecordKind::Knowledge);
@@ -250,7 +264,7 @@ mod tests {
#[test]
fn classifies_workflow() {
let cp = layout()
.classify(&PathBuf::from("/ws/memory/workflow/wf.md"))
.classify(&PathBuf::from("/ws/.insomnia/memory/workflow/wf.md"))
.unwrap()
.unwrap();
assert_eq!(cp.kind, RecordKind::Workflow);
@@ -260,7 +274,7 @@ mod tests {
fn staging_returns_none() {
assert!(
layout()
.classify(&PathBuf::from("/ws/memory/_staging/abc.json"))
.classify(&PathBuf::from("/ws/.insomnia/memory/_staging/abc.json"))
.unwrap()
.is_none()
);
@@ -285,7 +299,7 @@ mod tests {
#[test]
fn invalid_slug_rejected() {
let err = layout()
.classify(&PathBuf::from("/ws/memory/decisions/Foo.md"))
.classify(&PathBuf::from("/ws/.insomnia/memory/decisions/Foo.md"))
.unwrap_err();
assert!(matches!(err, LintError::InvalidSlug(_)));
}
@@ -293,7 +307,7 @@ mod tests {
#[test]
fn nested_under_record_dir_rejected() {
let err = layout()
.classify(&PathBuf::from("/ws/memory/decisions/sub/foo.md"))
.classify(&PathBuf::from("/ws/.insomnia/memory/decisions/sub/foo.md"))
.unwrap_err();
assert!(matches!(err, LintError::InvalidPath(_)));
}
@@ -301,7 +315,7 @@ mod tests {
#[test]
fn unknown_top_level_dir_rejected() {
let err = layout()
.classify(&PathBuf::from("/ws/memory/something/foo.md"))
.classify(&PathBuf::from("/ws/.insomnia/memory/something/foo.md"))
.unwrap_err();
assert!(matches!(err, LintError::InvalidPath(_)));
}