docs: define modular monolith governance standard

This commit is contained in:
leefer
2026-07-29 17:01:08 +08:00
parent 110d4b3421
commit 444f3c1556
3 changed files with 356 additions and 0 deletions
+4
View File
@@ -1,5 +1,9 @@
# Architecture
The normative governance contract is documented in
`docs/governance/architecture-standard.md`. This file describes the currently deployed
shape; the standard defines the target boundaries and the rules applied during migration.
The application intentionally keeps a small deployment footprint: one Python process, one
SQLite database, and a build-free browser client. The internal boundaries are nevertheless
explicit so new features do not bypass account isolation or data-quality rules.
@@ -0,0 +1,75 @@
# ADR 0001: Govern as a Modular Monolith
Status: Accepted
Date: 2026-07-29
## Context
The application is deployed on a LAN NAS as one Docker container with a Python HTTP process,
SQLite, a build-free browser client, scheduled refresh work, external market providers, and
LLM features. Product breadth has grown, while routing, persistence, frontend state, and CSS
remain concentrated in a few large files.
The system may later become internet-facing, gain more features, replace SQLite, or move jobs
to workers. It does not currently have load or team boundaries that justify distributed
services.
## Decision
Retain one deployable application and introduce strict internal modules, ports, adapters,
feature registries, data contracts, repository contracts, and regression gates.
The deployment remains:
```text
one image + one application process + one persistent data volume + port 8765
```
Internal code moves toward:
```text
delivery -> application services -> ports -> infrastructure
```
Compatibility facades permit incremental migration. No feature is rewritten solely to match
the target directory structure.
## Consequences
### Positive
- Current NAS deployment stays simple.
- Refactoring can proceed in reversible stages.
- Feature ownership and account boundaries become visible.
- Data, database, LLM, and job adapters can be replaced later.
- A measured hotspot can be extracted without first untangling business logic.
### Costs
- The transition temporarily contains old and new entrypoints.
- Boundary tests and registries require ongoing maintenance.
- A single process remains a capacity and fault-isolation limit until infrastructure is
deliberately extracted.
## Rejected Alternatives
### Immediate microservices
Rejected because they add network contracts, service discovery, deployment coordination,
distributed tracing, and failure modes before load requires them.
### Full framework rewrite
Rejected because replacing the HTTP and frontend frameworks while moving boundaries would
combine structural and behavioral risk.
### Continue patching flat modules
Rejected because current file size, direct provider creation, global page state, and CSS
override layers already make regressions difficult to isolate.
## Revisit Conditions
Reconsider service extraction when one module has independently measured scaling needs,
requires a separate availability boundary, or needs an independent release lifecycle.
+277
View File
@@ -0,0 +1,277 @@
# Application Architecture Standard
Status: Accepted
Date: 2026-07-29
## 1. Objective
The application remains a deployable modular monolith while its internal boundaries are made
explicit. Governance must preserve current functionality, account isolation, visual behavior,
and Docker simplicity. A structural refactor is not permission to change product behavior.
## 2. Target Topology
```text
browser
-> frontend shared API client
-> backend HTTP controllers
-> feature application services
-> repositories / DataGateway / LLMGateway
-> SQLite / market providers / model providers
background jobs
-> the same feature application services
-> the same repositories and gateways
```
The browser and jobs are two delivery mechanisms. Neither owns business rules.
## 3. Target Source Layout
```text
frontend/
shared/
tokens.css
shell.js
api.js
state.js
components/
pages/<feature>/
page.js
desktop.css
mobile.css
pages.config.js
backend/
bootstrap/
http/
router.py
auth.py
errors.py
schemas/
features/<feature>/
routes.py
service.py
repository.py
schemas.py
data/
gateway.py
policy.py
contracts.py
providers/
database/
connection.py
migrations/
repositories/
jobs/
llm/
gateway.py
usage.py
models.py
prompts/
features_config.py
tests/
data/
server.py
```
The migration may use compatibility facades. Old modules are removed only after all callers
move and regression gates pass.
## 4. Dependency Direction
Allowed direction:
```text
HTTP / jobs -> feature services -> ports -> infrastructure adapters
frontend pages -> shared components/state/API -> backend API
```
Forbidden dependencies:
1. Frontend code must not call external market or LLM providers directly.
2. HTTP controllers must not contain scoring, screening, divination, or persistence logic.
3. Feature services must not instantiate Tushare, iFinD, Eastmoney, Tencent, SQLite, or LLM
clients directly.
4. Provider adapters must not import feature services.
5. Repositories must not call external providers.
6. Features must not read another feature's tables directly; cross-feature work goes through
an application service or declared read model.
7. Background jobs must call the same services used by HTTP flows rather than duplicate
calculations.
## 5. Feature Ownership
Each feature owns its routes, application service, schemas, repository interface, page module,
tests, and documentation. Initial feature IDs are defined in `features.config.json` during
Stage 04.
Shared code is allowed only when at least two features use the same stable behavior. A shared
module must not branch on page names or feature IDs to emulate unrelated components.
## 6. HTTP Contract
- Public application APIs use `/api/<resource>` and UTF-8 JSON.
- Controllers validate transport input, authorize the request, call one application service,
and serialize the result.
- Dates exposed to users use `YYYY-MM-DD`; provider-specific compact dates stay inside
adapters.
- Datetimes include an explicit timezone. Market time is interpreted as Asia/Shanghai.
- Successful collections use `items`; pagination uses `page`, `page_size`, and `total` when
required.
- Errors use a stable `code`, a user-safe `message`, and a request correlation ID. Provider
credentials and raw response bodies never appear in browser errors.
- Existing response shapes remain compatible until a versioned migration is approved.
- Backend authorization is authoritative. Hidden frontend controls are not a security rule.
## 7. Data Contract
Every calculation field declares:
- canonical field ID and Chinese display label;
- entity and frequency;
- type, unit, precision, timezone, and adjustment mode;
- unique primary source and permitted fallback sources;
- display-only or calculation-eligible status;
- freshness and completeness thresholds;
- missing-value behavior;
- point-in-time availability rules;
- owning dataset and persistence location.
Missing calculation data fails closed. A strategy may not silently replace a required field
with a proxy and continue under the original strategy name. Display fallbacks cannot enter
screening, scoring, backtesting, sentiment, or Wentian calculations unless explicitly approved
for that canonical field.
One OHLC bar or factor history must not mix providers. Source changes are recorded with the
stored observation and invalidate incompatible cached calculations.
## 8. Market Provider Policy
- Tushare is the default deterministic source for master data, calendar, daily bars,
fundamentals, valuations, industry data, lists, and post-close datasets.
- iFinD is the default source for licensed realtime snapshots, intraday data, dynamic auction
observations, charts, and approved event enrichment.
- Eastmoney and Tencent public endpoints are display or observation fallbacks only unless a
field contract explicitly promotes them.
- All provider traffic passes through `DataGateway`; provider classes only translate their own
protocol into canonical contracts.
- Retries, timeouts, quotas, cache TTL, circuit state, and provenance are centralized.
## 9. Persistence Standard
- SQLite WAL remains the current deployment database.
- Schema changes use ordered migration files with an immutable version ID.
- A migration is transactional where SQLite permits it and must be safe to run once.
- Destructive migrations require a verified backup and explicit acceptance.
- User-owned tables include `user_id`, a foreign key, an ownership index, and cross-account
tests.
- Repositories return domain-shaped records; controllers never execute SQL.
- Financial and research data preserve announcement timestamps to prevent look-ahead use.
- A future PostgreSQL adapter must satisfy the same repository contracts.
## 10. Background Jobs
- Jobs declare an ID, schedule, input date policy, dependencies, lock key, timeout, retry
policy, and idempotency key.
- Job runs persist start, completion, failure code, retry count, source coverage, and output
version.
- A process restart must not duplicate a completed post-close calculation.
- Exceptions are logged and surfaced in system management; they are never silently discarded.
- The current in-process runner may remain, but jobs cannot depend on thread-local request
context. This permits later extraction to a worker without changing business services.
## 11. LLM Standard
All model calls pass through `LLMGateway`, which owns:
- membership authorization and feature availability;
- quota reservation, settlement, and daily limits;
- model-pool selection and fallback;
- timeout, cancellation, retry, and streaming protocol;
- prompt version, model, latency, token usage, and failure audit;
- removal of secrets and engineering details from user-visible errors.
Skill evidence, source material, and data profiles remain separate from reusable prompt
templates. Deterministic calculations happen before the LLM call and are not delegated to the
model.
## 12. Frontend Standard
- `shared/api.js` is the only browser request exit.
- `shared/shell.js` owns sidebar, topbar, market summary, status bar, global dialogs, and page
mounting.
- `pages.config.js` owns navigation metadata and page loading, not authorization truth.
- A page module fetches data, owns page-local state, and composes shared components.
- Global mutable page state and cross-page DOM queries are prohibited after migration.
- Tables use shared shells with page-owned column schemas and formatters.
- Opening, closing, focus management, and feedback for dialogs use one dialog service.
## 13. CSS and Design Tokens
The cascade order is fixed:
```text
tokens -> reset/base -> shell/layout -> shared components -> page styles -> theme overrides
```
- Literal colors, font sizes, and spacing are introduced through tokens first.
- Page styles are scoped to the page root and cannot redefine the shell or another page.
- A later file cannot be used indefinitely as a patch layer for an earlier file.
- Shared components have one authoritative definition.
- CSS is removed only after selector/reference scanning, light/dark screenshots, and full
browser regression.
## 14. Mobile Standard
Mobile shares tokens, data, permissions, components, and state with desktop, but it may use a
different composition and interaction model.
- Shared mobile shell rules live in `shared/mobile-shell.css`.
- Each complex page may own `mobile.css` and a mobile view renderer.
- One global all-page `mobile.css` override pile is prohibited.
- Desktop tables may become summary lists and detail views on mobile.
- Mobile acceptance is performed independently at 390x844 and 430x932.
- Mobile changes must not alter approved desktop geometry.
## 15. Configuration and Secrets
- Environment variables seed deployment configuration; encrypted system settings hold managed
runtime credentials.
- Tokens, API keys, refresh tokens, model secrets, and encryption keys never enter Git, logs,
API responses, or browser storage.
- Feature flags, page metadata, field contracts, and permissions are versioned configuration,
not ad hoc conditionals.
## 16. Observability
External calls record provider, operation, elapsed time, cache result, freshness, normalized
error code, and correlation ID. Logs must not contain secrets or personal birth data.
Health checks distinguish process health, database health, provider degradation, background
job health, and model availability. Provider degradation does not make the process health
endpoint fail unless the application itself cannot serve stored data.
## 17. Change and Release Contract
Every governance phase must:
1. begin from a clean Git worktree;
2. preserve the Stage 01 baseline contract;
3. include migration or compatibility tests for changed boundaries;
4. pass `python tools/verify_baseline.py`;
5. run Playwright at phase boundaries affecting runtime or frontend behavior;
6. document residual risk;
7. create and push a dedicated rollback commit.
No phase combines framework replacement, visual redesign, and business-rule changes.
## 18. Public-Internet Evolution
The modular monolith remains the default. Reverse proxy/TLS, PostgreSQL, Redis, durable jobs,
central secrets, monitoring, and rate limiting can replace infrastructure adapters later.
Feature services and frontend contracts must not depend on whether those adapters are local or
remote. A feature is extracted into a separate service only when measured load, independent
release needs, or fault isolation justifies the operational cost.