dsh-novel-write-engine 0.2.7 → 0.2.9
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/CHANGELOG.md +42 -0
- package/README.md +27 -23
- package/briefs/P3.md +1 -0
- package/briefs/P4.md +1 -1
- package/briefs/P8.md +1 -1
- package/lib/client.js +928 -125
- package/lib/index.d.ts +151 -10
- package/lib/index.js +1498 -262
- package/lib/types/client/dto.d.ts +32 -9
- package/lib/types/client/panel.d.ts +19 -1
- package/lib/types/client/viewmodel.d.ts +13 -4
- package/lib/types/constants.d.ts +2 -0
- package/lib/types/panel-contract.d.ts +2 -2
- package/lib/types/types.d.ts +55 -2
- package/package.json +8 -3
- package/presets/novel-write-engine.patch.yml +4 -0
- package/rulepacks/default.yaml +13 -5
- package/skills/entry.md +2 -1
package/lib/index.js
CHANGED
|
@@ -15,6 +15,8 @@ const PANEL_ID = "novel-workshop";
|
|
|
15
15
|
const PANEL_ROUTE = "/api/novel-write-engine/panel";
|
|
16
16
|
const SKILL_NAME = "novel-write-engine";
|
|
17
17
|
const REMOTE_VERSION = 6;
|
|
18
|
+
/** User-visible panel/tutorial version; keep in sync with package.json. */
|
|
19
|
+
const PANEL_VERSION = "0.2.9";
|
|
18
20
|
/** The panel is a recent-history view; the wire payload and UI use one cap. */
|
|
19
21
|
const PANEL_CHAPTER_LIMIT = 30;
|
|
20
22
|
/** Project-relative locations. Every write goes through these constants. */
|
|
@@ -308,6 +310,33 @@ function allocateIds(prefix, nextSeq, count) {
|
|
|
308
310
|
nextSeq: nextSeq + count
|
|
309
311
|
};
|
|
310
312
|
}
|
|
313
|
+
// src/domain/narrator.ts
|
|
314
|
+
/**
|
|
315
|
+
* One narrator-alias contract shared by PG-05, state_delta and lock capture.
|
|
316
|
+
*
|
|
317
|
+
* The plan format is `角色名(第一人称)` or `角色名(第一人称「我」)`. A
|
|
318
|
+
* first-person manuscript says 「我」 instead of repeating the character name;
|
|
319
|
+
* every consumer must accept that alias, and none may infer it from a bare
|
|
320
|
+
* substring such as "林" inside "林砚".
|
|
321
|
+
*/
|
|
322
|
+
const narratorName = (narrator) => narrator.split(/[((]/, 1)[0].trim();
|
|
323
|
+
/** The only documented first-person alias is the pronoun itself. */
|
|
324
|
+
const narratorAliases = (narrator) => isFirstPersonNarrator(narrator) ? ["我"] : [];
|
|
325
|
+
/** Whether the plan declares first-person narration. */
|
|
326
|
+
const isFirstPersonNarrator = (narrator) => narrator.includes("第一人称") && !narrator.includes("第三人称");
|
|
327
|
+
/**
|
|
328
|
+
* True when `name` is the declared narrator and the manuscript uses the
|
|
329
|
+
* declared first-person alias. A name that merely contains the narrator name
|
|
330
|
+
* is not an alias.
|
|
331
|
+
*/
|
|
332
|
+
const isNarratorAlias = (name, narrator, text) => {
|
|
333
|
+
if (text.includes(name)) return true;
|
|
334
|
+
if (narratorName(narrator) !== name) return false;
|
|
335
|
+
if (!isFirstPersonNarrator(narrator)) return false;
|
|
336
|
+
return narratorAliases(narrator).some((alias) => text.includes(alias));
|
|
337
|
+
};
|
|
338
|
+
/** Paragraph used for a newly locked character; prefers the named/aliased one. */
|
|
339
|
+
const narratorAliasesFor = (name, narrator) => name === narratorName(narrator) ? narratorAliases(narrator) : [];
|
|
311
340
|
// src/domain/scoring.ts
|
|
312
341
|
/** The fifteen dimensions, grouped as the commercial weights demand. */
|
|
313
342
|
const SCORE_DIMENSIONS = [
|
|
@@ -1122,7 +1151,7 @@ const dashBudget = (ctx) => {
|
|
|
1122
1151
|
return [v(PUNCTUATION_RULES.DASH_BUDGET, "advisory", firstParagraphContaining(ctx.text, token), `dash weight ${weighted} (dialogue ${inDialogue}x double), budget is ${scaled} for ${ctx.sceneType}`)];
|
|
1123
1152
|
};
|
|
1124
1153
|
const periodDensity = (ctx) => {
|
|
1125
|
-
const { min, max } = ctx.rulepack.punctuation.period_density;
|
|
1154
|
+
const { min, max } = ctx.rulepack.genres[ctx.genre]?.profile?.period_density ?? ctx.rulepack.punctuation.period_density;
|
|
1126
1155
|
const dominant = ctx.rulepack.punctuation.dialogue_dominant_ratio;
|
|
1127
1156
|
const results = [];
|
|
1128
1157
|
splitParagraphs(ctx.text).forEach((paragraph, index) => {
|
|
@@ -1355,10 +1384,11 @@ const numericFactMismatch = (ctx) => {
|
|
|
1355
1384
|
for (const fact of ctx.facts) {
|
|
1356
1385
|
if (fact.alias.trim() === "" || !Number.isFinite(fact.value)) continue;
|
|
1357
1386
|
const tolerance = fact.tolerance ?? 0;
|
|
1358
|
-
const pattern = new RegExp(`${escapePattern(fact.alias)}\\s*(?:是|为|约|有|:|:)?\\s*(${NUMBER_TOKEN})\\s*${escapePattern(fact.unit)}`, "g");
|
|
1387
|
+
const pattern = new RegExp(`${escapePattern(fact.alias)}\\s*(?:是|为|约|有|:|:)?\\s*(${NUMBER_TOKEN})\\s*${escapePattern(fact.unit)}(?:\\s*([零一二两三四五六七八九]))?(?![零一二两三四五六七八九十百千万\\d])`, "g");
|
|
1359
1388
|
for (const match of ctx.text.matchAll(pattern)) {
|
|
1360
|
-
const
|
|
1361
|
-
if (
|
|
1389
|
+
const base = parseSmallNumber(match[1] ?? "");
|
|
1390
|
+
if (base === null) continue;
|
|
1391
|
+
const value = base + (match[2] === void 0 ? 0 : (CN_DIGITS[match[2]] ?? 0) / 10);
|
|
1362
1392
|
if (Math.abs(value - fact.value) <= tolerance) continue;
|
|
1363
1393
|
const paragraph = paragraphs.findIndex((candidate) => candidate.includes(match[0]));
|
|
1364
1394
|
results.push(v(SEMANTIC_RULES.NUMERIC_FACT_MISMATCH, "advisory", Math.max(1, paragraph + 1), `"${fact.alias}" declares ${fact.value}${fact.unit}, text shows ${value}${fact.unit}`));
|
|
@@ -1368,7 +1398,8 @@ const numericFactMismatch = (ctx) => {
|
|
|
1368
1398
|
return results;
|
|
1369
1399
|
};
|
|
1370
1400
|
const pleasureDensity = (ctx) => {
|
|
1371
|
-
const {
|
|
1401
|
+
const { keywords, window } = ctx.rulepack.semantics.pleasure_density;
|
|
1402
|
+
const min = ctx.rulepack.genres[ctx.genre]?.profile?.pleasure_density ?? ctx.rulepack.semantics.pleasure_density.min;
|
|
1372
1403
|
if (keywords.length === 0) return [];
|
|
1373
1404
|
const results = [];
|
|
1374
1405
|
if (slidingWindows(ctx.text, window).findIndex((slice) => {
|
|
@@ -1385,8 +1416,9 @@ const sensoryDensity = (ctx) => {
|
|
|
1385
1416
|
};
|
|
1386
1417
|
const waterWords = (ctx) => {
|
|
1387
1418
|
const { window, threshold, terms, scene_thresholds } = ctx.rulepack.semantics.water_words;
|
|
1419
|
+
const genreThreshold = ctx.rulepack.genres[ctx.genre]?.profile?.water_words;
|
|
1388
1420
|
if (terms.length === 0) return [];
|
|
1389
|
-
const sceneThreshold = scene_thresholds?.[ctx.sceneType] ?? threshold;
|
|
1421
|
+
const sceneThreshold = genreThreshold ?? scene_thresholds?.[ctx.sceneType] ?? threshold;
|
|
1390
1422
|
const results = [];
|
|
1391
1423
|
for (const slice of slidingWindows(ctx.text, window)) {
|
|
1392
1424
|
const density = terms.reduce((total, term) => total + countOccurrences(slice, term), 0) / window;
|
|
@@ -1554,25 +1586,24 @@ const resourceMismatch = (ctx) => {
|
|
|
1554
1586
|
resourceVisible = !(previousHasNumber && previousHasSign) && previousClauseText.includes(change.resource);
|
|
1555
1587
|
}
|
|
1556
1588
|
if (!resourceVisible) continue;
|
|
1557
|
-
const names = ctx.summary?.characters_appeared ?? [];
|
|
1558
|
-
const otherCharacter = names.
|
|
1559
|
-
let characterVisible = clauseText.
|
|
1560
|
-
if (!characterVisible && !otherCharacter) characterVisible = previousClauseText
|
|
1589
|
+
const names = [...ctx.summary?.characters_appeared ?? [], ...ctx.evidence.characters?.characters.map((character) => character.name) ?? []];
|
|
1590
|
+
const otherCharacter = mentionedNames$1(clauseText, names.filter((name) => name !== "" && name !== change.character)).length > 0;
|
|
1591
|
+
let characterVisible = mentionedNames$1(clauseText, [change.character]).length > 0 || !otherCharacter && mentionedNames$1(ctx.text.slice(sentence.start, sentence.end), [change.character]).length > 0;
|
|
1592
|
+
if (!characterVisible && !otherCharacter) characterVisible = mentionedNames$1(previousClauseText, [change.character]).length > 0;
|
|
1561
1593
|
if (!characterVisible && !otherCharacter && sentence.start > 0) {
|
|
1562
1594
|
const previous = sentenceBounds(ctx.text, sentence.start - 1, sentence.start - 1);
|
|
1563
1595
|
const previousText = ctx.text.slice(previous.start, previous.end);
|
|
1564
|
-
const named =
|
|
1596
|
+
const named = mentionedNames$1(previousText, names);
|
|
1565
1597
|
characterVisible = /(?:他|她|它|我|他们|她们)/.test(clauseText) && named.length === 1 && named[0] === change.character;
|
|
1566
1598
|
}
|
|
1567
1599
|
if (!characterVisible) continue;
|
|
1568
1600
|
conflict = {
|
|
1569
1601
|
index,
|
|
1570
|
-
|
|
1571
|
-
sign: sign[0]
|
|
1602
|
+
trigger: sign[0]
|
|
1572
1603
|
};
|
|
1573
1604
|
break;
|
|
1574
1605
|
}
|
|
1575
|
-
return conflict === null ? [] : [v(CROSS_RULES.RESOURCE_MISMATCH, "advisory", paragraphOfIndex(ctx.text, conflict.index), `${change.character}'s ${change.resource} change is ${change.delta >= 0 ? "+" : ""}${change.delta}, but the text states the opposite
|
|
1606
|
+
return conflict === null ? [] : [v(CROSS_RULES.RESOURCE_MISMATCH, "advisory", paragraphOfIndex(ctx.text, conflict.index), `${change.character}'s ${change.resource} change is ${change.delta >= 0 ? "+" : ""}${change.delta}, but the text states the opposite-sign trigger "${conflict.trigger}"`)];
|
|
1576
1607
|
});
|
|
1577
1608
|
};
|
|
1578
1609
|
const timeRegression = (ctx) => {
|
|
@@ -1596,6 +1627,31 @@ const timeRegression = (ctx) => {
|
|
|
1596
1627
|
if (before === null || current >= before) return [];
|
|
1597
1628
|
return [v(CROSS_RULES.TIME_REGRESSION, "advisory", last.chapter, `story time goes back from ${previous.story_time} to ${last.story_time}`)];
|
|
1598
1629
|
};
|
|
1630
|
+
const mentionedNames$1 = (text, names) => {
|
|
1631
|
+
const found = [];
|
|
1632
|
+
for (const name of names) {
|
|
1633
|
+
if (name === "") continue;
|
|
1634
|
+
let index = text.indexOf(name);
|
|
1635
|
+
let present = false;
|
|
1636
|
+
while (index >= 0) {
|
|
1637
|
+
if (!names.some((other) => {
|
|
1638
|
+
if (other.length <= name.length || !other.includes(name)) return false;
|
|
1639
|
+
let otherIndex = text.indexOf(other);
|
|
1640
|
+
while (otherIndex >= 0) {
|
|
1641
|
+
if (index >= otherIndex && index + name.length <= otherIndex + other.length) return true;
|
|
1642
|
+
otherIndex = text.indexOf(other, otherIndex + 1);
|
|
1643
|
+
}
|
|
1644
|
+
return false;
|
|
1645
|
+
})) {
|
|
1646
|
+
present = true;
|
|
1647
|
+
break;
|
|
1648
|
+
}
|
|
1649
|
+
index = text.indexOf(name, index + name.length);
|
|
1650
|
+
}
|
|
1651
|
+
if (present) found.push(name);
|
|
1652
|
+
}
|
|
1653
|
+
return found;
|
|
1654
|
+
};
|
|
1599
1655
|
const relationUnknown = (ctx) => {
|
|
1600
1656
|
const characters = ctx.evidence.characters;
|
|
1601
1657
|
const known = characters === null ? [] : characters.characters.map((character) => character.name);
|
|
@@ -1607,7 +1663,7 @@ const relationUnknown = (ctx) => {
|
|
|
1607
1663
|
if (names.size < 2) return [];
|
|
1608
1664
|
const results = [];
|
|
1609
1665
|
for (const [index, paragraph] of splitParagraphs(ctx.text).entries()) {
|
|
1610
|
-
const present = [...names]
|
|
1666
|
+
const present = mentionedNames$1(paragraph, [...names]);
|
|
1611
1667
|
for (let first = 0; first < present.length; first += 1) for (let second = first + 1; second < present.length; second += 1) {
|
|
1612
1668
|
const a = present[first];
|
|
1613
1669
|
const b = present[second];
|
|
@@ -1616,7 +1672,7 @@ const relationUnknown = (ctx) => {
|
|
|
1616
1672
|
}
|
|
1617
1673
|
return results;
|
|
1618
1674
|
};
|
|
1619
|
-
const
|
|
1675
|
+
const describedMentions = (name, locked, text) => {
|
|
1620
1676
|
const candidates = [];
|
|
1621
1677
|
const strippedLock = locked.split(name).join("");
|
|
1622
1678
|
for (const [index, paragraph] of splitParagraphs(text).entries()) {
|
|
@@ -1632,9 +1688,9 @@ const lastDescribedMention = (name, locked, text, order) => {
|
|
|
1632
1688
|
coverage
|
|
1633
1689
|
});
|
|
1634
1690
|
}
|
|
1635
|
-
|
|
1636
|
-
return order === "first" ? candidates[0] : candidates[candidates.length - 1];
|
|
1691
|
+
return candidates;
|
|
1637
1692
|
};
|
|
1693
|
+
const bestMention = (mentions) => mentions.reduce((best, mention) => best === null || mention.coverage > best.coverage ? mention : best, null);
|
|
1638
1694
|
const lockedDetailDrift = (ctx) => {
|
|
1639
1695
|
if (ctx.evidence.locks === null) return [];
|
|
1640
1696
|
const locks = ctx.evidence.locks;
|
|
@@ -1642,16 +1698,16 @@ const lockedDetailDrift = (ctx) => {
|
|
|
1642
1698
|
for (const [name, lock] of Object.entries(locks.locations)) {
|
|
1643
1699
|
if (!ctx.text.includes(name)) continue;
|
|
1644
1700
|
if (lock.layout.trim() === "") continue;
|
|
1645
|
-
const mention =
|
|
1701
|
+
const mention = bestMention(describedMentions(name, lock.layout, ctx.text));
|
|
1646
1702
|
if (mention === null) continue;
|
|
1647
1703
|
if (mention.coverage < ctx.rulepack.crosschapter.lock_similarity) results.push(v(CROSS_RULES.LOCKED_DETAIL_DRIFT, "advisory", mention.index + 1, `${name} is described differently from its locked layout; expected "${lock.layout.slice(0, 40)}", actual "${mention.text.slice(0, 60)}"`));
|
|
1648
1704
|
}
|
|
1649
1705
|
for (const [name, lock] of Object.entries(locks.characters)) {
|
|
1650
1706
|
if (!ctx.text.includes(name)) continue;
|
|
1651
1707
|
if (lock.appearance.trim() === "") continue;
|
|
1652
|
-
const mention =
|
|
1708
|
+
const mention = bestMention(describedMentions(name, lock.appearance, ctx.text));
|
|
1653
1709
|
if (mention === null) continue;
|
|
1654
|
-
if (mention.coverage < ctx.rulepack.crosschapter.lock_similarity) results.push(v(CROSS_RULES.LOCKED_DETAIL_DRIFT, "advisory", mention.index + 1, `${name} is described differently from
|
|
1710
|
+
if (mention.coverage < ctx.rulepack.crosschapter.lock_similarity) results.push(v(CROSS_RULES.LOCKED_DETAIL_DRIFT, "advisory", mention.index + 1, `${name} is described differently from its locked appearance; expected "${lock.appearance.slice(0, 40)}", actual "${mention.text.slice(0, 60)}"`));
|
|
1655
1711
|
}
|
|
1656
1712
|
return results;
|
|
1657
1713
|
};
|
|
@@ -2138,6 +2194,23 @@ const validateSummary = (kind, value, path) => {
|
|
|
2138
2194
|
numberAt(kind, summary.hook_strength, `${path}.hook_strength`);
|
|
2139
2195
|
stringAt(kind, summary.plot_advancement, `${path}.plot_advancement`);
|
|
2140
2196
|
};
|
|
2197
|
+
const validateArchivedCard = (kind, value, path) => {
|
|
2198
|
+
if (value === void 0) return;
|
|
2199
|
+
const card = objectAt(kind, value, path);
|
|
2200
|
+
stringAt(kind, card.title, `${path}.title`);
|
|
2201
|
+
stringAt(kind, card.scene_type, `${path}.scene_type`);
|
|
2202
|
+
const target = listAt(kind, card.target_words, `${path}.target_words`);
|
|
2203
|
+
if (target.length !== 2) invalid$2(kind, `${path}.target_words must contain two numbers`);
|
|
2204
|
+
for (const [index, item] of target.entries()) numberAt(kind, item, `${path}.target_words[${index}]`);
|
|
2205
|
+
for (const [index, item] of listAt(kind, card.cast, `${path}.cast`).entries()) stringAt(kind, item, `${path}.cast[${index}]`);
|
|
2206
|
+
for (const key of [
|
|
2207
|
+
"goal",
|
|
2208
|
+
"conflict",
|
|
2209
|
+
"hook",
|
|
2210
|
+
"notes"
|
|
2211
|
+
]) stringAt(kind, card[key], `${path}.${key}`);
|
|
2212
|
+
nullableString(kind, card.archived_at, `${path}.archived_at`);
|
|
2213
|
+
};
|
|
2141
2214
|
const validateNested = (kind, record) => {
|
|
2142
2215
|
switch (kind) {
|
|
2143
2216
|
case "project": {
|
|
@@ -2186,6 +2259,8 @@ const validateNested = (kind, record) => {
|
|
|
2186
2259
|
validateSummary(kind, row.summary, `${path}.summary`);
|
|
2187
2260
|
if (row.assigned_ids !== void 0) objectAt(kind, row.assigned_ids, `${path}.assigned_ids`);
|
|
2188
2261
|
optionalString(kind, row.archive_digest, `${path}.archive_digest`);
|
|
2262
|
+
optionalString(kind, row.archive_card_digest, `${path}.archive_card_digest`);
|
|
2263
|
+
validateArchivedCard(kind, row.archive_card, `${path}.archive_card`);
|
|
2189
2264
|
optionalString(kind, row.final_check, `${path}.final_check`);
|
|
2190
2265
|
optionalString(kind, row.archived_at, `${path}.archived_at`);
|
|
2191
2266
|
if (row.rounds !== void 0) {
|
|
@@ -2528,7 +2603,13 @@ var FsStore = class {
|
|
|
2528
2603
|
/** Absolute path of an internal file outside the project root (locks live at root). */
|
|
2529
2604
|
async ensureDir(relativePath) {
|
|
2530
2605
|
const absolute = await this.safePath(relativePath);
|
|
2531
|
-
await
|
|
2606
|
+
await this.ensureParent(absolute);
|
|
2607
|
+
try {
|
|
2608
|
+
await promises.mkdir(absolute);
|
|
2609
|
+
} catch (error) {
|
|
2610
|
+
if (error.code !== "EEXIST") throw ioFailure(error);
|
|
2611
|
+
if (!(await promises.lstat(absolute)).isDirectory()) throw new EngineFaultError(ERROR_CODES.IO_PERMISSION, `refusing to use "${relativePath}": it exists but is not a directory`, "remove or rename the conflicting file, then retry");
|
|
2612
|
+
}
|
|
2532
2613
|
return absolute;
|
|
2533
2614
|
}
|
|
2534
2615
|
async exists(relativePath) {
|
|
@@ -3163,13 +3244,23 @@ var FileLock = class {
|
|
|
3163
3244
|
if (record === null) {
|
|
3164
3245
|
if (Date.now() - mtimeMs <= this.staleMs) return false;
|
|
3165
3246
|
} else if (lockRecordIsLive(record)) return false;
|
|
3166
|
-
const claimPath = `${this.absolutePath}.reclaim
|
|
3247
|
+
const claimPath = `${this.absolutePath}.reclaim`;
|
|
3167
3248
|
let claimByRename = false;
|
|
3168
3249
|
try {
|
|
3169
3250
|
await promises.link(this.absolutePath, claimPath);
|
|
3170
3251
|
} catch (error) {
|
|
3171
3252
|
const code = error.code;
|
|
3172
3253
|
if (code === "ENOENT") return true;
|
|
3254
|
+
if (code === "EEXIST") {
|
|
3255
|
+
const claimMtime = await promises.stat(claimPath).then((info) => {
|
|
3256
|
+
return info.nlink > 1 ? info.ctimeMs : info.mtimeMs;
|
|
3257
|
+
}).catch(() => Date.now());
|
|
3258
|
+
if (Date.now() - claimMtime > this.staleMs) {
|
|
3259
|
+
await promises.rm(claimPath, { force: true }).catch(() => void 0);
|
|
3260
|
+
return true;
|
|
3261
|
+
}
|
|
3262
|
+
return false;
|
|
3263
|
+
}
|
|
3173
3264
|
if (code === "EPERM" || code === "EACCES") return false;
|
|
3174
3265
|
if ([
|
|
3175
3266
|
"ENOSYS",
|
|
@@ -3218,7 +3309,9 @@ const BACKUP_MANIFEST = "manifest.json";
|
|
|
3218
3309
|
/** Backup transaction directories, newest first by filesystem mtime. */
|
|
3219
3310
|
async function listBackups(store) {
|
|
3220
3311
|
const entries = [];
|
|
3221
|
-
for (const
|
|
3312
|
+
for (const entry of await store.entries(BACKUP_DIR)) {
|
|
3313
|
+
if (!entry.directory || entry.symbolicLink) continue;
|
|
3314
|
+
const name = entry.name;
|
|
3222
3315
|
if (name === "migrations") continue;
|
|
3223
3316
|
const info = await store.stat(joinRel(BACKUP_DIR, name));
|
|
3224
3317
|
if (info === null || !info.isDirectory) continue;
|
|
@@ -3379,6 +3472,7 @@ async function runTransaction(store, input) {
|
|
|
3379
3472
|
for (const file of input.files) {
|
|
3380
3473
|
const existing = before.get(file.path);
|
|
3381
3474
|
if (existing === null || existing === void 0) continue;
|
|
3475
|
+
await assertRootPresent();
|
|
3382
3476
|
await store.writeBytes(joinRel(staging, "before", file.path), existing);
|
|
3383
3477
|
}
|
|
3384
3478
|
if (input.backup) {
|
|
@@ -3386,8 +3480,10 @@ async function runTransaction(store, input) {
|
|
|
3386
3480
|
for (const file of input.files) {
|
|
3387
3481
|
const existing = before.get(file.path);
|
|
3388
3482
|
if (existing === null || existing === void 0) continue;
|
|
3483
|
+
await assertRootPresent();
|
|
3389
3484
|
await store.writeBytes(joinRel(stagingBackup, file.path), existing);
|
|
3390
3485
|
}
|
|
3486
|
+
await assertRootPresent();
|
|
3391
3487
|
await store.writeText(joinRel(stagingBackup, BACKUP_MARKER), "1");
|
|
3392
3488
|
await store.writeText(joinRel(stagingBackup, BACKUP_MANIFEST), `${JSON.stringify({
|
|
3393
3489
|
schema_version: 1,
|
|
@@ -3399,9 +3495,11 @@ async function runTransaction(store, input) {
|
|
|
3399
3495
|
...input.reason === void 0 ? {} : { reason: input.reason },
|
|
3400
3496
|
files: input.files.filter((file) => before.get(file.path) !== null && before.get(file.path) !== void 0).map((file) => file.path)
|
|
3401
3497
|
}, null, 2)}\n`);
|
|
3402
|
-
|
|
3403
|
-
await
|
|
3404
|
-
await
|
|
3498
|
+
await assertRootPresent();
|
|
3499
|
+
await store.ensureDir(BACKUP_DIR);
|
|
3500
|
+
await assertRootPresent();
|
|
3501
|
+
const targetBackup = await store.safePath(backupDir(input.txid));
|
|
3502
|
+
await promises.rename(await store.safePath(stagingBackup), targetBackup);
|
|
3405
3503
|
await syncDirectory(dirname(targetBackup));
|
|
3406
3504
|
backupMarked = true;
|
|
3407
3505
|
backupComplete = true;
|
|
@@ -4124,7 +4222,7 @@ function outlineSummary(document, archivedCount, target = null, archivedNumbers)
|
|
|
4124
4222
|
missingCards,
|
|
4125
4223
|
plannedCoverage,
|
|
4126
4224
|
archivedCoverage,
|
|
4127
|
-
coverage: plannedCoverage ??
|
|
4225
|
+
coverage: plannedCoverage ?? cardCoverage,
|
|
4128
4226
|
cardCoverage,
|
|
4129
4227
|
volumes: document.volumes.length
|
|
4130
4228
|
};
|
|
@@ -4305,12 +4403,38 @@ function normalizeOutlineChapters(input, options) {
|
|
|
4305
4403
|
if (target[0] !== base.target_words[0] || target[1] !== base.target_words[1]) throw new EngineInputError(ERROR_CODES.SCHEMA_INVALID, `chapter ${num} is archived; its target_words cannot change`, "call novel_reopen first if the manuscript must be revised");
|
|
4306
4404
|
const cast = raw.cast === void 0 ? base.cast : stringArray$1(raw.cast, `chapters[${index}].cast`, 8);
|
|
4307
4405
|
if (cast.length !== base.cast.length || cast.some((name) => !base.cast.includes(name))) throw new EngineInputError(ERROR_CODES.SCHEMA_INVALID, `chapter ${num} is archived; its cast cannot change`, "call novel_reopen first if the manuscript must be revised");
|
|
4406
|
+
const goal = raw.goal === void 0 || raw.goal === base.goal ? base.goal : text$2(raw.goal, `chapter ${num} goal`);
|
|
4407
|
+
const conflict = raw.conflict === void 0 || raw.conflict === base.conflict ? base.conflict : text$2(raw.conflict, `chapter ${num} conflict`);
|
|
4408
|
+
const hook = raw.hook === void 0 || raw.hook === base.hook ? base.hook : text$2(raw.hook, `chapter ${num} hook`);
|
|
4409
|
+
const notes = raw.notes === void 0 || raw.notes === base.notes ? base.notes : optionalText(raw.notes, `chapter ${num} notes`, LIMITS.maxOutlineNotesChars);
|
|
4410
|
+
for (const [field, value, previous] of [
|
|
4411
|
+
[
|
|
4412
|
+
"goal",
|
|
4413
|
+
goal,
|
|
4414
|
+
base.goal
|
|
4415
|
+
],
|
|
4416
|
+
[
|
|
4417
|
+
"conflict",
|
|
4418
|
+
conflict,
|
|
4419
|
+
base.conflict
|
|
4420
|
+
],
|
|
4421
|
+
[
|
|
4422
|
+
"hook",
|
|
4423
|
+
hook,
|
|
4424
|
+
base.hook
|
|
4425
|
+
],
|
|
4426
|
+
[
|
|
4427
|
+
"notes",
|
|
4428
|
+
notes,
|
|
4429
|
+
base.notes
|
|
4430
|
+
]
|
|
4431
|
+
]) if (value !== previous) throw new EngineInputError(ERROR_CODES.SCHEMA_INVALID, `chapter ${num} is archived; its ${field} cannot change`, "call novel_reopen first if the manuscript must be revised");
|
|
4308
4432
|
normalized.set(num, {
|
|
4309
4433
|
...base,
|
|
4310
|
-
goal
|
|
4311
|
-
conflict
|
|
4312
|
-
hook
|
|
4313
|
-
notes
|
|
4434
|
+
goal,
|
|
4435
|
+
conflict,
|
|
4436
|
+
hook,
|
|
4437
|
+
notes,
|
|
4314
4438
|
volume: volumeForChapter(options.volumes, num, raw.volume === void 0 ? base.volume : raw.volume),
|
|
4315
4439
|
status: "archived",
|
|
4316
4440
|
archived_at: base.archived_at ?? record.archived_at ?? null,
|
|
@@ -4718,7 +4842,7 @@ const pick = (values, mentioned, key, limit) => {
|
|
|
4718
4842
|
return [...matched, ...rest].slice(0, limit);
|
|
4719
4843
|
};
|
|
4720
4844
|
/** Protect the brief budget from a legal-but-huge planning document. */
|
|
4721
|
-
const clip = (value, limit) => {
|
|
4845
|
+
const clip$1 = (value, limit) => {
|
|
4722
4846
|
const chars = Array.from(value);
|
|
4723
4847
|
return chars.length <= limit ? value : `${chars.slice(0, limit).join("")}…`;
|
|
4724
4848
|
};
|
|
@@ -4727,44 +4851,44 @@ function worldbookBrief(document, mentioned, names) {
|
|
|
4727
4851
|
const named = names.join("\n");
|
|
4728
4852
|
const characters = pick(document.characters, named === "" ? mentioned : named, (entry) => entry.name, 8);
|
|
4729
4853
|
return {
|
|
4730
|
-
premise: clip(document.premise, 600),
|
|
4731
|
-
setting: clip(document.setting, 600),
|
|
4732
|
-
themes: document.themes.slice(0, 12).map((theme) => clip(theme, 80)),
|
|
4854
|
+
premise: clip$1(document.premise, 600),
|
|
4855
|
+
setting: clip$1(document.setting, 600),
|
|
4856
|
+
themes: document.themes.slice(0, 12).map((theme) => clip$1(theme, 80)),
|
|
4733
4857
|
characters: characters.map((entry) => ({
|
|
4734
4858
|
name: entry.name,
|
|
4735
4859
|
role: entry.role,
|
|
4736
|
-
description: clip(entry.description, 240),
|
|
4737
|
-
arc: clip(entry.arc, 160)
|
|
4860
|
+
description: clip$1(entry.description, 240),
|
|
4861
|
+
arc: clip$1(entry.arc, 160)
|
|
4738
4862
|
})),
|
|
4739
4863
|
locations: pick(document.locations, mentioned, (entry) => entry.name, 6).map((entry) => ({
|
|
4740
4864
|
name: entry.name,
|
|
4741
|
-
description: clip(entry.description, 240),
|
|
4742
|
-
layout: clip(entry.layout, 120),
|
|
4865
|
+
description: clip$1(entry.description, 240),
|
|
4866
|
+
layout: clip$1(entry.layout, 120),
|
|
4743
4867
|
capacity: entry.capacity
|
|
4744
4868
|
})),
|
|
4745
4869
|
factions: pick(document.factions, mentioned, (entry) => entry.name, 6).map((entry) => ({
|
|
4746
4870
|
name: entry.name,
|
|
4747
|
-
description: clip(entry.description, 240),
|
|
4748
|
-
goal: clip(entry.goal, 120)
|
|
4871
|
+
description: clip$1(entry.description, 240),
|
|
4872
|
+
goal: clip$1(entry.goal, 120)
|
|
4749
4873
|
})),
|
|
4750
4874
|
rules: pick(document.rules, mentioned, (entry) => entry.name, 6).map((entry) => ({
|
|
4751
4875
|
name: entry.name,
|
|
4752
|
-
description: clip(entry.description, 240)
|
|
4876
|
+
description: clip$1(entry.description, 240)
|
|
4753
4877
|
})),
|
|
4754
4878
|
glossary: pick(document.glossary, mentioned, (entry) => entry.term, 6).map((entry) => ({
|
|
4755
4879
|
term: entry.term,
|
|
4756
|
-
definition: clip(entry.definition, 160)
|
|
4880
|
+
definition: clip$1(entry.definition, 160)
|
|
4757
4881
|
})),
|
|
4758
4882
|
backstory: document.backstory.slice(0, 3).map((entry) => ({
|
|
4759
4883
|
time: entry.time,
|
|
4760
|
-
event: clip(entry.event, 160)
|
|
4884
|
+
event: clip$1(entry.event, 160)
|
|
4761
4885
|
})),
|
|
4762
4886
|
facts: (document.facts ?? []).slice(0, 20).map((fact) => ({
|
|
4763
|
-
alias: clip(fact.alias, 120),
|
|
4887
|
+
alias: clip$1(fact.alias, 120),
|
|
4764
4888
|
value: fact.value,
|
|
4765
|
-
unit: clip(fact.unit, 40),
|
|
4889
|
+
unit: clip$1(fact.unit, 40),
|
|
4766
4890
|
tolerance: fact.tolerance ?? 0,
|
|
4767
|
-
...fact.note === void 0 ? {} : { note: clip(fact.note, 160) }
|
|
4891
|
+
...fact.note === void 0 ? {} : { note: clip$1(fact.note, 160) }
|
|
4768
4892
|
}))
|
|
4769
4893
|
};
|
|
4770
4894
|
}
|
|
@@ -4772,14 +4896,14 @@ function worldbookBrief(document, mentioned, names) {
|
|
|
4772
4896
|
function bookBrief(document, chapter) {
|
|
4773
4897
|
const volume = document.volumes.find((candidate) => chapter >= candidate.start_chapter && chapter <= candidate.end_chapter) ?? null;
|
|
4774
4898
|
return {
|
|
4775
|
-
premise: clip(document.book.premise, 600),
|
|
4776
|
-
logline: clip(document.book.logline, 600),
|
|
4777
|
-
themes: document.book.themes.slice(0, 12).map((theme) => clip(theme, 80)),
|
|
4778
|
-
tone: clip(document.book.tone, 200),
|
|
4779
|
-
pov: clip(document.book.pov, 200),
|
|
4780
|
-
audience: clip(document.book.audience, 200),
|
|
4781
|
-
selling_points: document.book.selling_points.slice(0, 12).map((point) => clip(point, 120)),
|
|
4782
|
-
ending: clip(document.book.ending, 400),
|
|
4899
|
+
premise: clip$1(document.book.premise, 600),
|
|
4900
|
+
logline: clip$1(document.book.logline, 600),
|
|
4901
|
+
themes: document.book.themes.slice(0, 12).map((theme) => clip$1(theme, 80)),
|
|
4902
|
+
tone: clip$1(document.book.tone, 200),
|
|
4903
|
+
pov: clip$1(document.book.pov, 200),
|
|
4904
|
+
audience: clip$1(document.book.audience, 200),
|
|
4905
|
+
selling_points: document.book.selling_points.slice(0, 12).map((point) => clip$1(point, 120)),
|
|
4906
|
+
ending: clip$1(document.book.ending, 400),
|
|
4783
4907
|
strict: document.book.strict,
|
|
4784
4908
|
volume,
|
|
4785
4909
|
card: outlineCard(document, chapter)
|
|
@@ -4853,9 +4977,11 @@ function parseOutlineMarkdown(content) {
|
|
|
4853
4977
|
if (currentVolume === null) return;
|
|
4854
4978
|
const volume = currentVolume;
|
|
4855
4979
|
const chapters = chapterInputs.filter((card) => card.volume === volume.order);
|
|
4856
|
-
if (chapters.length > 0) {
|
|
4980
|
+
if (chapters.length > 0 && !volume.explicitRange) {
|
|
4857
4981
|
volume.start_chapter = Math.min(volume.start_chapter, ...chapters.map((card) => card.num));
|
|
4858
4982
|
volume.end_chapter = Math.max(volume.end_chapter, ...chapters.map((card) => card.num));
|
|
4983
|
+
} else if (chapters.length > 0) {
|
|
4984
|
+
for (const card of chapters) if (card.num < volume.start_chapter || card.num > volume.end_chapter) throw new EngineInputError(ERROR_CODES.SCHEMA_INVALID, `Markdown volume "${volume.title}" declares 第${volume.start_chapter}-${volume.end_chapter}章 but contains chapter ${card.num}`, "fix the declared range or move the card into the right volume");
|
|
4859
4985
|
} else if (!volume.explicitRange) {
|
|
4860
4986
|
warnings.push(`volume "${volume.title}" has neither a chapter range nor chapter cards; it was ignored`);
|
|
4861
4987
|
volumeInputs.pop();
|
|
@@ -5138,6 +5264,7 @@ const mergeVolumes = (options) => {
|
|
|
5138
5264
|
}
|
|
5139
5265
|
}
|
|
5140
5266
|
const byTitle = volumeByTitle(options.existingVolumes);
|
|
5267
|
+
const byId = new Map(options.existingVolumes.map((volume) => [volume.id, volume]));
|
|
5141
5268
|
const usedIds = new Set(options.existingVolumes.map((volume) => volume.id));
|
|
5142
5269
|
let nextGeneratedId = 1;
|
|
5143
5270
|
const generatedId = () => {
|
|
@@ -5148,21 +5275,39 @@ const mergeVolumes = (options) => {
|
|
|
5148
5275
|
return id;
|
|
5149
5276
|
};
|
|
5150
5277
|
const submitted = /* @__PURE__ */ new Set();
|
|
5278
|
+
const submittedIds = /* @__PURE__ */ new Set();
|
|
5279
|
+
const generatedIds = /* @__PURE__ */ new Set();
|
|
5151
5280
|
for (const [index, raw] of (options.volumes ?? []).entries()) {
|
|
5152
5281
|
if (!isRecord$2(raw)) throw new EngineInputError(ERROR_CODES.SCHEMA_INVALID, `volumes[${index}] must be an object`);
|
|
5153
5282
|
const title = text$1(raw.title, `volumes[${index}].title`);
|
|
5154
5283
|
if (submitted.has(title)) throw new EngineInputError(ERROR_CODES.SCHEMA_INVALID, `volumes contains duplicate title "${title}"`);
|
|
5155
5284
|
submitted.add(title);
|
|
5156
|
-
const base = byTitle.get(title);
|
|
5157
5285
|
const explicitId = typeof raw.id === "string" && raw.id.trim() !== "" ? raw.id.trim() : void 0;
|
|
5158
|
-
|
|
5286
|
+
if (explicitId !== void 0 && (submittedIds.has(explicitId) || generatedIds.has(explicitId))) throw new EngineInputError(ERROR_CODES.SCHEMA_INVALID, `volumes contains duplicate id "${explicitId}"`);
|
|
5287
|
+
if (explicitId !== void 0) submittedIds.add(explicitId);
|
|
5288
|
+
const byIdBase = explicitId === void 0 ? void 0 : byId.get(explicitId);
|
|
5289
|
+
const titleBase = byTitle.get(title);
|
|
5290
|
+
if (explicitId !== void 0 && byIdBase === void 0 && usedIds.has(explicitId)) throw new EngineInputError(ERROR_CODES.SCHEMA_INVALID, `volume id "${explicitId}" already belongs to another volume`);
|
|
5291
|
+
if (explicitId !== void 0 && titleBase !== void 0 && titleBase.id !== explicitId) throw new EngineInputError(ERROR_CODES.SCHEMA_INVALID, `volume "${title}" cannot change its stable id from "${titleBase.id}" to "${explicitId}"`);
|
|
5292
|
+
const base = byIdBase ?? titleBase;
|
|
5293
|
+
let id;
|
|
5294
|
+
if (base !== void 0) id = base.id;
|
|
5295
|
+
else if (explicitId !== void 0) id = explicitId;
|
|
5296
|
+
else {
|
|
5297
|
+
id = generatedId();
|
|
5298
|
+
generatedIds.add(id);
|
|
5299
|
+
}
|
|
5300
|
+
if (base !== void 0 && explicitId !== void 0 && base.id !== explicitId) throw new EngineInputError(ERROR_CODES.SCHEMA_INVALID, `volume "${base.title}" cannot change its stable id from "${base.id}" to "${explicitId}"`);
|
|
5301
|
+
if (base !== void 0 && base.title !== title) byTitle.delete(base.title);
|
|
5159
5302
|
if (!usedIds.has(id)) usedIds.add(id);
|
|
5160
|
-
|
|
5303
|
+
const next = {
|
|
5161
5304
|
...base ?? {},
|
|
5162
5305
|
...raw,
|
|
5163
5306
|
id,
|
|
5164
5307
|
title
|
|
5165
|
-
}
|
|
5308
|
+
};
|
|
5309
|
+
byTitle.set(title, next);
|
|
5310
|
+
byId.set(id, next);
|
|
5166
5311
|
}
|
|
5167
5312
|
for (const title of removed) {
|
|
5168
5313
|
if (submitted.has(title)) throw new EngineInputError(ERROR_CODES.SCHEMA_INVALID, `volume "${title}" cannot be both submitted and removed`);
|
|
@@ -5170,6 +5315,7 @@ const mergeVolumes = (options) => {
|
|
|
5170
5315
|
if (existing === void 0) throw new EngineInputError(ERROR_CODES.SCHEMA_INVALID, `cannot remove volume "${title}": it is not in the plan`);
|
|
5171
5316
|
if (existing.status === "done") throw new EngineInputError(ERROR_CODES.SCHEMA_INVALID, `cannot remove volume "${title}": its status is done`, "a completed volume is archive evidence; remove only planned/writing volumes");
|
|
5172
5317
|
byTitle.delete(title);
|
|
5318
|
+
byId.delete(existing.id);
|
|
5173
5319
|
}
|
|
5174
5320
|
const volumes = normalizeVolumes([...byTitle.values()], { totalChapters: options.totalChapters });
|
|
5175
5321
|
const before = volumeByTitle(options.existingVolumes);
|
|
@@ -5485,9 +5631,10 @@ const requireForeshadowFields = (value, where) => {
|
|
|
5485
5631
|
if (value.note !== void 0 && typeof value.note !== "string") fail$1(`${where}.note must be a string`);
|
|
5486
5632
|
if (value.new_info !== void 0 && typeof value.new_info !== "boolean") fail$1(`${where}.new_info must be a boolean`);
|
|
5487
5633
|
};
|
|
5634
|
+
const withoutQuotedSpeech = (text) => text.replace(/"[^"]*"|“[^”]*”|「[^」]*」|『[^』]*』/g, "");
|
|
5488
5635
|
const appearanceOf = (text, name, aliases = []) => {
|
|
5489
5636
|
const paragraphs = splitParagraphs(text);
|
|
5490
|
-
const source = paragraphs.find((paragraph) => paragraph.includes(name)
|
|
5637
|
+
const source = paragraphs.find((paragraph) => withoutQuotedSpeech(paragraph).includes(name)) ?? paragraphs.find((paragraph) => paragraph.includes(name)) ?? paragraphs.find((paragraph) => aliases.some((alias) => alias !== "" && withoutQuotedSpeech(paragraph).includes(alias))) ?? paragraphs[0] ?? "";
|
|
5491
5638
|
return Array.from(source).slice(0, 120).join("");
|
|
5492
5639
|
};
|
|
5493
5640
|
const named = (value) => isRecord(value) && typeof value.name === "string";
|
|
@@ -5578,12 +5725,8 @@ function applyStateDelta(input) {
|
|
|
5578
5725
|
const appeared = [...new Set(summary.characters_appeared)];
|
|
5579
5726
|
const byName = new Map(characters.characters.map((character) => [character.name, character]));
|
|
5580
5727
|
const narrator = input.narrator ?? "";
|
|
5581
|
-
const narratorName = narrator
|
|
5582
|
-
const
|
|
5583
|
-
const appearsInManuscript = (name) => {
|
|
5584
|
-
if (input.text.includes(name)) return true;
|
|
5585
|
-
return name === narratorName && narratorAliases.some((alias) => alias !== "" && input.text.includes(alias));
|
|
5586
|
-
};
|
|
5728
|
+
const narratorName$1 = narratorName(narrator);
|
|
5729
|
+
const appearsInManuscript = (name) => isNarratorAlias(name, narrator, input.text);
|
|
5587
5730
|
for (const name of appeared) {
|
|
5588
5731
|
if (!appearsInManuscript(name)) fail$1(`character "${name}" is listed in characters_appeared but does not appear in the manuscript`);
|
|
5589
5732
|
if (!Object.hasOwn(delta.characters, name)) fail$1(`character "${name}" appeared in the chapter but is not declared in characters`);
|
|
@@ -5605,16 +5748,20 @@ function applyStateDelta(input) {
|
|
|
5605
5748
|
if (existing !== void 0) fail$1(`character "${name}" already exists; use updated or no_change`);
|
|
5606
5749
|
if (!appeared.includes(name)) fail$1(`character "${name}" is created but does not appear in characters_appeared`);
|
|
5607
5750
|
const rawHint = input.roleHints?.[name];
|
|
5608
|
-
const hinted = canonicalRole(rawHint) ?? (rawHint === void 0 && (name === narratorName || name === narrator) ? "protagonist" : "extra");
|
|
5751
|
+
const hinted = canonicalRole(rawHint) ?? (rawHint === void 0 && (name === narratorName$1 || name === narrator) ? "protagonist" : "extra");
|
|
5609
5752
|
const currentState = declared.current_state ?? {};
|
|
5610
5753
|
validateResources(currentState, `characters.${name}`);
|
|
5754
|
+
const growth = declared.growth === void 0 ? [] : [{
|
|
5755
|
+
...declared.growth,
|
|
5756
|
+
chapter
|
|
5757
|
+
}];
|
|
5611
5758
|
const created = {
|
|
5612
5759
|
name,
|
|
5613
5760
|
role: normalizeRole(declared.role, hinted),
|
|
5614
5761
|
first_appearance: chapter,
|
|
5615
5762
|
profile: declared.profile ?? {},
|
|
5616
5763
|
current_state: currentState,
|
|
5617
|
-
growth
|
|
5764
|
+
growth,
|
|
5618
5765
|
updated_at: chapter
|
|
5619
5766
|
};
|
|
5620
5767
|
characters.characters.push(created);
|
|
@@ -5626,7 +5773,7 @@ function applyStateDelta(input) {
|
|
|
5626
5773
|
const rawHint = input.roleHints?.[name];
|
|
5627
5774
|
const hinted = canonicalRole(rawHint);
|
|
5628
5775
|
if (hinted !== null && hinted !== "extra") existing.role = hinted;
|
|
5629
|
-
else if (rawHint === void 0 && (name === narratorName || name === narrator)) existing.role = "protagonist";
|
|
5776
|
+
else if (rawHint === void 0 && (name === narratorName$1 || name === narrator)) existing.role = "protagonist";
|
|
5630
5777
|
}
|
|
5631
5778
|
if (declared.role !== void 0) existing.role = normalizeRole(declared.role, existing.role);
|
|
5632
5779
|
if (declared.action === "updated") {
|
|
@@ -5809,7 +5956,7 @@ function applyStateDelta(input) {
|
|
|
5809
5956
|
setOwn(locks.characters, name, {
|
|
5810
5957
|
first_appearance: character.first_appearance,
|
|
5811
5958
|
locked_at: chapter,
|
|
5812
|
-
appearance: appearanceOf(input.text, name, name
|
|
5959
|
+
appearance: appearanceOf(input.text, name, narratorAliasesFor(name, narrator))
|
|
5813
5960
|
});
|
|
5814
5961
|
}
|
|
5815
5962
|
for (const location of world.locations) if (!Object.hasOwn(locks.locations, location.name)) setOwn(locks.locations, location.name, {
|
|
@@ -6004,6 +6151,11 @@ function applyStateDelta(input) {
|
|
|
6004
6151
|
}
|
|
6005
6152
|
// src/engine/insight.ts
|
|
6006
6153
|
const average = (values) => values.length === 0 ? 0 : values.reduce((total, value) => total + value, 0) / values.length;
|
|
6154
|
+
const clip = (value, max) => {
|
|
6155
|
+
const chars = Array.from(value);
|
|
6156
|
+
return chars.length <= max ? value : `${chars.slice(0, Math.max(0, max - 1)).join("")}…`;
|
|
6157
|
+
};
|
|
6158
|
+
const clipList = (values, max) => values.map((value) => clip(value, max));
|
|
6007
6159
|
const median = (values) => {
|
|
6008
6160
|
if (values.length === 0) return 0;
|
|
6009
6161
|
const sorted = [...values].sort((left, right) => left - right);
|
|
@@ -6051,7 +6203,7 @@ function buildBookReport(input) {
|
|
|
6051
6203
|
}
|
|
6052
6204
|
const chapterReviews = [...finalReviews.values()];
|
|
6053
6205
|
const qualitySamples = chapterReviews.length;
|
|
6054
|
-
const qualityAttempts =
|
|
6206
|
+
const qualityAttempts = input.reviewAttempts ?? input.reviews.length;
|
|
6055
6207
|
const trendBasis = qualitySamples < 3 ? "insufficient_samples" : previous.length < 5 ? "insufficient_history" : "last_five_vs_previous_five";
|
|
6056
6208
|
const trend = trendBasis === "last_five_vs_previous_five" ? Math.round((average(lastFive) - average(previous)) * 10) / 10 : null;
|
|
6057
6209
|
const dimensions = /* @__PURE__ */ new Map();
|
|
@@ -6156,7 +6308,7 @@ function buildBookReport(input) {
|
|
|
6156
6308
|
target: input.targetChapters,
|
|
6157
6309
|
words,
|
|
6158
6310
|
average_words: averageWords,
|
|
6159
|
-
percent: input.targetChapters === null || input.targetChapters === 0 ?
|
|
6311
|
+
percent: input.targetChapters === null || input.targetChapters === 0 ? null : Math.min(100, Math.round(records.length / input.targetChapters * 1e3) / 10),
|
|
6160
6312
|
remaining_words: averageRemaining,
|
|
6161
6313
|
remaining_words_min: minimumRemaining,
|
|
6162
6314
|
remaining_words_average: averageRemaining,
|
|
@@ -6194,7 +6346,8 @@ function buildBookReport(input) {
|
|
|
6194
6346
|
dimensions: [...dimensions.entries()].map(([id, value]) => ({
|
|
6195
6347
|
id,
|
|
6196
6348
|
average: Math.round(value.total / value.samples * 10) / 10,
|
|
6197
|
-
samples: value.samples
|
|
6349
|
+
samples: value.samples,
|
|
6350
|
+
max: input.dimensionMax?.[id] ?? 0
|
|
6198
6351
|
})).sort((left, right) => compareText(left.id, right.id))
|
|
6199
6352
|
},
|
|
6200
6353
|
effort: {
|
|
@@ -6330,8 +6483,8 @@ function buildGuide(input) {
|
|
|
6330
6483
|
},
|
|
6331
6484
|
{
|
|
6332
6485
|
id: "write",
|
|
6333
|
-
title: input.finished ? "全书章节已写完" : `写第 ${input.chapter} 章`,
|
|
6334
|
-
detail: input.finished ? `已完成 ${input.archived} 章;下一步是导出成书。` : `当前阶段 ${input.stage};${input.blocking === 0 ? "没有阻塞" : `${input.blocking} 条阻塞`}。`,
|
|
6486
|
+
title: input.finished ? "全书章节已写完" : input.progress?.current !== null && input.progress?.current !== void 0 ? `写第 ${input.progress.current.chapter} 章` : input.progress?.next !== null && input.progress?.next !== void 0 ? `准备第 ${input.progress.next.chapter} 章(P1)` : `写第 ${input.chapter} 章`,
|
|
6487
|
+
detail: input.finished ? `已完成 ${input.archived} 章;下一步是导出成书。` : `${input.progress?.label ?? `当前阶段 ${input.stage}`};${input.blocking === 0 ? "没有阻塞" : `${input.blocking} 条阻塞`}。`,
|
|
6335
6488
|
done: input.finished,
|
|
6336
6489
|
tool: input.next.tool
|
|
6337
6490
|
},
|
|
@@ -6345,7 +6498,7 @@ function buildGuide(input) {
|
|
|
6345
6498
|
];
|
|
6346
6499
|
return {
|
|
6347
6500
|
state: input.finished ? "finished" : input.stateErrors > 0 || input.blocking > 0 ? "blocked" : input.stage === "idle" && input.archived === 0 && !outlineDone && !blueprintDone && !worldbookDone ? "new" : input.stage === "idle" && input.archived === 0 ? "planning" : "writing",
|
|
6348
|
-
title: input.finished ? "全书已完成" : `第 ${input.chapter} 章 · ${input.stage}`,
|
|
6501
|
+
title: input.finished ? "全书已完成" : input.progress?.label ?? `第 ${input.chapter} 章 · ${input.stage}`,
|
|
6349
6502
|
steps,
|
|
6350
6503
|
next: input.next,
|
|
6351
6504
|
commands: [
|
|
@@ -6602,13 +6755,13 @@ const ledgerWorldEntries = (world, kind) => {
|
|
|
6602
6755
|
const nameDescription = (entries, world, kind = "location") => {
|
|
6603
6756
|
const merged = /* @__PURE__ */ new Map();
|
|
6604
6757
|
for (const entry of [...entries, ...ledgerWorldEntries(world, kind)]) {
|
|
6605
|
-
const name = oneLine(entry.name);
|
|
6758
|
+
const name = clip(oneLine(entry.name), 80);
|
|
6606
6759
|
if (name === "") continue;
|
|
6607
6760
|
const existing = merged.get(name);
|
|
6608
6761
|
if (existing === void 0) {
|
|
6609
6762
|
merged.set(name, {
|
|
6610
6763
|
name,
|
|
6611
|
-
description: oneLine(entry.description),
|
|
6764
|
+
description: clip(oneLine(entry.description), 240),
|
|
6612
6765
|
...entry.anchors === void 0 || entry.anchors.length === 0 ? {} : { anchors: [...entry.anchors] }
|
|
6613
6766
|
});
|
|
6614
6767
|
continue;
|
|
@@ -6628,12 +6781,12 @@ function buildPanelBookData(input) {
|
|
|
6628
6781
|
const card = input.outline.chapters.find((candidate) => candidate.num === input.currentChapter);
|
|
6629
6782
|
const currentCard = card === void 0 ? null : {
|
|
6630
6783
|
num: card.num,
|
|
6631
|
-
title: card.title,
|
|
6784
|
+
title: clip(card.title, 120),
|
|
6632
6785
|
scene_type: card.scene_type,
|
|
6633
|
-
goal: card.goal,
|
|
6634
|
-
conflict: card.conflict,
|
|
6635
|
-
hook: card.hook,
|
|
6636
|
-
cast: [...card.cast],
|
|
6786
|
+
goal: clip(card.goal, 400),
|
|
6787
|
+
conflict: clip(card.conflict, 400),
|
|
6788
|
+
hook: clip(card.hook, 400),
|
|
6789
|
+
cast: [...card.cast].slice(0, 8).map((name) => clip(name, 80)),
|
|
6637
6790
|
target_words: [card.target_words[0], card.target_words[1]],
|
|
6638
6791
|
status: card.status
|
|
6639
6792
|
};
|
|
@@ -6650,8 +6803,8 @@ function buildPanelBookData(input) {
|
|
|
6650
6803
|
};
|
|
6651
6804
|
return {
|
|
6652
6805
|
identity: {
|
|
6653
|
-
author: input.project.author,
|
|
6654
|
-
description: input.project.description,
|
|
6806
|
+
author: clip(input.project.author, 80),
|
|
6807
|
+
description: clip(input.project.description, 400),
|
|
6655
6808
|
created_at: input.project.created_at,
|
|
6656
6809
|
targets: {
|
|
6657
6810
|
words_per_chapter: [input.project.targets.words_per_chapter[0], input.project.targets.words_per_chapter[1]],
|
|
@@ -6660,14 +6813,14 @@ function buildPanelBookData(input) {
|
|
|
6660
6813
|
}
|
|
6661
6814
|
},
|
|
6662
6815
|
blueprint: {
|
|
6663
|
-
premise: input.outline.book.premise,
|
|
6664
|
-
logline: input.outline.book.logline,
|
|
6665
|
-
themes:
|
|
6666
|
-
tone: input.outline.book.tone,
|
|
6667
|
-
pov: input.outline.book.pov,
|
|
6668
|
-
audience: input.outline.book.audience,
|
|
6669
|
-
selling_points:
|
|
6670
|
-
ending: input.outline.book.ending
|
|
6816
|
+
premise: clip(input.outline.book.premise, 400),
|
|
6817
|
+
logline: clip(input.outline.book.logline, 400),
|
|
6818
|
+
themes: clipList(input.outline.book.themes, 120).slice(0, 20),
|
|
6819
|
+
tone: clip(input.outline.book.tone, 400),
|
|
6820
|
+
pov: clip(input.outline.book.pov, 400),
|
|
6821
|
+
audience: clip(input.outline.book.audience, 400),
|
|
6822
|
+
selling_points: clipList(input.outline.book.selling_points, 200).slice(0, 20),
|
|
6823
|
+
ending: clip(input.outline.book.ending, 400)
|
|
6671
6824
|
},
|
|
6672
6825
|
progress: {
|
|
6673
6826
|
archived: report.progress.archived,
|
|
@@ -6693,9 +6846,9 @@ function buildPanelBookData(input) {
|
|
|
6693
6846
|
factions: nameDescription(input.worldbook.factions, input.world, "faction"),
|
|
6694
6847
|
rules: nameDescription(input.worldbook.rules, input.world, "rule"),
|
|
6695
6848
|
facts: (input.worldbook.facts ?? []).slice(0, 5).map((fact) => ({
|
|
6696
|
-
alias: fact.alias,
|
|
6849
|
+
alias: clip(fact.alias, 80),
|
|
6697
6850
|
value: fact.value,
|
|
6698
|
-
unit: fact.unit
|
|
6851
|
+
unit: clip(fact.unit, 40)
|
|
6699
6852
|
}))
|
|
6700
6853
|
},
|
|
6701
6854
|
cast: {
|
|
@@ -6738,11 +6891,11 @@ function buildPanelBookData(input) {
|
|
|
6738
6891
|
health: {
|
|
6739
6892
|
state_errors: input.stateErrors.slice(0, 10).map((entry) => ({ ...entry })),
|
|
6740
6893
|
outline: {
|
|
6741
|
-
present: input.outlinePresent,
|
|
6894
|
+
present: input.outlinePresent && input.outlineError === null,
|
|
6742
6895
|
error: input.outlineError
|
|
6743
6896
|
},
|
|
6744
6897
|
worldbook: {
|
|
6745
|
-
present: input.worldbookPresent,
|
|
6898
|
+
present: input.worldbookPresent && input.worldbookError === null,
|
|
6746
6899
|
error: input.worldbookError
|
|
6747
6900
|
},
|
|
6748
6901
|
pending: input.pending === null ? null : { ...input.pending }
|
|
@@ -6762,7 +6915,7 @@ const kindOf = (directory, suffix) => {
|
|
|
6762
6915
|
* still on disk: history is evidence, not a cache, and this function never
|
|
6763
6916
|
* deletes it.
|
|
6764
6917
|
*/
|
|
6765
|
-
async function listHistory(store, chapter) {
|
|
6918
|
+
async function listHistory(store, chapter, options = {}) {
|
|
6766
6919
|
const entries = [];
|
|
6767
6920
|
for (const kind of HISTORY_KINDS) {
|
|
6768
6921
|
const names = await store.list(joinRel(HISTORY_DIR, kind));
|
|
@@ -6775,14 +6928,14 @@ async function listHistory(store, chapter) {
|
|
|
6775
6928
|
const revision = Number(match.groups.revision);
|
|
6776
6929
|
if (chapter !== void 0 && num !== chapter) continue;
|
|
6777
6930
|
const path = joinRel(HISTORY_DIR, kind, name);
|
|
6778
|
-
const
|
|
6779
|
-
entries.push({
|
|
6931
|
+
const entry = {
|
|
6780
6932
|
kind,
|
|
6781
6933
|
chapter: num,
|
|
6782
6934
|
revision,
|
|
6783
|
-
path
|
|
6784
|
-
|
|
6785
|
-
|
|
6935
|
+
path
|
|
6936
|
+
};
|
|
6937
|
+
if (options.withChars === true) entry.chars = (await store.readText(path))?.length ?? 0;
|
|
6938
|
+
entries.push(entry);
|
|
6786
6939
|
}
|
|
6787
6940
|
}
|
|
6788
6941
|
return entries.sort((left, right) => left.chapter - right.chapter || right.revision - left.revision || compareText(left.kind, right.kind));
|
|
@@ -7957,7 +8110,7 @@ const SUMMARY_FIELDS = [
|
|
|
7957
8110
|
"plot_advancement"
|
|
7958
8111
|
];
|
|
7959
8112
|
/** PG-05: the ten summary fields exist and have the right shape. */
|
|
7960
|
-
const summarySchema = (input) => {
|
|
8113
|
+
const summarySchema$1 = (input) => {
|
|
7961
8114
|
if (input.operation === "submitRevision" && input.summaryProvided !== true) return [];
|
|
7962
8115
|
if (input.summary === null) return [block("PG-05", "no chapter summary supplied")];
|
|
7963
8116
|
const summary = input.summary;
|
|
@@ -8015,11 +8168,8 @@ const summarySchema = (input) => {
|
|
|
8015
8168
|
}
|
|
8016
8169
|
}
|
|
8017
8170
|
if (textArray(summary.characters_appeared)) {
|
|
8018
|
-
const narrator = input.pipeline.draft?.narrator ?? "";
|
|
8019
|
-
for (const name of summary.characters_appeared) {
|
|
8020
|
-
const alias = narrator.includes(name) && (narrator.includes("第一人称") || narrator.includes("「我」")) && input.text.includes("我");
|
|
8021
|
-
if (!input.text.includes(name) && !alias) problems.push(`characters_appeared lists "${name}", but the manuscript never mentions it`);
|
|
8022
|
-
}
|
|
8171
|
+
const narrator = input.narrator ?? input.pipeline.draft?.narrator ?? "";
|
|
8172
|
+
for (const name of summary.characters_appeared) if (!isNarratorAlias(name, narrator, input.text)) problems.push(`characters_appeared lists "${name}", but the manuscript never mentions it`);
|
|
8023
8173
|
}
|
|
8024
8174
|
const resources = summary.resource_changes;
|
|
8025
8175
|
if (!Array.isArray(resources) || !resources.every((item) => typeof item === "object" && item !== null && isText(item.character) && isText(item.resource) && Number.isInteger(item.delta) && item.delta !== 0 && isText(item.reason))) problems.push("resource_changes must contain character/resource/non-zero delta/reason");
|
|
@@ -8252,7 +8402,7 @@ const GATE_FUNCTIONS = {
|
|
|
8252
8402
|
"PG-02": stageOrder,
|
|
8253
8403
|
"PG-03": wordCount,
|
|
8254
8404
|
"PG-04": format,
|
|
8255
|
-
"PG-05": summarySchema,
|
|
8405
|
+
"PG-05": summarySchema$1,
|
|
8256
8406
|
"PG-06": sceneType,
|
|
8257
8407
|
"PG-07": score,
|
|
8258
8408
|
"PG-08": issuesCleared,
|
|
@@ -8363,12 +8513,67 @@ function briefStageFor(operation, stage) {
|
|
|
8363
8513
|
}
|
|
8364
8514
|
}
|
|
8365
8515
|
// src/engine/projections.ts
|
|
8516
|
+
/**
|
|
8517
|
+
* Turn the protocol pair (`chapter` + `stage`) into the only human-readable
|
|
8518
|
+
* chapter boundary. After archiving chapter N the protocol intentionally keeps
|
|
8519
|
+
* `stage: 'done'` and advances `chapter` to N+1; `done` describes chapter N,
|
|
8520
|
+
* not the chapter the pipeline now points at.
|
|
8521
|
+
*/
|
|
8522
|
+
function projectChapterProgress(input) {
|
|
8523
|
+
const archived = [...new Set(input.archived)].filter((num) => Number.isInteger(num) && num > 0).sort((left, right) => left - right);
|
|
8524
|
+
const lastArchived = archived.length === 0 ? null : archived[archived.length - 1];
|
|
8525
|
+
const finished = input.stage === "done" && input.target !== null && archived.length >= input.target;
|
|
8526
|
+
const base = {
|
|
8527
|
+
active_chapter: input.activeChapter,
|
|
8528
|
+
stage: input.stage,
|
|
8529
|
+
last_archived: lastArchived,
|
|
8530
|
+
finished
|
|
8531
|
+
};
|
|
8532
|
+
if (finished) return {
|
|
8533
|
+
...base,
|
|
8534
|
+
current: null,
|
|
8535
|
+
next: null,
|
|
8536
|
+
label: `全书完成:已归档 ${archived.length} 章`,
|
|
8537
|
+
resume: `全书已完成(已归档 ${archived.length} 章);如需续写,先用 novel_amend 提高 targets.total_chapters。`
|
|
8538
|
+
};
|
|
8539
|
+
if (input.stage === "done") {
|
|
8540
|
+
if (lastArchived === null) return {
|
|
8541
|
+
...base,
|
|
8542
|
+
current: null,
|
|
8543
|
+
next: null,
|
|
8544
|
+
label: `当前第 ${input.activeChapter} 章 · 阶段 done(尚无归档章)`,
|
|
8545
|
+
resume: `引擎处于 done,但章节索引里还没有已归档章;先核对项目状态。`
|
|
8546
|
+
};
|
|
8547
|
+
const nextChapter = input.activeChapter > lastArchived ? input.activeChapter : lastArchived + 1;
|
|
8548
|
+
return {
|
|
8549
|
+
...base,
|
|
8550
|
+
current: null,
|
|
8551
|
+
next: {
|
|
8552
|
+
chapter: nextChapter,
|
|
8553
|
+
stage: "P1"
|
|
8554
|
+
},
|
|
8555
|
+
label: `第 ${lastArchived} 章已归档 · 下一章第 ${nextChapter} 章待开始(P1)`,
|
|
8556
|
+
resume: `第 ${lastArchived} 章已归档;当前不处于写作阶段,下一章第 ${nextChapter} 章从 P1 开始。`
|
|
8557
|
+
};
|
|
8558
|
+
}
|
|
8559
|
+
return {
|
|
8560
|
+
...base,
|
|
8561
|
+
current: {
|
|
8562
|
+
chapter: input.activeChapter,
|
|
8563
|
+
stage: input.stage
|
|
8564
|
+
},
|
|
8565
|
+
next: null,
|
|
8566
|
+
label: `第 ${input.activeChapter} 章 · 阶段 ${input.stage}`,
|
|
8567
|
+
resume: `当前第 ${input.activeChapter} 章,阶段 ${input.stage}。`
|
|
8568
|
+
};
|
|
8569
|
+
}
|
|
8366
8570
|
/** One shared prompt template for `/novel resume` and the panel copy button. */
|
|
8367
8571
|
function formatResumePrompt(input) {
|
|
8572
|
+
if (input.progress?.finished === true) return "";
|
|
8368
8573
|
return [
|
|
8369
8574
|
`请继续《${input.title}》。`,
|
|
8370
8575
|
`项目根:${input.root}(第一次工具调用必须显式传 project_root=${input.root})。`,
|
|
8371
|
-
`当前第 ${input.chapter} 章,阶段 ${input.stage}。`,
|
|
8576
|
+
input.progress?.resume ?? `当前第 ${input.chapter} 章,阶段 ${input.stage}。`,
|
|
8372
8577
|
`下一步:${input.nextTool}。`,
|
|
8373
8578
|
"先调用 novel_status 核对项目根与书名,再按 novel_pipeline 的下一步执行;",
|
|
8374
8579
|
"只操作这个项目,不要读取或写入其他书。"
|
|
@@ -8434,8 +8639,11 @@ function projectSummary(snapshot) {
|
|
|
8434
8639
|
platform: snapshot.platform,
|
|
8435
8640
|
chapter: snapshot.chapter,
|
|
8436
8641
|
stage: snapshot.stage,
|
|
8642
|
+
...snapshot.progress === void 0 ? {} : { progress: snapshot.progress },
|
|
8437
8643
|
revision: snapshot.revision,
|
|
8438
8644
|
totalWords: snapshot.totalWords,
|
|
8645
|
+
...snapshot.archivedWords === void 0 ? {} : { archivedWords: snapshot.archivedWords },
|
|
8646
|
+
...snapshot.draftWords === void 0 ? {} : { draftWords: snapshot.draftWords },
|
|
8439
8647
|
totalChapters: snapshot.chapters.length,
|
|
8440
8648
|
averageScore: snapshot.averageScore ?? (snapshot.chapters.length === 0 ? 0 : snapshot.chapters.reduce((sum, chapter) => sum + chapter.score, 0) / snapshot.chapters.length),
|
|
8441
8649
|
...snapshot.volume === void 0 ? {} : { volume: snapshot.volume },
|
|
@@ -8886,6 +9094,10 @@ var EngineService = class {
|
|
|
8886
9094
|
briefContextCache = /* @__PURE__ */ new WeakMap();
|
|
8887
9095
|
/** The 15s panel poll reuses the heavy book projection until revision/damage changes. */
|
|
8888
9096
|
panelCache = /* @__PURE__ */ new Map();
|
|
9097
|
+
/** Section-level cache; writes bump revision, so a stale entry cannot survive a commit. */
|
|
9098
|
+
sectionCache = /* @__PURE__ */ new Map();
|
|
9099
|
+
journalCache = /* @__PURE__ */ new WeakMap();
|
|
9100
|
+
reviewKeyCache = /* @__PURE__ */ new WeakMap();
|
|
8889
9101
|
briefs;
|
|
8890
9102
|
now;
|
|
8891
9103
|
metadataMutex = new KeyedMutex();
|
|
@@ -10206,6 +10418,12 @@ var EngineService = class {
|
|
|
10206
10418
|
*/
|
|
10207
10419
|
async statusFor(snapshot, chapter) {
|
|
10208
10420
|
const metrics = await this.metricsFor(snapshot, chapter);
|
|
10421
|
+
const progress = projectChapterProgress({
|
|
10422
|
+
activeChapter: snapshot.pipeline.chapter,
|
|
10423
|
+
stage: snapshot.pipeline.stage,
|
|
10424
|
+
archived: snapshot.chapters.chapters.map((record) => record.num),
|
|
10425
|
+
target: snapshot.project.targets.total_chapters
|
|
10426
|
+
});
|
|
10209
10427
|
const journal = await this.journalBlocking(snapshot);
|
|
10210
10428
|
const pending = await inspectPending(this.storeFor(snapshot.root));
|
|
10211
10429
|
const pendingWarnings = pending.state === "complete" || pending.state === "untouched" ? [{
|
|
@@ -10229,6 +10447,7 @@ var EngineService = class {
|
|
|
10229
10447
|
active_chapter: snapshot.pipeline.chapter,
|
|
10230
10448
|
manuscript_hash: snapshot.pipeline.draft !== void 0 && snapshot.text !== null ? sha256(snapshot.text) : null,
|
|
10231
10449
|
stage: snapshot.pipeline.stage,
|
|
10450
|
+
progress,
|
|
10232
10451
|
rounds: { ...snapshot.pipeline.rounds },
|
|
10233
10452
|
blocking: [...this.effectiveBlocking(snapshot), ...journal],
|
|
10234
10453
|
...pendingWarnings.length === 0 ? {} : { warnings: pendingWarnings },
|
|
@@ -10410,7 +10629,7 @@ var EngineService = class {
|
|
|
10410
10629
|
};
|
|
10411
10630
|
}
|
|
10412
10631
|
case "history": {
|
|
10413
|
-
const entries = await listHistory(store, input.chapter);
|
|
10632
|
+
const entries = await listHistory(store, input.chapter, { withChars: true });
|
|
10414
10633
|
if (input.revision !== void 0) {
|
|
10415
10634
|
if (!Number.isInteger(input.revision) || input.revision < 1) throw new EngineInputError(ERROR_CODES.SCHEMA_INVALID, "history revision must be a positive integer");
|
|
10416
10635
|
const matching = entries.filter((entry) => entry.chapter === chapter && entry.revision === input.revision);
|
|
@@ -10540,6 +10759,12 @@ var EngineService = class {
|
|
|
10540
10759
|
}
|
|
10541
10760
|
async advise(input = {}) {
|
|
10542
10761
|
const snapshot = await this.load(input.root);
|
|
10762
|
+
const progress = projectChapterProgress({
|
|
10763
|
+
activeChapter: snapshot.pipeline.chapter,
|
|
10764
|
+
stage: snapshot.pipeline.stage,
|
|
10765
|
+
archived: snapshot.chapters.chapters.map((record) => record.num),
|
|
10766
|
+
target: snapshot.project.targets.total_chapters
|
|
10767
|
+
});
|
|
10543
10768
|
const journal = await this.journalBlocking(snapshot);
|
|
10544
10769
|
const next = journal.length > 0 && this.stateErrors(snapshot).length === 0 ? {
|
|
10545
10770
|
tool: "novel_audit",
|
|
@@ -10550,6 +10775,7 @@ var EngineService = class {
|
|
|
10550
10775
|
data: {
|
|
10551
10776
|
stage: snapshot.pipeline.stage,
|
|
10552
10777
|
chapter: snapshot.pipeline.chapter,
|
|
10778
|
+
progress,
|
|
10553
10779
|
tool: next.tool,
|
|
10554
10780
|
args: next.args,
|
|
10555
10781
|
reason: next.reason,
|
|
@@ -10747,11 +10973,23 @@ var EngineService = class {
|
|
|
10747
10973
|
detail: snapshot.projectError ?? `novel.yaml is readable`,
|
|
10748
10974
|
...snapshot.projectError === null ? {} : { hint: "read novel_state {kind:\"novel\"} and repair it with novel_amend" }
|
|
10749
10975
|
});
|
|
10976
|
+
const profileCount = Object.values(this.rulepack.genres).filter((genre) => genre.profile !== void 0).length;
|
|
10750
10977
|
add({
|
|
10751
|
-
id: "
|
|
10978
|
+
id: "rulepack",
|
|
10752
10979
|
severity: "ok",
|
|
10980
|
+
title: "规则包",
|
|
10981
|
+
detail: `${this.rulepack.name}@${this.rulepack.version} · ${Object.keys(this.rulepack.genres).length} 个题材 · ${profileCount} 个题材 profile`
|
|
10982
|
+
});
|
|
10983
|
+
add({
|
|
10984
|
+
id: "pipeline",
|
|
10985
|
+
severity: snapshot.pipelineError === null ? "ok" : "error",
|
|
10753
10986
|
title: "流水线",
|
|
10754
|
-
detail: `revision ${snapshot.pipeline.revision} ·
|
|
10987
|
+
detail: `revision ${snapshot.pipeline.revision} · ${projectChapterProgress({
|
|
10988
|
+
activeChapter: snapshot.pipeline.chapter,
|
|
10989
|
+
stage: snapshot.pipeline.stage,
|
|
10990
|
+
archived: snapshot.chapters.chapters.map((record) => record.num),
|
|
10991
|
+
target: snapshot.project.targets.total_chapters
|
|
10992
|
+
}).label}`
|
|
10755
10993
|
});
|
|
10756
10994
|
for (const error of this.stateErrors(snapshot)) add({
|
|
10757
10995
|
id: `state:${error.path}`,
|
|
@@ -11043,12 +11281,13 @@ var EngineService = class {
|
|
|
11043
11281
|
foreshadows: snapshot.foreshadows,
|
|
11044
11282
|
relationships: snapshot.relationships,
|
|
11045
11283
|
reviews: currentReviews,
|
|
11046
|
-
activity: journal.rows.slice(-8),
|
|
11284
|
+
activity: journal.rows.filter((row) => row.op !== "lock_reclaimed").slice(-8),
|
|
11047
11285
|
journal: journal.rows,
|
|
11048
11286
|
reviewAttempts,
|
|
11049
11287
|
finalChecks,
|
|
11050
11288
|
targetChapters: snapshot.project.targets.total_chapters,
|
|
11051
11289
|
minScore: this.config.quality.minScore,
|
|
11290
|
+
dimensionMax: Object.fromEntries(this.rulepack.quality.dimensions.map((dimension) => [dimension.id, dimension.max])),
|
|
11052
11291
|
foreshadowDormantAfter: this.rulepack.crosschapter.foreshadow_dormant_after
|
|
11053
11292
|
});
|
|
11054
11293
|
if (snapshot.projectError !== null) report.warnings.push(`项目定义不可读:${snapshot.projectError}`);
|
|
@@ -11124,7 +11363,13 @@ var EngineService = class {
|
|
|
11124
11363
|
const snapshot = await this.loadAt(selection);
|
|
11125
11364
|
await this.recordSelectionBestEffort(selection, snapshot);
|
|
11126
11365
|
const target = snapshot.project.targets.total_chapters;
|
|
11127
|
-
const
|
|
11366
|
+
const progress = projectChapterProgress({
|
|
11367
|
+
activeChapter: snapshot.pipeline.chapter,
|
|
11368
|
+
stage: snapshot.pipeline.stage,
|
|
11369
|
+
archived: snapshot.chapters.chapters.map((record) => record.num),
|
|
11370
|
+
target
|
|
11371
|
+
});
|
|
11372
|
+
const finished = progress.finished;
|
|
11128
11373
|
const journalBlocking = await this.journalBlocking(snapshot);
|
|
11129
11374
|
const next = journalBlocking.length > 0 && this.stateErrors(snapshot).length === 0 ? {
|
|
11130
11375
|
tool: "novel_audit",
|
|
@@ -11136,6 +11381,7 @@ var EngineService = class {
|
|
|
11136
11381
|
projectPresent: true,
|
|
11137
11382
|
chapter: snapshot.pipeline.chapter,
|
|
11138
11383
|
stage: snapshot.pipeline.stage,
|
|
11384
|
+
progress,
|
|
11139
11385
|
archived: snapshot.chapters.chapters.length,
|
|
11140
11386
|
target,
|
|
11141
11387
|
finished,
|
|
@@ -11535,6 +11781,7 @@ var EngineService = class {
|
|
|
11535
11781
|
summary: input.summary,
|
|
11536
11782
|
draftTitle: input.chapter_title,
|
|
11537
11783
|
sceneType: input.scene_type,
|
|
11784
|
+
narrator: input.narrator,
|
|
11538
11785
|
planTargetWords: snapshot.plan?.target_words ?? null,
|
|
11539
11786
|
planCast: snapshot.plan?.cast === void 0 ? null : snapshot.plan.cast.map((member) => member.name)
|
|
11540
11787
|
});
|
|
@@ -12119,6 +12366,10 @@ var EngineService = class {
|
|
|
12119
12366
|
record.archived_at = archivedAt;
|
|
12120
12367
|
const outline = markOutlineArchived(snapshot.outline, record, archivedAt, snapshot.project.targets.words_per_chapter);
|
|
12121
12368
|
this.assertOutlineWritable(snapshot, outline, "novel_archive");
|
|
12369
|
+
const archivedCard = outline.chapters.find((card) => card.num === chapter);
|
|
12370
|
+
if (archivedCard === void 0) throw new EngineFaultError(ERROR_CODES.TXN_ABORTED, `archive did not produce an outline card for chapter ${chapter}`, "restore the outline from backups/ and retry the archive");
|
|
12371
|
+
record.archive_card = archivedCardSnapshot(archivedCard);
|
|
12372
|
+
record.archive_card_digest = archivedCardDigest(record.archive_card);
|
|
12122
12373
|
const foreshadows = ledger.foreshadows;
|
|
12123
12374
|
const timeline = ledger.timeline;
|
|
12124
12375
|
for (const item of foreshadows.items) if (item.status !== "resolved" && chapter - item.last_touched >= this.rulepack.crosschapter.foreshadow_dormant_after) item.status = "dormant";
|
|
@@ -12319,32 +12570,24 @@ var EngineService = class {
|
|
|
12319
12570
|
oldDocument = narrow(kind, parse(backup));
|
|
12320
12571
|
} catch {}
|
|
12321
12572
|
}
|
|
12322
|
-
if (kind === "outline"
|
|
12573
|
+
if (kind === "outline") {
|
|
12323
12574
|
const old = recordOf(oldDocument);
|
|
12324
12575
|
const oldChapters = Array.isArray(old?.chapters) ? [...old.chapters] : [];
|
|
12325
|
-
const
|
|
12326
|
-
|
|
12576
|
+
const indexOf = /* @__PURE__ */ new Map();
|
|
12577
|
+
for (const [index, raw] of oldChapters.entries()) {
|
|
12578
|
+
const row = recordOf(raw);
|
|
12579
|
+
if (row !== null && Number.isInteger(row.num)) indexOf.set(row.num, index);
|
|
12580
|
+
}
|
|
12327
12581
|
for (const record of snapshot.chapters.chapters) {
|
|
12328
|
-
if (
|
|
12329
|
-
|
|
12330
|
-
|
|
12331
|
-
|
|
12332
|
-
|
|
12333
|
-
|
|
12334
|
-
|
|
12335
|
-
|
|
12336
|
-
|
|
12337
|
-
} catch {
|
|
12338
|
-
plan = null;
|
|
12339
|
-
}
|
|
12340
|
-
const planCast = Array.isArray(plan?.cast) ? plan.cast.map((member) => recordOf(member)?.name ?? "").filter((name) => name !== "") : [];
|
|
12341
|
-
const allowedTargets = [snapshot.project.targets.words_per_chapter, ...Array.isArray(plan?.target_words) ? [plan.target_words] : []];
|
|
12342
|
-
const allowedCasts = [planCast, autoCardCast(record)];
|
|
12343
|
-
const targetMatches = allowedTargets.some((target) => JSON.stringify(target) === JSON.stringify(candidate.target_words));
|
|
12344
|
-
const castMatches = allowedCasts.some((cast) => JSON.stringify(cast) === JSON.stringify(candidate.cast));
|
|
12345
|
-
if (!targetMatches || !castMatches) continue;
|
|
12346
|
-
oldChapters.push({ ...candidate });
|
|
12347
|
-
known.add(record.num);
|
|
12582
|
+
if (record.archive_card === void 0) continue;
|
|
12583
|
+
if (record.archive_card_digest === void 0 || record.archive_card_digest !== archivedCardDigest(record.archive_card)) throw new EngineFaultError(ERROR_CODES.SCHEMA_INVALID, `archived chapter ${record.num} has an archive_card whose digest does not match`, "restore state/chapters.yaml from backups/ or repair the card evidence manually");
|
|
12584
|
+
const archiveCardRow = archivedCardRecord(record);
|
|
12585
|
+
const index = indexOf.get(record.num);
|
|
12586
|
+
if (index === void 0) oldChapters.push(archiveCardRow);
|
|
12587
|
+
else oldChapters[index] = {
|
|
12588
|
+
...recordOf(oldChapters[index]) ?? {},
|
|
12589
|
+
...archiveCardRow
|
|
12590
|
+
};
|
|
12348
12591
|
}
|
|
12349
12592
|
oldDocument = {
|
|
12350
12593
|
...old ?? {},
|
|
@@ -12744,6 +12987,436 @@ var EngineService = class {
|
|
|
12744
12987
|
return projectGates(await this.panelSnapshot(input.root));
|
|
12745
12988
|
}
|
|
12746
12989
|
/**
|
|
12990
|
+
* Additive v7 section read model. Catalog sections reuse the Snapshot; only
|
|
12991
|
+
* the quality section reads archived reviews, and only chapter/quality
|
|
12992
|
+
* detail reads manuscripts or check files. Every section is byte-capped.
|
|
12993
|
+
*/
|
|
12994
|
+
async panelSection(input = { section: "overview" }) {
|
|
12995
|
+
const selection = await this.resolveRoot(input.root);
|
|
12996
|
+
if (input.root !== void 0 && selection.root !== null) {
|
|
12997
|
+
const selected = resolve(selection.root);
|
|
12998
|
+
const registry = await this.registry().read();
|
|
12999
|
+
const configured = this.defaultRoot();
|
|
13000
|
+
if (!(registry.active !== null && resolve(registry.active) === selected ? true : registry.projects.some((entry) => resolve(entry.root) === selected) || selected === resolve(configured))) throw new EngineInputError(ERROR_CODES.PROJECT_NOT_FOUND, `panel section root is not registered: ${selected}`, "用 /novel scan --register 或 /novel admin novel_registry_register 登记后刷新;面板不读取任意路径");
|
|
13001
|
+
}
|
|
13002
|
+
const snapshot = await this.loadAt(selection);
|
|
13003
|
+
const section = input.section;
|
|
13004
|
+
const query = input.query ?? {};
|
|
13005
|
+
const cacheable = section !== "chapter_detail" && section !== "quality_detail";
|
|
13006
|
+
let sectionKey = null;
|
|
13007
|
+
if (cacheable) {
|
|
13008
|
+
const journalIdentity = await this.panelJournalIdentity(snapshot);
|
|
13009
|
+
const reviewKey = section === "quality" ? await this.panelReviewKey(snapshot) : "";
|
|
13010
|
+
sectionKey = sha256(stableJson({
|
|
13011
|
+
root: snapshot.root,
|
|
13012
|
+
revision: snapshot.pipeline.revision,
|
|
13013
|
+
journalKey: journalIdentity.key,
|
|
13014
|
+
reviewKey,
|
|
13015
|
+
section,
|
|
13016
|
+
query,
|
|
13017
|
+
stage: snapshot.pipeline.stage,
|
|
13018
|
+
errors: [
|
|
13019
|
+
snapshot.projectError,
|
|
13020
|
+
snapshot.pipelineError,
|
|
13021
|
+
snapshot.planError,
|
|
13022
|
+
snapshot.reviewError,
|
|
13023
|
+
snapshot.checkError,
|
|
13024
|
+
snapshot.outlineError,
|
|
13025
|
+
snapshot.worldbookError,
|
|
13026
|
+
...snapshot.ledgerErrors.map((error) => error.code)
|
|
13027
|
+
]
|
|
13028
|
+
}));
|
|
13029
|
+
const cachedSection = this.sectionCache.get(sectionKey);
|
|
13030
|
+
if (cachedSection !== void 0) return cachedSection.value;
|
|
13031
|
+
}
|
|
13032
|
+
let items = null;
|
|
13033
|
+
let total = 0;
|
|
13034
|
+
let truncated = false;
|
|
13035
|
+
let nextCursor = null;
|
|
13036
|
+
if (section === "overview") {
|
|
13037
|
+
const panel = await this.panelSnapshot(void 0, {
|
|
13038
|
+
snapshot,
|
|
13039
|
+
includeQuality: false
|
|
13040
|
+
});
|
|
13041
|
+
items = {
|
|
13042
|
+
...panel,
|
|
13043
|
+
chapters: panel.chapters.slice(-5),
|
|
13044
|
+
bookData: panel.bookData === void 0 ? void 0 : {
|
|
13045
|
+
...panel.bookData,
|
|
13046
|
+
quality: {
|
|
13047
|
+
...panel.bookData.quality,
|
|
13048
|
+
dimensions: [],
|
|
13049
|
+
samples: 0,
|
|
13050
|
+
attempts: 0
|
|
13051
|
+
},
|
|
13052
|
+
effort: {
|
|
13053
|
+
...panel.bookData.effort,
|
|
13054
|
+
chapters: []
|
|
13055
|
+
}
|
|
13056
|
+
}
|
|
13057
|
+
};
|
|
13058
|
+
total = 1;
|
|
13059
|
+
} else if (section === "chapter_detail" || section === "quality_detail") {
|
|
13060
|
+
const chapter = query.chapter ?? snapshot.pipeline.chapter;
|
|
13061
|
+
if (!Number.isInteger(chapter) || chapter < 1) throw new EngineInputError(ERROR_CODES.SCHEMA_INVALID, "chapter detail requires a positive chapter");
|
|
13062
|
+
const row = snapshot.chapters.chapters.find((record) => record.num === chapter) ?? null;
|
|
13063
|
+
const active = chapter === snapshot.pipeline.chapter;
|
|
13064
|
+
const store = this.storeFor(snapshot.root);
|
|
13065
|
+
if (section === "quality_detail") {
|
|
13066
|
+
items = {
|
|
13067
|
+
chapter,
|
|
13068
|
+
review: await store.readText(reviewFile(chapter)),
|
|
13069
|
+
check: await store.readText(checkFile(chapter))
|
|
13070
|
+
};
|
|
13071
|
+
total = row === null && !active ? 0 : 1;
|
|
13072
|
+
} else {
|
|
13073
|
+
items = {
|
|
13074
|
+
chapter,
|
|
13075
|
+
row,
|
|
13076
|
+
manuscript: row === null || row.file === "" ? active ? snapshot.text : null : await store.readText(row.file),
|
|
13077
|
+
plan: await store.readText(planChapterFile(chapter)),
|
|
13078
|
+
world_plan: await store.readText(planWorldFile(chapter)),
|
|
13079
|
+
summary_events: (row?.summary.events ?? snapshot.pipeline.draft?.summary?.events ?? []).slice(0, 4)
|
|
13080
|
+
};
|
|
13081
|
+
total = row === null && !active ? 0 : 1;
|
|
13082
|
+
}
|
|
13083
|
+
} else if (section === "chapters") {
|
|
13084
|
+
const archivedCurrent = snapshot.chapters.chapters.some((record) => record.num === snapshot.pipeline.chapter);
|
|
13085
|
+
const draftWords = snapshot.text === null || archivedCurrent || snapshot.pipeline.stage === "done" ? 0 : countWordMetrics(snapshot.text).total_word_count;
|
|
13086
|
+
const rows = snapshot.chapters.chapters.map((record) => ({
|
|
13087
|
+
num: record.num,
|
|
13088
|
+
title: record.title,
|
|
13089
|
+
file: record.file,
|
|
13090
|
+
word_count: record.word_count,
|
|
13091
|
+
score: record.score,
|
|
13092
|
+
status: "archived",
|
|
13093
|
+
scene_type: record.scene_type,
|
|
13094
|
+
dialogue_ratio: record.dialogue_ratio,
|
|
13095
|
+
final_check: record.final_check ?? null,
|
|
13096
|
+
archived_at: record.archived_at ?? null,
|
|
13097
|
+
summary_events: record.summary.events.slice(0, 4),
|
|
13098
|
+
rounds: record.rounds ?? null
|
|
13099
|
+
}));
|
|
13100
|
+
if (!archivedCurrent && snapshot.pipeline.stage !== "done") rows.push({
|
|
13101
|
+
num: snapshot.pipeline.chapter,
|
|
13102
|
+
title: stripTitlePrefix(snapshot.plan?.title ?? "") || `第${snapshot.pipeline.chapter}章`,
|
|
13103
|
+
file: null,
|
|
13104
|
+
word_count: draftWords,
|
|
13105
|
+
score: snapshot.review?.total ?? null,
|
|
13106
|
+
status: "draft",
|
|
13107
|
+
scene_type: snapshot.plan?.scene_type ?? null,
|
|
13108
|
+
dialogue_ratio: null,
|
|
13109
|
+
final_check: null,
|
|
13110
|
+
archived_at: null,
|
|
13111
|
+
summary_events: (snapshot.pipeline.draft?.summary?.events ?? []).slice(0, 4),
|
|
13112
|
+
rounds: snapshot.pipeline.rounds
|
|
13113
|
+
});
|
|
13114
|
+
const sorted = rows.filter((row) => {
|
|
13115
|
+
if (query.status !== void 0 && query.status !== "" && row.status !== query.status) return false;
|
|
13116
|
+
if (query.scene !== void 0 && query.scene !== "" && row.scene_type !== query.scene) return false;
|
|
13117
|
+
if (query.search !== void 0 && query.search !== "") {
|
|
13118
|
+
if (!String(row.title ?? "").includes(query.search)) return false;
|
|
13119
|
+
}
|
|
13120
|
+
return true;
|
|
13121
|
+
}).sort((left, right) => {
|
|
13122
|
+
const byNum = Number(left.num) - Number(right.num);
|
|
13123
|
+
return query.sort === "num_asc" ? byNum : -byNum;
|
|
13124
|
+
});
|
|
13125
|
+
total = sorted.length;
|
|
13126
|
+
const page = this.panelSectionPage(sorted, query);
|
|
13127
|
+
items = page.items;
|
|
13128
|
+
truncated = page.truncated;
|
|
13129
|
+
nextCursor = page.nextCursor;
|
|
13130
|
+
} else if (section === "characters") {
|
|
13131
|
+
const seen = /* @__PURE__ */ new Map();
|
|
13132
|
+
for (const record of snapshot.chapters.chapters) for (const name of record.summary.characters_appeared) {
|
|
13133
|
+
const current = seen.get(name) ?? {
|
|
13134
|
+
first: record.num,
|
|
13135
|
+
last: record.num,
|
|
13136
|
+
appearances: 0
|
|
13137
|
+
};
|
|
13138
|
+
current.first = Math.min(current.first, record.num);
|
|
13139
|
+
current.last = Math.max(current.last, record.num);
|
|
13140
|
+
current.appearances += 1;
|
|
13141
|
+
seen.set(name, current);
|
|
13142
|
+
}
|
|
13143
|
+
const filtered = snapshot.characters.characters.map((character) => ({
|
|
13144
|
+
...character,
|
|
13145
|
+
...seen.get(character.name) ?? {
|
|
13146
|
+
first: character.first_appearance,
|
|
13147
|
+
last: null,
|
|
13148
|
+
appearances: 0
|
|
13149
|
+
}
|
|
13150
|
+
})).filter((row) => query.search === void 0 || query.search === "" ? true : row.name.includes(query.search) || row.role.includes(query.search));
|
|
13151
|
+
total = filtered.length;
|
|
13152
|
+
const page = this.panelSectionPage(filtered, query, 200);
|
|
13153
|
+
items = page.items;
|
|
13154
|
+
truncated = page.truncated;
|
|
13155
|
+
nextCursor = page.nextCursor;
|
|
13156
|
+
} else if (section === "foreshadows") {
|
|
13157
|
+
const filtered = snapshot.foreshadows.items.filter((row) => {
|
|
13158
|
+
if (query.status !== void 0 && query.status !== "" && row.status !== query.status) return false;
|
|
13159
|
+
return query.search === void 0 || query.search === "" ? true : row.title.includes(query.search);
|
|
13160
|
+
});
|
|
13161
|
+
const page = this.panelSectionPage(filtered, query, 200);
|
|
13162
|
+
total = filtered.length + snapshot.timeline.items.length + snapshot.relationships.items.length;
|
|
13163
|
+
truncated = page.truncated || snapshot.timeline.items.length > 200 || snapshot.relationships.items.length > 200;
|
|
13164
|
+
items = {
|
|
13165
|
+
foreshadows: page.items,
|
|
13166
|
+
timeline: snapshot.timeline.items.slice(0, 200),
|
|
13167
|
+
relationships: snapshot.relationships.items.slice(0, 200)
|
|
13168
|
+
};
|
|
13169
|
+
nextCursor = page.nextCursor;
|
|
13170
|
+
} else if (section === "timeline") {
|
|
13171
|
+
const filtered = snapshot.timeline.items.filter((row) => query.search === void 0 || query.search === "" ? true : row.description.includes(query.search) || (row.location ?? "").includes(query.search));
|
|
13172
|
+
total = filtered.length;
|
|
13173
|
+
const page = this.panelSectionPage(filtered, query, 200);
|
|
13174
|
+
items = page.items;
|
|
13175
|
+
truncated = page.truncated;
|
|
13176
|
+
nextCursor = page.nextCursor;
|
|
13177
|
+
} else if (section === "relationships") {
|
|
13178
|
+
const filtered = snapshot.relationships.items.filter((row) => query.search === void 0 || query.search === "" ? true : row.a.includes(query.search) || row.b.includes(query.search) || row.type.includes(query.search));
|
|
13179
|
+
total = filtered.length;
|
|
13180
|
+
const page = this.panelSectionPage(filtered, query, 200);
|
|
13181
|
+
items = page.items;
|
|
13182
|
+
truncated = page.truncated;
|
|
13183
|
+
nextCursor = page.nextCursor;
|
|
13184
|
+
} else if (section === "worldbook") {
|
|
13185
|
+
const world = snapshot.world;
|
|
13186
|
+
const book = snapshot.worldbook;
|
|
13187
|
+
const search = query.search?.trim() ?? "";
|
|
13188
|
+
const matches = (value) => {
|
|
13189
|
+
if (search === "") return true;
|
|
13190
|
+
if (typeof value === "string") return value.includes(search);
|
|
13191
|
+
if (typeof value === "number" || typeof value === "boolean") return String(value).includes(search);
|
|
13192
|
+
if (Array.isArray(value)) return value.some((item) => matches(item));
|
|
13193
|
+
if (typeof value === "object" && value !== null) return Object.values(value).some((item) => matches(item));
|
|
13194
|
+
return false;
|
|
13195
|
+
};
|
|
13196
|
+
const filterRows = (rows) => rows.filter((row) => matches(row));
|
|
13197
|
+
const capRows = (rows) => rows.slice(0, 200);
|
|
13198
|
+
const groups = {
|
|
13199
|
+
rules: filterRows(book.rules),
|
|
13200
|
+
factions: filterRows(book.factions),
|
|
13201
|
+
locations: filterRows(book.locations),
|
|
13202
|
+
characters: filterRows(book.characters),
|
|
13203
|
+
glossary: filterRows(book.glossary),
|
|
13204
|
+
backstory: filterRows(book.backstory),
|
|
13205
|
+
facts: filterRows(book.facts ?? []),
|
|
13206
|
+
themes: filterRows(book.themes)
|
|
13207
|
+
};
|
|
13208
|
+
const ledger = {
|
|
13209
|
+
locations: filterRows(world.locations),
|
|
13210
|
+
abilities: filterRows(world.abilities),
|
|
13211
|
+
artifacts: filterRows(world.artifacts),
|
|
13212
|
+
factions: filterRows(world.factions),
|
|
13213
|
+
rules: filterRows(world.rules)
|
|
13214
|
+
};
|
|
13215
|
+
const outlineCards = filterRows(snapshot.outline.chapters);
|
|
13216
|
+
const all = [
|
|
13217
|
+
...Object.values(groups),
|
|
13218
|
+
...Object.values(ledger),
|
|
13219
|
+
outlineCards
|
|
13220
|
+
];
|
|
13221
|
+
total = all.reduce((count, rows) => count + rows.length, 0);
|
|
13222
|
+
truncated = all.some((rows) => rows.length > 200);
|
|
13223
|
+
items = {
|
|
13224
|
+
premise: book.premise,
|
|
13225
|
+
setting: book.setting,
|
|
13226
|
+
blueprint: snapshot.outline.book,
|
|
13227
|
+
volumes: snapshot.outline.volumes,
|
|
13228
|
+
rules: capRows(groups.rules),
|
|
13229
|
+
factions: capRows(groups.factions),
|
|
13230
|
+
locations: capRows(groups.locations),
|
|
13231
|
+
characters: capRows(groups.characters),
|
|
13232
|
+
glossary: capRows(groups.glossary),
|
|
13233
|
+
backstory: capRows(groups.backstory),
|
|
13234
|
+
facts: capRows(groups.facts),
|
|
13235
|
+
themes: capRows(groups.themes),
|
|
13236
|
+
ledger: {
|
|
13237
|
+
locations: capRows(ledger.locations),
|
|
13238
|
+
abilities: capRows(ledger.abilities),
|
|
13239
|
+
artifacts: capRows(ledger.artifacts),
|
|
13240
|
+
factions: capRows(ledger.factions),
|
|
13241
|
+
rules: capRows(ledger.rules)
|
|
13242
|
+
},
|
|
13243
|
+
outline_cards: capRows(outlineCards),
|
|
13244
|
+
drift: outlineDrift(snapshot.outline, snapshot.chapters.chapters)
|
|
13245
|
+
};
|
|
13246
|
+
} else if (section === "quality") {
|
|
13247
|
+
const panel = await this.panelSnapshot(void 0, {
|
|
13248
|
+
snapshot,
|
|
13249
|
+
includeQuality: true
|
|
13250
|
+
});
|
|
13251
|
+
const maxById = new Map(this.rulepack.quality.dimensions.map((dimension) => [dimension.id, dimension.max]));
|
|
13252
|
+
items = {
|
|
13253
|
+
quality: panel.bookData?.quality ?? null,
|
|
13254
|
+
dimensions: (panel.bookData?.quality.dimensions ?? []).map((dimension) => ({
|
|
13255
|
+
...dimension,
|
|
13256
|
+
max: maxById.get(dimension.id) ?? null
|
|
13257
|
+
})),
|
|
13258
|
+
pacing: panel.bookData?.pacing ?? null,
|
|
13259
|
+
effort: panel.bookData?.effort ?? null
|
|
13260
|
+
};
|
|
13261
|
+
total = items === null ? 0 : 1;
|
|
13262
|
+
} else {
|
|
13263
|
+
const panel = await this.panelSnapshot(void 0, {
|
|
13264
|
+
snapshot,
|
|
13265
|
+
includeQuality: false
|
|
13266
|
+
});
|
|
13267
|
+
items = {
|
|
13268
|
+
health: panel.health ?? null,
|
|
13269
|
+
blocking: panel.blocking,
|
|
13270
|
+
state_errors: this.stateErrors(snapshot),
|
|
13271
|
+
outline: {
|
|
13272
|
+
present: snapshot.outlinePresent && snapshot.outlineError === null,
|
|
13273
|
+
file_existed: snapshot.outlinePresent,
|
|
13274
|
+
error: snapshot.outlineError
|
|
13275
|
+
},
|
|
13276
|
+
worldbook: {
|
|
13277
|
+
present: snapshot.worldbookPresent && snapshot.worldbookError === null,
|
|
13278
|
+
file_existed: snapshot.worldbookPresent,
|
|
13279
|
+
error: snapshot.worldbookError
|
|
13280
|
+
},
|
|
13281
|
+
drift: outlineDrift(snapshot.outline, snapshot.chapters.chapters),
|
|
13282
|
+
pending: panel.pending ?? null
|
|
13283
|
+
};
|
|
13284
|
+
total = 1;
|
|
13285
|
+
}
|
|
13286
|
+
const clipped = section === "chapter_detail" || section === "quality_detail" ? {
|
|
13287
|
+
items,
|
|
13288
|
+
truncated: false
|
|
13289
|
+
} : this.panelSectionClip(items, section);
|
|
13290
|
+
items = clipped.items;
|
|
13291
|
+
truncated = truncated || clipped.truncated;
|
|
13292
|
+
const budgeted = this.panelSectionBudget(items, 65536);
|
|
13293
|
+
items = budgeted.items;
|
|
13294
|
+
truncated = truncated || budgeted.truncated;
|
|
13295
|
+
if (budgeted.truncated) nextCursor = null;
|
|
13296
|
+
const updatedAt = this.now().toISOString();
|
|
13297
|
+
const etag = sha256(stableJson({
|
|
13298
|
+
root: snapshot.root,
|
|
13299
|
+
revision: snapshot.pipeline.revision,
|
|
13300
|
+
section,
|
|
13301
|
+
query,
|
|
13302
|
+
items,
|
|
13303
|
+
total,
|
|
13304
|
+
truncated
|
|
13305
|
+
}));
|
|
13306
|
+
const result = {
|
|
13307
|
+
payload_version: 7,
|
|
13308
|
+
section,
|
|
13309
|
+
items,
|
|
13310
|
+
total,
|
|
13311
|
+
returned: this.panelSectionReturned(items),
|
|
13312
|
+
truncated,
|
|
13313
|
+
next_cursor: nextCursor,
|
|
13314
|
+
updated_at: updatedAt,
|
|
13315
|
+
etag
|
|
13316
|
+
};
|
|
13317
|
+
if (sectionKey !== null) {
|
|
13318
|
+
this.sectionCache.set(sectionKey, {
|
|
13319
|
+
key: sectionKey,
|
|
13320
|
+
value: result
|
|
13321
|
+
});
|
|
13322
|
+
if (this.sectionCache.size > 64) {
|
|
13323
|
+
const oldest = this.sectionCache.keys().next().value;
|
|
13324
|
+
if (oldest !== void 0) this.sectionCache.delete(oldest);
|
|
13325
|
+
}
|
|
13326
|
+
}
|
|
13327
|
+
return result;
|
|
13328
|
+
}
|
|
13329
|
+
panelSectionPage(rows, query, defaultLimit = 50) {
|
|
13330
|
+
const requested = query.limit ?? defaultLimit;
|
|
13331
|
+
const limit = Number.isInteger(requested) ? Math.min(200, Math.max(1, requested)) : defaultLimit;
|
|
13332
|
+
const cursor = Number.parseInt(query.cursor ?? "0", 10);
|
|
13333
|
+
const start = Number.isInteger(cursor) && cursor >= 0 ? cursor : 0;
|
|
13334
|
+
const items = rows.slice(start, start + limit);
|
|
13335
|
+
const truncated = start + items.length < rows.length;
|
|
13336
|
+
return {
|
|
13337
|
+
items,
|
|
13338
|
+
truncated,
|
|
13339
|
+
nextCursor: truncated ? String(start + items.length) : null
|
|
13340
|
+
};
|
|
13341
|
+
}
|
|
13342
|
+
panelSectionReturned(items) {
|
|
13343
|
+
if (Array.isArray(items)) return items.length;
|
|
13344
|
+
if (typeof items === "object" && items !== null) return Object.values(items).reduce((total, value) => total + (Array.isArray(value) ? value.length : 1), 0);
|
|
13345
|
+
return items === null ? 0 : 1;
|
|
13346
|
+
}
|
|
13347
|
+
/** WP3.7 free-text caps before the byte budget; both report truncation. */
|
|
13348
|
+
panelSectionClip(value, section) {
|
|
13349
|
+
let truncated = false;
|
|
13350
|
+
const walk = (input, path) => {
|
|
13351
|
+
if (typeof input === "string") {
|
|
13352
|
+
const keys = path.filter((part) => !/^\d+$/.test(part));
|
|
13353
|
+
const key = keys[keys.length - 1] ?? "";
|
|
13354
|
+
const worldbookText = section === "worldbook" || keys.some((part) => part === "ledger" || part === "worldbook");
|
|
13355
|
+
const max = key === "description" && worldbookText ? 240 : 400;
|
|
13356
|
+
const chars = Array.from(input);
|
|
13357
|
+
if (chars.length > max) {
|
|
13358
|
+
truncated = true;
|
|
13359
|
+
return chars.slice(0, max).join("");
|
|
13360
|
+
}
|
|
13361
|
+
return input;
|
|
13362
|
+
}
|
|
13363
|
+
if (Array.isArray(input)) return input.map((item, index) => walk(item, [...path, String(index)]));
|
|
13364
|
+
if (typeof input === "object" && input !== null) return Object.fromEntries(Object.entries(input).map(([key, item]) => [key, walk(item, [...path, key])]));
|
|
13365
|
+
return input;
|
|
13366
|
+
};
|
|
13367
|
+
return {
|
|
13368
|
+
items: walk(value, []),
|
|
13369
|
+
truncated
|
|
13370
|
+
};
|
|
13371
|
+
}
|
|
13372
|
+
panelSectionBudget(value, limit) {
|
|
13373
|
+
const bytes = (input) => Buffer.byteLength(JSON.stringify(input) ?? "null");
|
|
13374
|
+
if (bytes(value) <= limit) return {
|
|
13375
|
+
items: value,
|
|
13376
|
+
truncated: false
|
|
13377
|
+
};
|
|
13378
|
+
const clipStrings = (input, maxChars) => {
|
|
13379
|
+
if (typeof input === "string") {
|
|
13380
|
+
const chars = Array.from(input);
|
|
13381
|
+
return chars.length <= maxChars ? input : chars.slice(0, maxChars).join("");
|
|
13382
|
+
}
|
|
13383
|
+
if (Array.isArray(input)) return input.map((item) => clipStrings(item, maxChars));
|
|
13384
|
+
if (typeof input === "object" && input !== null) return Object.fromEntries(Object.entries(input).map(([key, item]) => [key, clipStrings(item, maxChars)]));
|
|
13385
|
+
return input;
|
|
13386
|
+
};
|
|
13387
|
+
const popOne = (input) => {
|
|
13388
|
+
if (Array.isArray(input)) {
|
|
13389
|
+
if (input.length > 1) {
|
|
13390
|
+
input.pop();
|
|
13391
|
+
return true;
|
|
13392
|
+
}
|
|
13393
|
+
return input.some((item) => popOne(item));
|
|
13394
|
+
}
|
|
13395
|
+
if (typeof input === "object" && input !== null) return Object.values(input).some((item) => popOne(item));
|
|
13396
|
+
return false;
|
|
13397
|
+
};
|
|
13398
|
+
for (const maxChars of [
|
|
13399
|
+
2e3,
|
|
13400
|
+
1e3,
|
|
13401
|
+
500,
|
|
13402
|
+
200,
|
|
13403
|
+
80,
|
|
13404
|
+
20,
|
|
13405
|
+
0
|
|
13406
|
+
]) {
|
|
13407
|
+
const candidate = clipStrings(value, maxChars);
|
|
13408
|
+
while (bytes(candidate) > limit) if (!popOne(candidate)) break;
|
|
13409
|
+
if (bytes(candidate) <= limit) return {
|
|
13410
|
+
items: candidate,
|
|
13411
|
+
truncated: true
|
|
13412
|
+
};
|
|
13413
|
+
}
|
|
13414
|
+
return {
|
|
13415
|
+
items: Array.isArray(value) ? [] : typeof value === "object" && value !== null ? {} : typeof value === "string" ? "" : null,
|
|
13416
|
+
truncated: true
|
|
13417
|
+
};
|
|
13418
|
+
}
|
|
13419
|
+
/**
|
|
12747
13420
|
* Build the read-only panel payload. The query root is accepted only when it
|
|
12748
13421
|
* is registered, active or the configured root; arbitrary paths never reach
|
|
12749
13422
|
* the filesystem from the panel route.
|
|
@@ -12776,9 +13449,10 @@ var EngineService = class {
|
|
|
12776
13449
|
add(await optionFor(active, known !== void 0, known?.title, known?.created_at));
|
|
12777
13450
|
}
|
|
12778
13451
|
const configured = this.defaultRoot();
|
|
13452
|
+
const explicitConfigured = this.config.projectRoot !== "./novel" && configured !== resolve("./novel");
|
|
12779
13453
|
if (!rows.has(configured)) {
|
|
12780
13454
|
const option = await optionFor(configured, false);
|
|
12781
|
-
if (option.state !== "missing" || active === configured) add(option);
|
|
13455
|
+
if (option.state !== "missing" || active === configured || explicitConfigured) add(option);
|
|
12782
13456
|
}
|
|
12783
13457
|
const selectedRoot = input.root === void 0 ? void 0 : resolve(input.root);
|
|
12784
13458
|
if (selectedRoot !== void 0 && !rows.has(selectedRoot) && selectedRoot === configured) add(await optionFor(configured, false));
|
|
@@ -12842,7 +13516,7 @@ var EngineService = class {
|
|
|
12842
13516
|
};
|
|
12843
13517
|
let snapshot;
|
|
12844
13518
|
try {
|
|
12845
|
-
snapshot = await this.panelSnapshot(selection.root);
|
|
13519
|
+
snapshot = await this.panelSnapshot(selection.root, { includeQuality: input.lean !== true });
|
|
12846
13520
|
} catch (error) {
|
|
12847
13521
|
if (error instanceof EngineError) {
|
|
12848
13522
|
const busy = error.code === ERROR_CODES.TXN_IN_PROGRESS;
|
|
@@ -12863,19 +13537,29 @@ var EngineService = class {
|
|
|
12863
13537
|
}
|
|
12864
13538
|
const summary = projectSummary(snapshot);
|
|
12865
13539
|
if (option.state !== "ok" && option.title !== "") summary.title = option.title;
|
|
12866
|
-
const resume = summary.next === void 0 ? void 0 : { text: formatResumePrompt({
|
|
13540
|
+
const resume = summary.next === void 0 || summary.progress?.finished === true ? void 0 : { text: formatResumePrompt({
|
|
12867
13541
|
title: summary.title,
|
|
12868
13542
|
root: summary.root,
|
|
12869
13543
|
chapter: summary.chapter,
|
|
12870
13544
|
stage: summary.stage,
|
|
12871
|
-
nextTool: summary.next.tool
|
|
13545
|
+
nextTool: summary.next.tool,
|
|
13546
|
+
...summary.progress === void 0 ? {} : { progress: summary.progress }
|
|
12872
13547
|
}) };
|
|
13548
|
+
const chapters = projectChapters(snapshot);
|
|
13549
|
+
const etag = sha256(stableJson({
|
|
13550
|
+
root: snapshot.root,
|
|
13551
|
+
projects,
|
|
13552
|
+
selection,
|
|
13553
|
+
summary,
|
|
13554
|
+
chapters,
|
|
13555
|
+
...resume === void 0 ? {} : { resume }
|
|
13556
|
+
}));
|
|
12873
13557
|
return {
|
|
12874
13558
|
projects,
|
|
12875
13559
|
selection,
|
|
12876
13560
|
summary,
|
|
12877
|
-
chapters
|
|
12878
|
-
|
|
13561
|
+
chapters,
|
|
13562
|
+
etag,
|
|
12879
13563
|
...resume === void 0 ? {} : { resume }
|
|
12880
13564
|
};
|
|
12881
13565
|
}
|
|
@@ -12888,12 +13572,13 @@ var EngineService = class {
|
|
|
12888
13572
|
const nextTool = status.data.next?.tool;
|
|
12889
13573
|
return {
|
|
12890
13574
|
data: {
|
|
12891
|
-
text: nextTool === void 0 ? null : formatResumePrompt({
|
|
13575
|
+
text: nextTool === void 0 || status.data.progress?.finished === true ? null : formatResumePrompt({
|
|
12892
13576
|
title: status.data.project.title,
|
|
12893
13577
|
root: status.data.project.root,
|
|
12894
13578
|
chapter: status.data.chapter,
|
|
12895
13579
|
stage: status.data.stage,
|
|
12896
|
-
nextTool
|
|
13580
|
+
nextTool,
|
|
13581
|
+
...status.data.progress === void 0 ? {} : { progress: status.data.progress }
|
|
12897
13582
|
}),
|
|
12898
13583
|
selection: status.data.selection
|
|
12899
13584
|
},
|
|
@@ -12903,12 +13588,18 @@ var EngineService = class {
|
|
|
12903
13588
|
}
|
|
12904
13589
|
async stats(input = {}) {
|
|
12905
13590
|
const snapshot = await this.load(input.root);
|
|
12906
|
-
const
|
|
13591
|
+
const stateErrors = this.stateErrors(snapshot);
|
|
13592
|
+
const archivedWords = snapshot.chapters.chapters.reduce((total, chapter) => total + chapter.word_count, 0);
|
|
13593
|
+
const activeIndexed = snapshot.chapters.chapters.some((chapter) => chapter.num === snapshot.pipeline.chapter);
|
|
13594
|
+
const draftWords = snapshot.text === null || activeIndexed || snapshot.pipeline.stage === "done" ? 0 : countWordMetrics(snapshot.text).total_word_count;
|
|
13595
|
+
const totalWords = archivedWords + draftWords;
|
|
12907
13596
|
const scores = snapshot.chapters.chapters.map((chapter) => chapter.score);
|
|
12908
13597
|
const outline = outlineSummary(snapshot.outline, snapshot.chapters.chapters.length, snapshot.project.targets.total_chapters, snapshot.chapters.chapters.map((record) => record.num));
|
|
12909
13598
|
const counts = worldbookCounts(snapshot.worldbook);
|
|
12910
13599
|
return {
|
|
12911
13600
|
totalWords,
|
|
13601
|
+
archivedWords,
|
|
13602
|
+
draftWords,
|
|
12912
13603
|
chapters: snapshot.chapters.chapters.length,
|
|
12913
13604
|
revision: snapshot.pipeline.revision,
|
|
12914
13605
|
rulepack: `${this.rulepack.name}@${this.rulepack.version}`,
|
|
@@ -12923,7 +13614,9 @@ var EngineService = class {
|
|
|
12923
13614
|
blueprintReady: bookPlanPresent(snapshot.outline.book),
|
|
12924
13615
|
worldbookEntries: counts.characters + counts.locations + counts.factions + counts.rules + counts.glossary + counts.backstory + counts.facts,
|
|
12925
13616
|
strictOutline: snapshot.outline.book.strict,
|
|
12926
|
-
targetChapters: snapshot.project.targets.total_chapters
|
|
13617
|
+
targetChapters: snapshot.project.targets.total_chapters,
|
|
13618
|
+
state_errors: stateErrors,
|
|
13619
|
+
partial: stateErrors.length > 0
|
|
12927
13620
|
};
|
|
12928
13621
|
}
|
|
12929
13622
|
registry() {
|
|
@@ -12942,6 +13635,17 @@ var EngineService = class {
|
|
|
12942
13635
|
...inspection.state === "ok" && !sameIdentity ? { identity_changed: true } : {}
|
|
12943
13636
|
};
|
|
12944
13637
|
}));
|
|
13638
|
+
const configured = this.defaultRoot();
|
|
13639
|
+
if (configured !== "" && !document.projects.some((entry) => resolve(entry.root) === resolve(configured))) {
|
|
13640
|
+
const inspection = await inspectProject(configured);
|
|
13641
|
+
if (inspection.state === "missing") projects.push({
|
|
13642
|
+
root: configured,
|
|
13643
|
+
title: "配置项目根",
|
|
13644
|
+
created_at: "",
|
|
13645
|
+
updated_at: "",
|
|
13646
|
+
state: inspection.state
|
|
13647
|
+
});
|
|
13648
|
+
}
|
|
12945
13649
|
return {
|
|
12946
13650
|
active: document.active,
|
|
12947
13651
|
projects
|
|
@@ -13400,21 +14104,70 @@ var EngineService = class {
|
|
|
13400
14104
|
}
|
|
13401
14105
|
};
|
|
13402
14106
|
}
|
|
13403
|
-
|
|
13404
|
-
const
|
|
13405
|
-
|
|
14107
|
+
panelReviewKey(snapshot) {
|
|
14108
|
+
const cached = this.reviewKeyCache.get(snapshot);
|
|
14109
|
+
if (cached !== void 0) return cached;
|
|
14110
|
+
const pending = (async () => {
|
|
14111
|
+
const store = this.storeFor(snapshot.root);
|
|
14112
|
+
const names = await store.list("reviews");
|
|
14113
|
+
const files = await Promise.all(names.map(async (name) => {
|
|
14114
|
+
const stat = await store.stat(`reviews/${name}`);
|
|
14115
|
+
return [name, stat === null ? null : [stat.size, Math.round(stat.mtimeMs)]];
|
|
14116
|
+
}));
|
|
14117
|
+
return sha256(stableJson(files));
|
|
14118
|
+
})();
|
|
14119
|
+
this.reviewKeyCache.set(snapshot, pending);
|
|
14120
|
+
return pending;
|
|
14121
|
+
}
|
|
14122
|
+
panelJournalIdentity(snapshot) {
|
|
14123
|
+
const cached = this.journalCache.get(snapshot);
|
|
14124
|
+
if (cached !== void 0) return cached;
|
|
14125
|
+
const pending = (async () => {
|
|
14126
|
+
const journal = await readJournal(this.storeFor(snapshot.root));
|
|
14127
|
+
const journalBlocking = await this.journalBlocking(snapshot);
|
|
14128
|
+
const tail = journal.rows[journal.rows.length - 1];
|
|
14129
|
+
return {
|
|
14130
|
+
key: sha256(stableJson({
|
|
14131
|
+
rows: journal.rows.length,
|
|
14132
|
+
tail: tail === void 0 ? null : [
|
|
14133
|
+
tail.seq,
|
|
14134
|
+
tail.at,
|
|
14135
|
+
tail.status
|
|
14136
|
+
],
|
|
14137
|
+
blocking: journalBlocking
|
|
14138
|
+
})),
|
|
14139
|
+
journal,
|
|
14140
|
+
blocking: journalBlocking
|
|
14141
|
+
};
|
|
14142
|
+
})();
|
|
14143
|
+
this.journalCache.set(snapshot, pending);
|
|
14144
|
+
return pending;
|
|
14145
|
+
}
|
|
14146
|
+
async panelSnapshot(root, options = {}) {
|
|
14147
|
+
const snapshot = options.snapshot ?? await this.loadAt(await this.resolveRoot(root));
|
|
14148
|
+
const includeQuality = options.includeQuality !== false;
|
|
13406
14149
|
const archived = snapshot.chapters.chapters;
|
|
13407
14150
|
const archivedCurrent = archived.some((chapter) => chapter.num === snapshot.pipeline.chapter);
|
|
13408
14151
|
const target = snapshot.project.targets.total_chapters;
|
|
13409
|
-
const
|
|
13410
|
-
|
|
14152
|
+
const progress = projectChapterProgress({
|
|
14153
|
+
activeChapter: snapshot.pipeline.chapter,
|
|
14154
|
+
stage: snapshot.pipeline.stage,
|
|
14155
|
+
archived: archived.map((record) => record.num),
|
|
14156
|
+
target
|
|
14157
|
+
});
|
|
14158
|
+
const finished = progress.finished;
|
|
14159
|
+
const draftWords = snapshot.text === null || archivedCurrent || snapshot.pipeline.stage === "done" ? 0 : countWordMetrics(snapshot.text).total_word_count;
|
|
14160
|
+
const archivedWords = archived.reduce((total, chapter) => total + chapter.word_count, 0);
|
|
13411
14161
|
const pending = await inspectPending(this.storeFor(snapshot.root));
|
|
13412
14162
|
const stateErrorCount = this.stateErrors(snapshot).length;
|
|
13413
|
-
const
|
|
14163
|
+
const journalIdentity = await this.panelJournalIdentity(snapshot);
|
|
14164
|
+
const journalKey = journalIdentity.key;
|
|
14165
|
+
const journal = journalIdentity.journal;
|
|
14166
|
+
const journalBlocking = journalIdentity.blocking;
|
|
14167
|
+
const reviewKey = includeQuality ? await this.panelReviewKey(snapshot) : "";
|
|
14168
|
+
const panelCacheKey = `${snapshot.root}:${snapshot.pipeline.revision}:${pending.state}:${stateErrorCount}:${snapshot.outlineError ?? ""}:${snapshot.worldbookError ?? ""}:${JSON.stringify(snapshot.briefs)}:${journalKey}:${reviewKey}:${includeQuality ? "full" : "base"}`;
|
|
13414
14169
|
const cachedPanel = this.panelCache.get(snapshot.root);
|
|
13415
14170
|
if (cachedPanel?.key === panelCacheKey) return cachedPanel.snapshot;
|
|
13416
|
-
const journal = await readJournal(this.storeFor(snapshot.root));
|
|
13417
|
-
const journalBlocking = await this.journalBlocking(snapshot);
|
|
13418
14171
|
const outline = outlineSummary(snapshot.outline, archived.length, target, archived.map((record) => record.num));
|
|
13419
14172
|
const counts = worldbookCounts(snapshot.worldbook);
|
|
13420
14173
|
const scores = archived.slice(-10).map((chapter) => chapter.score);
|
|
@@ -13461,26 +14214,31 @@ var EngineService = class {
|
|
|
13461
14214
|
const qualityScores = scores.slice(-5);
|
|
13462
14215
|
const previousScores = scores.slice(-10, -5);
|
|
13463
14216
|
const store = this.storeFor(snapshot.root);
|
|
13464
|
-
const reviewNames = new Set(await store.list(REVIEW_DIR));
|
|
13465
|
-
const sampleChapters = /* @__PURE__ */ new Set();
|
|
13466
14217
|
const currentReviews = [];
|
|
13467
|
-
|
|
13468
|
-
|
|
13469
|
-
|
|
13470
|
-
|
|
13471
|
-
|
|
13472
|
-
|
|
13473
|
-
|
|
13474
|
-
|
|
13475
|
-
|
|
13476
|
-
|
|
14218
|
+
let samples = 0;
|
|
14219
|
+
let attempts = 0;
|
|
14220
|
+
if (includeQuality) {
|
|
14221
|
+
const reviewNames = new Set(await store.list(REVIEW_DIR));
|
|
14222
|
+
const sampleChapters = /* @__PURE__ */ new Set();
|
|
14223
|
+
for (const record of archived) {
|
|
14224
|
+
const name = reviewFile(record.num).slice(`${REVIEW_DIR}/`.length);
|
|
14225
|
+
if (!reviewNames.has(name)) continue;
|
|
14226
|
+
try {
|
|
14227
|
+
const review = await store.readDoc(reviewFile(record.num), "review");
|
|
14228
|
+
if (review !== null) {
|
|
14229
|
+
sampleChapters.add(record.num);
|
|
14230
|
+
currentReviews.push(review);
|
|
14231
|
+
}
|
|
14232
|
+
} catch {}
|
|
14233
|
+
}
|
|
14234
|
+
samples = sampleChapters.size;
|
|
14235
|
+
const archivedChapterNums = new Set(archived.map((record) => record.num));
|
|
14236
|
+
attempts = [...(await this.reviewAttemptsByChapter(store, journal.rows, [...archivedChapterNums], sampleChapters)).values()].reduce((total, count) => total + count, 0);
|
|
13477
14237
|
}
|
|
13478
|
-
const samples = sampleChapters.size;
|
|
13479
|
-
const archivedChapterNums = new Set(archived.map((record) => record.num));
|
|
13480
|
-
const attempts = [...(await this.reviewAttemptsByChapter(store, journal.rows, [...archivedChapterNums], sampleChapters)).values()].reduce((total, count) => total + count, 0);
|
|
13481
14238
|
const qualityTrendBasis = samples < 3 ? "insufficient_samples" : previousScores.length < 5 ? "insufficient_history" : "last_five_vs_previous_five";
|
|
13482
14239
|
const quality = {
|
|
13483
|
-
averageScore:
|
|
14240
|
+
averageScore: archived.length === 0 ? 0 : archived.reduce((total, chapter) => total + chapter.score, 0) / archived.length,
|
|
14241
|
+
last_ten_average: scores.length === 0 ? null : scores.reduce((total, score) => total + score, 0) / scores.length,
|
|
13484
14242
|
scores,
|
|
13485
14243
|
trend: qualityTrendBasis === "last_five_vs_previous_five" ? Math.round((qualityScores.reduce((total, score) => total + score, 0) / qualityScores.length - previousScores.reduce((total, score) => total + score, 0) / previousScores.length) * 10) / 10 : null,
|
|
13486
14244
|
trend_basis: qualityTrendBasis,
|
|
@@ -13497,37 +14255,58 @@ var EngineService = class {
|
|
|
13497
14255
|
hint: `第 ${violation.location.paragraph} 段`
|
|
13498
14256
|
}));
|
|
13499
14257
|
const legacy = await this.legacyIssues(snapshot, false);
|
|
13500
|
-
const activity = journal.rows.slice(-5).map((row) => ({
|
|
14258
|
+
const activity = journal.rows.filter((row) => row.op !== "lock_reclaimed").slice(-5).map((row) => ({
|
|
13501
14259
|
at: row.at,
|
|
13502
14260
|
op: row.op,
|
|
13503
14261
|
chapter: row.chapter,
|
|
13504
14262
|
status: row.status
|
|
13505
14263
|
}));
|
|
13506
14264
|
const draftMetrics = snapshot.text === null ? null : countWordMetrics(snapshot.text);
|
|
14265
|
+
const panelReport = buildBookReport({
|
|
14266
|
+
project: snapshot.project,
|
|
14267
|
+
outline: snapshot.outline,
|
|
14268
|
+
outlinePresent: snapshot.outlinePresent,
|
|
14269
|
+
outlineError: snapshot.outlineError,
|
|
14270
|
+
worldbook: snapshot.worldbook,
|
|
14271
|
+
worldbookPresent: snapshot.worldbookPresent,
|
|
14272
|
+
worldbookError: snapshot.worldbookError,
|
|
14273
|
+
world: snapshot.world,
|
|
14274
|
+
chapters: archived,
|
|
14275
|
+
characters: snapshot.characters,
|
|
14276
|
+
timeline: snapshot.timeline,
|
|
14277
|
+
foreshadows: snapshot.foreshadows,
|
|
14278
|
+
relationships: snapshot.relationships,
|
|
14279
|
+
reviews: currentReviews,
|
|
14280
|
+
activity: journal.rows.filter((row) => row.op !== "lock_reclaimed").slice(-8),
|
|
14281
|
+
journal: journal.rows,
|
|
14282
|
+
reviewAttempts: attempts,
|
|
14283
|
+
finalChecks: Object.fromEntries(archived.filter((record) => record.final_check === "pass" || record.final_check === "fail").map((record) => [record.num, record.final_check])),
|
|
14284
|
+
targetChapters: target,
|
|
14285
|
+
minScore: this.config.quality.minScore,
|
|
14286
|
+
dimensionMax: Object.fromEntries(this.rulepack.quality.dimensions.map((dimension) => [dimension.id, dimension.max])),
|
|
14287
|
+
foreshadowDormantAfter: this.rulepack.crosschapter.foreshadow_dormant_after
|
|
14288
|
+
});
|
|
14289
|
+
if (!includeQuality) {
|
|
14290
|
+
const baseScores = archived.map((chapter) => chapter.score);
|
|
14291
|
+
const sorted = [...baseScores].sort((left, right) => left - right);
|
|
14292
|
+
const middle = Math.floor(sorted.length / 2);
|
|
14293
|
+
const median = sorted.length === 0 ? 0 : sorted.length % 2 === 0 ? (sorted[middle - 1] + sorted[middle]) / 2 : sorted[middle];
|
|
14294
|
+
panelReport.quality = {
|
|
14295
|
+
...panelReport.quality,
|
|
14296
|
+
average: baseScores.length === 0 ? 0 : baseScores.reduce((total, score) => total + score, 0) / baseScores.length,
|
|
14297
|
+
median,
|
|
14298
|
+
min: baseScores.length === 0 ? 0 : Math.min(...baseScores),
|
|
14299
|
+
max: baseScores.length === 0 ? 0 : Math.max(...baseScores),
|
|
14300
|
+
last_five: baseScores.slice(-5),
|
|
14301
|
+
trend: null,
|
|
14302
|
+
trend_basis: "insufficient_samples",
|
|
14303
|
+
samples: 0,
|
|
14304
|
+
attempts: 0,
|
|
14305
|
+
dimensions: []
|
|
14306
|
+
};
|
|
14307
|
+
}
|
|
13507
14308
|
const bookData = buildPanelBookData({
|
|
13508
|
-
report:
|
|
13509
|
-
project: snapshot.project,
|
|
13510
|
-
outline: snapshot.outline,
|
|
13511
|
-
outlinePresent: snapshot.outlinePresent,
|
|
13512
|
-
outlineError: snapshot.outlineError,
|
|
13513
|
-
worldbook: snapshot.worldbook,
|
|
13514
|
-
worldbookPresent: snapshot.worldbookPresent,
|
|
13515
|
-
worldbookError: snapshot.worldbookError,
|
|
13516
|
-
world: snapshot.world,
|
|
13517
|
-
chapters: archived,
|
|
13518
|
-
characters: snapshot.characters,
|
|
13519
|
-
timeline: snapshot.timeline,
|
|
13520
|
-
foreshadows: snapshot.foreshadows,
|
|
13521
|
-
relationships: snapshot.relationships,
|
|
13522
|
-
reviews: currentReviews,
|
|
13523
|
-
activity: journal.rows.slice(-8),
|
|
13524
|
-
journal: journal.rows,
|
|
13525
|
-
reviewAttempts: attempts,
|
|
13526
|
-
finalChecks: Object.fromEntries(archived.filter((record) => record.final_check === "pass" || record.final_check === "fail").map((record) => [record.num, record.final_check])),
|
|
13527
|
-
targetChapters: target,
|
|
13528
|
-
minScore: this.config.quality.minScore,
|
|
13529
|
-
foreshadowDormantAfter: this.rulepack.crosschapter.foreshadow_dormant_after
|
|
13530
|
-
}),
|
|
14309
|
+
report: panelReport,
|
|
13531
14310
|
project: snapshot.project,
|
|
13532
14311
|
worldbook: snapshot.worldbook,
|
|
13533
14312
|
world: snapshot.world,
|
|
@@ -13538,7 +14317,7 @@ var EngineService = class {
|
|
|
13538
14317
|
stage: snapshot.pipeline.stage,
|
|
13539
14318
|
rounds: snapshot.pipeline.rounds,
|
|
13540
14319
|
draftWords,
|
|
13541
|
-
lastTenAverage: scores.length === 0 ?
|
|
14320
|
+
lastTenAverage: scores.length === 0 ? null : scores.reduce((total, score) => total + score, 0) / scores.length,
|
|
13542
14321
|
manuscriptHash: snapshot.pipeline.draft?.content_hash ?? (snapshot.text === null ? null : sha256(snapshot.text)),
|
|
13543
14322
|
metrics: draftMetrics,
|
|
13544
14323
|
stateErrors: this.stateErrors(snapshot).map((error) => ({
|
|
@@ -13558,7 +14337,7 @@ var EngineService = class {
|
|
|
13558
14337
|
num: snapshot.pipeline.chapter,
|
|
13559
14338
|
title: stripTitlePrefix(snapshot.plan?.title ?? "") || `第${snapshot.pipeline.chapter}章`,
|
|
13560
14339
|
wordCount: draftWords,
|
|
13561
|
-
score: snapshot.review?.total ??
|
|
14340
|
+
score: snapshot.review?.total ?? null,
|
|
13562
14341
|
status: blocking.length > 0 ? "blocked" : "draft",
|
|
13563
14342
|
details: (snapshot.pipeline.draft?.summary?.events ?? []).join(";").slice(0, 240)
|
|
13564
14343
|
};
|
|
@@ -13569,6 +14348,7 @@ var EngineService = class {
|
|
|
13569
14348
|
platform: snapshot.project.platform,
|
|
13570
14349
|
chapter: snapshot.pipeline.chapter,
|
|
13571
14350
|
stage: snapshot.pipeline.stage,
|
|
14351
|
+
progress,
|
|
13572
14352
|
revision: snapshot.pipeline.revision,
|
|
13573
14353
|
rounds: snapshot.pipeline.rounds,
|
|
13574
14354
|
blocking,
|
|
@@ -13589,7 +14369,9 @@ var EngineService = class {
|
|
|
13589
14369
|
finished,
|
|
13590
14370
|
chapters: archived,
|
|
13591
14371
|
draft,
|
|
13592
|
-
totalWords:
|
|
14372
|
+
totalWords: archivedWords + draftWords,
|
|
14373
|
+
archivedWords,
|
|
14374
|
+
draftWords,
|
|
13593
14375
|
averageScore: archived.length === 0 ? 0 : archived.reduce((total, chapter) => total + chapter.score, 0) / archived.length,
|
|
13594
14376
|
phases: phaseStrip(snapshot.pipeline.stage, finished),
|
|
13595
14377
|
book,
|
|
@@ -13697,6 +14479,12 @@ var EngineService = class {
|
|
|
13697
14479
|
nextStep(snapshot) {
|
|
13698
14480
|
const stage = snapshot.pipeline.stage;
|
|
13699
14481
|
const chapter = snapshot.pipeline.chapter;
|
|
14482
|
+
const progress = projectChapterProgress({
|
|
14483
|
+
activeChapter: chapter,
|
|
14484
|
+
stage,
|
|
14485
|
+
archived: snapshot.chapters.chapters.map((record) => record.num),
|
|
14486
|
+
target: snapshot.project.targets.total_chapters
|
|
14487
|
+
});
|
|
13700
14488
|
if (snapshot.chapters.chapters.some((record) => record.num === chapter)) return {
|
|
13701
14489
|
tool: "novel_amend",
|
|
13702
14490
|
args: { path: joinRel(STATE_DIR, STATE_FILES.chapters) },
|
|
@@ -13747,8 +14535,7 @@ var EngineService = class {
|
|
|
13747
14535
|
};
|
|
13748
14536
|
}
|
|
13749
14537
|
const step = STAGE_NEXT[stage];
|
|
13750
|
-
|
|
13751
|
-
if (stage === "done" && target !== null && snapshot.chapters.chapters.length >= target) return {
|
|
14538
|
+
if (progress.finished) return {
|
|
13752
14539
|
tool: "novel_export",
|
|
13753
14540
|
args: { kind: "epub" },
|
|
13754
14541
|
reason: "全书已完成;可导出成书,或结束本书"
|
|
@@ -14356,6 +15143,7 @@ var EngineService = class {
|
|
|
14356
15143
|
briefStage: extras.briefStage ?? briefStageFor(operation, snapshot.pipeline.stage),
|
|
14357
15144
|
draftTitle: composeTitle(snapshot.pipeline.chapter, extras.draftTitle ?? snapshot.pipeline.draft?.title ?? snapshot.plan?.title ?? null, this.rulepack.format.title.pattern),
|
|
14358
15145
|
sceneType: extras.sceneType ?? snapshot.pipeline.draft?.scene_type ?? snapshot.plan?.scene_type ?? null,
|
|
15146
|
+
narrator: extras.narrator ?? snapshot.pipeline.draft?.narrator ?? snapshot.plan?.narrator ?? null,
|
|
14359
15147
|
summary: extras.summary ?? snapshot.pipeline.draft?.summary ?? snapshot.plan?.summary ?? null,
|
|
14360
15148
|
summaryProvided: extras.summaryProvided,
|
|
14361
15149
|
summaryOnly: extras.summaryOnly,
|
|
@@ -15087,6 +15875,29 @@ const machineWarnings = (machine, acknowledged = []) => machine.violations.filte
|
|
|
15087
15875
|
message: `${violation.detail} (paragraph ${violation.location.paragraph})`
|
|
15088
15876
|
}));
|
|
15089
15877
|
/** 0.1.2 archive digests used localeCompare; keep that comparison only for upgrades. */
|
|
15878
|
+
const archivedCardSnapshot = (card) => ({
|
|
15879
|
+
title: card.title,
|
|
15880
|
+
scene_type: card.scene_type,
|
|
15881
|
+
target_words: [card.target_words[0], card.target_words[1]],
|
|
15882
|
+
cast: [...card.cast],
|
|
15883
|
+
goal: card.goal,
|
|
15884
|
+
conflict: card.conflict,
|
|
15885
|
+
hook: card.hook,
|
|
15886
|
+
notes: card.notes,
|
|
15887
|
+
archived_at: card.archived_at
|
|
15888
|
+
});
|
|
15889
|
+
const archivedCardDigest = (card) => `v1:${sha256(stableJson(card))}`;
|
|
15890
|
+
const archivedCardRecord = (record) => {
|
|
15891
|
+
const card = record.archive_card;
|
|
15892
|
+
if (card === void 0) return {};
|
|
15893
|
+
return {
|
|
15894
|
+
num: record.num,
|
|
15895
|
+
...card,
|
|
15896
|
+
status: "archived",
|
|
15897
|
+
word_count: record.word_count,
|
|
15898
|
+
score: record.score
|
|
15899
|
+
};
|
|
15900
|
+
};
|
|
15090
15901
|
const legacyStableJson = (value) => {
|
|
15091
15902
|
const normalize = (input) => {
|
|
15092
15903
|
if (Array.isArray(input)) return input.map(normalize);
|
|
@@ -15236,6 +16047,9 @@ const amendProblems = (kind, oldDocument, nextDocument, snapshot, path, archived
|
|
|
15236
16047
|
}
|
|
15237
16048
|
if (before.content_hash !== after.content_hash) problems.push(`archived chapter ${String(before.num)} cannot change its content_hash`);
|
|
15238
16049
|
if (before.archive_digest !== after.archive_digest) problems.push(`archived chapter ${String(before.num)} cannot change its archive_digest`);
|
|
16050
|
+
if (before.archive_card === void 0 && after.archive_card !== void 0) problems.push(`archived chapter ${String(before.num)} cannot add archive_card through amend`);
|
|
16051
|
+
else if (before.archive_card !== void 0 && (after.archive_card === void 0 || stableJson(before.archive_card) !== stableJson(after.archive_card))) problems.push(`archived chapter ${String(before.num)} cannot change its archive_card`);
|
|
16052
|
+
if (before.archive_card_digest !== after.archive_card_digest) problems.push(`archived chapter ${String(before.num)} cannot change its archive_card_digest`);
|
|
15239
16053
|
if (stableJson(before.assigned_ids ?? {}) !== stableJson(after.assigned_ids ?? {})) problems.push(`archived chapter ${String(before.num)} cannot change its assigned_ids`);
|
|
15240
16054
|
for (const field of [
|
|
15241
16055
|
"title",
|
|
@@ -15410,6 +16224,10 @@ const amendProblems = (kind, oldDocument, nextDocument, snapshot, path, archived
|
|
|
15410
16224
|
}
|
|
15411
16225
|
const indexed = new Set(snapshot.chapters.chapters.map((record) => record.num));
|
|
15412
16226
|
const chaptersUnreadable = snapshot.ledgerErrors.some((error) => error.code === "CHAPTERS_UNREADABLE");
|
|
16227
|
+
if (chaptersUnreadable) {
|
|
16228
|
+
for (const [num, card] of byNum) if (card.status === "archived" && !oldCards.has(num)) problems.push(`cannot verify archived chapter ${num} while chapters.yaml is unreadable; restore the chapter index from backups/ first`);
|
|
16229
|
+
for (const num of archivedChapterNumbers ?? []) if (byNum.get(num)?.status !== "archived") problems.push(`cannot verify archived chapter ${num} while chapters.yaml is unreadable; keep its archived card`);
|
|
16230
|
+
}
|
|
15413
16231
|
if (!chaptersUnreadable) {
|
|
15414
16232
|
for (const [num, card] of byNum) if (card.status === "archived" && !indexed.has(num)) problems.push(`archived chapter ${num} has a card but is not in the chapter index`);
|
|
15415
16233
|
}
|
|
@@ -15446,6 +16264,12 @@ const amendProblems = (kind, oldDocument, nextDocument, snapshot, path, archived
|
|
|
15446
16264
|
problems.push(`archived chapter ${record.num} has no previous outline card; restore its archive backup before repairing target_words/cast/archived_at`);
|
|
15447
16265
|
continue;
|
|
15448
16266
|
}
|
|
16267
|
+
for (const field of [
|
|
16268
|
+
"goal",
|
|
16269
|
+
"conflict",
|
|
16270
|
+
"hook",
|
|
16271
|
+
"notes"
|
|
16272
|
+
]) if (stableJson(before[field]) !== stableJson(card[field])) problems.push(`archived chapter ${record.num} cannot change its ${field}`);
|
|
15449
16273
|
if (JSON.stringify(before.target_words) !== JSON.stringify(card.target_words)) problems.push(`archived chapter ${record.num} cannot change its target_words`);
|
|
15450
16274
|
const beforeCast = Array.isArray(before.cast) ? before.cast : [];
|
|
15451
16275
|
const afterCast = Array.isArray(card.cast) ? card.cast : [];
|
|
@@ -15457,6 +16281,27 @@ const amendProblems = (kind, oldDocument, nextDocument, snapshot, path, archived
|
|
|
15457
16281
|
for (const [num, card] of oldCards) if (card.status === "archived") expected.add(num);
|
|
15458
16282
|
const missing = [...expected].filter((num) => byNum.get(num)?.status !== "archived");
|
|
15459
16283
|
if (missing.length > 0) problems.push(`cannot verify archived cards while chapters.yaml is unreadable; keep archived cards for ${missing.join(", ")}`);
|
|
16284
|
+
for (const [num, before] of oldCards) {
|
|
16285
|
+
if (before.status !== "archived") continue;
|
|
16286
|
+
const card = byNum.get(num);
|
|
16287
|
+
if (card === void 0) continue;
|
|
16288
|
+
if (card.status !== "archived") problems.push(`archived chapter ${num} cannot be changed back to planned`);
|
|
16289
|
+
for (const field of [
|
|
16290
|
+
"title",
|
|
16291
|
+
"scene_type",
|
|
16292
|
+
"word_count",
|
|
16293
|
+
"score",
|
|
16294
|
+
"goal",
|
|
16295
|
+
"conflict",
|
|
16296
|
+
"hook",
|
|
16297
|
+
"notes"
|
|
16298
|
+
]) if (stableJson(before[field]) !== stableJson(card[field])) problems.push(`archived chapter ${num} cannot change its ${field}`);
|
|
16299
|
+
if (JSON.stringify(before.target_words) !== JSON.stringify(card.target_words)) problems.push(`archived chapter ${num} cannot change its target_words`);
|
|
16300
|
+
const beforeCast = Array.isArray(before.cast) ? before.cast : [];
|
|
16301
|
+
const afterCast = Array.isArray(card.cast) ? card.cast : [];
|
|
16302
|
+
if (beforeCast.length !== afterCast.length || afterCast.some((name) => !beforeCast.includes(name))) problems.push(`archived chapter ${num} cannot change its cast`);
|
|
16303
|
+
if (before.archived_at !== card.archived_at && !(before.archived_at === null && typeof card.archived_at === "string")) problems.push(`archived chapter ${num} cannot change archived_at`);
|
|
16304
|
+
}
|
|
15460
16305
|
}
|
|
15461
16306
|
return problems;
|
|
15462
16307
|
}
|
|
@@ -15676,16 +16521,17 @@ function diffLines(before, after) {
|
|
|
15676
16521
|
const arraySemanticsDescription = (semantics, description) => `[x-semantics: ${semantics}] ${description}`;
|
|
15677
16522
|
// src/tools/index.ts
|
|
15678
16523
|
/**
|
|
15679
|
-
*
|
|
15680
|
-
*
|
|
15681
|
-
* string
|
|
16524
|
+
* Compatibility helper for already-serialized legacy payloads and dynamic
|
|
16525
|
+
* maps. Model-facing schemas are typed and reject strings before this runs;
|
|
16526
|
+
* the string branch is kept only for old callers that invoke the adapter
|
|
16527
|
+
* directly.
|
|
15682
16528
|
*/
|
|
15683
16529
|
const asJson = (value, field) => {
|
|
15684
16530
|
if (typeof value !== "string") return value;
|
|
15685
16531
|
try {
|
|
15686
16532
|
return JSON.parse(value);
|
|
15687
16533
|
} catch {
|
|
15688
|
-
throw new EngineInputError(ERROR_CODES.SCHEMA_INVALID, `${field} must be a JSON object
|
|
16534
|
+
throw new EngineInputError(ERROR_CODES.SCHEMA_INVALID, `${field} must be a JSON object`, `send ${field} as an object, not a serialized string`);
|
|
15689
16535
|
}
|
|
15690
16536
|
};
|
|
15691
16537
|
/** Turn an engine failure into the message shape tools surface. */
|
|
@@ -15755,17 +16601,255 @@ const stringArray = (semantics, description) => ({
|
|
|
15755
16601
|
items: { type: "string" },
|
|
15756
16602
|
description: arraySemanticsDescription(semantics, description)
|
|
15757
16603
|
});
|
|
16604
|
+
const str = { type: "string" };
|
|
16605
|
+
const int = { type: "integer" };
|
|
16606
|
+
const num$1 = { type: "number" };
|
|
16607
|
+
const bool$1 = { type: "boolean" };
|
|
16608
|
+
const nullableInt = { oneOf: [{ type: "integer" }, { type: "null" }] };
|
|
16609
|
+
const nullableStr = { oneOf: [{ type: "string" }, { type: "null" }] };
|
|
16610
|
+
const required = (value) => ({
|
|
16611
|
+
...value,
|
|
16612
|
+
required: true
|
|
16613
|
+
});
|
|
16614
|
+
const optional = (value) => {
|
|
16615
|
+
const copy = { ...value };
|
|
16616
|
+
delete copy.required;
|
|
16617
|
+
return copy;
|
|
16618
|
+
};
|
|
16619
|
+
const arrayOf = (items) => ({
|
|
16620
|
+
type: "array",
|
|
16621
|
+
items
|
|
16622
|
+
});
|
|
16623
|
+
const objectOf = (properties) => ({
|
|
16624
|
+
type: "object",
|
|
16625
|
+
additionalProperties: false,
|
|
16626
|
+
properties
|
|
16627
|
+
});
|
|
16628
|
+
const openObject = (description) => ({
|
|
16629
|
+
type: "object",
|
|
16630
|
+
additionalProperties: true,
|
|
16631
|
+
...description === void 0 ? {} : { description }
|
|
16632
|
+
});
|
|
16633
|
+
const openObjectWith = (properties, description) => ({
|
|
16634
|
+
type: "object",
|
|
16635
|
+
additionalProperties: true,
|
|
16636
|
+
properties,
|
|
16637
|
+
...description === void 0 ? {} : { description }
|
|
16638
|
+
});
|
|
16639
|
+
const readingProofRef = objectOf({
|
|
16640
|
+
chapter: int,
|
|
16641
|
+
paragraph: required(int),
|
|
16642
|
+
quote: required(str)
|
|
16643
|
+
});
|
|
16644
|
+
const proofSchema = required(objectOf({
|
|
16645
|
+
opening: required(readingProofRef),
|
|
16646
|
+
middle: required(readingProofRef),
|
|
16647
|
+
ending: required(readingProofRef)
|
|
16648
|
+
}));
|
|
16649
|
+
const issueSchema = objectOf({
|
|
16650
|
+
id: required(str),
|
|
16651
|
+
severity: required({
|
|
16652
|
+
type: "string",
|
|
16653
|
+
enum: ["blocker", "advisory"]
|
|
16654
|
+
}),
|
|
16655
|
+
dimension: required(str),
|
|
16656
|
+
location: required(str),
|
|
16657
|
+
problem: required(str),
|
|
16658
|
+
fix: required(str),
|
|
16659
|
+
status: required({
|
|
16660
|
+
type: "string",
|
|
16661
|
+
enum: [
|
|
16662
|
+
"open",
|
|
16663
|
+
"fixed",
|
|
16664
|
+
"waived"
|
|
16665
|
+
]
|
|
16666
|
+
}),
|
|
16667
|
+
evidence: str
|
|
16668
|
+
});
|
|
16669
|
+
const issueUpdateSchema = objectOf({
|
|
16670
|
+
id: required(str),
|
|
16671
|
+
status: required({
|
|
16672
|
+
type: "string",
|
|
16673
|
+
enum: [
|
|
16674
|
+
"open",
|
|
16675
|
+
"fixed",
|
|
16676
|
+
"waived"
|
|
16677
|
+
]
|
|
16678
|
+
}),
|
|
16679
|
+
evidence: str,
|
|
16680
|
+
severity: {
|
|
16681
|
+
type: "string",
|
|
16682
|
+
enum: ["blocker", "advisory"]
|
|
16683
|
+
},
|
|
16684
|
+
dimension: str,
|
|
16685
|
+
location: str,
|
|
16686
|
+
problem: str,
|
|
16687
|
+
fix: str
|
|
16688
|
+
});
|
|
16689
|
+
const dimensionSchema = objectOf({
|
|
16690
|
+
id: required(str),
|
|
16691
|
+
max: required(int),
|
|
16692
|
+
deductions: required(arrayOf(objectOf({
|
|
16693
|
+
rule: required(str),
|
|
16694
|
+
location: required(str),
|
|
16695
|
+
delta: required(int),
|
|
16696
|
+
note: required(str)
|
|
16697
|
+
})))
|
|
16698
|
+
});
|
|
16699
|
+
const summarySchema = required(objectOf({
|
|
16700
|
+
events: required(arrayOf(str)),
|
|
16701
|
+
characters_appeared: required(arrayOf(str)),
|
|
16702
|
+
foreshadows: required(objectOf({
|
|
16703
|
+
planted: required(arrayOf(objectOf({
|
|
16704
|
+
key: required(str),
|
|
16705
|
+
title: required(str),
|
|
16706
|
+
description: required(str),
|
|
16707
|
+
anchor: str
|
|
16708
|
+
}))),
|
|
16709
|
+
advanced: required(arrayOf(objectOf({
|
|
16710
|
+
key: required(str),
|
|
16711
|
+
note: required(str),
|
|
16712
|
+
new_info: required(bool$1)
|
|
16713
|
+
}))),
|
|
16714
|
+
resolved: required(arrayOf(str))
|
|
16715
|
+
})),
|
|
16716
|
+
new_elements: required(objectOf({
|
|
16717
|
+
locations: required(arrayOf(str)),
|
|
16718
|
+
abilities: required(arrayOf(str)),
|
|
16719
|
+
artifacts: required(arrayOf(str))
|
|
16720
|
+
})),
|
|
16721
|
+
new_element_anchors: openObject("实体名到正文锚点数组的动态映射;值形状由引擎校验"),
|
|
16722
|
+
resource_changes: required(arrayOf(objectOf({
|
|
16723
|
+
character: required(str),
|
|
16724
|
+
resource: required(str),
|
|
16725
|
+
delta: required(int),
|
|
16726
|
+
reason: required(str)
|
|
16727
|
+
}))),
|
|
16728
|
+
opening: required(str),
|
|
16729
|
+
continuity: required(objectOf({
|
|
16730
|
+
opens_from: required(str),
|
|
16731
|
+
ends_with: required(str)
|
|
16732
|
+
})),
|
|
16733
|
+
hook_strength: required({
|
|
16734
|
+
type: "integer",
|
|
16735
|
+
enum: [
|
|
16736
|
+
1,
|
|
16737
|
+
2,
|
|
16738
|
+
3,
|
|
16739
|
+
4,
|
|
16740
|
+
5
|
|
16741
|
+
]
|
|
16742
|
+
}),
|
|
16743
|
+
pleasure_points: required(arrayOf(str)),
|
|
16744
|
+
plot_advancement: required({
|
|
16745
|
+
type: "string",
|
|
16746
|
+
enum: [
|
|
16747
|
+
"high",
|
|
16748
|
+
"mid",
|
|
16749
|
+
"low"
|
|
16750
|
+
]
|
|
16751
|
+
})
|
|
16752
|
+
}));
|
|
16753
|
+
const volumeSchema = objectOf({
|
|
16754
|
+
id: str,
|
|
16755
|
+
title: required(str),
|
|
16756
|
+
start_chapter: required(int),
|
|
16757
|
+
end_chapter: required(int),
|
|
16758
|
+
goal: str,
|
|
16759
|
+
arc: str,
|
|
16760
|
+
climax: str,
|
|
16761
|
+
status: {
|
|
16762
|
+
type: "string",
|
|
16763
|
+
enum: [
|
|
16764
|
+
"planned",
|
|
16765
|
+
"writing",
|
|
16766
|
+
"done"
|
|
16767
|
+
]
|
|
16768
|
+
}
|
|
16769
|
+
});
|
|
16770
|
+
const chapterCardSchema = openObjectWith({
|
|
16771
|
+
num: required(int),
|
|
16772
|
+
volume: nullableInt,
|
|
16773
|
+
title: required(str),
|
|
16774
|
+
scene_type: required({
|
|
16775
|
+
type: "string",
|
|
16776
|
+
enum: SCENE_TYPES
|
|
16777
|
+
}),
|
|
16778
|
+
goal: required(str),
|
|
16779
|
+
conflict: required(str),
|
|
16780
|
+
hook: required(str),
|
|
16781
|
+
cast: arrayOf(str),
|
|
16782
|
+
target_words: required(arrayOf(int)),
|
|
16783
|
+
notes: str,
|
|
16784
|
+
status: {
|
|
16785
|
+
type: "string",
|
|
16786
|
+
enum: ["planned", "archived"]
|
|
16787
|
+
},
|
|
16788
|
+
archived_at: nullableStr,
|
|
16789
|
+
word_count: nullableInt,
|
|
16790
|
+
score: nullableInt
|
|
16791
|
+
});
|
|
16792
|
+
const dynamicSection = (description) => openObject(description);
|
|
16793
|
+
const stateDeltaSchema = required(objectOf({
|
|
16794
|
+
characters: required(dynamicSection("人物名到 {action,reason?,role?,profile?,current_state?,growth?} 的映射")),
|
|
16795
|
+
foreshadows: required(dynamicSection("key 到 {action,title?,anchor?,description?,note?,new_info?} 的映射")),
|
|
16796
|
+
timeline: required(dynamicSection("summary.events 下标到 {action,story_time?,description?,location?,characters?} 的映射")),
|
|
16797
|
+
world: required(dynamicSection("名称到 {action,kind?,description?,layout?,capacity?} 的映射")),
|
|
16798
|
+
relationships: required(dynamicSection("\"甲|乙\" 到 {action,type?,description?,note?} 的映射"))
|
|
16799
|
+
}));
|
|
16800
|
+
const handoffSchema = objectOf({
|
|
16801
|
+
volume: int,
|
|
16802
|
+
volume_name: str,
|
|
16803
|
+
act: str,
|
|
16804
|
+
act_progress: str
|
|
16805
|
+
});
|
|
16806
|
+
const worldbookRule = objectOf({
|
|
16807
|
+
name: required(str),
|
|
16808
|
+
description: required(str)
|
|
16809
|
+
});
|
|
16810
|
+
const worldbookFaction = objectOf({
|
|
16811
|
+
name: required(str),
|
|
16812
|
+
description: required(str),
|
|
16813
|
+
goal: str
|
|
16814
|
+
});
|
|
16815
|
+
const worldbookLocation = objectOf({
|
|
16816
|
+
name: required(str),
|
|
16817
|
+
description: required(str),
|
|
16818
|
+
layout: str,
|
|
16819
|
+
capacity: int
|
|
16820
|
+
});
|
|
16821
|
+
const worldbookCharacter = objectOf({
|
|
16822
|
+
name: required(str),
|
|
16823
|
+
role: required(str),
|
|
16824
|
+
description: required(str),
|
|
16825
|
+
arc: str
|
|
16826
|
+
});
|
|
16827
|
+
const worldbookGlossary = objectOf({
|
|
16828
|
+
term: required(str),
|
|
16829
|
+
definition: required(str)
|
|
16830
|
+
});
|
|
16831
|
+
const worldbookBackstory = objectOf({
|
|
16832
|
+
time: required(str),
|
|
16833
|
+
event: required(str)
|
|
16834
|
+
});
|
|
16835
|
+
const worldbookFact = objectOf({
|
|
16836
|
+
alias: required(str),
|
|
16837
|
+
value: required(num$1),
|
|
16838
|
+
unit: required(str),
|
|
16839
|
+
tolerance: num$1,
|
|
16840
|
+
note: str
|
|
16841
|
+
});
|
|
15758
16842
|
const root = {
|
|
15759
16843
|
type: "string",
|
|
15760
|
-
description: "Project root
|
|
16844
|
+
description: "Project root; omit to use the session binding or the only project."
|
|
15761
16845
|
};
|
|
15762
16846
|
const initRoot = {
|
|
15763
16847
|
type: "string",
|
|
15764
|
-
description: "
|
|
16848
|
+
description: "New project root; omit to use the configured projectRoot."
|
|
15765
16849
|
};
|
|
15766
16850
|
const utilityRoot = {
|
|
15767
16851
|
type: "string",
|
|
15768
|
-
description: "Project root for
|
|
16852
|
+
description: "Project root for project-backed mode; text-only calls ignore it."
|
|
15769
16853
|
};
|
|
15770
16854
|
/**
|
|
15771
16855
|
* The twenty-eight tools are thin adapters: narrow parameters, call one service
|
|
@@ -15847,9 +16931,11 @@ function createNovelTools(engine, options = {}) {
|
|
|
15847
16931
|
render: (_args, raw) => {
|
|
15848
16932
|
const value = payload(raw);
|
|
15849
16933
|
const active = value.active_chapter ?? value.chapter;
|
|
15850
|
-
const
|
|
16934
|
+
const progressLabel = value.progress?.label ?? `第 ${value.chapter} 章 · 阶段 ${value.stage}`;
|
|
15851
16935
|
return text([
|
|
15852
|
-
`《${value.project.title}
|
|
16936
|
+
`《${value.project.title}》`,
|
|
16937
|
+
`进度:${progressLabel}`,
|
|
16938
|
+
...active === value.chapter ? [] : [`字数查询:第 ${value.chapter} 章(活动章第 ${active} 章)`],
|
|
15853
16939
|
...value.selection === void 0 ? [] : [`项目根 ${value.selection.root} · 选择来源 ${value.selection.source}`],
|
|
15854
16940
|
...value.rounds === void 0 ? [] : [`轮次:评审 ${value.rounds.review} / 修订 ${value.rounds.revise} / 校验 ${value.rounds.check}`],
|
|
15855
16941
|
value.blocking.length === 0 ? "门控:全部通过" : `门控:${value.blocking.length} 条阻塞`,
|
|
@@ -15908,11 +16994,19 @@ function createNovelTools(engine, options = {}) {
|
|
|
15908
16994
|
},
|
|
15909
16995
|
constraints: stringArray("replace", "P1 constraints; supplied list replaces the plan list."),
|
|
15910
16996
|
characters: {
|
|
15911
|
-
|
|
16997
|
+
...arrayOf(objectOf({
|
|
16998
|
+
name: required(str),
|
|
16999
|
+
role: required(str),
|
|
17000
|
+
current_state: required(str)
|
|
17001
|
+
})),
|
|
15912
17002
|
description: arraySemanticsDescription("replace", "P1 cast; supplied list replaces the cast.")
|
|
15913
17003
|
},
|
|
15914
17004
|
locations: {
|
|
15915
|
-
|
|
17005
|
+
...arrayOf(objectOf({
|
|
17006
|
+
name: required(str),
|
|
17007
|
+
layout: required(str),
|
|
17008
|
+
capacity: required(num$1)
|
|
17009
|
+
})),
|
|
15916
17010
|
description: arraySemanticsDescription("replace", "P1 locations; supplied list replaces them.")
|
|
15917
17011
|
},
|
|
15918
17012
|
continuity_notes: stringArray("replace", "P1 continuity notes; supplied list replaces them."),
|
|
@@ -15965,8 +17059,14 @@ function createNovelTools(engine, options = {}) {
|
|
|
15965
17059
|
description: arraySemanticsDescription("replace", "Plan [min,max] word target.")
|
|
15966
17060
|
},
|
|
15967
17061
|
cast: {
|
|
15968
|
-
|
|
15969
|
-
|
|
17062
|
+
...arrayOf(objectOf({
|
|
17063
|
+
name: required(str),
|
|
17064
|
+
entry_state: str,
|
|
17065
|
+
goal: str,
|
|
17066
|
+
knowledge: arrayOf(str),
|
|
17067
|
+
exit_change: str
|
|
17068
|
+
})),
|
|
17069
|
+
description: arraySemanticsDescription("replace", "Optional 1–8 characters on stage this chapter; names must exist in the P1 world plan or the character ledger.")
|
|
15970
17070
|
},
|
|
15971
17071
|
project_root: root
|
|
15972
17072
|
},
|
|
@@ -16010,9 +17110,8 @@ function createNovelTools(engine, options = {}) {
|
|
|
16010
17110
|
required: true
|
|
16011
17111
|
},
|
|
16012
17112
|
summary: {
|
|
16013
|
-
|
|
16014
|
-
|
|
16015
|
-
description: "The ten-field chapter summary required by PG-05. Pass an object (events, characters_appeared, foreshadows, new_elements, resource_changes, opening, continuity, hook_strength, pleasure_points, plot_advancement); a JSON string is accepted too."
|
|
17113
|
+
...summarySchema,
|
|
17114
|
+
description: "The ten-field chapter summary required by PG-05; send a JSON object, not a JSON string."
|
|
16016
17115
|
},
|
|
16017
17116
|
project_root: root
|
|
16018
17117
|
},
|
|
@@ -16036,12 +17135,12 @@ function createNovelTools(engine, options = {}) {
|
|
|
16036
17135
|
required: true
|
|
16037
17136
|
},
|
|
16038
17137
|
dimensions: {
|
|
16039
|
-
|
|
17138
|
+
...arrayOf(dimensionSchema),
|
|
16040
17139
|
required: true,
|
|
16041
17140
|
description: arraySemanticsDescription("replace", "Fifteen scored dimensions.")
|
|
16042
17141
|
},
|
|
16043
17142
|
issues: {
|
|
16044
|
-
|
|
17143
|
+
...arrayOf(issueSchema),
|
|
16045
17144
|
description: arraySemanticsDescription("replace", "Review issue list.")
|
|
16046
17145
|
},
|
|
16047
17146
|
total: {
|
|
@@ -16050,8 +17149,7 @@ function createNovelTools(engine, options = {}) {
|
|
|
16050
17149
|
description: "150 - Σdelta, the same number the review reports."
|
|
16051
17150
|
},
|
|
16052
17151
|
proof: {
|
|
16053
|
-
|
|
16054
|
-
required: true,
|
|
17152
|
+
...proofSchema,
|
|
16055
17153
|
description: arraySemanticsDescription("replace", "Three reading-proof quotes.")
|
|
16056
17154
|
},
|
|
16057
17155
|
project_root: root
|
|
@@ -16078,20 +17176,24 @@ function createNovelTools(engine, options = {}) {
|
|
|
16078
17176
|
type: "string",
|
|
16079
17177
|
description: "Full revised text; mutually exclusive with patch."
|
|
16080
17178
|
},
|
|
16081
|
-
patch: {
|
|
16082
|
-
|
|
16083
|
-
|
|
16084
|
-
|
|
17179
|
+
patch: objectOf({
|
|
17180
|
+
expected_hash: required(str),
|
|
17181
|
+
edits: required(arrayOf(objectOf({
|
|
17182
|
+
old_text: required(str),
|
|
17183
|
+
new_text: required(str),
|
|
17184
|
+
occurrence: int
|
|
17185
|
+
})))
|
|
17186
|
+
}),
|
|
16085
17187
|
summary: {
|
|
16086
|
-
|
|
17188
|
+
...optional(summarySchema),
|
|
16087
17189
|
description: "Full ten-field summary. Omit content+patch with a summary to repair only pipeline.draft.summary; this does not consume a revise round."
|
|
16088
17190
|
},
|
|
16089
17191
|
issues: {
|
|
16090
|
-
|
|
17192
|
+
...arrayOf(issueUpdateSchema),
|
|
16091
17193
|
description: arraySemanticsDescription("replace", "Updated issue statuses and evidence for PG-08.")
|
|
16092
17194
|
},
|
|
16093
17195
|
proof: {
|
|
16094
|
-
|
|
17196
|
+
...optional(proofSchema),
|
|
16095
17197
|
description: arraySemanticsDescription("replace", "Three quotes from the revised text; required unless this is a summary-only repair. PG-09 verifies them.")
|
|
16096
17198
|
},
|
|
16097
17199
|
project_root: root
|
|
@@ -16117,7 +17219,24 @@ function createNovelTools(engine, options = {}) {
|
|
|
16117
17219
|
},
|
|
16118
17220
|
scope: stringArray("replace", "Requested check scopes; supplied list replaces the request."),
|
|
16119
17221
|
semantic: {
|
|
16120
|
-
|
|
17222
|
+
...arrayOf(objectOf({
|
|
17223
|
+
id: required({
|
|
17224
|
+
type: "string",
|
|
17225
|
+
enum: [
|
|
17226
|
+
"L2-SCENE-ANCHOR",
|
|
17227
|
+
"L2-CHARACTER-STATE",
|
|
17228
|
+
"L2-CAUSALITY",
|
|
17229
|
+
"L2-VOICE",
|
|
17230
|
+
"L2-POV"
|
|
17231
|
+
]
|
|
17232
|
+
}),
|
|
17233
|
+
verdict: required({
|
|
17234
|
+
type: "string",
|
|
17235
|
+
enum: ["pass", "fail"]
|
|
17236
|
+
}),
|
|
17237
|
+
evidence: required(str),
|
|
17238
|
+
note: required(str)
|
|
17239
|
+
})),
|
|
16121
17240
|
required: true,
|
|
16122
17241
|
description: arraySemanticsDescription("replace", "Exactly the five P6 semantic items, each with a verbatim evidence quote.")
|
|
16123
17242
|
},
|
|
@@ -16146,10 +17265,10 @@ function createNovelTools(engine, options = {}) {
|
|
|
16146
17265
|
required: true
|
|
16147
17266
|
},
|
|
16148
17267
|
state_delta: {
|
|
16149
|
-
|
|
16150
|
-
|
|
17268
|
+
...stateDeltaSchema,
|
|
17269
|
+
description: "Five ledger sections; entry values are dynamic maps validated by the engine."
|
|
16151
17270
|
},
|
|
16152
|
-
handoff:
|
|
17271
|
+
handoff: handoffSchema,
|
|
16153
17272
|
project_root: root
|
|
16154
17273
|
},
|
|
16155
17274
|
output: {
|
|
@@ -16259,7 +17378,7 @@ function createNovelTools(engine, options = {}) {
|
|
|
16259
17378
|
]
|
|
16260
17379
|
},
|
|
16261
17380
|
content: {
|
|
16262
|
-
|
|
17381
|
+
...openObject("按 path 的完整替换文档;根必须是映射。"),
|
|
16263
17382
|
required: true
|
|
16264
17383
|
},
|
|
16265
17384
|
reason: {
|
|
@@ -16402,7 +17521,7 @@ function createNovelTools(engine, options = {}) {
|
|
|
16402
17521
|
description: "Require a chapter card before P2/P3."
|
|
16403
17522
|
},
|
|
16404
17523
|
volumes: {
|
|
16405
|
-
|
|
17524
|
+
...arrayOf(volumeSchema),
|
|
16406
17525
|
description: arraySemanticsDescription("replace", "Volume list: [{title,start_chapter,end_chapter,goal?,arc?,climax?,status?}]. Replaces the current volumes; dropping any volume requires confirm_remove:true.")
|
|
16407
17526
|
},
|
|
16408
17527
|
confirm_remove: {
|
|
@@ -16432,15 +17551,25 @@ function createNovelTools(engine, options = {}) {
|
|
|
16432
17551
|
description: "Upsert volume/chapter cards by key: chapters by num, volumes by title. Omitted cards are preserved; deletion requires remove/remove_volumes. Archived cards are preserved and cannot be rewritten. Use dry_run to preview. On the first submission strict mode defaults on; with an existing card, PG-13 checks title, scene, target words and cast even in non-strict mode.",
|
|
16433
17552
|
parameters: {
|
|
16434
17553
|
book: {
|
|
16435
|
-
|
|
17554
|
+
...objectOf({
|
|
17555
|
+
premise: str,
|
|
17556
|
+
logline: str,
|
|
17557
|
+
themes: stringArray("replace", "Book themes; supplied list replaces the theme list."),
|
|
17558
|
+
tone: str,
|
|
17559
|
+
pov: str,
|
|
17560
|
+
audience: str,
|
|
17561
|
+
selling_points: stringArray("replace", "Selling points; supplied list replaces the list."),
|
|
17562
|
+
ending: str,
|
|
17563
|
+
strict: bool$1
|
|
17564
|
+
}),
|
|
16436
17565
|
description: "Optional partial book blueprint patch."
|
|
16437
17566
|
},
|
|
16438
17567
|
volumes: {
|
|
16439
|
-
|
|
17568
|
+
...arrayOf(volumeSchema),
|
|
16440
17569
|
description: arraySemanticsDescription("upsert", "Volume list: [{title,start_chapter,end_chapter,goal?,arc?,climax?,status?}]. Upserts by title; omitted volumes are kept.")
|
|
16441
17570
|
},
|
|
16442
17571
|
chapters: {
|
|
16443
|
-
|
|
17572
|
+
...arrayOf(chapterCardSchema),
|
|
16444
17573
|
description: arraySemanticsDescription("upsert", "Chapter cards: [{num,title,scene_type,goal,conflict,hook,cast?,target_words:[min,max],volume?,notes?}]. Upserts by num; omitted cards are kept.")
|
|
16445
17574
|
},
|
|
16446
17575
|
remove: {
|
|
@@ -16507,31 +17636,31 @@ function createNovelTools(engine, options = {}) {
|
|
|
16507
17636
|
premise: { type: "string" },
|
|
16508
17637
|
setting: { type: "string" },
|
|
16509
17638
|
rules: {
|
|
16510
|
-
|
|
17639
|
+
...arrayOf(worldbookRule),
|
|
16511
17640
|
description: arraySemanticsDescription("replace", "Rules section list.")
|
|
16512
17641
|
},
|
|
16513
17642
|
factions: {
|
|
16514
|
-
|
|
17643
|
+
...arrayOf(worldbookFaction),
|
|
16515
17644
|
description: arraySemanticsDescription("replace", "Factions section list.")
|
|
16516
17645
|
},
|
|
16517
17646
|
locations: {
|
|
16518
|
-
|
|
17647
|
+
...arrayOf(worldbookLocation),
|
|
16519
17648
|
description: arraySemanticsDescription("replace", "Locations section list.")
|
|
16520
17649
|
},
|
|
16521
17650
|
characters: {
|
|
16522
|
-
|
|
17651
|
+
...arrayOf(worldbookCharacter),
|
|
16523
17652
|
description: arraySemanticsDescription("replace", "Characters section list.")
|
|
16524
17653
|
},
|
|
16525
17654
|
glossary: {
|
|
16526
|
-
|
|
17655
|
+
...arrayOf(worldbookGlossary),
|
|
16527
17656
|
description: arraySemanticsDescription("replace", "Glossary section list.")
|
|
16528
17657
|
},
|
|
16529
17658
|
backstory: {
|
|
16530
|
-
|
|
17659
|
+
...arrayOf(worldbookBackstory),
|
|
16531
17660
|
description: arraySemanticsDescription("replace", "Backstory section list.")
|
|
16532
17661
|
},
|
|
16533
17662
|
facts: {
|
|
16534
|
-
|
|
17663
|
+
...arrayOf(worldbookFact),
|
|
16535
17664
|
description: arraySemanticsDescription("replace", "Declared numeric canon: [{alias,value,unit,tolerance?,note?}].")
|
|
16536
17665
|
},
|
|
16537
17666
|
themes: stringArray("replace", "Worldbook themes; supplied list replaces the list."),
|
|
@@ -16679,6 +17808,9 @@ const NEXT_DETAILS = {
|
|
|
16679
17808
|
novel_rollback: "回退到更早阶段重建当前章。",
|
|
16680
17809
|
novel_amend: "按明确的修复理由修正一份状态文档。",
|
|
16681
17810
|
novel_audit: "先查看事务日志,确认并恢复损坏的历史。",
|
|
17811
|
+
novel_restore: "按备份清单恢复损坏的状态文档或当前章。",
|
|
17812
|
+
novel_state: "查看状态文档、大纲、世界观或历史原文。",
|
|
17813
|
+
novel_pipeline: "按当前阶段领取下一步工具与参数。",
|
|
16682
17814
|
novel_export: "导出成书或书级提案。",
|
|
16683
17815
|
novel_diff: "对比当前正文与上一版或给定文本。",
|
|
16684
17816
|
novel_word_count: "按引擎字数口径核对正文长度。"
|
|
@@ -16803,9 +17935,11 @@ const failure = (error) => {
|
|
|
16803
17935
|
const formatPercent = (value) => value === null || value === void 0 ? "—" : `${Math.round(value * 1e3) / 10}%`;
|
|
16804
17936
|
const statusText = async (engine) => {
|
|
16805
17937
|
const { data } = await engine.getStatus({});
|
|
17938
|
+
const progressLabel = data.progress?.label ?? `第 ${data.chapter} 章 · 阶段 ${data.stage}`;
|
|
16806
17939
|
return [
|
|
16807
17940
|
`《${data.project.title}》 ${data.project.genre} · ${data.project.platform}`,
|
|
16808
|
-
|
|
17941
|
+
`进度:${progressLabel}`,
|
|
17942
|
+
...data.chapter === data.active_chapter ? [] : [`字数查询:第 ${data.chapter} 章(活动章第 ${data.active_chapter} 章)`],
|
|
16809
17943
|
...data.selection === void 0 ? [] : [`项目根 ${data.selection.root} · 选择来源 ${data.selection.source}`],
|
|
16810
17944
|
`轮次 评审 ${data.rounds.review} / 修订 ${data.rounds.revise} / 校验 ${data.rounds.check}`,
|
|
16811
17945
|
data.metrics === null ? "字数 —" : `字数 ${data.metrics.total_word_count}(汉字 ${data.metrics.cjk_chars} + 标点 ${data.metrics.punctuation_chars},含标点)`,
|
|
@@ -16852,11 +17986,13 @@ function createCommands(engine, host = {}) {
|
|
|
16852
17986
|
case "next": {
|
|
16853
17987
|
const { data } = await engine.advise({});
|
|
16854
17988
|
const reason = NEXT_DETAILS[data.tool] ?? data.reason;
|
|
16855
|
-
return ok(
|
|
17989
|
+
return ok(`进度:${data.progress?.label ?? `第 ${data.chapter} 章 · 阶段 ${data.stage}`}\n下一步:${data.tool}\n理由:${reason}${data.blocking.length === 0 ? "" : `\n阻塞 ${data.blocking.length} 条`}`);
|
|
16856
17990
|
}
|
|
16857
17991
|
case "stats": {
|
|
16858
17992
|
const stats = await engine.stats({});
|
|
16859
|
-
|
|
17993
|
+
const chapterError = (stats.state_errors ?? []).find((error) => error.path === "state/chapters.yaml");
|
|
17994
|
+
if (chapterError !== void 0) return bad(`章节索引不可读(${chapterError.code}):${chapterError.message}\n修复前不提供 0 值统计;先 novel_amend state/chapters.yaml 或从 backups/ 恢复。`);
|
|
17995
|
+
return ok(`章节 ${stats.chapters} 章 · 总字数 ${stats.totalWords}(已归档 ${stats.archivedWords} + 草稿 ${stats.draftWords}) · 平均分 ${stats.averageScore} · 未回收伏笔 ${stats.openForeshadows} · 大纲 ${stats.totalCards} 卡(规划覆盖 ${formatPercent(stats.outlineCoverage)} · 归档覆盖 ${formatPercent(stats.archivedCoverage)}${stats.missingCards > 0 ? ` · 缺卡 ${stats.missingCards}` : ""})· 蓝图 ${stats.blueprintReady ? "就绪" : "未就绪"} · 严格 ${stats.strictOutline ? "开" : "关"} · 目标 ${stats.targetChapters ?? "未设"} 章 · 世界观 ${stats.worldbookEntries} 条 · revision ${stats.revision} · 规则包 ${stats.rulepack}${stats.partial ?? false ? " · 部分状态不可读" : ""}`);
|
|
16860
17996
|
}
|
|
16861
17997
|
case "outline": {
|
|
16862
17998
|
const status = await engine.getStatus({});
|
|
@@ -16886,7 +18022,7 @@ function createCommands(engine, host = {}) {
|
|
|
16886
18022
|
case "report": {
|
|
16887
18023
|
const { data } = await engine.report({});
|
|
16888
18024
|
return ok([
|
|
16889
|
-
`进度:${data.progress.archived}/${data.progress.target ?? "—"} 章 · ${data.progress.words} 字 · ${data.progress.percent}
|
|
18025
|
+
`进度:${data.progress.archived}/${data.progress.target ?? "—"} 章 · ${data.progress.words} 字 · ${data.progress.percent === null ? "—" : `${data.progress.percent}%`}`,
|
|
16890
18026
|
`大纲:${data.outline.planned} 计划 / ${data.outline.archived} 归档 · 漂移 ${data.outline.drift.length} 项`,
|
|
16891
18027
|
`质量:均分 ${data.quality.average} · 中位 ${data.quality.median} · 趋势 ${data.quality.trend === null ? "—" : `${data.quality.trend >= 0 ? "+" : ""}${data.quality.trend}`} · ${data.quality.samples} 章样本 / ${data.quality.attempts} 次评审`,
|
|
16892
18028
|
`伏笔:未回收 ${data.foreshadows.open} · 逾期 ${data.foreshadows.overdue}`,
|
|
@@ -17000,8 +18136,10 @@ function createCommands(engine, host = {}) {
|
|
|
17000
18136
|
}
|
|
17001
18137
|
case "resume": {
|
|
17002
18138
|
const { data } = await engine.resumePrompt({});
|
|
17003
|
-
if (data.text
|
|
17004
|
-
|
|
18139
|
+
if (data.text !== null) return ok(data.text);
|
|
18140
|
+
const status = await engine.getStatus({});
|
|
18141
|
+
if (status.data.progress?.finished === true) return ok(status.data.progress.resume);
|
|
18142
|
+
return bad("当前项目没有可续写的下一步;先用 /novel status 查看项目状态。");
|
|
17005
18143
|
}
|
|
17006
18144
|
case "admin": {
|
|
17007
18145
|
if (rest.length === 0) return ok(adminHelp());
|
|
@@ -17061,7 +18199,9 @@ function createCommands(engine, host = {}) {
|
|
|
17061
18199
|
if (invocation.signal.aborted) return bad("已取消");
|
|
17062
18200
|
try {
|
|
17063
18201
|
const stats = await engine.stats({});
|
|
17064
|
-
|
|
18202
|
+
const chapterError = (stats.state_errors ?? []).find((error) => error.path === "state/chapters.yaml");
|
|
18203
|
+
if (chapterError !== void 0) return bad(`章节索引不可读(${chapterError.code}):${chapterError.message}\n修复前不提供 0 值统计;先 novel_amend state/chapters.yaml 或从 backups/ 恢复。`);
|
|
18204
|
+
return ok(`章节 ${stats.chapters} 章 · 总字数 ${stats.totalWords}(已归档 ${stats.archivedWords} + 草稿 ${stats.draftWords}) · 平均分 ${stats.averageScore} · 未回收伏笔 ${stats.openForeshadows} · 大纲 ${stats.totalCards} 卡(规划覆盖 ${formatPercent(stats.outlineCoverage)} · 归档覆盖 ${formatPercent(stats.archivedCoverage)}${stats.missingCards > 0 ? ` · 缺卡 ${stats.missingCards}` : ""})· 蓝图 ${stats.blueprintReady ? "就绪" : "未就绪"} · 严格 ${stats.strictOutline ? "开" : "关"} · 目标 ${stats.targetChapters ?? "未设"} 章 · 世界观 ${stats.worldbookEntries} 条 · revision ${stats.revision} · 规则包 ${stats.rulepack}${stats.partial ?? false ? " · 部分状态不可读" : ""}`);
|
|
17065
18205
|
} catch (error) {
|
|
17066
18206
|
return failure(error);
|
|
17067
18207
|
}
|
|
@@ -17127,10 +18267,34 @@ function createSkill() {
|
|
|
17127
18267
|
};
|
|
17128
18268
|
}
|
|
17129
18269
|
// src/remote/index.ts
|
|
17130
|
-
|
|
17131
|
-
|
|
18270
|
+
const SECTION_NAMES = /* @__PURE__ */ new Set([
|
|
18271
|
+
"overview",
|
|
18272
|
+
"chapters",
|
|
18273
|
+
"characters",
|
|
18274
|
+
"foreshadows",
|
|
18275
|
+
"timeline",
|
|
18276
|
+
"relationships",
|
|
18277
|
+
"worldbook",
|
|
18278
|
+
"quality",
|
|
18279
|
+
"quality_detail",
|
|
18280
|
+
"health",
|
|
18281
|
+
"chapter_detail"
|
|
18282
|
+
]);
|
|
18283
|
+
/** Build the payload for one optional panel query root; section is additive. */
|
|
18284
|
+
async function panelPayload(engine, root, section, query = {}, lean = false) {
|
|
18285
|
+
if (section !== void 0 && SECTION_NAMES.has(section)) return {
|
|
18286
|
+
...await engine.panelSection({
|
|
18287
|
+
...root === void 0 ? {} : { root },
|
|
18288
|
+
section,
|
|
18289
|
+
query
|
|
18290
|
+
}),
|
|
18291
|
+
schemaVersion: 6
|
|
18292
|
+
};
|
|
17132
18293
|
return {
|
|
17133
|
-
...await engine.panelBundle(
|
|
18294
|
+
...await engine.panelBundle({
|
|
18295
|
+
...root === void 0 ? {} : { root },
|
|
18296
|
+
lean
|
|
18297
|
+
}),
|
|
17134
18298
|
schemaVersion: 6
|
|
17135
18299
|
};
|
|
17136
18300
|
}
|
|
@@ -17143,6 +18307,34 @@ const queryRoot = (url) => {
|
|
|
17143
18307
|
return;
|
|
17144
18308
|
}
|
|
17145
18309
|
};
|
|
18310
|
+
const queryPanel = (url) => {
|
|
18311
|
+
if (url === void 0 || url === "") return { query: {} };
|
|
18312
|
+
try {
|
|
18313
|
+
const parsed = new URL(url, "http://panel.local");
|
|
18314
|
+
const section = parsed.searchParams.get("section") ?? void 0;
|
|
18315
|
+
const query = {};
|
|
18316
|
+
for (const key of [
|
|
18317
|
+
"search",
|
|
18318
|
+
"status",
|
|
18319
|
+
"scene",
|
|
18320
|
+
"sort",
|
|
18321
|
+
"cursor"
|
|
18322
|
+
]) {
|
|
18323
|
+
const value = parsed.searchParams.get(key);
|
|
18324
|
+
if (value !== null && value !== "") query[key] = value;
|
|
18325
|
+
}
|
|
18326
|
+
const limit = parsed.searchParams.get("limit");
|
|
18327
|
+
if (limit !== null && /^\d+$/.test(limit)) query.limit = Number(limit);
|
|
18328
|
+
const chapter = parsed.searchParams.get("chapter");
|
|
18329
|
+
if (chapter !== null && /^\d+$/.test(chapter)) query.chapter = Number(chapter);
|
|
18330
|
+
return section === void 0 ? { query } : {
|
|
18331
|
+
section,
|
|
18332
|
+
query
|
|
18333
|
+
};
|
|
18334
|
+
} catch {
|
|
18335
|
+
return { query: {} };
|
|
18336
|
+
}
|
|
18337
|
+
};
|
|
17146
18338
|
const denyAll = () => 503;
|
|
17147
18339
|
const rejectionText = {
|
|
17148
18340
|
401: "unauthorized",
|
|
@@ -17169,7 +18361,24 @@ function panelHandler(engine, guard = denyAll) {
|
|
|
17169
18361
|
return;
|
|
17170
18362
|
}
|
|
17171
18363
|
try {
|
|
17172
|
-
const
|
|
18364
|
+
const { section, query } = queryPanel(request.url);
|
|
18365
|
+
let lean = false;
|
|
18366
|
+
try {
|
|
18367
|
+
lean = new URL(request.url ?? "", "http://panel.local").searchParams.get("lean") === "1";
|
|
18368
|
+
} catch {}
|
|
18369
|
+
const payload = await panelPayload(engine, queryRoot(request.url), section, query, lean);
|
|
18370
|
+
const etag = typeof payload.etag === "string" ? payload.etag : null;
|
|
18371
|
+
if (etag !== null) {
|
|
18372
|
+
response.setHeader("etag", `"${etag}"`);
|
|
18373
|
+
const ifNoneMatch = request.headers?.["if-none-match"];
|
|
18374
|
+
const candidate = Array.isArray(ifNoneMatch) ? ifNoneMatch[0] : ifNoneMatch;
|
|
18375
|
+
if (candidate === `"${etag}"` || candidate === etag) {
|
|
18376
|
+
response.statusCode = 304;
|
|
18377
|
+
response.setHeader("cache-control", "no-store");
|
|
18378
|
+
response.end("");
|
|
18379
|
+
return;
|
|
18380
|
+
}
|
|
18381
|
+
}
|
|
17173
18382
|
const body = method === "HEAD" ? "" : JSON.stringify(payload);
|
|
17174
18383
|
response.statusCode = 200;
|
|
17175
18384
|
response.setHeader("content-type", "application/json; charset=utf-8");
|
|
@@ -17274,7 +18483,7 @@ const DEFAULT_RULEPACK = {
|
|
|
17274
18483
|
},
|
|
17275
18484
|
period_density: {
|
|
17276
18485
|
min: 1.5,
|
|
17277
|
-
max:
|
|
18486
|
+
max: 10
|
|
17278
18487
|
},
|
|
17279
18488
|
ellipsis_run: {
|
|
17280
18489
|
max: 1,
|
|
@@ -17483,7 +18692,34 @@ function compileRulepack(raw) {
|
|
|
17483
18692
|
}
|
|
17484
18693
|
const genres = object(root.genres, "genres");
|
|
17485
18694
|
knownKeys(genres, GENRES, "genres");
|
|
17486
|
-
for (const name of GENRES)
|
|
18695
|
+
for (const name of GENRES) {
|
|
18696
|
+
const entry = object(genres[name], `genres.${name}`);
|
|
18697
|
+
knownKeys(entry, ["note", "profile"], `genres.${name}`);
|
|
18698
|
+
if (entry.profile !== void 0) {
|
|
18699
|
+
const profile = object(entry.profile, `genres.${name}.profile`);
|
|
18700
|
+
knownKeys(profile, [
|
|
18701
|
+
"period_density",
|
|
18702
|
+
"water_words",
|
|
18703
|
+
"pleasure_density"
|
|
18704
|
+
], `genres.${name}.profile`);
|
|
18705
|
+
if (profile.period_density !== void 0) {
|
|
18706
|
+
const density = object(profile.period_density, `genres.${name}.profile.period_density`);
|
|
18707
|
+
knownKeys(density, ["min", "max"], `genres.${name}.profile.period_density`);
|
|
18708
|
+
const min = num(density.min, `genres.${name}.profile.period_density.min`);
|
|
18709
|
+
const max = num(density.max, `genres.${name}.profile.period_density.max`);
|
|
18710
|
+
if (min < 0 || max < 0) invalid(`genres.${name}.profile.period_density must be non-negative`);
|
|
18711
|
+
if (max > 100) invalid(`genres.${name}.profile.period_density.max must not exceed 100 per 100 chars`);
|
|
18712
|
+
if (min > max) invalid(`genres.${name}.profile.period_density.min must not exceed max`);
|
|
18713
|
+
}
|
|
18714
|
+
if (profile.water_words !== void 0) {
|
|
18715
|
+
const value = num(profile.water_words, `genres.${name}.profile.water_words`);
|
|
18716
|
+
if (value < 0 || value > 1) invalid(`genres.${name}.profile.water_words must be 0..1`);
|
|
18717
|
+
}
|
|
18718
|
+
if (profile.pleasure_density !== void 0) {
|
|
18719
|
+
if (num(profile.pleasure_density, `genres.${name}.profile.pleasure_density`) <= 0) invalid(`genres.${name}.profile.pleasure_density must be > 0`);
|
|
18720
|
+
}
|
|
18721
|
+
}
|
|
18722
|
+
}
|
|
17487
18723
|
section("crosschapter", [
|
|
17488
18724
|
"duplicate_similarity",
|
|
17489
18725
|
"duplicate_advisory",
|
|
@@ -17567,7 +18803,7 @@ function compileRulepack(raw) {
|
|
|
17567
18803
|
textList(array(conclusionFirst.causals, "lexicon.conclusion_first.causals"), "lexicon.conclusion_first.causals");
|
|
17568
18804
|
for (const genre of GENRES) {
|
|
17569
18805
|
const guide = object(pack.genres[genre], `genres.${genre}`);
|
|
17570
|
-
knownKeys(guide, ["note"], `genres.${genre}`);
|
|
18806
|
+
knownKeys(guide, ["note", "profile"], `genres.${genre}`);
|
|
17571
18807
|
if (typeof guide.note !== "string") invalid(`genres.${genre}.note must be a string`);
|
|
17572
18808
|
}
|
|
17573
18809
|
const nonNegative = (value, path) => {
|
|
@@ -17837,4 +19073,4 @@ function connectionFence(scope) {
|
|
|
17837
19073
|
}
|
|
17838
19074
|
if (Object.prototype.hasOwnProperty.call(candidate, "connection")) return candidate.connection;
|
|
17839
19075
|
}
|
|
17840
|
-
export { ARCHIVE_DIGESTS_FILE, BACKUP_DIR, BRIEF_RECEIPTS_FILE, BriefLibrary, CHAPTERS_DIR, CHECK_DIR, CORE_TOOLS, Config, DEFAULT_GENRE, DEFAULT_PLATFORM, ERROR_CODES, EXPORT_DIR, EngineBlockedError, EngineError, EngineFaultError, EngineInputError, EngineService, GENRES, HISTORY_DIR, HISTORY_KINDS, INITIAL_STAGE, LIMITS, LOCK_FILE, METADATA_LOCK_FILE, OPERATIONS, OUTLINE_DEPRECATED_FILE, OUTLINE_DIR, OUTLINE_FILE, PANEL_CHAPTER_LIMIT, PANEL_ID, PANEL_ROUTE, PHASE_STRIP, PLAN_DIR, PLATFORMS, PLUGIN_NAME, PROJECT_FILE, REGISTRY_FILE, REMOTE_VERSION, REVIEW_DIR, SCENE_TYPES, SCENE_TYPE_DEFAULT, SCHEMA_VERSION, SESSION_BINDINGS_FILE, SKILL_NAME, STAGING_DIR, STATE_DIR, STATE_FILES, TOOL_NAMES, WORLDBOOK_FILE, applicableGates, apply, blockedError, briefStageFor, chapterFile, chapterHeading, chapterHistoryFile, checkFile, checkHistoryFile, deprecatedChapterFile, deprecatedHistoryFile, evaluateGates, faultError, formatResumePrompt, gateName, inject, inputError, loadRulepack, name, parseParagraphLocation, phaseStrip, planChapterFile, planHistoryFile, planWorldFile, projectChapter, projectChapters, projectGates, projectSummary, renderBlurb, renderCover, renderEpubChapter, renderMarkdown, renderPlainText, renderProposal, reviewFile, reviewHistoryFile, verifyReference };
|
|
19076
|
+
export { ARCHIVE_DIGESTS_FILE, BACKUP_DIR, BRIEF_RECEIPTS_FILE, BriefLibrary, CHAPTERS_DIR, CHECK_DIR, CORE_TOOLS, Config, DEFAULT_GENRE, DEFAULT_PLATFORM, ERROR_CODES, EXPORT_DIR, EngineBlockedError, EngineError, EngineFaultError, EngineInputError, EngineService, GENRES, HISTORY_DIR, HISTORY_KINDS, INITIAL_STAGE, LIMITS, LOCK_FILE, METADATA_LOCK_FILE, OPERATIONS, OUTLINE_DEPRECATED_FILE, OUTLINE_DIR, OUTLINE_FILE, PANEL_CHAPTER_LIMIT, PANEL_ID, PANEL_ROUTE, PANEL_VERSION, PHASE_STRIP, PLAN_DIR, PLATFORMS, PLUGIN_NAME, PROJECT_FILE, REGISTRY_FILE, REMOTE_VERSION, REVIEW_DIR, SCENE_TYPES, SCENE_TYPE_DEFAULT, SCHEMA_VERSION, SESSION_BINDINGS_FILE, SKILL_NAME, STAGING_DIR, STATE_DIR, STATE_FILES, TOOL_NAMES, WORLDBOOK_FILE, applicableGates, apply, blockedError, briefStageFor, chapterFile, chapterHeading, chapterHistoryFile, checkFile, checkHistoryFile, deprecatedChapterFile, deprecatedHistoryFile, evaluateGates, faultError, formatResumePrompt, gateName, inject, inputError, loadRulepack, name, parseParagraphLocation, phaseStrip, planChapterFile, planHistoryFile, planWorldFile, projectChapter, projectChapterProgress, projectChapters, projectGates, projectSummary, renderBlurb, renderCover, renderEpubChapter, renderMarkdown, renderPlainText, renderProposal, reviewFile, reviewHistoryFile, verifyReference };
|