Project Secruna — Codebase Complexity Audit¶
Date: 2026-05-03
Author: Claude (research-only audit, no code changes)
Scope: packages/rekognise-core/src/rekognise/**, apps/cp-api/src/cp_api/**
Tools: Glob, Grep, Read, radon cc/raw, AST length analysis.
1. Executive summary¶
For a three-month-old codebase shipping nine connectors, this is healthier than it has any right to be: average cyclomatic complexity across 420 analysed blocks is A (3.25), modules are small (median connector file ~110 LOC), and tests exist for every connector's normalize path. The architecture is "duck-typed Connector with async def discover()" — pragmatic, not academic, exactly what a solo founder should ship.
That said, four hairballs are starting to form:
shared/vendor_inference.pyis a 264-LOC, CC=85 god-function that already covers nine sources and grows linearly with each new vendor.apps/cp-api/src/cp_api/routers/connections.pyis 1,136 LOC of OAuth-flow copy-paste — eight near-identical KV-write blocks, four near-identicalstart/callbackpairs.- No connector base / no shared HTTP client — every connector hand-rolls pagination, 401/403 silent-skip, and (for Azure/M365/Azure-passive) MSAL token exchange. The next connector will replicate this for the fourth time.
canonical_id()is copy-pasted into 12 normalize files, all identical.DiscoveredArtifactlives atdata_plane/azure_connector/normalize.pyand is imported from there by every other connector — accidental coupling.
| Top wins (do these) | Top traps (don't do these) |
|---|---|
Move DiscoveredArtifact + canonical_id to data_plane/types.py (-200 LOC, 0 risk) |
Don't introduce an ABC Connector with abstract methods — current Protocol-by-convention is fine |
Extract BaseDataPlaneClient wrapping httpx with paginate / 401-skip / token header (kills ~150 LOC across 7 connectors) |
Don't unify the 9 token-fetch flows under one factory — three auth shapes (MSAL refresh, OAuth refresh, OAuth M2M, IAM AssumeRole, App PEM, raw API key) is honest variance |
Extract _kv_set_secret(connection_id, value, name_suffix) helper (kills 8 copies) |
Don't refactor vendor_inference into a registry yet — the dispatch is ugly but readable; refactor when adding source #11 |
Convert orchestrator's if/elif provider chain (10 branches) to a registry dict |
Don't merge active + passive Azure into one class — they fan out very differently after token-fetch |
Add a DiscoveryAuditLogger and call it from the orchestrator (currently zero audit trail for runs) |
Don't try to share normalize files between active + passive twins — the field shapes legitimately diverge |
2. Findings by section¶
A. Connector architecture¶
A.1 No abstract base / Protocol exists¶
grep -rn "Protocol\|ABC\|@abstractmethod" data_plane/ returns zero hits. The Connector "interface" is a duck-typed convention: every class implements async def discover(self) -> AsyncIterator[DiscoveredArtifact] and the orchestrator (runner.py:223) iterates it. Adding a Protocol is cheap (one declaration, no code removed) and worth doing for typing + IDE help. Do not introduce an ABC with default implementations — the constructors take wildly different shapes (refresh token, IAM Role + external_id, admin key, App PEM key, OAuth M2M client_id/secret, workspace URL + PAT) and forcing inheritance is premature.
A.2 Active vs passive twin duplication¶
Concrete duplication numbers, AST-extracted:
| Pair | Active+norm | Passive+norm | Shared logic | Diverging logic |
|---|---|---|---|---|
| Azure / passive-Azure | 113+73 | 138+89 | _acquire_access_token (~17 lines) identical except for _sync_acquire vs _sync rename, MSAL config, error-check |
Resource Graph KQL vs ARM Activity Log REST, different scopes, different normalization |
| AWS / passive-AWS | 129+83 | 159+132 | _assume_role (10 lines) identical except RoleSessionName value, region iter, boto3 client w/ creds |
Bedrock+SageMaker vs CloudTrail, lookback window, error-tolerance posture |
| GCP / passive-GCP | 146+77 | 143+160 | _exchange_refresh_token (~13 lines) byte-for-byte identical, page-token loop |
Different APIs (aiplatform/notebooks vs cloud logging), filter syntax |
Aggregate copy-paste budget:
- _acquire_access_token (Azure flavour): 3 copies (active, passive, M365), ~51 lines
- _exchange_refresh_token (GCP OAuth): 2 byte-identical copies, ~26 lines
- _assume_role (AWS STS): 2 copies, ~20 lines
- Page-token while-loop: ~9 occurrences, ~108 lines
- 401/403 silent-skip block: ~12 occurrences, ~72 lines
Total mechanical duplication: ~280 LOC of 3,429 connector LOC = ~8%. Not catastrophic, but precisely the "every connector reinvents this" that a BaseDataPlaneClient obliterates.
A.3 Common patterns I'd expect DRY'd, by status¶
| Pattern | Status | Where |
|---|---|---|
| OAuth refresh-token exchange (Microsoft) | 3 copies, near-identical | azure_connector/connector.py:53, m365_connector/connector.py:40, passive_collector/azure_activity_log.py:55 |
| OAuth refresh-token exchange (Google) | 2 byte-identical copies | gcp_connector/connector.py:52, passive_collector/gcp_cloud_logging.py:63 |
| AssumeRole (AWS) | 2 near-identical copies | aws_connector/connector.py:40, passive_collector/aws_cloudtrail.py:67 |
| Page-token while-loop | 9 hand-rolled copies | gcp ×2, github ×4, openai, anthropic, databricks |
| 401/403 silent-skip | 12 hand-rolled copies | every connector that hits HTTP |
| Retry-on-transient-error | NOT IMPLEMENTED | no tenacity; one 429 from OpenAI/GitHub kills the run |
| Audit-log entry on yield | NOT IMPLEMENTED | grep AuditLogEntry data_plane/ returns 0; only API routers write entries |
| Structured logging with tenant context | mixed: connectors use stdlib logging, API uses structlog, no contextvars for tenant_id |
|
| Secret retrieval from KV | orchestrator does it once correctly; connections router does it 8 times inline | runner.py:89 (good), routers/connections.py (bad: 8 copies) |
A.4 What is HARD to add today?¶
These changes would touch 9+ files because there's no seam:
- Per-connector rate limit / retry on 429-503 — every
httpx.AsyncClientandboto3.clientis wrapped separately. 9 file edits. - Distributed tracing — adding spans around
client.get()calls means editing every connector. - Tenant context in connector logs —
tenant_idis only known to the orchestrator, never passed to connectors. - Capping max artifacts per run — orchestrator counts but doesn't enforce; trivial to wrap the iterator centrally though.
- Per-connector dry-run mode — same fan-out.
A BaseDataPlaneClient wrapping httpx.AsyncClient with auth header injection, page-token paginator, 401/403 silent-skip, optional retry, and structlog binding for (tenant_id, source, run_id) — collapses all of this into one place.
B. Test parity across connectors¶
B.1 Per-connector test inventory¶
| Connector | Src LOC | Test LOC | Normalize tests | .discover() flow test |
|---|---|---|---|---|
| azure | 203 | 71 | yes | no |
| aws | 229 | 56 | yes | no |
| gcp | 242 | 58 | yes | no |
| m365 | 129 | 44 | yes | no |
| openai | 154 | 38 | yes | no |
| anthropic | 139 | 33 | yes | no |
| databricks | 220 | 202 | yes | partial (oauth only) |
| github | 660 | 322 | yes | no |
| passive-azure | 138 | 148 | yes | yes — gold standard |
| passive-aws | 159 | 97 | yes | no |
| passive-gcp | 143 | 131 | yes | no |
Pattern landed by the agent that built passive-azure (tests/unit/data_plane/passive_collector/test_collector.py): httpx.MockTransport injected via fake AsyncClient, monkeypatching _acquire_access_token. Portable to every other HTTP-based connector (8 of 9). Only aws_connector (boto3) needs different tooling — moto or stubbed boto factories.
Risk: of 9 production connectors, 8 have zero end-to-end test of the discover flow. A typo in a page-token field name would silently truncate pagination in production until a customer notices.
C. Cross-cutting concerns¶
C.1 shared/vendor_inference.py (264 LOC, CC=85)¶
The highest-complexity function in the codebase: infer_vendor is a flat dispatch covering 10 sources × N kinds × N model-id substring tests, already touched 5 times judging by inline "Plan 22d / Plan 24c" comments.
CC=85 looks scary but it's straight-line dispatch, not deep nesting — "long" rather than "tangled". The current structure has one virtue: one screen, easy to verify.
Do not prematurely refactor. When source #11 lands, refactor to a registry: INFERRERS: dict[str, Callable] + @register("azure") decorators. Effort then: M (3-4h). Effort now: zero.
Risk if left alone: a contributor adds a branch in the wrong place. Mitigate by keeping test ratio high in tests/unit/shared/test_vendor_inference.py.
C.2 Audit log¶
Connectors emit zero audit-log entries. grep AuditLogEntry data_plane/ returns 0. The orchestrator creates a DiscoveryRun row but never an AuditLogEntry. Discovery run started/failed/silently-skipped/completed → no audit entry from any of these.
API routers hand-roll AuditLogEntry(...) in 11 places with no helper: every call site manually fills actor_user_id, actor_tenant_id, target_id, target_type, action, context. Action names (inventory.system.viewed, hitl.verdict.edited, admin.tenant.impersonated) are free-form strings — typos go uncaught.
Impact: EU AI Act compliance requires a complete audit trail of automated decisions. Discovery runs that classify a system as prohibited leave zero audit trace from the data plane — a real exposure for a compliance product.
Recommendation: add rekognise.shared.audit.service.AuditService.log(session, *, action: Literal[...], actor, target, context). Wire into runner.py at every state transition; backfill routers in a follow-up.
C.3 Error handling¶
Three styles coexist with no contract:
- Silent-skip (most cloud connectors):
try/except Exception: logger.warning(...); return []. Examples:aws_connector/connector.py:65,87,111,passive_collector/aws_cloudtrail.py:106-133. - Hard fail on auth (SaaS admin-key connectors):
if response.status_code in (401, 403): raise PermissionError(...). Examples:openai_connector/connector.py:37,anthropic_connector/connector.py:39,databricks_connector/connector.py:47. - Orchestrator catch-all:
except Exception as e: errors.append(...)at the iterator boundary (runner.py:286).
Both connector styles are individually defensible (admin key revoked = real problem; partial cloud inventory = better than nothing) but no rule says which style applies where. New contributors will pick wrong.
Define a ConnectorError taxonomy in data_plane/types.py: PermanentAuthError → mark connection degraded; PartialReadError(scope=...) → record per-scope, run partial; TransientError → retry then promote. Effort S to define + adopt in 2 connectors as proof, M to fully roll out.
D. Wider codebase complexity hotspots¶
Ranked by radon cc × LOC × bug-likelihood.
| # | File / function | LOC | CC | Why complex / risk | Refactor sketch | Effort |
|---|---|---|---|---|---|---|
| 1 | routers/connections.py |
1136 | get_connection_health D(21), gcp_callback C(12), github_callback C(11) |
15 endpoints, 4 near-identical OAuth start/callback pairs, 8 inline KV writes, twin-creation duplicated. OAuth bug in callback #5 won't be fixed in #1-4. |
(a) Extract _persist_secret(...), (b) _create_passive_twin(...), © Split per-provider into cp/connections/<provider>_router.py |
M |
| 2 | routers/admin.py |
948 | impersonate C(17), list_audit_log C(13) |
21 endpoints in one file; impersonation + audit + tenant CRUD coexist. Security review must re-read 948 lines. | Pure file-split: admin/{tenants,impersonation,audit}_router.py, include_router under /admin |
S (~2h) |
| 3 | routers/inventory.py |
880 | get_system_detail D(29), export_system_annex_iv D(24) |
7 endpoints, two are 200+ line functions mixing fetch + render. Annex-IV export is 208 lines (a compliance officer's PDF breaking late at night). | Pull cp/inventory/system_detail_view.py (fetch) and reuse existing cp/export/annex_iv.py (router currently duplicates logic) |
M |
| 4 | shared/vendor_inference.py |
264 | F(85) | Mega-dispatch across 10 sources. | Registry + per-source files (see C.1) | M — defer until next vendor |
| 5 | data_plane/orchestrator/runner.py |
305 | run D(28), 249-line method |
Loads connection + secret, dispatches via 122-line if/elif, iterates, UPSERTs, classifies, finalizes. Adding connector #11 means editing this method. |
(a) Connector registry, (b) _persist_artifact(), © move classification to async (Plan 03 already plans this) |
M |
| 6 | routers/hitl.py |
600 | edit_facts C(12) — 163 lines |
Audit-write + history + verdict-update inline; failure mid-flight leaves verdict inconsistent. | Extract cp/hitl/edit_facts_service.py as one transactional function |
S |
| 7 | routers/connections.py::get_connection_health (978-1103) |
126 | D(21) | One function probes all 10 providers via inline if/elif. Adding a provider requires editing this and runner.py. |
Add async def probe() to each connector; get_connection_health dispatches |
M (touches 9 connectors) |
| 8 | routers/connections.py::scope_diff + required_scopes_for |
50 | B(6) | Each provider's required scopes is duplicated between connection-creation, scope-validation, and consent-URL building. | One source of truth: cp/connections/<provider>/scopes.py exporting REQUIRED_SCOPES: frozenset[str] |
S |
| 9 | passive_collector/normalize_dns.py |
136 | two B(10) functions | Two big normalizers handle multiple DNS event shapes. | Extract parse_hostname(event, *, key_paths) -> str helper |
S |
| 10 | passive_collector/aws_cloudtrail.py::_scan_region |
69 | C(12) | Triple-nested loop (event-source × page × event) with try/except inside. Page-token typo silently truncates. | Generic _paginate_boto3(client.lookup_events, ...) helper |
S |
Files >500 LOC: 5 total — routers/connections.py (1136), routers/admin.py (948), scripts/seed_demo.py (895, but it's a seed script — fine), routers/inventory.py (880), routers/hitl.py (600).
Files >1000 LOC: 1 — routers/connections.py (1136). This is the canonical splitter target.
Top-10 longest functions (AST-extracted, including blank/comment lines):
| LOC | File | Function |
|---|---|---|
| 249 | data_plane/orchestrator/runner.py:57 |
DiscoveryOrchestrator.run |
| 208 | apps/cp-api/.../inventory.py:286 |
export_system_annex_iv |
| 190 | shared/vendor_inference.py:75 |
infer_vendor |
| 169 | apps/cp-api/.../inventory.py:551 |
get_system_detail |
| 163 | apps/cp-api/.../hitl.py:175 |
edit_facts |
| 142 | apps/cp-api/.../admin.py:758 |
impersonate |
| 130 | apps/cp-api/.../inventory.py:148 |
export_inventory_pdf |
| 126 | apps/cp-api/.../connections.py:978 |
get_connection_health |
| 103 | apps/cp-api/.../connections.py:652 |
gcp_callback |
| 97 | apps/cp-api/.../inventory.py:48 |
list_systems |
The connector code itself does not appear in this list — every connector function is <70 LOC. The complexity has clustered in routers + orchestrator + vendor_inference, exactly where you'd predict for a shipped MVP.
E. Skill / library opportunities¶
E.1 Libraries to adopt¶
tenacityfor retries — adopt now. Zero retries means one transient 429 from OpenAI/GitHub kills a run. 5-line decorator on_get/_list_*, gated tohttpx.HTTPStatusErrorwithstatus_code in (429, 502, 503, 504). Effort S, payoff high.structlog.contextvars.bind_contextvars— adopt now in connectors. cp-api already uses structlog; data_plane uses stdlib. Bind(tenant_id, run_id, source)once at orchestrator entry, every log line inherits. Effort S, payoff huge for production debugging.pydanticforDiscoveredArtifact— skip. The dataclass is fine; adding pydantic validates data you're feeding via your own normalize functions.aiobotocorefor async AWS — defer. Currentboto3inasyncio.to_threadworks.opentelemetry-apiauto-instrumentation forhttpx— adopt later (Plan 03 territory).
E.2 Should we adopt an ABC Connector base class with default paginate, oauth_refresh?¶
No — but yes to a BaseDataPlaneClient. The constructors are too different to share a base ctor (refresh token vs IAM Role vs admin key vs App PEM vs OAuth M2M). But every connector that uses HTTP could share:
class BaseDataPlaneClient:
def __init__(self, *, base_url, auth_header_factory: Callable[[], str], retry_policy=...): ...
async def get(self, path, **params) -> dict: ...
async def paginate(self, path, *, items_key: str, cursor_in: str, cursor_out: str) -> AsyncIterator[dict]: ...
# 401/403 → raise PermissionError; 429/5xx → retry; otherwise return body.
This kills the 9 page-token loops and 12 silent-skip blocks. Connectors retain their bespoke discovery logic but get the HTTP plumbing for free.
E.3 Should passive collectors share a base class with active ones?¶
No. Passive twins differ from active in three meaningful ways: (a) lookback window, (b) silent-skip on AccessDenied is the expected path, not the exception, © de-dup-by-resource-id is a passive-specific concern. A shared interface (Connector Protocol) is enough; a shared base would force inappropriate uniformity.
E.4 Five highest-leverage shared modules¶
Pick these, in order:
data_plane/types.py— ownsDiscoveredArtifact,canonical_id(), theConnectorProtocol,ConnectorErrortaxonomy. Replaces the 12 copy-pastedcanonical_idand the awkward import-from-azure pattern. ~50 LOC removed once + everywhere imports normalize.data_plane/_http.py(BaseDataPlaneClient) — wrapshttpx.AsyncClientwith auth header factory, page-token paginator, retry, 401/403 silent-skip. Adopted by 7 connectors (azure, m365, gcp, openai, anthropic, databricks, github passive-azure, passive-gcp). ~280 LOC removed.shared/audit/service.py(AuditService.log) — single helper for both data plane (discovery run lifecycle) and API (existing 11 hand-rolledAuditLogEntry(...)). WireLiteral["discovery.run.started", ...]to catch action-name typos. +1 file, ~30 LOC; replaces ~150 LOC of inline construction.cp/connections/secrets.py(store_connection_secret) — kills the 8 KV-write copy-paste blocks inrouters/connections.py. ~120 LOC removed from a 1136-LOC file.- Connector registry in
data_plane/orchestrator/registry.py— replaces the 122-lineif/elif providerchain inrunner.py:96-218with a dict lookup + factory call. Adding connector #10 becomes a one-line registration, not arunner.pyedit.
These five together eliminate ~600+ LOC of duplication and move the pain points to one-line registration / one-line wiring for future work.
3. Section F — Top 5 single-PR low-hanging fruit (ranked by leverage / risk)¶
Each <1 day, each removes ≥30% of code in affected files, each makes the next connector easier, each is mechanical:
-
Move
DiscoveredArtifactandcanonical_idintodata_plane/types.py. Affected: 13 normalize files + 1 dedup file + 1 test. Removes 12 copies ofcanonical_id(~5 lines × 12 = 60 lines) and the awkward "import from azure_connector" pattern. Risk: zero — pure rename and re-export. Effort: 30 min. -
Add
tenacityretry toBaseDataPlaneClient(after step 1) on 429/5xx, max 3 attempts, exponential backoff. Affected: ~7 connectors get free retry. Removes manualtry/except httpx.HTTPStatusErrorboilerplate where present (~30 lines). Risk: low — retries are idempotent for our list operations. Effort: 4 hours. -
Extract
cp/connections/secrets.py::store_connection_secret(settings, connection_id, value, suffix) -> str. Affected:apps/cp-api/src/cp_api/routers/connections.py(8 inline copies). Removes ~80 LOC. Risk: zero — pure refactor of pure function. Effort: 1 hour. -
Replace orchestrator
if/elif providerchain with a registry dict. Affected:data_plane/orchestrator/runner.py:96-218(122 lines collapse to ~20). Risk: low — every branch becomes a small factory function with the same signature. Effort: 3 hours. -
Pass
tenant_id,run_id,sourceinto connectors viastructlog.contextvarsat orchestrator entry. Affected: orchestrator + every connector's logger. Removes ad-hocextra={"region": ...}tagging where it exists, gives uniform field shape. Risk: zero. Effort: 2 hours.
Bonus 6th (slightly larger but worth flagging as next-up): Split routers/admin.py (948 LOC) into 3 files (tenants, impersonation, audit). Pure file-split, all imports stay the same, the FastAPI router can be assembled via include_router. ~2 hours.
4. Anti-patterns / things to NOT do¶
-
Do NOT introduce an
ABC Connectorwith abstractdiscover(). The Protocol-by-convention works, the existingasync def discover() -> AsyncIterator[DiscoveredArtifact]is enough contract. Adding inheritance forces premature uniformity on constructors that legitimately differ. -
Do NOT collapse active + passive normalize files. They share the
DiscoveredArtifacttype but legitimately differ in field shapes (shadow,confidence,last_caller,event_sourceare passive-only; passive twins set differentkindstrings on purpose). Trying to share normalize code would force conditional logic that's worse than duplication. -
Do NOT refactor
vendor_inference.pyinto a registry yet. CC=85 looks scary onradonbut it's a flat dispatch, not deep nesting. Refactor cost is 4 hours, value is saved only when source #11 is added — defer the work to that PR. -
Do NOT unify the 5 token-fetch flows under one
TokenProviderabstraction. MSAL refresh, OAuth refresh, OAuth M2M, IAM AssumeRole, and raw API key are five fundamentally different protocols. The_acquire_access_tokentriplicate (Azure / M365 / passive-Azure) IS worth deduping (one helper), but pretending all five share an interface adds a layer with no payoff. -
Do NOT build a "MultiTenant" abstraction yet. The single-tenant-per-connection assumption is everywhere (
connection.tenant_idis on every model). Multi-tenant data isolation is already enforced at thetenant_contextPostgreSQL RLS level. Adding a parallel app-level abstraction would double-check the same invariant and add bugs. -
Do NOT introduce a "ConnectorRunner" base class that wraps the iteration in
runner.py. The orchestrator'sasync for artifact in connector.discover()loop is 4 lines; the rest of the long method is persist/classify orchestration that's product-specific, not connector-specific. Splitting just the connector loop into a base "runner" is yak-shaving. -
Do NOT migrate AWS to
aiobotocorepurely for "async purity". Currentasyncio.to_thread(self._list_region, region)is fine, fast, and well-tested. Switching libraries is a week of work and a regression surface.
5. Suggested sequencing¶
Phase 1 — plumbing wins (do first, 1-2 days total):
data_plane/types.py—DiscoveredArtifact,canonical_id,ConnectorProtocol. (unblocks everything else)cp/connections/secrets.py::store_connection_secret. (no dependency)data_plane/orchestrator/registry.py— replace theelifchain. (depends on #1)structlog.contextvarsbinding at orchestrator entry. (depends on #1)
Phase 2 — HTTP base + retry (3-4 days):
data_plane/_http.py::BaseDataPlaneClientwith paginate / silent-skip / retry. (depends on #1)- Migrate connectors one at a time to BaseDataPlaneClient: anthropic, openai, m365, gcp, databricks, github, passive-azure, passive-gcp. (Skip
aws_connectorandpassive-aws— boto3, different shape.) - Add a connector-level test for each migrated connector following
test_collector.pypattern. (this is where the test parity gap closes for free)
Phase 3 — audit + observability (2-3 days, do before EU AI Act audit):
shared/audit/service.py::AuditService.logwithLiteralaction names.- Wire into
runner.pyfor run lifecycle (started/completed/failed/partial). - Backfill router call sites (11 places) to use the helper.
Phase 4 — router split (1 day, low priority but easy):
- Split
routers/admin.pyandrouters/connections.py. Pure file moves, no behaviour change.
Phase 5 — defer until needed:
vendor_inferenceregistry refactor — wait until source #11.- ABC base class — never (or only when you have 15+ connectors and the Protocol is genuinely cramping you).
- aiobotocore migration — only if AWS becomes a measurable latency bottleneck.
6. Concrete metrics¶
Codebase shape¶
- Total Python files (excl. proto, tests, venv): 103 (88 in
packages/rekognise-core, 15 inapps/cp-api). - Total LOC analysed: 11,387 (per
radon raw). - SLOC: 8,676.
- Comment ratio: 8% (low — but most files have module docstrings, which radon doesn't count consistently).
- Average cyclomatic complexity: A (3.25) across 420 blocks. Healthy.
Connectors¶
- Connectors shipped: 9 active + 3 passive twins = 12 collectors.
- Total connector LOC: 3,429 (data_plane/).
- Connector source files: 36 (.py, excl. init).
- Smallest connector: m365 (129 LOC src).
- Largest connector: github (660 LOC src) — large because of code-search + workflow-scan sub-modules; actual
connector.pyis 264 LOC. - Estimated mechanical duplication: ~280 LOC (~8%).
Test parity¶
- Test files in
tests/unit/data_plane/: 18. - Total test LOC for data_plane: 1,514.
- Connectors with normalize tests: 9/9 (parity good).
- Connectors with
discover()flow tests: 1/9 (passive-azure only) + 1 partial (databricks oauth). - Test:source ratio per connector (LOC): databricks 0.92, github 0.49, passive-azure 1.07, m365 0.34, openai 0.25, anthropic 0.24, azure 0.35, aws 0.24, gcp 0.24.
Files >500 LOC¶
| LOC | File |
|---|---|
| 1136 | apps/cp-api/src/cp_api/routers/connections.py |
| 948 | apps/cp-api/src/cp_api/routers/admin.py |
| 895 | scripts/seed_demo.py (seed script — exempt) |
| 880 | apps/cp-api/src/cp_api/routers/inventory.py |
| 600 | apps/cp-api/src/cp_api/routers/hitl.py |
Files >1000 LOC: 1 (connections.py).
Top 10 longest functions¶
(See section D above.)
Top complexity hotspots (radon)¶
| CC | Block | File:line |
|---|---|---|
| 85 | infer_vendor (F) |
shared/vendor_inference.py:75 |
| 29 | get_system_detail (D) |
routers/inventory.py:551 |
| 28 | DiscoveryOrchestrator.run (D) |
data_plane/orchestrator/runner.py:57 |
| 24 | export_system_annex_iv (D) |
routers/inventory.py:286 |
| 21 | get_connection_health (D) |
routers/connections.py:978 |
| 17 | impersonate (C) |
routers/admin.py:758 |
| 16 | normalize_cloudtrail_event (C) |
passive_collector/normalize_aws.py:66 |
| 16 | DiscoveryOrchestrator (C) |
runner.py:30 |
| 15 | list_systems (C) |
routers/inventory.py:48 |
| 15 | normalize_gcp_log_entry (C) |
passive_collector/normalize_gcp.py:104 |
Audit-log writes¶
| Location | AuditLogEntry inserts |
|---|---|
apps/cp-api/src/cp_api/routers/admin.py |
6 |
apps/cp-api/src/cp_api/routers/hitl.py |
3 |
apps/cp-api/src/cp_api/routers/inventory.py |
2 |
packages/rekognise-core/src/rekognise/data_plane/** |
0 |
7. Honest verdict¶
Yes-but. This codebase is unusually clean for a 3-month-old, solo-founder, 9-connector product. Average CC is A, every connector has normalize tests, the architecture is duck-typed-Protocol pragmatism (correct for the stage), and the directory structure is consistent. There are no architectural mistakes that would force a rewrite.
The "but":
- One 264-LOC dispatch (vendor_inference) is starting to god-function.
- One router is 1,136 lines and has 8 copies of the same KV write.
- Connectors are missing flow-level tests for 8 of 9 vendors.
- Discovery runs leave zero audit trail — a real exposure for an EU AI Act compliance product.
- The BaseDataPlaneClient abstraction is overdue: ~280 LOC of pagination + auth-error handling is hand-rolled across files.
None of these are emergencies. All five top-leverage refactors are <1 day each, and Phase 1 alone (1-2 days) would remove ~250 LOC and give a clean seam for connectors #10-#15. The codebase will reward investment cleanly; it has not yet accreted hairballs that demand triage.
Recommendation: do Phase 1 + Phase 3 (audit) before the next compliance-focused customer call. Defer Phases 2 and 4 to a quiet week. Do not refactor vendor_inference until you ship vendor #11.