@struktoai/mirage-core
Version:
A Unified Virtual Filesystem For AI Agents
113 lines • 5.26 kB
JavaScript
// ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
import { mountedPath } from "../../utils/key_prefix.js";
import { recordQueries } from "../../utils/record_search.js";
import { PATH_SAFE } from "../hierarchy/codec.js";
import { filtersOf, tableOf } from "../vector/scope.js";
import { holdingClause, rowsWhere, textColumns } from "./query.js";
import { cellText, renderCard } from "./render.js";
import { detectFor } from "./scope.js";
// What a card spells outside its string values: a boolean, a null, a float no
// JSON number holds, and the blob line's name.
export const CARD_WORDS = new Set([
'true',
'false',
'null',
'nan',
'inf',
'infinity',
'blob',
]);
/**
* A row's card path below its table: its group values as the listing names
* them, then `<id>.md`. Mirrors Python's `card_path`.
*/
export function cardPath(config, row) {
const segments = [];
for (const column of config.groupBy) {
const value = row[column];
if (value !== null && value !== undefined)
segments.push(PATH_SAFE.encode(cellText(value)));
}
return [...segments, `${cellText(row[config.idColumn])}.md`];
}
/** A ranked row's path below its table, and its card. */
export function hit(accessor, row) {
return [cardPath(accessor.config, row), renderCard(row, accessor.config)];
}
/**
* The cards under `under` whose row holds `text` in a string column.
*
* A card is `column: value` lines, so a line grep matches holds the text in
* one value, unless it reaches into the column's name, the title's `# ` or the
* separator: the text is asked only when `recordQueries` passes it with every
* rendered column's name and `CARD_WORDS` as keys, it does not start with a
* space, and no column's name or the blob line's extension holds it. A value that is not a string spells only digits and signs, or one of
* `CARD_WORDS`; a table that renders a list, struct or bytes column reads every
* card, as does a scope inside a card. The title column is searched even when it
* is the vector or blob column, since the title line renders it. More rows
* holding the text than a listing shows (`maxRows`) reads every card too, rather
* than fetching rows the walk never reaches. Mirrors Python's `files_containing`.
*/
export async function filesContaining(accessor, text, under, wholeWord, ignoreCase) {
if (text.startsWith(' '))
return null;
const config = accessor.config;
const skipped = new Set([config.vectorColumn, config.blobColumn].filter((c) => c !== null && c !== config.titleColumn));
const found = [];
for (const scope of under) {
const match = detectFor(accessor)(scope.mountPath);
let tables;
if (match.kind === 'root') {
tables = config.table !== null ? [config.table] : await accessor.driver.listTables();
}
else if (match.kind === 'group') {
tables = [tableOf(config.table ?? null, match)];
}
else {
return null;
}
const filters = filtersOf(config.groupBy, match);
for (const table of tables) {
const columns = await textColumns(accessor, table, skipped);
if (columns === null)
return null;
const rendered = (await accessor.driver.tableColumns(table)).filter((c) => !skipped.has(c));
const labels = rendered.map((c) => c.toLowerCase());
if (config.blobColumn !== null)
labels.push(config.blobExt.toLowerCase());
if (labels.some((label) => label.includes(text.toLowerCase())))
return null;
const queries = recordQueries(text, new Set([...labels, ...CARD_WORDS]), wholeWord);
if (queries === null)
return null;
if (columns.length === 0)
continue;
const rows = await rowsWhere(accessor, table, holdingClause(columns, queries, ignoreCase, filters), [config.idColumn, ...config.groupBy], config.maxRows);
if (rows === null)
return null;
const base = config.table !== null ? '' : `/${table}`;
for (const row of rows) {
const id = row[config.idColumn];
if (id === null || id === undefined)
continue;
if (config.groupBy.some((g) => row[g] === null || row[g] === undefined))
continue;
found.push(mountedPath(scope, `${base}/${cardPath(config, row).join('/')}`));
}
}
}
return found;
}
//# sourceMappingURL=search.js.map