This document defines the coding conventions for the rag-cli project.
- C++17 is the target language standard
- Use modern C++ features where appropriate (std::optional, std::filesystem, etc.)
- Avoid C-style constructs unless interfacing with system APIs (e.g., CURL, termios)
- Use
#pragma oncefor include guards - Organize includes in this order:
- Standard library headers (
<string>,<vector>, etc.) - Third-party headers (
<nlohmann/json.hpp>,<curl/curl.h>, etc.) - Project headers (
"config.hpp","console.hpp", etc.)
- Standard library headers (
- All code within
namespace rag { }
.cppfiles for implementations,.hppfor headers- Include corresponding header first in
.cppfiles - Then system headers, then third-party headers, then project headers
- Files: snake_case (e.g.,
markdown_renderer.hpp,openai_client.cpp) - Classes: PascalCase (e.g.,
MarkdownRenderer,OpenAIClient,ChatSession) - Functions/Methods: snake_case (e.g.,
load_settings,stream_response,add_user_message) - Member variables: snake_case with trailing underscore (e.g.,
api_key_,colors_enabled_,conversation_) - Local variables: snake_case
- Constants: UPPER_SNAKE_CASE for macros, PascalCase for constexpr (e.g.,
OPENAI_API_BASE,SETTINGS_FILE)
Use standard library types appropriately:
size_tfor sizes and indicesint64_tfor timestampsstd::stringfor textstd::optional<T>for values that may not existstd::vector<T>,std::map<K,V>for containersstd::function<void(const std::string&)>for callbacks
- Use
structfor plain data types with public members - PascalCase for struct names
- Add doc comments explaining the struct's purpose
/**
* Metadata for a single indexed file.
*
* Tracks the OpenAI file ID and modification timestamp to enable incremental
* updates when files change.
*/
struct FileMetadata {
std::string openai_file_id; // OpenAI file ID for this file.
int64_t last_modified; // Unix timestamp of last modification.
};- Public interface first
- Private implementation details last
- Within each section:
- Static methods
- Constructors/destructors
- Regular methods
- Member variables
class Console {
public:
// Public interface
private:
// Private implementation
};Comments should be proper English sentences ending with a period. Be accurate and concise. Only comment things that a code reader should really have brought to their attention.
- Class/struct purpose: Every class and struct should have a doc comment explaining its role.
- Public method purpose: Every public method should have a comment describing what it does.
- Field purpose: Fields in data structures should have inline comments explaining their role.
- Global/constant purpose: Explain what globals and constants represent.
- Non-obvious code: Add inline comments where logic is not self-evident.
- Pre-conditions: Mention when a method requires certain conditions.
Use /** */ multiline format with * on continuation lines:
/**
* Terminal output helper with color support.
*
* Provides styled output methods that automatically handle ANSI color codes
* based on terminal capabilities. Falls back to plain text when colors are
* not supported (e.g., when TERM=dumb or output is not a TTY).
*/
class Console {Place a single-line // comment immediately before each public method:
// Creates a Console instance and detects color support.
Console();
// Prints text without a trailing newline.
void print(const std::string& text) const;
// Prints error message in red.
void print_error(const std::string& text) const;
// Prompts the user for input with an optional default value.
std::string prompt(const std::string& message, const std::string& default_value = "") const;Use inline // comments after field declarations:
std::string api_key_; // OpenAI API key.
std::vector<Message> conversation_; // In-memory conversation history.
std::string log_path_; // Path to the log file.
bool colors_enabled_; // True if terminal supports ANSI colors.Use // ========== Section Name ========== to visually delineate areas of code:
// ========== Basic Output ==========
void print(const std::string& text) const;
void println(const std::string& text = "") const;
// ========== Colored Output ==========
void print_error(const std::string& text) const;
void print_warning(const std::string& text) const;
void print_success(const std::string& text) const;
// ========== HTTP Helpers ==========
std::string http_get(const std::string& url);
std::string http_post_json(const std::string& url, const nlohmann::json& body);Use decorative separators for major file sections:
// ============================================================================
// Signal Handling
// ============================================================================
static Console* g_console = nullptr;
static std::atomic<bool>* g_stop_spinner = nullptr;- 4 spaces per indentation level (no tabs)
- Continuation lines aligned appropriately
- Opening brace on same line for functions, control structures, and classes
- Closing brace on its own line
void function() {
if (condition) {
// code
}
}- Soft limit: 120 characters
- Hard limit: Avoid exceeding 140 characters
- Break long parameter lists across multiple lines
- Space after keywords:
if (,while (,for ( - Space around binary operators:
a + b,x = y - No space after unary operators:
!flag,++i - No space inside parentheses:
(expr)not( expr ) - Space after commas:
foo(a, b, c)
Use constructor initialization lists:
ChatSession::ChatSession(const std::string& system_prompt, const std::string& log_dir) :
log_path_(generate_log_path(log_dir)) {
// Constructor body
}Always use braces, even for single statements:
if (condition) {
statement();
}Multi-line conditions align naturally:
if (j.contains("error") &&
j["error"].contains("message")) {
// Handle error
}Prefer early returns for error conditions:
std::optional<Settings> load_settings() {
if (!fs::exists(SETTINGS_FILE)) {
return std::nullopt;
}
// Main logic
}switch (choice) {
case '1':
return "low";
case '2':
return "medium";
case '3':
return "high";
default:
return "";
}- Use RAII for resource management
- Use
std::unique_ptr<T>for unique ownership - Prefer stack allocation where possible
- Use
std::mutexandstd::lock_guard<std::mutex>for synchronization - Use
std::atomic<T>for simple shared state - Document threading requirements in comments
std::atomic<bool> stop_spinner{false};
std::thread spinner_thread([&]() {
while (!stop_spinner.load()) {
// Animate spinner
}
});Use auto when type is obvious from context:
auto it = THINKING_MAP.find(thinking); // Iterator type obvious
auto settings = load_settings(); // Type in function nameAvoid auto when type clarity is important:
std::string response = http_get(url); // Not: auto response = http_get(url);
size_t count = files.size(); // Not: auto count = files.size();Prefer range-based for loops:
for (const auto& msg : conversation) {
input.push_back(msg.to_json());
}
for (const auto& pattern : patterns) {
// Process pattern
}Always use nullptr, never NULL or 0 for pointers:
if (api_key == nullptr) { }Use std::optional for values that may not exist:
std::optional<Settings> load_settings();
auto existing = load_settings();
if (existing.has_value() && existing->is_valid()) {
// Use settings
}- Mark methods
constwhen they don't modify state - Use
const &for read-only parameters of non-trivial types - Use
constfor local variables that won't change
void print(const std::string& text) const;
const std::vector<Message>& get_conversation() const;Use std::function for callbacks:
void stream_response(
const std::string& model,
const std::vector<Message>& conversation,
std::function<void(const std::string&)> on_text
);Use lambdas for inline callbacks:
client.stream_response(model, conversation, [&](const std::string& delta) {
renderer.feed(delta);
streamed_text += delta;
});- Minimize macro usage
- Use
constexprinstead of#definefor constants - Use
inlinefunctions instead of function macros
Acceptable macro uses:
- Include guards (
#pragma once) - Platform-specific code
- Use exceptions for API failures (
std::runtime_error) - Return
std::optionalor empty values for expected missing data - Use early returns to handle error conditions
if (j.contains("error")) {
throw std::runtime_error("API error: " + j["error"]["message"].get<std::string>());
}See the Comments section above for detailed guidance on writing comments. Key principles:
- All comments are proper English sentences ending with periods.
- Every class/struct has a
/** */doc comment explaining its purpose. - Every public method has a
//comment describing what it does. - Fields have inline
//comments explaining their role. - Use
// ========== Section ==========headings to organize code.
- Use descriptive test case names
- Group related tests with tags
- Use section separators for test organization
TEST_CASE("Plain text passes through", "[markdown]") {
OutputCollector output;
MarkdownRenderer renderer(std::ref(output));
renderer.feed("Hello world");
renderer.finish();
REQUIRE(strip_ansi(output.result).find("Hello world") != std::string::npos);
}
TEST_CASE("ATX heading level 1", "[markdown][headings]") {
// Test implementation
}Use section separators in test files:
// ============================================================================
// Basic text rendering
// ============================================================================
TEST_CASE("Plain text passes through", "[markdown]") { ... }
// ============================================================================
// Code blocks
// ============================================================================
TEST_CASE("Fenced code block with language", "[markdown][code]") { ... }- clang-format: Automatic code formatting (configuration to be added)
- clang-tidy: Static analysis and linting
- cmake: Build system
- ninja: Fast build tool
mkdir build && cd build
cmake ..
ninja
./test_markdown_renderer # Run tests