API Reference

Top-level modules that survived the 2026-05 restructure. Removed scitex_dev.{skills,fix,ci,deploy,versions,docs,search,_discovery,_manifest,_builder} were absorbed into _cli/, _creds/, _ecosystem/, etc. — see the CLI surface and per-subcommand entry points for the live API.

CLI entry point

Backward-compat shim — re-exports from scitex_dev._dispatch.

The reusable docs/skills CLI mixins moved to scitex_dev._dispatch in 0.11.0. This shim keeps from scitex_dev.cli import … working for downstream packages (figrecipe, scitex-io, scitex-app, etc.) until they migrate. Will be removed in 0.12.0.

Also the public promotion target for attach_shell_completion (previously only reachable via the private scitex_dev._cli._completion path — see docs/adr/0003-ecosystem-boundary-ports-and-producers.md). Resolved lazily via __getattr__ (PEP 562) rather than a top-level import: eagerly importing scitex_dev._cli triggers _cli/_root.py’s module-level registration of the ENTIRE subcommand tree (several hundred ms) as a side effect, which every other symbol in this lightweight shim module deliberately avoids paying.

scitex_dev.cli.docs_click_group(package: str, name: str = 'docs')[source]

Create a Click command group for docs (requires Click installed).

Usage::

from scitex_dev.cli import docs_click_group cli.add_command(docs_click_group(package=”scitex-writer”))

scitex_dev.cli.register_docs_subcommand(subparsers: _SubParsersAction, package: str) → ArgumentParser[source]

Register docs with list and get verb subcommands.

Usage::

scitex-stats docs list # List doc pages scitex-stats docs get # Show available pages scitex-stats docs get api # Show specific page

scitex_dev.cli.register_skills_subcommand(subparsers: _SubParsersAction, package: str) → ArgumentParser[source]

Register skills with list and get verb subcommands.

Usage::

scitex-stats skills list # List skill pages scitex-stats skills get # Show main SKILL.md scitex-stats skills get test-selection # Show a reference page

scitex_dev.cli.skills_click_group(package: str, name: str = 'skills')[source]

Create a Click command group for skills (requires Click installed).

Usage::

from scitex_dev.cli import skills_click_group cli.add_command(skills_click_group(package=”scitex-app”))

CLI subcommand groups

SciTeX-dev CLI package — entry point + reusable subcommand mixins.

Module map: - _root — top-level Click group main (the scitex-dev console script entry point) - _utils — handle_result, run_as_cli, wrap_as_cli, json/dry-run option helpers - _completion — shell tab-completion installer - _doctor — scitex-dev doctor - _stats — scitex-dev ecosystem stats - audit/ — _summary (was _cli_audit), _api, _project, _skills - ecosystem/ — _registry (was _cli_ecosystem) - quality/ — _check (was _cli_quality), _frontmatter - skills/ — _manage (was _cli_skills), _tags

The main callable is the [project.scripts] target — scitex_dev._cli:main must remain importable.

scitex_dev._cli.add_dry_run_argument(parser: Any) → None[source]

Add --dry-run flag to an argparse parser.

scitex_dev._cli.add_json_argument(parser: Any) → None[source]

Add --json flag to an argparse parser.

scitex_dev._cli.dry_run_option(fn: Callable) → Callable[source]

Click decorator: adds --dry-run flag.

Uses lazy import click so scitex-dev stays stdlib-only.

scitex_dev._cli.handle_result(result: Result, as_json: bool = False, file: Any = None) → int[source]

Format and print a Result, return the exit code.

Parameters:
  • result (Result) – The structured result to display.

  • as_json (bool) – If True, output full JSON. If False, human-friendly text.

  • file (file-like | None) – Output stream. Defaults to stdout/stderr based on success.

Returns:

Exit code suitable for sys.exit().

Return type:

int

scitex_dev._cli.json_option(fn: Callable) → Callable[source]

Click decorator: adds --json flag as as_json parameter.

Uses lazy import click so scitex-dev stays stdlib-only.

scitex_dev._cli.run_as_cli(fn: Callable, as_json: bool = False, **kwargs: Any) → None[source]

Call a @supports_return_as function and exit with proper code.

Parameters:
  • fn (Callable) – A function decorated with @supports_return_as.

  • as_json (bool) – If True, output full JSON.

  • **kwargs – Arguments to pass to fn.

scitex_dev._cli.wrap_as_cli(fn: Callable, as_json: bool = False, **kwargs: Any) → None[source]

Call any function and display its result via CLI.

Like wrap_as_mcp but for terminal output. Wraps any plain function in Result, formats based on as_json, and exits with proper exit code.

Parameters:
  • fn (Callable) – Any callable returning data or raising exceptions.

  • as_json (bool) – If True, output full JSON Result. If False, human-friendly text.

  • **kwargs – Arguments to pass to fn.

Core

Core glue: config, types, errors, dispatch, discovery, manifests.

Ecosystem

SciTeX ecosystem registry, dependency graph, and package-state utilities.

Module map: - _core — ECOSYSTEM registry + get_all_packages / get_local_path - _graph — pyproject dependency graph (discover, parse, mermaid/dot, cycles) - _packages — multi-host package SHA collection + audit/sync helpers

scitex_dev._ecosystem.get_all_packages() → List[str][source]

Registered ecosystem package names — a CURATED SCOPE, not a census.

NOT A COMPLETE ENUMERATION OF THE FLEET. This returns exactly the packages someone added to ECOSYSTEM. Distributions can exist, ship, and FEDERATE INTO SCITEX-DEV’S OWN ENTRY-POINT GROUPS while absent from it — measured 2026-07-29, four of them: scitex-storage, scitex-cards, scitex-linter, scitex-gen.

Do not use this as a DENOMINATOR for “the fleet”. It answers “which packages are in scope for ecosystem-wide operations”, which is a different question and a smaller set.

The cost of the other reading is on record. scitex-storage is not a member, declares a system_deps entry point, and entry-point discovery reads the INSTALLED ENVIRONMENT rather than this dict — so its fclones declaration reached apt-get install and detonated a container bake that nothing here could have predicted. Anyone auditing “who declares system deps” from this function would have answered 4 when the answer was 5.

Two enumeration sources exist and they have OPPOSITE blind spots:

entry points -> only what is INSTALLED in the calling env ECOSYSTEM -> only what was DECLARED a member

Neither is complete, and reaching for the more official-looking one is what produced the miss. For a real census use gh repo list scitex-ai, or enumerate entry points from the environment you actually care about.

Nothing records WHY a given package is in or out — there are no written curation criteria — so absence from this dict does not distinguish “deliberately out of scope” from “nobody added it yet”. Whether membership should GATE federation is open (ecosystem-registry-is-not-a-complete-fleet-enumeration-20260730); it would break the four distributions above, including sac, so it is not a change to make casually.

scitex_dev._ecosystem.get_local_path(package: str) → Path | None[source]

Get expanded local path for a package.

scitex_dev._ecosystem.is_mcp_mountable(package: str) → bool[source]

Return True iff package may be auto-mounted onto the umbrella MCP.

A package is NOT mountable when any of the following hold:

  • it is unknown to the registry (fail closed — nothing to mount),

  • its ECOSYSTEM record is archived,

  • its category is one the aggregator never mounts (umbrella / template),

  • its record sets "mcp_mountable": False,

  • it is listed in _MCP_UNMOUNTABLE.

This is the SSoT the umbrella aggregator consults so the skip policy lives in one place (scitex-dev), not duplicated in scitex-python.

scitex_dev._ecosystem.mountable_peers() → List[str][source]

Return the ordered list of packages the umbrella MCP should mount.

scitex_dev._ecosystem.should_skip_audit(package: str, auditor: str) → tuple[bool, str][source]

Return (skip, reason) for running auditor on package.

auditor is one of the keys in _CATEGORY_SKIP. Unknown auditors return (False, "") — fail open so a typo doesn’t silently skip.

Skip semantics:

  • archived packages are skipped for every auditor.

  • per-auditor categories listed in _CATEGORY_SKIP are skipped.

  • unknown package (not in ECOSYSTEM) is NOT skipped — the auditor’s own not-found path will handle it.

Credentials

Ecosystem-wide Claude Code credential rotation.

Multiplexes ~/.claude/.credentials.json to the GitHub Actions secret slot CLAUDE_CODE_CREDENTIALS_JSON (with sha256 sidecar variable CLAUDE_CODE_CREDENTIALS_JSON_SHA256) across every package in the SciTeX ecosystem registry.

Distinct from the single-repo sac dev upload-credentials-to-github command — that one writes the package-prefixed SAC_… slot. This module uses the canonical un-prefixed name shared by every scitex package that consumes Claude Code OAuth.

class scitex_dev._creds.RotateResult(package: str, repo: str | None, status: str, message: str = '')[source]

Per-repo rotation outcome.

is_error() → bool[source]
message: str = ''
package: str
repo: str | None
status: str
scitex_dev._creds.rotate_all(*, packages: Iterable[str] | None = None, only: Iterable[str] | None = None, exclude: Iterable[str] | None = None, source_path: Path = PosixPath('/home/ywatanabe/.cache/scitex-dev-release/publisher029-1112/home/.claude/.credentials.json'), dry_run: bool = False, force: bool = False, ecosystem: dict | None = None, local_path_lookup=None) → list[RotateResult][source]

Rotate the credential across every (or a filtered set of) repo.

Returns one RotateResult per package processed. Returns an empty list if the source is silently absent/expired — that is the “fresh laptop / expired token” exit-0 path, deliberately quiet.

scitex_dev._creds.validate_source(path: Path = PosixPath('/home/ywatanabe/.cache/scitex-dev-release/publisher029-1112/home/.claude/.credentials.json'), *, now_ms: Callable[[], int]=<function _now_ms>) → SourceState | None[source]

Return SourceState, or None if rotation should be a silent no-op.

Silent-no-op (returns None) is reserved for the operator-on-a-fresh- -laptop / expired-token cases that should NOT spam every hour:

  • source file is missing

  • claudeAiOauth.expiresAt (or expires_at) is in the past

Raises ValueError for cases that DO deserve a loud error:

  • file present but doesn’t parse as JSON

  • file parses but has no claudeAiOauth key

Docs (search / index helpers)

Docs build, search, and skills aggregation utilities.

Branding

Ecosystem branding / translation registry.

Single source of truth for SciTeX-ecosystem brand identities (pypi name, import name, umbrella attr, alias, display, method-prefix, env-prefix). See registry.yaml for entries and BRANDING_TRANSLATION_REGISTRY_PLAN.md for design rationale.

Runtime API (minimal — only what figrecipe + socialia need):

get(brand_key, field)               -> str
translate(name, *, from_brand, to_brand) -> str
register_method_aliases(cls, *, brand_key) -> None
get_env(key, *, brand_key, default=None) -> str | None

Plus low-level helpers used by the auditor:

get_brand(brand_key) -> Brand
iter_brands() -> Iterator[tuple[str, Brand]]
class scitex_dev._branding.Brand[source]
alias: str
display: str
env_prefix: str
import_: str
method_prefix: str
native_brand: str
pypi: str
umbrella_attr: str
umbrella_brand: str
scitex_dev._branding.get(brand_key: str, field: str) → Any[source]

Look up a single field. No fallbacks — registry is authoritative.

scitex_dev._branding.get_brand(brand_key: str) → Brand[source]

Whole entry for one brand. Raises KeyError if unknown.

scitex_dev._branding.get_env(key: str, *, brand_key: str, default: str | None = None) → str | None[source]

Read an environment variable using the brand’s declared env_prefix, with a fallback to the counterpart brand’s prefix (for white-label rebrands), then the unprefixed name, then default.

Lookup order:
  1. <this brand env_prefix>_<key>

  2. <counterpart env_prefix>_<key> (if a counterpart exists)

  3. <key> (unprefixed)

  4. default

scitex_dev._branding.iter_brands() → Iterator[tuple[str, Brand]][source]

Iterate (brand_key, entry). For audit rules.

scitex_dev._branding.register_method_aliases(cls, *, brand_key: str) → None[source]

Walk dir(cls) and bind the counterpart-brand’s prefix as aliases.

For each method starting with <this brand>.method_prefix, bind an attribute named with the counterpart’s method_prefix that points at the same function. The alias gets __module__ rebound to the counterpart’s import path so help() on the umbrella side doesn’t leak the native package name.

No-op if the brand has no counterpart or no method_prefix.

scitex_dev._branding.translate(name: str, *, from_brand: str, to_brand: str) → str[source]

Layer-5 only — swap one brand’s method_prefix for the counterpart’s.

Raises ValueError if from_brand/to_brand are not declared as native/umbrella counterparts in the registry.

>>> translate("fr_conf_mat", from_brand="figrecipe", to_brand="scitex-plt")
'stx_conf_mat'

Release

Release-related utilities (CI, deploy, version bumpers, lint gates, RTD onboarding).

Testing helpers

Pytest helpers for downstream SciTeX packages.

Public re-exports:

  • audit_all_for_package — assert audit-all <pkg> exits 0.

  • classify_audit_outcome — grade a run PASS / FAIL / UNKNOWN.

classify_audit_outcome is public because “the audit could not run” is a verdict other tooling needs to be able to ASK FOR, not a private detail of one assertion’s wording.

Each ecosystem package drops a one-liner test that calls this helper so the package’s local pytest run includes the same audit gates that CI used to run as a separate workflow. See the auto-generated tests/test_audit.py template (via scitex-dev ecosystem write-audit-test <pkg>) and the skill leaf _skills/general/02_package/07_github-actions.md for the canonical shape.

exception scitex_dev.testing.ForeignImportError[source]

The package under test resolves outside the tree under test.

exception scitex_dev.testing.PackageNotImportableError[source]

The package under test could not be imported at all.

A SUBCLASS so every existing except ForeignImportError keeps working: a caller that wants “the guard refused” does not have to learn a second name, and a caller that wants to distinguish “wrong tree” from “no package” now can.

scitex_dev.testing.assert_imports_tree_under_test(package: str, root: str | PathLike[str], *, env_var: str = 'SCITEX_ALLOW_FOREIGN_IMPORT', stream=None) → Path[source]

Import package and raise unless it resolves INSIDE root.

scitex_dev.testing.assert_path_inside_tree(package: str, package_path: str | PathLike[str], root: str | PathLike[str], *, env_var: str = 'SCITEX_ALLOW_FOREIGN_IMPORT', stream=None) → Path[source]

Raise unless package_path is inside root. Both are resolved.

Parameters:
  • package – the importable name, used only in the message.

  • package_path – where the package was found.

  • root – the tree the caller believes it is grading.

  • env_var – the opt-out variable. Defaults to the shared DEFAULT_ENV_VAR; override only when a leaf genuinely needs its own scope.

  • stream – where the opt-out notice is written (default sys.stderr).

Returns:

The resolved package path, so a caller can log what it graded.

Raises:

ForeignImportError – naming BOTH paths and the fixes, in preference order.

The opt-out is for deliberately testing an INSTALLED build (verifying a wheel, say), and it PRINTS LOUDLY when used. AN OPT-OUT THAT HIDES ITSELF IS THE DEFECT WEARING THE FIX’S CLOTHES — the whole family this guard exists for is “the report does not say what answered it”, and a silent bypass would reproduce it one level up.

scitex_dev.testing.audit_all_for_package(distribution: str, *, path: str | Path | None = None, timeout: float = 550.0, skip_rules: tuple[str, ...] = (), launcher: Sequence[str] | None = None) → None[source]

Run scitex-dev ecosystem audit-all <distribution> and assert exit 0.

Parameters:
  • distribution – ECOSYSTEM key (e.g. "scitex-io", "scitex-stats", "socialia").

  • path –

    The checkout to audit — pass it, and pass the tree the test itself lives in. PASS THIS. Without it, audit-all has no argument telling it WHICH tree you meant, so it falls back to resolving the distribution by import location / a ~/proj/<name> development guess. On a CI runner that guess is somebody else’s tree — or the wrong commit of your own — and the gate then grades source that is NOT the commit under test while reporting a confident pass/fail. A gate that grades the wrong tree is worse than no gate.

    The idiomatic call from a package’s audit gate is therefore to anchor on the test file itself, which is by construction inside the checkout pytest is running against. N counts the DIRECTORY levels from the test file up to the package root, so it depends on where the gate lives — copying an N from an example written for a different depth silently anchors on a SUBDIRECTORY, and the audit then grades that subdirectory as if it were the package:

    # tests/test_audit.py           -> parents[1]
    # tests/develop/test_audit.py   -> parents[2]   <- the shape
    #                                  `ecosystem install-audit-gate`
    #                                  generates
    from pathlib import Path
    audit_all_for_package(
        "scitex-io", path=Path(__file__).resolve().parents[2]
    )
    

    (ecosystem install-audit-gate derives N from the path it is about to write rather than hardcoding it — see _cli.ecosystem._cmds._install_gate.anchor_depth.)

    Threaded through as --path <path> to the audit-all CLI, which forwards it to ALL SIX sub-auditors. Every one of them reads the source tree, so every one of them needs it: audit-project, audit-django, audit-python-apis, audit-skills, audit-mcp-tools, and audit-cli — the last one reads the tree twice over, for its static §2/§11 source scans AND for the per-package .scitex/dev/cli-audit-dict.yaml custom dictionary. (An earlier version of this docstring listed only the first three, which is how audit-cli’s dictionary lookup stayed cwd-rooted long enough to bite scitex-storage: four auditors printed via explicit while audit-cli graded another checkout’s dict, and the run’s output looked internally consistent.) The distribution NAME is still required and is still used for skill/rule lookup; path only decides which source gets read.

    None (the default) preserves the historical resolve-by-guess behaviour for callers that haven’t migrated. It is a compatibility shim, not a recommendation — and it is no longer SILENT: omitting path prints a warning to stderr naming the cwd the guess will start from (see warn_on_guessed_path()).

  • timeout – Per-test wall-clock cap. audit-all fans out to SIX sub-auditors (audit-cli, audit-mcp-tools, audit-skills, audit-python-apis, audit-project, audit-django), each a fresh Python subprocess that re-imports the target package — real measured wall-clock on a loaded/NFS-backed host runs well past the previous 120s default even when the audit itself is completely clean (verified directly: scitex-dev ecosystem audit-all scitex-dev –path <worktree> exits 0, and a bare untimed run completed clean end-to-end in ~455s under load). 120s was tuned for “a slow PyPI install,” not for the six-subprocess audit sweep itself. 550s gives real headroom while staying under the test_audit_all_clean call site’s @pytest.mark.timeout(600) override (see tests/develop/test_audit.py / the ecosystem install-audit-gate template that generates it for every other package) so THIS subprocess timeout fires first with a readable audit-output error message, instead of pytest-timeout’s generic SIGALRM traceback winning the race. A genuinely wedged auditor still times out, just later.

Notes

Local audit bypass.

Set the literal SCITEX_DEV_SKIP_AUDIT=1 in the environment to skip the audit (the test calls pytest.skip instead of running the subprocess). Use during a remediation push when pre-existing violations would block unrelated test runs, or when developing locally without scitex-dev’s audit corpus available. CI for release branches MUST NOT set this — drift goes silent. Other values, including 0 and false, do not bypass the audit.

Raises:

AssertionError – If the subprocess returns non-zero. The full stdout + stderr are included in the message so the failing rule is visible in the test report without re-running the audit by hand. The message states WHICH of the two failing verdicts applies — see scitex_dev.testing._audit_outcome. FAIL opens with “reported violations”, NAMES THE RULE CODES THAT FIRED, and then digests the finding lines; UNKNOWN opens with “COULD NOT RUN” and quotes the underlying error. Both fail the test; only one of them means “go read the rule corpus”. The codes are on the FIRST line because that is the only line pytest’s short test summary info carries, and it is what CI notifications and gh pr checks triage are built from:: audit-all reported violations for ‘sac’ (exit=1): PS-140, SK-302 Without them that sentence is a constant, so unrelated failures are indistinguishable without opening each job log — which is how seventeen PRs failing four different PR-local rules were escalated as one fleet-wide outage (scitex-dev#593).

scitex_dev.testing.classify_audit_outcome(returncode: int, output: str) → tuple[str, list[str]][source]

Grade one audit-all run. Returns (verdict, evidence).

evidence is the could-not-run lines for VERDICT_UNKNOWN, the finding digest for VERDICT_FAIL, and empty for VERDICT_PASS.

The rule:

  • exit 0 -> PASS.

  • exit != 0 AND the output says it could not run -> UNKNOWN.

  • exit 2 -> UNKNOWN even with no marker. Exit 2 is audit-all declining to grade (usage error, unreadable skip-rule config) or a sub-auditor reporting “could not locate the tree” — by construction “I did not grade”, never “I graded and found something”.

  • anything else non-zero -> FAIL.

UNKNOWN is still a FAILING verdict for the caller. It changes the claim, not the colour.

scitex_dev.testing.could_not_run_evidence(output: str) → list[str][source]

Lines showing the audit COULD NOT RUN, in the order they appeared.

An empty list means “nothing in this output says the audit was prevented from running” — which, paired with a non-zero exit, licenses FAIL.

Two filters keep this from crying wolf:

  • A line carrying a RULE ID is an attributable FINDING, whatever words it contains. An auditor is entitled to report [STX-NET001] ... requests.get(...) without that being a crash.

  • A line whose text BEFORE the marker labels it a warning/info notice is a degraded-but-running announcement. See _NOTICE_TOKENS.

scitex_dev.testing.make_pytest_configure(package: str, root: str | PathLike[str], *, env_var: str = 'SCITEX_ALLOW_FOREIGN_IMPORT')[source]

Build a pytest_configure hook wrapping the assertion.

For a leaf’s conftest.py:

from pathlib import Path
from scitex_dev.testing import make_pytest_configure

pytest_configure = make_pytest_configure(
    "figrecipe", Path(__file__).resolve().parent.parent
)

The FUNCTION above is the primitive and this is the convenience, not the other way round: the check is useful anywhere a runner decides what it is grading — a CI preflight, a release script, audit-all itself, which has the same defect one level up. If the only entry point were a pytest hook, every non-pytest caller would re-implement it, which is the duplication this module exists to end.

scitex_dev.testing.resolve_package_path(package: str, *, tree_under_test: str | PathLike[str] | None = None) → Path[source]

Where package actually resolves, fully resolved.

Split out from the assertion so the CONTAINMENT DECISION below is a pure function of two paths. That is not tidiness: the subtle half of this guard is the symlink handling, and separating the two lets it be tested against real directories and real links rather than against a stand-in for the import system.

tree_under_test appears ONLY in the failure message. A bare ModuleNotFoundError is loud and honest, so it was never a defect — but it says neither WHICH TREE was being checked nor THAT A GUARD WAS RUNNING, and naming what it looked at is this module’s entire value. A guard whose own failure does not identify itself reproduces, one level up, the family it exists to catch.

The original exception is CHAINED, never replaced: the import error is the actual diagnosis (a typo, a missing install, a broken dependency), and this only adds the context it lacks.

Linter

SciTeX Linter — enforce reproducible research patterns via AST analysis.

scitex_dev.linter.list_rules(category: str = None) → list[source]

Return all rules (built-in + plugin), optionally filtered by category.

Parameters:

category (str, optional) – If provided, only return rules whose category matches this value.

Returns:

All matching Rule objects from built-in definitions and loaded plugins.

Return type:

list of Rule

MCP server

scitex-dev’s OWN FastMCP server.

The MCP helper utilities (wrap_as_mcp, register_docs_tools, get_tools_sync, …) used to live here but are now ecosystem-glue under scitex_dev._ecosystem._mcp and re-exported as the public scitex_dev.ecosystem surface. This package retains only the _server module that hosts scitex-dev’s own MCP tools.

Dashboard

Flask dashboard for scitex version management.

scitex_dev.dashboard.create_app() → Flask[source]

Create and configure the Flask application.

Returns:

Configured Flask application.

Return type:

Flask

scitex_dev.dashboard.run_dashboard(host: str = '0.0.0.0', port: int = 5000, debug: bool = False, open_browser: bool = True, force: bool = False) → None[source]

Run the Flask dashboard server.

Parameters:
  • host (str) – Host to bind to. Default “127.0.0.1”.

  • port (int) – Port to listen on. Default 5000.

  • debug (bool) – Enable Flask debug mode.

  • open_browser (bool) – Open browser automatically.

  • force (bool) – Kill existing process using the port if any.

Installed CLI providers

Installed CLI declarations, independent of Python, MCP and skills policy.

Discovery reads metadata only. declared means an owned entry-point target, not a callable or Click command; hosts must resolve those only on invocation.

class scitex_dev.cli_providers.CliProvider(name: str | None, target: str | None, distribution: str | None, version: str | None, status: str, source: str, console_script: str | None = None, reason: str = '')[source]

One declaration/evidence row; non-eligible rows must not be mounted.

Names on none, invalid and ambiguous rows are reserved too: wrapper/registry fallback must never resurrect them. Only declared and legacy_unknown are candidates for lazy invocation-time validation.

scitex_dev.cli_providers.list_cli_providers(*, package: str | None = None, path: Sequence[str | Path] | None = None) → tuple[CliProvider, ...][source]

Read installed declarations without loading entry points or leaf modules.

Explicit declarations suppress legacy fallback for their entire owner. Duplicated owners and command names are ambiguous, including opt-out names. Ambiguity is determined BEFORE an optional owner filter. path limits the metadata search (useful for isolated environments and real dist-info tests). Installed headers share one snapshot within this call; a subsequent call reads installed state again rather than reusing a process-global cache.