# 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.