@cloud411716/fancy-webnovel 1.0.46 → 3.0.0
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/client.js +369 -0
- package/cordis.patch.yml +4 -13
- package/docs/douyin-plugin.md +118 -0
- package/docs/material-library/README.md +404 -0
- package/docs/material-library/import.js +437 -0
- package/docs/material-library/prompt.md +370 -0
- package/docs/material-library/queries.surql +94 -0
- package/docs/material-library/sample.jsonl +14 -0
- package/docs/material-library/schema.surql +46 -0
- package/docs/mcp-profile.example.yml +24 -0
- package/docs/story-graph-design.md +602 -0
- package/docs/story-graph-validation.md +81 -0
- package/docs/workspace-mcp.md +284 -0
- package/index.js +26 -38
- package/lib/atomic-write.js +109 -0
- package/lib/chapter-order.js +100 -0
- package/lib/chinese-num.js +111 -0
- package/lib/confirmation.js +80 -0
- package/lib/db-init.js +123 -0
- package/lib/deployed-state.js +141 -0
- package/lib/direction-labels.js +265 -0
- package/lib/error-codes.js +104 -0
- package/lib/infra.js +594 -0
- package/lib/llm-wiki.js +109 -0
- package/lib/material-db.js +149 -0
- package/lib/path-guard.js +150 -0
- package/lib/projects.js +400 -0
- package/lib/scraper-utils.js +305 -0
- package/lib/surreal-server.js +338 -0
- package/lib/topic-decision.js +860 -0
- package/lib/topic-fields.js +251 -0
- package/lib/topic-schema.js +225 -0
- package/lib/workspace.js +501 -0
- package/package.json +62 -5
- package/plugins/fancy-analyze/analyze-definitions.md +519 -0
- package/plugins/fancy-analyze/command-input.js +65 -0
- package/plugins/fancy-analyze/index.js +2024 -0
- package/plugins/fancy-analyze/ingest-coverage.js +107 -0
- package/plugins/fancy-analyze/schema-analyze.md +627 -0
- package/plugins/fancy-analyze/schema-write.md +178 -0
- package/plugins/fancy-analyze/scripts/append-summary-chapter.js +612 -0
- package/plugins/fancy-analyze/scripts/split_source.js +292 -0
- package/plugins/fancy-analyze/summary-progress.js +495 -0
- package/plugins/fancy-analyze/templates.js +1179 -0
- package/plugins/fancy-bootstrap/index.js +313 -0
- package/plugins/fancy-bootstrap/scripts/init.js +177 -0
- package/plugins/fancy-bootstrap/scripts/path-utils.js +21 -0
- package/plugins/fancy-bootstrap/templates.js +71 -0
- package/plugins/fancy-db/index.js +172 -0
- package/plugins/fancy-douyin/deploy.js +134 -0
- package/plugins/fancy-douyin/index.js +551 -0
- package/plugins/fancy-douyin/prompts/01-analysis-prompt.md +84 -0
- package/plugins/fancy-douyin/scripts/crawl-note.cjs +194 -0
- package/plugins/fancy-douyin/scripts/lib.cjs +197 -0
- package/plugins/fancy-douyin/scripts/scan-account.cjs +128 -0
- package/plugins/fancy-douyin/scripts/start-chrome.cjs +203 -0
- package/plugins/fancy-douyin/state.js +197 -0
- package/plugins/fancy-douyin/templates.js +127 -0
- package/plugins/fancy-export/exporter.js +805 -0
- package/plugins/fancy-export/index.js +63 -0
- package/plugins/fancy-import/import-core.js +1118 -0
- package/plugins/fancy-import/import-driver.js +604 -0
- package/plugins/fancy-import/index.js +738 -0
- package/plugins/fancy-import/prompts/01-import-prompt.md +131 -0
- package/plugins/fancy-import/templates.js +58 -0
- package/plugins/fancy-material/exporter.js +153 -0
- package/plugins/fancy-material/genres.js +256 -0
- package/plugins/fancy-material/index.js +835 -0
- package/plugins/fancy-material/inventory.js +143 -0
- package/plugins/fancy-material/kinds.js +313 -0
- package/plugins/fancy-material/queue.js +55 -0
- package/plugins/fancy-material/scripts/query.js +254 -0
- package/plugins/fancy-material/templates.js +258 -0
- package/plugins/fancy-material/tool-deploy.js +138 -0
- package/plugins/fancy-material/validate.js +149 -0
- package/plugins/fancy-project/index.js +133 -0
- package/plugins/fancy-scan/existing-block.js +100 -0
- package/plugins/fancy-scan/index.js +708 -0
- package/plugins/fancy-scan/prompts/01-analysis-prompt.md +302 -0
- package/plugins/fancy-scan/scripts/append-block.js +725 -0
- package/plugins/fancy-scan/scripts/report-validate.js +824 -0
- package/plugins/fancy-scan/scripts/run-scan.js +667 -0
- package/plugins/fancy-scan/scripts/scrapers/fanqie-rank-scraper.js +923 -0
- package/plugins/fancy-scan/scripts/scrapers/jjwxc-rank-scraper.js +469 -0
- package/plugins/fancy-scan/scripts/scrapers/qimao-rank-scraper.js +629 -0
- package/plugins/fancy-scan/templates/blocks.js +155 -0
- package/plugins/fancy-scan/templates/platforms.js +372 -0
- package/plugins/fancy-scan/templates/report-schema.js +90 -0
- package/plugins/fancy-scan/templates.js +694 -0
- package/plugins/fancy-scan/tool-deploy.js +335 -0
- package/plugins/fancy-scan/wait-turn.js +46 -0
- package/plugins/fancy-story-builder/db/README.md +106 -0
- package/plugins/fancy-story-builder/db/client.js +228 -0
- package/plugins/fancy-story-builder/db/commands.js +145 -0
- package/plugins/fancy-story-builder/db/ids.js +39 -0
- package/plugins/fancy-story-builder/db/import.js +798 -0
- package/plugins/fancy-story-builder/db/project.js +299 -0
- package/plugins/fancy-story-builder/db/query.js +361 -0
- package/plugins/fancy-story-builder/db/schema.js +417 -0
- package/plugins/fancy-story-builder/db/stage-sync.js +238 -0
- package/plugins/fancy-story-builder/deploy-manifest.js +239 -0
- package/plugins/fancy-story-builder/deslop-driver.js +821 -0
- package/plugins/fancy-story-builder/flesh-output.js +168 -0
- package/plugins/fancy-story-builder/index.js +6861 -0
- package/plugins/fancy-story-builder/patch-driver.js +390 -0
- package/plugins/fancy-story-builder/prompts/01-skeleton-prompt.md +563 -0
- package/plugins/fancy-story-builder/prompts/02-flesh-prompt.md +602 -0
- package/plugins/fancy-story-builder/prompts/03-rhythm-prompt.md +539 -0
- package/plugins/fancy-story-builder/prompts/04-writing-prompt.md +225 -0
- package/plugins/fancy-story-builder/prompts/05-deslop-prompt.md +96 -0
- package/plugins/fancy-story-builder/purpose-template.md +83 -0
- package/plugins/fancy-story-builder/references/formulas-rhythm.json +302 -0
- package/plugins/fancy-story-builder/references/formulas-skeleton.json +402 -0
- package/plugins/fancy-story-builder/references/genre-profiles.json +3514 -0
- package/plugins/fancy-story-builder/references/glossary.md +594 -0
- package/plugins/fancy-story-builder/references/index.md +581 -0
- package/plugins/fancy-story-builder/references//345/206/231/344/275/234/346/214/207/345/257/274/00-index.md +174 -0
- package/plugins/fancy-story-builder/references//345/206/231/344/275/234/346/214/207/345/257/274/01-scene.md +150 -0
- package/plugins/fancy-story-builder/references//345/206/231/344/275/234/346/214/207/345/257/274/02-chapter-workflow.md +99 -0
- package/plugins/fancy-story-builder/references//345/206/231/344/275/234/346/214/207/345/257/274/03-opening-chapter.md +104 -0
- package/plugins/fancy-story-builder/references//345/206/231/344/275/234/346/214/207/345/257/274/04-dialogue-mastery.md +274 -0
- package/plugins/fancy-story-builder/references//345/206/231/344/275/234/346/214/207/345/257/274/05-genre-formulas.md +432 -0
- package/plugins/fancy-story-builder/references//345/206/231/344/275/234/346/214/207/345/257/274/06-tone-ensemble.md +259 -0
- package/plugins/fancy-story-builder/references//345/206/231/344/275/234/346/214/207/345/257/274/07-ledger-audit.md +222 -0
- package/plugins/fancy-story-builder/references//345/206/231/344/275/234/346/214/207/345/257/274/08-diagnosis-revision.md +205 -0
- package/plugins/fancy-story-builder/references//345/206/231/344/275/234/346/214/207/345/257/274/09-reading-power.md +135 -0
- package/plugins/fancy-story-builder/references//345/206/231/344/275/234/346/214/207/345/257/274/10-deslop.md +129 -0
- package/plugins/fancy-story-builder/references//350/212/202/345/245/217/346/214/207/345/257/274/00-index.md +98 -0
- package/plugins/fancy-story-builder/references//350/212/202/345/245/217/346/214/207/345/257/274/01-upgrade-rhythm.md +300 -0
- package/plugins/fancy-story-builder/references//350/212/202/345/245/217/346/214/207/345/257/274/02-arc-rhythm.md +168 -0
- package/plugins/fancy-story-builder/references//350/212/202/345/245/217/346/214/207/345/257/274/03-highpoint-hooks.md +189 -0
- package/plugins/fancy-story-builder/references//350/212/202/345/245/217/346/214/207/345/257/274/04-satisfaction.md +226 -0
- package/plugins/fancy-story-builder/references//350/212/202/345/245/217/346/214/207/345/257/274/05-foreshadow-density.md +195 -0
- package/plugins/fancy-story-builder/references//350/212/202/345/245/217/346/214/207/345/257/274/06-chapter-outline.md +226 -0
- package/plugins/fancy-story-builder/references//350/212/202/345/245/217/346/214/207/345/257/274/07-audit.md +181 -0
- package/plugins/fancy-story-builder/references//350/241/200/350/202/211/346/214/207/345/257/274/00-index.md +100 -0
- package/plugins/fancy-story-builder/references//350/241/200/350/202/211/346/214/207/345/257/274/01-character.md +243 -0
- package/plugins/fancy-story-builder/references//350/241/200/350/202/211/346/214/207/345/257/274/02-worldbuilding.md +245 -0
- package/plugins/fancy-story-builder/references//350/241/200/350/202/211/346/214/207/345/257/274/03-relations.md +185 -0
- package/plugins/fancy-story-builder/references//350/241/200/350/202/211/346/214/207/345/257/274/04-emotion-methods.md +286 -0
- package/plugins/fancy-story-builder/references//350/241/200/350/202/211/346/214/207/345/257/274/05-audit.md +187 -0
- package/plugins/fancy-story-builder/references//351/252/250/346/236/266/346/214/207/345/257/274/00-index.md +91 -0
- package/plugins/fancy-story-builder/references//351/252/250/346/236/266/346/214/207/345/257/274/01-positioning.md +209 -0
- package/plugins/fancy-story-builder/references//351/252/250/346/236/266/346/214/207/345/257/274/02-packaging.md +135 -0
- package/plugins/fancy-story-builder/references//351/252/250/346/236/266/346/214/207/345/257/274/03-entities.md +305 -0
- package/plugins/fancy-story-builder/references//351/252/250/346/236/266/346/214/207/345/257/274/04-conflict.md +328 -0
- package/plugins/fancy-story-builder/references//351/252/250/346/236/266/346/214/207/345/257/274/05-quota.md +187 -0
- package/plugins/fancy-story-builder/references//351/252/250/346/236/266/346/214/207/345/257/274/06-audit.md +160 -0
- package/plugins/fancy-story-builder/rhythm-output.js +140 -0
- package/plugins/fancy-story-builder/schema-setting.json +1100 -0
- package/plugins/fancy-story-builder/schema-setting.md +232 -0
- package/plugins/fancy-story-builder/scripts/ai-patterns.js +619 -0
- package/plugins/fancy-story-builder/scripts/brief-validate.js +164 -0
- package/plugins/fancy-story-builder/scripts/check-guide-compliance.js +289 -0
- package/plugins/fancy-story-builder/scripts/content-quality.js +416 -0
- package/plugins/fancy-story-builder/scripts/deslop-validate.js +143 -0
- package/plugins/fancy-story-builder/scripts/error-classes.js +112 -0
- package/plugins/fancy-story-builder/scripts/flesh-validate.js +1312 -0
- package/plugins/fancy-story-builder/scripts/formula-engine.js +112 -0
- package/plugins/fancy-story-builder/scripts/graph-utils.js +1484 -0
- package/plugins/fancy-story-builder/scripts/label-coverage.js +171 -0
- package/plugins/fancy-story-builder/scripts/prompt-drift-check.js +441 -0
- package/plugins/fancy-story-builder/scripts/reading-power.js +298 -0
- package/plugins/fancy-story-builder/scripts/reference-log.js +110 -0
- package/plugins/fancy-story-builder/scripts/rhythm-validate.js +1312 -0
- package/plugins/fancy-story-builder/scripts/schema-pages.js +2668 -0
- package/plugins/fancy-story-builder/scripts/semantic-lint.js +381 -0
- package/plugins/fancy-story-builder/scripts/skeleton-quota.js +145 -0
- package/plugins/fancy-story-builder/scripts/skeleton-validate.js +1973 -0
- package/plugins/fancy-story-builder/scripts/stage-batches.js +1405 -0
- package/plugins/fancy-story-builder/scripts/stage-contract.js +429 -0
- package/plugins/fancy-story-builder/scripts/story-artifacts.js +933 -0
- package/plugins/fancy-story-builder/scripts/upstream-query.js +198 -0
- package/plugins/fancy-story-builder/scripts/v2/baseline.js +112 -0
- package/plugins/fancy-story-builder/scripts/v2/cast.js +517 -0
- package/plugins/fancy-story-builder/scripts/v2/context.js +434 -0
- package/plugins/fancy-story-builder/scripts/v2/contracts.js +493 -0
- package/plugins/fancy-story-builder/scripts/v2/core.js +419 -0
- package/plugins/fancy-story-builder/scripts/v2/engine.js +1068 -0
- package/plugins/fancy-story-builder/scripts/v2/export-markdown.js +794 -0
- package/plugins/fancy-story-builder/scripts/v2/genre.js +195 -0
- package/plugins/fancy-story-builder/scripts/v2/knowledge.js +321 -0
- package/plugins/fancy-story-builder/scripts/v2/macro.js +409 -0
- package/plugins/fancy-story-builder/scripts/v2/manifest.js +538 -0
- package/plugins/fancy-story-builder/scripts/v2/migrate.js +137 -0
- package/plugins/fancy-story-builder/scripts/v2/prompts.js +1335 -0
- package/plugins/fancy-story-builder/scripts/v2/rhythm.js +618 -0
- package/plugins/fancy-story-builder/scripts/v2/scale.js +843 -0
- package/plugins/fancy-story-builder/scripts/v2/snapshot.js +121 -0
- package/plugins/fancy-story-builder/scripts/v2/stages.js +273 -0
- package/plugins/fancy-story-builder/scripts/v2/writing.js +216 -0
- package/plugins/fancy-story-builder/scripts/writing-rules.js +167 -0
- package/plugins/fancy-story-builder/scripts/writing-validate.js +682 -0
- package/plugins/fancy-story-builder/skeleton-output.js +199 -0
- package/plugins/fancy-story-builder/stage-driver.js +1049 -0
- package/plugins/fancy-story-builder/storydb/README.md +271 -0
- package/plugins/fancy-story-builder/storydb/cli.js +458 -0
- package/plugins/fancy-story-builder/storydb/client.js +351 -0
- package/plugins/fancy-story-builder/storydb/compile.js +452 -0
- package/plugins/fancy-story-builder/storydb/context.js +288 -0
- package/plugins/fancy-story-builder/storydb/definitions.js +699 -0
- package/plugins/fancy-story-builder/storydb/driver.js +106 -0
- package/plugins/fancy-story-builder/storydb/dsh-tools.js +313 -0
- package/plugins/fancy-story-builder/storydb/export.js +175 -0
- package/plugins/fancy-story-builder/storydb/ids.js +31 -0
- package/plugins/fancy-story-builder/storydb/impact.js +170 -0
- package/plugins/fancy-story-builder/storydb/import.js +559 -0
- package/plugins/fancy-story-builder/storydb/mcp.js +99 -0
- package/plugins/fancy-story-builder/storydb/prompts.js +952 -0
- package/plugins/fancy-story-builder/storydb/stage-command.js +379 -0
- package/plugins/fancy-story-builder/storydb/stage.js +426 -0
- package/plugins/fancy-story-builder/storydb/store.js +891 -0
- package/plugins/fancy-story-builder/storydb/tools.js +592 -0
- package/plugins/fancy-story-builder/storydb/validation.js +183 -0
- package/plugins/fancy-story-builder/templates/v2/10-scale/scale-template.json +144 -0
- package/plugins/fancy-story-builder/templates/v2/20-macro/emotion-template.json +15 -0
- package/plugins/fancy-story-builder/templates/v2/20-macro/endings-template.json +16 -0
- package/plugins/fancy-story-builder/templates/v2/20-macro/lines-template.json +28 -0
- package/plugins/fancy-story-builder/templates/v2/20-macro/roles-template.json +10 -0
- package/plugins/fancy-story-builder/templates/v2/20-macro/story-template.json +19 -0
- package/plugins/fancy-story-builder/templates/v2/20-macro/volumes-template.json +24 -0
- package/plugins/fancy-story-builder/templates/v2/20-macro/world-template.json +24 -0
- package/plugins/fancy-story-builder/templates/v2/30-cast/characters-template.json +49 -0
- package/plugins/fancy-story-builder/templates/v2/30-cast/climaxes-template.json +14 -0
- package/plugins/fancy-story-builder/templates/v2/30-cast/conflicts-template.json +30 -0
- package/plugins/fancy-story-builder/templates/v2/30-cast/factions-template.json +18 -0
- package/plugins/fancy-story-builder/templates/v2/30-cast/foreshadows-template.json +29 -0
- package/plugins/fancy-story-builder/templates/v2/30-cast/information-template.json +29 -0
- package/plugins/fancy-story-builder/templates/v2/30-cast/items-template.json +13 -0
- package/plugins/fancy-story-builder/templates/v2/30-cast/locations-template.json +12 -0
- package/plugins/fancy-story-builder/templates/v2/30-cast/payoffs-template.json +16 -0
- package/plugins/fancy-story-builder/templates/v2/30-cast/relations-template.json +44 -0
- package/plugins/fancy-story-builder/templates/v2/40-rhythm/chapter-map-template.json +81 -0
- package/plugins/fancy-story-builder/templates/v2/40-rhythm/outline-template.json +162 -0
- package/plugins/fancy-story-builder/templates/v2/50-writing/ledger-template.json +46 -0
- package/plugins/fancy-story-builder/templates/v2/60-deslop/report-template.json +10 -0
- package/plugins/fancy-story-builder/templates//350/212/202/345/245/217//346/227/266/345/272/217/346/230/240/345/260/204/346/250/241/346/235/277.json +427 -0
- package/plugins/fancy-story-builder/templates//350/212/202/345/245/217//347/253/240/350/212/202/347/273/206/347/272/262/346/250/241/346/235/277.json +34 -0
- package/plugins/fancy-story-builder/templates//350/212/202/345/245/217//347/253/240/350/212/202/350/247/204/345/210/222/346/250/241/346/235/277.json +24 -0
- package/plugins/fancy-story-builder/templates//350/212/202/345/245/217//350/212/202/345/245/217/345/217/202/346/225/260/346/250/241/346/235/277.json +27 -0
- package/plugins/fancy-story-builder/templates//350/241/200/350/202/211//344/270/226/347/225/214/350/247/202/350/247/204/345/210/231/346/250/241/346/235/277.json +168 -0
- package/plugins/fancy-story-builder/templates//350/241/200/350/202/211//345/205/263/351/224/256/351/201/223/345/205/267/346/250/241/346/235/277.json +184 -0
- package/plugins/fancy-story-builder/templates//350/241/200/350/202/211//345/205/266/344/273/226/345/256/236/344/275/223/346/250/241/346/235/277.json +62 -0
- package/plugins/fancy-story-builder/templates//350/241/200/350/202/211//345/206/262/347/252/201/346/250/241/346/235/277.json +131 -0
- package/plugins/fancy-story-builder/templates//350/241/200/350/202/211//345/211/247/346/203/205/346/250/241/346/235/277.json +85 -0
- package/plugins/fancy-story-builder/templates//350/241/200/350/202/211//345/212/277/345/212/233/346/250/241/346/235/277.json +103 -0
- package/plugins/fancy-story-builder/templates//350/241/200/350/202/211//345/234/260/347/202/271/346/250/241/346/235/277.json +68 -0
- package/plugins/fancy-story-builder/templates//350/241/200/350/202/211//347/273/223/345/261/200/346/250/241/346/235/277.json +142 -0
- package/plugins/fancy-story-builder/templates//350/241/200/350/202/211//350/247/222/350/211/262/346/250/241/346/235/277.json +292 -0
- package/plugins/fancy-story-builder/templates//351/252/250/346/236/266//345/205/263/347/263/273/346/250/241/346/235/277.json +266 -0
- package/plugins/fancy-story-builder/templates//351/252/250/346/236/266//345/256/236/344/275/223/346/250/241/346/235/277.json +336 -0
- package/plugins/fancy-story-builder/templates//351/252/250/346/236/266//345/261/202/347/272/247/346/250/241/346/235/277.json +65 -0
- package/plugins/fancy-story-builder/templates//351/252/250/346/236/266//346/241/206/346/236/266/346/250/241/346/235/277.json +308 -0
- package/plugins/fancy-story-builder/templates.js +1446 -0
- package/plugins/fancy-story-builder/wiki-references.js +540 -0
- package/plugins/fancy-story-builder/writing-driver.js +1388 -0
- package/plugins/fancy-topic/index.js +1057 -0
- package/plugins/fancy-topic/prompts/01-exploration-prompt.md +115 -0
- package/plugins/fancy-topic/scripts/save-suggestion.js +219 -0
- package/plugins/fancy-topic/scripts/select-topic.js +317 -0
- package/plugins/fancy-topic/templates.js +476 -0
- package/plugins/fancy-topic/tool-deploy.js +200 -0
- package/commands/bootstrap/index.js +0 -164
- package/commands/bootstrap/init-project.js +0 -121
- package/commands/scan/index.js +0 -283
- package/commands/scan/platforms/fanqie.js +0 -146
- package/commands/scan/platforms/jinjiang.js +0 -143
- package/commands/scan/platforms/qidian.js +0 -102
- package/commands/scan/platforms/qimao.js +0 -127
- package/commands/scan/scraper.js +0 -195
- package/infra.js +0 -160
|
@@ -0,0 +1,452 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compile storydb definitions into:
|
|
3
|
+
* - SurrealQL DDL
|
|
4
|
+
* - per-table schema cards
|
|
5
|
+
* - JSON schemas for tool arguments
|
|
6
|
+
*
|
|
7
|
+
* One definition, three consumers. This is what keeps the LLM-facing schema
|
|
8
|
+
* and the database schema from drifting apart.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { ENTITIES, EDGES, HOOK_TYPES, EMOTION_PHASES } from './definitions.js';
|
|
12
|
+
import { validationDdl, SCHEMA_VERSION, VALIDATION_VERSION } from './validation.js';
|
|
13
|
+
|
|
14
|
+
function q(value) {
|
|
15
|
+
return JSON.stringify(String(value));
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function enumAssert(values) {
|
|
19
|
+
return '$value IN [' + values.map(q).join(', ') + ']';
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function baseType(field) {
|
|
23
|
+
switch (field.type) {
|
|
24
|
+
case 'string':
|
|
25
|
+
case 'text':
|
|
26
|
+
return 'string';
|
|
27
|
+
case 'int':
|
|
28
|
+
return 'int';
|
|
29
|
+
case 'float':
|
|
30
|
+
return 'float';
|
|
31
|
+
case 'bool':
|
|
32
|
+
return 'bool';
|
|
33
|
+
case 'datetime':
|
|
34
|
+
return 'datetime';
|
|
35
|
+
case 'array':
|
|
36
|
+
return 'array';
|
|
37
|
+
case 'object':
|
|
38
|
+
return 'object FLEXIBLE';
|
|
39
|
+
case 'record':
|
|
40
|
+
return 'record<' + field.table + '>';
|
|
41
|
+
case 'enum':
|
|
42
|
+
return 'string';
|
|
43
|
+
default:
|
|
44
|
+
return 'string';
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function fieldDdl(table, name, field) {
|
|
49
|
+
const base = baseType(field);
|
|
50
|
+
const optional = !field.required && !['array', 'object'].includes(field.type);
|
|
51
|
+
const type = optional ? 'option<' + base + '>' : base;
|
|
52
|
+
const parts = [`DEFINE FIELD IF NOT EXISTS ${name} ON ${table} TYPE ${type}`];
|
|
53
|
+
|
|
54
|
+
if (field.type === 'array') {
|
|
55
|
+
parts.push('DEFAULT []');
|
|
56
|
+
if (field.required && field.nonEmpty) parts.push('ASSERT array::len($value) > 0');
|
|
57
|
+
}
|
|
58
|
+
if (field.type === 'enum') {
|
|
59
|
+
const assert = field.required
|
|
60
|
+
? enumAssert(field.values)
|
|
61
|
+
: `$value = NONE OR ${enumAssert(field.values)}`;
|
|
62
|
+
parts.push('ASSERT ' + assert);
|
|
63
|
+
}
|
|
64
|
+
if (field.type === 'object' && field.required) {
|
|
65
|
+
parts.push('ASSERT $value != NONE');
|
|
66
|
+
}
|
|
67
|
+
if (field.default !== undefined) {
|
|
68
|
+
const v = field.default;
|
|
69
|
+
parts.push('DEFAULT ' + (typeof v === 'string' ? q(v) : String(v)));
|
|
70
|
+
}
|
|
71
|
+
return parts.join(' ') + ';';
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function entityDdl(key, def) {
|
|
75
|
+
const table = def.table;
|
|
76
|
+
const lines = [
|
|
77
|
+
`DEFINE TABLE IF NOT EXISTS ${table} SCHEMAFULL;`,
|
|
78
|
+
];
|
|
79
|
+
if (key === 'story') {
|
|
80
|
+
// story:main is the singleton root; no `story` back-reference.
|
|
81
|
+
} else {
|
|
82
|
+
lines.push(`DEFINE FIELD IF NOT EXISTS story ON ${table} TYPE record<story>;`);
|
|
83
|
+
if (!def.internal) {
|
|
84
|
+
lines.push(`DEFINE FIELD IF NOT EXISTS entity_id ON ${table} TYPE string;`);
|
|
85
|
+
lines.push(`DEFINE INDEX IF NOT EXISTS ${table}_entity ON ${table} FIELDS story, entity_id UNIQUE;`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
for (const [name, field] of Object.entries(def.fields || {})) {
|
|
89
|
+
lines.push(fieldDdl(table, name, field));
|
|
90
|
+
}
|
|
91
|
+
lines.push(`DEFINE FIELD IF NOT EXISTS payload ON ${table} TYPE object FLEXIBLE;`);
|
|
92
|
+
lines.push(`DEFINE FIELD IF NOT EXISTS created_at ON ${table} TYPE datetime VALUE time::now();`);
|
|
93
|
+
lines.push(`DEFINE FIELD IF NOT EXISTS updated_at ON ${table} TYPE datetime VALUE time::now();`);
|
|
94
|
+
return lines;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function edgeDdl(key, def) {
|
|
98
|
+
const table = def.table;
|
|
99
|
+
const lines = [
|
|
100
|
+
`DEFINE TABLE IF NOT EXISTS ${table} SCHEMAFULL TYPE RELATION IN ${def.in} OUT ${def.out} ENFORCED;`,
|
|
101
|
+
`DEFINE FIELD IF NOT EXISTS story ON ${table} TYPE record<story>;`,
|
|
102
|
+
`DEFINE FIELD IF NOT EXISTS from_id ON ${table} TYPE string;`,
|
|
103
|
+
`DEFINE FIELD IF NOT EXISTS to_id ON ${table} TYPE string;`,
|
|
104
|
+
`DEFINE FIELD IF NOT EXISTS payload ON ${table} TYPE object FLEXIBLE;`,
|
|
105
|
+
`DEFINE FIELD IF NOT EXISTS created_at ON ${table} TYPE datetime VALUE time::now();`,
|
|
106
|
+
];
|
|
107
|
+
for (const [name, field] of Object.entries(def.fields || {})) {
|
|
108
|
+
lines.push(fieldDdl(table, name, field));
|
|
109
|
+
}
|
|
110
|
+
// Prevents duplicate structural edges; action edges are intentionally not unique.
|
|
111
|
+
if (['has_volume', 'has_act', 'has_chapter', 'chapter_cast', 'chapter_location'].includes(key)) {
|
|
112
|
+
lines.push(`DEFINE INDEX IF NOT EXISTS ${table}_pair ON ${table} FIELDS in, out UNIQUE;`);
|
|
113
|
+
}
|
|
114
|
+
return lines;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function knowledgeRules() {
|
|
118
|
+
return `DEFINE EVENT OVERWRITE knowledge_rules ON TABLE knowledge_event WHEN $event = 'CREATE' THEN {
|
|
119
|
+
IF $after.change = 'confirms' AND $after.coverage != 'full' { THROW 'confirms 必须配 coverage=full'; };
|
|
120
|
+
IF $after.change = 'partially_learns' AND $after.coverage != 'partial' { THROW 'partially_learns 必须配 coverage=partial'; };
|
|
121
|
+
IF $after.change = 'forgets' AND $after.coverage != 'unknown' { THROW 'forgets 必须配 coverage=unknown'; };
|
|
122
|
+
IF $after.change = 'misinforms' AND $after.coverage != 'false' { THROW 'misinforms 必须配 coverage=false'; };
|
|
123
|
+
IF $after.how = 'told' AND $after.source_character = NONE { THROW 'how=told 必须写 source_character'; };
|
|
124
|
+
IF $after.subject_type = 'information' AND array::len(SELECT VALUE id FROM information WHERE story = $after.story AND entity_id = $after.subject_id LIMIT 1) = 0 { THROW '知识事件引用了不存在的信息差:' + $after.subject_id; };
|
|
125
|
+
IF $after.subject_type = 'foreshadow' AND array::len(SELECT VALUE id FROM foreshadow WHERE story = $after.story AND entity_id = $after.subject_id LIMIT 1) = 0 { THROW '知识事件引用了不存在的伏笔:' + $after.subject_id; };
|
|
126
|
+
LET $prev = SELECT VALUE state FROM knowledge_state WHERE story = $after.story AND character = $after.character AND subject_type = $after.subject_type AND subject_id = $after.subject_id LIMIT 1;
|
|
127
|
+
LET $from = IF array::len($prev) > 0 { $prev[0] } ELSE { 'unknown' };
|
|
128
|
+
IF $from = 'full' AND $after.change IN ['learns','partially_learns'] { THROW '已 full,不能再写成 learns/partially_learns:' + $after.character_id + ' / ' + $after.subject_id; };
|
|
129
|
+
IF $from = 'false' AND $after.change IN ['learns','partially_learns','confirms'] { THROW '之前是误信,必须先 corrects:' + $after.character_id + ' / ' + $after.subject_id; };
|
|
130
|
+
IF $from = 'unknown' AND $after.change = 'confirms' { THROW '尚未学会,不能直接 confirms:' + $after.character_id + ' / ' + $after.subject_id; };
|
|
131
|
+
UPSERT knowledge_state SET story = $after.story, character = $after.character, subject_type = $after.subject_type, subject_id = $after.subject_id, state = $after.coverage WHERE story = $after.story AND character = $after.character AND subject_type = $after.subject_type AND subject_id = $after.subject_id;
|
|
132
|
+
};`;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function chapterGuard() {
|
|
136
|
+
return `DEFINE EVENT OVERWRITE chapter_guard ON TABLE chapter WHEN $event = 'CREATE' OR $event = 'UPDATE' THEN {
|
|
137
|
+
IF array::len(SELECT VALUE id FROM volume WHERE story = $after.story AND volume_id = $after.volume LIMIT 1) = 0 { THROW '章节引用了不存在的卷:' + $after.volume; };
|
|
138
|
+
IF array::len(SELECT VALUE id FROM act WHERE story = $after.story AND act_id = $after.act LIMIT 1) = 0 { THROW '章节引用了不存在的幕:' + $after.act; };
|
|
139
|
+
IF $after.emotion.value != NONE AND ($after.emotion.value < 1 OR $after.emotion.value > 10) { THROW 'emotion.value 必须在 1-10'; };
|
|
140
|
+
IF $after.hook.strength != NONE AND !($after.hook.strength IN ['强','中','弱']) { THROW 'hook.strength 必须是 强/中/弱'; };
|
|
141
|
+
LET $prev = SELECT * FROM chapter WHERE story = $after.story AND display_no < $after.display_no ORDER BY display_no DESC LIMIT 1;
|
|
142
|
+
IF array::len($prev) > 0 {
|
|
143
|
+
LET $p = $prev[0];
|
|
144
|
+
IF $p.time.story_day != NONE AND $after.time.story_day != NONE AND $after.time.story_day < $p.time.story_day AND $after.time.flashback != true {
|
|
145
|
+
THROW '时间线倒退:第 ' + <string>$after.display_no + ' 章 story_day=' + <string>$after.time.story_day + ' 早于第 ' + <string>$p.display_no + ' 章 story_day=' + <string>$p.time.story_day + '(闪回应标记 time.flashback=true)';
|
|
146
|
+
};
|
|
147
|
+
IF $p.hook.type != NONE AND $after.hook.type != NONE AND $p.hook.type = $after.hook.type {
|
|
148
|
+
LET $prev2 = SELECT * FROM chapter WHERE story = $after.story AND display_no < $p.display_no ORDER BY display_no DESC LIMIT 1;
|
|
149
|
+
IF array::len($prev2) > 0 AND $prev2[0].hook.type = $after.hook.type {
|
|
150
|
+
THROW '钩子重复:第 ' + <string>$prev2[0].display_no + '/' + <string>$p.display_no + '/' + <string>$after.display_no + ' 章连续使用 ' + $after.hook.type;
|
|
151
|
+
};
|
|
152
|
+
};
|
|
153
|
+
};
|
|
154
|
+
};`;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function foreshadowOrder() {
|
|
158
|
+
return `DEFINE EVENT OVERWRITE foreshadow_order ON TABLE foreshadow_action WHEN $event = 'CREATE' THEN {
|
|
159
|
+
IF $after.action IN ['progress','reveal'] {
|
|
160
|
+
LET $plant = SELECT * FROM foreshadow_action WHERE story = $after.story AND to_id = $after.to_id AND action = 'plant' AND chapter_no <= $after.chapter_no LIMIT 1;
|
|
161
|
+
IF array::len($plant) = 0 { THROW '伏笔 ' + $after.to_id + ' 在第 ' + <string>$after.chapter_no + ' 章之前没有埋设,不能 ' + $after.action; };
|
|
162
|
+
};
|
|
163
|
+
IF $after.action = 'plant' {
|
|
164
|
+
LET $reveal = SELECT * FROM foreshadow_action WHERE story = $after.story AND to_id = $after.to_id AND action = 'reveal' AND chapter_no <= $after.chapter_no LIMIT 1;
|
|
165
|
+
IF array::len($reveal) > 0 { THROW '伏笔 ' + $after.to_id + ' 已在更早章节回收,不能再埋设'; };
|
|
166
|
+
};
|
|
167
|
+
};`;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function payoffOrder() {
|
|
171
|
+
return `DEFINE EVENT OVERWRITE payoff_order ON TABLE payoff_action WHEN $event = 'CREATE' THEN {
|
|
172
|
+
IF $after.action = 'payoff' {
|
|
173
|
+
LET $setup = SELECT * FROM payoff_action
|
|
174
|
+
WHERE story = $after.story AND to_id = $after.to_id AND action = 'setup'
|
|
175
|
+
ORDER BY chapter_no ASC LIMIT 1;
|
|
176
|
+
IF array::len($setup) > 0 AND $setup[0].chapter_no > $after.chapter_no {
|
|
177
|
+
THROW '爽点 ' + $after.to_id + ' 的兑现章(' + <string>$after.chapter_no + ')早于铺垫章(' + <string>$setup[0].chapter_no + ')';
|
|
178
|
+
};
|
|
179
|
+
};
|
|
180
|
+
IF $after.action = 'setup' {
|
|
181
|
+
LET $payoff = SELECT * FROM payoff_action
|
|
182
|
+
WHERE story = $after.story AND to_id = $after.to_id AND action = 'payoff'
|
|
183
|
+
AND chapter_no < $after.chapter_no
|
|
184
|
+
ORDER BY chapter_no DESC LIMIT 1;
|
|
185
|
+
IF array::len($payoff) > 0 {
|
|
186
|
+
THROW '爽点 ' + $after.to_id + ' 的铺垫章(' + <string>$after.chapter_no + ')晚于已有的兑现章(' + <string>$payoff[0].chapter_no + ')';
|
|
187
|
+
};
|
|
188
|
+
};
|
|
189
|
+
};`;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function completeGuards() {
|
|
193
|
+
const out = [];
|
|
194
|
+
for (const [key, def] of Object.entries(ENTITIES)) {
|
|
195
|
+
const reqs = (def.requirements || []).filter((r) => r.hard);
|
|
196
|
+
if (!reqs.length || key === 'story') continue;
|
|
197
|
+
const table = def.table;
|
|
198
|
+
out.push(`DEFINE EVENT OVERWRITE complete_${table} ON TABLE ${table}
|
|
199
|
+
WHEN $event = 'UPDATE' AND $after.status = 'complete'
|
|
200
|
+
THEN {
|
|
201
|
+
LET $pending = SELECT * FROM requirement
|
|
202
|
+
WHERE story = $after.story
|
|
203
|
+
AND subject = '${table}:' + $after.entity_id
|
|
204
|
+
AND status = 'pending'
|
|
205
|
+
AND hard = true;
|
|
206
|
+
IF array::len($pending) > 0 {
|
|
207
|
+
THROW '${def.label || table} 还有 ' + <string>array::len($pending) + ' 个必填待办未完成,不能标记 complete';
|
|
208
|
+
};
|
|
209
|
+
};`);
|
|
210
|
+
}
|
|
211
|
+
return out;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function fillEvents() {
|
|
215
|
+
const out = [];
|
|
216
|
+
for (const [key, def] of Object.entries(EDGES)) {
|
|
217
|
+
if (!def.fills) continue;
|
|
218
|
+
const table = def.table;
|
|
219
|
+
const kindExpr = (actionField) =>
|
|
220
|
+
`'${Object.values(def.fills)[0]}'`; // replaced per action below
|
|
221
|
+
for (const [action, kind] of Object.entries(def.fills)) {
|
|
222
|
+
out.push(`DEFINE EVENT OVERWRITE fill_${table}_${action} ON TABLE ${table}
|
|
223
|
+
WHEN $event = 'CREATE' AND $after.action = ${q(action)}
|
|
224
|
+
THEN {
|
|
225
|
+
UPDATE requirement SET status = 'filled', filled_by = <string>$after.id, updated_at = time::now()
|
|
226
|
+
WHERE story = $after.story AND subject = '${def.out}:' + $after.to_id AND kind = ${q(kind)} AND status = 'pending';
|
|
227
|
+
};`);
|
|
228
|
+
out.push(`DEFINE EVENT OVERWRITE unfill_${table}_${action} ON TABLE ${table}
|
|
229
|
+
WHEN $event = 'DELETE' AND $before.action = ${q(action)}
|
|
230
|
+
THEN {
|
|
231
|
+
IF array::len(SELECT * FROM ${table} WHERE story = $before.story AND to_id = $before.to_id AND action = ${q(action)} LIMIT 1) = 0 {
|
|
232
|
+
UPDATE requirement SET status = 'pending', filled_by = NONE, updated_at = time::now()
|
|
233
|
+
WHERE story = $before.story AND subject = '${def.out}:' + $before.to_id AND kind = ${q(kind)} AND status = 'filled';
|
|
234
|
+
};
|
|
235
|
+
};`);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// outline created -> chapter_outline filled
|
|
240
|
+
out.push(`DEFINE EVENT OVERWRITE fill_chapter_outline ON TABLE outline
|
|
241
|
+
WHEN $event = 'CREATE'
|
|
242
|
+
THEN {
|
|
243
|
+
UPDATE requirement SET status = 'filled', filled_by = <string>$after.id, updated_at = time::now()
|
|
244
|
+
WHERE story = $after.story AND subject = 'chapter:' + $after.chapter_id AND kind = 'chapter_outline' AND status = 'pending';
|
|
245
|
+
};`);
|
|
246
|
+
out.push(`DEFINE EVENT OVERWRITE unfill_chapter_outline ON TABLE outline
|
|
247
|
+
WHEN $event = 'DELETE'
|
|
248
|
+
THEN {
|
|
249
|
+
IF array::len(SELECT * FROM outline WHERE story = $before.story AND chapter_id = $before.chapter_id LIMIT 1) = 0 {
|
|
250
|
+
UPDATE requirement SET status = 'pending', filled_by = NONE, updated_at = time::now()
|
|
251
|
+
WHERE story = $before.story AND subject = 'chapter:' + $before.chapter_id AND kind = 'chapter_outline' AND status = 'filled';
|
|
252
|
+
};
|
|
253
|
+
};`);
|
|
254
|
+
|
|
255
|
+
// chapter_text created -> chapter_draft filled
|
|
256
|
+
out.push(`DEFINE EVENT OVERWRITE fill_chapter_draft ON TABLE chapter_text
|
|
257
|
+
WHEN $event = 'CREATE'
|
|
258
|
+
THEN {
|
|
259
|
+
UPDATE requirement SET status = 'filled', filled_by = <string>$after.id, updated_at = time::now()
|
|
260
|
+
WHERE story = $after.story AND subject = 'chapter:' + $after.chapter_id AND kind = 'chapter_draft' AND status = 'pending';
|
|
261
|
+
};`);
|
|
262
|
+
out.push(`DEFINE EVENT OVERWRITE unfill_chapter_draft ON TABLE chapter_text
|
|
263
|
+
WHEN $event = 'DELETE'
|
|
264
|
+
THEN {
|
|
265
|
+
IF array::len(SELECT * FROM chapter_text WHERE story = $before.story AND chapter_id = $before.chapter_id LIMIT 1) = 0 {
|
|
266
|
+
UPDATE requirement SET status = 'pending', filled_by = NONE, updated_at = time::now()
|
|
267
|
+
WHERE story = $before.story AND subject = 'chapter:' + $before.chapter_id AND kind = 'chapter_draft' AND status = 'filled';
|
|
268
|
+
};
|
|
269
|
+
};`);
|
|
270
|
+
|
|
271
|
+
// scene created -> outline_scenes filled
|
|
272
|
+
out.push(`DEFINE EVENT OVERWRITE fill_outline_scenes ON TABLE scene
|
|
273
|
+
WHEN $event = 'CREATE'
|
|
274
|
+
THEN {
|
|
275
|
+
UPDATE requirement SET status = 'filled', filled_by = <string>$after.id, updated_at = time::now()
|
|
276
|
+
WHERE story = $after.story AND subject = 'outline:' + $after.outline_id AND kind = 'outline_scenes' AND status = 'pending';
|
|
277
|
+
};`);
|
|
278
|
+
|
|
279
|
+
return out;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
export const SEARCH_INDEX_DDL = {
|
|
283
|
+
modern: `DEFINE INDEX IF NOT EXISTS chapter_text_fts ON chapter_text FIELDS content SEARCH ANALYZER story_zh BM25(1.2, 0.75);`,
|
|
284
|
+
legacy: `DEFINE INDEX IF NOT EXISTS chapter_text_fts ON chapter_text FIELDS content FULLTEXT ANALYZER story_zh BM25(1.2, 0.75);`,
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
export const DDL = [
|
|
288
|
+
// ------------------------------------------------------------------
|
|
289
|
+
// core tables
|
|
290
|
+
// ------------------------------------------------------------------
|
|
291
|
+
...Object.entries(ENTITIES).flatMap(([key, def]) => entityDdl(key, def)),
|
|
292
|
+
...Object.entries(EDGES).flatMap(([key, def]) => edgeDdl(key, def)),
|
|
293
|
+
|
|
294
|
+
// ------------------------------------------------------------------
|
|
295
|
+
// knowledge timeline
|
|
296
|
+
// ------------------------------------------------------------------
|
|
297
|
+
`DEFINE TABLE IF NOT EXISTS knowledge_event SCHEMAFULL;`,
|
|
298
|
+
`DEFINE FIELD IF NOT EXISTS story ON knowledge_event TYPE record<story>;`,
|
|
299
|
+
`DEFINE FIELD IF NOT EXISTS chapter ON knowledge_event TYPE record<chapter>;`,
|
|
300
|
+
`DEFINE FIELD IF NOT EXISTS chapter_no ON knowledge_event TYPE int;`,
|
|
301
|
+
`DEFINE FIELD IF NOT EXISTS subject_type ON knowledge_event TYPE string ASSERT $value IN ['information','foreshadow'];`,
|
|
302
|
+
`DEFINE FIELD IF NOT EXISTS subject_id ON knowledge_event TYPE string;`,
|
|
303
|
+
`DEFINE FIELD IF NOT EXISTS character ON knowledge_event TYPE record<character>;`,
|
|
304
|
+
`DEFINE FIELD IF NOT EXISTS character_id ON knowledge_event TYPE string;`,
|
|
305
|
+
`DEFINE FIELD IF NOT EXISTS change ON knowledge_event TYPE string ASSERT $value IN ['learns','partially_learns','confirms','corrects','forgets','misinforms'];`,
|
|
306
|
+
`DEFINE FIELD IF NOT EXISTS coverage ON knowledge_event TYPE string ASSERT $value IN ['unknown','partial','full','false'];`,
|
|
307
|
+
`DEFINE FIELD IF NOT EXISTS knowledge ON knowledge_event TYPE string;`,
|
|
308
|
+
`DEFINE FIELD IF NOT EXISTS how ON knowledge_event TYPE string ASSERT $value IN ['witnessed','told','inferred','document','rumor','experience','other'];`,
|
|
309
|
+
`DEFINE FIELD IF NOT EXISTS source_character ON knowledge_event TYPE option<record<character>>;`,
|
|
310
|
+
`DEFINE FIELD IF NOT EXISTS scene ON knowledge_event TYPE string;`,
|
|
311
|
+
`DEFINE FIELD IF NOT EXISTS evidence ON knowledge_event TYPE string;`,
|
|
312
|
+
`DEFINE FIELD IF NOT EXISTS payload ON knowledge_event TYPE object FLEXIBLE;`,
|
|
313
|
+
`DEFINE FIELD IF NOT EXISTS created_at ON knowledge_event TYPE datetime VALUE time::now();`,
|
|
314
|
+
|
|
315
|
+
`DEFINE TABLE IF NOT EXISTS knowledge_state SCHEMAFULL;`,
|
|
316
|
+
`DEFINE FIELD IF NOT EXISTS story ON knowledge_state TYPE record<story>;`,
|
|
317
|
+
`DEFINE FIELD IF NOT EXISTS character ON knowledge_state TYPE record<character>;`,
|
|
318
|
+
`DEFINE FIELD IF NOT EXISTS subject_type ON knowledge_state TYPE string;`,
|
|
319
|
+
`DEFINE FIELD IF NOT EXISTS subject_id ON knowledge_state TYPE string;`,
|
|
320
|
+
`DEFINE FIELD IF NOT EXISTS state ON knowledge_state TYPE string ASSERT $value IN ['unknown','partial','full','false'];`,
|
|
321
|
+
`DEFINE INDEX IF NOT EXISTS knowledge_state_key ON knowledge_state FIELDS story, character, subject_type, subject_id UNIQUE;`,
|
|
322
|
+
|
|
323
|
+
// ------------------------------------------------------------------
|
|
324
|
+
// metrics (time series)
|
|
325
|
+
// ------------------------------------------------------------------
|
|
326
|
+
`DEFINE TABLE IF NOT EXISTS chapter_metric SCHEMAFULL;`,
|
|
327
|
+
`DEFINE FIELD IF NOT EXISTS story ON chapter_metric TYPE record<story>;`,
|
|
328
|
+
`DEFINE FIELD IF NOT EXISTS chapter ON chapter_metric TYPE record<chapter>;`,
|
|
329
|
+
`DEFINE FIELD IF NOT EXISTS chapter_no ON chapter_metric TYPE int;`,
|
|
330
|
+
`DEFINE FIELD IF NOT EXISTS emotion_value ON chapter_metric TYPE option<int>;`,
|
|
331
|
+
`DEFINE FIELD IF NOT EXISTS hook_strength ON chapter_metric TYPE option<string>;`,
|
|
332
|
+
`DEFINE FIELD IF NOT EXISTS word_target ON chapter_metric TYPE int;`,
|
|
333
|
+
`DEFINE FIELD IF NOT EXISTS word_count ON chapter_metric TYPE option<int>;`,
|
|
334
|
+
`DEFINE INDEX IF NOT EXISTS chapter_metric_key ON chapter_metric FIELDS story, chapter_no UNIQUE;`,
|
|
335
|
+
|
|
336
|
+
// ------------------------------------------------------------------
|
|
337
|
+
// full-text index on prose
|
|
338
|
+
// ------------------------------------------------------------------
|
|
339
|
+
`DEFINE ANALYZER IF NOT EXISTS story_zh TOKENIZERS class FILTERS lowercase, ngram(1,2);`,
|
|
340
|
+
|
|
341
|
+
// ------------------------------------------------------------------
|
|
342
|
+
// hard rules / triggers
|
|
343
|
+
// ------------------------------------------------------------------
|
|
344
|
+
knowledgeRules(),
|
|
345
|
+
chapterGuard(),
|
|
346
|
+
...completeGuards(),
|
|
347
|
+
foreshadowOrder(),
|
|
348
|
+
payoffOrder(),
|
|
349
|
+
...fillEvents(),
|
|
350
|
+
|
|
351
|
+
// ------------------------------------------------------------------
|
|
352
|
+
// database-owned metadata and validation
|
|
353
|
+
// ------------------------------------------------------------------
|
|
354
|
+
`DEFINE TABLE IF NOT EXISTS fancy_meta SCHEMAFULL;`,
|
|
355
|
+
`DEFINE FIELD IF NOT EXISTS schema_version ON fancy_meta TYPE int;`,
|
|
356
|
+
`DEFINE FIELD IF NOT EXISTS validation_version ON fancy_meta TYPE int;`,
|
|
357
|
+
`DEFINE FIELD IF NOT EXISTS updated_at ON fancy_meta TYPE datetime VALUE time::now();`,
|
|
358
|
+
validationDdl(),
|
|
359
|
+
`DEFINE FUNCTION OVERWRITE fn::fancy::version() { RETURN { schema_version: ${Number(SCHEMA_VERSION)}, validation_version: ${Number(VALIDATION_VERSION)} }; };`,
|
|
360
|
+
];
|
|
361
|
+
|
|
362
|
+
const SPECIAL_CARDS = {
|
|
363
|
+
knowledge_event: `knowledge_event
|
|
364
|
+
chapter_id: string, 必填
|
|
365
|
+
chapter_no: int, 必填
|
|
366
|
+
subject_type: enum[information, foreshadow]
|
|
367
|
+
subject_id: string
|
|
368
|
+
character_id: string
|
|
369
|
+
change: enum[learns, partially_learns, confirms, corrects, forgets, misinforms]
|
|
370
|
+
coverage: enum[unknown, partial, full, false]
|
|
371
|
+
knowledge: string
|
|
372
|
+
how: enum[witnessed, told, inferred, document, rumor, experience, other]
|
|
373
|
+
source_character: string|null(how=told 必填)
|
|
374
|
+
scene: string
|
|
375
|
+
evidence: string
|
|
376
|
+
apply op: { "op": "knowledge", "data": { ... } }`,
|
|
377
|
+
chapter_metric: `chapter_metric
|
|
378
|
+
chapter_no: int
|
|
379
|
+
emotion_value: int 1-10
|
|
380
|
+
hook_strength: enum[强, 中, 弱]
|
|
381
|
+
word_target: int
|
|
382
|
+
word_count: int`,
|
|
383
|
+
};
|
|
384
|
+
|
|
385
|
+
export function schemaCard(table) {
|
|
386
|
+
if (SPECIAL_CARDS[table]) return SPECIAL_CARDS[table];
|
|
387
|
+
const def = ENTITIES[table] || EDGES[table];
|
|
388
|
+
if (!def) return null;
|
|
389
|
+
const isEdge = Boolean(EDGES[table]);
|
|
390
|
+
const lines = [];
|
|
391
|
+
lines.push(def.table + ' — ' + (def.label || def.table));
|
|
392
|
+
if (isEdge) {
|
|
393
|
+
lines.push(' edge: ' + def.in + ' -> ' + def.out);
|
|
394
|
+
}
|
|
395
|
+
if (!isEdge && !def.internal) {
|
|
396
|
+
const idKey = ['volume_id', 'act_id', 'line_id', 'node_id', 'ending_id', 'role_id', 'chapter_id']
|
|
397
|
+
.find((key) => def.fields && def.fields[key]);
|
|
398
|
+
if (['scale', 'world_doc', 'story_doc', 'emotion_doc'].includes(def.table)) {
|
|
399
|
+
lines.push(' id: 单例(不需要写 id)');
|
|
400
|
+
} else if (def.table === 'scene') {
|
|
401
|
+
lines.push(' id: 自动生成 SC_<outline_id>_<scene_order>');
|
|
402
|
+
} else if (idKey) {
|
|
403
|
+
lines.push(' id: string,create 时写 ' + idKey);
|
|
404
|
+
} else {
|
|
405
|
+
lines.push(' id: string,create 时写 data.id');
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
lines.push(' fields:');
|
|
409
|
+
for (const [name, field] of Object.entries(def.fields || {})) {
|
|
410
|
+
const type = field.type === 'enum' ? 'enum[' + field.values.join(', ') + ']' : field.type;
|
|
411
|
+
lines.push(' ' + name + ': ' + type + (field.required ? ', 必填' : ''));
|
|
412
|
+
}
|
|
413
|
+
if (!isEdge) {
|
|
414
|
+
const reqs = (def.requirements || []).map((r) => r.kind + (r.hard ? '(hard)' : '(soft)'));
|
|
415
|
+
if (reqs.length) lines.push(' requirements: ' + reqs.join(', '));
|
|
416
|
+
const edges = Object.values(EDGES).filter((e) => e.in === def.table || e.out === def.table);
|
|
417
|
+
if (edges.length) {
|
|
418
|
+
lines.push(' edges:');
|
|
419
|
+
for (const e of edges) {
|
|
420
|
+
lines.push(' ' + e.table + ': ' + e.in + ' -> ' + e.out);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
return lines.join('\n');
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
export function toolSchema(kind) {
|
|
428
|
+
const def = ENTITIES[kind];
|
|
429
|
+
if (!def) return null;
|
|
430
|
+
const properties = {};
|
|
431
|
+
const required = [];
|
|
432
|
+
for (const [name, field] of Object.entries(def.fields || {})) {
|
|
433
|
+
let schema;
|
|
434
|
+
switch (field.type) {
|
|
435
|
+
case 'int': schema = { type: 'integer' }; break;
|
|
436
|
+
case 'float': schema = { type: 'number' }; break;
|
|
437
|
+
case 'bool': schema = { type: 'boolean' }; break;
|
|
438
|
+
case 'array': schema = { type: 'array', items: {} }; break;
|
|
439
|
+
case 'object': schema = { type: 'object', additionalProperties: true }; break;
|
|
440
|
+
case 'enum': schema = { type: 'string', enum: field.values }; break;
|
|
441
|
+
case 'record': schema = { type: 'string' }; break;
|
|
442
|
+
default: schema = { type: 'string' };
|
|
443
|
+
}
|
|
444
|
+
if (field.required) required.push(name);
|
|
445
|
+
properties[name] = schema;
|
|
446
|
+
}
|
|
447
|
+
return { type: 'object', properties, required };
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
export function allSchemaCards() {
|
|
451
|
+
return Object.keys({ ...ENTITIES, ...EDGES }).map((table) => schemaCard(table)).filter(Boolean);
|
|
452
|
+
}
|