diff --git a/AGENT.md b/AGENT.md index 2e8374c..33467e9 100644 --- a/AGENT.md +++ b/AGENT.md @@ -1,13 +1,24 @@ # AGENT.md -This file contains instructions and project-specific rules for AI agents (e.g., Claude Code). +This file contains instructions and project-specific rules for AI agents. -## Project Overview -`docgraph` is a tool for validating and managing graph structures within Markdown files. +## Core Identity: Reasoning over Retrieval + +`docgraph` is a **formal verification engine** for documentation. The agent MUST traverse the graph causally, proven by +`docgraph` tools, rather than guessing or searching text loosely. ## Skills -Project-specific skills are defined in `.claude/skills` (linked from `.agent/skills`). Please use them proactively. + +Use project-specific skills strictly in this order: + +1. **Environment (Bootstrap)**: If `docgraph` is missing, see checks in `compose/SKILL.md`. +2. **Ontology (Compose)**: Create/Edit `docgraph.toml` (Semantic Axiom). +3. **Consistency (Validate)**: Check topological correctness. +4. **Determinism (Align)**: Verify semantic links against schema. +5. **Convergence (Reasoning)**: Traverse the graph to answer questions. ## Rules + - Respond in Japanese. -- Use **conventional commits** for commit messages, written in **English**. +- Commit messages: **Conventional Commits** in **English**. +- Commits SHOULD be granular and atomic. diff --git a/benches/performance.rs b/benches/performance.rs index 0bccd95..bb7b003 100644 --- a/benches/performance.rs +++ b/benches/performance.rs @@ -6,7 +6,9 @@ use tempfile::tempdir; use docgraph::core::collect::collect_workspace_all; use docgraph::core::config::Config; +use docgraph::core::engine::execute_query; use docgraph::core::lint::check_workspace; +use docgraph::core::parser::parse_query; /// Generate test workspace with specified number of files and nodes fn generate_test_workspace(dir: &Path, num_files: usize, nodes_per_file: usize) { @@ -89,5 +91,73 @@ fn bench_lint_1000_nodes(c: &mut Criterion) { }); } -criterion_group!(benches, bench_collect_1000_nodes, bench_lint_1000_nodes); +fn bench_query_match_label(c: &mut Criterion) { + let dir = tempdir().expect("Failed to create temp dir"); + generate_test_workspace(dir.path(), 100, 10); + + let (nodes, _refs) = collect_workspace_all(dir.path(), &[], None); + let config = Config::load(dir.path()).expect("Failed to load config"); + let query = parse_query("MATCH (n:FR) RETURN n.id").expect("Failed to parse query"); + + c.bench_function("query_match_label_1000_nodes", |b| { + b.iter(|| { + let result = execute_query(&query, &nodes, &config); + assert!( + result.rows.len() >= 1000, + "Expected 1000+ rows, got {}", + result.rows.len() + ); + }) + }); +} + +fn bench_query_where_filter(c: &mut Criterion) { + let dir = tempdir().expect("Failed to create temp dir"); + generate_test_workspace(dir.path(), 100, 10); + + let (nodes, _refs) = collect_workspace_all(dir.path(), &[], None); + let config = Config::load(dir.path()).expect("Failed to load config"); + let query = + parse_query("MATCH (n) WHERE n.type = \"FR\" RETURN n.id").expect("Failed to parse query"); + + c.bench_function("query_where_filter_1000_nodes", |b| { + b.iter(|| { + let result = execute_query(&query, &nodes, &config); + assert!( + result.rows.len() >= 1000, + "Expected 1000+ rows, got {}", + result.rows.len() + ); + }) + }); +} + +fn bench_query_relationship(c: &mut Criterion) { + let dir = tempdir().expect("Failed to create temp dir"); + generate_test_workspace(dir.path(), 100, 10); + + let (nodes, _refs) = collect_workspace_all(dir.path(), &[], None); + let config = Config::load(dir.path()).expect("Failed to load config"); + let query = + parse_query("MATCH (a:FR)-[]->(b:FR) RETURN a.id, b.id").expect("Failed to parse query"); + + c.bench_function("query_relationship_1000_nodes", |b| { + b.iter(|| { + let result = execute_query(&query, &nodes, &config); + assert!( + !result.rows.is_empty(), + "Expected some relationship results" + ); + }) + }); +} + +criterion_group!( + benches, + bench_collect_1000_nodes, + bench_lint_1000_nodes, + bench_query_match_label, + bench_query_where_filter, + bench_query_relationship +); criterion_main!(benches); diff --git a/docgraph-plugin/skills/align/SKILL.md b/docgraph-plugin/skills/align/SKILL.md index 8e8c597..2dea072 100644 --- a/docgraph-plugin/skills/align/SKILL.md +++ b/docgraph-plugin/skills/align/SKILL.md @@ -1,129 +1,175 @@ --- name: align -description: Deep Consistency Gate - Verify Vertical and Horizontal relationship integrity. +description: Semantic Rigidity Gate - Ensure each node has a single unambiguous causal role. --- -# Deep Consistency Gate (Architecture & Meaning) +# Semantic Rigidity Gate -This skill serves as the **gate for depth and relationship integrity** within the documentation graph. It focuses on -ensuring that nodes are not only correct in isolation (as verified by `validate`) but also perfectly aligned with their -context (Vertical) and their peers (Horizontal). +This skill determines whether a node has a uniquely interpretable meaning in the graph. -> [!IMPORTANT] This is an **Architecture & Meaning Gate**. Flag any semantic "fog" (unclear boundaries, implicit -> assumptions, or overloaded terms). Every node must be fully justified by its context. +Alignment does not evaluate completeness or quality. It eliminates semantic ambiguity that would cause reasoning +divergence. -## Prerequisites +> [!IMPORTANT] A node is aligned only if its meaning has exactly one causal role. If multiple interpretations are +> possible, alignment fails. -- **`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 +## Core Principle -### 0. Validation Pre-requisite +Validation ensures the graph can exist. Alignment ensures the graph means only one thing. -- **Level**: STRICT -- **Policy**: - - If `validate` status is unknown or FAIL -> **STOP** and return FAIL. - - Do not re-evaluate surface items (naming, templates) already covered by `validate`. +If alignment fails → different agents may derive different conclusions. -### 1. Vertical Consistency (Traceability & Context) +Alignment verifies determinism of interpretation, not properties of the node alone. -- **Level**: STRICT for missing links, HEURISTIC for semantic clarity. -- **Vertical Expectations**: - - **Parents (Inbound)**: Define the "Why" (intent, requirement, or goal). - - **This Node**: Defines the "What" at its specific abstraction level. - - **Children (Outbound)**: Define the "How" (realization, implementation, or breakdown). +Alignment guarantees that traversal meaning is path-independent. -1. **Context Check**: Use `docgraph describe ` 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: - - Is this node's responsibility fully and exclusively covered by its children? +--- -### 2. Horizontal Consistency (Peer Alignment & MECE) +## 0. Precondition -- **Level**: HEURISTIC -- **Baseline Rule**: Use the dominant pattern among existing peer nodes. Do not invent new abstraction levels unless - proposing an explicit refactor. +The node MUST pass `validate`. -1. **Peer Identification**: Use `docgraph list "_*"`. -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? +If validation fails → STOP -### 3. Structural SRP Check +--- -- **Level**: HEURISTIC -- **Note**: `validate` checks surface SRP; `align` checks structural SRP (depth, cohesion, and abstraction fit). +## 1. Semantic Topology from Schema -1. **Cohesion**: Are all elements within this node tightly related to the "What" definition? -2. **Abstraction**: Is the node at the correct level relative to its parents and peers? +After confirming `validate` PASS, the agent MUST read `docgraph.toml` to derive the semantic topology used for +alignment. -### 4. Proposals & Impact Analysis +`docgraph.toml` is the sole normative source. If unread, the agent MUST stop. -- **Level**: MANDATORY +The agent MUST map schema elements into interpretation constraints: -When proposing changes (Clarify Context, Split, Merge, or Move): +- Node type → causal role candidates (e.g., UC=Intent, FR=Responsibility, MOD=Realization, ADR=Rationale) +- Relation type (`r.type`) → allowed causal questions (Why/What/How/Boundary/Justification) +- Which relations define "peerhood" for horizontal comparison -1. **Affected Nodes**: List all nodes (parents, peers, children) that will be affected. -2. **Re-validation**: Indicate whether `validate` must be re-run for any affected nodes. -3. **Safety**: Ensure no existing references are broken without a remediation plan. +Alignment MUST only judge determinism using this schema-derived topology. If the mapping cannot be established, +alignment MUST fail (unknown semantics). -## Workflow Cases +--- -### Case 1: TYPE_ID (e.g., FR, MOD) +## 2. Role Determinism -- Perform a full graph consistency review for the given type. +The node must belong to exactly one abstraction level: -### Case 2: NODE_ID (e.g., FR_LOGIN) +- Intent (why) +- Responsibility (what) +- Realization (how) +- Constraint (boundary) +- Rationale (justification) -- **Status**: Focused Refinement. -- Perform a deep analysis specifically for the node and its immediate relations. Do not scan the entire graph unless - necessary for baseline identification. +If the node can be interpreted as more than one → FAIL -## Alignment Analysis Report +The role must be inferable from relations, not from description wording. If role changes when ignoring prose, alignment +fails. -You **must** provide the analysis in the following format: +--- -### Target +## 3. Vertical Determinism -- **Node/Scope**: [ID or Type] -- **Baseline Peer Pattern**: [Description of dominant convention] +Parent and child relations must not redefine the node's role. -### Consistency Analysis +Invalid cases: -| Dimension | Check Item | Result | Analysis / Evidence | -| :--------------- | :-------------- | :-------- | :------------------ | -| **Prerequisite** | Validate PASS | PASS/FAIL | | -| **Vertical** | Parents (Why) | PASS/FAIL | | -| **Vertical** | Children (How) | PASS/FAIL | | -| **Horizontal** | Peer MECE | PASS/FAIL | | -| **SRP** | Abstraction Fit | PASS/FAIL | | +- Parent treats node as requirement, child treats it as implementation +- Node alternates abstraction level depending on traversal direction -### Refinement Proposals +Result: FAIL -- **Proposal**: [Description] -- **Affected IDs**: [List] -- **Re-validate Required**: [Yes/No] +--- -### Quality Gate Checklist +## 4. Semantic Collision -In your final report, you **must** include this checklist to demonstrate deep architectural verification: +The graph must not provide multiple answers to the same causal question. -- [ ] **Prerequisite PASS**: The node has successfully cleared the `validate` skill (Quality Gate). -- [ ] **Vertical Alignment**: Why (Parent), What (This Node), and How (Child) are semantically consistent. -- [ ] **Horizontal MECE**: Responsibility is mutually exclusive and follows the dominant peer baseline. -- [ ] **Semantic Clarity**: No "semantic fog" or ambiguous boundaries identified. -- [ ] **Impact Analysis**: All affected nodes are listed, and re-validation needs are clearly stated. +Invalid cases: -## Final Decision +- synonymous responsibilities split across nodes +- overlapping definitions describing identical behavior +- two nodes satisfy the same parent relation +- peer boundaries depend on interpretation + +Result: FAIL + +--- + +## Minimal Tooling (Observation Only) + +Alignment MUST use `docgraph` for observation. Tools materialize evidence; they do not drive exploration. + +### `docgraph describe` + +Inspect the target node and its immediate inbound/outbound relations. + +```bash +docgraph describe +``` + +### `docgraph query` (bounded) + +Use bounded queries only to detect ambiguity patterns, never to discover new intent. + +Typical checks: + +- Competing parents (multiple "Why" justifications) + +```bash +docgraph query "MATCH (p)-[r]->(n) WHERE n.id='' RETURN p.id, p.type, r.type" +``` -### Decision Semantics +- Competing children (multiple "How" realizations that imply different roles) + +```bash +docgraph query "MATCH (n)-[r]->(c) WHERE n.id='' RETURN c.id, c.type, r.type" +``` + +- Peer collision (potential synonyms within the same type) + +```bash +docgraph query "MATCH (m:) RETURN m.id, m.title" +``` + +> [!CAUTION] Queries MUST be bounded (1-hop or limited results). If multi-hop traversal is required, switch to +> `reasoning`. + +--- + +## What Alignment Does NOT Check + +Alignment does not enforce: + +- completeness +- coverage +- implementation presence +- formatting quality +- directory structure + +--- + +## Alignment Report + +### Target + +- **Node**: [ID] + +### Determinism Results + +| Check | Result | Notes | +| :------------------- | :-------- | :---- | +| Schema topology | READ | | +| Role determinism | PASS/FAIL | | +| Vertical determinism | PASS/FAIL | | +| Semantic collision | PASS/FAIL | | + +--- + +## Final Decision -- **PASS**: Node shows deep integrity and may be merged/applied. -- **FAIL**: Structural or semantic issues identified. MUST NOT be merged. +**PASS** → Node meaning is uniquely defined **FAIL** → Node meaning is ambiguous **FINAL DECISION: [PASS/FAIL]** 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/compose/SKILL.md b/docgraph-plugin/skills/compose/SKILL.md new file mode 100644 index 0000000..17e291e --- /dev/null +++ b/docgraph-plugin/skills/compose/SKILL.md @@ -0,0 +1,361 @@ +--- +name: compose +description: Schema Composition - Design the graph topology and generate docgraph.toml for deterministic traversal. +--- + +# Schema Composition + +This skill creates the normative topology of the documentation graph by generating `docgraph.toml`. + +The schema defines what can exist, how it can connect, and what causal traversal means. + +> [!IMPORTANT] The schema must minimize ambiguity while allowing evolution. Restrict relations by direction and target +> types to enforce determinism. + +--- + +## Environment Bootstrap + +`compose` assumes the documentation graph can be observed and validated. If `docgraph` is not available, the agent MUST +install it. + +The graph system cannot exist without the graph tool. + +### Install `docgraph` + +macOS / Linux: + +```bash +curl -fsSL https://raw.githubusercontent.com/sonesuke/docgraph/main/install.sh | bash +``` + +Windows (PowerShell): + +```powershell +powershell -c "irm https://raw.githubusercontent.com/sonesuke/docgraph/main/install.ps1 | iex" +``` + +### Verify installation + +```bash +docgraph --version +``` + +If the command fails, the agent MUST stop and report environment failure. + +--- + +## Core Axiom + +The schema must minimize ambiguity while preserving evolution. + +Too strict → the graph cannot grow. Too loose → `align` cannot determine unique meaning. + +Every schema decision is a trade-off between these two forces. + +The schema is the only place where new semantics are introduced. All other skills only enforce or traverse what the +schema defines. + +--- + +## Outputs + +- `docgraph.toml` (the sole normative source) +- Minimal templates per node type (optional, in `doc/templates/`) + +--- + +## 0. Inputs + +Before composing, the agent MUST establish: + +- **Domain**: What kind of system or product is being documented? +- **Terminal nodes**: What counts as "done"? (Implementation? Tests? Metrics?) +- **Primary reasoning direction**: Top-down (Why→How) or bottom-up (How→Why)? + +If these inputs are unavailable, the agent MUST assume defaults and state them explicitly. + +--- + +## 1. Role Inventory + +Define the reasoning roles required by the project. + +Minimum viable set: + +| Role | Causal Question | Required | +| :------------- | :-------------- | :------- | +| Intent | Why? | Yes | +| Responsibility | What? | Yes | +| Realization | How? | Yes | +| Evidence | Proven? | No | +| Constraint | Boundary? | No | +| Rationale | Justified? | No | +| Domain | Context? | No | + +Intent, Responsibility, and Realization are always required. The others depend on project maturity. + +--- + +## 2. Type Mapping + +Map concrete node types to roles. The mapping MUST be explicit and complete. + +> [!NOTE] Node types are schema-specific. The skill must work for any naming scheme. + +Every node type MUST belong to exactly one role. If a type spans two roles, the schema is ambiguous. + +### Example: Product Spec World (non-normative) + +| Role | Node Types | +| :------------- | :----------- | +| Intent | UC, ACT | +| Responsibility | FR, NFR | +| Realization | MOD, IF, CC | +| Evidence | TEST, METRIC | +| Constraint | CON | +| Rationale | ADR | +| Domain | DAT | + +--- + +## 3. Relation Primitives + +Define a small controlled set of relation types (`rel`) that correspond to causal questions. `rel` becomes `r.type` in +Cypher. + +> [!IMPORTANT] `rel` is a controlled vocabulary. Any `rel` not declared here is invalid. + +Recommended minimal set: + +| Relation | From Role | To Role | Question | +| :------------- | :------------- | :------------- | :---------------------- | +| refines | Intent | Responsibility | What must be satisfied? | +| realized_by | Responsibility | Realization | How is it achieved? | +| constrained_by | Responsibility | Constraint | What boundaries apply? | +| justified_by | Realization | Rationale | Why this design? | +| verified_by | Realization | Evidence | Is it proven? | +| depends_on | Realization | Realization | What depends on this? | +| defines | Domain | any | What context applies? | + +Each relation MUST have a clear causal direction. Bidirectional relations indicate ambiguity. + +--- + +## 4. Target Restrictions + +For each relation type, restrict allowed source and target node types. + +This is where determinism is enforced. The narrower the targets, the less `align` has to guess. + +Example constraints in `docgraph.toml`: + +```toml +[nodes.FR] +desc = "Functional Requirement" +template = "doc/templates/functional.md" +rules = [ + { dir = "to", targets = ["MOD"], min = 1, desc = "Must be realized", rel = "realized_by" }, +] +``` + +### Rule Syntax + +Each rule has the following fields: + +| Field | Description | +| :-------- | :----------------------------------------------------------------------------- | +| `dir` | `"to"` (this node references target) or `"from"` (target references this node) | +| `targets` | Array of allowed node type IDs, or `["*"]` for any type | +| `min` | Minimum required count (`0` = optional, `1+` = required) | +| `desc` | Human-readable justification for the rule | +| `rel` | Relation type identifier, snake_case. Used as `r.type` in Cypher queries | + +### Special Patterns + +**`dir = "from"`**: Declares an _inbound_ expectation. The rule says "this node expects to be referenced by targets". +Use when the _target_ side owns the link in the markdown. + +**`targets = ["*"]`**: Accepts references from any node type. + +> [!CAUTION] `"*"` disables type-level restriction. If overused, `align` cannot determine causal role from relations +> alone. + +> [!IMPORTANT] `targets=["*"]` is allowed, but it shifts the burden to determinism. When using `targets=["*"]`, the +> schema MUST ensure role determinism using one of: +> +> - the source node type has a unique role by definition, OR +> - the `rel` itself implies a unique causal question independent of target type, OR +> - an additional rule narrows interpretation (e.g., required inbound/outbound constraints). + +### Determinism Rule (for `targets=["*"]`) + +If `targets=["*"]` is used, the schema MUST state why interpretation remains unambiguous. This justification MUST +reference Role Inventory and Relation Primitives. + +Design principles: + +- Every Responsibility MUST reach at least one Realization +- Realization SHOULD be justifiable (link to Rationale) +- Constraints MUST target Responsibility, not Realization directly + +--- + +## 5. Closure Requirements + +Verify that the schema allows valid traversal from Intent to terminal nodes. + +The agent MUST check: + +- Every Responsibility type has at least one path to a Realization type +- If Evidence types exist, at least one Realization type can reach them +- No role is completely isolated (unreachable from any other role) + +If closure fails, the schema has a structural gap. + +--- + +## 6. Template Generation + +Each node type in `docgraph.toml` SHOULD have a `template` field pointing to a template file. + +Templates define the minimal document structure for a node type. They ensure new nodes are created with the correct +anchor, required sections, and placeholder links. + +### Template Syntax Rules + +**Anchor** (required, first line): + +```markdown + +``` + +`*` is replaced with the actual ID suffix when instantiating (e.g., `FR_001`). + +**Heading level**: Match the document nesting depth. + +- Top-level documents: `# {Title}` or `## {Title}` +- Nodes embedded in a shared file: `### {Title}` + +**Link placeholder format**: + +```markdown +[{TARGET*TYPE}* (\_)](*#{TARGET_TYPE}_*) +``` + +- `{TARGET_TYPE}*` → type prefix + wildcard ID (e.g., `MOD*`) +- `(*)` → placeholder display name +- `(*#{TARGET_TYPE}_*)` → placeholder anchor reference + +**Required sections** (`min >= 1` rules): Always include. Name the section after the `rel` value. + +**Optional sections** (`min = 0` rules): Include with `(Optional)` suffix in the heading. + +> [!NOTE] Template examples below are illustrative. Replace section headings with the `rel` values declared in your +> schema. + +### Template Patterns + +**Rich node** (has outbound rules): Include link sections per rule. + +```markdown + + +## {Title} + +{Description} + +### + +- [* (\_)](*#_*) + +### (Optional) + +- [* (\_)](*#_*) +``` + +**Leaf node** (only inbound `from` rules, no outbound): Minimal template with no link sections. + +```markdown + + +### {Title} + +{Description} +``` + +**Rationale node** (ADR): Domain-specific sections instead of link sections. + +```markdown + + +# {Title} + +{Description} + +## Decision + +{Decision} + +## Rationale + +{Rationale} +``` + +### Registering Templates + +The `template` field in `docgraph.toml`: + +```toml +[nodes.UC] +desc = "Use Case" +template = "doc/templates/usecases.md" +``` + +--- + +## 7. Generate `docgraph.toml` + +Emit the schema. This file becomes the sole normative source for all other skills. + +### `[graph]` Section + +Define global graph settings before node definitions. The `ignore` field excludes paths from graph traversal. + +```toml +[graph] +ignore = ["README.md", "SECURITY.md", "doc/templates"] +``` + +Typical ignore targets: + +- Non-document files (README, SECURITY, CHANGELOG) +- Template directories (templates are blueprints, not graph nodes) +- Plugin/tool configuration directories + +### `[nodes.*]` Sections + +Emit one section per node type, following the Type Mapping and Target Restrictions defined above. + +Emit the schema. This file becomes the sole normative source for all other skills. + +After generation: + +1. Run `docgraph check` to verify the schema and graph loader are consistent. +2. If existing documents/nodes exist: + - Run `validate` on a representative node of each major role. + - Run `align` on at least one node per role boundary (e.g., Intent/Responsibility, Responsibility/Realization). +3. If no nodes exist yet: + - Generate one minimal stub per required role using templates, then run `validate` and `align`. + +--- + +## What Compose Does NOT Do + +Compose does not: + +- Write document content +- Validate existing nodes +- Judge alignment or reasoning correctness + +Compose creates the rules. Other skills enforce them. 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/reasoning/SKILL.md b/docgraph-plugin/skills/reasoning/SKILL.md new file mode 100644 index 0000000..db90215 --- /dev/null +++ b/docgraph-plugin/skills/reasoning/SKILL.md @@ -0,0 +1,190 @@ +--- +name: reasoning +description: + Graph Reasoning - Navigate the documentation graph to progressively refine a hypothesis until a stable conclusion is + formed. +--- + +# Graph Reasoning + +This skill defines how the agent thinks using the documentation graph. + +The graph is not a database to search. It is a reasoning space to traverse. + +The agent MUST navigate nodes to progressively refine a hypothesis until a stable conclusion is formed. + +> [!IMPORTANT] The agent does not retrieve answers. It stabilizes understanding by traversing causality. + +--- + +## 0. Semantic Topology Understanding + +Before any traversal, the agent MUST interpret the meaning of the graph structure. + +**Action**: Read `docgraph.toml` in the project root. + +The agent reads `docgraph.toml` and classifies each node type into a reasoning role: + +| Role | Meaning | Example Node Types | +| :------------------------ | :---------------------------------- | :----------------- | +| **Intent** (WHY) | Purpose and motivation | UC, ACT | +| **Constraint** (BOUNDARY) | Non-negotiable conditions | CON | +| **Responsibility** (WHAT) | Obligations the system must fulfill | FR, NFR | +| **Realization** (HOW) | Concrete mechanisms and structures | MOD, IF, CC | +| **Evidence** (PROOF) | Verification artifacts (optional) | TEST, MET, LOG | +| **Rationale** (DECISION) | Design justification | ADR | +| **Domain** (CONTEXT) | Shared data models | DAT | + +### Semantic Gravity + +The agent MUST never move arbitrarily across node categories. + +Movement is constrained by semantic gravity: + +- **Intent** pulls toward purpose +- **Responsibility** pulls toward obligation +- **Realization** pulls toward mechanism +- **Evidence** pulls toward validation +- **Constraint** pulls toward non-negotiable invariants +- **Rationale** pulls toward justification + +If the agent cannot name the gravitational pull driving a move, the move is invalid. + +--- + +## 1. Reasoning Loop + +The agent operates in a continuous loop: + +1. **Hold a hypothesis** — a claim about the graph's state +2. **Move to the next node** — following semantic gravity +3. **Update the hypothesis** — based on what was found + +This is not search. This is exploration. + +The loop terminates when the hypothesis is stable: no further traversal changes the conclusion. + +--- + +## 2. Allowed Reasoning Moves + +Every move MUST preserve causal meaning. Each edge carries a question the agent is asking. + +``` +Intent → Responsibility + "What must be satisfied?" + +Responsibility → Realization + "How is it achieved?" + +Realization → Realization + "What depends on this?" + +Responsibility → Constraint + "What boundaries apply?" + +Realization → Rationale + "Why was this design chosen?" + +Realization → Evidence + "Is it proven?" + +Evidence → Responsibility + "What failed or needs adjustment?" +``` + +The `r.type` field in `docgraph.toml` (`rel`) names the semantic relationship. + +A move is valid only if `r.type` matches the question of that move. If the mapping is unknown, the agent MUST stop and +re-classify the edge semantics from `docgraph.toml`. + +--- + +## 3. Reasoning Patterns + +### 3.1 Closure (Is it complete?) + +Start from a node. Attempt to reach a terminal. If no path reaches a Realization or Evidence node, the structure is +incomplete. + +```bash +docgraph query "MATCH (n)-[r*1..3]->(m:MOD) WHERE n.id = '' RETURN m.id, r.type" +``` + +### 3.2 Forward (How is it realized?) + +Start from Intent or Responsibility. Follow semantic gravity downward. Each step asks: "How does this become concrete?" + +```bash +docgraph query "MATCH (n)-[r]->(m) WHERE n.id = '' RETURN m.id, m.type, r.type" +``` + +### 3.3 Backward (Why does this exist?) + +Start from Realization. Follow relationships upward. Each step asks: "What justified this?" + +```bash +docgraph query "MATCH (n)-[r]->(m) WHERE m.id = '' RETURN n.id, n.type, r.type" +``` + +### 3.4 Counterfactual (What breaks if removed?) + +Hypothetically remove a node. Trace all dependents. A node loses its justification when all incoming edges with a +justification `r.type` (e.g., `used_by`, `realized_by`, `constrained_by`) originate from the removed node. + +```bash +docgraph query "MATCH (n)-[r]->(m) WHERE m.id = '' RETURN n.id, r.type" +``` + +--- + +## 4. Tools + +Tools materialize reasoning decisions. They do not drive them. + +> [!CAUTION] Cypher MUST NOT be used to discover what to think. Cypher is only used to materialize a reasoning decision +> already made. +> +> If the agent starts from a query instead of a hypothesis, it is performing search, not reasoning. + +### `docgraph query` + +Executes a Cypher query to confirm or refute a hypothesis. + +```bash +docgraph query "" +``` + +### `docgraph describe` + +Inspects a single node and its immediate relationships. + +```bash +docgraph describe +``` + +--- + +## 5. Reasoning Report + +Structure findings as reasoning conclusions, not search results. + +### Hypothesis + +- **Initial**: [What the agent believed before traversal] +- **Final**: [What the agent concluded after traversal] + +### Traversal Trace + +Show the path taken and the reasoning at each step: + +``` +UC_001 --(uses)--> FR_001 --(realized_by)--> MOD_001 + "What must be satisfied?" "How is it achieved?" +``` + +### Stability Assessment + +- **Conclusion Stable**: [Yes/No] +- **Unresolved Nodes**: [List of nodes that could not be reached or justified] +- **Integrity**: [PASS/FAIL] 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..9046381 100644 --- a/docgraph-plugin/skills/validate/SKILL.md +++ b/docgraph-plugin/skills/validate/SKILL.md @@ -1,160 +1,165 @@ --- name: validate -description: Validation Quality Gate - Ensure project integrity and consistency. +description: Graph Integrity Gate - Ensure nodes and relations can exist in the causal graph. --- -# Validation Quality Gate +# Graph Integrity Gate -This skill serves as the **definitive quality threshold** for the documentation graph. It must be executed strictly to -ensure that all semantic, structural, and consistency rules are met before finalizing changes. +This skill determines whether a node and its local causal subgraph can exist. -> [!IMPORTANT] This skill must perform checks **strictly and rigorously**. Accuracy in ID naming, responsibility scope, -> and template adherence is critical for the integrity of the documentation graph. **Even cosmetic inconsistencies must -> be flagged.** +Validation does not evaluate quality or design semantics. It verifies structural conditions required for causal +traversal. -> [!NOTE] For detailed usage and available options of any `docgraph` subcommand, always refer to `docgraph --help` or -> `docgraph --help`. +> [!IMPORTANT] A node is valid only if its presence does not create a logical contradiction in the graph. Cosmetic or +> stylistic issues must NOT cause failure. -## 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 +## Core Principle -## Workflow Steps +Validation checks **existence**, not interpretation. -### 0. Automated Check Pre-requisites +- If validation fails → the graph contains a structural contradiction +- If validation passes → reasoning may safely operate -- **Level**: STRICT -- **Failure Policy**: FAIL immediately if any check fails. +Validation rejects impossible structures, not incomplete ones. -Before performing manual semantic checks, ensure all automated validations pass. +Validation guarantees the graph is a possible world, not a complete world. -1. **Formatting**: Identify the appropriate Markdown formatter for the project (e.g., Prettier, Biome) and verify - Markdown style (e.g., `npm run format:md -- --check`). -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 +## 0. Schema Topology Understanding -- **Level**: PREPARATION -- **Purpose**: - - Identify peer nodes for comparison (naming, placement, structure). - - Establish the dominant convention (baseline) for the specific node type. +Before running validation, the agent MUST read `docgraph.toml` to establish the allowed structural topology. -List elements within the target scope to understand the current state. Use `docgraph list`. +`docgraph.toml` is the sole normative source. If unread, the agent MUST stop. -### 2. ID and Title Correspondence +The agent MUST extract: -- **Level**: STRICT -- **Failure Policy**: FAIL immediately. +- Allowed node types and their identifiers +- Allowed relation types (`r.type`) and their directionality +- Allowed target types for each relation +- Required and forbidden relation rules -Verify if the ID mnemonic matches the Title content. +All validation decisions MUST be justified against this topology. -- `FR_LOGIN` -> "User Login" (Correct) -- `FR_AUTH` -> "User Login" (Incorrect - too broad or mismatched) +--- -### 3. Prefix Consistency +## 1. Automated Structural Checks -- **Level**: STRICT -- **Failure Policy**: FAIL immediately. +Run automatic checks before manual inspection. -Verify if elements in the same category share the same prefix: +```bash +docgraph check +``` -- `FR_` (Functional Requirements) -- `NFR_` (Non-Functional Requirements) -- `UC_` (Use Cases) -- `MOD_` (Modules) -- `IF_` (Interfaces) -- `CC_` (Cross-cutting Concepts) -- `ADR_` (Architecture Decision Records) -- `BB_` (Building Blocks) +If this fails → **FAIL immediately** -### 4. File Placement and Categorization +--- -- **Level**: STRICT -- **Failure Policy**: FAIL immediately. -- **Rule of Thumb**: Generally, nodes with the same prefix should be grouped in the same file or a specific directory - structure. **This is not an exception mechanism.** +## 2. Schema Integrity -Verify if the file location is appropriate for the ID prefix and consistent with baseline nodes identified in Step 1. +All nodes and relations must conform to `docgraph.toml`. -- `FR_LOGIN` should be in `doc/requirements/functional/...` +Failure cases: -### 5. Template and Structure Validation +- Undefined node type +- Undefined relation type +- Disallowed relation direction -- **Level**: STRICT -- **Failure Policy**: FAIL immediately. +Result: FAIL -Verify if the node's content follows the defined template and structure rules. +--- -1. **Retrieve Template**: Use `docgraph type `. -2. **Retrieve Content**: Use `docgraph describe `. -3. **Compare**: - - Are all required sections (headers) present? - - Do list items (dependencies) match the expected patterns? +## 3. Referential Integrity -### 6. Single Responsibility Principle (SRP) Check +All references must resolve. -- **Level**: HEURISTIC (Judgment required) -- **Failure Policy**: FAIL with remediation proposal if violated. +- inbound references resolve +- outbound references resolve -Verify if the node represents a single responsibility at the appropriate granularity for its type. +Dangling references → FAIL -- **Goal**: One ID should correspond to one clear concept or requirement. -- **Check**: Does this node try to address multiple unrelated issues? +--- -### 7. Remediation Safety Rules +## 4. Identity Uniqueness -- **Level**: MANDATORY +Node identifiers must be globally unique. -Any change to ID or file location **MUST** follow these safety rules: +Duplicate ID → FAIL -1. **Reference Check**: Run `docgraph describe ` and enumerate all inbound/outbound relations. -2. **Impact Assessment**: Verify that proposed changes do not break existing references unless explicitly handled. +--- -### 8. Remediation Proposals +## 5. Causal Contradictions -If issues are found, propose: +The local graph must not contain logical contradictions that prevent traversal. -- **Rename ID**: Suggest a better ID (after Safety Rules). -- **Move File**: Suggest moving the definition (after Safety Rules). -- **Fix Structure**: Propose adding missing sections or correcting links. -- **Split Node**: Propose splitting the node into multiple IDs with narrower scopes. +A structure is invalid only if realization is impossible, not merely incomplete. -## Validation Report +Examples: -You **must** provide the evaluation results in the following format: +- constraints block all realizations +- cyclic dependencies preventing resolution +- mutually exclusive requirements +- relation semantics conflict +- schema makes realization unreachable -### Target +Any detected → FAIL + +--- -- **Scope**: [e.g., FR_LOGIN] -- **Baseline Category**: [e.g., FR_ nodes in doc/requirements/functional/] +## Minimal Tooling -### Findings +### `docgraph check` -| Category | Evaluated Item | Result | Notes/Details | -| :-------- | :-------------------- | :-------- | :------------ | -| Automated | format / check | PASS/FAIL | | -| ID Naming | Mnemonic consistency | PASS/FAIL | | -| Structure | Template adherence | PASS/FAIL | | -| SRP | Single responsibility | PASS/FAIL | | +```bash +docgraph check +``` -### Quality Gate Checklist +### `docgraph describe` (optional) -- [ ] **Automated Checks**: Markdown format, `docgraph check`, etc. -- [ ] **ID Naming**: ID is underscore-separated and mnemonic. -- [ ] **File Placement**: Consistent with peer baseline. -- [ ] **Template Adherence**: All sections from `docgraph type` present. -- [ ] **SRP Compliance**: Single responsibility at node level. +Use only when confirming unresolved references around a specific node. -## Final Decision +```bash +docgraph describe +``` + +--- + +## What Validation Does NOT Check -### Decision Semantics +The following MUST NOT cause failure: + +- naming elegance +- directory placement +- template formatting +- SRP granularity +- documentation completeness + +These belong to alignment or hygiene review. + +--- + +## Validation Report + +### Target + +- **Node**: [ID] + +### Structural Results + +| Check | Result | Notes | +| :-------------------- | :-------- | :---- | +| Schema topology | READ | | +| Schema integrity | PASS/FAIL | | +| Referential integrity | PASS/FAIL | | +| Identity uniqueness | PASS/FAIL | | +| Causal contradictions | PASS/FAIL | | + +--- + +## Final Decision -- **PASS**: Node meets all strict criteria and may be merged/applied. -- **FAIL**: Node MUST NOT be merged. Remediation MUST be completed and re-validated. +**PASS** → Node may exist in the graph **FAIL** → Graph integrity would be broken **FINAL DECISION: [PASS/FAIL]** diff --git a/docgraph.toml b/docgraph.toml index cee65f5..b644250 100644 --- a/docgraph.toml +++ b/docgraph.toml @@ -2,7 +2,7 @@ # Based on the Dependency Model specification [graph] -ignore = ["README.md", "SECURITY.md", ".claude", ".claude-plugin", "docgraph-plugin", "doc/templates"] +ignore = ["README.md", "SECURITY.md", ".agent", ".claude", ".claude-plugin", "docgraph-plugin", "doc/templates"] # Core specification types [nodes.UC] @@ -11,13 +11,13 @@ template = "doc/templates/usecases.md" rules = [ { dir = "to", targets = [ "ACT", - ], min = 1, desc = "Every use case must identify the initiating participant (Actor) to define scope and responsibility", context = "triggered by" }, + ], min = 1, desc = "Every use case must identify the initiating participant (Actor) to define scope and responsibility", rel = "triggered_by" }, { dir = "to", targets = [ "IF", - ], min = 1, desc = "Business scenarios must specify their interaction points (Interfaces) to ensure architectural decoupling", context = "interacts through" }, + ], min = 1, desc = "Business scenarios must specify their interaction points (Interfaces) to ensure architectural decoupling", rel = "interacts_through" }, { dir = "to", targets = [ "FR", - ], min = 1, desc = "Use cases must be connected to functional outcomes (FR) to maintain business value traceability", context = "uses" }, + ], min = 1, desc = "Use cases must be connected to functional outcomes (FR) to maintain business value traceability", rel = "uses" }, ] [nodes.FR] @@ -26,22 +26,22 @@ template = "doc/templates/functional.md" rules = [ { dir = "from", targets = [ "UC", - ], min = 1, desc = "Every functional requirement must be driven by at least one business scenario (UC) to ensure user value", context = "used by" }, + ], min = 1, desc = "Every functional requirement must be driven by at least one business scenario (UC) to ensure user value", rel = "used_by" }, { dir = "from", targets = [ "CON", - ], min = 0, desc = "Functional requirements define how the system must behave within foundational constraints", context = "constrained by" }, + ], min = 0, desc = "Functional requirements define how the system must behave within foundational constraints", rel = "constrained_by" }, { dir = "to", targets = [ "MOD", - ], min = 1, desc = "Every requirement must be allocated to a structural component (Module) to ensure no functionality is left unimplemented", context = "implemented by" }, + ], min = 1, desc = "Every requirement must be allocated to a structural component (Module) to ensure no functionality is left unimplemented", rel = "realized_by" }, { dir = "to", targets = [ "NFR", - ], min = 0, desc = "Functional requirements may drive specific quality attribute requirements (NFR)", context = "qualified by" }, + ], min = 0, desc = "Functional requirements may drive specific quality attribute requirements (NFR)", rel = "qualified_by" }, { dir = "to", targets = [ "IF", - ], min = 0, desc = "Functional requirements may define public API contracts or user interfaces (IF)", context = "defines" }, + ], min = 0, desc = "Functional requirements may define public API contracts or user interfaces (IF)", rel = "defines" }, { dir = "to", targets = [ "CC", - ], min = 0, desc = "Functional requirements may be reflected in cross-cutting concerns (CC) that apply across multiple modules", context = "reflected in" }, + ], min = 0, desc = "Functional requirements may be reflected in cross-cutting concerns (CC) that apply across multiple modules", rel = "reflected_in" }, ] [nodes.NFR] @@ -50,16 +50,16 @@ template = "doc/templates/non-functional.md" rules = [ { dir = "from", targets = [ "FR", - ], min = 0, desc = "Quality requirements define the performance or security expectations for functional behavior", context = "qualifies" }, + ], min = 0, desc = "Quality requirements define the performance or security expectations for functional behavior", rel = "qualifies" }, { dir = "from", targets = [ "CON", - ], min = 0, desc = "Quality requirements define the quality attributes dictated by foundational constraints", context = "constrained by" }, + ], min = 0, desc = "Quality requirements define the quality attributes dictated by foundational constraints", rel = "constrained_by" }, { dir = "to", targets = [ "CC", - ], min = 0, desc = "Quality requirements are reflected in cross-cutting designs (CC) that apply across modules", context = "reflected in" }, + ], min = 0, desc = "Quality requirements are reflected in cross-cutting designs (CC) that apply across modules", rel = "reflected_in" }, { dir = "to", targets = [ "MOD", - ], min = 0, desc = "Quality requirements may be realized through specific implementation details in modules (MOD)", context = "implemented by" }, + ], min = 0, desc = "Quality requirements may be realized through specific implementation details in modules (MOD)", rel = "realized_by" }, ] [nodes.CON] @@ -69,7 +69,7 @@ rules = [ { dir = "to", targets = [ "FR", "NFR", - ], min = 1, desc = "Foundational constraints must be embodied in actionable functional or quality requirements to guide system behavior", context = "constrains" }, + ], min = 1, desc = "Foundational constraints must be embodied in actionable functional or quality requirements to guide system behavior", rel = "constrains" }, ] [nodes.MOD] @@ -81,7 +81,7 @@ rules = [ "NFR", "IF", "CC", - ], min = 1, desc = "Every structural module must be justified by at least one requirement or architectural concept to ensure purposeful design", context = "implemented by" }, + ], min = 1, desc = "Every structural module must be justified by at least one requirement or architectural concept to ensure purposeful design", rel = "realized_by" }, ] # Architecture types @@ -92,13 +92,13 @@ rules = [ { dir = "from", targets = [ "NFR", "FR", - ], min = 0, desc = "Cross-cutting concerns address requirements that span multiple system modules", context = "reflected by" }, + ], min = 0, desc = "Cross-cutting concerns address requirements that span multiple system modules", rel = "reflected_by" }, { dir = "to", targets = [ "ADR", - ], min = 0, desc = "Cross-cutting concern designs lead to specific architectural decisions (ADR)", context = "leads to" }, + ], min = 0, desc = "Cross-cutting concern designs lead to specific architectural decisions (ADR)", rel = "justified_by" }, { dir = "to", targets = [ "MOD", - ], min = 0, desc = "Cross-cutting concerns are realized through implementation patterns in specific modules (MOD)", context = "implemented by" }, + ], min = 0, desc = "Cross-cutting concerns are realized through implementation patterns in specific modules (MOD)", rel = "realized_by" }, ] # Documentation types @@ -108,10 +108,10 @@ template = "doc/templates/actors.md" rules = [ { dir = "from", targets = [ "*", - ], min = 0, desc = "Actors represent external entities interacting with the system across various boundary points", context = "involved" }, + ], min = 0, desc = "Actors represent external entities interacting with the system across various boundary points", rel = "involves" }, { dir = "from", targets = [ "UC", - ], min = 1, desc = "System actors must specify the business scenarios (UC) they participate in", context = "involved in" }, + ], min = 1, desc = "System actors must specify the business scenarios (UC) they participate in", rel = "involved_in" }, ] [nodes.DAT] @@ -120,7 +120,7 @@ template = "doc/templates/data.md" rules = [ { dir = "from", targets = [ "*", - ], min = 0, desc = "Data entities provide shared domain models utilized across multiple system components", context = "uses data" }, + ], min = 0, desc = "Data entities provide shared domain models utilized across multiple system components", rel = "uses_data" }, ] [nodes.IF] @@ -129,16 +129,16 @@ template = "doc/templates/interface.md" rules = [ { dir = "from", targets = [ "*", - ], min = 0, desc = "System interfaces define the public contracts for communication and integration points", context = "integrated with" }, + ], min = 0, desc = "System interfaces define the public contracts for communication and integration points", rel = "integrated_with" }, { dir = "from", targets = [ "UC", - ], min = 1, desc = "Exposed interfaces must be justified by at least one business scenario (UC)", context = "justified by" }, + ], min = 1, desc = "Exposed interfaces must be justified by at least one business scenario (UC)", rel = "justified_by" }, { dir = "from", targets = [ "FR", - ], min = 1, desc = "Interfaces must be connected to functional requirements to define their behavior", context = "defined by" }, + ], min = 1, desc = "Interfaces must be connected to functional requirements to define their behavior", rel = "defined_by" }, { dir = "to", targets = [ "MOD", - ], min = 1, desc = "Every interface specification must be implemented by a module (MOD) to ensure its realization", context = "implemented by" }, + ], min = 1, desc = "Every interface specification must be implemented by a module (MOD) to ensure its realization", rel = "realized_by" }, ] [nodes.ADR] @@ -147,5 +147,5 @@ template = "doc/templates/decision.md" rules = [ { dir = "from", targets = [ "*", - ], min = 0, desc = "Architectural decisions provide the design rationale and historical context for system elements", context = "informed by" }, + ], min = 0, desc = "Architectural decisions provide the design rationale and historical context for system elements", rel = "informed_by" }, ] 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/cli/handlers/type_cmd.rs b/src/cli/handlers/type_cmd.rs index 42f948a..8cbc63c 100644 --- a/src/cli/handlers/type_cmd.rs +++ b/src/cli/handlers/type_cmd.rs @@ -70,8 +70,8 @@ fn show_type_details(config: &Config, id: &str) -> ExitCode { .map(|m: usize| format!(" max={}", m)) .unwrap_or_default(); - let desc = match &rule.context { - Some(ctx) => format!("(Context: {}) {}", ctx, rule.desc.as_deref().unwrap_or("")), + let desc = match &rule.rel { + Some(r) => format!("(Rel: {}) {}", r, rule.desc.as_deref().unwrap_or("")), None => rule.desc.as_deref().unwrap_or("").to_string(), }; diff --git a/src/core/config.rs b/src/core/config.rs index 7d2cf17..99be28a 100644 --- a/src/core/config.rs +++ b/src/core/config.rs @@ -38,7 +38,7 @@ pub struct RuleConfig { pub min: Option, pub max: Option, pub desc: Option, - pub context: Option, + pub rel: Option, } impl Config { diff --git a/src/core/engine/mod.rs b/src/core/engine/mod.rs index c7794de..f873733 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, + rel: 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 @@ -69,33 +82,42 @@ pub fn execute_query(query: &ast::Query, nodes: &[SpecBlock]) -> QueryResult { let mut item_projections = Vec::new(); // Store closure or flag to know how to expand each item for item in &query.return_clause.items { - if let Some(ref alias) = item.alias { - expanded_columns.push(alias.clone()); - item_projections.push(Projection::Single(item.expression.clone())); - } else { - match &item.expression { - ast::Expression::Comparison(comp) - if comp.operator.is_none() && comp.right.is_none() => - { - if let Some(ref prop) = comp.left.property { - expanded_columns.push(format!("{}.{}", comp.left.variable, prop)); - item_projections.push(Projection::Single(item.expression.clone())); - } else { - // Expand node variable - let var = &comp.left.variable; - expanded_columns.push(format!("{}.id", var)); - expanded_columns.push(format!("{}.type", var)); - expanded_columns.push(format!("{}.name", var)); - expanded_columns.push(format!("{}.file", var)); - expanded_columns.push(format!("{}.line", var)); - expanded_columns.push(format!("{}.content", var)); - item_projections.push(Projection::Node(var.clone())); + match &item.expression { + ast::Expression::Comparison(comp) + if comp.operator.is_none() + && comp.right.is_none() + && comp.left.property.is_none() + && item.alias.is_none() => + { + // Expand node variable (only if NO alias and NO property) + let var = &comp.left.variable; + expanded_columns.push(format!("{}.id", var)); + expanded_columns.push(format!("{}.type", var)); + expanded_columns.push(format!("{}.name", var)); + expanded_columns.push(format!("{}.file", var)); + expanded_columns.push(format!("{}.line", var)); + expanded_columns.push(format!("{}.content", var)); + item_projections.push(Projection::Node(var.clone())); + } + _ => { + // Single column (with or without alias) + if let Some(ref alias) = item.alias { + expanded_columns.push(alias.clone()); + } else { + match &item.expression { + ast::Expression::Comparison(comp) + if comp.operator.is_none() && comp.right.is_none() => + { + if let Some(ref prop) = comp.left.property { + expanded_columns.push(format!("{}.{}", comp.left.variable, prop)); + } else { + expanded_columns.push(comp.left.variable.clone()); + } + } + _ => expanded_columns.push("expression".to_string()), } } - _ => { - expanded_columns.push("expression".to_string()); - item_projections.push(Projection::Single(item.expression.clone())); - } + item_projections.push(Projection::Single(item.expression.clone())); } } } @@ -106,11 +128,11 @@ 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]; + if let Some(EntityId::Node(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())); @@ -161,14 +183,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 +211,28 @@ 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 rel type + // key: source_idx, value: (target_idx, rel) + 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 rel from docgraph.toml + let rel = find_relationship_rel(config, &node.node_type, &target_node.node_type); + + forward_adj + .entry(idx) + .or_default() + .push((target_idx, rel.clone())); + backward_adj.entry(target_idx).or_default().push((idx, rel)); } } } @@ -205,50 +241,81 @@ 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) { + if let Some(EntityId::Node(start_idx)) = bindings.get(start_node_var) { + let start_idx = *start_idx; // BFS for reachability let mut queue = std::collections::VecDeque::new(); - queue.push_back((start_idx, 0)); + queue.push_back((start_idx, 0, None::)); // (curr, dist, last_rel) let mut visited = std::collections::HashSet::new(); visited.insert((start_idx, 0)); - while let Some((curr, dist)) = queue.pop_front() { + 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 + // 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 { + false + } } else { - end_node_pat.labels.contains(&node.node_type) + true }; - 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 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) + }; + + 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 + && let Some(ref r_var) = rel_pat.variable + && let Some(ref rel_val) = last_rel + { + new_bindings.insert( + r_var.clone(), + EntityId::Relationship { + from_idx: start_idx, + to_idx: curr, + rel: rel_val.clone(), + }, + ); + } + + // Bind end variable + if let Some(ref var) = end_node_pat.variable { + if let Some(EntityId::Node(prev_idx)) = bindings.get(var) { + if *prev_idx == curr { + next_bindings.push(new_bindings); + } + } else { + new_bindings.insert(var.clone(), EntityId::Node(curr)); + next_bindings.push(new_bindings); } } else { - let mut new_bindings = bindings.clone(); - new_bindings.insert(var.clone(), curr); next_bindings.push(new_bindings); } - } else { - next_bindings.push(bindings.clone()); } } } - // Continue traversal based on direction + // 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) { @@ -270,10 +337,10 @@ fn match_relationship_pattern( } } - 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 +351,45 @@ fn match_relationship_pattern( next_bindings } -fn evaluate_expression(expr: &ast::Expression, bindings: &Bindings, nodes: &[SpecBlock]) -> bool { +fn find_relationship_rel(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()) + && let Some(ref rel_val) = rule.rel + { + return rel_val.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()) + && let Some(ref rel_val) = rule.rel + { + return rel_val.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 +397,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 +425,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 +443,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 { rel, .. } => { + if let Some(ref prop) = pv.property { + match prop.as_str() { + "type" => rel.clone(), + _ => "null".to_string(), + } + } else { + rel.clone() + } } - } else { - node.id.clone() } } else { "null".to_string() @@ -416,7 +527,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 +538,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 +547,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 +558,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 +568,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 +580,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 +591,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 +604,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 +612,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 +623,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 +638,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 +646,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 +660,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 +678,127 @@ mod tests { assert_eq!(result.rows[0][1], "UC"); assert_eq!(result.rows[0][2], "User Login"); } + + #[test] + fn test_execute_relationship_type_and_rel() { + 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()], + rel: 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()], + rel: 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); + } + + #[test] + fn test_execute_alias_and_ops() { + let nodes = mock_nodes(); + let config = Config::default(); + + // Testing Alias and Comparison Ops + let q = crate::core::parser::parse_query( + "MATCH (n:UC) RETURN n.id AS identifier, n.name AS name", + ) + .unwrap(); + let result = execute_query(&q, &nodes, &config); + assert_eq!(result.columns[0], "identifier"); + assert_eq!(result.columns[1], "name"); + + // Comparison ops (Number literal - though our values are strings usually) + let q = + crate::core::parser::parse_query("MATCH (n) WHERE n.id < \"ZZ\" RETURN n.id").unwrap(); + let result = execute_query(&q, &nodes, &config); + assert!(!result.rows.is_empty()); + } + + #[test] + fn test_execute_reverse_and_both_directions() { + let nodes = mock_nodes(); + let mut config = Config::default(); + use crate::core::config::{NodeConfig, RuleConfig}; + + // UC -> FR + let mut uc_rules = NodeConfig::default(); + uc_rules.rules.push(RuleConfig { + dir: "to".to_string(), + targets: vec!["FR".to_string()], + rel: Some("uses".to_string()), + ..Default::default() + }); + config.nodes.insert("UC".to_string(), uc_rules); + + // Reverse matching: (f:FR)<-[]-(u:UC) + let q = crate::core::parser::parse_query("MATCH (f:FR)<-[r]-(u:UC) RETURN r.type").unwrap(); + let result = execute_query(&q, &nodes, &config); + assert_eq!(result.rows.len(), 1); + assert_eq!(result.rows[0][0], "uses"); + + // Undirected matching: (u:UC)-[]-(f:FR) + let q = crate::core::parser::parse_query("MATCH (u:UC)-[r]-(f:FR) RETURN r.type").unwrap(); + let result = execute_query(&q, &nodes, &config); + assert_eq!(result.rows.len(), 1); + assert_eq!(result.rows[0][0], "uses"); + + // Undirected from other side + let q = crate::core::parser::parse_query("MATCH (f:FR)-[r]-(u:UC) RETURN r.type").unwrap(); + let result = execute_query(&q, &nodes, &config); + assert_eq!(result.rows.len(), 1); + assert_eq!(result.rows[0][0], "uses"); + } + + #[test] + fn test_execute_match_mismatch() { + let nodes = mock_nodes(); + let config = Config::default(); + + // Variable already bound to different node + let q = crate::core::parser::parse_query("MATCH (n:UC), (m:FR) WHERE n = m RETURN n.id") + .unwrap(); + let result = execute_query(&q, &nodes, &config); + assert_eq!(result.rows.len(), 0); + + // End node label mismatch in relationship + let q = crate::core::parser::parse_query("MATCH (u:UC)-[]->(f:MOD) RETURN u.id").unwrap(); + let result = execute_query(&q, &nodes, &config); + assert_eq!(result.rows.len(), 0); + } } diff --git a/src/core/parser/cypher.pest b/src/core/parser/cypher.pest index ca4104a..dcbbe7d 100644 --- a/src/core/parser/cypher.pest +++ b/src/core/parser/cypher.pest @@ -75,7 +75,7 @@ where_clause = { expression } // Supports aliasing with AS. // return_clause = { return_item ~ ("," ~ SP? ~ return_item)* } -return_item = { expression ~ (SP ~ AS ~ SP ~ variable)? } +return_item = { expression ~ (AS ~ variable)? } // // Expressions diff --git a/src/core/parser/mod.rs b/src/core/parser/mod.rs index 81dbc4c..530920a 100644 --- a/src/core/parser/mod.rs +++ b/src/core/parser/mod.rs @@ -157,7 +157,14 @@ fn parse_return_item(pair: Pair) -> Result { let expr_pair = inner.next().unwrap(); let expression = parse_expression(expr_pair)?; - let alias = inner.next().map(|p| p.as_str().to_string()); + let mut alias = None; + for p in inner { + match p.as_rule() { + Rule::variable => alias = Some(p.as_str().to_string()), + Rule::AS => {} // Skip AS keyword + _ => {} + } + } Ok(ast::ReturnItem { expression, alias }) } @@ -328,4 +335,12 @@ mod tests { let parsed = parse_query(q).unwrap(); assert!(parsed.where_clause.is_some()); } + + #[test] + fn test_parse_alias() { + let q = "MATCH (n) RETURN n.id AS identifier"; + let parsed = parse_query(q).unwrap(); + let item = &parsed.return_clause.items[0]; + assert_eq!(item.alias, Some("identifier".to_string())); + } } diff --git a/src/core/rules/dg006.rs b/src/core/rules/dg006.rs index 44cc73a..13de5e1 100644 --- a/src/core/rules/dg006.rs +++ b/src/core/rules/dg006.rs @@ -53,7 +53,7 @@ pub fn check_relationships(config: &Config, blocks: &[SpecBlock]) -> Vec Vec max { - let label = rule.context.as_deref().unwrap_or("be referenced by"); + let label = rule.rel.as_deref().unwrap_or("be referenced by"); let mut message = format!( "LIMIT EXCEEDED: Node '{}' (type {}) can {} at most {} {}. (Found {})", block.id, @@ -137,7 +137,7 @@ pub fn check_relationships(config: &Config, blocks: &[SpecBlock]) -> Vec Vec max { - let label = rule.context.as_deref().unwrap_or("have relation to"); + let label = rule.rel.as_deref().unwrap_or("have relation to"); let mut message = format!( "LIMIT EXCEEDED: Node '{}' (type {}) can {} at most {} {}. (Found {})", block.id, @@ -223,7 +223,7 @@ pub fn check_relationships(config: &Config, blocks: &[SpecBlock]) -> Vec Vec Vec(&body_buf), ) { - // In a real async test harness, we might block here if channel full, - // but for tests it is usually fine. let _ = reader_tx.blocking_send(val); } } else { // EOF or broken pipe - // break; // For robustness, we might want to break, but let's keep retrying or just exit? - // If read_line returns 0, it is EOF. - // The above loop condition handles `> 0`. If it's 0, we exit outer loop? - // Wait, verify logic. - // If buffer is empty at start of loop body (after read_line), we check return value. - // Actually let's refine this loop slightly effectively. break; } } @@ -96,43 +88,9 @@ impl LspClient { }); self.write(req)?; - // Wait for response with matching ID - // Note: This simple implementation effectively blocks other notifications while waiting for response. - // For simple sequential tests, this is fine. For complex interleaved tests, need a better loop. - // BUT, given the "helper method" request, we probably want to queue notifications separately? - // The implementation below consumes messages. If it's a notification, we might lose it if we don't store it. - // Wait... user suggestion said: "LspClient は前の骨格(Content-Lengthフレーミング+pending response+notif queue)でOK" - // I should probably start a background tokio task to route messages, but `send_request` needs to be easy. - - // Let's implement a loop here that buffers notifications if they are not the response. - // Wait, self.receiver only has one consumer (this test code). - // Since we are inside `send_request`, we can loop `self.receiver`, if it is a notification, we buffer it (where?), if response matches, return. - // BUT `buffer` ownership is tricky if we are mutable. - - // SIMPLIFICATION: - // We will assume that `send_request` waits for response. - // If we receive a notification while waiting, we can put it into an internal queue? - // Ah, `self` is `&mut`. - - // Actually, for the tests, we often "wait for notification" OR "send request". - // But if `publishDiagnostics` arrives exactly while we await `hover` response, we must not drop it. - // So we need a shared `notification_queue`. - - // Refactoring: - // We really want `next_response(id)` and `next_notification()`. - // To make this robust without rigorous background tasks in the Test struct itself (which is async): - // We can just Peek? No mpsc is not peekable. - - // Let's rely on the simple pattern: - // Reads from `receiver` loop. - // If msg.id == id -> Return. - // If msg has no id (notification) -> Push to internal `pending_notifications`. - // If msg has other id -> Panic (or buffer? usually means error in test logic). - + // Read messages until we get a response with the matching ID. + // Notifications arriving in the meantime are buffered in `pending_notifications`. loop { - // Check pending notifications first? No, we are looking for response. - - // Read from channel let msg = match timeout(Duration::from_secs(5), self.receiver.recv()).await { Ok(Some(m)) => m, Ok(None) => anyhow::bail!("Channel closed"), @@ -150,11 +108,7 @@ impl LspClient { ); } } else { - // It is a notification (no id) - // We must store it so `wait_notification` can find it. - // But `self` is borrowed mutably here. - // We can't easily push to a field if we don't have one or if strict borrowck. - // Let's add `pending_notifications` field to struct. + // Buffer notifications so `wait_notification` can retrieve them later. self.pending_notifications.push(msg); } } @@ -189,9 +143,8 @@ impl LspClient { if msg.get("id").is_none() { Some(msg) } else { - // Unexpected response? eprintln!("Unexpected response in next_notification: {:?}", msg); - None // or Loop? + None } } _ => None, @@ -223,9 +176,9 @@ impl LspClient { match timeout(tick, self.receiver.recv()).await { Ok(Some(msg)) => { if msg.get("id").is_some() { - // Uh oh, response arriving uninvited. Buffer it? - // Implementation simplified: we won't handle uninvited responses here well. - eprintln!("Warning: Received uninvited response"); + eprintln!( + "Warning: received unexpected response while waiting for notification" + ); continue; } @@ -246,5 +199,32 @@ impl LspClient { } } -// Add the field -// We need to re-declare struct with all fields +impl Drop for LspClient { + fn drop(&mut self) { + // stdin is closed automatically when dropped, signaling EOF to the server. + + // Try to wait gracefully for a short time + let start = std::time::Instant::now(); + while start.elapsed() < std::time::Duration::from_millis(100) { + if let Ok(Some(_)) = self.child.try_wait() { + // Process has exited + if let Some(handle) = self.reader_thread.take() { + let _ = handle.join(); + } + return; + } + std::thread::sleep(std::time::Duration::from_millis(10)); + } + + // If still running, kill it forcefully + if let Ok(None) = self.child.try_wait() { + let _ = self.child.kill(); + let _ = self.child.wait(); + } + + // Join the reader thread (don't block indefinitely during test cleanup) + if let Some(handle) = self.reader_thread.take() { + let _ = handle.join(); + } + } +}