Catalog PyPI
PACKAGE EXPLORERView in registry

3tears-enforcement

PyPI

Static-analysis enforcement scanners for the 3tears ecosystem (cache primitive contract, underscore-access discipline, codebase conventions, etc.)

Versions collected1
Direct dependencies3From release 0.72.0
Last observed20 Oct, 15:37 UTC

README

Version 0.72.0 documentation

3tears-enforcement

Shared static-analysis enforcement scanners for the 3tears ecosystem.

What this package does

Each module under threetears.enforcement.<domain> ships an AST-based scanner that enforces a single architectural invariant across a Python source tree. Consumer repos import the scanner, inject their per-repo configuration (allowlists, exemption files, src roots), and run it from a thin pytest test class.

This replaces a previous pattern in which the same enforcement test files were vendored verbatim across multiple repos with manual sync requirements. The shared package eliminates duplication and drift while keeping per-repo configuration where it belongs.

Domains

Adopting invalidation_listener in a repo that already runs a local copy. Three things change, and only the first is obvious.

  1. The exemption format. The hub's pre-shared walker keyed by BARE MODULE PATH (src/aibots/gateway/acl.py). The shared parser wants a path:line:symbol triple and rejects anything else with ExemptionError, so an unmigrated file does not degrade -- it aborts the domain. Rewrite each entry as <path>:*:<registry-spelling>, keeping its # rationale: line. * means "any line in that file", which is what you want: keying an exemption to a line number silently stops matching when the line moves.
  2. minimum_live_registries is yours to supply, and it is REQUIRED with no default. Count the L2-live registries your repo actually wires and put that number in the shell. It is the floor that makes a green report mean anything.
  3. Repo-specific shell assertions do not come across. A local walker typically carries checks the shared domain has no equivalent for -- which processes must be in scope, that an L1-only registry is not dragged in, detector positive/negative cases. Those are yours to keep: port them into the thin shell rather than assuming the domain replaced them, because deleting the local file deletes them silently.

This note describes a one-time migration and should be deleted once every consumer has adopted the domain. Tracked as ENF-7WQ2 in the 14-eng-ai-bot backlog, which is this family's backlog of record -- not this repo's, so do not go looking for it here.

ModuleInvariant enforced
audit_detailsEvery audit details key a producer publishes is classified (safe, family-safe, or personal) under threetears.agent.audit's erasure rule, and every details argument is statically readable. Wrapper helpers are refused until declared in forwarders, then read at their call sites. Classification is injected, the predicate included: pass safe_keys_for=safe_detail_keys_for, is_classified=is_classified_detail_key. Family keys are credited only for statically resolved event types (literal, module constant local or imported, imported-module attribute, conditional over both branches, helper parameter through every caller, a forwarder's own inner construction when its call site names none), and only when safe for every resolved value. No exemptions file.
cacheEvery stateful data surface routes through BaseCollection; no bespoke SQLiteBackend wrappers; no direct pool access to Collection-backed tables; every migration-defined table has a Collection class.
underscore_accessUnderscore prefix is a stability contract: no cross-module private import, no cross-class protected access, modules with public names have __all__, no subclass shadowing of base private attributes, no __all__ listing private names. slf001_policy_findings(repo_root, exemptions_path) holds the suppression policy: no inline noqa for SLF001 anywhere, and a per-file SLF001 ignore only on a src module (one per third-party library whose private members it confines) that has exemptions-ledger entries. Enable it with the thin shell in pragma_policy's module docstring, asserting floors on scanned_python_files and slf001_ignored_files. scan_private_bindings(repo_root, exemptions_path) holds the two spellings every other check passes, over src, tests and scripts: shape G, a private name or module bound by an import (a test importing _helper from the module under test or from a sibling test module, or importing through pkg._internals), and shape H, a private name bound by a string (patch, patch.object, patch.multiple, patch.dict, monkeypatch.setattr/delattr, mocker.spy, import_module). A src module may bind its own package's privates; a private module or package under a tests tree is a violation wherever it is imported from, its own tests tree included, because shared test support has a plain name (conftest.py, __init__.py, other dunders and data files are never findings); a recorded third-party confinement module may import its library's privates; nothing else is sanctioned, and patching a library's private from a test is a violation. The full rules are in private_binding's module docstring. Enable it with the thin shell there: a floor on files_scanned, imports_examined and binding_calls_examined, the positive control undetected_planted_controls(tmp_path), and an empty private_binding_findings.
codebase_conventionsNo bare print(), no stdlib logging.getLogger (use threetears.observe), from __future__ import annotations required, return type annotations required.
coercion_coverageTool subclasses override execute, never run, preserving the normalize_kwargs → execute input-coercion path.
dependency_alignmentDeclared dependencies match actual imports (no undeclared module-top sibling import, no declaration nothing imports); designated contracts packages import only stdlib, their own namespace, and configured extras; a package with a pinned DependencyFloor declares exactly the ruled hard-dependency list, no more and no fewer.
imports_declaredA consumer repo's every non-stdlib, non-first-party import comes from a distribution its pyproject.toml declares, resolved at the full module path through the INSTALLED distributions' file lists (editable checkouts included), never a hand map. Both inputs carry a non-vacuity guard (required_import_roots, required_owned_modules), and imports nothing installed provides are reported apart. run_imports_declared_enforcement(ImportsDeclaredConfig(...)). No exemptions file.
dict_state_detectionNo raw dict/OrderedDict persistent state in __init__; use SQLiteBackend (L1) or NATS KV for shared state.
grep_q_pipefailNo pipe (| or |&) into a quiet grep/egrep/fgrep (-q anywhere in its flags, --quiet, --silent) in a *.sh that sets pipefail or in a GitHub workflow (a shell: bash step runs with it; every workflow line is held to it): grep exits on its first match, the writer dies of SIGPIPE, and pipefail fails a check that matched. Continued lines are joined, comments skipped, and a scan that finds no files is refused. run_grep_q_pipefail_enforcement(GrepQPipefailConfig(repo_root=...)).
invalidation_listenerEvery L2-live CollectionRegistry starts the cross-pod invalidation listener, and every start is paired with a stop. Carries a REQUIRED non-vacuity floor: a scan reaching nothing reports what a clean repo reports.
logger_coverageEvery production module declares a module-level log = get_logger(__name__) unless explicitly exempt.
migration_yugabyte_safetyMigration shapes are yugabyte-safe per threetears.core.data.migrations.enforcement.
nats_wrapper_usageAll nats-py imports route through threetears.nats.NatsClient; no direct import nats.
no_silent_swallowException handlers must log, re-raise, or carry # NOSILENT: <reason>.
sender_postureEvery NATS receiving site (a subscribe, a JetStream consumer, the result waiter, a KV watch, an owner-forward handler) declares how it authenticates its sender with # sender-posture: <posture> [open <ROW>]: verified-token, signed-envelope, own-consumer, trigger-only, read-only, unauthenticated (only with an open row), or primitive (only inside the repo's named primitive functions, path::Qual.name). Each repo's allowlist carries exactly the open rows, in both directions, and pins how many sites in each file declare each row: a site cannot open a row the list lacks, a new site cannot ride an existing line, and a line whose row no site still declares fails, so the list only shrinks. sender_posture_findings(SenderPostureConfig(...)), with find_subscribe_sites for the shell's non-vacuity floor; excluded_prefixes leaves test doubles shipped under src unwalked; each must end in /, and one matching no module is a finding.
releaseRelease tooling with one owner. api_growth_findings(ApiGrowthConfig(...)) refuses public-API growth inside a patch since the latest vX.Y.Z tag, and never reads an unparseable module or a non-literal __all__ as empty; http_routes and a repo's own extractors add surface a name sweep cannot see. untagged_checkout_findings binds every CI job running the gate to a checkout with the tags. threetears-release patch|minor|major|release|sync|verify X.Y.Z is the version bump each repo's scripts/bump-version.sh wraps in one line, configured by [tool.threetears-release].
print_log_linesIn every module that imports 3tears (scope_import_roots, default ("threetears",)): no print(..., file=sys.stderr) and no direct stdlib logging call (getLogger, basicConfig, a root-logger call such as logging.warning) -- log through threetears.observe.get_logger. For an application on 3tears, where codebase_conventions and no_stdlib_logging are too broad: print to stdout is left alone, modules that do not import 3tears are not scanned, and a command line whose stderr is its answer goes in exempt_files. One call takes # print-log: ok. Non-vacuity floor minimum_scoped_modules (default 1) fails a scan that reaches no 3tears module, in every mode. run_print_log_lines_enforcement(PrintLogLinesConfig(...)).
no_stdlib_loggingNo production module imports stdlib logging directly; use threetears.observe.

How to use it

Each domain exposes a configuration dataclass and a high-level runner:

# tests/enforcement/test_cache_primitive_usage.py
from pathlib import Path
from threetears.enforcement.cache import CacheEnforcementConfig, run_cache_enforcement

_CONFIG = CacheEnforcementConfig(
    repo_root=Path(__file__).parents[2],
    allowed_sqlite_construction_sites=frozenset({
        "packages/registry/src/threetears/registry/l1_cache.py",
    }),
    collection_table_allowlist={
        "memories": "MemoriesCollection",
        # ... per-repo
    },
    migration_table_allowlist=frozenset({"_schema_migrations"}),
    exemptions_path=Path(__file__).parent / "_cache_exemptions.txt",
    enforcement_mode_env_var="CACHE_ENFORCEMENT_MODE",
)

class TestCachePrimitiveUsage:
    def test_no_bespoke_sqlite_backend_construction(self) -> None:
        run_cache_enforcement(_CONFIG, walker="sqlite_construction")

    def test_no_bespoke_cache_wrapper_classes(self) -> None:
        run_cache_enforcement(_CONFIG, walker="wrapper_class")

    def test_no_direct_pool_access_to_collection_tables(self) -> None:
        run_cache_enforcement(_CONFIG, walker="pool_access")

    def test_all_tables_have_collections(self) -> None:
        run_cache_enforcement(_CONFIG, walker="missing_collection")

Per-repo exemption files (e.g., _cache_exemptions.txt) stay in the consumer repo's tests/enforcement/ directory. The package's parse_exemptions_with_rationale reads them at test time.

How to onboard a new repo

  1. Add 3tears-enforcement as a dev dependency.
  2. For each domain you want to enforce: create a thin shell test file at tests/enforcement/test_<domain>.py following the pattern above. Inject your repo's allowlists/exemptions.
  3. Create per-domain exemption files at tests/enforcement/_<domain>_exemptions.txt if needed. Every exemption requires a preceding # rationale: <specific reason> line.
  4. Run pytest tests/enforcement/ to verify the scanners work against your tree.

How to add a new enforcement domain

  1. Create src/threetears/enforcement/<domain>/ with walkers.py, config.py, runner.py, and __init__.py.
  2. Use common/ helpers (ast_helpers, collection_registry, repo_layout, pyproject_discovery, inheritance, exemptions, modes, violations, reports). Do not duplicate scaffolding. Two in particular, because both have already been re-implemented in this package rather than imported:
    • ast_helpers owns the dotted-name spelling family -- dotted, callee_names, receiver, argument_spellings. If you are about to write a loop that walks an ast.Attribute chain accumulating segments, it exists. Three copies of that walk shipped here before it was shared, each differing by one detail that turned out to be a parameter (dotted's unwrap_subscript is the survivor).
    • collection_registry owns "which CollectionRegistry names are L2-live" -- take the answer from there rather than re-deriving it. CLIENT_SPELLINGS is a heuristic list that grows, so a second copy goes stale silently while still compiling.
  3. Walkers return list[Violation]. Configs are frozen dataclasses. Runners orchestrate walker → exemption-application → mode-resolution → report.
  4. Write unit tests in tests/<domain>/.
  5. Document the domain in this README.

Downloads & popularity

Awaiting observations

No metric observations yet

Download counts and popularity will appear here as the collector receives them.

Versions 1

Collected releases
VersionPublishedDirect dependenciesOpen version
0.72.020 Oct 20263

Package advisories

0
No matching advisories collected

This describes our collected data; it is not a security assessment.

Repository

ACCESSIBLE
pacepace/3tears

Shared repository data · GitHub

License detected in repository
MIT
License branch / ref
develop
Last push
20 Oct 2026
Archived
No
Contributor identities
— · partial collection

Public sponsor counts have not been collected. These signals belong to the repository owner.

Downloads & popularity

0count
Gauge · observed snapshots20 Oct 2026
10.50
2026-10-20: 0 · observed 2026-10-20T17:18:19.8928356Z
20 Oct20 Oct

github · 1 observations shown. Missing days are unknown.

View observed values
Date / source periodValueObserved at
2026-10-2002026-10-20T17:18:19.8928356Z

Metadata history

Meaningful changes
Current collected state Current

Observed since 20 Oct, 15:37 UTC

No superseded metadata states collected. Unchanged observations do not create new revisions.

Record identity

package:660c2e6c08ae90045a132fdca490e765519942ac5661a452316a8f8b6f1d167f