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
docswithlistandgetverb 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
skillswithlistandgetverb 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
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-runflag to an argparse parser.
- scitex_dev._cli.dry_run_option(fn: Callable) Callable[source]
Click decorator: adds
--dry-runflag.Uses lazy
import clickso 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.
- scitex_dev._cli.json_option(fn: Callable) Callable[source]
Click decorator: adds
--jsonflag asas_jsonparameter.Uses lazy
import clickso 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_asfunction 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_mcpbut for terminal output. Wraps any plain function in Result, formats based onas_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
Trueiffpackagemay 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
ECOSYSTEMrecord isarchived,its
categoryis 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 runningauditoronpackage.auditoris 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_SKIPare 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.
- 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/.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
RotateResultper 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/.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(orexpires_at) is in the past
Raises
ValueErrorfor cases that DO deserve a loud error:file present but doesn’t parse as JSON
file parses but has no
claudeAiOauthkey
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]]
- 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:
<this brand env_prefix>_<key><counterpart env_prefix>_<key>(if a counterpart exists)<key>(unprefixed)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 sohelp()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 ForeignImportErrorkeeps 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
packageand raise unless it resolves INSIDEroot.
- 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_pathis insideroot. 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 theaudit-allCLI, 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.yamlcustom 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 printedvia explicitwhile 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;pathonly 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: omittingpathprints a warning to stderr naming the cwd the guess will start from (seewarn_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_cleancall site’s@pytest.mark.timeout(600)override (seetests/develop/test_audit.py/ theecosystem install-audit-gatetemplate 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=1in the environment to skip the audit (the test callspytest.skipinstead 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, including0andfalse, 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.FAILopens with “reported violations”, NAMES THE RULE CODES THAT FIRED, and then digests the finding lines;UNKNOWNopens 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’sshort test summary infocarries, and it is what CI notifications andgh pr checkstriage 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).evidenceis the could-not-run lines forVERDICT_UNKNOWN, the finding digest forVERDICT_FAIL, and empty forVERDICT_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_configurehook 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-allitself, 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
packageactually 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_testappears ONLY in the failure message. A bareModuleNotFoundErroris 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.
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
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,invalidandambiguousrows are reserved too: wrapper/registry fallback must never resurrect them. Onlydeclaredandlegacy_unknownare 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.
pathlimits 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.