' =================================================================== ' File: fingerprint-strong.puml ' Purpose: Single source of truth for module-level activity + per-function sequences. ' Module: fingerprint-strong.js — Stable fingerprints for assistant message elements. ' 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 (fingerprint-strong.js) ============================= @startuml title fingerprint-strong.js — Branch Flow (full module) start :fingerprint-strong; fork ' -------- MSG_SELECTORS (const) -------- partition "MSG_SELECTORS (assistant message selectors)" #E7FAE3 { :[ '[data-message-author-role=\"assistant\"]',\n '.chat-message:not([data-message-author-role=\"user\"])',\n '.message-content' ]; :Used by context scans and ordinal computation; kill } fork again ' -------- norm(s) -------- partition "norm(s)" #FFF6D1 { :norm; :remove \\r and zero-width spaces;\ncollapse spaces before \\n; trim; :return normalized string; kill } fork again ' -------- hash(s) -------- partition "hash(s)" #FFE1DB { :hash; :djb2-xor over chars;\nreturn unsigned base36; kill } fork again ' -------- commandLikeText(el) -------- partition "commandLikeText(el)" #DCF9EE { :commandLikeText; :prefer
 blocks that contain full @bridge@...@end@ with action:;\nelse fallback to el.textContent (<=2000 chars), normalized;
    :return text slice;
    kill
  }
fork again
  ' -------- prevContextHash(el) --------
  partition "prevContextHash(el)" #FFE6F0 {
    :prevContextHash;
    :scan previous assistant messages via MSG_SELECTORS;\ncollect trailing text up to 2000 chars; hash();
    :return base36;
    kill
  }
fork again
  ' -------- intraPrefixHash(el) --------
  partition "intraPrefixHash(el)" #E6F3FF {
    :intraPrefixHash;
    :find first @bridge@ block; hash the text just before it (<=2000 chars);\nif none, hash trailing slice of whole text;
    :return base36;
    kill
  }
fork again
  ' -------- domHint(node) --------
  partition "domHint(node)" #F0E6FA {
    :domHint;
    :build tag#id.class (first class only), slice 40 chars;
    :return small hint;
    kill
  }
fork again
  ' -------- ordinalForKey(el, key) --------
  partition "ordinalForKey(el, key)" #E7FAF7 {
    :ordinalForKey;
    :scan all message nodes; compute same-key for peers;\nreturn index among matches;
    kill
  }
fork again
  ' -------- fingerprintElement(el) --------
  partition "fingerprintElement(el)" #FFF2E7 {
    :fingerprintElement;
    :ch = hash(commandLikeText(el));\nph = prevContextHash(el);\nih = intraPrefixHash(el);\nkey=`ch:..|ph:..|ih:..`;\nn = ordinalForKey(el,key);\ndh = hash(domHint(el));\nreturn key+`|hint:${dh}|n:${n}`;
    kill
  }
fork again
  ' -------- getStableFingerprint(el) --------
  partition "getStableFingerprint(el)" #E7F7FF {
    :getStableFingerprint;
    :if dataset.aiRcStableFp present -> return;\nelse compute fingerprintElement(el) and cache in dataset;
    :return fp;
    kill
  }
fork again
  ' -------- expose globals --------
  partition "bootstrap (expose globals)" #FFF7E7 {
    :window.AI_REPO_FINGERPRINT = fingerprintElement;\nwindow.AI_REPO_STABLE_FINGERPRINT = getStableFingerprint;
    kill
  }
end fork
@enduml

' ==== METHOD: norm(s) =======================================================
@startuml
title fingerprint-strong:norm(s): \n Normalize text for stable hashing

participant "Caller" as CL
participant "norm(s)" as NRM

activate CL
CL -> NRM : s
activate NRM
NRM -> NRM : s = (s||'')\n  .replace(/\\r/g,'')\n  .replace(/\\u200b/g,'')\n  .replace(/[ \\t]+\\n/g,'\\n')\n  .trim()
NRM --> CL : normalized s
deactivate NRM
deactivate CL
@enduml

' ==== METHOD: hash(s) =======================================================
@startuml
title fingerprint-strong:hash(s): \n djb2-xor -> unsigned base36

participant "Caller" as CL
participant "hash(s)" as HSH

activate CL
CL -> HSH : s
activate HSH
HSH -> HSH : h=5381; for each char: h=((h<<5)+h)^code
HSH --> CL : (h>>>0).toString(36)
deactivate HSH
deactivate CL
@enduml

' ==== METHOD: commandLikeText(el) ==========================================
@startuml
title fingerprint-strong:commandLikeText(el): \n Prefer fenced @bridge@ blocks; fallback to raw text

participant "Caller" as CL
participant "commandLikeText(el)" as CLT
participant "norm(s)" as NRM
participant "DOM" as DOM

activate CL
CL -> CLT : el
activate CLT

CLT -> DOM : el.querySelectorAll('pre code, pre, code')
DOM --> CLT : NodeList blocks

loop for each block b
  CLT -> NRM : norm(b.textContent||'')
  NRM --> CLT : t
  alt looks like full @bridge@ ... @end@ and contains 'action:'
    CLT --> CL : t
    deactivate CLT
    deactivate CL
    return
  end
end

' fallback to element text
CLT -> NRM : norm((el.textContent||'').slice(0,2000))
NRM --> CLT : text
CLT --> CL : text
deactivate CLT
deactivate CL
@enduml

' ==== METHOD: prevContextHash(el) ==========================================
@startuml
title fingerprint-strong:prevContextHash(el): \n Hash trailing text of prior assistant messages (≤2000 chars)

participant "Caller" as CL
participant "prevContextHash(el)" as PCH
participant "DOM" as DOM
participant "norm(s)" as NRM
participant "hash(s)" as HSH

activate CL
CL -> PCH : el
activate PCH

PCH -> DOM : querySelectorAll(MSG_SELECTORS.join(','))
DOM --> PCH : list[]
PCH -> PCH : idx = list.indexOf(el)
alt idx <= 0
  PCH --> CL : "0"
  deactivate PCH
  deactivate CL
  return
end

PCH -> PCH : rem=2000; buf=''
loop for i = idx-1 down to 0 while rem>0
  PCH -> NRM : norm(list[i].textContent||'')
  NRM --> PCH : t
  alt t non-empty
    PCH -> PCH : take = t.slice(-rem); buf = take + buf; rem -= take.length
  end
end

PCH -> HSH : hash(buf.slice(-2000))
HSH --> PCH : ph
PCH --> CL : ph
deactivate PCH
deactivate CL
@enduml

' ==== METHOD: intraPrefixHash(el) ==========================================
@startuml
title fingerprint-strong:intraPrefixHash(el): \n Hash text immediately before first command block

participant "Caller" as CL
participant "intraPrefixHash(el)" as IPH
participant "hash(s)" as HSH
participant "norm(s)" as NRM

activate CL
CL -> IPH : el
activate IPH

IPH -> IPH : t = el.textContent||''
IPH -> IPH : m = t.match(/@bridge@[\\s\\S]*?@end@/m)
IPH -> IPH : endIdx = m ? t.indexOf(m[0]) : t.length
IPH -> NRM : norm(t.slice(max(0,endIdx-2000), endIdx))
NRM --> IPH : slice
IPH -> HSH : hash(slice)
HSH --> IPH : ih
IPH --> CL : ih
deactivate IPH
deactivate CL
@enduml

' ==== METHOD: domHint(node) ================================================
@startuml
title fingerprint-strong:domHint(node): \n Tiny tag#id.class hint (first class, ≤40 chars)

participant "Caller" as CL
participant "domHint(node)" as DH
participant "DOM" as DOM

activate CL
CL -> DH : node
activate DH
alt !node
  DH --> CL : ""
  deactivate DH
  deactivate CL
  return
end

DH -> DOM : node.id / node.className / node.tagName
DOM --> DH : values
DH -> DH : cls = (typeof className==='string' ? className.split(' ')[0] : '')\n hint = `${tag}#${id}.${cls}`.slice(0,40)
DH --> CL : hint
deactivate DH
deactivate CL
@enduml

' ==== METHOD: ordinalForKey(el, key) =======================================
@startuml
title fingerprint-strong:ordinalForKey(el, key): \n Index el among elements with same ch|ph|ih

participant "Caller" as CL
participant "ordinalForKey(el, key)" as ORD
participant "DOM" as DOM
participant "commandLikeText(el)" as CLT
participant "prevContextHash(el)" as PCH
participant "intraPrefixHash(el)" as IPH
participant "hash(s)" as HSH

activate CL
CL -> ORD : el, key
activate ORD

ORD -> DOM : querySelectorAll(MSG_SELECTORS.join(','))
DOM --> ORD : list[]
ORD -> ORD : n = 0
loop for each node in list
  alt node === el
    ORD -> ORD : nodeKey = key
  else other node
    ORD -> CLT : commandLikeText(node)
    CLT --> ORD : text
    ORD -> HSH : hash(text.slice(0,2000))
    HSH --> ORD : ch
    ORD -> PCH : prevContextHash(node)
    PCH --> ORD : ph
    ORD -> IPH : intraPrefixHash(node)
    IPH --> ORD : ih
    ORD -> ORD : nodeKey = `ch:${ch}|ph:${ph}|ih:${ih}`
  end

  alt nodeKey == key
    alt node === el
      ORD --> CL : n
      deactivate ORD
      deactivate CL
      return
    else
      ORD -> ORD : n++
    end
  end
end

ORD --> CL : n
deactivate ORD
deactivate CL
@enduml

' ==== METHOD: fingerprintElement(el) =======================================
@startuml
title fingerprint-strong:fingerprintElement(el): \n Compose ch|ph|ih + hint + ordinal

participant "Caller" as CL
participant "fingerprintElement(el)" as FPE
participant "commandLikeText(el)" as CLT
participant "prevContextHash(el)" as PCH
participant "intraPrefixHash(el)" as IPH
participant "domHint(node)" as DH
participant "ordinalForKey(el, key)" as ORD
participant "hash(s)" as HSH

activate CL
CL -> FPE : el
activate FPE

' ch
FPE -> CLT : commandLikeText(el)
CLT --> FPE : text
FPE -> HSH : hash(text.slice(0,2000))
HSH --> FPE : ch

' ph / ih
FPE -> PCH : prevContextHash(el)
PCH --> FPE : ph
FPE -> IPH : intraPrefixHash(el)
IPH --> FPE : ih

FPE -> FPE : key = `ch:${ch}|ph:${ph}|ih:${ih}`

' ordinal
FPE -> ORD : ordinalForKey(el, key)
ORD --> FPE : n

' hint
FPE -> DH : domHint(el)
DH --> FPE : hint
FPE -> HSH : hash(hint)
HSH --> FPE : dh

FPE --> CL : `ch:${ch}|ph:${ph}|ih:${ih}|hint:${dh}|n:${n}`
deactivate FPE
deactivate CL
@enduml

' ==== METHOD: getStableFingerprint(el) =====================================
@startuml
title fingerprint-strong:getStableFingerprint(el): \n Cache-first wrapper over fingerprintElement(el)

participant "Caller" as CL
participant "getStableFingerprint(el)" as GSF
participant "fingerprintElement(el)" as FPE
participant "Dataset" as DATA

activate CL
CL -> GSF : el
activate GSF

GSF -> DATA : read el.dataset.aiRcStableFp
DATA --> GSF : fp | undefined
alt cached
  GSF --> CL : fp
else not cached
  GSF -> FPE : fingerprintElement(el)
  FPE --> GSF : fp
  GSF -> DATA : el.dataset.aiRcStableFp = fp (try/catch)
  DATA --> GSF : ok | ignored
  GSF --> CL : fp
end

deactivate GSF
deactivate CL
@enduml

' ==== METHOD: bootstrap (expose globals) ===================================
@startuml
title fingerprint-strong:bootstrap: \n Expose helpers on window for consumers

participant "Caller" as CL
participant "bootstrap" as BOOT
participant "fingerprintElement(el)" as FPE
participant "getStableFingerprint(el)" as GSF
participant "window" as WIN

activate CL
CL -> BOOT : module load
activate BOOT
BOOT -> WIN : WIN.AI_REPO_FINGERPRINT = FPE
WIN --> BOOT : ok
BOOT -> WIN : WIN.AI_REPO_STABLE_FINGERPRINT = GSF
WIN --> BOOT : ok
BOOT --> CL : (void)
deactivate BOOT
deactivate CL
@enduml

' ==== LEGEND ===============================================================
@startuml
legend bottom
== fingerprint-strong 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 module lifeline; the module name appears in the title only.
  3) Include every directly-called helper/system as a participant
     (e.g., "commandLikeText()", "prevContextHash()", "intraPrefixHash()", "domHint()", "ordinalForKey()", "hash()", "norm()", "Dataset", "DOM", "window").
  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.
  B) One partition per function; soft background color; terminate branches with 'kill'.
  C) Keep wording aligned with code (e.g., 2000-char slices, base36 hash, dataset cache key).

• Color palette (soft pastels)
• Use --> for returns; -> for calls.
• Participants use quoted function names for internals; plain nouns for systems ("DOM", "Dataset", "window").
• Keep this legend at the end of the file to standardize edits.
endlegend
@enduml