/**
 * Base Graph Manager
 *
 * Provides unified entity management across any repo type.
 * Integrates Git, Markdown, and Asset services.
 */
import { GitService, GitUser } from '../services/git.js';
import { MarkdownDocument } from '../services/markdown.js';
import type { BodyWriteCoordinator, BodyWriteResult } from '../server/body-write-coordinator.js';
import { AssetService, AssetInfo, AssetRecord, AssetConfig, type AssetUploadResult } from '../services/assets/index.js';
import type { AssetIndexService } from '../services/assets/asset-index-service.js';
import { EntityType, type EntityData, type DatasetRow } from './validation.js';
import { SchemaRegistry } from './schema-registry.js';
import { type DatasetFilter } from '../services/csv-service.js';
import { type HistoryCommitChange } from './history-diff.js';
import { type VectorService } from '../services/vector-service.js';
export interface GraphConfig {
    repoPath: string;
    repoName: string;
    assetConfig?: AssetConfig;
    gitUser: GitUser;
    vectorService?: VectorService;
    schemaRegistry?: SchemaRegistry;
    /** When true, all git operations are skipped. Used for the private/ graph. */
    noGit?: boolean;
    /** Workspace root path. Used to anchor cross-repo artefacts like the
     *  PDF text sidecar at `<workspaceRoot>/.studiograph/assets-text/...`.
     *  Optional — when undefined, sidecar cleanup is a no-op. */
    workspaceRoot?: string;
}
export interface EntityFile {
    id: string;
    path: string;
    /** Repo-relative POSIX path of the containing folder. "" for repo root. */
    folder_path: string;
    entityType: string;
    data: EntityData;
    document: MarkdownDocument;
}
export interface FolderEntry {
    /** Repo-relative POSIX path. */
    path: string;
    /** Last segment of the path. */
    name: string;
    /** Optional human-readable label. Set only when an explicit override exists
     *  in the folder's `.studiograph-folder.json` sidecar; clients fall back to
     *  toDisplayName(name) when undefined. */
    display_name?: string;
    /** Stable folder id from the sidecar, when assigned. Eagerly written on
     *  folder creation; absent for folders never mutated through the app (those
     *  fall back to a path-based scope key). Reads never generate one. */
    id?: string;
}
export interface FolderDeleteResult {
    entriesDeleted: number;
    subfoldersDeleted: number;
}
export interface SearchOptions {
    entityType?: EntityType;
    query?: string;
    filters?: Record<string, any>;
    visibility?: Array<'public' | 'team' | 'admin' | 'restricted'>;
    tags?: string[];
    dateRange?: {
        from: string;
        to: string;
    };
    sortBy?: 'created_at' | 'updated_at' | 'relevance';
    limit?: number;
    /**
     * When true, score only on name match — skip frontmatter field scan and
     * full-body content scan. Used by autosuggest, where scanning every
     * entity's markdown body dominates wall time.
     */
    nameOnly?: boolean;
}
export interface CreateEntityOptions {
    entityType: EntityType;
    entityId: string;
    data: Record<string, any>;
    content?: string;
    commitMessage?: string;
    skipCommit?: boolean;
    /**
     * Repo-relative folder to create the entry in. Empty/undefined = repo root.
     * Parent folders are auto-created (mkdir -p semantics).
     */
    folderPath?: string;
    /** Override commit author (for per-user attribution in web UI). */
    user?: GitUser;
    /**
     * Suppress the fire-and-forget vector upsert for THIS create. Used by the
     * asset-upload flow, where the entity is created as a `__pending__` stub
     * (no sidecar, no `indexable`) and then finalized by a follow-up `update`.
     * Indexing the stub is both wasteful (name-only, pre-sidecar) and racy: its
     * async embedding can resolve AFTER the finalizing update's body-inclusive
     * upsert and clobber the good row, leaving the asset searchable by filename
     * only. Skipping it means the single authoritative upsert is the finalizing
     * update, after the sidecar is on disk. Default false (every other path
     * indexes on create as before).
     */
    skipVectorIndex?: boolean;
}
/**
 * Body-write provenance. Every `graph.update` call that passes `content`
 * must declare its `source`.
 *
 * - `'autosave'`: web editor autosave PUT. Content came from a live Y.Text;
 *   the live Y.Doc is already authoritative, so we write disk + commit
 *   under the per-doc lock but do NOT re-apply to the Y.Doc (that would
 *   race in-flight keystrokes typed during the round trip).
 * - `'server-write'`: any server-side caller (MCP `update_entity`, agent
 *   tools, event processor). Routes through `applyAgentContent` under the
 *   per-doc lock so live editors receive the broadcast post-commit.
 */
export type BodyWriteSource = 'autosave' | 'server-write';
export interface UpdateEntityOptions {
    entityType: EntityType;
    entityId: string;
    data?: Record<string, any>;
    content?: string;
    commitMessage?: string;
    skipCommit?: boolean;
    /** Override commit author (for per-user attribution in web UI). */
    user?: GitUser;
    /**
     * Required when `content !== undefined`. See {@link BodyWriteSource}.
     */
    source?: BodyWriteSource;
    /**
     * Optional precondition for body writes. When set, the server compares
     * this hash to `sha256(existing.document.content)` BEFORE mutating
     * anything. Mismatch throws {@link StaleBaseRevisionError} so the caller
     * can refetch + recompute. When omitted, last-writer-wins.
     *
     * Content hash, not `updated_at`, because two `new Date().toISOString()`
     * stamps can alias to the same millisecond on fast hardware.
     */
    baseRevision?: string;
    /**
     * Optional sink for the body-write broadcast disposition. Only fires on
     * the coordinator (body-write) path. Lets a caller (e.g. MCP
     * update_entity) surface `broadcast-failed` in its diagnostics instead
     * of the disposition being discarded after `writeBody` returns.
     */
    onBroadcast?: (disposition: BodyWriteResult['broadcast']) => void;
}
/**
 * Thrown by `graph.update` when the git commit fails after the disk write
 * has already landed. The .md is updated and observable to subsequent
 * readers via `graph.get`, but no commit exists — the repo is dirty.
 *
 * The caller MUST surface this typed error so an operator can recover
 * (manual `git add && git commit`, or `git checkout -- <path>` to revert
 * disk). v3-of-the-plan claim of "auto-convergence via sessionManager"
 * was false outside the autosave PUT path; Phase 2 explicitly registers
 * server-side dirty files with sessionManager when available, but the
 * caller still gets the typed error so it knows the write was partial.
 */
export declare class CommitFailedDirtyError extends Error {
    readonly entityType: string;
    readonly entityId: string;
    readonly path: string;
    readonly code: "COMMIT_FAILED_DIRTY";
    constructor(entityType: string, entityId: string, path: string, cause: unknown);
}
/**
 * Thrown by `graph.update` when a body write supplies a `baseRevision`
 * that doesn't match the current body's content hash. Caller (autosave,
 * MCP, agent, event processor) refetches and retries against the new
 * revision.
 */
export declare class StaleBaseRevisionError extends Error {
    readonly entityType: string;
    readonly entityId: string;
    readonly expected: string;
    readonly actual: string;
    readonly code: "STALE_BASE_REVISION";
    constructor(entityType: string, entityId: string, expected: string, actual: string);
}
/**
 * Callbacks that fire AFTER successful structural mutations. Used by the Yjs
 * persistence layer so snapshot files follow the .md file (rename → rename
 * snapshot, delete → delete snapshot, type change → rename across type dirs).
 *
 * Hooked at this layer because the four mutation methods (delete, rename,
 * changeType, plus cross-repo create+delete via /transfer) are the canonical
 * mutation surface — every code path that moves an entity goes through them.
 *
 * Body-write hooks (onBodyWritten, applyAgentContent, withDocLock,
 * assertWritable, onCommitFailedDirty) used to live here too. Phase 4
 * extracted them into {@link BodyWriteCoordinator} — different concurrency
 * profile, single critical section. This interface now covers ONLY the
 * structural-mutation notifications that drive snapshot bookkeeping.
 */
export interface GraphPersistenceCallbacks {
    onDelete?(repo: string, entityType: string, entityId: string): void | Promise<void>;
    onRename?(repo: string, entityType: string, oldId: string, newId: string): void | Promise<void>;
    onChangeType?(repo: string, entityId: string, fromType: string, toType: string): void | Promise<void>;
}
/**
 * Thrown by `restoreFile` when the path the recreate would land at is
 * currently occupied by a different entity. Carries the occupant's
 * coordinates so callers (agent tool, REST endpoint) can surface a
 * useful error to the user.
 */
export declare class PathConflictError extends Error {
    readonly path: string;
    readonly occupiedBy: {
        entityType: string;
        entityId: string;
    };
    readonly code: "PATH_CONFLICT";
    constructor(path: string, occupiedBy: {
        entityType: string;
        entityId: string;
    });
}
export declare class BaseGraphManager {
    private repoPath;
    readonly repoName: string;
    private gitService?;
    private noGit;
    private markdownService;
    private csvService;
    private assetService?;
    /** Workspace-global ownership index for immutable-keyed assets. Injected by
     *  WorkspaceManager (one per workspace). Undefined in coordinator-less test
     *  / CLI graphs — those fall back to the legacy `{type}/{id}` key scheme. */
    private assetIndex?;
    private gitUser;
    private vectorService?;
    private schemaRegistry?;
    private workspaceRoot?;
    /** Cache for history-panel change summaries, keyed by `${relPath}@${hash}`.
     *  A commit's diff against its parent is immutable, so this never needs
     *  invalidation — entries only accumulate as history is viewed. */
    private historyChangeCache;
    private persistenceCallbacks?;
    private bodyWriteCoordinator?;
    constructor(config: GraphConfig);
    /**
     * Friendly display label for an entity type — custom-type label when the
     * workspace defines one, otherwise the title-cased key ("deal" → "Deal").
     * Used to build human-readable commit messages.
     */
    private typeLabel;
    /** Build an EntityRef for a commit message from a type, id, and optional title. */
    private commitRef;
    /**
     * Build the friendly "Edited …" message for a deferred (batched) commit,
     * given the "type/id" pairs accumulated by the SessionManager. Each title is
     * resolved from the in-memory index at flush time (cheap lookup); an entity
     * deleted before the flush falls back to its slug id.
     */
    buildEditedBatchMessage(entities: Array<{
        type: string;
        id: string;
    }>): string;
    /**
     * Change summary for one commit in a file's history — which frontmatter
     * fields moved and whether the body changed — by diffing the file at `hash`
     * against its true git parent (`hash^`). Used by the entity History panel.
     *
     * `relPath` is repo-relative. Null parent (root commit, or file absent at the
     * parent) → `created`. Result is immutable per (path, hash), so it's cached.
     * Returns null only when the file isn't present at `hash`.
     */
    getCommitChange(relPath: string, hash: string): HistoryCommitChange | null;
    /** Expose git service for deferred commit operations (SessionManager). */
    getGitService(): GitService | undefined;
    /** Absolute repo path on disk. Used by callers that need to convert
     *  an entity's absolute path to a repo-relative one (e.g. for `git log`). */
    getRepoPath(): string;
    /**
     * Find an entity at a specific commit (existing or deleted) and
     * return its repo-relative path + content. Public surface for the
     * history tools — wraps the private `findEntityInCommit` walker.
     * Returns null when no .md file at that commit matches the
     * (entityType, entityId) pair.
     */
    getEntityAtCommit(entityType: EntityType, entityId: string, commitHash: string): {
        path: string;
        content: string;
    } | null;
    /**
     * `git pull` this repo, then reconcile any live editor Y.Doc whose .md the
     * pull rewrote. Mirrors the restore-reconcile fix (see `restoreFile`): a
     * pull changes files on disk WITHOUT flowing through the coordinator's
     * body-write path, so an open editor still holds the pre-pull content and
     * would sync it back over the pull on its next autosave (silent loss of the
     * pulled-in changes). Routing each changed entity through the coordinator
     * acquires the per-doc lock and smart-merges disk into the live Y.Doc.
     *
     * The wrinkle vs. restore: a pull is repo-wide, so we diff HEAD before/after
     * to find the changed .md files and reconcile EACH one's docName rather than
     * a single known entity. To avoid parsing every changed file on a large pull
     * when nobody is editing, the loop is gated to the entities that actually
     * have a live Y.Doc open (`coordinator.activeDocNames()`).
     *
     * Throws if the pull itself fails (merge conflict, no upstream, etc.) — the
     * caller (POST /api/pull) catches per-repo. Reconcile only runs after a
     * successful pull, so a conflicted/aborted pull never reconciles.
     *
     * Returns the docNames that were reconciled (empty when nothing was open,
     * the repo has no git service, or the pull was a no-op fast-forward to the
     * same HEAD).
     */
    pullAndReconcile(): Promise<{
        reconciled: string[];
    }>;
    /** Workspace root passed in via GraphConfig. Used by the asset upload
     *  service to anchor PDF text sidecars at workspace level (so all repos
     *  share the same `assets-text/` tree, mirroring the shared
     *  `.studiograph/assets/` directory the local backend uses). */
    getWorkspaceRoot(): string | undefined;
    /**
     * Install structural-mutation callbacks. Called once after construction by
     * the server bootstrap (or test setup) so the Yjs persistence layer can
     * react to rename/delete/changeType without route-layer code calling
     * persistence APIs directly.
     */
    setPersistenceCallbacks(callbacks: GraphPersistenceCallbacks | undefined): void;
    /**
     * Install the body-write coordinator. graph.update delegates the body-write
     * critical section (lock, base-hash, atomic disk write, commit, broadcast,
     * mdRevisionHash bookkeeping, assertWritable) to the coordinator. When
     * unset (test fixtures without a Y.Doc layer), body writes fall back to a
     * direct disk-write + commit path with no lock/broadcast — preserves the
     * legacy "no callbacks" behavior some test setups rely on.
     */
    setBodyWriteCoordinator(coordinator: BodyWriteCoordinator | undefined): void;
    /**
     * Ensure repository exists
     */
    private ensureRepo;
    /**
     * Compute the on-disk path for a new entity.
     *
     * - When `folderPath` is provided, the entry lives at {repo}/{folderPath}/{id}.md
     *   regardless of type. This is the nested-folders layout.
     * - Without `folderPath` (or empty string), the entry lands at the repo
     *   root: {repo}/{id}.md. The historical {repo}/{plural-type}/{id}.md
     *   fallback was confusing — entries hidden under a typed directory the
     *   list view never showed at repo root, contradicting the agent tool
     *   description that says "Omit to land at repo root."
     *
     * Note: this is for *new* paths only. Existing entries living under the
     * legacy {plural-type}/ directories continue to be readable via
     * findEntityFile() — only the create path moved.
     */
    private getEntityPath;
    private fileIndex?;
    /**
     * Cache of the parsed `list()` view, keyed by `entityType ?? '*'`. Built
     * lazily on first read; invalidated alongside `fileIndex` on every
     * mutation. Without this, autosuggest walks the entire markdown tree
     * and parses YAML frontmatter for every entry on every keystroke (5–7s
     * on workspaces with a few thousand entries).
     *
     * Staleness contract: identical to `fileIndex` — single-process trust
     * model. Git pulls and out-of-process file edits would equally miss
     * the existing `fileIndex` cache; no regression there.
     */
    private listCache?;
    /** Drop the cached file index so the next read rebuilds it. */
    private invalidateIndex;
    /**
     * Walk every .md file under this repo, calling `visit` with the absolute
     * path. Skips hidden directories (.git, .studiograph, etc.) and files that
     * start with ".".
     */
    private walkMarkdownFiles;
    /**
     * Build (or fetch the cached) "type/id" → absolute path index.
     * Type comes from frontmatter `entity_type`; if missing, falls back to the
     * top-level plural-type directory name (legacy layout). Files without either
     * (e.g. README.md) are skipped — they aren't entities.
     */
    private getFileIndex;
    /**
     * Infer entity type from a file's location for legacy layouts where the
     * file sits directly inside a top-level plural-type directory
     * ({repo}/meetings/foo.md). Returns undefined for any other layout.
     */
    private inferLegacyType;
    /**
     * Resolve an entity's absolute path by (type, id). Returns undefined if not
     * found. Uses the file index — the index keys by `${entityType}/${entityId}`
     * which is type-aware, unlike the on-disk filename. (We previously had a
     * legacy {plural-type}/{id}.md fast path here; once the create path moved
     * to flat repo-root layout, that fast path could return a different
     * type's file at the same id.)
     */
    private findEntityFile;
    /**
     * Validate entity data against schema
     */
    private validateEntity;
    /**
     * Create a new entity
     */
    create(options: CreateEntityOptions): Promise<EntityFile>;
    /**
     * Get an entity by type and ID. Searches nested folders if the entry isn't
     * at the legacy {plural-type}/{id}.md path.
     */
    get(entityType: EntityType, entityId: string): EntityFile;
    /**
     * The live editor body for an entity when a seeded Y.Doc is open, else
     * null. `get` reads disk, which lags the live Y.Doc by the autosave
     * debounce + commit — so a user's freshly-placed image/asset embed exists
     * in the editor before it reaches disk. Callers that must reflect what the
     * user is actually looking at (the get_entity surfaces, the whole-body
     * asset-drop guard) prefer this and fall back to disk on null.
     *
     * Returns null when no coordinator is wired (CLI / test fixtures) — those
     * setups have no live editor layer, so disk IS the authoritative body.
     */
    getLiveBody(entityType: EntityType, entityId: string): string | null;
    /**
     * Get an entity by ID only (type resolved from the filesystem).
     *
     * Walks the file index to find any entry with this id, regardless of folder
     * depth or type. Used by the agent's `get_entity` tool when the caller
     * doesn't know the entity's type.
     */
    getById(entityId: string): EntityFile;
    /**
     * Get an entity by its file path — repo-relative (as returned by the
     * search/list tools) or absolute. The most direct resolver: the caller
     * never needs the canonical type, which closes the search→get_entity
     * round-trip for entries whose frontmatter `type` field differs from
     * `entity_type` (e.g. an `event` carrying `type: meeting`). The path is
     * contained to this repo — a `..` escape or out-of-repo path is rejected
     * as not-found rather than read.
     */
    getByPath(filePath: string): EntityFile;
    /**
     * Merge a caller's `data` onto a base frontmatter, always stamping
     * `updated_at` / `updated_by`, then validate. Even content-only edits
     * (editor autosave, where `data` is undefined) re-stamp the audit fields —
     * otherwise the trail freezes at whatever the last data-change user was,
     * which for scaffolded files is "studiograph".
     *
     * `baseFrontmatter` is the frontmatter to merge onto. The body-write path
     * passes the CURRENT on-disk frontmatter (read inside the per-doc lock) so
     * a concurrent metadata-only edit isn't clobbered; the frontmatter-only
     * path passes `existing.data` (synchronous, no lock window).
     *
     * Validation uses lenient mode + a Date-coercion fallback when no new data
     * is supplied, since the existing entity may have been stored without all
     * required fields, and gray-matter would otherwise serialize Date objects
     * as full ISO timestamps.
     */
    private composeUpdatedFrontmatter;
    /**
     * Update an existing entity
     */
    update(options: UpdateEntityOptions): Promise<EntityFile>;
    /**
     * Delete an entity
     */
    /**
     * Delete multiple entities in a single git transaction. Each entity's
     * file is removed from disk; folder markers (.gitkeep) are added where
     * the deletion left a folder empty. Then everything commits as one
     * atomic operation.
     *
     * Compared to looping `delete()` per entity, this is a real performance
     * primitive: one fork of git, one index update, one commit, one ref
     * advance — instead of N. Bulk delete of a few hundred entities goes
     * from minutes to seconds.
     *
     * Errors per entity (e.g. "not found") don't abort the batch — they're
     * collected in `errors` and the rest still commit. Returns both the
     * successfully-deleted set (for vector cleanup + WS broadcast) and the
     * errors so the caller can surface partial failures.
     */
    bulkDelete(specs: Array<{
        entityType: EntityType;
        entityId: string;
    }>, options?: {
        commitMessage?: string;
        user?: GitUser;
    }): Promise<{
        deleted: Array<{
            entityType: EntityType;
            entityId: string;
            path: string;
            title?: string;
        }>;
        errors: Array<{
            entityType: EntityType;
            entityId: string;
            error: string;
        }>;
    }>;
    /**
     * Commit a set of already-on-disk file changes as a single git
     * operation. Used by callers (e.g. the import route) that perform many
     * `skipCommit: true` writes and want one transaction at the end instead
     * of per-file commits.
     *
     * `entries` describes what each file represents so the transaction can
     * be staged correctly. No-op when nothing is staged or git isn't
     * available.
     */
    commitPending(entries: Array<{
        operation: 'create' | 'update' | 'delete';
        entityType: EntityType;
        entityId: string;
        filePath: string;
    }>, options: {
        commitMessage: string;
        user?: GitUser;
        extraPaths?: string[];
    }): Promise<{
        committed: boolean;
    }>;
    delete(entityType: EntityType, entityId: string, commitMessage?: string, user?: GitUser, opts?: {
        keepAssets?: boolean;
    }): Promise<void>;
    /**
     * Rename an entity (change its ID). Preserves all data, content, and source_ref.
     * Uses git mv for clean history. Updates entity_id in frontmatter.
     */
    rename(entityType: EntityType, oldId: string, newId: string, commitMessage?: string, user?: GitUser): Promise<EntityFile>;
    /**
     * Change an entity's type. Updates frontmatter only — the file stays in
     * its current folder. In the type-agnostic folder model, type is a
     * frontmatter property independent of location. (Legacy behavior moved
     * the file between {plural-type}/ dirs; that's no longer correct because
     * those dirs are now ordinary user-visible folders, not type markers.)
     *
     * If the user wants the file to also move (e.g. out of a "meetings/"
     * folder when changed to a note), they invoke move() separately.
     */
    changeType(entityId: string, fromType: EntityType, toType: EntityType, commitMessage?: string, user?: GitUser): Promise<EntityFile>;
    /**
     * Move an entry to a different folder within the same repo. The filename
     * (and therefore the entity id) and type are unchanged. Auto-creates parent
     * folders. Refuses if the target path already has an entry with this id —
     * pass `overwrite: true` to replace it, but only when the occupant is
     * itself a parseable entity (never silently clobber arbitrary files like
     * README.md or .gitkeep).
     */
    move(entityType: EntityType, entityId: string, targetFolderPath: string, commitMessage?: string, user?: GitUser, overwrite?: boolean): Promise<EntityFile>;
    /**
     * Restore an entity to its content at a specific commit. Single-entity
     * scope only — multi-entity rollback (e.g. "undo this commit's effects
     * across all touched files") is a separate, more cautious flow that
     * isn't built yet.
     *
     * Two paths through the method:
     *   - **Overwrite**: the entity exists today at path `P`. Read content
     *     from `git show <hash>:<P>` and write it back to `P`.
     *   - **Recreate**: the entity is gone today. Walk the historical
     *     commit's tree to find the .md file whose basename matches
     *     `entityId` AND whose frontmatter `entity_type` matches
     *     `entityType`. That path is the restore target. The walk only
     *     fires when the entity is truly absent — overwrites stay cheap.
     *
     * Throws a `PathConflictError` if the recreate target path is now
     * occupied by a DIFFERENT entity (different type+id) — recreating
     * over it would silently overwrite unrelated work. Caller surfaces
     * the conflict to the user; resolution is "move or delete the
     * occupant first."
     */
    restoreFile(entityType: EntityType, entityId: string, commitHash: string, user?: GitUser): Promise<{
        action: 'recreate' | 'overwrite';
        path: string;
    }>;
    /**
     * Walk a commit's tree looking for a .md file whose basename matches
     * `entityId` AND whose frontmatter `entity_type` matches `entityType`.
     * Returns repo-relative path + content when found, otherwise null.
     * Used by `restoreFile` for the recreate-from-deletion case.
     */
    private findEntityInCommit;
    /**
     * Identify the entity (if any) currently occupying `absPath`. Returns
     * null when the path holds something that isn't an entity (README, etc).
     * Mirrors the inference rules used in `getFileIndex`.
     */
    private identifyEntityAtPath;
    /**
     * Enumerate the distinct entity types present in this repo. Used by
     * /api/repos to let the sidebar render the Views list without a full
     * entity fetch per folder.
     *
     * Derived from the file index (which reads frontmatter), so it works for
     * entries in any folder — not just the legacy {plural-type}/ dirs.
     */
    listEntityTypes(): string[];
    /**
     * Folder lifecycle: a directory is treated as a first-class persistent
     * object. It exists from the moment it's created (createFolder, the destination
     * of a moved entry, etc.) until it's explicitly removed via a folder-level
     * operation (deleteFolder, renameFolder, moveFolderToRepo). Entry-level
     * operations (create, delete, move, transfer) never decide whether a folder
     * lives or dies.
     *
     * `listFolders()` requires either a .md child or a .gitkeep marker for a
     * directory to surface in the API, so the two helpers below are the single
     * place that adds and removes the marker:
     *
     *  - `ensureFolderMarker` — call after an op that may have left `dir`
     *    without .md children. Writes `.gitkeep` and returns its path so the
     *    caller can stage it in the same commit.
     *  - `removeFolderMarker` — call after an op that placed an .md into `dir`.
     *    Drops a now-redundant `.gitkeep` so the repo stays tidy.
     *
     * Both no-op for the repo root (always implicit) and for paths that don't
     * exist. Both return `null` when nothing changed.
     */
    private ensureFolderMarker;
    private removeFolderMarker;
    /**
     * Walk up from `dir`, removing each directory that became empty after a
     * folder-level removal (deleteFolder, renameFolder, moveFolderToRepo).
     * Stops at the repo root. Silently no-ops if a directory is missing,
     * non-empty, or can't be removed.
     *
     * NOT called by entry-level operations (create/delete/move/transfer) —
     * they preserve folder structure regardless of whether they emptied a
     * directory. Use `ensureFolderMarker` instead for those paths.
     */
    private pruneEmptyAncestors;
    /**
     * List entities of a given type (or all types if not specified).
     *
     * Walks the entire repo tree so entries in nested folders are discovered.
     * The type filter is applied against frontmatter `entity_type`; legacy
     * entries without explicit frontmatter type fall back to the depth-1
     * plural-type directory name (so `meetings/foo.md` still resolves as a
     * meeting). Files with no inferable type (README.md, CONTRIBUTING.md, etc.)
     * are skipped.
     */
    list(entityType?: EntityType, limit?: number): EntityFile[];
    /**
     * Search entities
     */
    search(options: SearchOptions): EntityFile[];
    /**
     * Resolve a markdown file (anywhere under the repo) to its (type, id),
     * tolerating both legacy {plural-type}/{id}.md and nested-folder layouts.
     * Returns undefined for files without a recognizable entity type.
     */
    private resolveFileToEntity;
    /**
     * Get entities related to a given entity (via wikilinks)
     */
    getRelated(entityType: EntityType, entityId: string): EntityFile[];
    /**
     * Get entities that link to a given entity (backlinks)
     */
    getBacklinks(entityType: EntityType, entityId: string): EntityFile[];
    /**
     * Validate wikilinks in an entity
     */
    validateWikilinks(entityType: EntityType, entityId: string): {
        valid: string[];
        broken: string[];
    };
    /**
     * Refactor a wikilink (rename entity and update all references)
     */
    refactorWikilink(oldId: string, newId: string, commitMessage?: string): Promise<{
        updated: number;
    }>;
    /**
     * List every non-empty folder in this repo, plus folders explicitly kept
     * via .gitkeep. Returns repo-relative POSIX paths, sorted, deduplicated.
     * The repo root ("") is intentionally omitted — it's implicit.
     */
    listFolders(): FolderEntry[];
    /**
     * Read the display_name override for a single folder, or null if no sidecar
     * exists / is unreadable. Used by mutation paths that need the current
     * override before deciding whether to rewrite it.
     */
    readFolderDisplayName(folderPath: string): string | null;
    /**
     * Write or remove the per-folder display_name sidecar. Pass `null` /
     * empty-string to remove (returns to slug fallback). Returns the absolute
     * path of the sidecar (for git staging) when one was written or removed,
     * or null when no change was needed (idempotent).
     *
     * Caller is responsible for committing — the helper only touches the
     * filesystem. Folder must already exist; this never creates one.
     */
    writeFolderDisplayName(folderPath: string, displayName: string | null): string | null;
    /**
     * Bulk-write display_name sidecars for many folders at once. Used by the
     * import path to record original-casing overrides for every nested
     * directory it slugified. First-writer-wins: existing sidecars are NOT
     * overwritten, so a second import doesn't clobber a user's manual rename.
     *
     * Returns the list of sidecar paths actually written (for git staging).
     * Skipped paths (folder missing, empty display, display_name override
     * already set) are quietly omitted.
     */
    recordFolderDisplays(entries: Array<{
        path: string;
        display_name: string;
    }>): string[];
    /**
     * Create a folder at the given path (mkdir -p semantics) and drop a
     * .gitkeep so git tracks it even when empty. Validates the path. No-op if
     * the folder already exists; .gitkeep is added if missing.
     */
    createFolder(folderPath: string, commitMessage?: string, user?: GitUser, options?: {
        displayName?: string;
    }): Promise<{
        alreadyExisted: boolean;
    }>;
    /**
     * Ensure the folder at `folderPath` (or the repo root when empty) carries a
     * stable sidecar id, minting + committing one if absent. Returns the id.
     *
     * Saved views key folder scopes on this id (D2) so a view survives folder
     * rename / move — the sidecar travels with the directory the same way an
     * entity's frontmatter travels with its .md file. Idempotent: an existing id
     * is returned untouched, never regenerated.
     */
    ensureFolderId(folderPath: string, user?: GitUser): string;
    /**
     * Resolve a stable sidecar folder id to its repo-relative path within this
     * repo, or null when no folder here carries that id. Returns `''` when the
     * id belongs to the repo root.
     */
    findFolderPathById(id: string): string | null;
    /**
     * All folder ids carried in this repo — the root sidecar (path `''`) plus
     * every subfolder — as `{ id, path }` pairs. Built from a single
     * `listFolders()` walk so a caller resolving many ids at once (e.g. the saved
     * views GET) can index once instead of re-walking the tree per id.
     */
    folderIdEntries(): Array<{
        id: string;
        path: string;
    }>;
    /**
     * Rename / move a folder within the repo. Moves the entire subtree
     * atomically via filesystem rename. Refuses if the target path is already
     * occupied.
     */
    renameFolder(fromPath: string, toPath: string, commitMessage?: string, user?: GitUser, options?: {
        displayName?: string | null;
    }): Promise<void>;
    /**
     * Move a folder (and its full subtree) into a different repo at `targetFolderPath`.
     *
     * The naive way to do this is to iterate the source's entries and call
     * `transfer` per entry — that's what the web UI used to do, and it costs
     * 2 git commits per entry (1600 commits for 800 files), each blocking the
     * Node event loop via `execSync`. This method performs the move as a
     * single FS-level rename + one commit per repo, regardless of how many
     * files are inside.
     *
     * Returns the count of entries + subfolders moved so the caller can
     * surface progress.
     */
    moveFolderToRepo(sourceFolderPath: string, target: BaseGraphManager, targetFolderPath: string, options?: {
        commitMessage?: string;
        user?: GitUser;
    }): Promise<FolderDeleteResult>;
    /**
     * Move a batch of entries (potentially across many source folders) into a
     * single target folder, optionally in a different repo. The web UI's
     * multi-select bulk move used to call /move or /transfer per entry — that
     * was 1-2 sync git commits per entry, blocking the event loop on each one.
     * This collapses the operation to a single FS rename per file plus one
     * commit on each repo touched.
     */
    moveEntitiesToRepo(entries: Array<{
        entityType: EntityType;
        entityId: string;
    }>, target: BaseGraphManager, targetFolderPath: string, options?: {
        commitMessage?: string;
        user?: GitUser;
    }): Promise<{
        moved: number;
    }>;
    /**
     * Delete a folder. Refuses non-empty folders unless `recursive: true`.
     * Returns the count of entries + subfolders removed (for blast-radius UI).
     */
    deleteFolder(folderPath: string, options?: {
        recursive?: boolean;
        commitMessage?: string;
        user?: GitUser;
    }): Promise<FolderDeleteResult>;
    /**
     * Count the entries (.md files with entity_type) and subfolders inside an
     * absolute directory, recursively. Used to surface blast radius in the
     * delete-folder approval UI.
     */
    private countFolderContents;
    /**
     * Return the storage backend the graph is bound to, or `undefined` if
     * no asset service is configured. Used by routes that need to branch
     * between local-stream and R2-redirect behavior.
     */
    getAssetStorage(): 'local' | 'r2' | undefined;
    /** The asset storage service (raw key copy/exists), if configured. Used by
     *  the one-time key migration, which operates on raw `{...}/{file}` keys. */
    getAssetService(): AssetService | undefined;
    /**
     * Download the bytes of an asset OWNED by `assetEntityId` in THIS repo.
     * Used by the convert flow to turn a stored .docx/.xlsx into a derived
     * note/dataset. Backend-agnostic (local reads disk, R2 GetObjects).
     *
     * SECURITY: the asset's `asset_url` frontmatter is attacker-influenceable
     * (a folder member can create an `asset` entity whose url points at another
     * folder's storage key). So ownership is NOT taken from the url — it's
     * resolved through the authoritative asset index, scoped to
     * `(this.repoName, 'asset', assetEntityId)`, exactly like the GET
     * /api/assets serve route. The url is used only to disambiguate which of the
     * entity's own files to fetch (by filename). Unindexed legacy assets fall
     * back to the legacy `asset/{entityId}/{file}` key bound to this same entity
     * id; anything else is refused.
     */
    /**
     * Download an immutable-keyed asset's bytes by its assetId + filename. The
     * blob store is workspace-global (the key `{assetId}/{filename}` is
     * independent of repo), so any graph can fetch any asset; the caller — the
     * caption sweep — scopes to the rows it enumerated from assets.db.
     */
    downloadAssetById(assetId: string, filename: string): Promise<Buffer>;
    /** Pre-signed URL for an immutable-key asset ({assetId}/{filename}). */
    getAssetSignedUrlByAssetId(assetId: string, filename: string, method?: 'GET' | 'HEAD', expiresIn?: number): Promise<string>;
    /**
     * Mint a short-lived pre-signed URL for an asset. Only supported by
     * remote backends (R2); the local backend throws. `method` selects
     * the verb the URL is signed against — HEAD callers need a HEAD URL.
     */
    getAssetSignedUrl(entityType: EntityType, entityId: string, filename: string, keyPrefix?: string, expiresIn?: number, method?: 'GET' | 'HEAD'): Promise<string>;
    /**
     * Upload asset for an entity
     */
    uploadAsset(entityType: EntityType, entityId: string, filename: string, buffer: Buffer, keyPrefix?: string, 
    /** Auth user id of the uploader (web). Recorded in the asset index for
     *  the "delete by uploader + workspace owner" rule. Omit for CLI/import/
     *  agent uploads — stored as null (unknown → owner-only delete). */
    opts?: {
        uploaderUserId?: string | null;
        /**
         * Override the recorded owner. Default = the `(entityType, entityId)`
         * passed above (the entity that holds the blob — e.g. an image embedded
         * in a note). The asset-as-embed model (see `~/.claude/plans/asset-as-embed.md`)
         * uploads FREE-STANDING assets owned by a folder or the workspace, so the
         * blob survives deletion/move of any entity that merely embeds it. When
         * `owner` is set, `(entityType, entityId)` are ignored for ownership.
         */
        owner?: {
            type: string;
            id: string;
        };
        /** Display name recorded in the index row. Defaults to the filename.
         *  Matters for free-standing assets, where the row's name is the manager
         *  label (no owning entity to borrow a name from). */
        name?: string;
    }): Promise<AssetUploadResult>;
    /** Inject the workspace-global asset ownership index (WorkspaceManager). */
    setAssetIndex(index: AssetIndexService | undefined): void;
    /** The asset ownership index, if wired. */
    getAssetIndex(): AssetIndexService | undefined;
    /**
     * Remove every immutable-keyed asset owned by (entityType, entityId): the
     * blob + its index row. Used by delete/bulkDelete/deleteFolder. Blob errors
     * are non-fatal (markdown is the system of record). No-op without an index.
     * Owner-scoped, so it only touches THIS entity's assets — a blob another
     * entity also embeds (copied URL) is not enumerated here.
     */
    private cleanupOwnedAssets;
    /** Delete a single index-tracked asset by its immutable id: blob + index row.
     *  The asset-manager delete path for free-standing (folder/workspace-owned)
     *  assets, which have no owning entity to route an entity-coordinate delete
     *  through. Blob errors are non-fatal (the index row is the manager's source
     *  of truth, same as cleanupOwnedAssets). No-op without an index/service. */
    deleteAssetById(assetId: string, filename: string): Promise<void>;
    /** Re-point the owning coordinates of every asset owned by an entity, with
     *  no blob movement (keys are immutable). Used by rename (ownerId),
     *  changeType (ownerType), and cross-repo move/transfer (repo). No-op
     *  without an index. */
    private reassignOwnedAssets;
    /**
     * List assets for an entity
     */
    listAssets(entityType: EntityType, entityId: string, keyPrefix?: string): Promise<AssetInfo[]>;
    /**
     * List every asset in the backend in one round-trip, tagged with
     * (entityType, entityId) origin. Workspace-wide since the asset backend
     * is shared across repos — callers filter by repo via {@link listEntityIds}.
     */
    listAllAssets(): Promise<AssetRecord[]>;
    /**
     * Enumerate (type, id) pairs in this repo. Used by endpoints (like
     * /api/repos/:repo/assets) that need to answer "does entity X belong to
     * this repo?".
     *
     * Backed by the file index, which reads frontmatter — so entries in
     * nested folders are included alongside legacy {plural-type}/ entries.
     */
    listEntityIds(): Array<{
        entityType: EntityType;
        id: string;
    }>;
    /**
     * Resolve the absolute on-disk markdown path for an entity. Goes through
     * the file index so it works for both the flat repo-root layout and
     * arbitrary nested folders. Use this anywhere callers need a path —
     * never reconstruct one from `${plural-type}/${id}.md`, which assumes
     * the legacy typed-folder layout and silently misses everything else.
     */
    getEntityFilePath(entityType: EntityType, entityId: string): string | undefined;
    /**
     * Delete an asset
     */
    deleteAsset(entityType: EntityType, entityId: string, filename: string, keyPrefix?: string): Promise<void>;
    /**
     * Delete all assets for an entity (when entity is deleted)
     */
    deleteAllAssets(entityType: EntityType, entityId: string, keyPrefix?: string): Promise<number>;
    /**
     * Resolve the authoritative CSV body for a dataset: the live editor body
     * when one exists (the grid may be ahead of disk), else the on-disk body.
     */
    private datasetBody;
    /**
     * Get dataset rows, optionally filtered. Columns come from the CSV header.
     */
    getDatasetRows(entityId: string, filter?: DatasetFilter): DatasetRow[];
    /**
     * Column names for a dataset, read from the CSV header. Independent of the
     * rows so callers get the real columns even for a header-only dataset or a
     * zero-match filter (where deriving columns from result rows would be empty).
     */
    getDatasetColumns(entityId: string): string[];
    /**
     * Append rows to a dataset entity.
     *
     * The existing CSV body is preserved verbatim and the new rows are appended
     * after it — we do NOT re-parse and re-stringify existing rows. That keeps
     * datasets with duplicate column headers from collapsing (csv-parse's
     * object mode keeps only the last same-named column) and avoids reformatting
     * unrelated cells. New rows are mapped to the existing header columns; keys
     * not in the header are dropped (query the columns first). When the body has
     * no header yet, the columns are taken from the new rows.
     *
     * The write routes through `update()` so it shares the BodyWriteCoordinator
     * contract (per-doc lock, base-hash precondition, applyAgentContent, commit)
     * instead of writing to disk directly — same guarantees the agent's markdown
     * writes get. `baseRevision` makes a concurrent change since our read fail
     * loudly rather than silently back-stomp.
     */
    appendDatasetRows(entityId: string, newRows: Array<Record<string, unknown>>, commitMessage?: string): Promise<EntityFile>;
    /**
     * Get repository status
     */
    getStatus(): {
        modified: string[];
        added: string[];
        deleted: string[];
        untracked: string[];
    };
    /**
     * Get commit history
     */
    getHistory(limit?: number): Array<{
        hash: string;
        message: string;
        author: {
            name: string;
            email: string;
        };
        timestamp: number;
    }>;
}
