dsh-novel-write-engine 0.2.8 → 0.2.10
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 +31 -0
- package/README.md +26 -26
- package/briefs/P3.md +1 -0
- package/briefs/P4.md +1 -1
- package/briefs/P8.md +1 -1
- package/lib/client.js +1961 -120
- package/lib/index.d.ts +83 -8
- package/lib/index.js +1246 -225
- package/lib/types/client/dto.d.ts +12 -9
- package/lib/types/client/panel.d.ts +19 -1
- package/lib/types/client/viewmodel.d.ts +11 -4
- package/lib/types/constants.d.ts +2 -0
- package/lib/types/panel-contract.d.ts +2 -2
- package/lib/types/types.d.ts +18 -1
- 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.10";
|
|
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. */
|
|
@@ -1149,7 +1151,7 @@ const dashBudget = (ctx) => {
|
|
|
1149
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}`)];
|
|
1150
1152
|
};
|
|
1151
1153
|
const periodDensity = (ctx) => {
|
|
1152
|
-
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;
|
|
1153
1155
|
const dominant = ctx.rulepack.punctuation.dialogue_dominant_ratio;
|
|
1154
1156
|
const results = [];
|
|
1155
1157
|
splitParagraphs(ctx.text).forEach((paragraph, index) => {
|
|
@@ -1382,10 +1384,11 @@ const numericFactMismatch = (ctx) => {
|
|
|
1382
1384
|
for (const fact of ctx.facts) {
|
|
1383
1385
|
if (fact.alias.trim() === "" || !Number.isFinite(fact.value)) continue;
|
|
1384
1386
|
const tolerance = fact.tolerance ?? 0;
|
|
1385
|
-
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");
|
|
1386
1388
|
for (const match of ctx.text.matchAll(pattern)) {
|
|
1387
|
-
const
|
|
1388
|
-
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);
|
|
1389
1392
|
if (Math.abs(value - fact.value) <= tolerance) continue;
|
|
1390
1393
|
const paragraph = paragraphs.findIndex((candidate) => candidate.includes(match[0]));
|
|
1391
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}`));
|
|
@@ -1395,7 +1398,8 @@ const numericFactMismatch = (ctx) => {
|
|
|
1395
1398
|
return results;
|
|
1396
1399
|
};
|
|
1397
1400
|
const pleasureDensity = (ctx) => {
|
|
1398
|
-
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;
|
|
1399
1403
|
if (keywords.length === 0) return [];
|
|
1400
1404
|
const results = [];
|
|
1401
1405
|
if (slidingWindows(ctx.text, window).findIndex((slice) => {
|
|
@@ -1412,8 +1416,9 @@ const sensoryDensity = (ctx) => {
|
|
|
1412
1416
|
};
|
|
1413
1417
|
const waterWords = (ctx) => {
|
|
1414
1418
|
const { window, threshold, terms, scene_thresholds } = ctx.rulepack.semantics.water_words;
|
|
1419
|
+
const genreThreshold = ctx.rulepack.genres[ctx.genre]?.profile?.water_words;
|
|
1415
1420
|
if (terms.length === 0) return [];
|
|
1416
|
-
const sceneThreshold = scene_thresholds?.[ctx.sceneType] ?? threshold;
|
|
1421
|
+
const sceneThreshold = genreThreshold ?? scene_thresholds?.[ctx.sceneType] ?? threshold;
|
|
1417
1422
|
const results = [];
|
|
1418
1423
|
for (const slice of slidingWindows(ctx.text, window)) {
|
|
1419
1424
|
const density = terms.reduce((total, term) => total + countOccurrences(slice, term), 0) / window;
|
|
@@ -1581,25 +1586,24 @@ const resourceMismatch = (ctx) => {
|
|
|
1581
1586
|
resourceVisible = !(previousHasNumber && previousHasSign) && previousClauseText.includes(change.resource);
|
|
1582
1587
|
}
|
|
1583
1588
|
if (!resourceVisible) continue;
|
|
1584
|
-
const names = ctx.summary?.characters_appeared ?? [];
|
|
1585
|
-
const otherCharacter = names.
|
|
1586
|
-
let characterVisible = clauseText.
|
|
1587
|
-
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;
|
|
1588
1593
|
if (!characterVisible && !otherCharacter && sentence.start > 0) {
|
|
1589
1594
|
const previous = sentenceBounds(ctx.text, sentence.start - 1, sentence.start - 1);
|
|
1590
1595
|
const previousText = ctx.text.slice(previous.start, previous.end);
|
|
1591
|
-
const named =
|
|
1596
|
+
const named = mentionedNames$1(previousText, names);
|
|
1592
1597
|
characterVisible = /(?:他|她|它|我|他们|她们)/.test(clauseText) && named.length === 1 && named[0] === change.character;
|
|
1593
1598
|
}
|
|
1594
1599
|
if (!characterVisible) continue;
|
|
1595
1600
|
conflict = {
|
|
1596
1601
|
index,
|
|
1597
|
-
|
|
1598
|
-
sign: sign[0]
|
|
1602
|
+
trigger: sign[0]
|
|
1599
1603
|
};
|
|
1600
1604
|
break;
|
|
1601
1605
|
}
|
|
1602
|
-
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}"`)];
|
|
1603
1607
|
});
|
|
1604
1608
|
};
|
|
1605
1609
|
const timeRegression = (ctx) => {
|
|
@@ -1623,6 +1627,31 @@ const timeRegression = (ctx) => {
|
|
|
1623
1627
|
if (before === null || current >= before) return [];
|
|
1624
1628
|
return [v(CROSS_RULES.TIME_REGRESSION, "advisory", last.chapter, `story time goes back from ${previous.story_time} to ${last.story_time}`)];
|
|
1625
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
|
+
};
|
|
1626
1655
|
const relationUnknown = (ctx) => {
|
|
1627
1656
|
const characters = ctx.evidence.characters;
|
|
1628
1657
|
const known = characters === null ? [] : characters.characters.map((character) => character.name);
|
|
@@ -1634,7 +1663,7 @@ const relationUnknown = (ctx) => {
|
|
|
1634
1663
|
if (names.size < 2) return [];
|
|
1635
1664
|
const results = [];
|
|
1636
1665
|
for (const [index, paragraph] of splitParagraphs(ctx.text).entries()) {
|
|
1637
|
-
const present = [...names]
|
|
1666
|
+
const present = mentionedNames$1(paragraph, [...names]);
|
|
1638
1667
|
for (let first = 0; first < present.length; first += 1) for (let second = first + 1; second < present.length; second += 1) {
|
|
1639
1668
|
const a = present[first];
|
|
1640
1669
|
const b = present[second];
|
|
@@ -1643,7 +1672,7 @@ const relationUnknown = (ctx) => {
|
|
|
1643
1672
|
}
|
|
1644
1673
|
return results;
|
|
1645
1674
|
};
|
|
1646
|
-
const
|
|
1675
|
+
const describedMentions = (name, locked, text) => {
|
|
1647
1676
|
const candidates = [];
|
|
1648
1677
|
const strippedLock = locked.split(name).join("");
|
|
1649
1678
|
for (const [index, paragraph] of splitParagraphs(text).entries()) {
|
|
@@ -1659,9 +1688,9 @@ const lastDescribedMention = (name, locked, text, order) => {
|
|
|
1659
1688
|
coverage
|
|
1660
1689
|
});
|
|
1661
1690
|
}
|
|
1662
|
-
|
|
1663
|
-
return order === "first" ? candidates[0] : candidates[candidates.length - 1];
|
|
1691
|
+
return candidates;
|
|
1664
1692
|
};
|
|
1693
|
+
const bestMention = (mentions) => mentions.reduce((best, mention) => best === null || mention.coverage > best.coverage ? mention : best, null);
|
|
1665
1694
|
const lockedDetailDrift = (ctx) => {
|
|
1666
1695
|
if (ctx.evidence.locks === null) return [];
|
|
1667
1696
|
const locks = ctx.evidence.locks;
|
|
@@ -1669,16 +1698,16 @@ const lockedDetailDrift = (ctx) => {
|
|
|
1669
1698
|
for (const [name, lock] of Object.entries(locks.locations)) {
|
|
1670
1699
|
if (!ctx.text.includes(name)) continue;
|
|
1671
1700
|
if (lock.layout.trim() === "") continue;
|
|
1672
|
-
const mention =
|
|
1701
|
+
const mention = bestMention(describedMentions(name, lock.layout, ctx.text));
|
|
1673
1702
|
if (mention === null) continue;
|
|
1674
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)}"`));
|
|
1675
1704
|
}
|
|
1676
1705
|
for (const [name, lock] of Object.entries(locks.characters)) {
|
|
1677
1706
|
if (!ctx.text.includes(name)) continue;
|
|
1678
1707
|
if (lock.appearance.trim() === "") continue;
|
|
1679
|
-
const mention =
|
|
1708
|
+
const mention = bestMention(describedMentions(name, lock.appearance, ctx.text));
|
|
1680
1709
|
if (mention === null) continue;
|
|
1681
|
-
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)}"`));
|
|
1682
1711
|
}
|
|
1683
1712
|
return results;
|
|
1684
1713
|
};
|
|
@@ -2165,6 +2194,23 @@ const validateSummary = (kind, value, path) => {
|
|
|
2165
2194
|
numberAt(kind, summary.hook_strength, `${path}.hook_strength`);
|
|
2166
2195
|
stringAt(kind, summary.plot_advancement, `${path}.plot_advancement`);
|
|
2167
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
|
+
};
|
|
2168
2214
|
const validateNested = (kind, record) => {
|
|
2169
2215
|
switch (kind) {
|
|
2170
2216
|
case "project": {
|
|
@@ -2213,6 +2259,8 @@ const validateNested = (kind, record) => {
|
|
|
2213
2259
|
validateSummary(kind, row.summary, `${path}.summary`);
|
|
2214
2260
|
if (row.assigned_ids !== void 0) objectAt(kind, row.assigned_ids, `${path}.assigned_ids`);
|
|
2215
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`);
|
|
2216
2264
|
optionalString(kind, row.final_check, `${path}.final_check`);
|
|
2217
2265
|
optionalString(kind, row.archived_at, `${path}.archived_at`);
|
|
2218
2266
|
if (row.rounds !== void 0) {
|
|
@@ -3196,13 +3244,23 @@ var FileLock = class {
|
|
|
3196
3244
|
if (record === null) {
|
|
3197
3245
|
if (Date.now() - mtimeMs <= this.staleMs) return false;
|
|
3198
3246
|
} else if (lockRecordIsLive(record)) return false;
|
|
3199
|
-
const claimPath = `${this.absolutePath}.reclaim
|
|
3247
|
+
const claimPath = `${this.absolutePath}.reclaim`;
|
|
3200
3248
|
let claimByRename = false;
|
|
3201
3249
|
try {
|
|
3202
3250
|
await promises.link(this.absolutePath, claimPath);
|
|
3203
3251
|
} catch (error) {
|
|
3204
3252
|
const code = error.code;
|
|
3205
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
|
+
}
|
|
3206
3264
|
if (code === "EPERM" || code === "EACCES") return false;
|
|
3207
3265
|
if ([
|
|
3208
3266
|
"ENOSYS",
|
|
@@ -4164,7 +4222,7 @@ function outlineSummary(document, archivedCount, target = null, archivedNumbers)
|
|
|
4164
4222
|
missingCards,
|
|
4165
4223
|
plannedCoverage,
|
|
4166
4224
|
archivedCoverage,
|
|
4167
|
-
coverage: plannedCoverage ??
|
|
4225
|
+
coverage: plannedCoverage ?? cardCoverage,
|
|
4168
4226
|
cardCoverage,
|
|
4169
4227
|
volumes: document.volumes.length
|
|
4170
4228
|
};
|
|
@@ -4784,7 +4842,7 @@ const pick = (values, mentioned, key, limit) => {
|
|
|
4784
4842
|
return [...matched, ...rest].slice(0, limit);
|
|
4785
4843
|
};
|
|
4786
4844
|
/** Protect the brief budget from a legal-but-huge planning document. */
|
|
4787
|
-
const clip = (value, limit) => {
|
|
4845
|
+
const clip$1 = (value, limit) => {
|
|
4788
4846
|
const chars = Array.from(value);
|
|
4789
4847
|
return chars.length <= limit ? value : `${chars.slice(0, limit).join("")}…`;
|
|
4790
4848
|
};
|
|
@@ -4793,44 +4851,44 @@ function worldbookBrief(document, mentioned, names) {
|
|
|
4793
4851
|
const named = names.join("\n");
|
|
4794
4852
|
const characters = pick(document.characters, named === "" ? mentioned : named, (entry) => entry.name, 8);
|
|
4795
4853
|
return {
|
|
4796
|
-
premise: clip(document.premise, 600),
|
|
4797
|
-
setting: clip(document.setting, 600),
|
|
4798
|
-
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)),
|
|
4799
4857
|
characters: characters.map((entry) => ({
|
|
4800
4858
|
name: entry.name,
|
|
4801
4859
|
role: entry.role,
|
|
4802
|
-
description: clip(entry.description, 240),
|
|
4803
|
-
arc: clip(entry.arc, 160)
|
|
4860
|
+
description: clip$1(entry.description, 240),
|
|
4861
|
+
arc: clip$1(entry.arc, 160)
|
|
4804
4862
|
})),
|
|
4805
4863
|
locations: pick(document.locations, mentioned, (entry) => entry.name, 6).map((entry) => ({
|
|
4806
4864
|
name: entry.name,
|
|
4807
|
-
description: clip(entry.description, 240),
|
|
4808
|
-
layout: clip(entry.layout, 120),
|
|
4865
|
+
description: clip$1(entry.description, 240),
|
|
4866
|
+
layout: clip$1(entry.layout, 120),
|
|
4809
4867
|
capacity: entry.capacity
|
|
4810
4868
|
})),
|
|
4811
4869
|
factions: pick(document.factions, mentioned, (entry) => entry.name, 6).map((entry) => ({
|
|
4812
4870
|
name: entry.name,
|
|
4813
|
-
description: clip(entry.description, 240),
|
|
4814
|
-
goal: clip(entry.goal, 120)
|
|
4871
|
+
description: clip$1(entry.description, 240),
|
|
4872
|
+
goal: clip$1(entry.goal, 120)
|
|
4815
4873
|
})),
|
|
4816
4874
|
rules: pick(document.rules, mentioned, (entry) => entry.name, 6).map((entry) => ({
|
|
4817
4875
|
name: entry.name,
|
|
4818
|
-
description: clip(entry.description, 240)
|
|
4876
|
+
description: clip$1(entry.description, 240)
|
|
4819
4877
|
})),
|
|
4820
4878
|
glossary: pick(document.glossary, mentioned, (entry) => entry.term, 6).map((entry) => ({
|
|
4821
4879
|
term: entry.term,
|
|
4822
|
-
definition: clip(entry.definition, 160)
|
|
4880
|
+
definition: clip$1(entry.definition, 160)
|
|
4823
4881
|
})),
|
|
4824
4882
|
backstory: document.backstory.slice(0, 3).map((entry) => ({
|
|
4825
4883
|
time: entry.time,
|
|
4826
|
-
event: clip(entry.event, 160)
|
|
4884
|
+
event: clip$1(entry.event, 160)
|
|
4827
4885
|
})),
|
|
4828
4886
|
facts: (document.facts ?? []).slice(0, 20).map((fact) => ({
|
|
4829
|
-
alias: clip(fact.alias, 120),
|
|
4887
|
+
alias: clip$1(fact.alias, 120),
|
|
4830
4888
|
value: fact.value,
|
|
4831
|
-
unit: clip(fact.unit, 40),
|
|
4889
|
+
unit: clip$1(fact.unit, 40),
|
|
4832
4890
|
tolerance: fact.tolerance ?? 0,
|
|
4833
|
-
...fact.note === void 0 ? {} : { note: clip(fact.note, 160) }
|
|
4891
|
+
...fact.note === void 0 ? {} : { note: clip$1(fact.note, 160) }
|
|
4834
4892
|
}))
|
|
4835
4893
|
};
|
|
4836
4894
|
}
|
|
@@ -4838,14 +4896,14 @@ function worldbookBrief(document, mentioned, names) {
|
|
|
4838
4896
|
function bookBrief(document, chapter) {
|
|
4839
4897
|
const volume = document.volumes.find((candidate) => chapter >= candidate.start_chapter && chapter <= candidate.end_chapter) ?? null;
|
|
4840
4898
|
return {
|
|
4841
|
-
premise: clip(document.book.premise, 600),
|
|
4842
|
-
logline: clip(document.book.logline, 600),
|
|
4843
|
-
themes: document.book.themes.slice(0, 12).map((theme) => clip(theme, 80)),
|
|
4844
|
-
tone: clip(document.book.tone, 200),
|
|
4845
|
-
pov: clip(document.book.pov, 200),
|
|
4846
|
-
audience: clip(document.book.audience, 200),
|
|
4847
|
-
selling_points: document.book.selling_points.slice(0, 12).map((point) => clip(point, 120)),
|
|
4848
|
-
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),
|
|
4849
4907
|
strict: document.book.strict,
|
|
4850
4908
|
volume,
|
|
4851
4909
|
card: outlineCard(document, chapter)
|
|
@@ -4919,9 +4977,11 @@ function parseOutlineMarkdown(content) {
|
|
|
4919
4977
|
if (currentVolume === null) return;
|
|
4920
4978
|
const volume = currentVolume;
|
|
4921
4979
|
const chapters = chapterInputs.filter((card) => card.volume === volume.order);
|
|
4922
|
-
if (chapters.length > 0) {
|
|
4980
|
+
if (chapters.length > 0 && !volume.explicitRange) {
|
|
4923
4981
|
volume.start_chapter = Math.min(volume.start_chapter, ...chapters.map((card) => card.num));
|
|
4924
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");
|
|
4925
4985
|
} else if (!volume.explicitRange) {
|
|
4926
4986
|
warnings.push(`volume "${volume.title}" has neither a chapter range nor chapter cards; it was ignored`);
|
|
4927
4987
|
volumeInputs.pop();
|
|
@@ -5204,6 +5264,7 @@ const mergeVolumes = (options) => {
|
|
|
5204
5264
|
}
|
|
5205
5265
|
}
|
|
5206
5266
|
const byTitle = volumeByTitle(options.existingVolumes);
|
|
5267
|
+
const byId = new Map(options.existingVolumes.map((volume) => [volume.id, volume]));
|
|
5207
5268
|
const usedIds = new Set(options.existingVolumes.map((volume) => volume.id));
|
|
5208
5269
|
let nextGeneratedId = 1;
|
|
5209
5270
|
const generatedId = () => {
|
|
@@ -5214,21 +5275,39 @@ const mergeVolumes = (options) => {
|
|
|
5214
5275
|
return id;
|
|
5215
5276
|
};
|
|
5216
5277
|
const submitted = /* @__PURE__ */ new Set();
|
|
5278
|
+
const submittedIds = /* @__PURE__ */ new Set();
|
|
5279
|
+
const generatedIds = /* @__PURE__ */ new Set();
|
|
5217
5280
|
for (const [index, raw] of (options.volumes ?? []).entries()) {
|
|
5218
5281
|
if (!isRecord$2(raw)) throw new EngineInputError(ERROR_CODES.SCHEMA_INVALID, `volumes[${index}] must be an object`);
|
|
5219
5282
|
const title = text$1(raw.title, `volumes[${index}].title`);
|
|
5220
5283
|
if (submitted.has(title)) throw new EngineInputError(ERROR_CODES.SCHEMA_INVALID, `volumes contains duplicate title "${title}"`);
|
|
5221
5284
|
submitted.add(title);
|
|
5222
|
-
const base = byTitle.get(title);
|
|
5223
5285
|
const explicitId = typeof raw.id === "string" && raw.id.trim() !== "" ? raw.id.trim() : void 0;
|
|
5224
|
-
|
|
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);
|
|
5225
5302
|
if (!usedIds.has(id)) usedIds.add(id);
|
|
5226
|
-
|
|
5303
|
+
const next = {
|
|
5227
5304
|
...base ?? {},
|
|
5228
5305
|
...raw,
|
|
5229
5306
|
id,
|
|
5230
5307
|
title
|
|
5231
|
-
}
|
|
5308
|
+
};
|
|
5309
|
+
byTitle.set(title, next);
|
|
5310
|
+
byId.set(id, next);
|
|
5232
5311
|
}
|
|
5233
5312
|
for (const title of removed) {
|
|
5234
5313
|
if (submitted.has(title)) throw new EngineInputError(ERROR_CODES.SCHEMA_INVALID, `volume "${title}" cannot be both submitted and removed`);
|
|
@@ -5236,6 +5315,7 @@ const mergeVolumes = (options) => {
|
|
|
5236
5315
|
if (existing === void 0) throw new EngineInputError(ERROR_CODES.SCHEMA_INVALID, `cannot remove volume "${title}": it is not in the plan`);
|
|
5237
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");
|
|
5238
5317
|
byTitle.delete(title);
|
|
5318
|
+
byId.delete(existing.id);
|
|
5239
5319
|
}
|
|
5240
5320
|
const volumes = normalizeVolumes([...byTitle.values()], { totalChapters: options.totalChapters });
|
|
5241
5321
|
const before = volumeByTitle(options.existingVolumes);
|
|
@@ -6071,6 +6151,11 @@ function applyStateDelta(input) {
|
|
|
6071
6151
|
}
|
|
6072
6152
|
// src/engine/insight.ts
|
|
6073
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));
|
|
6074
6159
|
const median = (values) => {
|
|
6075
6160
|
if (values.length === 0) return 0;
|
|
6076
6161
|
const sorted = [...values].sort((left, right) => left - right);
|
|
@@ -6118,7 +6203,7 @@ function buildBookReport(input) {
|
|
|
6118
6203
|
}
|
|
6119
6204
|
const chapterReviews = [...finalReviews.values()];
|
|
6120
6205
|
const qualitySamples = chapterReviews.length;
|
|
6121
|
-
const qualityAttempts =
|
|
6206
|
+
const qualityAttempts = input.reviewAttempts ?? input.reviews.length;
|
|
6122
6207
|
const trendBasis = qualitySamples < 3 ? "insufficient_samples" : previous.length < 5 ? "insufficient_history" : "last_five_vs_previous_five";
|
|
6123
6208
|
const trend = trendBasis === "last_five_vs_previous_five" ? Math.round((average(lastFive) - average(previous)) * 10) / 10 : null;
|
|
6124
6209
|
const dimensions = /* @__PURE__ */ new Map();
|
|
@@ -6223,7 +6308,7 @@ function buildBookReport(input) {
|
|
|
6223
6308
|
target: input.targetChapters,
|
|
6224
6309
|
words,
|
|
6225
6310
|
average_words: averageWords,
|
|
6226
|
-
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),
|
|
6227
6312
|
remaining_words: averageRemaining,
|
|
6228
6313
|
remaining_words_min: minimumRemaining,
|
|
6229
6314
|
remaining_words_average: averageRemaining,
|
|
@@ -6261,7 +6346,8 @@ function buildBookReport(input) {
|
|
|
6261
6346
|
dimensions: [...dimensions.entries()].map(([id, value]) => ({
|
|
6262
6347
|
id,
|
|
6263
6348
|
average: Math.round(value.total / value.samples * 10) / 10,
|
|
6264
|
-
samples: value.samples
|
|
6349
|
+
samples: value.samples,
|
|
6350
|
+
max: input.dimensionMax?.[id] ?? 0
|
|
6265
6351
|
})).sort((left, right) => compareText(left.id, right.id))
|
|
6266
6352
|
},
|
|
6267
6353
|
effort: {
|
|
@@ -6669,13 +6755,13 @@ const ledgerWorldEntries = (world, kind) => {
|
|
|
6669
6755
|
const nameDescription = (entries, world, kind = "location") => {
|
|
6670
6756
|
const merged = /* @__PURE__ */ new Map();
|
|
6671
6757
|
for (const entry of [...entries, ...ledgerWorldEntries(world, kind)]) {
|
|
6672
|
-
const name = oneLine(entry.name);
|
|
6758
|
+
const name = clip(oneLine(entry.name), 80);
|
|
6673
6759
|
if (name === "") continue;
|
|
6674
6760
|
const existing = merged.get(name);
|
|
6675
6761
|
if (existing === void 0) {
|
|
6676
6762
|
merged.set(name, {
|
|
6677
6763
|
name,
|
|
6678
|
-
description: oneLine(entry.description),
|
|
6764
|
+
description: clip(oneLine(entry.description), 240),
|
|
6679
6765
|
...entry.anchors === void 0 || entry.anchors.length === 0 ? {} : { anchors: [...entry.anchors] }
|
|
6680
6766
|
});
|
|
6681
6767
|
continue;
|
|
@@ -6695,12 +6781,12 @@ function buildPanelBookData(input) {
|
|
|
6695
6781
|
const card = input.outline.chapters.find((candidate) => candidate.num === input.currentChapter);
|
|
6696
6782
|
const currentCard = card === void 0 ? null : {
|
|
6697
6783
|
num: card.num,
|
|
6698
|
-
title: card.title,
|
|
6784
|
+
title: clip(card.title, 120),
|
|
6699
6785
|
scene_type: card.scene_type,
|
|
6700
|
-
goal: card.goal,
|
|
6701
|
-
conflict: card.conflict,
|
|
6702
|
-
hook: card.hook,
|
|
6703
|
-
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)),
|
|
6704
6790
|
target_words: [card.target_words[0], card.target_words[1]],
|
|
6705
6791
|
status: card.status
|
|
6706
6792
|
};
|
|
@@ -6717,8 +6803,8 @@ function buildPanelBookData(input) {
|
|
|
6717
6803
|
};
|
|
6718
6804
|
return {
|
|
6719
6805
|
identity: {
|
|
6720
|
-
author: input.project.author,
|
|
6721
|
-
description: input.project.description,
|
|
6806
|
+
author: clip(input.project.author, 80),
|
|
6807
|
+
description: clip(input.project.description, 400),
|
|
6722
6808
|
created_at: input.project.created_at,
|
|
6723
6809
|
targets: {
|
|
6724
6810
|
words_per_chapter: [input.project.targets.words_per_chapter[0], input.project.targets.words_per_chapter[1]],
|
|
@@ -6727,14 +6813,14 @@ function buildPanelBookData(input) {
|
|
|
6727
6813
|
}
|
|
6728
6814
|
},
|
|
6729
6815
|
blueprint: {
|
|
6730
|
-
premise: input.outline.book.premise,
|
|
6731
|
-
logline: input.outline.book.logline,
|
|
6732
|
-
themes:
|
|
6733
|
-
tone: input.outline.book.tone,
|
|
6734
|
-
pov: input.outline.book.pov,
|
|
6735
|
-
audience: input.outline.book.audience,
|
|
6736
|
-
selling_points:
|
|
6737
|
-
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)
|
|
6738
6824
|
},
|
|
6739
6825
|
progress: {
|
|
6740
6826
|
archived: report.progress.archived,
|
|
@@ -6760,9 +6846,9 @@ function buildPanelBookData(input) {
|
|
|
6760
6846
|
factions: nameDescription(input.worldbook.factions, input.world, "faction"),
|
|
6761
6847
|
rules: nameDescription(input.worldbook.rules, input.world, "rule"),
|
|
6762
6848
|
facts: (input.worldbook.facts ?? []).slice(0, 5).map((fact) => ({
|
|
6763
|
-
alias: fact.alias,
|
|
6849
|
+
alias: clip(fact.alias, 80),
|
|
6764
6850
|
value: fact.value,
|
|
6765
|
-
unit: fact.unit
|
|
6851
|
+
unit: clip(fact.unit, 40)
|
|
6766
6852
|
}))
|
|
6767
6853
|
},
|
|
6768
6854
|
cast: {
|
|
@@ -6805,11 +6891,11 @@ function buildPanelBookData(input) {
|
|
|
6805
6891
|
health: {
|
|
6806
6892
|
state_errors: input.stateErrors.slice(0, 10).map((entry) => ({ ...entry })),
|
|
6807
6893
|
outline: {
|
|
6808
|
-
present: input.outlinePresent,
|
|
6894
|
+
present: input.outlinePresent && input.outlineError === null,
|
|
6809
6895
|
error: input.outlineError
|
|
6810
6896
|
},
|
|
6811
6897
|
worldbook: {
|
|
6812
|
-
present: input.worldbookPresent,
|
|
6898
|
+
present: input.worldbookPresent && input.worldbookError === null,
|
|
6813
6899
|
error: input.worldbookError
|
|
6814
6900
|
},
|
|
6815
6901
|
pending: input.pending === null ? null : { ...input.pending }
|
|
@@ -6829,7 +6915,7 @@ const kindOf = (directory, suffix) => {
|
|
|
6829
6915
|
* still on disk: history is evidence, not a cache, and this function never
|
|
6830
6916
|
* deletes it.
|
|
6831
6917
|
*/
|
|
6832
|
-
async function listHistory(store, chapter) {
|
|
6918
|
+
async function listHistory(store, chapter, options = {}) {
|
|
6833
6919
|
const entries = [];
|
|
6834
6920
|
for (const kind of HISTORY_KINDS) {
|
|
6835
6921
|
const names = await store.list(joinRel(HISTORY_DIR, kind));
|
|
@@ -6842,14 +6928,14 @@ async function listHistory(store, chapter) {
|
|
|
6842
6928
|
const revision = Number(match.groups.revision);
|
|
6843
6929
|
if (chapter !== void 0 && num !== chapter) continue;
|
|
6844
6930
|
const path = joinRel(HISTORY_DIR, kind, name);
|
|
6845
|
-
const
|
|
6846
|
-
entries.push({
|
|
6931
|
+
const entry = {
|
|
6847
6932
|
kind,
|
|
6848
6933
|
chapter: num,
|
|
6849
6934
|
revision,
|
|
6850
|
-
path
|
|
6851
|
-
|
|
6852
|
-
|
|
6935
|
+
path
|
|
6936
|
+
};
|
|
6937
|
+
if (options.withChars === true) entry.chars = (await store.readText(path))?.length ?? 0;
|
|
6938
|
+
entries.push(entry);
|
|
6853
6939
|
}
|
|
6854
6940
|
}
|
|
6855
6941
|
return entries.sort((left, right) => left.chapter - right.chapter || right.revision - left.revision || compareText(left.kind, right.kind));
|
|
@@ -8024,7 +8110,7 @@ const SUMMARY_FIELDS = [
|
|
|
8024
8110
|
"plot_advancement"
|
|
8025
8111
|
];
|
|
8026
8112
|
/** PG-05: the ten summary fields exist and have the right shape. */
|
|
8027
|
-
const summarySchema = (input) => {
|
|
8113
|
+
const summarySchema$1 = (input) => {
|
|
8028
8114
|
if (input.operation === "submitRevision" && input.summaryProvided !== true) return [];
|
|
8029
8115
|
if (input.summary === null) return [block("PG-05", "no chapter summary supplied")];
|
|
8030
8116
|
const summary = input.summary;
|
|
@@ -8316,7 +8402,7 @@ const GATE_FUNCTIONS = {
|
|
|
8316
8402
|
"PG-02": stageOrder,
|
|
8317
8403
|
"PG-03": wordCount,
|
|
8318
8404
|
"PG-04": format,
|
|
8319
|
-
"PG-05": summarySchema,
|
|
8405
|
+
"PG-05": summarySchema$1,
|
|
8320
8406
|
"PG-06": sceneType,
|
|
8321
8407
|
"PG-07": score,
|
|
8322
8408
|
"PG-08": issuesCleared,
|
|
@@ -9008,6 +9094,10 @@ var EngineService = class {
|
|
|
9008
9094
|
briefContextCache = /* @__PURE__ */ new WeakMap();
|
|
9009
9095
|
/** The 15s panel poll reuses the heavy book projection until revision/damage changes. */
|
|
9010
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();
|
|
9011
9101
|
briefs;
|
|
9012
9102
|
now;
|
|
9013
9103
|
metadataMutex = new KeyedMutex();
|
|
@@ -10539,7 +10629,7 @@ var EngineService = class {
|
|
|
10539
10629
|
};
|
|
10540
10630
|
}
|
|
10541
10631
|
case "history": {
|
|
10542
|
-
const entries = await listHistory(store, input.chapter);
|
|
10632
|
+
const entries = await listHistory(store, input.chapter, { withChars: true });
|
|
10543
10633
|
if (input.revision !== void 0) {
|
|
10544
10634
|
if (!Number.isInteger(input.revision) || input.revision < 1) throw new EngineInputError(ERROR_CODES.SCHEMA_INVALID, "history revision must be a positive integer");
|
|
10545
10635
|
const matching = entries.filter((entry) => entry.chapter === chapter && entry.revision === input.revision);
|
|
@@ -10883,9 +10973,16 @@ var EngineService = class {
|
|
|
10883
10973
|
detail: snapshot.projectError ?? `novel.yaml is readable`,
|
|
10884
10974
|
...snapshot.projectError === null ? {} : { hint: "read novel_state {kind:\"novel\"} and repair it with novel_amend" }
|
|
10885
10975
|
});
|
|
10976
|
+
const profileCount = Object.values(this.rulepack.genres).filter((genre) => genre.profile !== void 0).length;
|
|
10886
10977
|
add({
|
|
10887
|
-
id: "
|
|
10978
|
+
id: "rulepack",
|
|
10888
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",
|
|
10889
10986
|
title: "流水线",
|
|
10890
10987
|
detail: `revision ${snapshot.pipeline.revision} · ${projectChapterProgress({
|
|
10891
10988
|
activeChapter: snapshot.pipeline.chapter,
|
|
@@ -11184,12 +11281,13 @@ var EngineService = class {
|
|
|
11184
11281
|
foreshadows: snapshot.foreshadows,
|
|
11185
11282
|
relationships: snapshot.relationships,
|
|
11186
11283
|
reviews: currentReviews,
|
|
11187
|
-
activity: journal.rows.slice(-8),
|
|
11284
|
+
activity: journal.rows.filter((row) => row.op !== "lock_reclaimed").slice(-8),
|
|
11188
11285
|
journal: journal.rows,
|
|
11189
11286
|
reviewAttempts,
|
|
11190
11287
|
finalChecks,
|
|
11191
11288
|
targetChapters: snapshot.project.targets.total_chapters,
|
|
11192
11289
|
minScore: this.config.quality.minScore,
|
|
11290
|
+
dimensionMax: Object.fromEntries(this.rulepack.quality.dimensions.map((dimension) => [dimension.id, dimension.max])),
|
|
11193
11291
|
foreshadowDormantAfter: this.rulepack.crosschapter.foreshadow_dormant_after
|
|
11194
11292
|
});
|
|
11195
11293
|
if (snapshot.projectError !== null) report.warnings.push(`项目定义不可读:${snapshot.projectError}`);
|
|
@@ -12268,6 +12366,10 @@ var EngineService = class {
|
|
|
12268
12366
|
record.archived_at = archivedAt;
|
|
12269
12367
|
const outline = markOutlineArchived(snapshot.outline, record, archivedAt, snapshot.project.targets.words_per_chapter);
|
|
12270
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);
|
|
12271
12373
|
const foreshadows = ledger.foreshadows;
|
|
12272
12374
|
const timeline = ledger.timeline;
|
|
12273
12375
|
for (const item of foreshadows.items) if (item.status !== "resolved" && chapter - item.last_touched >= this.rulepack.crosschapter.foreshadow_dormant_after) item.status = "dormant";
|
|
@@ -12468,32 +12570,24 @@ var EngineService = class {
|
|
|
12468
12570
|
oldDocument = narrow(kind, parse(backup));
|
|
12469
12571
|
} catch {}
|
|
12470
12572
|
}
|
|
12471
|
-
if (kind === "outline"
|
|
12573
|
+
if (kind === "outline") {
|
|
12472
12574
|
const old = recordOf(oldDocument);
|
|
12473
12575
|
const oldChapters = Array.isArray(old?.chapters) ? [...old.chapters] : [];
|
|
12474
|
-
const
|
|
12475
|
-
|
|
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
|
+
}
|
|
12476
12581
|
for (const record of snapshot.chapters.chapters) {
|
|
12477
|
-
if (
|
|
12478
|
-
|
|
12479
|
-
|
|
12480
|
-
|
|
12481
|
-
|
|
12482
|
-
|
|
12483
|
-
|
|
12484
|
-
|
|
12485
|
-
|
|
12486
|
-
} catch {
|
|
12487
|
-
plan = null;
|
|
12488
|
-
}
|
|
12489
|
-
const planCast = Array.isArray(plan?.cast) ? plan.cast.map((member) => recordOf(member)?.name ?? "").filter((name) => name !== "") : [];
|
|
12490
|
-
const allowedTargets = [snapshot.project.targets.words_per_chapter, ...Array.isArray(plan?.target_words) ? [plan.target_words] : []];
|
|
12491
|
-
const allowedCasts = [planCast, autoCardCast(record)];
|
|
12492
|
-
const targetMatches = allowedTargets.some((target) => JSON.stringify(target) === JSON.stringify(candidate.target_words));
|
|
12493
|
-
const castMatches = allowedCasts.some((cast) => JSON.stringify(cast) === JSON.stringify(candidate.cast));
|
|
12494
|
-
if (!targetMatches || !castMatches) continue;
|
|
12495
|
-
oldChapters.push({ ...candidate });
|
|
12496
|
-
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
|
+
};
|
|
12497
12591
|
}
|
|
12498
12592
|
oldDocument = {
|
|
12499
12593
|
...old ?? {},
|
|
@@ -12893,6 +12987,436 @@ var EngineService = class {
|
|
|
12893
12987
|
return projectGates(await this.panelSnapshot(input.root));
|
|
12894
12988
|
}
|
|
12895
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
|
+
/**
|
|
12896
13420
|
* Build the read-only panel payload. The query root is accepted only when it
|
|
12897
13421
|
* is registered, active or the configured root; arbitrary paths never reach
|
|
12898
13422
|
* the filesystem from the panel route.
|
|
@@ -12925,9 +13449,10 @@ var EngineService = class {
|
|
|
12925
13449
|
add(await optionFor(active, known !== void 0, known?.title, known?.created_at));
|
|
12926
13450
|
}
|
|
12927
13451
|
const configured = this.defaultRoot();
|
|
13452
|
+
const explicitConfigured = this.config.projectRoot !== "./novel" && configured !== resolve("./novel");
|
|
12928
13453
|
if (!rows.has(configured)) {
|
|
12929
13454
|
const option = await optionFor(configured, false);
|
|
12930
|
-
if (option.state !== "missing" || active === configured) add(option);
|
|
13455
|
+
if (option.state !== "missing" || active === configured || explicitConfigured) add(option);
|
|
12931
13456
|
}
|
|
12932
13457
|
const selectedRoot = input.root === void 0 ? void 0 : resolve(input.root);
|
|
12933
13458
|
if (selectedRoot !== void 0 && !rows.has(selectedRoot) && selectedRoot === configured) add(await optionFor(configured, false));
|
|
@@ -12991,7 +13516,7 @@ var EngineService = class {
|
|
|
12991
13516
|
};
|
|
12992
13517
|
let snapshot;
|
|
12993
13518
|
try {
|
|
12994
|
-
snapshot = await this.panelSnapshot(selection.root);
|
|
13519
|
+
snapshot = await this.panelSnapshot(selection.root, { includeQuality: input.lean !== true });
|
|
12995
13520
|
} catch (error) {
|
|
12996
13521
|
if (error instanceof EngineError) {
|
|
12997
13522
|
const busy = error.code === ERROR_CODES.TXN_IN_PROGRESS;
|
|
@@ -13020,12 +13545,21 @@ var EngineService = class {
|
|
|
13020
13545
|
nextTool: summary.next.tool,
|
|
13021
13546
|
...summary.progress === void 0 ? {} : { progress: summary.progress }
|
|
13022
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
|
+
}));
|
|
13023
13557
|
return {
|
|
13024
13558
|
projects,
|
|
13025
13559
|
selection,
|
|
13026
13560
|
summary,
|
|
13027
|
-
chapters
|
|
13028
|
-
|
|
13561
|
+
chapters,
|
|
13562
|
+
etag,
|
|
13029
13563
|
...resume === void 0 ? {} : { resume }
|
|
13030
13564
|
};
|
|
13031
13565
|
}
|
|
@@ -13054,6 +13588,7 @@ var EngineService = class {
|
|
|
13054
13588
|
}
|
|
13055
13589
|
async stats(input = {}) {
|
|
13056
13590
|
const snapshot = await this.load(input.root);
|
|
13591
|
+
const stateErrors = this.stateErrors(snapshot);
|
|
13057
13592
|
const archivedWords = snapshot.chapters.chapters.reduce((total, chapter) => total + chapter.word_count, 0);
|
|
13058
13593
|
const activeIndexed = snapshot.chapters.chapters.some((chapter) => chapter.num === snapshot.pipeline.chapter);
|
|
13059
13594
|
const draftWords = snapshot.text === null || activeIndexed || snapshot.pipeline.stage === "done" ? 0 : countWordMetrics(snapshot.text).total_word_count;
|
|
@@ -13079,7 +13614,9 @@ var EngineService = class {
|
|
|
13079
13614
|
blueprintReady: bookPlanPresent(snapshot.outline.book),
|
|
13080
13615
|
worldbookEntries: counts.characters + counts.locations + counts.factions + counts.rules + counts.glossary + counts.backstory + counts.facts,
|
|
13081
13616
|
strictOutline: snapshot.outline.book.strict,
|
|
13082
|
-
targetChapters: snapshot.project.targets.total_chapters
|
|
13617
|
+
targetChapters: snapshot.project.targets.total_chapters,
|
|
13618
|
+
state_errors: stateErrors,
|
|
13619
|
+
partial: stateErrors.length > 0
|
|
13083
13620
|
};
|
|
13084
13621
|
}
|
|
13085
13622
|
registry() {
|
|
@@ -13098,6 +13635,17 @@ var EngineService = class {
|
|
|
13098
13635
|
...inspection.state === "ok" && !sameIdentity ? { identity_changed: true } : {}
|
|
13099
13636
|
};
|
|
13100
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
|
+
}
|
|
13101
13649
|
return {
|
|
13102
13650
|
active: document.active,
|
|
13103
13651
|
projects
|
|
@@ -13556,9 +14104,48 @@ var EngineService = class {
|
|
|
13556
14104
|
}
|
|
13557
14105
|
};
|
|
13558
14106
|
}
|
|
13559
|
-
|
|
13560
|
-
const
|
|
13561
|
-
|
|
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;
|
|
13562
14149
|
const archived = snapshot.chapters.chapters;
|
|
13563
14150
|
const archivedCurrent = archived.some((chapter) => chapter.num === snapshot.pipeline.chapter);
|
|
13564
14151
|
const target = snapshot.project.targets.total_chapters;
|
|
@@ -13573,19 +14160,12 @@ var EngineService = class {
|
|
|
13573
14160
|
const archivedWords = archived.reduce((total, chapter) => total + chapter.word_count, 0);
|
|
13574
14161
|
const pending = await inspectPending(this.storeFor(snapshot.root));
|
|
13575
14162
|
const stateErrorCount = this.stateErrors(snapshot).length;
|
|
13576
|
-
const
|
|
13577
|
-
const
|
|
13578
|
-
const
|
|
13579
|
-
const
|
|
13580
|
-
|
|
13581
|
-
|
|
13582
|
-
journalTail.seq,
|
|
13583
|
-
journalTail.at,
|
|
13584
|
-
journalTail.status
|
|
13585
|
-
],
|
|
13586
|
-
blocking: journalBlocking
|
|
13587
|
-
}));
|
|
13588
|
-
const panelCacheKey = `${snapshot.root}:${snapshot.pipeline.revision}:${pending.state}:${stateErrorCount}:${snapshot.outlineError ?? ""}:${snapshot.worldbookError ?? ""}:${JSON.stringify(snapshot.briefs)}:${journalKey}`;
|
|
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"}`;
|
|
13589
14169
|
const cachedPanel = this.panelCache.get(snapshot.root);
|
|
13590
14170
|
if (cachedPanel?.key === panelCacheKey) return cachedPanel.snapshot;
|
|
13591
14171
|
const outline = outlineSummary(snapshot.outline, archived.length, target, archived.map((record) => record.num));
|
|
@@ -13634,26 +14214,31 @@ var EngineService = class {
|
|
|
13634
14214
|
const qualityScores = scores.slice(-5);
|
|
13635
14215
|
const previousScores = scores.slice(-10, -5);
|
|
13636
14216
|
const store = this.storeFor(snapshot.root);
|
|
13637
|
-
const reviewNames = new Set(await store.list(REVIEW_DIR));
|
|
13638
|
-
const sampleChapters = /* @__PURE__ */ new Set();
|
|
13639
14217
|
const currentReviews = [];
|
|
13640
|
-
|
|
13641
|
-
|
|
13642
|
-
|
|
13643
|
-
|
|
13644
|
-
|
|
13645
|
-
|
|
13646
|
-
|
|
13647
|
-
|
|
13648
|
-
|
|
13649
|
-
|
|
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);
|
|
13650
14237
|
}
|
|
13651
|
-
const samples = sampleChapters.size;
|
|
13652
|
-
const archivedChapterNums = new Set(archived.map((record) => record.num));
|
|
13653
|
-
const attempts = [...(await this.reviewAttemptsByChapter(store, journal.rows, [...archivedChapterNums], sampleChapters)).values()].reduce((total, count) => total + count, 0);
|
|
13654
14238
|
const qualityTrendBasis = samples < 3 ? "insufficient_samples" : previousScores.length < 5 ? "insufficient_history" : "last_five_vs_previous_five";
|
|
13655
14239
|
const quality = {
|
|
13656
|
-
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,
|
|
13657
14242
|
scores,
|
|
13658
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,
|
|
13659
14244
|
trend_basis: qualityTrendBasis,
|
|
@@ -13670,37 +14255,58 @@ var EngineService = class {
|
|
|
13670
14255
|
hint: `第 ${violation.location.paragraph} 段`
|
|
13671
14256
|
}));
|
|
13672
14257
|
const legacy = await this.legacyIssues(snapshot, false);
|
|
13673
|
-
const activity = journal.rows.slice(-5).map((row) => ({
|
|
14258
|
+
const activity = journal.rows.filter((row) => row.op !== "lock_reclaimed").slice(-5).map((row) => ({
|
|
13674
14259
|
at: row.at,
|
|
13675
14260
|
op: row.op,
|
|
13676
14261
|
chapter: row.chapter,
|
|
13677
14262
|
status: row.status
|
|
13678
14263
|
}));
|
|
13679
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
|
+
}
|
|
13680
14308
|
const bookData = buildPanelBookData({
|
|
13681
|
-
report:
|
|
13682
|
-
project: snapshot.project,
|
|
13683
|
-
outline: snapshot.outline,
|
|
13684
|
-
outlinePresent: snapshot.outlinePresent,
|
|
13685
|
-
outlineError: snapshot.outlineError,
|
|
13686
|
-
worldbook: snapshot.worldbook,
|
|
13687
|
-
worldbookPresent: snapshot.worldbookPresent,
|
|
13688
|
-
worldbookError: snapshot.worldbookError,
|
|
13689
|
-
world: snapshot.world,
|
|
13690
|
-
chapters: archived,
|
|
13691
|
-
characters: snapshot.characters,
|
|
13692
|
-
timeline: snapshot.timeline,
|
|
13693
|
-
foreshadows: snapshot.foreshadows,
|
|
13694
|
-
relationships: snapshot.relationships,
|
|
13695
|
-
reviews: currentReviews,
|
|
13696
|
-
activity: journal.rows.slice(-8),
|
|
13697
|
-
journal: journal.rows,
|
|
13698
|
-
reviewAttempts: attempts,
|
|
13699
|
-
finalChecks: Object.fromEntries(archived.filter((record) => record.final_check === "pass" || record.final_check === "fail").map((record) => [record.num, record.final_check])),
|
|
13700
|
-
targetChapters: target,
|
|
13701
|
-
minScore: this.config.quality.minScore,
|
|
13702
|
-
foreshadowDormantAfter: this.rulepack.crosschapter.foreshadow_dormant_after
|
|
13703
|
-
}),
|
|
14309
|
+
report: panelReport,
|
|
13704
14310
|
project: snapshot.project,
|
|
13705
14311
|
worldbook: snapshot.worldbook,
|
|
13706
14312
|
world: snapshot.world,
|
|
@@ -13711,7 +14317,7 @@ var EngineService = class {
|
|
|
13711
14317
|
stage: snapshot.pipeline.stage,
|
|
13712
14318
|
rounds: snapshot.pipeline.rounds,
|
|
13713
14319
|
draftWords,
|
|
13714
|
-
lastTenAverage: scores.length === 0 ?
|
|
14320
|
+
lastTenAverage: scores.length === 0 ? null : scores.reduce((total, score) => total + score, 0) / scores.length,
|
|
13715
14321
|
manuscriptHash: snapshot.pipeline.draft?.content_hash ?? (snapshot.text === null ? null : sha256(snapshot.text)),
|
|
13716
14322
|
metrics: draftMetrics,
|
|
13717
14323
|
stateErrors: this.stateErrors(snapshot).map((error) => ({
|
|
@@ -13731,7 +14337,7 @@ var EngineService = class {
|
|
|
13731
14337
|
num: snapshot.pipeline.chapter,
|
|
13732
14338
|
title: stripTitlePrefix(snapshot.plan?.title ?? "") || `第${snapshot.pipeline.chapter}章`,
|
|
13733
14339
|
wordCount: draftWords,
|
|
13734
|
-
score: snapshot.review?.total ??
|
|
14340
|
+
score: snapshot.review?.total ?? null,
|
|
13735
14341
|
status: blocking.length > 0 ? "blocked" : "draft",
|
|
13736
14342
|
details: (snapshot.pipeline.draft?.summary?.events ?? []).join(";").slice(0, 240)
|
|
13737
14343
|
};
|
|
@@ -15269,6 +15875,29 @@ const machineWarnings = (machine, acknowledged = []) => machine.violations.filte
|
|
|
15269
15875
|
message: `${violation.detail} (paragraph ${violation.location.paragraph})`
|
|
15270
15876
|
}));
|
|
15271
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
|
+
};
|
|
15272
15901
|
const legacyStableJson = (value) => {
|
|
15273
15902
|
const normalize = (input) => {
|
|
15274
15903
|
if (Array.isArray(input)) return input.map(normalize);
|
|
@@ -15418,6 +16047,9 @@ const amendProblems = (kind, oldDocument, nextDocument, snapshot, path, archived
|
|
|
15418
16047
|
}
|
|
15419
16048
|
if (before.content_hash !== after.content_hash) problems.push(`archived chapter ${String(before.num)} cannot change its content_hash`);
|
|
15420
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`);
|
|
15421
16053
|
if (stableJson(before.assigned_ids ?? {}) !== stableJson(after.assigned_ids ?? {})) problems.push(`archived chapter ${String(before.num)} cannot change its assigned_ids`);
|
|
15422
16054
|
for (const field of [
|
|
15423
16055
|
"title",
|
|
@@ -15592,6 +16224,10 @@ const amendProblems = (kind, oldDocument, nextDocument, snapshot, path, archived
|
|
|
15592
16224
|
}
|
|
15593
16225
|
const indexed = new Set(snapshot.chapters.chapters.map((record) => record.num));
|
|
15594
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
|
+
}
|
|
15595
16231
|
if (!chaptersUnreadable) {
|
|
15596
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`);
|
|
15597
16233
|
}
|
|
@@ -15885,16 +16521,17 @@ function diffLines(before, after) {
|
|
|
15885
16521
|
const arraySemanticsDescription = (semantics, description) => `[x-semantics: ${semantics}] ${description}`;
|
|
15886
16522
|
// src/tools/index.ts
|
|
15887
16523
|
/**
|
|
15888
|
-
*
|
|
15889
|
-
*
|
|
15890
|
-
* 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.
|
|
15891
16528
|
*/
|
|
15892
16529
|
const asJson = (value, field) => {
|
|
15893
16530
|
if (typeof value !== "string") return value;
|
|
15894
16531
|
try {
|
|
15895
16532
|
return JSON.parse(value);
|
|
15896
16533
|
} catch {
|
|
15897
|
-
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`);
|
|
15898
16535
|
}
|
|
15899
16536
|
};
|
|
15900
16537
|
/** Turn an engine failure into the message shape tools surface. */
|
|
@@ -15964,17 +16601,255 @@ const stringArray = (semantics, description) => ({
|
|
|
15964
16601
|
items: { type: "string" },
|
|
15965
16602
|
description: arraySemanticsDescription(semantics, description)
|
|
15966
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
|
+
});
|
|
15967
16842
|
const root = {
|
|
15968
16843
|
type: "string",
|
|
15969
|
-
description: "Project root
|
|
16844
|
+
description: "Project root; omit to use the session binding or the only project."
|
|
15970
16845
|
};
|
|
15971
16846
|
const initRoot = {
|
|
15972
16847
|
type: "string",
|
|
15973
|
-
description: "
|
|
16848
|
+
description: "New project root; omit to use the configured projectRoot."
|
|
15974
16849
|
};
|
|
15975
16850
|
const utilityRoot = {
|
|
15976
16851
|
type: "string",
|
|
15977
|
-
description: "Project root for
|
|
16852
|
+
description: "Project root for project-backed mode; text-only calls ignore it."
|
|
15978
16853
|
};
|
|
15979
16854
|
/**
|
|
15980
16855
|
* The twenty-eight tools are thin adapters: narrow parameters, call one service
|
|
@@ -16119,11 +16994,19 @@ function createNovelTools(engine, options = {}) {
|
|
|
16119
16994
|
},
|
|
16120
16995
|
constraints: stringArray("replace", "P1 constraints; supplied list replaces the plan list."),
|
|
16121
16996
|
characters: {
|
|
16122
|
-
|
|
16997
|
+
...arrayOf(objectOf({
|
|
16998
|
+
name: required(str),
|
|
16999
|
+
role: required(str),
|
|
17000
|
+
current_state: required(str)
|
|
17001
|
+
})),
|
|
16123
17002
|
description: arraySemanticsDescription("replace", "P1 cast; supplied list replaces the cast.")
|
|
16124
17003
|
},
|
|
16125
17004
|
locations: {
|
|
16126
|
-
|
|
17005
|
+
...arrayOf(objectOf({
|
|
17006
|
+
name: required(str),
|
|
17007
|
+
layout: required(str),
|
|
17008
|
+
capacity: required(num$1)
|
|
17009
|
+
})),
|
|
16127
17010
|
description: arraySemanticsDescription("replace", "P1 locations; supplied list replaces them.")
|
|
16128
17011
|
},
|
|
16129
17012
|
continuity_notes: stringArray("replace", "P1 continuity notes; supplied list replaces them."),
|
|
@@ -16176,8 +17059,14 @@ function createNovelTools(engine, options = {}) {
|
|
|
16176
17059
|
description: arraySemanticsDescription("replace", "Plan [min,max] word target.")
|
|
16177
17060
|
},
|
|
16178
17061
|
cast: {
|
|
16179
|
-
|
|
16180
|
-
|
|
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.")
|
|
16181
17070
|
},
|
|
16182
17071
|
project_root: root
|
|
16183
17072
|
},
|
|
@@ -16221,9 +17110,8 @@ function createNovelTools(engine, options = {}) {
|
|
|
16221
17110
|
required: true
|
|
16222
17111
|
},
|
|
16223
17112
|
summary: {
|
|
16224
|
-
|
|
16225
|
-
|
|
16226
|
-
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."
|
|
16227
17115
|
},
|
|
16228
17116
|
project_root: root
|
|
16229
17117
|
},
|
|
@@ -16247,12 +17135,12 @@ function createNovelTools(engine, options = {}) {
|
|
|
16247
17135
|
required: true
|
|
16248
17136
|
},
|
|
16249
17137
|
dimensions: {
|
|
16250
|
-
|
|
17138
|
+
...arrayOf(dimensionSchema),
|
|
16251
17139
|
required: true,
|
|
16252
17140
|
description: arraySemanticsDescription("replace", "Fifteen scored dimensions.")
|
|
16253
17141
|
},
|
|
16254
17142
|
issues: {
|
|
16255
|
-
|
|
17143
|
+
...arrayOf(issueSchema),
|
|
16256
17144
|
description: arraySemanticsDescription("replace", "Review issue list.")
|
|
16257
17145
|
},
|
|
16258
17146
|
total: {
|
|
@@ -16261,8 +17149,7 @@ function createNovelTools(engine, options = {}) {
|
|
|
16261
17149
|
description: "150 - Σdelta, the same number the review reports."
|
|
16262
17150
|
},
|
|
16263
17151
|
proof: {
|
|
16264
|
-
|
|
16265
|
-
required: true,
|
|
17152
|
+
...proofSchema,
|
|
16266
17153
|
description: arraySemanticsDescription("replace", "Three reading-proof quotes.")
|
|
16267
17154
|
},
|
|
16268
17155
|
project_root: root
|
|
@@ -16289,20 +17176,24 @@ function createNovelTools(engine, options = {}) {
|
|
|
16289
17176
|
type: "string",
|
|
16290
17177
|
description: "Full revised text; mutually exclusive with patch."
|
|
16291
17178
|
},
|
|
16292
|
-
patch: {
|
|
16293
|
-
|
|
16294
|
-
|
|
16295
|
-
|
|
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
|
+
}),
|
|
16296
17187
|
summary: {
|
|
16297
|
-
|
|
17188
|
+
...optional(summarySchema),
|
|
16298
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."
|
|
16299
17190
|
},
|
|
16300
17191
|
issues: {
|
|
16301
|
-
|
|
17192
|
+
...arrayOf(issueUpdateSchema),
|
|
16302
17193
|
description: arraySemanticsDescription("replace", "Updated issue statuses and evidence for PG-08.")
|
|
16303
17194
|
},
|
|
16304
17195
|
proof: {
|
|
16305
|
-
|
|
17196
|
+
...optional(proofSchema),
|
|
16306
17197
|
description: arraySemanticsDescription("replace", "Three quotes from the revised text; required unless this is a summary-only repair. PG-09 verifies them.")
|
|
16307
17198
|
},
|
|
16308
17199
|
project_root: root
|
|
@@ -16328,7 +17219,24 @@ function createNovelTools(engine, options = {}) {
|
|
|
16328
17219
|
},
|
|
16329
17220
|
scope: stringArray("replace", "Requested check scopes; supplied list replaces the request."),
|
|
16330
17221
|
semantic: {
|
|
16331
|
-
|
|
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
|
+
})),
|
|
16332
17240
|
required: true,
|
|
16333
17241
|
description: arraySemanticsDescription("replace", "Exactly the five P6 semantic items, each with a verbatim evidence quote.")
|
|
16334
17242
|
},
|
|
@@ -16357,10 +17265,10 @@ function createNovelTools(engine, options = {}) {
|
|
|
16357
17265
|
required: true
|
|
16358
17266
|
},
|
|
16359
17267
|
state_delta: {
|
|
16360
|
-
|
|
16361
|
-
|
|
17268
|
+
...stateDeltaSchema,
|
|
17269
|
+
description: "Five ledger sections; entry values are dynamic maps validated by the engine."
|
|
16362
17270
|
},
|
|
16363
|
-
handoff:
|
|
17271
|
+
handoff: handoffSchema,
|
|
16364
17272
|
project_root: root
|
|
16365
17273
|
},
|
|
16366
17274
|
output: {
|
|
@@ -16470,7 +17378,7 @@ function createNovelTools(engine, options = {}) {
|
|
|
16470
17378
|
]
|
|
16471
17379
|
},
|
|
16472
17380
|
content: {
|
|
16473
|
-
|
|
17381
|
+
...openObject("按 path 的完整替换文档;根必须是映射。"),
|
|
16474
17382
|
required: true
|
|
16475
17383
|
},
|
|
16476
17384
|
reason: {
|
|
@@ -16613,7 +17521,7 @@ function createNovelTools(engine, options = {}) {
|
|
|
16613
17521
|
description: "Require a chapter card before P2/P3."
|
|
16614
17522
|
},
|
|
16615
17523
|
volumes: {
|
|
16616
|
-
|
|
17524
|
+
...arrayOf(volumeSchema),
|
|
16617
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.")
|
|
16618
17526
|
},
|
|
16619
17527
|
confirm_remove: {
|
|
@@ -16643,15 +17551,25 @@ function createNovelTools(engine, options = {}) {
|
|
|
16643
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.",
|
|
16644
17552
|
parameters: {
|
|
16645
17553
|
book: {
|
|
16646
|
-
|
|
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
|
+
}),
|
|
16647
17565
|
description: "Optional partial book blueprint patch."
|
|
16648
17566
|
},
|
|
16649
17567
|
volumes: {
|
|
16650
|
-
|
|
17568
|
+
...arrayOf(volumeSchema),
|
|
16651
17569
|
description: arraySemanticsDescription("upsert", "Volume list: [{title,start_chapter,end_chapter,goal?,arc?,climax?,status?}]. Upserts by title; omitted volumes are kept.")
|
|
16652
17570
|
},
|
|
16653
17571
|
chapters: {
|
|
16654
|
-
|
|
17572
|
+
...arrayOf(chapterCardSchema),
|
|
16655
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.")
|
|
16656
17574
|
},
|
|
16657
17575
|
remove: {
|
|
@@ -16718,31 +17636,31 @@ function createNovelTools(engine, options = {}) {
|
|
|
16718
17636
|
premise: { type: "string" },
|
|
16719
17637
|
setting: { type: "string" },
|
|
16720
17638
|
rules: {
|
|
16721
|
-
|
|
17639
|
+
...arrayOf(worldbookRule),
|
|
16722
17640
|
description: arraySemanticsDescription("replace", "Rules section list.")
|
|
16723
17641
|
},
|
|
16724
17642
|
factions: {
|
|
16725
|
-
|
|
17643
|
+
...arrayOf(worldbookFaction),
|
|
16726
17644
|
description: arraySemanticsDescription("replace", "Factions section list.")
|
|
16727
17645
|
},
|
|
16728
17646
|
locations: {
|
|
16729
|
-
|
|
17647
|
+
...arrayOf(worldbookLocation),
|
|
16730
17648
|
description: arraySemanticsDescription("replace", "Locations section list.")
|
|
16731
17649
|
},
|
|
16732
17650
|
characters: {
|
|
16733
|
-
|
|
17651
|
+
...arrayOf(worldbookCharacter),
|
|
16734
17652
|
description: arraySemanticsDescription("replace", "Characters section list.")
|
|
16735
17653
|
},
|
|
16736
17654
|
glossary: {
|
|
16737
|
-
|
|
17655
|
+
...arrayOf(worldbookGlossary),
|
|
16738
17656
|
description: arraySemanticsDescription("replace", "Glossary section list.")
|
|
16739
17657
|
},
|
|
16740
17658
|
backstory: {
|
|
16741
|
-
|
|
17659
|
+
...arrayOf(worldbookBackstory),
|
|
16742
17660
|
description: arraySemanticsDescription("replace", "Backstory section list.")
|
|
16743
17661
|
},
|
|
16744
17662
|
facts: {
|
|
16745
|
-
|
|
17663
|
+
...arrayOf(worldbookFact),
|
|
16746
17664
|
description: arraySemanticsDescription("replace", "Declared numeric canon: [{alias,value,unit,tolerance?,note?}].")
|
|
16747
17665
|
},
|
|
16748
17666
|
themes: stringArray("replace", "Worldbook themes; supplied list replaces the list."),
|
|
@@ -16890,6 +17808,9 @@ const NEXT_DETAILS = {
|
|
|
16890
17808
|
novel_rollback: "回退到更早阶段重建当前章。",
|
|
16891
17809
|
novel_amend: "按明确的修复理由修正一份状态文档。",
|
|
16892
17810
|
novel_audit: "先查看事务日志,确认并恢复损坏的历史。",
|
|
17811
|
+
novel_restore: "按备份清单恢复损坏的状态文档或当前章。",
|
|
17812
|
+
novel_state: "查看状态文档、大纲、世界观或历史原文。",
|
|
17813
|
+
novel_pipeline: "按当前阶段领取下一步工具与参数。",
|
|
16893
17814
|
novel_export: "导出成书或书级提案。",
|
|
16894
17815
|
novel_diff: "对比当前正文与上一版或给定文本。",
|
|
16895
17816
|
novel_word_count: "按引擎字数口径核对正文长度。"
|
|
@@ -17069,7 +17990,9 @@ function createCommands(engine, host = {}) {
|
|
|
17069
17990
|
}
|
|
17070
17991
|
case "stats": {
|
|
17071
17992
|
const stats = await engine.stats({});
|
|
17072
|
-
|
|
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 ? " · 部分状态不可读" : ""}`);
|
|
17073
17996
|
}
|
|
17074
17997
|
case "outline": {
|
|
17075
17998
|
const status = await engine.getStatus({});
|
|
@@ -17099,7 +18022,7 @@ function createCommands(engine, host = {}) {
|
|
|
17099
18022
|
case "report": {
|
|
17100
18023
|
const { data } = await engine.report({});
|
|
17101
18024
|
return ok([
|
|
17102
|
-
`进度:${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}%`}`,
|
|
17103
18026
|
`大纲:${data.outline.planned} 计划 / ${data.outline.archived} 归档 · 漂移 ${data.outline.drift.length} 项`,
|
|
17104
18027
|
`质量:均分 ${data.quality.average} · 中位 ${data.quality.median} · 趋势 ${data.quality.trend === null ? "—" : `${data.quality.trend >= 0 ? "+" : ""}${data.quality.trend}`} · ${data.quality.samples} 章样本 / ${data.quality.attempts} 次评审`,
|
|
17105
18028
|
`伏笔:未回收 ${data.foreshadows.open} · 逾期 ${data.foreshadows.overdue}`,
|
|
@@ -17276,7 +18199,9 @@ function createCommands(engine, host = {}) {
|
|
|
17276
18199
|
if (invocation.signal.aborted) return bad("已取消");
|
|
17277
18200
|
try {
|
|
17278
18201
|
const stats = await engine.stats({});
|
|
17279
|
-
|
|
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 ? " · 部分状态不可读" : ""}`);
|
|
17280
18205
|
} catch (error) {
|
|
17281
18206
|
return failure(error);
|
|
17282
18207
|
}
|
|
@@ -17342,10 +18267,34 @@ function createSkill() {
|
|
|
17342
18267
|
};
|
|
17343
18268
|
}
|
|
17344
18269
|
// src/remote/index.ts
|
|
17345
|
-
|
|
17346
|
-
|
|
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
|
+
};
|
|
17347
18293
|
return {
|
|
17348
|
-
...await engine.panelBundle(
|
|
18294
|
+
...await engine.panelBundle({
|
|
18295
|
+
...root === void 0 ? {} : { root },
|
|
18296
|
+
lean
|
|
18297
|
+
}),
|
|
17349
18298
|
schemaVersion: 6
|
|
17350
18299
|
};
|
|
17351
18300
|
}
|
|
@@ -17358,6 +18307,34 @@ const queryRoot = (url) => {
|
|
|
17358
18307
|
return;
|
|
17359
18308
|
}
|
|
17360
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
|
+
};
|
|
17361
18338
|
const denyAll = () => 503;
|
|
17362
18339
|
const rejectionText = {
|
|
17363
18340
|
401: "unauthorized",
|
|
@@ -17384,7 +18361,24 @@ function panelHandler(engine, guard = denyAll) {
|
|
|
17384
18361
|
return;
|
|
17385
18362
|
}
|
|
17386
18363
|
try {
|
|
17387
|
-
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
|
+
}
|
|
17388
18382
|
const body = method === "HEAD" ? "" : JSON.stringify(payload);
|
|
17389
18383
|
response.statusCode = 200;
|
|
17390
18384
|
response.setHeader("content-type", "application/json; charset=utf-8");
|
|
@@ -17489,7 +18483,7 @@ const DEFAULT_RULEPACK = {
|
|
|
17489
18483
|
},
|
|
17490
18484
|
period_density: {
|
|
17491
18485
|
min: 1.5,
|
|
17492
|
-
max:
|
|
18486
|
+
max: 10
|
|
17493
18487
|
},
|
|
17494
18488
|
ellipsis_run: {
|
|
17495
18489
|
max: 1,
|
|
@@ -17698,7 +18692,34 @@ function compileRulepack(raw) {
|
|
|
17698
18692
|
}
|
|
17699
18693
|
const genres = object(root.genres, "genres");
|
|
17700
18694
|
knownKeys(genres, GENRES, "genres");
|
|
17701
|
-
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
|
+
}
|
|
17702
18723
|
section("crosschapter", [
|
|
17703
18724
|
"duplicate_similarity",
|
|
17704
18725
|
"duplicate_advisory",
|
|
@@ -17782,7 +18803,7 @@ function compileRulepack(raw) {
|
|
|
17782
18803
|
textList(array(conclusionFirst.causals, "lexicon.conclusion_first.causals"), "lexicon.conclusion_first.causals");
|
|
17783
18804
|
for (const genre of GENRES) {
|
|
17784
18805
|
const guide = object(pack.genres[genre], `genres.${genre}`);
|
|
17785
|
-
knownKeys(guide, ["note"], `genres.${genre}`);
|
|
18806
|
+
knownKeys(guide, ["note", "profile"], `genres.${genre}`);
|
|
17786
18807
|
if (typeof guide.note !== "string") invalid(`genres.${genre}.note must be a string`);
|
|
17787
18808
|
}
|
|
17788
18809
|
const nonNegative = (value, path) => {
|
|
@@ -18052,4 +19073,4 @@ function connectionFence(scope) {
|
|
|
18052
19073
|
}
|
|
18053
19074
|
if (Object.prototype.hasOwnProperty.call(candidate, "connection")) return candidate.connection;
|
|
18054
19075
|
}
|
|
18055
|
-
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, projectChapterProgress, 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 };
|