Files
Dario LipicarandClaude Opus 5 4fb867a72a docs: the universal/cdylib hosting step is logos-qt-host-generator (#17)
These were already wrong before any deletion. module-builder repointed the
cdylib Qt glue onto logos-plugin-qt's logos-qt-host-generator (5081088) and the
docs kept naming logos-qt-generator, so the pipeline they describe has not been
the pipeline that runs for some time.

That matters more here than in most docs: guidelines/ and docs/ are SHIPPED —
flake.nix copies them into the dev-boost output, and generate-agents-md.ts
embeds them into every scaffolded module's AGENTS.md/CLAUDE.md. So the stale
line was being handed to module authors, and to agents reading the scaffold, as
the instruction. That is why the six doctests/outputs/ files change too: they
are the recorded scaffolder output, and they carry the same text.

Only the middle step moves. `logos-cpp-generator --backend cdylib` — the Qt-free
C-ABI export wrapper — is a different tool doing a different job and is
deliberately untouched; it remains correct in every one of these blocks.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 23:00:07 -03:00

2024 lines
77 KiB
Plaintext

# Logos Development Platform — Full Documentation
# logos-dev-boost
## Overall Description
logos-dev-boost is a developer acceleration tool for the Logos modular application platform. It provides AI coding agents and human developers with accurate, always-available knowledge of the Logos SDK, build system, module architecture, and development workflows.
The tool solves a fundamental problem: AI agents (Claude Code, Cursor, Copilot, Codex) have no training data about the Logos ecosystem. They hallucinate APIs, use wrong build commands, generate Qt-dependent code where pure C++ is required, and cannot navigate the multi-repo architecture. Human developers face a similar but smaller-scale problem — the onboarding path from "I want to build a Logos module" to a working, packaged, tested plugin is steep.
logos-dev-boost addresses this by operating at three levels:
1. **Always-loaded context** — `AGENTS.md` / `CLAUDE.md` files with a compressed documentation index. Loaded automatically at session start. Based on Next.js research showing 100% AI eval pass rate with bundled docs versus 79% with skills-only approaches.
2. **On-demand skills** — Detailed, step-by-step task guides that activate when the agent works on a specific task. Follows the Agent Skills specification for cross-tool compatibility.
3. **MCP server** — Live project introspection tools (project info, documentation search, API reference, build help, scaffolding) via the Model Context Protocol.
## Definitions & Acronyms
| Term | Definition |
|------|------------|
| **Universal Module** | A Logos module whose implementation is pure C++ (no Qt types). All Qt glue is generated at build time by the header-first cdylib pipeline. Identified by `"interface": "universal"` in metadata.json. |
| **UI App** | A QML-based UI component displayed as a tab in Basecamp's MDI workspace. Either pure QML (calls backend modules via `logos.callModule()`) or QML + process-isolated C++ backend (Qt Remote Objects). Identified by `"type": "ui_qml"` in metadata.json. |
| **LIDL** | Logos Interface Definition Language — a lightweight DSL for declaring module interfaces. The universal pipeline derives a `.lidl` from your C++ header automatically; you can also hand-write one (the `cdylib` interface). Both produce identical generated output. |
| **Provider Glue** | Generated code (`_cdylib_glue.{h,cpp}`, `_module_impl.cpp`) that wraps a pure C++ implementation class: a uniform Qt-plugin glue over the common module-impl C ABI, plus a Qt-free C-ABI export wrapper. |
| **Client Stub** | Generated type-safe C++ wrapper class that callers use to invoke a module's methods without string-based dispatch. |
| **LogosAPI** | The runtime API that modules use to call other modules. Provides `callModule(name, method, args)` which returns a `LogosResult`. |
| **LogosResult** | Structured return type for cross-module calls. Contains `success()`, `data()` (QVariant), and `errorMessage()`. |
| **LGX** | Logos Package Format — gzip tar archives with platform-specific variants for distributing modules and UI apps. |
| **logoscore** | Headless CLI runtime that loads modules and optionally calls their methods. Used for testing modules without the full GUI. |
| **logos_host** | Per-module host process spawned by `liblogos_core`. Each module runs in isolation, communicating via Qt Remote Objects IPC. |
| **MCP** | Model Context Protocol — open standard for AI agent tool integration. logos-dev-boost exposes tools via MCP's stdio transport. |
| **Agent Skill** | A portable knowledge module (SKILL.md + optional assets) that AI agents activate on demand. Follows the agentskills.io specification. |
## Domain Model
### Two Component Types
This distinction is fundamental to the entire Logos ecosystem and to everything logos-dev-boost teaches:
**Logos Modules (core)** are process-isolated backend services. The developer writes a plain C++ implementation class using standard types (`std::string`, `int64_t`, `std::vector<T>`, `bool`). No Qt types appear in user code. The build system runs the universal codegen pipeline (header → `.lidl` → cdylib glue) to generate all Qt glue: the uniform Qt-plugin glue and a Qt-free C-ABI export wrapper around your class. Modules are loaded by `logoscore` (headless) or `logos-basecamp` (GUI) via `liblogos_core`. Each runs in its own isolated `logos_host` process and communicates via Qt Remote Objects IPC.
Reference implementation: `logos-accounts-module` — `metadata.json` has `"interface": "universal"`, `src/accounts_module_impl.h` is pure C++, and `mkLogosModule` runs the universal codegen automatically (no `preConfigure`).
**UI Apps** (`"type": "ui_qml"`) are QML-based UI components displayed as tabs in Basecamp's MDI workspace. Two subtypes exist: pure QML apps (no C++, call backend modules via `logos.callModule()`) and QML + C++ backend apps (process-isolated C++ backend communicating via Qt Remote Objects, QML gets a typed replica via `logos.module()`).
```
Logos Module (universal) UI App (ui_qml)
───────────────────────── ──────────────────────────
User writes: Pure C++ impl header QML (pure) or QML + .rep + C++ plugin
(std::string, int64_t, etc.) (Qt types OK in backend)
Generated: .lidl + cdylib glue + plugin class QTRO source/replica (from .rep)
(header-first cdylib pipeline)
metadata.json: "interface": "universal" "type": "ui_qml"
"type": "core" "view": "Main.qml"
Loaded by: logoscore / liblogos_core Basecamp / standalone runner
Runs in: Isolated logos_host process QML in-process, C++ backend in logos_host
Has UI: No Yes (tab in MDI workspace)
```
### Universal Module Type System
The code generator maps C++ standard types to LIDL types to Qt types:
| C++ type | LIDL type | Qt type |
|----------|-----------|---------|
| `std::string` / `const std::string&` | `tstr` | `QString` |
| `bool` | `bool` | `bool` |
| `int64_t` | `int` | `int` |
| `uint64_t` | `uint` | `int` |
| `double` | `float64` | `double` |
| `void` | `void` | `void` |
| `std::vector<std::string>` | `[tstr]` | `QStringList` |
| `std::vector<uint8_t>` | `bstr` | `QByteArray` |
| `std::vector<int64_t>` | `[int]` | `QVariantList` |
| `std::vector<bool>` | `[bool]` | `QVariantList` |
| `LogosMap` | `{tstr: any}` | `QVariantMap` |
| `LogosList` | `[any]` | `QVariantList` |
Module authors only work with the C++ column. The generator handles everything else. `LogosMap`/`LogosList` (from `<logos_json.h>`) are `nlohmann::json` aliases for returning rich structured data while keeping the impl Qt-free.
### How logos-dev-boost Layers Work Together
```
Layer 1: AGENTS.md / CLAUDE.md Always loaded at session start
(compressed docs index) Every AI tool reads these automatically
Layer 2: Guidelines Loaded into AGENTS.md content
(core, universal-module, Conventions the agent must always follow
ui-app, nix-build, etc.)
Layer 3: Skills Activated on demand by the AI agent
(create-module, package, Detailed step-by-step task guides
test, wrap-lib, etc.)
Layer 4: MCP Server Called by the agent when it needs live data
(project-info, search-docs, Parses the actual project on disk
api-reference, build-help)
Layer 5: Scaffolding Generates new projects from templates
(init command, templates) Pre-configured with correct AI context
```
## User/Agent Journeys
### Journey 1: Create a Universal C++ Module
The primary journey. A developer (or AI agent) creates a pure C++ module with no Qt in user code.
**Step 1: Scaffold the project**
```
nix run github:logos-co/logos-dev-boost -- init crypto_utils --type module
```
Or tell an AI agent in an empty directory: "create a new Logos module called crypto_utils that provides hashing utilities"
The `create-universal-module` skill activates. Output:
```
crypto_utils/
├── src/
│ ├── crypto_utils_impl.h # Pure C++ class (std::string, bool, etc.)
│ └── crypto_utils_impl.cpp # Implementation stubs
├── metadata.json # "interface": "universal", "type": "core"
├── CMakeLists.txt # logos_module() — generated_code globbed automatically
├── flake.nix # mkLogosModule (universal codegen runs automatically)
├── tests/
│ ├── main.cpp # LOGOS_TEST_MAIN() entry point
│ ├── test_crypto_utils.cpp # Unit tests using LOGOS_TEST() and assertions
│ └── CMakeLists.txt # logos_test() macro (auto-detected by builder)
├── CLAUDE.md # Generated: knows this is a universal module
├── AGENTS.md # Universal context for any AI tool
└── .mcp.json # MCP server registration
```
**Step 2: Implement business logic in pure C++**
```cpp
#pragma once
#include <string>
#include <vector>
#include <cstdint>
class CryptoUtilsImpl {
public:
std::string hash(const std::string& input);
bool verify(const std::string& input, const std::string& hash);
std::string generateKey(int64_t bits);
std::vector<std::string> listAlgorithms();
};
```
No `Q_OBJECT`, no `Q_INVOKABLE`, no `QString`. The code generator handles all Qt integration at build time.
**Step 3: Build**
```bash
nix build
```
The codegen runs automatically — `mkLogosModule` invokes the universal pipeline (you write no `preConfigure`). It derives a `.lidl` from the impl header, then emits the Qt-plugin glue and a Qt-free C-ABI export wrapper:
```bash
logos-cpp-generator --header-to-lidl src/crypto_utils_impl.h \
--impl-class CryptoUtilsImpl --metadata metadata.json \
-o ./generated_code/crypto_utils.lidl
logos-qt-host-generator --lidl ./generated_code/crypto_utils.lidl --backend cdylib \
--output-dir ./generated_code
logos-cpp-generator --lidl ./generated_code/crypto_utils.lidl --backend cdylib \
--impl-class CryptoUtilsImpl --impl-header crypto_utils_impl.h \
--output-dir ./generated_code
```
This produces `generated_code/crypto_utils.lidl`, `crypto_utils_cdylib_glue.{h,cpp}` (uniform Qt-plugin glue), and `crypto_utils_module_impl.cpp` (Qt-free C-ABI export wrapper). `LogosModule.cmake` globs these automatically — you don't list them in `CMakeLists.txt`.
**Step 4: Test with logoscore**
```bash
logoscore -D -m ./result/lib &
logoscore load-module crypto_utils
logoscore call crypto_utils hash hello_world
logoscore stop
```
**Step 5: Unit test (no logoscore needed)**
```bash
nix build .#unit-tests -L
```
Unit tests use logos-test-framework (`LOGOS_TEST()` macros, `LOGOS_ASSERT_*`) and instantiate `CryptoUtilsImpl` directly — it is a plain C++ class with no framework dependencies. `logos-module-builder` auto-detects `tests/CMakeLists.txt` and creates the `unit-tests` target.
**Step 6: Inter-module communication**
Other modules call crypto_utils via LogosAPI:
```cpp
LogosResult result = api->callModule("crypto_utils", "hash", {"hello"});
if (result.success()) {
std::string hashValue = result.data().toString().toStdString();
}
```
**Step 7: Package for distribution**
```bash
lgx create crypto_utils
lgx add crypto_utils.lgx -v linux-x86_64 -f ./result/lib/crypto_utils_plugin.so
lgx add crypto_utils.lgx -v darwin-arm64 -f ./result/lib/crypto_utils_plugin.dylib
lgx verify crypto_utils.lgx
```
**What logos-dev-boost provides at each step:**
- Step 1: `init` command scaffolds from universal module template; generated CLAUDE.md/AGENTS.md teach agents the universal pattern
- Step 2: Guidelines ensure pure C++, no Qt types; the type mapping table is always available
- Step 3: Build help explains the codegen pipeline; troubleshooting for common generator errors
- Steps 4-5: Testing skill covers logos-test-framework unit tests (LOGOS_TEST, LogosTestContext, mocking) and logoscore integration tests
- Step 6: Inter-module comm skill explains LogosAPI patterns and dependency declaration
- Step 7: Packaging skill covers the full LGX workflow
### Journey 2: Wrap an External C/C++ Library as a Module
Like `logos-accounts-module` wrapping `go-wallet-sdk`, or a module wrapping libsodium.
**Step 1: Scaffold with external lib flag**
```bash
nix run github:logos-co/logos-dev-boost -- init sodium_module --type module --external-lib
```
Output includes `lib/` directory structure and `metadata.json` with `"nix.external_libraries"` pre-configured.
**Step 2: Configure the external library in metadata.json**
```json
{
"nix": {
"external_libraries": [{
"name": "libsodium",
"build_command": "make",
"output_pattern": "build/libsodium.*"
}]
}
}
```
**Step 3: Write impl header wrapping the C API**
```cpp
#pragma once
#include <string>
#include <vector>
extern "C" {
#include "lib/sodium.h"
}
class SodiumModuleImpl {
public:
std::string encrypt(const std::string& plaintext, const std::string& key);
std::string decrypt(const std::string& ciphertext, const std::string& key);
std::string generateKey();
};
```
The external C API is accessed via `extern "C"` includes. The impl class presents a clean C++ interface that the generator can process.
**Steps 4+:** Same as Journey 1 (build, test, package).
### Journey 3a: Create a Pure QML UI App
A Basecamp UI App with no C++ — QML only, calls backend modules via `logos.callModule()`.
**Step 1: Scaffold**
```bash
nix run github:logos-co/logos-dev-boost -- init notes_ui --type ui-qml
```
Output:
```
notes_ui/
├── Main.qml # QML entry point
├── metadata.json # "type": "ui_qml", "view": "Main.qml"
├── flake.nix # mkLogosQmlModule
├── CLAUDE.md
└── AGENTS.md
```
**Step 2: Develop the QML UI**
```qml
import QtQuick 2.15
import QtQuick.Controls 2.15
Item {
Button {
text: "Save Note"
onClicked: {
var result = logos.callModule("storage_module", "save", [noteField.text])
console.log("Saved:", result)
}
}
}
```
**Step 3: Build and run**
```bash
nix build
nix run . # standalone app with QML Inspector on localhost:3768
```
The QML Inspector MCP server starts automatically. AI agents can use `qml_screenshot`, `qml_find_and_click`, `qml_get_tree`, etc. to interact with and verify the UI. Write `.mjs` test files in `tests/` for headless CI testing via `nix build .#integration-test`.
### Journey 3b: Create a QML + C++ Backend UI App
A Basecamp UI App with process-isolated C++ backend and QML frontend.
**Step 1: Scaffold**
```bash
nix run github:logos-co/logos-dev-boost -- init notes_app --type ui-qml-backend
```
Output:
```
notes_app/
├── src/
│ ├── notes_app.rep # Qt Remote Objects interface
│ ├── notes_app_interface.h # extends PluginInterface
│ ├── notes_app_plugin.h # SimpleSource + ViewPluginBase
│ ├── notes_app_plugin.cpp # implementation
│ └── qml/
│ └── Main.qml # QML frontend (logos.module() replica)
├── metadata.json # "type": "ui_qml", "main": "notes_app_plugin"
├── CMakeLists.txt # REP_FILE
├── flake.nix # mkLogosQmlModule
├── CLAUDE.md
└── AGENTS.md
```
**Step 2: Define the backend interface (.rep file)**
```
class NotesApp
{
PROP(QString status READWRITE)
PROP(QVariantList notes READWRITE)
SLOT(void addNote(const QString& title))
SLOT(void deleteNote(int index))
}
```
**Step 3: Implement the C++ backend**
```cpp
class NotesAppPlugin : public NotesAppSimpleSource,
public NotesAppInterface,
public NotesAppViewPluginBase
{
Q_OBJECT
Q_PLUGIN_METADATA(IID NotesAppInterface_iid FILE "metadata.json")
Q_INTERFACES(NotesAppInterface)
public:
Q_INVOKABLE void initLogos(LogosAPI* api) {
m_logosAPI = api;
setBackend(this);
}
void addNote(const QString& title) override { /* ... */ }
void deleteNote(int index) override { /* ... */ }
};
```
**Step 4: Develop the QML frontend**
```qml
import QtQuick
import QtQuick.Controls
Item {
id: root
readonly property var backend: logos.module("notes_app")
property bool ready: false
Connections {
target: logos
function onViewModuleReadyChanged(moduleName, isReady) {
if (moduleName === "notes_app")
root.ready = isReady && root.backend !== null;
}
}
Component.onCompleted: {
root.ready = root.backend !== null && logos.isViewModuleReady("notes_app");
}
ListView {
model: backend ? backend.notes : []
delegate: Text { text: modelData.title }
}
Button {
text: "Add Note"
enabled: root.ready
onClicked: logos.watch(backend.addNote("New Note"),
function() { console.log("Added") },
function(err) { console.log("Error:", err) }
)
}
}
```
**Step 5: Build and test**
```bash
nix build
nix run . # standalone app with QML Inspector on localhost:3768
```
AI agents can test the running UI via MCP tools (`qml_screenshot`, `qml_find_and_click`, etc.). Write `.mjs` test files in `tests/` for headless CI via `nix build .#integration-test`.
**The C++/QML boundary** (taught by guidelines):
| Concern | Goes in C++ | Goes in QML |
|---------|-------------|-------------|
| Data models, state | `PROP()` in `.rep` file | Bind to `backend.property` |
| Business logic | `SLOT()` in `.rep` + implement in plugin | Never — no JS business logic |
| Module calls | `LogosAPI*` in `initLogos()` | `logos.callModule()` (pure QML only) |
| File I/O, networking | Always C++ | Never |
| UI layout, styling | Never | Always — `Logos.Theme`, `Logos.Controls` |
| User interactions | `SLOT()` methods | `logos.watch(backend.doX())` |
| Plugin lifecycle | `initLogos()` + `setBackend(this)` | N/A |
### Journey 4: AI Agent Building a Module from Scratch
What happens when a developer tells an AI agent "create a module that provides encryption utilities":
1. Agent reads AGENTS.md (always loaded) — knows about universal interface, Logos ecosystem, type system, build pipeline. This is the critical difference from not having logos-dev-boost.
2. Agent activates `create-universal-module` skill — gets step-by-step template with correct file structure, `metadata.json` schema, and the `mkLogosModule` `flake.nix` pattern (universal codegen runs automatically).
3. Agent writes pure C++ impl header — guidelines ensure it uses `std::string` not `QString`, `int64_t` not `int`, returns meaningful types from the type mapping table.
4. Agent writes `flake.nix` — skill provides the exact `mkLogosModule` template (universal codegen runs automatically; no `preConfigure`) with the correct `logos-module-builder` input.
5. Agent builds with `nix build` — build-help guidelines explain the pipeline. If errors occur, agent knows common fixes: generator type mapping issues, missing `find_package`, `metadata.json`/header class name mismatch.
6. Agent runs unit tests with `nix build .#unit-tests -L` — the scaffolded `tests/` directory uses logos-test-framework (`LOGOS_TEST()`, `LOGOS_ASSERT_*`). Tests are auto-detected by `logos-module-builder`. Agent also tests with `logoscore` for integration testing — testing skill provides exact commands and expected output patterns.
**Without logos-dev-boost:** Agent would write `Q_INVOKABLE` methods, use `QString` everywhere, try `cmake --build` instead of `nix build`, hallucinate a `LogosPlugin` base class that doesn't exist, and have no idea about the code generator pipeline.
### Journey 5: Installing logos-dev-boost for an Existing Project
For a developer with an existing Logos module who wants AI assistance:
**Step 1: Run the installer**
```bash
nix run github:logos-co/logos-dev-boost -- install
```
**Step 2: Interactive configuration**
```
Detected: Universal C++ module (accounts_module)
SDK version: logos-cpp-sdk 0.3.0
Which AI tools do you use?
[x] Claude Code
[x] Cursor
[ ] Codex
[ ] Gemini CLI
Generated:
CLAUDE.md (always-loaded context for Claude Code)
AGENTS.md (universal context for any AI tool)
.cursor/rules/logos.mdc (Cursor-specific rules)
.claude/skills/ (8 skills for Claude Code)
.mcp.json (MCP server registration)
.logos-dev-boost/ (pre-built MCP server binary)
```
**Step 3: AI tools auto-detect configuration**
- Claude Code reads `CLAUDE.md` automatically, discovers `.claude/skills/`, connects to MCP server via `.mcp.json`
- Cursor reads `AGENTS.md` automatically, loads `.cursor/rules/logos.mdc`, connects to MCP server
- Manual fallback if auto-detection fails:
- Claude Code: `claude mcp add -s local -t stdio logos-dev-boost node .logos-dev-boost/mcp-server/index.js`
- Cursor: Command Palette -> "/open MCP Settings" -> toggle on `logos-dev-boost`
- Codex: `codex mcp add logos-dev-boost -- node .logos-dev-boost/mcp-server/index.js`
## Features & Requirements
### Phase 1: Foundation (MVP)
- Always-loaded context files (AGENTS.md, CLAUDE.md) with compressed documentation index
- 7 guideline files covering core conventions, universal modules, UI apps, Nix build, testing, metadata.json, and code generation
- 8 on-demand skills for common development tasks
- Scaffolding templates for universal modules, external library modules, and UI apps
- Context file generators (AGENTS.md, CLAUDE.md, .cursor/rules, llms.txt)
- Nix flake with `init`, `install`, and `generate` commands
### Phase 2: MCP Server
- Live project introspection via 5 MCP tools (project-info, search-docs, api-reference, build-help, scaffold)
- Full-text documentation search over bundled docs
- Context-aware build commands with troubleshooting
- Interactive installer that detects AI tools and generates per-tool configuration
### Phase 3: Rich Features
- Semantic documentation search with local ONNX embeddings
- Cross-repo dependency graph tool
- LIDL language validation and preview
- Integration with logos-qt-mcp for combined dev-time and runtime introspection
### Phase 4: Ecosystem
- Third-party module skills (module authors ship skills in their repos)
- Hosted documentation API with centralized semantic search
- CI integration (`logos-dev-boost check` validates project configuration)
- Auto-update for context files when dependencies change
## Success Metrics
1. **Module creation time** — An AI agent can scaffold, build, and test a new universal C++ module in under 5 minutes (currently impossible without deep knowledge)
2. **Zero hallucinated APIs** — Agents never suggest non-existent Logos APIs or use Qt types in universal module code
3. **Build success rate** — Agent-generated Nix flakes and C++ impl headers build on first try
4. **Correct interface choice** — Agents use the universal interface for modules and ui_qml for UI apps, never mixing the two
5. **Onboarding time** — New human developers can create their first module in under 30 minutes with AI assistance
## Supported Platforms
logos-dev-boost runs on any platform with Nix:
- Linux (x86_64, aarch64)
- macOS (x86_64, aarch64)
The generated context files (AGENTS.md, CLAUDE.md, skills) are plain text and work on any platform.
---
# Project Description
## Project Structure
```
logos-dev-boost/
├── docs/ # Documentation (this directory)
│ ├── index.md # Entry point
│ ├── spec.md # Domain spec: purpose, journeys, features
│ └── project.md # Implementation details (this file)
├── guidelines/ # Always-loaded AI conventions
│ ├── core.md # Two component types, naming, file structure
│ ├── universal-module.md # Pure C++ impl pattern, type mapping, codegen
│ ├── ui-app.md # ui_qml apps: pure QML + QML with C++ backend
│ ├── nix-build.md # Flake structure, build commands, overrides
│ ├── testing.md # logoscore, unit tests, TEST_GROUPS
│ ├── metadata-json.md # Full metadata.json schema
│ └── codegen.md # logos-cpp-generator pipeline, LIDL, types
├── skills/ # On-demand task knowledge (agentskills.io)
│ ├── create-universal-module/ # Scaffold + implement a universal C++ module
│ ├── wrap-external-lib/ # Wrap a C/C++ library as a module
│ ├── create-ui-app/ # QML UI app for Basecamp (pure QML or QML + backend)
│ ├── package-lgx/ # Create + distribute LGX packages
│ ├── inter-module-comm/ # LogosAPI patterns, dependency declaration
│ ├── testing-modules/ # logoscore + unit testing patterns
│ ├── nix-flake-setup/ # Flake config, overrides, workspace integration
│ └── add-to-workspace/ # Register module in logos-workspace
├── templates/ # Scaffolding templates (used by init command)
│ ├── universal-module/ # Pure C++ module
│ ├── universal-module-extlib/ # With external library wrapping
│ └── ui-app/ # QML UI app (pure QML + QML with backend)
├── generators/ # Context file generators (TypeScript)
│ ├── generate-agents-md.ts # AGENTS.md with compressed docs index
│ ├── generate-claude-md.ts # CLAUDE.md (imports AGENTS.md content)
│ ├── generate-cursor-rules.ts # .cursor/rules/logos.mdc
│ └── generate-llms-txt.ts # llms.txt from docs/
├── mcp-server/ # MCP server (TypeScript, stdio transport)
│ ├── index.ts # Server entry point
│ └── tools/ # MCP tool implementations
│ ├── project-info.ts # SDK version, module type, build targets
│ ├── search-docs.ts # Full-text search over docs/ (fuse.js)
│ ├── api-reference.ts # Type system, LogosAPI, LogosResult
│ ├── build-help.ts # Context-aware build commands
│ └── scaffold.ts # Wraps init templates
├── installer/ # One-command setup
│ ├── cli.ts # CLI entry point (init, install, generate)
│ └── install.ts # IDE detection, file generation
├── tests/ # Tests
├── flake.nix # Nix package definition
├── package.json # Node.js package
├── tsconfig.json # TypeScript configuration
└── README.md
```
## Stack, Frameworks & Dependencies
| Component | Technology | Purpose |
|-----------|-----------|---------|
| MCP server | TypeScript, `@modelcontextprotocol/sdk` | AI agent tool integration via stdio |
| Documentation search | fuse.js | Full-text search over bundled docs |
| Context generators | TypeScript, Node.js | Generate AGENTS.md, CLAUDE.md, .cursor/rules |
| Scaffolding | Nix flake templates, `logos-module-builder` | Project initialization |
| Build/packaging | Nix flakes | Self-contained offline-capable distribution |
### External Logos Dependencies
| Dependency | Role | Source |
|-----------|------|--------|
| logos-module-builder | Nix flake templates for module scaffolding | github:logos-co/logos-module-builder |
| logos-cpp-sdk | SDK headers, code generator (`logos-cpp-generator`) | github:logos-co/logos-cpp-sdk |
| logos-liblogos | Runtime library documentation reference | github:logos-co/logos-liblogos |
## Components
### Guidelines
Guidelines are Markdown files loaded into the always-on context (AGENTS.md / CLAUDE.md). Each is kept under 2000 tokens to minimize context cost while providing essential conventions.
| Guideline | Content |
|-----------|---------|
| `core.md` | Two component types (Universal Module vs UI App), naming conventions, file structure, `metadata.json` as source of truth |
| `universal-module.md` | Pure C++ impl pattern, type mapping table, `"interface": "universal"`, impl class naming (`<name>_impl.h`), public methods = module API |
| `ui-app.md` | `ui_qml` apps: pure QML (logos.callModule bridge) and QML + C++ backend (Qt Remote Objects, .rep file, SimpleSource + ViewPluginBase, logos.module() replica) |
| `nix-build.md` | Flake structure, `follows` declarations, `preConfigure` for codegen, build commands, `--auto-local` |
| `testing.md` | Unit tests (call impl class directly), logoscore integration tests, `TEST_GROUPS`, `nix flake check` |
| `metadata-json.md` | Full schema including `"interface"`, `"dependencies"`, `"nix"` config, `"external_libraries"`, `"cmake"` settings |
| `codegen.md` | header-first cdylib pipeline, C++ to LIDL type mapping, generated file structure, LIDL format |
### Skills
Skills are on-demand knowledge modules following the agentskills.io specification. Each lives in its own directory with a `SKILL.md` file containing YAML frontmatter (`name`, `description`) and Markdown instructions.
| Skill | Trigger | Content |
|-------|---------|---------|
| `create-universal-module` | Creating a new Logos module | Full scaffold: metadata.json, flake.nix with preConfigure, CMakeLists.txt, impl header template |
| `wrap-external-lib` | Wrapping a C/C++ library | External library config, `extern "C"` patterns, based on logos-accounts-module |
| `create-ui-app` | Creating a Basecamp UI app | Two paths: pure QML (Main.qml + logos.callModule) or QML + C++ backend (.rep, SimpleSource, ViewPluginBase, logos.module replica) |
| `package-lgx` | Packaging for distribution | lgx create/add/verify workflow, portable builds, nix-bundle-lgx |
| `inter-module-comm` | Module-to-module calls | `LogosAPI::callModule()`, dependency declaration, `LogosResult` handling |
| `testing-modules` | Writing tests | Unit tests, logoscore integration, TEST_GROUPS, mock transport |
| `nix-flake-setup` | Nix configuration | Flake template, inputs, follows, preConfigure, override-input |
| `add-to-workspace` | Registering in logos-workspace | flake.nix inputs, scripts/ws REPOS, dep-graph.nix |
### Generators
TypeScript scripts that produce IDE-specific context files from the guidelines and documentation:
| Generator | Output | Description |
|-----------|--------|-------------|
| `generate-agents-md.ts` | `AGENTS.md` | Universal context file. Compressed docs index (~8-15KB), conventions, type system. Works with all AI tools. |
| `generate-claude-md.ts` | `CLAUDE.md` | Claude Code-specific context. Imports AGENTS.md content plus Claude-specific skill references. |
| `generate-cursor-rules.ts` | `.cursor/rules/logos.mdc` | Cursor-specific rules with file glob patterns for activation. |
| `generate-llms-txt.ts` | `llms.txt`, `llms-full.txt` | Machine-readable documentation index following the llms.txt specification. |
Generators detect the project type from `metadata.json` (`"interface": "universal"` vs `"type": "ui_qml"`) and include context specific to that project type.
### MCP Server
TypeScript MCP server using `@modelcontextprotocol/sdk` with stdio transport. Provides live project introspection tools.
| Tool | Input | Output |
|------|-------|--------|
| `logos_project_info` | (none — reads current directory) | Project type, interface type, SDK version, dependencies, build targets |
| `logos_search_docs` | `{ "query": "..." }` | Ranked search results from bundled documentation |
| `logos_api_reference` | `{ "interface": "LogosAPI" }` | Type mapping table, method signatures, usage examples |
| `logos_build_help` | `{ "action": "build" }` | Context-aware build commands, codegen pipeline explanation, troubleshooting |
| `logos_scaffold` | `{ "name": "...", "type": "module" }` | Creates project from template, returns file list and next steps |
### Installer
The installer (`install.ts`) performs interactive setup:
1. Detects project type from `flake.nix` inputs and `metadata.json`
2. Detects installed AI tools (checks for `.claude/`, `.cursor/`, presence of `claude`/`codex`/`gemini` in PATH)
3. Generates appropriate context files per detected tool
4. Builds MCP server locally (via Nix) to `.logos-dev-boost/`
5. Writes `.mcp.json` pointing to the built MCP server binary
6. Installs skills to tool-specific directories
## CLI Reference
### `logos-dev-boost init`
Scaffold a new Logos project.
```bash
nix run github:logos-co/logos-dev-boost -- init <name> --type <module|ui-qml|ui-qml-backend> [--external-lib]
```
| Argument | Description |
|----------|-------------|
| `<name>` | Project name (snake_case, e.g., `crypto_utils`) |
| `--type module` | Universal C++ module (default) |
| `--type ui-qml` | Pure QML UI app for Basecamp (no C++) |
| `--type ui-qml-backend` | QML + process-isolated C++ backend UI app |
| `--external-lib` | Add external library wrapping scaffold (modules only) |
Creates the directory, generates the appropriate template (mkLogosModule for modules, mkLogosQmlModule for UI apps), adds AI context files, and initializes git.
### `logos-dev-boost install`
Configure AI tools for an existing Logos project.
```bash
nix run github:logos-co/logos-dev-boost -- install
```
Interactive prompt asks which AI tools to configure. Generates context files and MCP server registration.
### `logos-dev-boost generate`
Regenerate context files (useful after updating dependencies or documentation).
```bash
nix run github:logos-co/logos-dev-boost -- generate [--agents-md] [--claude-md] [--cursor-rules] [--llms-txt]
```
Without flags, regenerates all context files. With flags, regenerates only the specified files.
## Operational
### Building logos-dev-boost
```bash
# Install dependencies
npm install
# Build TypeScript
npm run build
# Or via Nix (produces self-contained package)
nix build
```
### Testing
```bash
# Run tests
npm test
# Via Nix
nix flake check
```
### Nix Package Outputs
```nix
packages.default # Full logos-dev-boost (docs + guidelines + skills + generators + MCP server)
packages.docs # Documentation bundle only
apps.default # CLI entry point (init, install, generate)
apps.mcp-server # MCP server only
```
## Extension Points
### Custom Guidelines
Add `.md` files to your project's `.ai/guidelines/` directory. These are merged with logos-dev-boost guidelines when generating AGENTS.md / CLAUDE.md.
### Custom Skills
Add `SKILL.md` files to your project's `.ai/skills/<skill-name>/` directory. These are installed alongside logos-dev-boost skills.
### Third-Party Module Skills
Module authors can ship skills in their repos at `resources/boost/skills/<skill-name>/SKILL.md`. When a project depends on that module and runs `logos-dev-boost install`, these skills are automatically discovered and installed.
## Relationship to logos-module-builder
logos-dev-boost wraps `logos-module-builder` for scaffolding — it does not duplicate its templates. The `init` command calls `nix flake init -t logos-module-builder` (appropriate variant) and then layers on:
- Universal module impl header template (for `--type module`)
- Pure QML template (for `--type ui-qml`) or QML + C++ backend template (for `--type ui-qml-backend`)
- AI context files (AGENTS.md, CLAUDE.md, .mcp.json)
- Test skeleton
This means logos-dev-boost always uses the latest logos-module-builder templates for Nix/CMake scaffolding.
## Consumers
- AI coding agents (Claude Code, Cursor, Copilot, Codex, Gemini) via AGENTS.md, CLAUDE.md, skills, and MCP
- Human developers via documentation, scaffolding, and generated project structure
- CI/CD pipelines via `logos-dev-boost check` (Phase 4)
---
# Code Generator (logos-cpp-generator)
## Overview
`logos-cpp-generator` bridges pure C++ module implementations to the Logos runtime's Qt plugin system. Module authors write standard C++ and the generator produces all Qt boilerplate automatically.
Universal modules are **header-first cdylibs**: the generator derives a LIDL contract from your impl header, then emits a Qt-free cdylib (exporting the common module-impl C ABI) wrapped by a uniform Qt-plugin glue that `logos_host` loads unchanged. Your module's own translation units stay Qt-free — Qt appears only in the generated glue.
## The universal pipeline
This is how universal modules are built. You write only the impl class; `logos-module-builder` runs every step below for you in `preConfigure` (see [In flake.nix](#in-flakenix)).
```
C++ impl header (your code)
▼ logos-cpp-generator --header-to-lidl
parseImplHeader() — extracts public methods, maps C++ types to LIDL types
<name>.lidl (derived interface contract; also the events sidecar dependents consume)
├──► logos-qt-host-generator --lidl --backend cdylib
│ └──► <name>_cdylib_glue.h / <name>_cdylib_glue.cpp
│ — uniform Qt-plugin glue over the module-impl C ABI
└──► logos-cpp-generator --lidl --backend cdylib
└──► <name>_module_impl.cpp
— Qt-free C-ABI export wrapper around your impl class
```
### Commands
```bash
# 1. Derive the LIDL contract from your impl header.
logos-cpp-generator --header-to-lidl src/<name>_impl.h \
--impl-class <ImplClassName> \
--metadata metadata.json \
-o ./generated_code/<name>.lidl
# 2. Generate the uniform Qt-plugin glue (logos_host loads it unchanged).
logos-qt-host-generator --lidl ./generated_code/<name>.lidl \
--backend cdylib \
--output-dir ./generated_code
# 3. Generate the Qt-free C-ABI export wrapper (+ typed event emitters)
# around your hand-written impl class.
logos-cpp-generator --lidl ./generated_code/<name>.lidl \
--backend cdylib \
--impl-class <ImplClassName> \
--impl-header <name>_impl.h \
--output-dir ./generated_code
```
You never run these by hand — `mkLogosModule` invokes them automatically when `metadata.json` declares `"interface": "universal"`.
| Flag | Description |
|------|-------------|
| `--header-to-lidl <path>` | Path to the pure C++ impl header to derive the LIDL contract from |
| `--lidl <path>` | Path to a `.lidl` contract (steps 2 & 3 consume the file emitted by step 1) |
| `--backend cdylib` | Emit the cdylib module-impl C ABI artifacts (glue + export wrapper) |
| `--impl-class <name>` | Name of the C++ implementation class (PascalCase + `Impl`) |
| `--impl-header <name>` | Header filename (for include directives in generated code) |
| `--metadata <path>` | Path to metadata.json (provides name, version, description) |
| `-o <path>` / `--output-dir <path>` | Output `.lidl` file (step 1) / directory for generated files (steps 2 & 3) |
### Generated Files
All land in `generated_code/`. Don't edit them and don't list them in `CMakeLists.txt` — `LogosModule.cmake` globs them automatically (see [In CMakeLists.txt](#in-cmakeliststxt)).
**`<name>.lidl`** — the interface contract derived from your impl header. Doubles as the published events sidecar that dependents' typed-event codegen consumes.
**`<name>_cdylib_glue.h` / `<name>_cdylib_glue.cpp`** — the uniform Qt-plugin glue. A `QObject` subclass with `Q_PLUGIN_METADATA` + `Q_INTERFACES` and a `LogosProviderObject` that marshals method calls to JSON and forwards them to the cdylib's module-impl C ABI (`dispatch` / `getMethods` / `set_context` / emit callback / `accept_token`). The glue is identical regardless of the module's source language — it only knows the C ABI.
**`<name>_module_impl.cpp`** — the Qt-free C-ABI export wrapper. Implements the common module-impl C ABI (`logos_module_impl.h`) around your hand-written impl class, plus typed event emitters. This translation unit links no Qt; it is what makes a universal module a cdylib.
### Type Mapping Table
| C++ type | LIDL type | JSON / wire |
|----------|-----------|-------------|
| `std::string` / `const std::string&` | `tstr` | string |
| `bool` | `bool` | bool |
| `int64_t` | `int` | number |
| `uint64_t` | `uint` | number |
| `double` | `float64` | number |
| `void` | `void` | — |
| `std::vector<std::string>` | `[tstr]` | array of string |
| `std::vector<uint8_t>` | `bstr` | `{"_bytes":"<base64url>"}` |
| `std::vector<int64_t>` | `[int]` | array of number |
| `LogosMap` | `{tstr: any}` | object |
| `LogosList` | `[any]` | array |
| Anything else | `any` | any |
`LogosMap` and `LogosList` (from `<logos_json.h>`) are `nlohmann::json` aliases for returning structured data without Qt. The generator sets a `jsonReturn` flag on these methods so the dispatch layer carries the JSON through faithfully.
## LIDL (define the contract first)
LIDL is a lightweight Interface Definition Language. The universal pipeline derives a `.lidl` from your header automatically (step 1 above), but you can also hand-write one to define the interface before the implementation:
```
module crypto_utils {
version "1.0.0"
description "Cryptographic utilities"
method hash(input: tstr) -> tstr
method verify(input: tstr, hash: tstr) -> bool
method generateKey(bits: int) -> tstr
method listAlgorithms() -> [tstr]
}
```
A hand-written `.lidl` feeds steps 2 & 3 directly (this is the `cdylib` interface; the `universal` interface just derives the `.lidl` from your header first). Both routes produce identical generated output.
## Common Issues
- **Unknown type warning**: If the generator encounters a C++ type not in the mapping table, it maps to `any`. Prefer explicit types from the table.
- **Class not found**: `--impl-class` must exactly match the class name in the header (case-sensitive).
- **metadata.json mismatch**: The `name` in metadata.json must match the expected plugin binary name.
- **Generated files not found by CMake**: You do *not* list `generated_code/` files in `SOURCES` — `LogosModule.cmake` globs them. Just make sure `generated_code` is in `INCLUDE_DIRS`.
## In CMakeLists.txt
List only your own sources. `LogosModule.cmake` globs `generated_code/*.cpp` and `*.h` automatically (excluding `logos_sdk`/`*_api`), so the generated glue is picked up without being named:
```cmake
logos_module(
NAME my_module
SOURCES
src/my_module_impl.h
src/my_module_impl.cpp
INCLUDE_DIRS
${CMAKE_CURRENT_SOURCE_DIR}/generated_code
)
```
## In flake.nix
You don't write a `preConfigure` — `mkLogosModule` runs the universal pipeline for you when `metadata.json` sets `"interface": "universal"`:
```nix
{
inputs = {
logos-module-builder.url = "github:logos-co/logos-module-builder";
nix-bundle-lgx.url = "github:logos-co/nix-bundle-lgx";
};
outputs = inputs@{ logos-module-builder, ... }:
logos-module-builder.lib.mkLogosModule {
src = ./.;
configFile = ./metadata.json;
flakeInputs = inputs;
};
}
```
---
# Logos Core Conventions
## Two Component Types
Logos has two fundamentally different types of components. Always identify which you are building before writing code.
**Logos Modules** (`"type": "core"`) — Process-isolated backend services. Pure C++ implementation using standard types. No Qt types in user code. All Qt glue is generated at build time. Loaded by `logoscore` or `liblogos_core`. Each runs in its own `logos_host` subprocess.
**UI Apps** (`"type": "ui_qml"`) — QML-based UI apps displayed as tabs in Basecamp's MDI workspace. Two subtypes: pure QML (no C++, calls modules via `logos.callModule()`) or QML + C++ backend (process-isolated via Qt Remote Objects, QML gets a typed replica via `logos.module()`).
**Rule:** Never mix these. A module is either core (headless, universal interface) or UI (visual, ui_qml). If something needs both backend logic and a UI, create a core module for the logic and a separate UI app that calls it, or use a QML + backend app. The `full-app` scaffold type does exactly this — it creates a `module/` subdirectory (core) and a `ui/` subdirectory (UI app) with a shared root.
## Full App Layout
When a project requires both a module and a UI, use the `full-app` layout:
```
logos-<name>/ # Open this in your IDE
<name>-module/ # Universal C++ module (core backend)
metadata.json # "type": "core", "interface": "universal"
flake.nix # standalone — cd <name>-module && nix build
src/<name>_impl.h
src/<name>_impl.cpp
<name>-ui/ # Basecamp UI app (frontend)
metadata.json # "type": "ui_qml", "dependencies": ["<name>"]
flake.nix # includes <name>.url = "path:../<name>-module"
src/<name>_ui_plugin.h/cpp
src/<Pascal>UiBackend.h/cpp
src/qml/Main.qml
project.json # { "type": "full-app", "name": "<name>" }
AGENTS.md / CLAUDE.md / .mcp.json
```
Scaffold with: `logos-dev-boost init <name> --type full-app`
Each sub-project is a standalone flake — build inside the sub-directory:
```bash
cd <name>-module && git init && git add -A && nix build
cd ../<name>-ui && git init && git add -A && nix build
```
## Module Naming
- Module names use `snake_case`: `crypto_utils`, `accounts_module`, `storage_module`
- Impl class is `PascalCase` + `Impl`: `CryptoUtilsImpl`, `AccountsModuleImpl`
- Impl header is `<name>_impl.h`: `crypto_utils_impl.h`
- Plugin binary is `<name>_plugin.so/.dylib`: `crypto_utils_plugin.so`
- The `name` in `metadata.json` must match the binary name prefix exactly
## File Structure
Universal module:
```
my_module/
├── src/
│ ├── my_module_impl.h # Public API (pure C++ types)
│ └── my_module_impl.cpp # Implementation
├── metadata.json # "interface": "universal", "type": "core"
├── CMakeLists.txt # logos_module() macro
├── flake.nix # preConfigure runs logos-cpp-generator
└── tests/
```
Pure QML app:
```
my_app/
├── Main.qml # QML entry point
├── metadata.json # "type": "ui_qml", "view": "Main.qml"
└── flake.nix # mkLogosQmlModule
```
QML + C++ backend app:
```
my_app/
├── src/
│ ├── my_app.rep # Qt Remote Objects interface
│ ├── my_app_interface.h # extends PluginInterface
│ ├── my_app_plugin.h/cpp # SimpleSource + ViewPluginBase
│ └── qml/Main.qml # QML frontend (logos.module() replica)
├── metadata.json # "type": "ui_qml", "main": "my_app_plugin"
├── CMakeLists.txt # logos_module() with REP_FILE
└── flake.nix # mkLogosQmlModule
```
## metadata.json Is the Source of Truth
Every module and UI app must have a `metadata.json`. It declares identity, type, interface, dependencies, and build configuration. The `name` field must match the binary name prefix. The `dependencies` array must list exact `name` values from dependent modules' own `metadata.json` files.
## Inter-Module Communication
All cross-module calls go through `LogosAPI`:
```cpp
LogosResult result = api->callModule("module_name", "method_name", {arg1, arg2});
if (result.success()) {
QVariant data = result.data();
}
```
Always handle the case where a target module is not loaded. Always declare dependencies in `metadata.json`.
---
# metadata.json Schema
## Full Schema
```json
{
"name": "my_module",
"version": "1.0.0",
"description": "What this module does",
"author": "Author Name",
"type": "core",
"interface": "universal",
"category": "general",
"main": "my_module_plugin",
"dependencies": [],
"include": [],
"capabilities": [],
"nix": {
"packages": {
"build": [],
"runtime": []
},
"external_libraries": [],
"cmake": {
"find_packages": [],
"extra_sources": [],
"extra_include_dirs": [],
"extra_link_libraries": []
}
}
}
```
## Required Fields
| Field | Type | Description |
|-------|------|-------------|
| `name` | string | Module identifier. Must match binary prefix: `my_module` -> `my_module_plugin.so` |
| `version` | string | Semantic version (`"1.0.0"`) |
| `type` | string | `"core"` for modules, `"ui_qml"` for UI apps |
| `main` | string | Plugin binary name without extension: `"my_module_plugin"` (optional for pure QML UI apps) |
## Universal Module Fields
| Field | Value | Description |
|-------|-------|-------------|
| `interface` | `"universal"` | Signals that this module uses pure C++ impl + code generation |
When `"interface": "universal"` is set, `mkLogosModule` automatically runs the universal codegen pipeline (header → `.lidl` → cdylib glue) before CMake — no `preConfigure` needed. See [codegen.md](codegen.md).
## Dependencies
```json
"dependencies": ["storage_module", "crypto_module"]
```
Values must match the `name` field in the dependency module's own `metadata.json`. The runtime loads dependencies before the module.
Flake input attribute names should also match the dependency module names when possible. Example: if you depend on `storage_module` from repo `logos-storage-module`, the flake input should be named `logos-storage-module`.
## External Libraries
```json
"nix": {
"external_libraries": [
{
"name": "mylib",
"build_command": "make static-library",
"output_pattern": "build/libmylib.*"
}
]
}
```
For Go libraries, add `"go_build": true`. The external library source is provided as a non-flake input in `flake.nix` and mapped via `externalLibInputs`.
## Nix Packages
```json
"nix": {
"packages": {
"build": ["pkg-config"],
"runtime": ["nlohmann_json", "openssl"]
}
}
```
`build` packages are available during compilation only. `runtime` packages are linked and available at runtime.
## CMake Configuration
```json
"nix": {
"cmake": {
"find_packages": ["Threads", "OpenSSL"],
"extra_sources": ["src/helper.cpp"],
"extra_include_dirs": ["include"],
"extra_link_libraries": ["Threads::Threads"]
}
}
```
These values are passed to CMake by the `logos_module()` macro. They supplement, not replace, the automatic SDK and Qt dependencies.
## UI App Specific Fields
```json
{
"type": "ui_qml",
"view": "Main.qml",
"icon": "icon.png",
"category": "tools"
}
```
| Field | Type | Description |
|-------|------|-------------|
| `view` | string | **Required** for `ui_qml`. Path to QML entry point (e.g., `"Main.qml"` or `"qml/Main.qml"`) |
| `main` | string | **Optional**. If present, indicates a C++ backend plugin (e.g., `"my_app_plugin"`). If absent, the app is pure QML |
Pure QML apps have no `"main"` field — no C++ compilation occurs. QML + backend apps have both `"main"` (C++ plugin) and `"view"` (QML entry point).
UI apps do not use `"interface": "universal"` — they use `mkLogosQmlModule` in their flake.nix.
---
# Nix Build Patterns
## All Builds Go Through Nix
Never run raw `cmake` without `nix develop` or `ws develop`. The Nix build system provides Qt, the SDK, the code generator, and all dependencies. Running `cmake --build` outside Nix will fail.
## Flake Structure for Universal Modules
```nix
{
description = "My Logos Module";
inputs = {
logos-module-builder.url = "github:logos-co/logos-module-builder";
nix-bundle-lgx.url = "github:logos-co/nix-bundle-lgx";
};
outputs = inputs@{ logos-module-builder, ... }:
logos-module-builder.lib.mkLogosModule {
src = ./.;
configFile = ./metadata.json;
flakeInputs = inputs;
};
}
```
No `preConfigure` is needed. When `metadata.json` declares `"interface": "universal"`, `mkLogosModule` runs the universal codegen pipeline (header → `.lidl` → cdylib glue) for you before CMake. See [codegen.md](codegen.md).
## Flake Structure for Modules with External Libraries
Add the external library as a non-flake input and pass it via `externalLibInputs`:
```nix
inputs = {
logos-module-builder.url = "github:logos-co/logos-module-builder";
nix-bundle-lgx.url = "github:logos-co/nix-bundle-lgx";
my-lib = {
url = "github:org/my-lib/commit-hash";
flake = false;
};
};
outputs = inputs@{ logos-module-builder, ... }:
logos-module-builder.lib.mkLogosModule {
src = ./.;
configFile = ./metadata.json;
flakeInputs = inputs;
externalLibInputs = {
mylib = inputs.my-lib;
};
};
```
The universal codegen still runs automatically — `externalLibInputs` only adds the external library to the build; it doesn't change the codegen pipeline.
## Build Commands
```bash
nix build # Build the module
nix build .#lib # Build just the shared library
nix flake check -L # Run tests
nix develop # Enter dev shell with build tools
```
Inside the workspace (multi-repo):
```bash
ws build my-module # Build
ws build my-module --auto-local # Build with local dirty dep overrides
ws test my-module # Run tests
ws test my-module --auto-local # Test with local overrides
ws develop my-module # Enter dev shell
```
## Key Rules
- Flake inputs must be tracked by git. Run `git add <file>` before Nix can see new files.
- `nixpkgs` follows `logos-cpp-sdk` via `logos-module-builder`. Never pin a separate nixpkgs.
- Qt version is fixed by `logos-cpp-sdk`. All repos must use the same Qt to avoid version conflicts.
- Use `-L` flag to stream build logs: `nix build -L`
- Use `--override-input` to test with local dependency changes (the `ws` CLI does this for you with `--auto-local`).
## Flake Structure for UI QML Apps
Pure QML (no C++ backend):
```nix
{
inputs.logos-module-builder.url = "github:logos-co/logos-module-builder";
outputs = inputs@{ logos-module-builder, ... }:
logos-module-builder.lib.mkLogosQmlModule {
src = ./.;
configFile = ./metadata.json;
flakeInputs = inputs;
};
}
```
QML + C++ backend:
```nix
{
inputs = {
logos-module-builder.url = "github:logos-co/logos-module-builder";
# Add module dependencies as inputs:
# some_module.url = "github:logos-co/logos-some-module";
};
outputs = inputs@{ logos-module-builder, ... }:
logos-module-builder.lib.mkLogosQmlModule {
src = ./.;
configFile = ./metadata.json;
flakeInputs = inputs;
};
}
```
Both use `mkLogosQmlModule` (not `mkLogosModule`). No `preConfigure` needed — the `.rep` file (if present) is handled automatically via `REP_FILE` in CMakeLists.txt.
## Dev Shell for CMake Iteration
```bash
nix develop
cmake -B build -GNinja && cmake --build build
```
The dev shell provides all build dependencies. Use this for rapid C++ iteration without full Nix rebuilds.
---
# Testing Logos Modules
## Unit Tests with logos-test-framework
Universal modules have a plain C++ impl class. Test it using the logos-test-framework, which is provided automatically by `logos-module-builder`.
### Test File Structure
```
tests/
├── main.cpp # LOGOS_TEST_MAIN() entry point
├── test_my_module.cpp # Test cases using LOGOS_TEST()
└── CMakeLists.txt # logos_test() macro
```
### Writing Tests
```cpp
// tests/main.cpp
#include <logos_test.h>
LOGOS_TEST_MAIN()
```
```cpp
// tests/test_my_module.cpp
#include <logos_test.h>
#include "../src/my_module_impl.h"
LOGOS_TEST(hash_returns_nonempty_string) {
MyModuleImpl impl;
LOGOS_ASSERT_FALSE(impl.hash("hello").empty());
}
LOGOS_TEST(verify_matches_hash) {
MyModuleImpl impl;
auto hash = impl.hash("hello");
LOGOS_ASSERT_TRUE(impl.verify("hello", hash));
}
```
### CMakeLists.txt for Tests
```cmake
# tests/CMakeLists.txt
cmake_minimum_required(VERSION 3.14)
project(MyModuleTests LANGUAGES CXX)
include(LogosTest)
logos_test(
NAME my_module_tests
MODULE_SOURCES ../src/my_module_impl.cpp
TEST_SOURCES
main.cpp
test_my_module.cpp
)
```
The `logos_test()` CMake macro handles all framework wiring: Qt dependencies, SDK mock headers, include paths, and CTest registration.
### Mocking Other Modules
Use `LogosTestContext` when your module calls other modules:
```cpp
LOGOS_TEST(calls_waku_publish) {
auto t = LogosTestContext("chat_module");
t.mockModule("waku_module", "relayPublish").returns(true);
ChatImpl impl;
t.init(&impl);
impl.sendMessage("hello");
LOGOS_ASSERT(t.moduleCalled("waku_module", "relayPublish"));
}
```
### Mocking C Libraries
For modules wrapping external C/C++ libraries, write mock stubs:
```cpp
// tests/mocks/mock_libcalc.cpp
#include <logos_clib_mock.h>
extern "C" { #include "libcalc.h" }
extern "C" int calc_add(int a, int b) {
LOGOS_CMOCK_RECORD("calc_add");
return LOGOS_CMOCK_RETURN(int, "calc_add");
}
```
Reference them in CMake:
```cmake
logos_test(
NAME calc_module_tests
MODULE_SOURCES ../src/calc_module_impl.cpp
TEST_SOURCES main.cpp test_calc.cpp
MOCK_C_SOURCES mocks/mock_libcalc.cpp
)
```
### Running Unit Tests
```bash
nix build .#unit-tests -L # Build and run unit tests
nix flake check -L # All Nix checks including tests
```
`logos-module-builder` auto-detects `tests/CMakeLists.txt` and adds `checks.<system>.unit-tests` and `packages.<system>.unit-tests` automatically.
## Integration Tests with logoscore
Test the module as a loaded plugin via the headless runtime. Start a clean daemon,
load the module(s), then call methods with the `call` client command:
```bash
# Start a clean daemon, then load the module(s) (deps resolved automatically)
logoscore -D -m ./result/lib &
logoscore load-module my_module
logoscore load-module other_module
# Call methods (positional args; @file reads a parameter from a file)
logoscore call my_module doSomething test_input
logoscore call my_module init config
logoscore call my_module process data
logoscore call my_module callOther hello
# Stop the daemon when done
logoscore stop
```
logoscore arguments:
- `-D` -- Start the daemon
- `-m <path>` -- Directory to scan for module plugins (repeatable)
- `-l <mod1,mod2>` -- Comma-separated modules to pre-load on startup
- `call <module> <method> [args...]` -- Call a method on a loaded module
Type auto-detection in `call` args: `true`/`false` -> bool, `42` -> int, `3.14` -> double, else -> string. Use `@filename` to load file content as an argument.
### TEST_GROUPS
The test runner supports groups for selective testing:
```bash
TEST_GROUPS=basic ws test logos-test-modules --auto-local
TEST_GROUPS=ipc ws test logos-test-modules --auto-local
TEST_GROUPS=basic,ipc,errors ws test logos-test-modules --auto-local
```
### Running Tests via Nix
```bash
nix build .#unit-tests -L # Run unit tests
nix flake check -L # Run all checks defined in the flake
ws test my-module # In the workspace
ws test my-module --auto-local # With local dep overrides
ws test --all --type cpp # All C++ repos
```
## 3. UI Integration Tests (QML Inspector)
UI apps (`type: "ui_qml"`) can be tested via the QML Inspector MCP server built into `logos-standalone-app`. Tests interact with the live UI — clicking buttons, reading text, taking screenshots.
### Test file pattern
```javascript
// tests/smoke.mjs
const { resolve } = await import("node:path");
const { test, run } = await import(
resolve(process.env.LOGOS_QT_MCP || "./result-mcp", "test-framework/framework.mjs")
);
test("my_app: basic interaction", async (app) => {
await app.expectTexts(["My App"]);
await app.click("Add");
await app.expectTexts(["Result:"]);
});
run();
```
### Test API
| Method | Description |
|--------|-------------|
| `app.click(text, opts?)` | Find element by text and click it |
| `app.expectTexts(texts)` | Assert all texts are visible |
| `app.waitFor(fn, opts)` | Poll until fn succeeds (timeout, interval, description) |
| `app.screenshot()` | Capture current state |
| `app.findByType(type)` | Find elements by QML type |
| `app.findByProperty(prop, value)` | Find elements by property |
| `app.getTree()` | Get full QML element tree |
### Running UI tests
```bash
# Interactive (app already running on localhost:3768)
node tests/smoke.mjs
# CI mode (launches app headless, tests, exits)
node tests/smoke.mjs --ci ./result/bin/logos-standalone-app --verbose
# Hermetic via Nix (offscreen, no display needed)
nix build .#integration-test
```
Modules with `.mjs` test files in `tests/` automatically get `nix build .#integration-test` via `mkPluginTest`.
### MCP tools for AI agents
When the app is running, the `.mcp.json` auto-registers these tools with Claude Code / Cursor:
`qml_screenshot`, `qml_find_and_click`, `qml_find_by_type`, `qml_find_by_property`, `qml_list_interactive`, `qml_get_tree`
This lets AI agents visually verify UI changes, click through workflows, and debug layout issues in real time.
## Key Testing Rules
- Unit tests use `LOGOS_TEST()` and `LOGOS_ASSERT_*` macros from `<logos_test.h>`
- Unit tests should NOT require logoscore -- instantiate the impl class directly
- `tests/CMakeLists.txt` must use `include(LogosTest)` + `logos_test()`
- Use `LogosTestContext` for mocking module calls and C library functions
- Integration tests verify the full plugin lifecycle (load, call, response)
- In CI, start the daemon in the background, run your `call`s, then `logoscore stop` so the job exits
- 30-second timeout per `call`; exit code non-zero on failure
- After adding `checks` to a repo's `flake.nix`, run `ws sync-graph` so the workspace discovers them
---
# UI App Development
UI apps use `"type": "ui_qml"` in metadata.json. There are two subtypes:
1. **Pure QML** — no C++ compilation, QML files only, calls backend modules via `logos.callModule()`
2. **QML + C++ Backend** — process-isolated C++ backend (Qt Remote Objects), QML frontend runs in-process
## Pure QML Apps
Simplest UI app type. No compilation, no C++ code. The host loads QML directly.
### metadata.json
```json
{
"name": "my_app",
"type": "ui_qml",
"version": "1.0.0",
"description": "My QML UI application",
"view": "Main.qml",
"icon": null,
"category": "tools",
"dependencies": ["some_backend_module"]
}
```
Key fields: `"type": "ui_qml"` and `"view"` pointing to the QML entry point. No `"main"` field.
### QML Pattern
```qml
import QtQuick 2.15
import QtQuick.Controls 2.15
Item {
id: root
function callBackend(method, args) {
if (typeof logos === "undefined" || !logos.callModule) {
console.log("Logos bridge not available")
return
}
return logos.callModule("some_backend_module", method, args)
}
Button {
text: "Do Something"
onClicked: {
var result = callBackend("myMethod", ["arg1", "arg2"])
console.log("Result:", result)
}
}
}
```
The `logos` bridge is injected by the host (Basecamp or standalone runner). Use `logos.callModule(moduleName, method, args)` to call backend modules.
### flake.nix
Add backend module dependencies (from `metadata.json` `"dependencies"`) as flake inputs. The input attribute name must match the dependency name.
```nix
{
inputs = {
logos-module-builder.url = "github:logos-co/logos-module-builder";
# Each metadata.json dependency needs a matching flake input.
# Use path: for local development, github: for CI/published modules:
some_backend_module.url = "path:../logos-some-backend-module";
# some_backend_module.url = "github:logos-co/logos-some-backend-module";
};
outputs = inputs@{ logos-module-builder, ... }:
logos-module-builder.lib.mkLogosQmlModule {
src = ./.;
configFile = ./metadata.json;
flakeInputs = inputs;
};
}
```
Uses `mkLogosQmlModule` (not `mkLogosModule`). No `preConfigure` needed.
### Resolving Module Dependencies
Each backend module must be built with its shared library (`.so`/`.dylib`) present in `lib/`. Three ways to point at a dependency:
| Approach | `flake.nix` input URL | Build command |
|----------|----------------------|---------------|
| Local path in flake.nix | `path:../logos-my-module` | `nix run .` |
| Remote URL + local override | `github:org/repo` | `nix run . --override-input dep_name path:../logos-my-module` |
| Fully remote | `github:org/repo` | `nix run .` |
`--override-input` overrides a flake input at build time without editing `flake.nix` — useful for quick iteration.
## QML + C++ Backend Apps
For apps that need business logic, state management, or access to system APIs. The C++ backend runs in a **separate isolated process** (`logos_host`), communicating with the QML frontend via Qt Remote Objects IPC.
### metadata.json
```json
{
"name": "my_app",
"type": "ui_qml",
"version": "1.0.0",
"description": "My UI app with C++ backend",
"main": "my_app_plugin",
"view": "qml/Main.qml",
"icon": null,
"category": "tools",
"dependencies": []
}
```
Key difference from pure QML: has `"main"` field pointing to the C++ plugin binary, and `"view"` points to `qml/Main.qml` (inside `src/`).
### .rep File (Qt Remote Objects Interface)
```
class MyApp
{
PROP(QString status READWRITE)
SLOT(int doSomething(int a, int b))
}
```
Defines the IPC interface. Properties auto-sync to QML replicas. Slots are callable from QML.
### C++ Plugin Class
The plugin inherits three bases:
- `MyAppSimpleSource` — generated from .rep, provides property storage + slot declarations
- `MyAppInterface` — extends `PluginInterface`, used for Qt plugin loading
- `MyAppViewPluginBase` — provides `setBackend()` to wire up Qt Remote Objects
```cpp
class MyAppPlugin : public MyAppSimpleSource,
public MyAppInterface,
public MyAppViewPluginBase
{
Q_OBJECT
Q_PLUGIN_METADATA(IID MyAppInterface_iid FILE "metadata.json")
Q_INTERFACES(MyAppInterface)
public:
explicit MyAppPlugin(QObject* parent = nullptr);
QString name() const override { return "my_app"; }
QString version() const override { return "1.0.0"; }
Q_INVOKABLE void initLogos(LogosAPI* api);
// Implement slots from .rep
int doSomething(int a, int b) override;
private:
LogosAPI* m_logosAPI = nullptr;
};
```
In `initLogos()`, call `setBackend(this)` to wire up the Qt Remote Objects source:
```cpp
void MyAppPlugin::initLogos(LogosAPI* api) {
m_logosAPI = api;
setBackend(this);
}
```
### CMakeLists.txt
```cmake
cmake_minimum_required(VERSION 3.14)
project(MyAppPlugin LANGUAGES CXX)
if(DEFINED ENV{LOGOS_MODULE_BUILDER_ROOT})
include($ENV{LOGOS_MODULE_BUILDER_ROOT}/cmake/LogosModule.cmake)
else()
message(FATAL_ERROR "LogosModule.cmake not found. Set LOGOS_MODULE_BUILDER_ROOT.")
endif()
logos_module(
NAME my_app
REP_FILE src/my_app.rep
SOURCES
src/my_app_interface.h
src/my_app_plugin.h
src/my_app_plugin.cpp
)
```
Key: `REP_FILE` tells the build system to generate Qt Remote Objects source/replica headers from the `.rep` file.
### QML Frontend
```qml
import QtQuick
import QtQuick.Controls
Item {
id: root
readonly property var backend: logos.module("my_app")
property bool ready: false
readonly property string status: backend ? backend.status : ""
Connections {
target: logos
function onViewModuleReadyChanged(moduleName, isReady) {
if (moduleName === "my_app")
root.ready = isReady && root.backend !== null;
}
}
Component.onCompleted: {
root.ready = root.backend !== null && logos.isViewModuleReady("my_app");
}
Button {
text: "Do Something"
enabled: root.ready
onClicked: {
logos.watch(backend.doSomething(1, 2),
function(value) { console.log("Result:", value) },
function(error) { console.log("Error:", error) }
)
}
}
Text {
text: "Status: " + root.status
}
}
```
Key QML APIs:
- `logos.module("name")` — returns a typed Qt Remote Objects replica
- `logos.isViewModuleReady("name")` — checks backend connection (use `viewModuleReadyChanged` signal for reactivity)
- `logos.watch(pendingReply, onSuccess, onError)` — handles async slot calls
**Important:** `isViewModuleReady()` is a `Q_INVOKABLE` method, not a property. A QML binding like `readonly property bool ready: logos.isViewModuleReady("name")` will never re-evaluate. Use the `Connections` + `Component.onCompleted` pattern shown above instead.
## C++/QML Boundary Rules
| Concern | C++ (backend plugin) | QML |
| -------------------- | ---------------------------------------- | -------------------------------------- |
| Data models, state | `PROP()` in `.rep` file | Bind to `backend.property` |
| Business logic | Implement as `SLOT()` in `.rep` | Never — no JS business logic |
| Module calls | Via `LogosAPI*` in `initLogos()` | Via `logos.callModule()` (pure QML) |
| File I/O, networking | Always C++ | Never |
| UI layout, styling | Never | QML; use `Logos.Theme` inside Basecamp |
| User interactions | `SLOT()` methods | `onClicked: logos.watch(backend.doX())`|
| Plugin lifecycle | `initLogos()` + `setBackend(this)` | N/A |
## Build and Test
```bash
git init && git add -A # nix needs files tracked
nix build # compiles (backend) or packages (pure QML)
nix run . # standalone app
# Override a module dependency at build time (no flake.nix edits needed):
nix run . --override-input some_module path:../logos-some-module
```
## Calling Logos Modules
From C++ backend (in `initLogos` or slot implementations):
```cpp
auto* client = m_logosAPI->getClient("storage_module");
QVariant result = client->invokeRemoteMethod("storage_module", "save", key, value);
```
From pure QML:
```qml
var result = logos.callModule("storage_module", "save", [key, value])
```
Always declare module dependencies in `metadata.json` `"dependencies"` so they are loaded before the UI app.
## UI Integration Testing (QML Inspector + MCP)
`logos-standalone-app` includes a QML Inspector MCP server that lets AI agents and test scripts interact with the running UI — take screenshots, click elements, inspect the QML tree.
### Available MCP tools
| Tool | Description |
|------|-------------|
| `qml_screenshot` | Capture a screenshot of the current app state |
| `qml_find_and_click` | Find a UI element by text and click it |
| `qml_find_by_type` | Locate elements by QML type name |
| `qml_find_by_property` | Locate elements by property value |
| `qml_list_interactive` | List all clickable/interactive elements |
| `qml_get_tree` | Get the full QML element tree |
### Interactive testing (AI agent workflow)
```bash
nix build && nix run . # launches app with inspector on localhost:3768
```
The `.mcp.json` in the project directory auto-registers the MCP server with Claude Code and other MCP clients. The AI agent can then screenshot, click buttons, and verify UI state in real time.
### Writing integration test files
```javascript
// tests/smoke.mjs
const { test, run } = await import(
resolve(process.env.LOGOS_QT_MCP || "./result-mcp", "test-framework/framework.mjs")
);
test("my_app: click add and verify result", async (app) => {
await app.click("Add");
await app.expectTexts(["Result:"]);
});
run();
```
### Running tests
```bash
# Interactive (app already running)
node tests/smoke.mjs
# CI mode (launches app headless, tests, exits)
node tests/smoke.mjs --ci ./result/bin/logos-standalone-app --verbose
# Hermetic via Nix (headless, offscreen)
nix build .#integration-test
```
### Nix `mkPluginTest` builder
UI modules with `.mjs` test files in `tests/` automatically get `nix build .#integration-test`. For manual setup:
```nix
integration-test = logos-standalone-app.lib.${system}.mkPluginTest {
inherit pkgs;
pluginPkg = myModulePackage;
testFiles = [ ./tests/smoke.mjs ];
name = "my-module-integration-test";
};
```
The builder runs headless (`QT_QPA_PLATFORM=offscreen`), connects to the QML inspector, and executes each test file sequentially.
---
# Universal Module Development
## The Universal Interface Pattern
Universal modules use **pure C++** for their implementation. You write a single implementation class using standard C++ types. The build system generates all Qt/plugin infrastructure automatically — universal modules are **header-first cdylibs** (see [codegen.md](codegen.md)).
**You write:** A C++ class with `std::string`, `int64_t`, `bool`, `std::vector<T>`.
**The generator produces:** a derived `.lidl` contract, the uniform Qt-plugin glue, and a Qt-free C-ABI export wrapper around your class — so your code never touches Qt.
## Rules
- **NO Qt types** in your impl header or implementation: no `QString`, `QObject`, `Q_INVOKABLE`, `QVariant`
- **NO Qt includes** in your impl header (Qt headers in `.cpp` are OK if needed for internal use, but the public API must be pure C++)
- Set `"interface": "universal"` in `metadata.json`
- Name the impl class `<PascalCaseName>Impl` (e.g., `CryptoUtilsImpl`)
- Name the impl header `<name>_impl.h` (e.g., `crypto_utils_impl.h`)
- Only `public` methods become module API methods. Private/protected are ignored by the generator.
- Constructors, destructors, typedefs, and using declarations are skipped by the generator.
## Type Mapping
| Use this in your C++ | Generator maps to | Qt type produced |
|----------------------|-------------------|-----------------|
| `std::string` / `const std::string&` | `tstr` | `QString` |
| `bool` | `bool` | `bool` |
| `int64_t` | `int` | `int` |
| `uint64_t` | `uint` | `int` |
| `double` | `float64` | `double` |
| `void` | `void` | `void` |
| `std::vector<std::string>` | `[tstr]` | `QStringList` |
| `std::vector<uint8_t>` | `bstr` | `QByteArray` |
| `std::vector<int64_t>` | `[int]` | `QVariantList` |
| `std::vector<double>` | `[float64]` | `QVariantList` |
| `std::vector<bool>` | `[bool]` | `QVariantList` |
| `LogosMap` | `{tstr: any}` | `QVariantMap` |
| `LogosList` | `[any]` | `QVariantList` |
`LogosMap` and `LogosList` (from `<logos_json.h>`) are aliases for `nlohmann::json`. Use them when you need to return structured objects or arrays while keeping your impl Qt-free. The generator automatically converts them to `QVariantMap`/`QVariantList` in the glue layer.
If you use a type not in this table, the generator maps it to `any` (`QVariant`). Prefer explicit types from the table for type safety.
## Emitting Events
To emit events from your module, declare a public `emitEvent` callback in your impl header:
```cpp
#include <functional>
std::function<void(const std::string& eventName, const std::string& data)> emitEvent;
```
The generator detects this automatically and wires it to the Logos event system. Call it from your implementation:
```cpp
if (emitEvent) {
emitEvent("somethingHappened", someData);
}
```
No `events` array in `metadata.json` is needed — the generator infers everything from the header.
## Impl Header Template
```cpp
#pragma once
#include <string>
#include <vector>
#include <cstdint>
class MyModuleImpl {
public:
MyModuleImpl();
~MyModuleImpl();
std::string doSomething(const std::string& input);
bool validate(const std::string& data);
int64_t count();
std::vector<std::string> listItems();
private:
// Private members are not exposed as module API
};
```
## Build Pipeline
You don't write a `preConfigure` or run the generator — `mkLogosModule` runs the universal pipeline automatically when `metadata.json` sets `"interface": "universal"`. It derives a `.lidl` from your impl header, then emits the uniform Qt-plugin glue and a Qt-free C-ABI export wrapper around your class (run for you, you don't invoke these):
```bash
logos-cpp-generator --header-to-lidl src/<name>_impl.h \
--impl-class <ImplClassName> --metadata metadata.json \
-o ./generated_code/<name>.lidl
logos-qt-host-generator --lidl ./generated_code/<name>.lidl --backend cdylib \
--output-dir ./generated_code
logos-cpp-generator --lidl ./generated_code/<name>.lidl --backend cdylib \
--impl-class <ImplClassName> --impl-header <name>_impl.h \
--output-dir ./generated_code
```
This produces `generated_code/<name>.lidl`, `<name>_cdylib_glue.{h,cpp}`, and `<name>_module_impl.cpp`. You do **not** list these in `CMakeLists.txt` — `LogosModule.cmake` globs `generated_code/` automatically. See [codegen.md](codegen.md) for details.
## Testing
Unit tests instantiate the impl class directly — it is a plain C++ class:
```cpp
#include "my_module_impl.h"
// No Qt test framework needed for basic tests
MyModuleImpl impl;
assert(impl.doSomething("test") == "expected");
```
Integration tests use `logoscore` (start a daemon, then call via the client):
```bash
logoscore -D -m ./result/lib &
logoscore load-module my_module
logoscore call my_module doSomething test
logoscore stop
```
---