@cloud411716/fancy-webnovel 2.2.0 → 3.0.3
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/README.md +65 -0
- package/client.js +201 -15
- package/cordis.patch.yml +5 -1
- 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 +12 -7
- package/lib/atomic-write.js +109 -0
- package/lib/chapter-order.js +100 -0
- package/lib/confirmation.js +80 -0
- package/lib/db-init.js +123 -0
- package/lib/deployed-state.js +11 -11
- package/lib/direction-labels.js +265 -0
- package/lib/error-codes.js +31 -31
- package/lib/infra.js +29 -4
- 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 +140 -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 +31 -7
- package/plugins/fancy-analyze/analyze-definitions.md +519 -0
- package/plugins/fancy-analyze/index.js +1128 -77
- package/plugins/fancy-analyze/ingest-coverage.js +107 -0
- package/plugins/fancy-analyze/schema-analyze.md +355 -107
- package/plugins/fancy-analyze/scripts/append-summary-chapter.js +612 -0
- package/plugins/fancy-analyze/summary-progress.js +71 -7
- package/plugins/fancy-analyze/templates.js +437 -139
- package/plugins/fancy-bootstrap/index.js +276 -131
- package/plugins/fancy-bootstrap/scripts/init.js +59 -25
- package/plugins/fancy-bootstrap/templates.js +35 -18
- 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-driver.js +29 -6
- package/plugins/fancy-import/index.js +13 -5
- package/plugins/fancy-import/prompts/01-import-prompt.md +39 -14
- package/plugins/fancy-import/templates.js +17 -8
- 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 +145 -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 +410 -21
- 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 +169 -22
- package/plugins/fancy-scan/scripts/scrapers/fanqie-rank-scraper.js +145 -16
- package/plugins/fancy-scan/scripts/scrapers/jjwxc-rank-scraper.js +179 -49
- package/plugins/fancy-scan/scripts/scrapers/qimao-rank-scraper.js +38 -20
- 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 +545 -146
- 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/flesh-output.js +19 -6
- package/plugins/fancy-story-builder/index.js +4105 -396
- package/plugins/fancy-story-builder/patch-driver.js +390 -0
- package/plugins/fancy-story-builder/prompts/01-skeleton-prompt.md +29 -19
- package/plugins/fancy-story-builder/prompts/02-flesh-prompt.md +72 -10
- package/plugins/fancy-story-builder/prompts/03-rhythm-prompt.md +40 -5
- package/plugins/fancy-story-builder/prompts/04-writing-prompt.md +37 -0
- package/plugins/fancy-story-builder/purpose-template.md +42 -66
- package/plugins/fancy-story-builder/references/formulas-skeleton.json +229 -56
- package/plugins/fancy-story-builder/references/genre-profiles.json +3514 -0
- package/plugins/fancy-story-builder/references/glossary.md +15 -6
- package/plugins/fancy-story-builder/references/index.md +97 -6
- package/plugins/fancy-story-builder/references//350/212/202/345/245/217/346/214/207/345/257/274/00-index.md +1 -1
- package/plugins/fancy-story-builder/references//350/212/202/345/245/217/346/214/207/345/257/274/07-audit.md +25 -7
- package/plugins/fancy-story-builder/references//350/241/200/350/202/211/346/214/207/345/257/274/01-character.md +5 -1
- package/plugins/fancy-story-builder/references//350/241/200/350/202/211/346/214/207/345/257/274/03-relations.md +6 -2
- package/plugins/fancy-story-builder/references//350/241/200/350/202/211/346/214/207/345/257/274/05-audit.md +15 -5
- package/plugins/fancy-story-builder/references//351/252/250/346/236/266/346/214/207/345/257/274/00-index.md +1 -1
- package/plugins/fancy-story-builder/references//351/252/250/346/236/266/346/214/207/345/257/274/01-positioning.md +2 -1
- package/plugins/fancy-story-builder/references//351/252/250/346/236/266/346/214/207/345/257/274/03-entities.md +1 -1
- package/plugins/fancy-story-builder/references//351/252/250/346/236/266/346/214/207/345/257/274/06-audit.md +19 -1
- package/plugins/fancy-story-builder/schema-setting.json +1100 -0
- package/plugins/fancy-story-builder/schema-setting.md +201 -293
- package/plugins/fancy-story-builder/scripts/brief-validate.js +164 -0
- package/plugins/fancy-story-builder/scripts/content-quality.js +4 -1
- package/plugins/fancy-story-builder/scripts/flesh-validate.js +135 -12
- package/plugins/fancy-story-builder/scripts/graph-utils.js +146 -47
- package/plugins/fancy-story-builder/scripts/reference-log.js +110 -0
- package/plugins/fancy-story-builder/scripts/rhythm-validate.js +100 -4
- package/plugins/fancy-story-builder/scripts/schema-pages.js +2668 -0
- package/plugins/fancy-story-builder/scripts/semantic-lint.js +5 -1
- package/plugins/fancy-story-builder/scripts/skeleton-validate.js +158 -17
- package/plugins/fancy-story-builder/scripts/stage-batches.js +36 -2
- package/plugins/fancy-story-builder/scripts/stage-contract.js +12 -0
- package/plugins/fancy-story-builder/scripts/story-artifacts.js +933 -0
- package/plugins/fancy-story-builder/scripts/upstream-query.js +1 -1
- 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/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 +267 -44
- 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 +121 -19
- 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 +129 -21
- 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 +76 -17
- 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 +56 -8
- 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 +63 -13
- 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 +34 -6
- 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 +81 -18
- 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 +202 -42
- 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 +160 -27
- 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 +217 -41
- 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 +215 -41
- package/plugins/fancy-story-builder/templates.js +272 -74
- package/plugins/fancy-story-builder/wiki-references.js +429 -93
- package/plugins/fancy-story-builder/writing-driver.js +336 -9
- package/plugins/fancy-topic/index.js +414 -61
- 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 +106 -63
- package/plugins/fancy-topic/templates.js +225 -122
- package/plugins/fancy-topic/tool-deploy.js +200 -0
- package/lib/safe-rm.js +0 -94
|
@@ -0,0 +1,417 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SurrealDB schema for fancy-story-builder.
|
|
3
|
+
*
|
|
4
|
+
* Design:
|
|
5
|
+
* - relational tables : story / volume / act / chapter / quota-like summaries
|
|
6
|
+
* - document tables : characters, factions, locations, items, conflicts,
|
|
7
|
+
* foreshadows, payoffs, climaxes, information, outlines,
|
|
8
|
+
* chapter text, ledger rows
|
|
9
|
+
* - graph edges : relations, memberships, chapter_cast, *_action
|
|
10
|
+
* - event / timeline : knowledge_event + knowledge_state (replay)
|
|
11
|
+
* - time series : chapter_metric
|
|
12
|
+
* - KV / singleton : stage_doc
|
|
13
|
+
*
|
|
14
|
+
* Every table is SCHEMAFULL where the shape is known. Nested payloads use
|
|
15
|
+
* `object FLEXIBLE` so a stage can keep fields the query layer does not need
|
|
16
|
+
* yet without losing them on export.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
function entity(table, extra = []) {
|
|
20
|
+
return [
|
|
21
|
+
`DEFINE TABLE IF NOT EXISTS ${table} SCHEMAFULL;`,
|
|
22
|
+
`DEFINE FIELD IF NOT EXISTS story ON ${table} TYPE record<story>;`,
|
|
23
|
+
`DEFINE FIELD IF NOT EXISTS entity_id ON ${table} TYPE string;`,
|
|
24
|
+
`DEFINE FIELD IF NOT EXISTS name ON ${table} TYPE string;`,
|
|
25
|
+
`DEFINE FIELD IF NOT EXISTS type ON ${table} TYPE option<string>;`,
|
|
26
|
+
`DEFINE FIELD IF NOT EXISTS payload ON ${table} TYPE object FLEXIBLE;`,
|
|
27
|
+
`DEFINE INDEX IF NOT EXISTS ${table}_entity ON ${table} FIELDS story, entity_id UNIQUE;`,
|
|
28
|
+
...extra,
|
|
29
|
+
];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const F = (table, field, type, extra = '') =>
|
|
33
|
+
`DEFINE FIELD IF NOT EXISTS ${field} ON ${table} TYPE ${type}` +
|
|
34
|
+
`${type === 'array' && !/DEFAULT/.test(extra) ? ' DEFAULT []' : ''}${extra};`;
|
|
35
|
+
|
|
36
|
+
export const SCHEMA_STATEMENTS = [
|
|
37
|
+
// ------------------------------------------------------------------
|
|
38
|
+
// story / stage bookkeeping
|
|
39
|
+
// ------------------------------------------------------------------
|
|
40
|
+
`DEFINE TABLE IF NOT EXISTS story SCHEMAFULL;`,
|
|
41
|
+
F('story', 'title', 'string'),
|
|
42
|
+
F('story', 'genre', 'option<string>'),
|
|
43
|
+
F('story', 'platform', 'option<string>'),
|
|
44
|
+
F('story', 'target_chapters', 'int'),
|
|
45
|
+
F('story', 'target_words', 'int'),
|
|
46
|
+
F('story', 'avg_words_per_chapter', 'int'),
|
|
47
|
+
F('story', 'created_at', 'datetime', ' VALUE time::now()'),
|
|
48
|
+
F('story', 'updated_at', 'datetime', ' VALUE time::now()'),
|
|
49
|
+
|
|
50
|
+
`DEFINE TABLE IF NOT EXISTS stage_state SCHEMAFULL;`,
|
|
51
|
+
F('stage_state', 'story', 'record<story>'),
|
|
52
|
+
F('stage_state', 'stage', 'string'),
|
|
53
|
+
F('stage_state', 'status', 'string', " ASSERT $value IN ['draft','passed','failed']"),
|
|
54
|
+
F('stage_state', 'checked_at', 'option<datetime>'),
|
|
55
|
+
F('stage_state', 'errors', 'array', ' DEFAULT []'),
|
|
56
|
+
F('stage_state', 'warnings', 'array', ' DEFAULT []'),
|
|
57
|
+
`DEFINE INDEX IF NOT EXISTS stage_state_stage ON stage_state FIELDS story, stage UNIQUE;`,
|
|
58
|
+
|
|
59
|
+
// Generic lossless singleton documents: world / story / emotion / roles /
|
|
60
|
+
// volumes / lines / endings / scale / relations.
|
|
61
|
+
`DEFINE TABLE IF NOT EXISTS stage_doc SCHEMAFULL;`,
|
|
62
|
+
F('stage_doc', 'story', 'record<story>'),
|
|
63
|
+
F('stage_doc', 'stage', 'string'),
|
|
64
|
+
F('stage_doc', 'name', 'string'),
|
|
65
|
+
F('stage_doc', 'payload', 'object FLEXIBLE'),
|
|
66
|
+
`DEFINE INDEX IF NOT EXISTS stage_doc_key ON stage_doc FIELDS story, stage, name UNIQUE;`,
|
|
67
|
+
|
|
68
|
+
// ------------------------------------------------------------------
|
|
69
|
+
// scale: relational skeleton + document payload
|
|
70
|
+
// ------------------------------------------------------------------
|
|
71
|
+
`DEFINE TABLE IF NOT EXISTS scale SCHEMAFULL;`,
|
|
72
|
+
F('scale', 'story', 'record<story>'),
|
|
73
|
+
F('scale', 'capacity', 'any'),
|
|
74
|
+
F('scale', 'quotas', 'any'),
|
|
75
|
+
F('scale', 'rhythm_budget', 'any'),
|
|
76
|
+
F('scale', 'distribution', 'any'),
|
|
77
|
+
F('scale', 'constraints', 'any'),
|
|
78
|
+
F('scale', 'formulas', 'any'),
|
|
79
|
+
F('scale', 'derivations', 'any'),
|
|
80
|
+
F('scale', 'genre_profile', 'option<string>'),
|
|
81
|
+
F('scale', 'formula_version', 'option<string>'),
|
|
82
|
+
F('scale', 'payload', 'object FLEXIBLE'),
|
|
83
|
+
`DEFINE INDEX IF NOT EXISTS scale_story ON scale FIELDS story UNIQUE;`,
|
|
84
|
+
|
|
85
|
+
`DEFINE TABLE IF NOT EXISTS volume SCHEMAFULL;`,
|
|
86
|
+
F('volume', 'story', 'record<story>'),
|
|
87
|
+
F('volume', 'volume_id', 'string'),
|
|
88
|
+
F('volume', 'title', 'option<string>'),
|
|
89
|
+
F('volume', 'chapter_start', 'int'),
|
|
90
|
+
F('volume', 'chapter_end', 'int'),
|
|
91
|
+
F('volume', 'goal', 'option<string>'),
|
|
92
|
+
F('volume', 'core_conflict', 'option<string>'),
|
|
93
|
+
F('volume', 'emotional_arc', 'option<string>'),
|
|
94
|
+
F('volume', 'ending_hook', 'option<string>'),
|
|
95
|
+
F('volume', 'payload', 'object FLEXIBLE'),
|
|
96
|
+
`DEFINE INDEX IF NOT EXISTS volume_id ON volume FIELDS story, volume_id UNIQUE;`,
|
|
97
|
+
|
|
98
|
+
`DEFINE TABLE IF NOT EXISTS act SCHEMAFULL;`,
|
|
99
|
+
F('act', 'story', 'record<story>'),
|
|
100
|
+
F('act', 'act_id', 'string'),
|
|
101
|
+
F('act', 'volume_id', 'string'),
|
|
102
|
+
F('act', 'act_index', 'int'),
|
|
103
|
+
F('act', 'chapter_start', 'int'),
|
|
104
|
+
F('act', 'chapter_end', 'int'),
|
|
105
|
+
F('act', 'goal', 'option<string>'),
|
|
106
|
+
F('act', 'payload', 'object FLEXIBLE'),
|
|
107
|
+
`DEFINE INDEX IF NOT EXISTS act_id ON act FIELDS story, act_id UNIQUE;`,
|
|
108
|
+
|
|
109
|
+
// ------------------------------------------------------------------
|
|
110
|
+
// macro: lines / nodes / endings
|
|
111
|
+
// ------------------------------------------------------------------
|
|
112
|
+
`DEFINE TABLE IF NOT EXISTS line SCHEMAFULL;`,
|
|
113
|
+
F('line', 'story', 'record<story>'),
|
|
114
|
+
F('line', 'line_id', 'string'),
|
|
115
|
+
F('line', 'kind', 'string', " ASSERT $value IN ['main','branch']"),
|
|
116
|
+
F('line', 'name', 'string'),
|
|
117
|
+
F('line', 'payload', 'object FLEXIBLE'),
|
|
118
|
+
`DEFINE INDEX IF NOT EXISTS line_id ON line FIELDS story, line_id UNIQUE;`,
|
|
119
|
+
|
|
120
|
+
`DEFINE TABLE IF NOT EXISTS line_node SCHEMAFULL;`,
|
|
121
|
+
F('line_node', 'story', 'record<story>'),
|
|
122
|
+
F('line_node', 'node_id', 'string'),
|
|
123
|
+
F('line_node', 'line_id', 'string'),
|
|
124
|
+
F('line_node', 'volume', 'option<string>'),
|
|
125
|
+
F('line_node', 'act', 'option<string>'),
|
|
126
|
+
F('line_node', 'goal', 'option<string>'),
|
|
127
|
+
F('line_node', 'turn', 'option<string>'),
|
|
128
|
+
F('line_node', 'payload', 'object FLEXIBLE'),
|
|
129
|
+
`DEFINE INDEX IF NOT EXISTS line_node_id ON line_node FIELDS story, node_id UNIQUE;`,
|
|
130
|
+
|
|
131
|
+
`DEFINE TABLE IF NOT EXISTS ending SCHEMAFULL;`,
|
|
132
|
+
F('ending', 'story', 'record<story>'),
|
|
133
|
+
F('ending', 'ending_id', 'string'),
|
|
134
|
+
F('ending', 'level', 'option<string>'),
|
|
135
|
+
F('ending', 'type', 'option<string>'),
|
|
136
|
+
F('ending', 'direction', 'option<string>'),
|
|
137
|
+
F('ending', 'payload', 'object FLEXIBLE'),
|
|
138
|
+
`DEFINE INDEX IF NOT EXISTS ending_id ON ending FIELDS story, ending_id UNIQUE;`,
|
|
139
|
+
|
|
140
|
+
// ------------------------------------------------------------------
|
|
141
|
+
// cast: entities
|
|
142
|
+
// ------------------------------------------------------------------
|
|
143
|
+
...entity('character', [
|
|
144
|
+
F('character', 'role_slot', 'option<string>'),
|
|
145
|
+
F('character', 'faction', 'option<string>'),
|
|
146
|
+
F('character', 'description', 'option<string>'),
|
|
147
|
+
F('character', 'goal', 'option<string>'),
|
|
148
|
+
F('character', 'flaw', 'option<string>'),
|
|
149
|
+
F('character', 'arc', 'option<string>'),
|
|
150
|
+
F('character', 'traits', 'array'),
|
|
151
|
+
]),
|
|
152
|
+
...entity('faction', [
|
|
153
|
+
F('faction', 'description', 'option<string>'),
|
|
154
|
+
F('faction', 'goal', 'option<string>'),
|
|
155
|
+
F('faction', 'resources', 'option<string>'),
|
|
156
|
+
F('faction', 'territory', 'option<string>'),
|
|
157
|
+
F('faction', 'internal_conflict', 'option<string>'),
|
|
158
|
+
]),
|
|
159
|
+
...entity('location', [
|
|
160
|
+
F('location', 'description', 'option<string>'),
|
|
161
|
+
F('location', 'narrative_function', 'option<string>'),
|
|
162
|
+
F('location', 'rules', 'option<string>'),
|
|
163
|
+
]),
|
|
164
|
+
...entity('item', [
|
|
165
|
+
F('item', 'category', 'option<string>'),
|
|
166
|
+
F('item', 'description', 'option<string>'),
|
|
167
|
+
F('item', 'item_function', 'option<string>'),
|
|
168
|
+
F('item', 'owner', 'option<string>'),
|
|
169
|
+
F('item', 'limit', 'option<string>'),
|
|
170
|
+
]),
|
|
171
|
+
...entity('conflict', [
|
|
172
|
+
F('conflict', 'description', 'option<string>'),
|
|
173
|
+
F('conflict', 'core_goal', 'option<string>'),
|
|
174
|
+
F('conflict', 'stakes', 'option<string>'),
|
|
175
|
+
F('conflict', 'escalation_path', 'array'),
|
|
176
|
+
F('conflict', 'resolution_plan', 'option<object>'),
|
|
177
|
+
F('conflict', 'participants', 'array'),
|
|
178
|
+
]),
|
|
179
|
+
...entity('foreshadow', [
|
|
180
|
+
F('foreshadow', 'setup', 'option<string>'),
|
|
181
|
+
F('foreshadow', 'clues', 'array'),
|
|
182
|
+
F('foreshadow', 'payoff_target', 'option<string>'),
|
|
183
|
+
F('foreshadow', 'recovery_requirement', 'option<string>'),
|
|
184
|
+
F('foreshadow', 'red_herrings', 'array'),
|
|
185
|
+
F('foreshadow', 'scope', 'option<string>'),
|
|
186
|
+
F('foreshadow', 'initial_knowledge', 'array'),
|
|
187
|
+
]),
|
|
188
|
+
...entity('payoff', [
|
|
189
|
+
F('payoff', 'scale', 'option<string>'),
|
|
190
|
+
F('payoff', 'setup', 'option<string>'),
|
|
191
|
+
F('payoff', 'payoff', 'option<string>'),
|
|
192
|
+
F('payoff', 'emotional_effect', 'option<string>'),
|
|
193
|
+
F('payoff', 'required_setup', 'array'),
|
|
194
|
+
]),
|
|
195
|
+
...entity('climax', [
|
|
196
|
+
F('climax', 'description', 'option<string>'),
|
|
197
|
+
F('climax', 'payoff', 'option<string>'),
|
|
198
|
+
F('climax', 'participants', 'array'),
|
|
199
|
+
]),
|
|
200
|
+
|
|
201
|
+
`DEFINE TABLE IF NOT EXISTS information SCHEMAFULL;`,
|
|
202
|
+
F('information', 'story', 'record<story>'),
|
|
203
|
+
F('information', 'entity_id', 'string'),
|
|
204
|
+
F('information', 'info_name', 'string'),
|
|
205
|
+
F('information', 'conflict', 'option<string>'),
|
|
206
|
+
F('information', 'who_knows', 'array'),
|
|
207
|
+
F('information', 'who_does_not_know', 'array'),
|
|
208
|
+
F('information', 'resolution_method', 'option<string>'),
|
|
209
|
+
F('information', 'writing_ban', 'option<string>'),
|
|
210
|
+
F('information', 'initial_knowledge', 'array'),
|
|
211
|
+
F('information', 'payload', 'object FLEXIBLE'),
|
|
212
|
+
`DEFINE INDEX IF NOT EXISTS information_entity ON information FIELDS story, entity_id UNIQUE;`,
|
|
213
|
+
|
|
214
|
+
// ------------------------------------------------------------------
|
|
215
|
+
// cast: graph edges
|
|
216
|
+
// ------------------------------------------------------------------
|
|
217
|
+
`DEFINE TABLE IF NOT EXISTS relation SCHEMAFULL TYPE RELATION IN character OUT character ENFORCED;`,
|
|
218
|
+
F('relation', 'story', 'record<story>'),
|
|
219
|
+
F('relation', 'from_id', 'string'),
|
|
220
|
+
F('relation', 'to_id', 'string'),
|
|
221
|
+
F('relation', 'kind', 'string'),
|
|
222
|
+
F('relation', 'initial_state', 'option<string>'),
|
|
223
|
+
F('relation', 'changes', 'array'),
|
|
224
|
+
F('relation', 'payload', 'object FLEXIBLE'),
|
|
225
|
+
|
|
226
|
+
`DEFINE TABLE IF NOT EXISTS faction_relation SCHEMAFULL TYPE RELATION IN faction OUT faction ENFORCED;`,
|
|
227
|
+
F('faction_relation', 'story', 'record<story>'),
|
|
228
|
+
F('faction_relation', 'from_id', 'string'),
|
|
229
|
+
F('faction_relation', 'to_id', 'string'),
|
|
230
|
+
F('faction_relation', 'kind', 'string'),
|
|
231
|
+
F('faction_relation', 'payload', 'object FLEXIBLE'),
|
|
232
|
+
|
|
233
|
+
`DEFINE TABLE IF NOT EXISTS membership SCHEMAFULL TYPE RELATION IN character OUT faction ENFORCED;`,
|
|
234
|
+
F('membership', 'story', 'record<story>'),
|
|
235
|
+
F('membership', 'role', 'option<string>'),
|
|
236
|
+
F('membership', 'payload', 'object FLEXIBLE'),
|
|
237
|
+
|
|
238
|
+
`DEFINE TABLE IF NOT EXISTS conflict_participant SCHEMAFULL TYPE RELATION IN conflict OUT character ENFORCED;`,
|
|
239
|
+
F('conflict_participant', 'story', 'record<story>'),
|
|
240
|
+
|
|
241
|
+
`DEFINE TABLE IF NOT EXISTS climax_participant SCHEMAFULL TYPE RELATION IN climax OUT character ENFORCED;`,
|
|
242
|
+
F('climax_participant', 'story', 'record<story>'),
|
|
243
|
+
|
|
244
|
+
`DEFINE TABLE IF NOT EXISTS foreshadow_payoff SCHEMAFULL TYPE RELATION IN foreshadow OUT payoff ENFORCED;`,
|
|
245
|
+
F('foreshadow_payoff', 'story', 'record<story>'),
|
|
246
|
+
F('foreshadow_payoff', 'relation', 'option<string>'),
|
|
247
|
+
|
|
248
|
+
// ------------------------------------------------------------------
|
|
249
|
+
// rhythm: chapter + actions
|
|
250
|
+
// ------------------------------------------------------------------
|
|
251
|
+
`DEFINE TABLE IF NOT EXISTS chapter SCHEMAFULL;`,
|
|
252
|
+
F('chapter', 'story', 'record<story>'),
|
|
253
|
+
F('chapter', 'chapter_id', 'string'),
|
|
254
|
+
F('chapter', 'display_no', 'int'),
|
|
255
|
+
F('chapter', 'title', 'string'),
|
|
256
|
+
F('chapter', 'volume', 'string'),
|
|
257
|
+
F('chapter', 'act', 'string'),
|
|
258
|
+
F('chapter', 'pov', 'option<string>'),
|
|
259
|
+
F('chapter', 'word_target', 'int'),
|
|
260
|
+
F('chapter', 'emotion_value', 'option<int>'),
|
|
261
|
+
F('chapter', 'hook_type', 'option<string>'),
|
|
262
|
+
F('chapter', 'hook_strength', 'option<string>'),
|
|
263
|
+
F('chapter', 'time', 'object FLEXIBLE'),
|
|
264
|
+
F('chapter', 'why', 'object FLEXIBLE'),
|
|
265
|
+
F('chapter', 'how', 'object FLEXIBLE'),
|
|
266
|
+
F('chapter', 'emotion', 'object FLEXIBLE'),
|
|
267
|
+
F('chapter', 'hook', 'object FLEXIBLE'),
|
|
268
|
+
F('chapter', 'payload', 'object FLEXIBLE'),
|
|
269
|
+
`DEFINE INDEX IF NOT EXISTS chapter_no ON chapter FIELDS story, display_no UNIQUE;`,
|
|
270
|
+
`DEFINE INDEX IF NOT EXISTS chapter_id ON chapter FIELDS story, chapter_id UNIQUE;`,
|
|
271
|
+
|
|
272
|
+
`DEFINE TABLE IF NOT EXISTS chapter_cast SCHEMAFULL TYPE RELATION IN chapter OUT character ENFORCED;`,
|
|
273
|
+
F('chapter_cast', 'story', 'record<story>'),
|
|
274
|
+
F('chapter_cast', 'role', 'string', " ASSERT $value IN ['pov','cast','mentioned']"),
|
|
275
|
+
|
|
276
|
+
`DEFINE TABLE IF NOT EXISTS chapter_location SCHEMAFULL TYPE RELATION IN chapter OUT location ENFORCED;`,
|
|
277
|
+
F('chapter_location', 'story', 'record<story>'),
|
|
278
|
+
|
|
279
|
+
`DEFINE TABLE IF NOT EXISTS foreshadow_action SCHEMAFULL TYPE RELATION IN chapter OUT foreshadow ENFORCED;`,
|
|
280
|
+
F('foreshadow_action', 'story', 'record<story>'),
|
|
281
|
+
F('foreshadow_action', 'chapter_no', 'int'),
|
|
282
|
+
F('foreshadow_action', 'foreshadow_id', 'string'),
|
|
283
|
+
F('foreshadow_action', 'action', 'string', " ASSERT $value IN ['埋设','回收','推进','强化']"),
|
|
284
|
+
F('foreshadow_action', 'detail', 'option<string>'),
|
|
285
|
+
F('foreshadow_action', 'payload', 'object FLEXIBLE'),
|
|
286
|
+
|
|
287
|
+
`DEFINE TABLE IF NOT EXISTS payoff_action SCHEMAFULL TYPE RELATION IN chapter OUT payoff ENFORCED;`,
|
|
288
|
+
F('payoff_action', 'story', 'record<story>'),
|
|
289
|
+
F('payoff_action', 'chapter_no', 'int'),
|
|
290
|
+
F('payoff_action', 'payoff_id', 'string'),
|
|
291
|
+
F('payoff_action', 'action', 'string', " ASSERT $value IN ['铺垫','兑现','回收']"),
|
|
292
|
+
F('payoff_action', 'detail', 'option<string>'),
|
|
293
|
+
F('payoff_action', 'payload', 'object FLEXIBLE'),
|
|
294
|
+
|
|
295
|
+
`DEFINE TABLE IF NOT EXISTS info_action SCHEMAFULL TYPE RELATION IN chapter OUT information ENFORCED;`,
|
|
296
|
+
F('info_action', 'story', 'record<story>'),
|
|
297
|
+
F('info_action', 'chapter_no', 'int'),
|
|
298
|
+
F('info_action', 'info_id', 'string'),
|
|
299
|
+
F('info_action', 'state', 'string', " ASSERT $value IN ['created','progressed','resolved']"),
|
|
300
|
+
F('info_action', 'detail', 'option<string>'),
|
|
301
|
+
F('info_action', 'payload', 'object FLEXIBLE'),
|
|
302
|
+
|
|
303
|
+
`DEFINE TABLE IF NOT EXISTS ending_action SCHEMAFULL TYPE RELATION IN chapter OUT ending ENFORCED;`,
|
|
304
|
+
F('ending_action', 'story', 'record<story>'),
|
|
305
|
+
F('ending_action', 'chapter_no', 'int'),
|
|
306
|
+
F('ending_action', 'ending_id', 'string'),
|
|
307
|
+
F('ending_action', 'action', 'string', " ASSERT $value IN ['收束']"),
|
|
308
|
+
F('ending_action', 'detail', 'option<string>'),
|
|
309
|
+
F('ending_action', 'payload', 'object FLEXIBLE'),
|
|
310
|
+
|
|
311
|
+
// ------------------------------------------------------------------
|
|
312
|
+
// rhythm: knowledge timeline
|
|
313
|
+
// ------------------------------------------------------------------
|
|
314
|
+
`DEFINE TABLE IF NOT EXISTS knowledge_event SCHEMAFULL;`,
|
|
315
|
+
F('knowledge_event', 'story', 'record<story>'),
|
|
316
|
+
F('knowledge_event', 'chapter', 'record<chapter>'),
|
|
317
|
+
F('knowledge_event', 'chapter_no', 'int'),
|
|
318
|
+
F('knowledge_event', 'subject_type', 'string', " ASSERT $value IN ['information','foreshadow']"),
|
|
319
|
+
F('knowledge_event', 'subject_id', 'string'),
|
|
320
|
+
F('knowledge_event', 'character', 'record<character>'),
|
|
321
|
+
F('knowledge_event', 'character_id', 'string'),
|
|
322
|
+
F('knowledge_event', 'change', 'string', " ASSERT $value IN ['learns','partially_learns','confirms','corrects','forgets','misinforms']"),
|
|
323
|
+
F('knowledge_event', 'coverage', 'string', " ASSERT $value IN ['unknown','partial','full','false']"),
|
|
324
|
+
F('knowledge_event', 'knowledge', 'string'),
|
|
325
|
+
F('knowledge_event', 'how', 'string', " ASSERT $value IN ['witnessed','told','inferred','document','rumor','experience','other']"),
|
|
326
|
+
F('knowledge_event', 'source_character', 'option<record<character>>'),
|
|
327
|
+
F('knowledge_event', 'scene', 'string'),
|
|
328
|
+
F('knowledge_event', 'evidence', 'string'),
|
|
329
|
+
F('knowledge_event', 'payload', 'object FLEXIBLE'),
|
|
330
|
+
|
|
331
|
+
`DEFINE TABLE IF NOT EXISTS knowledge_state SCHEMAFULL;`,
|
|
332
|
+
F('knowledge_state', 'story', 'record<story>'),
|
|
333
|
+
F('knowledge_state', 'character', 'record<character>'),
|
|
334
|
+
F('knowledge_state', 'subject_type', 'string'),
|
|
335
|
+
F('knowledge_state', 'subject_id', 'string'),
|
|
336
|
+
F('knowledge_state', 'state', 'string', " ASSERT $value IN ['unknown','partial','full','false']"),
|
|
337
|
+
`DEFINE INDEX IF NOT EXISTS knowledge_state_key ON knowledge_state FIELDS story, character, subject_type, subject_id UNIQUE;`,
|
|
338
|
+
|
|
339
|
+
`DEFINE EVENT OVERWRITE knowledge_rules ON TABLE knowledge_event WHEN $event = 'CREATE' THEN {
|
|
340
|
+
IF $after.change = 'confirms' AND $after.coverage != 'full' { THROW 'confirms 必须配 coverage=full'; };
|
|
341
|
+
IF $after.change = 'partially_learns' AND $after.coverage != 'partial' { THROW 'partially_learns 必须配 coverage=partial'; };
|
|
342
|
+
IF $after.change = 'forgets' AND $after.coverage != 'unknown' { THROW 'forgets 必须配 coverage=unknown'; };
|
|
343
|
+
IF $after.change = 'misinforms' AND $after.coverage != 'false' { THROW 'misinforms 必须配 coverage=false'; };
|
|
344
|
+
IF $after.how = 'told' AND $after.source_character = NONE { THROW 'how=told 必须写 source_character'; };
|
|
345
|
+
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; };
|
|
346
|
+
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; };
|
|
347
|
+
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;
|
|
348
|
+
LET $from = IF array::len($prev) > 0 { $prev[0] } ELSE { 'unknown' };
|
|
349
|
+
IF $from = 'full' AND $after.change IN ['learns','partially_learns'] { THROW '已 full,不能再写成 learns/partially_learns:' + $after.character_id + ' / ' + $after.subject_id; };
|
|
350
|
+
IF $from = 'false' AND $after.change IN ['learns','partially_learns','confirms'] { THROW '之前是误信,必须先 corrects:' + $after.character_id + ' / ' + $after.subject_id; };
|
|
351
|
+
IF $from = 'unknown' AND $after.change = 'confirms' { THROW '尚未学会,不能直接 confirms:' + $after.character_id + ' / ' + $after.subject_id; };
|
|
352
|
+
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;
|
|
353
|
+
};`,
|
|
354
|
+
|
|
355
|
+
// ------------------------------------------------------------------
|
|
356
|
+
// rhythm: time series
|
|
357
|
+
// ------------------------------------------------------------------
|
|
358
|
+
`DEFINE TABLE IF NOT EXISTS chapter_metric SCHEMAFULL;`,
|
|
359
|
+
F('chapter_metric', 'story', 'record<story>'),
|
|
360
|
+
F('chapter_metric', 'chapter', 'record<chapter>'),
|
|
361
|
+
F('chapter_metric', 'chapter_no', 'int'),
|
|
362
|
+
F('chapter_metric', 'emotion_value', 'option<int>'),
|
|
363
|
+
F('chapter_metric', 'hook_strength', 'option<string>'),
|
|
364
|
+
F('chapter_metric', 'word_target', 'int'),
|
|
365
|
+
F('chapter_metric', 'word_count', 'option<int>'),
|
|
366
|
+
`DEFINE INDEX IF NOT EXISTS chapter_metric_key ON chapter_metric FIELDS story, chapter_no UNIQUE;`,
|
|
367
|
+
|
|
368
|
+
// ------------------------------------------------------------------
|
|
369
|
+
// outline / writing
|
|
370
|
+
// ------------------------------------------------------------------
|
|
371
|
+
`DEFINE TABLE IF NOT EXISTS outline SCHEMAFULL;`,
|
|
372
|
+
F('outline', 'story', 'record<story>'),
|
|
373
|
+
F('outline', 'chapter', 'record<chapter>'),
|
|
374
|
+
F('outline', 'chapter_id', 'string'),
|
|
375
|
+
F('outline', 'payload', 'object FLEXIBLE'),
|
|
376
|
+
`DEFINE INDEX IF NOT EXISTS outline_chapter ON outline FIELDS story, chapter_id UNIQUE;`,
|
|
377
|
+
|
|
378
|
+
`DEFINE TABLE IF NOT EXISTS chapter_text SCHEMAFULL;`,
|
|
379
|
+
F('chapter_text', 'story', 'record<story>'),
|
|
380
|
+
F('chapter_text', 'chapter', 'record<chapter>'),
|
|
381
|
+
F('chapter_text', 'chapter_id', 'string'),
|
|
382
|
+
F('chapter_text', 'display_no', 'int'),
|
|
383
|
+
F('chapter_text', 'content', 'string'),
|
|
384
|
+
F('chapter_text', 'word_count', 'int'),
|
|
385
|
+
F('chapter_text', 'updated_at', 'datetime', ' VALUE time::now()'),
|
|
386
|
+
`DEFINE INDEX IF NOT EXISTS chapter_text_key ON chapter_text FIELDS story, chapter_id UNIQUE;`,
|
|
387
|
+
|
|
388
|
+
`DEFINE TABLE IF NOT EXISTS ledger_entry SCHEMAFULL;`,
|
|
389
|
+
F('ledger_entry', 'story', 'record<story>'),
|
|
390
|
+
F('ledger_entry', 'chapter', 'record<chapter>'),
|
|
391
|
+
F('ledger_entry', 'chapter_id', 'string'),
|
|
392
|
+
F('ledger_entry', 'payload', 'object FLEXIBLE'),
|
|
393
|
+
`DEFINE INDEX IF NOT EXISTS ledger_entry_key ON ledger_entry FIELDS story, chapter_id UNIQUE;`,
|
|
394
|
+
|
|
395
|
+
// ------------------------------------------------------------------
|
|
396
|
+
// audit
|
|
397
|
+
// ------------------------------------------------------------------
|
|
398
|
+
`DEFINE TABLE IF NOT EXISTS decision_log SCHEMAFULL;`,
|
|
399
|
+
F('decision_log', 'story', 'record<story>'),
|
|
400
|
+
F('decision_log', 'stage', 'string'),
|
|
401
|
+
F('decision_log', 'scope', 'option<string>'),
|
|
402
|
+
F('decision_log', 'item', 'option<string>'),
|
|
403
|
+
F('decision_log', 'text', 'string'),
|
|
404
|
+
F('decision_log', 'source_path', 'option<string>'),
|
|
405
|
+
F('decision_log', 'reference', 'option<string>'),
|
|
406
|
+
F('decision_log', 'reason', 'option<string>'),
|
|
407
|
+
F('decision_log', 'created_at', 'datetime', ' VALUE time::now()'),
|
|
408
|
+
|
|
409
|
+
`DEFINE TABLE IF NOT EXISTS validation_issue SCHEMAFULL;`,
|
|
410
|
+
F('validation_issue', 'story', 'record<story>'),
|
|
411
|
+
F('validation_issue', 'stage', 'string'),
|
|
412
|
+
F('validation_issue', 'code', 'string'),
|
|
413
|
+
F('validation_issue', 'severity', 'string', " ASSERT $value IN ['error','warning']"),
|
|
414
|
+
F('validation_issue', 'path', 'option<string>'),
|
|
415
|
+
F('validation_issue', 'message', 'string'),
|
|
416
|
+
F('validation_issue', 'created_at', 'datetime', ' VALUE time::now()'),
|
|
417
|
+
];
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stage command wrapper: database is the truth, stage JSON is a projection.
|
|
3
|
+
*
|
|
4
|
+
* 1. open the project database
|
|
5
|
+
* 2. project existing DB data into `设定/...` so prompts/validators see it
|
|
6
|
+
* 3. run the existing stage handler (the LLM writes JSON)
|
|
7
|
+
* 4. import the JSON into SurrealDB in one transaction
|
|
8
|
+
* 5. if the DB rejects it, delete only the offending files and retry
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import fs from 'fs';
|
|
12
|
+
import path from 'path';
|
|
13
|
+
import { notify } from '#infra';
|
|
14
|
+
import {
|
|
15
|
+
dbErrorMessage,
|
|
16
|
+
ensureStory,
|
|
17
|
+
openStoryDb,
|
|
18
|
+
setStageState,
|
|
19
|
+
StoryDbError,
|
|
20
|
+
} from './client.js';
|
|
21
|
+
import { importStage } from './import.js';
|
|
22
|
+
import { projectForStage, writeStageStatus } from './project.js';
|
|
23
|
+
|
|
24
|
+
export function isStoryDbProject(projectRoot) {
|
|
25
|
+
try {
|
|
26
|
+
return fs.existsSync(path.join(projectRoot, '.fancy-storydb'));
|
|
27
|
+
} catch {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const STAGE_STATE = {
|
|
33
|
+
'10-scale': '10-scale',
|
|
34
|
+
'20-macro': '20-macro',
|
|
35
|
+
'30-cast': '30-cast',
|
|
36
|
+
'40-rhythm': '40-rhythm',
|
|
37
|
+
outline: '40-rhythm',
|
|
38
|
+
'50-writing': '50-writing',
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const CAST_FILES = {
|
|
42
|
+
character: 'characters.json',
|
|
43
|
+
faction: 'factions.json',
|
|
44
|
+
location: 'locations.json',
|
|
45
|
+
item: 'items.json',
|
|
46
|
+
conflict: 'conflicts.json',
|
|
47
|
+
foreshadow: 'foreshadows.json',
|
|
48
|
+
climax: 'climaxes.json',
|
|
49
|
+
payoff: 'payoffs.json',
|
|
50
|
+
information: 'information.json',
|
|
51
|
+
relation: 'relations.json',
|
|
52
|
+
faction_relation: 'relations.json',
|
|
53
|
+
membership: 'relations.json',
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const MACRO_FILES = ['world', 'story', 'volumes', 'lines', 'endings', 'emotion', 'roles'];
|
|
57
|
+
|
|
58
|
+
function stageDir(projectRoot, stageId) {
|
|
59
|
+
return path.join(projectRoot, '设定', stageId === 'outline' ? '40-rhythm' : stageId);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function chapterFromError(message) {
|
|
63
|
+
const out = [];
|
|
64
|
+
for (const match of String(message).matchAll(/第\s*(\d+)\s*章/g)) {
|
|
65
|
+
out.push(Number(match[1]));
|
|
66
|
+
}
|
|
67
|
+
for (const match of String(message).matchAll(/chapters\[(\d+)\]/g)) {
|
|
68
|
+
out.push(Number(match[1]) + 1);
|
|
69
|
+
}
|
|
70
|
+
return [...new Set(out)];
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function chunkFileForChapter(projectRoot, chapterNo) {
|
|
74
|
+
const dir = path.join(stageDir(projectRoot, '40-rhythm'), '.chapter-map');
|
|
75
|
+
if (!fs.existsSync(dir)) return null;
|
|
76
|
+
for (const name of fs.readdirSync(dir)) {
|
|
77
|
+
if (!/^V\d{3}-\d{2}\.json$/.test(name)) continue;
|
|
78
|
+
try {
|
|
79
|
+
const data = JSON.parse(fs.readFileSync(path.join(dir, name), 'utf8'));
|
|
80
|
+
if ((data.chapters || []).some((chapter) => Number(chapter.display_no) === chapterNo)) {
|
|
81
|
+
return path.join(dir, name);
|
|
82
|
+
}
|
|
83
|
+
} catch {
|
|
84
|
+
// unreadable shard: leave it for the stage validator
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Remove the smallest set of generated files that can explain a DB rejection.
|
|
92
|
+
* The existing stage handler regenerates missing files on the next attempt.
|
|
93
|
+
*/
|
|
94
|
+
export function removeBadFiles(err, stageId, projectRoot) {
|
|
95
|
+
const message = (err && (err.message || '') ) + ' ' + (err && err.detail ? JSON.stringify(err.detail) : '');
|
|
96
|
+
const removed = [];
|
|
97
|
+
const drop = (file) => {
|
|
98
|
+
try {
|
|
99
|
+
if (file && fs.existsSync(file)) {
|
|
100
|
+
fs.unlinkSync(file);
|
|
101
|
+
removed.push(file);
|
|
102
|
+
}
|
|
103
|
+
} catch {
|
|
104
|
+
// best effort
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
const chapters = chapterFromError(message);
|
|
109
|
+
if (chapters.length && (stageId === '40-rhythm' || stageId === 'outline')) {
|
|
110
|
+
for (const chapterNo of chapters) {
|
|
111
|
+
const chunk = chunkFileForChapter(projectRoot, chapterNo);
|
|
112
|
+
if (chunk) drop(chunk);
|
|
113
|
+
const outline = path.join(stageDir(projectRoot, '40-rhythm'), 'outlines', 'CH' + String(chapterNo).padStart(3, '0') + '.json');
|
|
114
|
+
if (stageId === 'outline') drop(outline);
|
|
115
|
+
}
|
|
116
|
+
if (stageId === '40-rhythm') {
|
|
117
|
+
drop(path.join(stageDir(projectRoot, '40-rhythm'), 'chapter_map.json'));
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (stageId === '30-cast') {
|
|
122
|
+
for (const [table, file] of Object.entries(CAST_FILES)) {
|
|
123
|
+
if (new RegExp('\\b' + table + '\\b').test(message)) {
|
|
124
|
+
drop(path.join(stageDir(projectRoot, '30-cast'), file));
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (stageId === '20-macro') {
|
|
130
|
+
for (const name of MACRO_FILES) {
|
|
131
|
+
if (new RegExp('\\b' + name + '\\b').test(message)) {
|
|
132
|
+
drop(path.join(stageDir(projectRoot, '20-macro'), name + '.json'));
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
if (!removed.length) drop(path.join(stageDir(projectRoot, '20-macro'), 'volumes.json'));
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (stageId === '10-scale') {
|
|
139
|
+
drop(path.join(stageDir(projectRoot, '10-scale'), 'scale.json'));
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (stageId === '50-writing') {
|
|
143
|
+
for (const chapterNo of chapters) {
|
|
144
|
+
drop(path.join(stageDir(projectRoot, '50-writing'), 'chapters', '第' + String(chapterNo).padStart(3, '0') + '章.md'));
|
|
145
|
+
}
|
|
146
|
+
drop(path.join(stageDir(projectRoot, '50-writing'), 'ledger.json'));
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return [...new Set(removed)];
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function storyInfo(topicData) {
|
|
153
|
+
const topic = topicData || {};
|
|
154
|
+
return {
|
|
155
|
+
title: topic.title || topic.book_name || topic.name || undefined,
|
|
156
|
+
genre: topic.genre || topic.type || undefined,
|
|
157
|
+
platform: topic.platform || undefined,
|
|
158
|
+
target_chapters: Number(topic.target_chapters) || undefined,
|
|
159
|
+
target_words: Number(topic.target_words) || undefined,
|
|
160
|
+
avg_words_per_chapter: Number(topic.avg_words_per_chapter) || undefined,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
async function markState(sdb, projectRoot, stageId, status, errors = [], warnings = []) {
|
|
165
|
+
const stateStage = STAGE_STATE[stageId] || stageId;
|
|
166
|
+
await setStageState(sdb, stateStage, status, errors, warnings);
|
|
167
|
+
if (stateStage !== 'outline') {
|
|
168
|
+
await writeStageStatus(sdb, projectRoot, stateStage);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Run a stage handler against the project database.
|
|
174
|
+
*
|
|
175
|
+
* The handler is the existing file-based implementation. It is treated as a
|
|
176
|
+
* pure "generate files" function; this wrapper owns persistence.
|
|
177
|
+
*/
|
|
178
|
+
export async function runWithStoryDb(opts) {
|
|
179
|
+
const {
|
|
180
|
+
session,
|
|
181
|
+
projectRoot,
|
|
182
|
+
stageId,
|
|
183
|
+
topicData,
|
|
184
|
+
handler,
|
|
185
|
+
maxAttempts = 2,
|
|
186
|
+
} = opts || {};
|
|
187
|
+
|
|
188
|
+
let sdb;
|
|
189
|
+
try {
|
|
190
|
+
sdb = await openStoryDb(projectRoot);
|
|
191
|
+
} catch (err) {
|
|
192
|
+
if (err instanceof StoryDbError && err.code === 'STORYDB_LOCKED') {
|
|
193
|
+
return notify(session, 'error', err.message + '\n\n请先关闭另一个正在使用同一项目的会话。');
|
|
194
|
+
}
|
|
195
|
+
return notify(session, 'error', '故事数据库打开失败:' + dbErrorMessage(err));
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
try {
|
|
199
|
+
await ensureStory(sdb, storyInfo(topicData));
|
|
200
|
+
await projectForStage(sdb, stageId, projectRoot);
|
|
201
|
+
|
|
202
|
+
let lastError = null;
|
|
203
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
|
|
204
|
+
const result = await handler();
|
|
205
|
+
if (result && result.kind === 'error') {
|
|
206
|
+
await markState(sdb, projectRoot, stageId, 'failed', [{ code: 'STAGE_FAILED', message: result.text }]);
|
|
207
|
+
return result;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
try {
|
|
211
|
+
await sdb.tx(async () => {
|
|
212
|
+
await importStage(sdb, stageId, projectRoot);
|
|
213
|
+
});
|
|
214
|
+
await markState(sdb, projectRoot, stageId, 'passed');
|
|
215
|
+
return result;
|
|
216
|
+
} catch (err) {
|
|
217
|
+
lastError = err;
|
|
218
|
+
const message = dbErrorMessage(err);
|
|
219
|
+
const detail = err && err.detail ? err.detail : null;
|
|
220
|
+
const removed = removeBadFiles(err, stageId, projectRoot);
|
|
221
|
+
await markState(sdb, projectRoot, stageId, 'failed', [{
|
|
222
|
+
code: err && err.code ? err.code : 'STORYDB_IMPORT',
|
|
223
|
+
message,
|
|
224
|
+
detail,
|
|
225
|
+
}]);
|
|
226
|
+
if (!removed.length || attempt >= maxAttempts) {
|
|
227
|
+
return notify(session, 'error',
|
|
228
|
+
'❌ 数据库拒绝了本阶段的产物,已回滚:\n' + message +
|
|
229
|
+
(removed.length ? '\n\n已删除待重生成的文件:\n- ' + removed.join('\n- ') : '') +
|
|
230
|
+
'\n\n数据未写入数据库;修复后重新运行本阶段即可。');
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
return notify(session, 'error', '❌ 数据库写入失败:' + dbErrorMessage(lastError));
|
|
235
|
+
} finally {
|
|
236
|
+
await sdb.close();
|
|
237
|
+
}
|
|
238
|
+
}
|