Developer Guide for Contributors

Welcome to the Tetromino codebase. This guide helps you understand the architecture, navigate the source, and contribute confidently. It assumes familiarity with TypeScript, Obsidian plugin development basics, and the Are.na platform.

For user-facing documentation, see [[USER_GUIDE]]. For deep testing guidance, see [[testing-guide]] and [[testing-strategy]].


Table of Contents

  1. Architecture Overview
  2. Module Guide
  3. Code Style and Conventions
  4. Adding Features
  5. Adding Tests
  6. Debugging
  7. Playbook Cross-References

Architecture Overview

Tetromino follows a linear data-flow architecture:

┌─────────────┐     ┌─────────────┐     ┌─────────────┐     ┌─────────────┐
│   Entry     │────▶│    API      │────▶│ SyncEngine  │────▶│    Vault    │
│   Point     │     │   Layer     │     │   Core      │     │   (Obsidian)│
└─────────────┘     └─────────────┘     └─────────────┘     └─────────────┘

Entry Point (src/main.ts)

ArenaSyncPlugin is the Obsidian Plugin subclass. It:

Key rule: main.ts should remain thin. Heavy lifting belongs in SyncEngine or ArenaApi.

API Layer (src/api.ts)

ArenaApi encapsulates all HTTP traffic to https://api.are.na/v3 via Obsidian’s requestUrl.

Responsibilities:

For endpoint details, retry formulas, rate-limit behaviour, and future-version migration guidance, see [[API_DESIGN]].

Sync Engine (src/sync-engine.ts)

SyncEngine is the heart of the plugin. It:

  1. Fetches channel metadata and all blocks via ArenaApi.
  2. Pre-fetches optional enrichments (block comments, connected channels, channel preview images).
  3. Converts each ArenaBlock into Markdown via blockToMarkdown (src/utils.ts).
  4. Creates, updates, moves, or skips vault files based on content-hash comparisons.
  5. Generates per-channel index notes and a master Are.na/overview.md.
  6. Produces a deterministic SyncResult with actions, diffs, moves, errors, and timing.

Concurrency is controlled via pMap helpers:

Determinism is guaranteed by:

Vault Layer (Obsidian APIs)

The vault is Obsidian’s Vault instance, accessed through app.vault. All file operations go through standard Obsidian APIs:

Folder creation is guarded by an async mutex in SyncEngine to prevent race conditions when multiple concurrent blocks need the same new folder.


Module Guide

File Lines Responsibility
src/main.ts ~628 Plugin lifecycle, settings persistence, command registration, high-level sync orchestration, migration UI, backup/restore.
src/api.ts ~553 Are.na REST client, auth, retries, caching, pagination, response normalisation, binary downloads.
src/sync-engine.ts ~1023 Core import logic: fetch, render, diff, write, move, index generation, progress reporting, sync-record management.
src/settings-tab.ts ~657 Obsidian PluginSettingTab UI for every plugin setting and channel-mapping row.
src/modals.ts ~214 Modal components: DiffModal, SyncSummaryModal, MigrationPreviewModal, BackupFileSuggestModal.
src/utils.ts ~420 Markdown rendering (blockToMarkdown), file-name utilities, hash computation, folder resolution, attachment-base resolution, pMap concurrency helper.
src/templateUtils.ts ~139 Custom template parser and renderer (Handlebars-like syntax: {{var}}, {{#if}}, {{#each}}).
src/types.ts ~219 TypeScript interfaces for Are.na API entities, plugin settings, sync records, and results. Also defines DEFAULT_SETTINGS.
src/diff.ts ~76 Unified-diff generator using LCS (longest-common-subsequence) with a safety cap (MAX_DIFF_CELLS).
src/migration.ts ~215 Attachment-migration planner and executor: detects path changes, computes move/update plans, rewrites wiki-link embeds in notes.
src/securityUtils.ts ~98 Markdown sanitizer that neutralises executable code blocks, dangerous HTML, event handlers, and dangerous URL protocols.
src/__mocks__/obsidian.ts ~166 Jest mock of the obsidian module for unit tests (stubs App, Vault, Plugin, Modal, Setting, etc.).
src/__tests__/fixtures.ts ~53 Simple factory functions (makeChannel, makeBlock) for test data.
src/__tests__/fixtures/scenarios.ts varies Richer scenario fixtures: paginated responses, error responses, multi-block channels.
src/__tests__/fixtures/vault.ts varies MockVault and makeMockApp for lightweight integration tests.

Code Style and Conventions

TypeScript Configuration

tsconfig.json enforces:

Rule: Do not relax these checks. Cast sparingly (as or as unknown as) and always justify with a comment.

Naming and Structure

Error Handling

Type Guards

The codebase prefers small, focused type guards over broad any:

function isRecord(val: unknown): val is Record<string, unknown> {
  return typeof val === "object" && val !== null && !Array.isArray(val);
}

Use these when normalising API responses or parsing user input.

Determinism Requirements

Tetromino’s core value is deterministic output. Any change that affects ordering, hashing, or rendering must preserve:

  1. Same Are.na channel + same settings → identical Markdown output.
  2. Sorted arrays (blocks, links, channels) must use stable comparators.
  3. Timestamps in output (e.g., lastSyncedAt) are acceptable variability points, but content hashes must remain stable when content has not changed.

Adding Features

Follow this workflow to add a new feature safely and maintainably.

1. Design

2. Write Tests First (or in Parallel)

3. Implement

4. Validate

npm run lint
npm test
npm run validate   # lint + test + build

5. Document

6. Review Checklist

Before requesting review, ensure:


Adding Tests

Tetromino uses Jest with ts-jest in a jsdom environment. The full testing manual lives in [[testing-guide]]; this section gives you the essential quick-start.

Test Layout

What Where
Unit tests for module X src/__tests__/X.test.ts
Extended / edge-case tests src/__tests__/X_extended.test.ts
Integration / flow tests src/__tests__/integration.test.ts
Determinism regression src/__tests__/determinism.test.ts
Shared fixtures src/__tests__/fixtures.ts, src/__tests__/fixtures/scenarios.ts, src/__tests__/fixtures/vault.ts
Obsidian mock src/__mocks__/obsidian.ts

Mocking Are.na API Responses

import * as obsidian from "obsidian";
import { ArenaApi } from "../api";

it("fetches a channel", async () => {
  const requestUrlMock = jest.spyOn(obsidian, "requestUrl").mockResolvedValueOnce({
    status: 200,
    headers: {},
    json: { data: { id: 42, title: "Rad Readings", slug: "rad-readings", /* ... */ } },
    arrayBuffer: new ArrayBuffer(0),
  });

  const api = new ArenaApi("token-123");
  const channel = await api.getChannel("rad-readings");
  expect(channel.title).toBe("Rad Readings");

  requestUrlMock.mockRestore();
});

Prefer the richer fixture helpers in src/__tests__/fixtures/scenarios.ts for multi-page or error scenarios.

Mocking the Vault (Unit Tests)

import { App, Vault } from "obsidian";
import { SyncEngine } from "../sync-engine";

const mockVault = {
  getAbstractFileByPath: jest.fn(),
  read: jest.fn(),
  create: jest.fn(),
  modify: jest.fn(),
  createFolder: jest.fn(),
} as unknown as jest.Mocked<Vault>;

const mockApp = { vault: mockVault } as unknown as App;
const engine = new SyncEngine(mockApp, mockApi, settings);

Integration Tests with MockVault

For tests that exercise real file-tree logic without mocking every vault call, use MockVault from src/__tests__/fixtures/vault.ts:

import { MockVault, makeMockApp } from "./fixtures/vault";

const vault = new MockVault();
const app = makeMockApp(vault);
const engine = new SyncEngine(app, api, settings);

await engine.syncChannel(mapping);
expect(vault.has("Are.na/chan/Block.md")).toBe(true);

Coverage Expectations

jest.config.cjs enforces:

New features should not drop these thresholds. Aim for the module you touched to be at or above the global threshold.

For exhaustive patterns (fake timers, multi-page mocks, error injection), see [[testing-guide]].


Debugging

Console Logs and Performance Marks

Tetromino uses console.time / console.timeEnd labels prefixed with arena-sync: to trace bottlenecks:

Enable Debug logging in the plugin settings to see [arena-sync] log lines for API requests, cache hits, and pagination stops.

Inspecting Plugin State

From Obsidian’s Developer Tools (Cmd+Opt+I / Ctrl+Shift+I):

// Access the active plugin instance
const plugin = app.plugins.plugins["tetromino"];

// Inspect current settings
console.log(plugin.settings);

// Inspect sync records
console.log(plugin.settings.syncRecords);

// Inspect the API client cache
console.log(plugin.api);

// Trigger a dry-run manually
await plugin.runSync(true);

Testing Locally in a Real Vault

  1. Build the plugin:
    npm run build
    
  2. Copy main.js, manifest.json, and styles.css (if any) to a test vault’s .obsidian/plugins/tetromino/ folder.
  3. Reload Obsidian (Cmd+R / Ctrl+R) or disable/enable the plugin.
  4. Point the test vault at a real or staging Are.na channel with a personal token.

Common Debug Scenarios

Symptom Likely Cause How to Verify
Notes not updating Hash matches existing content; check SyncResult.skipped Run dry-run and inspect diff modal
Missing images imageHandling setting or asset download failure Check SyncResult.errors and console for [arena-sync] download errors
Slow import Large channels, many enrichment API calls Look at console.timeEnd labels; reduce includeBlockComments / includeBlockConnectedChannels
Duplicate blocks Pagination boundary overlap Check arena-sync:fetch-blocks log for reason=duplicates
Vault permission errors Folder path blocked by Obsidian sandbox Ensure path is inside vault root; check normalizePath output

Playbook Cross-References

Tetromino’s development is organised into playbook phases in .maestro/playbooks/Initiation/:

Topic Playbook Phase What It Covers
Environment setup Phase 01 Node version, npm install, IDE config, first build.
Dev workflow Phase 02 Branching, commits, lint, test, build, PR template.
Bug triage Phase 03 How to reproduce, diagnose, fix, and regression-test bugs.
Feature development Phase 04 Design docs, scoping, implementation checklist, ADR guidance.
Testing and QA Phase 05 Test philosophy, coverage strategy, fixture patterns, CI gates.
Release procedures Phase 06 Version bump, changelog, release script, GitHub workflow.
Performance maintenance Phase 07 Benchmarks, profiling, optimisation checklist, regression guards.
Documentation Phase 08 Doc audits, user guide, developer guide, ADRs, FAQ, troubleshooting.
Community sustainability Phase 09 Governance, contributor onboarding, issue triage, long-term health.

When you start work on a new area, read the corresponding phase document for step-by-step instructions and checklists.


← Home


Last updated: 2026-06-26