UNPKG

@struktoai/mirage-core

Version:

A Unified Virtual Filesystem For AI Agents

198 lines • 7.9 kB
// ========= 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 { PathSpec } from "../../types.js"; import { stripSlash } from "../../utils/slash.js"; import { RAW } from "./codec.js"; export const ROOT = 'root'; export const INVALID = 'invalid'; /** * One dynamic segment of a scope: the key the decoded value is stored under, * and how the segment encodes it. `idKey` is set for `<label>__<id>` * composite segments: the decoded payload splits on its LAST `__` (so a * three-part `KEY__name__id` keeps `KEY__name` as the label), the label * stored under `name` and the id under `idKey`. A payload with no `__` or an * empty half does not match the scope. * * `idSplit: 'first'` cuts on the FIRST `__` instead, for a label that can * never hold one and an id that can: a Google app's title is sanitized, which * collapses every run of underscores, while a Drive file id may carry `__` * itself (`1D2dqZx96t_aSHk3kLorY__OWES6…`), and the last cut would read * half of it as the label. * * A `variadic` slot stands for a run of one or more consecutive segments * instead of exactly one. Segments before it anchor at the start of the * path, segments after it at the end, every segment in the run must decode, * and the stored value comes from the run's DEEPEST segment: notion's pages * nest arbitrarily, and `pages/a__1/b__2/page.json` stores `page=b, * page_id=2` because the innermost page is the one the path addresses. At * most one variadic slot per scope. */ export class Slot { name; codec; idKey; variadic; idSplit; constructor(name, codec = RAW, idKey = null, variadic = false, idSplit = 'last') { this.name = name; this.codec = codec; this.idKey = idKey; this.variadic = variadic; this.idSplit = idSplit; } } /** * One addressable position in a fixed API hierarchy. * * `kind` is the position's name; listers, probes and readers key on it. * `segments` is the path shape, literals and slots. `leaf` marks a file * rather than a directory, `filetype` its rendered content type. `probed` is whether * stat must prove existence (parent listing by default); false for positions * that exist by construction, like the top-level directories. */ export class Scope { kind; segments; leaf; filetype; probed; constructor(init) { this.kind = init.kind; this.segments = init.segments; this.leaf = init.leaf ?? false; this.filetype = init.filetype ?? null; this.probed = init.probed ?? true; } } /** Decode one path segment through a slot, null when it does not fit. */ export function decodeSlot(slot, part) { const decoded = slot.codec.decode(part); if (decoded === null) return null; if (slot.idKey !== null) { const cut = slot.idSplit === 'first' ? decoded.indexOf('__') : decoded.lastIndexOf('__'); const label = cut > 0 ? decoded.slice(0, cut) : ''; const ident = cut >= 0 ? decoded.slice(cut + 2) : ''; if (label === '' || ident === '') return null; return { [slot.name]: label, [slot.idKey]: ident }; } return { [slot.name]: decoded }; } /** The scope's variadic slot and its position, null when it has none. */ export function variadicSlot(segments) { let found = null; for (const [i, segment] of segments.entries()) { if (typeof segment !== 'string' && segment.variadic) { if (found !== null) throw new Error('a scope holds at most one variadic slot'); found = [i, segment]; } } return found; } function matchRun(segments, parts) { const slots = {}; for (const [i, segment] of segments.entries()) { const part = parts[i] ?? ''; if (typeof segment === 'string') { if (part !== segment) return null; continue; } const values = decodeSlot(segment, part); if (values === null) return null; Object.assign(slots, values); } return slots; } function matchSegments(segments, parts) { const found = variadicSlot(segments); if (found === null) { if (segments.length !== parts.length) return null; return matchRun(segments, parts); } if (parts.length < segments.length) return null; const [at, slot] = found; const tailLen = segments.length - at - 1; const head = matchRun(segments.slice(0, at), parts.slice(0, at)); if (head === null) return null; const tail = matchRun(segments.slice(at + 1), parts.slice(parts.length - tailLen)); if (tail === null) return null; let values = null; for (const part of parts.slice(at, parts.length - tailLen)) { values = decodeSlot(slot, part); if (values === null) return null; } if (values === null) return null; return { ...head, ...values, ...tail }; } /** Match path segments against the table, first declared scope wins. */ export function matchScope(scopes, parts) { for (const scope of scopes) { const slots = matchSegments(scope.segments, parts); if (slots !== null) return [scope, slots]; } return null; } /** * Build a path classifier from a scope table. * * The classifier is the single description of the backend's tree: readdir, * stat, read, and any search push-down all dispatch on its result, so the * file surface and the command surface cannot disagree about what a path * means. Hidden segments classify as invalid, which every consumer turns * into ENOENT. Scopes are matched in declaration order. * * Postgres's table, classified level by level: * * path kind slots * / root {} * /public schema {schema} * /public/tables kind {schema, kind} * /public/tables/books entity {schema, kind, entity} * /public/tables/books/rows.jsonl entity_rows {schema, kind, entity} * * `kind` names the level a path landed on; the slots identify the branch * taken at each dynamic level above it. Literal levels (`rows.jsonl`) * contribute no slot, and one dynamic level can contribute two (`idKey`). */ export function makeDetectScope(scopes) { for (const scope of scopes) variadicSlot(scope.segments); return function detectScope(path) { const raw = path instanceof PathSpec ? path.mountPath : path; const key = stripSlash(raw); if (key === '') return { kind: ROOT, vfsPath: raw, slots: {}, scope: null, pattern: null }; const parts = key.split('/'); if (parts.some((p) => p.startsWith('.'))) { return { kind: INVALID, vfsPath: raw, slots: {}, scope: null, pattern: null }; } const matched = matchScope(scopes, parts); if (matched === null) return { kind: INVALID, vfsPath: raw, slots: {}, scope: null, pattern: null }; const [scope, slots] = matched; return { kind: scope.kind, vfsPath: raw, slots, scope, pattern: null }; }; } //# sourceMappingURL=scope.js.map