@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,1360 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { mkdtemp, mkdir, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { WriterAgent } from "../agents/writer.js";
|
|
6
|
+
import { buildLengthSpec } from "../utils/length-metrics.js";
|
|
7
|
+
|
|
8
|
+
const ZERO_USAGE = {
|
|
9
|
+
promptTokens: 0,
|
|
10
|
+
completionTokens: 0,
|
|
11
|
+
totalTokens: 0,
|
|
12
|
+
} as const;
|
|
13
|
+
|
|
14
|
+
function createCaptureLogger() {
|
|
15
|
+
const infos: string[] = [];
|
|
16
|
+
const warnings: string[] = [];
|
|
17
|
+
|
|
18
|
+
const logger = {
|
|
19
|
+
debug() {},
|
|
20
|
+
info(message: string) {
|
|
21
|
+
infos.push(message);
|
|
22
|
+
},
|
|
23
|
+
warn(message: string) {
|
|
24
|
+
warnings.push(message);
|
|
25
|
+
},
|
|
26
|
+
error() {},
|
|
27
|
+
child() {
|
|
28
|
+
return logger;
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
return { logger, infos, warnings };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
describe("WriterAgent", () => {
|
|
36
|
+
afterEach(() => {
|
|
37
|
+
vi.restoreAllMocks();
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("renders per-chapter user context in governed creative prompts", () => {
|
|
41
|
+
const agent = new WriterAgent({
|
|
42
|
+
client: {
|
|
43
|
+
provider: "openai",
|
|
44
|
+
apiFormat: "chat",
|
|
45
|
+
stream: false,
|
|
46
|
+
defaults: {
|
|
47
|
+
temperature: 0.7,
|
|
48
|
+
maxTokens: 4096,
|
|
49
|
+
thinkingBudget: 0,
|
|
50
|
+
extra: {},
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
model: "test-model",
|
|
54
|
+
projectRoot: "/tmp/inkos-writer-context-test",
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
const prompt = (agent as unknown as {
|
|
58
|
+
buildGovernedUserPrompt(params: {
|
|
59
|
+
readonly chapterNumber: number;
|
|
60
|
+
readonly chapterMemo: {
|
|
61
|
+
readonly chapter: number;
|
|
62
|
+
readonly goal: string;
|
|
63
|
+
readonly isGoldenOpening: boolean;
|
|
64
|
+
readonly body: string;
|
|
65
|
+
readonly threadRefs: readonly string[];
|
|
66
|
+
};
|
|
67
|
+
readonly contextPackage: { readonly chapter: number; readonly selectedContext: readonly [] };
|
|
68
|
+
readonly ruleStack: {
|
|
69
|
+
readonly layers: readonly [];
|
|
70
|
+
readonly sections: { readonly hard: readonly string[]; readonly soft: readonly string[]; readonly diagnostic: readonly string[] };
|
|
71
|
+
readonly overrideEdges: readonly [];
|
|
72
|
+
readonly activeOverrides: readonly [];
|
|
73
|
+
};
|
|
74
|
+
readonly lengthSpec: ReturnType<typeof buildLengthSpec>;
|
|
75
|
+
readonly language?: "zh" | "en";
|
|
76
|
+
readonly externalContext?: string;
|
|
77
|
+
}): string;
|
|
78
|
+
}).buildGovernedUserPrompt({
|
|
79
|
+
chapterNumber: 7,
|
|
80
|
+
chapterMemo: {
|
|
81
|
+
chapter: 7,
|
|
82
|
+
goal: "推进账本线",
|
|
83
|
+
isGoldenOpening: false,
|
|
84
|
+
body: "## 当前任务\n围绕账本线推进。",
|
|
85
|
+
threadRefs: [],
|
|
86
|
+
},
|
|
87
|
+
contextPackage: { chapter: 7, selectedContext: [] },
|
|
88
|
+
ruleStack: {
|
|
89
|
+
layers: [],
|
|
90
|
+
sections: { hard: [], soft: [], diagnostic: [] },
|
|
91
|
+
overrideEdges: [],
|
|
92
|
+
activeOverrides: [],
|
|
93
|
+
},
|
|
94
|
+
lengthSpec: buildLengthSpec(1200, "zh"),
|
|
95
|
+
language: "zh",
|
|
96
|
+
externalContext: "本章标题:雨夜账本\n必须围绕账本失窃后的当面对质展开。",
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
expect(prompt).toContain("本章用户指令");
|
|
100
|
+
expect(prompt).toContain("本章标题:雨夜账本");
|
|
101
|
+
expect(prompt).toContain("当面对质");
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("caps oversized legacy truth files in creative prompts", () => {
|
|
105
|
+
const agent = new WriterAgent({
|
|
106
|
+
client: {
|
|
107
|
+
provider: "openai",
|
|
108
|
+
apiFormat: "chat",
|
|
109
|
+
stream: false,
|
|
110
|
+
defaults: {
|
|
111
|
+
temperature: 0.7,
|
|
112
|
+
maxTokens: 4096,
|
|
113
|
+
thinkingBudget: 0,
|
|
114
|
+
extra: {},
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
model: "test-model",
|
|
118
|
+
projectRoot: "/tmp/inkos-writer-context-budget-test",
|
|
119
|
+
});
|
|
120
|
+
const oversizedStoryBible = [
|
|
121
|
+
"BEGIN-STORY",
|
|
122
|
+
"旧设定。".repeat(4000),
|
|
123
|
+
"MIDDLE-MARKER",
|
|
124
|
+
"近期设定。".repeat(4000),
|
|
125
|
+
"LATEST-STORY",
|
|
126
|
+
].join("\n");
|
|
127
|
+
|
|
128
|
+
const prompt = (agent as unknown as {
|
|
129
|
+
buildUserPrompt(params: {
|
|
130
|
+
readonly chapterNumber: number;
|
|
131
|
+
readonly storyBible: string;
|
|
132
|
+
readonly currentState: string;
|
|
133
|
+
readonly ledger: string;
|
|
134
|
+
readonly hooks: string;
|
|
135
|
+
readonly recentChapters: string;
|
|
136
|
+
readonly lengthSpec: ReturnType<typeof buildLengthSpec>;
|
|
137
|
+
readonly chapterSummaries: string;
|
|
138
|
+
readonly subplotBoard: string;
|
|
139
|
+
readonly emotionalArcs: string;
|
|
140
|
+
readonly characterMatrix: string;
|
|
141
|
+
readonly language?: "zh" | "en";
|
|
142
|
+
}): string;
|
|
143
|
+
}).buildUserPrompt({
|
|
144
|
+
chapterNumber: 88,
|
|
145
|
+
storyBible: oversizedStoryBible,
|
|
146
|
+
currentState: "(文件尚未创建)",
|
|
147
|
+
ledger: "",
|
|
148
|
+
hooks: "(文件尚未创建)",
|
|
149
|
+
recentChapters: "",
|
|
150
|
+
lengthSpec: buildLengthSpec(1200, "zh"),
|
|
151
|
+
chapterSummaries: "(文件尚未创建)",
|
|
152
|
+
subplotBoard: "(文件尚未创建)",
|
|
153
|
+
emotionalArcs: "(文件尚未创建)",
|
|
154
|
+
characterMatrix: "(文件尚未创建)",
|
|
155
|
+
language: "zh",
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
expect(prompt).toContain("BEGIN-STORY");
|
|
159
|
+
expect(prompt).toContain("LATEST-STORY");
|
|
160
|
+
expect(prompt).toContain("InkOS context budget");
|
|
161
|
+
expect(prompt).toContain("story_bible");
|
|
162
|
+
expect(prompt).not.toContain("MIDDLE-MARKER");
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it("uses compact summary context plus selected long-range evidence during governed settlement", async () => {
|
|
166
|
+
const root = await mkdtemp(join(tmpdir(), "inkos-writer-test-"));
|
|
167
|
+
const bookDir = join(root, "book");
|
|
168
|
+
const storyDir = join(bookDir, "story");
|
|
169
|
+
await mkdir(storyDir, { recursive: true });
|
|
170
|
+
|
|
171
|
+
await Promise.all([
|
|
172
|
+
writeFile(join(storyDir, "story_bible.md"), "# Story Bible\n\n- The jade seal cannot be destroyed.\n", "utf-8"),
|
|
173
|
+
writeFile(join(storyDir, "volume_outline.md"), "# Volume Outline\n\n## Chapter 100\nTrack the merchant guild trail.\n", "utf-8"),
|
|
174
|
+
writeFile(join(storyDir, "style_guide.md"), "# Style Guide\n\n- Keep the prose restrained.\n", "utf-8"),
|
|
175
|
+
writeFile(join(storyDir, "current_state.md"), "# Current State\n\n- Lin Yue still hides the broken oath token.\n", "utf-8"),
|
|
176
|
+
writeFile(join(storyDir, "pending_hooks.md"), [
|
|
177
|
+
"# Pending Hooks",
|
|
178
|
+
"",
|
|
179
|
+
"| hook_id | 起始章节 | 类型 | 状态 | 最近推进 | 预期回收 | 备注 |",
|
|
180
|
+
"| --- | --- | --- | --- | --- | --- | --- |",
|
|
181
|
+
"| guild-route | 1 | mystery | open | 2 | 6 | Merchant guild trail |",
|
|
182
|
+
"| old-seal | 3 | artifact | open | 12 | 40 | Old seal detour |",
|
|
183
|
+
"| stale-ledger | 14 | mystery | open | 70 | 120 | Old ledger debt is dormant but unresolved |",
|
|
184
|
+
"| mentor-oath | 8 | relationship | open | 99 | 101 | Mentor oath debt with Lin Yue |",
|
|
185
|
+
].join("\n"), "utf-8"),
|
|
186
|
+
writeFile(join(storyDir, "chapter_summaries.md"), [
|
|
187
|
+
"# Chapter Summaries",
|
|
188
|
+
"",
|
|
189
|
+
"| 1 | Guild Trail | Merchant guild flees west | Route clues only | None | guild-route seeded | tense | action |",
|
|
190
|
+
"| 97 | Shrine Ash | Lin Yue | The old shrine proves empty | Frustration rises | none | bitter | setback |",
|
|
191
|
+
"| 98 | Trial Echo | Lin Yue | Mentor left without explanation | Oath token matters again | mentor-oath advanced | aching | fallout |",
|
|
192
|
+
"| 99 | Locked Gate | Lin Yue | Lin Yue chooses the mentor line over the guild line | Mentor conflict takes priority | mentor-oath advanced | focused | decision |",
|
|
193
|
+
].join("\n"), "utf-8"),
|
|
194
|
+
writeFile(join(storyDir, "subplot_board.md"), [
|
|
195
|
+
"# 支线进度板",
|
|
196
|
+
"",
|
|
197
|
+
"| 支线ID | 支线名 | 相关角色 | 起始章 | 最近活跃章 | 距今章数 | 状态 | 进度概述 | 回收ETA |",
|
|
198
|
+
"| --- | --- | --- | --- | --- | --- | --- | --- | --- |",
|
|
199
|
+
"| SP-mentor | 师债线 | Lin Yue | 8 | 99 | 1 | active | 师债继续推进 | 101 |",
|
|
200
|
+
"| SP-seal | 旧印支线 | Guildmaster Ren | 3 | 12 | 88 | closed | 旧印已回收 | 12 |",
|
|
201
|
+
].join("\n"), "utf-8"),
|
|
202
|
+
writeFile(join(storyDir, "emotional_arcs.md"), [
|
|
203
|
+
"# 情感弧线",
|
|
204
|
+
"",
|
|
205
|
+
"| 角色 | 章节 | 情绪状态 | 触发事件 | 强度(1-10) | 弧线方向 |",
|
|
206
|
+
"| --- | --- | --- | --- | --- | --- |",
|
|
207
|
+
"| Lin Yue | 40 | 麻木 | 旧印支线拖延 | 4 | 停滞 |",
|
|
208
|
+
"| Lin Yue | 99 | 紧绷 | 师债重新压上来 | 8 | 收紧 |",
|
|
209
|
+
].join("\n"), "utf-8"),
|
|
210
|
+
writeFile(join(storyDir, "character_matrix.md"), [
|
|
211
|
+
"# 角色交互矩阵",
|
|
212
|
+
"",
|
|
213
|
+
"### 角色档案",
|
|
214
|
+
"| 角色 | 核心标签 | 反差细节 | 说话风格 | 性格底色 | 与主角关系 | 核心动机 | 当前目标 |",
|
|
215
|
+
"| --- | --- | --- | --- | --- | --- | --- | --- |",
|
|
216
|
+
"| Lin Yue | oath | restraint | clipped | stubborn | self | repay debt | find mentor |",
|
|
217
|
+
"| Guildmaster Ren | guild | swagger | loud | opportunistic | rival | stall Mara | seize seal |",
|
|
218
|
+
].join("\n"), "utf-8"),
|
|
219
|
+
]);
|
|
220
|
+
|
|
221
|
+
const agent = new WriterAgent({
|
|
222
|
+
client: {
|
|
223
|
+
provider: "openai",
|
|
224
|
+
apiFormat: "chat",
|
|
225
|
+
stream: false,
|
|
226
|
+
defaults: {
|
|
227
|
+
temperature: 0.7,
|
|
228
|
+
maxTokens: 4096,
|
|
229
|
+
thinkingBudget: 0,
|
|
230
|
+
extra: {},
|
|
231
|
+
},
|
|
232
|
+
},
|
|
233
|
+
model: "test-model",
|
|
234
|
+
projectRoot: root,
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
const chatSpy = vi.spyOn(WriterAgent.prototype as never, "chat" as never)
|
|
238
|
+
.mockResolvedValueOnce({
|
|
239
|
+
content: [
|
|
240
|
+
"=== CHAPTER_TITLE ===",
|
|
241
|
+
"A Decision",
|
|
242
|
+
"",
|
|
243
|
+
"=== CHAPTER_CONTENT ===",
|
|
244
|
+
"Lin Yue turned away from the guild trail and chose the mentor debt.",
|
|
245
|
+
"",
|
|
246
|
+
"=== PRE_WRITE_CHECK ===",
|
|
247
|
+
"- ok",
|
|
248
|
+
].join("\n"),
|
|
249
|
+
usage: ZERO_USAGE,
|
|
250
|
+
})
|
|
251
|
+
.mockResolvedValueOnce({
|
|
252
|
+
content: "=== OBSERVATIONS ===\n- observed",
|
|
253
|
+
usage: ZERO_USAGE,
|
|
254
|
+
})
|
|
255
|
+
.mockResolvedValueOnce({
|
|
256
|
+
content: [
|
|
257
|
+
"=== POST_SETTLEMENT ===",
|
|
258
|
+
"| 伏笔变动 | mentor-oath 推进 | 同步更新伏笔池 |",
|
|
259
|
+
"",
|
|
260
|
+
"=== UPDATED_STATE ===",
|
|
261
|
+
"状态卡",
|
|
262
|
+
"",
|
|
263
|
+
"=== UPDATED_HOOKS ===",
|
|
264
|
+
"伏笔池",
|
|
265
|
+
"",
|
|
266
|
+
"=== CHAPTER_SUMMARY ===",
|
|
267
|
+
"| 100 | A Decision | Lin Yue | Chooses the mentor debt | Focus narrowed | mentor-oath advanced | tense | decision |",
|
|
268
|
+
"",
|
|
269
|
+
"=== UPDATED_SUBPLOTS ===",
|
|
270
|
+
"支线板",
|
|
271
|
+
"",
|
|
272
|
+
"=== UPDATED_EMOTIONAL_ARCS ===",
|
|
273
|
+
"情感弧线",
|
|
274
|
+
"",
|
|
275
|
+
"=== UPDATED_CHARACTER_MATRIX ===",
|
|
276
|
+
"角色矩阵",
|
|
277
|
+
].join("\n"),
|
|
278
|
+
usage: ZERO_USAGE,
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
try {
|
|
282
|
+
await agent.writeChapter({
|
|
283
|
+
book: {
|
|
284
|
+
id: "writer-book",
|
|
285
|
+
title: "Writer Book",
|
|
286
|
+
platform: "tomato",
|
|
287
|
+
genre: "xuanhuan",
|
|
288
|
+
status: "active",
|
|
289
|
+
targetChapters: 120,
|
|
290
|
+
chapterWordCount: 2200,
|
|
291
|
+
createdAt: "2026-03-23T00:00:00.000Z",
|
|
292
|
+
updatedAt: "2026-03-23T00:00:00.000Z",
|
|
293
|
+
},
|
|
294
|
+
bookDir,
|
|
295
|
+
chapterNumber: 100,
|
|
296
|
+
chapterIntent: [
|
|
297
|
+
"# Chapter Intent",
|
|
298
|
+
"",
|
|
299
|
+
"## Goal",
|
|
300
|
+
"Bring the focus back to the mentor oath conflict.",
|
|
301
|
+
"",
|
|
302
|
+
"## Hook Agenda",
|
|
303
|
+
"### Must Advance",
|
|
304
|
+
"- mentor-oath",
|
|
305
|
+
"",
|
|
306
|
+
"### Eligible Resolve",
|
|
307
|
+
"- none",
|
|
308
|
+
"",
|
|
309
|
+
"### Stale Debt",
|
|
310
|
+
"- stale-ledger",
|
|
311
|
+
"",
|
|
312
|
+
"### Avoid New Hook Families",
|
|
313
|
+
"- none",
|
|
314
|
+
].join("\n"),
|
|
315
|
+
contextPackage: {
|
|
316
|
+
chapter: 100,
|
|
317
|
+
selectedContext: [
|
|
318
|
+
{
|
|
319
|
+
source: "story/volume_outline.md",
|
|
320
|
+
reason: "Anchor the current beat.",
|
|
321
|
+
excerpt: "Bring the focus back to the mentor oath conflict.",
|
|
322
|
+
},
|
|
323
|
+
{
|
|
324
|
+
source: "story/chapter_summaries.md#99",
|
|
325
|
+
reason: "Relevant episodic memory.",
|
|
326
|
+
excerpt: "Locked Gate | Lin Yue chooses the mentor line over the guild line | mentor-oath advanced",
|
|
327
|
+
},
|
|
328
|
+
{
|
|
329
|
+
source: "story/pending_hooks.md#mentor-oath",
|
|
330
|
+
reason: "Carry forward unresolved hook.",
|
|
331
|
+
excerpt: "relationship | open | 101 | Mentor oath debt with Lin Yue",
|
|
332
|
+
},
|
|
333
|
+
{
|
|
334
|
+
source: "runtime/hook_debt#mentor-oath",
|
|
335
|
+
reason: "Explicit hook debt brief for the agenda target.",
|
|
336
|
+
excerpt: "mentor-oath | cadence: slow-burn | seed: ch8 River Camp - Mentor debt becomes personal | latest: ch99 Locked Gate - Lin Yue chooses the mentor line over the guild line | unpaid: reveal why the mentor broke the oath",
|
|
337
|
+
},
|
|
338
|
+
],
|
|
339
|
+
},
|
|
340
|
+
ruleStack: {
|
|
341
|
+
layers: [{ id: "L4", name: "current_task", precedence: 70, scope: "local" }],
|
|
342
|
+
sections: {
|
|
343
|
+
hard: ["current_state"],
|
|
344
|
+
soft: ["current_focus"],
|
|
345
|
+
diagnostic: ["continuity_audit"],
|
|
346
|
+
},
|
|
347
|
+
overrideEdges: [],
|
|
348
|
+
activeOverrides: [],
|
|
349
|
+
},
|
|
350
|
+
lengthSpec: buildLengthSpec(220, "zh"),
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
const settlePrompt = (chatSpy.mock.calls[2]?.[0] as ReadonlyArray<{ content: string }> | undefined)?.[1]?.content ?? "";
|
|
354
|
+
expect(settlePrompt).toContain("## 本章控制输入");
|
|
355
|
+
expect(settlePrompt).toContain("story/chapter_summaries.md#99");
|
|
356
|
+
expect(settlePrompt).toContain("| 99 | Locked Gate |");
|
|
357
|
+
expect(settlePrompt).toContain("## Hook Debt Briefs");
|
|
358
|
+
expect(settlePrompt).toContain("mentor-oath | cadence: slow-burn");
|
|
359
|
+
expect(settlePrompt).toContain("| stale-ledger | 14 | mystery | open | 70 | 120 | 中程 | 无 | | 否 | | | Old ledger debt is dormant but unresolved |");
|
|
360
|
+
expect(settlePrompt).not.toContain("| 1 | Guild Trail |");
|
|
361
|
+
expect(settlePrompt).not.toContain("old-seal");
|
|
362
|
+
expect(settlePrompt).not.toContain("Guildmaster Ren");
|
|
363
|
+
expect(settlePrompt).not.toContain("| Lin Yue | 40 | 麻木 |");
|
|
364
|
+
} finally {
|
|
365
|
+
await rm(root, { recursive: true, force: true });
|
|
366
|
+
}
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
it("builds structured runtime-state artifacts when settler returns a delta", async () => {
|
|
370
|
+
const root = await mkdtemp(join(tmpdir(), "inkos-writer-runtime-state-test-"));
|
|
371
|
+
const bookDir = join(root, "book");
|
|
372
|
+
const storyDir = join(bookDir, "story");
|
|
373
|
+
const chaptersDir = join(bookDir, "chapters");
|
|
374
|
+
await mkdir(storyDir, { recursive: true });
|
|
375
|
+
await mkdir(chaptersDir, { recursive: true });
|
|
376
|
+
|
|
377
|
+
await Promise.all([
|
|
378
|
+
writeFile(join(chaptersDir, "index.json"), JSON.stringify([
|
|
379
|
+
{ number: 1, title: "Ch1", status: "approved" },
|
|
380
|
+
{ number: 2, title: "Ch2", status: "approved" },
|
|
381
|
+
]), "utf-8"),
|
|
382
|
+
writeFile(join(storyDir, "story_bible.md"), "# Story Bible\n\n- The jade seal cannot be destroyed.\n", "utf-8"),
|
|
383
|
+
writeFile(join(storyDir, "volume_outline.md"), "# Volume Outline\n\n## Chapter 3\nTrace the debt through the river-port ledger.\n", "utf-8"),
|
|
384
|
+
writeFile(join(storyDir, "style_guide.md"), "# Style Guide\n\n- Keep the prose restrained.\n", "utf-8"),
|
|
385
|
+
writeFile(join(storyDir, "current_state.md"), [
|
|
386
|
+
"# Current State",
|
|
387
|
+
"",
|
|
388
|
+
"| Field | Value |",
|
|
389
|
+
"| --- | --- |",
|
|
390
|
+
"| Current Chapter | 2 |",
|
|
391
|
+
"| Current Goal | Find the vanished mentor |",
|
|
392
|
+
"| Current Conflict | Guild pressure keeps colliding with the debt trail |",
|
|
393
|
+
"",
|
|
394
|
+
].join("\n"), "utf-8"),
|
|
395
|
+
writeFile(join(storyDir, "pending_hooks.md"), [
|
|
396
|
+
"| hook_id | start_chapter | type | status | last_advanced | expected_payoff | notes |",
|
|
397
|
+
"| --- | --- | --- | --- | --- | --- | --- |",
|
|
398
|
+
"| mentor-debt | 1 | relationship | open | 2 | 6 | Still unresolved |",
|
|
399
|
+
"",
|
|
400
|
+
].join("\n"), "utf-8"),
|
|
401
|
+
writeFile(join(storyDir, "chapter_summaries.md"), [
|
|
402
|
+
"| chapter | title | characters | events | stateChanges | hookActivity | mood | chapterType |",
|
|
403
|
+
"| --- | --- | --- | --- | --- | --- | --- | --- |",
|
|
404
|
+
"| 2 | Old Ledger | Lin Yue | Lin Yue finds the old ledger | Debt sharpens | mentor-debt advanced | tense | mainline |",
|
|
405
|
+
"",
|
|
406
|
+
].join("\n"), "utf-8"),
|
|
407
|
+
]);
|
|
408
|
+
|
|
409
|
+
const agent = new WriterAgent({
|
|
410
|
+
client: {
|
|
411
|
+
provider: "openai",
|
|
412
|
+
apiFormat: "chat",
|
|
413
|
+
stream: false,
|
|
414
|
+
defaults: {
|
|
415
|
+
temperature: 0.7,
|
|
416
|
+
maxTokens: 4096,
|
|
417
|
+
thinkingBudget: 0,
|
|
418
|
+
extra: {},
|
|
419
|
+
},
|
|
420
|
+
},
|
|
421
|
+
model: "test-model",
|
|
422
|
+
projectRoot: root,
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
vi.spyOn(WriterAgent.prototype as never, "chat" as never)
|
|
426
|
+
.mockResolvedValueOnce({
|
|
427
|
+
content: [
|
|
428
|
+
"=== CHAPTER_TITLE ===",
|
|
429
|
+
"River Ledger",
|
|
430
|
+
"",
|
|
431
|
+
"=== CHAPTER_CONTENT ===",
|
|
432
|
+
"Lin Yue follows the debt into the river-port ledger.",
|
|
433
|
+
"",
|
|
434
|
+
"=== PRE_WRITE_CHECK ===",
|
|
435
|
+
"- ok",
|
|
436
|
+
].join("\n"),
|
|
437
|
+
usage: ZERO_USAGE,
|
|
438
|
+
})
|
|
439
|
+
.mockResolvedValueOnce({
|
|
440
|
+
content: "=== OBSERVATIONS ===\n- observed",
|
|
441
|
+
usage: ZERO_USAGE,
|
|
442
|
+
})
|
|
443
|
+
.mockResolvedValueOnce({
|
|
444
|
+
content: [
|
|
445
|
+
"=== POST_SETTLEMENT ===",
|
|
446
|
+
"- mentor-debt advanced",
|
|
447
|
+
"",
|
|
448
|
+
"=== RUNTIME_STATE_DELTA ===",
|
|
449
|
+
"```json",
|
|
450
|
+
JSON.stringify({
|
|
451
|
+
chapter: 3,
|
|
452
|
+
currentStatePatch: {
|
|
453
|
+
currentGoal: "Trace the debt through the river-port ledger.",
|
|
454
|
+
currentConflict: "Guild pressure keeps colliding with the debt trail.",
|
|
455
|
+
},
|
|
456
|
+
hookOps: {
|
|
457
|
+
upsert: [
|
|
458
|
+
{
|
|
459
|
+
hookId: "mentor-debt",
|
|
460
|
+
startChapter: 1,
|
|
461
|
+
type: "relationship",
|
|
462
|
+
status: "progressing",
|
|
463
|
+
lastAdvancedChapter: 3,
|
|
464
|
+
expectedPayoff: "Reveal the debt.",
|
|
465
|
+
notes: "The ledger clue sharpens the line.",
|
|
466
|
+
},
|
|
467
|
+
],
|
|
468
|
+
resolve: [],
|
|
469
|
+
defer: [],
|
|
470
|
+
},
|
|
471
|
+
chapterSummary: {
|
|
472
|
+
chapter: 3,
|
|
473
|
+
title: "River Ledger",
|
|
474
|
+
characters: "Lin Yue",
|
|
475
|
+
events: "Lin Yue follows the debt into the river-port ledger.",
|
|
476
|
+
stateChanges: "The debt line sharpens.",
|
|
477
|
+
hookActivity: "mentor-debt advanced",
|
|
478
|
+
mood: "tense",
|
|
479
|
+
chapterType: "investigation",
|
|
480
|
+
},
|
|
481
|
+
notes: [],
|
|
482
|
+
}, null, 2),
|
|
483
|
+
"```",
|
|
484
|
+
].join("\n"),
|
|
485
|
+
usage: ZERO_USAGE,
|
|
486
|
+
});
|
|
487
|
+
|
|
488
|
+
try {
|
|
489
|
+
const output = await agent.writeChapter({
|
|
490
|
+
book: {
|
|
491
|
+
id: "writer-book",
|
|
492
|
+
title: "Writer Book",
|
|
493
|
+
platform: "tomato",
|
|
494
|
+
genre: "xuanhuan",
|
|
495
|
+
status: "active",
|
|
496
|
+
targetChapters: 20,
|
|
497
|
+
chapterWordCount: 2200,
|
|
498
|
+
language: "en",
|
|
499
|
+
createdAt: "2026-03-25T00:00:00.000Z",
|
|
500
|
+
updatedAt: "2026-03-25T00:00:00.000Z",
|
|
501
|
+
},
|
|
502
|
+
bookDir,
|
|
503
|
+
chapterNumber: 3,
|
|
504
|
+
lengthSpec: buildLengthSpec(2200, "en"),
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
expect(output.runtimeStateDelta?.chapter).toBe(3);
|
|
508
|
+
expect(output.runtimeStateSnapshot?.manifest.lastAppliedChapter).toBe(3);
|
|
509
|
+
expect(output.updatedState).toContain("Trace the debt through the river-port ledger.");
|
|
510
|
+
expect(output.updatedHooks).toContain("mentor-debt");
|
|
511
|
+
expect(output.updatedChapterSummaries).toContain("River Ledger");
|
|
512
|
+
expect(output.chapterSummary).toContain("| 3 | River Ledger |");
|
|
513
|
+
} finally {
|
|
514
|
+
await rm(root, { recursive: true, force: true });
|
|
515
|
+
}
|
|
516
|
+
});
|
|
517
|
+
|
|
518
|
+
it("overrides hallucinated chapter numbers across both delta and summary row", async () => {
|
|
519
|
+
const root = await mkdtemp(join(tmpdir(), "inkos-writer-runtime-state-hallucinated-chapter-test-"));
|
|
520
|
+
const bookDir = join(root, "book");
|
|
521
|
+
const storyDir = join(bookDir, "story");
|
|
522
|
+
const chaptersDir = join(bookDir, "chapters");
|
|
523
|
+
await mkdir(storyDir, { recursive: true });
|
|
524
|
+
await mkdir(chaptersDir, { recursive: true });
|
|
525
|
+
|
|
526
|
+
await Promise.all([
|
|
527
|
+
writeFile(join(chaptersDir, "index.json"), JSON.stringify([
|
|
528
|
+
{ number: 1, title: "Ch1", status: "approved" },
|
|
529
|
+
{ number: 2, title: "Ch2", status: "approved" },
|
|
530
|
+
]), "utf-8"),
|
|
531
|
+
writeFile(join(storyDir, "story_bible.md"), "# Story Bible\n\n- The city still remembers 1988.\n", "utf-8"),
|
|
532
|
+
writeFile(join(storyDir, "volume_outline.md"), "# Volume Outline\n\n## Chapter 3\nTrace the debt through the river-port ledger.\n", "utf-8"),
|
|
533
|
+
writeFile(join(storyDir, "style_guide.md"), "# Style Guide\n\n- Keep the prose restrained.\n", "utf-8"),
|
|
534
|
+
writeFile(join(storyDir, "current_state.md"), [
|
|
535
|
+
"# Current State",
|
|
536
|
+
"",
|
|
537
|
+
"| Field | Value |",
|
|
538
|
+
"| --- | --- |",
|
|
539
|
+
"| Current Chapter | 2 |",
|
|
540
|
+
"| Current Goal | Find the vanished mentor |",
|
|
541
|
+
"| Current Conflict | Guild pressure keeps colliding with the debt trail |",
|
|
542
|
+
"",
|
|
543
|
+
].join("\n"), "utf-8"),
|
|
544
|
+
writeFile(join(storyDir, "pending_hooks.md"), [
|
|
545
|
+
"| hook_id | start_chapter | type | status | last_advanced | expected_payoff | notes |",
|
|
546
|
+
"| --- | --- | --- | --- | --- | --- | --- |",
|
|
547
|
+
"| mentor-debt | 1 | relationship | open | 2 | 6 | Still unresolved |",
|
|
548
|
+
"",
|
|
549
|
+
].join("\n"), "utf-8"),
|
|
550
|
+
writeFile(join(storyDir, "chapter_summaries.md"), [
|
|
551
|
+
"| chapter | title | characters | events | stateChanges | hookActivity | mood | chapterType |",
|
|
552
|
+
"| --- | --- | --- | --- | --- | --- | --- | --- |",
|
|
553
|
+
"| 2 | Old Ledger | Lin Yue | Lin Yue finds the old ledger | Debt sharpens | mentor-debt advanced | tense | mainline |",
|
|
554
|
+
"",
|
|
555
|
+
].join("\n"), "utf-8"),
|
|
556
|
+
]);
|
|
557
|
+
|
|
558
|
+
const agent = new WriterAgent({
|
|
559
|
+
client: {
|
|
560
|
+
provider: "openai",
|
|
561
|
+
apiFormat: "chat",
|
|
562
|
+
stream: false,
|
|
563
|
+
defaults: {
|
|
564
|
+
temperature: 0.7,
|
|
565
|
+
maxTokens: 4096,
|
|
566
|
+
thinkingBudget: 0,
|
|
567
|
+
extra: {},
|
|
568
|
+
},
|
|
569
|
+
},
|
|
570
|
+
model: "test-model",
|
|
571
|
+
projectRoot: root,
|
|
572
|
+
});
|
|
573
|
+
|
|
574
|
+
vi.spyOn(WriterAgent.prototype as never, "chat" as never)
|
|
575
|
+
.mockResolvedValueOnce({
|
|
576
|
+
content: [
|
|
577
|
+
"=== CHAPTER_TITLE ===",
|
|
578
|
+
"River Ledger",
|
|
579
|
+
"",
|
|
580
|
+
"=== CHAPTER_CONTENT ===",
|
|
581
|
+
"Lin Yue follows the debt into the river-port ledger. The old wall still carries the year 1988.",
|
|
582
|
+
"",
|
|
583
|
+
"=== PRE_WRITE_CHECK ===",
|
|
584
|
+
"- ok",
|
|
585
|
+
].join("\n"),
|
|
586
|
+
usage: ZERO_USAGE,
|
|
587
|
+
})
|
|
588
|
+
.mockResolvedValueOnce({
|
|
589
|
+
content: "=== OBSERVATIONS ===\n- observed",
|
|
590
|
+
usage: ZERO_USAGE,
|
|
591
|
+
})
|
|
592
|
+
.mockResolvedValueOnce({
|
|
593
|
+
content: [
|
|
594
|
+
"=== POST_SETTLEMENT ===",
|
|
595
|
+
"- mentor-debt advanced",
|
|
596
|
+
"",
|
|
597
|
+
"=== RUNTIME_STATE_DELTA ===",
|
|
598
|
+
"```json",
|
|
599
|
+
JSON.stringify({
|
|
600
|
+
chapter: 1988,
|
|
601
|
+
currentStatePatch: {
|
|
602
|
+
currentGoal: "Trace the debt through the river-port ledger.",
|
|
603
|
+
currentConflict: "Guild pressure keeps colliding with the debt trail.",
|
|
604
|
+
},
|
|
605
|
+
hookOps: {
|
|
606
|
+
upsert: [
|
|
607
|
+
{
|
|
608
|
+
hookId: "mentor-debt",
|
|
609
|
+
startChapter: 1,
|
|
610
|
+
type: "relationship",
|
|
611
|
+
status: "progressing",
|
|
612
|
+
lastAdvancedChapter: 1988,
|
|
613
|
+
expectedPayoff: "Reveal the debt.",
|
|
614
|
+
notes: "The ledger clue sharpens the line.",
|
|
615
|
+
},
|
|
616
|
+
],
|
|
617
|
+
resolve: [],
|
|
618
|
+
defer: [],
|
|
619
|
+
},
|
|
620
|
+
chapterSummary: {
|
|
621
|
+
chapter: 1988,
|
|
622
|
+
title: "River Ledger",
|
|
623
|
+
characters: "Lin Yue",
|
|
624
|
+
events: "Lin Yue follows the debt into the river-port ledger.",
|
|
625
|
+
stateChanges: "The debt line sharpens.",
|
|
626
|
+
hookActivity: "mentor-debt advanced",
|
|
627
|
+
mood: "tense",
|
|
628
|
+
chapterType: "investigation",
|
|
629
|
+
},
|
|
630
|
+
notes: [],
|
|
631
|
+
}, null, 2),
|
|
632
|
+
"```",
|
|
633
|
+
].join("\n"),
|
|
634
|
+
usage: ZERO_USAGE,
|
|
635
|
+
});
|
|
636
|
+
|
|
637
|
+
try {
|
|
638
|
+
const output = await agent.writeChapter({
|
|
639
|
+
book: {
|
|
640
|
+
id: "writer-book",
|
|
641
|
+
title: "Writer Book",
|
|
642
|
+
platform: "tomato",
|
|
643
|
+
genre: "xuanhuan",
|
|
644
|
+
status: "active",
|
|
645
|
+
targetChapters: 20,
|
|
646
|
+
chapterWordCount: 2200,
|
|
647
|
+
language: "en",
|
|
648
|
+
createdAt: "2026-03-25T00:00:00.000Z",
|
|
649
|
+
updatedAt: "2026-03-25T00:00:00.000Z",
|
|
650
|
+
},
|
|
651
|
+
bookDir,
|
|
652
|
+
chapterNumber: 3,
|
|
653
|
+
lengthSpec: buildLengthSpec(2200, "en"),
|
|
654
|
+
});
|
|
655
|
+
|
|
656
|
+
expect(output.runtimeStateDelta?.chapter).toBe(3);
|
|
657
|
+
expect(output.runtimeStateDelta?.chapterSummary?.chapter).toBe(3);
|
|
658
|
+
expect(output.runtimeStateSnapshot?.manifest.lastAppliedChapter).toBe(3);
|
|
659
|
+
expect(output.runtimeStateSnapshot?.hooks.hooks[0]?.lastAdvancedChapter).toBe(3);
|
|
660
|
+
expect(output.updatedHooks).toContain("| mentor-debt | 1 | relationship | progressing | 3 |");
|
|
661
|
+
expect(output.updatedChapterSummaries).toContain("| 3 | River Ledger |");
|
|
662
|
+
expect(output.chapterSummary).toContain("| 3 | River Ledger |");
|
|
663
|
+
} finally {
|
|
664
|
+
await rm(root, { recursive: true, force: true });
|
|
665
|
+
}
|
|
666
|
+
});
|
|
667
|
+
|
|
668
|
+
it("returns the arbiter-resolved delta instead of raw new-hook candidates", async () => {
|
|
669
|
+
const root = await mkdtemp(join(tmpdir(), "inkos-writer-arbiter-test-"));
|
|
670
|
+
const bookDir = join(root, "book");
|
|
671
|
+
const storyDir = join(bookDir, "story");
|
|
672
|
+
const chaptersDir = join(bookDir, "chapters");
|
|
673
|
+
await mkdir(storyDir, { recursive: true });
|
|
674
|
+
await mkdir(chaptersDir, { recursive: true });
|
|
675
|
+
|
|
676
|
+
await Promise.all([
|
|
677
|
+
writeFile(join(chaptersDir, "index.json"), JSON.stringify([
|
|
678
|
+
{ number: 1, title: "Ch1", status: "approved" },
|
|
679
|
+
{ number: 2, title: "Ch2", status: "approved" },
|
|
680
|
+
]), "utf-8"),
|
|
681
|
+
writeFile(join(storyDir, "story_bible.md"), "# Story Bible\n\n- Anonymous messages keep steering the debt trail.\n", "utf-8"),
|
|
682
|
+
writeFile(join(storyDir, "volume_outline.md"), "# Volume Outline\n\n## Chapter 3\nThe anonymous source widens from route to address.\n", "utf-8"),
|
|
683
|
+
writeFile(join(storyDir, "style_guide.md"), "# Style Guide\n\n- Keep the prose restrained.\n", "utf-8"),
|
|
684
|
+
writeFile(join(storyDir, "current_state.md"), [
|
|
685
|
+
"# Current State",
|
|
686
|
+
"",
|
|
687
|
+
"| Field | Value |",
|
|
688
|
+
"| --- | --- |",
|
|
689
|
+
"| Current Chapter | 2 |",
|
|
690
|
+
"| Current Goal | Find who fed the route to the anonymous source |",
|
|
691
|
+
"",
|
|
692
|
+
].join("\n"), "utf-8"),
|
|
693
|
+
writeFile(join(storyDir, "pending_hooks.md"), [
|
|
694
|
+
"| hook_id | start_chapter | type | status | last_advanced | expected_payoff | notes |",
|
|
695
|
+
"| --- | --- | --- | --- | --- | --- | --- |",
|
|
696
|
+
"| anonymous-source-scope | 1 | source-risk | open | 2 | Reveal how much the anonymous source already knew about the route. | The source knowledge question remains unresolved. |",
|
|
697
|
+
"",
|
|
698
|
+
].join("\n"), "utf-8"),
|
|
699
|
+
writeFile(join(storyDir, "chapter_summaries.md"), [
|
|
700
|
+
"| chapter | title | characters | events | stateChanges | hookActivity | mood | chapterType |",
|
|
701
|
+
"| --- | --- | --- | --- | --- | --- | --- | --- |",
|
|
702
|
+
"| 2 | Route Leak | Lin Yue | An anonymous source already knew the route | Suspicion sharpens | anonymous-source-scope advanced | tense | mainline |",
|
|
703
|
+
"",
|
|
704
|
+
].join("\n"), "utf-8"),
|
|
705
|
+
]);
|
|
706
|
+
|
|
707
|
+
const agent = new WriterAgent({
|
|
708
|
+
client: {
|
|
709
|
+
provider: "openai",
|
|
710
|
+
apiFormat: "chat",
|
|
711
|
+
stream: false,
|
|
712
|
+
defaults: {
|
|
713
|
+
temperature: 0.7,
|
|
714
|
+
maxTokens: 4096,
|
|
715
|
+
thinkingBudget: 0,
|
|
716
|
+
extra: {},
|
|
717
|
+
},
|
|
718
|
+
},
|
|
719
|
+
model: "test-model",
|
|
720
|
+
projectRoot: root,
|
|
721
|
+
});
|
|
722
|
+
|
|
723
|
+
vi.spyOn(WriterAgent.prototype as never, "chat" as never)
|
|
724
|
+
.mockResolvedValueOnce({
|
|
725
|
+
content: [
|
|
726
|
+
"=== CHAPTER_TITLE ===",
|
|
727
|
+
"Address Leak",
|
|
728
|
+
"",
|
|
729
|
+
"=== CHAPTER_CONTENT ===",
|
|
730
|
+
"Lin Yue realizes the anonymous source knew the address, not just the route.",
|
|
731
|
+
"",
|
|
732
|
+
"=== PRE_WRITE_CHECK ===",
|
|
733
|
+
"- ok",
|
|
734
|
+
].join("\n"),
|
|
735
|
+
usage: ZERO_USAGE,
|
|
736
|
+
})
|
|
737
|
+
.mockResolvedValueOnce({
|
|
738
|
+
content: "=== OBSERVATIONS ===\n- observed",
|
|
739
|
+
usage: ZERO_USAGE,
|
|
740
|
+
})
|
|
741
|
+
.mockResolvedValueOnce({
|
|
742
|
+
content: [
|
|
743
|
+
"=== POST_SETTLEMENT ===",
|
|
744
|
+
"- source scope widens",
|
|
745
|
+
"",
|
|
746
|
+
"=== RUNTIME_STATE_DELTA ===",
|
|
747
|
+
"```json",
|
|
748
|
+
JSON.stringify({
|
|
749
|
+
chapter: 3,
|
|
750
|
+
hookOps: {
|
|
751
|
+
upsert: [],
|
|
752
|
+
mention: [],
|
|
753
|
+
resolve: [],
|
|
754
|
+
defer: [],
|
|
755
|
+
},
|
|
756
|
+
newHookCandidates: [
|
|
757
|
+
{
|
|
758
|
+
type: "source-risk",
|
|
759
|
+
expectedPayoff: "Reveal how much the anonymous source already knew about the route and address.",
|
|
760
|
+
notes: "This chapter adds the address angle to the anonymous source question.",
|
|
761
|
+
},
|
|
762
|
+
],
|
|
763
|
+
chapterSummary: {
|
|
764
|
+
chapter: 3,
|
|
765
|
+
title: "Address Leak",
|
|
766
|
+
characters: "Lin Yue",
|
|
767
|
+
events: "Lin Yue realizes the anonymous source knew the address.",
|
|
768
|
+
stateChanges: "The source knowledge question widens.",
|
|
769
|
+
hookActivity: "anonymous-source-scope advanced",
|
|
770
|
+
mood: "tight",
|
|
771
|
+
chapterType: "investigation",
|
|
772
|
+
},
|
|
773
|
+
notes: [],
|
|
774
|
+
}, null, 2),
|
|
775
|
+
"```",
|
|
776
|
+
].join("\n"),
|
|
777
|
+
usage: ZERO_USAGE,
|
|
778
|
+
});
|
|
779
|
+
|
|
780
|
+
try {
|
|
781
|
+
const output = await agent.writeChapter({
|
|
782
|
+
book: {
|
|
783
|
+
id: "writer-book",
|
|
784
|
+
title: "Writer Book",
|
|
785
|
+
platform: "tomato",
|
|
786
|
+
genre: "other",
|
|
787
|
+
status: "active",
|
|
788
|
+
targetChapters: 20,
|
|
789
|
+
chapterWordCount: 2200,
|
|
790
|
+
language: "en",
|
|
791
|
+
createdAt: "2026-03-27T00:00:00.000Z",
|
|
792
|
+
updatedAt: "2026-03-27T00:00:00.000Z",
|
|
793
|
+
},
|
|
794
|
+
bookDir,
|
|
795
|
+
chapterNumber: 3,
|
|
796
|
+
lengthSpec: buildLengthSpec(2200, "en"),
|
|
797
|
+
});
|
|
798
|
+
|
|
799
|
+
expect(output.runtimeStateDelta?.hookOps.upsert).toEqual([
|
|
800
|
+
expect.objectContaining({
|
|
801
|
+
hookId: "anonymous-source-scope",
|
|
802
|
+
lastAdvancedChapter: 3,
|
|
803
|
+
}),
|
|
804
|
+
]);
|
|
805
|
+
expect(output.runtimeStateDelta?.newHookCandidates).toEqual([]);
|
|
806
|
+
expect(output.updatedHooks).toContain("anonymous-source-scope");
|
|
807
|
+
expect(output.updatedHooks).toContain("| anonymous-source-scope | 1 | source-risk | progressing | 3 |");
|
|
808
|
+
} finally {
|
|
809
|
+
await rm(root, { recursive: true, force: true });
|
|
810
|
+
}
|
|
811
|
+
});
|
|
812
|
+
|
|
813
|
+
it("logs localized phase messages for Chinese books", async () => {
|
|
814
|
+
const root = await mkdtemp(join(tmpdir(), "inkos-writer-test-"));
|
|
815
|
+
const bookDir = join(root, "book");
|
|
816
|
+
const storyDir = join(bookDir, "story");
|
|
817
|
+
const { logger, infos } = createCaptureLogger();
|
|
818
|
+
await mkdir(storyDir, { recursive: true });
|
|
819
|
+
|
|
820
|
+
await Promise.all([
|
|
821
|
+
writeFile(join(storyDir, "story_bible.md"), "# Story Bible\n", "utf-8"),
|
|
822
|
+
writeFile(join(storyDir, "volume_outline.md"), "# Volume Outline\n", "utf-8"),
|
|
823
|
+
writeFile(join(storyDir, "style_guide.md"), "# Style Guide\n", "utf-8"),
|
|
824
|
+
writeFile(join(storyDir, "current_state.md"), "# 当前状态\n", "utf-8"),
|
|
825
|
+
writeFile(join(storyDir, "pending_hooks.md"), "# 伏笔池\n", "utf-8"),
|
|
826
|
+
writeFile(join(storyDir, "chapter_summaries.md"), "# 章节摘要\n", "utf-8"),
|
|
827
|
+
writeFile(join(storyDir, "subplot_board.md"), "# 支线进度板\n", "utf-8"),
|
|
828
|
+
writeFile(join(storyDir, "emotional_arcs.md"), "# 情感弧线\n", "utf-8"),
|
|
829
|
+
writeFile(join(storyDir, "character_matrix.md"), "# 角色交互矩阵\n", "utf-8"),
|
|
830
|
+
]);
|
|
831
|
+
|
|
832
|
+
const agent = new WriterAgent({
|
|
833
|
+
client: {
|
|
834
|
+
provider: "openai",
|
|
835
|
+
apiFormat: "chat",
|
|
836
|
+
stream: false,
|
|
837
|
+
defaults: {
|
|
838
|
+
temperature: 0.7,
|
|
839
|
+
maxTokens: 4096,
|
|
840
|
+
thinkingBudget: 0,
|
|
841
|
+
extra: {},
|
|
842
|
+
},
|
|
843
|
+
},
|
|
844
|
+
model: "test-model",
|
|
845
|
+
projectRoot: root,
|
|
846
|
+
logger,
|
|
847
|
+
});
|
|
848
|
+
|
|
849
|
+
vi.spyOn(WriterAgent.prototype as never, "chat" as never)
|
|
850
|
+
.mockResolvedValueOnce({
|
|
851
|
+
content: [
|
|
852
|
+
"=== CHAPTER_TITLE ===",
|
|
853
|
+
"试炼前夜",
|
|
854
|
+
"",
|
|
855
|
+
"=== CHAPTER_CONTENT ===",
|
|
856
|
+
"林越在破庙外停住脚步,想起师门旧债。",
|
|
857
|
+
"",
|
|
858
|
+
"=== PRE_WRITE_CHECK ===",
|
|
859
|
+
"- ok",
|
|
860
|
+
].join("\n"),
|
|
861
|
+
usage: ZERO_USAGE,
|
|
862
|
+
})
|
|
863
|
+
.mockResolvedValueOnce({
|
|
864
|
+
content: "=== OBSERVATIONS ===\n- observed",
|
|
865
|
+
usage: ZERO_USAGE,
|
|
866
|
+
})
|
|
867
|
+
.mockResolvedValueOnce({
|
|
868
|
+
content: [
|
|
869
|
+
"=== POST_SETTLEMENT ===",
|
|
870
|
+
"| 伏笔变动 | mentor-oath 推进 | 同步更新伏笔池 |",
|
|
871
|
+
"",
|
|
872
|
+
"=== UPDATED_STATE ===",
|
|
873
|
+
"状态卡",
|
|
874
|
+
"",
|
|
875
|
+
"=== UPDATED_HOOKS ===",
|
|
876
|
+
"伏笔池",
|
|
877
|
+
"",
|
|
878
|
+
"=== CHAPTER_SUMMARY ===",
|
|
879
|
+
"| 1 | 试炼前夜 | 林越 | 林越记起师门旧债 | 决心加深 | mentor-oath advanced | tense | setup |",
|
|
880
|
+
"",
|
|
881
|
+
"=== UPDATED_SUBPLOTS ===",
|
|
882
|
+
"支线板",
|
|
883
|
+
"",
|
|
884
|
+
"=== UPDATED_EMOTIONAL_ARCS ===",
|
|
885
|
+
"情感弧线",
|
|
886
|
+
"",
|
|
887
|
+
"=== UPDATED_CHARACTER_MATRIX ===",
|
|
888
|
+
"角色矩阵",
|
|
889
|
+
].join("\n"),
|
|
890
|
+
usage: ZERO_USAGE,
|
|
891
|
+
});
|
|
892
|
+
|
|
893
|
+
try {
|
|
894
|
+
await agent.writeChapter({
|
|
895
|
+
book: {
|
|
896
|
+
id: "writer-book",
|
|
897
|
+
title: "Writer Book",
|
|
898
|
+
platform: "tomato",
|
|
899
|
+
genre: "xuanhuan",
|
|
900
|
+
status: "active",
|
|
901
|
+
targetChapters: 120,
|
|
902
|
+
chapterWordCount: 2200,
|
|
903
|
+
language: "zh",
|
|
904
|
+
createdAt: "2026-03-23T00:00:00.000Z",
|
|
905
|
+
updatedAt: "2026-03-23T00:00:00.000Z",
|
|
906
|
+
},
|
|
907
|
+
bookDir,
|
|
908
|
+
chapterNumber: 1,
|
|
909
|
+
lengthSpec: buildLengthSpec(220, "zh"),
|
|
910
|
+
});
|
|
911
|
+
|
|
912
|
+
expect(infos).toEqual(expect.arrayContaining([
|
|
913
|
+
"阶段 1:创作正文(第1章)",
|
|
914
|
+
"阶段 2:状态结算(第1章,18字)",
|
|
915
|
+
"阶段 2a:提取第1章事实",
|
|
916
|
+
"阶段 2b:把观察结果回写到真相文件",
|
|
917
|
+
]));
|
|
918
|
+
} finally {
|
|
919
|
+
await rm(root, { recursive: true, force: true });
|
|
920
|
+
}
|
|
921
|
+
});
|
|
922
|
+
|
|
923
|
+
it("injects an English variance brief into governed creative prompts", async () => {
|
|
924
|
+
const root = await mkdtemp(join(tmpdir(), "inkos-writer-variance-test-"));
|
|
925
|
+
const bookDir = join(root, "book");
|
|
926
|
+
const storyDir = join(bookDir, "story");
|
|
927
|
+
const chaptersDir = join(bookDir, "chapters");
|
|
928
|
+
await mkdir(storyDir, { recursive: true });
|
|
929
|
+
await mkdir(chaptersDir, { recursive: true });
|
|
930
|
+
|
|
931
|
+
await Promise.all([
|
|
932
|
+
writeFile(join(storyDir, "story_bible.md"), "# Story Bible\n\n- The registry seals matter.\n", "utf-8"),
|
|
933
|
+
writeFile(join(storyDir, "volume_outline.md"), "# Volume Outline\n\n## Chapter 4\nForce Mara back toward the ledger trail.\n", "utf-8"),
|
|
934
|
+
writeFile(join(storyDir, "style_guide.md"), "# Style Guide\n\n- Keep the prose lean.\n", "utf-8"),
|
|
935
|
+
writeFile(join(storyDir, "current_state.md"), "# Current State\n\n- Mara still hides the ledger fragment.\n", "utf-8"),
|
|
936
|
+
writeFile(join(storyDir, "pending_hooks.md"), [
|
|
937
|
+
"| hook_id | start_chapter | type | status | last_advanced | expected_payoff | notes |",
|
|
938
|
+
"| --- | --- | --- | --- | --- | --- | --- |",
|
|
939
|
+
"| ledger-fragment | 1 | mystery | open | 3 | 8 | Mara still hides the ledger fragment |",
|
|
940
|
+
].join("\n"), "utf-8"),
|
|
941
|
+
writeFile(join(storyDir, "chapter_summaries.md"), [
|
|
942
|
+
"| chapter | title | characters | events | stateChanges | hookActivity | mood | chapterType |",
|
|
943
|
+
"| --- | --- | --- | --- | --- | --- | --- | --- |",
|
|
944
|
+
"| 1 | Ledger | Mara | Mara hides the ledger | pressure tightens | none | tense | investigation |",
|
|
945
|
+
"| 2 | Ash | Mara,Taryn | Ash falls over the archive | pressure tightens | none | tense | investigation |",
|
|
946
|
+
"| 3 | Harbor | Mara,Taryn | The gate stays under watch | pressure tightens | none | tense | investigation |",
|
|
947
|
+
].join("\n"), "utf-8"),
|
|
948
|
+
writeFile(join(chaptersDir, "0001_Ledger.md"), "# Chapter 1 Ledger\n\nMara kept the ledger close to her chest. The corridor stayed quiet after the bell. There it was again.\n", "utf-8"),
|
|
949
|
+
writeFile(join(chaptersDir, "0002_Ash.md"), "# Chapter 2 Ash\n\nMara kept the ledger close to her chest while the ash fell. The corridor stayed quiet until Taryn stopped. There it was again.\n", "utf-8"),
|
|
950
|
+
writeFile(join(chaptersDir, "0003_Harbor.md"), "# Chapter 3 Harbor\n\nMara kept the ledger close to her chest near the harbor gate. The corridor stayed quiet while the guards changed. There it was again.\n", "utf-8"),
|
|
951
|
+
]);
|
|
952
|
+
|
|
953
|
+
const agent = new WriterAgent({
|
|
954
|
+
client: {
|
|
955
|
+
provider: "openai",
|
|
956
|
+
apiFormat: "chat",
|
|
957
|
+
stream: false,
|
|
958
|
+
defaults: {
|
|
959
|
+
temperature: 0.7,
|
|
960
|
+
maxTokens: 4096,
|
|
961
|
+
thinkingBudget: 0,
|
|
962
|
+
extra: {},
|
|
963
|
+
},
|
|
964
|
+
},
|
|
965
|
+
model: "test-model",
|
|
966
|
+
projectRoot: root,
|
|
967
|
+
});
|
|
968
|
+
|
|
969
|
+
const chatSpy = vi.spyOn(WriterAgent.prototype as never, "chat" as never)
|
|
970
|
+
.mockResolvedValueOnce({
|
|
971
|
+
content: [
|
|
972
|
+
"=== CHAPTER_TITLE ===",
|
|
973
|
+
"Pressure Ledger",
|
|
974
|
+
"",
|
|
975
|
+
"=== CHAPTER_CONTENT ===",
|
|
976
|
+
"Mara forced Taryn to answer beside the archive window.",
|
|
977
|
+
"",
|
|
978
|
+
"=== PRE_WRITE_CHECK ===",
|
|
979
|
+
"- ok",
|
|
980
|
+
].join("\n"),
|
|
981
|
+
usage: ZERO_USAGE,
|
|
982
|
+
})
|
|
983
|
+
.mockResolvedValueOnce({
|
|
984
|
+
content: "=== OBSERVATIONS ===\n- observed",
|
|
985
|
+
usage: ZERO_USAGE,
|
|
986
|
+
})
|
|
987
|
+
.mockResolvedValueOnce({
|
|
988
|
+
content: [
|
|
989
|
+
"=== POST_SETTLEMENT ===",
|
|
990
|
+
"- ledger-fragment advanced",
|
|
991
|
+
"",
|
|
992
|
+
"=== UPDATED_STATE ===",
|
|
993
|
+
"state",
|
|
994
|
+
"",
|
|
995
|
+
"=== UPDATED_HOOKS ===",
|
|
996
|
+
"hooks",
|
|
997
|
+
"",
|
|
998
|
+
"=== CHAPTER_SUMMARY ===",
|
|
999
|
+
"| 4 | Pressure Ledger | Mara,Taryn | Pressure rises | Trail narrows | ledger-fragment advanced | tense | confrontation |",
|
|
1000
|
+
"",
|
|
1001
|
+
"=== UPDATED_SUBPLOTS ===",
|
|
1002
|
+
"subplots",
|
|
1003
|
+
"",
|
|
1004
|
+
"=== UPDATED_EMOTIONAL_ARCS ===",
|
|
1005
|
+
"arcs",
|
|
1006
|
+
"",
|
|
1007
|
+
"=== UPDATED_CHARACTER_MATRIX ===",
|
|
1008
|
+
"matrix",
|
|
1009
|
+
].join("\n"),
|
|
1010
|
+
usage: ZERO_USAGE,
|
|
1011
|
+
});
|
|
1012
|
+
|
|
1013
|
+
try {
|
|
1014
|
+
await agent.writeChapter({
|
|
1015
|
+
book: {
|
|
1016
|
+
id: "writer-book",
|
|
1017
|
+
title: "Writer Book",
|
|
1018
|
+
platform: "other",
|
|
1019
|
+
genre: "other",
|
|
1020
|
+
status: "active",
|
|
1021
|
+
targetChapters: 20,
|
|
1022
|
+
chapterWordCount: 2200,
|
|
1023
|
+
language: "en",
|
|
1024
|
+
createdAt: "2026-03-26T00:00:00.000Z",
|
|
1025
|
+
updatedAt: "2026-03-26T00:00:00.000Z",
|
|
1026
|
+
},
|
|
1027
|
+
bookDir,
|
|
1028
|
+
chapterNumber: 4,
|
|
1029
|
+
chapterMemo: {
|
|
1030
|
+
chapter: 4,
|
|
1031
|
+
goal: "Force Mara back toward the ledger trail.",
|
|
1032
|
+
isGoldenOpening: false,
|
|
1033
|
+
body: "",
|
|
1034
|
+
threadRefs: ["ledger-fragment"],
|
|
1035
|
+
},
|
|
1036
|
+
contextPackage: {
|
|
1037
|
+
chapter: 4,
|
|
1038
|
+
selectedContext: [
|
|
1039
|
+
{
|
|
1040
|
+
source: "story/chapter_summaries.md#3",
|
|
1041
|
+
reason: "Carry recent pressure into the next chapter.",
|
|
1042
|
+
excerpt: "The gate stays under watch.",
|
|
1043
|
+
},
|
|
1044
|
+
],
|
|
1045
|
+
},
|
|
1046
|
+
ruleStack: {
|
|
1047
|
+
layers: [{ id: "L4", name: "current_task", precedence: 70, scope: "local" }],
|
|
1048
|
+
sections: {
|
|
1049
|
+
hard: ["current_state"],
|
|
1050
|
+
soft: ["current_focus"],
|
|
1051
|
+
diagnostic: ["continuity_audit"],
|
|
1052
|
+
},
|
|
1053
|
+
overrideEdges: [],
|
|
1054
|
+
activeOverrides: [],
|
|
1055
|
+
},
|
|
1056
|
+
lengthSpec: buildLengthSpec(2200, "en"),
|
|
1057
|
+
});
|
|
1058
|
+
|
|
1059
|
+
const creativePrompt = (chatSpy.mock.calls[0]?.[0] as ReadonlyArray<{ content: string }> | undefined)?.[1]?.content ?? "";
|
|
1060
|
+
expect(creativePrompt).toContain("## English Variance Brief");
|
|
1061
|
+
expect(creativePrompt).toContain("High-frequency phrases");
|
|
1062
|
+
expect(creativePrompt).toContain("Scene obligation");
|
|
1063
|
+
} finally {
|
|
1064
|
+
await rm(root, { recursive: true, force: true });
|
|
1065
|
+
}
|
|
1066
|
+
});
|
|
1067
|
+
|
|
1068
|
+
it("renders explicit title history, mood trail, and canon blocks in governed creative prompts", async () => {
|
|
1069
|
+
const root = await mkdtemp(join(tmpdir(), "inkos-writer-governed-evidence-test-"));
|
|
1070
|
+
const bookDir = join(root, "book");
|
|
1071
|
+
const storyDir = join(bookDir, "story");
|
|
1072
|
+
await mkdir(storyDir, { recursive: true });
|
|
1073
|
+
|
|
1074
|
+
await Promise.all([
|
|
1075
|
+
writeFile(join(storyDir, "story_bible.md"), "# Story Bible\n\n- Registry seals still matter.\n", "utf-8"),
|
|
1076
|
+
writeFile(join(storyDir, "volume_outline.md"), "# Volume Outline\n\n## Chapter 4\nPush Mara back toward the archive ledger.\n", "utf-8"),
|
|
1077
|
+
writeFile(join(storyDir, "style_guide.md"), "# Style Guide\n\n- Keep the prose lean.\n", "utf-8"),
|
|
1078
|
+
writeFile(join(storyDir, "current_state.md"), "# Current State\n\n- Mara still hides the ledger fragment.\n", "utf-8"),
|
|
1079
|
+
writeFile(join(storyDir, "pending_hooks.md"), "# Pending Hooks\n\n- ledger-fragment\n", "utf-8"),
|
|
1080
|
+
writeFile(join(storyDir, "chapter_summaries.md"), "# Chapter Summaries\n", "utf-8"),
|
|
1081
|
+
]);
|
|
1082
|
+
|
|
1083
|
+
const agent = new WriterAgent({
|
|
1084
|
+
client: {
|
|
1085
|
+
provider: "openai",
|
|
1086
|
+
apiFormat: "chat",
|
|
1087
|
+
stream: false,
|
|
1088
|
+
defaults: {
|
|
1089
|
+
temperature: 0.7,
|
|
1090
|
+
maxTokens: 4096,
|
|
1091
|
+
thinkingBudget: 0,
|
|
1092
|
+
extra: {},
|
|
1093
|
+
},
|
|
1094
|
+
},
|
|
1095
|
+
model: "test-model",
|
|
1096
|
+
projectRoot: root,
|
|
1097
|
+
});
|
|
1098
|
+
|
|
1099
|
+
const chatSpy = vi.spyOn(WriterAgent.prototype as never, "chat" as never)
|
|
1100
|
+
.mockResolvedValueOnce({
|
|
1101
|
+
content: [
|
|
1102
|
+
"=== CHAPTER_TITLE ===",
|
|
1103
|
+
"Archive Pressure",
|
|
1104
|
+
"",
|
|
1105
|
+
"=== CHAPTER_CONTENT ===",
|
|
1106
|
+
"Mara corners Taryn beside the archive ledger.",
|
|
1107
|
+
"",
|
|
1108
|
+
"=== PRE_WRITE_CHECK ===",
|
|
1109
|
+
"- ok",
|
|
1110
|
+
].join("\n"),
|
|
1111
|
+
usage: ZERO_USAGE,
|
|
1112
|
+
})
|
|
1113
|
+
.mockResolvedValueOnce({
|
|
1114
|
+
content: "=== OBSERVATIONS ===\n- observed",
|
|
1115
|
+
usage: ZERO_USAGE,
|
|
1116
|
+
})
|
|
1117
|
+
.mockResolvedValueOnce({
|
|
1118
|
+
content: [
|
|
1119
|
+
"=== POST_SETTLEMENT ===",
|
|
1120
|
+
"- ledger-fragment advanced",
|
|
1121
|
+
"",
|
|
1122
|
+
"=== UPDATED_STATE ===",
|
|
1123
|
+
"state",
|
|
1124
|
+
"",
|
|
1125
|
+
"=== UPDATED_HOOKS ===",
|
|
1126
|
+
"hooks",
|
|
1127
|
+
"",
|
|
1128
|
+
"=== CHAPTER_SUMMARY ===",
|
|
1129
|
+
"| 4 | Archive Pressure | Mara,Taryn | Pressure rises | Trail narrows | ledger-fragment advanced | tense | confrontation |",
|
|
1130
|
+
"",
|
|
1131
|
+
"=== UPDATED_SUBPLOTS ===",
|
|
1132
|
+
"subplots",
|
|
1133
|
+
"",
|
|
1134
|
+
"=== UPDATED_EMOTIONAL_ARCS ===",
|
|
1135
|
+
"arcs",
|
|
1136
|
+
"",
|
|
1137
|
+
"=== UPDATED_CHARACTER_MATRIX ===",
|
|
1138
|
+
"matrix",
|
|
1139
|
+
].join("\n"),
|
|
1140
|
+
usage: ZERO_USAGE,
|
|
1141
|
+
});
|
|
1142
|
+
|
|
1143
|
+
try {
|
|
1144
|
+
await agent.writeChapter({
|
|
1145
|
+
book: {
|
|
1146
|
+
id: "writer-book",
|
|
1147
|
+
title: "Writer Book",
|
|
1148
|
+
platform: "other",
|
|
1149
|
+
genre: "other",
|
|
1150
|
+
status: "active",
|
|
1151
|
+
targetChapters: 20,
|
|
1152
|
+
chapterWordCount: 2200,
|
|
1153
|
+
language: "en",
|
|
1154
|
+
createdAt: "2026-03-26T00:00:00.000Z",
|
|
1155
|
+
updatedAt: "2026-03-26T00:00:00.000Z",
|
|
1156
|
+
},
|
|
1157
|
+
bookDir,
|
|
1158
|
+
chapterNumber: 4,
|
|
1159
|
+
chapterMemo: {
|
|
1160
|
+
chapter: 4,
|
|
1161
|
+
goal: "Push Mara back toward the archive ledger.",
|
|
1162
|
+
isGoldenOpening: false,
|
|
1163
|
+
body: "",
|
|
1164
|
+
threadRefs: ["ledger-fragment"],
|
|
1165
|
+
},
|
|
1166
|
+
contextPackage: {
|
|
1167
|
+
chapter: 4,
|
|
1168
|
+
selectedContext: [
|
|
1169
|
+
{
|
|
1170
|
+
source: "story/chapter_summaries.md#recent_titles",
|
|
1171
|
+
reason: "Avoid repeated ledger titles.",
|
|
1172
|
+
excerpt: "1: Ledger in Rain | 2: Ledger at Dusk | 3: Harbor Ledger",
|
|
1173
|
+
},
|
|
1174
|
+
{
|
|
1175
|
+
source: "story/chapter_summaries.md#recent_mood_type_trail",
|
|
1176
|
+
reason: "Track recent emotional and chapter-type cadence.",
|
|
1177
|
+
excerpt: "1: tight / investigation | 2: tight / investigation | 3: tight / investigation",
|
|
1178
|
+
},
|
|
1179
|
+
{
|
|
1180
|
+
source: "story/parent_canon.md",
|
|
1181
|
+
reason: "Preserve parent canon constraints.",
|
|
1182
|
+
excerpt: "The mentor does not learn about the archive fire until volume two.",
|
|
1183
|
+
},
|
|
1184
|
+
{
|
|
1185
|
+
source: "story/fanfic_canon.md",
|
|
1186
|
+
reason: "Preserve extracted fanfic canon constraints.",
|
|
1187
|
+
excerpt: "Mara may diverge from the archive route, but the oath debt logic must stay intact.",
|
|
1188
|
+
},
|
|
1189
|
+
],
|
|
1190
|
+
},
|
|
1191
|
+
ruleStack: {
|
|
1192
|
+
layers: [{ id: "L4", name: "current_task", precedence: 70, scope: "local" }],
|
|
1193
|
+
sections: {
|
|
1194
|
+
hard: ["current_state"],
|
|
1195
|
+
soft: ["current_focus"],
|
|
1196
|
+
diagnostic: ["continuity_audit"],
|
|
1197
|
+
},
|
|
1198
|
+
overrideEdges: [],
|
|
1199
|
+
activeOverrides: [],
|
|
1200
|
+
},
|
|
1201
|
+
lengthSpec: buildLengthSpec(2200, "en"),
|
|
1202
|
+
});
|
|
1203
|
+
|
|
1204
|
+
const creativePrompt = (chatSpy.mock.calls[0]?.[0] as ReadonlyArray<{ content: string }> | undefined)?.[1]?.content ?? "";
|
|
1205
|
+
expect(creativePrompt).toContain("## Recent Title History");
|
|
1206
|
+
expect(creativePrompt).toContain("Ledger in Rain");
|
|
1207
|
+
expect(creativePrompt).toContain("## Recent Mood / Chapter Type Trail");
|
|
1208
|
+
expect(creativePrompt).toContain("tight / investigation");
|
|
1209
|
+
expect(creativePrompt).toContain("## Canon Evidence");
|
|
1210
|
+
expect(creativePrompt).toContain("archive fire until volume two");
|
|
1211
|
+
expect(creativePrompt).toContain("oath debt logic must stay intact");
|
|
1212
|
+
} finally {
|
|
1213
|
+
await rm(root, { recursive: true, force: true });
|
|
1214
|
+
}
|
|
1215
|
+
});
|
|
1216
|
+
|
|
1217
|
+
it("sanitizes governed control inputs so raw hook ids and control headings do not enter the creative prompt", async () => {
|
|
1218
|
+
const root = await mkdtemp(join(tmpdir(), "inkos-writer-hook-agenda-test-"));
|
|
1219
|
+
const bookDir = join(root, "book");
|
|
1220
|
+
const storyDir = join(bookDir, "story");
|
|
1221
|
+
await mkdir(storyDir, { recursive: true });
|
|
1222
|
+
|
|
1223
|
+
await Promise.all([
|
|
1224
|
+
writeFile(join(storyDir, "story_bible.md"), "# Story Bible\n\n- Registry seals still matter.\n", "utf-8"),
|
|
1225
|
+
writeFile(join(storyDir, "volume_outline.md"), "# Volume Outline\n\n## Chapter 4\nPush Mara back toward the archive ledger.\n", "utf-8"),
|
|
1226
|
+
writeFile(join(storyDir, "style_guide.md"), "# Style Guide\n\n- Keep the prose lean.\n", "utf-8"),
|
|
1227
|
+
writeFile(join(storyDir, "current_state.md"), "# Current State\n\n- Mara still hides the ledger fragment.\n", "utf-8"),
|
|
1228
|
+
writeFile(join(storyDir, "pending_hooks.md"), "# Pending Hooks\n\n- ledger-fragment\n", "utf-8"),
|
|
1229
|
+
writeFile(join(storyDir, "chapter_summaries.md"), "# Chapter Summaries\n", "utf-8"),
|
|
1230
|
+
]);
|
|
1231
|
+
|
|
1232
|
+
const agent = new WriterAgent({
|
|
1233
|
+
client: {
|
|
1234
|
+
provider: "openai",
|
|
1235
|
+
apiFormat: "chat",
|
|
1236
|
+
stream: false,
|
|
1237
|
+
defaults: {
|
|
1238
|
+
temperature: 0.7,
|
|
1239
|
+
maxTokens: 4096,
|
|
1240
|
+
thinkingBudget: 0,
|
|
1241
|
+
extra: {},
|
|
1242
|
+
},
|
|
1243
|
+
},
|
|
1244
|
+
model: "test-model",
|
|
1245
|
+
projectRoot: root,
|
|
1246
|
+
});
|
|
1247
|
+
|
|
1248
|
+
const chatSpy = vi.spyOn(WriterAgent.prototype as never, "chat" as never)
|
|
1249
|
+
.mockResolvedValueOnce({
|
|
1250
|
+
content: [
|
|
1251
|
+
"=== CHAPTER_TITLE ===",
|
|
1252
|
+
"Archive Pressure",
|
|
1253
|
+
"",
|
|
1254
|
+
"=== CHAPTER_CONTENT ===",
|
|
1255
|
+
"Mara corners Taryn beside the archive ledger.",
|
|
1256
|
+
"",
|
|
1257
|
+
"=== PRE_WRITE_CHECK ===",
|
|
1258
|
+
"- ok",
|
|
1259
|
+
].join("\n"),
|
|
1260
|
+
usage: ZERO_USAGE,
|
|
1261
|
+
})
|
|
1262
|
+
.mockResolvedValueOnce({
|
|
1263
|
+
content: "=== OBSERVATIONS ===\n- observed",
|
|
1264
|
+
usage: ZERO_USAGE,
|
|
1265
|
+
})
|
|
1266
|
+
.mockResolvedValueOnce({
|
|
1267
|
+
content: [
|
|
1268
|
+
"=== POST_SETTLEMENT ===",
|
|
1269
|
+
"- ledger-fragment advanced",
|
|
1270
|
+
"",
|
|
1271
|
+
"=== UPDATED_STATE ===",
|
|
1272
|
+
"state",
|
|
1273
|
+
"",
|
|
1274
|
+
"=== UPDATED_HOOKS ===",
|
|
1275
|
+
"hooks",
|
|
1276
|
+
"",
|
|
1277
|
+
"=== CHAPTER_SUMMARY ===",
|
|
1278
|
+
"| 4 | Archive Pressure | Mara,Taryn | Pressure rises | Trail narrows | ledger-fragment advanced | tense | confrontation |",
|
|
1279
|
+
"",
|
|
1280
|
+
"=== UPDATED_SUBPLOTS ===",
|
|
1281
|
+
"subplots",
|
|
1282
|
+
"",
|
|
1283
|
+
"=== UPDATED_EMOTIONAL_ARCS ===",
|
|
1284
|
+
"arcs",
|
|
1285
|
+
"",
|
|
1286
|
+
"=== UPDATED_CHARACTER_MATRIX ===",
|
|
1287
|
+
"matrix",
|
|
1288
|
+
].join("\n"),
|
|
1289
|
+
usage: ZERO_USAGE,
|
|
1290
|
+
});
|
|
1291
|
+
|
|
1292
|
+
try {
|
|
1293
|
+
await agent.writeChapter({
|
|
1294
|
+
book: {
|
|
1295
|
+
id: "writer-book",
|
|
1296
|
+
title: "Writer Book",
|
|
1297
|
+
platform: "other",
|
|
1298
|
+
genre: "other",
|
|
1299
|
+
status: "active",
|
|
1300
|
+
targetChapters: 20,
|
|
1301
|
+
chapterWordCount: 2200,
|
|
1302
|
+
language: "en",
|
|
1303
|
+
createdAt: "2026-03-26T00:00:00.000Z",
|
|
1304
|
+
updatedAt: "2026-03-26T00:00:00.000Z",
|
|
1305
|
+
},
|
|
1306
|
+
bookDir,
|
|
1307
|
+
chapterNumber: 4,
|
|
1308
|
+
chapterMemo: {
|
|
1309
|
+
chapter: 4,
|
|
1310
|
+
goal: "Push Mara back toward the archive ledger.",
|
|
1311
|
+
isGoldenOpening: false,
|
|
1312
|
+
body: "本章要做的是推进 ledger-fragment tension at the archive.",
|
|
1313
|
+
threadRefs: ["mentor-oath", "ledger-fragment"],
|
|
1314
|
+
},
|
|
1315
|
+
contextPackage: {
|
|
1316
|
+
chapter: 4,
|
|
1317
|
+
selectedContext: [
|
|
1318
|
+
{
|
|
1319
|
+
source: "story/pending_hooks.md#mentor-oath",
|
|
1320
|
+
reason: "Carry the unresolved oath line.",
|
|
1321
|
+
excerpt: "relationship | open | old oath debt",
|
|
1322
|
+
},
|
|
1323
|
+
],
|
|
1324
|
+
},
|
|
1325
|
+
ruleStack: {
|
|
1326
|
+
layers: [{ id: "L4", name: "current_task", precedence: 70, scope: "local" }],
|
|
1327
|
+
sections: {
|
|
1328
|
+
hard: ["current_state"],
|
|
1329
|
+
soft: ["current_focus"],
|
|
1330
|
+
diagnostic: ["continuity_audit"],
|
|
1331
|
+
},
|
|
1332
|
+
overrideEdges: [],
|
|
1333
|
+
activeOverrides: [],
|
|
1334
|
+
},
|
|
1335
|
+
lengthSpec: buildLengthSpec(2200, "en"),
|
|
1336
|
+
});
|
|
1337
|
+
|
|
1338
|
+
const systemPrompt = (chatSpy.mock.calls[0]?.[0] as ReadonlyArray<{ content: string }> | undefined)?.[0]?.content ?? "";
|
|
1339
|
+
const creativePrompt = (chatSpy.mock.calls[0]?.[0] as ReadonlyArray<{ content: string }> | undefined)?.[1]?.content ?? "";
|
|
1340
|
+
|
|
1341
|
+
expect(systemPrompt).not.toContain("Hook-A / Hook-B");
|
|
1342
|
+
expect(systemPrompt).toContain("真实 hook_id");
|
|
1343
|
+
// Enum/identifier fields (hookId, movement, chapterType) are NOT sanitized —
|
|
1344
|
+
// the writer needs them to understand which hook to move and what chapter type
|
|
1345
|
+
// to write. Free-text fields (goal, instruction, targetEffect) ARE sanitized.
|
|
1346
|
+
expect(creativePrompt).not.toContain("## Hook Agenda");
|
|
1347
|
+
// hookIds appear verbatim in Hook Plan (identifiers, not free text)
|
|
1348
|
+
expect(creativePrompt).toContain("mentor-oath");
|
|
1349
|
+
expect(creativePrompt).toContain("ledger-fragment");
|
|
1350
|
+
// But slug references INSIDE free text (targetEffect) are sanitized
|
|
1351
|
+
expect(creativePrompt).not.toContain("stale-ledger");
|
|
1352
|
+
expect(creativePrompt).not.toContain("H001");
|
|
1353
|
+
expect(creativePrompt).not.toContain("本章要做的");
|
|
1354
|
+
// The goal text should survive sanitization
|
|
1355
|
+
expect(creativePrompt).toContain("Push Mara back toward the archive ledger.");
|
|
1356
|
+
} finally {
|
|
1357
|
+
await rm(root, { recursive: true, force: true });
|
|
1358
|
+
}
|
|
1359
|
+
});
|
|
1360
|
+
});
|