' =================================================================== ' File: storage.puml ' Purpose: Single source of truth for module-level activity + per-method sequences. ' Module: storage.js — Conversation-aware de-duplication via localStorage. ' Edit rules: Follow the legend at bottom; preserve VIEW/METHOD anchors for automation. ' =================================================================== ' Neutral defaults — typography/layout only (keeps partition colors intact) skinparam Shadowing false skinparam SequenceMessageAlign center skinparam SequenceLifeLineBorderColor #666666 skinparam SequenceLifeLineBorderThickness 1 ' ==== VIEW: Branch Flow (storage.js) ======================================= @startuml title storage.js — Branch Flow (full module) start :storage; fork ' -------- ConversationHistory.constructor() -------- partition "ConversationHistory.constructor()" #E7FAE3 { :constructor; :conversationId = _getConversationId();\nkey = `ai_rc:conv:${conversationId}:processed`; :cache = _load(); _cleanupExpired(); kill } fork again ' -------- _getConversationId() -------- partition "_getConversationId()" #FFF6D1 { :_getConversationId; :host = location.hostname.replace('chat.openai.com','chatgpt.com');\nreturn `${host}:${location.pathname||'/'}`; kill } fork again ' -------- _load() / _save() -------- partition "_load() / _save()" #FFE1DB { :_load -> JSON.parse(localStorage.getItem(key)||'{}')\n(catch -> {}); :_save -> localStorage.setItem(key, JSON.stringify(cache))\n(catch -> logger.warn(...)); kill } fork again ' -------- isProcessed(el, idx=0) -------- partition "isProcessed(el, idx=0)" #DCF9EE { :isProcessed; :fp = _fingerprint(el, idx);\nreturn cache.hasOwnProperty(fp); kill } fork again ' -------- markProcessed(el, idx=0) -------- partition "markProcessed(el, idx=0)" #FFE6F0 { :markProcessed; :fp = _fingerprint(el, idx);\ncache[fp] = Date.now(); _save();\nif cfg.ui.showExecutedMarker -> _mark(el); kill } fork again ' -------- _fingerprint(el, idx) -------- partition "_fingerprint(el, idx)" #E6F3FF { :_fingerprint; :base = (window.AI_REPO_STABLE_FINGERPRINT? stable(el)\n : window.AI_REPO_FINGERPRINT? compat(el)\n : _hash((el.textContent||'').slice(0,1000))); :return `${base}|idx:${idx}`; kill } fork again ' -------- _hash(str) -------- partition "_hash(str)" #F0E6FA { :_hash; :djb2-xor over up to 1000 chars;\nreturn unsigned base36; kill } fork again ' -------- _mark(el) -------- partition "_mark(el)" #E7FAF7 { :_mark; :el.style.borderLeft = '3px solid #10B981' (try/catch); kill } fork again ' -------- _cleanupExpired() -------- partition "_cleanupExpired()" #FFF2E7 { :_cleanupExpired; :ttl = 30d; now = Date.now();\nfor (k,ts in cache) if !ts || now - ts > ttl -> delete;\nif dirty -> _save(); kill } fork again ' -------- clear() -------- partition "clear()" #E7F7FF { :clear; :cache = {}; _save(); kill } fork again ' -------- bootstrap (global) -------- partition "bootstrap (global)" #FFF7E7 { :window.AI_REPO_HISTORY = new ConversationHistory(); kill } end fork @enduml ' ==== METHOD: ConversationHistory.constructor() ============================ @startuml title storage:constructor(): \n Build per-conversation key, load cache, prune expired participant "Caller" as CL participant "constructor()" as CTOR participant "_getConversationId()" as GID participant "_load()" as LOAD participant "_cleanupExpired()" as CLEAN activate CL CL -> CTOR : new ConversationHistory() activate CTOR CTOR -> GID : _getConversationId() GID --> CTOR : conversationId CTOR -> CTOR : key = `ai_rc:conv:${conversationId}:processed` CTOR -> LOAD : _load() LOAD --> CTOR : cache object CTOR -> CLEAN : _cleanupExpired() CLEAN --> CTOR : done CTOR --> CL : instance deactivate CTOR deactivate CL @enduml ' ==== METHOD: _getConversationId() ======================================== @startuml title storage:_getConversationId(): \n host+path key (normalize chat.openai.com → chatgpt.com) participant "Caller" as CL participant "_getConversationId()" as GID participant "Location" as LOC activate CL CL -> GID : initial request activate GID GID -> LOC : read location.hostname, location.pathname LOC --> GID : hostname, pathname GID -> GID : host = hostname.replace('chat.openai.com','chatgpt.com') GID --> CL : `${host}:${pathname || '/'}` deactivate GID deactivate CL @enduml ' ==== METHOD: _load() / _save() =========================================== @startuml title storage:_load() / _save(): \n Safely read/write JSON cache in localStorage participant "Caller" as CL participant "_load()" as LOAD participant "_save()" as SAVE participant "localStorage" as LS participant "Logger" as LOG ' _load() activate CL CL -> LOAD : initial request activate LOAD LOAD -> LS : getItem(key) LS --> LOAD : json | null LOAD -> LOAD : JSON.parse(json||'{}') catch -> {} LOAD --> CL : cache object deactivate LOAD ' _save() CL -> SAVE : cache activate SAVE SAVE -> LS : setItem(key, JSON.stringify(cache)) alt success LS --> SAVE : ok SAVE --> CL : done else error LS --> SAVE : throw e SAVE -> LOG : warn('Failed to save history cache', {error:e.message}) LOG --> SAVE : logged SAVE --> CL : done end deactivate SAVE deactivate CL @enduml ' ==== METHOD: isProcessed(el, idx=0) ====================================== @startuml title storage:isProcessed(el, idx): \n Check cache using fingerprint+index key participant "Caller" as CL participant "isProcessed(el,idx)" as ISP participant "_fingerprint(el,idx)" as FP activate CL CL -> ISP : el, idx activate ISP ISP -> FP : _fingerprint(el, idx) FP --> ISP : fp ISP -> ISP : return Object.prototype.hasOwnProperty.call(cache, fp) ISP --> CL : true/false deactivate ISP deactivate CL @enduml ' ==== METHOD: markProcessed(el, idx=0) ===================================== @startuml title storage:markProcessed(el, idx): \n Record timestamp, persist, optionally mark DOM participant "Caller" as CL participant "markProcessed(el,idx)" as MK participant "_fingerprint(el,idx)" as FP participant "_save()" as SAVE participant "_mark(el)" as MRK participant "Config" as CFG activate CL CL -> MK : el, idx activate MK MK -> FP : _fingerprint(el, idx) FP --> MK : fp MK -> MK : cache[fp] = Date.now() MK -> SAVE : _save() SAVE --> MK : ok MK -> CFG : get('ui.showExecutedMarker') CFG --> MK : bool alt true MK -> MRK : _mark(el) MRK --> MK : done end MK --> CL : (void) deactivate MK deactivate CL @enduml ' ==== METHOD: _fingerprint(el, idx) ======================================== @startuml title storage:_fingerprint(el, idx): \n Use stable FP if available; else compat; else inline hash participant "Caller" as CL participant "_fingerprint(el,idx)" as FP participant "AI_REPO_STABLE_FINGERPRINT" as STAB participant "AI_REPO_FINGERPRINT" as COMP participant "_hash(str)" as HSH activate CL CL -> FP : el, idx activate FP FP -> STAB : exists? STAB --> FP : yes/no alt stable available FP -> STAB : STABLE_FINGERPRINT(el) STAB --> FP : base else no stable FP -> COMP : exists? COMP --> FP : yes/no alt compat available FP -> COMP : FINGERPRINT(el) COMP --> FP : base else none FP -> FP : text = (el.textContent||'').slice(0,1000) FP -> HSH : _hash(text) HSH --> FP : base end end FP --> CL : `${base}|idx:${idx}` deactivate FP deactivate CL @enduml ' ==== METHOD: _hash(str) =================================================== @startuml title storage:_hash(str): \n djb2-xor over up to 1000 chars → base36 participant "Caller" as CL participant "_hash(str)" as HSH activate CL CL -> HSH : str activate HSH HSH -> HSH : h=5381; for i CL : (h>>>0).toString(36) deactivate HSH deactivate CL @enduml ' ==== METHOD: _mark(el) ==================================================== @startuml title storage:_mark(el): \n Visual left border to indicate executed command participant "Caller" as CL participant "_mark(el)" as MRK participant "DOM" as DOM activate CL CL -> MRK : el activate MRK MRK -> DOM : el.style.borderLeft = '3px solid #10B981' (try/catch) DOM --> MRK : ok | ignored MRK --> CL : (void) deactivate MRK deactivate CL @enduml ' ==== METHOD: _cleanupExpired() ============================================ @startuml title storage:_cleanupExpired(): \n TTL pruning (30 days) + conditional save participant "Caller" as CL participant "_cleanupExpired()" as CLN participant "_save()" as SAVE activate CL CL -> CLN : initial request activate CLN CLN -> CLN : ttl=30*24*60*60*1000; now=Date.now(); dirty=false loop for each [k, ts] in Object.entries(cache) alt !ts or now - ts > ttl CLN -> CLN : delete cache[k]; dirty=true end end alt dirty CLN -> SAVE : _save() SAVE --> CLN : ok end CLN --> CL : (void) deactivate CLN deactivate CL @enduml ' ==== METHOD: clear() ====================================================== @startuml title storage:clear(): \n Reset cache and persist participant "Caller" as CL participant "clear()" as CLR participant "_save()" as SAVE activate CL CL -> CLR : initial request activate CLR CLR -> CLR : cache = {} CLR -> SAVE : _save() SAVE --> CLR : ok CLR --> CL : (void) deactivate CLR deactivate CL @enduml ' ==== METHOD: bootstrap (global) =========================================== @startuml title storage:bootstrap: \n Expose ConversationHistory singleton on window participant "Caller" as CL participant "bootstrap" as BOOT participant "ConversationHistory()" as CTOR participant "window" as WIN activate CL CL -> BOOT : module load activate BOOT BOOT -> CTOR : new ConversationHistory() CTOR --> BOOT : instance BOOT -> WIN : window.AI_REPO_HISTORY = instance WIN --> BOOT : ok BOOT --> CL : (void) deactivate BOOT deactivate CL @enduml ' ==== LEGEND =============================================================== @startuml legend bottom == storage UML Style Guide (for future edits) == • Scope: One .puml per module. Keep two views: (1) Activity "Branch Flow" for the whole module (partitions + soft colors), (2) Per-function Sequence diagrams for each exported or significant helper. • Sequence conventions: 1) First participant is the external caller (use "Caller" or a concrete origin). 2) Do NOT add a class lifeline; the module/class name appears in the title only. 3) Include every directly-called helper/system as a participant (e.g., "_fingerprint()", "_hash()", "_mark()", "Config", "Logger", "localStorage", "Location"). 4) Prefer simple messages; Use --> for returns; -> for calls. 5) Use activate/deactivate as you see fit for clarity. 6) Use alt blocks only when branches meaningfully change the message flow. • Activity view conventions: A) Start with module node then fork partitions for each function/method. B) One partition per function; soft background color; terminate branches with 'kill'. C) Keep wording aligned with code (e.g., 30-day TTL, base36 hashes, idx suffix, key prefix `ai_rc:conv:`). • Color palette (soft pastels) • Use --> for returns; -> for calls. • Participants use quoted method names for internals; plain nouns for systems ("localStorage", "Location", "window"). • Keep this legend at the end of the file to standardize edits. endlegend @enduml