Plan Q: Function Decomposition + Shared Abstractions Extraction
On this page
Status
| Step | Description | Status |
|---|---|---|
1 |
Plan filing + |
Done (2026-06-06) — !605 / b719a770 |
2 |
|
Done (2026-06-06) — !606 / fe34d5dd |
3 |
Service mains to final form (4 substeps; amended 2026-06-06) — original mono-step splits into 3a (extend bootstrap with |
Done (2026-06-06) — final form across 9 mains |
3 batch 1 |
Bootstrap helper batch-1 adoption — partial migration of cases/exchange/security mains to consume the bootstrap helpers shipped in Step 2 ( |
Done (2026-06-06) — !607 / deeb5276 |
3a |
Extend |
Done (2026-06-06) — !608 / baa7542b |
3b |
Forward-migrate batch-2 mains — placement/financial/reporting/rules mains adopt full helper suite ( |
Done (2026-06-06) — !609 / 812adfea |
3c |
Per-main |
Done (2026-06-06) — !610 / 1fc3fc04 (B2 56→49 LOCKED) |
3d |
Finalize intake + web (different-shape services) — |
Done (2026-06-06) — !611 / ed42a4a9 |
4 |
|
Done (2026-06-07) — !616 / e420b416 (B2 36→31 LOCKED; sql.rs 1245→131 LOC; 34 SqlRow impls; 4 hash-pinned tests for cases/placement/exchange/financial — reporting/security skipped due to pre-existing |
5 |
Identity render |
Done (2026-06-06) — !613 / 8234e251 (per-section sibling-fn extraction; B2 49→46; 4 hash-pinned tests) |
6 |
CLI fat-arm decomposition (2–3 sub-MRs by command batch) — each |
Done (2026-06-07) — 3 batch MRs (!617 / !618 / batch 3 MR); 16 B2 sites cleared via Args-struct migration; CRUD trait deferred (signatures heterogeneous enough that the per-arm helpers are clearer; revisit if Plan H follow-up surfaces duplication); B2 36→15 LOCKED |
7 |
xtask command |
Done (2026-06-06) — !614 / b2e8210b (B2 cleared 46→38 LOCKED; 8 sites under threshold) |
8 |
B1 service-api module decomposition — the 3 closest-to-threshold B1 violators dropped below 500 LOC via helper extraction: |
Done (2026-06-06) — !612 / 02ccdb0c (B1 cleared 11→8 LOCKED) |
9 |
Residual large-fn decomposition — |
Done (2026-06-07) — !615 / 4aa191c7 (into_response 131→68; bootstrap 136→50; introspection already 28 LOC pre-MR; B2 counter skip-test fns added; B2 38→36 LOCKED) |
10 |
Verify B1+B2 LOCKED + Plan N handoff — |
Done (2026-06-07) — !620 / 7e701d73. Lock B2 49 → 15, B1 8 → 8 (already at floor). |
11 |
Plan completion audit + archive — plan-completion-audit subagent verifies all 10 prior steps Done with MR + sha citations; |
Done (2026-06-07) — this MR (audit verified 10/10 prior steps Done with concrete MR+sha citations; 16 MRs + 3 batch MRs across Step 6 = 18 total MRs; 51 functions decomposed; 1 new crate added ( |
Epic: &41 (https://gitlab.com/groups/gadhs/application/ccwis/-/epics/41)
Issues: #526–#536 (one per step)
Branch prefix: docs/plan-q- for filing/archive, feat/plan-q-step<N>- for new functionality, refactor/plan-q-step<N>- for decomposition, chore/plan-q-step<N>- for ratchet/handoff
Milestone: TBD
Context
Plan N (quality-gate-enforcement) Steps 5–7 paid down 5 of 7 OVER quality budgets (B5, B4, B3a, B7, B3b). Two remain: B1 (8 → 11) route modules over 500 LOC and B2 (49 → 56) functions over 100 LOC. They block the Plan N Step 9 enforcement-gate flip under strict-no-grandfather.
Audit of all 53 B2 violators surfaced a deeper truth: most aren’t "legitimate carve-outs per §Style" — they’re fat-arm dispatchers (CLI run fns with 20–50 LOC of inline logic per match arm), duplicated-setup-not-extracted-yet (9 service main fns sharing ~85% of bootstrap code), and copy-paste-and-customize patterns (11 seed-data builders, 3 identity IaC renderers). The §Style "important function" carve-out is real but narrower than the lint was treating it as.
User principle (2026-06-06): "Just because a function is important doesn’t mean that bad coding practices get a pass. We still want to decompose unless decomposition makes the codebase worse."
This plan reads the B1+B2 lint signal as it was meant — find the duplicated bootstrap and the fat-arm code that’s been compounding — and turns it into architectural extraction with the B1/B2 clears as a happy consequence.
The DRY opportunities the lint is pointing at
-
craig-bootstrapshared crate (highest value). Existingcrates/craig-api/src/bootstrap.rs:149 bootstrap()extracts ~80 LOC of setup (Settings + telemetry + DB pool + auth + RabbitMQ + service/actor tokens). What it does NOT yet extract — and what’s duplicated 100–180 LOC at a time across 9 servicemainfns — is:sqlx::migrate!()invocation pattern,OutboxWorkerspawn, AuthzEngine boot sequence (~200 LOC each: cache-invalidation subscriber + warmup + TTL refresh task),ObjectStoreConfig::load() → Store::from_config(), shared HTTP client init, standard axum extension-layer stack, shutdown-token plumbing. Extracting reduces typical service mains from 200–300 LOC to ~80–120 LOC. -
SqlRowtrait + table-driven render fortools/craig-seed/src/sql.rs(5 render fns × ~200–350 LOC each = ~1300 LOC). Today eachrender_X_sqlre-implementsINSERT INTO X (cols) VALUESboilerplate + comma/semicolon row separator + NULL handling forOptionfields. After: atrait SqlRow+ genericrender_table<T: SqlRow>. Each entity implementsSqlRow; render fns shrink to 5-line orchestrators. -
CLI fat-arm → per-action helpers for
services/craig-cli/src/cmd/*.rs(18 modules, 100–320 LOC each). Eachpub async fn runis amatch cmd { … }with 20–50 LOC inline per arm. Decompose into per-action helpers (run_list,run_get,run_create, etc.). ~50% are structurally identical (build-query → call-API → check-status → print-result) — potential CRUD trait pattern. -
Identity render
Manifest+ per-resource serializer forxtask/src/cmd/identity/render/{zitadel,kanidm,authentik}.rs(3 backends, 213–244 LOC each). Each backend renders the same canonical list of resources using its own DSL. After:Manifeststruct + per-backend serializer trait. -
Residual decomposition for ~5 truly bloated production fns outside the above patterns: introspection client, error response handler, devstack guard, security tests, identity verify probes.
Threat Model
Three patterns that make the B1+B2 drift insidious until a refactor or partner integration surfaces them:
-
Fat-arm dispatchers normalize per-command bloat. A CLI
runfn that’s "just a match" can absorb 200 LOC of per-arm logic without any single arm crossing a clippy threshold. The aggregate fn crosses 100 LOC but the structure looks like a "wide-but-thin dispatcher" so reviewers wave it through. Plan Q’s Step 6 converts these to actual thin dispatchers. -
Duplicated bootstrap as accepted boilerplate. Each new service grew its
mainby copy-pasting from the most-recently-added service. ~85% of those 200 LOC are identical AuthzEngine boot, OutboxWorker spawn, ObjectStore init. The §Style "important function" tag let each new copy slide because "main is supposed to be wide." Plan Q’s Step 2-3 extracts the common layer. -
§Style "important function" tag retiring its own purpose. Plan I F-033 marked 100 of 220 over-40-LOC fns as "important-stay" — defensible at the time, but the tag drifted from "would-be-worse-to-decompose" toward "we’re tired of refactoring this." Plan Q Step 1 retires the tag and replaces it with a stricter test.
Cross-cutting Invariants
-
Decompose unless decomposition makes the codebase worse. The new §Style rule. An "important function" tag is not a permanent decomposition exemption. The test: does extraction improve clarity / testability / maintainability without losing the narrative arc? If yes, decompose. Worse-only carve-outs: thin dispatchers (
Variant(args) ⇒ helper::run(args)), bootstrap with single-screen narrative arc, single-site helpers introducing parameter sprawl. -
Strict no-grandfather on B1+B2. Like Plan N Phase B before it. Steps 3–9 pay down B1+B2 to the new actual floor; Step 10 ratchets the lock DOWN to that floor (never UP).
-
Byte-identity regression tests at extraction boundaries.
cargo xtask reseedmust produce byte-identical SQL bytes through Step 4 (proves SqlRow refactor is semantics-preserving).cargo xtask identity render --backend Xmust produce byte-identical output through Step 5 (proves Manifest refactor is semantics-preserving). -
Devstack smoke per service-main migration. Step 3 (9 service mains) sub-splits by 3-service batch; each batch runs
cargo xtask dev start+ integration tests before pushing. Catches subtle worker startup-order or telemetry init-timing regressions. -
Plan Q clears the precondition; Plan N flips the gate. Plan Q’s Step 10 hands off to Plan N Step 9 via a cross-link in the Plan N body Status table. The enforcement-flip MR itself belongs to Plan N.
Step DAG
Step 1 (filing + §Style doctrine)
│
├── Step 2 (craig-bootstrap foundation) ── Step 3 (9 service mains migration)
│
├── Step 4 (SqlRow + seed refactor)
├── Step 5 (identity render Manifest)
├── Step 6 (CLI fat-arm decomposition; 2-3 sub-MRs by command batch)
├── Step 7 (xtask command run decomposition)
├── Step 8 (B1 service api module decomposition)
└── Step 9 (residual large-fn decomposition)
│
└── Step 10 (verify B1+B2 LOCKED, hand off to Plan N)
│
└── Step 11 (audit + archive)
Steps 2-3 and 4-9 can run in parallel session-by-session. Step 10 blocks on all of Steps 2-9 done. Step 11 blocks on Step 10.
Risks
-
craig-bootstrap::BuilderAPI too rigid for one service’s specific needs. Mitigation: UseOption<…>flags + closures for service-specific extension points; revisit after Step 3 if any service can’t fit. -
Service-main migration introduces a subtle behavioral change (worker startup order, telemetry init timing). Mitigation: per-service devstack smoke after each batch; sub-MR sequence catches one regression before propagating.
-
SqlRowtrait too generic; loses NULL-handling nuance. Mitigation:SqlValuewrapper struct that encodes nullability + type; covers all current sql.rs renderers; fall back to inline rendering if a corner case doesn’t fit. -
CLI fat-arm extraction reveals near-but-not-quite-identical helpers; macro pressure grows. Mitigation: hold the line on trait-based decomposition through Step 6; revisit macro only if Step 6 final-batch shows >50% boilerplate duplication.
-
Identity render byte-identity regression slips. Mitigation: Step 5 explicit hash-comparison test against pre-refactor output for each backend.
-
Plan Q runs long; Plan N Step 9 waits indefinitely. Mitigation: Steps 2-3 alone clear most of B2; if Step 6 (CLI) drags, the B2 budget may already be at lock by Step 7 — verify at Step 10 if early handoff is possible.
-
[cfg(test)]test fns inquality_budgets.rssynthetic source still inflate B2 count. Mitigation: Step 9 includes a B2 counter improvement to skip[cfg(test)]fns via syn attr inspection (drops 1-4 sites with zero code churn).
Verification
After each step: workspace nextest green; clippy -D warnings clean; cargo xtask docs plan-lint clean; B1/B2 actuals trending down.
After Step 3: cargo xtask quality-budgets --report shows B2 dropped by 9 (service mains cleared).
After Step 10: cargo xtask quality-budgets --fail-on-regression exits 0 against ALL 8 historical locks. Plan N Step 9 ready to land.
After Step 11: plan archived; CLAUDE.md row added; Plan N Step 9 flip-on lands in a follow-up MR (Plan N’s responsibility, not Plan Q’s).