190 lines
25 KiB
Markdown
190 lines
25 KiB
Markdown
# Architecture and Product Decisions
|
|
|
|
> Phase 0 static assessment generated on 2026-07-29. No build, test, application startup, PostgreSQL connection, or Flyway migration was executed during this assessment.
|
|
|
|
## Decisions and constraints established by static evidence
|
|
|
|
### EDU-003 accepted decision — tenant resolution and student principal
|
|
|
|
The durable rationale, threat model, exact wire contract, compatibility policy, and EDU-004 test matrix are recorded in [`issues/EDU-003-tenant-resolution-decision.md`](issues/EDU-003-tenant-resolution-decision.md).
|
|
|
|
1. A **Tenant Locator Claim** is unauthenticated input, not identity. Browser `Origin` then `Referer` provides browser-context consistency evidence but is forgeable by non-browser callers.
|
|
2. A supplied hostname may only confirm browser-context evidence; disagreement is a conflict. Preserving browser headers or rejecting forwarding-header forgery does not authenticate `Origin`/`Referer`.
|
|
3. Headless clients use `tenantHandle`. The target currently has no distinct stable Tenant Code, so this is explicitly the unique System tenant `name` under a case-sensitive `^[A-Za-z0-9._-]{2,64}$`, operationally immutable public contract. Legacy public `tenantName` is rejected rather than aliased.
|
|
4. Public resolution discloses tenant existence on success. The guarantee is only that unknown, disabled, and expired tenants share one response. Reuse public throttling/ingress controls and emit structured probing metrics; spoof-resistant deployments require a future signed/authenticated locator.
|
|
5. Host identity is lowercase, trimmed, trailing-dot-free, bracket-free for IPv6, and port-independent. Canonical System website entries for this resolver are host-only; non-canonical scheme/path/port entries do not match and require configuration correction or a separately scoped Flyway/data ticket.
|
|
6. Local fallback is enabled only by `yudao.education.tenant-resolution.local-development-enabled`, default `false`; profiles do not enable it. With the flag true, code-less configured local-host resolution is permitted, while an explicit handle takes precedence.
|
|
7. `/education/context` is Member-only, obtains the full `LoginUser`, and continues deriving IDs only from security and tenant contexts. `TenantSecurityWebFilter` remains responsible for authenticated missing-tenant, mismatch, and availability checks.
|
|
8. Login-method metadata belongs to Member authentication. EDU-004 removes/deprecates Education `loginMethods` unless a minimal Member-owned interface is first proven necessary.
|
|
9. Exact public business failures use HTTP 200/CommonResult: invalid locator `1005001003` / `租户识别请求无效`; conflict `1005001008` / `租户识别信息冲突`; unknown/disabled/expired `1005001004` / `当前租户不可用`; all have null data and redacted detail. Success is code 0 and only `tenantId` plus `displayName`.
|
|
10. Retain `TenantCommonApi` as the generic System-owned seam. EDU-004 makes lookup methods required and adds System-owned `TenantApiImpl` contract tests; no Education locator concept enters System.
|
|
11. EDU-003 made no production, database, or Flyway change; verification is static only.
|
|
|
|
### EDU-005 accepted decision — PostgreSQL/Flyway takeover
|
|
|
|
The durable artifact classification, adoption matrix, version allocation, backfill policy, documentation corrections, and real-PostgreSQL verification gates are recorded in [`issues/EDU-005-flyway-takeover-decision.md`](issues/EDU-005-flyway-takeover-decision.md).
|
|
|
|
1. Education schema delivery is exclusively module-owned PostgreSQL Flyway under `yudao-module-education/src/main/resources/db/migration/education/`; root PostgreSQL scripts are manual bootstrap/design history and MySQL scripts are obsolete archives.
|
|
2. V4010 (`SELECT 1`) and V4020 (native catalog) remain byte-for-byte frozen because execution outside the inspected environment is unverified. The next planned project-wide version is V4030, subject to a fresh version scan at implementation time.
|
|
3. V4030 owns the final Practice core-loop schema, including sessions/questions, reports/details, wrong questions, favorites, and unified `education_idempotency`. Fresh schema does not create legacy answer/submit idempotency tables.
|
|
4. Existing manually bootstrapped databases require explicit schema comparison and adoption. A verified V4020-equivalent catalog may use an environment-specific 4020 baseline; incompatible environments require a higher-version correction, never falsified history.
|
|
5. Legacy idempotency data is backfilled into the unified table before any later forward cleanup. Legacy tables are preserved during initial adoption.
|
|
6. The earlier V4040 capability-seed plan was superseded after V4040 was allocated to the submit-claim lease. V4080/V4090 conditionally seed the approved capability plus question author/classify/publish/archive permissions when `system_menu` exists; role assignment is not seeded.
|
|
7. Docker/manual SQL initialization and MySQL rollback runbooks must be removed from active operations when EDU-006 lands. EDU-016's temporary test bridge becomes Flyway-driven after equivalence is proven.
|
|
8. The inspected local disposable `postgresdb` had no Flyway history and no Education tables. EDU-005 ran no migration and makes no migration-success claim.
|
|
|
|
### Other established constraints
|
|
|
|
1. Verified source provenance is limited: /Users/tiku1/code/tiku-backend has only main and origin/main at 033701a785c7012139e7f86995eea6041225592e; no local or remote feature/education-core-loop ref exists. Use main/033701a provisionally only, or obtain explicit approval for that baseline.
|
|
2. Verified target branch is feature/education-core-loop and its worktree is dirty. Preserve all existing changes and classify current state at execution time.
|
|
3. Classify target behavior as committed-and-tested, committed-but-not-runtime-verified, dirty/uncommitted, or absent before scheduling work. ce02f8a is committed core-loop evidence; native provider/catalog and much of the schema are dirty.
|
|
4. Verified V4010 is SELECT 1 and V4020 is native catalog only. Practice/report/idempotency/wrong/favorite DDL in sql/postgresql/education is untracked/manual and not proven active Flyway. Convert required DDL to immutable module-owned PostgreSQL Flyway migrations before claiming schema delivery; never modify published migrations.
|
|
5. Keep PostgreSQL/Flyway as the only new schema delivery mechanism. Historical MySQL files and root SQL are not active delivery unless explicitly labeled archival/manual and removed from operational runbooks.
|
|
6. Treat public tenant resolution as an unauthenticated disclosure surface requiring exact redaction and abuse controls. Separately preserve `TenantSecurityWebFilter` authenticated mismatch checks; the remaining principal issue is Member/UserType enforcement in `/education/context`.
|
|
7. Treat hostname port and stored-website representation as verified contradictions requiring alignment across implementation, properties, API documentation, System lookup behavior, configuration, and tests.
|
|
8. Treat native catalog isolation as an intentional TenantUtils.executeIgnore/manual-scope boundary, not evidence of a current leak. Make mapper audit and tenant/scope-consistent graph constraints concrete blockers before authoring.
|
|
9. Make the first slice provider-neutral or cover both providers because SCALAR_READ is the verified default and Java provider is conditional. The slice must include fresh browsing and persisted session restoration, with a common option-schema contract and fail-closed behavior.
|
|
10. Do not treat submit idempotency as complete: check-then-insert is not an atomic claim. Reserve keys atomically and define crash recovery before the submit slice.
|
|
11. Add first-class Auth/Profile/extended Learning, tenant appearance/integrations/secrets/codes, and granular platform-admin capability groups so every required legacy cluster has a disposition.
|
|
12. Do not expose paid/private practice until entitlement semantics and public target contracts are decided.
|
|
13. No tests, builds, PostgreSQL connections, Flyway execution, or runtime verification were performed by the Phase 0 assessment unless a later ticket explicitly records otherwise.
|
|
|
|
## Resolved decisions (post-Phase 0)
|
|
|
|
### Provider-authority decision (resolved 2026-07-30)
|
|
|
|
**Decision:** Coexistence with Scalar as the authoritative primary. ScalarCatalogProvider (HTTP data source, `SCALAR_READ`) is the authoritative primary for catalog read operations. JavaCatalogProvider (PostgreSQL direct, `JAVA_READ`) is the conditionally-activated secondary for tenants that have completed a catalog data migration into native PostgreSQL tables.
|
|
|
|
**Rules:**
|
|
1. `SCALAR_READ` is the default and authoritative mode. It is the source of real tenant catalog content from the running tiku-backend NestJS service.
|
|
2. `JAVA_READ` is conditionally activated only when `yudao.education.catalog-mode=JAVA_READ` is explicitly set.
|
|
3. Both providers implement the same `CatalogProvider` + `QuestionCatalogProvider` interfaces; `QuestionContentSafety` and `QuestionCatalogServiceImpl` enforce provider-neutral fail-closed rules.
|
|
4. JavaCatalogProvider must not be promoted to default without: (a) runtime-verified V4020 catalog tables, (b) dedicated contract test suite, (c) mapper audit of `TenantUtils.executeIgnore` boundary.
|
|
5. Provider mode is currently global; per-tenant override is a future concern.
|
|
|
|
**Rationale:** Scalar is the running production data source with 1084 lines of dedicated tests; Java native catalog is dirty, uncommitted, runtime-unverified. The code already implements coexistence through the provider interface — this decision formalizes the existing architecture.
|
|
|
|
**Actions:**
|
|
- Document as ADR in `docs/education/migration/decisions/provider-authority.md`
|
|
- Add `JavaCatalogProviderTest` contract test suite
|
|
- Add provider-neutral integration test for equivalent safe projections
|
|
- Update `CatalogProviderMode` Javadoc
|
|
|
|
**Evidence:** Multi-agent code audit of ScalarCatalogProvider (1084 lines of tests, 13 catalog endpoints, comprehensive error handling), JavaCatalogProvider (0 dedicated tests, `TenantUtils.executeIgnore` pattern, dirty V4020), provider switch mechanism.
|
|
|
|
### PUBLIC graph-semantics decision (resolved 2026-07-30)
|
|
|
|
**Decision:** Combination enforcement via database triggers, immutable ownership scope, and application-layer validation.
|
|
|
|
**Rules:**
|
|
1. **PUBLIC rows (tenant_id=0, scope='PUBLIC')** may only reference PUBLIC parents. PUBLIC is a self-contained content tree.
|
|
2. **Tenant-owned rows (tenant_id>0, scope='TENANT_OWNED')** may reference PUBLIC rows — this is the primary content-sharing mechanism.
|
|
3. **Tenant-owned rows referencing other-tenant rows** is forbidden.
|
|
4. Composite foreign keys `(tenant_id, parent_id)` are NOT used because the `tenant_id=0` sentinel naturally breaks FK matching for tenant→PUBLIC references.
|
|
5. A catalog row's `tenant_id` and `scope` are immutable after insert. Moving content between tenant-owned and PUBLIC scope requires an explicit copy/adoption workflow; publication changes lifecycle, not ownership.
|
|
|
|
**Enforcement strategy (combination):**
|
|
- **Database triggers** (primary guard): `education_check_reference_scope()` function with BEFORE INSERT/UPDATE triggers on every FK column of every catalog table. SECURITY DEFINER, fail-closed on violation.
|
|
- **Ownership immutability triggers** on all 11 catalog tables prevent parent mutations from invalidating existing references and remove the concurrent child-insert/parent-move race.
|
|
- **Application-layer validation** in service write paths (first line of defense).
|
|
- **Migration pre-validation** DO block that fails if existing data contains cross-scope violations.
|
|
|
|
**Flyway delivery:**
|
|
- V4070: Create reference and ownership-scope guard functions, attach triggers, then run historical pre-validation while migration DDL locks prevent concurrent writes.
|
|
|
|
**Rationale:** Legacy Supabase enforced tenant graph integrity via RLS policies and staged composite FK migration. The target's `tenant_id=0` sentinel breaks composite FK for tenant→PUBLIC references — only triggers handle all three reference scenarios correctly. Application-only enforcement is insufficient per fail-closed requirements (GOAL.md §2.3). Implementation review rejected redundant `UNIQUE (tenant_id, id)` constraints because V4020 IDs are already globally unique primary keys and no current query, conflict target, or composite FK consumes those indexes.
|
|
|
|
**Actions:**
|
|
- Done: create V4070 (19 reference guards, 11 immutable-scope guards, and historical pre-validation) via `flyway-postgresql`
|
|
- Add application-layer scope validation in catalog write services
|
|
- Extend `CatalogScopeQuery` with explicit tenant_id/disallowed-scope predicates
|
|
- Add focused integration tests for all reference scenarios
|
|
|
|
**Evidence:** Schema graph analysis of V4020 (11 catalog tables, all single-column FKs, no composite tenant enforcement), legacy RLS/trigger audit (Supabase RLS + staged composite FK migration), current CHECK constraints provide row-level but not cross-row enforcement.
|
|
|
|
### Native question authoring authority and lifecycle (resolved 2026-07-30)
|
|
|
|
**Decision:** Local tenant question authoring is available only under explicit `JAVA_READ` authority and fails before persistence access in `SCALAR_READ`. New questions are `DRAFT`; the current command surface permits only `DRAFT → PUBLISHED → ARCHIVED`, with no restore or retire command.
|
|
|
|
**Rules:**
|
|
1. Create, place, publish, and archive are explicit commands with separate System RBAC permissions; clients cannot submit tenant, scope, lifecycle, or actor fields.
|
|
2. Tenant authoring always creates `TENANT_OWNED` content for the current framework tenant. PUBLIC/platform-curator authoring is not part of this slice.
|
|
3. Question Content Versions are immutable. Publication changes lifecycle state for the current version and never changes `tenant_id`, `scope`, or content version.
|
|
4. Education appends a lifecycle audit in the same transaction as each state transition. The asynchronous platform operation log remains supplementary, not proof of atomic publication.
|
|
5. Direct question visibility follows the question's Publication State. Entry, node, and collection availability gates their own discovery routes and does not mutate the question lifecycle or historical practice snapshots.
|
|
6. Historical `HIDDEN`, `INACTIVE`, and `PUBLISHED/is_published=false` rows map to `ARCHIVED`; historical `DRAFT` rows become non-published; only consistent published rows remain `PUBLISHED`.
|
|
7. V4080 adds Question Version → Question as the twentieth guarded catalog edge. A version must have exactly the same tenant and scope as its question, and the version table becomes the twelfth catalog table whose ownership cannot change after insert.
|
|
8. Question Placement means assigning a DRAFT question's `node_id`; it does not mean Category CRUD. The `education_category` table has no Question relationship in the current target model.
|
|
9. Placement targets must be visible, active, selectable PUBLIC or same-tenant Content Nodes. Placement has an independent optimistic version, becomes immutable after publication, and publication CAS includes that version.
|
|
10. Node-route student visibility is evaluated atomically in the Question/Content Node query. Node availability gates that route but does not change direct question visibility or historical snapshots.
|
|
11. Tenant Content Node authoring is JAVA_READ-only, current-tenant TENANT_OWNED-only, and uses `DRAFT → ACTIVE → ARCHIVED` with one CAS `authoring_version` shared by revisions and transitions.
|
|
12. Content Node activation requires a structurally available entry and parent. Student discovery and Question Placement treat only ACTIVE nodes as available. Content Node lifecycle audit is transactional and append-only.
|
|
13. Content Node permissions are `education:content-node:author`, `education:content-node:publish`, and `education:content-node:archive`; V4100 seeds no permission, menu, role, or role grant.
|
|
14. The next bounded EDU-010 slice is a JAVA_READ-only current-tenant `TENANT_OWNED` Manual Question Collection attached to one existing ACTIVE, visible Content Node. Content Entry creation is excluded, so the target node and its structurally available Content Entry must be pre-provisioned.
|
|
15. A Manual Question Collection uses `DRAFT → ACTIVE → ARCHIVED` with one optimistic authoring version. Only DRAFT metadata and membership may change; ACTIVE collection content and membership are immutable, and ARCHIVED is terminal.
|
|
16. Membership is an ordered replace-all command available only in DRAFT. Every member must be a current-tenant `TENANT_OWNED` PUBLISHED Question, and duplicate Question IDs are rejected rather than collapsed or upserted.
|
|
17. Activation exposes the collection route only when the collection and its existing Content Node are available. Archiving closes only collection-route discovery; it does not alter direct Question visibility or historical Practice Question Snapshots.
|
|
18. `access_rules` is descriptive reserved metadata in this slice, not entitlement enforcement. PUBLIC Question curation, dynamic filters, paid/private access, Category, Practice Blueprint, and Content Entry creation are excluded.
|
|
|
|
19. V4120 closes the bounded EDU-010 Category and Practice Blueprint scope. Category is a standalone Subject-scoped catalog aggregate, not Question classification. Blueprint authoring is limited to exactly one current-tenant NODE or active Manual COLLECTION target; target ownership, route, and counts are server-derived.
|
|
20. Category and Practice Blueprint use the same `DRAFT → ACTIVE → ARCHIVED`, `authoring_version` CAS, transactional append-only audit, current-tenant ownership, and `JAVA_READ` authority rules as Content Node and Collection. Student reads require ACTIVE lifecycle plus route availability.
|
|
21. The six added permissions are `education:category:{author,publish,archive}` and `education:practice-blueprint:{author,publish,archive}`. V4120 conditionally seeds permission rows at IDs 6809-6814, fails on conflicting IDs, and assigns no roles.
|
|
|
|
**Permission and data-scope matrix:**
|
|
|
|
| Actor capability | Permission | Allowed rows | Data scope |
|
|
|---|---|---|---|
|
|
| Author | `education:question:author` | Create current-tenant `TENANT_OWNED` drafts | Current tenant; no request-supplied owner or tenant |
|
|
| Classifier | `education:question:classify` | Place or re-place current-tenant `TENANT_OWNED` drafts on allowed Content Nodes | Tenant-wide shared catalog asset; PUBLIC/same-tenant targets only |
|
|
| Publisher | `education:question:publish` | Current-tenant `TENANT_OWNED` drafts | Tenant-wide shared catalog asset; department/self DataPermission does not expand access |
|
|
| Archiver | `education:question:archive` | Current-tenant `TENANT_OWNED` published questions | Tenant-wide shared catalog asset; department/self DataPermission does not expand access |
|
|
| Content Node author | `education:content-node:author` | Create/revise current-tenant TENANT_OWNED drafts | Current tenant; structural entry/parent validation |
|
|
| Content Node publisher | `education:content-node:publish` | Activate current-tenant TENANT_OWNED drafts | Tenant-wide shared catalog asset; authoring-version CAS |
|
|
| Content Node archiver | `education:content-node:archive` | Archive current-tenant TENANT_OWNED active nodes | Tenant-wide shared catalog asset; authoring-version CAS |
|
|
| Collection author | `education:collection:author` | Create/revise current-tenant TENANT_OWNED Manual Question Collection drafts and replace ordered membership | Current tenant; DRAFT-only; existing ACTIVE node and PUBLISHED tenant questions |
|
|
| Collection publisher | `education:collection:publish` | Activate current-tenant TENANT_OWNED Manual Question Collection drafts | Tenant-wide shared catalog asset; authoring-version CAS |
|
|
| Collection archiver | `education:collection:archive` | Archive current-tenant TENANT_OWNED active Manual Question Collections | Tenant-wide shared catalog asset; authoring-version CAS |
|
|
| Category author | `education:category:author` | Create/revise current-tenant TENANT_OWNED Category drafts | Current tenant; active PUBLIC or same-tenant Subject reference |
|
|
| Category publisher | `education:category:publish` | Activate current-tenant TENANT_OWNED Category drafts | Tenant-wide shared catalog asset; authoring-version CAS |
|
|
| Category archiver | `education:category:archive` | Archive current-tenant TENANT_OWNED active Categories | Tenant-wide shared catalog asset; authoring-version CAS |
|
|
| Practice Blueprint author | `education:practice-blueprint:author` | Create/revise bounded NODE or COLLECTION blueprint drafts | Current tenant; target ownership and counts derived server-side |
|
|
| Practice Blueprint publisher | `education:practice-blueprint:publish` | Activate current-tenant TENANT_OWNED blueprint drafts | Tenant-wide shared catalog asset; target revalidation and CAS |
|
|
| Practice Blueprint archiver | `education:practice-blueprint:archive` | Archive current-tenant TENANT_OWNED active blueprints | Tenant-wide shared catalog asset; authoring-version CAS |
|
|
| Platform curator | none in this slice | No PUBLIC writes through these endpoints | Fail closed |
|
|
|
|
V4080/V4090 conditionally seed the five Education permissions (`education:capability` plus author/classify/publish/archive). V4110 conditionally seeds the collection author/publish/archive permissions at IDs 6806-6808. Seeds run only when `system_menu` exists, fail when a fixed ID is occupied by a different permission, and deliberately assign no role.
|
|
|
|
**Rationale:** A successful PostgreSQL write while Scalar remains authoritative would be student-invisible. Department/self DataPermission has no truthful meaning for tenant-wide shared catalog rows without a separate ownership model, so action permissions plus framework tenant isolation are explicit rather than applying a misleading annotation.
|
|
|
|
**ADR:** `docs/adr/0001-native-question-authoring-authority.md`.
|
|
|
|
The accepted Manual Question Collection slice does not warrant a separate ADR: it applies the already-recorded native-authority, tenant-ownership, lifecycle, optimistic-concurrency, route-gating, and snapshot-preservation decisions to one narrower aggregate, without adding a hard-to-reverse architectural trade-off.
|
|
|
|
### EDU-011 bounded import/export-assets capability (resolved 2026-07-31)
|
|
|
|
**Decision:** Education owns tenant import asset metadata and durable import-job business state. Infra remains the owner of generic private-file/platform facilities and is consumed only through public APIs.
|
|
|
|
1. The import job has exactly five persisted states: `PREVIEW`, `PENDING`, `PROCESSING`, `COMPLETED`, and `FAILED`.
|
|
2. Processing uses atomic claim, fenced lease token, heartbeat, expired-lease recovery, bounded attempts, and duplicate-safe tenant command keys.
|
|
3. Scanning fails closed. With no configured scanner adapter the result is `UNAVAILABLE`, never implicitly clean.
|
|
4. CSV/XLSX may produce metadata-only preview when no parser is available. Such a preview is not executable; execute requires a clean scan and explicit executable parsed content.
|
|
5. Export scope is request redaction policy only: answers and private fields must be excluded. No generated export file, export worker, or export-job persistence is delivered.
|
|
6. V4130 is the only EDU-011 migration. Production scanner integration, full parser delivery, retention/deletion automation, dead-letter/operator tooling, partial-row reporting, and legacy asset migration remain deferred.
|
|
7. Education may use public Infra APIs but may not depend on Infra DOs, mappers, `ServiceImpl` classes, or private implementation packages.
|
|
|
|
The bounded slice does not require a separate ADR: it keeps domain job state with Education and applies the repository's established public-module-boundary rule without introducing a new platform abstraction.
|
|
|
|
## Unresolved decisions
|
|
|
|
1. Which Auth/Profile/extended Learning semantics are replaced by Member/System/Infra versus Education-owned, including vocabulary, leaderboard, stats, trend, feedback, exam dates, notifications, points, and badges.
|
|
2. Whether tenant appearance, domains, payment accounts, auth providers, secrets, activation codes, coupons, integrations, marketing, public-bank grants, and sync are in scope or explicitly retired.
|
|
3. Whether legacy assets are migrated, re-uploaded, re-scanned, or retired, and who owns ClamAV/scanner integration.
|
|
4. Which legacy RLS, triggers, functions, grants, seeds, queue leases, retry behavior, and operational semantics are contractual and need Java/constraint/event/job reproduction.
|
|
5. Whether the ten required Phase 0 artifacts must be committed files or may remain in reviewed scratch form during discovery.
|
|
6. Whether a future System-owned immutable Tenant Code or signed bootstrap locator is required beyond the accepted public-handle/existence-disclosure contract.
|
|
7. Which shared environments already carry V4010/V4020 or manual equivalents and what explicit baseline/adoption record each requires. The root `sql/postgresql/education/` classification and the module-owned Flyway authority are resolved; disposable PostgreSQL execution evidence exists, but no shared-environment migration is claimed.
|
|
|
|
## Decision rule
|
|
|
|
Questions answerable from code, Git history, configuration, tests, or documentation must be investigated. Only genuine product choices should be escalated. Hard-to-reverse decisions should become ADRs before dependent implementation begins.
|