diff --git a/AGENT.md b/AGENT.md new file mode 100644 index 0000000..6c913b4 --- /dev/null +++ b/AGENT.md @@ -0,0 +1,14 @@ +# AGENT.md + +This file contains instructions and project-specific rules for AI agents (e.g., Claude Code). + +## Project Overview +`docgraph` is a tool for validating and managing graph structures within Markdown files. + +## Skills +Project-specific skills are defined in `.claude/skills` (linked from `.agent/skills`). Please use them proactively. +Specifically, use the `query` skill for all graph exploration, impact analysis, and traceability checks. + +## Rules +- Respond in Japanese. +- Use **conventional commits** for commit messages, written in **English**. diff --git a/CLAUDE.md b/CLAUDE.md new file mode 120000 index 0000000..ac534a3 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1 @@ +AGENT.md \ No newline at end of file diff --git a/docgraph-plugin/skills/align/SKILL.md b/docgraph-plugin/skills/align/SKILL.md index 8e8c597..db25fc4 100644 --- a/docgraph-plugin/skills/align/SKILL.md +++ b/docgraph-plugin/skills/align/SKILL.md @@ -22,11 +22,12 @@ context (Vertical) and their peers (Horizontal). ## Workflow Steps -### 0. Validation Pre-requisite +### 0. Structural & Validation Pre-requisite - **Level**: STRICT - **Policy**: - - If `validate` status is unknown or FAIL -> **STOP** and return FAIL. + 1. **Schema Context**: Read `docgraph.toml` to understand the valid node types and relationship rules for the target scope. + 2. **Validation Status**: If `validate` status is unknown or FAIL -> **STOP** and return FAIL. - Do not re-evaluate surface items (naming, templates) already covered by `validate`. ### 1. Vertical Consistency (Traceability & Context) @@ -37,7 +38,7 @@ context (Vertical) and their peers (Horizontal). - **This Node**: Defines the "What" at its specific abstraction level. - **Children (Outbound)**: Define the "How" (realization, implementation, or breakdown). -1. **Context Check**: Use `docgraph describe ` and verify: +1. **Context Check**: Use `docgraph describe ` or `docgraph query` and verify: - Does the parent node explicitly justify the existence of this node? - Is there any gap in how the parent's intent is carried over? 2. **Realization Check**: Verify child nodes: @@ -49,7 +50,8 @@ context (Vertical) and their peers (Horizontal). - **Baseline Rule**: Use the dominant pattern among existing peer nodes. Do not invent new abstraction levels unless proposing an explicit refactor. -1. **Peer Identification**: Use `docgraph list "_*"`. +1. **Peer Identification**: Use `docgraph query`. + - Example: `docgraph query "MATCH (n:FR) WHERE n.id STARTS WITH 'FR_AUTH' RETURN n.id"` 2. **Overlap Check**: Verify Mutually Exclusive and Collectively Exhaustive (MECE) status. - Does this node's responsibility overlap with peer nodes? - Is the granularity consistent with the peer baseline? diff --git a/docgraph-plugin/skills/ask/SKILL.md b/docgraph-plugin/skills/ask/SKILL.md deleted file mode 100644 index b3e24bb..0000000 --- a/docgraph-plugin/skills/ask/SKILL.md +++ /dev/null @@ -1,121 +0,0 @@ ---- -name: ask -description: Documentation Graph Query & Exploration - Find, investigate, and answer questions about the graph. ---- - -# Documentation Graph Query & Exploration - -This skill provides a structured methodology for exploring the documentation graph and answering specific questions -about its contents, relationships, and structure. It emphasizes using `docgraph` specific tools over general text search -(grep) to leverage the semantic structure of the graph. - -> [!TIP] Use this skill whenever you need to understand "how things are connected", "where a requirement is defined", or -> "what is the impact of a change". - -## Prerequisites - -- **`docgraph` CLI must be installed as a system binary** - - Install via: `curl -fsSL https://raw.githubusercontent.com/sonesuke/docgraph/main/install.sh | bash` - - Or build from source: `cargo install --path .` -- **This is NOT an npm or Python package** - do NOT use `npx` or `pipx` -- **Installation Verification**: Run `docgraph --version` to confirm the binary is available - -## Exploration Tools - -### 1. `docgraph list` (Discovery) - -Use to find nodes matching a pattern or within a scope. - -- **Usage**: `docgraph list [""] [--path ]` -- **Examples**: - - `docgraph list` - List all nodes in current directory - - `docgraph list --path ./doc` - List all nodes in ./doc - - `docgraph list "FR_*"` - List nodes matching pattern - - `docgraph list "FR_*" --path ./doc` - List FR nodes in ./doc -- **Benefit**: Quickly identifies relevant IDs without wading through raw file content. - -### 2. `docgraph type` (Structural Context) - -Use to understand the expected schema and constraints of a specific node type. - -- **Usage**: `docgraph type ` -- **Example**: `docgraph type FR` -- **Benefit**: Reveals what sections and dependencies are mandatory or optional. - -### 3. `docgraph describe` (Deep Dive) - -Use to retrieve the full content and immediate relations of a specific node. - -- **Usage**: `docgraph describe ` -- **Example**: `docgraph describe FR_LOGIN_001` -- **Benefit**: Provides a unified view of the node's text and its incoming/outgoing edges. - -### 4. `docgraph trace` (Impact & Lineage) - -Use to follow a chain of relationships (e.g., from requirement to code, or dependency tree). - -- **Usage**: `docgraph trace --direction ` -- **Example**: `docgraph trace FR_LOGIN_001 --direction down` -- **Benefit**: Visualizes the full path of derivation or satisfaction. - -### 6. Semantic Inference & Terminology Deduction (Cognitive Tool) - -Don't just look for matches; analyze how terms are used. - -- **Contextual Deduction**: If a term is used in multiple `FR` nodes related to "Auth", it likely refers to an - authentication concept. -- **ID Mnemonics**: Use the prefix and mnemonic structure (e.g., `API-BILL-*`) to infer the scope of a term. -- **Relational Meaning**: A node that "realizes" an `IF` (Interface) is likely an implementation detail (Module). - -### 5. `grep` (Keyword Fallback) - -Use only when you don't have a specific ID or node type to start with. - -- **Usage**: `grep -r "keyword" doc/` -- **Benefit**: Finds mentions of terms that aren't formal IDs. - ---- - -## Workflow Steps - -### 1. Intent Clarification - -Identify the core of the question: - -- **Structural**: "What is required for a Functional Requirement?" -> Use `type`. -- **Existence**: "Do we have a requirement for SSO?" -> Use `list` or `grep`. -- **Relationship**: "What modules implement the billing interface?" -> Use `describe` or `trace`. - -### 2. Multi-Layer Exploration - -Don't stop at the first finding. Follow the links: - -1. **Find** the starting node (`list`). -2. **Understand** the starting node (`describe`). -3. **Trace** the connections (`trace`) to see how it fits into the broader system. - -### 3. Contextual Synthesis & Inference - -Combine findings from the graph logic with the actual Markdown content. - -- Use `docgraph describe` to see the semantic links. -- Use `view_file` to read the narrative context. -- **Inference**: If a term is mentioned in the text but has no ID, search for related concepts using `list` or `grep` to - see if it's an alias or a broader system component. - -## Query Resolution Report - -When answering, structure your response as follows: - -### Findings Summary - -- **Primary Node(s)**: [ID(s) found] -- **Key Relationships**: [e.g., "Satisfies ADR_001", "Realized by MOD_AUTH"] - -### Detailed Analysis - -Provide the detailed answer built from the gathered context. - -### Navigation Trace (optional) - -If relevant, show the path taken: `FR_001` -> `IF_001` -> `MOD_001` diff --git a/docgraph-plugin/skills/impact/SKILL.md b/docgraph-plugin/skills/impact/SKILL.md deleted file mode 100644 index 5a2c03f..0000000 --- a/docgraph-plugin/skills/impact/SKILL.md +++ /dev/null @@ -1,103 +0,0 @@ ---- -name: impact -description: - Impact Analysis & Ripple Effect Assessment - Evaluate the consequences of adding or changing specifications. ---- - -# Impact Analysis & Ripple Effect Assessment - -This skill defines the process for analyzing the potential consequences of adding, modifying, or removing a -specification node within the documentation graph. It leverages the graph's traceability to identify "ripple effects" -across architectural layers (Requirements, Business, Architecture). - -> [!IMPORTANT] Impact analysis is mandatory before renaming IDs, moving files, or changing core requirement logic to -> ensure the integrity of the entire graph. - -## Prerequisites - -- **`docgraph` CLI must be installed as a system binary** - - Install via: `curl -fsSL https://raw.githubusercontent.com/sonesuke/docgraph/main/install.sh | bash` - - Or build from source: `cargo install --path .` -- **This is NOT an npm or Python package** - do NOT use `npx` or `pipx` -- **Installation Verification**: Run `docgraph --version` to confirm the binary is available - -## Assessment Tools - -### 1. `docgraph describe` (Local Impact) - -Identify immediate connections to the node being changed. - -- **Goal**: List all direct inbound and outbound references. -- **Workflow**: `docgraph describe ` and inspect the "Relations" section. - -### 2. `docgraph trace` (Global Ripple Effect) - -Follow the chain of dependencies to the ends of the graph. - -- **Downward Trace**: `docgraph trace --direction down` - - _Identifies_: What modules (`MOD`), interfaces (`IF`), or sub-requirements are forced to change or re-validate. -- **Upward Trace**: `docgraph trace --direction up` - - _Identifies_: What higher-level requirements (`FR`, `NFR`) or architectural decisions (`ADR`) might be affected or - lose their justification. - -### 3. `docgraph check` (Integrity Verification) - -Validate that the proposed change doesn't violate graph rules. - -- **Goal**: Detect broken links (`DG003`), template mismatches (`DG007`), or invalid reference directions (`DG006`). - ---- - -## Analysis Workflow - -### 1. Identify Entry Point - -Define the specific node ID and the nature of the change (Add/Modify/Delete). - -### 2. Upward Impact (Dependencies/Justification) - -Analyze what this node "satisfies" or "derives from". - -- If this node is deleted, are the parent nodes still fully justified? -- If this node is modified, does it still meet the parent's intent? - -### 3. Downward Impact (Satisfaction/Implementation) - -Analyze what "realizes" or "derives from" this node. - -- **Technical Impact**: Which Modules (`MOD`) or Interfaces (`IF`) need implementation changes? -- **Specification Impact**: Are there child requirements that become obsolete or conflicting? - -### 4. Cross-Cutting Impact - -Check for associations with Cross-cutting Concepts (`CC`). - -- Does the change impact global system qualities like "Security" or "Performance"? - ---- - -## Impact Assessment Report - -Structure your findings to provide a clear risk assessment: - -### Change Summary - -- **Target Node**: [ID] -- **Action**: [Add / Modify / Delete] - -### Affected Nodes - -| Layer | ID | Impact Level | Description of Impact | -| :----------- | :-------- | :----------- | :------------------------------------------------------- | -| Requirement | `FR_...` | HIGH/MED/LOW | e.g., "Parent requirement becomes partially unsatisfied" | -| Architecture | `MOD_...` | HIGH/MED/LOW | e.g., "Requires logic change in billing handler" | -| Decision | `ADR_...` | LOW | e.g., "Remains consistent with ADR_001" | - -### Verification Status - -- **Result**: [CLEAN / ERRORS FOUND] -- **Broken References**: [List of IDs or "None"] - -### Remediation Recommendation - -Propose steps to mitigate negative impacts (e.g., "Update `MOD_AUTH` to reflect the new interface"). diff --git a/docgraph-plugin/skills/query/SKILL.md b/docgraph-plugin/skills/query/SKILL.md new file mode 100644 index 0000000..3ff8654 --- /dev/null +++ b/docgraph-plugin/skills/query/SKILL.md @@ -0,0 +1,94 @@ +--- +name: query +description: Documentation Graph Query & Analysis - Explore, investigate, and assess the graph using semantic tools and Cypher. +--- + +# Documentation Graph Query & Analysis + +This skill provides a unified methodology for exploring the documentation graph, answering specific questions, and +analyzing the impact or traceability of specifications. It leverages `docgraph` specific tools to navigate the semantic +structure of the graph. + +> [!TIP] Use this skill for any graph-related investigation: from finding a requirement to analyzing the ripple effect +> of a change or verifying that an intent is fully realized. + +## Prerequisites + +- **`docgraph` CLI must be installed as a system binary** + - Install via: `curl -fsSL https://raw.githubusercontent.com/sonesuke/docgraph/main/install.sh | bash` + - Or build from source: `cargo install --path .` +- **Installation Verification**: Run `docgraph --version` to confirm the binary is available + +## Tools & Techniques + +### 0. Structural Understanding (Phase 0) + +Before executing any query, you **MUST** understand the project's graph schema. +- **Action**: Read `docgraph.toml` in the project root. +- **Objective**: Identify supported node types (`UC`, `FR`, `MOD`, etc.), their relationship rules (`dir`, `targets`), and their associated templates. + +### 1. `docgraph query` (Powerful Search) + +Uses Cypher-like syntax for precise, attribute-aware, and relational searching. **This is the preferred tool for complex +searches.** + +- **Usage**: `docgraph query ""` +- **Capabilities & Examples**: + - **Basic Discovery**: `docgraph query "MATCH (n:FR) RETURN n.id"` - List all nodes of a specific type. + - **Attribute Filtering**: `docgraph query "MATCH (n) WHERE n.name CONTAINS 'Auth' RETURN n.id, n.name"` - Search by properties. + - **Path Traversal**: `docgraph query "MATCH (fr:FR)-[*1..2]->(uc:UC) WHERE uc.id = 'UC_001' RETURN fr.id"` - Trace multi-hop relationships. + - **Relationship Semantics**: Use `docgraph.toml` context labels for filtering or reasoning. + - `docgraph query "MATCH (u:UC)-[r:uses]->(f:FR) RETURN f.id, r.type"` - Filter by relationship type and retrieve its label. + - `docgraph query "MATCH (n)-[r]->(m) WHERE r.type CONTAINS 'implements' RETURN n.id, m.id"` - Semantic filtering. + +### 2. `docgraph describe` (Exploration) + +Deep dive into a single node's content and immediate relations. `docgraph describe ` +- _Benefit_: Use this to see direct inbound/outbound links and the local "Why/How" context. + +### 3. `docgraph trace` (Lineage & Realization) + +Follow chains of relationships to verify traceability or global impact. + +- **Usage**: `docgraph trace --direction ` +- **Realization Check (Down)**: Verify if an abstract intent (FR) reaches a terminal implementation (MOD, IF). +- **Justification Check (Up)**: Verify the origin/intent of a low-level specification. + +--- + +## Workflow: Impact Analysis + +Use this workflow before renaming IDs, moving files, or changing core requirement logic. + +1. **Local Impact**: Use `docgraph describe ` to list direct relations. +2. **Global Ripple**: Use `docgraph trace --direction down` (to see what is broken downstream) and `--direction up` + (to see what loses its justification upstream). +3. **Cross-Cutting**: Search for related concepts using `docgraph query` or `grep`. +4. **Report**: List affected IDs and categorize impact level (HIGH/MED/LOW). + +## Workflow: Traceability Verification + +Use this workflow to ensure a node’s responsibility is fully traceable and ultimately realizable. + +1. **Downstream Traversal**: Use `trace --direction down`. +2. **Terminal Reachability**: Ensure at least one path reaches a realizable terminal (MOD, IF, CODE). +3. **Abstraction Monotonicity**: Ensure abstraction decreases (FR > UC > MOD > CODE). +4. **Semantic Continuity**: Verify that the core intent survives through the transitions without "semantic fog" or + drift. + +--- + +## Query & Analysis Report + +Structure your findings to provide clear insights: + +### Findings Summary +- **Scope/Target**: [ID or Query] +- **Key Relationships Found**: [e.g., "FR_001 realized by MOD_AUTH"] +- **Impact/Traceability Result**: [PASS/FAIL or Risk Level] + +### Detailed Analysis +Provide the detailed answer or assessment built from the gathered context. + +### Navigation Trace (optional) +`FR_001` -> `IF_001` -> `MOD_001` diff --git a/docgraph-plugin/skills/trace/SKILL.md b/docgraph-plugin/skills/trace/SKILL.md deleted file mode 100644 index f9d6340..0000000 --- a/docgraph-plugin/skills/trace/SKILL.md +++ /dev/null @@ -1,103 +0,0 @@ ---- -name: trace -description: Realization & Flow Gate - Verify Downstream Traceability and Realizability. ---- - -# Realization & Flow Gate - -This skill verifies that a node’s responsibility is fully traceable downstream and ultimately realizable. It focuses on -**paths**, ensuring that abstract intent can be traced to concrete realization. - -> [!IMPORTANT] A node is considered valid only if its intent can be traced through coherent responsibility transitions -> to at least one **realizable terminal**. A FAIL in `trace` indicates an unrealizable or misleading specification, not -> merely incomplete documentation. - -## Prerequisites - -- **`docgraph` CLI must be installed as a system binary** - - Install via: `curl -fsSL https://raw.githubusercontent.com/sonesuke/docgraph/main/install.sh | bash` - - Or build from source: `cargo install --path .` -- **This is NOT an npm or Python package** - do NOT use `npx` or `pipx` -- **Installation Verification**: Run `docgraph --version` to confirm the binary is available - -## Workflow Steps - -### 0. Pre-requisites - -- **Level**: STRICT -- **Policy**: - - The target node **MUST** have passed `validate` and `align`. - - Do not re-check hygiene (naming, structure) or peer alignment. - -### 1. Downstream Expansion (Graph Traversal) - -- **Goal**: Enumerate all possible downstream realization paths. -- **Process**: - 1. Starting from the target node, traverse all outbound relations. - 2. Traversal **MUST** be recursive. - 3. Traversal stops when a **terminal type** (project-defined realization boundaries: `BB`, `MOD`, `IF`, `DEP`, `CODE`) - is reached or a cycle is detected. - -### 2. Terminal Reachability Check - -- **Purpose**: Ensure at least one path reaches a realizable endpoint. -- **Fail Rule**: FAIL if all paths terminate at abstract nodes (FR, UC, ADR) without reaching a terminal boundary. - -### 3. Abstraction Level Monotonicity - -- **Goal**: Ensure abstraction decreases as we go downstream. -- **Rules**: - - Abstraction levels: `FR` (High) > `ADR/UC` (Mid) > `MOD/BB` (Low) > `CODE/IF` (Terminal). - - No "jump back" to higher abstraction levels is allowed. - - Abstraction may stay the same ONLY if the transition represents an explicit **refinement or decomposition** of - responsibility. Lateral transitions without semantic narrowing MUST be flagged. - -### 4. Responsibility Preservation (Semantic Continuity) - -- **Goal**: Ensure the core intent survives the journey. -- **Process**: Identify the semantic core from the target node’s title and primary description. -- **Detect**: - - **Semantic dilution**: Intent becomes too vague. - - **Responsibility drift**: Intent changes into something else. - - **Silent renaming**: Key terms change without justification. - -### 5. Realizability Check (Terminal Adequacy) - -- **Goal**: Confirm that the terminal node can actually realize the responsibility. -- **Rule**: Terminal nodes must define concrete behavior or interfaces. "Concept-only" terminals are failures. - -### 6. Path Sufficiency & Redundancy - -- **Check**: **PASS requires at least one fully coherent path.** -- **Check**: Multiple paths must be alternative realizations. Additional incomplete or broken paths MUST still be - reported as findings. - -## Trace Analysis Report - -You **must** provide the analysis in the following format: - -### Target - -- **Node**: [ID] -- **Terminal Goal**: [e.g., BB_ nodes] - -### Trace Summary - -| Path | Reaches Terminal | Abstraction OK | Semantic Continuity | Realizable | Failure Cause (if any) | -| :----- | :--------------- | :------------- | :------------------ | :--------- | :------------------------------------ | -| Path A | PASS/FAIL | PASS/FAIL | PASS/FAIL | PASS/FAIL | [e.g., Stalled at UC, Semantic drift] | -| Path B | ... | ... | ... | ... | ... | - -### Findings - -- **Continuity**: [Evidence of drift or preservation from the semantic core] -- **Traversal**: [Brief list of identified paths] - -## Final Decision - -### Decision Semantics - -- **PASS**: At least one path is complete, monotonic, semantically coherent, and realizable. -- **FAIL**: All paths break, stall, or drift. - -**FINAL DECISION: [PASS/FAIL]** diff --git a/docgraph-plugin/skills/validate/SKILL.md b/docgraph-plugin/skills/validate/SKILL.md index 77df97d..4153c3c 100644 --- a/docgraph-plugin/skills/validate/SKILL.md +++ b/docgraph-plugin/skills/validate/SKILL.md @@ -37,14 +37,15 @@ Before performing manual semantic checks, ensure all automated validations pass. 2. **Consistency**: Run `docgraph check` to ensure the internal graph logic is sound. 3. **Rust Integrity**: Run `cargo test` and `cargo clippy` if logic changes are involved. -### 1. Scope Analysis +### 1. Scope Analysis (Structural Context) -- **Level**: PREPARATION -- **Purpose**: +- **Goal**: + - Understand the schema constraints defined for the target type in `docgraph.toml`. - Identify peer nodes for comparison (naming, placement, structure). - Establish the dominant convention (baseline) for the specific node type. -List elements within the target scope to understand the current state. Use `docgraph list`. +List elements within the target scope to understand the current state. Use `docgraph query`. +- Example: `docgraph query "MATCH (n:FR) RETURN n.id"` ### 2. ID and Title Correspondence diff --git a/src/cli/handlers/query.rs b/src/cli/handlers/query.rs index eb19e09..bdc6842 100644 --- a/src/cli/handlers/query.rs +++ b/src/cli/handlers/query.rs @@ -20,7 +20,7 @@ fn try_query(query_str: String, format: OutputFormat, path: PathBuf) -> anyhow:: let (blocks, _) = collect::collect_workspace_all(&path, &config.graph.ignore, None); let query = crate::core::parser::parse_query(&query_str).context("failed to parse query")?; - let result = engine::execute_query(&query, &blocks); + let result = engine::execute_query(&query, &blocks, &config); match format { OutputFormat::Table => { diff --git a/src/core/engine/mod.rs b/src/core/engine/mod.rs index c7794de..72bf8b9 100644 --- a/src/core/engine/mod.rs +++ b/src/core/engine/mod.rs @@ -1,3 +1,4 @@ +use crate::core::config::Config; use crate::core::parser::ast; use crate::core::types::SpecBlock; use std::collections::HashMap; @@ -8,9 +9,19 @@ pub struct QueryResult { pub rows: Vec>, } -type Bindings = HashMap; +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum EntityId { + Node(usize), + Relationship { + from_idx: usize, + to_idx: usize, + context: String, + }, +} + +type Bindings = HashMap; -pub fn execute_query(query: &ast::Query, nodes: &[SpecBlock]) -> QueryResult { +pub fn execute_query(query: &ast::Query, nodes: &[SpecBlock], config: &Config) -> QueryResult { // 1. Match patterns let mut bindings_list: Vec = vec![HashMap::new()]; @@ -40,6 +51,7 @@ pub fn execute_query(query: &ast::Query, nodes: &[SpecBlock]) -> QueryResult { rel_pat, node_pat, nodes, + config, bindings_list, ); @@ -60,8 +72,9 @@ pub fn execute_query(query: &ast::Query, nodes: &[SpecBlock]) -> QueryResult { // 2. Filter with WHERE if let Some(where_clause) = &query.where_clause { - bindings_list - .retain(|bindings| evaluate_expression(&where_clause.expression, bindings, nodes)); + bindings_list.retain(|bindings| { + evaluate_expression(&where_clause.expression, bindings, nodes, config) + }); } // 3. Project with RETURN @@ -106,17 +119,23 @@ pub fn execute_query(query: &ast::Query, nodes: &[SpecBlock]) -> QueryResult { for proj in &item_projections { match proj { Projection::Single(expr) => { - row.push(evaluate_expression_value(expr, &bindings, nodes)); + row.push(evaluate_expression_value(expr, &bindings, nodes, config)); } Projection::Node(var) => { - if let Some(&idx) = bindings.get(var) { - let node = &nodes[idx]; - row.push(node.id.clone()); - row.push(node.node_type.clone()); - row.push(node.name.clone().unwrap_or_else(|| "null".to_string())); - row.push(node.file_path.to_string_lossy().to_string()); - row.push(node.line_start.to_string()); - row.push(node.content.clone()); + if let Some(entity) = bindings.get(var) { + if let EntityId::Node(idx) = entity { + let node = &nodes[*idx]; + row.push(node.id.clone()); + row.push(node.node_type.clone()); + row.push(node.name.clone().unwrap_or_else(|| "null".to_string())); + row.push(node.file_path.to_string_lossy().to_string()); + row.push(node.line_start.to_string()); + row.push(node.content.clone()); + } else { + for _ in 0..6 { + row.push("null".to_string()); + } + } } else { for _ in 0..6 { row.push("null".to_string()); @@ -161,14 +180,19 @@ fn match_node_pattern( // Bind variable if let Some(ref var) = node_pat.variable { - if let Some(&prev_idx) = bindings.get(var) { - if prev_idx != i { + if let Some(entity) = bindings.get(var) { + if let EntityId::Node(prev_idx) = entity { + if *prev_idx != i { + continue; + } + next_bindings.push(bindings.clone()); + } else { + // Var already bound to non-node? continue; } - next_bindings.push(bindings.clone()); } else { let mut new_bindings = bindings.clone(); - new_bindings.insert(var.clone(), i); + new_bindings.insert(var.clone(), EntityId::Node(i)); next_bindings.push(new_bindings); } } else { @@ -184,19 +208,25 @@ fn match_relationship_pattern( rel_pat: &ast::RelationshipPattern, end_node_pat: &ast::NodePattern, nodes: &[SpecBlock], + config: &Config, current_bindings: Vec, ) -> Vec { let mut next_bindings = Vec::new(); - // Build adjacency maps - let mut forward_adj: HashMap> = HashMap::new(); - let mut backward_adj: HashMap> = HashMap::new(); + // Build adjacency maps with context + // key: source_idx, value: (target_idx, context) + let mut forward_adj: HashMap> = HashMap::new(); + let mut backward_adj: HashMap> = HashMap::new(); for (idx, node) in nodes.iter().enumerate() { for edge in &node.edges { if let Some(target_idx) = nodes.iter().position(|n| n.id == edge.id) { - forward_adj.entry(idx).or_default().push(target_idx); - backward_adj.entry(target_idx).or_default().push(idx); + let target_node = &nodes[target_idx]; + // Find context from docgraph.toml + let context = find_relationship_context(config, &node.node_type, &target_node.node_type); + + forward_adj.entry(idx).or_default().push((target_idx, context.clone())); + backward_adj.entry(target_idx).or_default().push((idx, context)); } } } @@ -205,75 +235,108 @@ fn match_relationship_pattern( let max_hops = rel_pat.range.as_ref().and_then(|r| r.end).unwrap_or(1); for bindings in current_bindings { - if let Some(&start_idx) = bindings.get(start_node_var) { - // BFS for reachability - let mut queue = std::collections::VecDeque::new(); - queue.push_back((start_idx, 0)); - - let mut visited = std::collections::HashSet::new(); - visited.insert((start_idx, 0)); - - while let Some((curr, dist)) = queue.pop_front() { - if dist > max_hops { - continue; - } + if let Some(entity) = bindings.get(start_node_var) { + if let EntityId::Node(start_idx) = entity { + let start_idx = *start_idx; + // BFS for reachability + let mut queue = std::collections::VecDeque::new(); + queue.push_back((start_idx, 0, None::)); // (curr, dist, last_rel_context) + + let mut visited = std::collections::HashSet::new(); + visited.insert((start_idx, 0)); + + while let Some((curr, dist, last_rel)) = queue.pop_front() { + if dist > max_hops { + continue; + } - if dist >= min_hops && dist > 0 { - // Check if current node matches end_node_pat - let node = &nodes[curr]; - let label_match = if end_node_pat.labels.is_empty() { - true - } else { - end_node_pat.labels.contains(&node.node_type) - }; - - if label_match { - // Bind end variable - if let Some(ref var) = end_node_pat.variable { - if let Some(&prev_idx) = bindings.get(var) { - if prev_idx == curr { - next_bindings.push(bindings.clone()); - } + if dist >= min_hops && dist > 0 { + // Check rel_type if specified + let rel_match = if let Some(ref target_rel_type) = rel_pat.rel_type { + if let Some(ref actual_rel) = last_rel { + actual_rel == target_rel_type } else { - let mut new_bindings = bindings.clone(); - new_bindings.insert(var.clone(), curr); - next_bindings.push(new_bindings); + false } } else { - next_bindings.push(bindings.clone()); - } - } - } + true + }; + + if rel_match { + // Check if current node matches end_node_pat + let node = &nodes[curr]; + let label_match = if end_node_pat.labels.is_empty() { + true + } else { + end_node_pat.labels.contains(&node.node_type) + }; - // Continue traversal based on direction - if dist < max_hops { - let mut neighbors = Vec::new(); + if label_match { + let mut new_bindings = bindings.clone(); + + // Bind relationship variable if present (only for length 1 for now) + // Cypher behavior: (n)-[r*1..2]->(m) makes r a list. + // Our engine is MVP, let's only bind r if dist == 1. + if dist == 1 { + if let Some(ref r_var) = rel_pat.variable { + if let Some(ref ctx) = last_rel { + new_bindings.insert(r_var.clone(), EntityId::Relationship { + from_idx: start_idx, + to_idx: curr, + context: ctx.clone(), + }); + } + } + } - match rel_pat.direction { - ast::Direction::Right => { - if let Some(n) = forward_adj.get(&curr) { - neighbors.extend(n); + // Bind end variable + if let Some(ref var) = end_node_pat.variable { + if let Some(entity) = bindings.get(var) { + if let EntityId::Node(prev_idx) = entity { + if *prev_idx == curr { + next_bindings.push(new_bindings); + } + } + } else { + new_bindings.insert(var.clone(), EntityId::Node(curr)); + next_bindings.push(new_bindings); + } + } else { + next_bindings.push(new_bindings); + } } } - ast::Direction::Left => { - if let Some(n) = backward_adj.get(&curr) { - neighbors.extend(n); + } + + // Continue traversal + if dist < max_hops { + let mut neighbors = Vec::new(); + match rel_pat.direction { + ast::Direction::Right => { + if let Some(n) = forward_adj.get(&curr) { + neighbors.extend(n); + } } - } - ast::Direction::Both => { - if let Some(n) = forward_adj.get(&curr) { - neighbors.extend(n); + ast::Direction::Left => { + if let Some(n) = backward_adj.get(&curr) { + neighbors.extend(n); + } } - if let Some(n) = backward_adj.get(&curr) { - neighbors.extend(n); + ast::Direction::Both => { + if let Some(n) = forward_adj.get(&curr) { + neighbors.extend(n); + } + if let Some(n) = backward_adj.get(&curr) { + neighbors.extend(n); + } } } - } - for &next in neighbors { - if !visited.contains(&(next, dist + 1)) { - visited.insert((next, dist + 1)); - queue.push_back((next, dist + 1)); + for (next, ctx) in neighbors { + if !visited.contains(&(*next, dist + 1)) { + visited.insert((*next, dist + 1)); + queue.push_back((*next, dist + 1, Some(ctx.clone()))); + } } } } @@ -284,16 +347,43 @@ fn match_relationship_pattern( next_bindings } -fn evaluate_expression(expr: &ast::Expression, bindings: &Bindings, nodes: &[SpecBlock]) -> bool { +fn find_relationship_context(config: &Config, from_type: &str, to_type: &str) -> String { + if let Some(node_conf) = config.nodes.get(from_type) { + for rule in &node_conf.rules { + if rule.dir == "to" && rule.targets.contains(&to_type.to_string()) { + if let Some(ref ctx) = rule.context { + return ctx.clone(); + } + } + } + } + if let Some(node_conf) = config.nodes.get(to_type) { + for rule in &node_conf.rules { + if rule.dir == "from" && rule.targets.contains(&from_type.to_string()) { + if let Some(ref ctx) = rule.context { + return ctx.clone(); + } + } + } + } + "references".to_string() +} + +fn evaluate_expression( + expr: &ast::Expression, + bindings: &Bindings, + nodes: &[SpecBlock], + config: &Config, +) -> bool { match expr { ast::Expression::And(exprs) => exprs .iter() - .all(|e| evaluate_expression(e, bindings, nodes)), + .all(|e| evaluate_expression(e, bindings, nodes, config)), ast::Expression::Or(exprs) => exprs .iter() - .any(|e| evaluate_expression(e, bindings, nodes)), + .any(|e| evaluate_expression(e, bindings, nodes, config)), ast::Expression::Comparison(comp) => { - let left_val = evaluate_property_or_variable(&comp.left, bindings, nodes); + let left_val = evaluate_property_or_variable(&comp.left, bindings, nodes, config); if let Some(right_term) = &comp.right { let right_val = match right_term { ast::Term::Literal(lit) => match lit { @@ -301,7 +391,7 @@ fn evaluate_expression(expr: &ast::Expression, bindings: &Bindings, nodes: &[Spe ast::Literal::Number(n) => n.to_string(), }, ast::Term::PropertyOrVariable(pv) => { - evaluate_property_or_variable(pv, bindings, nodes) + evaluate_property_or_variable(pv, bindings, nodes, config) } }; @@ -329,13 +419,14 @@ fn evaluate_expression_value( expr: &ast::Expression, bindings: &Bindings, nodes: &[SpecBlock], + config: &Config, ) -> String { match expr { ast::Expression::Comparison(comp) => { if comp.operator.is_none() && comp.right.is_none() { - evaluate_property_or_variable(&comp.left, bindings, nodes) + evaluate_property_or_variable(&comp.left, bindings, nodes, config) } else { - evaluate_expression(expr, bindings, nodes).to_string() + evaluate_expression(expr, bindings, nodes, config).to_string() } } _ => "complex_expr".to_string(), @@ -346,23 +437,37 @@ fn evaluate_property_or_variable( pv: &ast::PropertyOrVariable, bindings: &Bindings, nodes: &[SpecBlock], + _config: &Config, ) -> String { - if let Some(&idx) = bindings.get(&pv.variable) { - let node = &nodes[idx]; - if let Some(ref prop) = pv.property { - match prop.as_str() { - "id" => node.id.clone(), - "node_type" => node.node_type.clone(), - // Alias 'type' to 'node_type' for user convenience if needed - "type" => node.node_type.clone(), - "name" => node.name.clone().unwrap_or_else(|| "null".to_string()), - "file" => node.file_path.to_string_lossy().to_string(), - "line" => node.line_start.to_string(), - "content" => node.content.clone(), - _ => "null".to_string(), + if let Some(entity) = bindings.get(&pv.variable) { + match entity { + EntityId::Node(idx) => { + let node = &nodes[*idx]; + if let Some(ref prop) = pv.property { + match prop.as_str() { + "id" => node.id.clone(), + "node_type" => node.node_type.clone(), + "type" => node.node_type.clone(), + "name" => node.name.clone().unwrap_or_else(|| "null".to_string()), + "file" => node.file_path.to_string_lossy().to_string(), + "line" => node.line_start.to_string(), + "content" => node.content.clone(), + _ => "null".to_string(), + } + } else { + node.id.clone() + } + } + EntityId::Relationship { context, .. } => { + if let Some(ref prop) = pv.property { + match prop.as_str() { + "type" => context.clone(), + _ => "null".to_string(), + } + } else { + context.clone() + } } - } else { - node.id.clone() } } else { "null".to_string() @@ -416,7 +521,8 @@ mod tests { let q = crate::core::parser::parse_query("MATCH (u:UC)-[*1..2]->(m:MOD) RETURN u.id, m.id") .unwrap(); let nodes = mock_nodes(); - let result = execute_query(&q, &nodes); + let config = Config::default(); + let result = execute_query(&q, &nodes, &config); assert_eq!(result.rows.len(), 1); assert_eq!(result.rows[0][0], "UC_001"); assert_eq!(result.rows[0][1], "MOD_001"); @@ -426,7 +532,8 @@ mod tests { fn test_execute_match_all() { let q = crate::core::parser::parse_query("MATCH (n) RETURN n.id").unwrap(); let nodes = mock_nodes(); - let result = execute_query(&q, &nodes); + let config = Config::default(); + let result = execute_query(&q, &nodes, &config); assert_eq!(result.rows.len(), 3); } @@ -434,7 +541,8 @@ mod tests { fn test_execute_match_label() { let q = crate::core::parser::parse_query("MATCH (n:UC) RETURN n.id").unwrap(); let nodes = mock_nodes(); - let result = execute_query(&q, &nodes); + let config = Config::default(); + let result = execute_query(&q, &nodes, &config); assert_eq!(result.rows.len(), 1); assert_eq!(result.rows[0][0], "UC_001"); } @@ -444,7 +552,8 @@ mod tests { let q = crate::core::parser::parse_query("MATCH (n) WHERE n.id = \"FR_001\" RETURN n.id") .unwrap(); let nodes = mock_nodes(); - let result = execute_query(&q, &nodes); + let config = Config::default(); + let result = execute_query(&q, &nodes, &config); assert_eq!(result.rows.len(), 1); assert_eq!(result.rows[0][0], "FR_001"); } @@ -453,7 +562,8 @@ mod tests { fn test_execute_property_access() { let q = crate::core::parser::parse_query("MATCH (n:UC) RETURN n.name, n.file").unwrap(); let nodes = mock_nodes(); - let result = execute_query(&q, &nodes); + let config = Config::default(); + let result = execute_query(&q, &nodes, &config); assert_eq!(result.rows.len(), 1); assert_eq!(result.rows[0][0], "User Login"); assert_eq!(result.rows[0][1], "test.md"); @@ -464,7 +574,8 @@ mod tests { let q = crate::core::parser::parse_query("MATCH (u:UC)-[]->(f:FR) RETURN u.id, f.id").unwrap(); let nodes = mock_nodes(); - let result = execute_query(&q, &nodes); + let config = Config::default(); + let result = execute_query(&q, &nodes, &config); assert_eq!(result.rows.len(), 1); assert_eq!(result.rows[0][0], "UC_001"); assert_eq!(result.rows[0][1], "FR_001"); @@ -474,10 +585,11 @@ mod tests { fn test_execute_where_operators() { let nodes = mock_nodes(); + let config = Config::default(); // Not Equal <> let q = crate::core::parser::parse_query("MATCH (n) WHERE n.id <> \"UC_001\" RETURN n.id") .unwrap(); - let result = execute_query(&q, &nodes); + let result = execute_query(&q, &nodes, &config); assert_eq!(result.rows.len(), 2); assert!(!result.rows.iter().any(|r| r[0] == "UC_001")); @@ -486,7 +598,7 @@ mod tests { "MATCH (n) WHERE n.name CONTAINS \"Login\" RETURN n.id", ) .unwrap(); - let result = execute_query(&q, &nodes); + let result = execute_query(&q, &nodes, &config); assert_eq!(result.rows.len(), 1); assert_eq!(result.rows[0][0], "UC_001"); @@ -494,7 +606,7 @@ mod tests { // (Alphabetical comparison for strings) let q = crate::core::parser::parse_query("MATCH (n) WHERE n.id > \"MOD_001\" RETURN n.id") .unwrap(); - let result = execute_query(&q, &nodes); + let result = execute_query(&q, &nodes, &config); // UC_001 > MOD_001 is true? 'U' > 'M'. Yes. // FR_001 > MOD_001? 'F' > 'M'? No. assert!(result.rows.iter().any(|r| r[0] == "UC_001")); @@ -505,12 +617,13 @@ mod tests { fn test_execute_where_logical() { let nodes = mock_nodes(); + let config = Config::default(); // AND let q = crate::core::parser::parse_query( "MATCH (n) WHERE n.node_type = \"UC\" AND n.name CONTAINS \"Login\" RETURN n.id", ) .unwrap(); - let result = execute_query(&q, &nodes); + let result = execute_query(&q, &nodes, &config); assert_eq!(result.rows.len(), 1); assert_eq!(result.rows[0][0], "UC_001"); @@ -519,7 +632,7 @@ mod tests { "MATCH (n) WHERE n.id = \"UC_001\" OR n.id = \"FR_001\" RETURN n.id", ) .unwrap(); - let result = execute_query(&q, &nodes); + let result = execute_query(&q, &nodes, &config); assert_eq!(result.rows.len(), 2); } @@ -527,10 +640,11 @@ mod tests { fn test_execute_match_multiple_labels() { let nodes = mock_nodes(); + let config = Config::default(); // (n:UC:FR) -> Should be OR (UC or FR) based on current implementation // Parser supports multi-labels. Engine `match_node_pattern` uses `labels.contains(&node.node_type)`. let q = crate::core::parser::parse_query("MATCH (n:UC:FR) RETURN n.id").unwrap(); - let result = execute_query(&q, &nodes); + let result = execute_query(&q, &nodes, &config); assert_eq!(result.rows.len(), 2); assert!(result.rows.iter().any(|r| r[0] == "UC_001")); assert!(result.rows.iter().any(|r| r[0] == "FR_001")); @@ -540,8 +654,9 @@ mod tests { #[test] fn test_execute_node_expansion() { let nodes = mock_nodes(); + let config = Config::default(); let q = crate::core::parser::parse_query("MATCH (n:UC) RETURN n").unwrap(); - let result = execute_query(&q, &nodes); + let result = execute_query(&q, &nodes, &config); // Should have 6 columns assert_eq!(result.columns.len(), 6); @@ -557,4 +672,53 @@ mod tests { assert_eq!(result.rows[0][1], "UC"); assert_eq!(result.rows[0][2], "User Login"); } + + #[test] + fn test_execute_relationship_type_and_context() { + use crate::core::config::{NodeConfig, RuleConfig}; + let nodes = mock_nodes(); + let mut config = Config::default(); + + // Setup rules in config + // UC uses FR + let mut uc_rules = NodeConfig::default(); + uc_rules.rules.push(RuleConfig { + dir: "to".to_string(), + targets: vec!["FR".to_string()], + context: Some("uses".to_string()), + ..Default::default() + }); + config.nodes.insert("UC".to_string(), uc_rules); + + // FR implemented by MOD + let mut fr_rules = NodeConfig::default(); + fr_rules.rules.push(RuleConfig { + dir: "to".to_string(), + targets: vec!["MOD".to_string()], + context: Some("implemented_by".to_string()), + ..Default::default() + }); + config.nodes.insert("FR".to_string(), fr_rules); + + // Test 1: Query with relationship variable and type + let q = crate::core::parser::parse_query("MATCH (u:UC)-[r]->(f:FR) RETURN u.id, r.type, f.id") + .unwrap(); + let result = execute_query(&q, &nodes, &config); + assert_eq!(result.rows.len(), 1); + assert_eq!(result.rows[0][0], "UC_001"); + assert_eq!(result.rows[0][1], "uses"); + assert_eq!(result.rows[0][2], "FR_001"); + + // Test 2: Filtering by relationship type + let q = crate::core::parser::parse_query("MATCH (u:UC)-[r:uses]->(f:FR) RETURN f.id") + .unwrap(); + let result = execute_query(&q, &nodes, &config); + assert_eq!(result.rows.len(), 1); + assert_eq!(result.rows[0][0], "FR_001"); + + let q = crate::core::parser::parse_query("MATCH (u:UC)-[r:other]->(f:FR) RETURN f.id") + .unwrap(); + let result = execute_query(&q, &nodes, &config); + assert_eq!(result.rows.len(), 0); + } }