Skip to content

Commit a022f31

Browse files
committed
feat: rename state dir .kq to .kqs with legacy migration
1 parent 50b13d7 commit a022f31

12 files changed

Lines changed: 83 additions & 52 deletions

File tree

.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ __pycache__/
2525
/models/
2626

2727
# Local knowledge graph state
28-
.kq/
28+
.kqs/
2929

3030
/.crew/
3131
/.crew

.kiro/steering/conventions.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ VideoCatDoc/ # корневой репозиторий про
1515
├── README.md # описание knowledge-репозитория
1616
├── docs/ # база знаний (markdown)
1717
├── tasks/ # задачи (TASK-NNN.md)
18-
└── .kq/ # sqlite-vec DB + кеш модели
18+
└── .kqs/ # sqlite-vec DB + кеш модели
1919
```
2020

2121
### Задачи

.kiro/steering/principles.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ Push — только по команде `kqs push`.
8080
### P-008: Trace Graph is a Cache
8181

8282
**Level:** MUST
83-
**Rule:** SQLite-граф трассировки — это кеш. Источник истины — .md файлы с Front Matter. При конфликте графа и .md побеждает .md. Пользователь может удалить `.kq/` и пересобрать граф.
83+
**Rule:** SQLite-граф трассировки — это кеш. Источник истины — .md файлы с Front Matter. При конфликте графа и .md побеждает .md. Пользователь может удалить `.kqs/` и пересобрать граф.
8484
**Reason:** Никакого lock-in. Пользователь всегда владеет данными.
8585
**Applies to:** SPEC, TASKS, EXEC
8686

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ members = ["kqs", "kq-core", "kq-embeddings", "kq-llm", "kq-config"]
44

55
[workspace.package]
66
edition = "2024"
7-
version = "0.1.1"
7+
version = "0.1.2"
88
authors = ["kq team"]
99
license = "MIT OR Apache-2.0"
1010
repository = "https://github.com/bimawa/KnowledgeQuery"

README.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ kqs doc list
8585

8686
## Document Types & the Document Chain
8787

88-
Every template in `.kq/templates/` carries a specific semantic load — the type is chosen
88+
Every template in `.kqs/templates/` carries a specific semantic load — the type is chosen
8989
by what question the document answers, not by preference. The types form a chain:
9090
each next type refines the previous one, from business intent down to the data
9191
specification that code implements.
@@ -386,7 +386,7 @@ kq-config/ # knowledge.toml parsing
386386

387387
```
388388
<project>/
389-
├── .kq/
389+
├── .kqs/
390390
│ ├── knowledge.toml # config
391391
│ ├── knowledge.db # SQLite: FTS + vectors + trace graph
392392
│ ├── events/ # CRDT task events
@@ -403,9 +403,9 @@ kq-config/ # knowledge.toml parsing
403403
Tasks use CRDT events instead of overwrites:
404404

405405
```
406-
create → .kq/events/TASK-001/20260709T100000Z-create.md
407-
assign alex → .kq/events/TASK-001/20260709T110000Z-assign-alex.md
408-
move review → .kq/events/TASK-001/20260709T120000Z-move-review.md
406+
create → .kqs/events/TASK-001/20260709T100000Z-create.md
407+
assign alex → .kqs/events/TASK-001/20260709T110000Z-assign-alex.md
408+
move review → .kqs/events/TASK-001/20260709T120000Z-move-review.md
409409
```
410410

411411
**Why:** no merge conflicts, status is computed by replay,

kq-config/src/lib.rs

Lines changed: 23 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,20 @@ pub fn default_knowledge_path() -> PathBuf {
1717
/// 3. Fall back to `~/.knowledge/` (with tilde expansion).
1818
///
1919
/// The `--repo` CLI flag should be passed as `custom` to override auto-detection.
20+
/// Path to the `~/.kqs/current-repo` marker, migrating the legacy
21+
/// `~/.kq/current-repo` file in place when present.
22+
pub fn current_repo_marker() -> PathBuf {
23+
let Some(home) = home_dir() else {
24+
return PathBuf::from(".kqs/current-repo");
25+
};
26+
let new = home.join(".kqs/current-repo");
27+
let old = home.join(".kq/current-repo");
28+
if old.exists() && !new.exists() {
29+
let _ = std::fs::rename(&old, &new);
30+
}
31+
new
32+
}
33+
2034
pub fn repo_path(custom: Option<&str>) -> Result<PathBuf> {
2135
if let Some(custom_path) = custom
2236
&& !custom_path.is_empty()
@@ -42,17 +56,15 @@ pub fn repo_path(custom: Option<&str>) -> Result<PathBuf> {
4256
}
4357
}
4458

45-
// Fallback: check ~/.kq/current-repo marker
46-
if let Some(home) = home_dir() {
47-
let marker = home.join(".kq/current-repo");
48-
if marker.exists() {
49-
let content = std::fs::read_to_string(&marker).unwrap_or_default();
50-
let path = content.trim().to_string();
51-
if !path.is_empty() {
52-
let expanded = expand_tilde(Path::new(&path));
53-
if expanded.join("knowledge.toml").exists() {
54-
return Ok(expanded);
55-
}
59+
// Fallback: check ~/.kqs/current-repo marker
60+
let marker = current_repo_marker();
61+
if marker.exists() {
62+
let content = std::fs::read_to_string(&marker).unwrap_or_default();
63+
let path = content.trim().to_string();
64+
if !path.is_empty() {
65+
let expanded = expand_tilde(Path::new(&path));
66+
if expanded.join("knowledge.toml").exists() {
67+
return Ok(expanded);
5668
}
5769
}
5870
}

kq-core/src/docs.rs

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,6 @@ use std::path::Path;
33

44
use anyhow::{Context, Result};
55

6-
const TEMPLATES_SUBDIR: &str = ".kq/templates";
7-
86
const GROK_CATEGORIES: &[(&str, &str)] = &[
97
("01-business-foundation", "Business Foundation"),
108
("02-product-ux", "Product & UX"),
@@ -258,10 +256,10 @@ pub fn init_with_docs(path: &Path) -> Result<()> {
258256
pub fn generate_doc(path: &Path, doc_type: &str, title: &str) -> Result<String> {
259257
// Support any type: known DOC_TYPES OR custom template file
260258
if !DOC_TYPES.iter().any(|(t, _)| *t == doc_type) {
261-
let template_file = path.join(TEMPLATES_SUBDIR).join(format!("{}.md", doc_type));
259+
let template_file = crate::state_dir(path).join("templates").join(format!("{}.md", doc_type));
262260
if !template_file.exists() {
263261
anyhow::bail!(
264-
"Unknown doc type '{}'. Use one of: {} or create .kq/templates/{}.md",
262+
"Unknown doc type '{}'. Use one of: {} or create .kqs/templates/{}.md",
265263
doc_type,
266264
DOC_TYPES.iter().map(|(t, _)| *t).collect::<Vec<_>>().join(", "),
267265
doc_type
@@ -333,10 +331,10 @@ pub fn list_docs(path: &Path) -> Result<Vec<(String, String)>> {
333331
Ok(results)
334332
}
335333

336-
/// Initialize template files in `.kq/templates/` for all document types.
334+
/// Initialize template files in `.kqs/templates/` for all document types.
337335
/// Creates the directory and writes default template `.md` files.
338336
pub fn init_templates(repo_path: &Path) -> Result<()> {
339-
let templates_dir = repo_path.join(TEMPLATES_SUBDIR);
337+
let templates_dir = crate::state_dir(repo_path).join("templates");
340338
fs::create_dir_all(&templates_dir)
341339
.with_context(|| format!("Failed to create templates dir at {}", templates_dir.display()))?;
342340

@@ -351,9 +349,9 @@ pub fn init_templates(repo_path: &Path) -> Result<()> {
351349
Ok(())
352350
}
353351

354-
/// Load template body from `.kq/templates/<type>.md` file, falling back to hardcoded template.
352+
/// Load template body from `.kqs/templates/<type>.md` file, falling back to hardcoded template.
355353
fn load_template(repo_path: &Path, doc_type: &str) -> Result<(&'static str, String)> {
356-
let template_file = repo_path.join(TEMPLATES_SUBDIR).join(format!("{}.md", doc_type));
354+
let template_file = crate::state_dir(repo_path).join("templates").join(format!("{}.md", doc_type));
357355

358356
if template_file.exists() {
359357
let body = fs::read_to_string(&template_file)
@@ -371,7 +369,7 @@ pub fn templates_list(repo_path: Option<&Path>) -> Vec<String> {
371369

372370
// Scan custom templates from filesystem
373371
if let Some(path) = repo_path {
374-
let templates_dir = path.join(TEMPLATES_SUBDIR);
372+
let templates_dir = crate::state_dir(path).join("templates");
375373
if templates_dir.is_dir()
376374
&& let Ok(entries) = fs::read_dir(&templates_dir)
377375
{

kq-core/src/init.rs

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ use kq_config::KnowledgeConfig;
88
///
99
/// This is the core implementation backing `kqs init`. It:
1010
/// - Resolves the target directory (default: `~/.knowledge/`)
11-
/// - Creates the directory structure (`docs/`, `tasks/`, `.kq/`)
11+
/// - Creates the directory structure (`docs/`, `tasks/`, `.kqs/`)
1212
/// - Initializes a git repository
1313
/// - Writes a default `knowledge.toml` configuration
1414
/// - Stages all files and creates an initial commit
@@ -50,14 +50,15 @@ pub fn init(path: Option<PathBuf>, remote: Option<String>, force: bool) -> Resul
5050
// 3. Create directory structure
5151
fs::create_dir_all(target_path.join("docs")).context("Failed to create docs/ directory")?;
5252
fs::create_dir_all(target_path.join("tasks")).context("Failed to create tasks/ directory")?;
53-
fs::create_dir_all(target_path.join(".kq")).context("Failed to create .kq/ directory")?;
53+
fs::create_dir_all(crate::state_dir(&target_path)).context("Failed to create .kqs/ directory")?;
5454
// 3.1 Create default templates
5555
crate::docs::init_templates(&target_path)?;
5656
// 3.2 Create events directory
57-
fs::create_dir_all(target_path.join(".kq/events")).context("Failed to create .kq/events/ directory")?;
57+
fs::create_dir_all(crate::state_dir(&target_path).join("events"))
58+
.context("Failed to create .kqs/events/ directory")?;
5859

5960
// 3.5 Create SQLite database with FTS5 schema
60-
let db_path = target_path.join(".kq/knowledge.db");
61+
let db_path = crate::state_dir(&target_path).join("knowledge.db");
6162
crate::db::init_db(&db_path).context("Failed to initialize FTS database")?;
6263

6364
// 4. Initialize git repository

kq-core/src/lib.rs

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,28 @@ pub mod typespec;
1616
pub mod vector;
1717
pub mod watcher;
1818

19-
use std::path::Path;
19+
use std::path::{Path, PathBuf};
2020
use std::sync::OnceLock;
2121

22+
/// Internal state directory name inside a knowledge repo.
23+
pub const STATE_DIR: &str = ".kqs";
24+
25+
/// Resolve the state directory (`.kqs`) for a knowledge repo.
26+
///
27+
/// On first use, migrates a legacy `.kq` directory in place so existing
28+
/// repositories keep their database, events and templates.
29+
pub fn state_dir(repo_path: &Path) -> PathBuf {
30+
let new = repo_path.join(STATE_DIR);
31+
let old = repo_path.join(".kq");
32+
if old.is_dir() && !new.exists() {
33+
match std::fs::rename(&old, &new) {
34+
Ok(()) => eprintln!("[kqs] Migrated legacy state directory .kq -> {STATE_DIR}"),
35+
Err(e) => eprintln!("[kqs] Failed to migrate .kq -> {STATE_DIR}: {e}"),
36+
}
37+
}
38+
new
39+
}
40+
2241
/// Operating mode for kqs.
2342
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2443
pub enum KqsMode {

kq-core/src/task.rs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -80,9 +80,9 @@ fn parse_event(content: &str) -> Result<TaskEvent> {
8080
Ok(event)
8181
}
8282

83-
/// Write an event file to `.kq/events/<task_id>/`.
83+
/// Write an event file to `.kqs/events/<task_id>/`.
8484
fn write_event(repo_path: &Path, task_id: &str, op: &str, value: &str) -> Result<TaskEvent> {
85-
let events_dir = repo_path.join(".kq/events").join(task_id);
85+
let events_dir = crate::state_dir(repo_path).join("events").join(task_id);
8686
fs::create_dir_all(&events_dir).context("Failed to create events directory")?;
8787

8888
let timestamp = chrono::Utc::now().format("%Y%m%dT%H%M%SZ").to_string();
@@ -395,7 +395,7 @@ fn read_task_file(path: &Path) -> Result<Task> {
395395
/// Create a new task with the given title, priority, and optional assignee.
396396
///
397397
/// Creates `tasks/TASK-NNN.md` with minimal frontmatter (title, priority, created)
398-
/// and a create event in `.kq/events/TASK-NNN/`.
398+
/// and a create event in `.kqs/events/TASK-NNN/`.
399399
pub fn task_new(title: &str, status: Status, priority: Priority, assignee: &str) -> Result<Task> {
400400
let tasks_dir = tasks_dir()?;
401401
fs::create_dir_all(&tasks_dir).context("Failed to create tasks directory")?;
@@ -422,7 +422,7 @@ pub fn task_new(title: &str, status: Status, priority: Priority, assignee: &str)
422422
}
423423
update_task_refs(&repo, &id)?;
424424

425-
let state = replay_events(&repo.join(".kq/events").join(&id)).unwrap_or_default();
425+
let state = replay_events(&crate::state_dir(&repo).join("events").join(&id)).unwrap_or_default();
426426

427427
Ok(Task {
428428
id,
@@ -443,7 +443,7 @@ pub(crate) fn update_task_refs(repo: &Path, id: &str) -> Result<()> {
443443
}
444444
let content = fs::read_to_string(&task_path)?;
445445

446-
let state = replay_events(&repo.join(".kq/events").join(id)).unwrap_or_default();
446+
let state = replay_events(&crate::state_dir(&repo).join("events").join(id)).unwrap_or_default();
447447

448448
// Extract immutable fields from existing frontmatter
449449
let (orig_title, orig_priority, orig_created) =

0 commit comments

Comments
 (0)