ref.md - Monster-file split plan (backend)
Author: retoor retoor@molodetz.nl
This document is the execution plan for splitting every backend "monster" file in DevPlace into a package of cohesive modules, without changing behavior and without breaking any importer or test. It is investigation output only; nothing here has been executed. Each section is self-contained and can be executed independently in the recommended order (Section 12).
1. Scope and definition
"Monster" = first-party backend Python file over 900 lines (excludes vendored JS,
generated artifacts, and the scratch/workspace copies under data/, var/, tmp/,
and the repo-root maak.py/d.py). The confirmed set:
| # | File | Lines | Split kind |
|---|---|---|---|
| 1 | devplacepy/docs_api.py |
5659 | package: one module per API group |
| 2 | devplacepy/database.py |
3764 | package: layered domain modules |
| 3 | devplacepy/services/bot/bot.py |
2999 | in-package: cooperative mixins |
| 4 | devplacepy/services/containers/files/bot.py |
2940 | SPECIAL - deployed single-file asset |
| 5 | devplacepy/services/containers/files/d.py |
2337 | SPECIAL - deployed single-file asset |
| 6 | devplacepy/services/devii/actions/catalog.py |
2036 | same-name package: one module per action domain |
| 7 | devplacepy/schemas.py |
1285 | package: domain schema modules |
| 8 | devplacepy/cli.py |
1080 | package: one module per command group |
| 9 | devplacepy/utils.py |
1024 | package: layered helper modules |
| 10 | devplacepy/services/news.py |
986 | package: pipeline-stage modules |
| 11 | devplacepy/services/devii/session.py |
962 | same-name package: extract stateless text/helpers |
| 12 | devplacepy/services/game/store.py |
948 | subpackage: cohesive store modules |
services/bot/llm.py (792) is below threshold and holds two realism mechanics; it is
explicitly left intact (Section 3.4).
2. Global rules for every split (apply to all sections)
These are invariants. Every section assumes them; deviations are called out per file.
-
Same import path. Convert
foo.pyinto a packagefoo/(directory with__init__.py). Because Python resolvesfoo/__init__.pyforimport ...foo, every existingfrom ...foo import Xandimport ...foo as m; m.Xkeeps resolving with no caller edit. This is the ONLY compatibility mechanism used. -
Exhaustive re-export in
__init__.py. The package__init__must re-export the complete public surface the codebase and tests currently import - including underscore-prefixed names that are imported elsewhere (they are part of the effective surface; e.g.database._now_iso,utils._resolve_user,docs_api._substitute,store._today). Prefer explicitfrom .module import name1, name2blocks overfrom .module import *so the surface is auditable and star-import name-drops (underscore filtering,__all__gaps) cannot silently regress it. -
No behavior change. Pure structural moves. No logic edits, no reordering of ordered data, no renames of public symbols, no signature changes.
-
Preserve every function-local (lazy) import. Many files defer imports inside function bodies specifically to break import cycles (
database<->utils,utils<->services.audit/templating/content,newsAI-key, gameleaderboard). These MUST stay function-local in the new modules. Hoisting a lazy import to module top is the single most likely way this refactor introduces a boot-time cycle. -
File header. Every new file starts on line 1 with
# retoor <retoor@molodetz.nl>(project rule). Carryfrom __future__ import annotationsinto any module that usesX | Nonestyle annotations where the original had it. -
No em-dash. Neither the em-dash character nor
—/—/—anywhere in new files. Hyphen only. Grep every new file before finishing. -
No forbidden names / no comments / no docstrings in source (project rules): do not introduce
_new/_old/_temp/_v2/better_/my_/the_, do not add comments or docstrings. -
_shared.pyis the sanctioned package-private helper module. The project already uses_shared.pyin eight router packages for cross-leaf helpers. Use that exact name for genuinely package-private construction helpers (docs_api builders, catalog builders, cli_audit_cli). Everything else is a normal named module. -
Validation after each split (never run the test suite):
python -c "from devplacepy.main import app"must import clean.python -m py_compileevery new file.- A public-surface diff: capture
sorted(n for n in dir(<module>) if not n.startswith('__'))BEFORE the split (from git or a saved snapshot) and assert the post-split package exposes a superset. This is the definitive check that no re-export was missed. - em-dash grep over every new file.
- The suite is run only if the user explicitly asks; the plan never runs it.
3. Per-file plans
3.1 docs_api.py (5659) -> devplacepy/docs_api/
Nature. A flat declarative module: a few construction helpers, one giant
API_GROUPS list literal (~5240 lines, docs_api.py:143-5384, 19 group dicts), then
a post-processing/registry/render tail. Endpoints are organized by API group; each group
is a self-contained dict literal built via the endpoint() factory.
Public surface (only these are imported externally):
API_GROUPS, build_services_group, get_group, api_doc_pages, render_group, and
_substitute (underscore but called by docs_export.py:119).
Importers: docs_search.py:9,87,90; docs_export.py:10,118,119,121;
services/xmlrpc/registry.py:8,63; routers/docs/pages.py:3; routers/docs/views.py:14.
Tests: tests/e2e/docs.py:9, tests/unit/project_files.py:381-385,
tests/api/auth/matrix.py:7, tests/api/docs/{index,downloadhtml,downloadmd}.py.
Target layout:
devplacepy/docs_api/
__init__.py # assembles API_GROUPS in order, runs the negotiation pass,
# re-exports the public surface (incl. _substitute)
_shared.py # field, endpoint, ROLE_LABELS, NON_BODY_ENDPOINTS, and the
# domain constant lists (VOTE_TARGETS, REACTION_TARGETS,
# BOOKMARK_TARGETS, COMMENT_TARGETS, PROJECT_TYPES,
# GIST_LANGUAGES, SERVICE_ACTIONS)
negotiation.py # _PAGE_RESPONSES, _ACTION_RESPONSES, _classify,
# _apply_negotiated_responses(groups) (schemas/docs_examples live here)
services_group.py # _service_control_endpoints, _field_to_param, _service_section,
# build_services_group
render.py # _GROUPS_BY_SLUG, get_group, api_doc_pages, _substitute, render_group
groups/
__init__.py # ORDERED_GROUPS = [conventions.GROUP, auth.GROUP, ... game.GROUP]
conventions.py # docs_api.py:145-293
auth.py # 295-427
lookups.py # 428-484
social_actions.py # 485-674
content.py # 675-1615 (largest group, ~940 lines)
profiles.py # 1616-2294
messaging.py # 2295-2365
notifications.py # 2366-2450
uploads.py # 2451-2552
project_files.py # 2553-3098
containers.py # 3099-3673 (admin: True)
tools.py # 3674-3902
push.py # 3903-3964
issues.py # 3965-4258
gateway.py # 4259-4449 (admin: True)
services.py # 4450-4456 (dynamic placeholder group dict - keep hollow)
admin.py # 4458-5193 (admin: True, ~735 lines)
game.py # 5194-5383
Each groups/*.py holds one GROUP = {...} literal importing field, endpoint,
ROLE_LABELS and the constant lists from .._shared (and constants.TOPICS/
REACTION_EMOJI where the literal uses them - re-scan each block for free names before
cutting).
__init__.py must, in this exact order:
from .groups import ORDERED_GROUPS as API_GROUPS(order is load-bearing).from .negotiation import _apply_negotiated_responsesthen call_apply_negotiated_responses(API_GROUPS)- reproduces the current import-time mutation atdocs_api.py:5494. Must run AFTERAPI_GROUPSis assembled.- Re-export
API_GROUPS,build_services_group,get_group,api_doc_pages,render_group,_substitute(and, for safety,endpoint,field,ROLE_LABELS, the constant lists).
Risks / ordering hazards (must be handled):
- Import-time negotiation mutation.
_apply_negotiated_responses()runs at module top today (docs_api.py:5494) and mutates each endpoint dict in place (fillssample_response, stampsnegotiation). Centralize this in__init__after assembly; if a group is read before the pass runs, endpoints lacknegotiation/sample_response. - Forward reference.
_classify(105-112) reads_PAGE_RESPONSES/_ACTION_RESPONSESdefined 5280 lines later. Colocate_classify+ both maps + the pass innegotiation.pyto make the dependency explicit. - Assembly-before-render cycle.
render.py(get_group/render_group/api_doc_pages/_GROUPS_BY_SLUG) needs the finalAPI_GROUPS. BuildAPI_GROUPSingroups/__init__.pyand import it into both package__init__andrender.py; do NOT letrender.pyimport the package__init__at module top (cycle). build_services_groupreuses core helpers (endpoint,fieldvia_field_to_param,_classify,_service_control_endpoints) and stampsnegotiationitself for its runtime group;services_group.pyimportsendpoint/fieldfrom_sharedand_classifyfromnegotiation; do NOT double-apply negotiation to it._substitutename must be preserved in the re-export (called asdocs_api._substitutebydocs_export.py:119).- Two groups slug
services(the hollow placeholder in the list + the runtimebuild_services_groupoutput). Keep the placeholder exactly where it is in the ordered list.
Test invariant: API_GROUPS order/content, group flags (slug/title/admin/dynamic/ endpoints), and every endpoint's post-negotiation shape (id/method/path/auth/min_role/ params/negotiation/sample_response) must be identical after the split. tests/api/auth/ matrix.py asserts len(endpoints) >= 50 and drives each endpoint's documented auth against
live behavior; tests/unit/project_files.py:381-385 asserts the project-files group's exact
endpoint id set.
3.2 database.py (3764) -> devplacepy/database/
Nature. The core data layer, imported nearly everywhere (677 from ...database import
statements across 329 files; hottest names get_table x404, refresh_snapshot x92,
set_setting x67, db x33). The __init__ MUST re-export essentially the entire namespace
including underscore helpers (_now_iso x6, _index, and whatever tests reach for).
The linchpin. A single core.py owns the db singleton, the import-time connection
side effects, the cross-worker cache-version primitives, index helpers, and trivial
accessors. Every other module imports only from core (and strictly-lower layers). Because
core.py is imported once, Python's module cache guarantees the single db object, exactly
as today.
Target layout (devplacepy/database/):
core.py # db, refresh_snapshot; import-time ensure_data_dirs + sqlite mkdir +
# dataset.connect (database.py:20-42); cache-version primitives
# (_ensure_cache_state, get_cache_version, bump_cache_version,
# sync_local_cache, _local_cache_versions, _cache_version_cache,
# _cache_state_ready); _index, _drop_index, _uid_index;
# get_table, _in_clause, _now_iso <- imported by ALL
settings.py # _settings_cache, get_setting, get_int_setting, set_setting,
# clear_settings_cache, internal_gateway_key
users.py # get_users_by_uids, search_users_by_username, set_user_timezone,
# admins cache (_admins_cache, invalidate_admins_cache,
# get_admin_uids, get_primary_admin_uid)
relations.py # user block/mute cache + accessors (get_user_relations, get_blocked_uids,
# get_muted_uids, get_silenced_uids, invalidate_user_relations)
pagination.py # PAGE_SIZE, build_pagination, paginate, paginate_diverse,
# interleave_by_author, get_user_post_count (imports core + relations)
soft_delete.py # SOFT_DELETE_TABLES, ensure_soft_delete_columns, soft_delete,
# soft_delete_in, restore, purge, list_deleted, count_deleted,
# restore_event, purge_event (imports core + pagination.build_pagination)
engagement.py # comment-count/vote/reaction/bookmark batch helpers, polls
comments.py # _drop_blocked, _build_comment_items, load_comments, recent/by-target loaders
attachments_data.py# get_attachments*, get_news_images_by_uids, delete_attachment*,
# _delete_attachment_file, get_user_media, get_deleted_media
usage.py # _add_usage, _get_usage, add/get_*_usage + *_USAGE_KEY constants
seo_meta.py # SEO_META_TYPES + seo_metadata CRUD
activity.py # user_activity counters + calendar/heatmap/streaks
ranking.py # VOTABLE_TARGETS, STAR_TARGETS, authors/leaderboard/stars,
# update_target_stars, get_target_owner_uid, soft_delete_engagement,
# delete_engagement
customization.py # customization prefs + override CRUD
email.py # email account CRUD
notifications.py # notification prefs + mark_notifications_read_by_target
forks.py # record_fork, get_fork_parent, count_forks, *_fork_relations
content.py # resolve_by_slug, resolve_object_url, text_search_clause,
# get_uids_by_username_match, get_daily_topic, get_featured_news
follows.py # get_follow_counts, get_follow_list, get_following_among
stats.py # get_site_stats, get_platform_analytics, get_gist_languages
deepsearch.py # _ds_now + deepsearch session/message/url-cache helpers
schema.py # init_db, _refresh_query_planner_stats, migrate_bug_tables_to_issue_tables,
# BUG_TABLE_RENAMES, migrate_ai_gateway_settings, OLD_GATEWAY_URL,
# backfill_api_keys, _backfill_gamification <- imports EVERYTHING; loaded last
__init__.py # explicit re-export of every module's public + test-touched underscore names
Where shared state lives (critical). The only truly global object is db (+ the
cache-version machinery), which lives in core.py. Every domain TTLCache/dict singleton
(_admins_cache, _settings_cache, _relations_cache, _customizations_cache,
_notification_prefs_cache, _stats_cache, _analytics_cache, _activity_cache,
_gist_languages_cache, _authors_cache, _comment_count_cache) is referenced only by its
own domain, so it moves WITH that domain's module - no shared-cache problem. Never re-create
a cache in __init__; only re-export. Two import paths to one cache would create two caches.
Layering (acyclic). core <- everything; pagination -> relations;
soft_delete -> pagination; comments -> users + engagement + relations;
ranking -> users; stats -> ranking; attachments_data -> content; content -> users;
schema imports any sibling at module top (nothing imports schema; loaded last in __init__).
Cycle-avoidance rules:
core.pyimports nothing from siblings.- Strict layer order:
core -> settings/users/relations -> pagination -> soft_delete -> domain modules -> schema. - Preserve every existing function-local import verbatim - the
utils,attachments,templating,services.audit.store/services.backup.store/services.openai_gateway.routingimports inside functions (e.g.init_dbatdatabase.py:951-961) exist to break the realdatabase <-> utilscycle. Do not hoist them. utils.pyusesdatabase's cache primitive under the"auth"name; those primitives are incore.pyand remain reachable asdatabase.bump_cache_version/database.sync_local_cachevia re-export.
Risks:
init_db()(503-1534) is one idempotent ordered routine - move it WHOLE intoschema.py; do not reorder its internal steps (migration-before-index, backfills/ANALYZE last at 1516-1519).- Import-time side effects (20-42) must live in
core.pyand run exactly once;coremust be the first thing__init__imports. - Underscore re-export gap is the highest-probability regression (
import *drops_now_iso/_index). Use explicit re-exports or per-module__all__including the imported underscores. get_table(404 importers) andrefresh_snapshot(92) - any miss breaks a huge fraction of the app at import; the public-surface diff (Section 2.9) is the guard.
Test footprint. tests/unit/database.py is the primary direct consumer (it also hits a live
server, so it is effectively api-tier); tests/unit/services/gitea.py and
tests/unit/services/openai_gateway/analytics.py import it for setup; dozens of api/e2e files use
get_table/set_setting/usage helpers indirectly. With a complete re-export, zero test edits are
needed.
3.3 services/bot/bot.py (2999) -> cooperative mixins in services/bot/
Nature. ONE class DevPlaceBot (bot.py:48), ~90 methods, every method reads/writes
shared self state (self.state persisted BotState, self.b browser, self.llm,
self._session_* counters, the _post_cache/_project_cache/_issue_cache/_session_history
caches). The only zero-behavior-change decomposition is cooperative mixins (Python MRO gives
byte-identical self semantics; the unbound-method unit test still resolves via inheritance).
There is NO @tool registry in this file (the closest analogue is the AI decision/menu engine).
Target layout (new modules beside the existing services/bot/ files):
| New module | Mixin | Moves from bot.py | Contents |
|---|---|---|---|
helpers.py |
BotHelpersMixin |
103-245 | instrumentation, cost/session banners, _thread_id, _pick_article, cache warm |
auth.py |
BotAuthMixin |
246-355 | handles, _ensure_auth, account-key adoption |
human.py |
BotHumanMixin |
356-522 | read/type/fatigue simulation, session type, page inspection |
agent.py |
BotAgentMixin |
523-757 | AI decision/menu/plan engine (_build_menu, _run_plan, _cycle_ai, dispatch, nav) |
engage.py |
BotEngageMixin |
753-1436 | voting, commenting, reacting (realism #4b) |
posting.py |
BotPostingMixin |
1438-1770 | gist + post creation, content open/click (realism #1, #3 call-sites) |
social.py |
BotSocialMixin |
1771-2378 | profile/project/issue/follow, notifications, mentions (#4b), messaging |
browse.py |
BotBrowseMixin |
2379-2502 | scroll, nav, browse, search, _engage_community |
loop.py |
BotLoopMixin |
2503-2999 | _cycle, run_forever (scheduling) |
bot.py shrinks to ~110 lines: the mixin imports, __init__, and:
class DevPlaceBot(
BotHelpersMixin, BotAuthMixin, BotHumanMixin, BotAgentMixin,
BotEngageMixin, BotPostingMixin, BotSocialMixin, BotBrowseMixin, BotLoopMixin,
):
...
DevPlaceBot stays importable from devplacepy.services.bot.bot (do NOT move the class file;
only lift methods into inherited mixins). Method names are already globally unique across the
90 methods - verify no collision when composing.
Realism contract (MUST NOT regress) - exact locations to preserve:
- Title rewrite:
LLMClient.generate_post/generate_post_title(inllm.py, untouched); call-sitesbot.py:205,211,1571,1577,1597move intohelpers.py/posting.pyunchanged. - Persona topic spread:
persona_article_score/pick_category(config.py, untouched); call-sites inhelpers.py/posting.py. - Gist quality gate:
LLMClient.gist_quality_check/quality_check(llm.py, untouched); enforced inposting.py(_create_gist) andengage.py(_comment_on_post). - Inter-bot threads:
ArticleRegistry(own module, untouched) PLUS in-bot taper (state.thread_reply_counts/MAX_THREAD_REPLIESbot.py:2163-2166,2272-2277,MAX_SIBLING_COMMENTS912,997-1020,1155-1164,_respond_to_mention2143,_reply_to_random_comment1100,_spoke_last_in_thread2305) - keep this logic together and unchanged acrossengage.py/social.py.
__init__.py - keep exporting ONLY BotsService (lazy-Playwright boundary). service.py
lazy-imports bot.py and treats ImportError as "install the bots extra". bot.py imports
faker and BotBrowser (Playwright) at module top. If the package __init__ eagerly imported
DevPlaceBot, importing devplacepy.services.bot (for BotsService, used by main.py) would
hard-fail on boxes without the extra. So: leave __init__.py exporting only BotsService; keep
DevPlaceBot/LLMClient importable from their own modules (all live importers already use those
paths - service.py:263, bot.py:39,59).
Stealth rule - already compliant, do not regress. The only outbound HTTP is
LLMClient._raw_call (llm.py:69) via stealth.stealth_sync_client. bot.py makes no bare
httpx calls (all page "fetch" is inside Playwright). No new module may introduce a bare
httpx.Client.
Test footprint. tests/unit/services/bot/bot.py imports bot as bot_module and
DevPlaceBot, calling DevPlaceBot._adopt_account_api_key(fake) as an unbound method - safe
under mixins (method stays an attribute via inheritance). Admin fleet api/e2e tests go through
BotsService, unaffected.
3.4 services/bot/llm.py (792) - LEAVE INTACT (explicit decision)
Below the 900-line threshold, a single cohesive LLMClient with shared _call/cost counters,
and it holds two realism mechanics (title rewrite, gist gate). A mixin split would be
net-negative and risk the contract. Keep it whole. (If ever reduced later, the only safe cut is
lifting the static text cleaners clean/strip_md/strip_label/strip_code_fences/_parse_json/
_clamp01/_as_int/_as_terms/_overlap_ratio into an llm_text.py of module functions that
LLMClient staticmethods delegate to, preserving LLMClient.clean as a public alias. Not part of
this plan.)
3.5 services/devii/actions/catalog.py (2036) -> same-name package catalog/
Nature. NOT the Catalog/Action classes (those live in actions/spec.py) and NOT the
live CATALOG (that is registry.py:26-46, Catalog(actions=ACTIONS + TASK_ACTIONS + ...),
with _assert_confirm_params(CATALOG) an import-time side effect at registry.py:68). This file
only defines ACTIONS (161 Action literals, catalog.py:43-2034), five private builder helpers
(path/query/body/upload/confirm, 8-37), two description constants (ATTACHMENTS, TARGET_TYPE,
40-41), and PLATFORM_CATALOG = Catalog(actions=ACTIONS) (2036). No logic.
Public surface: ACTIONS and PLATFORM_CATALOG only. Importers: actions/__init__.py:3,7,
registry.py:9,27, and tests tests/unit/project_files.py:79,366,368, tests/unit/schemas.py:117, 123,132, tests/unit/services/devii/actions/dispatcher.py:6,
tests/unit/services/devii/actions/catalog.py:3, tests/api/projects/files/{lines,replacelines}.py:79.
Target layout (services/devii/actions/catalog/):
__init__.py # imports each domain tuple, ACTIONS = (AUTH_ACTIONS + POSTS_ACTIONS + ...),
# PLATFORM_CATALOG = Catalog(actions=ACTIONS); __all__ = ["ACTIONS","PLATFORM_CATALOG"]
_shared.py # path(), query(), body(), upload(), confirm(), ATTACHMENTS, TARGET_TYPE
auth.py # AUTH_ACTIONS (catalog.py:44-105)
posts.py # POSTS_ACTIONS (106-185, incl. view_feed)
comments.py # COMMENTS_ACTIONS (186-216)
projects.py # PROJECTS_ACTIONS (217-342)
project_files.py # PROJECT_FILE_ACTIONS (343-511)
jobs.py # JOB_ACTIONS (zip+fork) (512-578)
tools.py # TOOLS_ACTIONS (579-679)
profile.py # PROFILE_ACTIONS (680-748)
messages.py # MESSAGE_ACTIONS (749-776)
notifications.py # NOTIFICATION_ACTIONS (777-810)
engagement.py # ENGAGEMENT_ACTIONS (811-880)
social.py # SOCIAL_ACTIONS (881-976, incl. leaderboard)
issues.py # ISSUE_ACTIONS (977-1122)
gists.py # GIST_ACTIONS (1123-1190)
news.py # NEWS_ACTIONS (1191-1211)
uploads.py # UPLOAD_ACTIONS (1212-1246)
admin.py # ADMIN_ACTIONS (1247-1661, ~415 lines; optional further sub-split)
dbapi.py # DBAPI_ACTIONS (1662-1778)
gateway.py # GATEWAY_ACTIONS (1779-1874)
game.py # GAME_ACTIONS (1875-2033)
Each domain module does from ._shared import path, query, body, confirm, ATTACHMENTS, TARGET_TYPE
and from ..spec import Action. dbapi.py, gateway.py, game.py must ALSO
from ..spec import Param - they construct Param(...) directly for boolean/integer/number
params. admin.py at ~415 lines may optionally be sub-split into admin_users.py/admin_services.py/
admin_backups.py/admin_analytics.py, but a single admin.py is already a large improvement.
Risks:
- Action ordering is behavior.
Catalog.tool_schemas()emits schemas inself.actionsorder (the LLM tool-list order). The__init__concatenation must reproduce the exact source order in the table above. Verify[a.name for a in ACTIONS]is identical before/after (order + set). - Import-time singleton + validation.
PLATFORM_CATALOGbuilds at import;registry.py:68runs_assert_confirm_params(CATALOG)at import.ACTIONSmust be fully assembled when the package__init__finishes. Domain modules import only from.._shared/..spec(leaf-level, no cycle). confirm-param invariant.registry._assert_confirm_params(49-65) requires every name indispatcher.CONFIRM_REQUIRED/CONDITIONAL_CONFIRMto declare aconfirmparam; dropping anyconfirm()/inline-confirm param makes importRuntimeError. Watch the inlinebody("confirm", ...)cases atcatalog.py:310-314,336-340,727-731.
Nothing outside the package changes; actions/__init__.py and registry.py keep their exact
imports.
3.6 schemas.py (1285) -> devplacepy/schemas/
Nature. Pure Pydantic *Out models, all subclassing one shared base _Out
(schemas.py:10-11). One import-time side effect: CommentItemOut.model_rebuild() at
schemas.py:1023 (resolves the self-referential children forward ref). No runtime logic.
48 files import from devplacepy.schemas (all from ...import X), each router pulling the models
it feeds to respond(..., model=XOut); the __init__ must re-export ALL public model names.
Model dependency graph (drives module order). UserOut is depended on by nearly every
wrapper; AttachmentOut/VotesOut/ReactionsOut/PollOut/BadgeOut/CommentItemOut feed the item
wrappers; cross-module inheritance: AdminUserOut(UserOut), AdminMediaItemOut(MediaItemOut),
ProjectListItemOut(ProjectOut), ProjectFileRawOut(ProjectFileOut). So the shared primitives
live in content.py/profile.py, imported first by the domain modules.
Target layout (devplacepy/schemas/):
__init__.py # explicit re-export of every *Out (+ ActionResult, ErrorOut, ValidationErrorOut)
base.py # _Out + generic envelopes (ActionResult, ErrorOut, ValidationErrorOut)
content.py # UserOut, AttachmentOut, VotesOut, ReactionsOut, Poll*, BadgeOut, PostOut, GistOut,
# ProjectOut, ProjectFile*, NewsOut, CommentOut, CommentEditOut, CommentItemOut,
# NotificationOut, MessageOut (+ CommentItemOut.model_rebuild() at bottom)
listings.py # FeedItemOut/GistItemOut/ProjectListItemOut/NewsListItemOut, Conversation,
# MessageItemOut, Notification*Out, Saved*, Leaderboard*, and the page-level
# FeedOut/PostDetailOut/ProjectsOut/ProjectDetailOut/Gists*/News*/Messages*/
# Notifications*/Saved*
profile.py # MediaItemOut, ProfileOut, TelegramPairOut
issues.py # IssueItemOut, IssueCommentOut, IssueDetailOut, IssueAttachmentsOut, IssueJobOut, IssuesOut
containers.py # InstanceOut, ScheduleOut, ContainersOut, AdminContainers*, Bot*
jobs.py # ZipJobOut, PlanningJobOut, ForkJobOut, SeoJobOut, SeoReportOut, SeoMetaOut,
# DeepsearchJobOut, DeepsearchSessionOut, DbQueryJobOut
backups.py # BackupStoragePathOut, BackupOut, BackupJobOut, BackupScheduleOut, BackupDashboardOut
admin.py # AdminUserOut, AdminUsersOut, AdminNews*, AdminSettingsOut, AdminNotificationsOut,
# AdminMedia*, AdminTrashOut/TrashItemOut
gateway.py # GatewayUsageOut, UserAiUsageOut
auth.py # AuthPageOut, Landing*, DeviiPageOut
audit.py # AuditLinkOut, AuditEntryOut, AuditLogOut, AuditEventOut
dbapi.py # DbTableOut..NlQueryOut
game.py # GameCropOut..GameLeaderboardOut
Every module does from devplacepy.schemas.base import _Out and imports the primitive models it
subclasses/references from content.py/profile.py. Use explicit re-export (not *) so ordering
does not matter and the surface is auditable.
Risks:
- Forward-ref rebuild. Keep
CommentItemOutand the whole content-primitive cluster incontent.pyand callCommentItemOut.model_rebuild()at the bottom of that module (after all referencing types are importable). - Cross-module inheritance requires the base imported before the subclass; per-file
from ...importhandles it since all base classes live incontent.py/profile.py. - Models reach routers only via
respond(model=...); a dropped export silently degrades JSON responses (Pydantic drops unknown keys). Re-export completeness is the whole risk.
Test footprint. tests/unit/schemas.py imports MediaItemOut, AdminMediaItemOut,
GameLegacyOut inside test functions - all must survive re-export.
3.7 cli.py (1080) -> devplacepy/cli/
Nature. Plain argparse. One main() builds the parser tree; each subcommand sets
func=cmd_*; main() dispatches args.func(args). Console-script entry pyproject.toml:40
devplace = "devplacepy.cli:main". Heavy imports are lazy inside each command body (containers,
jobs.queue, backup.store) - this keeps CLI import cheap and MUST be preserved.
Target layout (devplacepy/cli/):
__init__.py # from .main import main; re-export EVERY cmd_* (tests call cli.cmd_role_get etc.)
_shared.py # _audit_cli (cli.py:9-23)
roles.py # cmd_role_get/set + register_roles(sub) (26-61)
apikeys.py # cmd_apikey_* + register_apikeys(sub) (64-106)
tokens.py # cmd_token_* + register_tokens(sub) (109-211)
devii.py # cmd_devii_reset_quota + register_devii(sub) (214-254)
news.py # cmd_news_* + register_news(sub) (257-290)
attachments.py # cmd_attachments_prune + register(sub) (293-315)
jobs.py # zips/forks/seo/seo-meta/deepsearch prune-clear + _remove_*_artifacts (318-576)
backups.py # cmd_backups_* + register_backups(sub) (393-445)
containers.py # cmd_containers_* + register_containers(sub) (579-659)
migrate.py # _crc32/_migrate_*/_db helpers + cmd_migrate_data + cmd_emoji_sync (662-870)
main.py # build_parser()/main(): imports each module's register_* and dispatches;
# holds the `if __name__ == "__main__": main()` guard
Each leaf exposes register_<group>(subparsers) adding its parser(s) + set_defaults(func=...).
main.py calls them in order. __init__.py re-exports main AND every cmd_* because
tests/unit/cli.py (from devplacepy import cli) calls cli.cmd_role_get/set, cli.cmd_news_clear/ sanitize, cli.cmd_attachments_prune, cli.cmd_apikey_get/reset/backfill, cli.main directly.
Risks:
- Console-script entry
devplacepy.cli:mainmust keep resolving - satisfied byfrom .main import mainin__init__.py. - Preserve lazy in-function imports (avoid import-time cycles / heavy deps). Do not hoist.
- Move the
__main__guard intomain.py(or__init__callingmain()) sopython -m devplacepy.clibehavior is unchanged.
No production module imports devplacepy.cli (only tests). Sibling entrypoints
devplacepy.services.devii.cli:main and devplacepy.services.xmlrpc.server:main are unaffected.
3.8 utils.py (1024) -> devplacepy/utils/
Nature. The most import-sensitive file: 224 files import from it; source of several Jinja
globals and the notification/rewards/achievement funnels. Most-used: make_combined_slug (144),
generate_uid (127), clear_user_cache (32), not_found (22), get_current_user (20),
require_admin (19), require_user (16), is_admin (12), track_action (10). Several
underscore helpers are part of the effective surface (_resolve_user, _user_from_session,
_user_from_api_key, _user_from_devrant_token) and MUST be re-exported.
Target layout (devplacepy/utils/):
__init__.py # explicit re-export of the FULL public API incl. imported underscore helpers
passwords.py # hash_password/verify_password (+async), create_session (40-70)
authcache.py # _user_cache, _sync_auth_cache, clear_user_cache, clear_session_cache (73-90)
auth.py # _UNSET, _user_from_*, _resolve_user, get_current_user (92-249)
guards.py # safe_next, redirect_back, cookie_secure, client_ip, require_user,
# require_admin, require_user_api, not_found, is_admin, is_primary_admin (252-376)
text.py # strip_html, plain_float, _mark_floats, pretty_json, slugify, generate_uid,
# make_combined_slug, time_ago, format_date, extract_mentions (379-459, 1015-1024)
notifications.py # PUSH_ICON, DEFAULT_PUSH_URL, _push_tasks, _safe_notify, _schedule_push,
# _schedule_telegram, create_notification, _deliver_notification,
# create_mention_notifications, _deliver_mention_notifications (462-575, 989-1013)
badges.py # BADGE_CATALOG, BADGE_GROUPS, LEVEL_BADGES, get_badge, build_achievements,
# award_badge, notify_badge (671-745, 806-848)
rewards.py # LEVEL_XP, XP_*, level_for_xp, award_xp, _COUNT_MILESTONES, _milestone_metrics,
# check_milestone_badges, award_rewards, _apply_rewards, ACHIEVEMENTS,
# UNIQUE_ACTIONS, track_action, _apply_achievements (654-987)
accounts.py # _create_account, register_account, register_account_async (578-641)
Circular-import sensitivity (must preserve exactly):
clear_user_cacheis lazily imported byservices/correction.py:183and by 32 other files +cli.py:74; it MUST stay importable asdevplacepy.utils.clear_user_cachevia re-export.- Every cross-cutting lazy import inside utils stays function-local:
is_primary_admin->database.get_primary_admin_uid(328);require_user/require_admin->responses.wants_jsonservices.audit.record(303-304,336-337);_deliver_notification->templating.clear_unread_cache(531) +services.audit(557);award_xp/check_milestone_badges/award_badge/_create_account->services.audit;_milestone_metrics->database.get_streaks;_schedule_telegram->services.telegram.store;_safe_notify->devplacepy.push;_user_from_devrant_token/_user_from_access_token->services.devrant.tokens/services.access_tokens. Do NOT hoist any.
- Cache-name coupling: the auth cache uses
sync_local_cache("auth", ...)/bump_cache_version("auth")- do not rename the
"auth"key.
- do not rename the
Funnel pairing (do not split apart, do not double-wrap): each public funnel and its
background.submit worker must live in the same module: create_notification/_deliver_notification
and create_mention_notifications/_deliver_mention_notifications in notifications.py;
award_rewards/_apply_rewards and track_action/_apply_achievements in rewards.py. Both
notifications.py and rewards.py need the module-level from devplacepy.services.background import background import.
Jinja globals (register-once, from templating.py). templating.py:13-15,62-63,112,132-136
imports format_date, time_ago, get_badge, is_admin, is_primary_admin, pretty_json and
registers is_admin/is_primary_admin/format_date as callable globals (load-bearing per the
respond() collision rule). The registration stays in templating.py; the split must only keep
those import paths resolving via the package.
Cache invalidation invariant. _create_account calls database.invalidate_admins_cache() on the
first user (640) - keep that import in accounts.py.
Test footprint. tests/unit/utils.py imports award_badge, award_xp, get_badge, check_milestone_badges, create_mention_notifications, extract_mentions, generate_uid, hash_password, is_primary_admin, level_for_xp, safe_next, slugify, strip_html, time_ago, verify_password - every one
must survive re-export. After the split, diff dir(devplacepy.utils) public names against the
pre-split snapshot.
3.9 services/news.py (986) -> devplacepy/services/news/
Nature. NewsService(BaseService) plus a set of pure free functions and constants for the
import pipeline. main.py:88,236 imports and registers NewsService. The CLI does NOT import this
module (its news clear/news sanitize operate on raw tables via dataset + utils.strip_html).
Target layout (devplacepy/services/news/):
__init__.py # re-exports the full public + test surface; also re-binds `net_guard` and `httpx`
constants.py # all tuning constants + JUNK_PATTERNS + WHITESPACE_PATTERN + prose blocks
# (NEWS_SUMMARY, GRADE_PROMPT_SPEC, FORMAT_PROMPT_SPEC, GRADING_RULES_DESCRIPTION);
# numeric constants MUST precede GRADING_RULES_DESCRIPTION (it is an f-string over them)
models.py # ImageCandidate, ArticleGrade
clean.py # clean_news_text, _caps_ratio, _is_valid_url, reliability_reason, effective_score,
# _extract_grade, _strip_md_fence
images.py # _decode_phash, _get_article_images, _fetch_image_candidate,
# _flag_shared_placeholders, _primary_image
service.py # NewsService (run_once/collect_metrics/_store_*/_apply_landing_selection/
# _grade_article/_format_article/_grade_article_full/_resolve_uid) + _get_ai_key
__init__.py re-export must include at minimum: NewsService, ImageCandidate, clean_news_text,
reliability_reason, effective_score, _extract_grade, _get_ai_key, _get_article_images,
_flag_shared_placeholders, _primary_image, _strip_md_fence, and constants LANDING_MIN_SCORE,
LANDING_MAX, UNIQUE_IMAGE_BONUS, THIN_CONTENT_PENALTY; plus re-bind net_guard and httpx as
package attributes.
Load-bearing risk (dictates where _get_ai_key lives). tests/unit/services/news.py
monkeypatches names in the news-module namespace: monkeypatch.setattr(news_mod, "get_setting", ...)
(x10) where news_mod = devplacepy.services.news, and setattr(news_mod.net_guard, "guard_public_url", ...), and reads news_mod.httpx. _get_ai_key resolves get_setting from ITS OWN module globals. To
keep these tests passing with NO test edits, keep _get_ai_key and its from ...database import get_setting in the SAME module the test patches - i.e. keep them in service.py and ensure the test's
monkeypatch.setattr(news_mod, "get_setting", ...) still targets the name _get_ai_key reads. The safe
form: keep _get_ai_key + the get_setting import together in service.py, and have __init__ do
from .service import get_setting re-binding so the package-namespace patch and the resolving namespace
are the same object. Simplest conservative variant: peel off only the genuinely pure modules
(constants.py, models.py, clean.py, images.py) and keep NewsService + _get_ai_key +
get_setting/net_guard/httpx imports in service.py (which __init__ re-exports wholesale). This
removes most of the size while eliminating the namespace hazard.
Other risks:
GRADING_RULES_DESCRIPTIONf-string ordering (constants first)._decode_phashruns viarun_in_executor- pure, safe to relocate toimages.py.- BaseService contract:
run_once/collect_metrics/config_fields/interval_key/default_enabled/__init__(name="news", interval_seconds=3600)stay onNewsService. Methods usingself.log/self.get_configcannot become free functions. _grade_article_fullis pure but called asself._grade_article_full(...); leave it a method (lowest risk).
3.10 services/devii/session.py (962) -> same-name package session/ (extract, do not decompose)
Nature. ONE heavily-shared-mutable-state class DeviiSession (73-851) plus ~150 lines of
stateless constants/pure helpers. Constructed by hub.py:70-85 with an exact 11-positional +
3-keyword arg signature. _parse_topic is imported by tests/unit/services/devii/session.py:6.
Recommendation: extract the stateless text/helpers into a package, keep DeviiSession whole.
A true class decomposition would require threading _lock/_send_lock/_conns/_conn_meta/
agent._messages/self.tools/_pending/_buffer/_turn_epoch across objects and risks the
self.tools[:] aliasing and _turn_epoch interrupt guard. Not worth it.
Target layout (services/devii/session/):
__init__.py # from .core import DeviiSession; from .prompts import _parse_topic (+ optional constants)
prompts.py # DOCS_SYSTEM_PROMPT, DOCS_GREETING, DOCS_TOOLS, TOPIC_CLASSIFIER_PROMPT,
# NON_ADMIN_COST_RULE, LOGIN_REQUEST, BEHAVIOR_HEADER, _parse_topic(), _system_prompt_for()
# (session.py:854-947 + 40 + 67)
_helpers.py # _repair_history(), _now_iso(), _format_offset() (43-65, 950-962)
core.py # class DeviiSession + the numeric constants only it uses (27-31, 39)
This lifts ~150 lines out of the class file with zero behavior risk. __init__.py re-exports
DeviiSession (used by hub.py:15) and _parse_topic (test). prompts.py/_helpers.py import
nothing from core.py (no cycle). All prompt constants are used only inside the class today, so they
move freely.
Optional (secondary, only if further reduction is wanted): a mixin split of DeviiSession into
ConnectionMixin/PromptComposition/TurnControl/SpendTracking/Scheduling operating on one
shared self. Given the density of cross-group self. references (e.g. _run_turn touches
_refresh_tools, _refresh_system_prompt, _docs_topic_gate, _cost_snapshot, _record_spend,
_persist_history, _emit, _turn_epoch), this trades one long file for cross-mixin coupling - do it
only if the primary extraction is not enough.
Invariants that must not break (whichever depth is chosen):
self.toolsmutated in place, never reassigned._refresh_toolsusesself.tools[:] = ...because the same list object is bound intoAgenticController/Agent/react_loop. Reassigning freezes the tool list (virtual tools stop taking effect). Same in-place discipline forself.agent._messagesrewrites (_refresh_system_prompt544-547,reset_conversation357-375,_docs_topic_gate549-577,stop/_repair_history408-426).- Turn-epoch interrupt guard (
_turn_epochat184, incremented incancel_turns:384, checked in_run_turn:451,459,463) - keepspawn_turn/cancel_turns/_run_turnwith the shared_turn_epoch/_turnsstate (MEMORY: devii-browser-and-interrupt; do not regress). __init__positional-arg contract withhub.py:70-85must be preserved exactly.session.pyimportsCATALOGfrom.registry(import-time_assert_confirm_params); the package rename must not introduce a cycle -prompts.py/_helpers.pyimport nothing fromcore.py.from __future__ import annotationsin every extracted module (annotations useX | None).
3.11 services/game/store.py (948) -> subpackage services/game/store/
Nature. The DB/stateful layer of the already-package services/game/ (siblings: economy.py
pure rules - untouched; __init__.py does from . import economy, store; from .store import GameError).
Only store.py needs splitting. Every function funnels through four get_table accessors
(_farms/_plots/_quests/_steals) and _update_farm; the only module-level state is PERK_COLUMN
(built from economy.PERKS at import). The game tables carry no deleted_at (outside
SOFT_DELETE_TABLES) - no soft-delete concerns.
Target layout (convert store.py -> store/ package, keeping path services.game.store):
store/
__init__.py # re-exports EVERY name currently on store.py, incl. private helpers tests call
common.py # GameError, _now, _iso, _parse, _today, _parse_date, _farms, _plots, _quests,
# _steals, _lvl, PERK_COLUMN, _update_farm, last_steal_at, steal_cooldown_remaining
farm.py # get_farm, ensure_farm, _create_plot, get_plots, _plot_at, buy_plot, upgrade_ci, leaderboard
serialize.py # _watered_by, _plot_state, serialize_plot, serialize_farm, _serialize_perks,
# _serialize_legacy, _serialize_quests, _daily_available
quests.py # ensure_quests, advance_quests
actions.py # plant, harvest, water, steal, fertilize, prestige, upgrade_perk, upgrade_legacy,
# claim_daily, claim_quest, _auto_harvest
__init__.py re-export must include the router surface (ensure_farm, serialize_farm, plant,
harvest, buy_plot, upgrade_ci, water, steal, leaderboard, claim_daily, claim_quest,
upgrade_perk, upgrade_legacy, prestige, fertilize, GameError) plus the private symbols the
unit test calls directly: get_farm, get_plots, ensure_quests, advance_quests, _plot_at,
_today. services/game/__init__.py keeps working unchanged.
Importers: routers/game/index.py:18, routers/game/farm.py:11, routers/game/_shared.py:6 (all
from devplacepy.services.game import GameError, store then store.X). No CLI, no main.py.
Risks:
- Re-export private helpers (
_plot_at,_today, +get_farm/get_plots/ensure_quests/advance_quests) or the unit test breaks. No monkeypatch-of-module-name coupling here (unlike news), so the only hazard is a missing re-export. PERK_COLUMNbuilt fromeconomy.PERKSat import must live in the low-level shared module (common.py) imported byserialize.py/actions.py;common.pyimportseconomy, higher modules importcommon+economy(no cycle).- Dense intra-module call graph (
serialize_farm -> _auto_harvest -> advance_quests;actions -> ensure_farm/_update_farm/advance_quests) - group so lower-level modules (common/quests/farm) are imported by higher-level (serialize/actions), not vice-versa. leaderboardlazy importfrom devplacepy.database import get_users_by_uidsatstore.py:593- keep function-local when relocating.
4. SPECIAL CASE - container standalone scripts (files 4 and 5)
services/containers/files/bot.py (2940, COPY bot.py /usr/bin/botje.py) and
services/containers/files/d.py (2337, COPY d.py /usr/bin/d.py) are NOT server backend. They are
standalone single-file programs baked into the ppy image (ppy.Dockerfile:47-48, build context
devplacepy/services/containers/files, invoked python /usr/bin/botje.py). Their only hard
third-party dep is httpx. Nothing in devplacepy/ or tests/ imports either file (confirmed
grep) - they are not on sys.path as modules and have no test coverage. The only breakage surface is
the Docker build's COPY path and the documented invocation.
Binding constraint. A normal package split (like the server-side files) is incompatible with
the single-file COPY + bare-python exec contract: a package leaf COPY'd to /usr/bin/botje.py would
fail at the first cross-module from botpkg... import (no package root on the container's path). So the
in-place package approach used for every other monster does not apply here.
Options (pick per user direction; this plan does not choose for you):
-
(a) Keep as single-file monoliths. Zero Dockerfile/Makefile change; the contract stays intact. Trade-off: the two files remain large and duplicate ~2000 lines of agent code between them (
files/bot.py=files/d.py's agent + a DevPlace XML-RPC bot loop;files/d.py's stealth half is a pure-httpx sibling ofdevplacepy/stealth.py). This is the lowest-risk option. -
(b) Split into a source package + add a build step that re-stitches the single deployed artifact. Two sub-options, each with concrete required edits:
- zipapp: author
files/agent/as a package, build withpython -m zipapp files/agent -m "agent.cli:main" -o build/botje.pyz. Requires changingppy.Dockerfile:47-48COPY source + extension (.pyz), thechmod/lnlines (:52), the invocation in docs (templates/docs/getting-started-vibing.html:128-162) frompython /usr/bin/ botje.pytopython /usr/bin/botje.pyz, and themake ppytarget (Makefile:129-130) to run the bundler beforedocker build. Downside: breaks the documentedcp /usr/bin/botje.py /app/mybot.pycopy-and-edit single-file flow. - concatenation/stitch: a
makerecipe concatenates the package modules (deduped imports, in dependency order) into onebuild/bot.py, which the Dockerfile then COPYs. Keeps the.pyextension andpython /usr/bin/botje.pyintact; users edit generated output, not source. RequiresMakefile:129(add the bundler step + widen the rebuild-trigger set) andppy.Dockerfile:47-48(COPY frombuild/).
- zipapp: author
Note (not a recommendation, for context): the largest issue with these two files is duplication,
not size - files/bot.py and files/d.py share the entire agent (tool framework, react_loop,
CorpusIndex, embedded ChromeStealthClient). If the user later wants these deduplicated rather than
split, that is a different task; this plan records the fact only.
5. Cross-file execution order (recommended)
Order by blast radius (lowest first) so a mistake surfaces on a small surface before the wide ones, and so the widely-imported files are split last against a known-green tree:
services/game/store.py- self-contained subpackage, 3 importers, no cycles. Warm-up.services/devii/actions/catalog.py- pure data, 2 exported symbols, ordering-only risk.schemas.py- pure models, mechanical, wide but simple re-export.services/news.py- watch the monkeypatch namespace; conservative variant recommended.services/devii/session.py- extract stateless helpers only (keep class whole).services/bot/bot.py- mixin lift; keep__init__exporting onlyBotsService.cli.py- package with per-group modules; keep the console-script entry.docs_api.py- large but declarative; handle the three ordering hazards.utils.py- 224 importers, funnel + lazy-import + Jinja-global sensitivity.database.py- 329 importers, the core layer; do it last against a green tree.- Container
files/bot.py/files/d.py- only if the user opts into option (b); otherwise leave as-is per Section 4.
Each step is independent: split one file, run the Section-2.9 validation, commit, move on. Do not batch multiple monster splits into one unvalidated change.
6. Convention alignment (why packages, not _-suffixed files)
This plan uses the project's existing structural idiom, verified against the current tree:
- Router packages (
routers/issues/,routers/admin/,routers/profile/,routers/projects/) are directories whose__init__.pyaggregates leaf modules and re-exports the public symbol, each new file headed# retoor <retoor@molodetz.nl>. Every split above mirrors that. _shared.pyis the sanctioned package-private helper module (already used in eight router packages); it is used here only for genuine cross-leaf construction helpers (docs_api/_shared.py,catalog/_shared.py,cli/_shared.py), never as a dumping ground.- No
_v2/_refactored/_newsuffixes anywhere (forbidden by project rules). The compatibility mechanism is the same-name package + re-export, not a parallel file. services/already contains packages that expose astoresubmodule (services/audit/,services/backup/,services/containers/,services/game/), soservices/news/andservices/game/store/andservices/devii/session/andservices/devii/actions/catalog/all follow precedent.
7. Documentation follow-up (after any split is executed)
Per the project feature workflow, once a split is executed the structural change is documented in the three canonical files - but ONLY if a rule/convention changes. A pure module move that preserves every import path introduces no new architectural rule, so:
README.md: no change (no user-visible/route/env/dep change).AGENTS.md: add a short note under the relevant domain only if the new module boundary is worth navigational guidance (e.g. "docs_api is now a package, one module per API group").CLAUDE.md: no change unless the split establishes a NEW convention (it does not - it follows the existing package idiom).
This section is a reminder, not a task; the plan itself does not edit docs.
8. Summary table - target packages
| Source file | Target | Split style | Key invariant to protect |
|---|---|---|---|
docs_api.py |
docs_api/ (+groups/) |
one module per API group | import-time negotiation pass; group order; _substitute re-export |
database.py |
database/ |
layered domain modules on core.py |
db singleton in core; underscore re-exports; keep lazy imports |
services/bot/bot.py |
mixins in services/bot/ |
cooperative mixins | realism contract; __init__ exports only BotsService; stealth rule |
services/devii/actions/catalog.py |
catalog/ (+_shared.py) |
one module per action domain | ACTIONS order; confirm-param invariant; Param import in dbapi/gateway/game |
schemas.py |
schemas/ |
domain schema modules | CommentItemOut.model_rebuild(); cross-module inheritance order |
cli.py |
cli/ (+_shared.py) |
one module per command group | console-script cli:main; re-export all cmd_*; lazy imports |
utils.py |
utils/ |
layered helper modules | funnel pairing; lazy imports; Jinja-global paths; "auth" cache key |
services/news.py |
services/news/ |
pipeline-stage modules | monkeypatch namespace (_get_ai_key/get_setting); f-string const order |
services/devii/session.py |
session/ |
extract stateless text/helpers | self.tools[:] in-place; _turn_epoch guard; __init__ arg contract |
services/game/store.py |
services/game/store/ |
cohesive store modules | re-export private test helpers; PERK_COLUMN placement |
services/containers/files/bot.py |
SPECIAL (Section 4) | single-file OR build step | COPY-single-file + bare-exec contract |
services/containers/files/d.py |
SPECIAL (Section 4) | single-file OR build step | COPY-single-file + bare-exec contract |