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.
- bookwright/__init__.py +3 -0
- bookwright/__main__.py +6 -0
- bookwright/cli.py +19 -0
- bookwright/commands/__init__.py +0 -0
- bookwright/commands/_envelope.py +36 -0
- bookwright/commands/check.py +75 -0
- bookwright/commands/graph/__init__.py +23 -0
- bookwright/commands/graph/build.py +157 -0
- bookwright/commands/graph/envelope.py +26 -0
- bookwright/commands/graph/query.py +98 -0
- bookwright/commands/init/__init__.py +5 -0
- bookwright/commands/init/conflict.py +107 -0
- bookwright/commands/init/envelope.py +322 -0
- bookwright/commands/init/git.py +96 -0
- bookwright/commands/init/main.py +263 -0
- bookwright/commands/init/resolve.py +193 -0
- bookwright/commands/init/scaffold.py +242 -0
- bookwright/commands/init/validate.py +172 -0
- bookwright/commands/integration/__init__.py +22 -0
- bookwright/commands/integration/use.py +120 -0
- bookwright/commands/validate.py +160 -0
- bookwright/commands/version.py +35 -0
- bookwright/core/__init__.py +35 -0
- bookwright/core/_blocks.py +239 -0
- bookwright/core/_build.py +154 -0
- bookwright/core/_research_block.py +56 -0
- bookwright/core/_translate.py +90 -0
- bookwright/core/errors.py +127 -0
- bookwright/core/iso639_1.py +200 -0
- bookwright/core/manifest.py +343 -0
- bookwright/errors.py +47 -0
- bookwright/golem/__init__.py +71 -0
- bookwright/golem/base.py +200 -0
- bookwright/golem/errors.py +29 -0
- bookwright/golem/modules/__init__.py +1 -0
- bookwright/golem/modules/character.py +109 -0
- bookwright/golem/modules/event.py +91 -0
- bookwright/golem/modules/feature.py +161 -0
- bookwright/golem/modules/inference.py +41 -0
- bookwright/golem/modules/narrative.py +55 -0
- bookwright/golem/modules/provenance.py +197 -0
- bookwright/golem/modules/relationship.py +38 -0
- bookwright/golem/modules/setting.py +30 -0
- bookwright/golem/namespaces.py +332 -0
- bookwright/golem/serialize.py +25 -0
- bookwright/golem/slug.py +22 -0
- bookwright/indexers/__init__.py +47 -0
- bookwright/indexers/base.py +55 -0
- bookwright/indexers/errors.py +80 -0
- bookwright/indexers/rdflib_indexer.py +89 -0
- bookwright/integrations/__init__.py +155 -0
- bookwright/integrations/base.py +117 -0
- bookwright/integrations/claude/__init__.py +29 -0
- bookwright/integrations/constants.py +38 -0
- bookwright/integrations/descriptions.py +48 -0
- bookwright/integrations/errors.py +170 -0
- bookwright/integrations/generic/__init__.py +56 -0
- bookwright/integrations/lint.py +160 -0
- bookwright/integrations/materialize.py +202 -0
- bookwright/integrations/options.py +203 -0
- bookwright/io/__init__.py +1 -0
- bookwright/io/bible.py +500 -0
- bookwright/io/errors.py +98 -0
- bookwright/io/frontmatter.py +61 -0
- bookwright/io/fs.py +226 -0
- bookwright/io/manuscript.py +15 -0
- bookwright/io/project.py +21 -0
- bookwright/io/report.py +107 -0
- bookwright/io/research.py +427 -0
- bookwright/resources/__init__.py +1 -0
- bookwright/resources/commands/bookwright-analyze.md +66 -0
- bookwright/resources/commands/bookwright-bible.md +96 -0
- bookwright/resources/commands/bookwright-checklist.md +67 -0
- bookwright/resources/commands/bookwright-clarify.md +65 -0
- bookwright/resources/commands/bookwright-constitution.md +79 -0
- bookwright/resources/commands/bookwright-continuity.md +70 -0
- bookwright/resources/commands/bookwright-draft.md +74 -0
- bookwright/resources/commands/bookwright-outline.md +71 -0
- bookwright/resources/commands/bookwright-research.md +107 -0
- bookwright/resources/commands/bookwright-scenes.md +66 -0
- bookwright/resources/commands/bookwright-synopsis.md +67 -0
- bookwright/resources/commands/bookwright-verify.md +136 -0
- bookwright/resources/commands/references/golem-character.md +65 -0
- bookwright/resources/commands/references/golem-events-timeline.md +56 -0
- bookwright/resources/commands/references/golem-relationships.md +53 -0
- bookwright/resources/commands/references/greimas-actants.md +57 -0
- bookwright/resources/commands/references/pending-protocol.md +72 -0
- bookwright/resources/commands/references/propp-functions.md +54 -0
- bookwright/resources/commands/references/research-format.md +136 -0
- bookwright/resources/project/.bookwright/cache/.gitkeep +0 -0
- bookwright/resources/project/.bookwright/schema/.gitkeep +0 -0
- bookwright/resources/project/.bookwright/templates/.gitkeep +0 -0
- bookwright/resources/project/.gitignore +23 -0
- bookwright/resources/project/README.md.j2 +40 -0
- bookwright/resources/project/__init__.py +6 -0
- bookwright/resources/project/bible/characters/.gitkeep +0 -0
- bookwright/resources/project/bible/constitution.md.j2 +74 -0
- bookwright/resources/project/bible/glossary.md +36 -0
- bookwright/resources/project/bible/locations/.gitkeep +0 -0
- bookwright/resources/project/bible/pov-structure.md +43 -0
- bookwright/resources/project/bible/relationships.md +36 -0
- bookwright/resources/project/bible/research/_index.md +28 -0
- bookwright/resources/project/bible/research/sources.md +23 -0
- bookwright/resources/project/bible/settings/.gitkeep +0 -0
- bookwright/resources/project/bible/subplots.md +35 -0
- bookwright/resources/project/bible/themes.md +36 -0
- bookwright/resources/project/bible/timeline.md +38 -0
- bookwright/resources/project/manuscript/.gitkeep +0 -0
- bookwright/resources/project/outline/arcs.md +34 -0
- bookwright/resources/project/outline/scenes.md +31 -0
- bookwright/resources/project/outline/structure.md +35 -0
- bookwright/resources/project/outline/synopsis.md +25 -0
- bookwright/resources/schemas/__init__.py +19 -0
- bookwright/resources/schemas/golem-1.1/VERSION +1 -0
- bookwright/resources/schemas/golem-1.1/golem.ttl +1947 -0
- bookwright/resources/schemas/golem-1.1/version.json +8 -0
- bookwright/resources/templates/__init__.py +1 -0
- bookwright/resources/templates/bible/character.md.tmpl +63 -0
- bookwright/resources/templates/bible/location.md.tmpl +37 -0
- bookwright/resources/templates/bible/research/_index.md.tmpl +25 -0
- bookwright/resources/templates/bible/research/sources.md.tmpl +21 -0
- bookwright/resources/templates/bible/research/tema.md.tmpl +37 -0
- bookwright/resources/templates/bible/setting.md.tmpl +38 -0
- bookwright/resources/templates/manifest.template.toml +79 -0
- bookwright/resources/templates/manuscript/chapter.md.tmpl +36 -0
- bookwright/resources/templates/scenes/scene.md.tmpl +37 -0
- bookwright/resources/vocabularies/__init__.py +6 -0
- bookwright/resources/vocabularies/greimas.ttl +4 -0
- bookwright/resources/vocabularies/propp.ttl +4 -0
- bookwright/resources/vocabularies/sources.ttl +82 -0
- bookwright/validation/__init__.py +33 -0
- bookwright/validation/anchor_queries.py +223 -0
- bookwright/validation/base.py +233 -0
- bookwright/validation/queries.py +197 -0
- bookwright/validation/registry.py +185 -0
- bookwright/validation/report.py +106 -0
- bookwright/validation/runner.py +65 -0
- bookwright/validation/validators/__init__.py +9 -0
- bookwright/validation/validators/character_presence.py +202 -0
- bookwright/validation/validators/factual_anchor.py +291 -0
- bookwright/validation/validators/focalization.py +152 -0
- bookwright/validation/validators/setting_continuity.py +100 -0
- bookwright/validation/validators/temporal.py +277 -0
- bookwright_cli-0.2.0.dist-info/METADATA +218 -0
- bookwright_cli-0.2.0.dist-info/RECORD +149 -0
- bookwright_cli-0.2.0.dist-info/WHEEL +4 -0
- bookwright_cli-0.2.0.dist-info/entry_points.txt +2 -0
- bookwright_cli-0.2.0.dist-info/licenses/LICENSE +202 -0
- 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)."""
|