From fc09300368e723b6873253148674ad35548d6a7b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 9 Oct 2025 23:50:17 +0000 Subject: [PATCH 01/11] Initial plan From cba77c53aff285402d1beb4e2643312ad3904a80 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 9 Oct 2025 23:59:31 +0000 Subject: [PATCH 02/11] Add comprehensive MCP component research and recommendation Co-authored-by: danmarshall <11507384+danmarshall@users.noreply.github.com> --- docs/research/README.md | 93 +++++ docs/research/mcp-component-analysis.md | 490 ++++++++++++++++++++++++ 2 files changed, 583 insertions(+) create mode 100644 docs/research/README.md create mode 100644 docs/research/mcp-component-analysis.md diff --git a/docs/research/README.md b/docs/research/README.md new file mode 100644 index 00000000..52d6d291 --- /dev/null +++ b/docs/research/README.md @@ -0,0 +1,93 @@ +# MCP Component Research - Quick Summary + +**Research Date:** October 2025 +**Full Report:** [mcp-component-analysis.md](./mcp-component-analysis.md) + +## Question + +Should Chartifact implement a Model Context Protocol (MCP) component for creating and editing interactive documents? + +## Answer + +**NO - Do not implement MCP component at this time.** + +## Key Findings + +### 1. Documents Fit Entirely in LLM Context +- Largest example document: 741 lines (~5,000 tokens) +- Modern LLMs have 100K+ token context windows +- No need for partial document access via MCP + +### 2. JSON Format is Already LLM-Friendly +- Well-structured, predictable schema +- LLMs can read, understand, and edit directly +- No abstraction layer needed + +### 3. Current Editor is Incomplete +- Editor sends full documents (not deltas as stated in problem) +- No transaction system exists yet +- Should complete basic editor before adding MCP + +### 4. Implementation Cost is High +- **Estimated effort:** 5-7 weeks development +- **Code size:** 2,500-4,000 lines + tests + docs +- **Maintenance:** Ongoing as schema evolves + +### 5. Benefits are Marginal +- ❌ Atomic operations: Nice but unnecessary for single-user editing +- ❌ Partial access: Documents fit in context +- ❌ Transactions: Not a current requirement +- ❌ Conflict resolution: No collaborative editing yet +- ❌ External tools: None exist that need integration + +## Use Case Comparison + +| Task | Without MCP | With MCP | Winner | +|------|-------------|----------|--------| +| Create document | 1 step (generate JSON) | 10+ tool calls | Without MCP | +| Edit document | Read + edit + write | Read + multiple calls | Without MCP | +| Multi-step edit | Apply all changes at once | Multiple round-trips | Without MCP | +| Collaborative edit | Last write wins | Conflict detection | With MCP* | + +\* Not a current requirement + +## Recommendation + +### Instead of MCP, Do This: + +1. **Document JSON editing patterns** - Guide LLMs on how to edit documents effectively +2. **Complete the editor** - Finish basic functionality first +3. **Improve examples** - Add more examples showing document patterns +4. **Wait for pain points** - Implement MCP only if real needs emerge + +### When to Reconsider MCP + +Implement MCP if: +- ✅ Documents regularly exceed 50K tokens (unlikely) +- ✅ Users need collaborative editing (possible future) +- ✅ 3+ external tools need document manipulation (unlikely) +- ✅ Editor implements transaction system (natural evolution) + +## Phased Approach + +**Phase 1 (Now):** Direct JSON editing ✅ Working well + +**Phase 2 (If needed):** Create lightweight transaction library +- Shared between editor and tools +- No MCP protocol yet +- ~200-300 lines of code + +**Phase 3 (If needed):** Wrap transaction library in MCP +- Only if external tools need integration +- Or collaborative editing becomes priority + +## Conclusion + +**Costs outweigh benefits.** The JSON format is sufficient for LLM-based document creation and editing. MCP would add unnecessary complexity (2,500-4,000 lines) before core features are complete. Focus should remain on finishing the editor and improving documentation. + +MCP can be added later if genuine needs emerge. + +--- + +**Status:** Research complete, recommendation ready for review +**See:** [Full detailed analysis](./mcp-component-analysis.md) diff --git a/docs/research/mcp-component-analysis.md b/docs/research/mcp-component-analysis.md new file mode 100644 index 00000000..004058c4 --- /dev/null +++ b/docs/research/mcp-component-analysis.md @@ -0,0 +1,490 @@ +# MCP Component Research Report for Chartifact + +**Date:** October 2025 +**Author:** Research Analysis +**Status:** Recommendation - Do Not Implement + +## Executive Summary + +This report evaluates whether Chartifact should implement a Model Context Protocol (MCP) component for creating and editing interactive documents. After thorough analysis of the codebase, document sizes, current editing mechanisms, and implementation requirements, **we recommend NOT implementing an MCP component at this time**. + +The JSON format is already well-suited for LLM-based editing, documents are small enough to fit entirely in modern LLM context windows, and the implementation overhead (estimated 2,500-4,000 lines of code) significantly outweighs the marginal benefits. + +## Table of Contents + +1. [Background](#background) +2. [What is MCP?](#what-is-mcp) +3. [Current State Analysis](#current-state-analysis) +4. [MCP Implementation Requirements](#mcp-implementation-requirements) +5. [Use Case Analysis](#use-case-analysis) +6. [Cost-Benefit Analysis](#cost-benefit-analysis) +7. [Comparison to Similar Projects](#comparison-to-similar-projects) +8. [Recommendation](#recommendation) +9. [Future Considerations](#future-considerations) + +## Background + +The problem statement raised the question: + +> "It is unclear if there is a need for an mcp component to create/edit documents since the json format can be mutated by an LLM in context. Note that the editor package (although incomplete) sends delta documents instead of transactions." + +This research investigates whether an MCP server would provide value for Chartifact document manipulation. + +## What is MCP? + +Model Context Protocol (MCP) is an open protocol developed by Anthropic that enables standardized integration between LLM applications and external data sources/tools. MCP servers expose: + +1. **Resources**: Files, database records, API responses +2. **Tools**: Functions that can be called by the LLM +3. **Prompts**: Templated messages for common tasks + +MCP provides a standardized way for AI models to securely access and manipulate external systems. + +## Current State Analysis + +### Document Size Analysis + +Analysis of example documents in `packages/web-deploy/json/`: + +| Document Type | Lines | Approximate Tokens | +|--------------|-------|-------------------| +| Simple (grocery-list) | 230 | ~700-1,000 | +| Medium (activity-rings) | 500 | ~1,500-2,500 | +| Complex (habit-tracker) | 741 | ~2,200-3,700 | + +**Key Finding:** Even the largest Chartifact documents are under 1,000 lines and ~5,000 tokens. Modern LLMs (GPT-4, Claude) have 100K+ token context windows, so **all documents fit entirely in context with significant room for conversation**. + +### Current Editor Implementation + +Examined code in `packages/editor/src/editor.tsx`: + +```typescript +const sendEditToApp = (newPage: InteractiveDocument) => { + const pageMessage: EditorPageMessage = { + type: 'editorPage', + page: newPage, + sender: 'editor' + }; + postMessageTarget.postMessage(pageMessage, '*'); +}; +``` + +**Key Findings:** + +1. **No Delta/Patch System:** Despite the problem statement mentioning "delta documents," the code shows the editor sends **entire documents** via `EditorPageMessage` +2. **Simple Operations:** Only basic operations implemented: `deleteElement`, `deleteGroup` +3. **No Transactions:** No transaction IDs, optimistic locking, or conflict resolution +4. **PostMessage Architecture:** Uses browser postMessage API between editor and host + +### VSCode Extension + +From `packages/vscode/src/web/command-edit.ts`: + +- Reads `.idoc.json` and `.idoc.md` files directly +- Sends complete documents to editor webview +- No sophisticated transaction system +- Uses standard file system operations + +### JSON Format Characteristics + +The `InteractiveDocument` schema (see `docs/schema/idoc_v1.d.ts`): + +- Well-structured, predictable schema +- Clear hierarchy: document → groups → elements +- Strongly typed with TypeScript definitions +- JSON Schema available for validation +- Designed to be human-readable and LLM-friendly + +## MCP Implementation Requirements + +To implement a functional MCP server for Chartifact, we would need: + +### 1. Server Infrastructure (~500-1,000 lines) + +- Node.js/TypeScript MCP server +- Protocol handling (stdio or HTTP transport) +- Connection management and lifecycle +- Error handling and logging +- Configuration and deployment + +### 2. MCP Tools (~600-1,200 lines) + +Would need to implement tools such as: + +- `create_document` - Create new interactive document +- `read_document` - Read document content +- `update_title` - Update document title +- `update_layout_css` - Update CSS styling +- `add_group` - Add new group to document +- `delete_group` - Delete group from document +- `update_group` - Update group properties +- `add_element` - Add element to group +- `delete_element` - Delete element from group +- `update_element` - Update element content +- `add_variable` - Add reactive variable +- `update_variable` - Update variable definition +- `add_data_loader` - Add data source +- `update_data_loader` - Update data source configuration +- `validate_document` - Validate against schema + +Each tool requires: +- JSON schema for parameters +- Input validation +- Implementation logic +- Error handling +- Documentation + +### 3. Transaction System (~300-500 lines) + +- Transaction ID generation and tracking +- Optimistic concurrency control +- Conflict detection and resolution +- Rollback/undo support +- State management across operations + +### 4. File System Integration (~200-400 lines) + +- File watching for external changes +- Atomic write operations +- Concurrent access handling +- Backup and recovery +- Path resolution and validation + +### 5. Testing & Documentation (~1,000+ lines) + +- Unit tests for each tool +- Integration tests for workflows +- User documentation +- API reference documentation +- Example usage patterns + +**Total Estimated Implementation:** 2,500-4,000 lines of code + comprehensive documentation + +**Maintenance Burden:** Ongoing updates as document schema evolves, bug fixes, feature additions + +## Use Case Analysis + +### Scenario 1: LLM Creating a New Document + +**WITHOUT MCP (Current Approach):** +``` +User: "Create a sales dashboard" +LLM: [Reads schema and examples] + [Generates complete JSON document] + [Returns document to user] +User: [Saves to file or pastes into tool] +Result: Document created in 1 step +``` + +**WITH MCP:** +``` +User: "Create a sales dashboard" +LLM: [Calls create_document] + [Calls update_title] + [Calls update_layout_css] + [Calls add_group multiple times] + [Calls add_element for each element] + [Calls add_data_loader for each data source] + [Calls add_variable for each variable] +Result: Document created in 10+ steps +``` + +**Analysis:** MCP adds significant overhead with multiple tool calls, more complexity, and no clear benefit. JSON generation is faster and more natural for LLMs. + +### Scenario 2: LLM Editing an Existing Document + +**WITHOUT MCP (Current Approach):** +``` +LLM: [Reads document JSON from file - ~3,000 tokens] + [Understands structure] + [Makes surgical edit] + [Returns updated JSON] +User: [Saves updated file] +Result: Document edited, 1 read + 1 write +``` + +**WITH MCP:** +``` +LLM: [Calls read_document - returns ~3,000 tokens] + [Understands structure] + [Calls update_element with changes] +MCP: [Applies change atomically] + [Returns confirmation] +Result: Document edited, 1 read + 1 tool call +``` + +**Analysis:** Token usage is identical. MCP adds network/protocol overhead without meaningful benefit. The atomic operation is nice but unnecessary for single-user editing. + +### Scenario 3: Complex Multi-Step Edit + +**WITHOUT MCP:** +``` +LLM: [Reads full document] + [Plans multiple changes] + [Applies all changes to JSON in memory] + [Returns updated document] +Result: All changes applied atomically in one operation +``` + +**WITH MCP:** +``` +LLM: [Reads full document] + [Plans multiple changes] + [Calls update_element multiple times] + [Calls add_variable] + [Calls update_css] +Result: Changes applied in 5+ tool calls, each a network round-trip +``` + +**Analysis:** MCP is actually SLOWER for multi-step edits due to multiple round-trips. Without MCP, LLM can apply all changes in one operation. + +### Scenario 4: Collaborative Editing (Future) + +**WITHOUT MCP:** +``` +User A: [Makes edit, saves file] +User B: [Makes edit to same file] +Result: Last write wins, potential data loss +``` + +**WITH MCP:** +``` +User A: [Calls update_element] +MCP: [Applies with transaction ID] +User B: [Calls update_element on same element] +MCP: [Detects conflict, resolves or rejects] +Result: Conflict handled gracefully +``` + +**Analysis:** MCP provides value for collaborative editing. However, this is not a current requirement, and the editor package doesn't support this scenario yet. + +## Cost-Benefit Analysis + +### Implementation Costs + +| Category | Estimated Effort | Details | +|----------|-----------------|---------| +| Development | 3-4 weeks | Server infrastructure, tool implementation, transaction system | +| Testing | 1-2 weeks | Unit tests, integration tests, edge cases | +| Documentation | 1 week | User guide, API reference, examples | +| **Total Initial** | **5-7 weeks** | Full-time developer equivalent | +| Maintenance | Ongoing | Schema updates, bug fixes, feature additions | + +### Operational Costs + +- Additional deployment complexity (MCP server + main app) +- Version compatibility between server and clients +- Debugging more complex than direct file operations +- User learning curve for MCP-based workflow +- Network latency for each tool call + +### Benefits + +| Benefit | Value | Current Need | +|---------|-------|--------------| +| Atomic operations | Low | Documents are small, single-user editing | +| Transaction support | Low | No collaborative editing requirement | +| Structured API | Low | JSON schema already provides structure | +| Type safety | None | TypeScript + JSON Schema sufficient | +| Partial document access | None | Documents fit in full context | +| Conflict resolution | None | Not a current use case | +| External tool integration | Low | No external tools need integration | + +### Verdict + +**Costs significantly outweigh benefits** at the current scale and requirements of the project. + +## Comparison to Similar Projects + +### Projects WITH MCP Servers + +**Jupyter Notebooks:** +- Complex structure (code cells + output + metadata) +- Need to execute code server-side +- State management between cells +- Multiple kernel types +- Justification: Complexity requires abstraction + +**Database Systems:** +- Large data sets don't fit in context +- Need optimized query execution +- ACID transactions required +- Security and access control +- Justification: Scale requires protocol + +### Projects WITHOUT MCP Servers + +**Markdown Editors:** +- LLMs edit markdown directly +- Full file fits in context +- No special protocol needed +- Works well in practice + +**Configuration Files (JSON/YAML):** +- Tools edit directly via file system +- Schema validation via JSON Schema +- No MCP servers exist +- Chartifact is similar to this category + +**Conclusion:** Projects with simple, structured formats (like Chartifact) don't typically use MCP for editing. The protocol adds unnecessary complexity. + +## Recommendation + +### Primary Recommendation: DO NOT IMPLEMENT MCP COMPONENT + +**Rationale:** + +1. **JSON Format is Sufficient** + - Well-structured and LLM-friendly + - Documents fit entirely in context windows + - LLMs can read, understand, and edit directly + - No need for abstraction layer + +2. **Editor is Incomplete** + - Current editor sends full documents (not deltas) + - No transaction system implemented yet + - Adding MCP before completing basic functionality is premature + - Should finish editor first, then evaluate needs + +3. **No Current Pain Points** + - No reports of LLMs struggling with JSON editing + - No collaborative editing requirement + - No external tools needing integration + - Documents aren't too large for context + +4. **Implementation Burden** + - 5-7 weeks of development time + - 2,500-4,000 lines of code to maintain + - Ongoing maintenance as schema evolves + - Added deployment and debugging complexity + +5. **Premature Optimization** + - Solving problems that don't exist yet + - Can add later if needs emerge + - Easier to add than to remove + - Focus should be on core functionality + +### Alternative Approaches + +Instead of implementing MCP, consider: + +#### 1. Document JSON Editing Patterns for LLMs + +Create documentation showing LLMs how to effectively edit Chartifact documents: + +```markdown +# Editing Chartifact Documents with LLMs + +## Reading Documents +Load the .idoc.json file into context... + +## Making Surgical Edits +To update a specific element, find it by path... + +## Adding Components +To add a new chart, update the groups array... + +## Validation +Use the JSON schema at docs/schema/idoc_v1.json... +``` + +#### 2. Improve JSON Schema and Examples + +- Enhance JSON Schema with better descriptions +- Add more inline examples +- Create "how-to" example documents +- Document common patterns + +#### 3. Complete the Editor Package + +Focus on finishing the basic editor: +- Implement proper undo/redo +- Add more editing operations +- Improve UI/UX +- Add validation feedback + +#### 4. Create Optional JSON Patch Library + +If surgical editing becomes important: + +```typescript +// Lightweight library, no protocol overhead +import { applyPatch } from '@chartifact/document-patches'; + +const patch = [ + { op: 'replace', path: '/groups/0/elements/1', value: 'New text' } +]; +const updated = applyPatch(document, patch); +``` + +Benefits: +- Reusable in editor and other tools +- No server to maintain +- No protocol overhead +- Can expose via MCP later if needed +- Only 200-300 lines of code + +## Future Considerations + +### When to Reconsider MCP + +Implement MCP if any of these conditions occur: + +#### Trigger 1: Documents Exceed Context Windows +- **Threshold:** Documents regularly exceed 50K tokens +- **Likelihood:** Low (current max is ~5K tokens) +- **Timeline:** Not expected in near future + +#### Trigger 2: Collaborative Editing Requirement +- **Threshold:** Users request real-time multi-user editing +- **Likelihood:** Medium (useful for teams) +- **Timeline:** Could emerge with product growth + +#### Trigger 3: External Tool Ecosystem +- **Threshold:** 3+ external tools need document manipulation +- **Likelihood:** Low (currently only VSCode + web viewer) +- **Timeline:** Depends on adoption + +#### Trigger 4: Editor Implements Transactions +- **Threshold:** Editor package has working transaction system +- **Likelihood:** High (natural evolution) +- **Timeline:** Could be next phase of editor development + +### Phased Approach Recommendation + +**Phase 1 (Current): Direct JSON Editing** ✅ +- LLMs edit JSON directly +- VSCode extension reads/writes files +- Web viewer uses postMessage +- Status: Working well, no issues + +**Phase 2 (If Needed): Transaction Library** +- Create JSON patch/transaction library +- Implement in editor package +- Share between VSCode and web viewer +- No MCP server yet +- Trigger: Need for undo/redo or collaborative editing + +**Phase 3 (If Needed): MCP Server** +- Wrap transaction library in MCP protocol +- Expose as standardized tools +- Deploy for external integrations +- Trigger: External tools need integration + +## Conclusion + +After comprehensive analysis of the Chartifact codebase, document characteristics, and MCP implementation requirements, **we recommend NOT implementing an MCP component at this time**. + +The JSON format is well-suited for direct LLM manipulation, documents are small enough to fit entirely in context, and the implementation overhead (5-7 weeks, 2,500-4,000 lines) significantly outweighs the marginal benefits. + +Instead, focus should remain on: +1. Completing the core editor functionality +2. Improving documentation and examples for LLM-based editing +3. Waiting for real user pain points to emerge + +MCP can be reconsidered in the future if collaborative editing becomes important, external tools need integration, or documents grow significantly larger. The phased approach outlined above provides a clear path forward should these needs emerge. + +--- + +**Review Status:** Ready for review +**Next Steps:** Share with team for feedback and decision From 25479dfc3b53047717072ed6f23c09bcb6e06c3a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 10 Oct 2025 00:23:13 +0000 Subject: [PATCH 03/11] Revise MCP research based on feedback - recommend lightweight implementation Co-authored-by: danmarshall <11507384+danmarshall@users.noreply.github.com> --- docs/research/README.md | 139 ++++--- docs/research/mcp-component-analysis.md | 489 ++++++++++++++---------- 2 files changed, 364 insertions(+), 264 deletions(-) diff --git a/docs/research/README.md b/docs/research/README.md index 52d6d291..a0ccde93 100644 --- a/docs/research/README.md +++ b/docs/research/README.md @@ -1,6 +1,6 @@ # MCP Component Research - Quick Summary -**Research Date:** October 2025 +**Research Date:** October 2025 (Revised after feedback) **Full Report:** [mcp-component-analysis.md](./mcp-component-analysis.md) ## Question @@ -9,85 +9,112 @@ Should Chartifact implement a Model Context Protocol (MCP) component for creatin ## Answer -**NO - Do not implement MCP component at this time.** +**YES - Implement a lightweight transactional builder with MCP wrapper (1-2 days effort).** + +## Revised Analysis + +Initial recommendation was "No" based on overestimated complexity (5-7 weeks). After feedback, revised to lightweight approach: + +### Key Insights + +1. **Time estimate was way off**: 1-2 days for simple builder + MCP wrapper, not 5-7 weeks +2. **Multi-turn conversations benefit**: Token accumulation across many editing turns makes transactional operations valuable +3. **LLMs miss schema details**: Structured operations help prevent common mistakes +4. **Editor completion easier with builder**: Builder provides foundation for editor, not vice versa ## Key Findings -### 1. Documents Fit Entirely in LLM Context -- Largest example document: 741 lines (~5,000 tokens) -- Modern LLMs have 100K+ token context windows -- No need for partial document access via MCP +### 1. Multi-Turn Token Accumulation Matters +- Single document: 741 lines (~5,000 tokens) +- 20 editing turns: 100,000+ tokens (exhausts context) +- Transactional operations: Send only changes, not full document each time -### 2. JSON Format is Already LLM-Friendly -- Well-structured, predictable schema -- LLMs can read, understand, and edit directly -- No abstraction layer needed +### 2. Schema Clarity vs. Practice +- Schema is clear in theory +- LLMs can miss details in practice +- Structured builder prevents violations +- Sensible defaults help starting point -### 3. Current Editor is Incomplete -- Editor sends full documents (not deltas as stated in problem) -- No transaction system exists yet -- Should complete basic editor before adding MCP +### 3. Lightweight Implementation +- **Effort:** 1-2 days, not 5-7 weeks +- **Code:** 350-600 lines, not 2,500-4,000 +- **Maintenance:** Minimal - maps to schema -### 4. Implementation Cost is High -- **Estimated effort:** 5-7 weeks development -- **Code size:** 2,500-4,000 lines + tests + docs -- **Maintenance:** Ongoing as schema evolves +### 4. Editor Foundation +- Builder simplifies editor completion +- Ready-made operations (add, delete, update) +- Shared validation logic +- Easier undo/redo implementation -### 5. Benefits are Marginal -- ❌ Atomic operations: Nice but unnecessary for single-user editing -- ❌ Partial access: Documents fit in context -- ❌ Transactions: Not a current requirement -- ❌ Conflict resolution: No collaborative editing yet -- ❌ External tools: None exist that need integration +## Benefits vs. Costs -## Use Case Comparison +### Benefits (Revised) -| Task | Without MCP | With MCP | Winner | -|------|-------------|----------|--------| -| Create document | 1 step (generate JSON) | 10+ tool calls | Without MCP | -| Edit document | Read + edit + write | Read + multiple calls | Without MCP | -| Multi-step edit | Apply all changes at once | Multiple round-trips | Without MCP | -| Collaborative edit | Last write wins | Conflict detection | With MCP* | +| Benefit | Value | Reason | +|---------|-------|--------| +| Token efficiency | High | Multi-turn conversations stay in context | +| Schema validation | High | Structured operations prevent mistakes | +| Editor foundation | High | Builder simplifies editor completion | +| Sensible defaults | High | Start with working document, not blank | +| MCP tool access | Medium | Standardized protocol for external tools | -\* Not a current requirement +### Costs (Revised) + +| Cost | Effort | Details | +|------|--------|---------| +| Transactional builder | 1 day | Core operations, validation | +| MCP wrapper | 0.5 day | Thin shim over builder | +| File integration | 0.5 day | Load/save operations | +| **Total** | **1-2 days** | Focused development | + +**Verdict: Benefits outweigh costs** with lightweight approach. ## Recommendation -### Instead of MCP, Do This: +### Implement Lightweight Transactional Builder + MCP Wrapper -1. **Document JSON editing patterns** - Guide LLMs on how to edit documents effectively -2. **Complete the editor** - Finish basic functionality first -3. **Improve examples** - Add more examples showing document patterns -4. **Wait for pain points** - Implement MCP only if real needs emerge +**Why the change from original "No"?** -### When to Reconsider MCP +1. **Complexity overestimated**: 1-2 days, not 5-7 weeks +2. **Multi-turn conversations**: Token accumulation is real issue +3. **LLMs miss schema**: Structured operations help in practice +4. **Editor needs foundation**: Builder makes editor easier to complete -Implement MCP if: -- ✅ Documents regularly exceed 50K tokens (unlikely) -- ✅ Users need collaborative editing (possible future) -- ✅ 3+ external tools need document manipulation (unlikely) -- ✅ Editor implements transaction system (natural evolution) +### Implementation Plan -## Phased Approach +**Day 1: Transactional Builder** +- Create immutable document builder class +- Core operations: create, addGroup, addElement, delete* +- Type-safe API with validation +- Sensible defaults included -**Phase 1 (Now):** Direct JSON editing ✅ Working well +**Day 1-2: MCP Wrapper** +- Thin shim exposing builder via MCP protocol +- Tool definitions with JSON schemas +- Simple state management -**Phase 2 (If needed):** Create lightweight transaction library -- Shared between editor and tools -- No MCP protocol yet -- ~200-300 lines of code +**Day 2: File Integration** +- Load/save .idoc.json files +- Basic error handling -**Phase 3 (If needed):** Wrap transaction library in MCP -- Only if external tools need integration -- Or collaborative editing becomes priority +**Next: Refactor Editor** +- Use builder as foundation +- Simpler implementation +- Ready-made operations ## Conclusion -**Costs outweigh benefits.** The JSON format is sufficient for LLM-based document creation and editing. MCP would add unnecessary complexity (2,500-4,000 lines) before core features are complete. Focus should remain on finishing the editor and improving documentation. +**Revised recommendation: YES, implement lightweight version.** + +Original analysis overestimated complexity and underestimated benefits: +- **Complexity**: 1-2 days, not 5-7 weeks +- **Multi-turn value**: Token efficiency matters +- **Practical LLM behavior**: Structured operations help +- **Editor synergy**: Builder provides foundation -MCP can be added later if genuine needs emerge. +**Next step:** Implement simple transactional builder with MCP wrapper. --- -**Status:** Research complete, recommendation ready for review -**See:** [Full detailed analysis](./mcp-component-analysis.md) +**Status:** Research revised based on feedback, ready for implementation +**See:** [Full updated analysis](./mcp-component-analysis.md) diff --git a/docs/research/mcp-component-analysis.md b/docs/research/mcp-component-analysis.md index 004058c4..89f57373 100644 --- a/docs/research/mcp-component-analysis.md +++ b/docs/research/mcp-component-analysis.md @@ -2,13 +2,17 @@ **Date:** October 2025 **Author:** Research Analysis -**Status:** Recommendation - Do Not Implement +**Status:** Revised Recommendation - Implement Lightweight Version ## Executive Summary -This report evaluates whether Chartifact should implement a Model Context Protocol (MCP) component for creating and editing interactive documents. After thorough analysis of the codebase, document sizes, current editing mechanisms, and implementation requirements, **we recommend NOT implementing an MCP component at this time**. +This report evaluates whether Chartifact should implement a Model Context Protocol (MCP) component for creating and editing interactive documents. After thorough analysis and feedback, **we recommend implementing a lightweight transactional builder with an MCP wrapper**. -The JSON format is already well-suited for LLM-based editing, documents are small enough to fit entirely in modern LLM context windows, and the implementation overhead (estimated 2,500-4,000 lines of code) significantly outweighs the marginal benefits. +**Revised Analysis:** While the JSON format is well-suited for LLM-based editing, a simple transactional builder (1-2 days effort) would provide value by: +1. Offering structured operations that help LLMs avoid schema mistakes +2. Supporting multi-turn conversations where token accumulation matters +3. Serving as a foundation for completing the editor package +4. Enabling a thin MCP wrapper for standardized tool access ## Table of Contents @@ -54,6 +58,11 @@ Analysis of example documents in `packages/web-deploy/json/`: **Key Finding:** Even the largest Chartifact documents are under 1,000 lines and ~5,000 tokens. Modern LLMs (GPT-4, Claude) have 100K+ token context windows, so **all documents fit entirely in context with significant room for conversation**. +**Multi-Turn Consideration:** However, in a conversation with many editing turns (e.g., creating a "masterpiece" iteratively), token usage accumulates: +- 10 editing turns × 5,000 tokens per full document = 50,000 tokens +- 20 turns would exceed typical context windows +- A transactional approach (sending only changes) reduces this significantly + ### Current Editor Implementation Examined code in `packages/editor/src/editor.tsx`: @@ -95,139 +104,128 @@ The `InteractiveDocument` schema (see `docs/schema/idoc_v1.d.ts`): - JSON Schema available for validation - Designed to be human-readable and LLM-friendly +**Schema Clarity in Practice:** While the schema is clear, real-world usage shows LLMs can miss schema details, especially in complex documents. A transactional builder with well-defined operations (e.g., `addGroup`, `addElement`) would provide: +- Type-safe operations that prevent schema violations +- Starting point with sensible defaults +- Clearer API surface than raw JSON manipulation +- Better error messages when operations fail + ## MCP Implementation Requirements -To implement a functional MCP server for Chartifact, we would need: +### Original (Over-Engineered) Estimate -### 1. Server Infrastructure (~500-1,000 lines) +Initial analysis suggested a comprehensive implementation requiring 2,500-4,000 lines of code over 5-7 weeks. This included full transaction systems, conflict resolution, file watching, and extensive tooling. -- Node.js/TypeScript MCP server -- Protocol handling (stdio or HTTP transport) -- Connection management and lifecycle -- Error handling and logging -- Configuration and deployment +### Revised (Lightweight) Approach -### 2. MCP Tools (~600-1,200 lines) +**Realistic Implementation: 1-2 focused days** -Would need to implement tools such as: +The key insight is to start minimal and iterate: -- `create_document` - Create new interactive document -- `read_document` - Read document content -- `update_title` - Update document title -- `update_layout_css` - Update CSS styling -- `add_group` - Add new group to document -- `delete_group` - Delete group from document -- `update_group` - Update group properties -- `add_element` - Add element to group -- `delete_element` - Delete element from group -- `update_element` - Update element content -- `add_variable` - Add reactive variable -- `update_variable` - Update variable definition -- `add_data_loader` - Add data source -- `update_data_loader` - Update data source configuration -- `validate_document` - Validate against schema +#### 1. Transactional Builder (~200-300 lines) -Each tool requires: -- JSON schema for parameters -- Input validation -- Implementation logic -- Error handling -- Documentation +A simple builder class with core operations: +- `create()` - Initialize document with sensible defaults +- `addGroup(id, props?)` - Add group to document +- `addElement(groupId, element)` - Add element to group +- `deleteGroup(groupId)` - Remove group +- `deleteElement(groupId, elementIndex)` - Remove element +- `setTitle(title)` - Update document title +- `setCss(css)` - Update layout CSS +- `toJSON()` - Export current state -### 3. Transaction System (~300-500 lines) +Benefits: +- Type-safe operations +- Validation at each step +- Immutable updates (returns new state) +- Small, focused API surface -- Transaction ID generation and tracking -- Optimistic concurrency control -- Conflict detection and resolution -- Rollback/undo support -- State management across operations +#### 2. MCP Wrapper (~100-200 lines) -### 4. File System Integration (~200-400 lines) +A thin shim that exposes the builder via MCP protocol: +- Tool definitions for each builder method +- Parameter schemas (auto-generated from TypeScript) +- Error handling and validation +- State management (one document per session) -- File watching for external changes -- Atomic write operations -- Concurrent access handling -- Backup and recovery -- Path resolution and validation +The MCP server scaffolding is straightforward: +- Use existing MCP SDK +- Map tool calls to builder methods +- Return results in MCP format -### 5. Testing & Documentation (~1,000+ lines) +#### 3. File Integration (~50-100 lines) -- Unit tests for each tool -- Integration tests for workflows -- User documentation -- API reference documentation -- Example usage patterns +Basic file operations: +- Load from `.idoc.json` +- Save to `.idoc.json` +- Simple error handling -**Total Estimated Implementation:** 2,500-4,000 lines of code + comprehensive documentation +**Total Realistic Implementation:** 350-600 lines of focused code -**Maintenance Burden:** Ongoing updates as document schema evolves, bug fixes, feature additions +**Timeline:** 1-2 days with proper focus, not 5-7 weeks + +**Maintenance:** Minimal - builder operations map 1:1 to schema, MCP wrapper is thin ## Use Case Analysis -### Scenario 1: LLM Creating a New Document +### Scenario 1: Single Document Creation -**WITHOUT MCP (Current Approach):** +**WITHOUT Builder:** ``` -User: "Create a sales dashboard" -LLM: [Reads schema and examples] - [Generates complete JSON document] - [Returns document to user] -User: [Saves to file or pastes into tool] -Result: Document created in 1 step +LLM: [Generates complete JSON document] +Result: Document created in 1 step, ~500 tokens ``` -**WITH MCP:** +**WITH Builder:** ``` -User: "Create a sales dashboard" -LLM: [Calls create_document] - [Calls update_title] - [Calls update_layout_css] - [Calls add_group multiple times] - [Calls add_element for each element] - [Calls add_data_loader for each data source] - [Calls add_variable for each variable] -Result: Document created in 10+ steps +LLM: [Calls create_document with defaults] + [Calls addGroup, addElement as needed] +Result: Document created in 3-5 operations, clearer structure ``` -**Analysis:** MCP adds significant overhead with multiple tool calls, more complexity, and no clear benefit. JSON generation is faster and more natural for LLMs. +**Analysis:** Builder provides sensible defaults and prevents schema violations. Slight overhead but better guardrails. -### Scenario 2: LLM Editing an Existing Document +### Scenario 2: Multi-Turn Editing Session (Critical Case) -**WITHOUT MCP (Current Approach):** +**WITHOUT Builder (Full Document Each Time):** ``` -LLM: [Reads document JSON from file - ~3,000 tokens] - [Understands structure] - [Makes surgical edit] - [Returns updated JSON] -User: [Saves updated file] -Result: Document edited, 1 read + 1 write +Turn 1: LLM reads doc (5K tokens) → edits → returns doc (5K tokens) +Turn 2: LLM reads doc (5K tokens) → edits → returns doc (5K tokens) +Turn 3: LLM reads doc (5K tokens) → edits → returns doc (5K tokens) +... +Turn 20: Context exhausted at 100K tokens ``` -**WITH MCP:** +**WITH Builder (Transactional Operations):** ``` -LLM: [Calls read_document - returns ~3,000 tokens] - [Understands structure] - [Calls update_element with changes] -MCP: [Applies change atomically] - [Returns confirmation] -Result: Document edited, 1 read + 1 tool call +Turn 1: LLM reads doc (5K tokens) → calls addElement (50 tokens) +Turn 2: Calls updateTitle (30 tokens) +Turn 3: Calls deleteGroup (20 tokens) +... +Turn 20: Still have 40K tokens of context remaining ``` -**Analysis:** Token usage is identical. MCP adds network/protocol overhead without meaningful benefit. The atomic operation is nice but unnecessary for single-user editing. +**Analysis:** This is where the builder shines. Multi-turn conversations benefit significantly from transactional operations vs. sending full documents repeatedly. -### Scenario 3: Complex Multi-Step Edit +### Scenario 3: Schema Mistakes -**WITHOUT MCP:** +**WITHOUT Builder:** ``` -LLM: [Reads full document] - [Plans multiple changes] - [Applies all changes to JSON in memory] - [Returns updated document] -Result: All changes applied atomically in one operation +LLM: [Generates JSON with subtle schema violation] + [e.g., wrong property name, missing required field] +System: [Fails at render time or silently breaks] +User: [Has to debug and fix manually] ``` -**WITH MCP:** +**WITH Builder:** ``` +LLM: [Calls builder.addElement with invalid params] +Builder: [Validation fails immediately] + [Returns clear error: "element must have 'type' property"] +LLM: [Corrects and retries] +``` + +**Analysis:** Builder catches errors at operation time, not render time. Better DX and prevents broken documents. LLM: [Reads full document] [Plans multiple changes] [Calls update_element multiple times] @@ -260,39 +258,45 @@ Result: Conflict handled gracefully ## Cost-Benefit Analysis -### Implementation Costs +### Revised Implementation Costs | Category | Estimated Effort | Details | |----------|-----------------|---------| -| Development | 3-4 weeks | Server infrastructure, tool implementation, transaction system | -| Testing | 1-2 weeks | Unit tests, integration tests, edge cases | -| Documentation | 1 week | User guide, API reference, examples | -| **Total Initial** | **5-7 weeks** | Full-time developer equivalent | -| Maintenance | Ongoing | Schema updates, bug fixes, feature additions | - -### Operational Costs - -- Additional deployment complexity (MCP server + main app) -- Version compatibility between server and clients -- Debugging more complex than direct file operations -- User learning curve for MCP-based workflow -- Network latency for each tool call - -### Benefits - -| Benefit | Value | Current Need | -|---------|-------|--------------| -| Atomic operations | Low | Documents are small, single-user editing | -| Transaction support | Low | No collaborative editing requirement | -| Structured API | Low | JSON schema already provides structure | -| Type safety | None | TypeScript + JSON Schema sufficient | -| Partial document access | None | Documents fit in full context | -| Conflict resolution | None | Not a current use case | -| External tool integration | Low | No external tools need integration | +| Transactional Builder | 1 day | Core operations, type-safe API, validation | +| MCP Wrapper | 0.5 days | Thin shim over builder, tool definitions | +| File Integration | 0.5 days | Load/save, basic error handling | +| **Total Initial** | **1-2 days** | Focused development time | +| Testing | Included | Test builder operations as developed | +| Documentation | Minimal | API is self-documenting, add examples | +| Maintenance | Low | Builder maps to schema, MCP is thin wrapper | + +### Operational Benefits + +- **Reduced token usage in multi-turn conversations**: Transactional operations vs. full document resends +- **Schema validation**: Type-safe operations prevent common mistakes +- **Editor foundation**: Builder can be reused by editor package +- **Sensible defaults**: Starting with working skeleton vs. blank document +- **Clear API**: Well-defined operations vs. raw JSON manipulation +- **Future-proof**: Easy to extend with new operations + +### Benefits (Revised) + +| Benefit | Value | Notes | +|---------|-------|-------| +| Structured operations | **High** | Helps LLMs avoid schema mistakes | +| Token efficiency | **Medium** | Matters in multi-turn editing sessions | +| Editor foundation | **High** | Builder simplifies editor completion | +| Type safety | **Medium** | Runtime validation catches errors early | +| Sensible defaults | **High** | Better starting point than blank slate | +| External tool integration | **Medium** | MCP provides standardized access | ### Verdict -**Costs significantly outweigh benefits** at the current scale and requirements of the project. +**Benefits outweigh costs** with the lightweight approach. 1-2 days of work provides significant value, especially for: +1. Multi-turn editing conversations +2. Completing the editor package +3. Preventing schema violations +4. Standardized tool access via MCP ## Comparison to Similar Projects @@ -330,99 +334,142 @@ Result: Conflict handled gracefully ## Recommendation -### Primary Recommendation: DO NOT IMPLEMENT MCP COMPONENT +### Revised Recommendation: IMPLEMENT LIGHTWEIGHT TRANSACTIONAL BUILDER WITH MCP WRAPPER **Rationale:** -1. **JSON Format is Sufficient** - - Well-structured and LLM-friendly - - Documents fit entirely in context windows - - LLMs can read, understand, and edit directly - - No need for abstraction layer - -2. **Editor is Incomplete** - - Current editor sends full documents (not deltas) - - No transaction system implemented yet - - Adding MCP before completing basic functionality is premature - - Should finish editor first, then evaluate needs - -3. **No Current Pain Points** - - No reports of LLMs struggling with JSON editing - - No collaborative editing requirement - - No external tools needing integration - - Documents aren't too large for context - -4. **Implementation Burden** - - 5-7 weeks of development time - - 2,500-4,000 lines of code to maintain - - Ongoing maintenance as schema evolves - - Added deployment and debugging complexity +1. **Multi-Turn Conversations Benefit from Transactions** + - Editing a complex document across 20+ turns accumulates significant tokens + - Transactional operations send only changes, not full documents + - Reduces token usage and context window pressure + - Enables longer, more iterative editing sessions + +2. **LLMs Can Miss Schema Details** + - While the schema is clear, LLMs sometimes violate it in practice + - Transactional builder provides type-safe operations + - Validation at each step catches errors early + - Sensible defaults help LLMs start with working documents + +3. **Editor Completion is Easier WITH Transactional Builder** + - **Previous assumption was backwards**: The editor would be simpler to complete if built on a transactional foundation + - Builder provides ready-made operations (add, delete, update) + - Editor UI can directly call builder methods + - Shared validation and state management logic + - Undo/redo becomes easier with transaction history + +4. **Minimal Implementation Burden** + - **1-2 days**, not 5-7 weeks (original estimate was way off) + - ~350-600 lines of focused code + - Transactional builder is small API surface + - MCP wrapper is thin shim over builder + - Low maintenance - builder maps directly to schema + +5. **Multiple Benefits from Small Investment** + - Token efficiency in conversations + - Schema validation and error prevention + - Foundation for editor package + - Standardized MCP tool access + - Starting point with defaults vs. blank slate + +### Implementation Approach + +#### Step 1: Build Transactional Builder (Day 1) + +Create a simple, immutable document builder: -5. **Premature Optimization** - - Solving problems that don't exist yet - - Can add later if needs emerge - - Easier to add than to remove - - Focus should be on core functionality - -### Alternative Approaches - -Instead of implementing MCP, consider: - -#### 1. Document JSON Editing Patterns for LLMs - -Create documentation showing LLMs how to effectively edit Chartifact documents: +```typescript +class ChartifactBuilder { + private doc: InteractiveDocument; + + constructor(initial?: Partial) { + this.doc = this.createDefault(initial); + } + + private createDefault(partial?: Partial): InteractiveDocument { + // Return document with sensible defaults + return { + title: partial?.title || "New Document", + layout: { css: partial?.layout?.css || "" }, + groups: partial?.groups || [], + variables: partial?.variables || [], + dataLoaders: partial?.dataLoaders || [], + ...partial + }; + } + + addGroup(groupId: string, elements: Element[] = []): ChartifactBuilder { + // Immutable operation, returns new builder + } + + addElement(groupId: string, element: Element): ChartifactBuilder { + // Find group, add element, return new builder + } + + deleteGroup(groupId: string): ChartifactBuilder { + // Remove group, return new builder + } + + toJSON(): InteractiveDocument { + return this.doc; + } +} +``` -```markdown -# Editing Chartifact Documents with LLMs +Benefits: +- Immutable operations (no mutation bugs) +- Type-safe (TypeScript catches errors) +- Chainable API (fluent interface) +- Sensible defaults included -## Reading Documents -Load the .idoc.json file into context... +#### Step 2: Add MCP Wrapper (Day 1-2) -## Making Surgical Edits -To update a specific element, find it by path... +Expose builder via MCP protocol: -## Adding Components -To add a new chart, update the groups array... +```typescript +// Define MCP tools +const tools = [ + { + name: "create_document", + description: "Create new document with optional properties", + inputSchema: { /* JSON schema */ } + }, + { + name: "add_group", + description: "Add a group to the document", + inputSchema: { /* JSON schema */ } + }, + // ... more tools +]; -## Validation -Use the JSON schema at docs/schema/idoc_v1.json... +// Handle tool calls +async function handleToolCall(name: string, args: any) { + switch (name) { + case "create_document": + builder = new ChartifactBuilder(args); + return builder.toJSON(); + case "add_group": + builder = builder.addGroup(args.groupId, args.elements); + return builder.toJSON(); + // ... more handlers + } +} ``` -#### 2. Improve JSON Schema and Examples +#### Step 3: File Integration (Day 2) -- Enhance JSON Schema with better descriptions -- Add more inline examples -- Create "how-to" example documents -- Document common patterns +Add simple file operations: +- Load from filesystem +- Save to filesystem +- Basic error handling -#### 3. Complete the Editor Package - -Focus on finishing the basic editor: -- Implement proper undo/redo -- Add more editing operations -- Improve UI/UX -- Add validation feedback - -#### 4. Create Optional JSON Patch Library - -If surgical editing becomes important: - -```typescript -// Lightweight library, no protocol overhead -import { applyPatch } from '@chartifact/document-patches'; +#### Step 4: Use in Editor Package -const patch = [ - { op: 'replace', path: '/groups/0/elements/1', value: 'New text' } -]; -const updated = applyPatch(document, patch); -``` +Refactor editor to use builder: +- Replace manual document manipulation with builder calls +- Implement undo/redo using builder's immutable operations +- Add more editing operations easily -Benefits: -- Reusable in editor and other tools -- No server to maintain -- No protocol overhead -- Can expose via MCP later if needed -- Only 200-300 lines of code +Total effort: **1-2 focused days** ## Future Considerations @@ -473,16 +520,42 @@ Implement MCP if any of these conditions occur: ## Conclusion -After comprehensive analysis of the Chartifact codebase, document characteristics, and MCP implementation requirements, **we recommend NOT implementing an MCP component at this time**. +After comprehensive analysis and feedback review, **we recommend implementing a lightweight transactional builder with an MCP wrapper**. + +### Key Insights from Feedback + +1. **Time estimate was way off**: A simple transactional builder + MCP wrapper is 1-2 days, not 5-7 weeks +2. **Multi-turn conversations matter**: Token accumulation across many editing turns makes transactional operations valuable +3. **LLMs miss schema details**: While the schema is clear, a structured API helps prevent mistakes +4. **Editor completion is easier with builder**: The builder provides a foundation for the editor, not the other way around + +### Revised Assessment + +The lightweight approach provides significant value with minimal cost: + +**Benefits:** +- Reduces token usage in multi-turn editing conversations +- Provides type-safe operations that prevent schema violations +- Serves as foundation for completing editor package +- Offers sensible defaults for starting documents +- Enables standardized MCP tool access + +**Costs:** +- 1-2 days of focused development +- ~350-600 lines of maintainable code +- Minimal ongoing maintenance + +**Verdict:** Benefits clearly outweigh costs with the lightweight approach. -The JSON format is well-suited for direct LLM manipulation, documents are small enough to fit entirely in context, and the implementation overhead (5-7 weeks, 2,500-4,000 lines) significantly outweighs the marginal benefits. +### Next Steps -Instead, focus should remain on: -1. Completing the core editor functionality -2. Improving documentation and examples for LLM-based editing -3. Waiting for real user pain points to emerge +1. Implement transactional builder (Day 1) +2. Add MCP wrapper over builder (Day 1-2) +3. Basic file integration (Day 2) +4. Refactor editor to use builder foundation +5. Document usage patterns -MCP can be reconsidered in the future if collaborative editing becomes important, external tools need integration, or documents grow significantly larger. The phased approach outlined above provides a clear path forward should these needs emerge. +This provides immediate value while keeping implementation simple and maintainable. --- From b66f07591e055f2aa6fc6cc25489087defe53ebf Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 10 Oct 2025 16:14:59 +0000 Subject: [PATCH 04/11] Add Builder API sketch with comprehensive operations and examples Co-authored-by: danmarshall <11507384+danmarshall@users.noreply.github.com> --- docs/research/builder-api-sketch.md | 914 ++++++++++++++++++++++++++++ 1 file changed, 914 insertions(+) create mode 100644 docs/research/builder-api-sketch.md diff --git a/docs/research/builder-api-sketch.md b/docs/research/builder-api-sketch.md new file mode 100644 index 00000000..e66ddd6b --- /dev/null +++ b/docs/research/builder-api-sketch.md @@ -0,0 +1,914 @@ +# Chartifact Document Builder API Sketch + +**Date:** October 2025 +**Status:** API Design Proposal + +## Overview + +This document sketches out the API for a lightweight transactional builder for Chartifact interactive documents. The builder provides: + +1. **Immutable operations** - Each operation returns a new builder instance +2. **Type safety** - TypeScript types prevent schema violations +3. **Sensible defaults** - Documents start with working structure +4. **Chainable API** - Fluent interface for ergonomic usage +5. **Validation** - Errors caught at operation time, not render time + +## Core Builder Class + +```typescript +import { + InteractiveDocument, + ElementGroup, + PageElement, + InteractiveElement, + DataLoader, + Variable, + PageStyle +} from '@microsoft/chartifact-schema'; + +/** + * Immutable builder for creating and modifying Chartifact documents. + * Each operation returns a new builder instance with the updated document. + */ +class ChartifactBuilder { + private readonly doc: InteractiveDocument; + + constructor(initial?: Partial) { + this.doc = this.createDefault(initial); + } + + /** + * Get the current document as a plain object + */ + toJSON(): InteractiveDocument { + return JSON.parse(JSON.stringify(this.doc)); + } + + /** + * Get the current document as a JSON string + */ + toString(): string { + return JSON.stringify(this.doc, null, 2); + } + + /** + * Create a new builder from an existing document + */ + static fromDocument(doc: InteractiveDocument): ChartifactBuilder { + return new ChartifactBuilder(doc); + } + + /** + * Create a new builder from a JSON string + */ + static fromJSON(json: string): ChartifactBuilder { + return new ChartifactBuilder(JSON.parse(json)); + } + + /** + * Load a document from a file path + */ + static async fromFile(path: string): Promise { + // Implementation would use fs.readFile + throw new Error('Not implemented'); + } + + /** + * Save the current document to a file path + */ + async toFile(path: string): Promise { + // Implementation would use fs.writeFile + throw new Error('Not implemented'); + } + + // Private helper to create default document structure + private createDefault(partial?: Partial): InteractiveDocument { + return { + title: partial?.title || "New Document", + groups: partial?.groups || [{ + groupId: 'main', + elements: ['# Welcome\n\nStart building your interactive document.'] + }], + dataLoaders: partial?.dataLoaders || [], + variables: partial?.variables || [], + style: partial?.style, + resources: partial?.resources, + notes: partial?.notes, + ...partial + }; + } + + // Private helper for immutable updates + private clone(updates: Partial): ChartifactBuilder { + return new ChartifactBuilder({ + ...this.doc, + ...updates + }); + } +} +``` + +## Document-Level Operations + +```typescript +class ChartifactBuilder { + // ... previous code ... + + /** + * Set the document title + */ + setTitle(title: string): ChartifactBuilder { + return this.clone({ title }); + } + + /** + * Set or update the CSS styles + */ + setCSS(css: string | string[]): ChartifactBuilder { + return this.clone({ + style: { + ...this.doc.style, + css + } + }); + } + + /** + * Add CSS to existing styles + */ + addCSS(css: string): ChartifactBuilder { + const existingCSS = this.doc.style?.css || ''; + const newCSS = Array.isArray(existingCSS) + ? [...existingCSS, css] + : [existingCSS, css]; + return this.setCSS(newCSS); + } + + /** + * Set Google Fonts configuration + */ + setGoogleFonts(config: GoogleFontsSpec): ChartifactBuilder { + return this.clone({ + style: { + ...this.doc.style, + css: this.doc.style?.css || '', + googleFonts: config + } + }); + } + + /** + * Add a note to the document + */ + addNote(note: string): ChartifactBuilder { + return this.clone({ + notes: [...(this.doc.notes || []), note] + }); + } + + /** + * Clear all notes + */ + clearNotes(): ChartifactBuilder { + return this.clone({ notes: [] }); + } +} +``` + +## Group Operations + +```typescript +class ChartifactBuilder { + // ... previous code ... + + /** + * Add a new group to the document + */ + addGroup(groupId: string, elements: PageElement[] = []): ChartifactBuilder { + // Validate groupId doesn't already exist + if (this.doc.groups.some(g => g.groupId === groupId)) { + throw new Error(`Group '${groupId}' already exists`); + } + + return this.clone({ + groups: [ + ...this.doc.groups, + { groupId, elements } + ] + }); + } + + /** + * Insert a group at a specific index + */ + insertGroup(index: number, groupId: string, elements: PageElement[] = []): ChartifactBuilder { + if (this.doc.groups.some(g => g.groupId === groupId)) { + throw new Error(`Group '${groupId}' already exists`); + } + + const groups = [...this.doc.groups]; + groups.splice(index, 0, { groupId, elements }); + + return this.clone({ groups }); + } + + /** + * Remove a group by ID + */ + deleteGroup(groupId: string): ChartifactBuilder { + const groups = this.doc.groups.filter(g => g.groupId !== groupId); + + if (groups.length === this.doc.groups.length) { + throw new Error(`Group '${groupId}' not found`); + } + + return this.clone({ groups }); + } + + /** + * Rename a group + */ + renameGroup(oldGroupId: string, newGroupId: string): ChartifactBuilder { + if (this.doc.groups.some(g => g.groupId === newGroupId)) { + throw new Error(`Group '${newGroupId}' already exists`); + } + + const groups = this.doc.groups.map(g => + g.groupId === oldGroupId + ? { ...g, groupId: newGroupId } + : g + ); + + if (groups.every(g => g.groupId !== newGroupId)) { + throw new Error(`Group '${oldGroupId}' not found`); + } + + return this.clone({ groups }); + } + + /** + * Move a group to a new position + */ + moveGroup(groupId: string, toIndex: number): ChartifactBuilder { + const fromIndex = this.doc.groups.findIndex(g => g.groupId === groupId); + + if (fromIndex === -1) { + throw new Error(`Group '${groupId}' not found`); + } + + const groups = [...this.doc.groups]; + const [group] = groups.splice(fromIndex, 1); + groups.splice(toIndex, 0, group); + + return this.clone({ groups }); + } + + /** + * Get a group by ID (for inspection) + */ + getGroup(groupId: string): ElementGroup | undefined { + return this.doc.groups.find(g => g.groupId === groupId); + } + + /** + * Check if a group exists + */ + hasGroup(groupId: string): boolean { + return this.doc.groups.some(g => g.groupId === groupId); + } +} +``` + +## Element Operations + +```typescript +class ChartifactBuilder { + // ... previous code ... + + /** + * Add an element to a group + */ + addElement(groupId: string, element: PageElement): ChartifactBuilder { + const groups = this.doc.groups.map(g => + g.groupId === groupId + ? { ...g, elements: [...g.elements, element] } + : g + ); + + if (groups === this.doc.groups) { + throw new Error(`Group '${groupId}' not found`); + } + + return this.clone({ groups }); + } + + /** + * Add multiple elements to a group + */ + addElements(groupId: string, elements: PageElement[]): ChartifactBuilder { + const groups = this.doc.groups.map(g => + g.groupId === groupId + ? { ...g, elements: [...g.elements, ...elements] } + : g + ); + + if (groups === this.doc.groups) { + throw new Error(`Group '${groupId}' not found`); + } + + return this.clone({ groups }); + } + + /** + * Insert an element at a specific index in a group + */ + insertElement(groupId: string, index: number, element: PageElement): ChartifactBuilder { + const groups = this.doc.groups.map(g => { + if (g.groupId === groupId) { + const elements = [...g.elements]; + elements.splice(index, 0, element); + return { ...g, elements }; + } + return g; + }); + + if (groups === this.doc.groups) { + throw new Error(`Group '${groupId}' not found`); + } + + return this.clone({ groups }); + } + + /** + * Remove an element by index from a group + */ + deleteElement(groupId: string, elementIndex: number): ChartifactBuilder { + const groups = this.doc.groups.map(g => { + if (g.groupId === groupId) { + if (elementIndex < 0 || elementIndex >= g.elements.length) { + throw new Error(`Element index ${elementIndex} out of bounds in group '${groupId}'`); + } + const elements = g.elements.filter((_, i) => i !== elementIndex); + return { ...g, elements }; + } + return g; + }); + + if (groups === this.doc.groups) { + throw new Error(`Group '${groupId}' not found`); + } + + return this.clone({ groups }); + } + + /** + * Update an element at a specific index + */ + updateElement(groupId: string, elementIndex: number, element: PageElement): ChartifactBuilder { + const groups = this.doc.groups.map(g => { + if (g.groupId === groupId) { + if (elementIndex < 0 || elementIndex >= g.elements.length) { + throw new Error(`Element index ${elementIndex} out of bounds in group '${groupId}'`); + } + const elements = [...g.elements]; + elements[elementIndex] = element; + return { ...g, elements }; + } + return g; + }); + + if (groups === this.doc.groups) { + throw new Error(`Group '${groupId}' not found`); + } + + return this.clone({ groups }); + } + + /** + * Clear all elements from a group + */ + clearElements(groupId: string): ChartifactBuilder { + const groups = this.doc.groups.map(g => + g.groupId === groupId + ? { ...g, elements: [] } + : g + ); + + if (groups === this.doc.groups) { + throw new Error(`Group '${groupId}' not found`); + } + + return this.clone({ groups }); + } + + /** + * Add markdown text as an element + */ + addMarkdown(groupId: string, markdown: string): ChartifactBuilder { + return this.addElement(groupId, markdown); + } + + /** + * Add a chart element + */ + addChart(groupId: string, chartKey: string): ChartifactBuilder { + return this.addElement(groupId, { + type: 'chart', + chartKey + }); + } + + /** + * Add a checkbox element + */ + addCheckbox( + groupId: string, + variableId: string, + options?: { label?: string } + ): ChartifactBuilder { + return this.addElement(groupId, { + type: 'checkbox', + variableId, + ...options + }); + } + + /** + * Add a dropdown element + */ + addDropdown( + groupId: string, + variableId: string, + options: string[] | { dataSourceName: string; fieldName: string }, + config?: { label?: string; multiple?: boolean; size?: number } + ): ChartifactBuilder { + const element: DropdownElement = { + type: 'dropdown', + variableId, + ...(Array.isArray(options) + ? { options } + : { dynamicOptions: options } + ), + ...config + }; + return this.addElement(groupId, element); + } + + /** + * Add a slider element + */ + addSlider( + groupId: string, + variableId: string, + min: number, + max: number, + step: number, + options?: { label?: string } + ): ChartifactBuilder { + return this.addElement(groupId, { + type: 'slider', + variableId, + min, + max, + step, + ...options + }); + } + + /** + * Add a textbox element + */ + addTextbox( + groupId: string, + variableId: string, + options?: { label?: string; multiline?: boolean; placeholder?: string } + ): ChartifactBuilder { + return this.addElement(groupId, { + type: 'textbox', + variableId, + ...options + }); + } + + /** + * Add a number input element + */ + addNumber( + groupId: string, + variableId: string, + options?: { label?: string; min?: number; max?: number; step?: number; placeholder?: string } + ): ChartifactBuilder { + return this.addElement(groupId, { + type: 'number', + variableId, + ...options + }); + } + + /** + * Add a tabulator (table) element + */ + addTable( + groupId: string, + dataSourceName: string, + options?: { + variableId?: string; + editable?: boolean; + tabulatorOptions?: object + } + ): ChartifactBuilder { + return this.addElement(groupId, { + type: 'tabulator', + dataSourceName, + ...options + }); + } + + /** + * Add an image element + */ + addImage( + groupId: string, + url: string, + options?: { alt?: string; height?: number; width?: number } + ): ChartifactBuilder { + return this.addElement(groupId, { + type: 'image', + url, + ...options + }); + } + + /** + * Add a mermaid diagram element + */ + addMermaid( + groupId: string, + diagramText: string, + options?: { variableId?: string } + ): ChartifactBuilder { + return this.addElement(groupId, { + type: 'mermaid', + diagramText, + ...options + }); + } +} +``` + +## Data & Variable Operations + +```typescript +class ChartifactBuilder { + // ... previous code ... + + /** + * Add a variable to the document + */ + addVariable(variable: Variable): ChartifactBuilder { + // Validate variable ID doesn't already exist + if (this.doc.variables?.some(v => v.variableId === variable.variableId)) { + throw new Error(`Variable '${variable.variableId}' already exists`); + } + + return this.clone({ + variables: [...(this.doc.variables || []), variable] + }); + } + + /** + * Update an existing variable + */ + updateVariable(variableId: string, updates: Partial): ChartifactBuilder { + const variables = (this.doc.variables || []).map(v => + v.variableId === variableId + ? { ...v, ...updates } + : v + ); + + if (variables === this.doc.variables) { + throw new Error(`Variable '${variableId}' not found`); + } + + return this.clone({ variables }); + } + + /** + * Remove a variable + */ + deleteVariable(variableId: string): ChartifactBuilder { + const variables = (this.doc.variables || []).filter( + v => v.variableId !== variableId + ); + + if (variables.length === this.doc.variables?.length) { + throw new Error(`Variable '${variableId}' not found`); + } + + return this.clone({ variables }); + } + + /** + * Add a data loader + */ + addDataLoader(dataLoader: DataLoader): ChartifactBuilder { + // Validate dataSourceName doesn't already exist + const name = 'dataSourceName' in dataLoader ? dataLoader.dataSourceName : undefined; + if (name && this.doc.dataLoaders?.some(dl => 'dataSourceName' in dl && dl.dataSourceName === name)) { + throw new Error(`Data loader '${name}' already exists`); + } + + return this.clone({ + dataLoaders: [...(this.doc.dataLoaders || []), dataLoader] + }); + } + + /** + * Add an inline data source + */ + addInlineData( + dataSourceName: string, + content: object[] | string | string[], + options?: { format?: 'json' | 'csv' | 'tsv' | 'dsv'; delimiter?: string } + ): ChartifactBuilder { + return this.addDataLoader({ + type: 'inline', + dataSourceName, + content, + ...options + }); + } + + /** + * Add a URL-based data source + */ + addURLData( + dataSourceName: string, + url: string, + options?: { format?: 'json' | 'csv' | 'tsv' | 'dsv' } + ): ChartifactBuilder { + return this.addDataLoader({ + type: 'url', + dataSourceName, + url, + ...options + }); + } + + /** + * Remove a data loader + */ + deleteDataLoader(dataSourceName: string): ChartifactBuilder { + const dataLoaders = (this.doc.dataLoaders || []).filter( + dl => !('dataSourceName' in dl) || dl.dataSourceName !== dataSourceName + ); + + if (dataLoaders.length === this.doc.dataLoaders?.length) { + throw new Error(`Data loader '${dataSourceName}' not found`); + } + + return this.clone({ dataLoaders }); + } +} +``` + +## Chart Resources Operations + +```typescript +class ChartifactBuilder { + // ... previous code ... + + /** + * Add a chart specification to resources + */ + addChartSpec(chartKey: string, spec: object): ChartifactBuilder { + const charts = { + ...(this.doc.resources?.charts || {}), + [chartKey]: spec + }; + + return this.clone({ + resources: { + ...this.doc.resources, + charts + } + }); + } + + /** + * Update an existing chart specification + */ + updateChartSpec(chartKey: string, spec: object): ChartifactBuilder { + if (!this.doc.resources?.charts?.[chartKey]) { + throw new Error(`Chart '${chartKey}' not found`); + } + + return this.addChartSpec(chartKey, spec); + } + + /** + * Remove a chart specification + */ + deleteChartSpec(chartKey: string): ChartifactBuilder { + if (!this.doc.resources?.charts?.[chartKey]) { + throw new Error(`Chart '${chartKey}' not found`); + } + + const { [chartKey]: _, ...charts } = this.doc.resources.charts; + + return this.clone({ + resources: { + ...this.doc.resources, + charts + } + }); + } +} +``` + +## Usage Examples + +### Example 1: Create a Simple Dashboard + +```typescript +const builder = new ChartifactBuilder() + .setTitle('Sales Dashboard') + .setCSS(` + body { font-family: sans-serif; } + .container { max-width: 1200px; margin: 0 auto; } + `) + .addMarkdown('main', '# Sales Dashboard\n\nView key metrics below.') + .addInlineData('sales', [ + { month: 'Jan', revenue: 10000, profit: 2000 }, + { month: 'Feb', revenue: 12000, profit: 2400 }, + { month: 'Mar', revenue: 15000, profit: 3000 } + ]) + .addChartSpec('revenueChart', { + $schema: 'https://vega.github.io/schema/vega-lite/v5.json', + data: { name: 'sales' }, + mark: 'bar', + encoding: { + x: { field: 'month', type: 'ordinal' }, + y: { field: 'revenue', type: 'quantitative' } + } + }) + .addChart('main', 'revenueChart'); + +// Save to file +await builder.toFile('./sales-dashboard.idoc.json'); +``` + +### Example 2: Add Interactive Controls + +```typescript +const builder = ChartifactBuilder.fromDocument(existingDoc) + .addGroup('controls', []) + .addMarkdown('controls', '## Settings') + .addVariable({ + variableId: 'yearFilter', + type: 'number', + initialValue: 2024 + }) + .addSlider('controls', 'yearFilter', 2020, 2024, 1, { + label: 'Select Year' + }) + .addVariable({ + variableId: 'showDetails', + type: 'boolean', + initialValue: false + }) + .addCheckbox('controls', 'showDetails', { + label: 'Show detailed view' + }); +``` + +### Example 3: Modify Existing Document + +```typescript +// Load from file +const builder = await ChartifactBuilder.fromFile('./document.idoc.json'); + +// Make modifications +const updated = builder + .setTitle('Updated Title') + .deleteElement('main', 0) + .addMarkdown('main', '# New Header') + .addGroup('footer', ['Built with Chartifact']) + .addNote('Updated on ' + new Date().toISOString()); + +// Save back +await updated.toFile('./document.idoc.json'); +``` + +### Example 4: Chaining Operations + +```typescript +const doc = new ChartifactBuilder({ title: 'My Report' }) + .addGroup('header') + .addMarkdown('header', '# Annual Report 2024') + .addMarkdown('header', 'Executive Summary') + .addGroup('metrics') + .addInlineData('kpis', [ + { name: 'Revenue', value: 1000000 }, + { name: 'Growth', value: 15 } + ]) + .addTable('metrics', 'kpis', { editable: false }) + .addGroup('charts') + .addChartSpec('trend', { /* vega spec */ }) + .addChart('charts', 'trend') + .setCSS('.header { text-align: center; }') + .toJSON(); +``` + +## Validation & Error Handling + +The builder includes validation at each operation: + +```typescript +// Validation examples +try { + builder + .addGroup('main', []) // Error: 'main' already exists + .deleteGroup('nonexistent') // Error: Group not found + .deleteElement('main', 999) // Error: Index out of bounds + .addVariable({ /* duplicate variableId */ }); // Error: Variable exists +} catch (error) { + console.error('Builder operation failed:', error.message); +} +``` + +## MCP Tool Mapping + +Each builder method maps naturally to an MCP tool: + +| MCP Tool | Builder Method | Parameters | +|----------|---------------|------------| +| `create_document` | `new ChartifactBuilder()` | `{ title?, groups?, ... }` | +| `set_title` | `.setTitle()` | `{ title }` | +| `set_css` | `.setCSS()` | `{ css }` | +| `add_group` | `.addGroup()` | `{ groupId, elements? }` | +| `delete_group` | `.deleteGroup()` | `{ groupId }` | +| `add_element` | `.addElement()` | `{ groupId, element }` | +| `add_markdown` | `.addMarkdown()` | `{ groupId, markdown }` | +| `add_chart` | `.addChart()` | `{ groupId, chartKey }` | +| `add_variable` | `.addVariable()` | `{ variable }` | +| `add_data_loader` | `.addDataLoader()` | `{ dataLoader }` | +| `add_chart_spec` | `.addChartSpec()` | `{ chartKey, spec }` | +| `to_json` | `.toJSON()` | `{}` | +| `save_file` | `.toFile()` | `{ path }` | +| `load_file` | `ChartifactBuilder.fromFile()` | `{ path }` | + +## Implementation Notes + +### Immutability +All operations return new builder instances. This enables: +- Undo/redo by keeping history +- Safe concurrent operations +- Easier testing and debugging + +### Type Safety +TypeScript types ensure: +- Valid element types +- Required properties present +- Correct data structures +- IDE autocomplete support + +### Performance +- Shallow copying for immutability +- JSON serialization only when needed +- Validation is O(n) or better +- No deep cloning unless necessary + +### Extension Points +The builder can be extended: +- Custom validators +- Plugin operations +- Custom element types +- Lifecycle hooks + +## Next Steps + +1. **Implement core builder** (~1 day) + - Document operations + - Group operations + - Element operations + - Validation + +2. **Add file I/O** (~0.5 day) + - Load from filesystem + - Save to filesystem + - Error handling + +3. **Create MCP wrapper** (~0.5 day) + - Tool definitions + - Parameter schemas + - Error mapping + - State management + +4. **Testing & Documentation** (ongoing) + - Unit tests for each operation + - Integration examples + - API documentation + - Usage patterns + +Total estimated effort: **1-2 days** From 0a80944b26d239031038febd036cc2b0eadf82c7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 10 Oct 2025 16:53:25 +0000 Subject: [PATCH 05/11] Revise Builder API to svelte version - remove type-specific helpers, add getters, defer I/O Co-authored-by: danmarshall <11507384+danmarshall@users.noreply.github.com> --- docs/research/builder-api-sketch.md | 377 +++++++++++----------------- 1 file changed, 151 insertions(+), 226 deletions(-) diff --git a/docs/research/builder-api-sketch.md b/docs/research/builder-api-sketch.md index e66ddd6b..26a95601 100644 --- a/docs/research/builder-api-sketch.md +++ b/docs/research/builder-api-sketch.md @@ -1,7 +1,7 @@ # Chartifact Document Builder API Sketch -**Date:** October 2025 -**Status:** API Design Proposal +**Date:** October 2025 (Revised) +**Status:** API Design Proposal - Svelte Version ## Overview @@ -12,6 +12,7 @@ This document sketches out the API for a lightweight transactional builder for C 3. **Sensible defaults** - Documents start with working structure 4. **Chainable API** - Fluent interface for ergonomic usage 5. **Validation** - Errors caught at operation time, not render time +6. **Minimal surface area** - Generic operations only, no type-specific helpers to reduce maintenance overhead ## Core Builder Class @@ -65,21 +66,10 @@ class ChartifactBuilder { return new ChartifactBuilder(JSON.parse(json)); } - /** - * Load a document from a file path - */ - static async fromFile(path: string): Promise { - // Implementation would use fs.readFile - throw new Error('Not implemented'); - } - - /** - * Save the current document to a file path - */ - async toFile(path: string): Promise { - // Implementation would use fs.writeFile - throw new Error('Not implemented'); - } + // Note: File I/O operations (fromFile, toFile) are intentionally omitted. + // These should be handled by a dedicated I/O MCP server or external utilities. + // To save: write builder.toString() or builder.toJSON() to file + // To load: read file and use ChartifactBuilder.fromJSON() or fromDocument() // Private helper to create default document structure private createDefault(partial?: Partial): InteractiveDocument { @@ -114,6 +104,13 @@ class ChartifactBuilder { class ChartifactBuilder { // ... previous code ... + /** + * Get the document title + */ + getTitle(): string { + return this.doc.title; + } + /** * Set the document title */ @@ -121,6 +118,13 @@ class ChartifactBuilder { return this.clone({ title }); } + /** + * Get the CSS styles + */ + getCSS(): string | string[] | undefined { + return this.doc.style?.css; + } + /** * Set or update the CSS styles */ @@ -144,6 +148,13 @@ class ChartifactBuilder { return this.setCSS(newCSS); } + /** + * Get Google Fonts configuration + */ + getGoogleFonts(): GoogleFontsSpec | undefined { + return this.doc.style?.googleFonts; + } + /** * Set Google Fonts configuration */ @@ -157,6 +168,13 @@ class ChartifactBuilder { }); } + /** + * Get all notes + */ + getNotes(): string[] { + return this.doc.notes || []; + } + /** * Add a note to the document */ @@ -181,6 +199,27 @@ class ChartifactBuilder { class ChartifactBuilder { // ... previous code ... + /** + * Get all groups + */ + getGroups(): ElementGroup[] { + return [...this.doc.groups]; + } + + /** + * Get a group by ID + */ + getGroup(groupId: string): ElementGroup | undefined { + return this.doc.groups.find(g => g.groupId === groupId); + } + + /** + * Check if a group exists + */ + hasGroup(groupId: string): boolean { + return this.doc.groups.some(g => g.groupId === groupId); + } + /** * Add a new group to the document */ @@ -262,29 +301,44 @@ class ChartifactBuilder { return this.clone({ groups }); } - - /** - * Get a group by ID (for inspection) - */ - getGroup(groupId: string): ElementGroup | undefined { - return this.doc.groups.find(g => g.groupId === groupId); - } - - /** - * Check if a group exists - */ - hasGroup(groupId: string): boolean { - return this.doc.groups.some(g => g.groupId === groupId); - } } ``` ## Element Operations +**Philosophy: Keep it svelte.** We provide only generic element operations. +Type-specific helpers (addMarkdown, addChart, etc.) are intentionally omitted to minimize maintenance overhead. +Users should construct element objects directly and use the generic operations. + ```typescript class ChartifactBuilder { // ... previous code ... + /** + * Get all elements from a group + */ + getElements(groupId: string): PageElement[] { + const group = this.doc.groups.find(g => g.groupId === groupId); + if (!group) { + throw new Error(`Group '${groupId}' not found`); + } + return [...group.elements]; + } + + /** + * Get a single element by index from a group + */ + getElement(groupId: string, elementIndex: number): PageElement { + const group = this.doc.groups.find(g => g.groupId === groupId); + if (!group) { + throw new Error(`Group '${groupId}' not found`); + } + if (elementIndex < 0 || elementIndex >= group.elements.length) { + throw new Error(`Element index ${elementIndex} out of bounds in group '${groupId}'`); + } + return group.elements[elementIndex]; + } + /** * Add an element to a group */ @@ -400,167 +454,41 @@ class ChartifactBuilder { return this.clone({ groups }); } +} - /** - * Add markdown text as an element - */ - addMarkdown(groupId: string, markdown: string): ChartifactBuilder { - return this.addElement(groupId, markdown); - } +// Example usage with generic operations: +// For markdown (string): +builder.addElement('main', '# Hello World') - /** - * Add a chart element - */ - addChart(groupId: string, chartKey: string): ChartifactBuilder { - return this.addElement(groupId, { - type: 'chart', - chartKey - }); - } +// For a chart: +builder.addElement('main', { type: 'chart', chartKey: 'myChart' }) - /** - * Add a checkbox element - */ - addCheckbox( - groupId: string, - variableId: string, - options?: { label?: string } - ): ChartifactBuilder { - return this.addElement(groupId, { - type: 'checkbox', - variableId, - ...options - }); - } - - /** - * Add a dropdown element - */ - addDropdown( - groupId: string, - variableId: string, - options: string[] | { dataSourceName: string; fieldName: string }, - config?: { label?: string; multiple?: boolean; size?: number } - ): ChartifactBuilder { - const element: DropdownElement = { - type: 'dropdown', - variableId, - ...(Array.isArray(options) - ? { options } - : { dynamicOptions: options } - ), - ...config - }; - return this.addElement(groupId, element); - } - - /** - * Add a slider element - */ - addSlider( - groupId: string, - variableId: string, - min: number, - max: number, - step: number, - options?: { label?: string } - ): ChartifactBuilder { - return this.addElement(groupId, { - type: 'slider', - variableId, - min, - max, - step, - ...options - }); - } +// For a checkbox: +builder.addElement('main', { type: 'checkbox', variableId: 'showDetails', label: 'Show Details' }) - /** - * Add a textbox element - */ - addTextbox( - groupId: string, - variableId: string, - options?: { label?: string; multiline?: boolean; placeholder?: string } - ): ChartifactBuilder { - return this.addElement(groupId, { - type: 'textbox', - variableId, - ...options - }); - } +// For a slider: +builder.addElement('main', { type: 'slider', variableId: 'year', min: 2020, max: 2024, step: 1 }) +``` - /** - * Add a number input element - */ - addNumber( - groupId: string, - variableId: string, - options?: { label?: string; min?: number; max?: number; step?: number; placeholder?: string } - ): ChartifactBuilder { - return this.addElement(groupId, { - type: 'number', - variableId, - ...options - }); - } +## Data & Variable Operations - /** - * Add a tabulator (table) element - */ - addTable( - groupId: string, - dataSourceName: string, - options?: { - variableId?: string; - editable?: boolean; - tabulatorOptions?: object - } - ): ChartifactBuilder { - return this.addElement(groupId, { - type: 'tabulator', - dataSourceName, - ...options - }); - } +```typescript +class ChartifactBuilder { + // ... previous code ... /** - * Add an image element + * Get all variables */ - addImage( - groupId: string, - url: string, - options?: { alt?: string; height?: number; width?: number } - ): ChartifactBuilder { - return this.addElement(groupId, { - type: 'image', - url, - ...options - }); + getVariables(): Variable[] { + return [...(this.doc.variables || [])]; } /** - * Add a mermaid diagram element + * Get a specific variable by ID */ - addMermaid( - groupId: string, - diagramText: string, - options?: { variableId?: string } - ): ChartifactBuilder { - return this.addElement(groupId, { - type: 'mermaid', - diagramText, - ...options - }); + getVariable(variableId: string): Variable | undefined { + return this.doc.variables?.find(v => v.variableId === variableId); } -} -``` - -## Data & Variable Operations - -```typescript -class ChartifactBuilder { - // ... previous code ... /** * Add a variable to the document @@ -608,6 +536,22 @@ class ChartifactBuilder { return this.clone({ variables }); } + /** + * Get all data loaders + */ + getDataLoaders(): DataLoader[] { + return [...(this.doc.dataLoaders || [])]; + } + + /** + * Get a specific data loader by name + */ + getDataLoader(dataSourceName: string): DataLoader | undefined { + return this.doc.dataLoaders?.find( + dl => 'dataSourceName' in dl && dl.dataSourceName === dataSourceName + ); + } + /** * Add a data loader */ @@ -623,38 +567,6 @@ class ChartifactBuilder { }); } - /** - * Add an inline data source - */ - addInlineData( - dataSourceName: string, - content: object[] | string | string[], - options?: { format?: 'json' | 'csv' | 'tsv' | 'dsv'; delimiter?: string } - ): ChartifactBuilder { - return this.addDataLoader({ - type: 'inline', - dataSourceName, - content, - ...options - }); - } - - /** - * Add a URL-based data source - */ - addURLData( - dataSourceName: string, - url: string, - options?: { format?: 'json' | 'csv' | 'tsv' | 'dsv' } - ): ChartifactBuilder { - return this.addDataLoader({ - type: 'url', - dataSourceName, - url, - ...options - }); - } - /** * Remove a data loader */ @@ -672,16 +584,40 @@ class ChartifactBuilder { } ``` -## Chart Resources Operations +## Resources Operations + +**Note:** Renamed from "Chart Resources" to just "Resources" for generality. +Resources can contain charts and potentially other types in the future. ```typescript class ChartifactBuilder { // ... previous code ... /** - * Add a chart specification to resources + * Get all resources + */ + getResources(): InteractiveDocument['resources'] { + return this.doc.resources ? { ...this.doc.resources } : undefined; + } + + /** + * Get all chart specifications + */ + getCharts(): Record { + return { ...(this.doc.resources?.charts || {}) }; + } + + /** + * Get a specific chart specification */ - addChartSpec(chartKey: string, spec: object): ChartifactBuilder { + getChart(chartKey: string): object | undefined { + return this.doc.resources?.charts?.[chartKey]; + } + + /** + * Add or update a chart specification in resources + */ + setChart(chartKey: string, spec: object): ChartifactBuilder { const charts = { ...(this.doc.resources?.charts || {}), [chartKey]: spec @@ -696,20 +632,9 @@ class ChartifactBuilder { } /** - * Update an existing chart specification - */ - updateChartSpec(chartKey: string, spec: object): ChartifactBuilder { - if (!this.doc.resources?.charts?.[chartKey]) { - throw new Error(`Chart '${chartKey}' not found`); - } - - return this.addChartSpec(chartKey, spec); - } - - /** - * Remove a chart specification + * Remove a chart specification from resources */ - deleteChartSpec(chartKey: string): ChartifactBuilder { + deleteChart(chartKey: string): ChartifactBuilder { if (!this.doc.resources?.charts?.[chartKey]) { throw new Error(`Chart '${chartKey}' not found`); } From 715cd68d88586e509e3b27f2840d9d01344839c8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 10 Oct 2025 16:56:19 +0000 Subject: [PATCH 06/11] Addressing PR comments Co-authored-by: danmarshall <11507384+danmarshall@users.noreply.github.com> --- docs/research/builder-api-sketch.md | 175 ++++++++++++++++++++-------- 1 file changed, 125 insertions(+), 50 deletions(-) diff --git a/docs/research/builder-api-sketch.md b/docs/research/builder-api-sketch.md index 26a95601..94a23bbd 100644 --- a/docs/research/builder-api-sketch.md +++ b/docs/research/builder-api-sketch.md @@ -662,13 +662,17 @@ const builder = new ChartifactBuilder() body { font-family: sans-serif; } .container { max-width: 1200px; margin: 0 auto; } `) - .addMarkdown('main', '# Sales Dashboard\n\nView key metrics below.') - .addInlineData('sales', [ - { month: 'Jan', revenue: 10000, profit: 2000 }, - { month: 'Feb', revenue: 12000, profit: 2400 }, - { month: 'Mar', revenue: 15000, profit: 3000 } - ]) - .addChartSpec('revenueChart', { + .addElement('main', '# Sales Dashboard\n\nView key metrics below.') + .addDataLoader({ + type: 'inline', + dataSourceName: 'sales', + content: [ + { month: 'Jan', revenue: 10000, profit: 2000 }, + { month: 'Feb', revenue: 12000, profit: 2400 }, + { month: 'Mar', revenue: 15000, profit: 3000 } + ] + }) + .setChart('revenueChart', { $schema: 'https://vega.github.io/schema/vega-lite/v5.json', data: { name: 'sales' }, mark: 'bar', @@ -677,10 +681,11 @@ const builder = new ChartifactBuilder() y: { field: 'revenue', type: 'quantitative' } } }) - .addChart('main', 'revenueChart'); + .addElement('main', { type: 'chart', chartKey: 'revenueChart' }); -// Save to file -await builder.toFile('./sales-dashboard.idoc.json'); +// Export to JSON string for saving (use external I/O utility) +const json = builder.toString(); +// Or get as object: const doc = builder.toJSON(); ``` ### Example 2: Add Interactive Controls @@ -688,13 +693,18 @@ await builder.toFile('./sales-dashboard.idoc.json'); ```typescript const builder = ChartifactBuilder.fromDocument(existingDoc) .addGroup('controls', []) - .addMarkdown('controls', '## Settings') + .addElement('controls', '## Settings') .addVariable({ variableId: 'yearFilter', type: 'number', initialValue: 2024 }) - .addSlider('controls', 'yearFilter', 2020, 2024, 1, { + .addElement('controls', { + type: 'slider', + variableId: 'yearFilter', + min: 2020, + max: 2024, + step: 1, label: 'Select Year' }) .addVariable({ @@ -702,7 +712,9 @@ const builder = ChartifactBuilder.fromDocument(existingDoc) type: 'boolean', initialValue: false }) - .addCheckbox('controls', 'showDetails', { + .addElement('controls', { + type: 'checkbox', + variableId: 'showDetails', label: 'Show detailed view' }); ``` @@ -710,19 +722,21 @@ const builder = ChartifactBuilder.fromDocument(existingDoc) ### Example 3: Modify Existing Document ```typescript -// Load from file -const builder = await ChartifactBuilder.fromFile('./document.idoc.json'); +// Load from JSON string (external I/O handled separately) +const jsonString = readFileSync('./document.idoc.json', 'utf-8'); +const builder = ChartifactBuilder.fromJSON(jsonString); // Make modifications const updated = builder .setTitle('Updated Title') .deleteElement('main', 0) - .addMarkdown('main', '# New Header') + .addElement('main', '# New Header') .addGroup('footer', ['Built with Chartifact']) .addNote('Updated on ' + new Date().toISOString()); -// Save back -await updated.toFile('./document.idoc.json'); +// Export (external I/O handled separately) +const updatedJson = updated.toString(); +// writeFileSync('./document.idoc.json', updatedJson); ``` ### Example 4: Chaining Operations @@ -730,21 +744,49 @@ await updated.toFile('./document.idoc.json'); ```typescript const doc = new ChartifactBuilder({ title: 'My Report' }) .addGroup('header') - .addMarkdown('header', '# Annual Report 2024') - .addMarkdown('header', 'Executive Summary') + .addElement('header', '# Annual Report 2024') + .addElement('header', 'Executive Summary') .addGroup('metrics') - .addInlineData('kpis', [ - { name: 'Revenue', value: 1000000 }, - { name: 'Growth', value: 15 } - ]) - .addTable('metrics', 'kpis', { editable: false }) + .addDataLoader({ + type: 'inline', + dataSourceName: 'kpis', + content: [ + { name: 'Revenue', value: 1000000 }, + { name: 'Growth', value: 15 } + ] + }) + .addElement('metrics', { + type: 'tabulator', + dataSourceName: 'kpis', + editable: false + }) .addGroup('charts') - .addChartSpec('trend', { /* vega spec */ }) - .addChart('charts', 'trend') + .setChart('trend', { /* vega spec */ }) + .addElement('charts', { type: 'chart', chartKey: 'trend' }) .setCSS('.header { text-align: center; }') .toJSON(); ``` +## Getter Strategy + +**Strategy:** Provide getters for inspection without mutation. + +- **For simple values:** Return the value directly (e.g., `getTitle()` returns string) +- **For collections:** Return a shallow copy to prevent external mutation (e.g., `getGroups()` returns `[...groups]`) +- **For nested objects:** Return a shallow copy of the top-level object +- **Primary access:** Use `toJSON()` to get complete document when MCP needs full state + +**Getter Categories:** +1. **Document-level:** `getTitle()`, `getCSS()`, `getGoogleFonts()`, `getNotes()` +2. **Groups:** `getGroups()`, `getGroup(id)`, `hasGroup(id)` +3. **Elements:** `getElements(groupId)`, `getElement(groupId, index)` +4. **Variables:** `getVariables()`, `getVariable(id)` +5. **Data:** `getDataLoaders()`, `getDataLoader(name)` +6. **Resources:** `getResources()`, `getCharts()`, `getChart(key)` +7. **Complete document:** `toJSON()` - returns full InteractiveDocument + +**MCP Usage:** MCP tools can call `toJSON()` after any operation to return the complete updated document to the LLM. + ## Validation & Error Handling The builder includes validation at each operation: @@ -764,27 +806,48 @@ try { ## MCP Tool Mapping -Each builder method maps naturally to an MCP tool: +Each builder method maps naturally to an MCP tool. Note the svelte approach - generic operations only: | MCP Tool | Builder Method | Parameters | |----------|---------------|------------| | `create_document` | `new ChartifactBuilder()` | `{ title?, groups?, ... }` | +| `from_json` | `ChartifactBuilder.fromJSON()` | `{ json }` | +| `to_json` | `.toJSON()` | `{}` | +| `get_title` | `.getTitle()` | `{}` | | `set_title` | `.setTitle()` | `{ title }` | +| `get_css` | `.getCSS()` | `{}` | | `set_css` | `.setCSS()` | `{ css }` | +| `add_css` | `.addCSS()` | `{ css }` | +| `get_groups` | `.getGroups()` | `{}` | +| `get_group` | `.getGroup()` | `{ groupId }` | | `add_group` | `.addGroup()` | `{ groupId, elements? }` | | `delete_group` | `.deleteGroup()` | `{ groupId }` | +| `rename_group` | `.renameGroup()` | `{ oldGroupId, newGroupId }` | +| `get_elements` | `.getElements()` | `{ groupId }` | | `add_element` | `.addElement()` | `{ groupId, element }` | -| `add_markdown` | `.addMarkdown()` | `{ groupId, markdown }` | -| `add_chart` | `.addChart()` | `{ groupId, chartKey }` | +| `delete_element` | `.deleteElement()` | `{ groupId, elementIndex }` | +| `update_element` | `.updateElement()` | `{ groupId, elementIndex, element }` | +| `get_variables` | `.getVariables()` | `{}` | | `add_variable` | `.addVariable()` | `{ variable }` | +| `delete_variable` | `.deleteVariable()` | `{ variableId }` | +| `get_data_loaders` | `.getDataLoaders()` | `{}` | | `add_data_loader` | `.addDataLoader()` | `{ dataLoader }` | -| `add_chart_spec` | `.addChartSpec()` | `{ chartKey, spec }` | -| `to_json` | `.toJSON()` | `{}` | -| `save_file` | `.toFile()` | `{ path }` | -| `load_file` | `ChartifactBuilder.fromFile()` | `{ path }` | +| `delete_data_loader` | `.deleteDataLoader()` | `{ dataSourceName }` | +| `get_chart` | `.getChart()` | `{ chartKey }` | +| `set_chart` | `.setChart()` | `{ chartKey, spec }` | +| `delete_chart` | `.deleteChart()` | `{ chartKey }` | + +**Note on I/O:** File operations (save, load) are handled by a dedicated I/O MCP server, not the builder. ## Implementation Notes +### Svelte Philosophy +**Keep maintenance overhead minimal:** +- Generic operations only (no type-specific helpers like `addMarkdown`, `addChart`, etc.) +- Users construct element objects directly +- Reduces API surface area +- Less code to maintain as schema evolves + ### Immutability All operations return new builder instances. This enables: - Undo/redo by keeping history @@ -798,42 +861,54 @@ TypeScript types ensure: - Correct data structures - IDE autocomplete support +### Getter Strategy +- Getters return copies to prevent external mutation +- `toJSON()` provides complete document snapshot +- MCP can call `toJSON()` after operations to get updated state +- No need for complex getter/setter parity + ### Performance - Shallow copying for immutability - JSON serialization only when needed - Validation is O(n) or better - No deep cloning unless necessary +### I/O Separation +- Builder focuses on document manipulation +- File I/O handled by dedicated utilities or MCP server +- Cleaner separation of concerns +- Easier to test builder in isolation + ### Extension Points The builder can be extended: - Custom validators -- Plugin operations -- Custom element types -- Lifecycle hooks +- Plugin operations (if needed) +- Lifecycle hooks (if needed) ## Next Steps 1. **Implement core builder** (~1 day) - - Document operations - - Group operations - - Element operations + - Document operations (with getters) + - Group operations (with getters) + - Element operations (generic only) + - Variable operations (with getters) + - Data loader operations (with getters) + - Resources operations (with getters) - Validation -2. **Add file I/O** (~0.5 day) - - Load from filesystem - - Save to filesystem - - Error handling - -3. **Create MCP wrapper** (~0.5 day) - - Tool definitions - - Parameter schemas +2. **Create MCP wrapper** (~0.5-1 day) + - Tool definitions for each operation + - Parameter schemas (auto-generated from TypeScript) - Error mapping - - State management + - State management (builder instance per session) + - Return updated document via `toJSON()` after each operation -4. **Testing & Documentation** (ongoing) +3. **Testing & Documentation** (ongoing) - Unit tests for each operation - Integration examples - API documentation - Usage patterns +**Note:** File I/O intentionally omitted - handled by separate I/O MCP server or external utilities. + Total estimated effort: **1-2 days** From 02c4b9091f5ea4d5a9d43cf415dd197c8e89145b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 10 Oct 2025 17:00:50 +0000 Subject: [PATCH 07/11] Remove individual getters - use toJSON() for all state access to minimize maintenance Co-authored-by: danmarshall <11507384+danmarshall@users.noreply.github.com> --- docs/research/builder-api-sketch.md | 177 +++++----------------------- 1 file changed, 28 insertions(+), 149 deletions(-) diff --git a/docs/research/builder-api-sketch.md b/docs/research/builder-api-sketch.md index 94a23bbd..6722062d 100644 --- a/docs/research/builder-api-sketch.md +++ b/docs/research/builder-api-sketch.md @@ -104,13 +104,6 @@ class ChartifactBuilder { class ChartifactBuilder { // ... previous code ... - /** - * Get the document title - */ - getTitle(): string { - return this.doc.title; - } - /** * Set the document title */ @@ -118,13 +111,6 @@ class ChartifactBuilder { return this.clone({ title }); } - /** - * Get the CSS styles - */ - getCSS(): string | string[] | undefined { - return this.doc.style?.css; - } - /** * Set or update the CSS styles */ @@ -148,13 +134,6 @@ class ChartifactBuilder { return this.setCSS(newCSS); } - /** - * Get Google Fonts configuration - */ - getGoogleFonts(): GoogleFontsSpec | undefined { - return this.doc.style?.googleFonts; - } - /** * Set Google Fonts configuration */ @@ -168,13 +147,6 @@ class ChartifactBuilder { }); } - /** - * Get all notes - */ - getNotes(): string[] { - return this.doc.notes || []; - } - /** * Add a note to the document */ @@ -199,27 +171,6 @@ class ChartifactBuilder { class ChartifactBuilder { // ... previous code ... - /** - * Get all groups - */ - getGroups(): ElementGroup[] { - return [...this.doc.groups]; - } - - /** - * Get a group by ID - */ - getGroup(groupId: string): ElementGroup | undefined { - return this.doc.groups.find(g => g.groupId === groupId); - } - - /** - * Check if a group exists - */ - hasGroup(groupId: string): boolean { - return this.doc.groups.some(g => g.groupId === groupId); - } - /** * Add a new group to the document */ @@ -314,31 +265,6 @@ Users should construct element objects directly and use the generic operations. class ChartifactBuilder { // ... previous code ... - /** - * Get all elements from a group - */ - getElements(groupId: string): PageElement[] { - const group = this.doc.groups.find(g => g.groupId === groupId); - if (!group) { - throw new Error(`Group '${groupId}' not found`); - } - return [...group.elements]; - } - - /** - * Get a single element by index from a group - */ - getElement(groupId: string, elementIndex: number): PageElement { - const group = this.doc.groups.find(g => g.groupId === groupId); - if (!group) { - throw new Error(`Group '${groupId}' not found`); - } - if (elementIndex < 0 || elementIndex >= group.elements.length) { - throw new Error(`Element index ${elementIndex} out of bounds in group '${groupId}'`); - } - return group.elements[elementIndex]; - } - /** * Add an element to a group */ @@ -476,20 +402,6 @@ builder.addElement('main', { type: 'slider', variableId: 'year', min: 2020, max: class ChartifactBuilder { // ... previous code ... - /** - * Get all variables - */ - getVariables(): Variable[] { - return [...(this.doc.variables || [])]; - } - - /** - * Get a specific variable by ID - */ - getVariable(variableId: string): Variable | undefined { - return this.doc.variables?.find(v => v.variableId === variableId); - } - /** * Add a variable to the document */ @@ -536,22 +448,6 @@ class ChartifactBuilder { return this.clone({ variables }); } - /** - * Get all data loaders - */ - getDataLoaders(): DataLoader[] { - return [...(this.doc.dataLoaders || [])]; - } - - /** - * Get a specific data loader by name - */ - getDataLoader(dataSourceName: string): DataLoader | undefined { - return this.doc.dataLoaders?.find( - dl => 'dataSourceName' in dl && dl.dataSourceName === dataSourceName - ); - } - /** * Add a data loader */ @@ -593,27 +489,6 @@ Resources can contain charts and potentially other types in the future. class ChartifactBuilder { // ... previous code ... - /** - * Get all resources - */ - getResources(): InteractiveDocument['resources'] { - return this.doc.resources ? { ...this.doc.resources } : undefined; - } - - /** - * Get all chart specifications - */ - getCharts(): Record { - return { ...(this.doc.resources?.charts || {}) }; - } - - /** - * Get a specific chart specification - */ - getChart(chartKey: string): object | undefined { - return this.doc.resources?.charts?.[chartKey]; - } - /** * Add or update a chart specification in resources */ @@ -767,25 +642,27 @@ const doc = new ChartifactBuilder({ title: 'My Report' }) .toJSON(); ``` -## Getter Strategy +## Reading State Strategy -**Strategy:** Provide getters for inspection without mutation. +**Strategy:** Since the builder wraps a simple JSON object, the LLM can access the document directly when needed. -- **For simple values:** Return the value directly (e.g., `getTitle()` returns string) -- **For collections:** Return a shallow copy to prevent external mutation (e.g., `getGroups()` returns `[...groups]`) -- **For nested objects:** Return a shallow copy of the top-level object -- **Primary access:** Use `toJSON()` to get complete document when MCP needs full state +- **Primary access:** Use `toJSON()` to get the complete document +- **No individual getters:** Avoid maintenance overhead of getter methods for every property +- **Direct inspection:** The LLM can read any part of the returned JSON object as needed -**Getter Categories:** -1. **Document-level:** `getTitle()`, `getCSS()`, `getGoogleFonts()`, `getNotes()` -2. **Groups:** `getGroups()`, `getGroup(id)`, `hasGroup(id)` -3. **Elements:** `getElements(groupId)`, `getElement(groupId, index)` -4. **Variables:** `getVariables()`, `getVariable(id)` -5. **Data:** `getDataLoaders()`, `getDataLoader(name)` -6. **Resources:** `getResources()`, `getCharts()`, `getChart(key)` -7. **Complete document:** `toJSON()` - returns full InteractiveDocument +**Usage:** +```typescript +const doc = builder.toJSON(); +// Access any property directly from the JSON: +const title = doc.title; +const groups = doc.groups; +const firstGroup = doc.groups[0]; +const css = doc.style?.css; +const variables = doc.variables; +// etc. +``` -**MCP Usage:** MCP tools can call `toJSON()` after any operation to return the complete updated document to the LLM. +**MCP Usage:** MCP tools call `toJSON()` after operations to return the updated document. The LLM can then inspect any part of the document it needs. ## Validation & Error Handling @@ -806,36 +683,38 @@ try { ## MCP Tool Mapping -Each builder method maps naturally to an MCP tool. Note the svelte approach - generic operations only: +Each builder method maps naturally to an MCP tool. Note the svelte approach - generic operations only, no individual getters: | MCP Tool | Builder Method | Parameters | |----------|---------------|------------| | `create_document` | `new ChartifactBuilder()` | `{ title?, groups?, ... }` | | `from_json` | `ChartifactBuilder.fromJSON()` | `{ json }` | | `to_json` | `.toJSON()` | `{}` | -| `get_title` | `.getTitle()` | `{}` | | `set_title` | `.setTitle()` | `{ title }` | -| `get_css` | `.getCSS()` | `{}` | | `set_css` | `.setCSS()` | `{ css }` | | `add_css` | `.addCSS()` | `{ css }` | -| `get_groups` | `.getGroups()` | `{}` | -| `get_group` | `.getGroup()` | `{ groupId }` | | `add_group` | `.addGroup()` | `{ groupId, elements? }` | +| `insert_group` | `.insertGroup()` | `{ index, groupId, elements? }` | | `delete_group` | `.deleteGroup()` | `{ groupId }` | | `rename_group` | `.renameGroup()` | `{ oldGroupId, newGroupId }` | -| `get_elements` | `.getElements()` | `{ groupId }` | +| `move_group` | `.moveGroup()` | `{ groupId, toIndex }` | | `add_element` | `.addElement()` | `{ groupId, element }` | +| `add_elements` | `.addElements()` | `{ groupId, elements }` | +| `insert_element` | `.insertElement()` | `{ groupId, index, element }` | | `delete_element` | `.deleteElement()` | `{ groupId, elementIndex }` | | `update_element` | `.updateElement()` | `{ groupId, elementIndex, element }` | -| `get_variables` | `.getVariables()` | `{}` | +| `clear_elements` | `.clearElements()` | `{ groupId }` | | `add_variable` | `.addVariable()` | `{ variable }` | +| `update_variable` | `.updateVariable()` | `{ variableId, updates }` | | `delete_variable` | `.deleteVariable()` | `{ variableId }` | -| `get_data_loaders` | `.getDataLoaders()` | `{}` | | `add_data_loader` | `.addDataLoader()` | `{ dataLoader }` | | `delete_data_loader` | `.deleteDataLoader()` | `{ dataSourceName }` | -| `get_chart` | `.getChart()` | `{ chartKey }` | | `set_chart` | `.setChart()` | `{ chartKey, spec }` | | `delete_chart` | `.deleteChart()` | `{ chartKey }` | +| `add_note` | `.addNote()` | `{ note }` | +| `clear_notes` | `.clearNotes()` | `{}` | + +**Reading state:** MCP tools call `toJSON()` to get the complete document. The LLM can then inspect any part of the JSON directly. **Note on I/O:** File operations (save, load) are handled by a dedicated I/O MCP server, not the builder. From 2caad45f39f58b159460a4a3438f0091d36a8a05 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 10 Oct 2025 17:02:16 +0000 Subject: [PATCH 08/11] Addressing PR comments Co-authored-by: danmarshall <11507384+danmarshall@users.noreply.github.com> --- docs/research/builder-api-sketch.md | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/docs/research/builder-api-sketch.md b/docs/research/builder-api-sketch.md index 6722062d..0910007e 100644 --- a/docs/research/builder-api-sketch.md +++ b/docs/research/builder-api-sketch.md @@ -740,11 +740,11 @@ TypeScript types ensure: - Correct data structures - IDE autocomplete support -### Getter Strategy -- Getters return copies to prevent external mutation -- `toJSON()` provides complete document snapshot -- MCP can call `toJSON()` after operations to get updated state -- No need for complex getter/setter parity +### Reading State Strategy +- **No individual getters** - Avoids maintenance overhead +- **Use `toJSON()` for all reads** - Returns complete document as plain JSON +- **LLM inspects JSON directly** - Can access any property from the returned object +- **Simple and flexible** - No need to add getters as schema evolves ### Performance - Shallow copying for immutability @@ -767,13 +767,14 @@ The builder can be extended: ## Next Steps 1. **Implement core builder** (~1 day) - - Document operations (with getters) - - Group operations (with getters) + - Document operations + - Group operations - Element operations (generic only) - - Variable operations (with getters) - - Data loader operations (with getters) - - Resources operations (with getters) + - Variable operations + - Data loader operations + - Resources operations - Validation + - Single `toJSON()` method for reading state 2. **Create MCP wrapper** (~0.5-1 day) - Tool definitions for each operation From 761ece96247c99c19072f3a3db5f091f1c728e85 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 10 Oct 2025 17:24:11 +0000 Subject: [PATCH 09/11] Rename setChart/deleteChart to setResource/deleteResource and add transactional API for MCP Co-authored-by: danmarshall <11507384+danmarshall@users.noreply.github.com> --- docs/research/builder-api-sketch.md | 227 ++++++++++++++++++++++++++-- 1 file changed, 211 insertions(+), 16 deletions(-) diff --git a/docs/research/builder-api-sketch.md b/docs/research/builder-api-sketch.md index 0910007e..7d2fdb3b 100644 --- a/docs/research/builder-api-sketch.md +++ b/docs/research/builder-api-sketch.md @@ -490,40 +490,53 @@ class ChartifactBuilder { // ... previous code ... /** - * Add or update a chart specification in resources + * Add or update a resource in the resources section. + * This includes charts (stored in resources.charts) and potentially other resource types in the future. + * + * @param resourceType - The type of resource (e.g., 'charts') + * @param resourceKey - The key/name for this resource + * @param spec - The resource specification (e.g., Vega/Vega-Lite spec for charts) */ - setChart(chartKey: string, spec: object): ChartifactBuilder { - const charts = { - ...(this.doc.resources?.charts || {}), - [chartKey]: spec + setResource(resourceType: string, resourceKey: string, spec: object): ChartifactBuilder { + const resources = { + ...(this.doc.resources?.[resourceType] || {}), + [resourceKey]: spec }; return this.clone({ resources: { ...this.doc.resources, - charts + [resourceType]: resources } }); } /** - * Remove a chart specification from resources + * Remove a resource from the resources section + * + * @param resourceType - The type of resource (e.g., 'charts') + * @param resourceKey - The key/name for this resource */ - deleteChart(chartKey: string): ChartifactBuilder { - if (!this.doc.resources?.charts?.[chartKey]) { - throw new Error(`Chart '${chartKey}' not found`); + deleteResource(resourceType: string, resourceKey: string): ChartifactBuilder { + if (!this.doc.resources?.[resourceType]?.[resourceKey]) { + throw new Error(`Resource '${resourceKey}' not found in '${resourceType}'`); } - const { [chartKey]: _, ...charts } = this.doc.resources.charts; + const { [resourceKey]: _, ...resources } = this.doc.resources[resourceType]; return this.clone({ resources: { ...this.doc.resources, - charts + [resourceType]: resources } }); } } + +// Convenience methods for charts (most common resource type) +// Example usage: +// builder.setResource('charts', 'myChart', vegaSpec) +// builder.deleteResource('charts', 'myChart') ``` ## Usage Examples @@ -547,7 +560,7 @@ const builder = new ChartifactBuilder() { month: 'Mar', revenue: 15000, profit: 3000 } ] }) - .setChart('revenueChart', { + .setResource('charts', 'revenueChart', { $schema: 'https://vega.github.io/schema/vega-lite/v5.json', data: { name: 'sales' }, mark: 'bar', @@ -636,7 +649,7 @@ const doc = new ChartifactBuilder({ title: 'My Report' }) editable: false }) .addGroup('charts') - .setChart('trend', { /* vega spec */ }) + .setResource('charts', 'trend', { /* vega spec */ }) .addElement('charts', { type: 'chart', chartKey: 'trend' }) .setCSS('.header { text-align: center; }') .toJSON(); @@ -709,8 +722,8 @@ Each builder method maps naturally to an MCP tool. Note the svelte approach - ge | `delete_variable` | `.deleteVariable()` | `{ variableId }` | | `add_data_loader` | `.addDataLoader()` | `{ dataLoader }` | | `delete_data_loader` | `.deleteDataLoader()` | `{ dataSourceName }` | -| `set_chart` | `.setChart()` | `{ chartKey, spec }` | -| `delete_chart` | `.deleteChart()` | `{ chartKey }` | +| `set_resource` | `.setResource()` | `{ resourceType, resourceKey, spec }` | +| `delete_resource` | `.deleteResource()` | `{ resourceType, resourceKey }` | | `add_note` | `.addNote()` | `{ note }` | | `clear_notes` | `.clearNotes()` | `{}` | @@ -718,6 +731,188 @@ Each builder method maps naturally to an MCP tool. Note the svelte approach - ge **Note on I/O:** File operations (save, load) are handled by a dedicated I/O MCP server, not the builder. +## Transactional API for MCP + +**Question:** Can MCP take advantage of chainability? + +**Answer:** MCP tools operate one at a time, so direct chainability isn't available. However, we can provide a transactional API that accepts an array of operations to apply atomically. + +### Transaction Types + +```typescript +type Transaction = + | { op: 'setTitle', title: string } + | { op: 'setCSS', css: string | string[] } + | { op: 'addCSS', css: string } + | { op: 'setGoogleFonts', config: GoogleFontsSpec } + | { op: 'addNote', note: string } + | { op: 'clearNotes' } + | { op: 'addGroup', groupId: string, elements?: PageElement[] } + | { op: 'insertGroup', index: number, groupId: string, elements?: PageElement[] } + | { op: 'deleteGroup', groupId: string } + | { op: 'renameGroup', oldGroupId: string, newGroupId: string } + | { op: 'moveGroup', groupId: string, toIndex: number } + | { op: 'addElement', groupId: string, element: PageElement } + | { op: 'addElements', groupId: string, elements: PageElement[] } + | { op: 'insertElement', groupId: string, index: number, element: PageElement } + | { op: 'deleteElement', groupId: string, elementIndex: number } + | { op: 'updateElement', groupId: string, elementIndex: number, element: PageElement } + | { op: 'clearElements', groupId: string } + | { op: 'addVariable', variable: Variable } + | { op: 'updateVariable', variableId: string, updates: Partial } + | { op: 'deleteVariable', variableId: string } + | { op: 'addDataLoader', dataLoader: DataLoader } + | { op: 'deleteDataLoader', dataSourceName: string } + | { op: 'setResource', resourceType: string, resourceKey: string, spec: object } + | { op: 'deleteResource', resourceType: string, resourceKey: string }; +``` + +### Apply Transactions Method + +```typescript +class ChartifactBuilder { + // ... previous code ... + + /** + * Apply an array of transactions atomically. + * This is useful for MCP where chainability isn't available, + * allowing multiple operations to be applied in a single call. + * + * @param transactions - Array of transaction objects to apply + * @returns New builder instance with all transactions applied + */ + applyTransactions(transactions: Transaction[]): ChartifactBuilder { + let builder: ChartifactBuilder = this; + + for (const tx of transactions) { + switch (tx.op) { + case 'setTitle': + builder = builder.setTitle(tx.title); + break; + case 'setCSS': + builder = builder.setCSS(tx.css); + break; + case 'addCSS': + builder = builder.addCSS(tx.css); + break; + case 'setGoogleFonts': + builder = builder.setGoogleFonts(tx.config); + break; + case 'addNote': + builder = builder.addNote(tx.note); + break; + case 'clearNotes': + builder = builder.clearNotes(); + break; + case 'addGroup': + builder = builder.addGroup(tx.groupId, tx.elements); + break; + case 'insertGroup': + builder = builder.insertGroup(tx.index, tx.groupId, tx.elements); + break; + case 'deleteGroup': + builder = builder.deleteGroup(tx.groupId); + break; + case 'renameGroup': + builder = builder.renameGroup(tx.oldGroupId, tx.newGroupId); + break; + case 'moveGroup': + builder = builder.moveGroup(tx.groupId, tx.toIndex); + break; + case 'addElement': + builder = builder.addElement(tx.groupId, tx.element); + break; + case 'addElements': + builder = builder.addElements(tx.groupId, tx.elements); + break; + case 'insertElement': + builder = builder.insertElement(tx.groupId, tx.index, tx.element); + break; + case 'deleteElement': + builder = builder.deleteElement(tx.groupId, tx.elementIndex); + break; + case 'updateElement': + builder = builder.updateElement(tx.groupId, tx.elementIndex, tx.element); + break; + case 'clearElements': + builder = builder.clearElements(tx.groupId); + break; + case 'addVariable': + builder = builder.addVariable(tx.variable); + break; + case 'updateVariable': + builder = builder.updateVariable(tx.variableId, tx.updates); + break; + case 'deleteVariable': + builder = builder.deleteVariable(tx.variableId); + break; + case 'addDataLoader': + builder = builder.addDataLoader(tx.dataLoader); + break; + case 'deleteDataLoader': + builder = builder.deleteDataLoader(tx.dataSourceName); + break; + case 'setResource': + builder = builder.setResource(tx.resourceType, tx.resourceKey, tx.spec); + break; + case 'deleteResource': + builder = builder.deleteResource(tx.resourceType, tx.resourceKey); + break; + } + } + + return builder; + } +} +``` + +### Transactional Usage Example + +```typescript +// Instead of chaining (not available in MCP): +// builder.setTitle('Dashboard').addGroup('main').addElement('main', '# Hello') + +// Use transactions (available in MCP): +const updated = builder.applyTransactions([ + { op: 'setTitle', title: 'Dashboard' }, + { op: 'addGroup', groupId: 'main', elements: [] }, + { op: 'addElement', groupId: 'main', element: '# Hello' }, + { op: 'setResource', resourceType: 'charts', resourceKey: 'myChart', spec: { /* ... */ } }, + { op: 'addElement', groupId: 'main', element: { type: 'chart', chartKey: 'myChart' } } +]); + +const doc = updated.toJSON(); +``` + +### MCP Tool for Transactions + +```typescript +// MCP tool definition +{ + name: "apply_transactions", + description: "Apply multiple document operations atomically", + inputSchema: { + type: "object", + properties: { + transactions: { + type: "array", + items: { + // Transaction union type schema + } + } + } + } +} +``` + +**Benefits of Transactional API:** +- **Atomic operations** - All transactions succeed or fail together +- **Efficient** - Single MCP call instead of multiple round-trips +- **Flexible** - LLM can batch multiple edits +- **Compatible with chainable API** - Both patterns coexist + +**MCP Usage:** The LLM can either call individual tools (one operation at a time) or use `apply_transactions` (multiple operations at once). + ## Implementation Notes ### Svelte Philosophy From 28d1c6da20923fc8d74400041205c065f97b05f8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 14 Oct 2025 17:46:27 +0000 Subject: [PATCH 10/11] Enable bulk element operations - add setGroupElements() and emphasize bulk pattern as recommended approach Co-authored-by: danmarshall <11507384+danmarshall@users.noreply.github.com> --- docs/research/builder-api-sketch.md | 100 ++++++++++++++++++++++++++-- 1 file changed, 96 insertions(+), 4 deletions(-) diff --git a/docs/research/builder-api-sketch.md b/docs/research/builder-api-sketch.md index 7d2fdb3b..8b33ff7c 100644 --- a/docs/research/builder-api-sketch.md +++ b/docs/research/builder-api-sketch.md @@ -172,7 +172,11 @@ class ChartifactBuilder { // ... previous code ... /** - * Add a new group to the document + * Add a new group to the document with optional initial elements. + * This is the preferred way to create groups with content in bulk. + * + * @param groupId - Unique identifier for the group + * @param elements - Array of elements to initialize the group with (default: empty array) */ addGroup(groupId: string, elements: PageElement[] = []): ChartifactBuilder { // Validate groupId doesn't already exist @@ -380,6 +384,28 @@ class ChartifactBuilder { return this.clone({ groups }); } + + /** + * Replace all elements in a group with a new array. + * This is the preferred way to update group content in bulk rather than + * tracking individual element additions/deletions. + * + * @param groupId - The group to update + * @param elements - New array of elements to replace existing content + */ + setGroupElements(groupId: string, elements: PageElement[]): ChartifactBuilder { + const groups = this.doc.groups.map(g => + g.groupId === groupId + ? { ...g, elements } + : g + ); + + if (groups === this.doc.groups) { + throw new Error(`Group '${groupId}' not found`); + } + + return this.clone({ groups }); + } } // Example usage with generic operations: @@ -394,6 +420,20 @@ builder.addElement('main', { type: 'checkbox', variableId: 'showDetails', label: // For a slider: builder.addElement('main', { type: 'slider', variableId: 'year', min: 2020, max: 2024, step: 1 }) + +// Bulk operations (preferred for group content management): +// Create group with initial elements +builder.addGroup('dashboard', [ + '# Sales Dashboard', + '## Key Metrics', + { type: 'chart', chartKey: 'revenue' } +]) + +// Replace entire group content +builder.setGroupElements('dashboard', [ + '# Updated Dashboard', + { type: 'chart', chartKey: 'newChart' } +]) ``` ## Data & Variable Operations @@ -541,16 +581,16 @@ class ChartifactBuilder { ## Usage Examples -### Example 1: Create a Simple Dashboard +### Example 1: Create a Dashboard with Bulk Group Operations (Recommended) ```typescript +// Preferred approach: Use bulk operations for group content const builder = new ChartifactBuilder() .setTitle('Sales Dashboard') .setCSS(` body { font-family: sans-serif; } .container { max-width: 1200px; margin: 0 auto; } `) - .addElement('main', '# Sales Dashboard\n\nView key metrics below.') .addDataLoader({ type: 'inline', dataSourceName: 'sales', @@ -569,7 +609,12 @@ const builder = new ChartifactBuilder() y: { field: 'revenue', type: 'quantitative' } } }) - .addElement('main', { type: 'chart', chartKey: 'revenueChart' }); + // Create group with all elements at once (recommended pattern) + .addGroup('main', [ + '# Sales Dashboard', + '\n\nView key metrics below.\n', + { type: 'chart', chartKey: 'revenueChart' } + ]); // Export to JSON string for saving (use external I/O utility) const json = builder.toString(); @@ -717,6 +762,7 @@ Each builder method maps naturally to an MCP tool. Note the svelte approach - ge | `delete_element` | `.deleteElement()` | `{ groupId, elementIndex }` | | `update_element` | `.updateElement()` | `{ groupId, elementIndex, element }` | | `clear_elements` | `.clearElements()` | `{ groupId }` | +| `set_group_elements` | `.setGroupElements()` | `{ groupId, elements }` | | `add_variable` | `.addVariable()` | `{ variable }` | | `update_variable` | `.updateVariable()` | `{ variableId, updates }` | | `delete_variable` | `.deleteVariable()` | `{ variableId }` | @@ -758,6 +804,7 @@ type Transaction = | { op: 'deleteElement', groupId: string, elementIndex: number } | { op: 'updateElement', groupId: string, elementIndex: number, element: PageElement } | { op: 'clearElements', groupId: string } + | { op: 'setGroupElements', groupId: string, elements: PageElement[] } | { op: 'addVariable', variable: Variable } | { op: 'updateVariable', variableId: string, updates: Partial } | { op: 'deleteVariable', variableId: string } @@ -837,6 +884,9 @@ class ChartifactBuilder { case 'clearElements': builder = builder.clearElements(tx.groupId); break; + case 'setGroupElements': + builder = builder.setGroupElements(tx.groupId, tx.elements); + break; case 'addVariable': builder = builder.addVariable(tx.variable); break; @@ -881,6 +931,41 @@ const updated = builder.applyTransactions([ { op: 'addElement', groupId: 'main', element: { type: 'chart', chartKey: 'myChart' } } ]); +// Preferred: Use bulk element operations for group content management +const updatedBulk = builder.applyTransactions([ + { op: 'setTitle', title: 'Sales Dashboard' }, + { + op: 'addGroup', + groupId: 'header', + elements: [ + '# Sales Dashboard', + '## Q4 2024 Performance' + ] + }, + { + op: 'addGroup', + groupId: 'charts', + elements: [ + { type: 'chart', chartKey: 'revenue' }, + { type: 'chart', chartKey: 'profit' } + ] + } +]); + +// Update entire group content at once (typical pattern) +const replaced = builder.applyTransactions([ + { + op: 'setGroupElements', + groupId: 'dashboard', + elements: [ + '# Updated Dashboard', + '## New Content', + { type: 'chart', chartKey: 'newChart' }, + { type: 'tabulator', dataSourceName: 'metrics' } + ] + } +]); + const doc = updated.toJSON(); ``` @@ -941,6 +1026,13 @@ TypeScript types ensure: - **LLM inspects JSON directly** - Can access any property from the returned object - **Simple and flexible** - No need to add getters as schema evolves +### Bulk Operations Pattern (Recommended) +- **Group-level management** - Prefer `addGroup(id, elements[])` and `setGroupElements(id, elements[])` over individual element operations +- **Less granular tracking** - Individual element add/delete/update operations available but typically too granular +- **Simpler mental model** - Think of groups as content units rather than managing elements individually +- **Better for MCP** - Fewer transactions needed, more atomic updates +- **Typical workflow**: Create group with initial content, then use `setGroupElements()` to replace content when needed + ### Performance - Shallow copying for immutability - JSON serialization only when needed From a649c7b0b5094a2cd71ba59c61d828467d3a776c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 14 Oct 2025 17:56:42 +0000 Subject: [PATCH 11/11] Update all examples to use array-based bulk operations for group content instead of repeated addElement calls Co-authored-by: danmarshall <11507384+danmarshall@users.noreply.github.com> --- docs/research/builder-api-sketch.md | 76 ++++++++++++++++------------- 1 file changed, 43 insertions(+), 33 deletions(-) diff --git a/docs/research/builder-api-sketch.md b/docs/research/builder-api-sketch.md index 8b33ff7c..968af9dd 100644 --- a/docs/research/builder-api-sketch.md +++ b/docs/research/builder-api-sketch.md @@ -409,6 +409,7 @@ class ChartifactBuilder { } // Example usage with generic operations: +// Individual element operations (available but typically too granular): // For markdown (string): builder.addElement('main', '# Hello World') @@ -421,7 +422,7 @@ builder.addElement('main', { type: 'checkbox', variableId: 'showDetails', label: // For a slider: builder.addElement('main', { type: 'slider', variableId: 'year', min: 2020, max: 2024, step: 1 }) -// Bulk operations (preferred for group content management): +// Bulk operations (RECOMMENDED - preferred for group content management): // Create group with initial elements builder.addGroup('dashboard', [ '# Sales Dashboard', @@ -625,31 +626,33 @@ const json = builder.toString(); ```typescript const builder = ChartifactBuilder.fromDocument(existingDoc) - .addGroup('controls', []) - .addElement('controls', '## Settings') .addVariable({ variableId: 'yearFilter', type: 'number', initialValue: 2024 }) - .addElement('controls', { - type: 'slider', - variableId: 'yearFilter', - min: 2020, - max: 2024, - step: 1, - label: 'Select Year' - }) .addVariable({ variableId: 'showDetails', type: 'boolean', initialValue: false }) - .addElement('controls', { - type: 'checkbox', - variableId: 'showDetails', - label: 'Show detailed view' - }); + // Create group with all control elements at once (recommended pattern) + .addGroup('controls', [ + '## Settings', + { + type: 'slider', + variableId: 'yearFilter', + min: 2020, + max: 2024, + step: 1, + label: 'Select Year' + }, + { + type: 'checkbox', + variableId: 'showDetails', + label: 'Show detailed view' + } + ]); ``` ### Example 3: Modify Existing Document @@ -659,11 +662,14 @@ const builder = ChartifactBuilder.fromDocument(existingDoc) const jsonString = readFileSync('./document.idoc.json', 'utf-8'); const builder = ChartifactBuilder.fromJSON(jsonString); -// Make modifications +// Get current document to inspect +const doc = builder.toJSON(); +const mainElements = doc.groups.find(g => g.groupId === 'main')?.elements || []; + +// Make modifications - replace first element with new content const updated = builder .setTitle('Updated Title') - .deleteElement('main', 0) - .addElement('main', '# New Header') + .setGroupElements('main', ['# New Header', ...mainElements.slice(1)]) .addGroup('footer', ['Built with Chartifact']) .addNote('Updated on ' + new Date().toISOString()); @@ -676,10 +682,6 @@ const updatedJson = updated.toString(); ```typescript const doc = new ChartifactBuilder({ title: 'My Report' }) - .addGroup('header') - .addElement('header', '# Annual Report 2024') - .addElement('header', 'Executive Summary') - .addGroup('metrics') .addDataLoader({ type: 'inline', dataSourceName: 'kpis', @@ -688,15 +690,23 @@ const doc = new ChartifactBuilder({ title: 'My Report' }) { name: 'Growth', value: 15 } ] }) - .addElement('metrics', { - type: 'tabulator', - dataSourceName: 'kpis', - editable: false - }) - .addGroup('charts') .setResource('charts', 'trend', { /* vega spec */ }) - .addElement('charts', { type: 'chart', chartKey: 'trend' }) .setCSS('.header { text-align: center; }') + // Create all groups with their content at once (recommended pattern) + .addGroup('header', [ + '# Annual Report 2024', + 'Executive Summary' + ]) + .addGroup('metrics', [ + { + type: 'tabulator', + dataSourceName: 'kpis', + editable: false + } + ]) + .addGroup('charts', [ + { type: 'chart', chartKey: 'trend' } + ]) .toJSON(); ``` @@ -919,10 +929,10 @@ class ChartifactBuilder { ### Transactional Usage Example ```typescript -// Instead of chaining (not available in MCP): +// Individual element operations (available but typically too granular): // builder.setTitle('Dashboard').addGroup('main').addElement('main', '# Hello') -// Use transactions (available in MCP): +// Transactions support individual operations: const updated = builder.applyTransactions([ { op: 'setTitle', title: 'Dashboard' }, { op: 'addGroup', groupId: 'main', elements: [] }, @@ -931,7 +941,7 @@ const updated = builder.applyTransactions([ { op: 'addElement', groupId: 'main', element: { type: 'chart', chartKey: 'myChart' } } ]); -// Preferred: Use bulk element operations for group content management +// RECOMMENDED: Use bulk element operations for group content management const updatedBulk = builder.applyTransactions([ { op: 'setTitle', title: 'Sales Dashboard' }, {