Summary
AGENTS.md requires the CLI JSON path to keep working when Pydantic cannot be imported:
the CLI JSON path must keep its pydantic-free fallback so basic search output remains available when Pydantic cannot be imported
That fallback does not exist. Blocking Pydantic makes import agentgrep raise ImportError before any serializer is ever selected.
Worse, the regression test that was supposed to protect it passed green against a fallback that is unreachable.
Present in v0.1.0a33.
Reproduction
$ python -c "
import builtins
_real = builtins.__import__
def guard(name, *a, **k):
if name == 'pydantic' or name.startswith('pydantic.'):
raise ImportError('pydantic blocked')
return _real(name, *a, **k)
builtins.__import__ = guard
import agentgrep
"
ImportError: pydantic blocked
The package does not import at all, so the JSON serializer's except ImportError branch is never reached.
Why the existing test cannot catch it
test_json_output_falls_back_without_pydantic monkeypatches importlib.import_module inside an already-imported package. By the time it runs, every module-scope import pydantic has already been satisfied.
It therefore exercises the except ImportError inside the serializer-selection helper and proves nothing about the real import graph. It is a test asserting a property it structurally cannot observe.
The actual dependency
Two things make Pydantic mandatory.
A dead import at the package root
src/agentgrep/__init__.py#L60 is a bare import pydantic with no pydantic.* reference anywhere in the module body. It does nothing except make the whole package un-importable without Pydantic.
(This one is already deleted on the #112 branch — necessary, but not sufficient.)
Four modules on the search path subclass pydantic.BaseModel at module scope
Each was confirmed individually — with Pydantic blocked, importing any one of them raises.
So Pydantic is not an optional adapter at the boundary today. It is a hard, load-bearing runtime dependency of the core — the opposite of what AGENTS.md describes, and a direct contradiction of the stated policy that Pydantic is "a schema, validation, and adapter layer at explicit boundaries" rather than the semantic source of truth.
The fix
Give the four modules a Pydantic-free definition at module scope, and keep Pydantic where the policy says it belongs — at the MCP schema boundary, imported lazily.
events.py is the most tractable and the most instructive. Its models already set arbitrary_types_allowed=True so they can embed plain dataclasses — which means they cannot model_dump_json() anyway. Pydantic is buying discriminator metadata that isinstance already provides. There is little to lose and a hard dependency to shed.
stores.py is the load-bearing one. The catalog descriptors are the widest surface and they are constructed at import time.
Two properties to preserve while doing it:
- The cold-start budget (
agentgrep --help under 250 ms). Dropping four eager Pydantic model registrations should help rather than hurt, but it should be measured rather than assumed.
- The frozen/validated semantics the catalog relies on. A
@dataclass(frozen=True) gives immutability but not validation; whatever replaces BaseModel must not quietly loosen a guarantee the catalog tests depend on.
Acceptance
The two subprocess boundary tests added in #112 (test_cli_json_survives_missing_pydantic, for both json and ndjson) stop being xfail and pass on their own merits.
They run the real CLI in a real subprocess with Pydantic genuinely absent — so nothing else is needed to prove the fix. They are currently xfail(strict=True), which means the moment the fallback works they turn red until the marker is removed. This cannot be silently closed.
Summary
AGENTS.mdrequires the CLI JSON path to keep working when Pydantic cannot be imported:That fallback does not exist. Blocking Pydantic makes
import agentgrepraiseImportErrorbefore any serializer is ever selected.Worse, the regression test that was supposed to protect it passed green against a fallback that is unreachable.
Present in
v0.1.0a33.Reproduction
The package does not import at all, so the JSON serializer's
except ImportErrorbranch is never reached.Why the existing test cannot catch it
test_json_output_falls_back_without_pydanticmonkeypatchesimportlib.import_moduleinside an already-imported package. By the time it runs, every module-scopeimport pydantichas already been satisfied.It therefore exercises the
except ImportErrorinside the serializer-selection helper and proves nothing about the real import graph. It is a test asserting a property it structurally cannot observe.The actual dependency
Two things make Pydantic mandatory.
A dead import at the package root
src/agentgrep/__init__.py#L60is a bareimport pydanticwith nopydantic.*reference anywhere in the module body. It does nothing except make the whole package un-importable without Pydantic.(This one is already deleted on the #112 branch — necessary, but not sufficient.)
Four modules on the search path subclass
pydantic.BaseModelat module scopestores.pyDiscoverySpec,StoreDescriptor,StoreCatalogprogress.pyquery/ast.pyToken, the node unionevents.pySearchEventunionEach was confirmed individually — with Pydantic blocked, importing any one of them raises.
So Pydantic is not an optional adapter at the boundary today. It is a hard, load-bearing runtime dependency of the core — the opposite of what
AGENTS.mddescribes, and a direct contradiction of the stated policy that Pydantic is "a schema, validation, and adapter layer at explicit boundaries" rather than the semantic source of truth.The fix
Give the four modules a Pydantic-free definition at module scope, and keep Pydantic where the policy says it belongs — at the MCP schema boundary, imported lazily.
events.pyis the most tractable and the most instructive. Its models already setarbitrary_types_allowed=Trueso they can embed plain dataclasses — which means they cannotmodel_dump_json()anyway. Pydantic is buying discriminator metadata thatisinstancealready provides. There is little to lose and a hard dependency to shed.stores.pyis the load-bearing one. The catalog descriptors are the widest surface and they are constructed at import time.Two properties to preserve while doing it:
agentgrep --helpunder 250 ms). Dropping four eager Pydantic model registrations should help rather than hurt, but it should be measured rather than assumed.@dataclass(frozen=True)gives immutability but not validation; whatever replacesBaseModelmust not quietly loosen a guarantee the catalog tests depend on.Acceptance
The two subprocess boundary tests added in #112 (
test_cli_json_survives_missing_pydantic, for bothjsonandndjson) stop beingxfailand pass on their own merits.They run the real CLI in a real subprocess with Pydantic genuinely absent — so nothing else is needed to prove the fix. They are currently
xfail(strict=True), which means the moment the fallback works they turn red until the marker is removed. This cannot be silently closed.