Version: 1.0 Last Updated: 2025-10-26 Purpose: Document public interfaces and contracts for all services
Purpose: Create Task assets from Areas, Projects, or Prototypes
Dependencies: Vault
Public Methods:
async createTask(
sourceFile: TFile,
sourceMetadata: Record<string, any>,
sourceClass: string,
label?: string,
taskSize?: string | null,
): Promise<TFile>Contract:
- Input Validation:
sourceFilemust exist and be readablesourceClassmust be valid AssetClass (Area, Project, TaskPrototype, MeetingPrototype)taskSizemust be one of:S,M,L,XL, ornull
- Output Guarantee:
- Returns created
TFilewith UUID-based filename - File contains valid frontmatter with all required properties
- File created in same folder as
sourceFile
- Returns created
- Side Effects:
- Creates new file in vault
- Generates UUID for filename
- Inherits Algorithm section if source is TaskPrototype
- Error Conditions:
- Throws if
sourceFilenot readable - Throws if folder doesn't exist and can't be created
- Throws if file with UUID already exists (extremely rare)
- Throws if
Example:
const createdFile = await service.createTask(
areaFile,
{ exo__Asset_isDefinedBy: '"[[Ontology/EMS]]"' },
AssetClass.AREA,
"Review PR #123",
"M"
);
// Returns: TFile with path "path/550e8400-e29b-41d4-a716-446655440000.md"generateTaskFrontmatter(
sourceMetadata: Record<string, any>,
sourceName: string,
sourceClass: string,
label?: string,
uid?: string,
taskSize?: string | null,
): Record<string, any>Contract:
- Type: Pure function (no side effects)
- Input: Metadata and source information
- Output: Complete frontmatter object
- Deterministic: Same inputs → same output
- Testable: 100% unit testable without Obsidian
Generates:
{
exo__Asset_uid: uid || uuidv4(),
exo__Asset_label: label || auto-generated,
exo__Asset_createdAt: ISO 8601 timestamp,
exo__Asset_isDefinedBy: inherited from sourceMetadata,
exo__Instance_class: ['"[[ems__Task]]"'], // From INSTANCE_CLASS_MAP
ems__Effort_status: '"[[ems__EffortStatusDraft]]"',
ems__Effort_area: '"[[{sourceName}]]"', // If sourceClass is Area
ems__Effort_parent: '"[[{sourceName}]]"', // If sourceClass is Project
exo__Asset_prototype: '"[[{sourceName}]]"', // If sourceClass is Prototype
ems__Task_size: taskSize || undefined,
aliases: [label]
}async createRelatedTask(
taskFile: TFile,
taskMetadata: Record<string, any>,
label: string,
taskSize?: string | null,
): Promise<TFile>Contract:
- Creates bidirectional task relationship
- Adds
ems__Effort_relatedTaskto both tasks - Updates source file frontmatter automatically
Purpose: Create Project assets from Areas or Initiatives
Dependencies: Vault
Public Methods:
async createProject(
sourceFile: TFile,
sourceMetadata: Record<string, any>,
sourceClass: string,
label?: string,
uid?: string,
): Promise<TFile>Contract:
- Input Validation:
sourceClassmust be Area or Initiative
- Output Guarantee:
- Returns TFile with UUID filename
- Frontmatter includes
exo__Instance_class: ['"[[ems__Project]]"']
- Property Mapping:
- From Area → adds
ems__Effort_area - From Initiative → adds
ems__Effort_parent
- From Area → adds
generateProjectFrontmatter(
sourceMetadata: Record<string, any>,
sourceName: string,
sourceClass: string,
label?: string,
uid?: string,
): Record<string, any>Contract: Pure function, same pattern as generateTaskFrontmatter
Purpose: Create Concept assets in knowledge management system
Dependencies: Vault
Public Methods:
async createNarrowerConcept(
parentFile: TFile,
fileName: string,
definition: string,
aliases: string[],
): Promise<TFile>Contract:
- Input Validation:
parentFilemust beims__ConcepttypefileNamemust be valid (sanitized if needed)definitionshould not be empty (recommended)
- Output Guarantee:
- Creates file in
concepts/folder - Adds
.mdextension if missing - Generates UUID for
exo__Asset_uid
- Creates file in
- Frontmatter Generated:
exo__Asset_isDefinedBy: "[[!concepts]]" exo__Instance_class: ['"[[ims__Concept]]"'] ims__Concept_broader: "[[{parentFile.basename}]]" ims__Concept_definition: {definition} aliases: {aliases}
Purpose: Manage effort status transitions through workflow
Dependencies: Vault
Public Methods:
async moveToTodo(taskFile: TFile): Promise<void>
async moveToDoing(taskFile: TFile): Promise<void>
async moveToDone(taskFile: TFile): Promise<void>
async moveToBacklog(taskFile: TFile): Promise<void>
// ... etc for all statusesContract:
- Input Validation:
- File must exist and be readable
- File must be an Effort (Task/Project/Meeting)
- Workflow Validation:
- Validates transition is allowed (getPreviousStatusFromWorkflow)
- Projects: must go through ToDo before Doing
- Tasks: can skip to Doing from Backlog
- Side Effects:
- Updates
ems__Effort_statusproperty - Adds timestamp if transitioning to/from Doing
→ Doing: Setsems__Effort_startTimestamp← Doing: Setsems__Effort_endTimestamp→ Done: Setsems__Effort_resolutionTimestamp
- Updates
- Error Conditions:
- Throws if invalid transition
- Throws if file not found
Workflow State Machine:
Draft → Backlog → Analysis → ToDo → Doing → Done
↓
Trashed (from any state)
private getPreviousStatusFromWorkflow(
currentStatus: string,
instanceClass: string | string[] | null,
): string | null | undefinedContract:
- Type: Pure function
- Returns: Expected previous status,
nullif at start,undefinedif invalid - Logic:
- For Projects: ToDo → Doing → Done
- For Tasks: Backlog → Doing → Done (can skip Analysis/ToDo)
Purpose: Manage vote counting for effort prioritization
Dependencies: Vault
Public Methods:
async incrementEffortVotes(effortFile: TFile): Promise<number>Contract:
- Input Validation:
- File must be an Effort (Task/Project/Meeting)
- File must not be archived
- Output Guarantee:
- Returns new vote count after increment
- Vote count is always integer ≥1
- Side Effects:
- Creates
ems__Effort_votesproperty if doesn't exist (starts at 1) - Increments existing vote count by 1
- Preserves Unix (\n) or Windows (\r\n) line endings
- Creates
- Frontmatter Handling:
- Creates frontmatter if missing
- Preserves all other properties
- Maintains property order
Example:
const newVotes = await service.incrementEffortVotes(taskFile);
// Before: ems__Effort_votes: 3
// After: ems__Effort_votes: 4
// Returns: 4private extractVoteCount(content: string): numberContract:
- Pure function
- Returns current vote count or 0 if property doesn't exist
- Handles both Unix and Windows line endings
Purpose: Remove empty frontmatter properties
Dependencies: Vault
Public Methods:
async cleanEmptyProperties(file: TFile): Promise<void>Contract:
- Removes properties with empty values
- Empty values:
null,undefined,"",[],{} - Preserves non-empty properties
- Updates file in place
private removeEmptyPropertiesFromContent(content: string): stringContract:
- Pure function (string → string)
- Returns content with empty properties removed
- Preserves formatting
Purpose: Create child Area assets
Dependencies: Vault
Public Methods:
async createChildArea(
parentFile: TFile,
parentMetadata: Record<string, any>,
label?: string,
): Promise<TFile>Contract:
- Creates child area with
ems__Area_parentreference - Inherits
exo__Asset_isDefinedByfrom parent - Creates in same folder as parent
Purpose: Copy asset label to aliases array
Dependencies: Vault
Public Methods:
async copyLabelToAliases(file: TFile): Promise<void>Contract:
- Extracts
exo__Asset_label - Adds to
aliasesarray if not already present - Creates
aliasesproperty if missing
Purpose: Create CBT supervision notes
Dependencies: Vault
Public Methods:
async createSupervision(formData: SupervisionFormData): Promise<TFile>Contract:
- Creates supervision note with structured frontmatter
- Generates formatted body content (CBT format)
- Creates in
supervision/folder
Purpose: Build graph data from all notes
Dependencies: App, MetadataCache
Public Methods:
async buildGraphData(): Promise<GraphData>Contract:
- Scans all markdown files
- Extracts nodes (assets with labels)
- Extracts edges (property references)
- Returns
GraphDatawith nodes and edges arrays
Purpose: Build hierarchical tree of areas
Dependencies: Vault, MetadataCache
Public Methods:
buildHierarchy(currentAreaFile: TFile | null): AreaNode | nullContract:
- Builds tree from current area up to root
- Includes child areas
- Filters archived areas (if setting enabled)
Purpose: Move files to expected folders based on ontology
Dependencies: Vault, MetadataCache
Public Methods:
async repairFileLocation(file: TFile, expectedFolder: string): Promise<void>Contract:
- Moves file to expected folder
- Preserves filename
- Updates all WikiLink references automatically (Obsidian feature)
Purpose: Rename files to UUID format
Dependencies: App.fileManager
Public Methods:
async renameToUid(file: TFile): Promise<void>Contract:
- Renames file to
{exo__Asset_uid}.md - Uses
app.fileManager.renameFile()for proper link updating - Preserves folder location
All methods are pure functions (100% testable without Obsidian)
parse(content: string): { frontmatter: Record<string, any>; body: string }Contract:
- Extracts YAML frontmatter from markdown content
- Returns frontmatter object + body text
- Returns empty object if no frontmatter
updateProperty(content: string, property: string, value: any): stringContract:
- Pure function (no side effects)
- Updates property value in frontmatter
- Creates property if doesn't exist
- Preserves all other properties and formatting
removeProperty(content: string, property: string): stringContract:
- Removes property from frontmatter
- Preserves other properties
- Returns original content if property doesn't exist
getPropertyValue(frontmatterContent: string, property: string): string | nullContract:
- Extracts value of property from frontmatter string
- Returns
nullif property doesn't exist - Pure function
All methods are static pure functions
static toLocalTimestamp(date: Date): stringContract:
- Formats date to ISO 8601 local time:
YYYY-MM-DDTHH:mm:ss - Uses local timezone (NOT UTC)
- Pads single-digit values with zeros
- Pure, deterministic function
Example:
DateFormatter.toLocalTimestamp(new Date('2025-10-26T14:30:45'))
// Returns: "2025-10-26T14:30:45"static toDateWikilink(date: Date): stringContract:
- Formats date as WikiLink:
YYYY-MM-DD - Does NOT include brackets (caller adds them)
- Pure function
Example:
DateFormatter.toDateWikilink(new Date('2025-10-26'))
// Returns: "2025-10-26"static addDays(date: Date, days: number): DateContract:
- Adds/subtracts days from date
- Handles month/year boundaries correctly
- Returns new Date (doesn't mutate input)
- Pure function
All methods are static pure functions
static normalize(value: string | null | undefined): stringContract:
- Removes
[[]]brackets from WikiLinks - Removes quotes
"from values - Returns empty string for
null/undefined - Pure function
Examples:
WikiLinkHelpers.normalize('"[[ems__Task]]"') // Returns: "ems__Task"
WikiLinkHelpers.normalize('[[Area]]') // Returns: "Area"
WikiLinkHelpers.normalize('Task') // Returns: "Task"
WikiLinkHelpers.normalize(null) // Returns: ""static normalizeArray(values: string[] | string | null | undefined): string[]Contract:
- Normalizes each value in array
- Handles single string value (wraps in array)
- Returns empty array for
null/undefined - Pure function
static equals(
a: string | null | undefined,
b: string | null | undefined
): booleanContract:
- Compares normalized values (ignores brackets/quotes)
- Pure function
- Case-sensitive
static includes(
array: string[] | string | null | undefined,
value: string
): booleanContract:
- Checks if array includes value (after normalization)
- Handles single string as array
- Returns
falsefornull/undefined
All methods are static pure functions
static buildFileContent(
frontmatter: Record<string, any>,
bodyContent?: string
): stringContract:
- Pure function
- Builds complete markdown file content
- Formats frontmatter as YAML
- Appends body content if provided
- Returns valid markdown string
Output Format:
---
property1: value1
property2: value2
---
Body content herestatic isAssetArchived(metadata: Record<string, any>): booleanContract:
- Checks multiple archive properties:
exo__Asset_isArchivedarchived(Obsidian standard)
- Handles various formats:
true,1,"true","yes" - Returns
falseif no archive property found - Pure function
static ensureQuoted(value: string): stringContract:
- Adds quotes if not present:
value→"value" - Preserves existing quotes:
"value"→"value" - Pure function
static sortByPriority<T extends EffortItem>(a: T, b: T): numberContract:
- Pure comparison function for
Array.sort() - Sorting order:
- Non-trashed before trashed
- Not-done before done
- Higher votes before lower votes ⭐
- Earlier start time before later start time
- Returns:
-1,0, or1(standard comparator)
Example:
efforts.sort(EffortSortingHelpers.sortByPriority);
// Result: [active, voted tasks] → [low-voted tasks] → [done tasks] → [trashed]Purpose: Extract metadata from Obsidian cache
Dependencies: MetadataCache
Public Methods:
extractCommandVisibilityContext(file: TFile): CommandVisibilityContextContract:
- Returns context object for command visibility
- Normalizes WikiLinks
- Checks archive status (multiple formats)
- Returns default values if metadata missing
interface CommandVisibilityContext {
instanceClass: string | string[] | null;
currentStatus: string | string[] | null;
metadata: Record<string, any>;
isArchived: boolean;
currentFolder: string;
expectedFolder: string | null;
}export function canCreateTask(context: CommandVisibilityContext): booleanContract:
- Returns
trueifinstanceClassis Area or Project - Pure function (no side effects)
export function canCreateInstance(context: CommandVisibilityContext): booleanContract:
- Returns
trueifinstanceClassis TaskPrototype or MeetingPrototype - Pure function
export function canVoteOnEffort(context: CommandVisibilityContext): booleanContract:
- Returns
trueif asset is Effort AND not archived - Checks: Task, Project, or Meeting
- Pure function
All 25+ visibility functions follow same pattern:
- Input:
CommandVisibilityContext - Output:
boolean - Type: Pure function (testable without Obsidian)
- Side effects: None
export enum AssetClass {
AREA = "ems__Area",
TASK = "ems__Task",
PROJECT = "ems__Project",
MEETING = "ems__Meeting",
INITIATIVE = "ems__Initiative",
TASK_PROTOTYPE = "ems__TaskPrototype",
MEETING_PROTOTYPE = "ems__MeetingPrototype",
CONCEPT = "ims__Concept",
DAILY_NOTE = "pn__DailyNote",
}export enum EffortStatus {
DRAFT = "ems__EffortStatusDraft",
BACKLOG = "ems__EffortStatusBacklog",
ANALYSIS = "ems__EffortStatusAnalysis",
TODO = "ems__EffortStatusToDo",
DOING = "ems__EffortStatusDoing",
DONE = "ems__EffortStatusDone",
TRASHED = "ems__EffortStatusTrashed",
}export interface LabelInputModalResult {
label: string | null;
taskSize: string | null;
}export interface SupervisionFormData {
situation: string;
emotions: string;
thoughts: string;
behavior: string;
shortTermConsequences: string;
longTermConsequences: string;
}All pure functions guarantee:
- No side effects: Doesn't modify external state
- Deterministic: Same inputs → same outputs
- Testable: 100% unit testable without mocks
- Reusable: Can be used in any context (CLI, Web, Plugin)
All services guarantee:
- Input validation: Check parameters before operations
- Error handling: Throw meaningful errors
- Atomic operations: Either complete or revert
- Event consistency: Obsidian events fire correctly
Current signature (Plugin):
async createTask(sourceFile: TFile, ...): Promise<TFile>Future signature (Core):
async createTask(sourceFilePath: string, ...): Promise<string>Changes:
TFile→string(file path)Vault→IFileSystemAdapter(interface)- Return
string(path) instead ofTFile
All functions marked ✅ PURE can be copied to Core as-is:
generateTaskFrontmatter()generateProjectFrontmatter()- All WikiLinkHelpers methods
- All DateFormatter methods
- All MetadataHelpers methods
- All EffortSortingHelpers methods
- All CommandVisibility functions
- ARCHITECTURE.md - System architecture
- PROPERTY_SCHEMA.md - Property reference
- Source files in
/src/infrastructure/services/
Maintainer: @kitelev Related Issues: #122 (Core Extraction), #123 (Test Coverage), #124 (Architecture Docs)