authoringfw 0.1.0__tar.gz

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.
@@ -0,0 +1,38 @@
1
+ name: Publish to PyPI
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - "v*"
7
+ workflow_dispatch:
8
+
9
+ jobs:
10
+ test:
11
+ runs-on: ubuntu-latest
12
+ steps:
13
+ - uses: actions/checkout@v4
14
+ - uses: actions/setup-python@v5
15
+ with:
16
+ python-version: "3.11"
17
+ - name: Install and test
18
+ run: |
19
+ pip install -e ".[dev]"
20
+ pytest tests/ -v
21
+
22
+ publish:
23
+ needs: test
24
+ runs-on: ubuntu-latest
25
+ environment: pypi
26
+ permissions:
27
+ id-token: write
28
+ steps:
29
+ - uses: actions/checkout@v4
30
+ - uses: actions/setup-python@v5
31
+ with:
32
+ python-version: "3.11"
33
+ - name: Build
34
+ run: |
35
+ pip install hatch
36
+ hatch build
37
+ - name: Publish to PyPI
38
+ uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,11 @@
1
+ .venv/
2
+ __pycache__/
3
+ *.py[cod]
4
+ *.egg-info/
5
+ dist/
6
+ build/
7
+ .pytest_cache/
8
+ .ruff_cache/
9
+ .coverage
10
+ htmlcov/
11
+ *.log
@@ -0,0 +1,19 @@
1
+ # Changelog — authoringfw
2
+
3
+ ## [Unreleased]
4
+
5
+ ## [0.1.0] — 2026-02-28
6
+
7
+ ### Added
8
+ - Initial release
9
+ - `StyleProfile` — tone, POV, tense, vocabulary, sentence rhythm + `to_constraints()`
10
+ - `CharacterProfile` — name, role, traits, backstory, arc, relationships + `to_context_string()`
11
+ - `WorldContext` + `Location` — world rules, locations, lore + `to_context_string()`
12
+ - `VersionMetadata` — immutable content snapshot with SHA-256 hash, semver, LLM metadata
13
+ - `PhaseSnapshot` — project state at workflow phase boundary
14
+ - `ChangeType` enum (AI_GENERATED, HUMAN_EDITED, MERGED, REVERTED)
15
+ - `FormatProfile` — novel, essay, series, scientific with workflow phases and style constraints
16
+ - `WorkflowPhase` enum (IDEATION → PRODUCTION)
17
+ - `StepConfig` — per-step template and parameter config
18
+ - `get_format()` — lookup built-in format profiles
19
+ - `IStyleAdapter`, `ICharacterAdapter`, `IWorldAdapter` Protocol interfaces (`@runtime_checkable`)
@@ -0,0 +1,103 @@
1
+ Metadata-Version: 2.4
2
+ Name: authoringfw
3
+ Version: 0.1.0
4
+ Summary: Authoring Framework — domain schemas for AI-assisted creative writing applications
5
+ Project-URL: Homepage, https://github.com/achimdehnert/platform
6
+ Project-URL: Repository, https://github.com/achimdehnert/platform/tree/main/packages/authoringfw
7
+ Author-email: Achim Dehnert <achim@dehnert.com>
8
+ License: MIT
9
+ Keywords: ai,authoring,creative-writing,llm,pydantic,schema,story
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
16
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
17
+ Classifier: Topic :: Text Processing :: Linguistic
18
+ Requires-Python: >=3.11
19
+ Requires-Dist: pydantic>=2.6
20
+ Provides-Extra: dev
21
+ Requires-Dist: pytest-mock>=3.12; extra == 'dev'
22
+ Requires-Dist: pytest>=8.0; extra == 'dev'
23
+ Provides-Extra: testing
24
+ Requires-Dist: pytest-mock>=3.12; extra == 'testing'
25
+ Requires-Dist: pytest>=8.0; extra == 'testing'
26
+ Description-Content-Type: text/markdown
27
+
28
+ # authoringfw — Authoring Framework
29
+
30
+ Domain schemas for AI-assisted creative writing applications.
31
+
32
+ ## Installation
33
+
34
+ ```bash
35
+ pip install authoringfw
36
+ ```
37
+
38
+ ## Quick Start
39
+
40
+ ```python
41
+ from authoringfw import StyleProfile, CharacterProfile, WorldContext, get_format
42
+
43
+ # Style constraints for prompt injection
44
+ style = StyleProfile(tone="melancholic", pov="third_limited", tense="past")
45
+ constraints = style.to_constraints()
46
+
47
+ # Character context
48
+ alice = CharacterProfile(
49
+ name="Alice",
50
+ role="protagonist",
51
+ personality_traits=["brave", "curious"],
52
+ arc="From fear to courage",
53
+ )
54
+ print(alice.to_context_string())
55
+
56
+ # World context
57
+ world = WorldContext(
58
+ title="The Shattered Realms",
59
+ genre="fantasy",
60
+ world_rules=["Magic costs life force", "Dragons are extinct"],
61
+ )
62
+ print(world.to_context_string())
63
+
64
+ # Format profiles (novel, essay, series, scientific)
65
+ roman = get_format("roman")
66
+ print(roman.style_constraints)
67
+ ```
68
+
69
+ ## Schemas
70
+
71
+ - **`StyleProfile`** — tone, POV, tense, vocabulary, sentence rhythm
72
+ - **`CharacterProfile`** — name, role, traits, backstory, arc, relationships
73
+ - **`WorldContext`** — title, genre, setting, world rules, locations, lore
74
+ - **`VersionMetadata`** — immutable content snapshot with hash, semver, LLM metadata
75
+ - **`PhaseSnapshot`** — project state at a workflow phase boundary
76
+
77
+ ## Format Profiles
78
+
79
+ Built-in formats: `roman`, `essay`, `serie`, `scientific`
80
+
81
+ ```python
82
+ from authoringfw.formats.base import get_format, WorkflowPhase
83
+
84
+ novel = get_format("roman")
85
+ outline_steps = novel.steps_for_phase(WorkflowPhase.OUTLINE)
86
+ ```
87
+
88
+ ## Adapter Interfaces
89
+
90
+ Protocol-based adapters — no inheritance required:
91
+
92
+ ```python
93
+ from authoringfw.adapters.interfaces import IStyleAdapter
94
+
95
+ class MyStyleAdapter:
96
+ async def get_profile(self, style_id): ...
97
+ async def analyze_text(self, text): ...
98
+ def generate_style_constraints(self, profile): ...
99
+ async def score_conformity(self, text, profile): ...
100
+
101
+ adapter = MyStyleAdapter()
102
+ assert isinstance(adapter, IStyleAdapter) # True via @runtime_checkable
103
+ ```
@@ -0,0 +1,76 @@
1
+ # authoringfw — Authoring Framework
2
+
3
+ Domain schemas for AI-assisted creative writing applications.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ pip install authoringfw
9
+ ```
10
+
11
+ ## Quick Start
12
+
13
+ ```python
14
+ from authoringfw import StyleProfile, CharacterProfile, WorldContext, get_format
15
+
16
+ # Style constraints for prompt injection
17
+ style = StyleProfile(tone="melancholic", pov="third_limited", tense="past")
18
+ constraints = style.to_constraints()
19
+
20
+ # Character context
21
+ alice = CharacterProfile(
22
+ name="Alice",
23
+ role="protagonist",
24
+ personality_traits=["brave", "curious"],
25
+ arc="From fear to courage",
26
+ )
27
+ print(alice.to_context_string())
28
+
29
+ # World context
30
+ world = WorldContext(
31
+ title="The Shattered Realms",
32
+ genre="fantasy",
33
+ world_rules=["Magic costs life force", "Dragons are extinct"],
34
+ )
35
+ print(world.to_context_string())
36
+
37
+ # Format profiles (novel, essay, series, scientific)
38
+ roman = get_format("roman")
39
+ print(roman.style_constraints)
40
+ ```
41
+
42
+ ## Schemas
43
+
44
+ - **`StyleProfile`** — tone, POV, tense, vocabulary, sentence rhythm
45
+ - **`CharacterProfile`** — name, role, traits, backstory, arc, relationships
46
+ - **`WorldContext`** — title, genre, setting, world rules, locations, lore
47
+ - **`VersionMetadata`** — immutable content snapshot with hash, semver, LLM metadata
48
+ - **`PhaseSnapshot`** — project state at a workflow phase boundary
49
+
50
+ ## Format Profiles
51
+
52
+ Built-in formats: `roman`, `essay`, `serie`, `scientific`
53
+
54
+ ```python
55
+ from authoringfw.formats.base import get_format, WorkflowPhase
56
+
57
+ novel = get_format("roman")
58
+ outline_steps = novel.steps_for_phase(WorkflowPhase.OUTLINE)
59
+ ```
60
+
61
+ ## Adapter Interfaces
62
+
63
+ Protocol-based adapters — no inheritance required:
64
+
65
+ ```python
66
+ from authoringfw.adapters.interfaces import IStyleAdapter
67
+
68
+ class MyStyleAdapter:
69
+ async def get_profile(self, style_id): ...
70
+ async def analyze_text(self, text): ...
71
+ def generate_style_constraints(self, profile): ...
72
+ async def score_conformity(self, text, profile): ...
73
+
74
+ adapter = MyStyleAdapter()
75
+ assert isinstance(adapter, IStyleAdapter) # True via @runtime_checkable
76
+ ```
@@ -0,0 +1,55 @@
1
+ [project]
2
+ name = "authoringfw"
3
+ version = "0.1.0"
4
+ description = "Authoring Framework — domain schemas for AI-assisted creative writing applications"
5
+ readme = "README.md"
6
+ requires-python = ">=3.11"
7
+ license = {text = "MIT"}
8
+ authors = [
9
+ {name = "Achim Dehnert", email = "achim@dehnert.com"}
10
+ ]
11
+ keywords = ["llm", "authoring", "creative-writing", "ai", "story", "schema", "pydantic"]
12
+ classifiers = [
13
+ "Development Status :: 3 - Alpha",
14
+ "Intended Audience :: Developers",
15
+ "License :: OSI Approved :: MIT License",
16
+ "Programming Language :: Python :: 3.11",
17
+ "Programming Language :: Python :: 3.12",
18
+ "Topic :: Scientific/Engineering :: Artificial Intelligence",
19
+ "Topic :: Software Development :: Libraries :: Python Modules",
20
+ "Topic :: Text Processing :: Linguistic",
21
+ ]
22
+
23
+ dependencies = [
24
+ "pydantic>=2.6",
25
+ ]
26
+
27
+ [project.optional-dependencies]
28
+ dev = [
29
+ "pytest>=8.0",
30
+ "pytest-mock>=3.12",
31
+ ]
32
+ testing = [
33
+ "pytest>=8.0",
34
+ "pytest-mock>=3.12",
35
+ ]
36
+
37
+ [project.urls]
38
+ Homepage = "https://github.com/achimdehnert/platform"
39
+ Repository = "https://github.com/achimdehnert/platform/tree/main/packages/authoringfw"
40
+
41
+ [build-system]
42
+ requires = ["hatchling"]
43
+ build-backend = "hatchling.build"
44
+
45
+ [tool.hatch.build.targets.wheel]
46
+ packages = ["src/authoringfw"]
47
+
48
+ [tool.pytest.ini_options]
49
+ python_files = ["test_*.py"]
50
+ pythonpath = ["."]
51
+ testpaths = ["tests"]
52
+
53
+ [tool.ruff]
54
+ line-length = 100
55
+ target-version = "py311"
@@ -0,0 +1,24 @@
1
+ """
2
+ authoringfw — Authoring Framework
3
+
4
+ Domain schemas for AI-assisted creative writing applications.
5
+ """
6
+
7
+ __version__ = "0.1.0"
8
+
9
+ from authoringfw.schema.style import StyleProfile
10
+ from authoringfw.schema.character import CharacterProfile
11
+ from authoringfw.schema.world import WorldContext
12
+ from authoringfw.schema.versioning import VersionMetadata, ChangeType
13
+ from authoringfw.formats.base import FormatProfile, WorkflowPhase
14
+
15
+ __all__ = [
16
+ "StyleProfile",
17
+ "CharacterProfile",
18
+ "WorldContext",
19
+ "VersionMetadata",
20
+ "ChangeType",
21
+ "FormatProfile",
22
+ "WorkflowPhase",
23
+ "__version__",
24
+ ]
File without changes
@@ -0,0 +1,38 @@
1
+ """Protocol-based adapter interfaces for authoringfw.
2
+
3
+ Implementations use duck typing — no inheritance required.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from typing import Protocol, runtime_checkable
9
+
10
+ from authoringfw.schema.character import CharacterProfile
11
+ from authoringfw.schema.style import StyleProfile
12
+ from authoringfw.schema.world import WorldContext
13
+
14
+
15
+ @runtime_checkable
16
+ class IStyleAdapter(Protocol):
17
+ """Adapter for reading and scoring style profiles."""
18
+
19
+ async def get_profile(self, style_id: str) -> StyleProfile: ...
20
+ async def analyze_text(self, text: str) -> StyleProfile: ...
21
+ def generate_style_constraints(self, profile: StyleProfile) -> list[str]: ...
22
+ async def score_conformity(self, text: str, profile: StyleProfile) -> float: ...
23
+
24
+
25
+ @runtime_checkable
26
+ class ICharacterAdapter(Protocol):
27
+ """Adapter for reading character profiles."""
28
+
29
+ async def get_character(self, character_id: str) -> CharacterProfile: ...
30
+ async def list_characters(self, project_id: str) -> list[CharacterProfile]: ...
31
+
32
+
33
+ @runtime_checkable
34
+ class IWorldAdapter(Protocol):
35
+ """Adapter for reading world context."""
36
+
37
+ async def get_world(self, world_id: str) -> WorldContext: ...
38
+ async def list_locations(self, world_id: str) -> list[str]: ...
File without changes
@@ -0,0 +1,90 @@
1
+ """Base format profile and workflow phase definitions."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from enum import Enum
6
+ from typing import Any
7
+
8
+ from pydantic import BaseModel, Field
9
+
10
+
11
+ class WorkflowPhase(str, Enum):
12
+ IDEATION = "ideation"
13
+ CONCEPT = "concept"
14
+ OUTLINE = "outline"
15
+ FIRST_DRAFT = "first_draft"
16
+ REVISION = "revision"
17
+ CONSISTENCY = "consistency"
18
+ PRODUCTION = "production"
19
+
20
+
21
+ class StepConfig(BaseModel):
22
+ """Configuration for a single workflow step."""
23
+
24
+ name: str
25
+ phase: WorkflowPhase
26
+ prompt_template_id: str
27
+ max_tokens: int = 2000
28
+ temperature: float = 0.7
29
+ requires_human_approval: bool = False
30
+ metadata: dict[str, Any] = Field(default_factory=dict)
31
+
32
+
33
+ class FormatProfile(BaseModel):
34
+ """Defines the writing format and its workflow phases."""
35
+
36
+ format_type: str
37
+ display_name: str
38
+ description: str = ""
39
+ phases: list[WorkflowPhase] = Field(
40
+ default_factory=lambda: list(WorkflowPhase)
41
+ )
42
+ steps: list[StepConfig] = Field(default_factory=list)
43
+ style_constraints: list[str] = Field(default_factory=list)
44
+ metadata: dict[str, Any] = Field(default_factory=dict)
45
+
46
+ def steps_for_phase(self, phase: WorkflowPhase) -> list[StepConfig]:
47
+ return [s for s in self.steps if s.phase == phase]
48
+
49
+
50
+ ROMAN = FormatProfile(
51
+ format_type="roman",
52
+ display_name="Novel",
53
+ description="Long-form fiction with chapters, characters, and story arcs.",
54
+ style_constraints=["Show don't tell", "Consistent POV", "Scene & sequel structure"],
55
+ )
56
+
57
+ ESSAY = FormatProfile(
58
+ format_type="essay",
59
+ display_name="Essay",
60
+ description="Argumentative or descriptive non-fiction prose.",
61
+ style_constraints=["Clear thesis", "Logical argumentation", "Evidence-based claims"],
62
+ )
63
+
64
+ SERIE = FormatProfile(
65
+ format_type="serie",
66
+ display_name="Series",
67
+ description="Multi-volume fiction with shared universe and recurring characters.",
68
+ style_constraints=["Series-wide consistency", "Character continuity", "World-building coherence"],
69
+ )
70
+
71
+ SCIENTIFIC = FormatProfile(
72
+ format_type="scientific",
73
+ display_name="Scientific Paper",
74
+ description="Academic writing with citations, methodology, and structured sections.",
75
+ style_constraints=["Objective tone", "Precise terminology", "Citation required"],
76
+ )
77
+
78
+ FORMAT_REGISTRY: dict[str, FormatProfile] = {
79
+ "roman": ROMAN,
80
+ "essay": ESSAY,
81
+ "serie": SERIE,
82
+ "scientific": SCIENTIFIC,
83
+ }
84
+
85
+
86
+ def get_format(format_type: str) -> FormatProfile:
87
+ """Get a built-in format profile by type string."""
88
+ if format_type not in FORMAT_REGISTRY:
89
+ raise KeyError(f"Unknown format type '{format_type}'. Available: {list(FORMAT_REGISTRY)}")
90
+ return FORMAT_REGISTRY[format_type]
File without changes
@@ -0,0 +1,31 @@
1
+ """Character profile schema for AI-assisted authoring."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from pydantic import BaseModel, Field
8
+
9
+
10
+ class CharacterProfile(BaseModel):
11
+ """Represents a character in a story universe."""
12
+
13
+ name: str
14
+ role: str = "supporting"
15
+ description: str = ""
16
+ personality_traits: list[str] = Field(default_factory=list)
17
+ backstory: str = ""
18
+ relationships: dict[str, str] = Field(default_factory=dict)
19
+ arc: str = ""
20
+ metadata: dict[str, Any] = Field(default_factory=dict)
21
+
22
+ def to_context_string(self) -> str:
23
+ """Format character for prompt injection."""
24
+ lines = [f"**{self.name}** ({self.role})"]
25
+ if self.description:
26
+ lines.append(self.description)
27
+ if self.personality_traits:
28
+ lines.append(f"Traits: {', '.join(self.personality_traits)}")
29
+ if self.arc:
30
+ lines.append(f"Arc: {self.arc}")
31
+ return "\n".join(lines)
@@ -0,0 +1,30 @@
1
+ """Style profile schema for AI-assisted authoring."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from pydantic import BaseModel, Field
8
+
9
+
10
+ class StyleProfile(BaseModel):
11
+ """Defines the stylistic characteristics of a writing project."""
12
+
13
+ tone: str = "neutral"
14
+ pov: str = "third_limited"
15
+ tense: str = "past"
16
+ vocabulary_level: str = "medium"
17
+ sentence_rhythm: str = "varied"
18
+ author_signature: dict[str, Any] = Field(default_factory=dict)
19
+
20
+ model_config = {"frozen": False}
21
+
22
+ def to_constraints(self) -> list[str]:
23
+ """Return a list of natural-language style constraints for prompt injection."""
24
+ return [
25
+ f"Tone: {self.tone}",
26
+ f"Point of view: {self.pov}",
27
+ f"Tense: {self.tense}",
28
+ f"Vocabulary level: {self.vocabulary_level}",
29
+ f"Sentence rhythm: {self.sentence_rhythm}",
30
+ ]
@@ -0,0 +1,55 @@
1
+ """Versioning schema for AI-generated content snapshots."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import hashlib
6
+ from datetime import datetime, timezone
7
+ from enum import Enum
8
+ from typing import Any
9
+
10
+ from pydantic import BaseModel, Field
11
+
12
+
13
+ class ChangeType(str, Enum):
14
+ AI_GENERATED = "ai_generated"
15
+ HUMAN_EDITED = "human_edited"
16
+ MERGED = "merged"
17
+ REVERTED = "reverted"
18
+
19
+
20
+ class VersionMetadata(BaseModel):
21
+ """Immutable metadata snapshot for a content version."""
22
+
23
+ version_id: str
24
+ semver: str = "1.0.0"
25
+ phase: str = ""
26
+ node_id: str = ""
27
+ change_type: ChangeType = ChangeType.AI_GENERATED
28
+ author: str = ""
29
+ timestamp: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
30
+ parent_version_id: str | None = None
31
+ content_hash: str = ""
32
+ prompt_template_id: str = ""
33
+ prompt_template_version: str = ""
34
+ llm_model: str = ""
35
+ llm_temperature: float = 0.7
36
+ quality_scores: dict[str, Any] = Field(default_factory=dict)
37
+
38
+ model_config = {"frozen": True}
39
+
40
+ def compute_hash(self, content: str) -> "VersionMetadata":
41
+ """Return a new instance with content_hash filled in."""
42
+ h = hashlib.sha256(content.encode()).hexdigest()[:16]
43
+ return self.model_copy(update={"content_hash": h})
44
+
45
+
46
+ class PhaseSnapshot(BaseModel):
47
+ """A complete snapshot of a project at a workflow phase boundary."""
48
+
49
+ snapshot_id: str
50
+ phase: str
51
+ project_id: str
52
+ created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
53
+ node_versions: dict[str, str] = Field(default_factory=dict)
54
+ approved_by: str = ""
55
+ notes: str = ""
@@ -0,0 +1,42 @@
1
+ """World context schema for AI-assisted authoring."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from pydantic import BaseModel, Field
8
+
9
+
10
+ class Location(BaseModel):
11
+ """A location within a story world."""
12
+
13
+ name: str
14
+ description: str = ""
15
+ atmosphere: str = ""
16
+ metadata: dict[str, Any] = Field(default_factory=dict)
17
+
18
+
19
+ class WorldContext(BaseModel):
20
+ """The story universe — locations, lore, rules, and time period."""
21
+
22
+ title: str
23
+ genre: str = ""
24
+ setting: str = ""
25
+ time_period: str = ""
26
+ world_rules: list[str] = Field(default_factory=list)
27
+ locations: list[Location] = Field(default_factory=list)
28
+ lore: str = ""
29
+ metadata: dict[str, Any] = Field(default_factory=dict)
30
+
31
+ def to_context_string(self) -> str:
32
+ """Format world context for prompt injection."""
33
+ lines = [f"World: {self.title}"]
34
+ if self.genre:
35
+ lines.append(f"Genre: {self.genre}")
36
+ if self.setting:
37
+ lines.append(f"Setting: {self.setting}")
38
+ if self.time_period:
39
+ lines.append(f"Time period: {self.time_period}")
40
+ if self.world_rules:
41
+ lines.append("Rules: " + "; ".join(self.world_rules))
42
+ return "\n".join(lines)
File without changes
@@ -0,0 +1,99 @@
1
+ """Tests for authoringfw schema models."""
2
+
3
+ import pytest
4
+ from pydantic import ValidationError
5
+
6
+ from authoringfw.schema.style import StyleProfile
7
+ from authoringfw.schema.character import CharacterProfile
8
+ from authoringfw.schema.world import WorldContext, Location
9
+ from authoringfw.schema.versioning import VersionMetadata, ChangeType, PhaseSnapshot
10
+ from authoringfw.formats.base import FormatProfile, WorkflowPhase, get_format
11
+
12
+
13
+ def test_style_profile_defaults():
14
+ s = StyleProfile()
15
+ assert s.tone == "neutral"
16
+ assert s.pov == "third_limited"
17
+
18
+
19
+ def test_style_profile_to_constraints():
20
+ s = StyleProfile(tone="dark", pov="first_person", tense="present")
21
+ constraints = s.to_constraints()
22
+ assert any("dark" in c for c in constraints)
23
+ assert any("first_person" in c for c in constraints)
24
+
25
+
26
+ def test_character_profile_context_string():
27
+ c = CharacterProfile(
28
+ name="Alice",
29
+ role="protagonist",
30
+ personality_traits=["brave", "curious"],
31
+ arc="From fear to courage",
32
+ )
33
+ ctx = c.to_context_string()
34
+ assert "Alice" in ctx
35
+ assert "brave" in ctx
36
+ assert "courage" in ctx
37
+
38
+
39
+ def test_world_context_to_string():
40
+ w = WorldContext(
41
+ title="Middle Earth",
42
+ genre="fantasy",
43
+ setting="medieval",
44
+ world_rules=["Magic exists", "Dragons are rare"],
45
+ )
46
+ ctx = w.to_context_string()
47
+ assert "Middle Earth" in ctx
48
+ assert "fantasy" in ctx
49
+ assert "Magic exists" in ctx
50
+
51
+
52
+ def test_version_metadata_frozen():
53
+ v = VersionMetadata(version_id="v1", semver="1.0.0")
54
+ with pytest.raises(Exception):
55
+ v.version_id = "v2"
56
+
57
+
58
+ def test_version_metadata_compute_hash():
59
+ v = VersionMetadata(version_id="v1")
60
+ v2 = v.compute_hash("Hello world content")
61
+ assert v2.content_hash != ""
62
+ assert len(v2.content_hash) == 16
63
+
64
+
65
+ def test_change_type_values():
66
+ assert ChangeType.AI_GENERATED == "ai_generated"
67
+ assert ChangeType.HUMAN_EDITED == "human_edited"
68
+
69
+
70
+ def test_get_format_roman():
71
+ f = get_format("roman")
72
+ assert f.format_type == "roman"
73
+ assert len(f.style_constraints) > 0
74
+
75
+
76
+ def test_get_format_unknown():
77
+ with pytest.raises(KeyError) as exc:
78
+ get_format("nonexistent")
79
+ assert "nonexistent" in str(exc.value)
80
+
81
+
82
+ def test_workflow_phase_values():
83
+ assert WorkflowPhase.FIRST_DRAFT == "first_draft"
84
+ assert WorkflowPhase.PRODUCTION == "production"
85
+
86
+
87
+ def test_format_profile_steps_for_phase():
88
+ from authoringfw.formats.base import StepConfig
89
+ f = FormatProfile(
90
+ format_type="test",
91
+ display_name="Test",
92
+ steps=[
93
+ StepConfig(name="s1", phase=WorkflowPhase.OUTLINE, prompt_template_id="t1"),
94
+ StepConfig(name="s2", phase=WorkflowPhase.FIRST_DRAFT, prompt_template_id="t2"),
95
+ ],
96
+ )
97
+ outline_steps = f.steps_for_phase(WorkflowPhase.OUTLINE)
98
+ assert len(outline_steps) == 1
99
+ assert outline_steps[0].name == "s1"