feat(education): add native question publication lifecycle

This commit is contained in:
2026-07-30 21:31:08 +08:00
parent 4db4a7d371
commit 34bc1fe41e
22 changed files with 2044 additions and 20 deletions

View File

@@ -0,0 +1,5 @@
# Native question authoring follows the active catalog authority
Education exposes local question authoring only when `catalog-mode=JAVA_READ`; `SCALAR_READ` and other modes fail with an explicit business error before any database access. Tenant questions start as `DRAFT`, retain immutable content versions, move only through `DRAFT → PUBLISHED → ARCHIVED`, and append an Education-owned lifecycle audit in the same transaction, because a local write under Scalar authority would be invisible to students and the platform's asynchronous operation log cannot prove atomic publication.
`ARCHIVED` is terminal for the current command surface, and `RETIRED` is not introduced until its distinct business and restoration semantics are decided. Direct question visibility follows the question's own publication state; entry, node, and collection state gates discovery through those routes rather than rewriting the question lifecycle.

View File

@@ -29,7 +29,7 @@ The durable artifact classification, adoption matrix, version allocation, backfi
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 Education capability menu seed is a separate conditional V4040 owner only if the administrator endpoint remains approved; role assignment is not seeded.
6. The earlier V4040 capability-seed plan was superseded after V4040 was allocated to the submit-claim lease. V4080 conditionally seeds the approved capability plus question author/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.
@@ -102,6 +102,34 @@ The durable artifact classification, adoption matrix, version allocation, backfi
**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, 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.
**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 |
| 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 |
| Platform curator | none in this slice | No PUBLIC or tenant-owned writes through these endpoints | Fail closed |
V4080 conditionally seeds the four Education permissions (`education:capability` plus author/publish/archive) when the adopted platform schema contains `system_menu`. It fails when a fixed ID is already occupied by a different permission and deliberately does not assign the permissions to any 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`.
## 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.

View File

@@ -1,6 +1,6 @@
# EDU-010 — Tenant content publication and graph integrity
- **Status:** in progress — catalog graph-integrity slice delivered; authoring/publication lifecycle remains
- **Status:** in progress — catalog graph-integrity and the verified JAVA_READ tenant-question lifecycle slice are delivered; broader classification/collection authoring remains
- **Type:** implementation program
- **Phase:** 3
- **Blockers:** EDU-004 ✓ (done), EDU-009 ✓ (done), provider-authority decision ✓ (resolved 2026-07-30, see decisions.md), PUBLIC graph-semantics decision ✓ (resolved 2026-07-30, see decisions.md)
@@ -20,11 +20,12 @@ Authorized tenant administrators can author, classify, publish, archive, and ret
## Acceptance criteria
- [ ] Admin permission and data-scope matrix is explicit.
- [x] Admin permission and data-scope matrix is explicit.
- [x] Cross-tenant graph relationships cannot be persisted in the current V4020 catalog graph.
- [x] PUBLIC and tenant-owned reference rules are enforced across the current 19 reference edges, with exact trigger-mapping and focused behavior tests.
- [ ] Unpublished/archived content is never student-visible.
- [ ] Publication is transactional and auditable.
- [x] V4080 extends the graph guard to the new Question Version → Question edge and makes the version table the twelfth ownership-protected catalog table.
- [x] Unpublished/archived content is never student-visible.
- [x] Publication is transactional and auditable.
- [x] Database graph-integrity changes use `flyway-postgresql` and forward migration V4070.
## Delivery progress — graph-integrity slice (2026-07-30)
@@ -41,16 +42,45 @@ Delivered:
No non-Education module changed. Application rollback can disable native authoring when it is introduced; database recovery remains a higher forward migration and must preserve existing content.
## Remaining publication slice
## Publication-slice constraints (resolved)
Do not add a local-only admin write path while `SCALAR_READ` remains the default authoritative provider: a successful PostgreSQL write would not be visible to students. The next bounded slice is a `JAVA_READ`-only tenant question `draft → publish → archive → student read` path that fails closed in unsupported provider modes.
Do not add a local-only admin write path while `SCALAR_READ` remains the default authoritative provider: a successful PostgreSQL write would not be visible to students. The implemented bounded slice is a `JAVA_READ`-only tenant question `draft → publish → archive → student read` path that fails closed in unsupported provider modes.
Before its Flyway/application implementation, resolve and document:
The slice resolves the prerequisite decisions as follows:
1. the exact `DRAFT/PUBLISHED/ARCHIVED/RETIRED` state machine and mapping of V4020 `HIDDEN/INACTIVE` rows;
2. immutable question-version and transactional publication-audit ownership;
3. whether ancestor entry/node/collection/archive state makes a question unavailable to fresh student reads;
4. the explicit tenant-admin versus platform-curator permission/data-scope matrix.
1. the command surface is `DRAFTPUBLISHEDARCHIVED`; `RETIRED` remains undefined, and V4020 `HIDDEN/INACTIVE` rows map to `ARCHIVED`;
2. Education owns immutable question versions and transactional lifecycle audit;
3. a question's state controls direct visibility while entry/node/collection availability gates only route-specific discovery;
4. tenant admin action permissions are separate, tenant-wide, and tenant-bound; platform-curator/PUBLIC writes fail closed in this slice.
## Delivery progress — JAVA_READ tenant-question lifecycle slice (2026-07-30)
Delivered:
- Explicit Admin APIs and independent RBAC permissions for tenant question author, publish, and archive operations. Requests cannot choose tenant, scope, lifecycle, or actor identity:
- `POST /admin-api/education/questions/drafts``education:question:author`;
- `PUT /admin-api/education/questions/{id}/publish``education:question:publish`;
- `PUT /admin-api/education/questions/{id}/archive``education:question:archive`.
- Provider-authority guard: authoring is accepted only for `JAVA_READ`; `SCALAR_READ` fails before any Mapper access.
- V4080 changes native defaults to `DRAFT/false`, normalizes legacy visibility states, fails closed on unsafe historical published content, constrains the single direction `DRAFT → PUBLISHED → ARCHIVED`, creates immutable Question Content Versions, and creates append-only lifecycle audit facts.
- V4080 adds the Question Version → Question reference as the twentieth guarded catalog edge. In addition to the generic scope rule, a version's tenant and scope must exactly equal its question's ownership; the version table is the twelfth catalog table protected against ownership mutation.
- When the adopted platform schema contains `system_menu`, V4080 conditionally seeds the Education capability plus author/publish/archive permissions. A fixed ID already occupied by a different permission aborts migration, and no role assignment is seeded.
- Publication and archive use tenant/scope/expected-state CAS updates and append audit facts in the same transaction. Audit failure rolls back the state change.
- PostgreSQL integration-test coverage exercises the student read-after-write seam: draft is invisible, published content is returned only through the existing Safe Question projection, and archived content is invisible while stored snapshots remain independent.
- Direct question visibility is controlled by the question lifecycle. Container availability is a route-level discovery gate; classification, collection, and platform-curator write paths remain outside this bounded slice.
Permission and data-scope matrix is recorded in `07-decisions.md`. The three lifecycle endpoints manage tenant-wide shared catalog assets within the current framework tenant; department/self DataPermission does not grant additional row access, PUBLIC authoring fails closed, and the V4080 seed intentionally grants no role.
The listed acceptance criteria are satisfied for the native catalog graph and tenant-question lifecycle slice. EDU-010 remains in progress because its tenant-admin outcome also includes classification, collection, blueprint, retire, and platform-curator workflows that are not part of this bounded slice.
## Verification evidence (2026-07-30)
- `mvn -pl yudao-module-education test` with the five `EDU_TEST_POSTGRES_*` variables pointed at the local Docker PostgreSQL: **485 tests passed**, including 18 Flyway migration tests and five lifecycle PostgreSQL integration tests. V4080 was actually executed in disposable PostgreSQL schemas.
- The lifecycle integration tests prove concurrent double-publish has one success and one lifecycle conflict with exactly one publish audit, cross-tenant and PUBLIC management fail closed, and `draft → publish → archive` matches student visibility.
- The real Spring Method Security contract tests prove each of author/publish/archive requires its own permission and rejected calls do not reach the lifecycle service.
- `mvn -pl yudao-server -am -DskipTests clean compile`: **22 reactor modules passed**.
- `git diff --check`: passed.
- `target/classes/db/migration/education/V4080__add_question_publication_lifecycle.sql`: present after the module build.
## Risk and rollback