stratagate-dsh 0.2.25 → 0.2.26

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.2.26 - 2026-08-25
4
+
5
+ - Size Knowledge Graph nodes by their long-term importance using supporting Events, active relationships, sustained recent activity, and current-workspace affinity.
6
+ - Keep node sizing stable across search and type filters by deriving importance from the complete graph snapshot.
7
+ - Preserve selection as an independent outline and glow treatment instead of temporarily enlarging the selected node.
8
+
3
9
  ## 0.2.25 - 2026-08-24
4
10
 
5
11
  - Send every sealed conversation Block through its current decayed L0–L5 representation instead of a fixed L0/L1/L2 checkpoint.
package/dist/client.js CHANGED
@@ -207,7 +207,88 @@ window.__ModuleLoader__.load({
207
207
  h('div', { className: 'sg-node-bubble-foot' }, h('span', null, eventCount + ' 条相关事件'), h('button', { type: 'button', onClick: onViewDetails }, '查看详情 →')))
208
208
  }
209
209
 
210
- function GraphCanvas({ nodes, edges, selectedId, onSelect, showSummary = false, onViewDetails }) {
210
+ const GRAPH_NODE_RADIUS = { peripheral: 30, normal: 38, important: 46, core: 54 }
211
+ const GRAPH_IMPORTANCE_TEXT = { peripheral: '边缘节点', normal: '普通节点', important: '重要节点', core: '核心节点' }
212
+
213
+ function graphNameKey(value) {
214
+ return String(value || '').toLocaleLowerCase().replace(/[^\p{L}\p{N}]+/gu, '')
215
+ }
216
+
217
+ function graphTimestamp(value) {
218
+ const timestamp = Date.parse(String(value || ''))
219
+ return Number.isFinite(timestamp) ? timestamp : 0
220
+ }
221
+
222
+ // Importance is derived from the complete persisted graph snapshot. The caller
223
+ // deliberately computes this before search/type filters are applied, so a view
224
+ // change never changes the meaning of a node's size.
225
+ function graphNodeImportance(nodes, edges, project) {
226
+ const activeEdges = edges.filter((edge) => edge.status === 'active')
227
+ const activeSupportingEvents = (node) => (node.supportingEvents || []).filter((event) => event.status !== 'forgotten' && event.status !== 'archived')
228
+ const relationCounts = new Map(nodes.map((node) => [node.id, 0]))
229
+ const neighbors = new Map(nodes.map((node) => [node.id, new Set()]))
230
+ for (const edge of activeEdges) {
231
+ relationCounts.set(edge.fromNodeId, (relationCounts.get(edge.fromNodeId) || 0) + 1)
232
+ relationCounts.set(edge.toNodeId, (relationCounts.get(edge.toNodeId) || 0) + 1)
233
+ neighbors.get(edge.fromNodeId)?.add(edge.toNodeId)
234
+ neighbors.get(edge.toNodeId)?.add(edge.fromNodeId)
235
+ }
236
+
237
+ const projectKey = graphNameKey(project)
238
+ const directWorkspaceNodes = new Set(nodes.filter((node) => projectKey && [node.name, ...(node.aliases || [])].some((name) => {
239
+ const key = graphNameKey(name)
240
+ return key === projectKey || (key.length >= 5 && projectKey.length >= 5 && (key.includes(projectKey) || projectKey.includes(key)))
241
+ })).map((node) => node.id))
242
+ const workspaceAffinity = new Map(nodes.map((node) => [node.id, directWorkspaceNodes.has(node.id) ? 1 : 0]))
243
+ for (const nodeId of directWorkspaceNodes) {
244
+ for (const neighborId of neighbors.get(nodeId) || []) {
245
+ workspaceAffinity.set(neighborId, Math.max(workspaceAffinity.get(neighborId) || 0, .55))
246
+ for (const secondHopId of neighbors.get(neighborId) || []) {
247
+ workspaceAffinity.set(secondHopId, Math.max(workspaceAffinity.get(secondHopId) || 0, .25))
248
+ }
249
+ }
250
+ }
251
+
252
+ const supportCounts = new Map(nodes.map((node) => {
253
+ const supportingEvents = activeSupportingEvents(node)
254
+ return [node.id, supportingEvents.length ? new Set(supportingEvents.map((event) => event.id)).size : new Set(node.sourceEventIds || []).size]
255
+ }))
256
+ const maxSupport = Math.max(1, ...supportCounts.values())
257
+ const maxRelations = Math.max(1, ...relationCounts.values())
258
+ let latestActivity = 0
259
+ for (const node of nodes) {
260
+ latestActivity = Math.max(latestActivity, graphTimestamp(node.updatedAt))
261
+ for (const event of activeSupportingEvents(node)) latestActivity = Math.max(latestActivity, graphTimestamp(event.updatedAt || event.createdAt))
262
+ }
263
+ for (const edge of activeEdges) latestActivity = Math.max(latestActivity, graphTimestamp(edge.updatedAt))
264
+ const halfLife = 90 * 24 * 60 * 60 * 1000
265
+ const recency = (timestamp) => timestamp && latestActivity ? Math.exp(-Math.max(0, latestActivity - timestamp) / halfLife) : 0
266
+
267
+ const scored = nodes.map((node) => {
268
+ const supportingEvents = activeSupportingEvents(node)
269
+ const sustainedMentions = supportingEvents.map((event) => recency(graphTimestamp(event.updatedAt || event.createdAt))).sort((left, right) => right - left).slice(0, 3)
270
+ const recentScore = .25 * recency(graphTimestamp(node.updatedAt)) + .75 * sustainedMentions.reduce((sum, value) => sum + value, 0) / 3
271
+ const supportScore = Math.log1p(supportCounts.get(node.id) || 0) / Math.log1p(maxSupport)
272
+ const relationScore = Math.log1p(relationCounts.get(node.id) || 0) / Math.log1p(maxRelations)
273
+ const score = .42 * supportScore + .28 * relationScore + .15 * recentScore + .15 * (workspaceAffinity.get(node.id) || 0)
274
+ return { id: node.id, score }
275
+ })
276
+ const ordered = scored.map(({ score }) => score).sort((left, right) => left - right)
277
+ const quantile = (ratio) => ordered[Math.ceil(Math.max(0, ordered.length - 1) * ratio)] || 0
278
+ const spread = (ordered[ordered.length - 1] || 0) - (ordered[0] || 0)
279
+ const thresholds = { normal: quantile(.25), important: quantile(.6), core: quantile(.85) }
280
+ return new Map(scored.map(({ id, score }) => {
281
+ let tier
282
+ if (spread < .02) tier = score >= .65 ? 'core' : score >= .38 ? 'important' : 'normal'
283
+ else if (score >= thresholds.core) tier = 'core'
284
+ else if (score >= thresholds.important) tier = 'important'
285
+ else if (score >= thresholds.normal) tier = 'normal'
286
+ else tier = 'peripheral'
287
+ return [id, { score, tier, radius: GRAPH_NODE_RADIUS[tier] }]
288
+ }))
289
+ }
290
+
291
+ function GraphCanvas({ nodes, edges, importance, selectedId, onSelect, showSummary = false, onViewDetails }) {
211
292
  if (!nodes.length) return h(Empty, { title: '知识图谱正在形成', copy: 'Event 会在后台分批投影为节点与关系。' })
212
293
  const width = 760; const height = 560; const cx = width / 2; const cy = height / 2
213
294
  const placed = nodes.slice(0, 40).map((node, index) => {
@@ -230,8 +311,9 @@ window.__ModuleLoader__.load({
230
311
  }),
231
312
  placed.map(({ node, x, y }) => {
232
313
  const meta = NODE_META[node.type] || ['实体', '#64748b', '•']; const selected = node.id === selectedId
233
- return h('g', { key: node.id, className: 'sg-graph-node ' + (selected ? 'selected' : ''), onClick: () => onSelect(node.id), onKeyDown: (event) => { if (event.key === 'Enter' || event.key === ' ') { event.preventDefault(); onSelect(node.id) } }, role: 'button', tabIndex: 0, 'aria-label': node.name + ',' + meta[0] },
234
- h('circle', { cx: x, cy: y, r: selected ? 47 : 42, fill: meta[1], fillOpacity: '.28', stroke: meta[1], strokeWidth: selected ? '3' : '1.5' }),
314
+ const visualImportance = importance.get(node.id) || { tier: 'normal', radius: GRAPH_NODE_RADIUS.normal }
315
+ return h('g', { key: node.id, className: 'sg-graph-node ' + (selected ? 'selected' : ''), 'data-importance': visualImportance.tier, onClick: () => onSelect(node.id), onKeyDown: (event) => { if (event.key === 'Enter' || event.key === ' ') { event.preventDefault(); onSelect(node.id) } }, role: 'button', tabIndex: 0, 'aria-label': node.name + '' + meta[0] + '' + GRAPH_IMPORTANCE_TEXT[visualImportance.tier] },
316
+ h('circle', { cx: x, cy: y, r: visualImportance.radius, fill: meta[1], fillOpacity: '.28', stroke: meta[1], strokeWidth: selected ? '3' : '1.5' }),
235
317
  h('text', { x, y: y - 7, textAnchor: 'middle', className: 'sg-node-name' }, node.name.slice(0, 14)),
236
318
  h('text', { x, y: y + 14, textAnchor: 'middle', className: 'sg-node-type' }, meta[0]))
237
319
  })),
@@ -325,6 +407,7 @@ window.__ModuleLoader__.load({
325
407
  const [filtersOpen, setFiltersOpen] = React.useState(false)
326
408
  const [fullScreen, setFullScreen] = React.useState(false)
327
409
  const nodes = graph.nodes || []; const edges = graph.edges || []; const normalized = query.trim().toLocaleLowerCase()
410
+ const nodeImportance = React.useMemo(() => graphNodeImportance(nodes, edges, project), [nodes, edges, project])
328
411
  React.useEffect(() => {
329
412
  if (!fullScreen) return undefined
330
413
  const previous = document.body.style.overflow
@@ -362,7 +445,7 @@ window.__ModuleLoader__.load({
362
445
  filtersOpen ? filterControls : null,
363
446
  mode === 'graph'
364
447
  ? h('div', { className: 'sg-long-layout ' + (fullScreen ? '' : 'sg-summary-layout') },
365
- h(GraphCanvas, { nodes: visibleNodes, edges, selectedId: selectedNodeId, onSelect: setSelectedNodeId, showSummary: !fullScreen, onViewDetails: openExplorer }),
448
+ h(GraphCanvas, { nodes: visibleNodes, edges, importance: nodeImportance, selectedId: selectedNodeId, onSelect: setSelectedNodeId, showSummary: !fullScreen, onViewDetails: openExplorer }),
366
449
  fullScreen ? h(NodeDetailPanel, { node: nodes.find((node) => node.id === selectedNodeId), nodes, edges, events, onEvent: selectEvent }) : null)
367
450
  : h('div', { className: 'sg-long-layout ' + (fullScreen ? 'sg-timeline-layout' : 'sg-summary-layout') },
368
451
  h(TimelineList, { events, nodes, query, filters: eventFilters, onSelect: setSelectedEventId, selectedId: selectedEventId, floating: !fullScreen, onNode: selectNode, openSource: openEvent }),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "stratagate-dsh",
3
- "version": "0.2.25",
3
+ "version": "0.2.26",
4
4
  "description": "Automatic local-first cross-session memory for DeepSeek Harness with source-traceable recall",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",