dsh-prime-memory 0.26.5-beta.0 → 0.26.5

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.
@@ -927,6 +927,13 @@ export interface UiRecord {
927
927
  * 只在 `dsh-memory/session-recalled` 这条路径上有意义;缺省 = 无此概念(旧服务端)。
928
928
  */
929
929
  superseded?: boolean;
930
+ /**
931
+ * 路归因:hybrid 融合时这条命中来自哪几路(`'fts'|'vec'|'graph'|'recency'`)。
932
+ * 只在 `dsh-memory/session-recalled` 路径上有意义;缺省 = 无从归因
933
+ * (单路检索策略/旧文件条目/旧服务端),展示层不渲染徽标。纯投影字段,
934
+ * 存储侧落 `session-recalled.json` 的 lanes 键(读时拆管道串)。
935
+ */
936
+ lanes?: string[];
930
937
  /**
931
938
  * 成熟度(G3 读时派生):priority + 被用次数 + 老化权重合成。
932
939
  * 可选字段——旧 client 忽略=向后兼容;不参与存储/快照(纯投影)。
@@ -1228,6 +1235,69 @@ export interface GraphNodeGetResponse {
1228
1235
  /** 与该节点相连的 active 边。 */
1229
1236
  edges: GraphEdge[];
1230
1237
  }
1238
+ /** dsh-memory/graph-preview(面板图谱预览:缺省返回推荐种子榜,seedId 给定展开该节点邻域)。
1239
+ * 候选只含 active|disputed 节点×active 边(与检索同判据);空图/未启用返全零结构不抛。 */
1240
+ export interface GraphPreviewRequest {
1241
+ /** 给定即以该节点为唯一中心(悬挂 id 返空 seeds,不解析不抛——graph-node-get 同款宽容)。 */
1242
+ seedId?: string;
1243
+ /** 'full' = 响应附全图轻量块(mode:'full' 与大图星图共用同端点,一次 loadGraph 全供)。
1244
+ * 非 'full' 一律按缺省——无 mode 响应形状与旧版逐字一致(可选键缺席=零漂移)。 */
1245
+ mode?: 'full';
1246
+ }
1247
+ export interface GraphPreviewResponse {
1248
+ seeds: Array<{
1249
+ node: {
1250
+ id: string;
1251
+ name: string;
1252
+ type: string;
1253
+ degree: number;
1254
+ causalDegree: number;
1255
+ };
1256
+ /** 因果边优先→邻居度数降序→updatedAt 新先;长度 ≤ GRAPH_PREVIEW_NEIGHBORS。 */
1257
+ neighbors: Array<{
1258
+ id: string;
1259
+ name: string;
1260
+ type: string;
1261
+ relation: string;
1262
+ causal: boolean;
1263
+ }>;
1264
+ truncated: boolean;
1265
+ }>;
1266
+ stats: {
1267
+ nodes: number;
1268
+ edges: number;
1269
+ causalEdges: number;
1270
+ /** 未定投影 job 数(pending/running/failed 退避中),>0 → UI 显"构建中 x/y"(x=jobsTotal-pendingJobs)。 */
1271
+ pendingJobs: number;
1272
+ /** 全量 job 数(含各状态),"构建中 x/y"的分母——名字即分母,不拿 done 冒充分子。 */
1273
+ jobsTotal: number;
1274
+ /** 永久失败作业数(dead),可选(零漂移先例);>0 → UI 拆成"完成/失败/排队"三档显示。 */
1275
+ deadJobs?: number;
1276
+ };
1277
+ /** mode==='full' 才出现;全图星图轻量块(可见节点×active 边,截断标记 truncated)。
1278
+ * 不带此键时响应与旧形状逐字节一致——消费方按需读 `res.full?`。 */
1279
+ full?: {
1280
+ nodes: Array<{
1281
+ id: string;
1282
+ name: string;
1283
+ type: string;
1284
+ }>;
1285
+ edges: Array<{
1286
+ from: string;
1287
+ to: string;
1288
+ relation: string;
1289
+ causal: boolean;
1290
+ }>;
1291
+ truncated: boolean;
1292
+ };
1293
+ }
1294
+ /** dsh-memory/graph-cleanup-dead(清理历史失败作业:删除 status='dead' 的行,不影响已重入队的记录)。 */
1295
+ export interface GraphCleanupDeadRequest {
1296
+ olderThanMs?: number;
1297
+ }
1298
+ export interface GraphCleanupDeadResponse {
1299
+ removed: number;
1300
+ }
1231
1301
  /** dsh-memory/scenes(两族拼接的混合视图)。 */
1232
1302
  export interface ScenesResponse {
1233
1303
  items: Array<{
@@ -1778,6 +1848,8 @@ export interface DshMemoryRequestMap {
1778
1848
  'dsh-memory/conflicts-rejected': ConflictRejectedRequest;
1779
1849
  'dsh-memory/graph-search': GraphSearchRequest;
1780
1850
  'dsh-memory/graph-node-get': GraphNodeGetRequest;
1851
+ 'dsh-memory/graph-preview': GraphPreviewRequest;
1852
+ 'dsh-memory/graph-cleanup-dead': GraphCleanupDeadRequest;
1781
1853
  'dsh-memory/scenes': Record<string, never>;
1782
1854
  'dsh-memory/persona': Record<string, never>;
1783
1855
  'dsh-memory/log-tail': LogTailRequest;
@@ -1843,6 +1915,8 @@ export interface DshMemoryResponseMap {
1843
1915
  'dsh-memory/conflicts-rejected': ConflictRejectedResponse;
1844
1916
  'dsh-memory/graph-search': GraphSearchResponse;
1845
1917
  'dsh-memory/graph-node-get': GraphNodeGetResponse;
1918
+ 'dsh-memory/graph-preview': GraphPreviewResponse;
1919
+ 'dsh-memory/graph-cleanup-dead': GraphCleanupDeadResponse;
1846
1920
  'dsh-memory/scenes': ScenesResponse;
1847
1921
  'dsh-memory/persona': PersonaResponse;
1848
1922
  'dsh-memory/log-tail': LogTailResponse;
@@ -13,8 +13,10 @@ export declare const GRAPH_NODE_TYPE_SET: ReadonlySet<string>;
13
13
  /** 记录生命周期四态(节点/fact/边通用)。 */
14
14
  export declare const GRAPH_STATUSES: readonly GraphRecordStatus[];
15
15
  export declare const GRAPH_STATUS_SET: ReadonlySet<string>;
16
+ /** 因果边四词单源(投影提示词枚举、校验与面板预览加权共用——防两处词表漂移)。 */
17
+ export declare const GRAPH_CAUSAL_RELATIONS: readonly ["导致", "因为", "修复了", "取代"];
16
18
  /** 规范中文关系词(提示词给枚举;校验不做白名单——关系是开放词表,只做长度钳制)。
17
- * 后四个为因果边(导致/因为/修复了/取代)——让"前因后果"跨记录成链;仅当 graph.enabled 时
19
+ * 尾部即因果集 GRAPH_CAUSAL_RELATIONS——让"前因后果"跨记录成链;仅当 graph.enabled 时
18
20
  * 才参与投影(整块图谱默认关,不碰非图谱用户的蒸馏产物)。 */
19
21
  export declare const GRAPH_RELATION_WORDS: readonly ["使用", "属于", "创建", "参与", "贡献", "依赖", "位于", "相关", "导致", "因为", "修复了", "取代"];
20
22
  /** 提案字段上限(校验侧截断/丢弃与提示词内插共用同一组数字)。 */
@@ -4,10 +4,12 @@ export const GRAPH_NODE_TYPE_SET = new Set(GRAPH_NODE_TYPES);
4
4
  /** 记录生命周期四态(节点/fact/边通用)。 */
5
5
  export const GRAPH_STATUSES = ['active', 'superseded', 'disputed', 'archived'];
6
6
  export const GRAPH_STATUS_SET = new Set(GRAPH_STATUSES);
7
+ /** 因果边四词单源(投影提示词枚举、校验与面板预览加权共用——防两处词表漂移)。 */
8
+ export const GRAPH_CAUSAL_RELATIONS = ['导致', '因为', '修复了', '取代'];
7
9
  /** 规范中文关系词(提示词给枚举;校验不做白名单——关系是开放词表,只做长度钳制)。
8
- * 后四个为因果边(导致/因为/修复了/取代)——让"前因后果"跨记录成链;仅当 graph.enabled 时
10
+ * 尾部即因果集 GRAPH_CAUSAL_RELATIONS——让"前因后果"跨记录成链;仅当 graph.enabled 时
9
11
  * 才参与投影(整块图谱默认关,不碰非图谱用户的蒸馏产物)。 */
10
- export const GRAPH_RELATION_WORDS = ['使用', '属于', '创建', '参与', '贡献', '依赖', '位于', '相关', '导致', '因为', '修复了', '取代'];
12
+ export const GRAPH_RELATION_WORDS = ['使用', '属于', '创建', '参与', '贡献', '依赖', '位于', '相关', ...GRAPH_CAUSAL_RELATIONS];
11
13
  /** 提案字段上限(校验侧截断/丢弃与提示词内插共用同一组数字)。 */
12
14
  export const GRAPH_CAP = {
13
15
  /** 提案内临时引用(ref)长度。 */
@@ -0,0 +1,92 @@
1
+ import type { GraphEdge, GraphNode, GraphNodeType } from './types.js';
2
+ export interface GraphSeedNode {
3
+ id: string;
4
+ name: string;
5
+ type: GraphNodeType;
6
+ /** active 边双向计数(两端都可见才计入,与 degreeMaps 同口径)。 */
7
+ degree: number;
8
+ /** 其中因果边(constraints.GRAPH_CAUSAL_RELATIONS)计数。 */
9
+ causalDegree: number;
10
+ }
11
+ export interface GraphSeedNeighbor {
12
+ id: string;
13
+ name: string;
14
+ type: GraphNodeType;
15
+ /** 该条边的关系词(平行多边各占一条,标注各画各的)。 */
16
+ relation: string;
17
+ causal: boolean;
18
+ }
19
+ export interface GraphSeedView {
20
+ node: GraphSeedNode;
21
+ /** 因果边优先→邻居度数降序→updatedAt 新先→id 升序;长度 ≤ neighborLimit。 */
22
+ neighbors: GraphSeedNeighbor[];
23
+ /** 邻居超上限被截断的标记(UI 画 "+n" 角标)。 */
24
+ truncated: boolean;
25
+ }
26
+ export interface GraphPreviewStats {
27
+ nodes: number;
28
+ edges: number;
29
+ causalEdges: number;
30
+ /** 未定投影 job 数(pending/running/failed),>0 → UI "构建中 x/y"。 */
31
+ pendingJobs: number;
32
+ /** 全量 job 数(分母)。 */
33
+ jobsTotal: number;
34
+ /** 永久失败(dead)作业数,可选(零漂移先例);>0 → UI 拆三档。 */
35
+ deadJobs?: number;
36
+ }
37
+ export interface GraphPreviewResult {
38
+ seeds: GraphSeedView[];
39
+ stats: GraphPreviewStats;
40
+ }
41
+ export interface PreviewOpts {
42
+ /** 给定即只展开该节点为唯一中心(悬挂/不可见 → 空 seeds,不抛)。 */
43
+ seedId?: string;
44
+ seedLimit?: number;
45
+ neighborLimit?: number;
46
+ pendingJobs?: number;
47
+ jobsTotal?: number;
48
+ deadJobs?: number;
49
+ }
50
+ /** S-2:邻接统计——active 边且两端都可见才计度(双向)。 */
51
+ export interface DegreeMaps {
52
+ degree: Map<string, number>;
53
+ causal: Map<string, number>;
54
+ }
55
+ export declare function degreeMaps(nodes: readonly GraphNode[], edges: readonly GraphEdge[]): DegreeMaps;
56
+ /**
57
+ * S-3/S-4:预览主函数。
58
+ * - seedId 视图:直接展开该节点(跳过挑选);不可见/悬挂 → 空 seeds。
59
+ * - 榜单视图:score = degree + causal×WEIGHT;同分链 causal→sources→updatedAt→id↑;
60
+ * 候选落入已选种子的"自身∪邻居"覆盖集则跳过(相邻合并,画布片不重复);
61
+ * 孤立节点(度 0)不入榜——画布只有中心一个点没有信息量。
62
+ */
63
+ export declare function buildGraphPreview(nodes: readonly GraphNode[], edges: readonly GraphEdge[], opts?: PreviewOpts): GraphPreviewResult;
64
+ /** 全图节点轻量项(星图渲染只需这三字段,facts/sources 不下发——带宽与隐私面同步收紧)。 */
65
+ export interface FullViewNode {
66
+ id: string;
67
+ name: string;
68
+ type: GraphNodeType;
69
+ }
70
+ /** 全图边轻量项(causal 由单源词表判出,渲染侧不再抄词表)。 */
71
+ export interface FullViewEdge {
72
+ from: string;
73
+ to: string;
74
+ relation: string;
75
+ causal: boolean;
76
+ }
77
+ export interface GraphFullView {
78
+ nodes: FullViewNode[];
79
+ edges: FullViewEdge[];
80
+ /** 任一维触顶即 true(截断标记而非硬拒;渲染侧可据此提示"仅画部分")。 */
81
+ truncated: boolean;
82
+ }
83
+ export interface FullViewCaps {
84
+ nodeCap?: number;
85
+ edgeCap?: number;
86
+ }
87
+ /**
88
+ * 全图星图数据块(纯函数,不查库):可见节点轻量投影 × active 边,
89
+ * 双上限截断。**顺序纪律**:节点按入参序截断(调用方 loadGraph 的行序),
90
+ * 悬挂边(端点之一被截掉/不可见)在截断前剔除——星图上不画悬空线。
91
+ */
92
+ export declare function buildFullView(nodes: readonly GraphNode[], edges: readonly GraphEdge[], caps?: FullViewCaps): GraphFullView;
@@ -0,0 +1,186 @@
1
+ /**
2
+ * 面板图谱预览(读路径纯函数):种子推荐榜 + 指定节点邻域展开。
3
+ *
4
+ * 定位:图谱投影三表的"值得画哪些片"视图筛选,不查库、不落库、无 I/O——
5
+ * job 计数(pendingJobs/jobsTotal)由存储缝(graph-store.buildPreview)传入。
6
+ * 起点推荐有实证底座(2026-10-20 装机审计):关系全落规范词表、因果边 ~24%,
7
+ * 故 score 以 active 度数为主键、因果边加权——纯 top 度会漏"因→果"链。
8
+ *
9
+ * 判据与检索同源(search.ts):候选 = active|disputed 节点 × active 边;
10
+ * 确定性 = 同输入两次调用逐位一致(同分裁决链落到 id 升序兜底)。
11
+ */
12
+ import { GRAPH_CAUSAL_RELATIONS } from './constraints.js';
13
+ import { GRAPH_PREVIEW_CAUSAL_WEIGHT, GRAPH_PREVIEW_FULL_EDGES, GRAPH_PREVIEW_FULL_NODES, GRAPH_PREVIEW_NEIGHBORS, GRAPH_PREVIEW_SEEDS, } from './types.js';
14
+ const CAUSAL_SET = new Set(GRAPH_CAUSAL_RELATIONS);
15
+ /** 预览唯一判定的节点可见性(与 searchGraphNodes 候选集同式)。 */
16
+ function visible(n) {
17
+ return n.status === 'active' || n.status === 'disputed';
18
+ }
19
+ export function degreeMaps(nodes, edges) {
20
+ const candidate = new Set(nodes.filter(visible).map((n) => n.id));
21
+ const degree = new Map();
22
+ const causal = new Map();
23
+ for (const e of edges) {
24
+ if (e.status !== 'active')
25
+ continue;
26
+ if (!candidate.has(e.fromNodeId) || !candidate.has(e.toNodeId))
27
+ continue;
28
+ const isCausal = CAUSAL_SET.has(e.relation);
29
+ for (const id of [e.fromNodeId, e.toNodeId]) {
30
+ degree.set(id, (degree.get(id) ?? 0) + 1);
31
+ if (isCausal)
32
+ causal.set(id, (causal.get(id) ?? 0) + 1);
33
+ }
34
+ }
35
+ return { degree, causal };
36
+ }
37
+ /** 种子卡装配:邻居条目 + 因果优先排序 + 截断。 */
38
+ function seedView(center, edges, byId, dm, neighborLimit) {
39
+ const list = [];
40
+ for (const e of edges) {
41
+ if (e.status !== 'active')
42
+ continue;
43
+ let otherId;
44
+ if (e.fromNodeId === center.id)
45
+ otherId = e.toNodeId;
46
+ else if (e.toNodeId === center.id)
47
+ otherId = e.fromNodeId;
48
+ else
49
+ continue;
50
+ const o = byId.get(otherId);
51
+ if (!o || !visible(o))
52
+ continue;
53
+ list.push({
54
+ id: o.id,
55
+ name: o.name,
56
+ type: o.type,
57
+ relation: e.relation,
58
+ causal: CAUSAL_SET.has(e.relation),
59
+ _deg: dm.degree.get(o.id) ?? 0,
60
+ _upd: o.updatedAt,
61
+ });
62
+ }
63
+ list.sort((a, b) => {
64
+ if (a.causal !== b.causal)
65
+ return a.causal ? -1 : 1;
66
+ if (a._deg !== b._deg)
67
+ return b._deg - a._deg;
68
+ if (a._upd !== b._upd)
69
+ return a._upd < b._upd ? -1 : 1; // updatedAt 新(大)先
70
+ return a.id < b.id ? -1 : 1;
71
+ });
72
+ const truncated = list.length > neighborLimit;
73
+ return {
74
+ node: {
75
+ id: center.id,
76
+ name: center.name,
77
+ type: center.type,
78
+ degree: dm.degree.get(center.id) ?? 0,
79
+ causalDegree: dm.causal.get(center.id) ?? 0,
80
+ },
81
+ neighbors: list.slice(0, neighborLimit).map(({ _deg, _upd, ...view }) => view),
82
+ truncated,
83
+ };
84
+ }
85
+ /**
86
+ * S-3/S-4:预览主函数。
87
+ * - seedId 视图:直接展开该节点(跳过挑选);不可见/悬挂 → 空 seeds。
88
+ * - 榜单视图:score = degree + causal×WEIGHT;同分链 causal→sources→updatedAt→id↑;
89
+ * 候选落入已选种子的"自身∪邻居"覆盖集则跳过(相邻合并,画布片不重复);
90
+ * 孤立节点(度 0)不入榜——画布只有中心一个点没有信息量。
91
+ */
92
+ export function buildGraphPreview(nodes, edges, opts = {}) {
93
+ const byId = new Map(nodes.map((n) => [n.id, n]));
94
+ const dm = degreeMaps(nodes, edges);
95
+ const visNodes = nodes.filter(visible);
96
+ const visEdges = edges.filter((e) => e.status === 'active');
97
+ const stats = {
98
+ nodes: visNodes.length,
99
+ edges: visEdges.length,
100
+ causalEdges: visEdges.filter((e) => CAUSAL_SET.has(e.relation)).length,
101
+ pendingJobs: opts.pendingJobs ?? 0,
102
+ jobsTotal: opts.jobsTotal ?? 0,
103
+ ...(opts.deadJobs != null && opts.deadJobs > 0 ? { deadJobs: opts.deadJobs } : {}),
104
+ };
105
+ const seedLimit = Math.min(Math.max(Math.floor(opts.seedLimit ?? GRAPH_PREVIEW_SEEDS), 1), 20);
106
+ const neighborLimit = Math.min(Math.max(Math.floor(opts.neighborLimit ?? GRAPH_PREVIEW_NEIGHBORS), 1), 40);
107
+ if (opts.seedId !== undefined) {
108
+ const target = byId.get(opts.seedId);
109
+ if (!target || !visible(target))
110
+ return { seeds: [], stats };
111
+ return { seeds: [seedView(target, edges, byId, dm, neighborLimit)], stats };
112
+ }
113
+ const scored = visNodes
114
+ .map((n) => ({
115
+ n,
116
+ score: (dm.degree.get(n.id) ?? 0) + (dm.causal.get(n.id) ?? 0) * GRAPH_PREVIEW_CAUSAL_WEIGHT,
117
+ }))
118
+ .sort((a, b) => {
119
+ if (a.score !== b.score)
120
+ return b.score - a.score;
121
+ const ca = dm.causal.get(a.n.id) ?? 0;
122
+ const cb = dm.causal.get(b.n.id) ?? 0;
123
+ if (ca !== cb)
124
+ return cb - ca;
125
+ const sa = a.n.sourceRecordIds.length;
126
+ const sb = b.n.sourceRecordIds.length;
127
+ if (sa !== sb)
128
+ return sb - sa;
129
+ if (a.n.updatedAt !== b.n.updatedAt)
130
+ return a.n.updatedAt < b.n.updatedAt ? 1 : -1;
131
+ return a.n.id < b.n.id ? -1 : 1;
132
+ });
133
+ const picked = [];
134
+ const covered = new Set();
135
+ for (const { n, score } of scored) {
136
+ if (picked.length >= seedLimit)
137
+ break;
138
+ if (score === 0)
139
+ continue; // 孤立点不入榜(score 单调降,continue 等价于提前结束)
140
+ if (covered.has(n.id))
141
+ continue; // 相邻合并:已被前一种子的邻域覆盖
142
+ picked.push(n);
143
+ covered.add(n.id);
144
+ for (const e of visEdges) {
145
+ if (e.fromNodeId === n.id)
146
+ covered.add(e.toNodeId);
147
+ else if (e.toNodeId === n.id)
148
+ covered.add(e.fromNodeId);
149
+ }
150
+ }
151
+ return {
152
+ seeds: picked.map((n) => seedView(n, edges, byId, dm, neighborLimit)),
153
+ stats,
154
+ };
155
+ }
156
+ /**
157
+ * 全图星图数据块(纯函数,不查库):可见节点轻量投影 × active 边,
158
+ * 双上限截断。**顺序纪律**:节点按入参序截断(调用方 loadGraph 的行序),
159
+ * 悬挂边(端点之一被截掉/不可见)在截断前剔除——星图上不画悬空线。
160
+ */
161
+ export function buildFullView(nodes, edges, caps = {}) {
162
+ const nodeCap = Math.max(1, Math.floor(caps.nodeCap ?? GRAPH_PREVIEW_FULL_NODES));
163
+ const edgeCap = Math.max(1, Math.floor(caps.edgeCap ?? GRAPH_PREVIEW_FULL_EDGES));
164
+ const visibleNodes = nodes.filter(visible);
165
+ const nodesTruncated = visibleNodes.length > nodeCap;
166
+ const drawn = visibleNodes.slice(0, nodeCap);
167
+ const keep = new Set(drawn.map((n) => n.id));
168
+ const outEdges = [];
169
+ let edgesTruncated = false;
170
+ for (const e of edges) {
171
+ if (e.status !== 'active')
172
+ continue;
173
+ if (!keep.has(e.fromNodeId) || !keep.has(e.toNodeId))
174
+ continue;
175
+ if (outEdges.length >= edgeCap) {
176
+ edgesTruncated = true;
177
+ break;
178
+ }
179
+ outEdges.push({ from: e.fromNodeId, to: e.toNodeId, relation: e.relation, causal: CAUSAL_SET.has(e.relation) });
180
+ }
181
+ return {
182
+ nodes: drawn.map((n) => ({ id: n.id, name: n.name, type: n.type })),
183
+ edges: outEdges,
184
+ truncated: nodesTruncated || edgesTruncated,
185
+ };
186
+ }
@@ -13,15 +13,32 @@ export declare const GRAPH_FIELD_WEIGHTS: {
13
13
  * 图谱命中 → L1 记录 id 序列(§D 第 3 路 RRF 的输入;纯函数,无 I/O)。
14
14
  *
15
15
  * 图谱是**可重建派生投影**,节点/fact 的 `sourceRecordIds` 是它回链 L1 的唯一凭证
16
- * (types.ts:6-7)。此处把"节点排名"翻译成"L1 记录排名":
16
+ * (types.ts:6-7)。此处把"节点排名"翻译成"L1 记录排名",分两段:
17
17
  *
18
- * - **保序**:按命中的 score 降序依次展开来源,先出现的记录 = 更高排名;
19
- * - **去重**:同一 L1 记录可能被多个节点/fact 引用,只占首次出现的位置
20
- * (重复占位会凭空抬高该记录在 RRF 里的权重);
18
+ * - **直连段**:命中节点自身 + 其 active facts 的来源(task_9 原语义、原顺序);
19
+ * - **1-hop 段**:命中节点沿 **active 边**的双向邻居(邻居需 active|disputed)的
20
+ * 节点自身来源,按邻居 updatedAt 新→旧追加,整段新增封顶
21
+ * `GRAPH_HOP_RECORD_CAP`——这是图路"关系可达"增量价值的兑现面。
22
+ * hop 段只取邻居节点自身来源,不取 fact 来源(hop 是联想不是证据,噪声从紧)。
23
+ *
24
+ * 两段共用纪律:
25
+ * - **保序**:先出现的记录 = 更高排名(直连段恒先于 hop 段);已被直连段占位的
26
+ * id 不重复计入、也**不吃 hop 名额**(关联额度不被身份来源挤占);
27
+ * - **去重**:同一 L1 记录被多个节点/fact 引用,只占首次出现的位置;
21
28
  * - **节点自身来源优先于其 fact 来源**:节点的 sourceRecordIds 是它的身份来源,
22
29
  * 而单个 fact 的来源可能引向与该查询无关的旁支记录;
23
30
  * - **只取 active fact**:superseded 是历史状态,不该把旧记录拉进当前检索结果;
24
- * - **不查库**:返回的 id 可能已被删除(墓碑边界),由调用方按"取不到的跳过"处理。
31
+ * - **不查库**:返回的 id 可能已被删除(墓碑边界),由调用方按"取不到的跳过"处理,
32
+ * 不得补空占位(占位会在 RRF 里凭空加分)。
33
+ *
34
+ * edges/nodes 由调用方传入(loadGraph 结果,百~千量级全扫即可;>2k 再考虑预建索引,
35
+ * 见文件头演进阶梯)。族过滤不在本函数:邻居节点可跨族,兜底在 graphLane 尾部的
36
+ * 记录级 family/scope 双闸(宁可漏不可串落在记录上)。
37
+ */
38
+ export declare function expandHitRecordIds(hits: readonly GraphNodeSearchResult[], nodes: readonly GraphNode[], edges: readonly GraphEdge[]): string[];
39
+ /**
40
+ * 直连段回链(无 hop)——保留 task_9 的原语义形状(既有测试继续钉住);
41
+ * 生产链路一律走 expandHitRecordIds(向量种子并集 + 1-hop)。
25
42
  */
26
43
  export declare function graphHitRecordIds(hits: readonly GraphNodeSearchResult[]): string[];
27
44
  /**
@@ -34,7 +34,7 @@
34
34
  * - score 只用于排序,不是事实置信度;无词法命中返回空,不兜底。
35
35
  */
36
36
  import { tokenize } from '../util/text.js';
37
- import { GRAPH_SEARCH_LIMIT_MAX } from './types.js';
37
+ import { GRAPH_HOP_RECORD_CAP, GRAPH_SEARCH_LIMIT_MAX } from './types.js';
38
38
  /** 字段权重(检索命中计分;名称最高、关系词最低)。 */
39
39
  export const GRAPH_FIELD_WEIGHTS = {
40
40
  name: 6,
@@ -60,21 +60,37 @@ const FIELD_ORDER = ['name', 'aliases', 'tags', 'currentState', 'facts', 'relati
60
60
  function factValueText(value) {
61
61
  return Array.isArray(value) ? value.join(' ') : value;
62
62
  }
63
+ /** 节点新度序(updatedAt 新→旧,同值按 id 升序——与检索结果排序同式)。 */
64
+ function byNodeRecency(a, b) {
65
+ return a.updatedAt < b.updatedAt ? 1 : a.updatedAt > b.updatedAt ? -1 : a.id < b.id ? -1 : 1;
66
+ }
63
67
  /**
64
68
  * 图谱命中 → L1 记录 id 序列(§D 第 3 路 RRF 的输入;纯函数,无 I/O)。
65
69
  *
66
70
  * 图谱是**可重建派生投影**,节点/fact 的 `sourceRecordIds` 是它回链 L1 的唯一凭证
67
- * (types.ts:6-7)。此处把"节点排名"翻译成"L1 记录排名":
71
+ * (types.ts:6-7)。此处把"节点排名"翻译成"L1 记录排名",分两段:
72
+ *
73
+ * - **直连段**:命中节点自身 + 其 active facts 的来源(task_9 原语义、原顺序);
74
+ * - **1-hop 段**:命中节点沿 **active 边**的双向邻居(邻居需 active|disputed)的
75
+ * 节点自身来源,按邻居 updatedAt 新→旧追加,整段新增封顶
76
+ * `GRAPH_HOP_RECORD_CAP`——这是图路"关系可达"增量价值的兑现面。
77
+ * hop 段只取邻居节点自身来源,不取 fact 来源(hop 是联想不是证据,噪声从紧)。
68
78
  *
69
- * - **保序**:按命中的 score 降序依次展开来源,先出现的记录 = 更高排名;
70
- * - **去重**:同一 L1 记录可能被多个节点/fact 引用,只占首次出现的位置
71
- * (重复占位会凭空抬高该记录在 RRF 里的权重);
79
+ * 两段共用纪律:
80
+ * - **保序**:先出现的记录 = 更高排名(直连段恒先于 hop 段);已被直连段占位的
81
+ * id 不重复计入、也**不吃 hop 名额**(关联额度不被身份来源挤占);
82
+ * - **去重**:同一 L1 记录被多个节点/fact 引用,只占首次出现的位置;
72
83
  * - **节点自身来源优先于其 fact 来源**:节点的 sourceRecordIds 是它的身份来源,
73
84
  * 而单个 fact 的来源可能引向与该查询无关的旁支记录;
74
85
  * - **只取 active fact**:superseded 是历史状态,不该把旧记录拉进当前检索结果;
75
- * - **不查库**:返回的 id 可能已被删除(墓碑边界),由调用方按"取不到的跳过"处理。
86
+ * - **不查库**:返回的 id 可能已被删除(墓碑边界),由调用方按"取不到的跳过"处理,
87
+ * 不得补空占位(占位会在 RRF 里凭空加分)。
88
+ *
89
+ * edges/nodes 由调用方传入(loadGraph 结果,百~千量级全扫即可;>2k 再考虑预建索引,
90
+ * 见文件头演进阶梯)。族过滤不在本函数:邻居节点可跨族,兜底在 graphLane 尾部的
91
+ * 记录级 family/scope 双闸(宁可漏不可串落在记录上)。
76
92
  */
77
- export function graphHitRecordIds(hits) {
93
+ export function expandHitRecordIds(hits, nodes, edges) {
78
94
  const seen = new Set();
79
95
  const out = [];
80
96
  const take = (id) => {
@@ -83,6 +99,7 @@ export function graphHitRecordIds(hits) {
83
99
  seen.add(id);
84
100
  out.push(id);
85
101
  };
102
+ // 直连段(节点身份来源在前,active facts 在后)
86
103
  for (const h of hits) {
87
104
  for (const id of h.node.sourceRecordIds)
88
105
  take(id);
@@ -93,8 +110,53 @@ export function graphHitRecordIds(hits) {
93
110
  take(id);
94
111
  }
95
112
  }
113
+ if (edges.length === 0)
114
+ return out;
115
+ // hop 段:active 边的双向邻居 ∉ 命中集,邻居去重,状态需 active|disputed
116
+ const hitNodeIds = new Set(hits.map((h) => h.node.id));
117
+ const nodeById = new Map(nodes.map((n) => [n.id, n]));
118
+ const seenNeighbor = new Set();
119
+ const neighbors = [];
120
+ for (const e of edges) {
121
+ if (e.status !== 'active')
122
+ continue;
123
+ const dirs = [
124
+ [e.fromNodeId, e.toNodeId],
125
+ [e.toNodeId, e.fromNodeId],
126
+ ];
127
+ for (const [self, other] of dirs) {
128
+ if (!hitNodeIds.has(self) || hitNodeIds.has(other) || seenNeighbor.has(other))
129
+ continue;
130
+ const n = nodeById.get(other);
131
+ if (!n || (n.status !== 'active' && n.status !== 'disputed'))
132
+ continue;
133
+ seenNeighbor.add(other);
134
+ neighbors.push(n);
135
+ }
136
+ }
137
+ neighbors.sort(byNodeRecency);
138
+ let hopTaken = 0;
139
+ for (const n of neighbors) {
140
+ if (hopTaken >= GRAPH_HOP_RECORD_CAP)
141
+ break;
142
+ for (const id of n.sourceRecordIds) {
143
+ if (seen.has(id))
144
+ continue; // 已被直连段占用:不计入、不吃名额
145
+ if (hopTaken >= GRAPH_HOP_RECORD_CAP)
146
+ break;
147
+ take(id);
148
+ hopTaken++;
149
+ }
150
+ }
96
151
  return out;
97
152
  }
153
+ /**
154
+ * 直连段回链(无 hop)——保留 task_9 的原语义形状(既有测试继续钉住);
155
+ * 生产链路一律走 expandHitRecordIds(向量种子并集 + 1-hop)。
156
+ */
157
+ export function graphHitRecordIds(hits) {
158
+ return expandHitRecordIds(hits, [], []);
159
+ }
98
160
  /**
99
161
  * 图谱节点检索:返回按 score 降序的命中(最多 limit 条;limit 钳制 1~20)。
100
162
  * 候选节点 = active | disputed(superseded/archived 不进检索)。
@@ -134,9 +134,34 @@ export declare const GRAPH_JOB_BATCH = 8;
134
134
  export declare const GRAPH_PRIORITY_NEW = 10000;
135
135
  /** 存量补投影的优先级(启动/周期补齐;恒低于新蒸馏)。 */
136
136
  export declare const GRAPH_PRIORITY_BACKFILL = 100;
137
- /** 单 job 最大尝试次数:claim 时 +1,达到即转 dead(不再重试)。 */
137
+ /** 单 job 最大尝试次数(永久错误:校验/解析类——3 次不过就 dead)。
138
+ * 瞬态错误(aborted/timeout/网络)走 GRAPH_JOB_TRANSIENT_MAX_ATTEMPTS。 */
138
139
  export declare const GRAPH_JOB_MAX_ATTEMPTS = 3;
140
+ /** 瞬态错误(attempt 不消耗到 3 即 dead):网关负载或超时引起的 aborted、
141
+ * terminated、网络抖动、模型空输出——这些与 prompt/数据无关,退避后重试即恢复。
142
+ * 6 次 ≈ 5 轮退避(~30 分钟)覆盖 local-35 负载低谷。 */
143
+ export declare const GRAPH_JOB_TRANSIENT_MAX_ATTEMPTS = 6;
144
+ /** 图谱投影 LLM 单次调用超时(含整条回退链):沿用 llm.timeoutMs(120s) 在 8-record
145
+ * 批 + 上下文节点/边 + 12 关系词 prompt 下真机撞线(14 dead 全是 aborted×13 + terminated×1)。
146
+ * 照 backfill callTimeoutMs 先例给 240s,按「路由数 × 单路由耗时」留余量。 */
147
+ export declare const GRAPH_PROJECTION_CALL_TIMEOUT_MS = 240000;
139
148
  /** 失败指数退避基数:第 n 次失败后 nextAttemptAt = now + base × 2^(n-1)。 */
140
149
  export declare const GRAPH_JOB_BACKOFF_BASE_MS = 60000;
141
150
  /** 单次检索返回上限的硬钳制(工具与 RPC 两侧共用同一钳制值)。 */
142
151
  export declare const GRAPH_SEARCH_LIMIT_MAX = 20;
152
+ /** §D 第 3 路路内向量种子的节点条数上限(种子只是联想起点,独立于词法路 limit)。 */
153
+ export declare const GRAPH_VEC_SEED_CAP = 10;
154
+ /** 1-hop 边扩展回链的新增记录数封顶(hop 段专用名额,直连段不占;防 hub 节点邻接爆量)。 */
155
+ export declare const GRAPH_HOP_RECORD_CAP = 64;
156
+ /** 面板图谱预览:种子榜上限(画布片数)。 */
157
+ export declare const GRAPH_PREVIEW_SEEDS = 6;
158
+ /** 面板图谱预览:单种子绘制的邻居上限(超出置 truncated)。 */
159
+ export declare const GRAPH_PREVIEW_NEIGHBORS = 10;
160
+ /** 面板图谱预览:种子评分中因果边的权重(score = degree + causal×此值)——
161
+ * 因果链比共现邻接更值得画,预览侧单源读 GRAPH_CAUSAL_RELATIONS。 */
162
+ export declare const GRAPH_PREVIEW_CAUSAL_WEIGHT = 2;
163
+ /** 全图星图(mode:'full')节点上限:超限截断并置 truncated(截断标记而非硬拒——
164
+ * 星图是展示面,宁可少画几颗也不报错;G6 舒适区 <2000 留余量)。 */
165
+ export declare const GRAPH_PREVIEW_FULL_NODES = 1500;
166
+ /** 全图星图 active 边上限(同一 truncated 语义;悬挂边已在截断前剔除)。 */
167
+ export declare const GRAPH_PREVIEW_FULL_EDGES = 3000;
@@ -6,9 +6,34 @@ export const GRAPH_JOB_BATCH = 8;
6
6
  export const GRAPH_PRIORITY_NEW = 10000;
7
7
  /** 存量补投影的优先级(启动/周期补齐;恒低于新蒸馏)。 */
8
8
  export const GRAPH_PRIORITY_BACKFILL = 100;
9
- /** 单 job 最大尝试次数:claim 时 +1,达到即转 dead(不再重试)。 */
9
+ /** 单 job 最大尝试次数(永久错误:校验/解析类——3 次不过就 dead)。
10
+ * 瞬态错误(aborted/timeout/网络)走 GRAPH_JOB_TRANSIENT_MAX_ATTEMPTS。 */
10
11
  export const GRAPH_JOB_MAX_ATTEMPTS = 3;
12
+ /** 瞬态错误(attempt 不消耗到 3 即 dead):网关负载或超时引起的 aborted、
13
+ * terminated、网络抖动、模型空输出——这些与 prompt/数据无关,退避后重试即恢复。
14
+ * 6 次 ≈ 5 轮退避(~30 分钟)覆盖 local-35 负载低谷。 */
15
+ export const GRAPH_JOB_TRANSIENT_MAX_ATTEMPTS = 6;
16
+ /** 图谱投影 LLM 单次调用超时(含整条回退链):沿用 llm.timeoutMs(120s) 在 8-record
17
+ * 批 + 上下文节点/边 + 12 关系词 prompt 下真机撞线(14 dead 全是 aborted×13 + terminated×1)。
18
+ * 照 backfill callTimeoutMs 先例给 240s,按「路由数 × 单路由耗时」留余量。 */
19
+ export const GRAPH_PROJECTION_CALL_TIMEOUT_MS = 240_000;
11
20
  /** 失败指数退避基数:第 n 次失败后 nextAttemptAt = now + base × 2^(n-1)。 */
12
21
  export const GRAPH_JOB_BACKOFF_BASE_MS = 60_000;
13
22
  /** 单次检索返回上限的硬钳制(工具与 RPC 两侧共用同一钳制值)。 */
14
23
  export const GRAPH_SEARCH_LIMIT_MAX = 20;
24
+ /** §D 第 3 路路内向量种子的节点条数上限(种子只是联想起点,独立于词法路 limit)。 */
25
+ export const GRAPH_VEC_SEED_CAP = 10;
26
+ /** 1-hop 边扩展回链的新增记录数封顶(hop 段专用名额,直连段不占;防 hub 节点邻接爆量)。 */
27
+ export const GRAPH_HOP_RECORD_CAP = 64;
28
+ /** 面板图谱预览:种子榜上限(画布片数)。 */
29
+ export const GRAPH_PREVIEW_SEEDS = 6;
30
+ /** 面板图谱预览:单种子绘制的邻居上限(超出置 truncated)。 */
31
+ export const GRAPH_PREVIEW_NEIGHBORS = 10;
32
+ /** 面板图谱预览:种子评分中因果边的权重(score = degree + causal×此值)——
33
+ * 因果链比共现邻接更值得画,预览侧单源读 GRAPH_CAUSAL_RELATIONS。 */
34
+ export const GRAPH_PREVIEW_CAUSAL_WEIGHT = 2;
35
+ /** 全图星图(mode:'full')节点上限:超限截断并置 truncated(截断标记而非硬拒——
36
+ * 星图是展示面,宁可少画几颗也不报错;G6 舒适区 <2000 留余量)。 */
37
+ export const GRAPH_PREVIEW_FULL_NODES = 1500;
38
+ /** 全图星图 active 边上限(同一 truncated 语义;悬挂边已在截断前剔除)。 */
39
+ export const GRAPH_PREVIEW_FULL_EDGES = 3000;
@@ -531,7 +531,14 @@ usage) {
531
531
  usage?.recordAttempt(scoped.map((h) => h.id), fresh.slice(0, lines.length).map((h) => h.id));
532
532
  // G-live:按 **sessionKey**(=agent.session?.id ?? agent.id,与 pill 的 sessionId / 投影 header.id 同源,
533
533
  // 非 ownerId)记入本会话“活召回集”,供侧栏卡读取;压缩时同键 reset。
534
- stores.recalled?.add(sessionKey, fresh.slice(0, lines.length).map((h) => h.id), injection.id);
534
+ // 路归因随注入透传(hybrid 融合才挂 lanes;单路策略下表空=不记,徽标无从渲染)。
535
+ const injectedHits = fresh.slice(0, lines.length);
536
+ const laneById = new Map();
537
+ for (const h of injectedHits) {
538
+ if (h.lanes !== undefined && h.lanes.length > 0)
539
+ laneById.set(h.id, h.lanes);
540
+ }
541
+ stores.recalled?.add(sessionKey, injectedHits.map((h) => h.id), injection.id, laneById);
535
542
  // 入账在成功构造注入消息之后、返回 enter 之前——任何前置抛错路径账目零扰动
536
543
  const led = ledgerFor(payload.agent.id);
537
544
  recordRecallInjection(led, text.length);