Origin abstraction layer #9

Closed
opened 2026-02-05 18:26:02 +00:00 by forbes · 0 comments
Owner

Overview

Create the core abstraction layer that defines the interface for file origins and manages origin instances. This is the foundational component that all other origin system features depend on.

Key Insight: Origins don't change where files are stored - all documents are always saved locally. Origins change the workflow and identity model:

  • Local: Document identity = file path, no external tracking
  • Silo: Document identity = part number, syncs with database/MinIO

Parent Issue

Epic: #8 Unified File Origin System

Goals

  1. Define a clean interface (FileOrigin) that captures workflow differences between backends
  2. Create an OriginManager singleton to manage registered origins and track current selection
  3. Establish the configuration storage schema for origins
  4. Provide signals/callbacks for UI components to react to origin changes

Detailed Design

FileOrigin Interface

class FileOrigin : public QObject {
    Q_OBJECT
public:
    // Identity
    virtual QString id() const = 0;
    virtual QString name() const = 0;
    virtual QString nickname() const = 0;
    virtual QIcon icon() const = 0;
    virtual OriginType type() const = 0;
    
    // Workflow characteristics
    virtual bool tracksExternally() const = 0;      // Silo: true (database), Local: false
    virtual bool requiresAuthentication() const { return false; }
    
    // Capabilities - what extended features does this origin support?
    virtual bool supportsRevisions() const { return false; }
    virtual bool supportsBOM() const { return false; }
    virtual bool supportsPartNumbers() const { return false; }
    
    // Connection state (for remote-tracking origins)
    virtual ConnectionState connectionState() const { return ConnectionState::Connected; }
    virtual void connectOrigin() {}
    virtual void disconnectOrigin() {}
    
    // Document identity - how this origin identifies documents
    virtual QString documentIdentity(App::Document* doc) const = 0;
    // Local: returns file path
    // Silo: returns part number (from SiloPartNumber property)
    
    // Core operations - ALL save locally, but workflow differs
    virtual void newDocument() = 0;           // Local: new doc, Silo: part creation form
    virtual void openDocument() = 0;          // Local: file picker, Silo: database search pane
    virtual void saveDocument(App::Document* doc) = 0;  // Local: save, Silo: save + sync
    virtual void saveDocumentAs(App::Document* doc) = 0; // See below for complex behavior
    
    // Extended operations (Silo-only, default no-op)
    virtual void commitDocument(App::Document* doc) { Q_UNUSED(doc); }
    virtual void pullDocument(App::Document* doc) { Q_UNUSED(doc); }
    virtual void pushDocument(App::Document* doc) { Q_UNUSED(doc); }
    virtual void showInfo(App::Document* doc) { Q_UNUSED(doc); }
    virtual void showBOM(App::Document* doc) { Q_UNUSED(doc); }
    
    // Document ownership - based on document properties, NOT file path
    virtual bool ownsDocument(App::Document* doc) const = 0;
    // Local: true if doc has NO SiloPartNumber property
    // Silo: true if doc HAS SiloPartNumber property matching this instance
    
    virtual QString documentDisplaySuffix(App::Document* doc) const;
    
Q_SIGNALS:
    void connectionStateChanged(ConnectionState state);
    void operationStarted(const QString& operation);
    void operationFinished(const QString& operation, bool success);
};

Key Behavioral Differences

Operation Local Origin Silo Origin
New Create empty document Show part creation form (category, description from schema YAML)
Open File picker dialog Database search pane showing items (with local file status)
Save Save to disk Save to disk + upload to MinIO (sync)
Save As File picker for new path If local doc: migration workflow (register in DB). If Silo doc: copy workflow (new part number)
Identity File path Part number (SiloPartNumber property)

Document Ownership Logic

Ownership is determined by document properties, not file location:

// LocalFileOrigin
bool LocalFileOrigin::ownsDocument(App::Document* doc) const {
    // Owns documents that have NO Silo tracking
    return !hasSiloProperties(doc);
}

// SiloOrigin  
bool SiloOrigin::ownsDocument(App::Document* doc) const {
    // Owns documents tracked by THIS Silo instance
    auto* prop = doc->getPropertyByName("SiloOriginId");
    if (!prop) {
        // Legacy: check for SiloPartNumber without origin ID
        return hasSiloPartNumber(doc);
    }
    return prop->getValue() == this->id();
}

OriginManager Singleton

class OriginManager : public QObject {
    Q_OBJECT
public:
    static OriginManager* instance();
    
    void registerOrigin(FileOrigin* origin);
    void unregisterOrigin(const QString& id);
    
    QList<FileOrigin*> allOrigins() const;
    FileOrigin* originById(const QString& id) const;
    FileOrigin* localOrigin() const;
    
    FileOrigin* currentOrigin() const;
    void setCurrentOrigin(FileOrigin* origin);
    
    // Find which origin owns a document (by properties, not path)
    FileOrigin* originForDocument(App::Document* doc) const;
    
Q_SIGNALS:
    void currentOriginChanged(FileOrigin* newOrigin, FileOrigin* oldOrigin);
    void originRegistered(FileOrigin* origin);
    void originsListChanged();
};

Configuration Schema

User parameter:BaseApp/Preferences/Origins/
├── CurrentOriginId: "silo-work"
├── Local/
│   └── (minimal config)
├── Silo/
│   ├── work/
│   │   ├── Id: "silo-work"
│   │   ├── Nickname: "Work"
│   │   ├── ApiUrl: "https://..."
│   │   └── ...
│   └── production/

Implementation Tasks

  • Create FileOrigin abstract base class
  • Create OriginManager singleton
  • Define OriginType and ConnectionState enums
  • Implement documentIdentity() contract
  • Implement ownsDocument() contract (property-based)
  • Implement configuration loading/saving
  • Add Python bindings for Silo addon

Files to Create

  • src/Gui/FileOrigin.h / .cpp
  • src/Gui/OriginManager.h / .cpp

Acceptance Criteria

  • FileOrigin interface captures workflow differences (not storage differences)
  • Document ownership determined by properties, not file path
  • OriginManager manages origin lifecycle
  • Current origin persists across sessions
  • Python bindings work for Silo

Dependencies

None - foundational component

Blocking

## Overview Create the core abstraction layer that defines the interface for file origins and manages origin instances. This is the foundational component that all other origin system features depend on. **Key Insight**: Origins don't change where files are stored - all documents are always saved locally. Origins change the *workflow* and *identity model*: - **Local**: Document identity = file path, no external tracking - **Silo**: Document identity = part number, syncs with database/MinIO ## Parent Issue Epic: #8 Unified File Origin System ## Goals 1. Define a clean interface (`FileOrigin`) that captures workflow differences between backends 2. Create an `OriginManager` singleton to manage registered origins and track current selection 3. Establish the configuration storage schema for origins 4. Provide signals/callbacks for UI components to react to origin changes ## Detailed Design ### FileOrigin Interface ```cpp class FileOrigin : public QObject { Q_OBJECT public: // Identity virtual QString id() const = 0; virtual QString name() const = 0; virtual QString nickname() const = 0; virtual QIcon icon() const = 0; virtual OriginType type() const = 0; // Workflow characteristics virtual bool tracksExternally() const = 0; // Silo: true (database), Local: false virtual bool requiresAuthentication() const { return false; } // Capabilities - what extended features does this origin support? virtual bool supportsRevisions() const { return false; } virtual bool supportsBOM() const { return false; } virtual bool supportsPartNumbers() const { return false; } // Connection state (for remote-tracking origins) virtual ConnectionState connectionState() const { return ConnectionState::Connected; } virtual void connectOrigin() {} virtual void disconnectOrigin() {} // Document identity - how this origin identifies documents virtual QString documentIdentity(App::Document* doc) const = 0; // Local: returns file path // Silo: returns part number (from SiloPartNumber property) // Core operations - ALL save locally, but workflow differs virtual void newDocument() = 0; // Local: new doc, Silo: part creation form virtual void openDocument() = 0; // Local: file picker, Silo: database search pane virtual void saveDocument(App::Document* doc) = 0; // Local: save, Silo: save + sync virtual void saveDocumentAs(App::Document* doc) = 0; // See below for complex behavior // Extended operations (Silo-only, default no-op) virtual void commitDocument(App::Document* doc) { Q_UNUSED(doc); } virtual void pullDocument(App::Document* doc) { Q_UNUSED(doc); } virtual void pushDocument(App::Document* doc) { Q_UNUSED(doc); } virtual void showInfo(App::Document* doc) { Q_UNUSED(doc); } virtual void showBOM(App::Document* doc) { Q_UNUSED(doc); } // Document ownership - based on document properties, NOT file path virtual bool ownsDocument(App::Document* doc) const = 0; // Local: true if doc has NO SiloPartNumber property // Silo: true if doc HAS SiloPartNumber property matching this instance virtual QString documentDisplaySuffix(App::Document* doc) const; Q_SIGNALS: void connectionStateChanged(ConnectionState state); void operationStarted(const QString& operation); void operationFinished(const QString& operation, bool success); }; ``` ### Key Behavioral Differences | Operation | Local Origin | Silo Origin | |-----------|--------------|-------------| | **New** | Create empty document | Show part creation form (category, description from schema YAML) | | **Open** | File picker dialog | Database search pane showing items (with local file status) | | **Save** | Save to disk | Save to disk + upload to MinIO (sync) | | **Save As** | File picker for new path | If local doc: migration workflow (register in DB). If Silo doc: copy workflow (new part number) | | **Identity** | File path | Part number (`SiloPartNumber` property) | ### Document Ownership Logic Ownership is determined by **document properties**, not file location: ```cpp // LocalFileOrigin bool LocalFileOrigin::ownsDocument(App::Document* doc) const { // Owns documents that have NO Silo tracking return !hasSiloProperties(doc); } // SiloOrigin bool SiloOrigin::ownsDocument(App::Document* doc) const { // Owns documents tracked by THIS Silo instance auto* prop = doc->getPropertyByName("SiloOriginId"); if (!prop) { // Legacy: check for SiloPartNumber without origin ID return hasSiloPartNumber(doc); } return prop->getValue() == this->id(); } ``` ### OriginManager Singleton ```cpp class OriginManager : public QObject { Q_OBJECT public: static OriginManager* instance(); void registerOrigin(FileOrigin* origin); void unregisterOrigin(const QString& id); QList<FileOrigin*> allOrigins() const; FileOrigin* originById(const QString& id) const; FileOrigin* localOrigin() const; FileOrigin* currentOrigin() const; void setCurrentOrigin(FileOrigin* origin); // Find which origin owns a document (by properties, not path) FileOrigin* originForDocument(App::Document* doc) const; Q_SIGNALS: void currentOriginChanged(FileOrigin* newOrigin, FileOrigin* oldOrigin); void originRegistered(FileOrigin* origin); void originsListChanged(); }; ``` ### Configuration Schema ``` User parameter:BaseApp/Preferences/Origins/ ├── CurrentOriginId: "silo-work" ├── Local/ │ └── (minimal config) ├── Silo/ │ ├── work/ │ │ ├── Id: "silo-work" │ │ ├── Nickname: "Work" │ │ ├── ApiUrl: "https://..." │ │ └── ... │ └── production/ ``` ## Implementation Tasks - [ ] Create `FileOrigin` abstract base class - [ ] Create `OriginManager` singleton - [ ] Define `OriginType` and `ConnectionState` enums - [ ] Implement `documentIdentity()` contract - [ ] Implement `ownsDocument()` contract (property-based) - [ ] Implement configuration loading/saving - [ ] Add Python bindings for Silo addon ## Files to Create - `src/Gui/FileOrigin.h` / `.cpp` - `src/Gui/OriginManager.h` / `.cpp` ## Acceptance Criteria - [ ] FileOrigin interface captures workflow differences (not storage differences) - [ ] Document ownership determined by properties, not file path - [ ] OriginManager manages origin lifecycle - [ ] Current origin persists across sessions - [ ] Python bindings work for Silo ## Dependencies None - foundational component ## Blocking - #10, #11, #12
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: kindred/create#9