bookwright-cli 0.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (149) hide show
  1. bookwright/__init__.py +3 -0
  2. bookwright/__main__.py +6 -0
  3. bookwright/cli.py +19 -0
  4. bookwright/commands/__init__.py +0 -0
  5. bookwright/commands/_envelope.py +36 -0
  6. bookwright/commands/check.py +75 -0
  7. bookwright/commands/graph/__init__.py +23 -0
  8. bookwright/commands/graph/build.py +157 -0
  9. bookwright/commands/graph/envelope.py +26 -0
  10. bookwright/commands/graph/query.py +98 -0
  11. bookwright/commands/init/__init__.py +5 -0
  12. bookwright/commands/init/conflict.py +107 -0
  13. bookwright/commands/init/envelope.py +322 -0
  14. bookwright/commands/init/git.py +96 -0
  15. bookwright/commands/init/main.py +263 -0
  16. bookwright/commands/init/resolve.py +193 -0
  17. bookwright/commands/init/scaffold.py +242 -0
  18. bookwright/commands/init/validate.py +172 -0
  19. bookwright/commands/integration/__init__.py +22 -0
  20. bookwright/commands/integration/use.py +120 -0
  21. bookwright/commands/validate.py +160 -0
  22. bookwright/commands/version.py +35 -0
  23. bookwright/core/__init__.py +35 -0
  24. bookwright/core/_blocks.py +239 -0
  25. bookwright/core/_build.py +154 -0
  26. bookwright/core/_research_block.py +56 -0
  27. bookwright/core/_translate.py +90 -0
  28. bookwright/core/errors.py +127 -0
  29. bookwright/core/iso639_1.py +200 -0
  30. bookwright/core/manifest.py +343 -0
  31. bookwright/errors.py +47 -0
  32. bookwright/golem/__init__.py +71 -0
  33. bookwright/golem/base.py +200 -0
  34. bookwright/golem/errors.py +29 -0
  35. bookwright/golem/modules/__init__.py +1 -0
  36. bookwright/golem/modules/character.py +109 -0
  37. bookwright/golem/modules/event.py +91 -0
  38. bookwright/golem/modules/feature.py +161 -0
  39. bookwright/golem/modules/inference.py +41 -0
  40. bookwright/golem/modules/narrative.py +55 -0
  41. bookwright/golem/modules/provenance.py +197 -0
  42. bookwright/golem/modules/relationship.py +38 -0
  43. bookwright/golem/modules/setting.py +30 -0
  44. bookwright/golem/namespaces.py +332 -0
  45. bookwright/golem/serialize.py +25 -0
  46. bookwright/golem/slug.py +22 -0
  47. bookwright/indexers/__init__.py +47 -0
  48. bookwright/indexers/base.py +55 -0
  49. bookwright/indexers/errors.py +80 -0
  50. bookwright/indexers/rdflib_indexer.py +89 -0
  51. bookwright/integrations/__init__.py +155 -0
  52. bookwright/integrations/base.py +117 -0
  53. bookwright/integrations/claude/__init__.py +29 -0
  54. bookwright/integrations/constants.py +38 -0
  55. bookwright/integrations/descriptions.py +48 -0
  56. bookwright/integrations/errors.py +170 -0
  57. bookwright/integrations/generic/__init__.py +56 -0
  58. bookwright/integrations/lint.py +160 -0
  59. bookwright/integrations/materialize.py +202 -0
  60. bookwright/integrations/options.py +203 -0
  61. bookwright/io/__init__.py +1 -0
  62. bookwright/io/bible.py +500 -0
  63. bookwright/io/errors.py +98 -0
  64. bookwright/io/frontmatter.py +61 -0
  65. bookwright/io/fs.py +226 -0
  66. bookwright/io/manuscript.py +15 -0
  67. bookwright/io/project.py +21 -0
  68. bookwright/io/report.py +107 -0
  69. bookwright/io/research.py +427 -0
  70. bookwright/resources/__init__.py +1 -0
  71. bookwright/resources/commands/bookwright-analyze.md +66 -0
  72. bookwright/resources/commands/bookwright-bible.md +96 -0
  73. bookwright/resources/commands/bookwright-checklist.md +67 -0
  74. bookwright/resources/commands/bookwright-clarify.md +65 -0
  75. bookwright/resources/commands/bookwright-constitution.md +79 -0
  76. bookwright/resources/commands/bookwright-continuity.md +70 -0
  77. bookwright/resources/commands/bookwright-draft.md +74 -0
  78. bookwright/resources/commands/bookwright-outline.md +71 -0
  79. bookwright/resources/commands/bookwright-research.md +107 -0
  80. bookwright/resources/commands/bookwright-scenes.md +66 -0
  81. bookwright/resources/commands/bookwright-synopsis.md +67 -0
  82. bookwright/resources/commands/bookwright-verify.md +136 -0
  83. bookwright/resources/commands/references/golem-character.md +65 -0
  84. bookwright/resources/commands/references/golem-events-timeline.md +56 -0
  85. bookwright/resources/commands/references/golem-relationships.md +53 -0
  86. bookwright/resources/commands/references/greimas-actants.md +57 -0
  87. bookwright/resources/commands/references/pending-protocol.md +72 -0
  88. bookwright/resources/commands/references/propp-functions.md +54 -0
  89. bookwright/resources/commands/references/research-format.md +136 -0
  90. bookwright/resources/project/.bookwright/cache/.gitkeep +0 -0
  91. bookwright/resources/project/.bookwright/schema/.gitkeep +0 -0
  92. bookwright/resources/project/.bookwright/templates/.gitkeep +0 -0
  93. bookwright/resources/project/.gitignore +23 -0
  94. bookwright/resources/project/README.md.j2 +40 -0
  95. bookwright/resources/project/__init__.py +6 -0
  96. bookwright/resources/project/bible/characters/.gitkeep +0 -0
  97. bookwright/resources/project/bible/constitution.md.j2 +74 -0
  98. bookwright/resources/project/bible/glossary.md +36 -0
  99. bookwright/resources/project/bible/locations/.gitkeep +0 -0
  100. bookwright/resources/project/bible/pov-structure.md +43 -0
  101. bookwright/resources/project/bible/relationships.md +36 -0
  102. bookwright/resources/project/bible/research/_index.md +28 -0
  103. bookwright/resources/project/bible/research/sources.md +23 -0
  104. bookwright/resources/project/bible/settings/.gitkeep +0 -0
  105. bookwright/resources/project/bible/subplots.md +35 -0
  106. bookwright/resources/project/bible/themes.md +36 -0
  107. bookwright/resources/project/bible/timeline.md +38 -0
  108. bookwright/resources/project/manuscript/.gitkeep +0 -0
  109. bookwright/resources/project/outline/arcs.md +34 -0
  110. bookwright/resources/project/outline/scenes.md +31 -0
  111. bookwright/resources/project/outline/structure.md +35 -0
  112. bookwright/resources/project/outline/synopsis.md +25 -0
  113. bookwright/resources/schemas/__init__.py +19 -0
  114. bookwright/resources/schemas/golem-1.1/VERSION +1 -0
  115. bookwright/resources/schemas/golem-1.1/golem.ttl +1947 -0
  116. bookwright/resources/schemas/golem-1.1/version.json +8 -0
  117. bookwright/resources/templates/__init__.py +1 -0
  118. bookwright/resources/templates/bible/character.md.tmpl +63 -0
  119. bookwright/resources/templates/bible/location.md.tmpl +37 -0
  120. bookwright/resources/templates/bible/research/_index.md.tmpl +25 -0
  121. bookwright/resources/templates/bible/research/sources.md.tmpl +21 -0
  122. bookwright/resources/templates/bible/research/tema.md.tmpl +37 -0
  123. bookwright/resources/templates/bible/setting.md.tmpl +38 -0
  124. bookwright/resources/templates/manifest.template.toml +79 -0
  125. bookwright/resources/templates/manuscript/chapter.md.tmpl +36 -0
  126. bookwright/resources/templates/scenes/scene.md.tmpl +37 -0
  127. bookwright/resources/vocabularies/__init__.py +6 -0
  128. bookwright/resources/vocabularies/greimas.ttl +4 -0
  129. bookwright/resources/vocabularies/propp.ttl +4 -0
  130. bookwright/resources/vocabularies/sources.ttl +82 -0
  131. bookwright/validation/__init__.py +33 -0
  132. bookwright/validation/anchor_queries.py +223 -0
  133. bookwright/validation/base.py +233 -0
  134. bookwright/validation/queries.py +197 -0
  135. bookwright/validation/registry.py +185 -0
  136. bookwright/validation/report.py +106 -0
  137. bookwright/validation/runner.py +65 -0
  138. bookwright/validation/validators/__init__.py +9 -0
  139. bookwright/validation/validators/character_presence.py +202 -0
  140. bookwright/validation/validators/factual_anchor.py +291 -0
  141. bookwright/validation/validators/focalization.py +152 -0
  142. bookwright/validation/validators/setting_continuity.py +100 -0
  143. bookwright/validation/validators/temporal.py +277 -0
  144. bookwright_cli-0.2.0.dist-info/METADATA +218 -0
  145. bookwright_cli-0.2.0.dist-info/RECORD +149 -0
  146. bookwright_cli-0.2.0.dist-info/WHEEL +4 -0
  147. bookwright_cli-0.2.0.dist-info/entry_points.txt +2 -0
  148. bookwright_cli-0.2.0.dist-info/licenses/LICENSE +202 -0
  149. bookwright_cli-0.2.0.dist-info/licenses/NOTICE +14 -0
@@ -0,0 +1,154 @@
1
+ """Builder for fresh manifests from minimal inputs (FR-015..FR-017).
2
+
3
+ Internal helper for `bookwright.core.manifest`. `Manifest.build()` is the
4
+ public entry point and delegates here. Kept separate so the public model
5
+ module stays under the Principle IV 500-line ceiling.
6
+ See specs/002-manifest-model/contracts/manifest_api.md §`Manifest.build`.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from functools import cache
12
+ from importlib.resources import files as _resource_files
13
+ from typing import TYPE_CHECKING, Any
14
+
15
+ import tomlkit
16
+ from packaging.version import InvalidVersion, Version
17
+ from pydantic import ValidationError
18
+ from tomlkit.toml_document import TOMLDocument
19
+
20
+ from bookwright.core._translate import _translate_validation_error
21
+
22
+ if TYPE_CHECKING:
23
+ from bookwright.core.manifest import Manifest
24
+
25
+
26
+ _BUILD_OVERRIDE_ALLOWLIST_TABLE: dict[str, tuple[str, str]] = {
27
+ "language": ("book", "language"),
28
+ "type": ("book", "type"),
29
+ "subtitle": ("book", "subtitle"),
30
+ "genre": ("book", "genre"),
31
+ "target_length_words": ("book", "target_length_words"),
32
+ "status": ("book", "status"),
33
+ "book_metadata": ("book", "metadata"),
34
+ "vocabularies_active": ("vocabularies", "active"),
35
+ "validators_enabled": ("validators", "enabled"),
36
+ "validators_disabled": ("validators", "disabled"),
37
+ "validators_custom": ("validators", "custom"),
38
+ "paths_manuscript": ("paths", "manuscript"),
39
+ "paths_bible": ("paths", "bible"),
40
+ "paths_outline": ("paths", "outline"),
41
+ "paths_graph": ("paths", "graph"),
42
+ "paths_constitution": ("paths", "constitution"),
43
+ "integration_options": ("integration", "options"),
44
+ "integration_skills_dir": ("integration", "skills_dir"),
45
+ "manifest_version": ("bookwright", "manifest_version"),
46
+ "schema_version": ("bookwright", "schema_version"),
47
+ "cli_version_min": ("bookwright", "cli_version_min"),
48
+ "uri_base": ("bookwright", "uri_base"),
49
+ "indexer": ("bookwright", "indexer"),
50
+ }
51
+
52
+
53
+ @cache
54
+ def _template_text() -> str:
55
+ """Cached read of the bundled manifest template (text only)."""
56
+
57
+ resource = _resource_files("bookwright.resources.templates").joinpath("manifest.template.toml")
58
+ return resource.read_text(encoding="utf-8")
59
+
60
+
61
+ def _load_template_document() -> TOMLDocument:
62
+ """Return a fresh tomlkit document parsed from the cached template text.
63
+
64
+ The on-disk read is amortised via `_template_text()`; each call still
65
+ parses a new document so callers get independent mutable trees.
66
+ """
67
+
68
+ return tomlkit.parse(_template_text())
69
+
70
+
71
+ def _build_manifest( # noqa: PLR0913 — model_cls + 3 user inputs + 2 injected deps; injection breaks an import cycle with manifest.py.
72
+ model_cls: type[Manifest],
73
+ *,
74
+ title: str,
75
+ authors: list[str],
76
+ integration_key: str,
77
+ installed_version: str,
78
+ default_skills_dir: dict[str, str],
79
+ **overrides: Any,
80
+ ) -> Manifest:
81
+ """Construct a fresh manifest from minimal inputs.
82
+
83
+ `installed_version` and `default_skills_dir` are injected by the caller
84
+ so this helper does not need to import `bookwright.core.manifest`,
85
+ which would close an import cycle.
86
+
87
+ Override semantics: passing an override as `None` is treated as
88
+ "not supplied" (the template default stands). This keeps conditional
89
+ propagation patterns (`build(..., subtitle=user_subtitle)` where
90
+ `user_subtitle` may be None) from leaking tomlkit's
91
+ `ConvertError` on `NoneType`.
92
+ """
93
+
94
+ unknown = set(overrides) - _BUILD_OVERRIDE_ALLOWLIST_TABLE.keys()
95
+ if unknown:
96
+ names = ", ".join(repr(n) for n in sorted(unknown))
97
+ raise TypeError(f"build() got unexpected keyword argument(s): {names}")
98
+
99
+ # Drop explicit-None overrides up-front (treated as "not supplied").
100
+ effective_overrides: dict[str, Any] = {k: v for k, v in overrides.items() if v is not None}
101
+
102
+ document = _load_template_document()
103
+
104
+ # Apply the three required positional inputs.
105
+ document["book"]["title"] = title
106
+ document["book"]["authors"] = list(authors)
107
+ document["integration"]["key"] = integration_key
108
+
109
+ # cli_version_min default = installed CLI version. When the caller did
110
+ # NOT override cli_version_min and the installed version isn't valid
111
+ # PEP 440, fail with a clear message that blames the *environment*,
112
+ # not a field the user never supplied.
113
+ if "cli_version_min" not in effective_overrides:
114
+ try:
115
+ Version(installed_version)
116
+ except InvalidVersion as exc:
117
+ raise RuntimeError(
118
+ f"installed CLI version {installed_version!r} is not a valid "
119
+ f"PEP 440 string; cannot derive a cli_version_min default. "
120
+ f"Pass cli_version_min= explicitly."
121
+ ) from exc
122
+ document["bookwright"]["cli_version_min"] = installed_version
123
+
124
+ # uri_base has no default at build time (data-model.md). If the caller
125
+ # did not override it, surface the missing-field failure.
126
+ if "uri_base" not in effective_overrides:
127
+ del document["bookwright"]["uri_base"]
128
+
129
+ # skills_dir default depends on integration_key (FR-017).
130
+ if "integration_skills_dir" in effective_overrides:
131
+ document["integration"]["skills_dir"] = effective_overrides["integration_skills_dir"]
132
+ else:
133
+ try:
134
+ document["integration"]["skills_dir"] = default_skills_dir[integration_key]
135
+ except KeyError as exc:
136
+ raise TypeError(
137
+ f"build() requires explicit integration_skills_dir for unknown "
138
+ f"integration_key={integration_key!r}"
139
+ ) from exc
140
+
141
+ # Overlay caller-provided overrides.
142
+ for kwarg, value in effective_overrides.items():
143
+ if kwarg == "integration_skills_dir":
144
+ continue # already applied above
145
+ target_block, target_key = _BUILD_OVERRIDE_ALLOWLIST_TABLE[kwarg]
146
+ document[target_block][target_key] = value
147
+
148
+ # Re-parse through Pydantic for end-to-end validation (FR-016).
149
+ try:
150
+ instance = model_cls.model_validate(document.unwrap())
151
+ except ValidationError as exc:
152
+ raise _translate_validation_error(exc) from exc
153
+ instance._document = document
154
+ return instance
@@ -0,0 +1,56 @@
1
+ """The ``[research]`` manifest block model (FR-011..FR-016).
2
+
3
+ Extracted from ``manifest.py`` so that module stays under the Principle IV
4
+ 500-line ceiling (it is already 535 lines). ``Manifest`` imports
5
+ :class:`ResearchBlock` and exposes it as ``Manifest.research``; the public surface
6
+ re-exports it from ``bookwright.core``.
7
+
8
+ The three reliability values are duplicated from
9
+ ``golem.namespaces.RELIABILITY_IRI`` **on purpose**: ``core`` must not import
10
+ ``golem`` (layer direction, and the registry already late-imports to avoid a
11
+ cycle). A unit test asserts the ``Literal`` stays in sync with that vocabulary
12
+ (RB-8, research §R5) — the same anti-drift discipline as the SC-009 gate.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ from typing import Literal
18
+
19
+ from pydantic import BaseModel, ConfigDict, Field, field_validator
20
+ from pydantic_core import PydanticCustomError
21
+
22
+ from bookwright.core.iso639_1 import ISO_639_1_CODES
23
+
24
+
25
+ class ResearchBlock(BaseModel):
26
+ """``[research]`` block — optional research-system configuration.
27
+
28
+ All three fields carry documented defaults, so a manifest **without** a
29
+ ``[research]`` block loads with the same values an explicit default block
30
+ would produce (FR-012). ``extra="forbid", strict=True`` matches every sibling
31
+ block — an unknown key inside ``[research]`` is a validation error.
32
+ """
33
+
34
+ model_config = ConfigDict(extra="forbid", strict=True)
35
+
36
+ enabled: bool = True
37
+ source_languages: list[str] = Field(default_factory=list)
38
+ min_reliability_for_anchor: Literal["alta", "media", "baja"] = "media"
39
+
40
+ @field_validator("source_languages", mode="after")
41
+ @classmethod
42
+ def _check_source_languages(cls, value: list[str]) -> list[str]:
43
+ """Reject any non-ISO-639-1 entry, naming ``source_languages[i]`` (FR-016).
44
+
45
+ Mirrors ``BookBlock._check_language``; the ``index``/``value`` context is
46
+ spliced into the public ``field_path`` by ``_translate_validation_error``.
47
+ """
48
+
49
+ for index, entry in enumerate(value):
50
+ if entry not in ISO_639_1_CODES:
51
+ raise PydanticCustomError(
52
+ "not_iso_639_1",
53
+ "source_languages[{index}] '{value}' is not a valid ISO 639-1 code",
54
+ {"index": index, "value": entry},
55
+ )
56
+ return value
@@ -0,0 +1,90 @@
1
+ """Pydantic `ValidationError` → public `ManifestValidationError` translation.
2
+
3
+ Internal helper for `bookwright.core.manifest`. Kept separate so the
4
+ public model module stays under the Principle IV 500-line ceiling.
5
+ See specs/002-manifest-model/contracts/manifest_api.md.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from typing import Any
11
+
12
+ from pydantic import ValidationError
13
+
14
+ from bookwright.core.errors import ManifestValidationError, _FieldFailure
15
+
16
+ _PYDANTIC_TYPE_TO_KIND: dict[str, str] = {
17
+ "missing": "missing",
18
+ "string_type": "not_a_string",
19
+ "int_type": "not_an_integer",
20
+ "list_type": "not_a_list",
21
+ "dict_type": "not_a_dict",
22
+ "bool_type": "not_a_bool",
23
+ "literal_error": "not_in_enum",
24
+ "extra_forbidden": "unknown_key",
25
+ }
26
+
27
+ # Model-level errors (raised from `@model_validator`) carry no field location.
28
+ # Remap them to the field they conceptually belong to.
29
+ _ROOT_ERROR_REMAP: dict[str, str] = {
30
+ "installed_too_old": "bookwright.cli_version_min",
31
+ "installed_not_pep440": "bookwright.cli_version_min",
32
+ }
33
+
34
+
35
+ def _format_loc(loc: tuple[Any, ...]) -> str:
36
+ """Render a Pydantic location tuple as a dotted path with `[N]` indices."""
37
+
38
+ parts: list[str] = []
39
+ for piece in loc:
40
+ if isinstance(piece, int):
41
+ assert parts, "Pydantic loc never starts with an int"
42
+ parts[-1] = f"{parts[-1]}[{piece}]"
43
+ else:
44
+ parts.append(str(piece))
45
+ return ".".join(parts)
46
+
47
+
48
+ def _translate_validation_error(exc: ValidationError) -> ManifestValidationError:
49
+ """Convert a `pydantic.ValidationError` to the public `ManifestValidationError` shape."""
50
+
51
+ failures: list[_FieldFailure] = []
52
+ for err in exc.errors():
53
+ loc = err.get("loc", ())
54
+ field_path = _format_loc(loc)
55
+ rejected = err.get("input")
56
+ message = err.get("msg", "")
57
+ err_type = err.get("type", "")
58
+ ctx = err.get("ctx") or {}
59
+
60
+ if not field_path and err_type in _ROOT_ERROR_REMAP:
61
+ field_path = _ROOT_ERROR_REMAP[err_type]
62
+
63
+ # Pydantic built-in error types map to short kinds; custom errors
64
+ # (PydanticCustomError) carry their `error_type` here verbatim.
65
+ kind = _PYDANTIC_TYPE_TO_KIND.get(err_type, err_type or "validation")
66
+
67
+ # Prefer the offending value the validator named in `ctx["value"]`
68
+ # (e.g. authors[N] entry) over the validator's whole input.
69
+ if "value" in ctx:
70
+ rejected = ctx["value"]
71
+
72
+ rule_id = f"{field_path}.{kind}" if field_path else kind
73
+
74
+ # A field-level validator (e.g. `_check_authors`) that walks a list
75
+ # cannot embed the offending index in Pydantic's loc. When it surfaces
76
+ # `ctx["index"]` we splice the `[N]` suffix here so the public
77
+ # `field_path` matches the published contract (book.authors[N]) while
78
+ # `rule_id` stays index-free.
79
+ if "index" in ctx and isinstance(ctx["index"], int):
80
+ field_path = f"{field_path}[{ctx['index']}]"
81
+
82
+ failures.append(
83
+ _FieldFailure(
84
+ field_path=field_path,
85
+ rejected_value=rejected,
86
+ rule_id=rule_id,
87
+ message=message,
88
+ )
89
+ )
90
+ return ManifestValidationError(tuple(failures))
@@ -0,0 +1,127 @@
1
+ """Public exception hierarchy and warning model for the manifest module.
2
+
3
+ The error JSON shape is the canonical envelope owned by ``BookwrightError``
4
+ (this iteration normalized the former flat ``{"error": …}`` bodies onto it).
5
+ See specs/018-unified-error-envelope/contracts/error-envelope.md.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from dataclasses import dataclass
11
+ from pathlib import Path
12
+ from typing import Any
13
+
14
+ from pydantic import BaseModel
15
+
16
+ from bookwright.errors import BookwrightError
17
+
18
+
19
+ @dataclass(frozen=True)
20
+ class _FieldFailure:
21
+ """One field-level validation failure.
22
+
23
+ Internal type. The public JSON form is emitted under
24
+ ``ManifestValidationError``'s ``details["failures"]``.
25
+ """
26
+
27
+ field_path: str
28
+ rejected_value: Any
29
+ rule_id: str
30
+ message: str
31
+
32
+
33
+ class ManifestError(BookwrightError):
34
+ """Base for every failure mode the manifest module owns.
35
+
36
+ Abstract: declares no ``code`` and is never serialized directly.
37
+ """
38
+
39
+
40
+ class ManifestNotFoundError(ManifestError):
41
+ """The manifest file does not exist."""
42
+
43
+ code = "manifest_not_found"
44
+
45
+ def __init__(self, path: Path | str) -> None:
46
+ self.path = Path(path).resolve()
47
+ super().__init__(f"no manifest at {self.path}", {"path": str(self.path)})
48
+
49
+
50
+ class ManifestSyntaxError(ManifestError):
51
+ """The manifest file exists but is not valid TOML."""
52
+
53
+ code = "manifest_syntax"
54
+
55
+ def __init__(
56
+ self,
57
+ path: Path | str,
58
+ line: int | None,
59
+ column: int | None,
60
+ message: str,
61
+ ) -> None:
62
+ self.path = Path(path).resolve()
63
+ self.line = line
64
+ self.column = column
65
+ super().__init__(
66
+ message,
67
+ {"field": f"bookwright.{self.path.name}", "line": line, "column": column},
68
+ )
69
+
70
+
71
+ class ManifestValidationError(ManifestError):
72
+ """One or more field-level validation failures (FR-004..FR-013)."""
73
+
74
+ code = "manifest_validation"
75
+
76
+ def __init__(self, failures: tuple[_FieldFailure, ...]) -> None:
77
+ if not failures:
78
+ raise ValueError("ManifestValidationError requires at least one failure")
79
+ self.failures = failures
80
+ first = failures[0]
81
+ summary = (
82
+ f"{len(failures)} validation failure(s); first: {first.field_path}: {first.message}"
83
+ )
84
+ super().__init__(
85
+ summary,
86
+ {
87
+ "failures": [
88
+ {
89
+ "field": f.field_path,
90
+ "value": f.rejected_value,
91
+ "rule": f.rule_id,
92
+ "message": f.message,
93
+ }
94
+ for f in failures
95
+ ]
96
+ },
97
+ )
98
+
99
+
100
+ class ManifestOverwriteError(ManifestError):
101
+ """Refuse to overwrite an existing manifest (FR-019)."""
102
+
103
+ code = "manifest_overwrite_refused"
104
+
105
+ def __init__(self, path: Path | str) -> None:
106
+ self.path = Path(path).resolve()
107
+ super().__init__(
108
+ f"refuse to overwrite existing manifest at {self.path} (pass overwrite=True to force)",
109
+ {"path": str(self.path)},
110
+ )
111
+
112
+
113
+ class ManifestWarning(BaseModel):
114
+ """A non-fatal note attached to `Manifest.warnings` during `load()`."""
115
+
116
+ rule_id: str
117
+ field_path: str
118
+ offending_value: Any
119
+ message: str
120
+
121
+ def to_json(self) -> dict[str, Any]:
122
+ return {
123
+ "rule": self.rule_id,
124
+ "field": self.field_path,
125
+ "value": self.offending_value,
126
+ "message": self.message,
127
+ }
@@ -0,0 +1,200 @@
1
+ """ISO 639-1 alpha-2 language codes.
2
+
3
+ Source: ISO 639-1 (Alpha-2 Codes for Representation of Names of Languages).
4
+ The full registry is bundled in-package as a frozenset literal. No network
5
+ access, no curation, no editorial filtering — constructed languages with
6
+ official codes (eo, la, cu, ...) are included because they are part of the
7
+ standard. See research §R3 for the rationale.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ ISO_639_1_CODES: frozenset[str] = frozenset(
13
+ {
14
+ "aa",
15
+ "ab",
16
+ "ae",
17
+ "af",
18
+ "ak",
19
+ "am",
20
+ "an",
21
+ "ar",
22
+ "as",
23
+ "av",
24
+ "ay",
25
+ "az",
26
+ "ba",
27
+ "be",
28
+ "bg",
29
+ "bh",
30
+ "bi",
31
+ "bm",
32
+ "bn",
33
+ "bo",
34
+ "br",
35
+ "bs",
36
+ "ca",
37
+ "ce",
38
+ "ch",
39
+ "co",
40
+ "cr",
41
+ "cs",
42
+ "cu",
43
+ "cv",
44
+ "cy",
45
+ "da",
46
+ "de",
47
+ "dv",
48
+ "dz",
49
+ "ee",
50
+ "el",
51
+ "en",
52
+ "eo",
53
+ "es",
54
+ "et",
55
+ "eu",
56
+ "fa",
57
+ "ff",
58
+ "fi",
59
+ "fj",
60
+ "fo",
61
+ "fr",
62
+ "fy",
63
+ "ga",
64
+ "gd",
65
+ "gl",
66
+ "gn",
67
+ "gu",
68
+ "gv",
69
+ "ha",
70
+ "he",
71
+ "hi",
72
+ "ho",
73
+ "hr",
74
+ "ht",
75
+ "hu",
76
+ "hy",
77
+ "hz",
78
+ "ia",
79
+ "id",
80
+ "ie",
81
+ "ig",
82
+ "ii",
83
+ "ik",
84
+ "io",
85
+ "is",
86
+ "it",
87
+ "iu",
88
+ "ja",
89
+ "jv",
90
+ "ka",
91
+ "kg",
92
+ "ki",
93
+ "kj",
94
+ "kk",
95
+ "kl",
96
+ "km",
97
+ "kn",
98
+ "ko",
99
+ "kr",
100
+ "ks",
101
+ "ku",
102
+ "kv",
103
+ "kw",
104
+ "ky",
105
+ "la",
106
+ "lb",
107
+ "lg",
108
+ "li",
109
+ "ln",
110
+ "lo",
111
+ "lt",
112
+ "lu",
113
+ "lv",
114
+ "mg",
115
+ "mh",
116
+ "mi",
117
+ "mk",
118
+ "ml",
119
+ "mn",
120
+ "mr",
121
+ "ms",
122
+ "mt",
123
+ "my",
124
+ "na",
125
+ "nb",
126
+ "nd",
127
+ "ne",
128
+ "ng",
129
+ "nl",
130
+ "nn",
131
+ "no",
132
+ "nr",
133
+ "nv",
134
+ "ny",
135
+ "oc",
136
+ "oj",
137
+ "om",
138
+ "or",
139
+ "os",
140
+ "pa",
141
+ "pi",
142
+ "pl",
143
+ "ps",
144
+ "pt",
145
+ "qu",
146
+ "rm",
147
+ "rn",
148
+ "ro",
149
+ "ru",
150
+ "rw",
151
+ "sa",
152
+ "sc",
153
+ "sd",
154
+ "se",
155
+ "sg",
156
+ "si",
157
+ "sk",
158
+ "sl",
159
+ "sm",
160
+ "sn",
161
+ "so",
162
+ "sq",
163
+ "sr",
164
+ "ss",
165
+ "st",
166
+ "su",
167
+ "sv",
168
+ "sw",
169
+ "ta",
170
+ "te",
171
+ "tg",
172
+ "th",
173
+ "ti",
174
+ "tk",
175
+ "tl",
176
+ "tn",
177
+ "to",
178
+ "tr",
179
+ "ts",
180
+ "tt",
181
+ "tw",
182
+ "ty",
183
+ "ug",
184
+ "uk",
185
+ "ur",
186
+ "uz",
187
+ "ve",
188
+ "vi",
189
+ "vo",
190
+ "wa",
191
+ "wo",
192
+ "xh",
193
+ "yi",
194
+ "yo",
195
+ "za",
196
+ "zh",
197
+ "zu",
198
+ }
199
+ )
200
+ """All 184 ISO 639-1 alpha-2 codes (lowercase)."""