FCR Command — NLP Engine Architecture¶
Architecture contract
The NLP engine is a pure-function virtual machine that executes data-defined rules from JSON payloads. The engine contains zero language-specific logic; all vocabulary, regex patterns, and phrase mappings live exclusively in the payload files. This separation ensures the engine scales to any language by adding a JSON file — no code changes required.
Core model¶
| Layer | Ownership | Implementation |
|---|---|---|
| Engine (VM) | Core DSL execution, regex matching, title stripping, command dispatch | src/features/nlp/engine.ts |
| Payload (Lexer) | Language-specific regex rules and DSL action mappings | src/features/nlp/payloads/<locale>.json |
| Loader | Payload resolution, in-memory caching, disk caching, remote fetch | src/features/nlp/loader.ts |
| Smart Calendar | Pure-function title scanner for dynamic calendar name matching | src/features/nlp/smartCalendar.ts |
| Dispatcher | Maps NLPActionObject → plugin actions (cache/provider mutations, navigation, settings, sync) |
src/features/nlp/dispatcher.ts |
| Modal | User-facing input with live preview, debounced parsing | src/features/nlp/NLPCommandModal.ts |
Data flow¶
Raw Input String
│
▼
┌─────────────┐ ┌──────────────┐
│ NLP Loader │────▶│ JSON Payload │ (in-memory cache → disk cache → remote fetch)
└─────────────┘ └──────┬───────┘
│
┌───────────────────┘
▼
┌─────────────┐
│ NLP Engine │ For each rule: regex test → capture groups → DSL execution → title strip
│ (Pure VM) │ Short-circuits on command/navigation intents
└──────┬──────┘
│
▼
┌──────────────┐
│ ActionObject │ { intent, title, date, hours, minutes, targetCalendar, recurrence }
└──────┬───────┘
│
▼
┌─────────────────┐
│ Smart Calendar │ Scans remaining title for "in <name>" → resolves against actual calendars
│ (Pure function) │ Only runs for CREATE_EVENT when no explicit target set
└──────┬──────────┘
│
▼
┌────────────┐
│ Dispatcher │ CREATE_EVENT → EventCache.addEvent() → ProviderRegistry → Provider.createEvent()
│ │ NAVIGATE_* → InternalAPI.changeView()
│ │ OPEN_* → InternalAPI / PluginState
│ │ GOTO_DATE → changeView('timeGridDay') + gotoDate()
│ │ RESET_CACHE → EventCache.reset()
│ │ REVALIDATE_* → ProviderRegistry.revalidateRemoteCalendars()
│ │ SYNC_* → activitywatch/sync
│ │ SHOW_CHANGELOG → PluginState.showChangelog()
│ │ SHOW_MILESTONES → PluginState.showMilestones()
└────────────┘
Intent taxonomy¶
| Intent | Short-circuits? | Dispatcher target |
|---|---|---|
CREATE_EVENT |
No | EventCache.addEvent() with smart calendar resolution and provider-owned create endpoint |
NEW_EVENT |
No | openCreateModal() (blank form) |
NAVIGATE_DAY |
Yes | changeView('timeGridDay') |
NAVIGATE_WEEK |
Yes | changeView('timeGridWeek') |
NAVIGATE_MONTH |
Yes | changeView('dayGridMonth') |
OPEN_CALENDAR |
Yes | InternalAPI.openCalendar() |
OPEN_SIDEBAR |
Yes | InternalAPI.openSidebar() |
OPEN_SETTINGS |
Yes | PluginState.displaySettingsTab() |
OPEN_CHRONO |
Yes | Lazy-loads AnalysisView, opens in new tab |
SHOW_CHANGELOG |
Yes | PluginState.showChangelog() |
SHOW_MILESTONES |
Yes | PluginState.showMilestones() |
RESET_CACHE |
Yes | EventCache.reset() + Notice |
REVALIDATE_REMOTE |
Yes | ProviderRegistry.revalidateRemoteCalendars() |
SYNC_ACTIVITYWATCH |
Yes | Lazy-loads activitywatch/sync, checks enabled |
GOTO_DATE |
No | changeView('timeGridDay') + gotoDate(date) |
GOTO_DATE does NOT short-circuit so that subsequent date rules (e.g.,
tomorrow,next tuesday) can still modify the context date before the dispatcher navigates.
Provider delegation contract for CREATE_EVENT¶
CREATE_EVENT stays generic by design:
- Dispatcher builds a normalized
OFCEventpayload. - Dispatcher calls
EventCache.addEvent(calendarId, event). EventCacheroutes write throughProviderRegistryto the selected provider.- Provider decides endpoint behavior.
Example: TaskNotesProvider may delegate to native TaskNotes UI and throw DelegatedProviderActionError to signal an intentional handoff. Cache mutation handling then rolls back optimistic placeholder state without surfacing a generic create-failed error.
DSL command reference¶
| Command | Arguments | Behavior |
|---|---|---|
ADD_DAYS(x) |
Integer | Adds x days to context date |
SUBTRACT_DAYS(x) |
Integer | Subtracts x days from context date |
ADD_HOURS(x) |
Integer | Adds x hours (rolls over days via native Date) |
ADD_MINUTES(x) |
Integer | Adds x minutes (rolls over hours/days via native Date) |
ADD_WEEKS(x) |
Integer | Adds x * 7 days |
SET_TIME(h, m, meridiem) |
hours, minutes, "am"/"pm"/"" | Sets time with AM/PM conversion (12 am → 0, 4 pm → 16) |
NEXT_WEEKDAY(day) |
Weekday name or index (0-6) | Advances to next occurrence; wraps +7 if target == current day |
SET_DAY(day) |
Weekday name or index (0-6) | Sets to specific weekday of current week (can go backward) |
SET_INTENT(type) |
NLPIntent string | Sets intent; triggers short-circuit for non-event intents |
SET_TARGET(keyword) |
Calendar name string | Sets target calendar for routing |
SET_RECURRENCE(freq, interval, byDay?) |
freq, interval, optional weekday | Sets recurrence metadata |
Smart calendar resolution¶
The smart calendar resolver (smartCalendar.ts) is a pure function that runs after the engine and before the dispatcher. It scans the remaining title text for a trailing in <name> pattern and matches against actual configured calendar names (case-insensitive).
Algorithm:
1. Skip if targetCalendar is already set (explicit in <name> calendar rule ran)
2. Skip if intent is not CREATE_EVENT
3. Find all occurrences of in <text> in the title
4. For each occurrence (left-to-right), check if the suffix after "in" matches a calendar name
5. Use the last matching occurrence (rightmost) to avoid stripping location phrases like "Meeting in London in daily1"
This function has zero Obsidian dependencies, making it fully testable in Jest.
Payload schema¶
{
"version": 2,
"locale": "en",
"rules": [
{
"name": "rule_identifier",
"regex": "escaped regex with (capture groups)",
"flags": "i",
"actions": ["DSL_COMMAND($1, $2)"]
}
]
}
Ordering contract
Rules are evaluated sequentially from top to bottom. The array must be ordered by specificity: longest/most-complex patterns first, broad/simple patterns last. Violating this invariant causes partial matches to consume text that longer patterns need.
Rule evaluation semantics¶
- For each rule, test regex against the current (progressively stripped) input text
- On match: extract capture groups, strip matched substring, execute DSL actions
- If any action sets a short-circuiting intent → abort remaining rules
- After all rules: remaining text becomes event title
Invariants for contributors¶
- No logic in payloads. JSON files contain only regex patterns and DSL command strings
- No language-specific code in engine. The
WEEKDAY_INDEXlookup table supports multiple languages but the engine itself is language-agnostic - Short-circuit is intent-driven. All intents short-circuit except
CREATE_EVENTandGOTO_DATE - Title is the residual. After all matched substrings are stripped, the remaining whitespace-normalized text is the event title
- Smart calendar runs post-engine. It's a pure function applied to the action object, not part of the engine loop
- Payload ordering is the developer's responsibility. The engine does not sort or reorder rules
Integration anchors¶
src/features/nlp/engine.ts— Core VM, pure functionprocessNaturalLanguage()src/features/nlp/types.ts— All NLP type definitions (15 intents)src/features/nlp/loader.ts— Payload loading with three-tier resolutionsrc/features/nlp/smartCalendar.ts— Pure smart calendar resolversrc/features/nlp/dispatcher.ts— Action → plugin API bridge (orchestrator)src/features/nlp/NLPCommandModal.ts— Live preview modalsrc/features/nlp/registerNLPCommand.ts— Command palette registrationsrc/features/nlp/payloads/en.json— English payload v2 (bundled)src/features/nlp/index.ts— Public module API