next: ai part 11

This commit is contained in:
nym21
2026-07-29 10:50:14 +02:00
parent eccbda34ee
commit 1cdee36f2e
56 changed files with 2269 additions and 1037 deletions
+2
View File
@@ -328,6 +328,7 @@ export function createAskPage() {
const {
output,
artifacts = [],
capability,
metricPaths,
apiContext,
sourceContext,
@@ -369,6 +370,7 @@ export function createAskPage() {
content: response,
elapsedMs,
steps,
capability,
metricPaths,
...(apiContext ? { apiContext } : {}),
...(sourceContext?.length ? { sourceContext } : {}),
+7
View File
@@ -43,6 +43,7 @@ const CHART_COLORS = new Set([
* @property {number} [elapsedMs]
* @property {StoredResponseStep[]} [steps]
* @property {StoredArtifact[]} [artifacts]
* @property {string} [capability]
* @property {string[]} [metricPaths]
* @property {ApiContext} [apiContext]
* @property {SourceContext[]} [sourceContext]
@@ -276,6 +277,11 @@ function readMessage(value) {
)
: [];
const rawMetricPaths = message.metricPaths;
const capability =
typeof message.capability === "string" &&
/^[a-z][a-z0-9_]{0,63}$/.test(message.capability)
? message.capability
: undefined;
const hasMetricPaths = Array.isArray(rawMetricPaths);
const metricPaths = hasMetricPaths
? [...new Set(/** @type {string[]} */ (rawMetricPaths.filter(
@@ -295,6 +301,7 @@ function readMessage(value) {
...(elapsedMs !== undefined ? { elapsedMs } : {}),
...(steps.length ? { steps } : {}),
...(artifacts.length ? { artifacts } : {}),
...(capability ? { capability } : {}),
...(hasMetricPaths ? { metricPaths } : {}),
...(apiContext ? { apiContext } : {}),
...(sourceContext.length ? { sourceContext } : {}),
+8 -8
View File
@@ -137,10 +137,13 @@ export function createApiAnswerTool(grounding) {
index,
score: relevance(
grounding.question,
`${field.name} ${field.description ?? ""}`,
`${field.name} ${field.ownDescription || field.description || ""}`,
) +
relevance(grounding.question, field.name) +
relevance(grounding.question, field.ownDescription ?? "") -
relevance(
grounding.question,
field.ownDescription || field.description || "",
) -
Math.max(0, field.name.split(".").length - 1) * 2,
}))
.sort((left, right) => {
@@ -155,16 +158,13 @@ export function createApiAnswerTool(grounding) {
name !== previousName &&
!parameterNames.has(name.split(".").at(-1) ?? name)
);
const numericCandidates = answerCandidates.filter(
({ value }) => typeof value === "number",
);
const best = numericCandidates
const best = answerCandidates
.sort((left, right) => right.score - left.score || left.index - right.index)[0];
const runnerUp = numericCandidates
const runnerUp = answerCandidates
.filter(({ name }) => name !== best?.name)
.sort((left, right) => right.score - left.score || left.index - right.index)[0];
const direct = best && best.score >= 6 &&
best.score >= (runnerUp?.score ?? 0) + 2
best.score >= (runnerUp?.score ?? 0) + 0.5
? best
: undefined;
const siblings = best
+11 -6
View File
@@ -1,4 +1,4 @@
/** @param {unknown} value */
/** @param {unknown} value @returns {value is Record<string, unknown>} */
function isObject(value) {
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
}
@@ -17,16 +17,21 @@ function equalValue(left, right) {
* @param {Record<string, unknown>} [arguments_]
*/
export function focusApiData(data, arguments_ = {}) {
if (!Array.isArray(data)) return data;
if (data.length === 1) return data[0];
const page = isObject(data) &&
typeof data.count === "number" &&
Array.isArray(data.sample)
? data.sample
: data;
if (!Array.isArray(page)) return page;
if (page.length === 1) return page[0];
const supplied = Object.entries(arguments_);
if (!supplied.length) return data;
const matches = data.filter((item) =>
if (!supplied.length) return page;
const matches = page.filter((item) =>
isObject(item) &&
supplied.every(([name, value]) =>
Object.hasOwn(item, name) && equalValue(item[name], value)
)
);
return matches.length === 1 ? matches[0] : data;
return matches.length === 1 ? matches[0] : page;
}
+6 -3
View File
@@ -1,5 +1,5 @@
import { QuickMatch, QuickMatchConfig } from "../../../modules/quickmatch-js/0.5.0/src/index.js";
import { normalize } from "../text.js";
import { normalize, tokenAffinity } from "../text.js";
import { operationsFromOpenApi } from "./openapi.js";
const SEARCH_CANDIDATES = 256;
@@ -116,8 +116,11 @@ function searchOne(index, query, limit) {
const frequency = index.documentFrequency.get(word) ?? index.operations.length;
const idf = Math.log((index.operations.length + 1) / (frequency + 1)) + 1;
specificity += idf;
if (titleTokens.has(word)) titleMatched += 1;
score += idf * (titleTokens.has(word) ? 3 : 1);
const titleMatch = [...titleTokens].some((token) =>
tokenAffinity(word, token) >= 0.75
);
if (titleMatch) titleMatched += 1;
score += idf * (titleMatch ? 3 : 1);
}
return { operation, matched, titleMatched, score, specificity };
})
+112 -41
View File
@@ -7,6 +7,7 @@ import { prewarmApiIndex, terminateApiIndex } from "./api/index.js";
import { prewarmMetricIndex, terminateMetricIndex } from "./metrics/index.js";
import { renderEvidence } from "./render.js";
import { AskToolSession } from "./session/index.js";
import { arithmeticAnswer } from "./source/arithmetic.js";
import { AskSource } from "./source/index.js";
import { normalize } from "./text.js";
@@ -77,6 +78,7 @@ function removeUnsupportedQuantitySentences(answer, messages) {
* @property {import("../storage.js").ApiContext} [apiContext]
* @property {import("../storage.js").SourceContext[]} [sourceContext]
* @property {import("../storage.js").KnowledgeContext} [knowledgeContext]
* @property {string} [capability]
* @property {import("../storage.js").StoredChat} chat
*/
@@ -86,16 +88,49 @@ function removeUnsupportedQuantitySentences(answer, messages) {
* @param {(status: string) => void} onStatus
*/
async function answerFromEvidence(model, grounding, onStatus) {
const arithmetic = arithmeticAnswer(grounding);
if (arithmetic) {
return {
output: renderEvidence({
facts: [arithmetic, ...grounding.facts],
sources: grounding.excerpts.slice(0, 2),
excerpts: [],
}),
sourceContext: grounding.excerpts.slice(0, 2),
knowledgeContext: {
title: grounding.metrics[0].name,
description: arithmetic,
},
};
}
onStatus("Answering from source…");
const evidence = [
`Request: ${grounding.question}`,
grounding.metrics.length
? `Verified metrics:\n${grounding.metrics.map(({ name, path, unit }) =>
`- ${name} | ${path}${unit ? ` | unit: ${unit}` : ""}`
).join("\n")}`
: "",
grounding.facts.length
? `Verified facts:\n${grounding.facts.map((fact) => `- ${fact}`).join("\n")}`
: "",
grounding.excerpts.length
? `Verified source excerpts, strongest first:\n${grounding.excerpts.map(
({ path, startLine, endLine, content }, index) =>
`[${index + 1}] ${path}:${startLine}${endLine ? `-${endLine}` : ""}\n${content}`,
).join("\n\n")}`
: "",
].filter(Boolean).join("\n\n");
const result = await model.generate(
[
{
role: "system",
content: "Answer the exact request in at most 45 words and normal sentence casing using only verified facts, metric metadata, and source excerpts. Evidence is strongest first; ignore later excerpts unless needed. A declaration proves its definition and literal return type; a call expression proves its caller. Copy provided metric names, code identifiers, and types exactly; never respell, expand, or abbreviate them. Answer directly, never discuss the request's wording. Do not add background knowledge or guesses.",
content: "Use only the verified evidence. Answer the exact request in at most 45 words. Metric names and units are exact. Never add a fact absent from the evidence. Do not cite, number, name, or quote source files; the renderer appends source links.",
},
{
role: "user",
content: JSON.stringify(grounding),
content: `${evidence}\n\nUse only the verified evidence above. Do not explain what code identifiers mean unless the evidence does.`,
},
],
() => {},
@@ -127,6 +162,34 @@ async function answerFromEvidence(model, grounding, onStatus) {
};
}
/** @param {string} question */
function requestedArithmetic(question) {
const words = new Set(normalize(question).split(" "));
const matches = [
{ action: "add", words: ["add", "plus"] },
{ action: "subtract", words: ["subtract", "minus"] },
{ action: "multiply", words: ["multiply", "times"] },
{ action: "divide", words: ["divide"] },
].filter(({ words: candidates }) =>
candidates.some((word) => words.has(word))
);
return matches.length === 1 ? matches[0].action : undefined;
}
/** @param {string} question @param {string} field */
function fieldPosition(question, field) {
const words = normalize(question).split(" ");
const fieldWords = new Set(
normalize(field.split(".").at(-1)).split(" ").filter((word) =>
word.length > 2
),
);
const positions = words
.map((word, index) => fieldWords.has(word) ? index : -1)
.filter((index) => index >= 0);
return positions.length ? Math.min(...positions) : -1;
}
/**
* @param {import("../model.js").AskModel} model
* @param {NonNullable<ToolOutcome["apiGrounding"]>} grounding
@@ -134,7 +197,9 @@ async function answerFromEvidence(model, grounding, onStatus) {
*/
async function answerFromApi(model, grounding, onStatus) {
const apiAnswer = createApiAnswerTool(grounding);
const question = ` ${normalize(grounding.question)} `;
const normalizedQuestion = normalize(grounding.question);
const question = ` ${normalizedQuestion} `;
const arithmetic = requestedArithmetic(grounding.question);
const parameterNames = new Set(
grounding.operation.parameters.map(({ name }) => normalize(name)),
);
@@ -149,37 +214,52 @@ async function answerFromApi(model, grounding, onStatus) {
const name = normalize(field.name.split(".").at(-1));
return name && question.includes(` ${name} `);
});
const requestTokens = new Set(
normalize(grounding.question).split(" ").filter((token) => token.length > 2),
);
const canSelectDirectly = (/** @type {typeof apiAnswer.fields[number]} */ field) => {
const ownTokens = new Set(normalize(field.name).split(" "));
const qualifiers = [...requestTokens].filter((token) => !ownTokens.has(token));
return !apiAnswer.fields.some((candidate) =>
candidate.name !== field.name &&
qualifiers.some((token) =>
normalize(`${candidate.name} ${candidate.description ?? ""}`)
.split(" ")
.includes(token)
)
);
};
if (directFields.length === 1 && canSelectDirectly(directFields[0])) {
const field = directFields[0];
if (arithmetic && apiAnswer.previous && directFields.length === 1) {
const previous = apiAnswer.previous;
const current = directFields[0];
const previousPosition = fieldPosition(grounding.question, previous.name);
const currentPosition = fieldPosition(grounding.question, current.name);
const fromPosition = normalizedQuestion.split(" ").indexOf("from");
const reverseSubtract = arithmetic === "subtract" &&
fromPosition >= 0 &&
previousPosition >= 0 &&
previousPosition < fromPosition &&
currentPosition > fromPosition;
const [left, right] = reverseSubtract
? [current, previous]
: previousPosition >= 0 &&
currentPosition >= 0 &&
currentPosition < previousPosition
? [current, previous]
: [previous, current];
const label = `${left.name.split(".").at(-1)?.replaceAll("_", " ")} ${
arithmetic === "add"
? "plus"
: arithmetic === "subtract"
? "minus"
: arithmetic === "multiply"
? "times"
: "divided by"
} ${right.name.split(".").at(-1)?.replaceAll("_", " ")}`;
return {
output: finishApiAnswer(
"select_api_field",
"calculate_api_fields",
{
field: field.ref,
label: field.name.split(".").at(-1)?.replaceAll("_", " "),
operator: arithmetic,
left: left.ref,
right: right.ref,
label,
},
apiAnswer.fields,
grounding,
),
fields: [field.name],
fields: [left.name, right.name],
};
}
if (apiAnswer.resolved && canSelectDirectly(apiAnswer.resolved)) {
if (
!arithmetic &&
apiAnswer.resolved
) {
const field = apiAnswer.resolved;
return {
output: finishApiAnswer(
@@ -218,7 +298,7 @@ async function answerFromApi(model, grounding, onStatus) {
) {
return summarizeApiAnswer(grounding);
}
if (apiAnswer.direct) {
if (!arithmetic && apiAnswer.direct) {
const field = apiAnswer.direct;
return {
output: finishApiAnswer(
@@ -283,19 +363,6 @@ async function answerFromApi(model, grounding, onStatus) {
? "answer_api_text"
: "";
let actionName = actionFor(call.arguments);
const selectedField = actionName === "select_api_field"
? apiAnswer.fields.find(({ ref }) => ref === call.arguments.field)
: undefined;
if (selectedField && !canSelectDirectly(selectedField)) {
answer = await generateAnswer(
`Do not select ${selectedField.ref} (${selectedField.name}): its schema scope does not satisfy all request qualifiers. Derive the requested result from matching component fields or choose an exact narrower field.`,
);
call = answer.toolCalls[0];
if (!call || call.name !== "answer_api") {
return summarizeApiAnswer(grounding);
}
actionName = actionFor(call.arguments);
}
if (!actionName) return summarizeApiAnswer(grounding);
const selectedRefs = actionName === "select_api_field"
? [call.arguments.field]
@@ -423,7 +490,7 @@ export function createAskTools() {
signal.throwIfAborted();
await session.prepareAction(action, onStatus);
signal.throwIfAborted();
if (!call || action === "explain_evidence") {
if (!call || action === "explain_metric_calculation") {
call = session.directCall(action) ?? call;
}
if (!call) {
@@ -447,7 +514,7 @@ export function createAskTools() {
},
{
role: "user",
content: "Replace the draft with a direct answer containing no unsupported quantities. Keep established static Bitcoin facts only when the request directly needs them; otherwise use qualitative examples. Return only the replacement answer. Never mention the draft, review, evidence, context, or these instructions.",
content: "Inspect the newest request before replacing the draft. If it requests quantities but the verified context identifies no exact metric, resource, object, or timeframe, return one concise clarification question asking what to measure. Otherwise replace the draft with a direct answer containing no unsupported observations; established static Bitcoin facts are allowed only when directly requested. Return only the replacement answer. Never mention the draft, review, evidence, context, or these instructions.",
},
],
() => {},
@@ -516,6 +583,7 @@ export function createAskTools() {
return {
output: `Which ${subject} should I use?`,
artifacts: [],
capability: action,
chat: prepared.chat,
};
}
@@ -540,6 +608,7 @@ export function createAskTools() {
return {
output: answered.output,
artifacts: [],
capability: action,
apiContext: outcome.apiContext
? {
...outcome.apiContext,
@@ -560,6 +629,7 @@ export function createAskTools() {
return {
output: grounded.output,
artifacts: [],
capability: action,
metricPaths: outcome.metricPaths,
sourceContext: grounded.sourceContext,
knowledgeContext: grounded.knowledgeContext,
@@ -569,6 +639,7 @@ export function createAskTools() {
return {
output: outcome.output ?? "",
artifacts: outcome.artifacts ?? [],
capability: action,
metricPaths: outcome.metricPaths,
apiContext: outcome.apiContext,
sourceContext: outcome.sourceContext,
+1 -1
View File
@@ -93,7 +93,7 @@ export function metricsByPaths(paths, onProgress) {
return index.request("byPaths", { paths }, onProgress);
}
/** @param {{ name: string }} metric @param {string} query @returns {Promise<{ totalSeries: number, groups: { family: string, examples: string[] }[], series: (CatalogMetric & { selector: string, matchedTerms: number })[] } | undefined>} */
/** @param {{ name: string }} metric @param {string} query @returns {Promise<{ totalSeries: number, groups: { family: string, examples: string[] }[], series: (CatalogMetric & { selector: string, matchedTerms: number, specificity: number })[] } | undefined>} */
export function metricVariants(metric, query = "") {
return index.request("variants", {
name: metric.name,
+60 -30
View File
@@ -258,26 +258,6 @@ function variants(index, name, path, query) {
}
if (candidates.length <= 1) return undefined;
const preferredPaths = new Map(
index.matcher.matchesWith(searchable(query), index.config.withLimit(SEARCH_CANDIDATES))
.map((document, rank) => [index.byDocument.get(document)?.path, rank]),
);
const queryTerms = queryVocabulary(query);
const ranked = candidates
.map((candidate) => ({
...publicMetric(candidate),
rank: preferredPaths.get(candidate.path) ?? SEARCH_CANDIDATES,
queryMatches: searchable(candidate.path)
.split(" ")
.filter((token) => queryTerms.has(token)).length,
}))
.sort((left, right) =>
right.queryMatches - left.queryMatches ||
Number(right.name === name) - Number(left.name === name) ||
left.rank - right.rank ||
left.path.localeCompare(right.path)
);
let commonSuffix = candidates[0].path.split(".");
for (const candidate of candidates.slice(1)) {
const path = candidate.path.split(".");
@@ -290,7 +270,7 @@ function variants(index, name, path, query) {
commonSuffix = count ? commonSuffix.slice(-count) : [];
}
const selectors = ranked.map((candidate) => {
const selectors = candidates.map((candidate) => {
const path = candidate.path.split(".");
return commonSuffix.length ? path.slice(0, -commonSuffix.length) : path;
});
@@ -305,8 +285,52 @@ function variants(index, name, path, query) {
commonPrefix = commonPrefix.slice(0, count);
}
const preferredPaths = new Map(
index.matcher.matchesWith(searchable(query), index.config.withLimit(SEARCH_CANDIDATES))
.map((document, rank) => [index.byDocument.get(document)?.path, rank]),
);
const queryTerms = queryVocabulary(query);
const selectorTokens = selectors.map((selector) =>
[...new Set(
searchable(selector.slice(commonPrefix.length).join(" "))
.split(" ")
.filter(Boolean),
)]
);
const selectorFrequency = new Map();
for (const tokens of selectorTokens) {
for (const token of tokens) {
selectorFrequency.set(token, (selectorFrequency.get(token) ?? 0) + 1);
}
}
const ranked = candidates
.map((candidate, candidateIndex) => {
const matches = selectorTokens[candidateIndex].filter((token) =>
queryTerms.has(token)
);
return {
...publicMetric(candidate),
selector: selectors[candidateIndex],
rank: preferredPaths.get(candidate.path) ?? SEARCH_CANDIDATES,
queryMatches: matches.length,
specificity: matches.reduce((sum, token) =>
sum +
Math.log(
(candidates.length + 1) /
((selectorFrequency.get(token) ?? candidates.length) + 1),
) + 1, 0),
};
})
.sort((left, right) =>
right.specificity - left.specificity ||
right.queryMatches - left.queryMatches ||
Number(right.name === name) - Number(left.name === name) ||
left.rank - right.rank ||
left.path.localeCompare(right.path)
);
const groups = new Map();
for (const selector of selectors) {
for (const { selector } of ranked) {
const varying = selector.slice(commonPrefix.length);
const family = varying[0] ?? commonPrefix.at(-1) ?? "root";
const value = varying.slice(1).join(" / ") || varying[0] || "all";
@@ -320,13 +344,20 @@ function variants(index, name, path, query) {
totalSeries: ranked.length,
groups: [...groups.values()].slice(0, 8),
series: ranked.slice(0, 16).map((
{ path, name: metricName, suggestedUnit, indexes, type },
index,
{
path,
name: metricName,
suggestedUnit,
indexes,
type,
selector: selectorParts,
queryMatches,
specificity,
},
) => {
const selector = selectors[index]
const selector = selectorParts
.slice(commonPrefix.length)
.join(" ") || selectors[index].at(-1) || "";
const selectorTokens = new Set(searchable(selector).split(" "));
.join(" ") || selectorParts.at(-1) || "";
return {
path,
name: metricName,
@@ -334,9 +365,8 @@ function variants(index, name, path, query) {
indexes,
type,
selector,
matchedTerms: [...selectorTokens].filter((token) =>
token && queryTerms.has(token)
).length,
matchedTerms: queryMatches,
specificity,
};
}),
};
+34 -15
View File
@@ -87,11 +87,11 @@ export function availableActions(evidence) {
"read_metric_at",
"read_metric_range",
"build_metric_chart",
"list_metric_variants",
"list_metric_cohorts_variants",
);
}
if (evidence.guideOptions.length || evidence.metricOptions.length) {
actions.push("explain_evidence");
actions.push("explain_metric_calculation");
}
if (!evidence.metricOptions.length) actions.push("find_chart_metrics");
actions.push("search_source");
@@ -109,9 +109,9 @@ const ROUTE_DESCRIPTIONS = {
read_metric_at: "Choose when the requested result is a metric value at a stated block height, date, or position.",
read_metric_range: "Choose when the requested result is metric values across a stated range.",
build_metric_chart: "Choose when the requested result is a new metric chart or graph.",
list_metric_variants: "Choose only when the requested result is a list of available cohorts, groupings, or series variants.",
list_metric_cohorts_variants: "Choose only when the requested result is a list of available cohorts, groupings, or series variants.",
select_metric_variant: "Choose when the request selects one matched cohort or series variant without requesting a value or chart yet.",
explain_evidence: "Choose for a Bitview metric definition grounded in matched metric evidence.",
explain_metric_calculation: "Choose for a Bitview metric definition grounded in matched metric evidence.",
find_chart_metrics: "Choose when the user asks which chart metrics exist but no exact metric matched yet.",
search_source: "Choose for a question about BRK repository code, implementation, callers, or source structure.",
call_api: "Choose for a concrete blockchain record or resource when a generated operation can accept the supplied or contextual identifier.",
@@ -152,7 +152,7 @@ export function directAction(evidence, question) {
for (const [actionTerm, actionsForTerm] of owners) {
if (
actionsForTerm.length === 1 &&
tokenAffinity(queryTerm, actionTerm) >= 0.75
tokenAffinity(queryTerm, actionTerm) >= 0.68
) {
matched.add(actionsForTerm[0]);
}
@@ -163,6 +163,23 @@ export function directAction(evidence, question) {
const variants = evidence.metricOptions.filter(
(/** @type {any} */ { origin }) => origin === "variant",
);
const mentioned = evidence.metricOptions.filter(
(/** @type {any} */ { origin }) => origin === "mentioned",
);
if (variants.length === 1 && matched.size > 1) {
const resultActions = [...matched].filter((action) =>
action !== "list_metric_cohorts_variants" &&
action !== "select_metric_variant"
);
if (resultActions.length === 1) return resultActions[0];
}
if (
matched.size === 0 &&
variants.length + mentioned.length === 1 &&
evidence.context.capability === "read_latest_metric"
) {
return evidence.context.capability;
}
return matched.size === 0 && variants.length === 1
? "select_metric_variant"
: undefined;
@@ -192,12 +209,14 @@ export function capabilityMetrics(evidence, action) {
(/** @type {any} */ { origin }) => origin === "variant",
);
const options = mentioned.length
? [...new Map(
[...mentioned, ...contextual].map((option) => [
option.metric.path,
option,
]),
).values()]
? action === "build_metric_chart"
? [...new Map(
[...mentioned, ...contextual].map((option) => [
option.metric.path,
option,
]),
).values()]
: mentioned
: variants.length
? action === "build_metric_chart" && contextual.length
? [...variants, ...contextual]
@@ -380,7 +399,7 @@ export function actionTool(evidence, action) {
["refs", ...(asksContextDecision ? ["includeContext"] : [])],
);
}
if (action === "list_metric_variants") {
if (action === "list_metric_cohorts_variants") {
return tool(
action,
"Select the one metric whose source-derived variants were requested.",
@@ -396,7 +415,7 @@ export function actionTool(evidence, action) {
["refs"],
);
}
if (action === "explain_evidence") {
if (action === "explain_metric_calculation") {
const evidenceOptions = [...sourceOptions, ...guideOptions];
return tool(
action,
@@ -497,7 +516,7 @@ export function apiArgumentTool(operation) {
export const ROUTE_INSTRUCTION = `Choose one capability for the newest request from verified context and matches. Treat context.activeCapability as the active tool mode: continue it for an elliptical follow-up unless the newest request clearly selects a different available output.
The requested output wins: edit an active chart with its edit/style capability; otherwise choose the matching chart, latest-value, historical-value, range, variant-list, or variant-selection capability.
Use call_api for a concrete blockchain resource or its contextual follow-up, explain_evidence for a metric definition, find_chart_metrics to discover real chart series when none matched yet, describe_capabilities only for a request about the assistant itself, and answer_general for ordinary Bitcoin knowledge or conversation.
Use call_api for a concrete blockchain resource or its contextual follow-up, explain_metric_calculation for a metric definition, find_chart_metrics to discover real chart series when none matched yet, describe_capabilities only for a request about the assistant itself, and answer_general for ordinary Bitcoin knowledge or conversation.
Use search_source only when the request explicitly asks about BRK repository code, source location, implementation, or callers. Never choose it merely because source matches exist.
Use clarify when essential information is missing. In particular, a requested quantitative result without a matched metric, API resource, or quantitative context needs one concise clarification instead of a qualitative answer or guessed dataset. With call_api select apiRef. With search_source provide sourceQuery.
Call choose_capability exactly once.`;
@@ -524,7 +543,7 @@ export function actionInstruction(action) {
if (action === "set_chart_view_scale") {
return `${common} Apply only the explicitly requested view or scale.`;
}
if (action === "explain_evidence") {
if (action === "explain_metric_calculation") {
return `${common} Select the one excerpt that directly answers the request. For a metric definition, prefer its computation or formula over UI configuration, imports, aggregation, or downstream usage. Select matching metrics when the request is about a metric.`;
}
if (action === "search_source") {
@@ -50,6 +50,7 @@ export async function loadSessionContext(history, onProgress) {
return {
...(chart ? { chart } : {}),
...(message.capability ? { capability: message.capability } : {}),
metrics: metrics.filter(({ path }) => activePaths.includes(path)),
recentMetrics: metrics.filter(({ path }) => recentPaths.includes(path)),
...(operation
+121 -11
View File
@@ -10,12 +10,15 @@ import { normalize } from "../text.js";
import {
explicitArguments,
hasRequiredArguments,
reusableArguments,
} from "../api/routing.js";
const MAX_METRICS = 5;
const MAX_API = 4;
const MAX_API = 6;
const MAX_API_CANDIDATES = 64;
const MAX_SOURCE = 6;
const MAX_GUIDES = 2;
const MAX_SOURCE_SCHEMA_QUERIES = 2;
/** @param {string} value */
function label(value) {
@@ -33,13 +36,90 @@ function unique(values, key) {
});
}
/** @param {unknown} value */
function searchTerms(value) {
return new Set(
normalize(value).split(" ").filter((term) => term.length >= 3),
);
}
/**
* Turn generated response-schema matches into source-level symbols. A nested
* response field already tells us both its owning Rust type and its field
* name, so source lookup can be precise without maintaining API-to-code maps.
*
* @param {string} question
* @param {import("../api/index.js").ApiOperation[]} operations
*/
export function schemaSourceQueries(question, operations) {
const query = searchTerms(question);
if (!query.size) return [];
const documents = operations.flatMap((operation) =>
operation.response.fields.map((field, index) => ({
operation,
field,
index,
names: searchTerms(field.name),
terms: searchTerms(
`${operation.summary} ${field.name} ${field.ownDescription}`,
),
}))
);
const frequency = new Map();
for (const { terms } of documents) {
for (const term of terms) {
frequency.set(term, (frequency.get(term) ?? 0) + 1);
}
}
const candidates = documents.map(
({ operation, field, index, names, terms }) => {
const matched = [...query].filter((term) => terms.has(term));
const named = matched.filter((term) => names.has(term)).length;
const parts = field.name.split(".");
const parentName = parts.slice(0, -1).join(".");
const parent = parentName
? operation.response.fields.find(({ name }) => name === parentName)
: undefined;
const owner = parent?.type || operation.response.type;
const fieldName = parts.at(-1) ?? field.name;
return {
query: `${owner} ${fieldName}`,
matched: matched.length,
named,
specificity: matched.reduce((sum, term) =>
sum + Math.log(
(documents.length + 1) /
((frequency.get(term) ?? documents.length) + 1),
) + 1, 0),
rank: Number(operation.score ?? 0),
index,
};
},
)
.filter(({ matched, named }) => matched >= 2 && named > 0)
.sort((left, right) =>
right.specificity - left.specificity ||
right.matched - left.matched ||
right.rank - left.rank ||
left.index - right.index
);
const strongest = candidates[0]?.specificity ?? 0;
return unique(
candidates
.filter(({ specificity }) => specificity === strongest)
.map(({ query }) => query),
(query) => normalize(query),
).slice(0, MAX_SOURCE_SCHEMA_QUERIES);
}
/** @param {any} metric */
function acceptsMetric(metric) {
return Number(metric.matchedTerms ?? 0) >= 2;
}
/** @param {any} operation @param {string} question */
function acceptsApi(operation, question) {
/** @param {any} operation @param {string} question @param {any} previous */
function acceptsApi(operation, question, previous) {
const specificity = Number(operation.specificity ?? 0);
const required = operation.parameters.some(
(/** @type {any} */ parameter) => parameter.required,
@@ -48,8 +128,11 @@ function acceptsApi(operation, question) {
return Number(operation.titleMatchedTerms ?? 0) > 0 &&
specificity >= 2.5;
}
return specificity >= 1.5 &&
hasRequiredArguments(operation, explicitArguments(operation, question));
return Number(operation.titleMatchedTerms ?? 0) > 0 &&
(
hasRequiredArguments(operation, explicitArguments(operation, question)) ||
Boolean(reusableArguments(operation, previous))
);
}
/** @param {any} match */
@@ -107,7 +190,7 @@ export async function collectEvidence({
),
searchApi(
[question],
MAX_API,
MAX_API_CANDIDATES,
() => onStatus("Indexing API…"),
),
searchLearn(question, MAX_GUIDES),
@@ -133,13 +216,16 @@ export async function collectEvidence({
origin: explicit ? "mentioned" : "search",
};
});
const variantMetrics = (await Promise.all(
const variantResults = await Promise.all(
context.metrics.map((/** @type {any} */ metric) =>
metricVariants(metric, question)
),
)).flatMap((variants) =>
)
);
const variantMetrics = variantResults.flatMap((variants) =>
variants?.series
.filter((/** @type {any} */ { matchedTerms }) => matchedTerms > 0)
.filter((/** @type {any} */ { specificity }) =>
Number(specificity ?? 0) >= 3
)
.map((/** @type {any} */ metric) => {
const name = label(metric.name);
const selector = label(metric.selector);
@@ -152,6 +238,11 @@ export async function collectEvidence({
};
}) ?? []
);
const variantMiss = context.metrics.length > 0 &&
variantResults.some(Boolean) &&
variantMetrics.length === 0 &&
!linkedMetrics.some(({ origin }) => origin === "mentioned") &&
foundMetrics.some(acceptsMetric);
const metrics = unique(
[
@@ -173,7 +264,16 @@ export async function collectEvidence({
const api = unique(
[
...(context.api ? [context.api.operation] : []),
...foundApi.filter((operation) => acceptsApi(operation, question)),
...foundApi
.filter((operation) => acceptsApi(operation, question, context.api))
.sort((left, right) =>
Number(right.titleMatchedTerms ?? 0) -
Number(left.titleMatchedTerms ?? 0) ||
right.response.fields.length - left.response.fields.length ||
Number(right.matchedTerms ?? 0) -
Number(left.matchedTerms ?? 0) ||
Number(right.score ?? 0) - Number(left.score ?? 0)
),
],
(operation) => operation.key,
).slice(0, MAX_API);
@@ -222,9 +322,11 @@ export async function collectEvidence({
return {
metricOptions,
apiOptions,
apiCandidates: foundApi,
sourceOptions,
guideOptions,
context,
variantMiss,
};
}
@@ -250,6 +352,10 @@ export async function collectSourceOptions({
const metricSubject = metric
? await sourceMetricSubject(metric)
: undefined;
const schemaQueries = schemaSourceQueries(
question,
evidence.apiCandidates ?? [],
);
const queries = [
...(metricSubject
? [{
@@ -260,6 +366,10 @@ export async function collectSourceOptions({
focus: /** @type {const} */ ("implementation"),
}]
: []),
...schemaQueries.map((query) => ({
query,
focus: /** @type {const} */ ("implementation"),
})),
{ query: question, focus: undefined },
].filter((value, index, values) =>
values.findIndex((candidate) =>
+77 -13
View File
@@ -5,6 +5,8 @@ import { resolveChartUnit } from "../chart/units.js";
import { readMetric } from "../data.js";
import { metricVariants, searchMetrics } from "../metrics/index.js";
import { renderData } from "../render.js";
import { normalize } from "../text.js";
import { schemaSourceQueries } from "./evidence.js";
const CHART_VIEWS = new Set(["line", "area", "stacked", "bar", "dots"]);
const CHART_SCALES = new Set(["linear", "log"]);
@@ -31,7 +33,7 @@ function uniqueRefs(value) {
/** @param {string} value */
function label(value) {
return value.replaceAll("_", " ");
return normalize(value);
}
/** @param {string | undefined} value */
@@ -51,6 +53,42 @@ function sourceSubject(value) {
);
}
/** @param {string} content @param {string} field */
function computesField(content, field) {
return content.split("\n").some((line) => {
const normalized = normalize(line);
return normalized.includes(field) &&
["+=", "-=", "*=", "/=", " + ", " - ", " * ", " / "]
.some((operator) => line.includes(operator));
});
}
/** @param {any} result @param {string} query */
function rankedSchemaMatches(result, query) {
const parts = query.trim().split(/\s+/);
const field = normalize(parts.at(-1) ?? "");
const owner = normalize(parts.slice(0, -1).join(" "));
const terms = normalize(query).split(" ").filter(Boolean);
return [...result.matches].sort((left, right) => {
const leftContent = normalize(left.content);
const rightContent = normalize(right.content);
const leftOwner = owner && leftContent.includes(owner) ? 1 : 0;
const rightOwner = owner && rightContent.includes(owner) ? 1 : 0;
const leftComputes = leftOwner && computesField(left.content, field) ? 1 : 0;
const rightComputes = rightOwner && computesField(right.content, field) ? 1 : 0;
const leftPath = terms.filter((term) =>
normalize(left.path).split(" ").includes(term)
).length;
const rightPath = terms.filter((term) =>
normalize(right.path).split(" ").includes(term)
).length;
return rightComputes - leftComputes ||
rightOwner - leftOwner ||
rightPath - leftPath ||
Number(right.score ?? 0) - Number(left.score ?? 0);
});
}
export class CapabilityExecutor {
/**
* @param {Object} options
@@ -149,6 +187,10 @@ export class CapabilityExecutor {
/** @param {Record<string, unknown>} arguments_ @param {(status: string) => void} onStatus */
async searchSource(arguments_, onStatus) {
const query = requiredString(arguments_.query, "a source search query");
const schemaQueries = schemaSourceQueries(
this.question,
this.evidence.apiCandidates ?? [],
);
const subject = sourceSubject(
this.evidence.context.knowledge?.description,
);
@@ -166,32 +208,47 @@ export class CapabilityExecutor {
? `${query} ${subject}`
: query;
const searches = [
{ query, path: undefined },
{ query, path: undefined, focus: undefined },
...schemaQueries.map((schemaQuery) => ({
query: schemaQuery,
path: undefined,
focus: /** @type {const} */ ("implementation"),
})),
...(contextualQuery === query
? []
: [{ query: contextualQuery, path: undefined }]),
...(subject ? [{ query: subject, path: undefined }] : []),
...paths.map((path) => ({ query: contextualQuery, path })),
: [{ query: contextualQuery, path: undefined, focus: undefined }]),
...(subject
? [{ query: subject, path: undefined, focus: undefined }]
: []),
...paths.map((path) => ({
query: contextualQuery,
path,
focus: undefined,
})),
];
onStatus("Searching source…");
const results = await Promise.all(
searches.map(({ query: scopedQuery, path }) =>
searches.map(({ query: scopedQuery, path, focus }) =>
this.source.search(
scopedQuery,
path,
undefined,
focus,
({ loaded, total }) =>
onStatus(`Indexing source · ${loaded} / ${total}`),
)
),
);
const scopedResults = results.filter((_, index) => searches[index].path);
const schemaResults = results.slice(1, 1 + schemaQueries.length);
const contextualIndex = 1 + schemaQueries.length;
const contextualResult = contextualQuery === query
? undefined
: results[1];
: results[contextualIndex];
const subjectResult = subject
? results[contextualQuery === query ? 1 : 2]
? results[
contextualIndex + (contextualQuery === query ? 0 : 1)
]
: undefined;
const rawResult = results[0];
const seeded = this.evidence.sourceOptions.map(
@@ -199,6 +256,13 @@ export class CapabilityExecutor {
);
const excerpts = [...new Map([
...(paths.length ? [] : seeded),
...schemaResults.flatMap((result, index) =>
rankedSchemaMatches(result, schemaQueries[index]).slice(0, 2)
.map((/** @type {any} */ match) => ({
...match,
revision: result.revision,
}))
),
...scopedResults.flatMap((result) =>
result.matches.slice(0, 1).map((/** @type {any} */ match) => ({
...match,
@@ -261,8 +325,8 @@ export class CapabilityExecutor {
const groups = variants.groups
.map((group) =>
group.examples.length === 1 && group.examples[0] === group.family
? group.family
: `${group.family}: ${group.examples.join(", ")}`
? label(group.family)
: `${label(group.family)}: ${group.examples.map(label).join(", ")}`
)
.join("; ");
return {
@@ -491,13 +555,13 @@ export class CapabilityExecutor {
return this.describeCapabilities(call.arguments);
}
if (call.name === "clarify") return this.clarify(call.arguments);
if (call.name === "explain_evidence") {
if (call.name === "explain_metric_calculation") {
return await this.explain(call.arguments);
}
if (call.name === "search_source") {
return await this.searchSource(call.arguments, onStatus);
}
if (call.name === "list_metric_variants") {
if (call.name === "list_metric_cohorts_variants") {
return await this.listVariants(call.arguments);
}
if (call.name === "find_chart_metrics") {
+83 -31
View File
@@ -31,8 +31,8 @@ function schemaTokens(values) {
}
/** @param {Set<string>} query @param {Set<string>} document */
function overlaps(query, document) {
return [...query].some((token) => document.has(token));
function overlapCount(query, document) {
return [...query].filter((token) => document.has(token)).length;
}
export class AskToolSession {
@@ -107,7 +107,8 @@ export class AskToolSession {
? "source"
: context.knowledge
? "general"
: undefined,
: undefined,
previousCapability: context.capability,
...(context.chart
? {
activeChart: {
@@ -186,8 +187,47 @@ export class AskToolSession {
directRoute() {
if (!this.evidence) return undefined;
if (this.evidence.variantMiss) {
const metric = this.evidence.context.metrics[0];
return {
action: "clarify",
call: {
name: "clarify",
arguments: {
question: `I could not find a matching variant of ${
normalize(metric?.name ?? "the active metric").replaceAll("_", " ")
}. Which available cohort or variant should I use?`,
},
},
};
}
const action = directAction(this.evidence, this.question);
if (action === "search_source") {
if (this.evidence.context.source.length) return undefined;
return {
action,
call: {
name: action,
arguments: { query: this.question },
},
};
}
if (action) {
return {
action,
call: this.directCall(action),
};
}
if (
this.evidence.context.metrics.length ||
this.evidence.context.chart
) {
return undefined;
}
const query = schemaTokens([this.question]);
const contextKey = this.evidence.context.api?.operation.key;
const apiMatches = [];
for (const { ref, operation } of this.evidence.apiOptions) {
const required = schemaTokens(
operation.parameters
@@ -203,44 +243,56 @@ export class AskToolSession {
field.description,
]),
);
const fieldMatch = overlaps(query, returned);
const suppliedResource = required.size > 0 && overlaps(query, required);
const fieldMatches = overlapCount(query, returned);
const suppliedResource = required.size > 0 &&
overlapCount(query, required) > 0;
const inheritedResource = reusableArguments(
operation,
this.evidence.context.api,
);
if (
fieldMatch &&
(suppliedResource || operation.key === contextKey)
fieldMatches > 0 &&
(suppliedResource || inheritedResource || operation.key === contextKey)
) {
return {
apiMatches.push({
score: fieldMatches,
action: "call_api",
call: {
name: "call_api",
arguments: { ref },
},
};
});
}
}
const action = directAction(this.evidence, this.question);
if (action === "search_source") {
return {
action,
call: {
name: action,
arguments: { query: this.question },
},
};
apiMatches.sort((left, right) => right.score - left.score);
if (
apiMatches[0] &&
apiMatches[0].score > (apiMatches[1]?.score ?? 0)
) {
return apiMatches[0];
}
if (action) {
return {
action,
call: this.directCall(action),
};
}
return undefined;
const supplied = this.evidence.apiOptions.find(({ operation }) =>
hasRequiredArguments(
operation,
explicitArguments(operation, this.question),
)
);
return supplied
? {
action: "call_api",
call: {
name: "call_api",
arguments: { ref: supplied.ref },
},
}
: undefined;
}
/** @param {string} action @param {(status: string) => void} onStatus */
async prepareAction(action, onStatus) {
if (
action !== "explain_evidence" ||
action !== "explain_metric_calculation" ||
!this.evidence ||
!this.refs
) return;
@@ -318,7 +370,7 @@ export class AskToolSession {
};
}
if (action === "list_metric_variants") {
if (action === "list_metric_cohorts_variants") {
const variants = evidence.metricOptions.filter(
({ origin }) => origin === "variant",
);
@@ -355,7 +407,7 @@ export class AskToolSession {
}
}
if (action !== "explain_evidence") return undefined;
if (action !== "explain_metric_calculation") return undefined;
const { context, metricOptions, sourceOptions, guideOptions } = evidence;
const contextual = context.metrics[0]
? metricOptions.find(({ metric }) =>
@@ -403,9 +455,9 @@ export class AskToolSession {
"add_chart_series",
"remove_chart_series",
"replace_chart_series",
"list_metric_variants",
"list_metric_cohorts_variants",
"select_metric_variant",
"explain_evidence",
"explain_metric_calculation",
].includes(action)) {
evidence.metrics = actionMetrics.map(
(/** @type {any} */ { ref, label, metric, origin }) => ({
@@ -421,7 +473,7 @@ export class AskToolSession {
}),
);
}
if (action === "explain_evidence" || action === "search_source") {
if (action === "explain_metric_calculation" || action === "search_source") {
evidence.source = sourceOptions.map(
(/** @type {any} */ { ref, source }) => ({
ref,
+134
View File
@@ -0,0 +1,134 @@
import { normalize } from "../text.js";
const COMPOUND_ASSIGNMENT = /^(.+?)\s*(\+=|-=|\*=|\/=)\s*(.+?);?\s*$/;
const LOCAL_ASSIGNMENT =
/^\s*(?:let|const|var)\s+([A-Za-z_]\w*)(?:\s*:[^=]+)?\s*=\s*(.+?);?\s*$/;
const COMMENT_FORMULA =
/^\s*(?:\/\/\/?|#|\*)?\s*([A-Za-z_]\w*)\s*=\s*(.+?)\s*$/;
/** @param {string} value */
function metricTokens(value) {
return normalize(value).split(" ").filter((token) => token.length > 2);
}
/** @param {string} line @param {string[]} tokens */
function overlap(line, tokens) {
const words = new Set(normalize(line).split(" "));
return tokens.filter((token) => words.has(token)).length;
}
/** @param {string} value */
function cleanExpression(value) {
let cleaned = value
.replace(/\.as_u\d+\(\)/g, "")
.replace(/\bself\./g, "")
.replace(/\b([A-Za-z_]\w*)_u\d+\b/g, "$1")
.replace(/\s*\*\s*/g, " × ")
.replace(/\s*\/\s*/g, " ÷ ")
.replace(/\s+/g, " ")
.replace(/;$/, "")
.trim();
while (/\(\(([^()]+)\)\)/.test(cleaned)) {
cleaned = cleaned.replace(/\(\(([^()]+)\)\)/g, "($1)");
}
return cleaned;
}
/** @param {string} expression @param {string[]} preceding */
function expandLocals(expression, preceding) {
let expanded = expression;
for (let pass = 0; pass < 2; pass += 1) {
const identifiers = new Set(expanded.match(/\b[A-Za-z_]\w*\b/g) ?? []);
let changed = false;
for (const line of [...preceding].reverse()) {
const match = line.match(LOCAL_ASSIGNMENT);
if (
!match ||
!identifiers.has(match[1]) ||
!/(?:\s[+\-*/]\s)/.test(match[2])
) {
continue;
}
expanded = expanded.replace(
new RegExp(`\\b${match[1]}\\b`, "g"),
`(${match[2]})`,
);
changed = true;
}
if (!changed) break;
}
return cleanExpression(expanded);
}
/** @param {string | undefined} unit */
function displayUnit(unit) {
if (!unit) return "";
return unit.length <= 5 ? unit.toUpperCase() : unit;
}
/**
* Turn a literal source formula into a concise answer without asking the model
* to invent meanings for code identifiers.
*
* @param {{ metrics: { name: string, unit?: string }[], excerpts: { content: string }[] }} grounding
*/
export function arithmeticAnswer(grounding) {
if (grounding.metrics.length !== 1) return undefined;
const metric = grounding.metrics[0];
const tokens = metricTokens(metric.name);
if (!tokens.length) return undefined;
for (const { content } of grounding.excerpts) {
const lines = content.split("\n");
const candidates = lines
.flatMap((line, index) => {
const match = line.match(COMPOUND_ASSIGNMENT);
return match
? [{ index, match, matched: overlap(match[1], tokens) }]
: [];
})
.sort((left, right) =>
right.matched - left.matched || left.index - right.index
);
const candidate = candidates[0];
if (candidate?.matched) {
const [, , operator, right] = candidate.match;
const expression = expandLocals(
right,
lines.slice(0, candidate.index),
);
const action = operator === "+="
? "adds"
: operator === "-="
? "subtracts"
: operator === "*="
? "multiplies its running value by"
: "divides its running value by";
const target = operator === "+=" || operator === "-="
? `${action} \`${expression}\` to its running total`
: `${action} \`${expression}\``;
const unit = displayUnit(metric.unit);
return `**${metric.name}** ${target}.${unit ? ` It is reported in ${unit}.` : ""}`;
}
const formulas = lines
.flatMap((line, index) => {
const match = line.match(COMMENT_FORMULA);
const arithmetic = match &&
/(?:\s[+\-*/]\s|[Σ∑])/.test(match[2]);
return arithmetic
? [{ index, match, matched: overlap(match[1], tokens) }]
: [];
})
.sort((left, right) =>
right.matched - left.matched || left.index - right.index
);
const formula = formulas[0];
if (formula?.matched) {
const expression = cleanExpression(formula.match[2]);
const unit = displayUnit(metric.unit);
return `**${metric.name}** is calculated as \`${expression}\`.${unit ? ` It is reported in ${unit}.` : ""}`;
}
}
return undefined;
}
+4 -2
View File
@@ -16,13 +16,15 @@ export class AskSource {
* @param {string} query
* @param {string | undefined} path
* @param {"definition" | "implementation" | "availability" | undefined} focus
* @param {(progress: { loaded: number, total: number }) => void} onProgress
* @param {((progress: { loaded: number, total: number }) => void) | undefined} [onProgress]
*/
search(query, path, focus, onProgress) {
return this.#client.request(
"search",
{ query, path, focus },
({ loaded, total }) => onProgress({ loaded, total }),
onProgress
? ({ loaded, total }) => onProgress({ loaded, total })
: undefined,
);
}
+34 -10
View File
@@ -29,10 +29,17 @@ function declarations(lines) {
/** @param {string} text @param {number} line @param {number | undefined} declaration */
function excerptAt(text, line, declaration) {
const lines = text.split("\n");
const start = Math.max(1, line - 3);
const end = Math.min(lines.length, line + 12);
const local = lines.slice(start - 1, end).join("\n");
const declarationLine = declaration === undefined ? undefined : declaration + 1;
const nearbyDeclaration = declarationLine !== undefined &&
line - declarationLine < EXCERPT_WINDOW_LINES - 3;
const start = nearbyDeclaration ? declarationLine : Math.max(1, line - 3);
const end = Math.min(
lines.length,
nearbyDeclaration
? start + EXCERPT_WINDOW_LINES - 1
: line + 12,
);
const local = lines.slice(start - 1, end).join("\n");
const content = declarationLine !== undefined && declarationLine < start
? `${lines[declarationLine - 1]}\n...\n${local}`
: local;
@@ -94,6 +101,16 @@ function computesQueryDirectly(content, query) {
});
}
/** @param {string} content @param {string} query */
function containsDirectFormula(content, query) {
return content.split("\n").some((line) => {
const assignment = line.match(/^(.+?)(?:\+=|-=|\*=|\/=|=)(.+)$/);
return assignment &&
normalize(assignment[1]).includes(query) &&
/[+\-*/×÷Σ∑]/.test(assignment[2]);
});
}
/** @param {{ path: string, text: string }[]} files */
export function createSourceSearchIndex(files) {
/** @type {Map<string, number[]>} */
@@ -297,16 +314,22 @@ export function searchSource(index, rawQuery, pathPrefix = "", focus = undefined
? ""
: normalize(file.text.split("\n")[declaration]);
const definitionScore = focus === "definition" &&
declarationText.includes(query)
(
declarationText.includes(query) ||
excerpt.content.split("\n").some((line) =>
DECLARATION.test(line) && normalize(line).includes(query)
)
)
? 60
: 0;
const implementationScore = focus === "implementation"
const computesDirectly = focus === "implementation" &&
computesQueryDirectly(excerpt.content, query);
const implementationScore = computesDirectly
? computationWeight(excerpt.content) * 30
: 0;
const directImplementationScore =
focus === "implementation" &&
computesQueryDirectly(excerpt.content, query)
? 40
const directImplementationScore = computesDirectly ? 40 : 0;
const formulaScore = containsDirectFormula(excerpt.content, query)
? 80
: 0;
return {
...match,
@@ -314,7 +337,8 @@ export function searchSource(index, rawQuery, pathPrefix = "", focus = undefined
localPhraseOccurrences * 10 +
definitionScore +
implementationScore +
directImplementationScore,
directImplementationScore +
formulaScore,
phraseOccurrences: localPhraseOccurrences,
...excerpt,
};
+2
View File
@@ -1,10 +1,12 @@
const CAMEL_BOUNDARY = /([a-z0-9])([A-Z])/g;
const CAMEL_NUMBER_BOUNDARY = /([A-Z][a-z]+)(\d)/g;
const NON_WORD = /[^a-z0-9%]+/g;
/** @param {unknown} value */
export function normalize(value) {
return String(value)
.replace(CAMEL_BOUNDARY, "$1 $2")
.replace(CAMEL_NUMBER_BOUNDARY, "$1 $2")
.toLowerCase()
.replace(NON_WORD, " ")
.trim()