Request Lifecycle & Ambient Triggering #
The Janitor operates as a silent background agent. It never interrupts the writer's typing flow and executes strictly on non-blocking background threads:
// 1. Boundary Trigger: Fires every 100 words drafted
const currBoundary = Math.floor(words / 100);
if (currBoundary > prevBoundary) {
triggerJanitorAnalysis();
}
// 2. Inactivity Trigger: Fires after 10 seconds of idle typing
clearTimeout(janitorInactivityTimer);
janitorInactivityTimer = setTimeout(() => triggerJanitorAnalysis(), 10000);
When triggered, the frontend posts the chapter's raw TipTap HTML to POST /api/project/janitor/analyze. The backend strips HTML while preserving character offsets, extracts word boundaries, routes the text to the matching language analyzer (janitor.py, hun_janitor.py, or pol_janitor.py), and returns a structured list of actionable suggestion cards.
The 9-Analyzer Pipeline #
Each analysis pass executes 9 dedicated linguistic and worldbuilding checks:
| Analyzer | Runic Token | Target & Architectural Logic | Cap |
|---|---|---|---|
1. Link Existinglink_existing |
α | Matches untagged character, location, or lore names against the SQLite database using regex boundary search. Skips text already enclosed in existing TipTap spans. | 5 cards |
2. Create Entitycreate_entity |
α’ | Runs spaCy NER (Named Entity Recognition) over the first 5000 characters. Maps PERSON β Character, GPE/LOC/FAC β Location, and ORG β Lore. |
5 cards |
3. Shorthand Aliasalias |
α¦ | Detects multi-word entity parts (β₯ 4 chars) appearing standalone without the full entity name nearby (>200 chars), suggesting formal alias creation. | 3 cards |
4. Spellcheck Typotypo |
α± | Runs tokenized words through localized phunspell (Hunspell) dictionaries. Automatically whitelists all registered project entities and aliases. |
3 cards |
5. Weak Synonymssynonym |
α | Matches overused weak words ("walked", "said", "very", "got") and queries NLTK WordNet synsets for vivid lexical alternatives. | 2 cards |
6. Weak Adverbsweak_adverbs |
α | Flags adverb-verb pairings that signal lazy telling (e.g. "walked slowly", "said quietly"). Uses POS tagging and language suffix rules (-ly in EN, -an/-en/-ul/-ΓΌl in HU). |
5 cards |
7. Passive Voicepassive_voice |
α | Flags passive constructions: English auxpass dependency tags; Hungarian verbal adverbs with -va/-ve suffixes (hatΓ‘rozΓ³i igenΓ©v); Polish byΔ/zostaΔ auxiliaries. |
3 cards |
8. Show, Don't Tellshow_dont_tell |
α | Executes a 4-detector pipeline measuring abstract exposition, emotional labelling, and POV camera filtering outside of dialogue blocks. | 5 cards |
9. Sentence Pacingpacing |
α« | Compares sentence-initial words across 3 consecutive sentences using spaCy sentence segmentation. Flags monotone repetitive openings. | 2 cards |
Show, Don't Tell: The 4 Detectors #
Before running the detectors, the engine strips all dialogue sentences to avoid falsely flagging spoken character conversation (excluding quotation marks "..." in English/Polish and em-dashes β ... in Hungarian).
| Detector | Pattern Detected | EN Confidence | HU / PL Confidence |
|---|---|---|---|
emotion_label |
Linking verb (be, feel, seem) + emotion adjective (furious, anxious) as an adjectival complement (acomp). Exempts physical states (tall, dead, born). |
0.85 |
0.75 |
filter_verb |
Perception verb (see, hear, notice, watch) with both subject and object clauses β filtering narrative immediacy through a sensory lens. | 0.60 |
0.50 |
realize_verb |
Cognitive verb (realize, understand, know, decide) with a complement clause (ccomp / xcomp). |
0.65 |
0.55 |
adverb_emotion |
Speech verb as syntactic root + emotion adverb modifier ("she said angrily", "he shouted furiously"). | 0.75 |
0.65 |
Authors can calibrate the sensitivity slider in Project Settings β Janitor (janitor_sdt_confidence from 0.30 to 0.90, default 0.50) to trade precision for recall.
Fuzzy Radius Offset Mitigation #
Because TipTap ProseMirror calculates node distances without counting invisible inter-paragraph \n separators, raw character coordinates from Python's plain-text scan will experience slight coordinate drift in large multi-page chapters.
To ensure 100% click-to-highlight accuracy, `JanitorPanel.jsx` implements a Fuzzy Radius Search:
- The frontend receives the backend's approximate
char_offsetcoordinate. - It scans neighboring ProseMirror text nodes within a safe radius (Β±150 characters).
- It executes an exact string match for
matched_text, anchoring the visual pulse highlight and one-click replacement directly to the active DOM element.