feat(db,contracts): W5-C subscription units migration 0025, contracts and mapping

- subscription: status pending (TMDB not ready: searchable/evaluable, not completable),
  last_searched_at / next_search_at
- content_unit (season): episode_total ledger with source / lock / timestamp
- acquisition_plan: unit_keys (Slot × unit reservation) and file_selection; drop the
  one-in-flight-per-slot unique index (checked under slot row lock per unit instead)
- new tables slot_deferral, slot_unit (per-unit quality ledger), subscription_history
- contracts: SubscriptionParams, SubscriptionHistory, SlotDeferral, SlotUnit, SubscriptionProgress,
  EpisodeTotalSet, CandidateEvaluation pri/trace/unitKeys and decision deferred;
  endpoints subscriptions.search/progress/setEpisodeTotal and subscription-history
- mapping: evaluation messages via describeViolation, pri/trace passthrough

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
rootandClaude Opus 5.5 committed 2026-09-27 23:33:56 +08:00
1 parent d162d464c4
commit 1dd460d359
9 files changed
+405 -34

No files matched your search

+14
View File
@@ -59,6 +59,10 @@ export type Segment = z.infer<typeof Segment>;
export const ContentUnitStatus = z.enum(["provisional", "confirmed", "retired"]);
export type ContentUnitStatus = z.infer<typeof ContentUnitStatus>;
/** 季总集数来源:tmdb 元数据 / 插件注入(只升)/ 人工锁定 / 回退被已覆盖最高集兜住 / 沿用 */
export const EpisodeTotalSource = z.enum(["tmdb", "injected", "manual", "covered_floor", "previous"]);
export type EpisodeTotalSource = z.infer<typeof EpisodeTotalSource>;
export const ContentUnit = EntityBase.extend({
mediaEntityId: Id,
parentUnitId: Id.nullable(),
@@ -77,6 +81,12 @@ export const ContentUnit = EntityBase.extend({
numberingLocked: z.boolean(),
metadata: JsonObject,
fieldLocks: z.array(z.string()),
/** 季单元的总集数账本(只升;回退不低于已覆盖最高集);null = 未知,追更不结束 */
episodeTotal: z.number().int().nonnegative().nullable(),
episodeTotalSource: EpisodeTotalSource.nullable(),
/** 人工锁定总集数:元数据刷新与注入不再改写 */
episodeTotalLocked: z.boolean(),
episodeTotalAt: Timestamp.nullable(),
});
export type ContentUnit = z.infer<typeof ContentUnit>;
export const ContentUnitCreate = ContentUnit.omit(SERVER_MANAGED).partial({
@@ -93,6 +103,10 @@ export const ContentUnitCreate = ContentUnit.omit(SERVER_MANAGED).partial({
numberingLocked: true,
metadata: true,
fieldLocks: true,
episodeTotal: true,
episodeTotalSource: true,
episodeTotalLocked: true,
episodeTotalAt: true,
});
export type ContentUnitCreate = z.infer<typeof ContentUnitCreate>;
export const ContentUnitUpdate = ContentUnitCreate.omit({ mediaEntityId: true }).partial();