원클릭으로
nova-patterns
Nova plugin coding standards, compliance rules, and design patterns. Manually maintained reference for development.
Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
메뉴
Nova plugin coding standards, compliance rules, and design patterns. Manually maintained reference for development.
Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
SOC 직업 분류 기준
Auto-generated Nova codebase map. Regenerate with $nova-codebase-map.
Nova product positioning, roadmap context, monetization guardrails, and shipped-vs-planned guidance. Read before roadmap/spec/product work.
Obsidian plugin development best practices, ESLint rules, submission requirements, and API patterns. Auto-loads when working on Obsidian plugin code.
| name | nova-patterns |
| description | Nova plugin coding standards, compliance rules, and design patterns. Manually maintained reference for development. |
Nova is an AI writing plugin for Obsidian that enables direct, in-place editing. This skill documents its coding standards, compliance rules, and design patterns.
For roadmap, spec, pricing, or feature-prioritization work, pair this skill with nova-product.
All TypeScript files MUST have a standardized header comment:
/**
* @file ModuleName - One-line description of purpose
*/
This enables automated codebase documentation. Run /project:sync-codebase after adding new files.
Nova uses several communication patterns depending on the relationship between components:
Components receive their dependencies via constructor parameters. This is the default for tightly-coupled components.
// Core services receive dependencies at construction
this.promptBuilder = new PromptBuilder(this.documentEngine, this.conversationManager);
this.documentEngine = new DocumentEngine(this.app, this.conversationManager);
Tightly-coupled components call methods on each other directly. Decoupling is a case-by-case decision, not a blanket rule.
// Direct calls between related components are fine
const conversation = this.conversationManager.getConversation(file);
const context = this.documentEngine.getDocumentContext(editor, file);
Cross-plugin and layout-level communication uses Obsidian's built-in workspace events.
this.app.workspace.on('file-open', (file) => this.handleFileOpen(file));
this.app.workspace.on('layout-change', () => this.handleLayoutChange());
Lightweight cross-component notifications (e.g., settings changes, license updates) use CustomEvent on document.
// Dispatch
document.dispatchEvent(new CustomEvent('nova-provider-configured', { detail: { provider } }));
// Listen (always via registerDomEvent for cleanup)
this.registerDomEvent(document, 'nova-provider-configured', this.handleProviderConfigured.bind(this));
src/ui/)export class MyComponent {
private plugin: NovaPlugin;
private containerEl: HTMLElement;
constructor(plugin: NovaPlugin, containerEl: HTMLElement) {
this.plugin = plugin;
this.containerEl = containerEl;
// NO side effects in constructor - no DOM, no events, no API calls
}
async init(): Promise<void> {
// All setup happens here
this.buildUI();
this.registerEvents();
}
private buildUI(): void {
// Use Obsidian's DOM helpers
const header = this.containerEl.createEl('div', { cls: 'nova-header' });
header.setText('Title');
}
private registerEvents(): void {
// ALWAYS use plugin registration for automatic cleanup
this.plugin.registerDomEvent(this.containerEl, 'click', (e) => {
this.handleClick(e);
});
}
destroy(): void {
// Usually empty - registration handles cleanup
// Only needed for non-registered resources
}
}
src/core/)Services handle business logic and are injected into consumers via constructors:
export class MyService {
constructor(private app: App, private conversationManager: ConversationManager) {
// NO side effects in constructor
}
async init(): Promise<void> {
// Async initialization goes here
}
async performAction(params: ActionParams): Promise<Result> {
try {
const result = await this.doWork(params);
return result;
} catch (error) {
Logger.error('Action failed', { error, params });
throw error;
}
}
}
src/ai/providers/)All providers implement a common interface:
interface AIProvider {
name: string;
generateResponse(
messages: ConversationMessage[],
options: GenerationOptions
): AsyncGenerator<StreamingResponse>;
getModelInfo(): ModelInfo;
validateApiKey(): Promise<boolean>;
getContextLimit(): number;
}
// Usage in streaming
async *generateResponse(messages, options) {
for await (const chunk of this.callAPI(messages)) {
yield {
type: 'content',
content: chunk.text,
finished: chunk.done
};
}
}
Nova uses TimeoutManager for Obsidian-compliant timeout handling:
import { TimeoutManager } from '../utils/timeout-manager';
// WRONG: Unregistered timeout
setTimeout(() => this.doSomething(), 1000);
// CORRECT: Registered timeout with cleanup
TimeoutManager.addTimeout(
this.plugin,
() => this.doSomething(),
1000,
'optional-id-for-cancellation'
);
// Cancel a specific timeout
TimeoutManager.clearTimeout('optional-id-for-cancellation');
// For intervals (recurring)
this.plugin.registerInterval(
window.setInterval(() => this.poll(), 5000)
);
Use the Logger utility, never console.log:
import { Logger } from '../utils/logger';
// Levels: debug, info, warn, error
Logger.debug('Detailed info', { context });
Logger.info('Normal operation', { data });
Logger.warn('Potential issue', { warning });
Logger.error('Failed operation', { error, context });
// In production, debug is suppressed
// Console.log is NEVER acceptable
async performRiskyOperation(): Promise<void> {
try {
await this.riskyCall();
} catch (error) {
// 1. Log with context
Logger.error('Operation failed', {
error,
operation: 'riskyCall',
context: this.getContext()
});
// 2. User-friendly notification
new Notice('Something went wrong. Please try again.');
// 3. Re-throw only if caller needs to handle
throw error;
}
}
| Type | Convention | Example |
|---|---|---|
| Classes | PascalCase | StreamingManager |
| Interfaces | PascalCase, prefix I optional | AIProvider or ISettings |
| Functions/Methods | camelCase | handleClick() |
| Variables | camelCase | currentMessage |
| Constants | SCREAMING_SNAKE | MAX_CONTEXT_TOKENS |
| Files | kebab-case | streaming-manager.ts |
| CSS Classes | BEM-ish with nova prefix | nova-sidebar__header |
Location: test/
// File: component-name.test.ts
describe('ComponentName', () => {
let component: ComponentName;
let mockPlugin: jest.Mocked<NovaPlugin>;
beforeEach(() => {
mockPlugin = createMockPlugin();
component = new ComponentName(mockPlugin);
});
afterEach(() => {
jest.clearAllMocks();
});
// CORRECT: Behavior-focused test names
it('should persist conversation state between sessions', async () => {
// Arrange
const conversation = createTestConversation();
// Act
await component.saveConversation(conversation);
const loaded = await component.loadConversation(conversation.id);
// Assert
expect(loaded).toEqual(conversation);
});
// WRONG: Implementation-focused
it('should call saveData method', () => { /* ... */ });
});
See test/__mocks__/ for consistent Obsidian API mocks:
obsidian.ts - Core Obsidian mocksworkspace.ts - Workspace and view mocksvault.ts - File system mocksThe IntentDetector classifies user input into categories:
type Intent =
| 'CONTENT' // Add/edit document content at cursor
| 'METADATA' // Modify tags, frontmatter, properties
| 'CHAT' // Conversational response, no document edit
| 'COMMAND'; // Explicit command execution
// Examples:
// "add a conclusion here" -> CONTENT
// "add tags: productivity" -> METADATA
// "what should I write about?" -> CHAT
// "/expand-outline" -> COMMAND
The StreamingManager handles real-time text generation:
// Key features:
// - 60fps smooth updates
// - Automatic scroll following
// - Error recovery with partial content preservation
// - Cross-platform (desktop/mobile) optimization
await streamingManager.streamToEditor(
aiStream,
editor,
{
startPosition: cursor,
enableAutoScroll: true,
onError: (error) => this.handleStreamError(error)
}
);
Magic strings and selectors should go in src/constants.ts (not yet consistently applied across the codebase):
// CORRECT
import { CSS_CLASSES, TIMEOUTS } from '../constants';
element.addClass(CSS_CLASSES.SIDEBAR_HEADER);
// WRONG
element.addClass('nova-sidebar-header');
See also: .claude/skills/nova-codebase/SKILL.md for current file structure and exports.