25 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.
- A Tenant Locator Claim is unauthenticated input, not identity. Browser
OriginthenRefererprovides browser-context consistency evidence but is forgeable by non-browser callers. - 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. - Headless clients use
tenantHandle. The target currently has no distinct stable Tenant Code, so this is explicitly the unique System tenantnameunder a case-sensitive^[A-Za-z0-9._-]{2,64}$, operationally immutable public contract. Legacy publictenantNameis rejected rather than aliased. - 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.
- 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.
- Local fallback is enabled only by
yudao.education.tenant-resolution.local-development-enabled, defaultfalse; profiles do not enable it. With the flag true, code-less configured local-host resolution is permitted, while an explicit handle takes precedence. /education/contextis Member-only, obtains the fullLoginUser, and continues deriving IDs only from security and tenant contexts.TenantSecurityWebFilterremains responsible for authenticated missing-tenant, mismatch, and availability checks.- Login-method metadata belongs to Member authentication. EDU-004 removes/deprecates Education
loginMethodsunless a minimal Member-owned interface is first proven necessary. - Exact public business failures use HTTP 200/CommonResult: invalid locator
1005001003/租户识别请求无效; conflict1005001008/租户识别信息冲突; unknown/disabled/expired1005001004/当前租户不可用; all have null data and redacted detail. Success is code 0 and onlytenantIdplusdisplayName. - Retain
TenantCommonApias the generic System-owned seam. EDU-004 makes lookup methods required and adds System-ownedTenantApiImplcontract tests; no Education locator concept enters System. - 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.
- 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. - 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. - 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. - 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.
- Legacy idempotency data is backfilled into the unified table before any later forward cleanup. Legacy tables are preserved during initial adoption.
- 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_menuexists; role assignment is not seeded. - 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.
- The inspected local disposable
postgresdbhad no Flyway history and no Education tables. EDU-005 ran no migration and makes no migration-success claim.
Other established constraints
- 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.
- Verified target branch is feature/education-core-loop and its worktree is dirty. Preserve all existing changes and classify current state at execution time.
- Classify target behavior as committed-and-tested, committed-but-not-runtime-verified, dirty/uncommitted, or absent before scheduling work.
ce02f8ais committed core-loop evidence; native provider/catalog and much of the schema are dirty. - 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.
- 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.
- Treat public tenant resolution as an unauthenticated disclosure surface requiring exact redaction and abuse controls. Separately preserve
TenantSecurityWebFilterauthenticated mismatch checks; the remaining principal issue is Member/UserType enforcement in/education/context. - Treat hostname port and stored-website representation as verified contradictions requiring alignment across implementation, properties, API documentation, System lookup behavior, configuration, and tests.
- 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.
- 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.
- 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.
- 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.
- Do not expose paid/private practice until entitlement semantics and public target contracts are decided.
- 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:
SCALAR_READis the default and authoritative mode. It is the source of real tenant catalog content from the running tiku-backend NestJS service.JAVA_READis conditionally activated only whenyudao.education.catalog-mode=JAVA_READis explicitly set.- Both providers implement the same
CatalogProvider+QuestionCatalogProviderinterfaces;QuestionContentSafetyandQuestionCatalogServiceImplenforce provider-neutral fail-closed rules. - JavaCatalogProvider must not be promoted to default without: (a) runtime-verified V4020 catalog tables, (b) dedicated contract test suite, (c) mapper audit of
TenantUtils.executeIgnoreboundary. - 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
JavaCatalogProviderTestcontract test suite - Add provider-neutral integration test for equivalent safe projections
- Update
CatalogProviderModeJavadoc
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:
- PUBLIC rows (tenant_id=0, scope='PUBLIC') may only reference PUBLIC parents. PUBLIC is a self-contained content tree.
- Tenant-owned rows (tenant_id>0, scope='TENANT_OWNED') may reference PUBLIC rows — this is the primary content-sharing mechanism.
- Tenant-owned rows referencing other-tenant rows is forbidden.
- Composite foreign keys
(tenant_id, parent_id)are NOT used because thetenant_id=0sentinel naturally breaks FK matching for tenant→PUBLIC references. - A catalog row's
tenant_idandscopeare 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
CatalogScopeQuerywith 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:
-
Create, place, publish, and archive are explicit commands with separate System RBAC permissions; clients cannot submit tenant, scope, lifecycle, or actor fields.
-
Tenant authoring always creates
TENANT_OWNEDcontent for the current framework tenant. PUBLIC/platform-curator authoring is not part of this slice. -
Question Content Versions are immutable. Publication changes lifecycle state for the current version and never changes
tenant_id,scope, or content version. -
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.
-
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.
-
Historical
HIDDEN,INACTIVE, andPUBLISHED/is_published=falserows map toARCHIVED; historicalDRAFTrows become non-published; only consistent published rows remainPUBLISHED. -
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.
-
Question Placement means assigning a DRAFT question's
node_id; it does not mean Category CRUD. Theeducation_categorytable has no Question relationship in the current target model. -
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.
-
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.
-
Tenant Content Node authoring is JAVA_READ-only, current-tenant TENANT_OWNED-only, and uses
DRAFT → ACTIVE → ARCHIVEDwith one CASauthoring_versionshared by revisions and transitions. -
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.
-
Content Node permissions are
education:content-node:author,education:content-node:publish, andeducation:content-node:archive; V4100 seeds no permission, menu, role, or role grant. -
The next bounded EDU-010 slice is a JAVA_READ-only current-tenant
TENANT_OWNEDManual 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. -
A Manual Question Collection uses
DRAFT → ACTIVE → ARCHIVEDwith one optimistic authoring version. Only DRAFT metadata and membership may change; ACTIVE collection content and membership are immutable, and ARCHIVED is terminal. -
Membership is an ordered replace-all command available only in DRAFT. Every member must be a current-tenant
TENANT_OWNEDPUBLISHED Question, and duplicate Question IDs are rejected rather than collapsed or upserted. -
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.
-
access_rulesis 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. -
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.
-
Category and Practice Blueprint use the same
DRAFT → ACTIVE → ARCHIVED,authoring_versionCAS, transactional append-only audit, current-tenant ownership, andJAVA_READauthority rules as Content Node and Collection. Student reads require ACTIVE lifecycle plus route availability. -
The six added permissions are
education:category:{author,publish,archive}andeducation: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.
- The import job has exactly five persisted states:
PREVIEW,PENDING,PROCESSING,COMPLETED, andFAILED. - Processing uses atomic claim, fenced lease token, heartbeat, expired-lease recovery, bounded attempts, and duplicate-safe tenant command keys.
- Scanning fails closed. With no configured scanner adapter the result is
UNAVAILABLE, never implicitly clean. - 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.
- 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.
- 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.
- Education may use public Infra APIs but may not depend on Infra DOs, mappers,
ServiceImplclasses, 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
- 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.
- 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.
- Whether legacy assets are migrated, re-uploaded, re-scanned, or retired, and who owns ClamAV/scanner integration.
- Which legacy RLS, triggers, functions, grants, seeds, queue leases, retry behavior, and operational semantics are contractual and need Java/constraint/event/job reproduction.
- Whether the ten required Phase 0 artifacts must be committed files or may remain in reviewed scratch form during discovery.
- Whether a future System-owned immutable Tenant Code or signed bootstrap locator is required beyond the accepted public-handle/existence-disclosure contract.
- 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.