@konneal/engine 0.2.99 → 0.2.100

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.
@@ -1,8 +1,8 @@
1
1
  import {
2
2
  handleAsk
3
- } from "./chunk-2SWMTCVX.js";
3
+ } from "./chunk-GYCUMFOA.js";
4
4
  import "./chunk-QNLFNEI4.js";
5
- import "./chunk-TXPWLKCI.js";
5
+ import "./chunk-BBKAETKM.js";
6
6
  import "./chunk-KNCRYAX3.js";
7
7
  import "./chunk-DTW4UW2E.js";
8
8
  import "./chunk-A3QHHUN5.js";
@@ -114,6 +114,69 @@ var STOP = /* @__PURE__ */ new Set([
114
114
  "their",
115
115
  "there"
116
116
  ]);
117
+ function rowMeta(r) {
118
+ return {
119
+ doc_id: String(r.doc_id ?? ""),
120
+ docidentifier: String(r.docidentifier ?? ""),
121
+ doctype: String(r.doctype ?? ""),
122
+ doc_number: String(r.doc_number ?? ""),
123
+ edition: String(r.edition ?? ""),
124
+ language: String(r.language ?? "en"),
125
+ clause_anchor: String(r.clause_anchor ?? ""),
126
+ clause_title: String(r.clause_title ?? ""),
127
+ tier: String(r.tier ?? ""),
128
+ corpus: String(r.corpus ?? ""),
129
+ text_ref: "",
130
+ status: String(r.status ?? "unknown"),
131
+ superseded_by: String(r.superseded_by ?? ""),
132
+ // contract v2 over the lexical lane: typed chunks arriving via BM25
133
+ // keep their unit identity ([[u:…]] refs, typed pin, retyping check)
134
+ unit_id: String(r.unit_id ?? "") || void 0,
135
+ block: String(r.block ?? "") || void 0
136
+ };
137
+ }
138
+ function rowsToHits(rows) {
139
+ return rows.map((r) => {
140
+ const bm25 = typeof r.rank === "number" ? r.rank : 0;
141
+ return {
142
+ id: String(r.id),
143
+ score: 1 / (1 + Math.max(0, bm25)),
144
+ metadata: rowMeta(r),
145
+ text: String(r.text ?? "")
146
+ };
147
+ });
148
+ }
149
+ function rowsToMatches(rows) {
150
+ return rows.map((r) => {
151
+ const bm25 = typeof r.rank === "number" ? Math.max(0, r.rank) : 0;
152
+ return {
153
+ id: String(r.id),
154
+ score: 1 / (1 + bm25),
155
+ metadata: { ...rowMeta(r), chunk_text: String(r.text ?? "") }
156
+ };
157
+ });
158
+ }
159
+ async function lexicalWithin(env, query, docNumbers, k = 12) {
160
+ const match2 = ftsMatchQuery(query);
161
+ if (!match2 || !docNumbers.length) return [];
162
+ try {
163
+ const placeholders = docNumbers.map((_, i) => `?${i + 2}`).join(",");
164
+ const res = await env.DB.prepare(
165
+ `SELECT c.id, c.doc_id, c.docidentifier, c.doctype, c.doc_number, c.edition,
166
+ c.language, c.clause_anchor, c.clause_title, c.status, c.superseded_by,
167
+ c.corpus, c.tier, c.text, c.unit_id, c.block, bm25(chunks_fts) AS rank
168
+ FROM chunks_fts
169
+ JOIN chunks c ON c.rowid = chunks_fts.rowid
170
+ WHERE chunks_fts MATCH ?1 AND c.doc_number IN (${placeholders})
171
+ ORDER BY rank
172
+ LIMIT ?${docNumbers.length + 2}`
173
+ ).bind(match2, ...docNumbers, k).all();
174
+ return rowsToMatches(res.results ?? []);
175
+ } catch (e) {
176
+ console.log("lexical-within failed:", String(e).slice(0, 200));
177
+ return [];
178
+ }
179
+ }
117
180
  async function lexicalPrefilter(env, query, k = LEXICAL_K) {
118
181
  const match2 = ftsMatchQuery(query);
119
182
  if (!match2) return [];
@@ -129,34 +192,7 @@ async function lexicalPrefilter(env, query, k = LEXICAL_K) {
129
192
  LIMIT ?2`
130
193
  ).bind(match2, k).all();
131
194
  const rows = res.results ?? [];
132
- return rows.map((r, i) => {
133
- const meta = {
134
- doc_id: String(r.doc_id ?? ""),
135
- docidentifier: String(r.docidentifier ?? ""),
136
- doctype: String(r.doctype ?? ""),
137
- doc_number: String(r.doc_number ?? ""),
138
- edition: String(r.edition ?? ""),
139
- language: String(r.language ?? "en"),
140
- clause_anchor: String(r.clause_anchor ?? ""),
141
- clause_title: String(r.clause_title ?? ""),
142
- tier: String(r.tier ?? ""),
143
- corpus: String(r.corpus ?? ""),
144
- text_ref: "",
145
- status: String(r.status ?? "unknown"),
146
- superseded_by: String(r.superseded_by ?? ""),
147
- // contract v2 over the lexical lane: typed chunks arriving via BM25
148
- // keep their unit identity ([[u:…]] refs, typed pin, retyping check)
149
- unit_id: String(r.unit_id ?? "") || void 0,
150
- block: String(r.block ?? "") || void 0
151
- };
152
- const bm25 = typeof r.rank === "number" ? r.rank : i;
153
- return {
154
- id: String(r.id),
155
- score: 1 / (1 + Math.max(0, bm25)),
156
- metadata: meta,
157
- text: String(r.text ?? "")
158
- };
159
- });
195
+ return rowsToHits(rows);
160
196
  } catch (e) {
161
197
  console.log("lexical prefilter failed:", String(e).slice(0, 200));
162
198
  return [];
@@ -21935,7 +21971,14 @@ var oimlPubid = {
21935
21971
  var plainSlug = {
21936
21972
  parse: () => null,
21937
21973
  scanQuestion: () => null,
21938
- graphDocNumber: () => null,
21974
+ // the graph node id scheme (doc:OIML-<TYPE>-<NUM>-<EDITION>) is the
21975
+ // GRAPH's own identity, estate-wide — not publisher-flavored — so the
21976
+ // plain codec parses it too (a null here silenced the concept-graph
21977
+ // lane's resolution for its whole life)
21978
+ graphDocNumber: (nodeId) => {
21979
+ const m = nodeId.match(/^doc:OIML-[A-Z]-(\d+)-/);
21980
+ return m ? m[1] : null;
21981
+ },
21939
21982
  familyOf: () => null
21940
21983
  };
21941
21984
  var REGISTRY = {
@@ -22477,14 +22520,10 @@ var conceptGraph = {
22477
22520
  }
22478
22521
  }
22479
22522
  if (numbers.size) {
22480
- const gc = await c.env.VECTORIZE.query(c.vector, {
22481
- topK: 12,
22482
- returnMetadata: "all",
22483
- filter: { doc_number: { $in: [...numbers] } }
22484
- });
22523
+ const gc = await lexicalWithin(c.env, c.rq || c.query, [...numbers], 12);
22485
22524
  const seenIds0 = new Set(c.matches.map((m) => m.id));
22486
22525
  let merged0 = 0;
22487
- for (const m of (gc.matches ?? []).slice(0, 6)) {
22526
+ for (const m of gc.slice(0, 6)) {
22488
22527
  if (!seenIds0.has(m.id)) {
22489
22528
  c.matches.push({ id: m.id, score: m.score * THRESHOLDS.conceptGraphDiscount, metadata: m.metadata });
22490
22529
  seenIds0.add(m.id);
@@ -22502,24 +22541,20 @@ var graphLane = {
22502
22541
  failure: "additive",
22503
22542
  when: (c) => !!c.opts.graphDocNumbers?.length && c.vector.length > 0,
22504
22543
  prefetch: (c) => {
22505
- c.lane["graph-lane"] = c.env.VECTORIZE.query(c.vector, {
22506
- topK: 15,
22507
- returnMetadata: "all",
22508
- filter: { doc_number: { $in: c.opts.graphDocNumbers } }
22509
- });
22544
+ c.lane["graph-lane"] = lexicalWithin(c.env, c.rq || c.query, c.opts.graphDocNumbers, 15);
22510
22545
  },
22511
22546
  run: async (c) => {
22512
22547
  const g = await c.lane["graph-lane"];
22513
22548
  const seenIds = new Set(c.matches.map((m) => m.id));
22514
22549
  let merged = 0;
22515
- for (const m of (g.matches ?? []).slice(0, 10)) {
22550
+ for (const m of (g ?? []).slice(0, 10)) {
22516
22551
  if (!seenIds.has(m.id)) {
22517
22552
  c.matches.push({ id: m.id, score: m.score * THRESHOLDS.graphLaneDiscount, metadata: m.metadata });
22518
22553
  seenIds.add(m.id);
22519
22554
  merged++;
22520
22555
  }
22521
22556
  }
22522
- console.log("graph lane:", g.matches?.length ?? 0, "hits,", merged, "merged");
22557
+ console.log("graph lane:", g?.length ?? 0, "hits,", merged, "merged");
22523
22558
  }
22524
22559
  };
22525
22560
 
@@ -56,7 +56,7 @@ import {
56
56
  tokenBudget,
57
57
  understandQuery,
58
58
  usageTotal
59
- } from "./chunk-TXPWLKCI.js";
59
+ } from "./chunk-BBKAETKM.js";
60
60
  import {
61
61
  embed,
62
62
  generateOnce,
@@ -6,7 +6,7 @@ import {
6
6
  sessionFrom,
7
7
  telemetry,
8
8
  understandQuery
9
- } from "./chunk-TXPWLKCI.js";
9
+ } from "./chunk-BBKAETKM.js";
10
10
  import {
11
11
  portModelRunner
12
12
  } from "./chunk-KNCRYAX3.js";
package/dist/lexical.d.ts CHANGED
@@ -2,6 +2,18 @@ import type { Hit } from "./pipeline";
2
2
  /** Build a safe FTS5 MATCH query from user text: alphanumeric tokens,
3
3
  * joined with OR so jargon hits don't require full-phrase match. */
4
4
  export declare function ftsMatchQuery(query: string): string | null;
5
+ /** Lexical ranking WITHIN a document set (the graph lanes' identity
6
+ * resolution): BM25 over the corpus's own FTS, restricted to the
7
+ * documents' doc_number values. This is the working substitute for
8
+ * Vectorize metadata filtering, which is dead on this index (measured
9
+ * 2026-10-05: filters return empty for values proven present). */
10
+ export declare function lexicalWithin(env: {
11
+ DB: D1Database;
12
+ }, query: string, docNumbers: string[], k?: number): Promise<{
13
+ id: string;
14
+ score: number;
15
+ metadata: Record<string, unknown>;
16
+ }[]>;
5
17
  export declare function lexicalPrefilter(env: {
6
18
  DB: D1Database;
7
19
  }, query: string, k?: number): Promise<Hit[]>;
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  handleSearch
3
- } from "./chunk-GY33RTEO.js";
4
- import "./chunk-TXPWLKCI.js";
3
+ } from "./chunk-OREMF5LK.js";
4
+ import "./chunk-BBKAETKM.js";
5
5
  import "./chunk-KNCRYAX3.js";
6
6
  import "./chunk-DTW4UW2E.js";
7
7
  import "./chunk-A3QHHUN5.js";
@@ -4,7 +4,7 @@ import {
4
4
  handleMemories,
5
5
  scoreFaithfulness,
6
6
  scoreJudge
7
- } from "../../chunk-2SWMTCVX.js";
7
+ } from "../../chunk-GYCUMFOA.js";
8
8
  import {
9
9
  TOOLS_REGISTRY,
10
10
  bindModelNode,
@@ -12,7 +12,7 @@ import {
12
12
  } from "../../chunk-QNLFNEI4.js";
13
13
  import {
14
14
  handleSearch
15
- } from "../../chunk-GY33RTEO.js";
15
+ } from "../../chunk-OREMF5LK.js";
16
16
  import {
17
17
  buildMessages,
18
18
  citations,
@@ -34,7 +34,7 @@ import {
34
34
  sessionFrom,
35
35
  telemetry,
36
36
  understandQuery
37
- } from "../../chunk-TXPWLKCI.js";
37
+ } from "../../chunk-BBKAETKM.js";
38
38
  import {
39
39
  cfBlobs,
40
40
  embed,
@@ -996,7 +996,7 @@ async function callToolOnce(env, ctx, _req, tier, key, name, args, t0) {
996
996
  const r = await runTool({ ...env, DB: env.DB }, { name, args }, "mcp");
997
997
  payload = r ? { tool: r.name, query: r.query, result: r.output } : { error: { message: `tool ${name} returned nothing for the given arguments` } };
998
998
  } else {
999
- const res = name === "ask" ? await (await import("../../ask-W6NUC3MG.js")).handleAsk(env, ctx, inner, tier, key) : await (await import("../../search-EQAOPFCC.js")).handleSearch(env, ctx, inner, tier, key);
999
+ const res = name === "ask" ? await (await import("../../ask-KP42T6DH.js")).handleAsk(env, ctx, inner, tier, key) : await (await import("../../search-4CBKKNBJ.js")).handleSearch(env, ctx, inner, tier, key);
1000
1000
  payload = await res.json().catch(() => ({ error: { message: "tool transport failed", status: res.status } }));
1001
1001
  }
1002
1002
  telemetry(env, ctx, tier, `mcp:${name}`, null, !payload?.error, JSON.stringify(payload).length, await sha256Hex(`${name}:${JSON.stringify(args ?? {})}`), void 0, void 0, { durationMs: Date.now() - t0 });
package/package.json CHANGED
@@ -27,7 +27,7 @@
27
27
  "playwright": "^1.62.1",
28
28
  "yaml": "^2.9.1"
29
29
  },
30
- "version": "0.2.99",
30
+ "version": "0.2.100",
31
31
  "description": "The Konneal engine: the publisher-agnostic build pipeline and API plane for standards intelligence (retrieval, answer contract, verdicts, evaluation).",
32
32
  "license": "BSD-3-Clause",
33
33
  "type": "module",
@@ -142,7 +142,14 @@ export const oimlPubid: RefCodec = {
142
142
  export const plainSlug: RefCodec = {
143
143
  parse: () => null,
144
144
  scanQuestion: () => null,
145
- graphDocNumber: () => null,
145
+ // the graph node id scheme (doc:OIML-<TYPE>-<NUM>-<EDITION>) is the
146
+ // GRAPH's own identity, estate-wide — not publisher-flavored — so the
147
+ // plain codec parses it too (a null here silenced the concept-graph
148
+ // lane's resolution for its whole life)
149
+ graphDocNumber: (nodeId) => {
150
+ const m = nodeId.match(/^doc:OIML-[A-Z]-(\d+)-/);
151
+ return m ? m[1] : null;
152
+ },
146
153
  familyOf: () => null,
147
154
  };
148
155
 
@@ -31,6 +31,89 @@ const STOP = new Set([
31
31
  "about", "into", "than", "then", "its", "it", "their", "there",
32
32
  ]);
33
33
 
34
+ function rowMeta(r: any): ChunkMeta {
35
+ return {
36
+ doc_id: String(r.doc_id ?? ""),
37
+ docidentifier: String(r.docidentifier ?? ""),
38
+ doctype: String(r.doctype ?? ""),
39
+ doc_number: String(r.doc_number ?? ""),
40
+ edition: String(r.edition ?? ""),
41
+ language: String(r.language ?? "en"),
42
+ clause_anchor: String(r.clause_anchor ?? ""),
43
+ clause_title: String(r.clause_title ?? ""),
44
+ tier: String(r.tier ?? ""),
45
+ corpus: String(r.corpus ?? ""),
46
+ text_ref: "",
47
+ status: String(r.status ?? "unknown"),
48
+ superseded_by: String(r.superseded_by ?? ""),
49
+ // contract v2 over the lexical lane: typed chunks arriving via BM25
50
+ // keep their unit identity ([[u:…]] refs, typed pin, retyping check)
51
+ unit_id: String(r.unit_id ?? "") || undefined,
52
+ block: String(r.block ?? "") || undefined,
53
+ };
54
+ }
55
+
56
+ function rowsToHits(rows: any[]): Hit[] {
57
+ return rows.map((r: any) => {
58
+ // rank is bm25 (lower better) → convert to positive score that RRF can ignore
59
+ // (RRF uses rank position, not score). score kept for logging only.
60
+ const bm25 = typeof r.rank === "number" ? r.rank : 0;
61
+ return {
62
+ id: String(r.id),
63
+ score: 1 / (1 + Math.max(0, bm25)),
64
+ metadata: rowMeta(r),
65
+ text: String(r.text ?? ""),
66
+ } satisfies Hit;
67
+ });
68
+ }
69
+
70
+ /** Lexical matches for the lane merges: match-shaped, with the text
71
+ * riding the metadata (toHits reads chunk_text out of it). */
72
+ function rowsToMatches(rows: any[]): { id: string; score: number; metadata: Record<string, unknown> }[] {
73
+ return rows.map((r: any) => {
74
+ const bm25 = typeof r.rank === "number" ? Math.max(0, r.rank) : 0;
75
+ return {
76
+ id: String(r.id),
77
+ score: 1 / (1 + bm25),
78
+ metadata: { ...rowMeta(r), chunk_text: String(r.text ?? "") },
79
+ };
80
+ });
81
+ }
82
+
83
+ /** Lexical ranking WITHIN a document set (the graph lanes' identity
84
+ * resolution): BM25 over the corpus's own FTS, restricted to the
85
+ * documents' doc_number values. This is the working substitute for
86
+ * Vectorize metadata filtering, which is dead on this index (measured
87
+ * 2026-10-05: filters return empty for values proven present). */
88
+ export async function lexicalWithin(
89
+ env: { DB: D1Database },
90
+ query: string,
91
+ docNumbers: string[],
92
+ k = 12,
93
+ ): Promise<{ id: string; score: number; metadata: Record<string, unknown> }[]> {
94
+ const match = ftsMatchQuery(query);
95
+ if (!match || !docNumbers.length) return [];
96
+ try {
97
+ const placeholders = docNumbers.map((_, i) => `?${i + 2}`).join(",");
98
+ const res = await env.DB.prepare(
99
+ `SELECT c.id, c.doc_id, c.docidentifier, c.doctype, c.doc_number, c.edition,
100
+ c.language, c.clause_anchor, c.clause_title, c.status, c.superseded_by,
101
+ c.corpus, c.tier, c.text, c.unit_id, c.block, bm25(chunks_fts) AS rank
102
+ FROM chunks_fts
103
+ JOIN chunks c ON c.rowid = chunks_fts.rowid
104
+ WHERE chunks_fts MATCH ?1 AND c.doc_number IN (${placeholders})
105
+ ORDER BY rank
106
+ LIMIT ?${docNumbers.length + 2}`,
107
+ )
108
+ .bind(match, ...docNumbers, k)
109
+ .all();
110
+ return rowsToMatches(res.results ?? []);
111
+ } catch (e) {
112
+ console.log("lexical-within failed:", String(e).slice(0, 200));
113
+ return [];
114
+ }
115
+ }
116
+
34
117
  export async function lexicalPrefilter(env: { DB: D1Database }, query: string, k = LEXICAL_K): Promise<Hit[]> {
35
118
  const match = ftsMatchQuery(query);
36
119
  if (!match) return [];
@@ -49,36 +132,7 @@ export async function lexicalPrefilter(env: { DB: D1Database }, query: string, k
49
132
  .bind(match, k)
50
133
  .all();
51
134
  const rows = res.results ?? [];
52
- return rows.map((r: any, i: number) => {
53
- const meta: ChunkMeta = {
54
- doc_id: String(r.doc_id ?? ""),
55
- docidentifier: String(r.docidentifier ?? ""),
56
- doctype: String(r.doctype ?? ""),
57
- doc_number: String(r.doc_number ?? ""),
58
- edition: String(r.edition ?? ""),
59
- language: String(r.language ?? "en"),
60
- clause_anchor: String(r.clause_anchor ?? ""),
61
- clause_title: String(r.clause_title ?? ""),
62
- tier: String(r.tier ?? ""),
63
- corpus: String(r.corpus ?? ""),
64
- text_ref: "",
65
- status: String(r.status ?? "unknown"),
66
- superseded_by: String(r.superseded_by ?? ""),
67
- // contract v2 over the lexical lane: typed chunks arriving via BM25
68
- // keep their unit identity ([[u:…]] refs, typed pin, retyping check)
69
- unit_id: String(r.unit_id ?? "") || undefined,
70
- block: String(r.block ?? "") || undefined,
71
- };
72
- // rank is bm25 (lower better) → convert to positive score that RRF can ignore
73
- // (RRF uses rank position, not score). score kept for logging only.
74
- const bm25 = typeof r.rank === "number" ? r.rank : i;
75
- return {
76
- id: String(r.id),
77
- score: 1 / (1 + Math.max(0, bm25)),
78
- metadata: meta,
79
- text: String(r.text ?? ""),
80
- } satisfies Hit;
81
- });
135
+ return rowsToHits(rows);
82
136
  } catch (e) {
83
137
  console.log("lexical prefilter failed:", String(e).slice(0, 200));
84
138
  return [];
@@ -20,6 +20,7 @@
20
20
  import { THRESHOLDS } from "../config.ts";
21
21
  import type { Stage } from "./types.ts";
22
22
  import { refCodec } from "../codecs.ts";
23
+ import { lexicalWithin } from "../lexical.ts";
23
24
 
24
25
  export const conceptGraph: Stage = {
25
26
  name: "concept-graph",
@@ -49,14 +50,14 @@ export const conceptGraph: Stage = {
49
50
  }
50
51
  }
51
52
  if (numbers.size) {
52
- const gc = await c.env.VECTORIZE.query(c.vector, {
53
- topK: 12,
54
- returnMetadata: "all",
55
- filter: { doc_number: { $in: [...numbers] } },
56
- });
53
+ // identity resolution: the linked documents' query-relevant
54
+ // candidates rank by the corpus's own BM25 within the set — the
55
+ // index filter this lane used returned empty for its whole life
56
+ // (Vectorize metadata filtering is dead on this index)
57
+ const gc = await lexicalWithin(c.env, c.rq || c.query, [...numbers], 12);
57
58
  const seenIds0 = new Set(c.matches.map((m: any) => m.id));
58
59
  let merged0 = 0;
59
- for (const m of (gc.matches ?? []).slice(0, 6)) {
60
+ for (const m of gc.slice(0, 6)) {
60
61
  if (!seenIds0.has(m.id)) {
61
62
  c.matches.push({ id: m.id, score: m.score * THRESHOLDS.conceptGraphDiscount, metadata: m.metadata });
62
63
  seenIds0.add(m.id);
@@ -1,7 +1,15 @@
1
1
  // Graph lane (additive): the caller-resolved document numbers (relaton
2
- // family/successor resolution done in index.ts) merge their top
3
- // candidates at a discount.
2
+ // family/successor resolution done in index.ts) merge their
3
+ // query-relevant candidates at a discount.
4
+ //
5
+ // Identity resolution, not index filtering: the candidates rank by the
6
+ // corpus's own BM25 WITHIN the document set (lexicalWithin) — Vectorize
7
+ // metadata filtering is dead on this index (measured 2026-10-05:
8
+ // filters return empty for values proven present), so the filter this
9
+ // lane used for its whole life merged nothing. The lexical rows carry
10
+ // their own text and metadata; no index round trip is needed at all.
4
11
  import { THRESHOLDS } from "../config.ts";
12
+ import { lexicalWithin } from "../lexical.ts";
5
13
  import type { Stage } from "./types.ts";
6
14
 
7
15
  export const graphLane: Stage = {
@@ -9,23 +17,19 @@ export const graphLane: Stage = {
9
17
  failure: "additive",
10
18
  when: (c) => !!c.opts.graphDocNumbers?.length && c.vector.length > 0,
11
19
  prefetch: (c) => {
12
- c.lane["graph-lane"] = c.env.VECTORIZE.query(c.vector, {
13
- topK: 15,
14
- returnMetadata: "all",
15
- filter: { doc_number: { $in: c.opts.graphDocNumbers } },
16
- });
20
+ c.lane["graph-lane"] = lexicalWithin(c.env, c.rq || c.query, c.opts.graphDocNumbers!, 15);
17
21
  },
18
22
  run: async (c) => {
19
23
  const g = (await c.lane["graph-lane"]!) as any;
20
24
  const seenIds = new Set(c.matches.map((m: any) => m.id));
21
25
  let merged = 0;
22
- for (const m of (g.matches ?? []).slice(0, 10)) {
26
+ for (const m of (g ?? []).slice(0, 10)) {
23
27
  if (!seenIds.has(m.id)) {
24
28
  c.matches.push({ id: m.id, score: m.score * THRESHOLDS.graphLaneDiscount, metadata: m.metadata });
25
29
  seenIds.add(m.id);
26
30
  merged++;
27
31
  }
28
32
  }
29
- console.log("graph lane:", g.matches?.length ?? 0, "hits,", merged, "merged");
33
+ console.log("graph lane:", g?.length ?? 0, "hits,", merged, "merged");
30
34
  },
31
35
  };