refactor(8.0): API-surface + quality polish from the readiness audit
- find() search-mode: collapsed the two overlapping options to one canonical `searchMode: SearchMode` (the redundant `mode` alias was silently ignored on the primary find() path while honored on the historical path — a footgun) and removed the unwired `explain?` FindParams field (never read by find()). - Documentation accuracy on the public type surface: entity ids documented as UUID v7 (auto) / v5 (natural key), not v4; dropped the stale "Brainy 3.0" banners and "backward compatibility" hedging on the fresh-8.0 Result type; documented the via/type alias; removed the dead GraphConstraints.bidirectional; fixed the no-op `EmbeddedGraphVerb = Omit<GraphVerb,'source'>` to omit the real sourceId; corrected the GraphIndexProvider.isInitialized JSDoc; documented removeMany's per-chunk generation granularity; JSDoc'd the public VFS surface. - Honest perf comments in source: removed unmeasured billion-scale figures from the LSMTree / graphAdjacencyIndex headers (the JS fallback is in-memory after open; native is the scale path). - Robustness: getVerbMetadata propagates read errors symmetrically with getNounMetadata; the find() egress integrity-guard keeps a row when the JS matcher doesn't implement an operator the provider already matched on; hardened the boundary-no-native CI guard to catch side-effect imports + re-exports. - Tests: de-theatricalized a relateMany test to assert the real contract; fixed a stale Model-B header. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
47e8031124
commit
a52dba2168
10 changed files with 346 additions and 45 deletions
|
|
@ -88,6 +88,17 @@ export class VirtualFileSystem implements IVirtualFileSystem {
|
|||
// Uses deterministic UUID format for storage compatibility
|
||||
private static readonly VFS_ROOT_ID = '00000000-0000-0000-0000-000000000000'
|
||||
|
||||
/**
|
||||
* Construct a VFS bound to a Brainy instance.
|
||||
*
|
||||
* The VFS stores every file and directory as a Brainy entity and models the
|
||||
* directory tree with graph relationships. No I/O happens here — call
|
||||
* {@link init} (or rely on `brain.init()`, which auto-initializes the VFS)
|
||||
* before using any filesystem operation.
|
||||
*
|
||||
* @param brain - The Brainy instance backing this filesystem. When omitted, a
|
||||
* fresh `new Brainy()` is created and owned by this VFS.
|
||||
*/
|
||||
constructor(brain?: Brainy) {
|
||||
this.brain = brain || new Brainy()
|
||||
this.contentCache = new Map()
|
||||
|
|
@ -1135,7 +1146,6 @@ export class VirtualFileSystem implements IVirtualFileSystem {
|
|||
type: [NounType.File, NounType.Document, NounType.Media],
|
||||
limit: options?.limit || 10,
|
||||
offset: options?.offset,
|
||||
explain: options?.explain,
|
||||
where: {
|
||||
vfsType: 'file' // Search VFS files
|
||||
}
|
||||
|
|
@ -1250,6 +1260,17 @@ export class VirtualFileSystem implements IVirtualFileSystem {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch a Brainy entity by id and normalize it into a {@link VFSEntity}.
|
||||
*
|
||||
* Backfills VFS metadata for legacy entities that stored fields at the top
|
||||
* level (pre-nested-metadata layout) and guarantees the root directory always
|
||||
* carries valid directory metadata.
|
||||
*
|
||||
* @param id - The Brainy entity id to fetch.
|
||||
* @returns The entity with a populated `metadata.vfsType` and related VFS fields.
|
||||
* @throws {VFSError} ENOENT when no entity exists for the given id.
|
||||
*/
|
||||
async getEntityById(id: string): Promise<VFSEntity> {
|
||||
const entity = await this.brain.get(id)
|
||||
|
||||
|
|
@ -1572,8 +1593,17 @@ export class VirtualFileSystem implements IVirtualFileSystem {
|
|||
}
|
||||
}
|
||||
|
||||
// ============= Not Yet Implemented =============
|
||||
// ============= Lifecycle, POSIX & Extended Operations =============
|
||||
|
||||
/**
|
||||
* Release all resources held by the VFS.
|
||||
*
|
||||
* Stops background cache eviction, tears down the path resolver, and clears the
|
||||
* content cache and watcher registry. After close the VFS is marked
|
||||
* uninitialized; call {@link init} again before reusing it.
|
||||
*
|
||||
* @returns A promise that resolves once cleanup is complete.
|
||||
*/
|
||||
async close(): Promise<void> {
|
||||
// Cleanup PathResolver resources
|
||||
if (this.pathResolver) {
|
||||
|
|
@ -1595,6 +1625,13 @@ export class VirtualFileSystem implements IVirtualFileSystem {
|
|||
this.initialized = false
|
||||
}
|
||||
|
||||
/**
|
||||
* Change the permission bits of a file or directory (POSIX `chmod`).
|
||||
*
|
||||
* @param path - The VFS path whose permissions should change.
|
||||
* @param mode - The new permission bits (e.g. `0o644`), stored in entity metadata.
|
||||
* @returns A promise that resolves once the permissions are persisted.
|
||||
*/
|
||||
async chmod(path: string, mode: number): Promise<void> {
|
||||
await this.ensureInitialized()
|
||||
|
||||
|
|
@ -1616,6 +1653,14 @@ export class VirtualFileSystem implements IVirtualFileSystem {
|
|||
this.invalidateCaches(path)
|
||||
}
|
||||
|
||||
/**
|
||||
* Change the owner and group of a file or directory (POSIX `chown`).
|
||||
*
|
||||
* @param path - The VFS path whose ownership should change.
|
||||
* @param uid - The new owner user id, stored in entity metadata.
|
||||
* @param gid - The new owning group id, stored in entity metadata.
|
||||
* @returns A promise that resolves once the ownership is persisted.
|
||||
*/
|
||||
async chown(path: string, uid: number, gid: number): Promise<void> {
|
||||
await this.ensureInitialized()
|
||||
|
||||
|
|
@ -1638,6 +1683,14 @@ export class VirtualFileSystem implements IVirtualFileSystem {
|
|||
this.invalidateCaches(path)
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the access and modification timestamps of a file or directory (POSIX `utimes`).
|
||||
*
|
||||
* @param path - The VFS path to update.
|
||||
* @param atime - The new access time.
|
||||
* @param mtime - The new modification time.
|
||||
* @returns A promise that resolves once the timestamps are persisted.
|
||||
*/
|
||||
async utimes(path: string, atime: Date, mtime: Date): Promise<void> {
|
||||
await this.ensureInitialized()
|
||||
|
||||
|
|
@ -1659,6 +1712,20 @@ export class VirtualFileSystem implements IVirtualFileSystem {
|
|||
this.invalidateCaches(path)
|
||||
}
|
||||
|
||||
/**
|
||||
* Rename or move a file or directory in place.
|
||||
*
|
||||
* Updates the entity's path metadata without rewriting content (preserving the
|
||||
* underlying blob and entity id). When the parent directory changes, a new
|
||||
* `Contains` edge is added to the destination parent. Renaming a directory
|
||||
* cascades the path change to every descendant.
|
||||
*
|
||||
* @param oldPath - The existing VFS path.
|
||||
* @param newPath - The destination VFS path; must not already exist.
|
||||
* @returns A promise that resolves once the rename is complete.
|
||||
* @throws {VFSError} ENOENT when `oldPath` does not exist.
|
||||
* @throws {VFSError} EEXIST when `newPath` already exists.
|
||||
*/
|
||||
async rename(oldPath: string, newPath: string): Promise<void> {
|
||||
await this.ensureInitialized()
|
||||
|
||||
|
|
@ -1732,6 +1799,21 @@ export class VirtualFileSystem implements IVirtualFileSystem {
|
|||
this.triggerWatchers(newPath, 'rename')
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy a file or directory to a new path.
|
||||
*
|
||||
* Files are duplicated as new entities (sharing content via the content-addressed
|
||||
* blob store); directories are copied recursively. Unless `overwrite` is set, an
|
||||
* existing destination is rejected.
|
||||
*
|
||||
* @param src - The source VFS path to copy from.
|
||||
* @param dest - The destination VFS path to copy to.
|
||||
* @param options - Copy options such as `overwrite`, `deepCopy`, `preserveVector`,
|
||||
* and `preserveRelationships`.
|
||||
* @returns A promise that resolves once the copy is complete.
|
||||
* @throws {VFSError} ENOENT when `src` does not exist.
|
||||
* @throws {VFSError} EEXIST when `dest` exists and `overwrite` is not set.
|
||||
*/
|
||||
async copy(src: string, dest: string, options?: CopyOptions): Promise<void> {
|
||||
await this.ensureInitialized()
|
||||
|
||||
|
|
@ -1919,6 +2001,20 @@ export class VirtualFileSystem implements IVirtualFileSystem {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Move a file or directory to a new path.
|
||||
*
|
||||
* Implemented as an in-place {@link rename} rather than copy-then-delete: on the
|
||||
* content-addressed blob store a copy would share the source content hash, so
|
||||
* deleting the source would orphan the destination. Renaming preserves the blob
|
||||
* and entity id and handles directory descendants.
|
||||
*
|
||||
* @param src - The source VFS path to move from.
|
||||
* @param dest - The destination VFS path to move to; must not already exist.
|
||||
* @returns A promise that resolves once the move is complete.
|
||||
* @throws {VFSError} ENOENT when `src` does not exist.
|
||||
* @throws {VFSError} EEXIST when `dest` already exists.
|
||||
*/
|
||||
async move(src: string, dest: string): Promise<void> {
|
||||
await this.ensureInitialized()
|
||||
|
||||
|
|
@ -1932,6 +2028,18 @@ export class VirtualFileSystem implements IVirtualFileSystem {
|
|||
await this.rename(src, dest)
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a symbolic link at `path` that points to `target` (POSIX `symlink`).
|
||||
*
|
||||
* The link is stored as a dedicated `vfs-symlink` entity whose
|
||||
* `metadata.symlinkTarget` records the target path; it is linked into its parent
|
||||
* directory with a `Contains` edge.
|
||||
*
|
||||
* @param target - The path the symlink should resolve to.
|
||||
* @param path - The VFS path at which to create the symlink; must not already exist.
|
||||
* @returns A promise that resolves once the symlink is created.
|
||||
* @throws {VFSError} EEXIST when `path` already exists.
|
||||
*/
|
||||
async symlink(target: string, path: string): Promise<void> {
|
||||
await this.ensureInitialized()
|
||||
|
||||
|
|
@ -1987,6 +2095,13 @@ export class VirtualFileSystem implements IVirtualFileSystem {
|
|||
await this.pathResolver.createPath(path, entity)
|
||||
}
|
||||
|
||||
/**
|
||||
* Read the target of a symbolic link (POSIX `readlink`).
|
||||
*
|
||||
* @param path - The VFS path of the symlink.
|
||||
* @returns The stored target path, or an empty string when no target is recorded.
|
||||
* @throws {VFSError} EINVAL when `path` is not a symbolic link.
|
||||
*/
|
||||
async readlink(path: string): Promise<string> {
|
||||
await this.ensureInitialized()
|
||||
|
||||
|
|
@ -2001,6 +2116,17 @@ export class VirtualFileSystem implements IVirtualFileSystem {
|
|||
return entity.metadata.symlinkTarget || ''
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve a path to its canonical form, following symbolic links (POSIX `realpath`).
|
||||
*
|
||||
* Symlinks are followed iteratively until a non-symlink target is reached, up to a
|
||||
* fixed depth limit that guards against link cycles.
|
||||
*
|
||||
* @param path - The VFS path to resolve.
|
||||
* @returns The fully resolved (non-symlink) path.
|
||||
* @throws {VFSError} ENOENT when the path (or a link in the chain) does not exist.
|
||||
* @throws {VFSError} ELOOP when too many symbolic links are encountered.
|
||||
*/
|
||||
async realpath(path: string): Promise<string> {
|
||||
await this.ensureInitialized()
|
||||
|
||||
|
|
@ -2030,12 +2156,27 @@ export class VirtualFileSystem implements IVirtualFileSystem {
|
|||
throw new VFSError(VFSErrorCode.ELOOP, `Too many symbolic links: ${path}`, path, 'realpath')
|
||||
}
|
||||
|
||||
/**
|
||||
* Read a single extended attribute of a file or directory (POSIX `getxattr`).
|
||||
*
|
||||
* @param path - The VFS path to read.
|
||||
* @param name - The extended-attribute key to fetch.
|
||||
* @returns The attribute value, or `undefined` when the attribute is not set.
|
||||
*/
|
||||
async getxattr(path: string, name: string): Promise<any> {
|
||||
const entityId = await this.pathResolver.resolve(path)
|
||||
const entity = await this.getEntityById(entityId)
|
||||
return entity.metadata.attributes?.[name]
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a single extended attribute on a file or directory (POSIX `setxattr`).
|
||||
*
|
||||
* @param path - The VFS path to update.
|
||||
* @param name - The extended-attribute key to set.
|
||||
* @param value - The value to store under `name`.
|
||||
* @returns A promise that resolves once the attribute is persisted.
|
||||
*/
|
||||
async setxattr(path: string, name: string, value: any): Promise<void> {
|
||||
await this.ensureInitialized()
|
||||
|
||||
|
|
@ -2059,12 +2200,25 @@ export class VirtualFileSystem implements IVirtualFileSystem {
|
|||
this.invalidateCaches(path)
|
||||
}
|
||||
|
||||
/**
|
||||
* List the extended-attribute names set on a file or directory (POSIX `listxattr`).
|
||||
*
|
||||
* @param path - The VFS path to inspect.
|
||||
* @returns The list of extended-attribute keys (empty when none are set).
|
||||
*/
|
||||
async listxattr(path: string): Promise<string[]> {
|
||||
const entityId = await this.pathResolver.resolve(path)
|
||||
const entity = await this.getEntityById(entityId)
|
||||
return Object.keys(entity.metadata.attributes || {})
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a single extended attribute from a file or directory (POSIX `removexattr`).
|
||||
*
|
||||
* @param path - The VFS path to update.
|
||||
* @param name - The extended-attribute key to remove.
|
||||
* @returns A promise that resolves once the attribute is removed.
|
||||
*/
|
||||
async removexattr(path: string, name: string): Promise<void> {
|
||||
await this.ensureInitialized()
|
||||
|
||||
|
|
@ -2089,6 +2243,17 @@ export class VirtualFileSystem implements IVirtualFileSystem {
|
|||
this.invalidateCaches(path)
|
||||
}
|
||||
|
||||
/**
|
||||
* List the paths related to a file or directory via graph edges (both directions).
|
||||
*
|
||||
* Walks outgoing and incoming relationships and maps each connected entity back to
|
||||
* its VFS path, recording the relationship type and direction.
|
||||
*
|
||||
* @param path - The VFS path whose relationships to list.
|
||||
* @param options - Reserved relationship-filtering options.
|
||||
* @returns Related entries, each with the related `path`, `relationship` type, and
|
||||
* `direction` (`'from'` for outgoing edges, `'to'` for incoming).
|
||||
*/
|
||||
async getRelated(path: string, options?: RelatedOptions): Promise<Array<{
|
||||
path: string
|
||||
relationship: string
|
||||
|
|
@ -2132,6 +2297,15 @@ export class VirtualFileSystem implements IVirtualFileSystem {
|
|||
return results
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the non-hierarchical relationships of a file or directory.
|
||||
*
|
||||
* Returns both outgoing and incoming edges as {@link Relation} objects, excluding
|
||||
* the `Contains` edges that model the directory tree itself.
|
||||
*
|
||||
* @param path - The VFS path whose relationships to return.
|
||||
* @returns The list of semantic relationships connected to the path.
|
||||
*/
|
||||
async getRelationships(path: string): Promise<Relation[]> {
|
||||
await this.ensureInitialized()
|
||||
|
||||
|
|
@ -2179,6 +2353,14 @@ export class VirtualFileSystem implements IVirtualFileSystem {
|
|||
return relationships
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a graph relationship between two VFS paths.
|
||||
*
|
||||
* @param from - The source VFS path.
|
||||
* @param to - The target VFS path.
|
||||
* @param type - The relationship (verb) type to create; coerced to a {@link VerbType}.
|
||||
* @returns A promise that resolves once the relationship is created.
|
||||
*/
|
||||
async addRelationship(from: string, to: string, type: string): Promise<void> {
|
||||
await this.ensureInitialized()
|
||||
|
||||
|
|
@ -2198,6 +2380,18 @@ export class VirtualFileSystem implements IVirtualFileSystem {
|
|||
this.invalidateCaches(to)
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a graph relationship between two VFS paths.
|
||||
*
|
||||
* Deletes outgoing edges from `from` to `to`; when `type` is given, only edges of
|
||||
* that relationship type are removed.
|
||||
*
|
||||
* @param from - The source VFS path.
|
||||
* @param to - The target VFS path.
|
||||
* @param type - Optional relationship type to match; when omitted, all matching
|
||||
* edges to `to` are removed.
|
||||
* @returns A promise that resolves once the relationship(s) are removed.
|
||||
*/
|
||||
async removeRelationship(from: string, to: string, type?: string): Promise<void> {
|
||||
await this.ensureInitialized()
|
||||
|
||||
|
|
@ -2220,12 +2414,25 @@ export class VirtualFileSystem implements IVirtualFileSystem {
|
|||
this.invalidateCaches(to)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the todo items attached to a file or directory.
|
||||
*
|
||||
* @param path - The VFS path whose todos to read.
|
||||
* @returns The stored todo list, or `undefined` when none are attached.
|
||||
*/
|
||||
async getTodos(path: string): Promise<VFSMetadata['todos']> {
|
||||
const entityId = await this.pathResolver.resolve(path)
|
||||
const entity = await this.getEntityById(entityId)
|
||||
return entity.metadata.todos
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace the entire todo list attached to a file or directory.
|
||||
*
|
||||
* @param path - The VFS path to update.
|
||||
* @param todos - The full set of todo items to store (overwrites any existing list).
|
||||
* @returns A promise that resolves once the todos are persisted.
|
||||
*/
|
||||
async setTodos(path: string, todos: VFSTodo[]): Promise<void> {
|
||||
await this.ensureInitialized()
|
||||
|
||||
|
|
@ -2247,6 +2454,16 @@ export class VirtualFileSystem implements IVirtualFileSystem {
|
|||
this.invalidateCaches(path)
|
||||
}
|
||||
|
||||
/**
|
||||
* Append a single todo item to a file or directory.
|
||||
*
|
||||
* Generates an id when one is not supplied and applies default `priority`
|
||||
* (`'medium'`) and `status` (`'pending'`).
|
||||
*
|
||||
* @param path - The VFS path to attach the todo to.
|
||||
* @param todo - The todo to add; `id`, `priority`, and `status` are optional.
|
||||
* @returns A promise that resolves once the todo is persisted.
|
||||
*/
|
||||
async addTodo(path: string, todo: VFSTodo): Promise<void> {
|
||||
await this.ensureInitialized()
|
||||
|
||||
|
|
@ -2724,18 +2941,39 @@ export class VirtualFileSystem implements IVirtualFileSystem {
|
|||
return results
|
||||
}
|
||||
|
||||
/**
|
||||
* Open a file as a Node-compatible readable stream.
|
||||
*
|
||||
* @param path - The VFS path of the file to read.
|
||||
* @param options - Stream options (e.g. byte range, chunk size).
|
||||
* @returns A readable stream that yields the file's bytes.
|
||||
*/
|
||||
createReadStream(path: string, options?: ReadStreamOptions): NodeJS.ReadableStream {
|
||||
// Lazy import to avoid circular dependencies
|
||||
const { VFSReadStream } = require('./streams/VFSReadStream.js')
|
||||
return new VFSReadStream(this, path, options)
|
||||
}
|
||||
|
||||
/**
|
||||
* Open a file as a Node-compatible writable stream.
|
||||
*
|
||||
* @param path - The VFS path of the file to write.
|
||||
* @param options - Stream options controlling how buffered data is flushed.
|
||||
* @returns A writable stream whose contents are persisted to the file on finish.
|
||||
*/
|
||||
createWriteStream(path: string, options?: WriteStreamOptions): NodeJS.WritableStream {
|
||||
// Lazy import to avoid circular dependencies
|
||||
const { VFSWriteStream } = require('./streams/VFSWriteStream.js')
|
||||
return new VFSWriteStream(this, path, options)
|
||||
}
|
||||
|
||||
/**
|
||||
* Watch a path for changes and invoke a listener on filesystem events.
|
||||
*
|
||||
* @param path - The VFS path to watch.
|
||||
* @param listener - Called with the event type (`'rename'` or `'change'`) and the path.
|
||||
* @returns A handle whose `close()` method deregisters the listener.
|
||||
*/
|
||||
watch(path: string, listener: WatchListener): { close(): void } {
|
||||
if (!this.watchers.has(path)) {
|
||||
this.watchers.set(path, new Set())
|
||||
|
|
@ -2808,14 +3046,35 @@ export class VirtualFileSystem implements IVirtualFileSystem {
|
|||
yield* importer.importStream(sourcePath, options)
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a change listener for a path (Node `fs.watchFile`-style convenience).
|
||||
*
|
||||
* Delegates to {@link watch} without returning the watcher handle; remove the
|
||||
* listener with {@link unwatchFile}.
|
||||
*
|
||||
* @param path - The VFS path to watch.
|
||||
* @param listener - Called with the event type and path on each change.
|
||||
*/
|
||||
watchFile(path: string, listener: WatchListener): void {
|
||||
this.watch(path, listener)
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop watching a path, removing all listeners registered for it.
|
||||
*
|
||||
* @param path - The VFS path to stop watching.
|
||||
*/
|
||||
unwatchFile(path: string): void {
|
||||
this.watchers.delete(path)
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve a path and return its backing {@link VFSEntity}.
|
||||
*
|
||||
* @param path - The VFS path to resolve.
|
||||
* @returns The entity (with normalized VFS metadata) for the path.
|
||||
* @throws {VFSError} ENOENT when the path cannot be resolved.
|
||||
*/
|
||||
async getEntity(path: string): Promise<VFSEntity> {
|
||||
const entityId = await this.pathResolver.resolve(path)
|
||||
return this.getEntityById(entityId)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue