-
-
Notifications
You must be signed in to change notification settings - Fork 0
Implement Kiro spec development workflow #5
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Implement Kiro spec development workflow #5
Conversation
Implement Task 1 from the document-outliner-with-lsp Kiro spec. Created: - Directory structure for VS Code extension (extensions/doctk-outliner/) - Directory structure for language server (src/doctk/lsp/) - TypeScript interfaces for tree nodes and operations (types.ts) - Python protocols for document operations (protocols.py) - Build configuration files (tsconfig.json, package.json) - Language configuration for doctk DSL This establishes the foundational structure for the outliner extension and LSP server, satisfying Requirements 15 and 20. Task: .kiro/specs/document-outliner-with-lsp/tasks.md#1 Requirements: 15, 20
Implement Task 2 from the document-outliner-with-lsp Kiro spec. Created comprehensive document structure operations: - StructureOperations class with all operations - DocumentTreeBuilder for node mapping and lookup - promote() and demote() to adjust heading levels (1-6) - move_up() and move_down() to reorder siblings - nest() to move sections under new parents - unnest() to move sections up one level - Validation methods for all operations Key features: - Immutable operations (returns new Document instances) - Node ID-based addressing (h1-0, h2-1, etc.) - Edge case handling (level limits, boundary checks) - Error handling with descriptive messages Testing: - 34 comprehensive unit tests - Test coverage: 77.78% for operations.py - Tests for all operations, edge cases, and immutability - Complex scenario tests with mixed document types All tests pass successfully. Task: .kiro/specs/document-outliner-with-lsp/tasks.md#2 Requirements: 2.2, 2.3, 3.2, 3.3, 3.4, 3.5, 20
Implement Task 3 from the document-outliner-with-lsp Kiro spec. Python side (JSON-RPC bridge): - ExtensionBridge class with JSON-RPC 2.0 protocol - Handles all document operations via stdin/stdout - Method routing for promote, demote, move_up, move_down, nest, unnest - Validation methods for all operations - Comprehensive error handling and serialization - Main entry point for standalone execution TypeScript side (PythonBridge client): - PythonBridge class that spawns Python process via uv - JSON-RPC request/response handling over stdio - Promise-based async API for all operations - Process lifecycle management (start, stop, restart) - Automatic restart on crash (up to 3 attempts) - Request timeout handling (default 10s) - Buffered line-based communication - Convenience methods for all operations Testing: - 18 comprehensive integration tests - Test coverage: 67.96% for bridge.py - Tests for all operations (promote, demote, move_up, move_down, nest, unnest) - Tests for validation operations - Error handling tests (invalid JSON-RPC, unknown methods, missing params) - Complex scenario tests (operation chains, roundtrips, reordering) - Error recovery tests Key features: - Standard JSON-RPC 2.0 protocol - Clean separation of concerns - Type-safe TypeScript interface - Robust error handling - Automatic process management All 18 tests pass successfully. Task: .kiro/specs/document-outliner-with-lsp/tasks.md#3 Requirements: 18, 20
Summary of ChangesHello @tommcd, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed! This pull request lays the groundwork for a Highlights
Using Gemini Code AssistThe full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips. Invoking Gemini You can request assistance from Gemini at any point by creating a comment using either
Customization To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a Limitations & Feedback Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here. You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension. Footnotes
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Code Review
This pull request introduces the foundational components for a VS Code document outliner extension, including the TypeScript-based extension frontend and a Python-based JSON-RPC backend for document manipulation. The changes are well-structured, establishing a clear separation between the extension UI logic and the core document operations. My review focuses on improving correctness, performance, and maintainability. I've identified a critical bug in the document manipulation logic that could lead to data corruption, as well as opportunities to improve type safety in the TypeScript code, enhance robustness in the process management, and optimize the communication protocol for better performance.
| def move_up(document: Document[Node], node_id: str) -> tuple[Document[Node], OperationResult]: | ||
| """ | ||
| Move a node up in the sibling order. | ||
|
|
||
| Args: | ||
| document: The document to operate on | ||
| node_id: The ID of the node to move up | ||
|
|
||
| Returns: | ||
| Tuple of (modified document, operation result) | ||
| """ | ||
| from doctk.lsp.protocols import OperationResult | ||
|
|
||
| tree_builder = DocumentTreeBuilder(document) | ||
| node = tree_builder.find_node(node_id) | ||
|
|
||
| if node is None: | ||
| return document, OperationResult( | ||
| success=False, error=f"Node not found: {node_id}" | ||
| ) | ||
|
|
||
| if not isinstance(node, Heading): | ||
| return document, OperationResult( | ||
| success=False, error=f"Node {node_id} is not a heading" | ||
| ) | ||
|
|
||
| # Get the index of the node | ||
| node_index = tree_builder.get_node_index(node_id) | ||
| if node_index is None: | ||
| return document, OperationResult( | ||
| success=False, error=f"Could not find index for node: {node_id}" | ||
| ) | ||
|
|
||
| # Check if already at the top (first sibling of its level) | ||
| if node_index == 0: | ||
| return document, OperationResult( | ||
| success=True, | ||
| document=document.to_string(), | ||
| error=None, | ||
| ) | ||
|
|
||
| # Find the previous sibling (same level or higher) | ||
| prev_index = node_index - 1 | ||
| while prev_index >= 0: | ||
| prev_node = document.nodes[prev_index] | ||
| if isinstance(prev_node, Heading): | ||
| # Found a heading - check if it's a valid swap target | ||
| if prev_node.level <= node.level: | ||
| break | ||
| prev_index -= 1 | ||
|
|
||
| # If we can't find a valid previous sibling, stay in place | ||
| if prev_index < 0: | ||
| return document, OperationResult( | ||
| success=True, | ||
| document=document.to_string(), | ||
| error=None, | ||
| ) | ||
|
|
||
| # Swap the nodes | ||
| new_nodes = list(document.nodes) | ||
| new_nodes[prev_index], new_nodes[node_index] = ( | ||
| new_nodes[node_index], | ||
| new_nodes[prev_index], | ||
| ) | ||
| new_document = Document(new_nodes) | ||
|
|
||
| return new_document, OperationResult( | ||
| success=True, document=new_document.to_string() | ||
| ) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The move_up operation is fundamentally flawed. It only swaps the Heading node with a previous Heading node in the flat list of document nodes. It does not move any of the content (e.g., Paragraphs or sub-headings) that belong to that section. This will lead to document corruption, where content ends up under the wrong heading.
For example, if you move up "Section 2" in a document like:
# Section 1
Some text for section 1.
# Section 2
Some text for section 2.The result will be:
# Section 2
Some text for section 1.
# Section 1
Some text for section 2.The content is now mismatched with its heading.
To fix this, the operation needs to identify the entire block of nodes that constitutes a section (the heading and all nodes until the next heading of the same or lower level) and move that entire block.
This same critical issue exists in move_down and nest operations. The unit tests for these operations are insufficient as they only use documents containing headings, which hides this bug.
| private async waitForReady(): Promise<void> { | ||
| // For now, just wait a bit for the process to start | ||
| // In the future, we could send a ping request to verify | ||
| return new Promise((resolve) => setTimeout(resolve, 100)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The waitForReady method uses a fixed setTimeout of 100ms to wait for the Python process to initialize. This can lead to a race condition where requests are sent before the Python bridge is ready, especially on slower systems or under heavy load. This can cause initial operations to fail unpredictably.
A more robust approach would be to implement a handshake mechanism. The Python process could print a specific message (e.g., "BRIDGE_READY") to stdout once it's fully initialized and ready to accept requests. The waitForReady method in TypeScript should then listen on stdout for this message before resolving the start promise. This would eliminate the race condition. The comment on line 349 correctly identifies this as a point for future improvement, and it's important enough to address for reliability.
| async promote(document: string, nodeId: string): Promise<any> { | ||
| return this.call('promote', { document, node_id: nodeId }); | ||
| } | ||
|
|
||
| /** | ||
| * Execute a demote operation. | ||
| */ | ||
| async demote(document: string, nodeId: string): Promise<any> { | ||
| return this.call('demote', { document, node_id: nodeId }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The operation wrapper methods like promote and demote currently return Promise<any>, which bypasses TypeScript's type safety benefits. The types.ts file already defines a useful OperationResult interface that should be used here.
To improve type safety and maintainability, you should type the return values of these methods. This can be achieved by making the call method generic and using specific types for each operation.
For example, you could modify call to be async call<T>(...) and then use it like this:
import { OperationResult } from './types';
// ...
async promote(document: string, nodeId: string): Promise<OperationResult> {
return this.call<OperationResult>('promote', { document, node_id: nodeId });
}This change should be applied to demote, moveUp, moveDown, nest, and unnest. Similarly, you could create a ValidationResult interface in types.ts for the validate* methods.
| keybindings: { | ||
| /** Keybinding for promote operation */ | ||
| promote: string; | ||
| /** Keybinding for demote operation */ | ||
| demote: string; | ||
| /** Keybinding for move up operation */ | ||
| moveUp: string; | ||
| /** Keybinding for move down operation */ | ||
| moveDown: string; | ||
| /** Keybinding for delete operation */ | ||
| delete: string; | ||
| }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The DoctkConfiguration interface includes a keybindings property. This seems to imply that keybindings are configurable by the user through settings, similar to the outliner, lsp, and performance properties. However, keybindings in package.json are contributions, not user-configurable settings that are accessed via vscode.workspace.getConfiguration. This can be misleading and suggests incorrect usage. If this type is intended to be populated from vscode.workspace.getConfiguration('doctk'), the keybindings property will not be present. It should be removed to accurately reflect the configurable settings.
| except Exception as e: | ||
| return self._error_response(request_id, -32603, f"Internal error: {str(e)}") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The _execute_method catches a broad Exception and returns a generic "Internal error" message to the client. While this prevents the bridge from crashing, it hides the root cause of the error from the developer, making debugging difficult.
You should log the full traceback to stderr so that it can be seen in the extension's logs.
| except Exception as e: | |
| return self._error_response(request_id, -32603, f"Internal error: {str(e)}") | |
| except Exception as e: | |
| import traceback | |
| traceback.print_exc(file=sys.stderr) | |
| return self._error_response(request_id, -32603, f"Internal error: {str(e)}") |
| if not document_text or not node_id: | ||
| raise ValueError("Missing required parameters: document, node_id") | ||
|
|
||
| doc = Document.from_string(document_text) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In _handle_promote and all other operation handlers, the entire document text is received from the client and parsed into a Document object using Document.from_string(). This happens for every single operation. For large documents, this repeated serialization, transfer, and parsing can be very inefficient and lead to noticeable delays in the UI.
A more performant approach would be to make the Python bridge stateful. The document could be sent once and kept in memory on the Python side. The client would then send incremental changes or operations, and the Python side would update its in-memory representation of the document. This is the standard model for Language Servers.
While the current approach is simpler, given that performance settings are part of the extension's configuration, this is a significant optimization to consider for improving user experience with large files.
| def promote(document: Document[Node], node_id: str) -> tuple[Document[Node], OperationResult]: | ||
| """ | ||
| Decrease heading level by one (e.g., h3 -> h2). | ||
|
|
||
| Args: | ||
| document: The document to operate on | ||
| node_id: The ID of the node to promote | ||
|
|
||
| Returns: | ||
| Tuple of (modified document, operation result) | ||
| """ | ||
| from doctk.lsp.protocols import OperationResult | ||
|
|
||
| tree_builder = DocumentTreeBuilder(document) | ||
| node = tree_builder.find_node(node_id) | ||
|
|
||
| if node is None: | ||
| return document, OperationResult( | ||
| success=False, error=f"Node not found: {node_id}" | ||
| ) | ||
|
|
||
| if not isinstance(node, Heading): | ||
| return document, OperationResult( | ||
| success=False, error=f"Node {node_id} is not a heading" | ||
| ) | ||
|
|
||
| # Validate: already at minimum level? | ||
| if node.level <= 1: | ||
| return document, OperationResult( | ||
| success=True, | ||
| document=document.to_string(), | ||
| error=None, | ||
| ) | ||
|
|
||
| # Get the index of the node | ||
| node_index = tree_builder.get_node_index(node_id) | ||
| if node_index is None: | ||
| return document, OperationResult( | ||
| success=False, error=f"Could not find index for node: {node_id}" | ||
| ) | ||
|
|
||
| # Create new promoted node | ||
| promoted_node = node.promote() | ||
|
|
||
| # Create new document with updated node | ||
| new_nodes = list(document.nodes) | ||
| new_nodes[node_index] = promoted_node | ||
| new_document = Document(new_nodes) | ||
|
|
||
| return new_document, OperationResult( | ||
| success=True, document=new_document.to_string() | ||
| ) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The promote method (and other operations like demote, move_up, etc.) returns a tuple tuple[Document[Node], OperationResult]. However, the OperationResult object already contains the string representation of the modified document. The ExtensionBridge correctly ignores the Document[Node] part of the tuple.
This is redundant and makes the method signature confusing. The method should be simplified to return only OperationResult. This change should be applied to all similar structural operations (demote, move_up, etc.).
def promote(document: Document[Node], node_id: str) -> OperationResult:
"""
Decrease heading level by one (e.g., h3 -> h2).
Args:
document: The document to operate on
node_id: The ID of the node to promote
Returns:
Operation result
"""
from doctk.lsp.protocols import OperationResult
tree_builder = DocumentTreeBuilder(document)
node = tree_builder.find_node(node_id)
if node is None:
return OperationResult(success=False, error=f"Node not found: {node_id}")
if not isinstance(node, Heading):
return OperationResult(
success=False, error=f"Node {node_id} is not a heading"
)
# Validate: already at minimum level?
if node.level <= 1:
return OperationResult(
success=True,
document=document.to_string(),
error=None,
)
# Get the index of the node
node_index = tree_builder.get_node_index(node_id)
if node_index is None:
return OperationResult(
success=False, error=f"Could not find index for node: {node_id}"
)
# Create new promoted node
promoted_node = node.promote()
# Create new document with updated node
new_nodes = list(document.nodes)
new_nodes[node_index] = promoted_node
new_document = Document(new_nodes)
return OperationResult(success=True, document=new_document.to_string())| Returns: | ||
| Tuple of (modified document, operation result) | ||
| """ | ||
| from doctk.lsp.protocols import OperationResult |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The import from doctk.lsp.protocols import OperationResult is done locally inside the promote method, and similarly in other methods in this class. This is generally discouraged as it can hide dependencies and affect readability.
These imports should be moved to the top of the file. If this was done to avoid a circular import, it might be better to refactor the modules to break the cycle. Based on the current structure, it doesn't seem like a top-level import would cause a cycle.
Implement Task 4 from the document-outliner-with-lsp Kiro spec. VS Code Extension Components: 1. DocumentOutlineProvider (outlineProvider.ts - 230 lines): - Implements TreeDataProvider<OutlineNode> interface - Parses Markdown documents to extract heading hierarchy - Builds OutlineNode tree with unique IDs (h1-0, h2-1, etc.) - Tracks node ranges for navigation - Provides getTreeItem(), getChildren(), getParent() methods - Icons based on heading level (h1-h6) - Tooltips with node metadata - Debounced refresh mechanism (300ms) - Document update synchronization 2. Extension Entry Point (extension.ts - 210 lines): - Activates extension for Markdown files - Registers tree view in Explorer sidebar - Initializes PythonBridge for backend operations - Registers all commands (promote, demote, move_up, move_down, etc.) - Navigate to node command for clicking tree items - Listens for editor changes and document updates - Executes operations via Python bridge - Applies workspace edits with undo support - Error handling and user notifications Testing (test_outline_integration.py - 400+ lines): - 17 comprehensive integration tests - All tests passing Test categories: - Tree building from various structures (simple, nested, complex) - Node ID generation and uniqueness - Mixed content with paragraphs - Markdown parsing integration - Operation integration (promote, demote, move) - Edge cases (empty docs, skipped levels, reverse order) Key features: - ✅ Complete TreeDataProvider implementation - ✅ Markdown parsing with heading extraction - ✅ Hierarchical tree structure - ✅ Unique node ID generation - ✅ Range tracking for navigation - ✅ Debounced refresh (300ms) - ✅ Document synchronization - ✅ Command integration - ✅ Python bridge integration - ✅ Workspace edit support - ✅ Comprehensive testing All 17 tests pass successfully. Task: .kiro/specs/document-outliner-with-lsp/tasks.md#4 Requirements: 1.1, 1.2, 1.3, 1.4, 16.1, 16.2, 16.3
This commit addresses all issues identified in the Gemini Code Assist review of PR #5 (#5 (review)). CRITICAL FIXES: - Fix move_up/move_down/nest operations to move entire sections (heading + content) instead of just heading nodes. Added get_section_range() helper method to DocumentTreeBuilder to identify complete sections. This prevents document corruption where content would end up under the wrong heading. (operations.py:70-100, 293-450) HIGH PRIORITY FIXES: - Implement handshake mechanism in waitForReady to avoid race conditions. Python bridge now prints "BRIDGE_READY" when initialized, and TypeScript waits for this signal before resolving the start promise. (bridge.py:313, pythonBridge.ts:348-366) MEDIUM PRIORITY FIXES: - Add proper TypeScript type annotations for operation methods. Created ValidationResult interface and updated all operation/validation methods to use OperationResult/ ValidationResult instead of Promise<any>. Made call() method generic. (types.ts:89-97, pythonBridge.ts:7, 143, 183-235) - Remove keybindings from DoctkConfiguration interface. Keybindings are contributions in package.json, not user-configurable settings from workspace.getConfiguration(). (types.ts:104-115) - Add traceback logging to error handling in bridge.py for easier debugging. Full traceback is now printed to stderr when exceptions occur. (bridge.py:7, 61) - Simplify operation method signatures to return only OperationResult instead of redundant tuple[Document[Node], OperationResult]. Updated all callers in bridge.py to handle single return value. (operations.py:105, 157, 257, 348, 489, 589; bridge.py:107-173) - Move local imports to top of file in operations.py. Converted TYPE_CHECKING imports to regular imports for better readability and to avoid hiding dependencies. (operations.py:3-8) TEST UPDATES: - Updated all LSP operation tests to handle new return signature (single OperationResult instead of tuple). All 34 tests now pass. (test_lsp_operations.py) Verified with: uv run pytest tests/unit/test_lsp_operations.py -v Result: 34 passed in 1.49s
- Remove unused imports (Optional, Any, Node, json, pytest, Document, Heading) - Update type annotations to use X | None instead of Optional[X] - Remove quotes from type annotations (leverage __future__ annotations) - Fix ambiguous variable name 'l' -> 'line' in test_bridge.py - Remove unused 'builder' variables in test_outline_integration.py All tests still passing (69/69).
Add detailed design and implementation plan for 2 remaining PR #6 review issues: 1. CRITICAL: Granular document edits (Issue #1) - Added 'Optimized Edits (CRITICAL IMPROVEMENT)' section to design.md - Explains problem: full document replacement clears undo/redo, loses cursor - Solution: Backend computes ModifiedRange[], frontend applies granular edits - Added 5 subtasks (22.5.1-22.5.5) to tasks.md with implementation steps 2. MEDIUM: Centralize ID generation (Issue #5) - Added 'Centralized Node ID Generation' section to design.md - Explains problem: Duplicate ID logic in frontend and backend - Solution: Backend as single source of truth for tree structure & IDs - Added 4 subtasks (22.6.1-22.6.4) to tasks.md with implementation steps Both sections include: - Problem statement and impact - Proposed solution with code examples - Design rationale - Backend and frontend changes - Testing requirements This provides clear roadmap for addressing the remaining architectural issues identified in #6 (review)
Implement Task 9 from vscode-outliner-extension spec to centralize
node ID generation in the backend, ensuring consistency across all
operations and eliminating duplicate ID generation logic.
Changes:
- Added getDocumentTree() method to PythonBridge TypeScript class to
request tree structure with backend-assigned IDs via JSON-RPC
- Added BackendTreeNode and DocumentTreeResponse type definitions to
support backend tree serialization format
- Updated DocumentOutlineProvider to use backend tree when available:
- Added pythonBridge constructor parameter
- Implemented deserializeBackendTree() method to convert backend
TreeNode structure to frontend OutlineNode format
- Modified updateFromDocument() to call backend getDocumentTree()
with fallback to local parsing if backend unavailable
- Updated extension.ts to initialize DocumentOutlineProvider with
pythonBridge for centralized ID generation
- Kept local parseDocument() as robust fallback when backend offline
- Fixed Python code formatting in dsl/executor.py and dsl/lexer.py
(pre-existing formatting issues found during quality checks)
Task completion:
- Task 9.1: Frontend now requests tree from backend ✓
- Task 9.2: Backend tests already comprehensive (25 tests) ✓
- Task 8.1: Granular edits already implemented ✓
Tests:
- All 168 Python tests pass (2 skipped)
- Backend has 13 tests for build_tree_with_ids
- Backend has 12 tests for get_document_tree RPC method
- Ruff linting and formatting checks pass
This addresses PR #6 Issue #5 (Medium priority) and ensures node IDs
are generated consistently by the backend, preventing ID mismatches
between tree view and operations.
**Problem:** Document._build_id_index() only indexed top-level nodes, causing find_node() to fail for nested structures like ListItems within Lists and Paragraphs within BlockQuotes. This violated Task 1.5's O(1) lookup requirement for all nodes in the tree. **Solution:** - Added _index_node_recursive() method to traverse entire document tree - Recursively indexes nodes in Heading.children, List.items, ListItem.content, BlockQuote.content - Updated _build_id_index() docstring to clarify all nodes in tree - Updated find_node() docstring with nested node examples **Tests Added:** - 10 comprehensive tests in test_document_indexing.py - Tests cover top-level, nested ListItems, BlockQuote content, Heading children, deeply nested structures **Validation:** - All 81 tests passing (71 original + 10 new) - Coverage for core.py improved from 58.54% to 66.91% - Ruff compliant Fixes: Finding #5 from code review Validates: Task 1.5 - Document ID Indexing
No description provided.