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,38 @@
1
+ """Relationship module: social relationships and the roles within them."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import ClassVar
6
+
7
+ from rdflib.term import URIRef
8
+
9
+ from bookwright.golem.base import CrossRef, GolemEntity, SluggedEntity
10
+ from bookwright.golem.namespaces import CLASS_IRI, PARTICIPANT, REFERS_TO
11
+
12
+
13
+ class SocialRelationship(SluggedEntity):
14
+ """A social relationship (``golem:G4_Social_Relationship``).
15
+
16
+ Each participant is linked by one ``dlp:participant`` triple (FR-015).
17
+ """
18
+
19
+ golem_class: ClassVar[URIRef] = CLASS_IRI["SocialRelationship"]
20
+ path_segment: ClassVar[str] = "relationship"
21
+ cross_refs: ClassVar[tuple[CrossRef, ...]] = (
22
+ CrossRef("participants", PARTICIPANT, multi=True),
23
+ )
24
+
25
+ participants: tuple[GolemEntity | URIRef, ...] = ()
26
+
27
+
28
+ class RelationshipRole(SluggedEntity):
29
+ """A relationship role (``golem:G6_Relationship_Role``).
30
+
31
+ When set, the role refers to its owning relationship (``crm:P67_refers_to``).
32
+ """
33
+
34
+ golem_class: ClassVar[URIRef] = CLASS_IRI["RelationshipRole"]
35
+ path_segment: ClassVar[str] = "relationship-role"
36
+ cross_refs: ClassVar[tuple[CrossRef, ...]] = (CrossRef("relationship", REFERS_TO),)
37
+
38
+ relationship: GolemEntity | URIRef | None = None
@@ -0,0 +1,30 @@
1
+ """Setting module: settings and the narrative locations placed within them."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import ClassVar
6
+
7
+ from rdflib.term import URIRef
8
+
9
+ from bookwright.golem.base import CrossRef, GolemEntity, SluggedEntity
10
+ from bookwright.golem.namespaces import CLASS_IRI, GENERIC_LOCATION
11
+
12
+
13
+ class Setting(SluggedEntity):
14
+ """A setting (``golem:G12_Setting``). Identity only in v0."""
15
+
16
+ golem_class: ClassVar[URIRef] = CLASS_IRI["Setting"]
17
+ path_segment: ClassVar[str] = "setting"
18
+
19
+
20
+ class NarrativeLocation(SluggedEntity):
21
+ """A narrative location (``golem:G13_Narrative_Location``).
22
+
23
+ When set, the location is ``dlp:generic-location`` of its setting (FR-015).
24
+ """
25
+
26
+ golem_class: ClassVar[URIRef] = CLASS_IRI["NarrativeLocation"]
27
+ path_segment: ClassVar[str] = "location"
28
+ cross_refs: ClassVar[tuple[CrossRef, ...]] = (CrossRef("setting", GENERIC_LOCATION),)
29
+
30
+ setting: GolemEntity | URIRef | None = None
@@ -0,0 +1,332 @@
1
+ """Namespaces, class/predicate IRIs, prefix binding and the frozen ontology.
2
+
3
+ All IRIs are hard-coded (confirmed against the vendored ``golem.ttl`` @
4
+ ``f666128a…``); the Turtle is never parsed at import time, keeping construction
5
+ cheap and side-effect-free (research D5). The frozen ontology is loaded only by
6
+ :func:`load_frozen_ontology` / :func:`frozen_terms`, which back the term-closure
7
+ test (SC-003).
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from importlib import resources
13
+ from typing import NamedTuple
14
+
15
+ from rdflib import Graph, Namespace
16
+ from rdflib.namespace import RDF, RDFS, XSD
17
+ from rdflib.term import URIRef
18
+
19
+ from bookwright.resources.schemas import SCHEMA_DIR
20
+
21
+ __all__ = [
22
+ "ASSIGNED",
23
+ "ASSIGNED_ATTRIBUTE_TO",
24
+ "BEGIN_OF_BEGIN",
25
+ "BW",
26
+ "BW_ACCESS_DATE",
27
+ "BW_ASSERTED_BY",
28
+ "BW_AUTHOR",
29
+ "BW_CLAIM",
30
+ "BW_CONSTRAINS",
31
+ "BW_OPEN",
32
+ "BW_ORIGINAL_LANGUAGE",
33
+ "BW_ORIGINAL_QUOTE",
34
+ "BW_PROMOTES",
35
+ "BW_REFERENCE",
36
+ "BW_RELIABILITY",
37
+ "BW_RELIABILITY_JUSTIFICATION",
38
+ "BW_SUPPORTED_BY",
39
+ "BW_TRANSLATION",
40
+ "CLASS_IRI",
41
+ "CRM",
42
+ "CSM",
43
+ "DLP",
44
+ "DURATION",
45
+ "E52_TIME_SPAN",
46
+ "EDNS",
47
+ "END_OF_END",
48
+ "FOLLOWS",
49
+ "GENERICALLY_DEPENDENT_ON",
50
+ "GENERIC_LOCATION",
51
+ "GOLEM",
52
+ "HAS_DIMENSION",
53
+ "HAS_FEATURE",
54
+ "HAS_TIME_SPAN",
55
+ "HAS_TYPE",
56
+ "HAS_VALUE",
57
+ "PARTICIPANT",
58
+ "PLAYS",
59
+ "PRECEDES",
60
+ "PROPER_PART",
61
+ "RDF",
62
+ "RDFS",
63
+ "REFERS_TO",
64
+ "RELIABILITY_IRI",
65
+ "SOURCE_TYPE_IRI",
66
+ "TEMPORALLY_INCLUDED_IN",
67
+ "TEMPORALLY_INCLUDES",
68
+ "TEMPORALLY_OVERLAPS",
69
+ "TEMPORAL_LOCATION",
70
+ "TEMPORAL_RELATIONS",
71
+ "TR",
72
+ "USED_SPECIFIC_OBJECT",
73
+ "XSD",
74
+ "TemporalRelation",
75
+ "bind_prefixes",
76
+ "frozen_terms",
77
+ "load_frozen_ontology",
78
+ "timeline_uri",
79
+ ]
80
+
81
+ # --- Namespaces -------------------------------------------------------------
82
+
83
+ GOLEM = Namespace("https://w3id.org/golem/ontology#")
84
+ CRM = Namespace("http://www.cidoc-crm.org/cidoc-crm/")
85
+ # The DOLCE-Lite-Plus layer the frozen GOLEM ontology actually emits its
86
+ # participation / part / dependence / location predicates from (research D5).
87
+ DLP = Namespace("http://www.ontologydesignpatterns.org/ont/dlp/DOLCE-Lite.owl#")
88
+ # The DOLCE ExtendedDnS layer that supplies ``plays`` (character →
89
+ # narrative role) — a *different* file from the DOLCE-Lite ``DLP`` above, kept
90
+ # bound to its own ``edns`` prefix so the distinction stays visible (FR-018).
91
+ EDNS = Namespace("http://www.ontologydesignpatterns.org/ont/dlp/ExtendedDnS.owl#")
92
+ # The DOLCE TemporalRelations layer: the five qualitative event relations plus
93
+ # ``temporal-location`` (interval → boundary). All five relations are frozen
94
+ # (research D11, verified against ``golem.ttl``).
95
+ TR = Namespace("http://www.ontologydesignpatterns.org/ont/dlp/TemporalRelations.owl#")
96
+ # The DOLCE CommonSenseMapping layer supplying ``duration`` (event → its
97
+ # time-interval, ⊑ ``temporal-location``).
98
+ CSM = Namespace("http://www.ontologydesignpatterns.org/ont/dlp/CommonSenseMapping.owl#")
99
+ # Bookwright's own vocabulary — the research/provenance terms (``bw:reference``,
100
+ # ``bw:claim``, ``bw:constrains``, the source-type / reliability E55 individuals).
101
+ # Declared in ``resources/vocabularies/sources.ttl``, **never** in the frozen
102
+ # ``golem.ttl``; intentionally outside the ``CLASS_IRI`` closure (Constitution X;
103
+ # research D3).
104
+ BW = Namespace("https://bookwright.dev/vocab/bw#")
105
+
106
+ _PREFIXES: tuple[tuple[str, Namespace], ...] = (
107
+ ("golem", GOLEM),
108
+ ("crm", CRM),
109
+ ("dlp", DLP),
110
+ ("edns", EDNS),
111
+ ("tr", TR),
112
+ ("csm", CSM),
113
+ ("bw", BW),
114
+ ("rdf", Namespace(str(RDF))),
115
+ ("rdfs", Namespace(str(RDFS))),
116
+ ("xsd", Namespace(str(XSD))),
117
+ )
118
+
119
+
120
+ def bind_prefixes(graph: Graph) -> None:
121
+ """Bind exactly one short prefix per namespace, deterministically (FR-010).
122
+
123
+ ``replace=True`` overrides rdflib's auto-bound aliases so serialized Turtle
124
+ is byte-stable across runs.
125
+ """
126
+ for prefix, namespace in _PREFIXES:
127
+ graph.bind(prefix, namespace, replace=True, override=True)
128
+
129
+
130
+ # --- Concept class IRIs (FR-004 local names) --------------------------------
131
+
132
+ CLASS_IRI: dict[str, URIRef] = {
133
+ "Character": GOLEM["G1_Character"],
134
+ "Object": GOLEM["G16_Object"],
135
+ "SocialRelationship": GOLEM["G4_Social_Relationship"],
136
+ "RelationshipRole": GOLEM["G6_Relationship_Role"],
137
+ "NarrativeEvent": GOLEM["G5_Narrative_Event"],
138
+ "PsychologicalState": GOLEM["G3_Psychological_State"],
139
+ "Setting": GOLEM["G12_Setting"],
140
+ "NarrativeLocation": GOLEM["G13_Narrative_Location"],
141
+ "NarrativeUnit": GOLEM["G9_Narrative_Unit"],
142
+ "NarrativeFunction": GOLEM["G10_Narrative_Function"],
143
+ "NarrativeRole": GOLEM["G11_Narrative_Role"],
144
+ "NarrativeSequence": GOLEM["G7_Narrative_Sequence"],
145
+ "AttributeAssignment": CRM["E13_Attribute_Assignment"],
146
+ # Character-scoped attribute-carrier classes (FR-020). These are NOT
147
+ # narrative concepts — they are excluded from the CONCEPTS registry — but
148
+ # their rdf:type IRIs live here so the closure test (SC-003) covers them too.
149
+ "CharacterFeature": GOLEM["G17_Character_Feature"],
150
+ "Dimension": CRM["E54_Dimension"],
151
+ "Type": CRM["E55_Type"],
152
+ # The DOLCE-Lite time-interval carrying an event's begin/end boundaries
153
+ # (research D11). Closure-safe — emitted by NarrativeEvent's interval triples.
154
+ "TimeInterval": DLP["time-interval"],
155
+ }
156
+ """Class name → rdf:type IRI. Every value is asserted ∈ frozen_terms() (SC-003)."""
157
+
158
+ # --- Cross-reference predicate IRIs (FR-015) -------------------------------
159
+
160
+ PARTICIPANT = DLP["participant"]
161
+ """Perdurant → endurant participation (relationship / event participants)."""
162
+ PROPER_PART = DLP["proper-part"]
163
+ """Whole → ordered part (narrative sequence → its units)."""
164
+ GENERICALLY_DEPENDENT_ON = DLP["generically-dependent-on"]
165
+ """Dependent → bearer (psychological state → its character)."""
166
+ GENERIC_LOCATION = DLP["generic-location"]
167
+ """Located → locus (narrative location → its setting)."""
168
+ REFERS_TO = CRM["P67_refers_to"]
169
+ """Generic cross-reference (role → relationship, unit → function/role, premise)."""
170
+ ASSIGNED_ATTRIBUTE_TO = CRM["P140_assigned_attribute_to"]
171
+ """Attribute assignment → the entity it makes an attribution about."""
172
+ ASSIGNED = CRM["P141_assigned"]
173
+ """Attribute assignment → the attribute it asserts."""
174
+ USED_SPECIFIC_OBJECT = CRM["P16_used_specific_object"]
175
+ """Attribute assignment → the source used in the inference (carries the path)."""
176
+
177
+ # --- Character-attribute predicates (FR-017/018/019, D14) -------------------
178
+
179
+ HAS_FEATURE = GOLEM["GP0_has_feature"]
180
+ """Character → one of its character features (biographical or free-text)."""
181
+ PLAYS = EDNS["plays"]
182
+ """Character → a narrative role it plays (DOLCE ExtendedDnS, distinct from dlp)."""
183
+ HAS_TYPE = CRM["P2_has_type"]
184
+ """Biographical feature → its E55_Type (the birth / death individual)."""
185
+ HAS_DIMENSION = CRM["P43_has_dimension"]
186
+ """Biographical feature → its E54_Dimension (carrying the year value)."""
187
+ HAS_VALUE = CRM["P90_has_value"]
188
+ """Dimension → its primitive value (the year as an xsd:gYear literal)."""
189
+
190
+ # --- Temporal-interval predicates (FR-015, research D11) --------------------
191
+
192
+ DURATION = CSM["duration"]
193
+ """Event → its time-interval (⊑ ``temporal-location``); carries the begin/end span."""
194
+ TEMPORAL_LOCATION = TR["temporal-location"]
195
+ """Interval → one of its begin/end boundary intervals."""
196
+ FOLLOWS = TR["follows"]
197
+ """Event strictly after another (the canonical strict-order relation)."""
198
+ PRECEDES = TR["precedes"]
199
+ """Event strictly before another (inverse direction of ``follows``)."""
200
+ TEMPORALLY_OVERLAPS = TR["temporally-overlaps"]
201
+ """Two events share part of their extent (symmetric)."""
202
+ TEMPORALLY_INCLUDES = TR["temporally-includes"]
203
+ """Event whose extent contains another's (containment)."""
204
+ TEMPORALLY_INCLUDED_IN = TR["temporally-included-in"]
205
+ """Event whose extent is contained within another's (inverse containment)."""
206
+
207
+ # --- Research / provenance predicates (iteration 012, research D3) -----------
208
+ # Bookwright's own ``bw:`` terms. Declared in ``sources.ttl``, referenced from
209
+ # ``golem/modules/provenance.py``; NONE are added to ``CLASS_IRI`` or the
210
+ # closure-checked lists in ``test_namespaces.py`` — they sit outside the frozen
211
+ # GOLEM ontology by design (Constitution X).
212
+
213
+ BW_REFERENCE = BW["reference"]
214
+ """Source → its bibliographic reference or URL (``xsd:string``)."""
215
+ BW_AUTHOR = BW["author"]
216
+ """Source → its author (``xsd:string``)."""
217
+ BW_ORIGINAL_LANGUAGE = BW["originalLanguage"]
218
+ """Source → the ISO 639-1 code of its original language (``xsd:string``)."""
219
+ BW_RELIABILITY = BW["reliability"]
220
+ """Source → its reliability E55_Type individual (``alta``/``media``/``baja``)."""
221
+ BW_RELIABILITY_JUSTIFICATION = BW["reliabilityJustification"]
222
+ """Source → the prose justifying its reliability rating (``xsd:string``)."""
223
+ BW_ACCESS_DATE = BW["accessDate"]
224
+ """Source → the date it was consulted (``xsd:date``)."""
225
+ BW_ORIGINAL_QUOTE = BW["originalQuote"]
226
+ """Source → the verbatim quote in its original language (``xsd:string``)."""
227
+ BW_TRANSLATION = BW["translation"]
228
+ """Source → the quote's translation; emitted iff source language ≠ book language."""
229
+ BW_CLAIM = BW["claim"]
230
+ """Finding → the real-world assertion it records (``xsd:string``)."""
231
+ BW_ASSERTED_BY = BW["assertedBy"]
232
+ """Finding → who asserts the claim — agent or author (``xsd:string``)."""
233
+ BW_SUPPORTED_BY = BW["supportedBy"]
234
+ """Finding → one supporting Source (one triple per source)."""
235
+ BW_OPEN = BW["open"]
236
+ """Finding → ``true`` when it is an unresolved open question (``xsd:boolean``)."""
237
+ BW_PROMOTES = BW["promotes"]
238
+ """Anchor → the Finding it promotes into a binding constraint."""
239
+ BW_CONSTRAINS = BW["constrains"]
240
+ """Anchor → the narrative entity (or the timeline) the constraint bears on."""
241
+
242
+ SOURCE_TYPE_IRI: dict[str, URIRef] = {
243
+ "primaria": BW["source-type/primaria"],
244
+ "secundaria": BW["source-type/secundaria"],
245
+ "oficial": BW["source-type/oficial"],
246
+ "académica": BW["source-type/academica"],
247
+ "periodística": BW["source-type/periodistica"],
248
+ "testimonial": BW["source-type/testimonial"],
249
+ }
250
+ """Front-matter ``type`` value → its ``a crm:E55_Type`` individual (ASCII-slugged
251
+ IRI). The accented Spanish key is the author-facing value; the IRI is slugged
252
+ (research D4). Declared in ``sources.ttl``; enforced by the ``Source`` ``Literal``."""
253
+
254
+ RELIABILITY_IRI: dict[str, URIRef] = {
255
+ "alta": BW["reliability/alta"],
256
+ "media": BW["reliability/media"],
257
+ "baja": BW["reliability/baja"],
258
+ }
259
+ """Front-matter ``reliability`` value → its ``a crm:E55_Type`` individual IRI."""
260
+
261
+ # --- Reused CIDOC-CRM time-span terms (anchor time-span, research D5) --------
262
+ # Already-bound ``crm:`` predicates/classes referenced directly; never added to
263
+ # ``CLASS_IRI`` (the frozen closure is GOLEM's, not the whole of CIDOC-CRM).
264
+
265
+ HAS_TIME_SPAN = CRM["P4_has_time-span"]
266
+ """Anchor → its ``E52_Time-Span`` sub-node (optional, research D5)."""
267
+ E52_TIME_SPAN = CRM["E52_Time-Span"]
268
+ """``rdf:type`` of an anchor's time-span sub-node."""
269
+ BEGIN_OF_BEGIN = CRM["P82a_begin_of_the_begin"]
270
+ """Time-span → its begin year (``xsd:gYear``)."""
271
+ END_OF_END = CRM["P82b_end_of_the_end"]
272
+ """Time-span → its end year (``xsd:gYear``)."""
273
+
274
+
275
+ def timeline_uri(uri_base: str) -> URIRef:
276
+ """The well-known, **untyped** timeline IRI an anchor constrains (research D10).
277
+
278
+ GOLEM has no single node for "the timeline" (it is a collection of events), yet
279
+ FR-009 lets an anchor constrain it. This returns the conventional ``{uri_base}
280
+ timeline`` IRI — referenced as the object of ``bw:constrains`` only, never typed
281
+ — so era-level anchors resolve without introducing a new GOLEM class.
282
+ """
283
+ return URIRef(f"{uri_base}timeline")
284
+
285
+
286
+ class TemporalRelation(NamedTuple):
287
+ """One qualitative event-to-event temporal relation (research D11).
288
+
289
+ ``name`` is the single canonical key used across every layer — the bible
290
+ frontmatter key, the :class:`NarrativeEvent` field, the SPARQL projection key,
291
+ and the validator's predicate map — so the layers never drift apart or fork on
292
+ spelling. ``predicate`` is the frozen ``TR:*`` IRI the relation serializes to.
293
+ """
294
+
295
+ name: str
296
+ predicate: URIRef
297
+
298
+
299
+ TEMPORAL_RELATIONS: tuple[TemporalRelation, ...] = (
300
+ TemporalRelation("follows", FOLLOWS),
301
+ TemporalRelation("precedes", PRECEDES),
302
+ TemporalRelation("overlaps", TEMPORALLY_OVERLAPS),
303
+ TemporalRelation("includes", TEMPORALLY_INCLUDES),
304
+ TemporalRelation("included_in", TEMPORALLY_INCLUDED_IN),
305
+ )
306
+ """The five qualitative temporal relations, in canonical order — the single source
307
+ of truth every consumer derives its own view from (cross_refs, bible keys, queries,
308
+ the ``temporal`` validator)."""
309
+
310
+ # --- Frozen ontology --------------------------------------------------------
311
+
312
+ _SCHEMA_PACKAGE = "bookwright.resources.schemas"
313
+ _ONTOLOGY_RELPATH = f"{SCHEMA_DIR}/golem.ttl"
314
+
315
+
316
+ def load_frozen_ontology() -> Graph:
317
+ """Parse the vendored frozen GOLEM ontology into a fresh rdflib graph."""
318
+ resource = resources.files(_SCHEMA_PACKAGE).joinpath(_ONTOLOGY_RELPATH)
319
+ data = resource.read_text(encoding="utf-8")
320
+ graph = Graph()
321
+ graph.parse(data=data, format="turtle")
322
+ return graph
323
+
324
+
325
+ def frozen_terms() -> set[URIRef]:
326
+ """Every IRI defined or referenced in the frozen ontology (closure backstop).
327
+
328
+ Term closure (SC-003) holds iff every class/predicate emitted by any
329
+ ``to_triples()`` is a member of this set.
330
+ """
331
+ graph = load_frozen_ontology()
332
+ return {term for triple in graph for term in triple if isinstance(term, URIRef)}
@@ -0,0 +1,25 @@
1
+ """Collection-level Turtle serialization (research D8, FR-012)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Iterable
6
+
7
+ from rdflib import Graph
8
+
9
+ from bookwright.golem.base import GolemEntity
10
+ from bookwright.golem.namespaces import bind_prefixes
11
+
12
+
13
+ def to_turtle(entities: Iterable[GolemEntity]) -> str:
14
+ """Serialize ``entities`` to a single prefixed Turtle document.
15
+
16
+ Builds a fresh graph, binds the short prefixes (FR-010), adds every triple
17
+ each entity yields, and serializes. The output parses back through rdflib
18
+ and round-trips isomorphically (SC-004).
19
+ """
20
+ graph = Graph()
21
+ bind_prefixes(graph)
22
+ for entity in entities:
23
+ for triple in entity.to_triples():
24
+ graph.add(triple)
25
+ return graph.serialize(format="turtle")
@@ -0,0 +1,22 @@
1
+ """Deterministic, ASCII-only slug generation (design § 4.5, FR-005/006)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from slugify import slugify
6
+
7
+ from bookwright.golem.errors import EmptySlugError
8
+
9
+
10
+ def make_slug(name: str) -> str:
11
+ """Return the canonical slug for ``name``.
12
+
13
+ Uses ``python-slugify`` in its default mode: lowercase, ASCII
14
+ transliteration (``José Peña`` → ``jose-pena``), single-hyphen separators,
15
+ edges trimmed. The rule is pure, so the same name always yields the same
16
+ slug (FR-005). A name that reduces to the empty string (e.g. punctuation
17
+ only) is rejected loudly with :class:`EmptySlugError` (FR-006).
18
+ """
19
+ slug = slugify(name)
20
+ if not slug:
21
+ raise EmptySlugError(name)
22
+ return slug
@@ -0,0 +1,47 @@
1
+ """Pluggable graph-engine seam: the ``Indexer`` protocol + a name→class registry.
2
+
3
+ Adding a future engine is one ``INDEXER_REGISTRY`` entry — never a change to
4
+ build/query command code (FR-008). ``GrafeoIndexer`` is intentionally **not**
5
+ registered (deferred to v0.3 — Principle X).
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from .base import Indexer, IndexTriple
11
+ from .errors import (
12
+ GraphLoadError,
13
+ GraphNotBuiltError,
14
+ IndexerError,
15
+ InvalidQueryError,
16
+ UnknownIndexerError,
17
+ )
18
+ from .rdflib_indexer import RdflibIndexer
19
+
20
+ INDEXER_REGISTRY: dict[str, type[Indexer]] = {"rdflib": RdflibIndexer}
21
+ """Engine name → concrete class. The manifest's ``[bookwright] indexer`` keys it."""
22
+
23
+
24
+ def resolve_indexer(name: str) -> type[Indexer]:
25
+ """Return the engine class registered under ``name`` (FR-007).
26
+
27
+ Raises :class:`UnknownIndexerError` — naming the unknown engine and listing
28
+ the available ones — when ``name`` is not registered.
29
+ """
30
+ try:
31
+ return INDEXER_REGISTRY[name]
32
+ except KeyError as exc:
33
+ raise UnknownIndexerError(name, available=sorted(INDEXER_REGISTRY)) from exc
34
+
35
+
36
+ __all__ = [
37
+ "INDEXER_REGISTRY",
38
+ "GraphLoadError",
39
+ "GraphNotBuiltError",
40
+ "IndexTriple",
41
+ "Indexer",
42
+ "IndexerError",
43
+ "InvalidQueryError",
44
+ "RdflibIndexer",
45
+ "UnknownIndexerError",
46
+ "resolve_indexer",
47
+ ]
@@ -0,0 +1,55 @@
1
+ """The ``Indexer`` protocol — the stable engine seam build/query depend on.
2
+
3
+ Both ``graph`` verbs depend only on this structural contract (design § 12.1),
4
+ never on a concrete engine (FR-005/008). A future engine conforms by shape
5
+ alone — ``Protocol`` over ABC keeps it decoupled (R4).
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from collections.abc import Iterable
11
+ from pathlib import Path
12
+ from typing import Any, Protocol, runtime_checkable
13
+
14
+ from rdflib.term import Literal, URIRef
15
+
16
+ IndexTriple = tuple[
17
+ URIRef | str,
18
+ URIRef | str,
19
+ URIRef | Literal | str | int | float,
20
+ ]
21
+ """One triple accepted by :meth:`Indexer.add_triple` (IRI-like ``str`` coerced)."""
22
+
23
+
24
+ @runtime_checkable
25
+ class Indexer(Protocol):
26
+ """A pluggable graph engine (design § 12.1)."""
27
+
28
+ def load(self, ttl_path: Path) -> None:
29
+ """Parse a Turtle file into the engine's graph."""
30
+ ...
31
+
32
+ def save(self, ttl_path: Path) -> None:
33
+ """Serialize to Turtle with short prefixes bound (FR-015); make parent dirs."""
34
+ ...
35
+
36
+ def add_triple(
37
+ self,
38
+ s: URIRef | str,
39
+ p: URIRef | str,
40
+ o: URIRef | Literal | str | int | float,
41
+ ) -> None:
42
+ """Add one triple; IRI-like ``str`` subjects/predicates coerce to ``URIRef``."""
43
+ ...
44
+
45
+ def query(self, sparql: str) -> Iterable[dict[str, Any]]:
46
+ """Run a SELECT; yield one dict per row (var → ``str``). Raise on bad SPARQL."""
47
+ ...
48
+
49
+ # NOTE: a `construct(sparql) -> Indexer` verb is intentionally absent — no v0
50
+ # command runs CONSTRUCT. Re-add it here alongside its first consumer (the
51
+ # validation system, iteration 10) rather than carrying an unused seam now.
52
+
53
+ def count(self) -> int:
54
+ """Return the number of triples currently held."""
55
+ ...
@@ -0,0 +1,80 @@
1
+ """Exception hierarchy for the indexer (graph-engine) seam.
2
+
3
+ Every concrete error inherits the canonical ``--json`` envelope from the shared
4
+ ``BookwrightError`` base (Principle IX, data-model § 6); this module declares only
5
+ each error's ``code`` and ``details``.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from bookwright.errors import BookwrightError
11
+
12
+
13
+ class IndexerError(BookwrightError):
14
+ """Base for every failure mode the ``bookwright.indexers`` package owns.
15
+
16
+ Abstract: declares no ``code`` and is never serialized directly.
17
+ """
18
+
19
+
20
+ class UnknownIndexerError(IndexerError):
21
+ """The manifest names an engine that is not in ``INDEXER_REGISTRY`` (FR-007).
22
+
23
+ Carries the offending name and the sorted set of registered engines so the
24
+ error names both the unknown engine and the available ones.
25
+ """
26
+
27
+ code = "unknown_indexer"
28
+
29
+ def __init__(self, name: str, available: list[str]) -> None:
30
+ self.name = name
31
+ self.available = available
32
+ super().__init__(
33
+ f"unknown indexer {name!r}; available: {', '.join(available)}",
34
+ {"name": name, "available": available},
35
+ )
36
+
37
+
38
+ class GraphNotBuiltError(IndexerError):
39
+ """``graph query`` ran with no ``bible/graph.ttl`` on disk (FR-016)."""
40
+
41
+ code = "graph_not_built"
42
+
43
+ def __init__(self, path: str) -> None:
44
+ self.path = path
45
+ super().__init__(
46
+ f"no graph at {path}; run `bookwright graph build` first",
47
+ {"path": path},
48
+ )
49
+
50
+
51
+ class GraphLoadError(IndexerError):
52
+ """``bible/graph.ttl`` exists but the engine could not parse it (FR-016).
53
+
54
+ Distinct from :class:`GraphNotBuiltError` (no file at all): the file is there
55
+ but malformed — e.g. a hand-edit broke the Turtle. Surfaced as a clean error
56
+ envelope, never a raw rdflib traceback.
57
+ """
58
+
59
+ code = "graph_load_failed"
60
+
61
+ def __init__(self, path: str, reason: str) -> None:
62
+ self.path = path
63
+ self.reason = reason
64
+ super().__init__(
65
+ f"could not parse graph at {path}: {reason}",
66
+ {"path": path, "reason": reason},
67
+ )
68
+
69
+
70
+ class InvalidQueryError(IndexerError):
71
+ """A malformed SPARQL string was handed to ``query`` (FR-016).
72
+
73
+ No partial rows are yielded before this is raised.
74
+ """
75
+
76
+ code = "invalid_query"
77
+
78
+ def __init__(self, reason: str) -> None:
79
+ self.reason = reason
80
+ super().__init__(f"invalid SPARQL query: {reason}", {"reason": reason})