@struktoai/mirage-core
Version:
A Unified Virtual Filesystem For AI Agents
101 lines • 4.73 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 { jsonText } from "../core/render/json.js";
const HEX_LETTERS = /^[a-f]+$/;
// The letters a JSON escape ends with: `\b \f \n \r \t`, and the hex digit
// closing a `\u00XX` control character. Each can run into the word after it,
// so `\nice` holds "nice" where the message holds "ice".
const ESCAPE_LETTERS = new Set('abcdefnrt');
// The largest rendered file whose text a listing entry keeps (`keptText`).
export const KEPT_TEXT_MAX = 8192;
// A word a provider's search reads as grep -w does: ASCII letters and digits,
// with sentence punctuation around them at most.
const ASCII_SPACE = /[ \t\n\r\f\v]+/;
const PLAIN_WORD = /^[.,;:!?"'()[\]]*[A-Za-z0-9]+[.,;:!?"'()[\]]*$/;
/**
* The searches whose hits together hold every mounted record `text` may
* match, or null when a provider's search cannot say.
*
* A mounted message is the provider's record as JSON, which holds more than
* its search reads: key names, ids, counts, timestamps and flags, spelled
* with JSON escapes. `text` is searched only when it is ASCII letters and
* spaces, so no digit, quote or backslash can match outside the text the
* search reads, and when none of its words is one of `keys` (the record's
* key names and fixed values, lowercase) or a run of hex letters, which an
* id can hold. Without whole-word matching a text inside a key is refused
* too. A text starting with a letter an escape ends with may start inside
* one, so the rest of it is searched as well (alone without whole-word
* matching, where it is the wider search); such a letter alone is refused.
*/
export function recordQueries(text, keys, wholeWord) {
if (text.trim() === '' || !/^[A-Za-z ]+$/.test(text))
return null;
const words = text
.toLowerCase()
.split(/ +/)
.filter((word) => word !== '');
if (words.some((word) => keys.has(word) || HEX_LETTERS.test(word)))
return null;
const [only = ''] = words;
if (!wholeWord && words.length === 1 && [...keys].some((key) => key.includes(only))) {
return null;
}
if (!ESCAPE_LETTERS.has(text.charAt(0).toLowerCase()))
return [text];
const rest = text.slice(1).trim();
if (rest === '')
return null;
return wholeWord ? [text, rest] : [rest];
}
/**
* The words of `text` a provider's search may split otherwise than grep -w: a
* link, an address, markup, an entity, a hyphenated or non-ASCII word. Words
* split at ASCII whitespace only, so both hosts split alike and a non-ASCII
* space stays inside its word.
*/
export function nonPlainWords(text) {
return text.split(ASCII_SPACE).filter((word) => word !== '' && !PLAIN_WORD.test(word));
}
/** The normalized fields and words a provider's search does not read. */
export function unsearchedText(fields, searched) {
const rest = [];
const words = [];
for (const [key, value] of fields) {
if (!searched.has(key))
rest.push([key, value]);
else if (typeof value === 'string')
for (const word of nonPlainWords(value))
words.push(word);
}
return [jsonText(Object.fromEntries(rest)), ...words].join(' ').toLowerCase();
}
/**
* Whether a record may hold one of the asked texts (each searched text's
* lowercase words): each word of one is named for the record by the
* provider's search, asked word by word, or held by the text the listing kept.
*/
export function mayHold(asked, named, recordId, kept) {
return asked.some((words) => words.every((word) => named.get(word)?.has(recordId) === true || kept.includes(word)));
}
/**
* A listing entry's `extra` keeping its file's rendered text, lowercased, so
* grep can check the file without reading it; empty past `KEPT_TEXT_MAX`
* bytes, where the file is read as usual.
*/
export function keptText(rendered) {
if (rendered.byteLength > KEPT_TEXT_MAX)
return {};
return { text: new TextDecoder('utf-8', { ignoreBOM: true }).decode(rendered).toLowerCase() };
}
//# sourceMappingURL=record_search.js.map