bcbd1bdb22
Integ / changes (push) Has been skipped
Pre-commit / pre-commit (push) Failing after 1s
CLI exit codes / changes (push) Has been skipped
Test (Install) / changes (push) Has been skipped
Test (Python) / changes (push) Has been skipped
Test (TypeScript) / changes (push) Has been skipped
CLI exit codes / cli-gate (push) Has been cancelled
Test (Install) / test-install-gate (push) Has been cancelled
Integ / integ-gate (push) Has been cancelled
Test (Python) / test-python-gate (push) Has been cancelled
Test (TypeScript) / test-typescript-gate (push) Has been cancelled
Test (Install) / python-minimal (3.12) (push) Has been cancelled
Test (Install) / python-minimal (3.11) (push) Has been cancelled
Test (Install) / python-extra (agno, mirage.agents.agno) (push) Has been cancelled
Test (Install) / python-extra (chroma, mirage.resource.chroma) (push) Has been cancelled
Test (Install) / python-extra (pdf, mirage.core.filetype.pdf) (push) Has been cancelled
Integ / integ (push) Has been cancelled
Integ / integ-database (push) Has been cancelled
Integ / integ-database-ts (push) Has been cancelled
Integ / integ-data (push) Has been cancelled
Integ / integ-ssh (push) Has been cancelled
Integ / integ-ssh-ts (push) Has been cancelled
Test (Python) / audit (push) Has been cancelled
Test (TypeScript) / test (push) Has been cancelled
Test (TypeScript) / python-fs-shim (push) Has been cancelled
CLI exit codes / Python CLI (push) Has been cancelled
CLI exit codes / TypeScript CLI (push) Has been cancelled
CLI exit codes / Cross-language snapshot interop (push) Has been cancelled
Test (Python) / test (push) Has been cancelled
Test (Python) / import-isolation (deepagents, openai, mirage.agents.openai_agents) (push) Has been cancelled
Test (Python) / import-isolation (deepagents, pydantic-ai, mirage.agents.pydantic_ai) (push) Has been cancelled
Integ / integ-ts (push) Has been cancelled
Integ / integ-fuse (push) Has been cancelled
Test (Install) / python-extra (databricks, mirage.resource.databricks_volume) (push) Has been cancelled
Test (Install) / python-extra (deepagents, mirage.agents.langchain) (push) Has been cancelled
Test (Install) / python-extra (email, mirage.resource.email) (push) Has been cancelled
Test (Install) / python-extra (fuse, mirage.fuse.mount) (push) Has been cancelled
Test (Install) / python-extra (hdf5, mirage.core.filetype.hdf5) (push) Has been cancelled
Test (Install) / python-extra (hf, mirage.resource.hf_buckets) (push) Has been cancelled
Test (Install) / python-extra (lancedb, mirage.resource.lancedb) (push) Has been cancelled
Test (Install) / python-extra (langfuse, mirage.resource.langfuse) (push) Has been cancelled
Test (Install) / python-extra (mongodb, mirage.resource.mongodb) (push) Has been cancelled
Test (Install) / python-extra (nextcloud, mirage.resource.nextcloud) (push) Has been cancelled
Test (Install) / python-extra (openai, mirage.agents.openai_agents) (push) Has been cancelled
Test (Install) / python-extra (openhands, mirage.agents.openhands, 3.12) (push) Has been cancelled
Test (Install) / python-extra (parquet, mirage.core.filetype.parquet) (push) Has been cancelled
Test (Install) / python-extra (postgres, mirage.resource.postgres) (push) Has been cancelled
Test (Install) / python-extra (pydantic-ai, mirage.agents.pydantic_ai) (push) Has been cancelled
Test (Install) / python-extra (qdrant, mirage.resource.qdrant) (push) Has been cancelled
Test (Install) / python-extra (redis, mirage.resource.redis) (push) Has been cancelled
Test (Install) / python-extra (s3, mirage.resource.s3) (push) Has been cancelled
Test (Install) / python-extra (ssh, mirage.resource.ssh) (push) Has been cancelled
Test (Install) / ts-minimal (push) Has been cancelled
605 lines
22 KiB
Python
605 lines
22 KiB
Python
# ========= 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 logging
|
|
from collections.abc import Callable
|
|
|
|
from mirage.accessor.base import Accessor
|
|
from mirage.cache.index import IndexCacheStore
|
|
from mirage.commands.builtin.grep_helper import BINARY_EXTENSIONS
|
|
from mirage.commands.resolve import get_extension
|
|
from mirage.core.jq import is_jsonl_path, is_streamable_jsonl_expr
|
|
from mirage.provision.types import Precision, ProvisionResult
|
|
from mirage.types import FileType, PathSpec
|
|
from mirage.utils.key_prefix import rekey
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
# Cap on entries visited by a planning walk (grep -r): beyond it the
|
|
# estimate degrades to an UNKNOWN floor instead of walking forever.
|
|
MAX_PLAN_WALK = 1000
|
|
|
|
|
|
async def _expand_globs(
|
|
resolve_glob: Callable | None,
|
|
accessor: Accessor,
|
|
paths: list[PathSpec],
|
|
index: IndexCacheStore | None,
|
|
) -> list[PathSpec]:
|
|
"""Expand glob operands the way the executor would.
|
|
|
|
Without a resolver (or on any backend error) the original paths are
|
|
returned, whose pattern entries then stat-fail into UNKNOWN floors,
|
|
which is the pre-expansion behavior.
|
|
"""
|
|
if resolve_glob is None:
|
|
return paths
|
|
if not any(isinstance(p, PathSpec) and p.pattern for p in paths):
|
|
return paths
|
|
try:
|
|
return await resolve_glob(accessor, paths, index)
|
|
except Exception as exc:
|
|
logger.debug("provision glob expansion failed: %s", exc)
|
|
return paths
|
|
|
|
|
|
async def _walk_files(
|
|
readdir: Callable,
|
|
stat: Callable,
|
|
accessor: Accessor,
|
|
roots: list[PathSpec],
|
|
index: IndexCacheStore | None,
|
|
) -> tuple[list[tuple[str, int]], bool]:
|
|
"""Walk directories the way grep -r does, collecting file sizes.
|
|
|
|
Directories recurse; files skipped by the executor (columnar
|
|
BINARY_EXTENSIONS) are skipped here too so the estimate matches
|
|
what the run would read. Returns (sized files, complete): complete
|
|
is False when the walk was capped or any entry failed to resolve,
|
|
in which case the totals are only floors.
|
|
"""
|
|
sized: list[tuple[str, int]] = []
|
|
complete = True
|
|
visited = 0
|
|
queue: list[PathSpec] = list(roots)
|
|
while queue:
|
|
p = queue.pop(0)
|
|
visited += 1
|
|
if visited > MAX_PLAN_WALK:
|
|
return sized, False
|
|
try:
|
|
s = await stat(accessor, p, index)
|
|
except Exception as exc:
|
|
logger.debug("provision walk stat failed for %s: %s", p.virtual,
|
|
exc)
|
|
complete = False
|
|
continue
|
|
if s.type == FileType.DIRECTORY:
|
|
try:
|
|
entries = await readdir(accessor, p, index)
|
|
except Exception as exc:
|
|
logger.debug("provision walk readdir failed for %s: %s",
|
|
p.virtual, exc)
|
|
complete = False
|
|
continue
|
|
queue.extend(
|
|
PathSpec.from_str_path(e, rekey(p.virtual, p.resource_path, e))
|
|
for e in entries)
|
|
continue
|
|
if get_extension(p.virtual) in BINARY_EXTENSIONS:
|
|
continue
|
|
if s.size is None:
|
|
complete = False
|
|
continue
|
|
sized.append((p.virtual, s.size))
|
|
return sized, complete
|
|
|
|
|
|
async def _resolve_sizes(
|
|
stat: Callable,
|
|
accessor: Accessor,
|
|
paths: list[PathSpec],
|
|
index: IndexCacheStore | None,
|
|
) -> tuple[list[tuple[str, int]], int]:
|
|
resolved: list[tuple[str, int]] = []
|
|
missing = 0
|
|
for p in paths:
|
|
path_str = p.virtual if isinstance(p, PathSpec) else p
|
|
size = None
|
|
if index is not None:
|
|
lookup = await index.get(path_str)
|
|
if lookup.entry is not None:
|
|
size = lookup.entry.size
|
|
if size is None:
|
|
# A provision estimate must degrade, never fail: any stat
|
|
# error (missing file, transient backend error) leaves the
|
|
# path unresolved and the result UNKNOWN (mirrors TS).
|
|
try:
|
|
file_stat = await stat(accessor, p, index)
|
|
size = file_stat.size
|
|
except Exception as exc:
|
|
logger.debug("provision stat failed for %s: %s", path_str, exc)
|
|
if size is not None:
|
|
resolved.append((path_str, size))
|
|
else:
|
|
missing += 1
|
|
return resolved, missing
|
|
|
|
|
|
def make_file_read_provision(stat: Callable,
|
|
resolve_glob: Callable | None = None) -> Callable:
|
|
"""Cost estimate for full file reads (cat, wc), generic over stat."""
|
|
|
|
async def file_read_provision(
|
|
accessor: Accessor,
|
|
paths: list[PathSpec],
|
|
*_args: str,
|
|
command: str = "",
|
|
index: IndexCacheStore | None = None,
|
|
**kwargs,
|
|
) -> ProvisionResult:
|
|
if not paths:
|
|
# Pathless invocations are stdin-driven (pipe stage, heredoc,
|
|
# or an immediate missing-operand error): zero backend bytes.
|
|
return ProvisionResult(command=command, precision=Precision.EXACT)
|
|
paths = await _expand_globs(resolve_glob, accessor, paths, index)
|
|
if not paths:
|
|
return ProvisionResult(command=command,
|
|
precision=Precision.UNKNOWN)
|
|
resolved, missing = await _resolve_sizes(stat, accessor, paths, index)
|
|
total = sum(size for _, size in resolved)
|
|
if missing > 0 or not resolved:
|
|
# Sizes we could not resolve (virtual/rendered files) are
|
|
# carried as UNKNOWN precision; the totals are floors.
|
|
return ProvisionResult(
|
|
command=command,
|
|
network_read_low=total,
|
|
network_read_high=total,
|
|
read_ops=len(paths),
|
|
precision=Precision.UNKNOWN,
|
|
)
|
|
return ProvisionResult(
|
|
command=command,
|
|
network_read_low=total,
|
|
network_read_high=total,
|
|
read_ops=len(resolved),
|
|
precision=Precision.EXACT,
|
|
)
|
|
|
|
return file_read_provision
|
|
|
|
|
|
def make_head_tail_provision(stat: Callable,
|
|
resolve_glob: Callable | None = None) -> Callable:
|
|
"""Cost estimate for partial reads (head, tail), generic over stat."""
|
|
|
|
async def head_tail_provision(
|
|
accessor: Accessor,
|
|
paths: list[PathSpec],
|
|
*_args: str,
|
|
command: str = "",
|
|
n: str | int | None = None,
|
|
c: str | int | None = None,
|
|
index: IndexCacheStore | None = None,
|
|
**kwargs,
|
|
) -> ProvisionResult:
|
|
if not paths:
|
|
# Pathless invocations are stdin-driven (pipe stage, heredoc,
|
|
# or an immediate missing-operand error): zero backend bytes.
|
|
return ProvisionResult(command=command, precision=Precision.EXACT)
|
|
paths = await _expand_globs(resolve_glob, accessor, paths, index)
|
|
if not paths:
|
|
return ProvisionResult(command=command,
|
|
precision=Precision.UNKNOWN)
|
|
resolved, missing = await _resolve_sizes(stat, accessor, paths, index)
|
|
if missing > 0 or not resolved:
|
|
full = sum(size for _, size in resolved)
|
|
return ProvisionResult(
|
|
command=command,
|
|
network_read_low=0,
|
|
network_read_high=full,
|
|
read_ops=len(paths),
|
|
precision=Precision.UNKNOWN,
|
|
)
|
|
# A byte-count -c only; boolean -c flags (e.g. file -c) don't cap.
|
|
if c is not None and not isinstance(c, bool):
|
|
c_bytes = int(c)
|
|
total = sum(min(c_bytes, size) for _, size in resolved)
|
|
return ProvisionResult(
|
|
command=command,
|
|
network_read_low=total,
|
|
network_read_high=total,
|
|
read_ops=len(resolved),
|
|
precision=Precision.EXACT,
|
|
)
|
|
full = sum(size for _, size in resolved)
|
|
return ProvisionResult(
|
|
command=command,
|
|
network_read_low=0,
|
|
network_read_high=full,
|
|
read_ops=len(resolved),
|
|
precision=Precision.RANGE,
|
|
)
|
|
|
|
return head_tail_provision
|
|
|
|
|
|
async def metadata_provision(
|
|
accessor: Accessor,
|
|
paths: list[PathSpec],
|
|
*_args: str,
|
|
command: str = "",
|
|
index: IndexCacheStore | None = None,
|
|
**kwargs,
|
|
) -> ProvisionResult:
|
|
"""Cost estimate for metadata-only ops (stat, ls, find)."""
|
|
n = max(1, len(paths) if paths else 1)
|
|
return ProvisionResult(
|
|
command=command,
|
|
network_read_low=0,
|
|
network_read_high=0,
|
|
read_ops=n,
|
|
precision=Precision.EXACT,
|
|
)
|
|
|
|
|
|
async def exact_zero_provision(command: str = "") -> ProvisionResult:
|
|
"""Zero-cost EXACT estimate for backends whose listing is in memory.
|
|
|
|
Chat/KB backends materialize their virtual tree from state the mount
|
|
already fetched, so metadata commands cost no backend I/O at all
|
|
(unlike :func:`metadata_provision`, which charges one op per operand).
|
|
|
|
Args:
|
|
command (str): the shell line being estimated, for display.
|
|
"""
|
|
return ProvisionResult(
|
|
command=command,
|
|
network_read_low=0,
|
|
network_read_high=0,
|
|
read_ops=0,
|
|
precision=Precision.EXACT,
|
|
)
|
|
|
|
|
|
async def index_hit_read_provision(
|
|
accessor: Accessor,
|
|
paths: list[PathSpec],
|
|
command: str,
|
|
index: IndexCacheStore | None = None,
|
|
) -> ProvisionResult:
|
|
"""Charge one read op per index-cached operand, zero network bytes.
|
|
|
|
The chat backends (discord, slack, ...) rebuild file bytes from API
|
|
state, so a read costs ops rather than sized transfers; operands the
|
|
index has never seen leave the estimate UNKNOWN.
|
|
|
|
Args:
|
|
accessor (Accessor): backend handle, unused but part of the
|
|
provision call shape.
|
|
paths (list[PathSpec]): operand paths as parsed.
|
|
command (str): the shell line being estimated, for display.
|
|
index (IndexCacheStore | None): the per-call cache index.
|
|
"""
|
|
if not paths:
|
|
return ProvisionResult(command=command, precision=Precision.UNKNOWN)
|
|
ops = 0
|
|
if index is not None:
|
|
for p in paths:
|
|
path_str = p.virtual if isinstance(p, PathSpec) else p
|
|
lookup = await index.get(path_str)
|
|
if lookup.entry is not None:
|
|
ops += 1
|
|
return ProvisionResult(
|
|
command=command,
|
|
network_read_low=0,
|
|
network_read_high=0,
|
|
read_ops=ops,
|
|
precision=Precision.EXACT,
|
|
)
|
|
|
|
|
|
def make_jq_provision(stat: Callable) -> Callable:
|
|
"""Provision for jq: streamable jsonl reads a range, else whole file."""
|
|
|
|
async def jq_provision(
|
|
accessor: Accessor,
|
|
paths: list[PathSpec] | None = None,
|
|
*texts: str,
|
|
index: IndexCacheStore | None = None,
|
|
**kwargs,
|
|
) -> ProvisionResult:
|
|
if not paths:
|
|
# A pathless jq filters stdin (or errors without an expr):
|
|
# zero backend bytes either way.
|
|
return ProvisionResult(command="jq", precision=Precision.EXACT)
|
|
if not texts:
|
|
return ProvisionResult(command="jq", precision=Precision.UNKNOWN)
|
|
p = paths[0]
|
|
key = p.mount_path if isinstance(p, PathSpec) else p
|
|
try:
|
|
file_stat = await stat(accessor, p, index)
|
|
except Exception as exc:
|
|
logger.debug("provision stat failed for %s: %s", key, exc)
|
|
return ProvisionResult(command="jq", precision=Precision.UNKNOWN)
|
|
expr = texts[0]
|
|
shown = p.virtual if isinstance(p, PathSpec) else p
|
|
rendered = f"jq {expr!r} {shown}"
|
|
if file_stat.size is None:
|
|
return ProvisionResult(command=rendered,
|
|
read_ops=1,
|
|
precision=Precision.UNKNOWN)
|
|
file_size = file_stat.size
|
|
if is_jsonl_path(key) and is_streamable_jsonl_expr(expr):
|
|
return ProvisionResult(
|
|
command=rendered,
|
|
network_read_low=0,
|
|
network_read_high=file_size,
|
|
read_ops=1,
|
|
precision=Precision.RANGE,
|
|
)
|
|
return ProvisionResult(
|
|
command=rendered,
|
|
network_read_low=file_size,
|
|
network_read_high=file_size,
|
|
read_ops=1,
|
|
precision=Precision.EXACT,
|
|
)
|
|
|
|
return jq_provision
|
|
|
|
|
|
def make_sed_provision(stat: Callable) -> Callable:
|
|
"""Provision for sed: operands are read fully; -i writes back, so
|
|
the output keeps the read total as a floor with UNKNOWN."""
|
|
base = make_file_read_provision(stat)
|
|
|
|
async def sed_provision(
|
|
accessor: Accessor,
|
|
paths: list[PathSpec],
|
|
*_args: str,
|
|
command: str = "",
|
|
i: bool = False,
|
|
index: IndexCacheStore | None = None,
|
|
**kwargs,
|
|
) -> ProvisionResult:
|
|
result = await base(accessor, paths, command=command, index=index)
|
|
if i:
|
|
result.precision = Precision.UNKNOWN
|
|
return result
|
|
|
|
return sed_provision
|
|
|
|
|
|
def make_search_provision(stat: Callable,
|
|
resolve_glob: Callable | None = None,
|
|
readdir: Callable | None = None) -> Callable:
|
|
"""Provision for grep/rg/jq: render pattern then delegate to file_read.
|
|
|
|
With -r/-R and a readdir, directory operands are walked the way the
|
|
executor walks them (recursing subdirectories, skipping columnar
|
|
files), so a recursive search over an indexed tree prices exactly.
|
|
"""
|
|
base = make_file_read_provision(stat, resolve_glob)
|
|
|
|
async def search_provision(
|
|
accessor: Accessor,
|
|
paths: list[PathSpec],
|
|
*texts: str,
|
|
command: str = "",
|
|
index: IndexCacheStore | None = None,
|
|
r: bool = False,
|
|
R: bool = False,
|
|
**kwargs,
|
|
) -> ProvisionResult:
|
|
rendered = (command or ""
|
|
) + " " + " ".join(list(texts) + [str(p) for p in paths])
|
|
if (r or R) and readdir is not None and paths:
|
|
roots = await _expand_globs(resolve_glob, accessor, paths, index)
|
|
sized, complete = await _walk_files(readdir, stat, accessor, roots,
|
|
index)
|
|
total = sum(size for _, size in sized)
|
|
if not complete or not sized:
|
|
return ProvisionResult(
|
|
command=rendered,
|
|
network_read_low=total,
|
|
network_read_high=total,
|
|
read_ops=max(len(sized), len(paths)),
|
|
precision=Precision.UNKNOWN,
|
|
)
|
|
return ProvisionResult(
|
|
command=rendered,
|
|
network_read_low=total,
|
|
network_read_high=total,
|
|
read_ops=len(sized),
|
|
precision=Precision.EXACT,
|
|
)
|
|
return await base(accessor, paths, command=rendered, index=index)
|
|
|
|
return search_provision
|
|
|
|
|
|
def make_transform_provision(stat: Callable,
|
|
resolve_glob: Callable | None = None) -> Callable:
|
|
"""Provision for read-transform-write commands (gzip, tar, split).
|
|
|
|
The operands are read fully, so the read side is a known floor, but
|
|
the output size (compression ratio, piece count) is unknowable
|
|
before running, so precision stays UNKNOWN.
|
|
"""
|
|
base = make_file_read_provision(stat, resolve_glob)
|
|
|
|
async def transform_provision(
|
|
accessor: Accessor,
|
|
paths: list[PathSpec],
|
|
*_args: str,
|
|
command: str = "",
|
|
index: IndexCacheStore | None = None,
|
|
**kwargs,
|
|
) -> ProvisionResult:
|
|
result = await base(accessor, paths, command=command, index=index)
|
|
if paths:
|
|
# Output size (compression ratio, piece count) is unknowable.
|
|
# A pathless transform filters stdin to stdout: exact zero.
|
|
result.precision = Precision.UNKNOWN
|
|
return result
|
|
|
|
return transform_provision
|
|
|
|
|
|
def make_copy_provision(stat: Callable,
|
|
resolve_glob: Callable | None = None) -> Callable:
|
|
"""Provision for cp: bytes bracket 0 (server-side copy) to the total.
|
|
|
|
Reads the source sizes and reports both network_read and
|
|
network_write as a 0..total range: a same-backend copy can be
|
|
server-side (zero client bytes) while a streamed copy moves the
|
|
full byte count each way.
|
|
"""
|
|
|
|
async def copy_provision(
|
|
accessor: Accessor,
|
|
paths: list[PathSpec],
|
|
*_args: str,
|
|
command: str = "",
|
|
index: IndexCacheStore | None = None,
|
|
**kwargs,
|
|
) -> ProvisionResult:
|
|
paths = await _expand_globs(resolve_glob, accessor, paths, index)
|
|
sources = paths[:-1] if len(paths) > 1 else paths
|
|
if not sources:
|
|
return ProvisionResult(command=command,
|
|
precision=Precision.UNKNOWN)
|
|
resolved, missing = await _resolve_sizes(stat, accessor, sources,
|
|
index)
|
|
total = sum(size for _, size in resolved)
|
|
precision = (Precision.UNKNOWN
|
|
if missing > 0 or not resolved else Precision.RANGE)
|
|
return ProvisionResult(
|
|
command=command,
|
|
network_read_low=0,
|
|
network_read_high=total,
|
|
network_write_low=0,
|
|
network_write_high=total,
|
|
read_ops=len(sources),
|
|
precision=precision,
|
|
)
|
|
|
|
return copy_provision
|
|
|
|
|
|
async def write_metadata_provision(
|
|
accessor: Accessor,
|
|
paths: list[PathSpec],
|
|
*_args: str,
|
|
command: str = "",
|
|
r: bool = False,
|
|
R: bool = False,
|
|
index: IndexCacheStore | None = None,
|
|
**kwargs,
|
|
) -> ProvisionResult:
|
|
"""Provision for metadata-only writes (rm, mkdir, touch, ln).
|
|
|
|
These never move content bytes on any backend; the op count is the
|
|
operand count. A recursive rm walks an unknown subtree, so its op
|
|
count is only a floor and precision degrades to UNKNOWN.
|
|
"""
|
|
n = max(1, len(paths) if paths else 1)
|
|
precision = Precision.UNKNOWN if r or R else Precision.EXACT
|
|
return ProvisionResult(
|
|
command=command,
|
|
network_read_low=0,
|
|
network_read_high=0,
|
|
read_ops=n,
|
|
precision=precision,
|
|
)
|
|
|
|
|
|
async def pure_provision(
|
|
accessor: Accessor | None = None,
|
|
paths: list[PathSpec] | None = None,
|
|
*_args: str,
|
|
command: str = "",
|
|
**kwargs,
|
|
) -> ProvisionResult:
|
|
"""Provision for pure local computation (seq, date, bc): zero cost."""
|
|
return ProvisionResult(command=command, precision=Precision.EXACT)
|
|
|
|
|
|
FILE_READ_COMMANDS = frozenset({
|
|
"awk", "base64", "cat", "cmp", "column", "comm", "cut", "diff", "expand",
|
|
"fmt", "fold", "iconv", "join", "look", "md5", "nl", "paste", "rev",
|
|
"sha256sum", "shuf", "sort", "strings", "tac", "tr", "tsort", "unexpand",
|
|
"uniq", "wc", "xxd", "zcat"
|
|
})
|
|
# file reads a bounded prefix (magic bytes), so it shares head/tail's
|
|
# 0..size range estimate.
|
|
HEAD_TAIL_COMMANDS = frozenset({"file", "head", "tail"})
|
|
SEARCH_COMMANDS = frozenset({"grep", "rg", "zgrep"})
|
|
METADATA_COMMANDS = frozenset({
|
|
"basename", "dirname", "du", "find", "ls", "readlink", "realpath", "stat",
|
|
"tree"
|
|
})
|
|
TRANSFORM_COMMANDS = frozenset(
|
|
{"csplit", "gunzip", "gzip", "patch", "split", "tar", "unzip", "zip"})
|
|
WRITE_METADATA_COMMANDS = frozenset({"ln", "mkdir", "mktemp", "rm", "touch"})
|
|
|
|
|
|
def default_provision(name: str,
|
|
stat: Callable,
|
|
resolve_glob: Callable | None = None,
|
|
readdir: Callable | None = None) -> Callable | None:
|
|
"""Default cost estimator for a factory-built command, by family.
|
|
|
|
Whole-file readers stat their operands and charge the byte total;
|
|
searches charge a worst-case full read; metadata commands charge op
|
|
counts only; transforms keep the read floor with UNKNOWN output;
|
|
cp brackets 0..total on both read and write; metadata writes are
|
|
zero-byte op counts. mv, tee, and anything unlisted return None so
|
|
the planner reports UNKNOWN (mv may be a free rename or a full
|
|
cross-mount copy; tee's stdin size is unknowable). A backend
|
|
disables a default by passing an explicit None in
|
|
provision_overrides.
|
|
|
|
Args:
|
|
name (str): Builder/command name.
|
|
stat (Callable): Backend stat used to resolve operand sizes.
|
|
resolve_glob (Callable | None): Backend glob resolver; byte
|
|
estimators expand pattern operands with it instead of
|
|
flooring them to UNKNOWN.
|
|
readdir (Callable | None): Backend readdir; recursive searches
|
|
walk directories with it for exact totals.
|
|
|
|
Returns:
|
|
Callable | None: Provision function, or None when the family has
|
|
no sensible generic estimate.
|
|
"""
|
|
if name in FILE_READ_COMMANDS:
|
|
return make_file_read_provision(stat, resolve_glob)
|
|
if name in HEAD_TAIL_COMMANDS:
|
|
return make_head_tail_provision(stat, resolve_glob)
|
|
if name in SEARCH_COMMANDS:
|
|
return make_search_provision(stat, resolve_glob, readdir)
|
|
if name in METADATA_COMMANDS:
|
|
return metadata_provision
|
|
if name in TRANSFORM_COMMANDS:
|
|
return make_transform_provision(stat, resolve_glob)
|
|
if name in WRITE_METADATA_COMMANDS:
|
|
return write_metadata_provision
|
|
if name == "cp":
|
|
return make_copy_provision(stat, resolve_glob)
|
|
if name == "jq":
|
|
return make_jq_provision(stat)
|
|
return None
|