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,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")
|
bookwright/golem/slug.py
ADDED
|
@@ -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})
|