dsh-novel-write-engine 0.1.0 → 0.1.2
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 +39 -0
- package/README.md +60 -40
- package/briefs/P1.md +6 -4
- package/briefs/P4.md +4 -1
- package/briefs/P5.md +5 -4
- package/briefs/P6.md +8 -2
- package/lib/index.d.ts +50 -3
- package/lib/index.js +968 -214
- package/lib/types/types.d.ts +16 -0
- package/package.json +18 -18
- package/skills/entry.md +1 -1
package/lib/index.js
CHANGED
|
@@ -350,7 +350,7 @@ function applyExclusions(dimensions, exclusions) {
|
|
|
350
350
|
const canonical = (location) => {
|
|
351
351
|
if (typeof location !== "string") return "";
|
|
352
352
|
const paragraph = /第\s*(\d+)\s*段/.exec(location);
|
|
353
|
-
return paragraph === null ? location.trim() : `paragraph:${paragraph[1]}`;
|
|
353
|
+
return paragraph === null ? location.trim() : `paragraph:${Number(paragraph[1])}`;
|
|
354
354
|
};
|
|
355
355
|
const deductionsOf = (dimension) => {
|
|
356
356
|
if (typeof dimension !== "object" || dimension === null) return [];
|
|
@@ -648,27 +648,28 @@ function nextStage(stage, op, openBlockers) {
|
|
|
648
648
|
function availableOps(stage) {
|
|
649
649
|
return TRANSITIONS.filter((transition) => transition.from === stage).map((transition) => transition.op);
|
|
650
650
|
}
|
|
651
|
-
/** Whether
|
|
652
|
-
function roundsExhausted(rounds) {
|
|
651
|
+
/** Whether one retry budget (or, without a counter, any budget) is spent. */
|
|
652
|
+
function roundsExhausted(rounds, counter) {
|
|
653
|
+
if (counter !== void 0) return rounds[counter] >= LIMITS.maxRounds;
|
|
653
654
|
return rounds.review >= LIMITS.maxRounds || rounds.revise >= LIMITS.maxRounds || rounds.check >= LIMITS.maxRounds;
|
|
654
655
|
}
|
|
655
656
|
/** The operation a stage expects next, used by `advise` and the status `next` block. */
|
|
656
657
|
const STAGE_NEXT = {
|
|
657
658
|
idle: {
|
|
658
659
|
tool: "novel_plan_world",
|
|
659
|
-
reason: "
|
|
660
|
+
reason: "submit the world plan"
|
|
660
661
|
},
|
|
661
662
|
P2: {
|
|
662
663
|
tool: "novel_plan_chapter",
|
|
663
|
-
reason: "
|
|
664
|
+
reason: "submit the chapter plan"
|
|
664
665
|
},
|
|
665
666
|
P3: {
|
|
666
667
|
tool: "novel_write_draft",
|
|
667
|
-
reason: "
|
|
668
|
+
reason: "submit the draft"
|
|
668
669
|
},
|
|
669
670
|
P4: {
|
|
670
671
|
tool: "novel_review",
|
|
671
|
-
reason: "
|
|
672
|
+
reason: "submit the scored review"
|
|
672
673
|
},
|
|
673
674
|
P5: {
|
|
674
675
|
tool: "novel_revise",
|
|
@@ -676,11 +677,11 @@ const STAGE_NEXT = {
|
|
|
676
677
|
},
|
|
677
678
|
P6: {
|
|
678
679
|
tool: "novel_check",
|
|
679
|
-
reason: "run the consistency check
|
|
680
|
+
reason: "run the consistency check"
|
|
680
681
|
},
|
|
681
682
|
ready_to_archive: {
|
|
682
|
-
tool: "
|
|
683
|
-
reason: "all gates pass;
|
|
683
|
+
tool: "novel_archive",
|
|
684
|
+
reason: "all gates pass; archive the chapter with its state_delta"
|
|
684
685
|
},
|
|
685
686
|
done: {
|
|
686
687
|
tool: "novel_plan_world",
|
|
@@ -1022,10 +1023,16 @@ const adjectiveDensity = (ctx) => {
|
|
|
1022
1023
|
return [v(SEMANTIC_RULES.ADJECTIVE_DENSITY, "advisory", 1, `adjectives every ${(100 / density).toFixed(1)} chars; limit is ${max}%`)];
|
|
1023
1024
|
};
|
|
1024
1025
|
const slidingWindows = (text, size) => {
|
|
1025
|
-
|
|
1026
|
+
const positions = [];
|
|
1027
|
+
for (let index = 0; index < text.length; index += 1) if (/\S/.test(text[index])) positions.push(index);
|
|
1028
|
+
if (positions.length <= size) return [text];
|
|
1026
1029
|
const windows = [];
|
|
1027
|
-
for (let
|
|
1028
|
-
|
|
1030
|
+
for (let startIndex = 0; startIndex + size <= positions.length; startIndex += 1) {
|
|
1031
|
+
const start = positions[startIndex];
|
|
1032
|
+
const end = startIndex + size < positions.length ? positions[startIndex + size] : text.length;
|
|
1033
|
+
windows.push(text.slice(start, end));
|
|
1034
|
+
}
|
|
1035
|
+
const tail = text.slice(positions[positions.length - size]);
|
|
1029
1036
|
if (windows[windows.length - 1] !== tail) windows.push(tail);
|
|
1030
1037
|
return windows;
|
|
1031
1038
|
};
|
|
@@ -1063,7 +1070,23 @@ const duplicateParagraph = (ctx) => {
|
|
|
1063
1070
|
const previous = ctx.evidence.previousTexts.length > 0 ? ctx.evidence.previousTexts : ctx.evidence.previousText === null ? [] : [ctx.evidence.previousText];
|
|
1064
1071
|
if (previous.length === 0) return [];
|
|
1065
1072
|
const minChars = ctx.rulepack.crosschapter.duplicate_min_chars;
|
|
1066
|
-
const longParagraphs = (text) =>
|
|
1073
|
+
const longParagraphs = (text) => {
|
|
1074
|
+
const units = [];
|
|
1075
|
+
let buffer = "";
|
|
1076
|
+
for (const paragraph of splitParagraphs(text)) {
|
|
1077
|
+
buffer += paragraph;
|
|
1078
|
+
if (Array.from(buffer.replace(/\s+/g, "")).length >= minChars) {
|
|
1079
|
+
units.push(buffer);
|
|
1080
|
+
buffer = "";
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
if (buffer !== "") {
|
|
1084
|
+
if (units.length === 0) {
|
|
1085
|
+
if (Array.from(buffer.replace(/\s+/g, "")).length >= minChars) units.push(buffer);
|
|
1086
|
+
} else units[units.length - 1] += buffer;
|
|
1087
|
+
}
|
|
1088
|
+
return units.join("\n\n");
|
|
1089
|
+
};
|
|
1067
1090
|
const best = maxParagraphSimilarity(longParagraphs(ctx.text), previous.map((text) => longParagraphs(text)).filter((text) => text !== ""));
|
|
1068
1091
|
if (best < ctx.rulepack.crosschapter.duplicate_advisory) return [];
|
|
1069
1092
|
const severity = best >= ctx.rulepack.crosschapter.duplicate_similarity ? "blocker" : "advisory";
|
|
@@ -1087,10 +1110,18 @@ const revisitedTooSoon = (ctx) => {
|
|
|
1087
1110
|
}).map((item) => v(CROSS_RULES.FS_REVISITED_TOO_SOON, "advisory", 1, `foreshadow ${item.id} reappears after ${ctx.chapter - item.last_touched} chapters (minimum ${min})`));
|
|
1088
1111
|
};
|
|
1089
1112
|
const resourceMismatch = (ctx) => {
|
|
1090
|
-
|
|
1113
|
+
const changes = ctx.summary?.resource_changes ?? [];
|
|
1114
|
+
const positive = /(增加|多了|得到|获得|赚|加|上升|增|\+)/;
|
|
1115
|
+
const negative = /(减少|少了|失去|消耗|花|损失|下降|减|-|−)/;
|
|
1116
|
+
return changes.flatMap((change) => {
|
|
1091
1117
|
if (change.delta === 0) return [];
|
|
1092
|
-
const magnitude =
|
|
1093
|
-
return ctx.text.
|
|
1118
|
+
const magnitude = Math.abs(change.delta);
|
|
1119
|
+
return [...ctx.text.matchAll(/\d+/g)].some((match) => {
|
|
1120
|
+
if (Number(match[0]) !== magnitude) return false;
|
|
1121
|
+
const index = match.index ?? 0;
|
|
1122
|
+
const window = ctx.text.slice(Math.max(0, index - 80), Math.min(ctx.text.length, index + match[0].length + 80));
|
|
1123
|
+
return window.includes(change.character) && window.includes(change.resource) && (change.delta > 0 ? positive : negative).test(window);
|
|
1124
|
+
}) ? [] : [v(CROSS_RULES.RESOURCE_MISMATCH, "advisory", 1, `${change.character}'s ${change.resource} change (${change.delta}) is not stated with the right sign in the text`)];
|
|
1094
1125
|
});
|
|
1095
1126
|
};
|
|
1096
1127
|
const timeRegression = (ctx) => {
|
|
@@ -1246,6 +1277,11 @@ const lexiconCodes = /* @__PURE__ */ new Set([
|
|
|
1246
1277
|
"GOD_VIEW",
|
|
1247
1278
|
"CONCLUSION_FIRST"
|
|
1248
1279
|
]);
|
|
1280
|
+
const integrityCodes = new Set(Object.values(CROSS_RULES));
|
|
1281
|
+
/** Which runCheck scope owns one machine finding. */
|
|
1282
|
+
function scopeOfRule(code) {
|
|
1283
|
+
return integrityCodes.has(code) ? "integrity" : "content";
|
|
1284
|
+
}
|
|
1249
1285
|
/**
|
|
1250
1286
|
* Run the deterministic pass. Format and punctuation run before the lexicon so
|
|
1251
1287
|
* the report reads in the order an author fixes things; the lexicon runs last
|
|
@@ -1340,7 +1376,7 @@ function resolveInside(root, relativePath) {
|
|
|
1340
1376
|
const absoluteRoot = resolve(root);
|
|
1341
1377
|
const absolute = resolve(absoluteRoot, relativePath);
|
|
1342
1378
|
const inside = relative(absoluteRoot, absolute);
|
|
1343
|
-
if (inside === "" || inside.startsWith(
|
|
1379
|
+
if (inside === "" || inside === ".." || inside.startsWith(`..${sep}`) || isAbsolute(inside)) throw new EngineFaultError(ERROR_CODES.PATH_ESCAPE, `refusing path "${relativePath}": escapes project root`);
|
|
1344
1380
|
return absolute;
|
|
1345
1381
|
}
|
|
1346
1382
|
// src/store/migrate.ts
|
|
@@ -1415,6 +1451,7 @@ const SCHEMAS = {
|
|
|
1415
1451
|
rounds: req("object"),
|
|
1416
1452
|
briefs: req("object"),
|
|
1417
1453
|
blocking: req("array"),
|
|
1454
|
+
review_digest: f("string"),
|
|
1418
1455
|
last_action: f("any")
|
|
1419
1456
|
},
|
|
1420
1457
|
chapters: { chapters: req("array") },
|
|
@@ -1726,10 +1763,11 @@ function narrow(kind, value) {
|
|
|
1726
1763
|
if (typeof version !== "number" || !Number.isInteger(version)) return invalid$1(kind, "missing schema_version");
|
|
1727
1764
|
if (version > 1) throw new EngineFaultError(ERROR_CODES.SCHEMA_TOO_NEW, `refusing ${kind}: schema_version ${version} is newer than supported 1`, "upgrade the plugin; the file was left untouched");
|
|
1728
1765
|
if (version !== 1) invalid$1(kind, `unsupported schema_version ${version}`);
|
|
1766
|
+
if (!Object.hasOwn(SCHEMAS, kind)) invalid$1(kind, `unknown document kind "${String(kind)}"`);
|
|
1729
1767
|
const schema = SCHEMAS[kind];
|
|
1730
1768
|
for (const key of Object.keys(record)) {
|
|
1731
1769
|
if (key === "schema_version") continue;
|
|
1732
|
-
if (!(key
|
|
1770
|
+
if (!Object.hasOwn(schema, key)) invalid$1(kind, `unknown key "${key}"`);
|
|
1733
1771
|
}
|
|
1734
1772
|
for (const [key, field] of Object.entries(schema)) {
|
|
1735
1773
|
const fieldValue = record[key];
|
|
@@ -1836,7 +1874,12 @@ var FsStore = class {
|
|
|
1836
1874
|
async readDoc(relativePath, kind) {
|
|
1837
1875
|
const text = await this.readText(relativePath);
|
|
1838
1876
|
if (text === null) return null;
|
|
1839
|
-
|
|
1877
|
+
let parsed;
|
|
1878
|
+
try {
|
|
1879
|
+
parsed = parse(text);
|
|
1880
|
+
} catch (error) {
|
|
1881
|
+
throw new EngineFaultError(ERROR_CODES.SCHEMA_INVALID, `${relativePath} is not valid YAML: ${error instanceof Error ? error.message : String(error)}`, "fix the YAML syntax; the engine will not rewrite a document it cannot parse", { cause: error });
|
|
1882
|
+
}
|
|
1840
1883
|
return narrow(kind, (typeof parsed === "object" && parsed !== null && typeof parsed.schema_version === "number" ? parsed.schema_version : 1) < 1 ? migrateDocument(parsed, MIGRATIONS, 1) : parsed);
|
|
1841
1884
|
}
|
|
1842
1885
|
/** Write text atomically: temp file, fsync, rename, directory fsync. */
|
|
@@ -2080,6 +2123,9 @@ async function recoverPending(store) {
|
|
|
2080
2123
|
/** Throw before a write when the journal or the last commit is inconsistent. */
|
|
2081
2124
|
async function assertJournalConsistent(store) {
|
|
2082
2125
|
await recoverPending(store);
|
|
2126
|
+
const { records } = await readJournal(store);
|
|
2127
|
+
const committed = new Set(records.filter((record) => record.status === "committed" && record.op !== "lock_reclaimed").map((record) => record.txid));
|
|
2128
|
+
for (const entry of await store.list(STAGING_DIR)) if (committed.has(entry)) await store.removeTree(joinRel(STAGING_DIR, entry)).catch(() => void 0);
|
|
2083
2129
|
const { malformed, mismatches } = await reconcileJournal(store);
|
|
2084
2130
|
assertJournalHealthy(malformed);
|
|
2085
2131
|
if (mismatches.length > 0) {
|
|
@@ -2113,6 +2159,8 @@ var KeyedMutex = class {
|
|
|
2113
2159
|
return this.tails.size;
|
|
2114
2160
|
}
|
|
2115
2161
|
};
|
|
2162
|
+
/** Nonces this process currently holds; same-pid locks not in this set are stale. */
|
|
2163
|
+
const liveNonces = /* @__PURE__ */ new Set();
|
|
2116
2164
|
const isLockRecord = (value) => typeof value === "object" && value !== null && typeof value.pid === "number" && typeof value.startedAt === "string" && (value.nonce === void 0 || typeof value.nonce === "string");
|
|
2117
2165
|
const processAlive = (pid) => {
|
|
2118
2166
|
if (pid === process.pid) return true;
|
|
@@ -2155,6 +2203,7 @@ var FileLock = class {
|
|
|
2155
2203
|
}
|
|
2156
2204
|
await handle.close();
|
|
2157
2205
|
this.held = record;
|
|
2206
|
+
if (record.nonce !== void 0) liveNonces.add(record.nonce);
|
|
2158
2207
|
return;
|
|
2159
2208
|
} catch (error) {
|
|
2160
2209
|
if (error.code !== "EEXIST") throw error;
|
|
@@ -2168,7 +2217,8 @@ var FileLock = class {
|
|
|
2168
2217
|
const record = await this.read();
|
|
2169
2218
|
const held = this.held;
|
|
2170
2219
|
this.held = null;
|
|
2171
|
-
if (
|
|
2220
|
+
if (held?.nonce !== void 0) liveNonces.delete(held.nonce);
|
|
2221
|
+
if (record !== null && held !== null && record.pid === held.pid && record.nonce === held.nonce) await promises.rm(this.absolutePath, { force: true }).catch(() => void 0);
|
|
2172
2222
|
}
|
|
2173
2223
|
/** Read the current lock record, or null when absent/held by a foreign process. */
|
|
2174
2224
|
async read() {
|
|
@@ -2199,30 +2249,25 @@ var FileLock = class {
|
|
|
2199
2249
|
}
|
|
2200
2250
|
if (record === null) {
|
|
2201
2251
|
if (Date.now() - mtimeMs <= this.staleMs) return false;
|
|
2202
|
-
} else {
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
}
|
|
2252
|
+
} else if (record.pid === process.pid) {
|
|
2253
|
+
if (record.nonce !== void 0 && liveNonces.has(record.nonce)) return false;
|
|
2254
|
+
} else if (processAlive(record.pid)) return false;
|
|
2206
2255
|
const claimPath = `${this.absolutePath}.reclaim-${randomUUID()}`;
|
|
2207
2256
|
try {
|
|
2208
|
-
await promises.
|
|
2257
|
+
await promises.link(this.absolutePath, claimPath);
|
|
2209
2258
|
} catch (error) {
|
|
2210
2259
|
if (error.code === "ENOENT") return true;
|
|
2211
2260
|
throw error;
|
|
2212
2261
|
}
|
|
2213
|
-
|
|
2214
|
-
moved = await this.readAt(claimPath);
|
|
2262
|
+
const moved = await this.readAt(claimPath);
|
|
2215
2263
|
const claimMtime = await promises.stat(claimPath).then((info) => info.mtimeMs).catch(() => Date.now());
|
|
2216
2264
|
if (!(record === null ? moved === null && Date.now() - claimMtime > this.staleMs : moved !== null && moved.pid === record.pid && moved.startedAt === record.startedAt && moved.nonce === record.nonce)) {
|
|
2217
|
-
|
|
2218
|
-
await promises.link(claimPath, this.absolutePath);
|
|
2219
|
-
await promises.rm(claimPath, { force: true });
|
|
2220
|
-
} catch {
|
|
2221
|
-
await promises.rm(claimPath, { force: true }).catch(() => void 0);
|
|
2222
|
-
}
|
|
2265
|
+
await promises.rm(claimPath, { force: true }).catch(() => void 0);
|
|
2223
2266
|
return false;
|
|
2224
2267
|
}
|
|
2225
|
-
await
|
|
2268
|
+
const current = await this.readAt(this.absolutePath);
|
|
2269
|
+
if (record === null ? current === null : current !== null && current.pid === record.pid && current.startedAt === record.startedAt && current.nonce === record.nonce) await promises.rm(this.absolutePath, { force: true }).catch(() => void 0);
|
|
2270
|
+
await promises.rm(claimPath, { force: true }).catch(() => void 0);
|
|
2226
2271
|
await this.onReclaim?.(record);
|
|
2227
2272
|
return true;
|
|
2228
2273
|
}
|
|
@@ -2294,6 +2339,12 @@ async function restoreTargets(store, renamed, before) {
|
|
|
2294
2339
|
* that terminal row).
|
|
2295
2340
|
*/
|
|
2296
2341
|
async function runTransaction(store, input) {
|
|
2342
|
+
const staged = /* @__PURE__ */ new Set();
|
|
2343
|
+
for (const file of input.files) {
|
|
2344
|
+
if (staged.has(file.path)) throw new EngineFaultError(ERROR_CODES.TXN_ABORTED, `transaction ${input.txid} stages ${file.path} more than once`, "each target may appear once per transaction; merge the writes before staging");
|
|
2345
|
+
if (file.text !== void 0 && file.bytes !== void 0) throw new EngineFaultError(ERROR_CODES.TXN_ABORTED, `transaction ${input.txid} stages both text and bytes for ${file.path}`, "choose one representation per staged file");
|
|
2346
|
+
staged.add(file.path);
|
|
2347
|
+
}
|
|
2297
2348
|
const staging = stagingDir(input.txid);
|
|
2298
2349
|
const expected = /* @__PURE__ */ new Map();
|
|
2299
2350
|
const before = /* @__PURE__ */ new Map();
|
|
@@ -2416,8 +2467,13 @@ var ProjectRegistry = class {
|
|
|
2416
2467
|
async read() {
|
|
2417
2468
|
const text = await this.store.readText(REGISTRY_FILE);
|
|
2418
2469
|
if (text === null) return empty();
|
|
2419
|
-
|
|
2420
|
-
|
|
2470
|
+
let raw;
|
|
2471
|
+
try {
|
|
2472
|
+
raw = parse(text);
|
|
2473
|
+
} catch (error) {
|
|
2474
|
+
throw new EngineFaultError(ERROR_CODES.SCHEMA_INVALID, `${REGISTRY_FILE} is not valid YAML: ${error instanceof Error ? error.message : String(error)}`, "fix or delete the registry file; projects on disk are unaffected", { cause: error });
|
|
2475
|
+
}
|
|
2476
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) throw new EngineFaultError(ERROR_CODES.SCHEMA_INVALID, `${REGISTRY_FILE} is not an object`);
|
|
2421
2477
|
const record = raw;
|
|
2422
2478
|
if (record.schema_version !== 1) throw new EngineFaultError(ERROR_CODES.SCHEMA_INVALID, `${REGISTRY_FILE} has an unsupported schema_version`);
|
|
2423
2479
|
return {
|
|
@@ -2786,11 +2842,7 @@ var BriefLibrary = class {
|
|
|
2786
2842
|
};
|
|
2787
2843
|
}
|
|
2788
2844
|
};
|
|
2789
|
-
const hash = (text) => {
|
|
2790
|
-
let value = 0;
|
|
2791
|
-
for (let index = 0; index < text.length; index += 1) value = Math.imul(31, value) + text.charCodeAt(index) | 0;
|
|
2792
|
-
return `fnv:${(value >>> 0).toString(16)}`;
|
|
2793
|
-
};
|
|
2845
|
+
const hash = (text) => `sha256:${createHash("sha256").update(text, "utf8").digest("hex")}`;
|
|
2794
2846
|
// src/engine/context.ts
|
|
2795
2847
|
const compactSummary = (row) => ({
|
|
2796
2848
|
num: row.num,
|
|
@@ -2813,7 +2865,7 @@ const pendingForeshadows = (ledger) => ledger.items.filter((item) => item.status
|
|
|
2813
2865
|
* pack is the minimum project state the model needs to apply them without
|
|
2814
2866
|
* guessing who exists, what was locked, or what the previous chapter ended on.
|
|
2815
2867
|
*/
|
|
2816
|
-
function
|
|
2868
|
+
function unbudgetedBriefContext(input) {
|
|
2817
2869
|
const previousEnding = input.previousText === null ? "" : input.previousText.trim().slice(-400);
|
|
2818
2870
|
const base = {
|
|
2819
2871
|
project: {
|
|
@@ -2825,7 +2877,10 @@ function briefContext(input) {
|
|
|
2825
2877
|
chapter: input.chapter,
|
|
2826
2878
|
stage: input.pipeline.stage,
|
|
2827
2879
|
scene_distribution: sceneDistribution(input.chapters),
|
|
2828
|
-
previous_ending: previousEnding
|
|
2880
|
+
previous_ending: previousEnding,
|
|
2881
|
+
...input.plan_error === void 0 || input.plan_error === null ? {} : { plan_error: input.plan_error },
|
|
2882
|
+
...input.review_error === void 0 || input.review_error === null ? {} : { review_error: input.review_error },
|
|
2883
|
+
...input.check_error === void 0 || input.check_error === null ? {} : { check_error: input.check_error }
|
|
2829
2884
|
};
|
|
2830
2885
|
switch (input.stage) {
|
|
2831
2886
|
case "P1": return {
|
|
@@ -2853,6 +2908,7 @@ function briefContext(input) {
|
|
|
2853
2908
|
...base,
|
|
2854
2909
|
text: input.text ?? "",
|
|
2855
2910
|
summary: input.plan?.summary ?? null,
|
|
2911
|
+
plan: input.plan,
|
|
2856
2912
|
machine: input.machine ?? null,
|
|
2857
2913
|
characters: input.characters.characters,
|
|
2858
2914
|
check: input.check,
|
|
@@ -2864,6 +2920,7 @@ function briefContext(input) {
|
|
|
2864
2920
|
case "P5": return {
|
|
2865
2921
|
...base,
|
|
2866
2922
|
text: input.text ?? "",
|
|
2923
|
+
plan: input.plan,
|
|
2867
2924
|
machine: input.machine ?? null,
|
|
2868
2925
|
review: input.review,
|
|
2869
2926
|
issues: input.review?.issues ?? []
|
|
@@ -2871,6 +2928,7 @@ function briefContext(input) {
|
|
|
2871
2928
|
case "P6": return {
|
|
2872
2929
|
...base,
|
|
2873
2930
|
text: input.text ?? "",
|
|
2931
|
+
plan: input.plan,
|
|
2874
2932
|
machine: input.machine ?? null,
|
|
2875
2933
|
characters: input.characters.characters,
|
|
2876
2934
|
world: input.world,
|
|
@@ -2888,6 +2946,7 @@ function briefContext(input) {
|
|
|
2888
2946
|
};
|
|
2889
2947
|
case "P8": return {
|
|
2890
2948
|
...base,
|
|
2949
|
+
plan: input.plan,
|
|
2891
2950
|
check: input.check,
|
|
2892
2951
|
review: input.review,
|
|
2893
2952
|
summary: input.plan?.summary ?? null,
|
|
@@ -2897,6 +2956,238 @@ function briefContext(input) {
|
|
|
2897
2956
|
default: return base;
|
|
2898
2957
|
}
|
|
2899
2958
|
}
|
|
2959
|
+
/** Per-stage context budgets, measured in JSON characters (metadata excluded). */
|
|
2960
|
+
const CONTEXT_BUDGETS = {
|
|
2961
|
+
P1: 6e3,
|
|
2962
|
+
P2: 8e3,
|
|
2963
|
+
P3: 16e3,
|
|
2964
|
+
P4: 24e3,
|
|
2965
|
+
P5: 24e3,
|
|
2966
|
+
P6: 32e3,
|
|
2967
|
+
P8: 12e3
|
|
2968
|
+
};
|
|
2969
|
+
const asRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value) ? value : null;
|
|
2970
|
+
const asArray = (value) => Array.isArray(value) ? value : null;
|
|
2971
|
+
const jsonSize = (value) => Array.from(JSON.stringify(value)).length;
|
|
2972
|
+
const mentionScore = (mentioned, value) => value !== "" && mentioned.includes(value) ? 1 : 0;
|
|
2973
|
+
/**
|
|
2974
|
+
* Apply the stage budget after the stage-specific pack exists. Protected keys
|
|
2975
|
+
* are never candidates; optional ledgers are trimmed round-robin so one large
|
|
2976
|
+
* list cannot starve every other kind of context.
|
|
2977
|
+
*/
|
|
2978
|
+
function withContextBudget(stage, raw) {
|
|
2979
|
+
const context = structuredClone(raw);
|
|
2980
|
+
const budget = CONTEXT_BUDGETS[stage] ?? 8e3;
|
|
2981
|
+
const protectedKeys = [
|
|
2982
|
+
"project",
|
|
2983
|
+
"chapter",
|
|
2984
|
+
"stage",
|
|
2985
|
+
"previous_ending",
|
|
2986
|
+
"text",
|
|
2987
|
+
"plan",
|
|
2988
|
+
"precheck",
|
|
2989
|
+
"review",
|
|
2990
|
+
"check",
|
|
2991
|
+
"machine",
|
|
2992
|
+
"summary"
|
|
2993
|
+
];
|
|
2994
|
+
const mentioned = [
|
|
2995
|
+
typeof context.text === "string" ? context.text : "",
|
|
2996
|
+
JSON.stringify(context.plan ?? ""),
|
|
2997
|
+
JSON.stringify(context.review ?? ""),
|
|
2998
|
+
JSON.stringify(context.check ?? ""),
|
|
2999
|
+
JSON.stringify(context.summary ?? "")
|
|
3000
|
+
].join("\n");
|
|
3001
|
+
const omitted = {};
|
|
3002
|
+
const truncated = /* @__PURE__ */ new Set();
|
|
3003
|
+
const candidates = [];
|
|
3004
|
+
const stableKey = (value) => {
|
|
3005
|
+
const row = asRecord(value);
|
|
3006
|
+
if (row === null) return JSON.stringify(value) ?? "";
|
|
3007
|
+
for (const key of [
|
|
3008
|
+
"id",
|
|
3009
|
+
"name",
|
|
3010
|
+
"title"
|
|
3011
|
+
]) {
|
|
3012
|
+
const item = row[key];
|
|
3013
|
+
if (typeof item === "string" && item !== "") return item;
|
|
3014
|
+
}
|
|
3015
|
+
return JSON.stringify(row);
|
|
3016
|
+
};
|
|
3017
|
+
const compareText = (left, right) => left < right ? -1 : left > right ? 1 : 0;
|
|
3018
|
+
const addArray = (key, array, rank, tie = stableKey) => {
|
|
3019
|
+
let prepared = false;
|
|
3020
|
+
const sort = () => {
|
|
3021
|
+
if (prepared) return;
|
|
3022
|
+
prepared = true;
|
|
3023
|
+
array()?.sort((left, right) => {
|
|
3024
|
+
const byRank = rank(right) - rank(left);
|
|
3025
|
+
return byRank !== 0 ? byRank : compareText(tie(left), tie(right));
|
|
3026
|
+
});
|
|
3027
|
+
};
|
|
3028
|
+
candidates.push({
|
|
3029
|
+
key,
|
|
3030
|
+
count: () => array()?.length ?? 0,
|
|
3031
|
+
trim: () => {
|
|
3032
|
+
sort();
|
|
3033
|
+
const list = array();
|
|
3034
|
+
if (list === null || list.length === 0) return false;
|
|
3035
|
+
list.pop();
|
|
3036
|
+
return true;
|
|
3037
|
+
},
|
|
3038
|
+
prepare: sort
|
|
3039
|
+
});
|
|
3040
|
+
};
|
|
3041
|
+
const addObject = (key, map, rank) => {
|
|
3042
|
+
let keys = null;
|
|
3043
|
+
const ensure = () => {
|
|
3044
|
+
const target = map();
|
|
3045
|
+
keys ??= target === null ? [] : Object.keys(target);
|
|
3046
|
+
keys.sort((left, right) => {
|
|
3047
|
+
const byRank = rank(right, map()?.[right]) - rank(left, map()?.[left]);
|
|
3048
|
+
return byRank !== 0 ? byRank : compareText(left, right);
|
|
3049
|
+
});
|
|
3050
|
+
return keys;
|
|
3051
|
+
};
|
|
3052
|
+
candidates.push({
|
|
3053
|
+
key,
|
|
3054
|
+
count: () => Object.keys(map() ?? {}).length,
|
|
3055
|
+
trim: () => {
|
|
3056
|
+
const target = map();
|
|
3057
|
+
if (target === null) return false;
|
|
3058
|
+
const keyToDelete = ensure().pop();
|
|
3059
|
+
if (keyToDelete === void 0 || !Object.hasOwn(target, keyToDelete)) return false;
|
|
3060
|
+
delete target[keyToDelete];
|
|
3061
|
+
return true;
|
|
3062
|
+
},
|
|
3063
|
+
prepare: ensure
|
|
3064
|
+
});
|
|
3065
|
+
};
|
|
3066
|
+
const chapter = (value) => asRecord(value)?.num ?? 0;
|
|
3067
|
+
addArray("characters", () => asArray(context.characters), (value) => {
|
|
3068
|
+
const row = asRecord(value);
|
|
3069
|
+
const name = typeof row?.name === "string" ? row.name : "";
|
|
3070
|
+
return mentionScore(mentioned, name) * 1e3 + Number(row?.first_appearance ?? 0);
|
|
3071
|
+
}, (value) => {
|
|
3072
|
+
const row = asRecord(value);
|
|
3073
|
+
return typeof row?.name === "string" ? row.name : stableKey(value);
|
|
3074
|
+
});
|
|
3075
|
+
const world = asRecord(context.world);
|
|
3076
|
+
for (const key of [
|
|
3077
|
+
"locations",
|
|
3078
|
+
"abilities",
|
|
3079
|
+
"artifacts",
|
|
3080
|
+
"factions",
|
|
3081
|
+
"rules"
|
|
3082
|
+
]) addArray(`world.${key}`, () => asArray(world?.[key]), (value) => {
|
|
3083
|
+
const row = asRecord(value);
|
|
3084
|
+
const name = typeof row?.name === "string" ? row.name : "";
|
|
3085
|
+
return mentionScore(mentioned, name) * 1e3 + Number(row?.first_appearance ?? 0);
|
|
3086
|
+
}, (value) => {
|
|
3087
|
+
const row = asRecord(value);
|
|
3088
|
+
return typeof row?.name === "string" ? row.name : stableKey(value);
|
|
3089
|
+
});
|
|
3090
|
+
const locks = asRecord(context.locks);
|
|
3091
|
+
for (const key of ["characters", "locations"]) addObject(`locks.${key}`, () => asRecord(locks?.[key]), (name) => mentionScore(mentioned, name) * 1e3);
|
|
3092
|
+
addArray("open_foreshadows", () => asArray(context.open_foreshadows), (value) => {
|
|
3093
|
+
const row = asRecord(value);
|
|
3094
|
+
const title = typeof row?.title === "string" ? row.title : "";
|
|
3095
|
+
return mentionScore(mentioned, title) * 1e3 - Number(row?.last_touched ?? 0);
|
|
3096
|
+
}, (value) => {
|
|
3097
|
+
const row = asRecord(value);
|
|
3098
|
+
return `${typeof row?.title === "string" ? row.title : ""}\u0000${typeof row?.id === "string" ? row.id : ""}`;
|
|
3099
|
+
});
|
|
3100
|
+
const foreshadows = asRecord(context.foreshadows);
|
|
3101
|
+
addArray("foreshadows", () => asArray(foreshadows?.items), (value) => {
|
|
3102
|
+
const row = asRecord(value);
|
|
3103
|
+
const title = typeof row?.title === "string" ? row.title : "";
|
|
3104
|
+
const id = typeof row?.id === "string" ? row.id : "";
|
|
3105
|
+
return Math.max(mentionScore(mentioned, title), mentionScore(mentioned, id)) * 1e3 - Number(row?.last_touched ?? 0);
|
|
3106
|
+
}, (value) => {
|
|
3107
|
+
const row = asRecord(value);
|
|
3108
|
+
return `${typeof row?.id === "string" ? row.id : ""}\u0000${typeof row?.title === "string" ? row.title : ""}`;
|
|
3109
|
+
});
|
|
3110
|
+
addArray("timeline", () => asArray(context.timeline), (value) => {
|
|
3111
|
+
const row = asRecord(value);
|
|
3112
|
+
const text = `${String(row?.description ?? "")}${String(row?.location ?? "")}`;
|
|
3113
|
+
return mentionScore(mentioned, text) * 1e3 + Number(row?.chapter ?? 0);
|
|
3114
|
+
}, (value) => {
|
|
3115
|
+
const row = asRecord(value);
|
|
3116
|
+
return `${typeof row?.id === "string" ? row.id : ""}\u0000${String(row?.chapter ?? "")}`;
|
|
3117
|
+
});
|
|
3118
|
+
const relationships = asRecord(context.relationships);
|
|
3119
|
+
addArray("relationships", () => asArray(relationships?.items), (value) => {
|
|
3120
|
+
const row = asRecord(value);
|
|
3121
|
+
const text = `${String(row?.a ?? "")}${String(row?.b ?? "")}`;
|
|
3122
|
+
return mentionScore(mentioned, text) * 1e3 + Number(row?.since ?? 0);
|
|
3123
|
+
}, (value) => {
|
|
3124
|
+
const row = asRecord(value);
|
|
3125
|
+
return `${typeof row?.id === "string" ? row.id : ""}\u0000${String(row?.a ?? "")}\u0000${String(row?.b ?? "")}`;
|
|
3126
|
+
});
|
|
3127
|
+
addArray("recent_summaries", () => asArray(context.recent_summaries), (value) => chapter(value), (value) => String(chapter(value)).padStart(8, "0"));
|
|
3128
|
+
addArray("score_history", () => asArray(context.score_history), (value) => Number(asRecord(value)?.num ?? 0), (value) => String(Number(asRecord(value)?.num ?? 0)).padStart(8, "0"));
|
|
3129
|
+
for (const candidate of candidates) candidate.prepare();
|
|
3130
|
+
const used = () => {
|
|
3131
|
+
const measure = { ...context };
|
|
3132
|
+
delete measure.context_meta;
|
|
3133
|
+
return jsonSize(measure);
|
|
3134
|
+
};
|
|
3135
|
+
const buildMeta = (finalUsed) => ({
|
|
3136
|
+
version: 1,
|
|
3137
|
+
stage,
|
|
3138
|
+
budget,
|
|
3139
|
+
used: finalUsed,
|
|
3140
|
+
over_budget: false,
|
|
3141
|
+
protected: protectedKeys,
|
|
3142
|
+
truncated: [...truncated].sort(),
|
|
3143
|
+
omitted: Object.fromEntries(Object.entries(omitted).sort(([left], [right]) => left.localeCompare(right))),
|
|
3144
|
+
strategy: "stage-v1"
|
|
3145
|
+
});
|
|
3146
|
+
const totalWith = (finalUsed, meta) => jsonSize({
|
|
3147
|
+
...context,
|
|
3148
|
+
context_meta: {
|
|
3149
|
+
...meta,
|
|
3150
|
+
used: finalUsed
|
|
3151
|
+
}
|
|
3152
|
+
});
|
|
3153
|
+
let finalUsed = used();
|
|
3154
|
+
let metaBase = buildMeta(finalUsed);
|
|
3155
|
+
while (totalWith(finalUsed, metaBase) > budget) {
|
|
3156
|
+
let changed = false;
|
|
3157
|
+
for (const candidate of candidates) {
|
|
3158
|
+
finalUsed = used();
|
|
3159
|
+
metaBase = buildMeta(finalUsed);
|
|
3160
|
+
if (totalWith(finalUsed, metaBase) <= budget) break;
|
|
3161
|
+
if (candidate.count() === 0) continue;
|
|
3162
|
+
candidate.prepare();
|
|
3163
|
+
if (candidate.trim()) {
|
|
3164
|
+
omitted[candidate.key] = (omitted[candidate.key] ?? 0) + 1;
|
|
3165
|
+
truncated.add(candidate.key);
|
|
3166
|
+
changed = true;
|
|
3167
|
+
}
|
|
3168
|
+
}
|
|
3169
|
+
finalUsed = used();
|
|
3170
|
+
metaBase = buildMeta(finalUsed);
|
|
3171
|
+
if (!changed) break;
|
|
3172
|
+
}
|
|
3173
|
+
const overBudget = totalWith(finalUsed, metaBase) > budget;
|
|
3174
|
+
const meta = {
|
|
3175
|
+
...metaBase,
|
|
3176
|
+
over_budget: overBudget
|
|
3177
|
+
};
|
|
3178
|
+
return {
|
|
3179
|
+
...context,
|
|
3180
|
+
context_meta: meta
|
|
3181
|
+
};
|
|
3182
|
+
}
|
|
3183
|
+
/**
|
|
3184
|
+
* Build the stage-specific context pack plus the deterministic budget report.
|
|
3185
|
+
* The budget report is part of the brief digest; a truncated pack cannot be
|
|
3186
|
+
* silently mistaken for the full ledger set.
|
|
3187
|
+
*/
|
|
3188
|
+
function briefContext(input) {
|
|
3189
|
+
return withContextBudget(input.stage, unbudgetedBriefContext(input));
|
|
3190
|
+
}
|
|
2900
3191
|
// src/engine/state-delta.ts
|
|
2901
3192
|
const SECTIONS = [
|
|
2902
3193
|
"characters",
|
|
@@ -2911,6 +3202,15 @@ const ACTIONS = [
|
|
|
2911
3202
|
"no_change"
|
|
2912
3203
|
];
|
|
2913
3204
|
const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3205
|
+
/** Define an own property even for names such as `__proto__` or `constructor`. */
|
|
3206
|
+
const setOwn = (record, key, value) => {
|
|
3207
|
+
Object.defineProperty(record, key, {
|
|
3208
|
+
value,
|
|
3209
|
+
enumerable: true,
|
|
3210
|
+
configurable: true,
|
|
3211
|
+
writable: true
|
|
3212
|
+
});
|
|
3213
|
+
};
|
|
2914
3214
|
function fail$1(detail) {
|
|
2915
3215
|
throw new EngineInputError(ERROR_CODES.DELTA_UNVERIFIED, `state_delta is incomplete: ${detail}`, "declare every entity the chapter touched as created, updated, or no_change with a reason");
|
|
2916
3216
|
}
|
|
@@ -2961,7 +3261,7 @@ function applyStateDelta(input) {
|
|
|
2961
3261
|
const foreshadows = structuredClone(input.foreshadows);
|
|
2962
3262
|
const timeline = structuredClone(input.timeline);
|
|
2963
3263
|
const relationships = structuredClone(input.relationships);
|
|
2964
|
-
const assigned =
|
|
3264
|
+
const assigned = Object.create(null);
|
|
2965
3265
|
const appeared = [...new Set(summary.characters_appeared)];
|
|
2966
3266
|
const byName = new Map(characters.characters.map((character) => [character.name, character]));
|
|
2967
3267
|
const narrator = input.narrator ?? "";
|
|
@@ -2973,7 +3273,7 @@ function applyStateDelta(input) {
|
|
|
2973
3273
|
};
|
|
2974
3274
|
for (const name of appeared) {
|
|
2975
3275
|
if (!appearsInManuscript(name)) fail$1(`character "${name}" is listed in characters_appeared but does not appear in the manuscript`);
|
|
2976
|
-
if (delta.characters
|
|
3276
|
+
if (!Object.hasOwn(delta.characters, name)) fail$1(`character "${name}" appeared in the chapter but is not declared in characters`);
|
|
2977
3277
|
}
|
|
2978
3278
|
for (const [name, raw] of Object.entries(delta.characters)) {
|
|
2979
3279
|
const declared = entry(raw, `characters.${name}`);
|
|
@@ -3020,7 +3320,7 @@ function applyStateDelta(input) {
|
|
|
3020
3320
|
}
|
|
3021
3321
|
for (const change of summary.resource_changes) {
|
|
3022
3322
|
if (!isRecord(change) || typeof change.character !== "string" || change.character.trim() === "" || typeof change.resource !== "string" || change.resource.trim() === "" || !Number.isInteger(change.delta) || typeof change.reason !== "string" || change.reason.trim() === "") fail$1("every resource_changes entry must declare character/resource/delta/reason");
|
|
3023
|
-
if (delta.characters
|
|
3323
|
+
if (!Object.hasOwn(delta.characters, change.character)) fail$1(`resource change for "${change.character}" has no character declaration`);
|
|
3024
3324
|
if (delta.characters[change.character].action === "no_change") fail$1(`resource change for "${change.character}" conflicts with a no_change declaration`);
|
|
3025
3325
|
const character = byName.get(change.character);
|
|
3026
3326
|
if (character === void 0) fail$1(`resource change names unknown character "${change.character}"`);
|
|
@@ -3067,8 +3367,8 @@ function applyStateDelta(input) {
|
|
|
3067
3367
|
["artifact", summary.new_elements.artifacts]
|
|
3068
3368
|
]) for (const name of names) {
|
|
3069
3369
|
if (!input.text.includes(name)) fail$1(`summary lists new ${kind} "${name}" but the manuscript never mentions it`);
|
|
3370
|
+
if (!Object.hasOwn(delta.world, name)) fail$1(`summary lists new ${kind} "${name}" but world.${name} is not declared`);
|
|
3070
3371
|
const declared = delta.world[name];
|
|
3071
|
-
if (declared === void 0) fail$1(`summary lists new ${kind} "${name}" but world.${name} is not declared`);
|
|
3072
3372
|
if (declared.action !== "created") fail$1(`world.${name} is new in this chapter; its action must be created`);
|
|
3073
3373
|
if (declared.kind !== void 0 && declared.kind !== kind) fail$1(`world.${name} is declared as ${declared.kind} but the summary lists it as ${kind}`);
|
|
3074
3374
|
}
|
|
@@ -3086,9 +3386,19 @@ function applyStateDelta(input) {
|
|
|
3086
3386
|
}
|
|
3087
3387
|
const existing = findWorld(name);
|
|
3088
3388
|
if (declared.action === "created") {
|
|
3389
|
+
if (name.trim() === "") fail$1("a created world entity has an empty name");
|
|
3089
3390
|
if (existing !== null) fail$1(`world entity "${name}" already exists; use updated or no_change`);
|
|
3391
|
+
if (!input.text.includes(name)) fail$1(`world entity "${name}" is created but does not appear in the manuscript`);
|
|
3090
3392
|
const kind = declared.kind ?? expected;
|
|
3091
3393
|
if (kind === null) fail$1(`world.${name} must declare kind (location|ability|artifact|faction|rule)`);
|
|
3394
|
+
if (![
|
|
3395
|
+
"location",
|
|
3396
|
+
"ability",
|
|
3397
|
+
"artifact",
|
|
3398
|
+
"faction",
|
|
3399
|
+
"rule"
|
|
3400
|
+
].includes(kind)) fail$1(`world.${name} has unknown kind "${String(kind)}"`);
|
|
3401
|
+
if (expected === null && (kind === "location" || kind === "ability" || kind === "artifact")) fail$1(`world.${name} is created but is not listed in summary.new_elements`);
|
|
3092
3402
|
if (kind === "location") world.locations.push({
|
|
3093
3403
|
name,
|
|
3094
3404
|
first_appearance: chapter,
|
|
@@ -3099,10 +3409,10 @@ function applyStateDelta(input) {
|
|
|
3099
3409
|
changes: []
|
|
3100
3410
|
});
|
|
3101
3411
|
else world[kind === "ability" ? "abilities" : kind === "artifact" ? "artifacts" : kind === "faction" ? "factions" : "rules"].push({
|
|
3412
|
+
...declared.detail ?? {},
|
|
3102
3413
|
name,
|
|
3103
3414
|
first_appearance: chapter,
|
|
3104
|
-
description: declared.description ?? ""
|
|
3105
|
-
...declared.detail ?? {}
|
|
3415
|
+
description: declared.description ?? ""
|
|
3106
3416
|
});
|
|
3107
3417
|
continue;
|
|
3108
3418
|
}
|
|
@@ -3124,35 +3434,45 @@ function applyStateDelta(input) {
|
|
|
3124
3434
|
}
|
|
3125
3435
|
}
|
|
3126
3436
|
}
|
|
3127
|
-
for (const name of appeared) if (locks.characters
|
|
3437
|
+
for (const name of appeared) if (!Object.hasOwn(locks.characters, name)) {
|
|
3128
3438
|
const character = byName.get(name);
|
|
3129
|
-
locks.characters
|
|
3439
|
+
setOwn(locks.characters, name, {
|
|
3130
3440
|
first_appearance: character.first_appearance,
|
|
3131
3441
|
locked_at: chapter,
|
|
3132
3442
|
appearance: appearanceOf(input.text, name, name === narratorName ? narratorAliases : [])
|
|
3133
|
-
};
|
|
3443
|
+
});
|
|
3134
3444
|
}
|
|
3135
|
-
for (const location of world.locations) if (locks.locations
|
|
3445
|
+
for (const location of world.locations) if (!Object.hasOwn(locks.locations, location.name)) setOwn(locks.locations, location.name, {
|
|
3136
3446
|
first_appearance: location.first_appearance,
|
|
3137
3447
|
locked_at: chapter,
|
|
3138
3448
|
layout: location.layout,
|
|
3139
3449
|
capacity: location.capacity
|
|
3140
|
-
};
|
|
3450
|
+
});
|
|
3141
3451
|
locks.locked_at_chapter = Math.max(locks.locked_at_chapter, chapter);
|
|
3142
3452
|
const byId = new Map(foreshadows.items.map((item) => [item.id, item]));
|
|
3143
3453
|
const plantedKeys = /* @__PURE__ */ new Set();
|
|
3144
3454
|
for (const plant of summary.foreshadows.planted) {
|
|
3145
3455
|
if (plant.key.trim() === "") fail$1("a planted foreshadow has an empty key");
|
|
3456
|
+
if (byId.has(plant.key)) fail$1(`foreshadow key "${plant.key}" collides with an existing id`);
|
|
3146
3457
|
if (plantedKeys.has(plant.key)) fail$1(`foreshadow key "${plant.key}" is planted twice`);
|
|
3147
3458
|
plantedKeys.add(plant.key);
|
|
3459
|
+
if (!Object.hasOwn(delta.foreshadows, plant.key)) fail$1(`foreshadows.${plant.key} is not declared`);
|
|
3148
3460
|
const declared = entry(delta.foreshadows[plant.key], `foreshadows.${plant.key}`);
|
|
3149
3461
|
requireReason(declared, `foreshadows.${plant.key}`);
|
|
3150
3462
|
if (declared.action !== "created") fail$1(`foreshadows.${plant.key} is planted; its action must be created`);
|
|
3463
|
+
if (typeof plant.title !== "string" || plant.title.trim() === "") fail$1(`foreshadow "${plant.key}" has an empty summary title`);
|
|
3464
|
+
if (!input.text.includes(plant.title)) fail$1(`planted foreshadow summary title "${plant.title}" does not appear in the manuscript`);
|
|
3465
|
+
if (declared.title !== void 0 && typeof declared.title !== "string") fail$1(`foreshadows.${plant.key}.title must be a string`);
|
|
3466
|
+
const title = declared.title ?? plant.title;
|
|
3467
|
+
if (title.trim() === "") fail$1(`foreshadow "${plant.key}" has an empty title`);
|
|
3468
|
+
if (!input.text.includes(title)) fail$1(`planted foreshadow "${title}" does not appear in the manuscript`);
|
|
3151
3469
|
const allocated = allocateIds("FS", foreshadows.next_seq, 1);
|
|
3470
|
+
const id = allocated.ids[0];
|
|
3471
|
+
if (byId.has(id)) fail$1(`foreshadows.next_seq is behind the ledger; ${id} already exists`);
|
|
3152
3472
|
foreshadows.next_seq = allocated.nextSeq;
|
|
3153
3473
|
const item = {
|
|
3154
|
-
id
|
|
3155
|
-
title
|
|
3474
|
+
id,
|
|
3475
|
+
title,
|
|
3156
3476
|
planted: chapter,
|
|
3157
3477
|
last_touched: chapter,
|
|
3158
3478
|
status: "planted",
|
|
@@ -3165,12 +3485,14 @@ function applyStateDelta(input) {
|
|
|
3165
3485
|
assigned[plant.key] = item.id;
|
|
3166
3486
|
}
|
|
3167
3487
|
for (const advance of summary.foreshadows.advanced) {
|
|
3488
|
+
if (!Object.hasOwn(delta.foreshadows, advance.key)) fail$1(`foreshadows.${advance.key} is not declared`);
|
|
3168
3489
|
const declared = entry(delta.foreshadows[advance.key], `foreshadows.${advance.key}`);
|
|
3169
3490
|
requireReason(declared, `foreshadows.${advance.key}`);
|
|
3170
3491
|
if (declared.action !== "updated") fail$1(`foreshadows.${advance.key} is advanced; its action must be updated`);
|
|
3171
3492
|
const id = assigned[advance.key] ?? advance.key;
|
|
3172
3493
|
const item = byId.get(id);
|
|
3173
3494
|
if (item === void 0) fail$1(`foreshadow "${advance.key}" does not exist; no fallback is allowed`);
|
|
3495
|
+
if (item.status === "resolved") fail$1(`foreshadow "${advance.key}" is already resolved`);
|
|
3174
3496
|
item.last_touched = chapter;
|
|
3175
3497
|
item.status = "advanced";
|
|
3176
3498
|
item.advance_log.push({
|
|
@@ -3181,12 +3503,14 @@ function applyStateDelta(input) {
|
|
|
3181
3503
|
assigned[advance.key] = item.id;
|
|
3182
3504
|
}
|
|
3183
3505
|
for (const key of summary.foreshadows.resolved) {
|
|
3506
|
+
if (!Object.hasOwn(delta.foreshadows, key)) fail$1(`foreshadows.${key} is not declared`);
|
|
3184
3507
|
const declared = entry(delta.foreshadows[key], `foreshadows.${key}`);
|
|
3185
3508
|
requireReason(declared, `foreshadows.${key}`);
|
|
3186
3509
|
if (declared.action !== "updated") fail$1(`foreshadows.${key} is resolved; its action must be updated`);
|
|
3187
3510
|
const id = assigned[key] ?? key;
|
|
3188
3511
|
const item = byId.get(id);
|
|
3189
3512
|
if (item === void 0) fail$1(`foreshadow "${key}" does not exist; no fallback is allowed`);
|
|
3513
|
+
if (item.status === "resolved") fail$1(`foreshadow "${key}" is already resolved`);
|
|
3190
3514
|
item.status = "resolved";
|
|
3191
3515
|
item.last_touched = chapter;
|
|
3192
3516
|
item.resolved_at = chapter;
|
|
@@ -3218,8 +3542,9 @@ function applyStateDelta(input) {
|
|
|
3218
3542
|
const declared = entry(delta.timeline[key], `timeline.${key}`);
|
|
3219
3543
|
if (declared.action !== "created") fail$1(`timeline.${key} is a new event; its action must be created`);
|
|
3220
3544
|
const allocated = allocateIds("TE", timeline.next_seq, 1);
|
|
3221
|
-
timeline.next_seq = allocated.nextSeq;
|
|
3222
3545
|
const id = allocated.ids[0];
|
|
3546
|
+
if (timeline.items.some((item) => item.id === id)) fail$1(`timeline.next_seq is behind the ledger; ${id} already exists`);
|
|
3547
|
+
timeline.next_seq = allocated.nextSeq;
|
|
3223
3548
|
timeline.items.push({
|
|
3224
3549
|
id,
|
|
3225
3550
|
chapter,
|
|
@@ -3246,8 +3571,9 @@ function applyStateDelta(input) {
|
|
|
3246
3571
|
if (existing !== void 0) fail$1(`relationship ${pairKey(a, b)} already exists`);
|
|
3247
3572
|
if (typeof declared.type !== "string" || declared.type.trim() === "") fail$1(`relationships.${key} is created but has no type`);
|
|
3248
3573
|
const allocated = allocateIds("RL", relationships.next_seq, 1);
|
|
3249
|
-
relationships.next_seq = allocated.nextSeq;
|
|
3250
3574
|
const id = allocated.ids[0];
|
|
3575
|
+
if (relationships.items.some((item) => item.id === id)) fail$1(`relationships.next_seq is behind the ledger; ${id} already exists`);
|
|
3576
|
+
relationships.next_seq = allocated.nextSeq;
|
|
3251
3577
|
relationships.items.push({
|
|
3252
3578
|
id,
|
|
3253
3579
|
a,
|
|
@@ -3301,6 +3627,23 @@ function evidenceQuote(value) {
|
|
|
3301
3627
|
candidate = candidate.replace(/^[「『“"']/, "").replace(/[」』”"']$/, "").trim();
|
|
3302
3628
|
return candidate;
|
|
3303
3629
|
}
|
|
3630
|
+
/** Sentence spans, including the terminating punctuation, for evidence de-duplication. */
|
|
3631
|
+
function sentenceSpans(text) {
|
|
3632
|
+
const spans = [];
|
|
3633
|
+
let start = 0;
|
|
3634
|
+
for (let index = 0; index < text.length; index += 1) if ("。!?!?".includes(text[index])) {
|
|
3635
|
+
spans.push({
|
|
3636
|
+
start,
|
|
3637
|
+
end: index + 1
|
|
3638
|
+
});
|
|
3639
|
+
start = index + 1;
|
|
3640
|
+
}
|
|
3641
|
+
if (start < text.length) spans.push({
|
|
3642
|
+
start,
|
|
3643
|
+
end: text.length
|
|
3644
|
+
});
|
|
3645
|
+
return spans;
|
|
3646
|
+
}
|
|
3304
3647
|
/**
|
|
3305
3648
|
* Validate the model-supplied L2 semantic items. Evidence must be a real
|
|
3306
3649
|
* substring of the manuscript; a failed verdict is itself a blocking finding.
|
|
@@ -3309,6 +3652,10 @@ function evidenceQuote(value) {
|
|
|
3309
3652
|
function evaluateSemantic(text, items) {
|
|
3310
3653
|
const blocking = [];
|
|
3311
3654
|
const seen = /* @__PURE__ */ new Set();
|
|
3655
|
+
const seenQuotes = /* @__PURE__ */ new Set();
|
|
3656
|
+
const spans = [];
|
|
3657
|
+
const sentences = sentenceSpans(text);
|
|
3658
|
+
const usedSentences = /* @__PURE__ */ new Set();
|
|
3312
3659
|
for (const [index, item] of items.entries()) {
|
|
3313
3660
|
const where = `semantic[${index}]`;
|
|
3314
3661
|
if (typeof item !== "object" || item === null) {
|
|
@@ -3353,11 +3700,11 @@ function evaluateSemantic(text, items) {
|
|
|
3353
3700
|
continue;
|
|
3354
3701
|
}
|
|
3355
3702
|
const quote = evidenceQuote(item.evidence);
|
|
3356
|
-
if (Array.from(quote).length <
|
|
3703
|
+
if (Array.from(quote).length < 8) {
|
|
3357
3704
|
blocking.push({
|
|
3358
3705
|
code: "SEMANTIC_EVIDENCE_INVALID",
|
|
3359
3706
|
scope: "check",
|
|
3360
|
-
detail: `${item.id} quotes fewer than
|
|
3707
|
+
detail: `${item.id} quotes fewer than 8 characters`
|
|
3361
3708
|
});
|
|
3362
3709
|
continue;
|
|
3363
3710
|
}
|
|
@@ -3369,6 +3716,40 @@ function evaluateSemantic(text, items) {
|
|
|
3369
3716
|
});
|
|
3370
3717
|
continue;
|
|
3371
3718
|
}
|
|
3719
|
+
if (seenQuotes.has(quote)) {
|
|
3720
|
+
blocking.push({
|
|
3721
|
+
code: "SEMANTIC_EVIDENCE_INVALID",
|
|
3722
|
+
scope: "check",
|
|
3723
|
+
detail: `${item.id} repeats another semantic item's quote; each check needs distinct evidence`
|
|
3724
|
+
});
|
|
3725
|
+
continue;
|
|
3726
|
+
}
|
|
3727
|
+
let start = text.indexOf(quote);
|
|
3728
|
+
let sentence = -1;
|
|
3729
|
+
while (start !== -1) {
|
|
3730
|
+
const end = start + quote.length;
|
|
3731
|
+
const overlaps = spans.some((span) => start < span.end && span.start < end);
|
|
3732
|
+
const sentenceIndex = sentences.findIndex((candidate) => start >= candidate.start && end <= candidate.end);
|
|
3733
|
+
if (!overlaps && sentenceIndex !== -1 && !usedSentences.has(sentenceIndex)) {
|
|
3734
|
+
sentence = sentenceIndex;
|
|
3735
|
+
break;
|
|
3736
|
+
}
|
|
3737
|
+
start = text.indexOf(quote, start + 1);
|
|
3738
|
+
}
|
|
3739
|
+
if (start === -1 || sentence === -1) {
|
|
3740
|
+
blocking.push({
|
|
3741
|
+
code: "SEMANTIC_EVIDENCE_INVALID",
|
|
3742
|
+
scope: "check",
|
|
3743
|
+
detail: `${item.id} cites overlapping text or the same sentence as another check; each check needs a distinct passage`
|
|
3744
|
+
});
|
|
3745
|
+
continue;
|
|
3746
|
+
}
|
|
3747
|
+
spans.push({
|
|
3748
|
+
start,
|
|
3749
|
+
end: start + quote.length
|
|
3750
|
+
});
|
|
3751
|
+
usedSentences.add(sentence);
|
|
3752
|
+
seenQuotes.add(quote);
|
|
3372
3753
|
if (item.verdict === "fail") blocking.push({
|
|
3373
3754
|
code: "SEMANTIC_CHECK_FAILED",
|
|
3374
3755
|
scope: "check",
|
|
@@ -3421,6 +3802,7 @@ const wordCount = (input) => {
|
|
|
3421
3802
|
};
|
|
3422
3803
|
/** PG-04: format blockers must be zero before length is judged. */
|
|
3423
3804
|
const format = (input, options) => {
|
|
3805
|
+
if (input.scope !== void 0 && !input.scope.includes("content")) return [];
|
|
3424
3806
|
const violations = runRules([
|
|
3425
3807
|
"MD_CONTAMINATION",
|
|
3426
3808
|
"ILLEGAL_QUOTE",
|
|
@@ -3501,6 +3883,7 @@ const summarySchema = (input) => {
|
|
|
3501
3883
|
if (!isText(summary.opening)) problems.push("opening must be text");
|
|
3502
3884
|
else {
|
|
3503
3885
|
const opening = summary.opening;
|
|
3886
|
+
if (Array.from(opening).length < LIMITS.minQuoteChars) problems.push(`opening must be at least ${LIMITS.minQuoteChars} characters`);
|
|
3504
3887
|
if (!splitParagraphs(input.text).slice(0, 2).some((paragraph) => paragraph.includes(opening))) problems.push("opening must be quoted from the first two paragraphs");
|
|
3505
3888
|
}
|
|
3506
3889
|
const continuity = summary.continuity;
|
|
@@ -3537,6 +3920,7 @@ const score = (input, options) => {
|
|
|
3537
3920
|
if (review === null) return [block("PG-07", "no review document supplied")];
|
|
3538
3921
|
if (!Array.isArray(review.dimensions)) return [block("PG-07", "review dimensions must be an array")];
|
|
3539
3922
|
const problems = reviewArithmeticProblems(review.dimensions, review.total);
|
|
3923
|
+
if (applyExclusions(review.dimensions, options.rulepack.exclusions).dropped > 0) problems.push("review contains mutually exclusive or duplicate deductions; resubmit it through novel_review");
|
|
3540
3924
|
if (review.content_hash !== sha256(input.text)) problems.push("review content_hash does not match the current manuscript");
|
|
3541
3925
|
const paragraphCount = splitParagraphs(input.text).length;
|
|
3542
3926
|
for (const dimension of review.dimensions) {
|
|
@@ -3616,18 +4000,22 @@ const readingProof = (input) => {
|
|
|
3616
4000
|
ending
|
|
3617
4001
|
]) if (problem !== null) problems.push(problem);
|
|
3618
4002
|
const paragraphs = splitParagraphs(input.text).length;
|
|
3619
|
-
if (input.proof.opening.paragraph > 2) problems.push("the opening quote must come from the first two paragraphs");
|
|
3620
|
-
if (input.proof.ending.paragraph < Math.max(1, paragraphs - 1)) problems.push("the ending quote must come from the last two paragraphs");
|
|
3621
4003
|
const actuals = [
|
|
3622
4004
|
input.proof.opening,
|
|
3623
4005
|
input.proof.middle,
|
|
3624
4006
|
input.proof.ending
|
|
3625
4007
|
].map((reference) => actualParagraph(input.text, reference));
|
|
4008
|
+
if (actuals[0] > 2) problems.push("the opening quote must come from the first two paragraphs");
|
|
4009
|
+
const middleMin = paragraphs <= 4 ? 2 : 3;
|
|
4010
|
+
const middleMax = paragraphs <= 4 ? Math.max(2, paragraphs - 1) : paragraphs - 2;
|
|
4011
|
+
if (actuals[1] > 0 && (actuals[1] < middleMin || actuals[1] > middleMax)) problems.push("the middle quote must come from the body of the chapter, not its first or last edges");
|
|
4012
|
+
if (actuals[2] > 0 && actuals[2] < Math.max(1, paragraphs - 1)) problems.push("the ending quote must come from the last two paragraphs");
|
|
3626
4013
|
if (new Set(actuals.filter((index) => index > 0)).size < 3) problems.push("the three quotes must come from three distinct paragraphs");
|
|
3627
4014
|
return problems.length === 0 ? [] : [block("PG-09", problems.join("; "), "quote at least 12 characters from the real paragraph you read")];
|
|
3628
4015
|
};
|
|
3629
4016
|
/** PG-10: cross-chapter reconciliation must be clean. */
|
|
3630
4017
|
const consistency = (input, options) => {
|
|
4018
|
+
if (input.scope !== void 0 && !input.scope.includes("integrity")) return [];
|
|
3631
4019
|
const violations = runRules([
|
|
3632
4020
|
"DUPLICATE_PARAGRAPH",
|
|
3633
4021
|
"FORESHADOW_DORMANT",
|
|
@@ -3662,8 +4050,9 @@ const consistency = (input, options) => {
|
|
|
3662
4050
|
/** PG-11: the review/revise budget is enforced by the engine, not self-reported. */
|
|
3663
4051
|
const rounds = (input) => {
|
|
3664
4052
|
if (!(input.operation === "submitReview" || input.operation === "submitRevision" || input.operation === "runCheck")) return [];
|
|
3665
|
-
|
|
3666
|
-
|
|
4053
|
+
const counter = input.operation === "submitReview" ? "review" : input.operation === "submitRevision" ? "revise" : "check";
|
|
4054
|
+
if (!roundsExhausted(input.rounds, counter)) return [];
|
|
4055
|
+
return [block("PG-11", `round limits reached for ${counter} (review ${input.rounds.review}, revise ${input.rounds.revise}, check ${input.rounds.check})`, "call novel_rollback to restart the chapter; the manuscript is kept for reference")];
|
|
3667
4056
|
};
|
|
3668
4057
|
/** PG-12: the archive transaction's preconditions, checked before staging. */
|
|
3669
4058
|
const archiveAtomic = (input) => {
|
|
@@ -3753,6 +4142,7 @@ function applicableGates(input) {
|
|
|
3753
4142
|
"PG-06",
|
|
3754
4143
|
"PG-07",
|
|
3755
4144
|
"PG-08",
|
|
4145
|
+
"PG-09",
|
|
3756
4146
|
"PG-10",
|
|
3757
4147
|
"PG-12"
|
|
3758
4148
|
];
|
|
@@ -3784,7 +4174,7 @@ const STAGE_TO_PHASE = {
|
|
|
3784
4174
|
P4: "P4",
|
|
3785
4175
|
P5: "P5",
|
|
3786
4176
|
P6: "P6",
|
|
3787
|
-
ready_to_archive: "
|
|
4177
|
+
ready_to_archive: "P7",
|
|
3788
4178
|
done: "P9"
|
|
3789
4179
|
};
|
|
3790
4180
|
/** The nine-phase progress strip: phases before the active one are done. */
|
|
@@ -3818,23 +4208,6 @@ function projectChapter(record) {
|
|
|
3818
4208
|
details: record.summary.events.join(";").slice(0, 240)
|
|
3819
4209
|
};
|
|
3820
4210
|
}
|
|
3821
|
-
/** Status report used by the tool and by commands; one function, three callers. */
|
|
3822
|
-
function projectStatus(snapshot, next) {
|
|
3823
|
-
return {
|
|
3824
|
-
project: {
|
|
3825
|
-
title: snapshot.title,
|
|
3826
|
-
genre: snapshot.genre,
|
|
3827
|
-
platform: snapshot.platform,
|
|
3828
|
-
root: snapshot.root
|
|
3829
|
-
},
|
|
3830
|
-
chapter: snapshot.chapter,
|
|
3831
|
-
stage: snapshot.stage,
|
|
3832
|
-
rounds: { ...snapshot.rounds },
|
|
3833
|
-
blocking: snapshot.blocking,
|
|
3834
|
-
metrics: null,
|
|
3835
|
-
next
|
|
3836
|
-
};
|
|
3837
|
-
}
|
|
3838
4211
|
function projectSummary(snapshot) {
|
|
3839
4212
|
return {
|
|
3840
4213
|
schemaVersion: 1,
|
|
@@ -3979,6 +4352,17 @@ const ROUND_TRANSITIONS = {
|
|
|
3979
4352
|
P5: "P5",
|
|
3980
4353
|
P6: "P6"
|
|
3981
4354
|
};
|
|
4355
|
+
/** Forward order of stages; rollback may only move to an equal or earlier rank. */
|
|
4356
|
+
const STAGE_RANK = {
|
|
4357
|
+
idle: 0,
|
|
4358
|
+
P2: 1,
|
|
4359
|
+
P3: 2,
|
|
4360
|
+
P4: 3,
|
|
4361
|
+
P5: 4,
|
|
4362
|
+
P6: 5,
|
|
4363
|
+
ready_to_archive: 6,
|
|
4364
|
+
done: 7
|
|
4365
|
+
};
|
|
3982
4366
|
const MUTABLE_STATE = {
|
|
3983
4367
|
[joinRel(STATE_DIR, STATE_FILES.pipeline)]: "pipeline",
|
|
3984
4368
|
[joinRel(STATE_DIR, STATE_FILES.chapters)]: "chapters",
|
|
@@ -4013,6 +4397,20 @@ var EngineService = class {
|
|
|
4013
4397
|
/** Compiled on first use so `apply()` performs no IO. */
|
|
4014
4398
|
rulepackSource;
|
|
4015
4399
|
compiled = null;
|
|
4400
|
+
disposed = false;
|
|
4401
|
+
/**
|
|
4402
|
+
* Release in-process caches on host teardown. The engine owns no file
|
|
4403
|
+
* handles, so this is deliberately synchronous and idempotent; it prevents a
|
|
4404
|
+
* disposed instance from keeping project stores and replay results alive.
|
|
4405
|
+
*/
|
|
4406
|
+
dispose() {
|
|
4407
|
+
if (this.disposed) return;
|
|
4408
|
+
this.disposed = true;
|
|
4409
|
+
this.stores.clear();
|
|
4410
|
+
this.replay.clear();
|
|
4411
|
+
this.lastRoot = null;
|
|
4412
|
+
this.compiled = null;
|
|
4413
|
+
}
|
|
4016
4414
|
get rulepack() {
|
|
4017
4415
|
this.compiled ??= this.rulepackSource();
|
|
4018
4416
|
return this.compiled;
|
|
@@ -4052,6 +4450,8 @@ var EngineService = class {
|
|
|
4052
4450
|
async initProject(input) {
|
|
4053
4451
|
const title = typeof input.title === "string" ? input.title.trim() : "";
|
|
4054
4452
|
if (title === "") throw new EngineInputError(ERROR_CODES.SCHEMA_INVALID, "title must not be empty");
|
|
4453
|
+
if (input.author !== void 0 && typeof input.author !== "string") throw new EngineInputError(ERROR_CODES.SCHEMA_INVALID, "author must be a string");
|
|
4454
|
+
if (input.description !== void 0 && typeof input.description !== "string") throw new EngineInputError(ERROR_CODES.SCHEMA_INVALID, "description must be a string");
|
|
4055
4455
|
if (!GENRES.includes(input.genre)) throw new EngineInputError(ERROR_CODES.SCHEMA_INVALID, `unknown genre "${String(input.genre)}"`, `allowed: ${GENRES.join(", ")}`);
|
|
4056
4456
|
if (!PLATFORMS.includes(input.platform)) throw new EngineInputError(ERROR_CODES.SCHEMA_INVALID, `unknown platform "${String(input.platform)}"`, `allowed: ${PLATFORMS.join(", ")}`);
|
|
4057
4457
|
if (input.words_per_chapter !== void 0) {
|
|
@@ -4119,6 +4519,7 @@ var EngineService = class {
|
|
|
4119
4519
|
});
|
|
4120
4520
|
}
|
|
4121
4521
|
async getStatus(input = {}) {
|
|
4522
|
+
if (input.chapter !== void 0) this.assertChapterParameter(input.chapter);
|
|
4122
4523
|
const snapshot = await this.load(input.root);
|
|
4123
4524
|
const chapter = input.chapter ?? snapshot.pipeline.chapter;
|
|
4124
4525
|
const metrics = await this.metricsFor(snapshot, chapter);
|
|
@@ -4132,9 +4533,11 @@ var EngineService = class {
|
|
|
4132
4533
|
root: snapshot.root
|
|
4133
4534
|
},
|
|
4134
4535
|
chapter,
|
|
4536
|
+
active_chapter: snapshot.pipeline.chapter,
|
|
4135
4537
|
stage: snapshot.pipeline.stage,
|
|
4136
4538
|
rounds: { ...snapshot.pipeline.rounds },
|
|
4137
|
-
blocking: snapshot
|
|
4539
|
+
blocking: this.effectiveBlocking(snapshot),
|
|
4540
|
+
state_errors: this.stateErrors(snapshot),
|
|
4138
4541
|
metrics,
|
|
4139
4542
|
next
|
|
4140
4543
|
},
|
|
@@ -4148,6 +4551,7 @@ var EngineService = class {
|
|
|
4148
4551
|
revision: 1,
|
|
4149
4552
|
warnings: []
|
|
4150
4553
|
};
|
|
4554
|
+
if (input.chapter !== void 0) this.assertChapterParameter(input.chapter);
|
|
4151
4555
|
const snapshot = await this.load(input.root);
|
|
4152
4556
|
return {
|
|
4153
4557
|
data: countWordMetrics((input.chapter === void 0 ? snapshot.text : await this.readChapter(snapshot, input.chapter)) ?? ""),
|
|
@@ -4156,6 +4560,7 @@ var EngineService = class {
|
|
|
4156
4560
|
};
|
|
4157
4561
|
}
|
|
4158
4562
|
async queryState(input) {
|
|
4563
|
+
if (input.chapter !== void 0) this.assertChapterParameter(input.chapter);
|
|
4159
4564
|
const snapshot = await this.load(input.root);
|
|
4160
4565
|
const chapter = input.chapter ?? snapshot.pipeline.chapter;
|
|
4161
4566
|
const store = this.storeFor(snapshot.root);
|
|
@@ -4206,12 +4611,12 @@ var EngineService = class {
|
|
|
4206
4611
|
warnings: []
|
|
4207
4612
|
};
|
|
4208
4613
|
case "review": return {
|
|
4209
|
-
data: snapshot.review,
|
|
4614
|
+
data: chapter === snapshot.pipeline.chapter ? snapshot.review : await store.readDoc(reviewFile(chapter), "review"),
|
|
4210
4615
|
revision: snapshot.pipeline.revision,
|
|
4211
4616
|
warnings: []
|
|
4212
4617
|
};
|
|
4213
4618
|
case "check": return {
|
|
4214
|
-
data: snapshot.check,
|
|
4619
|
+
data: chapter === snapshot.pipeline.chapter ? snapshot.check : await store.readDoc(checkFile(chapter), "check"),
|
|
4215
4620
|
revision: snapshot.pipeline.revision,
|
|
4216
4621
|
warnings: []
|
|
4217
4622
|
};
|
|
@@ -4223,7 +4628,7 @@ var EngineService = class {
|
|
|
4223
4628
|
case "planWorld": {
|
|
4224
4629
|
const raw = await store.readText(planWorldFile(chapter));
|
|
4225
4630
|
return {
|
|
4226
|
-
data: raw === null ? null :
|
|
4631
|
+
data: raw === null ? null : parsePlanYaml(raw, planWorldFile(chapter)),
|
|
4227
4632
|
revision: snapshot.pipeline.revision,
|
|
4228
4633
|
warnings: []
|
|
4229
4634
|
};
|
|
@@ -4231,7 +4636,7 @@ var EngineService = class {
|
|
|
4231
4636
|
case "planChapter": {
|
|
4232
4637
|
const raw = await store.readText(planChapterFile(chapter));
|
|
4233
4638
|
return {
|
|
4234
|
-
data: raw === null ? null :
|
|
4639
|
+
data: raw === null ? null : parsePlanYaml(raw, planChapterFile(chapter)),
|
|
4235
4640
|
revision: snapshot.pipeline.revision,
|
|
4236
4641
|
warnings: []
|
|
4237
4642
|
};
|
|
@@ -4249,59 +4654,65 @@ var EngineService = class {
|
|
|
4249
4654
|
tool: next.tool,
|
|
4250
4655
|
args: next.args,
|
|
4251
4656
|
reason: next.reason,
|
|
4252
|
-
blocking: snapshot
|
|
4657
|
+
blocking: this.effectiveBlocking(snapshot),
|
|
4658
|
+
state_errors: this.stateErrors(snapshot)
|
|
4253
4659
|
},
|
|
4254
4660
|
revision: snapshot.pipeline.revision,
|
|
4255
4661
|
warnings: []
|
|
4256
4662
|
};
|
|
4257
4663
|
}
|
|
4258
4664
|
async getBrief(input) {
|
|
4259
|
-
|
|
4260
|
-
const chapter = input.chapter ?? snapshot.pipeline.chapter;
|
|
4261
|
-
const context = this.briefContextFor(input.stage, snapshot, chapter);
|
|
4262
|
-
const digest = this.briefDigest(input.stage, snapshot, context);
|
|
4263
|
-
const variables = this.briefVariables(snapshot, chapter);
|
|
4264
|
-
const rendered = this.briefs.render(input.stage, variables);
|
|
4265
|
-
const fields = {
|
|
4266
|
-
stage: input.stage,
|
|
4267
|
-
chapter,
|
|
4268
|
-
digest,
|
|
4269
|
-
min_words: snapshot.project.targets.words_per_chapter[0],
|
|
4270
|
-
min_score: this.config.quality.minScore,
|
|
4271
|
-
scene_types: [
|
|
4272
|
-
"战斗/动作章",
|
|
4273
|
-
"日常过渡章",
|
|
4274
|
-
"情感爆发章",
|
|
4275
|
-
"纯叙事章"
|
|
4276
|
-
],
|
|
4277
|
-
platform_style: this.rulepack.platform[snapshot.project.platform],
|
|
4278
|
-
genre_guide: this.rulepack.genres[snapshot.project.genre],
|
|
4279
|
-
/** P4 must apply exclusions before its arithmetic closes. */
|
|
4280
|
-
score_exclusions: this.rulepack.exclusions,
|
|
4281
|
-
next_tool: NEXT_TOOL_BY_BRIEF[input.stage]
|
|
4282
|
-
};
|
|
4283
|
-
const existing = snapshot.pipeline.briefs[input.stage];
|
|
4284
|
-
if (existing !== void 0 && existing.digest === digest) return {
|
|
4285
|
-
data: {
|
|
4286
|
-
stage: input.stage,
|
|
4287
|
-
chapter,
|
|
4288
|
-
digest,
|
|
4289
|
-
title: rendered.title,
|
|
4290
|
-
content: rendered.content,
|
|
4291
|
-
content_hash: rendered.contentHash,
|
|
4292
|
-
fields,
|
|
4293
|
-
context
|
|
4294
|
-
},
|
|
4295
|
-
revision: snapshot.pipeline.revision,
|
|
4296
|
-
warnings: []
|
|
4297
|
-
};
|
|
4665
|
+
await this.load(input.root);
|
|
4298
4666
|
return this.mutate({
|
|
4299
4667
|
root: input.root,
|
|
4300
4668
|
op: "getBrief",
|
|
4301
|
-
run: async (
|
|
4302
|
-
const
|
|
4669
|
+
run: async (snapshot, context) => {
|
|
4670
|
+
const chapter = input.chapter ?? snapshot.pipeline.chapter;
|
|
4671
|
+
if (chapter !== snapshot.pipeline.chapter) throw new EngineInputError(ERROR_CODES.SCHEMA_INVALID, `brief ${input.stage} belongs to active chapter ${snapshot.pipeline.chapter}, not ${chapter}`, `omit chapter or pass ${snapshot.pipeline.chapter}`);
|
|
4672
|
+
const contextPack = this.briefContextFor(input.stage, snapshot, chapter);
|
|
4673
|
+
const digest = this.briefDigest(input.stage, snapshot, contextPack);
|
|
4674
|
+
const variables = this.briefVariables(snapshot, chapter);
|
|
4675
|
+
const rendered = this.briefs.render(input.stage, variables);
|
|
4676
|
+
const fields = {
|
|
4677
|
+
stage: input.stage,
|
|
4678
|
+
chapter,
|
|
4679
|
+
digest,
|
|
4680
|
+
min_words: snapshot.project.targets.words_per_chapter[0],
|
|
4681
|
+
min_score: this.config.quality.minScore,
|
|
4682
|
+
scene_types: [
|
|
4683
|
+
"战斗/动作章",
|
|
4684
|
+
"日常过渡章",
|
|
4685
|
+
"情感爆发章",
|
|
4686
|
+
"纯叙事章"
|
|
4687
|
+
],
|
|
4688
|
+
platform_style: this.rulepack.platform[snapshot.project.platform],
|
|
4689
|
+
genre_guide: this.rulepack.genres[snapshot.project.genre],
|
|
4690
|
+
/** P4 must apply exclusions before its arithmetic closes. */
|
|
4691
|
+
score_exclusions: this.rulepack.exclusions,
|
|
4692
|
+
next_tool: NEXT_TOOL_BY_BRIEF[input.stage],
|
|
4693
|
+
...snapshot.planError === null ? {} : { plan_error: snapshot.planError },
|
|
4694
|
+
...snapshot.reviewError === null ? {} : { review_error: snapshot.reviewError },
|
|
4695
|
+
...snapshot.checkError === null ? {} : { check_error: snapshot.checkError }
|
|
4696
|
+
};
|
|
4697
|
+
const data = {
|
|
4698
|
+
stage: input.stage,
|
|
4699
|
+
chapter,
|
|
4700
|
+
digest,
|
|
4701
|
+
title: rendered.title,
|
|
4702
|
+
content: rendered.content,
|
|
4703
|
+
content_hash: rendered.contentHash,
|
|
4704
|
+
fields,
|
|
4705
|
+
context: contextPack
|
|
4706
|
+
};
|
|
4707
|
+
const existing = snapshot.pipeline.briefs[input.stage];
|
|
4708
|
+
if (existing !== void 0 && existing.digest === digest) return {
|
|
4709
|
+
files: [],
|
|
4710
|
+
data,
|
|
4711
|
+
replay: true
|
|
4712
|
+
};
|
|
4713
|
+
const nextPipeline = pipelineWith(snapshot, context.revision, {
|
|
4303
4714
|
briefs: {
|
|
4304
|
-
...
|
|
4715
|
+
...snapshot.pipeline.briefs,
|
|
4305
4716
|
[input.stage]: {
|
|
4306
4717
|
issued_at: this.now().toISOString(),
|
|
4307
4718
|
digest
|
|
@@ -4315,22 +4726,27 @@ var EngineService = class {
|
|
|
4315
4726
|
text: yaml(nextPipeline)
|
|
4316
4727
|
}],
|
|
4317
4728
|
backup: false,
|
|
4318
|
-
data
|
|
4319
|
-
stage: input.stage,
|
|
4320
|
-
chapter,
|
|
4321
|
-
digest,
|
|
4322
|
-
title: rendered.title,
|
|
4323
|
-
content: rendered.content,
|
|
4324
|
-
content_hash: rendered.contentHash,
|
|
4325
|
-
fields,
|
|
4326
|
-
context
|
|
4327
|
-
}
|
|
4729
|
+
data
|
|
4328
4730
|
};
|
|
4329
4731
|
}
|
|
4330
4732
|
});
|
|
4331
4733
|
}
|
|
4332
4734
|
async submitWorldPlan(input) {
|
|
4333
4735
|
if (typeof input.objective !== "string" || input.objective.trim() === "") throw new EngineInputError(ERROR_CODES.SCHEMA_INVALID, "world plan objective must not be empty");
|
|
4736
|
+
const constraints = stringList(input.constraints, "constraints");
|
|
4737
|
+
const continuity_notes = stringList(input.continuity_notes, "continuity_notes");
|
|
4738
|
+
const characters = objectList(input.characters, "characters");
|
|
4739
|
+
for (const [index, character] of characters.entries()) {
|
|
4740
|
+
requireText(character, "name", `characters[${index}].name`);
|
|
4741
|
+
requireText(character, "role", `characters[${index}].role`);
|
|
4742
|
+
requireText(character, "current_state", `characters[${index}].current_state`);
|
|
4743
|
+
}
|
|
4744
|
+
const locations = objectList(input.locations, "locations");
|
|
4745
|
+
for (const [index, location] of locations.entries()) {
|
|
4746
|
+
requireText(location, "name", `locations[${index}].name`);
|
|
4747
|
+
requireText(location, "layout", `locations[${index}].layout`);
|
|
4748
|
+
if (typeof location.capacity !== "number" || !Number.isFinite(location.capacity)) throw new EngineInputError(ERROR_CODES.SCHEMA_INVALID, `locations[${index}].capacity must be a finite number`);
|
|
4749
|
+
}
|
|
4334
4750
|
return this.mutate({
|
|
4335
4751
|
root: input.root,
|
|
4336
4752
|
op: "submitWorldPlan",
|
|
@@ -4341,10 +4757,10 @@ var EngineService = class {
|
|
|
4341
4757
|
const plan = {
|
|
4342
4758
|
chapter,
|
|
4343
4759
|
objective: input.objective,
|
|
4344
|
-
constraints
|
|
4345
|
-
characters
|
|
4346
|
-
locations
|
|
4347
|
-
continuity_notes
|
|
4760
|
+
constraints,
|
|
4761
|
+
characters,
|
|
4762
|
+
locations,
|
|
4763
|
+
continuity_notes
|
|
4348
4764
|
};
|
|
4349
4765
|
const path = planWorldFile(chapter);
|
|
4350
4766
|
const nextPipeline = pipelineWith(snapshot, context.revision, {
|
|
@@ -4379,6 +4795,13 @@ var EngineService = class {
|
|
|
4379
4795
|
}
|
|
4380
4796
|
async submitChapterPlan(input) {
|
|
4381
4797
|
if (typeof input.title !== "string" || input.title.trim() === "") throw new EngineInputError(ERROR_CODES.SCHEMA_INVALID, "chapter plan title must not be empty");
|
|
4798
|
+
const titleBody = input.title.replace(/^第\s*\d+\s*章\s*/, "").trim();
|
|
4799
|
+
const titleLength = Array.from(titleBody).length;
|
|
4800
|
+
if (titleLength < 4 || titleLength > 8) throw new EngineInputError(ERROR_CODES.SCHEMA_INVALID, `chapter plan title body must be 4..8 characters, got ${titleLength}`, "use the same 4..8 character body the P3 title rule will judge");
|
|
4801
|
+
if (typeof input.narrator !== "string" || input.narrator.trim() === "") throw new EngineInputError(ERROR_CODES.SCHEMA_INVALID, "chapter plan narrator must not be empty");
|
|
4802
|
+
const beats = stringList(input.beats, "beats");
|
|
4803
|
+
if (beats.length < 5 || beats.length > 8) throw new EngineInputError(ERROR_CODES.SCHEMA_INVALID, `chapter plan beats must contain 5..8 non-empty items, got ${beats.length}`, "write the five to eight causal beats the P2 task book requires");
|
|
4804
|
+
for (const [field, value] of [["opening", input.opening], ["ending", input.ending]]) if (typeof value !== "string" || value.trim() === "") throw new EngineInputError(ERROR_CODES.SCHEMA_INVALID, `chapter plan ${field} must not be empty`);
|
|
4382
4805
|
if (!SCENE_TYPES.includes(input.scene_type)) throw new EngineInputError(ERROR_CODES.SCHEMA_INVALID, `unknown scene_type "${String(input.scene_type)}"`, `allowed: ${SCENE_TYPES.join(", ")}`);
|
|
4383
4806
|
if (!Array.isArray(input.target_words) || input.target_words.length !== 2) throw new EngineInputError(ERROR_CODES.SCHEMA_INVALID, "target_words must be [min, max] positive integers with min <= max");
|
|
4384
4807
|
const [min, max] = input.target_words;
|
|
@@ -4394,6 +4817,7 @@ var EngineService = class {
|
|
|
4394
4817
|
const plan = {
|
|
4395
4818
|
...input,
|
|
4396
4819
|
chapter,
|
|
4820
|
+
beats,
|
|
4397
4821
|
schema_version: 1
|
|
4398
4822
|
};
|
|
4399
4823
|
delete plan.root;
|
|
@@ -4451,7 +4875,7 @@ var EngineService = class {
|
|
|
4451
4875
|
const planPath = planChapterFile(chapter);
|
|
4452
4876
|
const planText = await context.store.readText(planPath);
|
|
4453
4877
|
const mergedPlan = {
|
|
4454
|
-
...(planText === null ? null :
|
|
4878
|
+
...(planText === null ? null : parsePlanYaml(planText, planPath)) ?? {
|
|
4455
4879
|
schema_version: 1,
|
|
4456
4880
|
chapter
|
|
4457
4881
|
},
|
|
@@ -4527,6 +4951,11 @@ var EngineService = class {
|
|
|
4527
4951
|
"fix"
|
|
4528
4952
|
]) if (typeof issue[field] !== "string" || issue[field].trim() === "") throw new EngineInputError(ERROR_CODES.SCHEMA_INVALID, `issue ${issue.id} has no ${field}`, "every issue needs id, severity, status, dimension, location, problem and fix");
|
|
4529
4953
|
if (issue.evidence !== void 0 && typeof issue.evidence !== "string") throw new EngineInputError(ERROR_CODES.SCHEMA_INVALID, `issue ${issue.id} evidence must be a string`, "quote the revised text as evidence, or omit the field");
|
|
4954
|
+
if (issue.severity === "blocker" && issue.status === "waived") throw new EngineInputError(ERROR_CODES.SCHEMA_INVALID, `issue ${issue.id} is a blocker and cannot be waived`, "fix the issue and quote the revised text, or keep it open");
|
|
4955
|
+
if (this.rulepack.quality.dimensions.find((item) => item.id === issue.dimension) === void 0) throw new EngineInputError(ERROR_CODES.SCHEMA_INVALID, `issue ${issue.id} references unknown dimension "${issue.dimension}"`, `use one of: ${this.rulepack.quality.dimensions.map((item) => item.id).join(", ")}`);
|
|
4956
|
+
const paragraph = parseParagraphLocation(issue.location);
|
|
4957
|
+
const paragraphCount = splitParagraphs(text).length;
|
|
4958
|
+
if (paragraph === null || paragraph < 1 || paragraph > paragraphCount) throw new EngineInputError(ERROR_CODES.SCHEMA_INVALID, `issue ${issue.id} location "${issue.location}" is outside the chapter`, `use 第 1 段 .. 第 ${paragraphCount} 段`);
|
|
4530
4959
|
}
|
|
4531
4960
|
const review = {
|
|
4532
4961
|
schema_version: 1,
|
|
@@ -4559,6 +4988,7 @@ var EngineService = class {
|
|
|
4559
4988
|
detail: `score ${input.total} is below the minimum ${this.config.quality.minScore}`,
|
|
4560
4989
|
hint: "revise the chapter and submit a new P4 review"
|
|
4561
4990
|
});
|
|
4991
|
+
const reviewDigest = sha256(stableJson(review));
|
|
4562
4992
|
const nextPipeline = pipelineWith(snapshot, context.revision, {
|
|
4563
4993
|
stage: next,
|
|
4564
4994
|
rounds: {
|
|
@@ -4566,7 +4996,8 @@ var EngineService = class {
|
|
|
4566
4996
|
review: snapshot.pipeline.rounds.review + 1
|
|
4567
4997
|
},
|
|
4568
4998
|
blocking,
|
|
4569
|
-
|
|
4999
|
+
review_digest: reviewDigest,
|
|
5000
|
+
last_action: action("novel_review", this.now(), reviewDigest)
|
|
4570
5001
|
});
|
|
4571
5002
|
return {
|
|
4572
5003
|
files: [{
|
|
@@ -4629,7 +5060,15 @@ var EngineService = class {
|
|
|
4629
5060
|
seen.add(submitted.id);
|
|
4630
5061
|
const before = byId.get(submitted.id);
|
|
4631
5062
|
if (before === void 0) throw new EngineInputError(ERROR_CODES.SCHEMA_INVALID, `revision references unknown issue ${submitted.id}`, "only update issues from the current review; new issues belong in the next review");
|
|
4632
|
-
|
|
5063
|
+
for (const field of [
|
|
5064
|
+
"dimension",
|
|
5065
|
+
"location",
|
|
5066
|
+
"problem",
|
|
5067
|
+
"fix"
|
|
5068
|
+
]) if (submitted[field] !== void 0 && submitted[field] !== before[field]) throw new EngineInputError(ERROR_CODES.SCHEMA_INVALID, `issue ${submitted.id} cannot change its ${field} during revision`, "only status and evidence may change; new findings belong in the next review");
|
|
5069
|
+
if (submitted.severity !== void 0 && submitted.severity !== before.severity) throw new EngineInputError(ERROR_CODES.SCHEMA_INVALID, `issue ${submitted.id} cannot change severity during revision`, "only status and evidence may change; a blocker stays a blocker until re-reviewed");
|
|
5070
|
+
if (before.severity === "blocker" && submitted.severity !== void 0 && submitted.severity !== "blocker") throw new EngineInputError(ERROR_CODES.SCHEMA_INVALID, `issue ${submitted.id} is a blocker and cannot be downgraded`, "keep severity blocker and mark it fixed with evidence");
|
|
5071
|
+
if (before.severity === "blocker" && submitted.status === "waived") throw new EngineInputError(ERROR_CODES.SCHEMA_INVALID, `issue ${submitted.id} is a blocker and cannot be waived`, "keep severity blocker and mark it fixed with evidence");
|
|
4633
5072
|
const index = issues.findIndex((issue) => issue.id === submitted.id);
|
|
4634
5073
|
issues[index] = {
|
|
4635
5074
|
...before,
|
|
@@ -4669,11 +5108,12 @@ var EngineService = class {
|
|
|
4669
5108
|
revise: snapshot.pipeline.rounds.revise + 1
|
|
4670
5109
|
},
|
|
4671
5110
|
blocking: [],
|
|
5111
|
+
...patchedReview === null ? {} : { review_digest: sha256(stableJson(patchedReview)) },
|
|
4672
5112
|
last_action: action("novel_revise", this.now(), sha256(input.content))
|
|
4673
5113
|
});
|
|
4674
5114
|
const planPath = planChapterFile(input.chapter);
|
|
4675
5115
|
const planText = await context.store.readText(planPath);
|
|
4676
|
-
const plan = planText === null ? null :
|
|
5116
|
+
const plan = planText === null ? null : parsePlanYaml(planText, planPath);
|
|
4677
5117
|
const files = [{
|
|
4678
5118
|
path: chapterFile(input.chapter),
|
|
4679
5119
|
text: input.content
|
|
@@ -4706,12 +5146,13 @@ var EngineService = class {
|
|
|
4706
5146
|
});
|
|
4707
5147
|
}
|
|
4708
5148
|
async runCheck(input) {
|
|
5149
|
+
const scope = normalizeCheckScope(input.scope);
|
|
4709
5150
|
return this.mutate({
|
|
4710
5151
|
root: input.root,
|
|
4711
5152
|
op: "runCheck",
|
|
4712
5153
|
key: (snapshot) => `check:${input.chapter}:${sha256(snapshot.text ?? "")}:${sha256(stableJson({
|
|
4713
5154
|
semantic: input.semantic ?? [],
|
|
4714
|
-
scope
|
|
5155
|
+
scope
|
|
4715
5156
|
}))}`,
|
|
4716
5157
|
run: async (snapshot, context) => {
|
|
4717
5158
|
this.assertChapter(snapshot, input.chapter);
|
|
@@ -4721,20 +5162,25 @@ var EngineService = class {
|
|
|
4721
5162
|
briefStage: "P6",
|
|
4722
5163
|
text,
|
|
4723
5164
|
proof: snapshot.review?.proof ?? null,
|
|
4724
|
-
semantic
|
|
5165
|
+
semantic,
|
|
5166
|
+
scope
|
|
4725
5167
|
});
|
|
4726
5168
|
const machine = this.machine(snapshot, text, { summary: snapshot.plan?.summary ?? null });
|
|
4727
|
-
const
|
|
5169
|
+
const scopedMachine = {
|
|
5170
|
+
...machine,
|
|
5171
|
+
violations: machine.violations.filter((violation) => scope.includes(scopeOfRule(violation.code)))
|
|
5172
|
+
};
|
|
5173
|
+
const machineBlocking = scopedMachine.violations.filter((violation) => violation.severity === "blocker");
|
|
4728
5174
|
const semanticResult = evaluateSemantic(text, semantic);
|
|
4729
5175
|
const verdict = machineBlocking.length === 0 && !semanticResult.failed ? "pass" : "fail";
|
|
4730
5176
|
const check = {
|
|
4731
5177
|
schema_version: 1,
|
|
4732
5178
|
chapter: input.chapter,
|
|
4733
|
-
scope
|
|
5179
|
+
scope,
|
|
4734
5180
|
content_hash: sha256(text),
|
|
4735
5181
|
machine: {
|
|
4736
|
-
violations:
|
|
4737
|
-
metrics:
|
|
5182
|
+
violations: scopedMachine.violations,
|
|
5183
|
+
metrics: scopedMachine.metrics
|
|
4738
5184
|
},
|
|
4739
5185
|
semantic: { items: semantic },
|
|
4740
5186
|
verdict
|
|
@@ -4745,7 +5191,7 @@ var EngineService = class {
|
|
|
4745
5191
|
scope: "check",
|
|
4746
5192
|
detail: violation.detail
|
|
4747
5193
|
})), ...semanticResult.blocking] : [];
|
|
4748
|
-
|
|
5194
|
+
const nextPipeline = pipelineWith(snapshot, context.revision, {
|
|
4749
5195
|
stage: nextStage,
|
|
4750
5196
|
rounds: {
|
|
4751
5197
|
...snapshot.pipeline.rounds,
|
|
@@ -4754,24 +5200,6 @@ var EngineService = class {
|
|
|
4754
5200
|
blocking: blockingItems,
|
|
4755
5201
|
last_action: action("novel_check", this.now(), sha256(text))
|
|
4756
5202
|
});
|
|
4757
|
-
if (verdict === "fail" && nextPipeline.briefs.P6 !== void 0) {
|
|
4758
|
-
const postCheck = {
|
|
4759
|
-
...snapshot,
|
|
4760
|
-
check,
|
|
4761
|
-
pipeline: nextPipeline
|
|
4762
|
-
};
|
|
4763
|
-
const digest = this.briefDigest("P6", postCheck, this.briefContextFor("P6", postCheck, input.chapter));
|
|
4764
|
-
nextPipeline = {
|
|
4765
|
-
...nextPipeline,
|
|
4766
|
-
briefs: {
|
|
4767
|
-
...nextPipeline.briefs,
|
|
4768
|
-
P6: {
|
|
4769
|
-
...nextPipeline.briefs.P6,
|
|
4770
|
-
digest
|
|
4771
|
-
}
|
|
4772
|
-
}
|
|
4773
|
-
};
|
|
4774
|
-
}
|
|
4775
5203
|
return {
|
|
4776
5204
|
files: [{
|
|
4777
5205
|
path: checkFile(input.chapter),
|
|
@@ -4783,11 +5211,11 @@ var EngineService = class {
|
|
|
4783
5211
|
data: {
|
|
4784
5212
|
chapter: input.chapter,
|
|
4785
5213
|
verdict,
|
|
4786
|
-
violations:
|
|
4787
|
-
metrics:
|
|
5214
|
+
violations: scopedMachine.violations.length,
|
|
5215
|
+
metrics: scopedMachine.metrics,
|
|
4788
5216
|
stage: nextStage
|
|
4789
5217
|
},
|
|
4790
|
-
warnings: machineWarnings(
|
|
5218
|
+
warnings: machineWarnings(scopedMachine)
|
|
4791
5219
|
};
|
|
4792
5220
|
}
|
|
4793
5221
|
});
|
|
@@ -4808,7 +5236,8 @@ var EngineService = class {
|
|
|
4808
5236
|
run: async (snapshot, context) => {
|
|
4809
5237
|
const already = snapshot.chapters.chapters.find((record) => record.num === input.chapter);
|
|
4810
5238
|
if (already !== void 0) {
|
|
4811
|
-
if (already.archive_digest
|
|
5239
|
+
if (already.archive_digest === void 0) throw new EngineFaultError(ERROR_CODES.SCHEMA_INVALID, `chapter ${input.chapter} has no archive_digest; the index entry is not trustworthy`, "restore the chapter index from backups/ or re-archive from the original manuscript");
|
|
5240
|
+
if (already.archive_digest !== archiveDigest) throw new EngineInputError(ERROR_CODES.SCHEMA_INVALID, `chapter ${input.chapter} is already archived with a different state_delta`, "archive is idempotent by chapter; start the next chapter instead of replaying old data");
|
|
4812
5241
|
const archivedText = await this.readChapter(snapshot, input.chapter);
|
|
4813
5242
|
if (archivedText === null || sha256(archivedText) !== already.content_hash) throw new EngineFaultError(ERROR_CODES.TXN_ABORTED, `archived chapter ${input.chapter} no longer matches its index`, "restore the chapter from backups/ before retrying");
|
|
4814
5243
|
return {
|
|
@@ -4827,11 +5256,14 @@ var EngineService = class {
|
|
|
4827
5256
|
const text = snapshot.text ?? "";
|
|
4828
5257
|
const summary = snapshot.plan?.summary;
|
|
4829
5258
|
if (summary === void 0) throw new EngineInputError(ERROR_CODES.SCHEMA_INVALID, "the draft has no summary; resubmit the draft with a summary", "call novel_write_draft with the full summary block");
|
|
5259
|
+
if (snapshot.review !== null && snapshot.pipeline.review_digest === void 0) throw new EngineFaultError(ERROR_CODES.SCHEMA_INVALID, "the stored review has no review_digest; it predates digest-bound reviews and cannot be archived blindly", "re-run novel_review for this chapter, then re-run the archive");
|
|
5260
|
+
if (snapshot.review !== null && snapshot.pipeline.review_digest !== void 0 && snapshot.pipeline.review_digest !== sha256(stableJson(snapshot.review))) throw new EngineFaultError(ERROR_CODES.SCHEMA_INVALID, "the stored review changed after submission; its digest no longer matches", "re-run novel_review instead of editing reviews/ directly");
|
|
4830
5261
|
await this.assertGates(snapshot, "archiveChapter", {
|
|
4831
5262
|
briefStage: "P8",
|
|
4832
5263
|
text,
|
|
4833
5264
|
summary,
|
|
4834
|
-
review: snapshot.review
|
|
5265
|
+
review: snapshot.review,
|
|
5266
|
+
proof: snapshot.review?.proof ?? null
|
|
4835
5267
|
});
|
|
4836
5268
|
if (text === "") throw new EngineInputError(ERROR_CODES.STAGE_ORDER, `chapter ${input.chapter} has no draft`);
|
|
4837
5269
|
const archiveBlockers = this.machine(snapshot, text, { summary }).violations.filter((violation) => violation.severity === "blocker");
|
|
@@ -4992,7 +5424,7 @@ var EngineService = class {
|
|
|
4992
5424
|
if (typeof input.content !== "object" || input.content === null || Array.isArray(input.content)) throw new EngineInputError(ERROR_CODES.SCHEMA_INVALID, "amend content must be an object");
|
|
4993
5425
|
const resolvedRoot = await this.resolveRoot(input.root);
|
|
4994
5426
|
const absolute = this.storeFor(resolvedRoot).path(input.path);
|
|
4995
|
-
const kind = MUTABLE_STATE[input.path];
|
|
5427
|
+
const kind = Object.hasOwn(MUTABLE_STATE, input.path) ? MUTABLE_STATE[input.path] : void 0;
|
|
4996
5428
|
if (kind === void 0 || absolute.length === 0) throw new EngineInputError(ERROR_CODES.SCHEMA_INVALID, `amend can only target state documents: ${Object.keys(MUTABLE_STATE).join(", ")}`, "edit novel content through the draft/revision tools, not amend");
|
|
4997
5429
|
return this.mutate({
|
|
4998
5430
|
root: resolvedRoot,
|
|
@@ -5041,6 +5473,7 @@ var EngineService = class {
|
|
|
5041
5473
|
this.assertChapter(snapshot, input.chapter);
|
|
5042
5474
|
if (!Object.prototype.hasOwnProperty.call(ROUND_TRANSITIONS, input.toStage)) throw new EngineInputError(ERROR_CODES.SCHEMA_INVALID, `unknown rollback target "${String(input.toStage)}"`, `allowed targets: ${Object.keys(ROUND_TRANSITIONS).join(", ")}`);
|
|
5043
5475
|
const stage = ROUND_TRANSITIONS[input.toStage];
|
|
5476
|
+
if (STAGE_RANK[stage] > STAGE_RANK[snapshot.pipeline.stage]) throw new EngineInputError(ERROR_CODES.STAGE_ORDER, `cannot roll back from ${snapshot.pipeline.stage} to later stage ${stage}`, "rollback only moves to the current stage or an earlier one; use the normal stage tools to advance");
|
|
5044
5477
|
const nextPipeline = pipelineWith(snapshot, context.revision, {
|
|
5045
5478
|
chapter: input.chapter,
|
|
5046
5479
|
stage,
|
|
@@ -5259,6 +5692,7 @@ var EngineService = class {
|
|
|
5259
5692
|
revision: snapshot.pipeline.revision,
|
|
5260
5693
|
warnings: []
|
|
5261
5694
|
};
|
|
5695
|
+
if (!Object.hasOwn(STATE_FILES, input.kind)) throw new EngineInputError(ERROR_CODES.SCHEMA_INVALID, `unknown state document "${String(input.kind)}"`, `allowed documents: ${Object.values(STATE_FILES).join(", ")}`);
|
|
5262
5696
|
const path = joinRel(STATE_DIR, STATE_FILES[input.kind]);
|
|
5263
5697
|
return await this.mutate({
|
|
5264
5698
|
root: input.root,
|
|
@@ -5296,7 +5730,7 @@ var EngineService = class {
|
|
|
5296
5730
|
title: (snapshot.plan?.title ?? "").replace(/^第\s*\d+\s*章\s*/, "").trim() || `第${snapshot.pipeline.chapter}章`,
|
|
5297
5731
|
wordCount: draftWords,
|
|
5298
5732
|
score: snapshot.review?.total ?? 0,
|
|
5299
|
-
status: snapshot.
|
|
5733
|
+
status: this.effectiveBlocking(snapshot).length > 0 ? "blocked" : "draft",
|
|
5300
5734
|
details: (snapshot.plan?.summary?.events ?? []).join(";").slice(0, 240)
|
|
5301
5735
|
};
|
|
5302
5736
|
return {
|
|
@@ -5308,7 +5742,7 @@ var EngineService = class {
|
|
|
5308
5742
|
stage: snapshot.pipeline.stage,
|
|
5309
5743
|
revision: snapshot.pipeline.revision,
|
|
5310
5744
|
rounds: snapshot.pipeline.rounds,
|
|
5311
|
-
blocking: snapshot
|
|
5745
|
+
blocking: this.effectiveBlocking(snapshot),
|
|
5312
5746
|
next: finished ? void 0 : this.nextStep(snapshot),
|
|
5313
5747
|
finished,
|
|
5314
5748
|
chapters: archived,
|
|
@@ -5318,31 +5752,224 @@ var EngineService = class {
|
|
|
5318
5752
|
};
|
|
5319
5753
|
}
|
|
5320
5754
|
nextStep(snapshot) {
|
|
5321
|
-
|
|
5322
|
-
|
|
5323
|
-
|
|
5324
|
-
|
|
5325
|
-
|
|
5326
|
-
|
|
5327
|
-
};
|
|
5755
|
+
const stage = snapshot.pipeline.stage;
|
|
5756
|
+
const chapter = snapshot.pipeline.chapter;
|
|
5757
|
+
if (this.stateErrors(snapshot).length > 0) {
|
|
5758
|
+
const recovery = this.recoveryFor(snapshot);
|
|
5759
|
+
const target = recovery.args.to_stage;
|
|
5760
|
+
if (STAGE_RANK[ROUND_TRANSITIONS[target]] < STAGE_RANK[stage]) return recovery;
|
|
5328
5761
|
}
|
|
5762
|
+
const reviewDigest = this.reviewDigestProblem(snapshot);
|
|
5763
|
+
if (reviewDigest !== null) return {
|
|
5764
|
+
tool: "novel_rollback",
|
|
5765
|
+
args: {
|
|
5766
|
+
chapter,
|
|
5767
|
+
to_stage: "P4"
|
|
5768
|
+
},
|
|
5769
|
+
reason: `${reviewDigest.code}: ${reviewDigest.detail}; ${reviewDigest.hint}`
|
|
5770
|
+
};
|
|
5771
|
+
const step = STAGE_NEXT[stage];
|
|
5329
5772
|
const target = snapshot.project.targets.total_chapters;
|
|
5330
|
-
if (
|
|
5773
|
+
if (stage === "done" && target !== null && snapshot.chapters.chapters.length >= target) return {
|
|
5331
5774
|
tool: "novel_export",
|
|
5332
5775
|
args: { kind: "epub" },
|
|
5333
5776
|
reason: "全书已完成;可导出成书,或结束本书"
|
|
5334
5777
|
};
|
|
5335
|
-
const
|
|
5336
|
-
|
|
5337
|
-
|
|
5338
|
-
|
|
5339
|
-
|
|
5778
|
+
const roundsBlock = this.roundsLimitBlock(snapshot);
|
|
5779
|
+
if (roundsBlock !== null) return {
|
|
5780
|
+
tool: "novel_rollback",
|
|
5781
|
+
args: {
|
|
5782
|
+
chapter,
|
|
5783
|
+
to_stage: "P4"
|
|
5784
|
+
},
|
|
5785
|
+
reason: `${roundsBlock.code}: ${roundsBlock.detail}; ${roundsBlock.hint ?? ""}`.trim()
|
|
5786
|
+
};
|
|
5787
|
+
const first = this.effectiveBlocking(snapshot)[0];
|
|
5788
|
+
const detail = first === void 0 ? "" : `${first.code}: ${first.detail}${first.hint === void 0 ? "" : ` (${first.hint})`}`;
|
|
5789
|
+
const briefStage = STATUS_BRIEF[stage];
|
|
5790
|
+
const followUp = (stage === "P5" || stage === "P6") && snapshot.pipeline.blocking.length > 0 ? "novel_revise" : step.tool;
|
|
5791
|
+
if (briefStage !== null && !this.briefIsCurrent(snapshot, briefStage)) return {
|
|
5792
|
+
tool: "novel_brief",
|
|
5793
|
+
args: {
|
|
5794
|
+
stage: briefStage,
|
|
5795
|
+
chapter
|
|
5796
|
+
},
|
|
5797
|
+
reason: detail === "" ? `fetch the ${briefStage} brief before ${followUp}` : `${detail}; fetch the ${briefStage} brief, then ${followUp}`
|
|
5798
|
+
};
|
|
5799
|
+
if (stage === "P6" && snapshot.checkError !== null) return {
|
|
5800
|
+
tool: "novel_check",
|
|
5801
|
+
args: this.nextArgs("P6", chapter),
|
|
5802
|
+
reason: `CHECK_UNREADABLE@${checkFile(chapter)}; repair the check with novel_check before any stale revision blocker`
|
|
5803
|
+
};
|
|
5804
|
+
if (snapshot.pipeline.blocking.length > 0) {
|
|
5805
|
+
if (stage === "P5" || stage === "P6") return {
|
|
5806
|
+
tool: "novel_revise",
|
|
5807
|
+
args: { chapter },
|
|
5808
|
+
reason: `${detail}; fix it with novel_revise`
|
|
5809
|
+
};
|
|
5810
|
+
return {
|
|
5811
|
+
tool: step.tool,
|
|
5812
|
+
args: this.nextArgs(stage, chapter),
|
|
5813
|
+
reason: `${detail}; ${step.reason}`
|
|
5814
|
+
};
|
|
5815
|
+
}
|
|
5340
5816
|
return {
|
|
5341
5817
|
tool: step.tool,
|
|
5342
|
-
args,
|
|
5818
|
+
args: this.nextArgs(stage, chapter),
|
|
5343
5819
|
reason: step.reason
|
|
5344
5820
|
};
|
|
5345
5821
|
}
|
|
5822
|
+
/** A computed blocker for the retry budget the next operation would consume. */
|
|
5823
|
+
roundsLimitBlock(snapshot) {
|
|
5824
|
+
const stage = snapshot.pipeline.stage;
|
|
5825
|
+
if (![
|
|
5826
|
+
"P4",
|
|
5827
|
+
"P5",
|
|
5828
|
+
"P6"
|
|
5829
|
+
].includes(stage)) return null;
|
|
5830
|
+
const counter = stage === "P4" ? "review" : stage === "P5" ? "revise" : "check";
|
|
5831
|
+
if (!roundsExhausted(snapshot.pipeline.rounds, counter)) return null;
|
|
5832
|
+
return {
|
|
5833
|
+
code: "PG-11",
|
|
5834
|
+
scope: "rounds",
|
|
5835
|
+
detail: `round limit reached for ${counter} (review ${snapshot.pipeline.rounds.review}, revise ${snapshot.pipeline.rounds.revise}, check ${snapshot.pipeline.rounds.check})`,
|
|
5836
|
+
hint: "call novel_rollback with to_stage P4; the manuscript is kept for reference"
|
|
5837
|
+
};
|
|
5838
|
+
}
|
|
5839
|
+
/** Pipeline blockers plus budget exhaustion, so status/panel cannot say "all clear". */
|
|
5840
|
+
effectiveBlocking(snapshot) {
|
|
5841
|
+
const state = this.stateErrors(snapshot).map((error) => ({
|
|
5842
|
+
code: "STATE_UNREADABLE",
|
|
5843
|
+
scope: "state",
|
|
5844
|
+
detail: `${error.code}: ${error.message}`,
|
|
5845
|
+
hint: this.recoveryFor(snapshot).reason
|
|
5846
|
+
}));
|
|
5847
|
+
const extra = this.roundsLimitBlock(snapshot);
|
|
5848
|
+
const reviewDigest = this.reviewDigestProblem(snapshot);
|
|
5849
|
+
const base = [
|
|
5850
|
+
...state,
|
|
5851
|
+
...reviewDigest === null ? [] : [reviewDigest],
|
|
5852
|
+
...snapshot.pipeline.blocking
|
|
5853
|
+
];
|
|
5854
|
+
if (extra === null) return base;
|
|
5855
|
+
return base.some((item) => item.code === extra.code) ? base : [...base, extra];
|
|
5856
|
+
}
|
|
5857
|
+
/**
|
|
5858
|
+
* A review that exists but cannot be tied to the pipeline digest would let a
|
|
5859
|
+
* hand-edited review pass archive. Archive already fails closed; status and
|
|
5860
|
+
* next must surface the same repair path instead of pointing at the archive
|
|
5861
|
+
* command forever.
|
|
5862
|
+
*/
|
|
5863
|
+
reviewDigestProblem(snapshot) {
|
|
5864
|
+
if (snapshot.pipeline.stage !== "ready_to_archive" || snapshot.review === null) return null;
|
|
5865
|
+
const chapter = snapshot.pipeline.chapter;
|
|
5866
|
+
const hint = "call novel_rollback with to_stage P4, then re-run novel_review before archiving";
|
|
5867
|
+
if (snapshot.pipeline.review_digest === void 0) return {
|
|
5868
|
+
code: "REVIEW_DIGEST_MISSING",
|
|
5869
|
+
scope: "state",
|
|
5870
|
+
detail: `review ${reviewFile(chapter)} has no pipeline.review_digest; it predates digest-bound reviews`,
|
|
5871
|
+
hint
|
|
5872
|
+
};
|
|
5873
|
+
if (snapshot.pipeline.review_digest !== sha256(stableJson(snapshot.review))) return {
|
|
5874
|
+
code: "REVIEW_DIGEST_MISMATCH",
|
|
5875
|
+
scope: "state",
|
|
5876
|
+
detail: `review ${reviewFile(chapter)} changed after submission; its digest no longer matches`,
|
|
5877
|
+
hint
|
|
5878
|
+
};
|
|
5879
|
+
return null;
|
|
5880
|
+
}
|
|
5881
|
+
stateErrors(snapshot) {
|
|
5882
|
+
const errors = [];
|
|
5883
|
+
const chapter = snapshot.pipeline.chapter;
|
|
5884
|
+
if (snapshot.planError !== null) errors.push({
|
|
5885
|
+
path: planChapterFile(chapter),
|
|
5886
|
+
code: "PLAN_UNREADABLE",
|
|
5887
|
+
message: snapshot.planError
|
|
5888
|
+
});
|
|
5889
|
+
if (snapshot.reviewError !== null) errors.push({
|
|
5890
|
+
path: reviewFile(chapter),
|
|
5891
|
+
code: "REVIEW_UNREADABLE",
|
|
5892
|
+
message: snapshot.reviewError
|
|
5893
|
+
});
|
|
5894
|
+
if (snapshot.checkError !== null) errors.push({
|
|
5895
|
+
path: checkFile(chapter),
|
|
5896
|
+
code: "CHECK_UNREADABLE",
|
|
5897
|
+
message: snapshot.checkError
|
|
5898
|
+
});
|
|
5899
|
+
return errors;
|
|
5900
|
+
}
|
|
5901
|
+
/** The earliest rebuild point for the current state damage. */
|
|
5902
|
+
recoveryFor(snapshot) {
|
|
5903
|
+
const chapter = snapshot.pipeline.chapter;
|
|
5904
|
+
const errors = this.stateErrors(snapshot);
|
|
5905
|
+
const desired = snapshot.planError !== null ? "P2" : snapshot.reviewError !== null ? "P4" : "P6";
|
|
5906
|
+
const target = this.clampRollbackTarget(snapshot.pipeline.stage, desired);
|
|
5907
|
+
const action = STAGE_RANK[ROUND_TRANSITIONS[target]] < STAGE_RANK[snapshot.pipeline.stage] ? `rollback to ${target} and rebuild through the normal stage tools` : `repair the file at ${snapshot.pipeline.stage} with its normal stage tool`;
|
|
5908
|
+
return {
|
|
5909
|
+
tool: "novel_rollback",
|
|
5910
|
+
args: {
|
|
5911
|
+
chapter,
|
|
5912
|
+
to_stage: target
|
|
5913
|
+
},
|
|
5914
|
+
reason: `${errors.map((error) => `${error.code}@${error.path}`).join("; ")}; ${action}`
|
|
5915
|
+
};
|
|
5916
|
+
}
|
|
5917
|
+
/** Never point at a stage later than the current one; rollback only moves backward. */
|
|
5918
|
+
clampRollbackTarget(stage, desired) {
|
|
5919
|
+
if (STAGE_RANK[ROUND_TRANSITIONS[desired]] <= STAGE_RANK[stage]) return desired;
|
|
5920
|
+
return {
|
|
5921
|
+
idle: "P1",
|
|
5922
|
+
P2: "P2",
|
|
5923
|
+
P3: "P3",
|
|
5924
|
+
P4: "P4",
|
|
5925
|
+
P5: "P5",
|
|
5926
|
+
P6: "P6",
|
|
5927
|
+
ready_to_archive: "P6",
|
|
5928
|
+
done: "P6"
|
|
5929
|
+
}[stage];
|
|
5930
|
+
}
|
|
5931
|
+
/** State damage that makes this operation unsafe to start. */
|
|
5932
|
+
stateGateBlock(snapshot, operation) {
|
|
5933
|
+
const planSensitive = [
|
|
5934
|
+
"submitDraft",
|
|
5935
|
+
"submitReview",
|
|
5936
|
+
"submitRevision",
|
|
5937
|
+
"runCheck",
|
|
5938
|
+
"archiveChapter"
|
|
5939
|
+
];
|
|
5940
|
+
const reviewSensitive = [
|
|
5941
|
+
"submitRevision",
|
|
5942
|
+
"runCheck",
|
|
5943
|
+
"archiveChapter"
|
|
5944
|
+
];
|
|
5945
|
+
const checkSensitive = ["archiveChapter"];
|
|
5946
|
+
const relevant = [];
|
|
5947
|
+
if (snapshot.planError !== null && planSensitive.includes(operation)) relevant.push(...this.stateErrors(snapshot).filter((error) => error.code === "PLAN_UNREADABLE"));
|
|
5948
|
+
if (snapshot.reviewError !== null && reviewSensitive.includes(operation)) relevant.push(...this.stateErrors(snapshot).filter((error) => error.code === "REVIEW_UNREADABLE"));
|
|
5949
|
+
if (snapshot.checkError !== null && checkSensitive.includes(operation)) relevant.push(...this.stateErrors(snapshot).filter((error) => error.code === "CHECK_UNREADABLE"));
|
|
5950
|
+
return relevant.map((error) => ({
|
|
5951
|
+
code: "STATE_UNREADABLE",
|
|
5952
|
+
scope: "state",
|
|
5953
|
+
detail: `${error.code}: ${error.message}`,
|
|
5954
|
+
hint: this.recoveryFor(snapshot).reason
|
|
5955
|
+
}));
|
|
5956
|
+
}
|
|
5957
|
+
/** Whether the stored brief still signs the current snapshot. */
|
|
5958
|
+
briefIsCurrent(snapshot, stage) {
|
|
5959
|
+
const record = snapshot.pipeline.briefs[stage];
|
|
5960
|
+
if (record === void 0) return false;
|
|
5961
|
+
const context = this.briefContextFor(stage, snapshot, snapshot.pipeline.chapter);
|
|
5962
|
+
return record.digest === this.briefDigest(stage, snapshot, context);
|
|
5963
|
+
}
|
|
5964
|
+
/** Arguments valid for the tool `STAGE_NEXT` recommends at this stage. */
|
|
5965
|
+
nextArgs(stage, chapter) {
|
|
5966
|
+
switch (stage) {
|
|
5967
|
+
case "idle":
|
|
5968
|
+
case "P2":
|
|
5969
|
+
case "done": return {};
|
|
5970
|
+
default: return { chapter };
|
|
5971
|
+
}
|
|
5972
|
+
}
|
|
5346
5973
|
async metricsFor(snapshot, chapter) {
|
|
5347
5974
|
if (chapter === snapshot.pipeline.chapter && snapshot.text !== null) return countWordMetrics(snapshot.text);
|
|
5348
5975
|
const text = await this.readChapter(snapshot, chapter);
|
|
@@ -5360,7 +5987,8 @@ var EngineService = class {
|
|
|
5360
5987
|
}
|
|
5361
5988
|
async chaptersForExport(snapshot, chapter) {
|
|
5362
5989
|
const records = snapshot.chapters.chapters;
|
|
5363
|
-
|
|
5990
|
+
const activeDraft = chapter !== void 0 && chapter === snapshot.pipeline.chapter && snapshot.text !== null;
|
|
5991
|
+
if (chapter !== void 0 && !records.some((record) => record.num === chapter) && !activeDraft) throw new EngineInputError(ERROR_CODES.STAGE_ORDER, `chapter ${chapter} is not archived and is not the active draft`, "archive the chapter first, or pass the active chapter to export its draft");
|
|
5364
5992
|
const selected = chapter === void 0 ? records : records.filter((record) => record.num === chapter);
|
|
5365
5993
|
const chapters = [];
|
|
5366
5994
|
for (const record of selected) {
|
|
@@ -5372,7 +6000,7 @@ var EngineService = class {
|
|
|
5372
6000
|
text
|
|
5373
6001
|
});
|
|
5374
6002
|
}
|
|
5375
|
-
if (chapters.length === 0 && snapshot.text !== null && chapter === void 0) chapters.push({
|
|
6003
|
+
if (chapters.length === 0 && snapshot.text !== null && (chapter === void 0 || chapter === snapshot.pipeline.chapter)) chapters.push({
|
|
5376
6004
|
num: snapshot.pipeline.chapter,
|
|
5377
6005
|
title: snapshot.plan?.title ?? "未命名",
|
|
5378
6006
|
text: snapshot.text
|
|
@@ -5419,8 +6047,11 @@ var EngineService = class {
|
|
|
5419
6047
|
timeline: snapshot.timeline,
|
|
5420
6048
|
relationships: snapshot.relationships,
|
|
5421
6049
|
plan: snapshot.plan,
|
|
6050
|
+
plan_error: snapshot.planError,
|
|
5422
6051
|
review: snapshot.review,
|
|
6052
|
+
review_error: snapshot.reviewError,
|
|
5423
6053
|
check: snapshot.check,
|
|
6054
|
+
check_error: snapshot.checkError,
|
|
5424
6055
|
text: snapshot.text,
|
|
5425
6056
|
machine,
|
|
5426
6057
|
previousText: snapshot.previousText,
|
|
@@ -5435,6 +6066,7 @@ var EngineService = class {
|
|
|
5435
6066
|
chapter: snapshot.pipeline.chapter,
|
|
5436
6067
|
pipelineStage: snapshot.pipeline.stage,
|
|
5437
6068
|
rounds: snapshot.pipeline.rounds,
|
|
6069
|
+
min_score: this.config.quality.minScore,
|
|
5438
6070
|
rulepack: `${this.rulepack.name}@${this.rulepack.version}`,
|
|
5439
6071
|
context
|
|
5440
6072
|
}));
|
|
@@ -5455,6 +6087,8 @@ var EngineService = class {
|
|
|
5455
6087
|
async assertGates(snapshot, operation, extras) {
|
|
5456
6088
|
const text = extras.text ?? snapshot.text ?? "";
|
|
5457
6089
|
if (!canRun(snapshot.pipeline.stage, operation)) throw new EngineInputError(ERROR_CODES.STAGE_ORDER, `${operation} is not allowed at stage ${snapshot.pipeline.stage}`, "call novel_status to see the next legal step");
|
|
6090
|
+
const stateBlocking = this.stateGateBlock(snapshot, operation);
|
|
6091
|
+
if (stateBlocking.length > 0) throw new EngineBlockedError(ERROR_CODES.GATE_BLOCKED, `${operation} blocked by unreadable state: ${stateBlocking[0].detail}`, stateBlocking, this.recoveryFor(snapshot).reason);
|
|
5458
6092
|
const archive = operation === "archiveChapter" ? {
|
|
5459
6093
|
chapterExists: snapshot.chapters.chapters.some((record) => record.num === snapshot.pipeline.chapter),
|
|
5460
6094
|
checkPassed: snapshot.check?.verdict === "pass",
|
|
@@ -5478,6 +6112,7 @@ var EngineService = class {
|
|
|
5478
6112
|
check: extras.check ?? snapshot.check,
|
|
5479
6113
|
semantic: extras.semantic ?? [],
|
|
5480
6114
|
proof: extras.proof ?? null,
|
|
6115
|
+
scope: extras.scope,
|
|
5481
6116
|
rounds: snapshot.pipeline.rounds,
|
|
5482
6117
|
archive,
|
|
5483
6118
|
evidence: {
|
|
@@ -5509,8 +6144,11 @@ var EngineService = class {
|
|
|
5509
6144
|
throw new EngineBlockedError(result.blocking.every((item) => item.code === "PG-09") ? ERROR_CODES.READING_PROOF_INVALID : ERROR_CODES.GATE_BLOCKED, `${operation} blocked by ${result.blocking.length} gate(s), first: ${first.code} ${first.detail}`, result.blocking, result.blocking.map((item) => `${item.code}: ${item.hint ?? item.detail}`).join(" | "));
|
|
5510
6145
|
}
|
|
5511
6146
|
}
|
|
5512
|
-
|
|
6147
|
+
assertChapterParameter(chapter) {
|
|
5513
6148
|
if (!Number.isInteger(chapter) || chapter < 1) throw new EngineInputError(ERROR_CODES.SCHEMA_INVALID, `chapter must be a positive integer (got ${String(chapter)})`);
|
|
6149
|
+
}
|
|
6150
|
+
assertChapter(snapshot, chapter) {
|
|
6151
|
+
this.assertChapterParameter(chapter);
|
|
5514
6152
|
if (chapter !== snapshot.pipeline.chapter) throw new EngineInputError(ERROR_CODES.STAGE_ORDER, `chapter ${chapter} is not the active chapter (${snapshot.pipeline.chapter})`, "archive the active chapter before starting the next one");
|
|
5515
6153
|
}
|
|
5516
6154
|
async load(root, retries = 1) {
|
|
@@ -5554,9 +6192,22 @@ var EngineService = class {
|
|
|
5554
6192
|
if (previous !== null) previousTexts.push(previous);
|
|
5555
6193
|
}
|
|
5556
6194
|
const planRaw = await store.readText(planChapterFile(chapter));
|
|
5557
|
-
|
|
5558
|
-
|
|
5559
|
-
|
|
6195
|
+
let plan = null;
|
|
6196
|
+
let planError = null;
|
|
6197
|
+
if (planRaw !== null) try {
|
|
6198
|
+
const parsed = parsePlanYaml(planRaw, planChapterFile(chapter));
|
|
6199
|
+
validateStoredChapterPlan(parsed, planChapterFile(chapter));
|
|
6200
|
+
plan = parsed;
|
|
6201
|
+
} catch (error) {
|
|
6202
|
+
if (error instanceof EngineFaultError && error.code === ERROR_CODES.SCHEMA_TOO_NEW) throw error;
|
|
6203
|
+
planError = errorMessage(error);
|
|
6204
|
+
}
|
|
6205
|
+
const reviewRead = await this.recoverableDoc(store, reviewFile(chapter), "review");
|
|
6206
|
+
const checkRead = await this.recoverableDoc(store, checkFile(chapter), "check");
|
|
6207
|
+
const review = reviewRead.value;
|
|
6208
|
+
const reviewError = reviewRead.error;
|
|
6209
|
+
const check = checkRead.value;
|
|
6210
|
+
const checkError = checkRead.error;
|
|
5560
6211
|
const latest = await store.readDoc(joinRel(STATE_DIR, STATE_FILES.pipeline), "pipeline");
|
|
5561
6212
|
if (latest === null || latest.revision !== pipeline.revision) {
|
|
5562
6213
|
if (retries > 0) return this.load(store.root, retries - 1);
|
|
@@ -5579,10 +6230,28 @@ var EngineService = class {
|
|
|
5579
6230
|
previousText,
|
|
5580
6231
|
previousTexts,
|
|
5581
6232
|
plan,
|
|
6233
|
+
planError,
|
|
5582
6234
|
review,
|
|
5583
|
-
|
|
6235
|
+
reviewError,
|
|
6236
|
+
check,
|
|
6237
|
+
checkError
|
|
5584
6238
|
};
|
|
5585
6239
|
}
|
|
6240
|
+
/** Read a review/check without letting one corrupt file wedge every read. */
|
|
6241
|
+
async recoverableDoc(store, path, kind) {
|
|
6242
|
+
try {
|
|
6243
|
+
return {
|
|
6244
|
+
value: await store.readDoc(path, kind),
|
|
6245
|
+
error: null
|
|
6246
|
+
};
|
|
6247
|
+
} catch (error) {
|
|
6248
|
+
if (error instanceof EngineFaultError && error.code === ERROR_CODES.SCHEMA_TOO_NEW) throw error;
|
|
6249
|
+
return {
|
|
6250
|
+
value: null,
|
|
6251
|
+
error: errorMessage(error)
|
|
6252
|
+
};
|
|
6253
|
+
}
|
|
6254
|
+
}
|
|
5586
6255
|
async mutate(options) {
|
|
5587
6256
|
const root = await this.resolveRoot(options.root);
|
|
5588
6257
|
const signal = this.signalContext.getStore();
|
|
@@ -5739,8 +6408,11 @@ var EngineService = class {
|
|
|
5739
6408
|
previousText: null,
|
|
5740
6409
|
previousTexts: [],
|
|
5741
6410
|
plan: null,
|
|
6411
|
+
planError: null,
|
|
5742
6412
|
review: null,
|
|
5743
|
-
|
|
6413
|
+
reviewError: null,
|
|
6414
|
+
check: null,
|
|
6415
|
+
checkError: null
|
|
5744
6416
|
};
|
|
5745
6417
|
}
|
|
5746
6418
|
};
|
|
@@ -5753,6 +6425,17 @@ const NEXT_TOOL_BY_BRIEF = {
|
|
|
5753
6425
|
P6: "novel_check",
|
|
5754
6426
|
P8: "novel_archive"
|
|
5755
6427
|
};
|
|
6428
|
+
/** The task book each stage must see before its next write operation. */
|
|
6429
|
+
const STATUS_BRIEF = {
|
|
6430
|
+
idle: "P1",
|
|
6431
|
+
P2: "P2",
|
|
6432
|
+
P3: "P3",
|
|
6433
|
+
P4: "P4",
|
|
6434
|
+
P5: "P5",
|
|
6435
|
+
P6: "P6",
|
|
6436
|
+
ready_to_archive: "P8",
|
|
6437
|
+
done: "P1"
|
|
6438
|
+
};
|
|
5756
6439
|
const yaml = (value) => stringify(value, { lineWidth: 0 });
|
|
5757
6440
|
const emptyChapters = () => ({
|
|
5758
6441
|
schema_version: 1,
|
|
@@ -5778,6 +6461,7 @@ const pipelineWith = (snapshot, revision, patch) => ({
|
|
|
5778
6461
|
rounds: snapshot.pipeline.rounds,
|
|
5779
6462
|
briefs: snapshot.pipeline.briefs,
|
|
5780
6463
|
blocking: snapshot.pipeline.blocking,
|
|
6464
|
+
review_digest: snapshot.pipeline.review_digest,
|
|
5781
6465
|
last_action: snapshot.pipeline.last_action,
|
|
5782
6466
|
...patch
|
|
5783
6467
|
});
|
|
@@ -5817,7 +6501,65 @@ const stableJson = (value) => {
|
|
|
5817
6501
|
};
|
|
5818
6502
|
return JSON.stringify(normalize(value));
|
|
5819
6503
|
};
|
|
5820
|
-
const
|
|
6504
|
+
const errorMessage = (error) => error instanceof Error ? `${error.message}${error.cause instanceof Error ? `: ${error.cause.message}` : ""}` : String(error);
|
|
6505
|
+
/** Validate the shape a stored chapter plan must have before any stage uses it. */
|
|
6506
|
+
const validateStoredChapterPlan = (plan, path) => {
|
|
6507
|
+
const fail = (detail) => {
|
|
6508
|
+
throw new EngineFaultError(ERROR_CODES.SCHEMA_INVALID, `${path} is not a usable chapter plan: ${detail}`);
|
|
6509
|
+
};
|
|
6510
|
+
const rawVersion = plan.schema_version;
|
|
6511
|
+
if (rawVersion !== void 0) {
|
|
6512
|
+
const version = typeof rawVersion === "number" ? rawVersion : null;
|
|
6513
|
+
if (version === null) fail("schema_version must be a positive integer");
|
|
6514
|
+
const checkedVersion = version;
|
|
6515
|
+
if (!Number.isInteger(checkedVersion) || checkedVersion < 1) fail("schema_version must be a positive integer");
|
|
6516
|
+
if (checkedVersion > 1) throw new EngineFaultError(ERROR_CODES.SCHEMA_TOO_NEW, `${path} uses schema_version ${checkedVersion}, newer than supported 1`, "upgrade the plugin; the plan was left untouched");
|
|
6517
|
+
if (checkedVersion !== 1) fail(`schema_version must be 1`);
|
|
6518
|
+
}
|
|
6519
|
+
if (!Number.isInteger(plan.chapter) || plan.chapter < 1) fail("chapter must be a positive integer");
|
|
6520
|
+
for (const key of [
|
|
6521
|
+
"title",
|
|
6522
|
+
"narrator",
|
|
6523
|
+
"opening",
|
|
6524
|
+
"ending"
|
|
6525
|
+
]) if (typeof plan[key] !== "string" || plan[key].trim() === "") fail(`${key} must be non-empty`);
|
|
6526
|
+
if (!SCENE_TYPES.includes(plan.scene_type)) fail(`unknown scene_type "${String(plan.scene_type)}"`);
|
|
6527
|
+
if (!Array.isArray(plan.beats) || plan.beats.some((beat) => typeof beat !== "string")) fail("beats must be a string array");
|
|
6528
|
+
if (!Array.isArray(plan.target_words) || plan.target_words.length !== 2 || plan.target_words.some((value) => !Number.isInteger(value) || value <= 0)) fail("target_words must be [min, max] positive integers");
|
|
6529
|
+
if (plan.summary !== void 0 && (typeof plan.summary !== "object" || plan.summary === null)) fail("summary must be an object when present");
|
|
6530
|
+
};
|
|
6531
|
+
const CHECK_SCOPES = ["content", "integrity"];
|
|
6532
|
+
const normalizeCheckScope = (value) => {
|
|
6533
|
+
if (value === void 0) return [...CHECK_SCOPES];
|
|
6534
|
+
if (!Array.isArray(value) || value.length === 0) throw new EngineInputError(ERROR_CODES.SCHEMA_INVALID, "scope must contain at least one of content, integrity");
|
|
6535
|
+
const unique = [...new Set(value)];
|
|
6536
|
+
for (const scope of unique) if (scope !== "content" && scope !== "integrity") throw new EngineInputError(ERROR_CODES.SCHEMA_INVALID, `unknown check scope "${String(scope)}"`, `allowed: ${CHECK_SCOPES.join(", ")}`);
|
|
6537
|
+
return CHECK_SCOPES.filter((scope) => unique.includes(scope));
|
|
6538
|
+
};
|
|
6539
|
+
const stringList = (value, field) => {
|
|
6540
|
+
if (value === void 0) return [];
|
|
6541
|
+
if (!Array.isArray(value) || value.some((item) => typeof item !== "string" || item.trim() === "")) throw new EngineInputError(ERROR_CODES.SCHEMA_INVALID, `${field} must be an array of non-empty strings`);
|
|
6542
|
+
return value;
|
|
6543
|
+
};
|
|
6544
|
+
const objectList = (value, field) => {
|
|
6545
|
+
if (value === void 0) return [];
|
|
6546
|
+
if (!Array.isArray(value) || value.some((item) => typeof item !== "object" || item === null || Array.isArray(item))) throw new EngineInputError(ERROR_CODES.SCHEMA_INVALID, `${field} must be an array of objects`);
|
|
6547
|
+
return value;
|
|
6548
|
+
};
|
|
6549
|
+
const requireText = (object, key, path) => {
|
|
6550
|
+
if (typeof object[key] !== "string" || object[key].trim() === "") throw new EngineInputError(ERROR_CODES.SCHEMA_INVALID, `${path} must be a non-empty string`);
|
|
6551
|
+
};
|
|
6552
|
+
/** Parse a plan file with a machine code instead of leaking a YAML parser error. */
|
|
6553
|
+
const parsePlanYaml = (text, path) => {
|
|
6554
|
+
let parsed;
|
|
6555
|
+
try {
|
|
6556
|
+
parsed = parse(text);
|
|
6557
|
+
} catch (error) {
|
|
6558
|
+
throw new EngineFaultError(ERROR_CODES.SCHEMA_INVALID, `${path} is not valid YAML: ${error instanceof Error ? error.message : String(error)}`, "fix the plan YAML; the engine will not silently discard a corrupt plan", { cause: error });
|
|
6559
|
+
}
|
|
6560
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) throw new EngineFaultError(ERROR_CODES.SCHEMA_INVALID, `${path} must be a YAML mapping`, "rewrite the plan as a mapping (title, scene_type, narrator, ...)");
|
|
6561
|
+
return parsed;
|
|
6562
|
+
};
|
|
5821
6563
|
/** Line diff with a shared prefix/suffix, enough for revision review. */
|
|
5822
6564
|
function diffLines(before, after) {
|
|
5823
6565
|
const left = before === "" ? [] : before.split("\n");
|
|
@@ -5867,7 +6609,10 @@ const asJson = (value, field) => {
|
|
|
5867
6609
|
};
|
|
5868
6610
|
/** Turn an engine failure into the message shape tools surface. */
|
|
5869
6611
|
const fail = (error) => {
|
|
5870
|
-
if (error instanceof EngineError)
|
|
6612
|
+
if (error instanceof EngineError) {
|
|
6613
|
+
const blocking = error instanceof EngineBlockedError && error.blocking.length > 0 ? `\nblocking: ${JSON.stringify(error.blocking)}` : "";
|
|
6614
|
+
throw new Error(`[${error.code}] ${error.message}${error.hint === void 0 ? "" : ` — ${error.hint}`}${blocking}`);
|
|
6615
|
+
}
|
|
5871
6616
|
throw error;
|
|
5872
6617
|
};
|
|
5873
6618
|
/** Wrap an execute body with cancellation and a machine-code error. */
|
|
@@ -5921,7 +6666,7 @@ const stringArray = {
|
|
|
5921
6666
|
};
|
|
5922
6667
|
const root = {
|
|
5923
6668
|
type: "string",
|
|
5924
|
-
description: "Project root
|
|
6669
|
+
description: "Project root. When omitted, resolves to the last active project, then the registry active root, then the configured projectRoot; pass it explicitly when several projects exist."
|
|
5925
6670
|
};
|
|
5926
6671
|
/**
|
|
5927
6672
|
* The eighteen tools are thin adapters: narrow parameters, call one service
|
|
@@ -5980,8 +6725,11 @@ function createNovelTools(engine) {
|
|
|
5980
6725
|
schema: { type: "json" },
|
|
5981
6726
|
render: (_args, raw) => {
|
|
5982
6727
|
const value = payload(raw);
|
|
6728
|
+
const active = value.active_chapter ?? value.chapter;
|
|
6729
|
+
const where = active === value.chapter ? `第 ${value.chapter} 章` : `当前第 ${active} 章 · 查询第 ${value.chapter} 章字数`;
|
|
5983
6730
|
return text([
|
|
5984
|
-
`《${value.project.title}》
|
|
6731
|
+
`《${value.project.title}》 ${where} · ${value.stage}`,
|
|
6732
|
+
...value.rounds === void 0 ? [] : [`轮次:评审 ${value.rounds.review} / 修订 ${value.rounds.revise} / 校验 ${value.rounds.check}`],
|
|
5985
6733
|
value.blocking.length === 0 ? "门控:全部通过" : `门控:${value.blocking.length} 条阻塞`,
|
|
5986
6734
|
...value.blocking.map((item) => ` - ${item.code}: ${item.detail}`),
|
|
5987
6735
|
`下一步:${value.next.tool} — ${value.next.reason}`
|
|
@@ -6017,7 +6765,7 @@ function createNovelTools(engine) {
|
|
|
6017
6765
|
schema: { type: "json" },
|
|
6018
6766
|
render: (_args, raw) => {
|
|
6019
6767
|
const value = payload(raw);
|
|
6020
|
-
return text(`${value.content}\n\n---\nbrief digest: ${value.digest}${value.context === void 0 ? "" : `\ncontext:\n${JSON.stringify(value.context
|
|
6768
|
+
return text(`${value.content}\n\n---\nbrief digest: ${value.digest}${value.fields === void 0 ? "" : `\nfields:\n${JSON.stringify(value.fields)}`}${value.context === void 0 ? "" : `\ncontext:\n${JSON.stringify(value.context)}`}`);
|
|
6021
6769
|
}
|
|
6022
6770
|
},
|
|
6023
6771
|
execute: guarded(async (args) => engine.getBrief({
|
|
@@ -6440,6 +7188,11 @@ function createNovelTools(engine) {
|
|
|
6440
7188
|
];
|
|
6441
7189
|
}
|
|
6442
7190
|
// src/commands/admin.ts
|
|
7191
|
+
/** Registry commands need an explicit root; do not pass undefined into path.resolve. */
|
|
7192
|
+
const rootArg = (args) => {
|
|
7193
|
+
if (typeof args.root !== "string" || args.root.trim() === "") throw new EngineInputError(ERROR_CODES.SCHEMA_INVALID, "registry operation requires a non-empty root", "pass {\"root\":\"/absolute/project/path\"}");
|
|
7194
|
+
return args.root;
|
|
7195
|
+
};
|
|
6443
7196
|
/**
|
|
6444
7197
|
* The ten tools hidden when `tools.mode=core`, plus the explicit
|
|
6445
7198
|
* registry/settings-pack operations that are command-only by design (§6.8).
|
|
@@ -6471,9 +7224,9 @@ const OPERATIONS$1 = {
|
|
|
6471
7224
|
novel_pipeline: (engine, args) => engine.advise(args),
|
|
6472
7225
|
novel_state: (engine, args) => engine.queryState(args),
|
|
6473
7226
|
novel_registry_list: (engine) => engine.listProjects(),
|
|
6474
|
-
novel_registry_register: (engine, args) => engine.registerProject({ root: args
|
|
6475
|
-
novel_registry_activate: (engine, args) => engine.setActiveProject({ root: args
|
|
6476
|
-
novel_registry_forget: (engine, args) => engine.forgetProject({ root: args
|
|
7227
|
+
novel_registry_register: (engine, args) => engine.registerProject({ root: rootArg(args) }),
|
|
7228
|
+
novel_registry_activate: (engine, args) => engine.setActiveProject({ root: rootArg(args) }),
|
|
7229
|
+
novel_registry_forget: (engine, args) => engine.forgetProject({ root: rootArg(args) }),
|
|
6477
7230
|
novel_import: (engine, args) => engine.importPack(args)
|
|
6478
7231
|
};
|
|
6479
7232
|
const ADMIN_OPERATION_NAMES = Object.keys(OPERATIONS$1);
|
|
@@ -6500,7 +7253,7 @@ function parseAdminArguments(raw, operation) {
|
|
|
6500
7253
|
}
|
|
6501
7254
|
/** Run one hidden operation and render its payload for the command result. */
|
|
6502
7255
|
async function runAdminOperation(engine, operation, rawArguments) {
|
|
6503
|
-
const handler = OPERATIONS$1[operation];
|
|
7256
|
+
const handler = Object.hasOwn(OPERATIONS$1, operation) ? OPERATIONS$1[operation] : void 0;
|
|
6504
7257
|
if (handler === void 0) throw new EngineInputError(ERROR_CODES.CONFIG_INVALID, `未知子命令 "${operation}"`, `available: ${ADMIN_OPERATION_NAMES.join(", ")}`);
|
|
6505
7258
|
const parsed = parseAdminArguments(rawArguments, operation);
|
|
6506
7259
|
const result = await handler(engine, parsed.root === void 0 && typeof parsed.project_root === "string" ? {
|
|
@@ -7200,6 +7953,7 @@ function apply(ctx, config) {
|
|
|
7200
7953
|
ctx.emit?.(event.name, event.payload);
|
|
7201
7954
|
};
|
|
7202
7955
|
const engine = new EngineService(engineConfig, () => loadRulepack(), { onEvent: emit });
|
|
7956
|
+
ctx.effect(() => () => engine.dispose(), `${name}: engine dispose`);
|
|
7203
7957
|
ctx.provide?.("novelEngine", engine);
|
|
7204
7958
|
for (const tool of createNovelTools(engine)) {
|
|
7205
7959
|
if (toolsMode === "core" && !CORE_TOOLS.includes(tool.name)) continue;
|
|
@@ -7241,4 +7995,4 @@ function connectionFence(scope) {
|
|
|
7241
7995
|
}
|
|
7242
7996
|
if (Object.prototype.hasOwnProperty.call(candidate, "connection")) return candidate.connection;
|
|
7243
7997
|
}
|
|
7244
|
-
export { BriefLibrary, Config, ERROR_CODES, EngineBlockedError, EngineError, EngineFaultError, EngineInputError, EngineService, GENRES, OPERATIONS, PLATFORMS, SCENE_TYPES, SCHEMA_VERSION, applicableGates, apply, blockedError, briefStageFor, chapterHeading, evaluateGates, faultError, gateName, inject, inputError, loadRulepack, name, parseParagraphLocation, phaseStrip, projectChapter, projectChapters, projectGates,
|
|
7998
|
+
export { BriefLibrary, Config, ERROR_CODES, EngineBlockedError, EngineError, EngineFaultError, EngineInputError, EngineService, GENRES, OPERATIONS, PLATFORMS, SCENE_TYPES, SCHEMA_VERSION, applicableGates, apply, blockedError, briefStageFor, chapterHeading, evaluateGates, faultError, gateName, inject, inputError, loadRulepack, name, parseParagraphLocation, phaseStrip, projectChapter, projectChapters, projectGates, projectSummary, renderBlurb, renderCover, renderEpubChapter, renderMarkdown, renderPlainText, verifyReference };
|