440 lines
12 KiB
Plaintext
440 lines
12 KiB
Plaintext
' ===================================================================
|
|
' 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 <pre><code> 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
|