diff --git a/control_plane/authoring/prompts.py b/control_plane/authoring/prompts.py index d091e85..216b313 100644 --- a/control_plane/authoring/prompts.py +++ b/control_plane/authoring/prompts.py @@ -42,7 +42,10 @@ End with [[END_OF_SCENE]] on its own line.""" STANDALONE_SCENE_PLAN_SYSTEM = """You are a fiction scene architect. Return one valid JSON object only. Preserve every supplied canon fact and source boundary. Plan a complete dramatized scene, not a synopsis. -Do not invent authority for provisional or planning sources, and do not silently resolve contradictions.""" +Do not invent authority for provisional or planning sources, and do not silently resolve contradictions. +When the cited context contains an APPROVED BOOK-STATE PLANNING PACKET, that packet is the hard narrative +scope boundary. Other source context may prevent contradictions but cannot authorize additional events, +agreements, state changes, explanations, or exact values.""" STANDALONE_SCENE_PLAN_TEMPLATE = """Plan one complete scene titled {title}. @@ -70,7 +73,10 @@ Return strict JSON in this shape: "boundary_constraints":[],"continuity_questions":[]}} Use 3-8 concrete beats. Mark only indispensable events required:true. Preserve unresolved continuity questions -instead of guessing. The ending state and final image must define where the scene stops.""" +instead of guessing. The ending state and final image must define where the scene stops. For a book-bound +scene, repeat the approved chapter beats without expanding their events or state changes. Include an exact +value only when it appears explicitly in the approved chapter packet or scene brief; omit numbers and +classifications found only in background source context.""" SCENE_IDEA_TYPES = { "quiet_connection": "A short, low-stakes character moment whose meaning comes from attention or choice.", diff --git a/control_plane/authoring/standalone_scenes.py b/control_plane/authoring/standalone_scenes.py index 55eea4e..1fa082f 100644 --- a/control_plane/authoring/standalone_scenes.py +++ b/control_plane/authoring/standalone_scenes.py @@ -642,6 +642,13 @@ class StandaloneSceneService: ) ) plan = self._normalize_plan(extract_json_object(response.content), scene) + discarded_exact_values = [] + discarded_proposed_beats = [] + if scene.book_state_id: + plan, discarded_proposed_beats = self._apply_book_chapter_scope(scene, plan) + plan["exact_values"], discarded_exact_values = self._scoped_exact_values( + scene, plan["exact_values"] + ) requirements = self._requirements(plan) scene.plan = plan scene.contract_requirements = requirements @@ -654,12 +661,62 @@ class StandaloneSceneService: "prompt_sha256": text_sha256(prompt), "response_sha256": text_sha256(response.content), "context_pack_sha256": pack["sha256"], + "discarded_out_of_scope_exact_values": discarded_exact_values, + "discarded_model_proposed_beats": discarded_proposed_beats, **self._book_state_metadata(scene), } scene.generation_metadata = metadata scene.save() return scene + def _apply_book_chapter_scope( + self, scene: StandaloneScene, plan: dict[str, Any] + ) -> tuple[dict[str, Any], list[dict[str, Any]]]: + chapter, _chapters, _index = self._book_chapter( + scene.book_state.content or {}, scene.book_chapter_key + ) + approved_beats = [ + { + "text": str(item.get("text") or "").strip(), + "required": bool(item.get("required")), + } + for item in chapter.get("beats") or [] + if isinstance(item, dict) and str(item.get("text") or "").strip() + ] + approved_text = {item["text"].casefold() for item in approved_beats} + discarded = [ + item + for item in plan["beats"] + if str(item.get("text") or "").strip().casefold() not in approved_text + ] + plan["beats"] = approved_beats + plan["purpose"] = str(chapter.get("purpose") or "").strip() + plan["ending_state"] = str(chapter.get("ending_state") or "").strip() + return plan, discarded + + def _scoped_exact_values( + self, scene: StandaloneScene, exact_values: list[Any] + ) -> tuple[list[Any], list[Any]]: + scope = "\n".join( + [ + self._book_planning_packet(scene), + scene.brief, + json.dumps(scene.constraints, ensure_ascii=False), + json.dumps(scene.forbidden_events, ensure_ascii=False), + json.dumps(scene.boundary_constraints, ensure_ascii=False), + ] + ).casefold() + allowed = [] + discarded = [] + for item in exact_values: + value = item.get("value") if isinstance(item, dict) else item + candidate = "" if value is None else str(value).strip().casefold() + if candidate and candidate in scope: + allowed.append(item) + else: + discarded.append(item) + return allowed, discarded + def prepare_context( self, scene: StandaloneScene, diff --git a/docs/fortune-below-bootstrap.md b/docs/fortune-below-bootstrap.md new file mode 100644 index 0000000..89c699a --- /dev/null +++ b/docs/fortune-below-bootstrap.md @@ -0,0 +1,139 @@ +# The Fortune Below Bootstrap + +This bootstrap registers source evidence only. It does not extract facts or promote generated prose into canon. + +## 1. Apply Artifex Migrations + +Use the configured PostgreSQL database so scene state and model-request provenance survive across processes. + +```bash +python manage.py migrate +``` + +## 2. Register Shared Hard Canon + +Shared canon belongs to a series-reference work. Book scene retrieval automatically includes this work. + +```bash +python manage.py story_sources register \ + --root S:/PycharmProjects/wraith-book-projects/AuthorCompanion/manuscripts/the-fortune-below \ + --series-slug labyrinth-hero \ + --series-title "Labyrinth Hero" \ + --work-slug fortune-below-series-reference \ + --work-title "Fortune Below Series Reference" \ + --work-type series_reference \ + --authority canon \ + --document-type canon \ + --include-glob "canon/fortune-below-world-lore.md" +``` + +`fortune-below-restructure-canon.md` and `fortune-below-series-arc.md` identify themselves as planning +documents. Archive them under `fortune-below-book-two-legacy` with `planning` authority; never register them +as shared canon. + +## 3. Register Mainline Book Two Planning + +Planning documents are retrievable only when a scene command explicitly includes `planning` authority. Do +not use a broad `book-two/planning/*.md` glob: that directory also contains Sabine/Corin side-novel material. + +```bash +python manage.py story_sources register \ + --root S:/PycharmProjects/wraith-book-projects/AuthorCompanion/manuscripts/the-fortune-below \ + --series-slug labyrinth-hero \ + --series-title "Labyrinth Hero" \ + --work-slug the-fortune-below \ + --work-title "The Fortune Below" \ + --authority planning \ + --document-type planning \ + --include-glob "book-two/planning/fortune-below-book-two-cerys-*.md" \ + --include-glob "book-two/planning/fortune-below-book-two-corins-*.md" \ + --include-glob "book-two/planning/fortune-below-book-two-male-friends-*.md" \ + --include-glob "book-two/planning/fortune-below-book-two-missing-scene-roadmap.md" \ + --include-glob "book-two/planning/fortune-below-book-two-permission-slips-*.md" +``` + +Register the current folded-route plan and approved pilot artifacts separately from the Artifex artifact root. + +## 4. Register The Sabine/Corin Side Novel + +The side novel is a distinct work even though its chronology overlaps Books Two and Three. + +```bash +python manage.py story_sources register \ + --root S:/PycharmProjects/wraith-book-projects/AuthorCompanion/manuscripts/the-fortune-below \ + --series-slug labyrinth-hero \ + --series-title "Labyrinth Hero" \ + --work-slug fortune-below-sabine-corin \ + --work-title "Fortune Below: Sabine and Corin" \ + --authority planning \ + --document-type planning \ + --include-glob "book-two/planning/*sabine-corin*.md" +``` + +Register selected Sabine/Corin scenes and manuscript drafts against this side work, never against +`the-fortune-below`. + +## 5. Archive Existing Mixed Book Two Material + +The old planning and scene directories mix mainline, side-novel, alternate, and superseded structures. Preserve +the complete directories under the `fortune-below-book-two-legacy` work before selecting any source into a +current work. The legacy work is `other`, so its documents are not visible to mainline or side-novel retrieval. + +Pure Sabine/Corin sources may then be registered separately under `fortune-below-sabine-corin`. Do not leave +active copies attached to `the-fortune-below`. + +## 6. Register Existing Mainline Scenes Safely + +Existing mainline scenes enter as provisional evidence. Do not use a broad `book-two/scenes/*.md` glob because +that directory contains side-novel drafts and noncanon experiments. + +```bash +python manage.py story_sources register \ + --root S:/PycharmProjects/wraith-book-projects/AuthorCompanion/manuscripts/the-fortune-below \ + --series-slug labyrinth-hero \ + --series-title "Labyrinth Hero" \ + --work-slug the-fortune-below \ + --work-title "The Fortune Below" \ + --authority provisional \ + --document-type scene \ + --include-glob "book-two/scenes/fortune-below-book-two-cerys-*.md" \ + --include-glob "book-two/scenes/fortune-below-book-two-selka-cerys-*.md" \ + --include-glob "book-two/scenes/fortune-below-book-two-estate-*.md" \ + --include-glob "book-two/scenes/fortune-below-book-two-short-ch01-*.md" +``` + +Register the ten-turn manuscript under the side work as provisional manuscript material until its assembly is +approved. + +```bash +python manage.py story_sources register \ + --root S:/PycharmProjects/wraith-book-projects/AuthorCompanion/manuscripts/the-fortune-below \ + --series-slug labyrinth-hero \ + --series-title "Labyrinth Hero" \ + --work-slug fortune-below-sabine-corin \ + --work-title "Fortune Below: Sabine and Corin" \ + --authority provisional \ + --document-type manuscript \ + --include-glob "book-two/ten-turn-manuscript/*.md" +``` + +## 7. Plan A Scene + +The default context includes hard canon only. This example deliberately includes planning material and pins the +two current rule documents. + +```bash +python manage.py fiction_scene run \ + --series-slug labyrinth-hero \ + --work-slug fortune-below-sabine-corin \ + --title "New Scene Title" \ + --brief S:/path/to/new-scene-brief.md \ + --target-words 2200 \ + --include-authority canon \ + --include-authority planning \ + --pin-document book-two/planning/fortune-below-sabine-corin-turn-and-household-rules.md \ + --pin-document book-two/planning/fortune-below-sabine-corin-physical-continuity-reconciliation.md +``` + +Inspect the returned plan before approving it. Final scene approval registers the artifact as `provisional`, not +`canon`. Mainline scene commands must not pin side-work documents. diff --git a/story_artifacts/the-fortune-below/source-boundaries/book-two-mainline-fact-ledger.md b/story_artifacts/the-fortune-below/source-boundaries/book-two-mainline-fact-ledger.md new file mode 100644 index 0000000..3ddfeb6 --- /dev/null +++ b/story_artifacts/the-fortune-below/source-boundaries/book-two-mainline-fact-ledger.md @@ -0,0 +1,50 @@ +# Book Two Mainline Fact Ledger + +## Authority + +This ledger separates mainline planning facts from the overlapping Sabine/Corin side novel. The approved +two-chapter book state is authoritative only for its pilot run. Everything beyond Chapter 2 remains +provisional until approved in a later book-state version. + +## Approved Pilot Scope + +### Chapter 1: Two Markers + +- Selka accepts a survey under an independently terminable contract. +- Selka retains ownership of her maps. +- Records appear to place the same damaged marker in two incompatible locations. +- Selka refuses to certify an explanation before controlled field testing. +- The doubled-marker contradiction remains unresolved at the chapter ending. + +### Chapter 2: What The Profit Means + +- The bank confirms the million-crown windfall. +- Corin proposes protected allocations; the allocations are not yet accomplished facts. +- Cerys recognizes that the proposal could make legal freedom financially possible. +- Cerys asks whether freedom can coexist with her chosen home, work, and belonging. +- Cerys is not released in the pilot. + +### Pilot Exclusions + +- Do not resolve the folded-route anomaly. +- Do not establish additional financial, contractual, legal, or route facts merely because they appear in + legacy planning. +- Do not introduce the Sabine/Corin game. + +## Provisional Mainline Scope After Chapter 2 + +The current folded-route restructure proposes, but has not canonized: + +- Selka's investigation as Book Two's external spine. +- Cerys's meaningful freedom as the emotional co-spine. +- Corin learning that money and equipment cannot replace another person's authority. +- Sabine's later rescue, prompt release, independent alternatives, chosen return, and paid estate work. +- Short externally observable Sabine/Corin background beats after Sabine is free. +- The folded-route crisis as the mainline climax. +- Hallow Street becoming independently owned while Corin becomes a guest rather than landlord. + +## Material Not Owned By Mainline + +The mainline does not own Sabine and Corin's private turns, scores, physical progression, declarations, +sealed-envelope events, threshold sequence, or ending. Those belong to the side novel and may enter a +mainline book only through a separately reviewed shared-continuity bridge. diff --git a/story_artifacts/the-fortune-below/source-boundaries/mainline-side-novel-separation.md b/story_artifacts/the-fortune-below/source-boundaries/mainline-side-novel-separation.md new file mode 100644 index 0000000..c3cf880 --- /dev/null +++ b/story_artifacts/the-fortune-below/source-boundaries/mainline-side-novel-separation.md @@ -0,0 +1,72 @@ +# Fortune Below Mainline And Sabine/Corin Source Boundary + +## Status + +This is an operating source-ownership boundary. It controls retrieval and planning scope; it does not +promote provisional events into canon. + +## Mainline Books Two And Three + +The mainline owns events that change the expedition, household, legal, financial, or publicly observable +state of the ensemble. This includes: + +- Selka's folded-route investigation and professional authority. +- Cerys's allocation, release, employment, belonging, and mainline relationship choices. +- The estate, Hallow Street, route-office, equipment, and expedition arcs. +- Sabine's rescue, legal release, independent alternatives, chosen return, paid work, and relationships with + the household when those events are selected into an approved mainline book state. +- Only the minimum externally observable Sabine/Corin continuity needed by another mainline scene. + +The approved two-chapter Book Two pilot contains no Sabine/Corin side-novel event. Chapter 1 excludes the +game entirely, and Chapter 2 concerns Cerys's freedom question. + +## Sabine/Corin Side Novel + +The side novel owns the private progression of Sabine and Corin's relationship across the time occupied by +Books Two and Three. This includes: + +- The game, turns, scores, rules, private experiments, and physical thresholds. +- Private declarations, the sealed envelope, its handling, and its resolution. +- The ten-turn manuscript, the preferred threshold sequence, alternate follow-ups, and rejected experiments. +- Scene-level causes and consequences that are not independently visible to the mainline ensemble. + +Mainline planning must not retrieve these materials. A side-novel event does not become a mainline fact merely +because its chronology overlaps a mainline book. + +## Shared Continuity + +Shared continuity must be deliberately copied into a reviewed bridge document. It is limited to public or +externally observable resulting states, never private scene choreography. + +Current provisional Book Two bridge candidates are: + +- Sabine and Corin begin a private game after Sabine is free and has chosen to remain at the estate. +- Mainline scenes may show brief, unexplained evidence that the game continues. +- The game remains unresolved at the end of the current provisional Book Two structure. + +Potential Book Three bridge facts remain unapproved until the side-novel chronology and manuscript assembly +are approved. Mainline work must not assume declarations, envelope resolution, physical completion, standing +permission, or expected repetition. + +## Retrieval Rules + +- Mainline work may retrieve mainline sources and reviewed shared-continuity bridges. +- Side-novel work may retrieve side-novel sources and reviewed shared-continuity bridges. +- Neither work may retrieve the other work's planning, scenes, or manuscript drafts directly. +- Series reference is reserved for genuinely shared world and established continuity material. +- A document labelled provisional or planning cannot be registered with canon authority. +- Mixed documents must be superseded and replaced by separately scoped documents rather than shared broadly. + +## Current Ownership Decisions + +- `book-two-folded-route-restructure-plan.md` belongs to mainline Book Two planning. +- `book-two-folded-route-pilot-2-chapters.json` and its chapter briefs belong to mainline Book Two. +- `sabine-corin-side-book-30-chapter-plan.md` belongs to side-novel planning. +- Sabine/Corin scene menus, turn plans, private-scene drafts, the ten-turn manuscript, threshold prose, and + optional follow-ups belong to the side novel. +- The complete `No Final Move` experiment belongs to the side work as a noncanon experiment, not as a + provisional mainline manuscript. +- `fortune-below-book-two-expanded-friendship-and-hallow-arcs.md` is mixed legacy planning and must be + superseded rather than used as a governing source by either work. +- `fortune-below-series-arc.md` and `fortune-below-restructure-canon.md` identify themselves as planning and + must not retain canon authority. diff --git a/story_artifacts/the-fortune-below/source-boundaries/sabine-corin-side-novel-ledger.md b/story_artifacts/the-fortune-below/source-boundaries/sabine-corin-side-novel-ledger.md new file mode 100644 index 0000000..26cbb48 --- /dev/null +++ b/story_artifacts/the-fortune-below/source-boundaries/sabine-corin-side-novel-ledger.md @@ -0,0 +1,43 @@ +# Sabine And Corin Side-Novel Ledger + +## Authority + +This is a provisional scope and continuity ledger for the side novel. It does not canonize its candidate +assembly or determine exactly which events fall during Book Two versus Book Three. + +## Mainline Prerequisites + +The side novel may rely on these events only after their corresponding mainline book states are approved: + +- Sabine is removed from an imminent coercive sale. +- Her legal claim is cancelled at the first practical opportunity. +- She receives real alternatives and can leave without losing safety or subsistence. +- She investigates those alternatives and returns by choice. +- She undertakes paid estate work with independent authority. + +The side novel must not rewrite the folded-route investigation, Cerys's freedom, expedition outcomes, estate +ownership, or other mainline state transitions. + +## Side-Novel Ownership + +The side novel exclusively owns: + +- Shopping With Corin, Full Attention, Two Cups, and the expensive dance lessons. +- The six-turn sequence and its individual chapter candidates. +- The First Journey, Envelope After, Long Game, and He Likes Her Happy. +- The ten-turn manuscript and all private relationship developments inside it. +- The preferred threshold run from Reserved Accidental Threshold Escalation through Fortunately. +- Optional follow-ups, rejected experiments, and candidate menus as noncanon alternatives. + +## Current Preferred Ending State + +The preferred ending material currently proposes that Sabine and Corin openly love one another, continue the +game, and have deliberately completed one previously deferred physical threshold. It does not create standing +permission, expected repetition, or an end to the game. These remain side-novel outcomes until the manuscript +and chronology are approved. + +## Mainline Export Rule + +No private side-novel event is exported automatically. A reviewed bridge may expose only the minimum public +result another book requires, such as the household noticing that the relationship has changed. Scores, +private choreography, envelope contents, and scene-level causality remain private to the side novel. diff --git a/tests/test_book_authoring_state.py b/tests/test_book_authoring_state.py index 10f720c..daac247 100644 --- a/tests/test_book_authoring_state.py +++ b/tests/test_book_authoring_state.py @@ -20,7 +20,7 @@ from control_plane.authoring.models import ( ) from control_plane.authoring.standalone_scenes import StandaloneSceneService from control_plane.projects.models import Project -from model_router.router import ModelResponseContract +from model_router.router import ModelCapability, ModelResponseContract pytestmark = pytest.mark.django_db(transaction=True) @@ -38,6 +38,42 @@ class FakeReviewRouter: ) +class OutOfScopePlanningRouter: + def complete(self, request): + assert str(request.purpose) == str(ModelCapability.STORY_PLANNING) + return ModelResponseContract( + model="test-planner", + content=json.dumps( + { + "purpose": "Perform the approved turn.", + "pov_character": "Protagonist", + "tense": "past", + "location": "Records room", + "time_context": "Chapter 1", + "present": ["Protagonist"], + "beats": [ + {"text": "The chapter performs turn 1.", "required": True}, + { + "text": "The planner adds a Floor ninety-six inspection.", + "required": True, + }, + ], + "exact_values": [ + {"label": "approved turn", "value": "turn 1"}, + {"label": "background floor", "value": "Floor ninety-six"}, + ], + "constraints": [], + "forbidden_events": [], + "ending_state": "Turn 1 is complete.", + "final_image": "The protagonist closes the record.", + "boundary_constraints": [], + "continuity_questions": [], + } + ), + metadata={}, + ) + + @pytest.fixture def work(tmp_path: Path) -> Work: series = Series.objects.create(title="Test Series", slug="test-series") @@ -338,6 +374,42 @@ def test_run_sync_uses_only_scenes_bound_to_exact_state_and_chapter(work: Work) assert revision.revision == 2 +def test_book_scene_plan_discards_exact_values_outside_approved_scope(work: Work) -> None: + state_service = BookStateService(FakeReviewRouter()) + state = state_service.create(work=work, content=book_content(1)) + make_review_ready(state) + state = state_service.approve(state) + state_service.start_run(state) + scene_service = StandaloneSceneService(OutOfScopePlanningRouter()) + scene = scene_service.create( + work=work, + title="Chapter 1", + brief="Perform turn 1 and stop.", + book_state=state, + book_chapter_key="chapter-1", + ) + + scene = scene_service.plan(scene) + + assert scene.plan["exact_values"] == [ + {"label": "approved turn", "value": "turn 1"} + ] + assert scene.plan["beats"] == [ + {"text": "The chapter performs turn 1.", "required": True} + ] + assert scene.plan["purpose"] == "Advance turn 1." + assert scene.plan["ending_state"] == "Turn 1 is complete." + assert scene.generation_metadata["planning"][ + "discarded_out_of_scope_exact_values" + ] == [{"label": "background floor", "value": "Floor ninety-six"}] + assert scene.generation_metadata["planning"]["discarded_model_proposed_beats"] == [ + { + "text": "The planner adds a Floor ninety-six inspection.", + "required": True, + } + ] + + def test_lifecycle_persists_audit_metadata_and_run_policy(work: Work) -> None: service = BookStateService(FakeReviewRouter()) state = service.create(