Files
ruoyi-vue-pro/docs/education/migration/07-decisions.md

26 KiB

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.

  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.

  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. Protected lifecycle transition tokens rely on PostgreSQL object ownership separation: an explicit Flyway owner role creates V4080/V4100/V4110 token tables, while the runtime master datasource role must neither own nor write them. Local/dev configuration requires explicit FLYWAY_USER/FLYWAY_PASSWORD; startup fails closed when any protected table is missing, runtime-owned, inherited through role membership, or runtime-writable. Deployment automation provisions roles and grants outside Flyway because migrations cannot safely create shared login roles or know credential policy.

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

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

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