feat(education): enforce catalog graph scope
This commit is contained in:
@@ -49,18 +49,70 @@ The durable artifact classification, adoption matrix, version allocation, backfi
|
||||
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.
|
||||
|
||||
## Unresolved decisions
|
||||
|
||||
1. The valid option schema for each question type, including whether absent options are legal; whether malformed published content is omitted or produces a controlled source failure.
|
||||
2. Whether PUBLIC tenant_id=0 rows may reference only PUBLIC parents, whether tenant-owned rows may reference global rows, and the precise composite constraint/trigger strategy.
|
||||
3. Whether untracked /Users/tiku1/code/ruoyi-vue-pro/sql/postgresql/education files are intended for promotion into Flyway or are design/manual artifacts.
|
||||
4. Whether V4010/V4020 or any manual core-loop DDL has ever run successfully in PostgreSQL; no runtime migration evidence exists.
|
||||
5. 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.
|
||||
6. 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.
|
||||
7. Whether legacy assets are migrated, re-uploaded, re-scanned, or retired, and who owns ClamAV/scanner integration.
|
||||
8. Which legacy RLS, triggers, functions, grants, seeds, queue leases, retry behavior, and operational semantics are contractual and need Java/constraint/event/job reproduction.
|
||||
9. Whether the ten required Phase 0 artifacts must be committed files or may remain in reviewed scratch form during discovery.
|
||||
10. Whether a future System-owned immutable Tenant Code or signed bootstrap locator is required beyond the accepted public-handle/existence-disclosure contract.
|
||||
2. Whether untracked /Users/tiku1/code/ruoyi-vue-pro/sql/postgresql/education files are intended for promotion into Flyway or are design/manual artifacts.
|
||||
3. Whether V4010/V4020 or any manual core-loop DDL has ever run successfully in PostgreSQL; no runtime migration evidence exists.
|
||||
4. 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.
|
||||
5. 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.
|
||||
6. Whether legacy assets are migrated, re-uploaded, re-scanned, or retired, and who owns ClamAV/scanner integration.
|
||||
7. Which legacy RLS, triggers, functions, grants, seeds, queue leases, retry behavior, and operational semantics are contractual and need Java/constraint/event/job reproduction.
|
||||
8. Whether the ten required Phase 0 artifacts must be committed files or may remain in reviewed scratch form during discovery.
|
||||
9. Whether a future System-owned immutable Tenant Code or signed bootstrap locator is required beyond the accepted public-handle/existence-disclosure contract.
|
||||
|
||||
## Decision rule
|
||||
|
||||
|
||||
Reference in New Issue
Block a user