@zwbigi/ink-xy 0.1.2 → 0.1.4
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/.next/BUILD_ID +1 -1
- package/.next/app-path-routes-manifest.json +2 -2
- package/.next/build-manifest.json +2 -2
- package/.next/server/app/_global-error.html +1 -1
- package/.next/server/app/_global-error.rsc +1 -1
- package/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/.next/server/app/_not-found.html +1 -1
- package/.next/server/app/_not-found.rsc +1 -1
- package/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
- package/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
- package/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
- package/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
- package/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
- package/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
- package/.next/server/app/api/inkos/route.js.nft.json +1 -1
- package/.next/server/app/api/skills/install/route.js.nft.json +1 -1
- package/.next/server/app/api/skills/search/route.js.nft.json +1 -1
- package/.next/server/app/index.html +1 -1
- package/.next/server/app/index.rsc +1 -1
- package/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
- package/.next/server/app/index.segments/_full.segment.rsc +1 -1
- package/.next/server/app/index.segments/_head.segment.rsc +1 -1
- package/.next/server/app/index.segments/_index.segment.rsc +1 -1
- package/.next/server/app/index.segments/_tree.segment.rsc +1 -1
- package/.next/server/app-paths-manifest.json +2 -2
- package/.next/server/chunks/162.js +1 -1
- package/.next/server/middleware-build-manifest.js +1 -1
- package/.next/server/pages/404.html +1 -1
- package/.next/server/pages/500.html +1 -1
- package/.next/trace +4 -4
- package/.next/trace-build +1 -1
- package/bin/pi-web.js +4 -1
- package/inkos/.env.example +20 -0
- package/inkos/.node-version +1 -0
- package/inkos/.nvmrc +1 -0
- package/inkos/CHANGELOG.md +787 -0
- package/inkos/CONTRIBUTING.md +89 -0
- package/inkos/LICENSE +661 -0
- package/inkos/README.en.md +483 -0
- package/inkos/README.ja.md +461 -0
- package/inkos/README.md +272 -0
- package/inkos/assets/15qun.jpg +0 -0
- package/inkos/assets/41777702961_.pic.jpg +0 -0
- package/inkos/assets/inkos-short-demo-cover.png +0 -0
- package/inkos/assets/inkos-text.svg +40 -0
- package/inkos/assets/logo.svg +47 -0
- package/inkos/assets/screenshot-chapters.png +0 -0
- package/inkos/assets/screenshot-pipeline.png +0 -0
- package/inkos/assets/screenshot-state.png +0 -0
- package/inkos/assets/screenshot-terminal.png +0 -0
- package/inkos/assets/wechat-group-v8.jpg +0 -0
- package/inkos/package.json +42 -0
- package/inkos/packages/cli/package.json +74 -0
- package/inkos/packages/cli/src/__tests__/analytics.test.ts +154 -0
- package/inkos/packages/cli/src/__tests__/cli-integration.test.ts +1031 -0
- package/inkos/packages/cli/src/__tests__/daemon.test.ts +93 -0
- package/inkos/packages/cli/src/__tests__/doctor.test.ts +36 -0
- package/inkos/packages/cli/src/__tests__/interact-command.test.ts +142 -0
- package/inkos/packages/cli/src/__tests__/interaction-tools.test.ts +107 -0
- package/inkos/packages/cli/src/__tests__/llm-overrides.test.ts +25 -0
- package/inkos/packages/cli/src/__tests__/localization.test.ts +121 -0
- package/inkos/packages/cli/src/__tests__/progress-text.test.ts +92 -0
- package/inkos/packages/cli/src/__tests__/project-bootstrap.test.ts +71 -0
- package/inkos/packages/cli/src/__tests__/publish-package.test.ts +272 -0
- package/inkos/packages/cli/src/__tests__/revision-command.test.ts +82 -0
- package/inkos/packages/cli/src/__tests__/runtime-requirements.test.ts +89 -0
- package/inkos/packages/cli/src/__tests__/short-fiction-command.test.ts +48 -0
- package/inkos/packages/cli/src/__tests__/studio-runtime.test.ts +142 -0
- package/inkos/packages/cli/src/__tests__/studio.test.ts +87 -0
- package/inkos/packages/cli/src/__tests__/tui-activity-state.test.ts +20 -0
- package/inkos/packages/cli/src/__tests__/tui-agent-session.test.ts +213 -0
- package/inkos/packages/cli/src/__tests__/tui-chat-depth.test.ts +27 -0
- package/inkos/packages/cli/src/__tests__/tui-chat-draft.test.ts +44 -0
- package/inkos/packages/cli/src/__tests__/tui-command.test.ts +86 -0
- package/inkos/packages/cli/src/__tests__/tui-composer-caret.test.ts +46 -0
- package/inkos/packages/cli/src/__tests__/tui-composer-display.test.ts +40 -0
- package/inkos/packages/cli/src/__tests__/tui-dashboard.test.tsx +219 -0
- package/inkos/packages/cli/src/__tests__/tui-effects-i18n.test.ts +29 -0
- package/inkos/packages/cli/src/__tests__/tui-i18n.test.ts +22 -0
- package/inkos/packages/cli/src/__tests__/tui-input-chrome.test.ts +10 -0
- package/inkos/packages/cli/src/__tests__/tui-input-history.test.ts +40 -0
- package/inkos/packages/cli/src/__tests__/tui-layout.test.ts +55 -0
- package/inkos/packages/cli/src/__tests__/tui-local-commands.test.ts +47 -0
- package/inkos/packages/cli/src/__tests__/tui-session-store.test.ts +81 -0
- package/inkos/packages/cli/src/__tests__/tui-setup-i18n.test.ts +31 -0
- package/inkos/packages/cli/src/__tests__/tui-slash-autocomplete.test.ts +33 -0
- package/inkos/packages/cli/src/commands/agent.ts +65 -0
- package/inkos/packages/cli/src/commands/analytics.ts +77 -0
- package/inkos/packages/cli/src/commands/audit.ts +52 -0
- package/inkos/packages/cli/src/commands/book.ts +260 -0
- package/inkos/packages/cli/src/commands/compose.ts +50 -0
- package/inkos/packages/cli/src/commands/config.ts +328 -0
- package/inkos/packages/cli/src/commands/consolidate.ts +50 -0
- package/inkos/packages/cli/src/commands/daemon.ts +121 -0
- package/inkos/packages/cli/src/commands/detect.ts +125 -0
- package/inkos/packages/cli/src/commands/doctor.ts +391 -0
- package/inkos/packages/cli/src/commands/draft.ts +43 -0
- package/inkos/packages/cli/src/commands/eval.ts +217 -0
- package/inkos/packages/cli/src/commands/export.ts +45 -0
- package/inkos/packages/cli/src/commands/fanfic.ts +183 -0
- package/inkos/packages/cli/src/commands/genre.ts +160 -0
- package/inkos/packages/cli/src/commands/import.ts +158 -0
- package/inkos/packages/cli/src/commands/init.ts +47 -0
- package/inkos/packages/cli/src/commands/interact.ts +109 -0
- package/inkos/packages/cli/src/commands/plan.ts +54 -0
- package/inkos/packages/cli/src/commands/radar.ts +60 -0
- package/inkos/packages/cli/src/commands/review.ts +253 -0
- package/inkos/packages/cli/src/commands/revise.ts +58 -0
- package/inkos/packages/cli/src/commands/short-fiction.ts +294 -0
- package/inkos/packages/cli/src/commands/status.ts +138 -0
- package/inkos/packages/cli/src/commands/studio.ts +194 -0
- package/inkos/packages/cli/src/commands/style.ts +99 -0
- package/inkos/packages/cli/src/commands/tui.ts +18 -0
- package/inkos/packages/cli/src/commands/update.ts +45 -0
- package/inkos/packages/cli/src/commands/write.ts +324 -0
- package/inkos/packages/cli/src/index.ts +5 -0
- package/inkos/packages/cli/src/interaction/tools.ts +49 -0
- package/inkos/packages/cli/src/localization.ts +215 -0
- package/inkos/packages/cli/src/program.ts +106 -0
- package/inkos/packages/cli/src/progress-text.ts +85 -0
- package/inkos/packages/cli/src/project-bootstrap.ts +175 -0
- package/inkos/packages/cli/src/runtime-requirements.ts +135 -0
- package/inkos/packages/cli/src/tui/__tests__/markdown.test.ts +64 -0
- package/inkos/packages/cli/src/tui/activity-state.ts +41 -0
- package/inkos/packages/cli/src/tui/agent-input.ts +264 -0
- package/inkos/packages/cli/src/tui/ansi.ts +72 -0
- package/inkos/packages/cli/src/tui/app.ts +130 -0
- package/inkos/packages/cli/src/tui/chat-depth.ts +22 -0
- package/inkos/packages/cli/src/tui/chat-draft.ts +42 -0
- package/inkos/packages/cli/src/tui/composer-caret.ts +22 -0
- package/inkos/packages/cli/src/tui/composer-display.ts +26 -0
- package/inkos/packages/cli/src/tui/dashboard-model.ts +164 -0
- package/inkos/packages/cli/src/tui/dashboard.tsx +544 -0
- package/inkos/packages/cli/src/tui/effects.ts +542 -0
- package/inkos/packages/cli/src/tui/i18n.ts +278 -0
- package/inkos/packages/cli/src/tui/input-history.ts +69 -0
- package/inkos/packages/cli/src/tui/local-commands.ts +55 -0
- package/inkos/packages/cli/src/tui/markdown.ts +64 -0
- package/inkos/packages/cli/src/tui/session-store.ts +6 -0
- package/inkos/packages/cli/src/tui/setup.ts +397 -0
- package/inkos/packages/cli/src/tui/slash-autocomplete.ts +62 -0
- package/inkos/packages/cli/src/tui/theme.ts +17 -0
- package/inkos/packages/cli/src/utils.ts +222 -0
- package/inkos/packages/cli/tsconfig.json +9 -0
- package/inkos/packages/core/genres/cozy.md +43 -0
- package/inkos/packages/core/genres/cultivation.md +42 -0
- package/inkos/packages/core/genres/dungeon-core.md +40 -0
- package/inkos/packages/core/genres/horror.md +51 -0
- package/inkos/packages/core/genres/isekai.md +43 -0
- package/inkos/packages/core/genres/litrpg.md +43 -0
- package/inkos/packages/core/genres/other.md +24 -0
- package/inkos/packages/core/genres/progression.md +41 -0
- package/inkos/packages/core/genres/romantasy.md +45 -0
- package/inkos/packages/core/genres/sci-fi.md +42 -0
- package/inkos/packages/core/genres/system-apocalypse.md +40 -0
- package/inkos/packages/core/genres/tower-climber.md +41 -0
- package/inkos/packages/core/genres/urban.md +53 -0
- package/inkos/packages/core/genres/xianxia.md +46 -0
- package/inkos/packages/core/genres/xuanhuan.md +64 -0
- package/inkos/packages/core/package.json +61 -0
- package/inkos/packages/core/src/__tests__/agent-max-tokens-policy.test.ts +29 -0
- package/inkos/packages/core/src/__tests__/agent-session.test.ts +866 -0
- package/inkos/packages/core/src/__tests__/agent-system-prompt.test.ts +167 -0
- package/inkos/packages/core/src/__tests__/agent-tools-params.test.ts +197 -0
- package/inkos/packages/core/src/__tests__/agent-tools.test.ts +421 -0
- package/inkos/packages/core/src/__tests__/ai-tells.test.ts +90 -0
- package/inkos/packages/core/src/__tests__/architect-phase5-consolidated.test.ts +445 -0
- package/inkos/packages/core/src/__tests__/architect-phase5.test.ts +455 -0
- package/inkos/packages/core/src/__tests__/architect-phase7.test.ts +210 -0
- package/inkos/packages/core/src/__tests__/architect.test.ts +859 -0
- package/inkos/packages/core/src/__tests__/audit-parse.test.ts +78 -0
- package/inkos/packages/core/src/__tests__/book-id.test.ts +26 -0
- package/inkos/packages/core/src/__tests__/book-session-store.test.ts +447 -0
- package/inkos/packages/core/src/__tests__/book-session.test.ts +113 -0
- package/inkos/packages/core/src/__tests__/chapter-analyzer.test.ts +574 -0
- package/inkos/packages/core/src/__tests__/chapter-memo-parser.test.ts +247 -0
- package/inkos/packages/core/src/__tests__/chapter-persistence.test.ts +198 -0
- package/inkos/packages/core/src/__tests__/chapter-review-cycle.test.ts +294 -0
- package/inkos/packages/core/src/__tests__/chapter-splitter.test.ts +156 -0
- package/inkos/packages/core/src/__tests__/chapter-state-recovery.test.ts +235 -0
- package/inkos/packages/core/src/__tests__/chapter-truth-validation.test.ts +253 -0
- package/inkos/packages/core/src/__tests__/composer.test.ts +627 -0
- package/inkos/packages/core/src/__tests__/config-loader.test.ts +325 -0
- package/inkos/packages/core/src/__tests__/config-migration.test.ts +102 -0
- package/inkos/packages/core/src/__tests__/consolidator.test.ts +32 -0
- package/inkos/packages/core/src/__tests__/context-filter.test.ts +60 -0
- package/inkos/packages/core/src/__tests__/context-transform.test.ts +108 -0
- package/inkos/packages/core/src/__tests__/continuity.test.ts +391 -0
- package/inkos/packages/core/src/__tests__/detection-insights.test.ts +59 -0
- package/inkos/packages/core/src/__tests__/detector.test.ts +86 -0
- package/inkos/packages/core/src/__tests__/draft-directive-parser.test.ts +386 -0
- package/inkos/packages/core/src/__tests__/edit-controller.test.ts +190 -0
- package/inkos/packages/core/src/__tests__/effective-llm-config.test.ts +486 -0
- package/inkos/packages/core/src/__tests__/fanfic-dimensions.test.ts +58 -0
- package/inkos/packages/core/src/__tests__/fanfic-models.test.ts +69 -0
- package/inkos/packages/core/src/__tests__/governed-working-set.test.ts +155 -0
- package/inkos/packages/core/src/__tests__/hook-arbiter.test.ts +124 -0
- package/inkos/packages/core/src/__tests__/hook-governance.test.ts +228 -0
- package/inkos/packages/core/src/__tests__/hook-health.test.ts +166 -0
- package/inkos/packages/core/src/__tests__/hook-ledger-validator.test.ts +236 -0
- package/inkos/packages/core/src/__tests__/hook-promotion.test.ts +192 -0
- package/inkos/packages/core/src/__tests__/hook-stale-detection.test.ts +136 -0
- package/inkos/packages/core/src/__tests__/index-notify-lazy.test.ts +20 -0
- package/inkos/packages/core/src/__tests__/interaction-chat-tokens.test.ts +170 -0
- package/inkos/packages/core/src/__tests__/interaction-models.test.ts +155 -0
- package/inkos/packages/core/src/__tests__/interaction-nl-router.test.ts +223 -0
- package/inkos/packages/core/src/__tests__/interaction-runtime.test.ts +633 -0
- package/inkos/packages/core/src/__tests__/interaction-tools.test.ts +343 -0
- package/inkos/packages/core/src/__tests__/length-metrics.test.ts +82 -0
- package/inkos/packages/core/src/__tests__/length-normalizer.test.ts +331 -0
- package/inkos/packages/core/src/__tests__/list-models.test.ts +109 -0
- package/inkos/packages/core/src/__tests__/llm-env.test.ts +31 -0
- package/inkos/packages/core/src/__tests__/logger.test.ts +175 -0
- package/inkos/packages/core/src/__tests__/long-span-fatigue.test.ts +160 -0
- package/inkos/packages/core/src/__tests__/memory-retrieval.test.ts +1303 -0
- package/inkos/packages/core/src/__tests__/models.test.ts +918 -0
- package/inkos/packages/core/src/__tests__/outline-paths.test.ts +97 -0
- package/inkos/packages/core/src/__tests__/path-safety.test.ts +22 -0
- package/inkos/packages/core/src/__tests__/persisted-governed-plan.test.ts +134 -0
- package/inkos/packages/core/src/__tests__/phase5-cleanup.test.ts +393 -0
- package/inkos/packages/core/src/__tests__/phase5-hotfix.test.ts +288 -0
- package/inkos/packages/core/src/__tests__/phase7-hotfix.test.ts +614 -0
- package/inkos/packages/core/src/__tests__/pipeline-agent.test.ts +354 -0
- package/inkos/packages/core/src/__tests__/pipeline-runner-memory-sync.test.ts +317 -0
- package/inkos/packages/core/src/__tests__/pipeline-runner.test.ts +5200 -0
- package/inkos/packages/core/src/__tests__/planner-context.test.ts +137 -0
- package/inkos/packages/core/src/__tests__/planner-prompts-ratio.test.ts +11 -0
- package/inkos/packages/core/src/__tests__/planner-prompts.test.ts +171 -0
- package/inkos/packages/core/src/__tests__/planner.test.ts +362 -0
- package/inkos/packages/core/src/__tests__/planning-materials.test.ts +90 -0
- package/inkos/packages/core/src/__tests__/polisher.test.ts +189 -0
- package/inkos/packages/core/src/__tests__/post-write-validator.test.ts +291 -0
- package/inkos/packages/core/src/__tests__/probe.test.ts +77 -0
- package/inkos/packages/core/src/__tests__/project-interaction.test.ts +241 -0
- package/inkos/packages/core/src/__tests__/provider.test.ts +953 -0
- package/inkos/packages/core/src/__tests__/providers-group.test.ts +34 -0
- package/inkos/packages/core/src/__tests__/providers-lookup.test.ts +81 -0
- package/inkos/packages/core/src/__tests__/providers-schema.test.ts +158 -0
- package/inkos/packages/core/src/__tests__/proxy-fetch.test.ts +75 -0
- package/inkos/packages/core/src/__tests__/revise-foundation.test.ts +514 -0
- package/inkos/packages/core/src/__tests__/reviser.test.ts +859 -0
- package/inkos/packages/core/src/__tests__/runtime-state-store.test.ts +388 -0
- package/inkos/packages/core/src/__tests__/scheduler.test.ts +123 -0
- package/inkos/packages/core/src/__tests__/secrets-migration.test.ts +71 -0
- package/inkos/packages/core/src/__tests__/secrets.test.ts +95 -0
- package/inkos/packages/core/src/__tests__/sensitive-words.test.ts +88 -0
- package/inkos/packages/core/src/__tests__/service-presets-regression.test.ts +73 -0
- package/inkos/packages/core/src/__tests__/service-resolver-regression.test.ts +75 -0
- package/inkos/packages/core/src/__tests__/service-resolver.test.ts +228 -0
- package/inkos/packages/core/src/__tests__/session-transcript-restore.test.ts +1311 -0
- package/inkos/packages/core/src/__tests__/session-transcript.test.ts +195 -0
- package/inkos/packages/core/src/__tests__/settler-delta-parser.test.ts +133 -0
- package/inkos/packages/core/src/__tests__/short-fiction-public.test.ts +241 -0
- package/inkos/packages/core/src/__tests__/spot-fix-patches.test.ts +104 -0
- package/inkos/packages/core/src/__tests__/state-manager.test.ts +1298 -0
- package/inkos/packages/core/src/__tests__/state-projections.test.ts +130 -0
- package/inkos/packages/core/src/__tests__/state-reducer.test.ts +372 -0
- package/inkos/packages/core/src/__tests__/state-validator-agent.test.ts +165 -0
- package/inkos/packages/core/src/__tests__/state-validator.test.ts +122 -0
- package/inkos/packages/core/src/__tests__/style-analyzer.test.ts +61 -0
- package/inkos/packages/core/src/__tests__/temperature-constraints.test.ts +57 -0
- package/inkos/packages/core/src/__tests__/v13-hotfix-round4.test.ts +343 -0
- package/inkos/packages/core/src/__tests__/verify-service.test.ts +77 -0
- package/inkos/packages/core/src/__tests__/webhook.test.ts +91 -0
- package/inkos/packages/core/src/__tests__/writer-parser.test.ts +348 -0
- package/inkos/packages/core/src/__tests__/writer-prompts.test.ts +269 -0
- package/inkos/packages/core/src/__tests__/writer.test.ts +1360 -0
- package/inkos/packages/core/src/agent/agent-session.ts +737 -0
- package/inkos/packages/core/src/agent/agent-system-prompt.ts +199 -0
- package/inkos/packages/core/src/agent/agent-tools.ts +835 -0
- package/inkos/packages/core/src/agent/context-transform.ts +85 -0
- package/inkos/packages/core/src/agent/index.ts +14 -0
- package/inkos/packages/core/src/agents/ai-tells.ts +161 -0
- package/inkos/packages/core/src/agents/architect.ts +1291 -0
- package/inkos/packages/core/src/agents/base.ts +100 -0
- package/inkos/packages/core/src/agents/chapter-analyzer.ts +634 -0
- package/inkos/packages/core/src/agents/composer.ts +469 -0
- package/inkos/packages/core/src/agents/consolidator.ts +218 -0
- package/inkos/packages/core/src/agents/continuity.ts +824 -0
- package/inkos/packages/core/src/agents/detection-insights.ts +72 -0
- package/inkos/packages/core/src/agents/detector.ts +224 -0
- package/inkos/packages/core/src/agents/en-prompt-sections.ts +129 -0
- package/inkos/packages/core/src/agents/fanfic-canon-importer.ts +146 -0
- package/inkos/packages/core/src/agents/fanfic-dimensions.ts +87 -0
- package/inkos/packages/core/src/agents/fanfic-prompt-sections.ts +109 -0
- package/inkos/packages/core/src/agents/foundation-reviewer.ts +204 -0
- package/inkos/packages/core/src/agents/length-normalizer.ts +218 -0
- package/inkos/packages/core/src/agents/observer-prompts.ts +127 -0
- package/inkos/packages/core/src/agents/planner-context.ts +297 -0
- package/inkos/packages/core/src/agents/planner-prompts.ts +404 -0
- package/inkos/packages/core/src/agents/planner.ts +783 -0
- package/inkos/packages/core/src/agents/polisher.ts +153 -0
- package/inkos/packages/core/src/agents/post-write-validator.ts +873 -0
- package/inkos/packages/core/src/agents/radar-source.ts +123 -0
- package/inkos/packages/core/src/agents/radar.ts +120 -0
- package/inkos/packages/core/src/agents/reviser.ts +701 -0
- package/inkos/packages/core/src/agents/rules-reader.ts +155 -0
- package/inkos/packages/core/src/agents/sensitive-words.ts +142 -0
- package/inkos/packages/core/src/agents/settler-delta-parser.ts +53 -0
- package/inkos/packages/core/src/agents/settler-parser.ts +38 -0
- package/inkos/packages/core/src/agents/settler-prompts.ts +230 -0
- package/inkos/packages/core/src/agents/short-fiction.ts +429 -0
- package/inkos/packages/core/src/agents/state-validator.ts +322 -0
- package/inkos/packages/core/src/agents/style-analyzer.ts +93 -0
- package/inkos/packages/core/src/agents/writer-parser.ts +178 -0
- package/inkos/packages/core/src/agents/writer-prompts.ts +899 -0
- package/inkos/packages/core/src/agents/writer.ts +1450 -0
- package/inkos/packages/core/src/index.ts +392 -0
- package/inkos/packages/core/src/interaction/book-session-store.ts +226 -0
- package/inkos/packages/core/src/interaction/draft-directive-parser.ts +266 -0
- package/inkos/packages/core/src/interaction/edit-controller.ts +270 -0
- package/inkos/packages/core/src/interaction/events.ts +41 -0
- package/inkos/packages/core/src/interaction/export-artifact.ts +151 -0
- package/inkos/packages/core/src/interaction/intents.ts +63 -0
- package/inkos/packages/core/src/interaction/modes.ts +13 -0
- package/inkos/packages/core/src/interaction/nl-router.ts +258 -0
- package/inkos/packages/core/src/interaction/project-control.ts +150 -0
- package/inkos/packages/core/src/interaction/project-session-store.ts +81 -0
- package/inkos/packages/core/src/interaction/project-tools.ts +704 -0
- package/inkos/packages/core/src/interaction/request-router.ts +5 -0
- package/inkos/packages/core/src/interaction/runtime.ts +1167 -0
- package/inkos/packages/core/src/interaction/session-transcript-legacy.ts +113 -0
- package/inkos/packages/core/src/interaction/session-transcript-restore.ts +607 -0
- package/inkos/packages/core/src/interaction/session-transcript-schema.ts +76 -0
- package/inkos/packages/core/src/interaction/session-transcript.ts +189 -0
- package/inkos/packages/core/src/interaction/session.ts +226 -0
- package/inkos/packages/core/src/interaction/truth-authority.ts +45 -0
- package/inkos/packages/core/src/llm/config-migration.ts +58 -0
- package/inkos/packages/core/src/llm/cover-providers.ts +45 -0
- package/inkos/packages/core/src/llm/provider.ts +1331 -0
- package/inkos/packages/core/src/llm/providers/endpoints/ai360.ts +42 -0
- package/inkos/packages/core/src/llm/providers/endpoints/anthropic.ts +82 -0
- package/inkos/packages/core/src/llm/providers/endpoints/astronCodingPlan.ts +30 -0
- package/inkos/packages/core/src/llm/providers/endpoints/baichuan.ts +28 -0
- package/inkos/packages/core/src/llm/providers/endpoints/bailian.ts +65 -0
- package/inkos/packages/core/src/llm/providers/endpoints/bailianCodingPlan.ts +30 -0
- package/inkos/packages/core/src/llm/providers/endpoints/custom.ts +22 -0
- package/inkos/packages/core/src/llm/providers/endpoints/deepseek.ts +35 -0
- package/inkos/packages/core/src/llm/providers/endpoints/giteeai.ts +41 -0
- package/inkos/packages/core/src/llm/providers/endpoints/githubCopilot.ts +43 -0
- package/inkos/packages/core/src/llm/providers/endpoints/glmCodingPlan.ts +28 -0
- package/inkos/packages/core/src/llm/providers/endpoints/google.ts +51 -0
- package/inkos/packages/core/src/llm/providers/endpoints/hunyuan.ts +42 -0
- package/inkos/packages/core/src/llm/providers/endpoints/infiniai.ts +72 -0
- package/inkos/packages/core/src/llm/providers/endpoints/internlm.ts +28 -0
- package/inkos/packages/core/src/llm/providers/endpoints/kimiCode.ts +23 -0
- package/inkos/packages/core/src/llm/providers/endpoints/kimiCodingPlan.ts +24 -0
- package/inkos/packages/core/src/llm/providers/endpoints/kkaiapi.ts +56 -0
- package/inkos/packages/core/src/llm/providers/endpoints/longcat.ts +25 -0
- package/inkos/packages/core/src/llm/providers/endpoints/minimax.ts +39 -0
- package/inkos/packages/core/src/llm/providers/endpoints/minimaxCodingPlan.ts +28 -0
- package/inkos/packages/core/src/llm/providers/endpoints/mistral.ts +40 -0
- package/inkos/packages/core/src/llm/providers/endpoints/modelscope.ts +30 -0
- package/inkos/packages/core/src/llm/providers/endpoints/moonshot.ts +39 -0
- package/inkos/packages/core/src/llm/providers/endpoints/newapi.ts +21 -0
- package/inkos/packages/core/src/llm/providers/endpoints/ollama.ts +73 -0
- package/inkos/packages/core/src/llm/providers/endpoints/openai.ts +77 -0
- package/inkos/packages/core/src/llm/providers/endpoints/opencodeCodingPlan.ts +30 -0
- package/inkos/packages/core/src/llm/providers/endpoints/openrouter.ts +87 -0
- package/inkos/packages/core/src/llm/providers/endpoints/ppio.ts +86 -0
- package/inkos/packages/core/src/llm/providers/endpoints/qiniu.ts +32 -0
- package/inkos/packages/core/src/llm/providers/endpoints/sensenova.ts +45 -0
- package/inkos/packages/core/src/llm/providers/endpoints/siliconcloud.ts +126 -0
- package/inkos/packages/core/src/llm/providers/endpoints/spark.ts +33 -0
- package/inkos/packages/core/src/llm/providers/endpoints/stepfun.ts +35 -0
- package/inkos/packages/core/src/llm/providers/endpoints/tencentcloud.ts +25 -0
- package/inkos/packages/core/src/llm/providers/endpoints/volcengine.ts +52 -0
- package/inkos/packages/core/src/llm/providers/endpoints/volcengineCodingPlan.ts +42 -0
- package/inkos/packages/core/src/llm/providers/endpoints/wenxin.ts +106 -0
- package/inkos/packages/core/src/llm/providers/endpoints/xai.ts +34 -0
- package/inkos/packages/core/src/llm/providers/endpoints/xiaomimimo.ts +26 -0
- package/inkos/packages/core/src/llm/providers/endpoints/zeroone.ts +34 -0
- package/inkos/packages/core/src/llm/providers/endpoints/zhipu.ts +61 -0
- package/inkos/packages/core/src/llm/providers/index.ts +71 -0
- package/inkos/packages/core/src/llm/providers/lookup.ts +70 -0
- package/inkos/packages/core/src/llm/providers/probe.ts +35 -0
- package/inkos/packages/core/src/llm/providers/types.ts +89 -0
- package/inkos/packages/core/src/llm/providers/verify.ts +104 -0
- package/inkos/packages/core/src/llm/secrets.ts +77 -0
- package/inkos/packages/core/src/llm/service-presets.ts +215 -0
- package/inkos/packages/core/src/llm/service-resolver.ts +91 -0
- package/inkos/packages/core/src/models/book-rules.ts +126 -0
- package/inkos/packages/core/src/models/book.ts +70 -0
- package/inkos/packages/core/src/models/chapter.ts +42 -0
- package/inkos/packages/core/src/models/detection.ts +25 -0
- package/inkos/packages/core/src/models/genre-profile.ts +36 -0
- package/inkos/packages/core/src/models/input-governance.ts +99 -0
- package/inkos/packages/core/src/models/length-governance.ts +46 -0
- package/inkos/packages/core/src/models/project.ts +161 -0
- package/inkos/packages/core/src/models/runtime-state.ts +144 -0
- package/inkos/packages/core/src/models/state.ts +52 -0
- package/inkos/packages/core/src/models/style-profile.ts +15 -0
- package/inkos/packages/core/src/notify/dispatcher.ts +96 -0
- package/inkos/packages/core/src/notify/feishu.ts +34 -0
- package/inkos/packages/core/src/notify/telegram.ts +25 -0
- package/inkos/packages/core/src/notify/webhook.ts +58 -0
- package/inkos/packages/core/src/notify/wechat-work.ts +22 -0
- package/inkos/packages/core/src/pipeline/agent.ts +691 -0
- package/inkos/packages/core/src/pipeline/chapter-persistence.ts +79 -0
- package/inkos/packages/core/src/pipeline/chapter-review-cycle.ts +324 -0
- package/inkos/packages/core/src/pipeline/chapter-state-recovery.ts +236 -0
- package/inkos/packages/core/src/pipeline/chapter-truth-validation.ts +145 -0
- package/inkos/packages/core/src/pipeline/detection-runner.ts +164 -0
- package/inkos/packages/core/src/pipeline/persisted-governed-plan.ts +216 -0
- package/inkos/packages/core/src/pipeline/runner.ts +3438 -0
- package/inkos/packages/core/src/pipeline/scheduler.ts +411 -0
- package/inkos/packages/core/src/pipeline/short-fiction-runner.ts +801 -0
- package/inkos/packages/core/src/prompts/index.ts +1 -0
- package/inkos/packages/core/src/prompts/short-fiction.ts +273 -0
- package/inkos/packages/core/src/state/manager.ts +560 -0
- package/inkos/packages/core/src/state/memory-db.ts +359 -0
- package/inkos/packages/core/src/state/runtime-state-store.ts +164 -0
- package/inkos/packages/core/src/state/state-bootstrap.ts +657 -0
- package/inkos/packages/core/src/state/state-projections.ts +255 -0
- package/inkos/packages/core/src/state/state-reducer.ts +260 -0
- package/inkos/packages/core/src/state/state-validator.ts +117 -0
- package/inkos/packages/core/src/utils/analytics.ts +92 -0
- package/inkos/packages/core/src/utils/book-id.ts +31 -0
- package/inkos/packages/core/src/utils/cadence-policy.ts +46 -0
- package/inkos/packages/core/src/utils/chapter-cadence.ts +211 -0
- package/inkos/packages/core/src/utils/chapter-memo-parser.ts +157 -0
- package/inkos/packages/core/src/utils/chapter-splitter.ts +80 -0
- package/inkos/packages/core/src/utils/config-loader.ts +29 -0
- package/inkos/packages/core/src/utils/context-assembly.ts +98 -0
- package/inkos/packages/core/src/utils/context-filter.ts +190 -0
- package/inkos/packages/core/src/utils/effective-llm-config.ts +529 -0
- package/inkos/packages/core/src/utils/governed-context.ts +101 -0
- package/inkos/packages/core/src/utils/governed-working-set.ts +395 -0
- package/inkos/packages/core/src/utils/hook-arbiter.ts +332 -0
- package/inkos/packages/core/src/utils/hook-governance.ts +199 -0
- package/inkos/packages/core/src/utils/hook-health.ts +189 -0
- package/inkos/packages/core/src/utils/hook-ledger-validator.ts +277 -0
- package/inkos/packages/core/src/utils/hook-lifecycle.ts +224 -0
- package/inkos/packages/core/src/utils/hook-policy.ts +115 -0
- package/inkos/packages/core/src/utils/hook-promotion.ts +313 -0
- package/inkos/packages/core/src/utils/hook-stale-detection.ts +168 -0
- package/inkos/packages/core/src/utils/length-metrics.ts +123 -0
- package/inkos/packages/core/src/utils/llm-endpoint-auth.ts +40 -0
- package/inkos/packages/core/src/utils/llm-env.ts +74 -0
- package/inkos/packages/core/src/utils/logger.ts +123 -0
- package/inkos/packages/core/src/utils/long-span-fatigue.ts +545 -0
- package/inkos/packages/core/src/utils/memory-retrieval.ts +527 -0
- package/inkos/packages/core/src/utils/narrative-control.ts +177 -0
- package/inkos/packages/core/src/utils/outline-paths.ts +275 -0
- package/inkos/packages/core/src/utils/path-safety.ts +11 -0
- package/inkos/packages/core/src/utils/planning-materials.ts +185 -0
- package/inkos/packages/core/src/utils/pov-filter.ts +149 -0
- package/inkos/packages/core/src/utils/proxy-fetch.ts +44 -0
- package/inkos/packages/core/src/utils/runtime-writer.ts +41 -0
- package/inkos/packages/core/src/utils/spot-fix-patches.ts +189 -0
- package/inkos/packages/core/src/utils/story-markdown.ts +346 -0
- package/inkos/packages/core/src/utils/web-search.ts +82 -0
- package/inkos/packages/core/src/utils/writing-methodology.ts +164 -0
- package/inkos/packages/core/tsconfig.json +8 -0
- package/inkos/packages/core/vitest.config.ts +7 -0
- package/inkos/packages/studio/components.json +25 -0
- package/inkos/packages/studio/index.html +13 -0
- package/inkos/packages/studio/package.json +72 -0
- package/inkos/packages/studio/postcss.config.js +3 -0
- package/inkos/packages/studio/src/App.test.ts +25 -0
- package/inkos/packages/studio/src/App.tsx +280 -0
- package/inkos/packages/studio/src/api/__tests__/normalize-base-url.test.ts +40 -0
- package/inkos/packages/studio/src/api/book-create.test.ts +104 -0
- package/inkos/packages/studio/src/api/book-create.ts +94 -0
- package/inkos/packages/studio/src/api/errors.ts +17 -0
- package/inkos/packages/studio/src/api/index.ts +30 -0
- package/inkos/packages/studio/src/api/lib/run-store.ts +177 -0
- package/inkos/packages/studio/src/api/lib/sse.ts +50 -0
- package/inkos/packages/studio/src/api/phase5-hotfix.test.ts +335 -0
- package/inkos/packages/studio/src/api/safety.ts +6 -0
- package/inkos/packages/studio/src/api/server.test.ts +3162 -0
- package/inkos/packages/studio/src/api/server.ts +3666 -0
- package/inkos/packages/studio/src/api/v13-hotfix-round4.test.ts +226 -0
- package/inkos/packages/studio/src/app-state.test.ts +8 -0
- package/inkos/packages/studio/src/app-state.ts +1 -0
- package/inkos/packages/studio/src/components/ConfirmDialog.tsx +95 -0
- package/inkos/packages/studio/src/components/ServiceConfigSourceCard.tsx +139 -0
- package/inkos/packages/studio/src/components/ServiceQuickLinks.tsx +65 -0
- package/inkos/packages/studio/src/components/Sidebar.tsx +652 -0
- package/inkos/packages/studio/src/components/ai-elements/code-block.tsx +562 -0
- package/inkos/packages/studio/src/components/ai-elements/confirmation.tsx +174 -0
- package/inkos/packages/studio/src/components/ai-elements/message.tsx +360 -0
- package/inkos/packages/studio/src/components/ai-elements/prompt-input.tsx +1457 -0
- package/inkos/packages/studio/src/components/ai-elements/reasoning.tsx +226 -0
- package/inkos/packages/studio/src/components/ai-elements/shimmer.tsx +77 -0
- package/inkos/packages/studio/src/components/ai-elements/tool.tsx +173 -0
- package/inkos/packages/studio/src/components/chat/BookSidebar.tsx +291 -0
- package/inkos/packages/studio/src/components/chat/ChatMessage.tsx +39 -0
- package/inkos/packages/studio/src/components/chat/QuickActions.tsx +73 -0
- package/inkos/packages/studio/src/components/chat/ToolExecutionSteps.tsx +320 -0
- package/inkos/packages/studio/src/components/chat/__tests__/ToolExecutionSteps.test.ts +114 -0
- package/inkos/packages/studio/src/components/chat-utils.ts +56 -0
- package/inkos/packages/studio/src/components/chatbar-state.test.ts +69 -0
- package/inkos/packages/studio/src/components/sidebar/ChaptersSection.tsx +66 -0
- package/inkos/packages/studio/src/components/sidebar/CharacterSection.tsx +129 -0
- package/inkos/packages/studio/src/components/sidebar/FoundationSection.tsx +61 -0
- package/inkos/packages/studio/src/components/sidebar/ProgressSection.tsx +124 -0
- package/inkos/packages/studio/src/components/sidebar/SidebarCard.tsx +30 -0
- package/inkos/packages/studio/src/components/sidebar/SummarySection.tsx +89 -0
- package/inkos/packages/studio/src/components/ui/alert.tsx +76 -0
- package/inkos/packages/studio/src/components/ui/badge.tsx +52 -0
- package/inkos/packages/studio/src/components/ui/button-group.tsx +87 -0
- package/inkos/packages/studio/src/components/ui/button.tsx +58 -0
- package/inkos/packages/studio/src/components/ui/collapsible.tsx +19 -0
- package/inkos/packages/studio/src/components/ui/command.tsx +194 -0
- package/inkos/packages/studio/src/components/ui/dialog.tsx +158 -0
- package/inkos/packages/studio/src/components/ui/dropdown-menu.tsx +266 -0
- package/inkos/packages/studio/src/components/ui/hover-card.tsx +51 -0
- package/inkos/packages/studio/src/components/ui/input-group.tsx +158 -0
- package/inkos/packages/studio/src/components/ui/input.tsx +20 -0
- package/inkos/packages/studio/src/components/ui/select.tsx +199 -0
- package/inkos/packages/studio/src/components/ui/separator.tsx +23 -0
- package/inkos/packages/studio/src/components/ui/spinner.tsx +10 -0
- package/inkos/packages/studio/src/components/ui/textarea.tsx +18 -0
- package/inkos/packages/studio/src/components/ui/tooltip.tsx +66 -0
- package/inkos/packages/studio/src/constants/service-groups.ts +29 -0
- package/inkos/packages/studio/src/hooks/use-api.test.ts +93 -0
- package/inkos/packages/studio/src/hooks/use-api.ts +189 -0
- package/inkos/packages/studio/src/hooks/use-book-activity.test.ts +129 -0
- package/inkos/packages/studio/src/hooks/use-book-activity.ts +180 -0
- package/inkos/packages/studio/src/hooks/use-colors.ts +27 -0
- package/inkos/packages/studio/src/hooks/use-hash-route.test.ts +101 -0
- package/inkos/packages/studio/src/hooks/use-hash-route.ts +89 -0
- package/inkos/packages/studio/src/hooks/use-i18n.ts +289 -0
- package/inkos/packages/studio/src/hooks/use-session-events.ts +64 -0
- package/inkos/packages/studio/src/hooks/use-sse.test.ts +52 -0
- package/inkos/packages/studio/src/hooks/use-sse.ts +92 -0
- package/inkos/packages/studio/src/hooks/use-theme.test.ts +31 -0
- package/inkos/packages/studio/src/hooks/use-theme.ts +75 -0
- package/inkos/packages/studio/src/index.css +323 -0
- package/inkos/packages/studio/src/lib/error-copy.test.ts +34 -0
- package/inkos/packages/studio/src/lib/error-copy.ts +37 -0
- package/inkos/packages/studio/src/lib/utils.ts +6 -0
- package/inkos/packages/studio/src/main.tsx +10 -0
- package/inkos/packages/studio/src/pages/Analytics.tsx +80 -0
- package/inkos/packages/studio/src/pages/BookCreate.tsx +895 -0
- package/inkos/packages/studio/src/pages/BookDetail.tsx +652 -0
- package/inkos/packages/studio/src/pages/ChapterReader.tsx +266 -0
- package/inkos/packages/studio/src/pages/ChatPage.tsx +521 -0
- package/inkos/packages/studio/src/pages/DaemonControl.tsx +116 -0
- package/inkos/packages/studio/src/pages/Dashboard.tsx +379 -0
- package/inkos/packages/studio/src/pages/DoctorView.tsx +82 -0
- package/inkos/packages/studio/src/pages/GenreManager.tsx +464 -0
- package/inkos/packages/studio/src/pages/ImportManager.tsx +216 -0
- package/inkos/packages/studio/src/pages/LanguageSelector.tsx +74 -0
- package/inkos/packages/studio/src/pages/LogViewer.tsx +82 -0
- package/inkos/packages/studio/src/pages/RadarView.tsx +157 -0
- package/inkos/packages/studio/src/pages/ServiceDetailPage.tsx +393 -0
- package/inkos/packages/studio/src/pages/ServiceListPage.tsx +463 -0
- package/inkos/packages/studio/src/pages/StyleManager.tsx +225 -0
- package/inkos/packages/studio/src/pages/TruthFiles.tsx +194 -0
- package/inkos/packages/studio/src/pages/chat-page-state.test.ts +206 -0
- package/inkos/packages/studio/src/pages/chat-page-state.ts +112 -0
- package/inkos/packages/studio/src/pages/page-state.test.ts +258 -0
- package/inkos/packages/studio/src/pages/service-detail-state.test.ts +294 -0
- package/inkos/packages/studio/src/pages/service-detail-state.ts +234 -0
- package/inkos/packages/studio/src/pages/style-manager-state.test.ts +22 -0
- package/inkos/packages/studio/src/pages/truth-files-state.test.ts +61 -0
- package/inkos/packages/studio/src/shared/contracts.ts +143 -0
- package/inkos/packages/studio/src/store/chat/__tests__/message-parts.test.ts +172 -0
- package/inkos/packages/studio/src/store/chat/index.ts +3 -0
- package/inkos/packages/studio/src/store/chat/initialState.ts +8 -0
- package/inkos/packages/studio/src/store/chat/message-policy.test.ts +16 -0
- package/inkos/packages/studio/src/store/chat/message-policy.ts +5 -0
- package/inkos/packages/studio/src/store/chat/parts-builder.ts +187 -0
- package/inkos/packages/studio/src/store/chat/selectors.ts +13 -0
- package/inkos/packages/studio/src/store/chat/slices/create/action.ts +10 -0
- package/inkos/packages/studio/src/store/chat/slices/create/initialState.ts +9 -0
- package/inkos/packages/studio/src/store/chat/slices/message/action.ts +417 -0
- package/inkos/packages/studio/src/store/chat/slices/message/initialState.ts +10 -0
- package/inkos/packages/studio/src/store/chat/slices/message/runtime.test.ts +21 -0
- package/inkos/packages/studio/src/store/chat/slices/message/runtime.ts +233 -0
- package/inkos/packages/studio/src/store/chat/slices/message/stream-events.ts +272 -0
- package/inkos/packages/studio/src/store/chat/store.ts +11 -0
- package/inkos/packages/studio/src/store/chat/types.ts +169 -0
- package/inkos/packages/studio/src/store/service/index.ts +2 -0
- package/inkos/packages/studio/src/store/service/store.ts +123 -0
- package/inkos/packages/studio/src/store/service/types.ts +50 -0
- package/inkos/packages/studio/tsconfig.json +24 -0
- package/inkos/packages/studio/tsconfig.server.json +11 -0
- package/inkos/packages/studio/vite.config.ts +34 -0
- package/inkos/packages/studio/vitest.config.ts +14 -0
- package/inkos/pnpm-lock.yaml +9569 -0
- package/inkos/pnpm-workspace.yaml +2 -0
- package/inkos/scripts/prepare-package-for-publish.mjs +135 -0
- package/inkos/scripts/restore-package-json.mjs +31 -0
- package/inkos/scripts/set-package-versions.mjs +74 -0
- package/inkos/scripts/verify-no-workspace-protocol.mjs +140 -0
- package/inkos/skills/SKILL.md +654 -0
- package/inkos/tsconfig.json +19 -0
- package/package.json +4 -3
- /package/.next/static/{F2hMZMf1IyCVAWpkbtRz7 → -3vIrBZXdQ0rp7Wa3Kz40}/_buildManifest.js +0 -0
- /package/.next/static/{F2hMZMf1IyCVAWpkbtRz7 → -3vIrBZXdQ0rp7Wa3Kz40}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,873 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Post-write rule-based validator.
|
|
3
|
+
*
|
|
4
|
+
* Deterministic, zero-LLM-cost checks that run after every chapter generation.
|
|
5
|
+
* Catches violations that prompt-only rules cannot guarantee.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { analyzeChapterCadence } from "../utils/chapter-cadence.js";
|
|
9
|
+
import type { BookRules } from "../models/book-rules.js";
|
|
10
|
+
import type { GenreProfile } from "../models/genre-profile.js";
|
|
11
|
+
|
|
12
|
+
export interface PostWriteViolation {
|
|
13
|
+
readonly rule: string;
|
|
14
|
+
readonly severity: "error" | "warning";
|
|
15
|
+
readonly description: string;
|
|
16
|
+
readonly suggestion: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function normalizePostWriteSurface(
|
|
20
|
+
content: string,
|
|
21
|
+
languageOverride?: "zh" | "en",
|
|
22
|
+
): string {
|
|
23
|
+
let normalized = stripPostWriteMetaLines(content);
|
|
24
|
+
if (languageOverride !== "en") {
|
|
25
|
+
normalized = normalized.replace(/——+/g, ",");
|
|
26
|
+
}
|
|
27
|
+
return normalized.trimEnd();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function stripPostWriteMetaLines(content: string): string {
|
|
31
|
+
const lines = content.split(/\r?\n/);
|
|
32
|
+
const filtered = lines.filter((line) =>
|
|
33
|
+
!/^\s*\[(?:polisher|writer|reviser|reviewer)-note\]\s*/i.test(line)
|
|
34
|
+
&& !/^\s*\[(?:润色|写作|修订|审稿)备注\]\s*/.test(line)
|
|
35
|
+
);
|
|
36
|
+
return filtered.join("\n");
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
interface ParagraphShape {
|
|
40
|
+
readonly paragraphs: ReadonlyArray<string>;
|
|
41
|
+
readonly shortThreshold: number;
|
|
42
|
+
readonly shortParagraphs: ReadonlyArray<string>;
|
|
43
|
+
readonly shortRatio: number;
|
|
44
|
+
readonly averageLength: number;
|
|
45
|
+
readonly maxConsecutiveShort: number;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// --- Marker word lists ---
|
|
49
|
+
|
|
50
|
+
/** AI转折/惊讶标记词 */
|
|
51
|
+
const SURPRISE_MARKERS = ["仿佛", "忽然", "竟然", "猛地", "猛然", "不禁", "宛如"];
|
|
52
|
+
|
|
53
|
+
/** 元叙事/编剧旁白模式 */
|
|
54
|
+
const META_NARRATION_PATTERNS = [
|
|
55
|
+
/到这里[,,]?算是/,
|
|
56
|
+
/接下来[,,]?(?:就是|将会|即将)/,
|
|
57
|
+
/(?:后面|之后)[,,]?(?:会|将|还会)/,
|
|
58
|
+
/(?:故事|剧情)(?:发展)?到了/,
|
|
59
|
+
/读者[,,]?(?:可能|应该|也许)/,
|
|
60
|
+
/我们[,,]?(?:可以|不妨|来看)/,
|
|
61
|
+
];
|
|
62
|
+
|
|
63
|
+
/** 分析报告式术语(禁止出现在正文中) */
|
|
64
|
+
const REPORT_TERMS = [
|
|
65
|
+
"核心动机", "信息边界", "信息落差", "核心风险", "利益最大化",
|
|
66
|
+
"当前处境", "行为约束", "性格过滤", "情绪外化", "锚定效应",
|
|
67
|
+
"沉没成本", "认知共鸣",
|
|
68
|
+
];
|
|
69
|
+
|
|
70
|
+
/** 作者说教词 */
|
|
71
|
+
const SERMON_WORDS = ["显然", "毋庸置疑", "不言而喻", "众所周知", "不难看出"];
|
|
72
|
+
|
|
73
|
+
/** 全场震惊类集体反应 */
|
|
74
|
+
const COLLECTIVE_SHOCK_PATTERNS = [
|
|
75
|
+
/(?:全场|众人|所有人|在场的人)[,,]?(?:都|全|齐齐|纷纷)?(?:震惊|惊呆|倒吸凉气|目瞪口呆|哗然|惊呼)/,
|
|
76
|
+
/(?:全场|一片)[,,]?(?:寂静|哗然|沸腾|震动)/,
|
|
77
|
+
];
|
|
78
|
+
|
|
79
|
+
// --- Validator ---
|
|
80
|
+
|
|
81
|
+
export function validatePostWrite(
|
|
82
|
+
content: string,
|
|
83
|
+
genreProfile: GenreProfile,
|
|
84
|
+
bookRules: BookRules | null,
|
|
85
|
+
languageOverride?: "zh" | "en",
|
|
86
|
+
): ReadonlyArray<PostWriteViolation> {
|
|
87
|
+
const violations: PostWriteViolation[] = [];
|
|
88
|
+
|
|
89
|
+
// Skip Chinese-specific rules for English content
|
|
90
|
+
const isEnglish = (languageOverride ?? genreProfile.language) === "en";
|
|
91
|
+
if (isEnglish) {
|
|
92
|
+
// For English, only run book-specific prohibitions and paragraph length check
|
|
93
|
+
return validatePostWriteEnglish(content, genreProfile, bookRules);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// 1. 硬性禁令: "不是…而是…" 句式
|
|
97
|
+
if (/不是[^,。!?\n]{0,30}[,,]?\s*而是/.test(content)) {
|
|
98
|
+
violations.push({
|
|
99
|
+
rule: "禁止句式",
|
|
100
|
+
severity: "error",
|
|
101
|
+
description: "出现了「不是……而是……」句式",
|
|
102
|
+
suggestion: "改用直述句",
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// 2. 硬性禁令: 破折号
|
|
107
|
+
if (content.includes("——")) {
|
|
108
|
+
violations.push({
|
|
109
|
+
rule: "禁止破折号",
|
|
110
|
+
severity: "error",
|
|
111
|
+
description: "出现了破折号「——」",
|
|
112
|
+
suggestion: "用逗号或句号断句",
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// 3. 转折/惊讶标记词密度 ≤ 1次/3000字
|
|
117
|
+
const markerCounts: Record<string, number> = {};
|
|
118
|
+
let totalMarkerCount = 0;
|
|
119
|
+
for (const word of SURPRISE_MARKERS) {
|
|
120
|
+
const matches = content.match(new RegExp(word, "g"));
|
|
121
|
+
const count = matches?.length ?? 0;
|
|
122
|
+
if (count > 0) {
|
|
123
|
+
markerCounts[word] = count;
|
|
124
|
+
totalMarkerCount += count;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
const markerLimit = Math.max(1, Math.floor(content.length / 3000));
|
|
128
|
+
if (totalMarkerCount > markerLimit) {
|
|
129
|
+
const detail = Object.entries(markerCounts)
|
|
130
|
+
.map(([w, c]) => `"${w}"×${c}`)
|
|
131
|
+
.join("、");
|
|
132
|
+
violations.push({
|
|
133
|
+
rule: "转折词密度",
|
|
134
|
+
severity: "warning",
|
|
135
|
+
description: `转折/惊讶标记词共${totalMarkerCount}次(上限${markerLimit}次/${content.length}字),明细:${detail}`,
|
|
136
|
+
suggestion: "改用具体动作或感官描写传递突然性",
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// 4. 高疲劳词检查(从 genreProfile 读取,单章每词 ≤ 1次)
|
|
141
|
+
const fatigueWords = bookRules?.fatigueWordsOverride && bookRules.fatigueWordsOverride.length > 0
|
|
142
|
+
? bookRules.fatigueWordsOverride
|
|
143
|
+
: genreProfile.fatigueWords;
|
|
144
|
+
for (const word of fatigueWords) {
|
|
145
|
+
const escaped = word.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
146
|
+
const matches = content.match(new RegExp(escaped, "g"));
|
|
147
|
+
const count = matches?.length ?? 0;
|
|
148
|
+
if (count > 1) {
|
|
149
|
+
violations.push({
|
|
150
|
+
rule: "高疲劳词",
|
|
151
|
+
severity: "warning",
|
|
152
|
+
description: `高疲劳词"${word}"出现${count}次(上限1次/章)`,
|
|
153
|
+
suggestion: `替换多余的"${word}"为同义但不同形式的表达`,
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// 5. 元叙事检查(编剧旁白)
|
|
159
|
+
for (const pattern of META_NARRATION_PATTERNS) {
|
|
160
|
+
const match = content.match(pattern);
|
|
161
|
+
if (match) {
|
|
162
|
+
violations.push({
|
|
163
|
+
rule: "元叙事",
|
|
164
|
+
severity: "warning",
|
|
165
|
+
description: `出现编剧旁白式表述:"${match[0]}"`,
|
|
166
|
+
suggestion: "删除元叙事,让剧情自然展开",
|
|
167
|
+
});
|
|
168
|
+
break; // 报一次即可
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// 6. 分析报告式术语
|
|
173
|
+
const foundTerms: string[] = [];
|
|
174
|
+
for (const term of REPORT_TERMS) {
|
|
175
|
+
if (content.includes(term)) {
|
|
176
|
+
foundTerms.push(term);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
if (foundTerms.length > 0) {
|
|
180
|
+
violations.push({
|
|
181
|
+
rule: "报告术语",
|
|
182
|
+
severity: "error",
|
|
183
|
+
description: `正文中出现分析报告术语:${foundTerms.map(t => `"${t}"`).join("、")}`,
|
|
184
|
+
suggestion: "这些术语只能用于 PRE_WRITE_CHECK 内部推理,正文中用口语化表达替代",
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// 7. 正文中的章节号指称(如"第33章"、"chapter 33")
|
|
189
|
+
const chapterRefPattern = /(?:第\s*\d+\s*章|[Cc]hapter\s+\d+)/g;
|
|
190
|
+
const chapterRefs = content.match(chapterRefPattern);
|
|
191
|
+
if (chapterRefs && chapterRefs.length > 0) {
|
|
192
|
+
const unique = [...new Set(chapterRefs)];
|
|
193
|
+
violations.push({
|
|
194
|
+
rule: isEnglish ? "chapter-number-reference" : "章节号指称",
|
|
195
|
+
severity: "error",
|
|
196
|
+
description: isEnglish
|
|
197
|
+
? `Chapter text contains explicit chapter number references: ${unique.map(r => `"${r}"`).join(", ")}. Characters do not know they are in a numbered chapter.`
|
|
198
|
+
: `正文中出现了章节号指称:${unique.map(r => `"${r}"`).join("、")}。角色不知道自己在第几章。`,
|
|
199
|
+
suggestion: isEnglish
|
|
200
|
+
? "Replace with natural references: 'that night', 'when the warehouse burned', 'the incident at the dock'"
|
|
201
|
+
: '改成自然表达:"那天晚上"、"仓库出事那次"、"码头上的事"',
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// 8. 作者说教词
|
|
206
|
+
const foundSermons: string[] = [];
|
|
207
|
+
for (const word of SERMON_WORDS) {
|
|
208
|
+
if (content.includes(word)) {
|
|
209
|
+
foundSermons.push(word);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
if (foundSermons.length > 0) {
|
|
213
|
+
violations.push({
|
|
214
|
+
rule: "作者说教",
|
|
215
|
+
severity: "warning",
|
|
216
|
+
description: `出现说教词:${foundSermons.map(w => `"${w}"`).join("、")}`,
|
|
217
|
+
suggestion: "删除说教词,让读者自己从情节中判断",
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// 8. 全场震惊类集体反应
|
|
222
|
+
for (const pattern of COLLECTIVE_SHOCK_PATTERNS) {
|
|
223
|
+
const match = content.match(pattern);
|
|
224
|
+
if (match) {
|
|
225
|
+
violations.push({
|
|
226
|
+
rule: "集体反应",
|
|
227
|
+
severity: "warning",
|
|
228
|
+
description: `出现集体反应套话:"${match[0]}"`,
|
|
229
|
+
suggestion: "改写成1-2个具体角色的身体反应",
|
|
230
|
+
});
|
|
231
|
+
break;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// 9. 连续"了"字检查(3句以上连续含"了")
|
|
236
|
+
const sentences = content
|
|
237
|
+
.split(/[。!?]/)
|
|
238
|
+
.map(s => s.trim())
|
|
239
|
+
.filter(s => s.length > 2);
|
|
240
|
+
|
|
241
|
+
let consecutiveLe = 0;
|
|
242
|
+
let maxConsecutiveLe = 0;
|
|
243
|
+
for (const sentence of sentences) {
|
|
244
|
+
if (sentence.includes("了")) {
|
|
245
|
+
consecutiveLe++;
|
|
246
|
+
maxConsecutiveLe = Math.max(maxConsecutiveLe, consecutiveLe);
|
|
247
|
+
} else {
|
|
248
|
+
consecutiveLe = 0;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
if (maxConsecutiveLe >= 6) {
|
|
252
|
+
violations.push({
|
|
253
|
+
rule: "连续了字",
|
|
254
|
+
severity: "warning",
|
|
255
|
+
description: `检测到${maxConsecutiveLe}句连续包含"了"字,节奏拖沓`,
|
|
256
|
+
suggestion: "保留最有力的一个「了」,其余改为无「了」句式",
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// 10. 段落长度检查(手机阅读适配:50-250字/段为宜)
|
|
261
|
+
const paragraphs = content
|
|
262
|
+
.split(/\n\s*\n/)
|
|
263
|
+
.map(p => p.trim())
|
|
264
|
+
.filter(p => p.length > 0);
|
|
265
|
+
|
|
266
|
+
const longParagraphs = paragraphs.filter(p => p.length > 300);
|
|
267
|
+
if (longParagraphs.length >= 2) {
|
|
268
|
+
violations.push({
|
|
269
|
+
rule: "段落过长",
|
|
270
|
+
severity: "warning",
|
|
271
|
+
description: `${longParagraphs.length}个段落超过300字,不适合手机阅读`,
|
|
272
|
+
suggestion: "长段落拆分为3-5行的短段落,在动作切换或情绪节点处断开",
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
violations.push(...detectParagraphShapeWarnings(content, "zh"));
|
|
277
|
+
|
|
278
|
+
// 11. Book-level prohibitions
|
|
279
|
+
// Short prohibitions (2-30 chars): exact substring match
|
|
280
|
+
// Long prohibitions (>30 chars): skip — these are conceptual rules for prompt-level enforcement only
|
|
281
|
+
if (bookRules?.prohibitions) {
|
|
282
|
+
for (const prohibition of bookRules.prohibitions) {
|
|
283
|
+
if (prohibition.length >= 2 && prohibition.length <= 30 && content.includes(prohibition)) {
|
|
284
|
+
violations.push({
|
|
285
|
+
rule: "本书禁忌",
|
|
286
|
+
severity: "error",
|
|
287
|
+
description: `出现了本书禁忌内容:"${prohibition}"`,
|
|
288
|
+
suggestion: "删除或改写该内容",
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
return violations;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Cross-chapter repetition check.
|
|
299
|
+
* Detects phrases from the current chapter that also appeared in recent chapters.
|
|
300
|
+
*/
|
|
301
|
+
export function detectCrossChapterRepetition(
|
|
302
|
+
currentContent: string,
|
|
303
|
+
recentChaptersContent: string,
|
|
304
|
+
language: "zh" | "en" = "zh",
|
|
305
|
+
): ReadonlyArray<PostWriteViolation> {
|
|
306
|
+
if (!recentChaptersContent || recentChaptersContent.length < 100) return [];
|
|
307
|
+
|
|
308
|
+
const violations: PostWriteViolation[] = [];
|
|
309
|
+
const isEnglish = language === "en";
|
|
310
|
+
|
|
311
|
+
if (isEnglish) {
|
|
312
|
+
// Extract 3-word phrases from current chapter
|
|
313
|
+
const words = currentContent.toLowerCase().replace(/[^\w\s']/g, "").split(/\s+/).filter(w => w.length > 2);
|
|
314
|
+
const phraseCounts = new Map<string, number>();
|
|
315
|
+
for (let i = 0; i < words.length - 2; i++) {
|
|
316
|
+
const phrase = `${words[i]} ${words[i + 1]} ${words[i + 2]}`;
|
|
317
|
+
phraseCounts.set(phrase, (phraseCounts.get(phrase) ?? 0) + 1);
|
|
318
|
+
}
|
|
319
|
+
// Check which repeated phrases (2+ in current) also appear in recent chapters
|
|
320
|
+
const recentLower = recentChaptersContent.toLowerCase();
|
|
321
|
+
const crossRepeats: string[] = [];
|
|
322
|
+
for (const [phrase, count] of phraseCounts) {
|
|
323
|
+
if (count >= 2 && recentLower.includes(phrase)) {
|
|
324
|
+
crossRepeats.push(`"${phrase}" (×${count})`);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
if (crossRepeats.length >= 3) {
|
|
328
|
+
violations.push({
|
|
329
|
+
rule: "Cross-chapter repetition",
|
|
330
|
+
severity: "warning",
|
|
331
|
+
description: `${crossRepeats.length} repeated phrases also found in recent chapters: ${crossRepeats.slice(0, 5).join(", ")}`,
|
|
332
|
+
suggestion: "Vary action verbs and descriptive phrases to avoid cross-chapter repetition",
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
} else {
|
|
336
|
+
// Chinese: 6-char ngrams
|
|
337
|
+
const chars = currentContent.replace(/[\s\n\r]/g, "");
|
|
338
|
+
const phraseCounts = new Map<string, number>();
|
|
339
|
+
for (let i = 0; i < chars.length - 5; i++) {
|
|
340
|
+
const phrase = chars.slice(i, i + 6);
|
|
341
|
+
if (/^[\u4e00-\u9fff]{6}$/.test(phrase)) {
|
|
342
|
+
phraseCounts.set(phrase, (phraseCounts.get(phrase) ?? 0) + 1);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
const recentClean = recentChaptersContent.replace(/[\s\n\r]/g, "");
|
|
346
|
+
const crossRepeats: string[] = [];
|
|
347
|
+
for (const [phrase, count] of phraseCounts) {
|
|
348
|
+
if (count >= 2 && recentClean.includes(phrase)) {
|
|
349
|
+
crossRepeats.push(`"${phrase}"(×${count})`);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
if (crossRepeats.length >= 3) {
|
|
353
|
+
violations.push({
|
|
354
|
+
rule: "跨章重复",
|
|
355
|
+
severity: "warning",
|
|
356
|
+
description: `${crossRepeats.length}个重复短语在近期章节中也出现过:${crossRepeats.slice(0, 5).join("、")}`,
|
|
357
|
+
suggestion: "变换动作描写和场景用语,避免跨章节机械重复",
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
return violations;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
export function detectParagraphLengthDrift(
|
|
366
|
+
currentContent: string,
|
|
367
|
+
recentChaptersContent: string,
|
|
368
|
+
language: "zh" | "en" = "zh",
|
|
369
|
+
): ReadonlyArray<PostWriteViolation> {
|
|
370
|
+
if (!recentChaptersContent || recentChaptersContent.trim().length === 0) return [];
|
|
371
|
+
|
|
372
|
+
const current = analyzeParagraphShape(currentContent, language);
|
|
373
|
+
const recent = analyzeParagraphShape(recentChaptersContent, language);
|
|
374
|
+
|
|
375
|
+
if (current.paragraphs.length < 4 || recent.paragraphs.length < 4) return [];
|
|
376
|
+
if (recent.averageLength <= 0 || current.averageLength <= 0) return [];
|
|
377
|
+
|
|
378
|
+
const shrinkRatio = current.averageLength / recent.averageLength;
|
|
379
|
+
const shortRatioDelta = current.shortRatio - recent.shortRatio;
|
|
380
|
+
|
|
381
|
+
if (shrinkRatio >= 0.6 || current.shortRatio < 0.5 || shortRatioDelta < 0.25) {
|
|
382
|
+
return [];
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
const dropPercent = Math.round((1 - shrinkRatio) * 100);
|
|
386
|
+
|
|
387
|
+
return [
|
|
388
|
+
language === "en"
|
|
389
|
+
? {
|
|
390
|
+
rule: "Paragraph density drift",
|
|
391
|
+
severity: "warning",
|
|
392
|
+
description: `Average paragraph length dropped from ${Math.round(recent.averageLength)} to ${Math.round(current.averageLength)} characters (${dropPercent}% shorter) compared with recent chapters.`,
|
|
393
|
+
suggestion: "Let action, observation, and reaction share paragraphs more often instead of cutting every beat into a single short line.",
|
|
394
|
+
}
|
|
395
|
+
: {
|
|
396
|
+
rule: "段落密度漂移",
|
|
397
|
+
severity: "warning",
|
|
398
|
+
description: `当前章平均段长从近期章节的${Math.round(recent.averageLength)}字降到${Math.round(current.averageLength)}字,缩短了${dropPercent}%。`,
|
|
399
|
+
suggestion: "不要把每个动作都切成单独短句;适当把动作、观察和反应并入同一段,恢复段落层次。",
|
|
400
|
+
},
|
|
401
|
+
];
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/** English-specific post-write validation rules. */
|
|
405
|
+
function validatePostWriteEnglish(
|
|
406
|
+
content: string,
|
|
407
|
+
genreProfile: GenreProfile,
|
|
408
|
+
bookRules: BookRules | null,
|
|
409
|
+
): ReadonlyArray<PostWriteViolation> {
|
|
410
|
+
const violations: PostWriteViolation[] = [];
|
|
411
|
+
|
|
412
|
+
// 1. AI-tell word density (from en-prompt-sections IRON LAW 3)
|
|
413
|
+
const aiTellWords = ["delve", "tapestry", "testament", "intricate", "pivotal", "vibrant", "embark", "comprehensive", "nuanced"];
|
|
414
|
+
for (const word of aiTellWords) {
|
|
415
|
+
const regex = new RegExp(`\\b${word}\\b`, "gi");
|
|
416
|
+
const matches = content.match(regex);
|
|
417
|
+
if (matches && matches.length > Math.ceil(content.length / 3000)) {
|
|
418
|
+
violations.push({
|
|
419
|
+
rule: "AI-tell word density",
|
|
420
|
+
severity: "warning",
|
|
421
|
+
description: `"${word}" appears ${matches.length} times (limit: 1 per 3000 chars)`,
|
|
422
|
+
suggestion: `Replace with a more specific word`,
|
|
423
|
+
});
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// 2. Paragraph overflow (same rule applies to English)
|
|
428
|
+
const paragraphs = content.split(/\n\s*\n/).filter((p) => p.trim().length > 0);
|
|
429
|
+
const longParagraphs = paragraphs.filter((p) => p.length > 500);
|
|
430
|
+
if (longParagraphs.length >= 2) {
|
|
431
|
+
violations.push({
|
|
432
|
+
rule: "Paragraph length",
|
|
433
|
+
severity: "warning",
|
|
434
|
+
description: `${longParagraphs.length} paragraphs exceed 500 characters`,
|
|
435
|
+
suggestion: "Break into shorter paragraphs for readability",
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
violations.push(...detectParagraphShapeWarnings(content, "en"));
|
|
440
|
+
|
|
441
|
+
// 2.5. Multi-character scene with almost no direct exchange
|
|
442
|
+
const quotedLines = content.match(/"[^"]+"/g) ?? [];
|
|
443
|
+
const englishNames = [...new Set(
|
|
444
|
+
(content.match(/\b[A-Z][a-z]{2,}\b/g) ?? [])
|
|
445
|
+
.filter((name) => !ENGLISH_NAME_STOP_WORDS.has(name)),
|
|
446
|
+
)];
|
|
447
|
+
if (englishNames.length >= 2 && quotedLines.length < 2 && content.length >= 120) {
|
|
448
|
+
violations.push({
|
|
449
|
+
rule: "Dialogue pressure",
|
|
450
|
+
severity: "warning",
|
|
451
|
+
description: `Multi-character scene appears to rely on narration with almost no direct exchange (${englishNames.slice(0, 3).join(", ")}).`,
|
|
452
|
+
suggestion: "Add at least one resistance-bearing exchange so characters push back, withhold, or pressure each other directly.",
|
|
453
|
+
});
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// 3. Book-specific prohibitions
|
|
457
|
+
if (bookRules?.prohibitions) {
|
|
458
|
+
for (const prohibition of bookRules.prohibitions) {
|
|
459
|
+
if (prohibition.length >= 2 && prohibition.length <= 50 && content.toLowerCase().includes(prohibition.toLowerCase())) {
|
|
460
|
+
violations.push({
|
|
461
|
+
rule: "Book prohibition",
|
|
462
|
+
severity: "error",
|
|
463
|
+
description: `Found banned content: "${prohibition}"`,
|
|
464
|
+
suggestion: "Remove or rewrite this content",
|
|
465
|
+
});
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
// 4. Genre fatigue words
|
|
471
|
+
const fatigueWords = bookRules?.fatigueWordsOverride && bookRules.fatigueWordsOverride.length > 0
|
|
472
|
+
? bookRules.fatigueWordsOverride
|
|
473
|
+
: genreProfile.fatigueWords;
|
|
474
|
+
for (const word of fatigueWords) {
|
|
475
|
+
const regex = new RegExp(`\\b${word}\\b`, "gi");
|
|
476
|
+
const matches = content.match(regex);
|
|
477
|
+
if (matches && matches.length > 1) {
|
|
478
|
+
violations.push({
|
|
479
|
+
rule: "Fatigue word",
|
|
480
|
+
severity: "warning",
|
|
481
|
+
description: `"${word}" appears ${matches.length} times (max 1 per chapter)`,
|
|
482
|
+
suggestion: "Vary the vocabulary",
|
|
483
|
+
});
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
return violations;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
function appendParagraphShapeWarnings(
|
|
491
|
+
violations: PostWriteViolation[],
|
|
492
|
+
content: string,
|
|
493
|
+
language: "zh" | "en",
|
|
494
|
+
): void {
|
|
495
|
+
const shape = analyzeParagraphShape(content, language);
|
|
496
|
+
if (shape.paragraphs.length < 4) return;
|
|
497
|
+
|
|
498
|
+
if (shape.shortParagraphs.length >= 4 && shape.shortRatio >= 0.6) {
|
|
499
|
+
violations.push(
|
|
500
|
+
language === "en"
|
|
501
|
+
? {
|
|
502
|
+
rule: "Paragraph fragmentation",
|
|
503
|
+
severity: "warning",
|
|
504
|
+
description: `${shape.shortParagraphs.length} of ${shape.paragraphs.length} paragraphs are shorter than ${shape.shortThreshold} characters.`,
|
|
505
|
+
suggestion: "Merge adjacent action, observation, and reaction beats so the chapter does not collapse into one-line paragraphs.",
|
|
506
|
+
}
|
|
507
|
+
: {
|
|
508
|
+
rule: "段落过碎",
|
|
509
|
+
severity: "warning",
|
|
510
|
+
description: `${shape.paragraphs.length}个段落里有${shape.shortParagraphs.length}个不足${shape.shortThreshold}字,段落被切得过碎。`,
|
|
511
|
+
suggestion: "把相邻的动作、观察、反应适当并段,不要每句话都单独起段。",
|
|
512
|
+
},
|
|
513
|
+
);
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
if (shape.maxConsecutiveShort >= 3) {
|
|
517
|
+
violations.push(
|
|
518
|
+
language === "en"
|
|
519
|
+
? {
|
|
520
|
+
rule: "Consecutive short paragraphs",
|
|
521
|
+
severity: "warning",
|
|
522
|
+
description: `${shape.maxConsecutiveShort} short paragraphs appear back to back.`,
|
|
523
|
+
suggestion: "Break the one-beat-per-paragraph rhythm by folding connected beats into fuller paragraphs.",
|
|
524
|
+
}
|
|
525
|
+
: {
|
|
526
|
+
rule: "连续短段",
|
|
527
|
+
severity: "warning",
|
|
528
|
+
description: `连续出现${shape.maxConsecutiveShort}个不足${shape.shortThreshold}字的短段,容易形成短句堆砌。`,
|
|
529
|
+
suggestion: "把连续的碎动作重新编组,至少让一个段落承载完整的动作链或情绪推进。",
|
|
530
|
+
},
|
|
531
|
+
);
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
export function detectParagraphShapeWarnings(
|
|
536
|
+
content: string,
|
|
537
|
+
language: "zh" | "en" = "zh",
|
|
538
|
+
): ReadonlyArray<PostWriteViolation> {
|
|
539
|
+
const violations: PostWriteViolation[] = [];
|
|
540
|
+
appendParagraphShapeWarnings(violations, content, language);
|
|
541
|
+
return violations;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
function isDialogueParagraph(paragraph: string): boolean {
|
|
545
|
+
const trimmed = paragraph.trim();
|
|
546
|
+
return /^[""「『'《]/.test(trimmed) || /^[""]/.test(trimmed) || /^——/.test(trimmed);
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
function analyzeParagraphShape(content: string, language: "zh" | "en"): ParagraphShape {
|
|
550
|
+
const paragraphs = extractParagraphs(content);
|
|
551
|
+
// Exclude dialogue lines from short paragraph counting — dialogue is naturally short
|
|
552
|
+
const narrativeParagraphs = paragraphs.filter((p) => !isDialogueParagraph(p));
|
|
553
|
+
const shortThreshold = language === "en" ? 120 : 35;
|
|
554
|
+
const shortParagraphs = narrativeParagraphs.filter((paragraph) => paragraph.length < shortThreshold);
|
|
555
|
+
const averageLength = paragraphs.length > 0
|
|
556
|
+
? paragraphs.reduce((sum, paragraph) => sum + paragraph.length, 0) / paragraphs.length
|
|
557
|
+
: 0;
|
|
558
|
+
|
|
559
|
+
let maxConsecutiveShort = 0;
|
|
560
|
+
let currentConsecutive = 0;
|
|
561
|
+
for (const paragraph of narrativeParagraphs) {
|
|
562
|
+
if (paragraph.length < shortThreshold) {
|
|
563
|
+
currentConsecutive++;
|
|
564
|
+
maxConsecutiveShort = Math.max(maxConsecutiveShort, currentConsecutive);
|
|
565
|
+
} else {
|
|
566
|
+
currentConsecutive = 0;
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
return {
|
|
571
|
+
paragraphs,
|
|
572
|
+
shortThreshold,
|
|
573
|
+
shortParagraphs,
|
|
574
|
+
shortRatio: narrativeParagraphs.length > 0 ? shortParagraphs.length / narrativeParagraphs.length : 0,
|
|
575
|
+
averageLength,
|
|
576
|
+
maxConsecutiveShort,
|
|
577
|
+
};
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
function extractParagraphs(content: string): string[] {
|
|
581
|
+
return content
|
|
582
|
+
.split(/\n\s*\n/)
|
|
583
|
+
.map((paragraph) => paragraph.trim())
|
|
584
|
+
.filter((paragraph) => paragraph.length > 0)
|
|
585
|
+
.filter((paragraph) => paragraph !== "---")
|
|
586
|
+
.filter((paragraph) => !paragraph.startsWith("#"));
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
const ENGLISH_NAME_STOP_WORDS = new Set([
|
|
590
|
+
"The",
|
|
591
|
+
"And",
|
|
592
|
+
"But",
|
|
593
|
+
"When",
|
|
594
|
+
"While",
|
|
595
|
+
"After",
|
|
596
|
+
"Before",
|
|
597
|
+
"Even",
|
|
598
|
+
"Then",
|
|
599
|
+
"They",
|
|
600
|
+
]);
|
|
601
|
+
|
|
602
|
+
const CHINESE_TITLE_STOP_WORDS = new Set([
|
|
603
|
+
"这次",
|
|
604
|
+
"正文",
|
|
605
|
+
"标题",
|
|
606
|
+
"重复",
|
|
607
|
+
"不同",
|
|
608
|
+
"完全",
|
|
609
|
+
"只是",
|
|
610
|
+
"碰巧",
|
|
611
|
+
"没有",
|
|
612
|
+
"回头",
|
|
613
|
+
]);
|
|
614
|
+
|
|
615
|
+
const CHINESE_TITLE_STOP_CHARS = new Set(["的", "了", "着", "一", "只", "从", "在", "和", "与", "把", "被", "有", "没", "里", "又", "才"]);
|
|
616
|
+
|
|
617
|
+
/**
|
|
618
|
+
* Detect duplicate or near-duplicate chapter titles.
|
|
619
|
+
* Compares the new title against existing chapter titles from index.
|
|
620
|
+
*/
|
|
621
|
+
export function detectDuplicateTitle(
|
|
622
|
+
newTitle: string,
|
|
623
|
+
existingTitles: ReadonlyArray<string>,
|
|
624
|
+
): ReadonlyArray<PostWriteViolation> {
|
|
625
|
+
if (!newTitle.trim()) return [];
|
|
626
|
+
|
|
627
|
+
const normalized = newTitle.trim().toLowerCase();
|
|
628
|
+
const violations: PostWriteViolation[] = [];
|
|
629
|
+
|
|
630
|
+
for (const existing of existingTitles) {
|
|
631
|
+
const existingNorm = existing.trim().toLowerCase();
|
|
632
|
+
if (!existingNorm) continue;
|
|
633
|
+
|
|
634
|
+
// Exact match
|
|
635
|
+
if (normalized === existingNorm) {
|
|
636
|
+
violations.push({
|
|
637
|
+
rule: "duplicate-title",
|
|
638
|
+
severity: "warning",
|
|
639
|
+
description: `章节标题"${newTitle}"与已有章节标题完全相同`,
|
|
640
|
+
suggestion: "更换一个不同的章节标题",
|
|
641
|
+
});
|
|
642
|
+
break;
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
// Near-duplicate: one is substring of the other, or only differs by punctuation/numbers
|
|
646
|
+
const stripPunct = (s: string) => s.replace(/[^\p{L}\p{N}]/gu, "");
|
|
647
|
+
if (stripPunct(normalized) === stripPunct(existingNorm)) {
|
|
648
|
+
violations.push({
|
|
649
|
+
rule: "near-duplicate-title",
|
|
650
|
+
severity: "warning",
|
|
651
|
+
description: `章节标题"${newTitle}"与已有标题"${existing}"高度相似`,
|
|
652
|
+
suggestion: "避免使用相似的章节标题",
|
|
653
|
+
});
|
|
654
|
+
break;
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
return violations;
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
export function resolveDuplicateTitle(
|
|
662
|
+
newTitle: string,
|
|
663
|
+
existingTitles: ReadonlyArray<string>,
|
|
664
|
+
language: "zh" | "en" = "zh",
|
|
665
|
+
options?: {
|
|
666
|
+
readonly content?: string;
|
|
667
|
+
},
|
|
668
|
+
): {
|
|
669
|
+
readonly title: string;
|
|
670
|
+
readonly issues: ReadonlyArray<PostWriteViolation>;
|
|
671
|
+
} {
|
|
672
|
+
const trimmed = newTitle.trim();
|
|
673
|
+
if (!trimmed) {
|
|
674
|
+
return { title: newTitle, issues: [] };
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
const duplicateIssues = detectDuplicateTitle(trimmed, existingTitles);
|
|
678
|
+
if (duplicateIssues.length > 0) {
|
|
679
|
+
const regenerated = regenerateDuplicateTitle(trimmed, existingTitles, language, options?.content);
|
|
680
|
+
if (regenerated && detectDuplicateTitle(regenerated, existingTitles).length === 0) {
|
|
681
|
+
return { title: regenerated, issues: duplicateIssues };
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
let counter = 2;
|
|
685
|
+
while (counter < 100) {
|
|
686
|
+
const candidate = language === "en"
|
|
687
|
+
? `${trimmed} (${counter})`
|
|
688
|
+
: `${trimmed}(${counter})`;
|
|
689
|
+
if (detectDuplicateTitle(candidate, existingTitles).length === 0) {
|
|
690
|
+
return { title: candidate, issues: duplicateIssues };
|
|
691
|
+
}
|
|
692
|
+
counter++;
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
return { title: trimmed, issues: duplicateIssues };
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
const collapseIssues = detectTitleCollapse(trimmed, existingTitles, language);
|
|
699
|
+
if (collapseIssues.length === 0) {
|
|
700
|
+
return { title: trimmed, issues: [] };
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
const regenerated = regenerateCollapsedTitle(trimmed, existingTitles, language, options?.content);
|
|
704
|
+
if (
|
|
705
|
+
regenerated
|
|
706
|
+
&& detectDuplicateTitle(regenerated, existingTitles).length === 0
|
|
707
|
+
&& detectTitleCollapse(regenerated, existingTitles, language).length === 0
|
|
708
|
+
) {
|
|
709
|
+
return { title: regenerated, issues: collapseIssues };
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
return { title: trimmed, issues: collapseIssues };
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
function detectTitleCollapse(
|
|
716
|
+
newTitle: string,
|
|
717
|
+
existingTitles: ReadonlyArray<string>,
|
|
718
|
+
language: "zh" | "en",
|
|
719
|
+
): ReadonlyArray<PostWriteViolation> {
|
|
720
|
+
const recentTitles = existingTitles
|
|
721
|
+
.map((title) => title.trim())
|
|
722
|
+
.filter(Boolean)
|
|
723
|
+
.slice(-3);
|
|
724
|
+
if (recentTitles.length < 3) {
|
|
725
|
+
return [];
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
const cadence = analyzeChapterCadence({
|
|
729
|
+
language,
|
|
730
|
+
rows: [...recentTitles, newTitle].map((title, index) => ({
|
|
731
|
+
chapter: index + 1,
|
|
732
|
+
title,
|
|
733
|
+
mood: "",
|
|
734
|
+
chapterType: "",
|
|
735
|
+
})),
|
|
736
|
+
});
|
|
737
|
+
const titlePressure = cadence.titlePressure;
|
|
738
|
+
if (!titlePressure || titlePressure.pressure !== "high") {
|
|
739
|
+
return [];
|
|
740
|
+
}
|
|
741
|
+
if (!newTitle.includes(titlePressure.repeatedToken)) {
|
|
742
|
+
return [];
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
return [
|
|
746
|
+
language === "en"
|
|
747
|
+
? {
|
|
748
|
+
rule: "title-collapse",
|
|
749
|
+
severity: "warning",
|
|
750
|
+
description: `Chapter title "${newTitle}" keeps leaning on the recent "${titlePressure.repeatedToken}" title shell.`,
|
|
751
|
+
suggestion: "Rename the chapter around a new image, action, consequence, or character focus.",
|
|
752
|
+
}
|
|
753
|
+
: {
|
|
754
|
+
rule: "title-collapse",
|
|
755
|
+
severity: "warning",
|
|
756
|
+
description: `章节标题"${newTitle}"仍在沿用近期围绕“${titlePressure.repeatedToken}”的命名壳。`,
|
|
757
|
+
suggestion: "换一个新的意象、动作、后果或人物焦点来命名。",
|
|
758
|
+
},
|
|
759
|
+
];
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
function regenerateDuplicateTitle(
|
|
763
|
+
baseTitle: string,
|
|
764
|
+
existingTitles: ReadonlyArray<string>,
|
|
765
|
+
language: "zh" | "en",
|
|
766
|
+
content?: string,
|
|
767
|
+
): string | undefined {
|
|
768
|
+
if (!content || !content.trim()) {
|
|
769
|
+
return undefined;
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
const qualifier = language === "en"
|
|
773
|
+
? extractEnglishTitleQualifier(baseTitle, existingTitles, content)
|
|
774
|
+
: extractChineseTitleQualifier(baseTitle, existingTitles, content);
|
|
775
|
+
if (!qualifier) {
|
|
776
|
+
return undefined;
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
return language === "en"
|
|
780
|
+
? `${baseTitle}: ${qualifier}`
|
|
781
|
+
: `${baseTitle}:${qualifier}`;
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
function regenerateCollapsedTitle(
|
|
785
|
+
baseTitle: string,
|
|
786
|
+
existingTitles: ReadonlyArray<string>,
|
|
787
|
+
language: "zh" | "en",
|
|
788
|
+
content?: string,
|
|
789
|
+
): string | undefined {
|
|
790
|
+
if (!content || !content.trim()) {
|
|
791
|
+
return undefined;
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
const fresh = language === "en"
|
|
795
|
+
? extractEnglishTitleQualifier(baseTitle, existingTitles, content)
|
|
796
|
+
: extractChineseTitleQualifier(baseTitle, existingTitles, content);
|
|
797
|
+
if (!fresh) {
|
|
798
|
+
return undefined;
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
return fresh === baseTitle ? undefined : fresh;
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
function extractEnglishTitleQualifier(
|
|
805
|
+
baseTitle: string,
|
|
806
|
+
existingTitles: ReadonlyArray<string>,
|
|
807
|
+
content: string,
|
|
808
|
+
): string | undefined {
|
|
809
|
+
const blocked = new Set(extractEnglishTitleTerms([baseTitle, ...existingTitles].join(" ")));
|
|
810
|
+
const words = (content.match(/[A-Za-z]{4,}/g) ?? [])
|
|
811
|
+
.map((word) => word.toLowerCase())
|
|
812
|
+
.filter((word) => !ENGLISH_NAME_STOP_WORDS.has(capitalize(word)))
|
|
813
|
+
.filter((word) => !blocked.has(word));
|
|
814
|
+
const first = words[0];
|
|
815
|
+
if (!first) {
|
|
816
|
+
return undefined;
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
const second = words.find((word) => word !== first && !blocked.has(word));
|
|
820
|
+
return second
|
|
821
|
+
? `${capitalize(first)} ${capitalize(second)}`
|
|
822
|
+
: capitalize(first);
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
function extractChineseTitleQualifier(
|
|
826
|
+
baseTitle: string,
|
|
827
|
+
existingTitles: ReadonlyArray<string>,
|
|
828
|
+
content: string,
|
|
829
|
+
): string | undefined {
|
|
830
|
+
const blocked = new Set(extractChineseTitleTerms([baseTitle, ...existingTitles].join("")));
|
|
831
|
+
const segments = content.match(/[\u4e00-\u9fff]+/g) ?? [];
|
|
832
|
+
|
|
833
|
+
for (const segment of segments) {
|
|
834
|
+
for (let start = 0; start < segment.length; start += 1) {
|
|
835
|
+
for (let size = 2; size <= 4; size += 1) {
|
|
836
|
+
const candidate = segment.slice(start, start + size).trim();
|
|
837
|
+
if (candidate.length < 2) continue;
|
|
838
|
+
if (CHINESE_TITLE_STOP_WORDS.has(candidate)) continue;
|
|
839
|
+
if ([...candidate].some((char) => CHINESE_TITLE_STOP_CHARS.has(char))) continue;
|
|
840
|
+
if (blocked.has(candidate)) continue;
|
|
841
|
+
return candidate;
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
return undefined;
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
function extractEnglishTitleTerms(text: string): string[] {
|
|
850
|
+
return [...new Set((text.match(/[A-Za-z]{4,}/g) ?? []).map((word) => word.toLowerCase()))];
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
function extractChineseTitleTerms(text: string): string[] {
|
|
854
|
+
const terms = new Set<string>();
|
|
855
|
+
const segments = text.match(/[\u4e00-\u9fff]+/g) ?? [];
|
|
856
|
+
|
|
857
|
+
for (const segment of segments) {
|
|
858
|
+
for (let start = 0; start < segment.length; start += 1) {
|
|
859
|
+
for (let size = 2; size <= 4; size += 1) {
|
|
860
|
+
const candidate = segment.slice(start, start + size).trim();
|
|
861
|
+
if (candidate.length < 2) continue;
|
|
862
|
+
if ([...candidate].some((char) => CHINESE_TITLE_STOP_CHARS.has(char))) continue;
|
|
863
|
+
terms.add(candidate);
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
return [...terms];
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
function capitalize(word: string): string {
|
|
872
|
+
return word.length === 0 ? word : `${word[0]!.toUpperCase()}${word.slice(1)}`;
|
|
873
|
+
}
|