API reference

This page exposes the internal modules through Sphinx autodoc. The public command-line interface is more stable than the Python internals.

Stability notes

  • Stable public API: The CLI commands (booktx init, booktx extract, booktx translate next, etc.) and their JSON output shapes are the primary stable interface.

  • Stable models: Pydantic models in booktx.models (Chunk, Record, TranslationStore, TranslationTask, Manifest) are serialization contracts used by the CLI and external tools.

  • Service modules: booktx.status, booktx.tasks, booktx.submissions, booktx.acceptance, booktx.rendering, booktx.io_utils contain the extracted service logic. Their public functions are stable within a release cycle.

  • Internal helpers: booktx.config, booktx.context, booktx.validate, booktx.build, booktx.chunking, booktx.placeholders are intended stable but may change between minor releases.

  • Legacy: booktx.html_io contains shared XHTML helpers that may be consolidated in future releases.

Project configuration and path resolution for legacy and profile layouts.

booktx now supports two project shapes.

Legacy single-layout projects:

book/
  source/
  .booktx/
    config.toml
    manifest.json
    names.json
    chunks/
    context.json
    context.md
    identity.json
    translation-store.json
    translation-version-ledger.json
    tasks/
    ingest/
    translated/
    reports/
    output/

Profile-layout projects:

book/
  source/
  .booktx/
    source-config.toml
    source-manifest.json
    names.json
    chapter-map.json
    chunks/
  translations/
    <profile>/
      config.toml
      identity.json
      context.json
      context.md
      translation-store.json
      translation-version-ledger.json
      tasks/
      ingest/
      translated/
      reports/
      output/

The shared .booktx/ tree is source-derived state only. Mutable translation state lives under the explicitly resolved profile.

exception booktx.config.BooktxError(code: str, message: str)[source]

User-facing error from booktx. Carries a stable code attribute.

__init__(code: str, message: str) None[source]
class booktx.config.Project(root: Path, source_dir: Path, booktx_dir: Path, translations_dir: Path, source_config_path: Path, config_path: Path, manifest_path: Path, names_path: Path, chunks_dir: Path, chapter_map_path: Path, profile: str | None, profile_dir: Path | None, profile_config_path: Path | None, context_json_path: Path | None, context_md_path: Path | None, identity_json_path: Path | None, store_path: Path | None, ledger_path: Path | None, translated_dir: Path | None, tasks_dir: Path | None, ingest_dir: Path | None, reports_dir: Path | None, output_dir: Path | None, source_config: SourceConfig, profile_config: ProfileConfig | None, config: ProjectConfig, layout_version: Literal['legacy', 'profiles'])[source]

Resolved project paths for either the legacy or profile layout.

root: Path
source_dir: Path
booktx_dir: Path
translations_dir: Path
source_config_path: Path
config_path: Path
manifest_path: Path
names_path: Path
chunks_dir: Path
chapter_map_path: Path
profile: str | None
profile_dir: Path | None
profile_config_path: Path | None
context_json_path: Path | None
context_md_path: Path | None
identity_json_path: Path | None
store_path: Path | None
ledger_path: Path | None
translated_dir: Path | None
tasks_dir: Path | None
ingest_dir: Path | None
reports_dir: Path | None
output_dir: Path | None
source_config: SourceConfig
profile_config: ProfileConfig | None
config: ProjectConfig
layout_version: Literal['legacy', 'profiles']
property source_path: Path
chunks() list[Path][source]
translated() list[Path][source]
chunk_ids() list[str][source]
translated_ids() list[str][source]
__init__(root: Path, source_dir: Path, booktx_dir: Path, translations_dir: Path, source_config_path: Path, config_path: Path, manifest_path: Path, names_path: Path, chunks_dir: Path, chapter_map_path: Path, profile: str | None, profile_dir: Path | None, profile_config_path: Path | None, context_json_path: Path | None, context_md_path: Path | None, identity_json_path: Path | None, store_path: Path | None, ledger_path: Path | None, translated_dir: Path | None, tasks_dir: Path | None, ingest_dir: Path | None, reports_dir: Path | None, output_dir: Path | None, source_config: SourceConfig, profile_config: ProfileConfig | None, config: ProjectConfig, layout_version: Literal['legacy', 'profiles']) None
booktx.config.detect_format(filename: str | Path) str[source]
booktx.config.source_config_path(root_or_project: Project | Path | str) Path[source]
booktx.config.translations_dir(root_or_project: Project | Path | str) Path[source]
booktx.config.profile_dir(root_or_project: Project | Path | str, profile_name: str) Path[source]
booktx.config.profile_config_path(root_or_project: Project | Path | str, profile_name: str) Path[source]
booktx.config.profile_root_marker_path(root_or_project: Project | Path | str, profile_name: str) Path[source]
booktx.config.profile_source_cache_dir(root_or_project: Project | Path | str, profile_name: str) Path[source]
booktx.config.list_profiles(project_or_root: Project | Path | str) list[str][source]
booktx.config.load_profile_config(project_or_root: Project | Path | str, profile_name: str) ProfileConfig[source]
booktx.config.load_profile_root_marker(profile_root: Path | str) ProfileRootMarker[source]
booktx.config.write_profile_config(project_or_root: Project | Path | str, cfg: ProfileConfig) None[source]
booktx.config.write_profile_root_marker(project_or_root: Project | Path | str, profile_name: str, *, profile_config: ProfileConfig | None = None) ProfileRootMarker[source]
booktx.config.resolve_profile_name(project: Project, explicit_profile: str | None, *, require_profile: bool, operation: str = 'command') str | None[source]
booktx.config.load_project(root: Path | str, *, profile: str | None = None, require_profile: bool = False) Project[source]
booktx.config.load_source_project(root: Path | str) Project[source]
booktx.config.load_profile_project(root: Path | str, profile: str) Project[source]
booktx.config.init_source_project(target: Path, *, source_language: str = 'en', source_file: Path | str | None = None, chunk_size: int = 50) Project[source]
booktx.config.init_project(target: Path, *, target_language: str = '', profile_name: str | None = None, source_language: str = 'en', source_file: Path | str | None = None, chunk_size: int = 50) Project[source]
booktx.config.create_profile(root: Path | str, profile_name: str, *, target_language: str, target_locale: str | None = None, actor: str | None = None, harness: str | None = None, model: str | None = None, output_filename: str | None = None, kind: Literal['translation', 'pass-through', 'selection'] = 'translation') Project[source]
booktx.config.migrate_current_project(root: Path | str, profile_name: str, *, target_language: str | None = None, target_locale: str | None = None, actor: str | None = None, harness: str | None = None, model: str | None = None, dry_run: bool = False) dict[str, object][source]
booktx.config.write_manifest(project: Project, manifest: Manifest) None[source]
booktx.config.load_manifest(project: Project) Manifest | None[source]
booktx.config.write_names(project: Project, names: NamesFile) None[source]
booktx.config.load_names(project: Project) NamesFile[source]
booktx.config.protected_terms_sha256(protected_terms: list[str]) str[source]
booktx.config.project_source_sha256(project: Project) str[source]
booktx.config.project_source_id(project: Project) str[source]
booktx.config.project_source_id_or_unavailable(project: Project) str[source]
booktx.config.current_source_sha256(project: Project) str[source]
booktx.config.extracted_source_sha256(project: Project) str[source]
booktx.config.translation_store_path(project: Project) Path[source]
booktx.config.translation_version_ledger_path(project: Project) Path[source]
booktx.config.identity_path(project: Project) Path[source]
booktx.config.load_translation_store(project: Project) TranslationStoreV2[source]
booktx.config.load_translation_version_ledger(project: Project) TranslationVersionLedger[source]
booktx.config.load_identity(project: Project) TranslationIdentity | None[source]
booktx.config.write_translation_store(project: Project, store: TranslationStoreV2) None[source]
booktx.config.write_translation_version_ledger(project: Project, ledger: TranslationVersionLedger) None[source]
booktx.config.write_identity(project: Project, identity: TranslationIdentity) None[source]
booktx.config.translation_task_dir(project: Project) Path[source]
booktx.config.translation_task_path(project: Project, task_id: str) Path[source]
booktx.config.translation_task_source_block_path(project: Project, task_id: str) Path[source]
booktx.config.translation_ingest_dir(project: Project) Path[source]
booktx.config.translation_ingest_path(project: Project, task_id: str) Path[source]
booktx.config.translation_ingest_block_path(project: Project, task_id: str) Path[source]
booktx.config.translation_todo_dir(project: Project) Path[source]

Profile-local directory for durable agent-run todo files.

Returns translations/<profile>/todos/. Raises BooktxError if no profile is selected.

booktx.config.translation_todo_json_path(project: Project, todo_id: str) Path[source]
booktx.config.translation_todo_markdown_path(project: Project, todo_id: str) Path[source]
booktx.config.review_todo_dir(project: Project) Path[source]

Profile-local directory for durable review todo files.

Returns translations/<profile>/review-todos/. Raises BooktxError if no profile is selected.

booktx.config.review_todo_json_path(project: Project, review_todo_id: str) Path[source]
booktx.config.review_todo_markdown_path(project: Project, review_todo_id: str) Path[source]
booktx.config.load_translation_task(project: Project, task_id: str) TranslationTask | None[source]
booktx.config.write_translation_task(project: Project, task: TranslationTask) None[source]
booktx.config.translation_review_dir(project: Project) Path[source]

Profile-local directory for durable review task artifacts.

Returns translations/<profile>/reviews/. Raises BooktxError if no profile is selected.

booktx.config.translation_review_task_path(project: Project, review_task_id: str) Path[source]
booktx.config.translation_review_source_block_path(project: Project, review_task_id: str) Path[source]
booktx.config.translation_review_ingest_block_path(project: Project, review_task_id: str) Path[source]
booktx.config.translation_source_index_path(project: Project) Path[source]

Profile-local generated source-only editor index path.

Returns translations/<profile>/source-index.json. The file is a rebuildable generated artifact, never canonical state.

booktx.config.translation_target_index_path(project: Project) Path[source]

Profile-local generated target-only editor index path.

Returns translations/<profile>/target-index.json. The file is a rebuildable generated artifact, never canonical state.

booktx.config.translation_source_target_index_path(project: Project) Path[source]

Profile-local generated source/target side-by-side editor index path.

Returns translations/<profile>/source-target-index.json. The file is a rebuildable generated artifact, never canonical state.

booktx.config.source_analysis_path(project: Project) Path[source]

Project-root canonical source-analysis JSON evidence path.

Returns .booktx/source-analysis.json. The file is rebuildable generated evidence (authoritative JSON report), never canonical translation state.

booktx.config.source_analysis_markdown_path(project: Project) Path[source]

Project-root generated source-analysis Markdown view path.

Returns .booktx/source-analysis.md. Generated from the canonical JSON report; never authoritative.

booktx.config.source_analysis_decisions_path(project: Project) Path[source]

Project-root durable source-analysis review/provenance sidecar.

booktx.config.profile_source_analysis_path(project: Project) Path[source]

Profile-local generated source-analysis snapshot JSON path.

Returns translations/<profile>/source-analysis.json. The snapshot embeds the same report payload and analysis_sha256 as the canonical project-root report inside a profile-scoped envelope. Rebuildable.

booktx.config.profile_source_analysis_markdown_path(project: Project) Path[source]

Profile-local generated source-analysis snapshot Markdown path.

Returns translations/<profile>/source-analysis.md. Generated from the profile snapshot’s embedded report; never authoritative.

booktx.config.context_sync_ledger_path(project: Project) Path[source]

Profile-local context-sync audit ledger path.

booktx.config.load_context_sync_ledger(project: Project) ContextSyncLedger[source]
booktx.config.write_context_sync_ledger(project: Project, ledger: ContextSyncLedger) None[source]
booktx.config.translation_selection_ledger_path(project: Project) Path[source]

Profile-local selection provenance ledger path.

booktx.config.load_translation_selection_ledger(project: Project) TranslationSelectionLedger[source]
booktx.config.write_translation_selection_ledger(project: Project, ledger: TranslationSelectionLedger) None[source]
booktx.config.canonical_language_key(language: str) str[source]

Return a canonical language shard key such as de or de-DE.

booktx.config.termbase_language_keys(project: Project, language: str | None = None) list[str][source]

Resolve the base-plus-locale language-key sequence for termbase reads.

booktx.config.global_termbase_dir() Path[source]

Directory containing user-global translation termbase shards.

booktx.config.global_termbase_path(language_key: str) Path[source]

Path to one user-global translation termbase shard.

booktx.config.project_termbase_path(project: Project, language_key: str) Path[source]

Path to one project/series termbase shard.

booktx.config.profile_termbase_path(project: Project, language_key: str) Path[source]

Path to one profile-local termbase override shard.

booktx.config.profile_termbase_snapshot_path(project: Project, language_key: str) Path[source]

Path to one frozen termbase snapshot shard for profile-root isolation.

booktx.config.judge_task_dir(project: Project) Path[source]

Profile-local directory for durable judge task artifacts.

booktx.config.judge_task_path(project: Project, judge_task_id: str) Path[source]
booktx.config.judge_task_source_block_path(project: Project, judge_task_id: str) Path[source]
booktx.config.judge_ingest_dir(project: Project) Path[source]

Profile-local directory for editable judge ingest artifacts.

booktx.config.judge_ingest_block_path(project: Project, judge_task_id: str) Path[source]
booktx.config.judge_ingest_decisions_path(project: Project, judge_task_id: str) Path[source]
booktx.config.judge_ingest_json_path(project: Project, judge_task_id: str) Path[source]
booktx.config.load_judge_task(project: Project, judge_task_id: str) JudgeTask | None[source]
booktx.config.write_judge_task(project: Project, task: JudgeTask) None[source]
booktx.config.load_translation_review_task(project: Project, review_task_id: str) TranslationReviewTask | None[source]
booktx.config.write_translation_review_task(project: Project, task: TranslationReviewTask) None[source]
booktx.config.find_source_file(project: Project, *, persist_discovery: bool = True) Path[source]

Resolve the project source file.

By default this persists a newly discovered source file/format back into the source config (a write side effect). Pass persist_discovery=False for read-only lookups such as status overviews and hashing, so a getter no longer mutates project state.

booktx.config.project_storage_root(project: Project) Path[source]

Return the root used for persisted profile-local path strings.

booktx.config.stored_path(project: Project, path: Path) str[source]

Persist a path relative to the resolved profile when available.

booktx.config.resolve_stored_path(project: Project, stored_rel_path: str) Path[source]

Resolve a persisted path against legacy root-relative or profile-relative roots.

Pydantic data models for the booktx translation contract.

This module defines the JSON shapes that cross the boundary between booktx and the translating coding agent:

  • Source chunk -> chunks/NNNN.json (written by booktx extract)

  • Translated chunk -> translated/NNNN.json (written by the agent)

Both must round-trip through JSON with stable field names and ordering.

class booktx.models.Placeholder(*, token: str, original: str, kind: Literal['name', 'tag'] = 'tag')[source]

A non-translatable span that was replaced by a token during extraction.

token is the exact placeholder text that appears in the source field (e.g. __NAME_001__ or __TAG_001__); original is the verbatim text that must be restored verbatim during build.

model_config = {'extra': 'forbid'}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

token: str
original: str
kind: Literal['name', 'tag']
class booktx.models.Record(*, id: str, source: str, protected_terms: list[str] = <factory>, placeholders: list[Placeholder] = <factory>, span_index: int | None = None, span_record_index: int | None = None, source_markup: Literal['plain:v1', 'epub-inline-xhtml: v1'] = 'plain:v1')[source]

A single translatable source record inside a source chunk.

model_config = {'extra': 'forbid'}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

id: str
source: str
protected_terms: list[str]
placeholders: list[Placeholder]
span_index: int | None
span_record_index: int | None
source_markup: Literal['plain:v1', 'epub-inline-xhtml:v1']
class booktx.models.TranslatedRecord(*, id: str, version: str | None = None, target: str)[source]

A single translated record inside a translated chunk.

model_config = {'extra': 'forbid'}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

id: str
version: str | None
target: str
class booktx.models.Chunk(*, schema_version: int = 2, chunk_id: str, chunk_size: Annotated[int, ~annotated_types.Ge(ge=1)] = 50, record_id_scheme: str = 'chunk-local:v1', source_language: str, target_language: str = '', records: list[Record] = <factory>)[source]

A source chunk file (chunks/NNNN.json).

model_config = {'extra': 'forbid'}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

schema_version: int
chunk_id: str
chunk_size: int
record_id_scheme: str
source_language: str
target_language: str
records: list[Record]
class booktx.models.TranslatedChunk(*, chunk_id: str, records: list[TranslatedRecord] = <factory>, **extra_data: Any)[source]

A translated chunk file (translated/NNNN.json).

model_config = {'extra': 'allow'}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

chunk_id: str
records: list[TranslatedRecord]
class booktx.models.StoredTranslationRecord(*, chunk_id: str, source_sha256: str = '', target: str, status: Literal['accepted'] = 'accepted', updated_at: str = '')[source]

One accepted record stored in translation-store.json.

model_config = {'extra': 'forbid'}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

chunk_id: str
source_sha256: str
target: str
status: Literal['accepted']
updated_at: str
class booktx.models.TranslationStore(*, version: int = 1, source_sha256: str = '', records: dict[str, ~booktx.models.StoredTranslationRecord]=<factory>)[source]

Primary record-level translation state owned by booktx.

model_config = {'extra': 'forbid'}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

version: int
source_sha256: str
records: dict[str, StoredTranslationRecord]
class booktx.models.TranslationCandidate(*, version: int, subversion: int, version_ref: str, baseline_ref: str | None = None, baseline_sha256: str | None = None, context_view_sha256: str | None = None, context_view_path: str | None = None, context_notes_scope: str | None = None, context_target_chapter_id: str | None = None, context_notes_through_chapter_id: str | None = None, target: str, status: str = 'accepted', created_at: str, updated_at: str, reviewed_at: str | None = None, reviewed_by: str | None = None, review_note: str | None = None)[source]

One candidate translation stored under a record version.

model_config = {'extra': 'forbid'}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

version: int
subversion: int
version_ref: str
baseline_ref: str | None
baseline_sha256: str | None
context_view_sha256: str | None
context_view_path: str | None
context_notes_scope: str | None
context_target_chapter_id: str | None
context_notes_through_chapter_id: str | None
target: str
status: str
created_at: str
updated_at: str
reviewed_at: str | None
reviewed_by: str | None
review_note: str | None
class booktx.models.StoredTranslationRecordV2(*, chunk_id: int, part_id: int, source_sha256: str, source: str, active_version: str | None = None, active_review: str | None = None, versions: list[TranslationCandidate] = <factory>, reviews: list[TranslationReviewCandidate] = <factory>)[source]

One source record with nested versioned translation candidates.

model_config = {'extra': 'forbid'}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

chunk_id: int
part_id: int
source_sha256: str
source: str
active_version: str | None
active_review: str | None
versions: list[TranslationCandidate]
reviews: list[TranslationReviewCandidate]
class booktx.models.TranslationStoreV2(*, version: Literal[2] = 2, source_sha256: str = '', records: dict[str, ~booktx.models.StoredTranslationRecordV2]=<factory>)[source]

Primary nested translation state for booktx.

model_config = {'extra': 'forbid'}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

version: Literal[2]
source_sha256: str
records: dict[str, StoredTranslationRecordV2]
class booktx.models.TranslationSubversionLedgerEntry(*, version: int, subversion: int, version_ref: str, context_sha256: str, context_path: str = '.booktx/context.json', baseline_sha256: str | None = None, baseline_path: str | None = None, legacy_full_context_sha256: str | None = None, legacy_full_context_path: str | None = None, context_label: str | None = None, created_at: str, updated_at: str, notes: str | None = None, forced: bool = False)[source]

One context-scoped subversion entry inside a major track.

model_config = {'extra': 'forbid'}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

version: int
subversion: int
version_ref: str
context_sha256: str
context_path: str
baseline_sha256: str | None
baseline_path: str | None
legacy_full_context_sha256: str | None
legacy_full_context_path: str | None
context_label: str | None
created_at: str
updated_at: str
notes: str | None
forced: bool
class booktx.models.TranslationTrackLedgerEntry(*, version: int, actor: str, harness: str, model: str, label: str | None = None, created_at: str, updated_at: str, subversions: dict[str, ~booktx.models.TranslationSubversionLedgerEntry]=<factory>)[source]

One major version track storing stable identity and subversions.

model_config = {'extra': 'forbid'}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

version: int
actor: str
harness: str
model: str
label: str | None
created_at: str
updated_at: str
subversions: dict[str, TranslationSubversionLedgerEntry]
class booktx.models.TranslationVersionLedger(*, version: Literal[1] = 1, source_sha256: str = '', active_version: str | None = None, tracks: dict[str, ~booktx.models.TranslationTrackLedgerEntry]=<factory>)[source]

Project-wide ledger assigning meaning to translation versions.

model_config = {'extra': 'forbid'}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

version: Literal[1]
source_sha256: str
active_version: str | None
tracks: dict[str, TranslationTrackLedgerEntry]
class booktx.models.TranslationIdentity(*, actor: str, harness: str, model: str)[source]

Stored defaults for new translation-version work.

model_config = {'extra': 'forbid'}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

actor: str
harness: str
model: str
class booktx.models.ApplicableTermbaseExampleSnapshot(*, source: str, good_target: str = '', bad_target: str = '', note: str = '')[source]

One compact termbase example persisted with a task snapshot.

model_config = {'extra': 'forbid'}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

source: str
good_target: str
bad_target: str
note: str
class booktx.models.ApplicableTermbaseUsageRuleSnapshot(*, rule_id: str, context_id: str, source_cue: str | None = None, source_regex: str | None = None, required_target_literals: list[str] = <factory>, required_target_regexes: list[str] = <factory>, allowed_target_literals: list[str] = <factory>, allowed_target_regexes: list[str] = <factory>, forbidden_target_literals: list[str] = <factory>, forbidden_target_regexes: list[str] = <factory>, severity: Literal['info', 'warn', 'error']='warn', prompt: str = '', fallback: bool = False)[source]

One compact termbase usage rule persisted with a task snapshot.

model_config = {'extra': 'forbid'}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

rule_id: str
context_id: str
source_cue: str | None
source_regex: str | None
required_target_literals: list[str]
required_target_regexes: list[str]
allowed_target_literals: list[str]
allowed_target_regexes: list[str]
forbidden_target_literals: list[str]
forbidden_target_regexes: list[str]
severity: Literal['info', 'warn', 'error']
prompt: str
fallback: bool
class booktx.models.ApplicableTermbaseEntrySnapshot(*, entry_id: str, kind: ~typing.Literal['flat_term', 'contextual_term', 'phrase_preference', 'collocation_preference', 'word_sense', 'style_preference', 'forbidden_literalism', 'world_term'] = 'phrase_preference', source: str, source_variants: list[str] = <factory>, source_regex: str | None = None, source_language: str = 'en', case_sensitive: bool = False, matched_source_cue: str, matched_source_span: tuple[int, int], target_preferred: list[str] = <factory>, target_allowed: list[str] = <factory>, target_forbidden: list[str] = <factory>, target_regex_forbidden: list[str] = <factory>, preferred_policy: ~typing.Literal['off', 'advisory', 'required'] = 'off', severity: ~typing.Literal['info', 'warn', 'error'] = 'warn', sense: str = '', rationale: str = '', examples: list[~booktx.models.ApplicableTermbaseExampleSnapshot] = <factory>, usage_rules: list[~booktx.models.ApplicableTermbaseUsageRuleSnapshot] = <factory>)[source]

One applicable termbase entry snapshot persisted on a task record.

model_config = {'extra': 'forbid'}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

entry_id: str
kind: Literal['flat_term', 'contextual_term', 'phrase_preference', 'collocation_preference', 'word_sense', 'style_preference', 'forbidden_literalism', 'world_term']
source: str
source_variants: list[str]
source_regex: str | None
source_language: str
case_sensitive: bool
matched_source_cue: str
matched_source_span: tuple[int, int]
target_preferred: list[str]
target_allowed: list[str]
target_forbidden: list[str]
target_regex_forbidden: list[str]
preferred_policy: Literal['off', 'advisory', 'required']
severity: Literal['info', 'warn', 'error']
sense: str
rationale: str
examples: list[ApplicableTermbaseExampleSnapshot]
usage_rules: list[ApplicableTermbaseUsageRuleSnapshot]
class booktx.models.ApplicableTermbaseFindingSnapshot(*, entry_id: str, rule_id: str = '', context_id: str = '', status: ~typing.Literal['forbidden_target', 'preferred_missing', 'clean'], rule_code: str, severity: ~typing.Literal['info', 'warn', 'error'], reason: str = '', target_forbidden_found: list[str] = <factory>, target_required_found: list[str] = <factory>, target_allowed_found: list[str] = <factory>, effective_candidate_ref: str = '', prompt: str = '', fallback: bool = False)[source]

One deterministic termbase audit finding persisted on a review task record.

model_config = {'extra': 'forbid'}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

entry_id: str
rule_id: str
context_id: str
status: Literal['forbidden_target', 'preferred_missing', 'clean']
rule_code: str
severity: Literal['info', 'warn', 'error']
reason: str
target_forbidden_found: list[str]
target_required_found: list[str]
target_allowed_found: list[str]
effective_candidate_ref: str
prompt: str
fallback: bool
class booktx.models.TranslationTaskRecord(*, id: str, chunk_id: str, source: str, protected_terms: list[str] = <factory>, placeholders: list[Placeholder] = <factory>, applicable_termbase: list[ApplicableTermbaseEntrySnapshot] = <factory>)[source]

A record assigned to a CLI-created translation task.

model_config = {'extra': 'forbid'}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

id: str
chunk_id: str
source: str
protected_terms: list[str]
placeholders: list[Placeholder]
applicable_termbase: list[ApplicableTermbaseEntrySnapshot]
class booktx.models.TranslationTask(*, version: int = 1, task_id: str, unit: ~typing.Literal['paragraph', 'batch', 'chunk', 'chapter'], chapter_id: str = '', chapter_title: str = '', profile: str = '', source_language: str, target_language: str, target_locale: str = '', translation_version: str | None = None, baseline_ref: str | None = None, baseline_sha256: str | None = None, context_sha256: str | None = None, context_view_sha256: str | None = None, context_view_path: str | None = None, mandatory_glossary_sha256: str | None = None, applicable_termbase_sha256: str | None = None, context_notes_scope: str | None = None, context_target_chapter_id: str | None = None, context_notes_through_chapter_id: str | None = None, source_sha256: str | None = None, profile_config_sha256: str | None = None, source_config_sha256: str | None = None, source_words: int = 0, record_count: int = 0, requested_max_words: int | None = None, todo_id: str | None = None, records: list[~booktx.models.TranslationTaskRecord] = <factory>)[source]

A persisted work item returned by booktx translate next.

model_config = {'extra': 'forbid'}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

version: int
task_id: str
unit: Literal['paragraph', 'batch', 'chunk', 'chapter']
chapter_id: str
chapter_title: str
profile: str
source_language: str
target_language: str
target_locale: str
translation_version: str | None
baseline_ref: str | None
baseline_sha256: str | None
context_sha256: str | None
context_view_sha256: str | None
context_view_path: str | None
mandatory_glossary_sha256: str | None
applicable_termbase_sha256: str | None
context_notes_scope: str | None
context_target_chapter_id: str | None
context_notes_through_chapter_id: str | None
source_sha256: str | None
profile_config_sha256: str | None
source_config_sha256: str | None
source_words: int
record_count: int
requested_max_words: int | None
todo_id: str | None
records: list[TranslationTaskRecord]
class booktx.models.StatusTotals(*, source_words: int = 0, translated_words: int = 0, remaining_words: int = 0, records_total: int = 0, records_translated: int = 0, records_remaining: int = 0, chunks_total: int = 0, chunks_complete: int = 0, chunks_partial: int = 0, chunks_pending: int = 0, chapters_total: int = 0, chapters_complete: int = 0, chapters_partial: int = 0, chapters_pending: int = 0, invalid_translation_files: int = 0, stale_translation_files: int = 0)[source]

Aggregate translation coverage totals.

Used by status --json and persisted in TranslationTodo.start_totals. Keep this model in models.py so durable todo validation never depends on a late Pydantic forward-reference rebuild.

model_config = {'extra': 'forbid'}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

source_words: int
translated_words: int
remaining_words: int
records_total: int
records_translated: int
records_remaining: int
chunks_total: int
chunks_complete: int
chunks_partial: int
chunks_pending: int
chapters_total: int
chapters_complete: int
chapters_partial: int
chapters_pending: int
invalid_translation_files: int
stale_translation_files: int
class booktx.models.TranslationTodoChapter(*, chapter_id: str, title: str = '', status: str, records_total: int, records_translated_at_start: int, records_remaining_at_start: int, source_words_remaining_at_start: int, pending_chunk_ids: list[str] = <factory>)[source]

One chapter selected for a bounded agent translation run.

Carries only coverage/state at the moment the todo was created; it never embeds source text. Source text belongs in per-task *.source.block.txt.

model_config = {'extra': 'forbid'}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

chapter_id: str
title: str
status: str
records_total: int
records_translated_at_start: int
records_remaining_at_start: int
source_words_remaining_at_start: int
pending_chunk_ids: list[str]
class booktx.models.TranslationTodo(*, version: Literal[1] = 1, todo_id: str, profile: str, target_language: str, target_locale: str = '', chapters_requested: int, batch_words: int, max_run_words: int | None = None, include_current: bool = True, created_at: str, baseline_ref: str | None = None, baseline_sha256: str | None = None, context_sha256: str | None = None, mandatory_glossary_sha256: str | None = None, source_sha256: str | None = None, start_totals: StatusTotals, chapters: list[TranslationTodoChapter] = <factory>)[source]

A durable run-control artifact for a bounded multi-chapter agent run.

Written by booktx translate todo-next under translations/<profile>/todos/<todo_id>.{json,md}. This is NOT a translation submission: it tells the agent how many chapters to complete, the per-task word budget, and the stop conditions.

model_config = {'extra': 'forbid'}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

version: Literal[1]
todo_id: str
profile: str
target_language: str
target_locale: str
chapters_requested: int
batch_words: int
max_run_words: int | None
include_current: bool
created_at: str
baseline_ref: str | None
baseline_sha256: str | None
context_sha256: str | None
mandatory_glossary_sha256: str | None
source_sha256: str | None
start_totals: StatusTotals
chapters: list[TranslationTodoChapter]
class booktx.models.NamesFile(*, protected_terms: list[str] = <factory>)[source]

The .booktx/names.json file holding manually protected terms.

model_config = {'extra': 'forbid'}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

protected_terms: list[str]
class booktx.models.SourceAnalysisPatternsConfig(*, world_morphemes: list[str] = <factory>, include_regex: list[str] = <factory>, exclude_regex: list[str] = <factory>)[source]

Optional source-analysis pattern configuration.

Stored under [source_analysis.patterns] in .booktx/source-config.toml.

model_config = {'extra': 'forbid'}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

world_morphemes: list[str]
include_regex: list[str]
exclude_regex: list[str]
class booktx.models.SourceAnalysisGenericLemmasConfig(*, extra: list[str] = <factory>)[source]

Optional project-local generic-lemma additions for source analysis.

model_config = {'extra': 'forbid'}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

extra: list[str]
class booktx.models.SourceAnalysisConfig(*, include_singletons: bool = True, max_per_bucket: Annotated[int, ~annotated_types.Ge(ge=1)] = 80, min_risk_score: float = 3.0, patterns: SourceAnalysisPatternsConfig = <factory>, generic_lemmas: SourceAnalysisGenericLemmasConfig = <factory>)[source]

Optional translation-risk mining configuration for source analysis.

model_config = {'extra': 'forbid'}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

include_singletons: bool
max_per_bucket: int
min_risk_score: float
patterns: SourceAnalysisPatternsConfig
generic_lemmas: SourceAnalysisGenericLemmasConfig
class booktx.models.SourceConfig(*, version: Literal[1] = 1, source_language: str = 'en', source_file: str = '', format: str = 'markdown', chunk_size: Annotated[int, Ge(ge=1)] = 50, source_analysis: SourceAnalysisConfig | None = None)[source]

Source-only config stored in .booktx/source-config.toml.

model_config = {'extra': 'forbid'}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

version: Literal[1]
source_language: str
source_file: str
format: str
chunk_size: int
source_analysis: SourceAnalysisConfig | None
class booktx.models.ProfileIdentityConfig(*, actor: str = 'user:unknown', harness: str = 'booktx', model: str = 'human')[source]

Initial identity defaults captured when a profile is created.

This is not the live identity. After creation, the authoritative identity lives in translations/<profile>/identity.json and is updated by booktx model set / actor set / harness set. Profile list and show render the resolved identity.json value, not this field.

model_config = {'extra': 'forbid'}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

actor: str
harness: str
model: str
class booktx.models.ProfileConfig(*, version: Literal[1] = 1, kind: Literal['translation', 'pass-through', 'selection']='translation', profile: str, source_language: str = 'en', target_language: str, target_locale: str | None = None, output_filename: str | None = None, identity: ProfileIdentityConfig = <factory>, selection: SelectionConfig | None = None, quality_review: QualityReviewConfig | None = None, indexes: IndexesConfig | None = None, epub_output: EpubOutputConfig | None = None)[source]

Per-profile translation config stored in translations/<profile>/config.toml.

model_config = {'extra': 'forbid'}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

version: Literal[1]
kind: Literal['translation', 'pass-through', 'selection']
profile: str
source_language: str
target_language: str
target_locale: str | None
output_filename: str | None
identity: ProfileIdentityConfig

Initial identity defaults; the live identity is identity.json (see above).

selection: SelectionConfig | None
quality_review: QualityReviewConfig | None
indexes: IndexesConfig | None
epub_output: EpubOutputConfig | None
class booktx.models.ProfileRootMarker(*, schema: Literal['booktx.profile-root.v1'] = 'booktx.profile-root.v1', profile: str, source_id: str, target_language: str, target_locale: str, mode_hint: Literal['profile-root'] = 'profile-root')[source]

Profile-root marker for isolated runtime resolution.

model_config = {'extra': 'forbid', 'populate_by_name': True, 'validate_by_alias': True, 'validate_by_name': True}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

schema_name: Literal['booktx.profile-root.v1']
profile: str
source_id: str
target_language: str
target_locale: str
mode_hint: Literal['profile-root']
class booktx.models.ProjectConfig(*, source_language: str = 'en', target_language: str = '', target_locale: str | None = None, output_filename: str | None = None, source_file: str = '', format: str = 'markdown', chunk_size: Annotated[int, Ge(ge=1)] = 50, epub_output: EpubOutputConfig | None = None)[source]

Effective runtime config, or the legacy .booktx/config.toml shape.

The TOML file mirrors these field names exactly so it stays human-editable.

model_config = {'extra': 'forbid'}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

source_language: str
target_language: str
target_locale: str | None
output_filename: str | None
source_file: str
format: str
chunk_size: int
epub_output: EpubOutputConfig | None
class booktx.models.EpubSpanRef(*, span_index: int, block_id: str, document_href: str, spine_index: int | None = None, tag_name: str, source_text: str, source_text_sha256: str, source_char_start: int | None = None, source_char_end: int | None = None, placeholders: list[Placeholder] = <factory>, protected_terms: list[str] = <factory>, source_view_text: str = '', source_view_sha256: str = '', source_markup: str = 'plain:v1', inline_skeleton: list[dict[str, ~typing.Any]]=<factory>, chapter_id: str | None = None, chapter_title: str | None = None)[source]

Stored span-to-EPUB-block mapping for the migrated EPUB pipeline.

model_config = {'extra': 'forbid'}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

span_index: int
block_id: str
document_href: str
spine_index: int | None
tag_name: str
source_text: str
source_text_sha256: str
source_char_start: int | None
source_char_end: int | None
placeholders: list[Placeholder]
protected_terms: list[str]
source_view_text: str
source_view_sha256: str
source_markup: str
inline_skeleton: list[dict[str, Any]]
chapter_id: str | None
chapter_title: str | None
class booktx.models.EpubNavigationRef(*, id: str, title: str, href: str | None = None, document_href: str | None = None, fragment: str | None = None, spine_index: int | None = None, source_char_start: int | None = None, source_byte_start: int | None = None, level: int = 1, parent_id: str | None = None, order: int = 0, children: list[str] = <factory>, source: str = 'nav')[source]

Stored navigation metadata for EPUB chapter detection.

model_config = {'extra': 'forbid'}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

id: str
title: str
href: str | None
document_href: str | None
fragment: str | None
spine_index: int | None
source_char_start: int | None
source_byte_start: int | None
level: int
parent_id: str | None
order: int
children: list[str]
source: str
class booktx.models.EpubTemplateData(*, pipeline: str, epub2text_schema: str, text2epub_manifest: dict[str, ~typing.Any]=<factory>, spans: list[EpubSpanRef] = <factory>, navigation: list[EpubNavigationRef] = <factory>, chapter_mapping: Literal['legacy', 'epub2text-block-v1']='legacy')[source]

Typed payload stored in Manifest.template for EPUB v2 projects.

model_config = {'extra': 'forbid'}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

pipeline: str
epub2text_schema: str
text2epub_manifest: dict[str, Any]
spans: list[EpubSpanRef]
navigation: list[EpubNavigationRef]
chapter_mapping: Literal['legacy', 'epub2text-block-v1']
class booktx.models.ManifestSource(*, filename: str, format: str, source_language: str, target_language: str = '', sha256: str = '')[source]

Metadata about the source document recorded in the manifest.

model_config = {'extra': 'forbid'}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

filename: str
format: str
source_language: str
target_language: str
sha256: str
class booktx.models.Manifest(*, version: int = 1, source: ManifestSource, chunk_count: int = 0, record_count: int = 0, chunk_size: Annotated[int, ~annotated_types.Ge(ge=1)] = 50, record_id_scheme: str = 'chunk-local:v1', segmenter: dict[str, ~typing.Any]=<factory>, names_sha256: str = '', template: dict[str, ~typing.Any]=<factory>)[source]

The .booktx/manifest.json content.

model_config = {'extra': 'forbid'}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

version: int
source: ManifestSource
chunk_count: int
record_count: int
chunk_size: int
record_id_scheme: str
segmenter: dict[str, Any]
names_sha256: str
template: dict[str, Any]
class booktx.models.TranslationReviewCandidate(*, pass_number: Annotated[int, Ge(ge=1)], run_number: Annotated[int, Ge(ge=1)], review_ref: str, base_kind: Literal['translation', 'review'], base_ref: str, base_target_sha256: str, target: str, target_sha256: str, status: Literal['accepted', 'rejected', 'superseded'] = 'accepted', created_at: str, updated_at: str, reviewed_by: str | None = None, review_model: str | None = None, review_task_id: str | None = None, review_note: str | None = None, context_view_sha256: str | None = None, context_view_path: str | None = None, review_window_sha256: str | None = None, review_policy_sha256: str | None = None)[source]

One reviewed target stored separately from translation versions.

A review candidate records one quality-improved output of a numbered review pass (R<pass>.<run>). It is derived from a base candidate, which is either a translation version (base_kind="translation") or an earlier review candidate (base_kind="review"). Review candidates never overwrite translation versions.

model_config = {'extra': 'forbid'}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

pass_number: int
run_number: int
review_ref: str
base_kind: Literal['translation', 'review']
base_ref: str
base_target_sha256: str
target: str
target_sha256: str
status: Literal['accepted', 'rejected', 'superseded']
created_at: str
updated_at: str
reviewed_by: str | None
review_model: str | None
review_task_id: str | None
review_note: str | None
context_view_sha256: str | None
context_view_path: str | None
review_window_sha256: str | None
review_policy_sha256: str | None
class booktx.models.ReviewContextRecord(*, id: str, chunk_id: str, source: str, effective_target: str | None = None, effective_ref: str | None = None, role: Literal['before', 'selected', 'after'])[source]

One neighbor or selected record in a review task context window.

model_config = {'extra': 'forbid'}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

id: str
chunk_id: str
source: str
effective_target: str | None
effective_ref: str | None
role: Literal['before', 'selected', 'after']
class booktx.models.TranslationReviewTaskRecord(*, id: str, chunk_id: str, source: str, base_kind: ~typing.Literal['translation', 'review'], base_ref: str, base_target: str, base_target_sha256: str, review_ref: str, pass_number: int, review_window_sha256: str, before: list[~booktx.models.ReviewContextRecord] = <factory>, after: list[~booktx.models.ReviewContextRecord] = <factory>, applicable_termbase: list[~booktx.models.ApplicableTermbaseEntrySnapshot] = <factory>, termbase_findings: list[~booktx.models.ApplicableTermbaseFindingSnapshot] = <factory>)[source]

One selected record in a review task.

Carries the base candidate being reviewed plus the review_ref this task will create, so a single task can safely include records whose next run number differs.

model_config = {'extra': 'forbid'}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

id: str
chunk_id: str
source: str
base_kind: Literal['translation', 'review']
base_ref: str
base_target: str
base_target_sha256: str
review_ref: str
pass_number: int
review_window_sha256: str
before: list[ReviewContextRecord]
after: list[ReviewContextRecord]
applicable_termbase: list[ApplicableTermbaseEntrySnapshot]
termbase_findings: list[ApplicableTermbaseFindingSnapshot]
class booktx.models.TranslationReviewTask(*, version: Literal[1] = 1, review_task_id: str, profile: str, chapter_id: str, chapter_title: str = '', pass_number: int, pass_name: str = '', pass_instructions: str = '', source_language: str, target_language: str, target_locale: str = '', context_view_sha256: str | None = None, context_view_path: str | None = None, source_sha256: str | None = None, profile_config_sha256: str | None = None, source_config_sha256: str | None = None, review_policy_sha256: str | None = None, mandatory_glossary_sha256: str | None = None, applicable_termbase_sha256: str | None = None, before_records: int, after_records: int, source_words: int, record_count: int, created_at: str, records: list[TranslationReviewTaskRecord] = <factory>)[source]

A persisted review work item returned by booktx review next.

model_config = {'extra': 'forbid'}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

version: Literal[1]
review_task_id: str
profile: str
chapter_id: str
chapter_title: str
pass_number: int
pass_name: str
pass_instructions: str
source_language: str
target_language: str
target_locale: str
context_view_sha256: str | None
context_view_path: str | None
source_sha256: str | None
profile_config_sha256: str | None
source_config_sha256: str | None
review_policy_sha256: str | None
mandatory_glossary_sha256: str | None
applicable_termbase_sha256: str | None
before_records: int
after_records: int
source_words: int
record_count: int
created_at: str
records: list[TranslationReviewTaskRecord]
class booktx.models.ReviewPassConfig(*, pass_number: Annotated[int, Ge(ge=1)], name: str = '', enabled: bool = True, mode: Literal['manual', 'after_chapter', 'before_build'] = 'manual', enforce: Literal['off', 'warn', 'error'] = 'off', base: Literal['active_translation', 'active_review'] = 'active_translation', required_base_pass: int | None = None, before_records: Annotated[int, Ge(ge=0), Le(le=20)] = 2, after_records: Annotated[int, Ge(ge=0), Le(le=20)] = 2, batch_words: Annotated[int, Ge(ge=1)] = 900, instructions: str = '', include_untranslated_neighbors: bool = False)[source]

Configuration for one numbered review pass.

model_config = {'extra': 'forbid'}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

pass_number: int
name: str
enabled: bool
mode: Literal['manual', 'after_chapter', 'before_build']
enforce: Literal['off', 'warn', 'error']
base: Literal['active_translation', 'active_review']
required_base_pass: int | None
before_records: int
after_records: int
batch_words: int
instructions: str
include_untranslated_neighbors: bool
class booktx.models.ReviewTodoPass(*, pass_number: Annotated[int, Ge(ge=1)], selection: Literal['missing', 'stale', 'reviewed', 'all', 'changed-base'] = 'missing', base: str = 'active_translation')[source]

One review pass selected for a durable review todo.

Carries the pass_number, selection mode, and base mode that will be used when this pass is resumed via review todo-resume.

model_config = {'extra': 'forbid'}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

pass_number: int
selection: Literal['missing', 'stale', 'reviewed', 'all', 'changed-base']
base: str
class booktx.models.ReviewTodoChapter(*, chapter_id: str, title: str = '', status: str, eligible_records_at_start: int = 0, missing_review_at_start: int = 0, stale_review_at_start: int = 0, pending_passes: list[int] = <factory>)[source]

One chapter tracked by a durable review todo.

Captures review coverage at the moment the todo was created so callers can detect progress and drift without re-reading the entire store.

model_config = {'extra': 'forbid'}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

chapter_id: str
title: str
status: str
eligible_records_at_start: int
missing_review_at_start: int
stale_review_at_start: int
pending_passes: list[int]
class booktx.models.ReviewTodo(*, version: Literal[1] = 1, review_todo_id: str, profile: str, passes: list[ReviewTodoPass] = <factory>, chapters_requested: int, batch_words: int, created_at: str, source_sha256: str | None = None, profile_config_sha256: str | None = None, source_config_sha256: str | None = None, start_snapshot_sha256: str | None = None, chapters: list[ReviewTodoChapter] = <factory>)[source]

A durable run-control artifact for a bounded multi-pass review run.

Written by booktx review todo-next under translations/<profile>/review-todos/<review_todo_id>.{json,md}. This tells the agent which review passes and chapters to process, the per-pass selection and base modes, and the stop conditions.

model_config = {'extra': 'forbid'}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

version: Literal[1]
review_todo_id: str
profile: str
passes: list[ReviewTodoPass]
chapters_requested: int
batch_words: int
created_at: str
source_sha256: str | None
profile_config_sha256: str | None
source_config_sha256: str | None
start_snapshot_sha256: str | None
chapters: list[ReviewTodoChapter]
class booktx.models.SelectionConfig(*, sources: list[str] = <factory>, allow_edited_targets: bool = True, require_all_sources: bool = False, purpose: Literal['compare', 'revise']='compare')[source]

Selection-profile configuration stored under [selection].

model_config = {'extra': 'forbid'}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

sources: list[str]
allow_edited_targets: bool
require_all_sources: bool
purpose: Literal['compare', 'revise']
class booktx.models.ContextSyncLedgerFinding(*, section: str = '', key: str = '', action: str = '', message: str = '')[source]

One persisted finding for a context-sync ledger entry.

model_config = {'extra': 'forbid'}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

section: str
key: str
action: str
message: str
class booktx.models.ContextSyncLedgerEntry(*, sync_id: str, source_profile: str, source_context_sha256: str = '', source_baseline_sha256: str = '', pack_sha256: str = '', sections: list[str] = <factory>, terms: list[str] = <factory>, question_ids: list[str] = <factory>, conflict: Literal['fail', 'keep-local', 'replace']='fail', old_baseline_sha256: str = '', new_baseline_sha256: str = '', changed: bool = False, applied_at: str, findings: list[ContextSyncLedgerFinding] = <factory>)[source]

One applied multi-profile context-sync event for a target profile.

model_config = {'extra': 'forbid'}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

sync_id: str
source_profile: str
source_context_sha256: str
source_baseline_sha256: str
pack_sha256: str
sections: list[str]
terms: list[str]
question_ids: list[str]
conflict: Literal['fail', 'keep-local', 'replace']
old_baseline_sha256: str
new_baseline_sha256: str
changed: bool
applied_at: str
findings: list[ContextSyncLedgerFinding]
class booktx.models.ContextSyncLedger(*, version: Literal[1] = 1, profile: str, entries: list[ContextSyncLedgerEntry] = <factory>)[source]

Profile-local audit log for applied context sync operations.

model_config = {'extra': 'forbid'}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

version: Literal[1]
profile: str
entries: list[ContextSyncLedgerEntry]
class booktx.models.JudgeTaskFinding(*, severity: Literal['info', 'warn', 'error'] = 'info', rule: str = '', message: str = '')[source]

Compact validation finding stored in judge task artifacts.

model_config = {'extra': 'forbid'}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

severity: Literal['info', 'warn', 'error']
rule: str
message: str
class booktx.models.JudgeTaskCandidate(*, label: str, profile: str, target_language: str, target_locale: str = '', selected_kind: ~typing.Literal['translation', 'review'], selected_ref: str, version_ref: str | None = None, review_ref: str | None = None, target: str, target_sha256: str, validation_findings: list[~booktx.models.JudgeTaskFinding] = <factory>)[source]

One candidate translation shown to the judge for a record.

model_config = {'extra': 'forbid'}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

label: str
profile: str
target_language: str
target_locale: str
selected_kind: Literal['translation', 'review']
selected_ref: str
version_ref: str | None
review_ref: str | None
target: str
target_sha256: str
validation_findings: list[JudgeTaskFinding]
class booktx.models.JudgeTaskRecord(*, id: str, chunk_id: str, source: str, source_sha256: str, applicable_termbase: list[ApplicableTermbaseEntrySnapshot] = <factory>, applicable_glossary: list[ApplicableGlossaryEntrySnapshot] = <factory>, candidates: list[JudgeTaskCandidate] = <factory>, missing_profiles: list[str] = <factory>, output_version_ref: str)[source]

One source record plus all judge candidates.

model_config = {'extra': 'forbid'}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

id: str
chunk_id: str
source: str
source_sha256: str
applicable_termbase: list[ApplicableTermbaseEntrySnapshot]
applicable_glossary: list[ApplicableGlossaryEntrySnapshot]
candidates: list[JudgeTaskCandidate]
missing_profiles: list[str]
output_version_ref: str
class booktx.models.JudgeTask(*, version: Literal[1] = 1, judge_task_id: str, profile: str, source_profiles: list[str] = <factory>, source_language: str, target_language: str, target_locale: str = '', chapter_id: str = '', chapter_title: str = '', created_at: str, source_sha256: str, profile_config_sha256: str | None = None, source_config_sha256: str | None = None, context_view_sha256: str | None = None, context_view_path: str | None = None, mandatory_glossary_sha256: str | None = None, applicable_termbase_sha256: str | None = None, source_access: Literal['live', 'snapshot']='live', source_snapshot_sha256: str | None = None, source_snapshot_path: str | None = None, source_candidates_sha256: str | None = None, selection_purpose: Literal['compare', 'revise']='compare', records: list[JudgeTaskRecord] = <factory>)[source]

Durable task artifact for a cross-profile judge run.

model_config = {'extra': 'forbid'}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

version: Literal[1]
judge_task_id: str
profile: str
source_profiles: list[str]
source_language: str
target_language: str
target_locale: str
chapter_id: str
chapter_title: str
created_at: str
source_sha256: str
profile_config_sha256: str | None
source_config_sha256: str | None
context_view_sha256: str | None
context_view_path: str | None
mandatory_glossary_sha256: str | None
applicable_termbase_sha256: str | None
source_access: Literal['live', 'snapshot']
source_snapshot_sha256: str | None
source_snapshot_path: str | None
source_candidates_sha256: str | None
selection_purpose: Literal['compare', 'revise']
records: list[JudgeTaskRecord]
class booktx.models.JudgeCandidateEvidence(*, label: str, profile: str, selected_kind: ~typing.Literal['translation', 'review'], selected_ref: str, version_ref: str | None = None, review_ref: str | None = None, target_sha256: str, validation_status: ~typing.Literal['ok', 'warning', 'error', 'missing'] = 'ok', findings: list[str] = <factory>)[source]

Persisted provenance for one candidate considered by the judge.

model_config = {'extra': 'forbid'}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

label: str
profile: str
selected_kind: Literal['translation', 'review']
selected_ref: str
version_ref: str | None
review_ref: str | None
target_sha256: str
validation_status: Literal['ok', 'warning', 'error', 'missing']
findings: list[str]
class booktx.models.JudgeDecision(*, record_id: str, output_version_ref: str, output_target_sha256: str | None = None, decision_kind: ~typing.Literal['copy', 'edited'], selected_profile: str | None = None, selected_kind: ~typing.Literal['translation', 'review'] | None = None, selected_ref: str | None = None, selected_target_sha256: str | None = None, judge_task_id: str, judge_model: str = '', reason: str = '', candidate_evidence_sha256: str = '', candidate_evidence: list[~booktx.models.JudgeCandidateEvidence] = <factory>, created_at: str, updated_at: str)[source]

Persisted judge decision for one selected record.

model_config = {'extra': 'forbid'}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

record_id: str
output_version_ref: str
output_target_sha256: str | None
decision_kind: Literal['copy', 'edited']
selected_profile: str | None
selected_kind: Literal['translation', 'review'] | None
selected_ref: str | None
selected_target_sha256: str | None
judge_task_id: str
judge_model: str
reason: str
candidate_evidence_sha256: str
candidate_evidence: list[JudgeCandidateEvidence]
created_at: str
updated_at: str
class booktx.models.TranslationSelectionLedger(*, version: Literal[1] = 1, profile: str, source_sha256: str, source_profiles: list[str] = <factory>, records: dict[str, ~booktx.models.JudgeDecision]=<factory>)[source]

Profile-local provenance ledger for a selection profile.

model_config = {'extra': 'forbid'}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

version: Literal[1]
profile: str
source_sha256: str
source_profiles: list[str]
records: dict[str, JudgeDecision]
class booktx.models.QualityReviewConfig(*, enabled: bool = False, active_passes: list[int] = <factory>, require_all_active_passes: bool = True, passes: list[ReviewPassConfig] = <factory>)[source]

Profile-level quality-review configuration.

Optional on ProfileConfig so existing config.toml files without a [quality_review] table round-trip unchanged.

model_config = {'extra': 'forbid'}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

enabled: bool
active_passes: list[int]
require_all_active_passes: bool
passes: list[ReviewPassConfig]
class booktx.models.IndexesConfig(*, auto_export_after_insert: bool = False, auto_export_after_review: bool = False, write_jsonl: bool = True)[source]

Optional auto-export index configuration.

Stored under [indexes] in translations/<profile>/config.toml. When None, no auto-export occurs.

model_config = {'extra': 'forbid'}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

auto_export_after_insert: bool
auto_export_after_review: bool
write_jsonl: bool
class booktx.models.EpubOutputConfig(*, language_policy: Literal['target', 'source', 'preserve', 'explicit'] = 'target', language: str | None = None, hyphenation: Literal['auto', 'manual', 'none', 'preserve'] = 'auto', inject_css: bool = True, patch_body_language: bool = False)[source]

Optional EPUB output-language and hyphenation policy.

Stored under [epub_output] in profile or legacy config.

When None (the default), booktx resolves effective policy from the profile kind: translation and legacy translation projects default to target language / auto hyphenation; pass-through profiles default to preserve / preserve (byte-identical output). Storing the model opts the project into an explicit policy.

See booktx.epub_output_policy for resolution and validation.

model_config = {'extra': 'forbid'}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

language_policy: Literal['target', 'source', 'preserve', 'explicit']
language: str | None
hyphenation: Literal['auto', 'manual', 'none', 'preserve']
inject_css: bool
patch_body_language: bool

Persistent translation context for a booktx project.

The translation contract in booktx.models preserves JSON structure, record ids, placeholders, tags, and protected names. It does not preserve translation intent: style, world terminology, and user-specific decisions.

This module owns the profile-local machine-readable translation context (translations/<profile>/context.json`) and the rendered human/agent view (translations/<profile>/context.md`) for normal profile workflows. context.json is authoritative; context.md is always regenerated from it.

The context is built deterministically and locally. It never calls an LLM, never makes a network request, and never approves a glossary target on its own. ready=false means translation must not begin; the user (or an agent driving the CLI on the user’s behalf) must answer the required questions first.

class booktx.context.StyleProfile(*, target_locale: str = 'de-DE', formality: Literal['informal', 'neutral', 'formal'] = 'neutral', register: str = '', prose_style: str = '', dialogue_style: str = '', sentence_policy: str = 'Prefer natural German prose; preserve meaning over word-for-word syntax.', punctuation_policy: str = '', units_policy: str = 'Keep source units unless the user says otherwise.')[source]

User-approved style decisions for the translation.

model_config = {'extra': 'forbid', 'populate_by_name': True, 'validate_by_alias': True, 'validate_by_name': True}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

target_locale: str
formality: Literal['informal', 'neutral', 'formal']
register_level: str
prose_style: str
dialogue_style: str
sentence_policy: str
punctuation_policy: str
units_policy: str
class booktx.context.GlossaryEntry(*, source: str, source_variants: list[str] = <factory>, target: str | None = None, target_variants: list[str] = <factory>, require_target: bool = False, forbidden_targets: list[str] = <factory>, category: str = 'term', status: Literal['open', 'approved', 'rejected']='open', notes: str = '', examples: list[str] = <factory>, case_sensitive: bool = False, enforce: Literal['off', 'warn', 'error']='warn', origin: Literal['core', 'seed', 'agent_review', 'user', 'legacy', 'imported', 'source_analysis']='user', source_analysis_candidate_id: str | None = None)[source]

One glossary term with optional approved target and forbidden targets.

model_config = {'extra': 'forbid'}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

source: str
source_variants: list[str]
target: str | None
target_variants: list[str]
require_target: bool
forbidden_targets: list[str]
category: str
status: Literal['open', 'approved', 'rejected']
notes: str
examples: list[str]
case_sensitive: bool
enforce: Literal['off', 'warn', 'error']
origin: Literal['core', 'seed', 'agent_review', 'user', 'legacy', 'imported', 'source_analysis']
source_analysis_candidate_id: str | None
class booktx.context.ContextQuestion(*, id: str, topic: str, question: str, answer: str | None = None, status: Literal['open', 'recommended', 'answered', 'skipped']='open', required: bool = True, origin: Literal['core', 'seed', 'agent_review', 'user', 'legacy', 'source_analysis']='core', source_analysis_candidate_ids: list[str] = <factory>, recommendation: str | None = None, recommendation_reason: str = '', recommendation_source: str = '', answer_source: Literal['user', 'imported', 'legacy', 'forced', 'agent'] | None=None, approved_by: str = '', approved_at: str = '')[source]

One question that may need a user answer before translation can begin.

required questions gate readiness: a context cannot be marked ready while any required question is still open.

model_config = {'extra': 'forbid'}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

id: str
topic: str
question: str
answer: str | None
status: Literal['open', 'recommended', 'answered', 'skipped']
required: bool
origin: Literal['core', 'seed', 'agent_review', 'user', 'legacy', 'source_analysis']
source_analysis_candidate_ids: list[str]
recommendation: str | None
recommendation_reason: str
recommendation_source: str
answer_source: Literal['user', 'imported', 'legacy', 'forced', 'agent'] | None
approved_by: str
approved_at: str
class booktx.context.ChapterContext(*, chapter_id: str, title: str = '', chunk_ids: list[str] = <factory>, source_summary: str = '', translation_summary: str = '', decisions_added: list[str] = <factory>, open_issues: list[str] = <factory>)[source]

Per-chapter notes appended as the agent completes chapters.

model_config = {'extra': 'forbid'}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

chapter_id: str
title: str
chunk_ids: list[str]
source_summary: str
translation_summary: str
decisions_added: list[str]
open_issues: list[str]
class booktx.context.TranslationContext(*, version: int = 1, source_language: str, target_language: str, source_title: str = '', source_author: str = '', source_sha256: str = '', ready: bool = False, ready_forced: bool = False, ready_reason: str = '', ready_by: str = '', ready_at: str = '', style: StyleProfile = <factory>, global_rules: list[str] = <factory>, glossary: list[GlossaryEntry] = <factory>, questions: list[ContextQuestion] = <factory>, chapter_contexts: list[ChapterContext] = <factory>)[source]

The authoritative translation context for one project.

model_config = {'extra': 'forbid'}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

version: int
source_language: str
target_language: str
source_title: str
source_author: str
source_sha256: str
ready: bool
ready_forced: bool
ready_reason: str
ready_by: str
ready_at: str
style: StyleProfile
global_rules: list[str]
glossary: list[GlossaryEntry]
questions: list[ContextQuestion]
chapter_contexts: list[ChapterContext]
booktx.context.apply_answer_to_context(context: TranslationContext, question_id: str, text: str) None[source]
booktx.context.context_path(project: Project) Path[source]

Path to the authoritative context JSON for the selected translation scope.

booktx.context.context_markdown_path(project: Project) Path[source]

Path to the rendered context markdown for the selected translation scope.

booktx.context.chapter_map_path(project: Project) Path[source]

Path to .booktx/chapter-map.json (owned by booktx.chapters).

booktx.context.load_context(project: Project) TranslationContext | None[source]

Load the context, or return None if no context file exists.

A corrupt context raises a ValueError so the caller can surface it rather than silently dropping the user’s decisions.

booktx.context.write_context(project: Project, context: TranslationContext) None[source]

Persist context to the resolved profile’s context.json.

booktx.context.default_context(project: Project, source_sha256: str = '') TranslationContext[source]

Build a not-ready context pre-filled from the project config.

Source/target languages and the source digest are taken from the project. Style and glossary start empty/seeded; required questions are open.

booktx.context.baseline_payload(context: TranslationContext) dict[str, object][source]

Return the semantic baseline payload for version resolution.

booktx.context.baseline_sha256(context: TranslationContext) str[source]

Return the baseline hash excluding chronological chapter notes.

booktx.context.chapter_notes_before_target(chapter_map: ChapterMap, notes: list[ChapterContext], target_chapter_id: str) list[ChapterContext][source]

Return prior chapter notes in chapter-map order for one target chapter.

booktx.context.context_history_dir(project: Project) Path[source]

Return the history directory adjacent to the live context files.

booktx.context.context_history_views_dir(project: Project) Path[source]

Return the directory containing immutable context view snapshots.

booktx.context.ensure_context_view_snapshot(project: Project, *, baseline_ref: str, baseline_sha256: str, target_chapter_id: str, notes_scope: str = 'before_target_chapter') ContextViewSnapshot[source]

Compose and persist an immutable task context view snapshot.

booktx.context.render_context_markdown(context: TranslationContext, *, view: Literal['full', 'effective', 'provenance'] = 'full') str[source]

Render a short, agent-readable markdown view of context.

The markdown is always derived from context; it is never authoritative.

booktx.context.write_context_markdown(project: Project, context: TranslationContext) None[source]

Render context to the resolved profile’s context.md.

booktx.context.parse_context_markdown_chapter_notes(markdown: str) list[ChapterContext][source]

Parse the rendered ## Chapter notes section from context.md.

Only the ## Chapter notes section is parsed; parsing stops at the next level-2 heading. Chapter headings must match the rendered shape ### 0006 or ### 0006 <separator> Title where <separator> is an em dash, en dash, or ASCII hyphen. The four bullet prefixes - Source summary:, - Translation summary:, - Decision:, and - Open issue: are parsed exactly. Unknown non-empty content inside a chapter note raises ValueError; nothing is silently mapped to open issues.

booktx.context.chapter_contexts_equivalent(left: ChapterContext, right: ChapterContext) bool[source]

Return True when two chapter notes have the same durable content.

chunk_ids is ignored because rendered Markdown does not include chunk ids; they are hydrated from chapter-map.json on import or upsert.

class booktx.context.ContextMarkdownDrift(*, missing_in_json: list[str] = <factory>, conflicting: list[str] = <factory>, parse_errors: list[str] = <factory>)[source]

Drift between rendered context.md chapter notes and context.json.

model_config = {'extra': 'forbid'}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

missing_in_json: list[str]
conflicting: list[str]
parse_errors: list[str]
property unsafe_to_overwrite: bool

True when overwriting context.md would discard Markdown-only notes.

booktx.context.analyze_context_markdown_drift(project: Project, context: TranslationContext) ContextMarkdownDrift[source]

Compare existing context.md chapter notes with context.json.

If context.md is missing, no drift is reported. Parser failures populate parse_errors and make the overwrite unsafe.

booktx.context.hydrate_chapter_contexts_from_chapter_map(project: Project, chapters: list[ChapterContext]) None[source]

Fill title and chunk_ids from chapter-map.json where absent.

Existing non-empty titles or chunk ids are never overwritten.

booktx.context.merge_chapter_contexts(context: TranslationContext, imported: list[ChapterContext], *, replace_existing: bool = False, append_existing_lists: bool = False) list[str][source]

Merge imported chapter notes into context and return changed ids.

Default behavior adds notes whose chapter ids are absent, treats equivalent notes as no-ops, and refuses differing existing notes. Pass replace_existing to replace durable fields (preserving or hydrating chunk_ids), or append_existing_lists to keep existing summaries unless empty and append non-duplicate decisions and open issues in order. The two modes are mutually exclusive.

booktx.context.upsert_chapter_context(context: TranslationContext, note: ChapterContext, *, replace_decisions: bool = False, replace_open_issues: bool = False, replace_all: bool = False) None[source]

Create or update one chapter note.

Title and summaries update only when provided. Decisions and open issues append by default (avoiding exact duplicates) and replace only when the matching replace flag is true.

When replace_all is true, the stored note is set exactly to the supplied values (empty strings and empty lists are allowed).

booktx.context.ensure_context_markdown_safe_to_overwrite(project: Project, context: TranslationContext, *, allow_discard_md_only: bool = False) None[source]

Raise ValueError if writing context.md would discard notes.

Runs drift analysis against the existing context.md. Pass “allow_discard_md_only=True`` for commands whose purpose is to overwrite Markdown despite unsafe drift.

booktx.context.seed_questions() list[ContextQuestion][source]

Generic initial questionnaire for any translation project.

The questions marked required=True gate ready. Typography and units are left optional: they affect style but a missing answer does not block the terminology safety that the context gate exists to provide.

booktx.context.seed_glossary() list[GlossaryEntry][source]

Generic glossary seeds (empty by default).

Book-specific seeds can be loaded via the --seed option on booktx context init. The Shadows-of-Apt template is available as --seed shadows-of-apt.

booktx.context.unresolved_required_questions(context: TranslationContext) list[ContextQuestion][source]
booktx.context.unapproved_required_questions(context: TranslationContext) list[ContextQuestion][source]
booktx.context.next_question_id(context: TranslationContext, prefix: str = 'Q') str[source]

Sentence segmentation and chunk packing.

The extractor hands chunking a list of protected prose spans (each already had names and inline tags replaced by placeholder tokens). chunking:

  1. Segments each span into sentences with phrasplit.

  2. Assigns each resulting sentence a stable record id.

  3. Packs records into Chunk objects of at most chunk_size records, numbering chunks from 1.

Record ids are NNNN-NNNNNN: the 4-digit chunk id, a dash, and a 1-based 6-digit index inside that chunk. Chunk ids are zero-padded 4-digit strings.

The goal stated in the spec is one source sentence to one translated sentence — chunking never merges or splits beyond what phrasplit returns.

class booktx.chunking.ProseSpan(text: str, placeholders: list[Placeholder], protected_terms: list[str], presegmented: bool = False, source_markup: Literal['plain:v1', 'epub-inline-xhtml:v1'] = 'plain:v1')[source]

A protected prose span produced by a format extractor.

text is the prose or inline-XHTML fragment with names already replaced by placeholder tokens. placeholders lists every placeholder that appears anywhere in text. Segmentation filters that list per record so each record carries only placeholders visible in its own source. protected_terms is the subset of names relevant to this span.

presegmented means the upstream extractor already selected this span as one sentence/record. In that case booktx must not run phrasplit again.

source_markup records the inline markup contract of the span so it can propagate into Record.source_markup for defense-in-depth record-level validation. The EPUB span manifest remains the authority at build time.

text: str
placeholders: list[Placeholder]
protected_terms: list[str]
presegmented: bool
source_markup: Literal['plain:v1', 'epub-inline-xhtml:v1']
__init__(text: str, placeholders: list[Placeholder], protected_terms: list[str], presegmented: bool = False, source_markup: Literal['plain:v1', 'epub-inline-xhtml:v1'] = 'plain:v1') None
booktx.chunking.segmenter_metadata(language: str) dict[str, object][source]

Return deterministic metadata describing the extraction segmenter.

booktx.chunking.segment_spans(spans: list[ProseSpan], *, language: str = 'en') list[Record][source]

Segment every span into one Record per sentence.

Empty/whitespace-only sentences are dropped so a span never yields a blank record. Each record carries only placeholders and protected terms visible in that record’s source text.

booktx.chunking.pack_chunks(records: list[Record], *, source_language: str, target_language: str = '', chunk_size: int = 50) list[Chunk][source]

Pack records into chunks of at most chunk_size and assign final ids.

Final record ids are NNNN-NNNNNN (chunk id + 1-based intra-chunk index).

booktx.chunking.spans_to_chunks(spans: list[ProseSpan], *, source_language: str, target_language: str = '', chunk_size: int = 50) list[Chunk][source]

Convenience: segment spans then pack into chunks.

Placeholder protection and restoration for names and inline non-translatable spans.

booktx never sends raw protected names or inline markup to the translating agent. Instead it replaces them with stable, collision-free tokens before segmentation, and restores the originals verbatim after the agent returns the translated text.

Two placeholder kinds are used (see booktx_coding_agent_start.md):

  • __NAME_NNN__ — a manually protected term from names.json (e.g. Alice -> __NAME_001__).

  • __TAG_NNN__ — an inline non-translatable span extracted from the document (inline code, URLs/link destinations) -> __TAG_001__.

Tokens are unique within a document and round-trip safe: applying protect_names() then restore() (or protect_tags() then restore()) returns the original text exactly.

booktx.placeholders.TRANSLATABLE_INLINE_PARENTS = {'blockquote', 'em', 'heading', 'list_item', 'paragraph', 'strong', 'table_cell', 'td', 'th'}

Translatable container types whose inline children we extract from.

booktx.placeholders.SKIP_BLOCK_TYPES = {'code_block', 'fence', 'html_block'}

Block types whose content must never be translated.

class booktx.placeholders.ProtectResult(text: str, placeholders: list[Placeholder])[source]

Outcome of a protect pass.

text has originals replaced by tokens. placeholders is ordered by first appearance and each Placeholder.original is the verbatim string to restore.

text: str
placeholders: list[Placeholder]
__init__(text: str, placeholders: list[Placeholder]) None
booktx.placeholders.protect_names(text: str, terms: list[str], *, start_index: int = 1) ProtectResult[source]

Replace each protected term in text with a __NAME_NNN__ token.

Matching is case-sensitive and whole-term (no word-boundary tricks) so multi-word names like Mr. Smith win over Mr.. Tokens are numbered by first appearance in the text (longest terms are matched first to avoid sub-string collisions, but the token id follows text order).

booktx.placeholders.protect_tags(text: str, originals: list[str], *, start_index: int = 1) ProtectResult[source]

Replace each inline non-translatable span with a __TAG_NNN__ token.

originals is the list of verbatim spans to hide (inline code, URLs, …) as discovered by the format extractor. They are replaced longest-first and only if the span actually occurs in text.

booktx.placeholders.restore(text: str, placeholders: list[Placeholder]) str[source]

Replace each token in text with its recorded original.

Used by the build step on the agent’s translated text. Tokens are restored verbatim; missing tokens are left in place (the validator flags those).

booktx.placeholders.span_token_ids(template: str) list[str][source]

Return the __SPANTX_NNNN__ tokens found in template, in order.

Markdown extraction and rebuild.

Extraction walks a markdown document with markdown_it, identifies the translatable prose spans (the inline tokens inside paragraphs, headings, list items, blockquotes, and table cells), hides inline code / link URLs / raw HTML behind __TAG_NNN__ tokens, hides protected names behind __NAME_NNN__ tokens, and returns:

  • a template — the original markdown with each translatable inline replaced by a __SPANTX_NNNN__ placeholder; and

  • a list of ProseSpan in placeholder order.

Non-translatable blocks (YAML front matter, fenced code, indented code, HTML blocks) are left untouched in the template.

Rebuild is the inverse: substitute each __SPANTX_NNNN__ with the supplied translated span text (already name/tag-restored by build.py).

class booktx.markdown_io.MarkdownExtraction(template: str, spans: list[ProseSpan], front_matter: str)[source]

Result of extract_markdown().

template: str
spans: list[ProseSpan]
front_matter: str
__init__(template: str, spans: list[ProseSpan], front_matter: str) None
booktx.markdown_io.FRONT_MATTER_RE = re.compile('\\A---\\r?\\n(.*?\\r?\\n)---\\r?\\n?', re.DOTALL)

YAML front matter at the very start of a document.

booktx.markdown_io.extract_markdown(text: str, *, protected_terms: list[str] | None = None) MarkdownExtraction[source]

Extract translatable spans from text.

Returns the rebuild template and the ordered prose spans.

booktx.markdown_io.build_markdown(template: str, span_replacements: list[str]) str[source]

Rebuild markdown by substituting each __SPANTX_NNNN__ in order.

span_replacements[i] replaces the (i+1)-th span placeholder. Extra replacements are ignored; missing replacements leave the token in place.

booktx.markdown_io.split_front_matter(text: str) tuple[str, str][source]

Split leading YAML front matter from the body.

Returns (front_matter_with_fences, body). If there is no front matter, front_matter is empty and body is the original text.

EPUB extraction and rebuild adapters over epub2text and text2epub.

class booktx.epub_io.EpubExtraction(spans: list[ProseSpan] = <factory>, span_refs: list[EpubSpanRef] = <factory>, text2epub_manifest: dict[str, object]=<factory>, source_sha256: str = '', navigation: list[EpubNavigationRef] = <factory>)[source]

Structured EPUB extraction data used by booktx.

spans: list[ProseSpan]
span_refs: list[EpubSpanRef]
text2epub_manifest: dict[str, object]
source_sha256: str
navigation: list[EpubNavigationRef]
__init__(spans: list[ProseSpan] = <factory>, span_refs: list[EpubSpanRef] = <factory>, text2epub_manifest: dict[str, object]=<factory>, source_sha256: str = '', navigation: list[EpubNavigationRef] = <factory>) None
booktx.epub_io.build_epub(source_path: str, output_path: str, extraction: EpubExtraction, span_replacements: list[str]) str[source]

Rebuild an EPUB via text2epub using one replacement per extracted span.

span_replacements provides one target string per entry in extraction.spans (sentence-level). Spans are grouped back into one text2epub.Replacement per block-level span_ref: for each span_ref the replacements whose span index falls in [span_ref.span_index, next_span_index) are joined with a space. When every span of a block is unchanged, the original raw block is reused so pass-through builds stay byte-identical even when the joined sentence XHTML differs from the stored block fragment.

booktx.epub_io.extract_epub(path: str, *, protected_terms: list[str] | None = None) EpubExtraction[source]

Extract translatable EPUB spans through epub2text structured blocks.

booktx.epub_io.read_epub(path: str) ZipFile[source]

Open an EPUB archive for direct ZIP-level inspection.

Helpers for the migrated EPUB extraction manifest.

booktx.epub_manifest.assert_source_sha(path: Path | str, expected_sha: str) None[source]

Raise if path does not match expected_sha.

booktx.epub_manifest.build_raw_block_index(structured: Any) dict[str, _RawBlock][source]

Map epub2text block ids to raw archive offsets and fragments.

booktx.epub_manifest.load_epub_template_from_manifest(manifest: Manifest) EpubTemplateData[source]

Validate and return the EPUB v2 template stored in manifest.

booktx.epub_manifest.sha256_path(path: Path | str) str[source]

Return the SHA256 hex digest of path.

booktx.epub_manifest.structured_to_navigation_refs(structured: Any) list[EpubNavigationRef][source]

Convert epub2text navigation entries into stored manifest refs.

booktx.epub_manifest.structured_to_span_refs(structured: Any, *, protected_terms: list[str], raw_block_index: dict[str, _RawBlock] | None = None) tuple[list[ProseSpan], list[EpubSpanRef]][source]

Build booktx spans plus ordered span refs from epub2text blocks.

booktx.epub_manifest.structured_to_text2epub_manifest(structured: Any, *, raw_block_index: dict[str, _RawBlock] | None = None) dict[str, object][source]

Convert an epub2text structured extraction to a text2epub manifest.

Chapter detection and chapter-map persistence for booktx.

class booktx.chapters.Chapter(*, chapter_id: str, title: str = '', chunk_ids: list[str] = <factory>, start_record_id: str = '', end_record_id: str = '', record_count: int = 0)[source]

One detected chapter and the chunk range it covers.

model_config = {'extra': 'forbid'}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

chapter_id: str
title: str
chunk_ids: list[str]
start_record_id: str
end_record_id: str
record_count: int
class booktx.chapters.ChapterMap(*, version: int = 2, source_sha256: str = '', chapters: list[Chapter] = <factory>)[source]

Detected chapters for a project.

model_config = {'extra': 'forbid'}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

version: int
source_sha256: str
chapters: list[Chapter]
booktx.chapters.detect_chapters(project: Project) ChapterMap[source]

Detect chapters and write no files.

booktx.chapters.load_chapter_map(project: Project) ChapterMap | None[source]
booktx.chapters.write_chapter_map(project: Project, chapter_map: ChapterMap) None[source]

Validation of agent-translated chunks against the booktx contract.

The validator loads every source chunk in .booktx/chunks/ and the matching translated chunk in .booktx/translated/ (if present), and enforces the hard rules from booktx_coding_agent_start.md:

  • The translated JSON must be valid JSON.

  • The record count must be unchanged.

  • No record id may change.

  • No target may be empty.

  • No placeholder may be removed or changed.

  • No protected name may be translated or removed.

  • The translated file must contain no commentary outside the JSON structure.

The goal is one source sentence to one translated sentence. The validator never merges or splits records.

A ValidationReport collects per-chunk findings and a summary, and is written to .booktx/reports/. validate_project returns the report and exits non-zero on any mandatory failure.

class booktx.validate.Severity[source]

Finding severity labels.

INFO = 'info'
WARN = 'warn'
ERROR = 'error'
class booktx.validate.Finding(chunk_id: str, severity: str, rule: str, message: str, record_id: str = '', record_ids: list[str] = <factory>, chapter_id: str = '', chapter_title: str = '', span_index: int | None = None, block_id: str = '', document_href: str = '', source: str = '', target: str = '', candidate_kind: str = '', candidate_ref: str = '', candidate_scope: str = '')[source]

One validation finding for one chunk.

Optional location/context fields are populated from EPUB inline-XHTML preflight findings and left empty for plain record-level findings. They are included in as_dict() only when non-empty so the JSON report stays backward compatible and readable.

chunk_id: str
severity: str
rule: str
message: str
record_id: str
record_ids: list[str]
chapter_id: str
chapter_title: str
span_index: int | None
block_id: str
document_href: str
source: str
target: str
candidate_kind: str
candidate_ref: str
candidate_scope: str
as_dict() dict[str, object][source]
__init__(chunk_id: str, severity: str, rule: str, message: str, record_id: str = '', record_ids: list[str] = <factory>, chapter_id: str = '', chapter_title: str = '', span_index: int | None = None, block_id: str = '', document_href: str = '', source: str = '', target: str = '', candidate_kind: str = '', candidate_ref: str = '', candidate_scope: str = '') None
class booktx.validate.ValidationReport(project: str, profile: str = '', target_language: str = '', target_locale: str = '', findings: list[Finding] = <factory>, chunks_checked: int = 0, chunks_passed: int = 0, chunks_missing_translation: int = 0, generated_at: str = '')[source]

Aggregated validation result for a project.

project: str
profile: str
target_language: str
target_locale: str
findings: list[Finding]
chunks_checked: int
chunks_passed: int
chunks_missing_translation: int
generated_at: str
property errors: list[Finding]
property warnings: list[Finding]
property passed: bool
as_dict() dict[str, object][source]
property blocking_findings: list[Finding]

Findings that affect pass/fail regardless of history mode.

Effective-output and structural findings always count. Inactive historical content findings are excluded: they describe candidates that are not the current output and must not fail a normal build.

property inactive_findings: list[Finding]

Findings about inactive historical translation candidates.

__init__(project: str, profile: str = '', target_language: str = '', target_locale: str = '', findings: list[Finding] = <factory>, chunks_checked: int = 0, chunks_passed: int = 0, chunks_missing_translation: int = 0, generated_at: str = '') None
class booktx.validate.EffectiveTranslations(chunks: dict[str, ~booktx.models.TranslatedChunk]=<factory>, findings: list[Finding] = <factory>)[source]

Merged accepted translations from the store and valid legacy chunks.

chunks: dict[str, TranslatedChunk]
findings: list[Finding]
__init__(chunks: dict[str, ~booktx.models.TranslatedChunk]=<factory>, findings: list[Finding] = <factory>) None
booktx.validate.load_validation_context(project: Project, *, context_view_path: str | None = None) TranslationContext | None[source]

Load the context that should be used for validation.

booktx.validate.strict_load_translated(path: Path) tuple[TranslatedChunk | None, str | None][source]

Load a translated chunk, detecting commentary outside the JSON.

Returns (model_or_None, error_message_or_None). We parse twice: once permissively into the model, once strictly to catch trailing commentary.

booktx.validate.validate_record_pair(source_rec: Record, target_rec: TranslatedRecord, chunk_id: str, context: TranslationContext | None = None) list[Finding][source]

Validate one translated record against one source record.

booktx.validate.load_effective_translated_chunks(project: Project, *, source_chunks: dict[str, Chunk] | None = None, context: TranslationContext | None = None, include_inactive_versions: bool = False, all_versions_strict: bool = False) EffectiveTranslations[source]

Merge valid legacy chunk files and accepted store records.

booktx.validate.validate_project(project: Project, *, include_inactive_versions: bool = False, all_versions_strict: bool = False, chapter_id: str | None = None, record_ids: set[str] | None = None, task_id: str | None = None, require_complete: bool = False) ValidationReport[source]

Validate every translated chunk in project.

Missing translations are not errors; only present-but-invalid translated files produce error findings. Stale translated files whose chunk id no longer exists produce a warning.

booktx.validate.validate_chunk_pair(source: Chunk, translated_path: Path | None, context: TranslationContext | None = None) list[Finding][source]

Validate one source chunk against its translated file (if any).

booktx.validate.review_coverage_findings(stored: StoredTranslationRecordV2, quality_cfg: QualityReviewConfig, chunk_id: str, record_id: str, *, force_error: bool = False) list[Finding][source]

Per-pass review-coverage findings for one record.

force_error (used by build --require-reviewed) treats every coverage gap as an ERROR regardless of the pass enforce setting. Otherwise gaps are emitted only when enforce is warn or error and the severity follows enforce.

booktx.validate.write_report(project: Project, report: ValidationReport) Path[source]

Persist the validation report to .booktx/reports/ and return it.

Rebuild the final translated document from validated translated chunks.

class booktx.build.BuildResult(*, output_path: Path, format: str, span_count: int, report: dict[str, object] | None = None)[source]

Outcome of a build run.

__init__(*, output_path: Path, format: str, span_count: int, report: dict[str, object] | None = None) None[source]
as_dict() dict[str, object][source]
exception booktx.build.BuildError[source]

User-facing error during build.

booktx.build.build_project(project: Project, *, require_complete: bool = False, require_reviewed: bool = False) BuildResult[source]

Build the translated output document for project.

booktx.build.records_to_span_text(span: ProseSpan, targets: list[str]) str[source]

Join translated record targets back into one span string.

Typed project status snapshot and runtime index.

This module owns the record/word/chunk/chapter aggregation that used to live as the private _project_status_snapshot dict inside booktx.cli. It returns a typed StatusSnapshot (which serializes to the stable status --json v1 shape) plus a StatusRuntimeIndex dataclass that carries the lookup maps the command layer still needs for task creation and record acceptance.

Design notes:

  • The public JSON shape is preserved exactly, including the nested record_range: {start, end} field on each chapter. Do not flatten it to start_record_id/end_record_id without intentionally versioning to v2.

  • The snapshot model has only public fields; no _private keys leak into JSON output. Runtime lookup maps live on the separate StatusRuntimeIndex so they never get serialized.

  • This module is intentionally free of Typer/Rich imports so it can be unit tested directly. CLI-specific error UX (_die) stays in cli.py; the caller computes context_exists/context_ready and passes them in.

class booktx.status.RecordRange(*, start: str, end: str)[source]

Inclusive record-id range covered by a chapter (nested in JSON v1).

model_config = {'extra': 'forbid'}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

start: str
end: str
class booktx.status.ChapterProgress(*, chapter_id: str, title: str, chunk_ids: list[str] = <factory>, pending_chunk_ids: list[str] = <factory>, record_range: RecordRange, records_total: int, records_translated: int, records_remaining: int, source_words_total: int, source_words_translated: int, source_words_remaining: int, status: str)[source]

Per-chapter translation coverage (matches the status --json v1 shape).

model_config = {'extra': 'forbid'}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

chapter_id: str
title: str
chunk_ids: list[str]
pending_chunk_ids: list[str]
record_range: RecordRange
records_total: int
records_translated: int
records_remaining: int
source_words_total: int
source_words_translated: int
source_words_remaining: int
status: str
class booktx.status.ChunkProgress(*, chunk_id: str, records_total: int, records_translated: int, records_remaining: int, source_words_total: int, source_words_translated: int, source_words_remaining: int, status: str)[source]

Per-chunk translation coverage (matches the status --json v1 shape).

model_config = {'extra': 'forbid'}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

chunk_id: str
records_total: int
records_translated: int
records_remaining: int
source_words_total: int
source_words_translated: int
source_words_remaining: int
status: str
class booktx.status.SourceStatus(*, filename: str, format: str, source_language: str, target_language: str, source_sha256: str, source_drifted: bool = False)[source]
model_config = {'extra': 'forbid'}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

filename: str
format: str
source_language: str
target_language: str
source_sha256: str
source_drifted: bool
class booktx.status.ContextStatus(*, exists: bool, ready: bool)[source]
model_config = {'extra': 'forbid'}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

exists: bool
ready: bool
class booktx.status.StatusTotals(*, source_words: int = 0, translated_words: int = 0, remaining_words: int = 0, records_total: int = 0, records_translated: int = 0, records_remaining: int = 0, chunks_total: int = 0, chunks_complete: int = 0, chunks_partial: int = 0, chunks_pending: int = 0, chapters_total: int = 0, chapters_complete: int = 0, chapters_partial: int = 0, chapters_pending: int = 0, invalid_translation_files: int = 0, stale_translation_files: int = 0)[source]

Aggregate translation coverage totals.

Used by status --json and persisted in TranslationTodo.start_totals. Keep this model in models.py so durable todo validation never depends on a late Pydantic forward-reference rebuild.

model_config = {'extra': 'forbid'}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

source_words: int
translated_words: int
remaining_words: int
records_total: int
records_translated: int
records_remaining: int
chunks_total: int
chunks_complete: int
chunks_partial: int
chunks_pending: int
chapters_total: int
chapters_complete: int
chapters_partial: int
chapters_pending: int
invalid_translation_files: int
stale_translation_files: int
class booktx.status.VersionCoverage(*, version_ref: str, version: int, subversion: int, records_with_candidate: int = 0, active_records: int = 0)[source]
model_config = {'extra': 'forbid'}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

version_ref: str
version: int
subversion: int
records_with_candidate: int
active_records: int
class booktx.status.TrackCoverage(*, version: int, label: str | None = None, records_with_candidate: int = 0, active_records: int = 0, latest_subversion: int | None = None)[source]
model_config = {'extra': 'forbid'}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

version: int
label: str | None
records_with_candidate: int
active_records: int
latest_subversion: int | None
class booktx.status.StatusSnapshot(*, version: int = 1, project: str, source: SourceStatus, context: ContextStatus, totals: StatusTotals, next: ChapterProgress | None = None, chapters: list[ChapterProgress] = <factory>, version_coverage: list[VersionCoverage] = <factory>, track_coverage: list[TrackCoverage] = <factory>)[source]

Typed project status. Serializes to the status --json v1 payload.

model_config = {'extra': 'forbid'}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

version: int
project: str
source: SourceStatus
context: ContextStatus
totals: StatusTotals
next: ChapterProgress | None
chapters: list[ChapterProgress]
version_coverage: list[VersionCoverage]
track_coverage: list[TrackCoverage]
class booktx.status.StatusRuntimeIndex(source_chunks: dict[str, Chunk], source_by_id: dict[str, SourceRecordView], translated_by_id: dict[str, TranslatedRecord], record_ids_by_chapter: dict[str, list[str]], record_to_chapter: dict[str, str], chapters_by_id: dict[str, ChapterProgress], chapter_summaries: list[ChapterProgress], chunk_summaries: list[ChunkProgress], record_error_by_id: dict[str, Finding])[source]

Lookup maps derived while building a StatusSnapshot.

These are intentionally not serialized. They carry the live objects the command layer needs for task creation, record acceptance, and selection.

source_chunks: dict[str, Chunk]
source_by_id: dict[str, SourceRecordView]
translated_by_id: dict[str, TranslatedRecord]
record_ids_by_chapter: dict[str, list[str]]
record_to_chapter: dict[str, str]
chapters_by_id: dict[str, ChapterProgress]
chapter_summaries: list[ChapterProgress]
chunk_summaries: list[ChunkProgress]
record_error_by_id: dict[str, Finding]
__init__(source_chunks: dict[str, Chunk], source_by_id: dict[str, SourceRecordView], translated_by_id: dict[str, TranslatedRecord], record_ids_by_chapter: dict[str, list[str]], record_to_chapter: dict[str, str], chapters_by_id: dict[str, ChapterProgress], chapter_summaries: list[ChapterProgress], chunk_summaries: list[ChunkProgress], record_error_by_id: dict[str, Finding]) None
class booktx.status.StatusBundle(snapshot: StatusSnapshot, index: StatusRuntimeIndex, epub_audit: EpubAuditSummary | None = None)[source]

A status snapshot paired with its runtime index.

snapshot: StatusSnapshot
index: StatusRuntimeIndex
epub_audit: EpubAuditSummary | None
__init__(snapshot: StatusSnapshot, index: StatusRuntimeIndex, epub_audit: EpubAuditSummary | None = None) None
class booktx.status.EpubAuditSummary(*, error_count: int = 0, warning_count: int = 0, has_blocking_errors: bool = False, findings: list[EpubAuditFindingSummary] = <factory>)[source]

Recomputed EPUB visible-TOC chapter-audit summary for the current source.

Always recomputed at status/gate time; the persisted report is user-facing only and is never trusted for workflow gating.

model_config = {'extra': 'forbid'}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

error_count: int
warning_count: int
has_blocking_errors: bool
findings: list[EpubAuditFindingSummary]
class booktx.status.ProfileOverview(*, profile: str, kind: str = 'translation', target_language: str, target_locale: str = '', model: str = '', path: str, translated_records: int = 0, total_records: int = 0)[source]
model_config = {'extra': 'forbid'}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

profile: str
kind: str
target_language: str
target_locale: str
model: str
path: str
translated_records: int
total_records: int
class booktx.status.ProfilesOverview(*, project: str, source: str = '', source_records: int = 0, profiles: list[ProfileOverview] = <factory>)[source]
model_config = {'extra': 'forbid'}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

project: str
source: str
source_records: int
profiles: list[ProfileOverview]
booktx.status.coverage_status(*, total: int, translated: int, has_error: bool) str[source]

Return the coverage status label for a chunk or chapter.

booktx.status.build_status_snapshot(proj: Project, *, context_exists: bool, context_ready: bool) StatusBundle[source]

Build a typed status snapshot and runtime index for proj.

context_exists/context_ready are computed by the caller (the CLI owns the invalid-context error UX). Everything else — source/chunk loading, effective-translation merge, chapter mapping, coverage math — is owned here.

booktx.status.build_profiles_overview(project: Project) ProfilesOverview[source]
booktx.status.selected_chapter(bundle: StatusBundle, chapter_id: str | None) ChapterProgress | None[source]

Return the focused chapter for the status/next commands.

chapter_id=None selects the first chapter with remaining records (snapshot.next), which is None when everything is translated. A specific chapter_id returns that chapter whether or not it still has remaining records; the caller decides how to react to a complete chapter. Returns None only when the id is unknown.

Translation-task creation, durable task paths, and submission hints.

Centralizes the durable-file layout and id derivation for translation tasks so the command layer stops reconstructing paths and submit-hints inline.

Profile-layout projects (primary):

translations/<profile>/tasks/<id>.json translations/<profile>/tasks/<id>.source.block.txt translations/<profile>/ingest/<id>.json translations/<profile>/ingest/<id>.block.txt

Legacy single-layout projects (compatibility only):

.booktx/tasks/<id>.json .booktx/tasks/<id>.source.block.txt .booktx/ingest/<id>.json .booktx/ingest/<id>.block.txt

All task-path access goes through translation_task_dir(project) which enforces the profile-required guard for source-only projects. The TaskPaths value object bundles the four per-task files and renders the project-relative display strings and submit commands the CLI prints.

class booktx.tasks.TaskPaths(task_json: Path, source_block: Path, ingest_json: Path, ingest_block: Path)[source]

The four durable files owned by one translation task.

task_json: Path
source_block: Path
ingest_json: Path
ingest_block: Path
display(root: Path, *, mode: RuntimeMode | None = None) TaskPathDisplay[source]
block_submit_hint(task_id: str, root: Path, *, mode: RuntimeMode | None = None) str[source]
json_submit_hint(task_id: str, root: Path, *, mode: RuntimeMode | None = None) str[source]
block_stdin_submit_hint(task_id: str, *, mode: RuntimeMode | None = None) str[source]
__init__(task_json: Path, source_block: Path, ingest_json: Path, ingest_block: Path) None
booktx.tasks.make_task_id(chapter_id: str, first_record_id: str, record_ids: list[str]) str[source]

Derive a deterministic, path-safe task id.

Uses a stable blake2s digest (digest_size=4) of the joined record ids instead of Python’s process-randomized hash(), plus a seconds-precision UTC timestamp so same-day collisions are extremely unlikely.

booktx.tasks.task_paths(project: Project, task_id: str) TaskPaths[source]

Return the TaskPaths bundle for task_id.

Routes through translation_task_dir(project) so a source-only project (no selected profile) hits the profile-required guard instead of silently assuming legacy .booktx/tasks paths.

booktx.tasks.project_relative(path: Path, root: Path) str[source]

Return a stable project-relative display path when possible.

booktx.tasks.limit_records_by_words(record_ids: list[str], source_by_id: Mapping[str, SourceRecordView], max_words: int) list[str][source]

Return the longest prefix of record_ids within max_words.

The first record is always included when record_ids is non-empty so a single long record still makes progress.

booktx.tasks.select_translation_record_ids(bundle: StatusBundle, chapter: ChapterProgress, *, unit: str, max_words: int) tuple[str, list[str]][source]

Select the record ids for the next translation task within chapter.

booktx.tasks.create_translation_task(project: Project, bundle: StatusBundle, chapter: ChapterProgress, *, mode: RuntimeMode | None = None, unit: str, record_ids: list[str], requested_max_words: int | None = None, todo_id: str | None = None) TranslationTask[source]

Build, persist, and render durable files for one translation task.

booktx.tasks.write_ingest_template(project: Project, task: TranslationTask) Path[source]

Create the durable JSON submission file for a task without overwriting work.

booktx.tasks.write_block_ingest_template(project: Project, task: TranslationTask, *, mode: RuntimeMode | None = None) Path[source]

Create the durable block submission file for a task without overwriting work.

The file starts with metadata comment headers (ignored by the block parser) followed by one >>> RECORD_ID header per record. The agent fills in the target text under each header.

booktx.tasks.write_task_source_block(project: Project, task: TranslationTask) Path[source]

Create the durable source-view file for a task without overwriting work.

Holds the original source text for each record in the task so a coding agent can translate against a stable file instead of a large stdout dump.

Submission payload parsing for the translate-accept commands.

Owns the three submission wire formats (JSON, TSV, block) and the input-source selection that the translate insert command used to inline. Parsers return booktx.acceptance.SubmittedRecord lists directly so the acceptance service never sees raw dicts.

Validation errors are raised as booktx.config.BooktxError; the CLI renders them via the shared _handle_booktx_error path, so messages stay identical to the previous inline _die calls.

class booktx.submissions.ParsedSubmission(records: list[SubmittedRecord], task_id: str | None = None, translation_version: str | None = None, profile: str | None = None)[source]

A parsed submission payload: optional task id + validated records.

__init__(records: list[SubmittedRecord], task_id: str | None = None, translation_version: str | None = None, profile: str | None = None) None[source]
records
task_id
translation_version
profile
booktx.submissions.parse_json_submission(text: str) ParsedSubmission[source]

Parse a JSON {"task_id": ..., "records": [{"id","target"}, ...]} payload.

booktx.submissions.parse_tsv_submission(text: str) ParsedSubmission[source]

Parse <record-id>\t<target> lines (blank lines ignored).

booktx.submissions.parse_block_submission(text: str) ParsedSubmission[source]

Parse the durable >>> <record-id> block format.

booktx.submissions.read_submission_file(path: Path, *, ingest_hint: str | None = None) str[source]

Read a submission file, raising BooktxError (never a traceback) on failure.

ingest_hint is a project-relative path to the canonical profile-local ingest file (e.g. translations/<profile>/ingest/<task>.block.txt). When the requested file is missing and differs from the hint, the error message points the agent at the generated ingest location instead of /tmp or other temporary paths.

booktx.submissions.resolve_submission(*, record_id: str | None, target: str | None, input_format: str, stdin: bool, json_file: Path | None, input_file: Path | None, ingest_hint: str | None = None) ParsedSubmission[source]

Select the input source and parse it into a ParsedSubmission.

Mirrors the previous inline dispatch in translate_insert: a single --record-id/--target pair wins, then --json-file, then --file (with --format), then --stdin. Raises BooktxError for any ambiguous or missing combination.

Translation-record acceptance service.

Centralizes the validate-and-persist flow that was duplicated between the translate insert (batch) and translate set-record (single-record) commands. Both commands used to re-implement the same steps: look up the source view, validate each record against the current context, bail on the first ERROR finding, then mutate the translation store with a shared timestamp and refresh the status snapshot.

This module owns the pure workflow:

  1. Resolve each submitted record id against the source index (raising booktx.config.BooktxError for duplicate / unknown / out-of-task ids, which the CLI renders exactly like any other user-facing error).

  2. Load the translation context once and validate every record against it.

  3. If any validation produced an ERROR finding, raise SubmissionValidationError carrying those findings — the store is not touched.

  4. Otherwise mutate the store atomically with one shared timestamp and return an AcceptResult describing the post-accept progress for the first affected chapter.

The CLI wrappers parse options, call accept_translation_records() (or accept_one_record()), then render the result. Console output is intentionally not produced here so the service is unit-testable without Typer/Rich.

class booktx.acceptance.SubmittedRecord(id: str, target: str)[source]

One validated submission item (record id + target text).

id: str
target: str
__init__(id: str, target: str) None
class booktx.acceptance.AcceptResult(accepted_records: int, target_words: int, version_ref: str = '', chapter_id: str = '', chapter_title: str = '', records_translated: int = 0, records_total: int = 0, records_remaining: int = 0)[source]

Post-accept progress for the first affected chapter.

chapter_id is empty when the accepted record(s) could not be mapped to a chapter; the CLI treats that as “no chapter line to print”.

accepted_records: int
target_words: int
version_ref: str
chapter_id: str
chapter_title: str
records_translated: int
records_total: int
records_remaining: int
__init__(accepted_records: int, target_words: int, version_ref: str = '', chapter_id: str = '', chapter_title: str = '', records_translated: int = 0, records_total: int = 0, records_remaining: int = 0) None
exception booktx.acceptance.SubmissionValidationError(findings: list[Finding])[source]

Raised when one or more submitted records failed ERROR-level validation.

Carries the ERROR findings so the CLI can render them with the existing submission-failure renderer. The translation store is never written when this is raised.

__init__(findings: list[Finding]) None[source]
booktx.acceptance.validate_submitted_records(proj: Project, bundle: StatusBundle, submitted: list[SubmittedRecord], *, task: TranslationTask | None, enforce_task_membership: bool = True) list[Finding][source]

Validate submitted records, raising BooktxError on id problems.

Context is loaded exactly once. Returns all findings (any severity); the caller decides whether ERROR findings should block the store write.

booktx.acceptance.accept_translation_records(proj: Project, submitted: list[SubmittedRecord], *, bundle: StatusBundle, task: TranslationTask | None = None, submission_translation_version: str | None = None, submission_profile: str | None = None, enforce_task_version: bool = False) AcceptResult[source]

Validate and atomically persist a batch of accepted records.

Raises BooktxError for duplicate / unknown / out-of-task ids and SubmissionValidationError when any record fails ERROR-level validation. On success the store is written once and an AcceptResult is returned.

Note: callers pass a fresh bundle built before acceptance. The returned progress fields reflect a refreshed snapshot taken after the store write, matching the historical CLI output exactly.

booktx.acceptance.accept_one_record(proj: Project, record_id: str, target: str, *, bundle: StatusBundle, task: TranslationTask | None = None, submission_translation_version: str | None = None, submission_profile: str | None = None, enforce_task_version: bool = False) AcceptResult[source]

Validate and persist a single accepted record.

Equivalent to accept_translation_records() with one item, but also enforces that a non-empty target was supplied (the single-record command rejects empty targets before reaching the store).

Rich/text/JSON output renderers.

Moved out of booktx.cli so command functions contain only option parsing + service call + renderer call. Each renderer owns the exact console output and can be unit-tested without Typer.

The module creates its own rich.console.Console so it never depends on the CLI module (which would create a circular import).

booktx.rendering.format_chunk_span(chunk_ids: list[str]) str[source]

Return a compact chunk-id range string for human display.

booktx.rendering.print_status_human(bundle: StatusBundle, chapter: ChapterProgress | None) None[source]

Render the human-readable booktx status summary to the console.

booktx.rendering.print_translate_task(task: TranslationTask, project: Project, *, mode: RuntimeMode | None = None, as_json: bool, output_format: str, show_sources: bool = False, show_template: bool = False) None[source]

Render a translate next / translate task result to the console.

Supports four output modes: --json, --format tsv, --format block (the durable agent workflow), and the default human-readable list.

booktx.rendering.render_submission_failures(findings: list[Finding]) None[source]

Render submission validation ERROR findings to the console.

Atomic write and timestamp helpers.

Centralizes the atomic-write pattern (write to a sibling temp file, then replace) that was previously duplicated as the private _write_json_atomic in booktx.config and reimplemented inline as direct Path.write_text calls in several modules.

All booktx persistence (translation store, context, chapter map, manifest, reports, chunks, task source files, and ingest templates) should route through write_text_atomic() or write_json_model_atomic() so an interrupted write never leaves a half-empty file in .booktx/.

booktx.io_utils.utc_timestamp() str[source]

Return the current UTC time as a second-precision ISO-8601 Z string.

Equivalent to the inline expression that was duplicated in the translate acceptance path. Microseconds are dropped so timestamps are stable and human-comparable.

booktx.io_utils.write_text_atomic(path: Path, text: str) None[source]

Write UTF-8 text atomically into path.

The file is written to a hidden sibling temp file in the same directory and then replaced into place, so readers either see the previous file or the complete new file, never a partial write. Parent directories are created on demand. The temp file is cleaned up if the write fails.

booktx.io_utils.write_json_model_atomic(path: Path, model: BaseModel, *, indent: int = 2, trailing_newline: bool = True) None[source]

Serialize a Pydantic model to JSON and write it atomically into path.

Mirrors the previous model_dump_json(indent=2) + "\n" convention used by the store, chapter map, and chunk writers. by_alias and other dump options must be applied by the caller via model.model_dump_json when needed; this helper is for the common default case.

booktx.io_utils.write_json_text_atomic(path: Path, text: str) None[source]

Write already-serialized JSON text atomically into path.

Used when the caller has already produced JSON via json.dumps or model_dump_json(by_alias=True) and just needs the atomic write plus a trailing newline. The trailing newline matches the historical convention.

Helpers for canonical record and version references.

class booktx.record_refs.RecordRef(chunk_id: int, part_id: int)[source]

Parsed record reference with stable canonical rendering.

chunk_id: int
part_id: int
property canonical_id: str
property compact_ref: str
__init__(chunk_id: int, part_id: int) None
class booktx.record_refs.VersionRef(version: int, subversion: int)[source]

Parsed dotted version reference with numeric ordering.

version: int
subversion: int
property version_ref: str
__init__(version: int, subversion: int) None
booktx.record_refs.canonical_record_id(chunk_id: int, part_id: int) str[source]

Return the canonical padded record id for integer ids.

booktx.record_refs.format_version_ref(version: int, subversion: int) str[source]

Return the canonical dotted version reference.

booktx.record_refs.parse_record_ref(value: str) RecordRef[source]

Parse a record reference into canonical integer parts.

booktx.record_refs.parse_version_ref(value: str) VersionRef[source]

Parse and validate a dotted version reference.

booktx.record_refs.resolve_record_range(value: str, *, ordered_record_ids: list[str], chapter_record_ids: dict[str, list[str]] | None = None) list[str][source]

Resolve one record range selector against source reading order.

Helpers for working with nested translation stores.

This module also hosts the legacy compatibility surface for the old flat (v1) TranslationStore: legacy_store_to_v2() and migrate_legacy_store() convert legacy stores into the nested v2 shape. They are kept here, clearly named, so legacy import/export stays quarantined from the active v2 store logic.

class booktx.translation_store.MigrationResult(store: TranslationStoreV2, migrated_records: int, missing_source_ids: list[str])[source]

Result of converting a legacy flat store into the v2 nested shape.

store: TranslationStoreV2
migrated_records: int
missing_source_ids: list[str]
__init__(store: TranslationStoreV2, migrated_records: int, missing_source_ids: list[str]) None
class booktx.translation_store.EffectiveCandidateError(rule: str, message: str)[source]

A blocking problem resolving the effective candidate for a record.

rule mirrors the validation rule names (active_review_missing, active_review_not_accepted, active_review_base_drift) so editor index errors stay aligned with build/validate findings.

rule: str
message: str
__init__(rule: str, message: str) None
class booktx.translation_store.EffectiveCandidateSelection(candidate: TranslationCandidate | TranslationReviewCandidate, selected_kind: Literal['translation', 'review'], selected_ref: str, version_ref: str | None, review_ref: str | None, review_chain: list[str])[source]

Effective output candidate plus provenance metadata.

selected_kind/selected_ref identify which candidate produces the output. version_ref is the base translation version the output derives from; review_ref is the selected review ref when the output is a review candidate. review_chain is the ordered review chain (earliest first) for review selections and empty for direct translations.

candidate: TranslationCandidate | TranslationReviewCandidate
selected_kind: Literal['translation', 'review']
selected_ref: str
version_ref: str | None
review_ref: str | None
review_chain: list[str]
__init__(candidate: TranslationCandidate | TranslationReviewCandidate, selected_kind: Literal['translation', 'review'], selected_ref: str, version_ref: str | None, review_ref: str | None, review_chain: list[str]) None
booktx.translation_store.active_candidate(record: StoredTranslationRecordV2) TranslationCandidate | None[source]

Return the active candidate for a record, if any.

booktx.translation_store.active_review_candidate(record: StoredTranslationRecordV2) TranslationReviewCandidate | None[source]

Return the usable active review candidate, or None if unusable.

A review is usable only when present, accepted, and chain-valid. Stale, rejected, cyclic, or invalid-pass-order active reviews are not returned.

booktx.translation_store.effective_candidate_selection(record: StoredTranslationRecordV2, *, strict_active_review: bool = True) EffectiveCandidateSelection | EffectiveCandidateError | None[source]

Resolve the effective candidate plus selection metadata for a record.

With strict_active_review=True (the default), a set-but-unusable active_review returns an EffectiveCandidateError instead of silently falling back to the active translation. This is the safe mode for editor indexes, which must never mask an invalid active review. With strict_active_review=False an unusable active review falls back to the accepted active translation, mirroring effective_target_candidate().

Returns None only when there is no accepted effective candidate.

booktx.translation_store.effective_target_candidate(record: StoredTranslationRecordV2) TranslationCandidate | TranslationReviewCandidate | None[source]

Resolve the effective output target for a record.

Prefers the active review candidate when present and chain-valid; otherwise falls back to the active translation version.

booktx.translation_store.ensure_store_record(store: TranslationStoreV2, record_ref: RecordRef | str, *, source: str, source_sha256: str) StoredTranslationRecordV2[source]

Return the v2 store record, creating it when needed.

booktx.translation_store.find_candidate(record: StoredTranslationRecordV2, version_ref: str) TranslationCandidate | None[source]

Return the matching candidate, if present.

booktx.translation_store.find_review_candidate(record: StoredTranslationRecordV2, review_ref: str) TranslationReviewCandidate | None[source]

Return the matching review candidate, if present.

booktx.translation_store.migrate_legacy_store(legacy: TranslationStore, *, source_records: dict[str, SourceRecordView], version_ref: str = '1.1') MigrationResult[source]

Convert a legacy store into v2 and report any records missing source data.

booktx.translation_store.legacy_store_to_v2(legacy: TranslationStore, *, source_records: dict[str, SourceRecordView] | None = None) TranslationStoreV2[source]

Convert a legacy flat store into the nested v2 shape in memory.

booktx.translation_store.resolve_review_base(record: StoredTranslationRecordV2, base_kind: Literal['translation', 'review'], base_ref: str) TranslationCandidate | TranslationReviewCandidate | None[source]

Resolve the base candidate a review was derived from.

booktx.translation_store.review_candidate_is_stale(record: StoredTranslationRecordV2, review: TranslationReviewCandidate) bool[source]

Return True when the direct base of a review is missing or drifted.

booktx.translation_store.review_chain_is_stale(record: StoredTranslationRecordV2, review_ref: str) bool[source]

Return True when any base in the review derivation chain is missing or drifted.

Walks from review_ref back to the translation base, rejecting missing bases, target-hash mismatches, and cycles.

booktx.translation_store.review_chain_refs(record: StoredTranslationRecordV2, review_ref: str) list[str] | None[source]

Return the ordered review chain ending at review_ref (earliest first).

The chain excludes the translation base. For R2.1 based on R1.1 based on a translation version, returns ["R1.1", "R2.1"]. Returns None when the chain is missing, stale (base_target_sha256 drift), cyclic, or violates the lexicographic (pass, run) order (so same-pass reruns such as R1.2 from R1.1 are valid).

booktx.translation_store.sha256_text(text: str) str[source]

Return the canonical SHA256 hex digest for a target/base text.

booktx.translation_store.upsert_translation_version(record: StoredTranslationRecordV2, version_ref: str, target: str, *, updated_at: str, status: str = 'accepted', activate: bool = False, baseline_ref: str | None = None, baseline_sha256: str | None = None, context_view_sha256: str | None = None, context_view_path: str | None = None, context_notes_scope: str | None = None, context_target_chapter_id: str | None = None, context_notes_through_chapter_id: str | None = None) TranslationCandidate[source]

Insert or update a candidate version on one record.

Version-ledger identity, context hashing, and version resolution helpers.

class booktx.versioning.VersionResolution(ledger: TranslationVersionLedger, identity: TranslationIdentity, version_ref: str, version: int, subversion: int, baseline_sha256: str, context_sha256: str, created_track: bool, created_subversion: bool)[source]

Resolved current translation version metadata.

ledger: TranslationVersionLedger
identity: TranslationIdentity
version_ref: str
version: int
subversion: int
baseline_sha256: str
context_sha256: str
created_track: bool
created_subversion: bool
__init__(ledger: TranslationVersionLedger, identity: TranslationIdentity, version_ref: str, version: int, subversion: int, baseline_sha256: str, context_sha256: str, created_track: bool, created_subversion: bool) None
booktx.versioning.canonical_json_sha256(data: object) str[source]

Hash canonical JSON with stable key ordering and separators.

booktx.versioning.current_baseline_sha256(project: Project) str[source]

Return the semantic baseline hash for the current project context.

booktx.versioning.current_context_sha256(project: Project) str[source]

Return the canonical context hash for the current project context.

booktx.versioning.default_identity() TranslationIdentity[source]

Return deterministic local defaults when no identity file exists.

booktx.versioning.fork_current_context(project: Project, *, note: str | None = None, context_label: str | None = None) VersionResolution[source]

Force a new subversion even when the current baseline hash is unchanged.

booktx.versioning.lookup_version(ledger: TranslationVersionLedger, version_ref: str) tuple[TranslationTrackLedgerEntry, TranslationSubversionLedgerEntry][source]

Resolve one dotted version reference against the ledger.

booktx.versioning.resolve_current_version(project: Project, *, actor: str | None = None, harness: str | None = None, model: str | None = None, force_new_context: bool = False, context_label: str | None = None, note: str | None = None) VersionResolution[source]

Resolve and persist the current ledger version for a translation write.

booktx.versioning.resolve_identity(project: Project, *, actor: str | None = None, harness: str | None = None, model: str | None = None) TranslationIdentity[source]

Resolve identity from explicit overrides, stored defaults, and fallbacks.

booktx.versioning.select_active_version(project: Project, version_ref: str) TranslationVersionLedger[source]

Select the project-wide active version in the ledger.

booktx.versioning.set_track_label(project: Project, major_version: int, label: str) TranslationVersionLedger[source]

Set the label for one major track.