@musnows/scriverse 0.7.11 → 0.7.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/store.js CHANGED
@@ -154,6 +154,29 @@ function knowledgeSectionsFromInput(sections, settingsMarkdown, settings, fallba
154
154
  function settingsFromKnowledgeSections(sections) {
155
155
  return sections.map((section) => section.contentMarkdown).filter((content) => content.trim());
156
156
  }
157
+ export const chapterOutlineBoardForeshadowSortSql = {
158
+ cte: `WITH foreshadow_associations AS MATERIALIZED (
159
+ SELECT foreshadow.work_id, occurrence.chapter_id, foreshadow.id AS foreshadow_id, foreshadow.status
160
+ FROM foreshadows foreshadow
161
+ JOIN foreshadow_occurrences occurrence ON occurrence.foreshadow_id = foreshadow.id
162
+ WHERE foreshadow.work_id = ?
163
+ UNION
164
+ SELECT foreshadow.work_id, foreshadow.planned_payoff_chapter_id AS chapter_id,
165
+ foreshadow.id AS foreshadow_id, foreshadow.status
166
+ FROM foreshadows foreshadow
167
+ WHERE foreshadow.work_id = ? AND foreshadow.planned_payoff_chapter_id IS NOT NULL
168
+ ), foreshadow_association_counts AS MATERIALIZED (
169
+ SELECT association.work_id, association.chapter_id,
170
+ SUM(CASE WHEN association.status IN ('planned', 'planted') THEN 1 ELSE 0 END) AS unresolved_count,
171
+ COUNT(*) AS total_count
172
+ FROM foreshadow_associations association
173
+ GROUP BY association.work_id, association.chapter_id
174
+ )`,
175
+ join: `LEFT JOIN foreshadow_association_counts sorted_association
176
+ ON sorted_association.work_id = chapter.work_id AND sorted_association.chapter_id = chapter.id`,
177
+ order: `COALESCE(sorted_association.unresolved_count, 0) DESC,
178
+ COALESCE(sorted_association.total_count, 0) DESC`
179
+ };
157
180
  const CHAPTER_OUTLINE_BOARD_PREVIEW_LENGTH = 600;
158
181
  function chapterOutlineBoardLikePattern(value) {
159
182
  return `%${value.normalize("NFKC").toLocaleLowerCase("zh-CN").replaceAll("\\", "\\\\").replaceAll("%", "\\%").replaceAll("_", "\\_")}%`;
@@ -1492,7 +1515,12 @@ export class Store {
1492
1515
  this.db.run(`UPDATE analysis_tasks SET status = 'expired', updated_at = ?
1493
1516
  WHERE work_id = ? AND status IN ('pending', 'running', 'completed', 'partial', 'review')
1494
1517
  AND (json_extract(scope_json, '$.type') = 'book'
1495
- OR (json_extract(scope_json, '$.type') = 'volume' AND json_extract(scope_json, '$.volumeId') = ?))`, timestamp, workId, volumeId);
1518
+ OR (json_extract(scope_json, '$.type') = 'volume' AND json_extract(scope_json, '$.volumeId') = ?)
1519
+ OR EXISTS (SELECT 1 FROM json_each(scope_json, '$.volumeIds') WHERE json_each.value = ?)
1520
+ OR EXISTS (
1521
+ SELECT 1 FROM json_each(scope_json, '$.chapterIds') selected_chapter
1522
+ WHERE selected_chapter.value IN (SELECT id FROM chapters WHERE volume_id = ?)
1523
+ ))`, timestamp, workId, volumeId, volumeId, volumeId);
1496
1524
  this.db.run("UPDATE works SET updated_at = ? WHERE id = ?", timestamp, workId);
1497
1525
  this.audit(workId, "volume.deleted", "volume", volumeId, {
1498
1526
  versionNo,
@@ -2445,9 +2473,14 @@ export class Store {
2445
2473
  AND NOT (status = 'pending' AND task_type = 'chapter-analysis'
2446
2474
  AND json_extract(scope_json, '$.chapterId') = ?)
2447
2475
  AND (json_extract(scope_json, '$.chapterId') = ?
2476
+ OR EXISTS (SELECT 1 FROM json_each(scope_json, '$.chapterIds') WHERE json_each.value = ?)
2448
2477
  OR json_extract(scope_json, '$.type') = 'book'
2449
2478
  OR (json_extract(scope_json, '$.type') = 'volume'
2450
- AND json_extract(scope_json, '$.volumeId') = (SELECT volume_id FROM chapters WHERE id = ?)))`, now(), workId, chapterId, chapterId, chapterId);
2479
+ AND (json_extract(scope_json, '$.volumeId') = (SELECT volume_id FROM chapters WHERE id = ?)
2480
+ OR EXISTS (
2481
+ SELECT 1 FROM json_each(scope_json, '$.volumeIds')
2482
+ WHERE json_each.value = (SELECT volume_id FROM chapters WHERE id = ?)
2483
+ ))))`, now(), workId, chapterId, chapterId, chapterId, chapterId, chapterId);
2451
2484
  const existing = this.db.get(`SELECT id FROM analysis_tasks WHERE work_id = ? AND task_type = 'chapter-analysis' AND status = 'pending'
2452
2485
  AND json_extract(scope_json, '$.chapterId') = ?`, workId, chapterId);
2453
2486
  if (!existing) {
@@ -2653,33 +2686,25 @@ export class Store {
2653
2686
  whereParams.push(...Array.from({ length: 8 }, () => pattern));
2654
2687
  }
2655
2688
  const whereSql = where.join(" AND ");
2656
- const associationCount = (statusSql = "") => `(
2657
- SELECT COUNT(*) FROM foreshadows sorted_foreshadow
2658
- WHERE sorted_foreshadow.work_id = chapter.work_id${statusSql}
2659
- AND (
2660
- sorted_foreshadow.planned_payoff_chapter_id = chapter.id
2661
- OR EXISTS (
2662
- SELECT 1 FROM foreshadow_occurrences sorted_occurrence
2663
- WHERE sorted_occurrence.foreshadow_id = sorted_foreshadow.id
2664
- AND sorted_occurrence.chapter_id = chapter.id
2665
- )
2666
- )
2667
- )`;
2668
2689
  const chapterTreeOrder = "chapter.sort_order, chapter.created_at, chapter.id";
2669
2690
  const chapterOrder = filters.sort === "status"
2670
2691
  ? `CASE WHEN outline.chapter_id IS NULL THEN 0 WHEN outline.status = 'draft' THEN 1 WHEN outline.status = 'ready' THEN 2 ELSE 3 END, ${chapterTreeOrder}`
2671
2692
  : filters.sort === "foreshadows"
2672
- ? `${associationCount(" AND sorted_foreshadow.status IN ('planned', 'planted')")} DESC, ${associationCount()} DESC, ${chapterTreeOrder}`
2693
+ ? `${chapterOutlineBoardForeshadowSortSql.order}, ${chapterTreeOrder}`
2673
2694
  : filters.sort === "title"
2674
2695
  ? `chapter.title COLLATE NOCASE, ${chapterTreeOrder}`
2675
2696
  : chapterTreeOrder;
2676
2697
  const orderSql = `volume.sort_order, volume.created_at, volume.id, ${chapterOrder}`;
2698
+ const foreshadowSortCte = filters.sort === "foreshadows" ? chapterOutlineBoardForeshadowSortSql.cte : "";
2699
+ const foreshadowSortJoin = filters.sort === "foreshadows" ? chapterOutlineBoardForeshadowSortSql.join : "";
2700
+ const foreshadowSortParams = filters.sort === "foreshadows" ? [workId, workId] : [];
2677
2701
  const total = numberValue(this.db.get(`SELECT COUNT(*) AS count
2678
2702
  FROM chapters chapter
2679
2703
  JOIN volumes volume ON volume.id = chapter.volume_id AND volume.work_id = chapter.work_id
2680
2704
  LEFT JOIN chapter_outlines outline ON outline.chapter_id = chapter.id
2681
2705
  WHERE ${whereSql}`, ...whereParams) ?? {}, "count");
2682
- const pageRows = this.db.all(`SELECT volume.id AS volume_id, volume.title AS volume_title, volume.sort_order AS volume_order,
2706
+ const pageRows = this.db.all(`${foreshadowSortCte}
2707
+ SELECT volume.id AS volume_id, volume.title AS volume_title, volume.sort_order AS volume_order,
2683
2708
  chapter.id AS chapter_id, chapter.title AS chapter_title, chapter.chapter_type,
2684
2709
  chapter.sort_order AS chapter_order,
2685
2710
  outline.chapter_id AS outline_chapter_id,
@@ -2691,9 +2716,10 @@ export class Store {
2691
2716
  FROM chapters chapter
2692
2717
  JOIN volumes volume ON volume.id = chapter.volume_id AND volume.work_id = chapter.work_id
2693
2718
  LEFT JOIN chapter_outlines outline ON outline.chapter_id = chapter.id
2719
+ ${foreshadowSortJoin}
2694
2720
  WHERE ${whereSql}
2695
2721
  ORDER BY ${orderSql}
2696
- LIMIT ? OFFSET ?`, previewLength, previewLength, previewLength, previewLength, previewLength, previewLength, previewLength, previewLength, ...whereParams, pagination.limit + 1, pagination.offset);
2722
+ LIMIT ? OFFSET ?`, ...foreshadowSortParams, previewLength, previewLength, previewLength, previewLength, previewLength, previewLength, previewLength, previewLength, ...whereParams, pagination.limit + 1, pagination.offset);
2697
2723
  const chapterPage = paginated(pageRows, pagination, total);
2698
2724
  const volumeRows = this.db.all(`SELECT volume.id, volume.title, volume.sort_order,
2699
2725
  COUNT(chapter.id) AS chapter_count
@@ -5936,65 +5962,98 @@ export class Store {
5936
5962
  }
5937
5963
  relationshipRosterSourceVersions(workId) {
5938
5964
  const versions = {};
5939
- for (const character of this.listCharacters(workId, false)) {
5940
- versions[`character:${String(character.id)}`] = Number(character.versionNo);
5941
- }
5942
- for (const race of this.listRaces(workId))
5943
- versions[`race:${String(race.id)}`] = Number(race.versionNo);
5944
- for (const organization of this.listOrganizations(workId)) {
5945
- versions[`organization:${String(organization.id)}`] = Number(organization.versionNo);
5946
- }
5947
- for (const relationship of this.listRelationships(workId)) {
5948
- versions[`relationship:${String(relationship.id)}`] = Number(relationship.versionNo);
5965
+ for (const row of this.db.all("SELECT id, version_no FROM characters WHERE work_id = ? AND merged_into_character_id IS NULL", workId)) {
5966
+ versions[`character:${requiredString(row, "id")}`] = numberValue(row, "version_no");
5949
5967
  }
5968
+ this.appendVersionedEntitySourceVersions(versions, workId, "race", "races");
5969
+ this.appendVersionedEntitySourceVersions(versions, workId, "organization", "organizations");
5970
+ this.appendVersionedEntitySourceVersions(versions, workId, "relationship", "relationships");
5950
5971
  return versions;
5951
5972
  }
5973
+ appendVersionedEntitySourceVersions(versions, workId, entityType, table) {
5974
+ const rows = this.db.all(`SELECT entity.id, COALESCE(MAX(version.version_no), 0) AS version_no
5975
+ FROM ${table} entity
5976
+ LEFT JOIN entity_versions version
5977
+ ON version.work_id = entity.work_id AND version.entity_type = ? AND version.entity_id = entity.id
5978
+ WHERE entity.work_id = ?
5979
+ GROUP BY entity.id`, entityType, workId);
5980
+ for (const row of rows) {
5981
+ versions[`${entityType}:${requiredString(row, "id")}`] = numberValue(row, "version_no");
5982
+ }
5983
+ }
5952
5984
  relationshipSettingsSourceVersions(workId) {
5953
5985
  const versions = this.relationshipRosterSourceVersions(workId);
5954
- const work = this.getWork(workId);
5955
- versions[`work:${workId}`] = Number(work.versionNo);
5956
- for (const setting of this.listSettings(workId))
5957
- versions[`setting:${String(setting.id)}`] = Number(setting.versionNo);
5958
- for (const character of this.listCharacters(workId, true)) {
5959
- for (const section of this.listCharacterProfileSections(String(character.id))) {
5960
- versions[`character-section:${String(section.id)}`] = Number(section.versionNo);
5961
- }
5962
- }
5963
- for (const track of this.listTimelineTracks(workId))
5964
- versions[`timeline-track:${String(track.id)}`] = Number(track.versionNo);
5965
- for (const event of this.listTimelineEvents(workId))
5966
- versions[`timeline-event:${String(event.id)}`] = Number(event.versionNo);
5967
- for (const outline of this.listChapterOutlines(workId)) {
5968
- const chapterId = String(outline.chapterId);
5969
- versions[`chapter-meta:${chapterId}`] = Number(this.getChapter(chapterId).versionNo);
5970
- versions[`chapter-outline:${chapterId}`] = this.currentEntityVersionNo("chapter-outline", chapterId);
5971
- }
5972
- for (const foreshadow of this.listForeshadows(workId)) {
5973
- versions[`foreshadow:${String(foreshadow.id)}`] = Number(foreshadow.versionNo);
5986
+ const work = this.db.get("SELECT version_no FROM works WHERE id = ? AND deleted_at IS NULL", workId);
5987
+ if (!work)
5988
+ throw notFound("作品");
5989
+ versions[`work:${workId}`] = numberValue(work, "version_no");
5990
+ this.appendVersionedEntitySourceVersions(versions, workId, "setting", "settings");
5991
+ for (const row of this.db.all(`SELECT section.id, section.version_no
5992
+ FROM character_profile_sections section
5993
+ JOIN characters character ON character.id = section.character_id
5994
+ WHERE section.work_id = ? AND character.merged_into_character_id IS NULL`, workId)) {
5995
+ versions[`character-section:${requiredString(row, "id")}`] = numberValue(row, "version_no");
5996
+ }
5997
+ this.appendVersionedEntitySourceVersions(versions, workId, "timeline-track", "timeline_tracks");
5998
+ this.appendVersionedEntitySourceVersions(versions, workId, "timeline-event", "timeline_events");
5999
+ for (const row of this.db.all(`SELECT chapter.id, chapter.version_no AS chapter_version_no,
6000
+ COALESCE(MAX(version.version_no), 0) AS outline_version_no
6001
+ FROM chapter_outlines outline
6002
+ JOIN chapters chapter ON chapter.id = outline.chapter_id
6003
+ LEFT JOIN entity_versions version
6004
+ ON version.work_id = chapter.work_id
6005
+ AND version.entity_type = 'chapter-outline'
6006
+ AND version.entity_id = chapter.id
6007
+ WHERE chapter.work_id = ? AND chapter.deleted_at IS NULL
6008
+ GROUP BY chapter.id, chapter.version_no`, workId)) {
6009
+ const chapterId = requiredString(row, "id");
6010
+ versions[`chapter-meta:${chapterId}`] = numberValue(row, "chapter_version_no");
6011
+ versions[`chapter-outline:${chapterId}`] = numberValue(row, "outline_version_no");
5974
6012
  }
5975
- for (const review of this.listReviewItems(workId)) {
5976
- versions[`review:${String(review.id)}`] = String(review.updatedAt);
6013
+ this.appendVersionedEntitySourceVersions(versions, workId, "foreshadow", "foreshadows");
6014
+ for (const row of this.db.all("SELECT id, updated_at FROM review_items WHERE work_id = ?", workId)) {
6015
+ versions[`review:${requiredString(row, "id")}`] = requiredString(row, "updated_at");
5977
6016
  }
5978
6017
  return versions;
5979
6018
  }
5980
6019
  analysisTaskSourceVersions(workId, scope) {
5981
6020
  const sourceVersions = {};
5982
- if (typeof scope.chapterId === "string") {
5983
- const chapter = this.getChapter(scope.chapterId);
5984
- if (chapter.workId !== workId)
6021
+ const selectedChapterIds = [
6022
+ ...(typeof scope.chapterId === "string" ? [scope.chapterId] : []),
6023
+ ...(Array.isArray(scope.chapterIds) ? scope.chapterIds.filter((value) => typeof value === "string") : [])
6024
+ ];
6025
+ for (const chapterId of [...new Set(selectedChapterIds)]) {
6026
+ const chapter = this.db.get("SELECT work_id, version_no FROM chapters WHERE id = ? AND deleted_at IS NULL", chapterId);
6027
+ if (!chapter)
6028
+ throw notFound("章节");
6029
+ if (requiredString(chapter, "work_id") !== workId)
5985
6030
  throw new AppError(400, "CHAPTER_WORK_MISMATCH", "章节不属于当前作品");
5986
- sourceVersions[scope.chapterId] = Number(chapter.versionNo);
5987
- }
5988
- else if (scope.type === "book" || scope.type === "volume") {
5989
- const tree = this.getWorkTree(workId);
5990
- const volumes = tree.volumes;
5991
- const selectedVolumes = scope.type === "volume"
5992
- ? volumes.filter((volume) => volume.id === scope.volumeId)
5993
- : volumes;
5994
- if (scope.type === "volume" && selectedVolumes.length === 0)
5995
- throw notFound("卷");
5996
- for (const chapter of selectedVolumes.flatMap((volume) => volume.chapters)) {
5997
- sourceVersions[String(chapter.id)] = Number(chapter.versionNo);
6031
+ sourceVersions[chapterId] = numberValue(chapter, "version_no");
6032
+ }
6033
+ if (scope.type === "book" || scope.type === "volume") {
6034
+ const selectedVolumeIds = [
6035
+ ...(typeof scope.volumeId === "string" ? [scope.volumeId] : []),
6036
+ ...(Array.isArray(scope.volumeIds) ? scope.volumeIds.filter((value) => typeof value === "string") : [])
6037
+ ];
6038
+ const selectedVolumeIdSet = new Set(selectedVolumeIds);
6039
+ if (scope.type === "volume") {
6040
+ if (selectedVolumeIdSet.size === 0)
6041
+ throw notFound("卷");
6042
+ const volumePlaceholders = [...selectedVolumeIdSet].map(() => "?").join(", ");
6043
+ const volumes = this.db.all(`SELECT id FROM volumes WHERE work_id = ? AND deleted_at IS NULL AND id IN (${volumePlaceholders})`, workId, ...selectedVolumeIdSet);
6044
+ if (volumes.length !== selectedVolumeIdSet.size)
6045
+ throw notFound("卷");
6046
+ }
6047
+ const volumeFilter = scope.type === "volume"
6048
+ ? `AND chapter.volume_id IN (${[...selectedVolumeIdSet].map(() => "?").join(", ")})`
6049
+ : "";
6050
+ const chapterRows = this.db.all(`SELECT chapter.id, chapter.version_no
6051
+ FROM chapters chapter
6052
+ JOIN volumes volume ON volume.id = chapter.volume_id
6053
+ WHERE chapter.work_id = ? AND chapter.deleted_at IS NULL AND volume.deleted_at IS NULL
6054
+ ${volumeFilter}`, workId, ...(scope.type === "volume" ? [...selectedVolumeIdSet] : []));
6055
+ for (const chapter of chapterRows) {
6056
+ sourceVersions[requiredString(chapter, "id")] = numberValue(chapter, "version_no");
5998
6057
  }
5999
6058
  }
6000
6059
  if (scope.previewRelationshipChanges === true) {
@@ -6029,10 +6088,12 @@ export class Store {
6029
6088
  for (const characterId of scope.characterIds) {
6030
6089
  if (typeof characterId !== "string")
6031
6090
  throw new AppError(400, "CHARACTER_REQUIRED", "被分析角色标识无效");
6032
- const character = this.getCharacter(characterId);
6033
- if (character.workId !== workId)
6091
+ const character = this.db.get("SELECT work_id, name FROM characters WHERE id = ?", characterId);
6092
+ if (!character)
6093
+ throw notFound("角色");
6094
+ if (requiredString(character, "work_id") !== workId)
6034
6095
  throw new AppError(400, "CHARACTER_WORK_MISMATCH", "被分析角色不属于当前作品");
6035
- targetCharacters.push({ id: characterId, name: String(character.name) });
6096
+ targetCharacters.push({ id: characterId, name: requiredString(character, "name") });
6036
6097
  }
6037
6098
  if (targetCharacters.length > 0)
6038
6099
  scope.targetCharacters = targetCharacters;
@@ -7158,8 +7219,22 @@ export class Store {
7158
7219
  }
7159
7220
  taskScopeSummaryFromMaps(scope, chapterSummaries, volumeTitles, characterNames, includeCharacterNames = true) {
7160
7221
  const targetedSuffix = this.taskTargetedSuffix(scope, characterNames, includeCharacterNames);
7222
+ if (Array.isArray(scope.chapterIds) && scope.chapterIds.length > 0) {
7223
+ const labels = scope.chapterIds
7224
+ .filter((chapterId) => typeof chapterId === "string")
7225
+ .map((chapterId) => chapterSummaries.get(chapterId) ?? "章节已删除");
7226
+ const preview = labels.slice(0, 3).join("、");
7227
+ return `指定章节(${labels.length}):${preview}${labels.length > 3 ? "……" : ""}${targetedSuffix}`;
7228
+ }
7161
7229
  if (typeof scope.chapterId === "string")
7162
7230
  return `${chapterSummaries.get(scope.chapterId) ?? "章节已删除"}${targetedSuffix}`;
7231
+ if (Array.isArray(scope.volumeIds) && scope.volumeIds.length > 0) {
7232
+ const labels = scope.volumeIds
7233
+ .filter((volumeId) => typeof volumeId === "string")
7234
+ .map((volumeId) => volumeTitles.get(volumeId) ? `分卷 · ${volumeTitles.get(volumeId)}` : "分卷已删除");
7235
+ const preview = labels.slice(0, 3).join("、");
7236
+ return `指定分卷(${labels.length}):${preview}${labels.length > 3 ? "……" : ""}${targetedSuffix}`;
7237
+ }
7163
7238
  if (scope.type === "volume" && typeof scope.volumeId === "string") {
7164
7239
  const title = volumeTitles.get(scope.volumeId);
7165
7240
  return `${title ? `分卷 · ${title}` : "分卷已删除"}${targetedSuffix}`;
@@ -7178,6 +7253,17 @@ export class Store {
7178
7253
  }
7179
7254
  taskScopeSummary(workId, scope, characterNames, includeCharacterNames = true) {
7180
7255
  const targetedSuffix = this.taskTargetedSuffix(scope, characterNames, includeCharacterNames);
7256
+ if (Array.isArray(scope.chapterIds) && scope.chapterIds.length > 0) {
7257
+ const labels = scope.chapterIds.map((chapterId) => {
7258
+ const chapter = this.db.get(`SELECT chapter.title AS title, volume.title AS volume_title
7259
+ FROM chapters chapter
7260
+ JOIN volumes volume ON volume.id = chapter.volume_id
7261
+ WHERE chapter.id = ? AND chapter.work_id = ? AND chapter.deleted_at IS NULL`, chapterId, workId);
7262
+ return chapter ? `${requiredString(chapter, "volume_title")} · ${requiredString(chapter, "title")}` : "章节已删除";
7263
+ });
7264
+ const preview = labels.slice(0, 3).join("、");
7265
+ return `指定章节(${labels.length}):${preview}${labels.length > 3 ? "……" : ""}${targetedSuffix}`;
7266
+ }
7181
7267
  if (typeof scope.chapterId === "string") {
7182
7268
  const chapter = this.db.get(`SELECT chapter.title AS title, volume.title AS volume_title
7183
7269
  FROM chapters chapter
@@ -7189,6 +7275,14 @@ export class Store {
7189
7275
  const volumeTitle = requiredString(chapter, "volume_title");
7190
7276
  return `${volumeTitle} · ${title}${targetedSuffix}`;
7191
7277
  }
7278
+ if (Array.isArray(scope.volumeIds) && scope.volumeIds.length > 0) {
7279
+ const labels = scope.volumeIds.map((volumeId) => {
7280
+ const volume = this.db.get("SELECT title FROM volumes WHERE id = ? AND work_id = ?", volumeId, workId);
7281
+ return volume ? `分卷 · ${requiredString(volume, "title")}` : "分卷已删除";
7282
+ });
7283
+ const preview = labels.slice(0, 3).join("、");
7284
+ return `指定分卷(${labels.length}):${preview}${labels.length > 3 ? "……" : ""}${targetedSuffix}`;
7285
+ }
7192
7286
  if (scope.type === "volume" && typeof scope.volumeId === "string") {
7193
7287
  const volume = this.db.get("SELECT title FROM volumes WHERE id = ? AND work_id = ?", scope.volumeId, workId);
7194
7288
  return `${volume ? `分卷 · ${requiredString(volume, "title")}` : "分卷已删除"}${targetedSuffix}`;
@@ -7296,6 +7390,25 @@ export class Store {
7296
7390
  return snapshotNames;
7297
7391
  }
7298
7392
  taskScopeDetails(workId, scope) {
7393
+ if (Array.isArray(scope.chapterIds) && scope.chapterIds.length > 0) {
7394
+ return scope.chapterIds.map((chapterId) => {
7395
+ const chapter = this.db.get(`SELECT chapter.id AS id, chapter.title AS title, chapter.version_no AS version_no,
7396
+ volume.id AS volume_id, volume.title AS volume_title
7397
+ FROM chapters chapter
7398
+ JOIN volumes volume ON volume.id = chapter.volume_id
7399
+ WHERE chapter.id = ? AND chapter.work_id = ? AND chapter.deleted_at IS NULL`, chapterId, workId);
7400
+ if (!chapter)
7401
+ return { type: "chapter", chapterId, missing: true };
7402
+ return {
7403
+ type: "chapter",
7404
+ chapterId: requiredString(chapter, "id"),
7405
+ title: requiredString(chapter, "title"),
7406
+ versionNo: numberValue(chapter, "version_no"),
7407
+ volumeId: requiredString(chapter, "volume_id"),
7408
+ volumeTitle: requiredString(chapter, "volume_title")
7409
+ };
7410
+ });
7411
+ }
7299
7412
  if (typeof scope.chapterId === "string") {
7300
7413
  const chapter = this.db.get(`SELECT chapter.id AS id, chapter.title AS title, chapter.version_no AS version_no,
7301
7414
  volume.id AS volume_id, volume.title AS volume_title
@@ -7313,6 +7426,24 @@ export class Store {
7313
7426
  volumeTitle: requiredString(chapter, "volume_title")
7314
7427
  }];
7315
7428
  }
7429
+ if (Array.isArray(scope.volumeIds) && scope.volumeIds.length > 0) {
7430
+ return scope.volumeIds.map((volumeId) => {
7431
+ const volume = this.db.get("SELECT id, title FROM volumes WHERE id = ? AND work_id = ?", volumeId, workId);
7432
+ if (!volume)
7433
+ return { type: "volume", volumeId, missing: true };
7434
+ const chapters = this.db.all("SELECT id, title, version_no FROM chapters WHERE volume_id = ? AND deleted_at IS NULL ORDER BY sort_order, created_at", volumeId);
7435
+ return {
7436
+ type: "volume",
7437
+ volumeId: requiredString(volume, "id"),
7438
+ title: requiredString(volume, "title"),
7439
+ chapters: chapters.map((item) => ({
7440
+ chapterId: requiredString(item, "id"),
7441
+ title: requiredString(item, "title"),
7442
+ versionNo: numberValue(item, "version_no")
7443
+ }))
7444
+ };
7445
+ });
7446
+ }
7316
7447
  if (scope.type === "volume" && typeof scope.volumeId === "string") {
7317
7448
  const volume = this.db.get("SELECT id, title FROM volumes WHERE id = ? AND work_id = ?", scope.volumeId, workId);
7318
7449
  if (!volume)