1window.utilCg = (function(){ 2 function clerpUUID(d){ 3 return 'ð¤' + d.featureIndex 4 } 5 6 function parseClerpUUID(str){ 7 var [featureIndex] = str.split('ð¤') 8 return {featureIndex} 9 } 10 11 function loadDatapath(urlStr){ 12 try { 13 var url = new URL(urlStr) 14 urlStr = url.searchParams.get('datapath') ?? urlStr 15 } catch {} 16 urlStr = urlStr?.replace('index.html', 'data.json').split('?')[0] || 'data.json' 17 18 try { 19 return util.getFile(urlStr) // seems unused - don't worry about it 20 } catch (exc) { 21 d3.select('body') 22 .html(`Couldn't load data from <code>${urlStr}</code>: ${exc}. Maybe you need to specify a <code>?datapath=</code> argument?`) 23 .st({color: '#c00', fontSize: '150%', padding: '1em', whiteSpace: 'pre-wrap'}) 24 throw exc 25 } 26 } 27 28 function saveHClerpsToLocalStorage(hClerps) { 29 const key = 'local-clerp' 30 const hClerpArray = Array.from(hClerps.entries()).filter(d => d[1]) 31 localStorage.setItem(key, JSON.stringify(hClerpArray)); 32 } 33 34 function getHClerpsFromLocalStorage() { 35 const key = 'local-clerp' 36 // We want to set on load here so that any page load will fix the key. 37 if (localStorage.getItem(key) === null) localStorage.setItem(key, '[]') 38 const hClerpArray = JSON.parse(localStorage.getItem(key)) 39 .filter(d => d[0] != clerpUUID({})) 40 return new Map(hClerpArray) 41 } 42 43 async function deDupHClerps() { 44 const remoteClerps = [] 45 let remoteMap = new Map(remoteClerps.map(d => { 46 let key = clerpUUID(d); 47 let clerp = d.clerp; 48 return [key, clerp]; 49 })); 50 51 let localClerps = getHClerpsFromLocalStorage() 52 let featureLookup = {} 53 data.features.forEach(d => featureLookup[clerpUUID(d)] = d) 54 55 // update feature data with current spreadsheet 56 // (why is this behind the "copy" button?) 57 Array.from(remoteMap).forEach(([key, value]) => { 58 if (featureLookup[key]) featureLookup[key].remoteClerp = value 59 }) 60 61 const deDupArray = Array.from(localClerps) 62 .filter(([key, localClerp]) => { 63 let remote = remoteMap.get(key); 64 65 // keep only local clerps that are different from remote 66 if (!remote) return true 67 // gdoc to local storage mangles quotes, don't force strict equality 68 function slugify(d){ return d ? d.replace(/['"]/g, '').trim() : ''} 69 if (slugify(remote) != slugify(localClerp)) return true 70 71 // if local changes are on remote, delete localClerp and set remoteClerp 72 localClerps.delete(key) 73 if (featureLookup[key]) featureLookup[key].localClerp = '' 74 }) 75 76 // copy feature.remoteClerp to node.remoteClerp 77 data.nodes?.forEach(node => { 78 var feature = data.features.idToFeature[node.featureId] 79 node.remoteClerp = feature.remoteClerp 80 node.localClerp = feature.localClerp 81 }) 82 83 saveHClerpsToLocalStorage(new Map(deDupArray)) 84 return new Map(deDupArray); 85 } 86 87 function tabifyHClerps(hClerps) { 88 return [] 89 } 90 91 function hClerpUpdateFn(params, data){ 92 const localClerps = getHClerpsFromLocalStorage(); 93 if (params) { 94 const [node, hClerp] = params; 95 localClerps.set(clerpUUID(node), hClerp) 96 saveHClerpsToLocalStorage(localClerps); 97 } 98 99 data.features.forEach(node => { 100 node.localClerp = localClerps.get(clerpUUID(node)) 101 node.ppClerp = node.localClerp || node.remoteClerp || node.clerp; 102 }) 103 104 data.nodes?.forEach(node => { 105 var feature = data.features.idToFeature[node.featureId] 106 if (!feature) return 107 node.localClerp = feature.localClerp 108 node.ppClerp = feature.ppClerp 109 }) 110 } 111 112 // Adds virtual logit node showing A-B logit difference based on url param logitDiff=â½tokenAâ½__vs__â½tokenBâ½ 113 function addVirtualDiff(data){ 114 // Filter out any previous virtual nodes/links 115 var nodes = data.nodes.filter(d => !d.isJsVirtual) 116 var links = data.links.filter(d => !d.isJsVirtual) 117 nodes.forEach(d => d.logitToken = d.clerp?.split(`"`)[1]?.split(`" k(p=`)[0]) 118 119 var [logitAStr, logitBStr] = util.params.get('logitDiff')?.split('__vs__') || [] 120 if (!logitAStr || !logitBStr) return {nodes, links} 121 var logitANode = nodes.find(d => d.logitToken == logitAStr) 122 var logitBNode = nodes.find(d => d.logitToken == logitBStr) 123 if (!logitANode || !logitBNode) return {nodes, links} 124 125 var virtualId = `virtual-diff-${logitAStr}
125-vs-${logitBStr}` 126 var diffNode = { 127 ...logitANode, 128 node_id: virtualId, 129 jsNodeId: virtualId, 130 feature: virtualId, 131 isJsVirtual: true, 132 logitToken: `${logitAStr} - ${logitBStr}`, 133 clerp: `Logit diff: ${logitAStr} - ${logitBStr}`, 134 } 135 nodes.push(diffNode) 136 137 var targetLinks = links.filter(d => d.target == logitANode.node_id || d.target == logitBNode.node_id) 138 d3.nestBy(targetLinks, d => d.source).map(sourceLinks => { 139 var linkA = sourceLinks.find(d => d.target == logitANode.node_id) 140 var linkB = sourceLinks.find(d => d.target == logitBNode.node_id) 141 142 links.push({ 143 source: sourceLinks[0].source, 144 target: diffNode.node_id, 145 weight: (linkA?.weight || 0) - (linkB?.weight || 0), 146 isJsVirtual: true 147 }) 148 }) 149 150 return {nodes, links} 151 } 152 153 // Decorates and mutates data.json 154 // - Adds pointers between node and links 155 // - Deletes very common features 156 // - Adds data.features and data.byStream 157 async function formatData(data, visState){ 158 var {metadata} = data 159 var {nodes, links} = addVirtualDiff(data) 160 161 var py_node_id_to_node = {} 162 var idToNode = {} 163 var maxLayer = d3.max(nodes.filter(d => d.feature_type != 'logit'), d => +d.layer) 164 nodes.forEach((d, i) => { 165 // To make hover state work across prompts, drop ctx from node id 166 d.featureId = `${d.layer}_${d.feature}` 167 168 d.active_feature_idx = d.feature 169 d.nodeIndex = i 170 171 if (d.feature_type == 'logit') d.layer = maxLayer + 1 172 173 // TODO: does this handle error nodes correctly? 174 if (d.feature_type == 'unexplored node' && !d.layer != 'E'){ 175 d.feature_type = 'cross layer transcoder' 176 } 177 178 // count from end to align last token on diff prompts 179 d.ctx_from_end = data.metadata.prompt_tokens.length - d.ctx_idx 180 181 // add clerp to embed and error nodes 182 if (d.feature_type.includes('error')){ 183 d.isError = true 184 185 if (!d.featureId.includes('__err_idx_')) d.featureId = d.featureId + '__err_idx_' + d.ctx_from_end 186 187 if (d.feature_type == 'mlp reconstruction error'){ 188 d.clerp = `Err: mlp â${util.ppToken(data.metadata.prompt_tokens[d.ctx_idx])}"` 189 } 190 } else if (d.feature_type == 'embedding'){ 191 d.clerp = `Emb: â${util.ppToken(data.metadata.prompt_tokens[d.ctx_idx])}"` 192 } 193 194 d.url = d.vis_link 195 d.isFeature = true 196 197 d.targetLinks = [] 198 d.sourceLinks = [] 199 200 // TODO: switch to featureIndex in graphgen 201 d.featureIndex = d.feature 202 203 d.nodeId = d.jsNodeId 204 if (d.feature_type == 'logit' && d.clerp) d.logitPct= +d.clerp.split('(p=')[1].split(')')[0] 205 idToNode[d.nodeId] = d 206 py_node_id_to_node[d.node_id] = d 207 208 if (d.feature_type == 'cross layer transcoder') d.clerp = "Feature " + d.featureIndex // override clerp for now 209 }) 210 211 // delete features that occur in than 2/3 of tokens 212 // TODO: more principled way of filtering them out â maybe by feature density? 213 var deletedFeatures = [] 214 var byFeatureId = d3.nestBy(nodes, d => d.featureId) 215 byFeatureId.forEach(feature => { 216 if (feature.length > metadata.prompt_tokens.length*2/3){ 217 deletedFeatures.push(feature) 218 feature.forEach(d => { 219 delete idToNode[d.nodeId] 220 delete py_node_id_to_node[d.node_id] 221 }) 222 } 223 }) 224 if (deletedFeatures.length) console.log({deletedFeatures}) 225 nodes = nodes.filter(d => idToNode[d.nodeId]) 226 nodes = d3.sort(nodes, d => +d.layer) 227 228 links = links.filter(d => py_node_id_to_node[d.source] && py_node_id_to_node[d.target]) 229 230 // connect links to nodes 231 links.forEach(link => { 232 link.sourceNode = py_node_id_to_node[link.source] 233 link.targetNode = py_node_id_to_node[link.target] 234 235 link.linkId = link.sourceNode.nodeId + '__' + link.targetNode.nodeId 236 237 link.sourceNode.targetLinks.push(link) 238 link.targetNode.sourceLinks.push(link) 239 link.absWeight = Math.abs(link.weight) 240 }) 241 links = d3.sort(links, d => d.absWeight) 242 243
244 nodes.forEach(d => { 245 d.inputAbsSum = d3.sum(d.sourceLinks, e => Math.abs(e.weight)) 246 d.sourceLinks.forEach(e => e.pctInput = e.weight/d.inputAbsSum) 247 d.inputError = d3.sum(d.sourceLinks.filter(e => e.sourceNode.isError), e => Math.abs(e.weight)) 248 d.pctInputError = d.inputError/d.inputAbsSum 249 }) 250 251 // convert layer/probe_location_idx to a streamIdx used to position nodes 252 var byStream = d3.nestBy(nodes, d => [d.layer, d.probe_location_idx] + '') 253 byStream = d3.sort(byStream, d => d[0].probe_location_idx) 254 byStream = d3.sort(byStream, d => d[0].layer == 'E' ? -1 : +d[0].layer) 255 byStream.forEach((stream, streamIdx) => { 256 stream.forEach(d => { 257 d.streamIdx = streamIdx 258 d.layerLocationLabel = layerLocationLabel(d.layer, d.probe_location_idx) 259 260 if (!visState.isHideLayer) d.streamIdx = isFinite(d.layer) ? +d.layer : 0 261 }) 262 }) 263 264 // add target_logit_effect__ columns for each logit 265 var logitNodeMap = new Map(nodes.filter(d => d.isLogit).map(d => [d.node_id, d.logitToken])) 266 nodes.forEach(node => { 267 node.targetLinks.forEach(link => { 268 if (!logitNodeMap.has(link.target)) return 269 node[`target_logit_effect__${logitNodeMap.get(link.target)}`] = link.weight 270 }) 271 }) 272 273 // add ppClerp 274 await Promise.all(nodes.map(async d => { 275 if (!d.clerp) d.clerp = '' 276 d.remoteClerp = '' 277 })) 278 279 // condense nodes into features, using last occurence of feature if necessary to point to a node 280 var features = d3.nestBy(nodes.filter(d => d.isFeature), d => d.featureId) 281 .map(d => ({ 282 featureId: d[0].featureId, 283 feature_type: d[0].feature_type, 284 clerp: d[0].clerp, 285 remoteClerp: d[0].remoteClerp, 286 layer: d[0].layer, 287 streamIdx: d[0].streamIdx, 288 probe_location_idx: d[0].probe_location_idx, 289 featureIndex: d[0].featureIndex, 290 top_logit_effects: d[0].top_logit_effects, 291 bottom_logit_effects: d[0].bottom_logit_effects, 292 top_embedding_effects: d[0].top_embedding_effects, 293 bottom_embedding_effects: d[0].bottom_embedding_effects, 294 url: d[0].url, 295 lastNodeId: d.at(-1).nodeId, 296 isLogit: d[0].isLogit, 297 isError: d[0].isError, 298 feature_type: d[0].feature_type, 299 })) 300 301 nodes.idToNode = idToNode 302 features.idToFeature = Object.fromEntries(features.map(d => [d.featureId, d])) 303 links.idToLink = Object.fromEntries(links.map(d => [d.linkId, d])) 304 305 Object.assign(data, {nodes, features, links, byStream}) 306 } 307 308 function initBcSync({visState, renderAll}){ 309 var bcStateSync = window.bcSync = window.bcSync || new BroadcastChannel('state-sync') 310 311 function broadcastState(){ 312 if (!visState.isSyncEnabled) return 313 bcStateSync.postMessage({ 314 pinnedIds: visState.pinnedIds, 315 hiddenIds: visState.hiddenIds, 316 clickedId: visState.clickedId, 317 hoveredId: visState.hoveredId, 318 pageUUID: visState.pageUUID, 319 isSyncEnabled: visState.isSyncEnabled, 320 }) 321 } 322 323 renderAll.pinnedIds.fns.push(ev => { if (!ev?.skipBroadcast) broadcastState() }) 324 renderAll.hiddenIds.fns.push(ev => { if (!ev?.skipBroadcast) broadcastState() }) 325 renderAll.clickedId.fns.push(ev => { if (!ev?.skipBroadcast) broadcastState() }) 326 renderAll.hoveredId.fns.push(ev => { if (!ev?.skipBroadcast) broadcastState() }) 327 328 bcStateSync.onmessage = ev => { 329 if (!visState.isSyncEnabled) return 330 if (visState.isSyncEnabled != ev.data.isSyncEnabled) return 331 if (ev.data.pageUUID == visState.pageUUID) return 332 333 if (JSON.stringify(visState.pinnedIds) != JSON.stringify(ev.data.pinnedIds)){ 334 visState.pinnedIds = ev.data.pinnedIds 335 renderAll.pinnedIds({skipBroadcast: true}) 336 } 337 338 if (JSON.stringify(visState.hiddenIds) != JSON.stringify(ev.data.hiddenIds)){ 339 visState.hiddenIds = ev.data.hiddenIds 340 renderAll.hiddenIds({skipBroadcast: true}) 341 } 342 343 if (visState.clickedId != ev.data.clickedId){ 344 visState.clickedId = ev.data.clickedId 345 renderAll.clickedId({skipBroadcast: true}) 346 } 347 348 if (visState.hoveredId != ev.data.hoveredId){ 349 visState.hoveredId = ev.data.hoveredId 350 renderAll.hoveredId({skipBroadcast: true}) 351 } 352 } 353 } 354 355 function addFeatureEvents(visState, renderAll) { 356 return function(selection) { 357 selection 358 .on('mouseover', (ev, d) => { 359 if (ev.shiftKey) return 360 if (visState.subgraph?.activeGrouping.isActive) return 361 ev.preventDefault() 362 hoverFeature(visState, renderAll, d) 363 }) 364 .on('mouseout', (ev, d) => { 365 if (ev.shiftKey) return 366 if (visState.subgraph?.activeGrouping.isActive) return 367 ev.preventDefault() 368 unHoverFeature(visState, renderAll) 369 }) 370 .on('click', (ev, d) => { 371 if (visState.subgraph?.activeGrouping.isActive) return
372 clickFeature(visState, renderAll, d, ev.metaKey || ev.ctrlKey) 373 }) 374 } 375 } 376 377 function hoverFeature(visState, renderAll, d) { 378 if (d.nodeId.includes('supernode-')) return 379 380 if (visState.hoveredId != d.featureId) { 381 visState.hoveredId = d.featureId 382 visState.hoveredCtxIdx = d.ctx_idx 383 renderAll.hoveredId() 384 } 385 } 386 387 function unHoverFeature(visState, renderAll) { 388 if (visState.hoveredId) { 389 visState.hoveredId = null 390 visState.hoveredCtxIdx = null 391 setTimeout(() => { 392 if (!visState.hoveredId) renderAll.hoveredId() 393 }) 394 } 395 } 396 function togglePinned(visState, renderAll, d) { 397 var index = visState.pinnedIds.indexOf(d.nodeId) 398 if (index == -1) { 399 visState.pinnedIds.push(d.nodeId) 400 } else { 401 visState.pinnedIds.splice(index, 1) 402 } 403 renderAll.pinnedIds() 404 } 405 406 function clickFeature(visState, renderAll, d, metaKey){ 407 console.log(d) 408 if (d.nodeId.includes('supernode-')) return 409 410 if (metaKey && visState.isEditMode) { 411 togglePinned(visState, renderAll, d) 412 } else { 413 if (visState.clickedId == d.nodeId) { 414 visState.clickedId = null 415 visState.clickedCtxIdx = null 416 } else { 417 visState.clickedId = d.nodeId 418 visState.clickedCtxIdx = d.ctx_idx 419 } 420 visState.hoveredId = null 421 visState.hoveredCtxIdx = null 422 renderAll.clickedId() 423 } 424 } 425 426 function showTooltip(ev, d) { 427 let tooltipSel = d3.select('.tooltip'), 428 x = ev.clientX, 429 y = ev.clientY, 430 bb = tooltipSel.node().getBoundingClientRect(), 431 left = d3.clamp(20, (x-bb.width/2), window.innerWidth - bb.width - 20), 432 top = innerHeight > y + 20 + bb.height ? y + 20 : y - bb.height - 20; 433 434 let tooltipHtml = !ev.metaKey ? (d.ppClerp || `F#${d.feature}`) : Object.keys(d) 435 .filter(str => typeof d[str] != 'object' && typeof d[str] != 'function' && !keysToSkip.has(str)) 436 .map(str => { 437 var val = d[str] 438 if (typeof val == 'number' && !Number.isInteger(val)) val = val.toFixed(6) 439 return `<div>${str}: <b>${val}</b></div>` 440 }) 441 .join('') 442 443 tooltipSel 444 .style('left', left +'px') 445 .style('top', top + 'px') 446 .html(tooltipHtml) 447 .classed('tooltip-hidden', false) 448 } 449 450 function addFeatureTooltip(selection){ 451 selection 452 .call(d3.attachTooltip, d3.select('.tooltip'), []) 453 .on('mouseover.tt', (ev, d) => { 454 var tooltipHtml = !ev.metaKey ? d.ppClerp : Object.keys(d) 455 .filter(str => typeof d[str] != 'object' && typeof d[str] != 'function' && !keysToSkip.has(str)) 456 .map(str => { 457 var val = d[str] 458 if (typeof val == 'number' && !Number.isInteger(val)) val = val.toFixed(6) 459 return `<div>${str}: <b>${val}</b></div>` 460 }) 461 .join('') 462 463 d3.select('.tooltip').html(tooltipHtml) 464 }) 465 } 466 467 function hideTooltip() { 468 d3.select('.tooltip').classed('tooltip-hidden', true); 469 } 470 471 function updateFeatureStyles(nodeSel){ 472 nodeSel.call(classAndRaise('hovered', e => e.featureId == visState.hoveredId)) 473 474 var pinnedIdSet = new Set(visState.pinnedIds) 475 nodeSel.call(classAndRaise('pinned', d => pinnedIdSet.has(d.nodeId))) 476 477 var hiddenIdSet = new Set(visState.hiddenIds) 478 nodeSel.call(classAndRaise('hidden', d => hiddenIdSet.has(d.featureId))) 479 480 if (nodeSel.datum().nodeId){ 481 nodeSel.call(classAndRaise('clicked', e => e.nodeId === visState.clickedId)) 482 } else { 483 nodeSel.call(classAndRaise('clicked', d => d.featureId == visState.clickedId)) 484 } 485 } 486 487 function classAndRaise(className, filterFn) { 488 return sel => { 489 sel 490 .classed(className, 0) 491 .filter(filterFn) 492 .classed(className, 1) 493 .raise() 494 } 495 } 496 497 var keysToSkip = new Set([ 498 'node_id', 'jsNodeId', 'nodeId', 'layerLocationLabel', 'remoteClerp', 'localClerp', 499 'tmpClickedTargetLink', 'tmpClickedLink', 'tmpClickedSourceLink', 500 'pos', 'xOffset', 'yOffset', 'sourceLinks', 'targetLinks', 'url', 'vis_link', 'run_idx', 501 'featureId', 'active_feature_idx', 'nodeIndex', 'isFeature', 'Distribution', 502 'clerp', 'ppClerp', 'is_target_logit', 'token_prob', 'reverse_ctx_idx', 'ctx_from_end', 'feature', 'logitToken', 503 'featureIndex', 'streamIdx', 'nodeColor', 'umap_enc_x', 'umap_enc_y', 'umap_dec_x', 'umap_dec_y', 'umap_concat_x', 'umap_concat_y', 504 ]) 505 506 507 function layerLocationLabel(layer, location) { 508 if (layer == 'E') return 'Emb' 509 if (layer == 'E1') return 'Lgt' 510 if (location === -1) return 'logit' 511 512 // TODO: is stream probe_location_idx no longer be saved out? 513 // NOTE: For now, location is literally ProbePointLocation 514 return `L${layer}` 515 } 516 517 var memoize = fn => { 518 var cache = new Map() 519 return (...args) => { 520 var key = JSON.stringify(args) 521 if (cache.has(key)) return cache.get(key) 522 var result = fn(...args) 523 cache.set(key, result) 524 return result 525 } 526 } 527 528 var bgColorToTextColor = memoize((backgroundColor, light='#fff', dark='#000') => { 529 if (!backgroundColor) return '' 530 var hsl = d3.hsl(backgroundColor) 531 return hsl.l > 0.55 ? dark : light 532 }) 533 534 // gradient for hover && pinned state 535 function addPinnedClickedGradient(svg){ 536 svg.append('defs').html(` 537 <linearGradient id='pinned-clicked-gradient' x1='0' x2='2' gradientUnits='userSpaceOnUse' spreadMethod='repeat'> 538 <stop offset='0' stop-color='#f0f' /> 539 <stop offset='70%' stop-color='#f0f' /> 540 <stop offset='71%' stop-color='#000' /> 541 <stop offset='100%' stop-color='#000' /> 542 </linearGradient> 543 `) 544 } 545 546 function renderFeatureRow(sel, visState, renderAll, linkKey='tmpClickedLink'){ 547 sel.st({ 548 background: d => d[linkKey]?.tmpColor, 549 color: d => bgColorToTextColor(d[linkKey]?.tmpColor, '#eee', '#555'), 550 }) 551 552 // add events in a timeout to avoid connection clicks leading to an instant hover 553 setTimeout(() =>
553 sel.call(addFeatureEvents(visState, renderAll)), 16) 554 555 let featureIconSel = sel.append('svg') 556 .at({width: 10, height: 10}) 557 558 let featureIcon = featureIconSel.append('g') 559 560 featureIcon.append('g.default-icon').append('text') 561 .text(d => featureTypeToText(d.feature_type)) 562 .at({ 563 fontSize: 9, 564 textAnchor: 'middle', 565 dominantBaseline: 'central', 566 dx: 5, 567 dy: 4, 568 }) 569 .at({fill: d => d[linkKey]?.tmpColor}) 570 571 572 sel.append('div.label') 573 .text(d => d.ppClerp) 574 .at({ title: d => d.ppClerp }) 575 576 sel 577 .filter(d => d[linkKey] && d[linkKey].tmpClickedCtxOffset != 0) 578 .append('div.ctx-offset') 579 .text(d => d[linkKey].tmpClickedCtxOffset < 0 ? 'â' : 'â') 580 581 if (!visState.isHideLayer){ 582 sel.append('div.layer') 583 .text(d => d.layerLocationLabel ?? layerLocationLabel(d.layer, d.probe_location_idx)); 584 } 585 586 sel.append('div.weight') 587 .text(d => d[linkKey] ? d3.format('+.3f')(d[linkKey].pctInput) : '') 588 } 589 590 function featureTypeToText(type){ 591 if (type == 'logit') return 'â ' 592 if (type == 'embedding') return 'â ' 593 if (type === 'mlp reconstruction error') return 'â' 594 return 'â' 595 596 } 597 598 599 return {
600 loadDatapath, 601 formatData, 602 initBcSync, 603 addFeatureEvents, 604 hoverFeature, 605 unHoverFeature, 606 clickFeature, 607 togglePinned, 608 layerLocationLabel, 609 keysToSkip, 610 addFeatureTooltip, 611 showTooltip, 612 hideTooltip, 613 updateFeatureStyles, 614 memoize, 615 bgColorToTextColor, 616 addPinnedClickedGradient, 617 renderFeatureRow, 618 saveHClerpsToLocalStorage, 619 getHClerpsFromLocalStorage, 620 hClerpUpdateFn, 621 deDupHClerps, 622 tabifyHClerps, 623 featureTypeToText, 624 } 625})() 626 627window.init?.()
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.