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,109 @@
|
|
|
1
|
+
"""Character module: agents and objects of the storyworld (GOLEM § character)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Callable, Iterable
|
|
6
|
+
from typing import ClassVar, TypeVar
|
|
7
|
+
|
|
8
|
+
from pydantic import PrivateAttr
|
|
9
|
+
from rdflib.term import URIRef
|
|
10
|
+
|
|
11
|
+
from bookwright.golem.base import CrossRef, DerivedAssertion, GolemEntity, SluggedEntity
|
|
12
|
+
from bookwright.golem.modules.feature import BioKind, CharacterFeature, CharacterRole
|
|
13
|
+
from bookwright.golem.namespaces import CLASS_IRI, HAS_FEATURE, PLAYS
|
|
14
|
+
|
|
15
|
+
_Node = TypeVar("_Node", bound=GolemEntity)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _dedup_nodes(texts: Iterable[str], factory: Callable[[str], _Node]) -> tuple[_Node, ...]:
|
|
19
|
+
"""Materialize one node per text, dropping later duplicates by URI (FR-021)."""
|
|
20
|
+
nodes: list[_Node] = []
|
|
21
|
+
seen: set[URIRef] = set()
|
|
22
|
+
for text in texts:
|
|
23
|
+
node = factory(text)
|
|
24
|
+
if node.uri not in seen:
|
|
25
|
+
seen.add(node.uri)
|
|
26
|
+
nodes.append(node)
|
|
27
|
+
return tuple(nodes)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class Character(SluggedEntity):
|
|
31
|
+
"""A character (``golem:G1_Character``).
|
|
32
|
+
|
|
33
|
+
Besides its identity, a character may carry the documented frontmatter:
|
|
34
|
+
``born`` / ``died`` years, free-text ``features``, and
|
|
35
|
+
``narrative_roles``. Each is materialized — once, deterministically, at
|
|
36
|
+
construction — as a character-scoped node under
|
|
37
|
+
:mod:`bookwright.golem.modules.feature`, linked by a frozen predicate
|
|
38
|
+
(``golem:GP0_has_feature`` for features, ``edns:plays`` for roles). A
|
|
39
|
+
character built with none of the four attributes has empty node tuples and
|
|
40
|
+
therefore emits only its ``rdf:type`` assertion.
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
golem_class: ClassVar[URIRef] = CLASS_IRI["Character"]
|
|
44
|
+
path_segment: ClassVar[str] = "character"
|
|
45
|
+
cross_refs: ClassVar[tuple[CrossRef, ...]] = (
|
|
46
|
+
CrossRef("_feature_nodes", HAS_FEATURE, multi=True, owned=True),
|
|
47
|
+
CrossRef("_role_nodes", PLAYS, multi=True, owned=True),
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
born: int | None = None
|
|
51
|
+
died: int | None = None
|
|
52
|
+
features: tuple[str, ...] = ()
|
|
53
|
+
narrative_roles: tuple[str, ...] = ()
|
|
54
|
+
|
|
55
|
+
_feature_nodes: tuple[CharacterFeature, ...] = PrivateAttr(default=())
|
|
56
|
+
_role_nodes: tuple[CharacterRole, ...] = PrivateAttr(default=())
|
|
57
|
+
|
|
58
|
+
def model_post_init(self, __context: object) -> None:
|
|
59
|
+
super().model_post_init(__context) # fixes slug + identity URI first
|
|
60
|
+
|
|
61
|
+
# Biographical features live under `feature/bio/…`, structurally apart
|
|
62
|
+
# from the free-text slug space, so they never collide with free-text
|
|
63
|
+
# values and only the free-text values need URI dedup (FR-021).
|
|
64
|
+
biographical: list[CharacterFeature] = []
|
|
65
|
+
if self.born is not None:
|
|
66
|
+
biographical.append(self._biographical("birth", self.born))
|
|
67
|
+
if self.died is not None:
|
|
68
|
+
biographical.append(self._biographical("death", self.died))
|
|
69
|
+
free_text = _dedup_nodes(
|
|
70
|
+
self.features,
|
|
71
|
+
lambda text: CharacterFeature(
|
|
72
|
+
uri_base=self.uri_base, character_uri=self.uri, label=text
|
|
73
|
+
),
|
|
74
|
+
)
|
|
75
|
+
self._feature_nodes = (*biographical, *free_text)
|
|
76
|
+
self._role_nodes = _dedup_nodes(
|
|
77
|
+
self.narrative_roles,
|
|
78
|
+
lambda text: CharacterRole(uri_base=self.uri_base, character_uri=self.uri, label=text),
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
def _biographical(self, kind: BioKind, year: int) -> CharacterFeature:
|
|
82
|
+
return CharacterFeature(
|
|
83
|
+
uri_base=self.uri_base, character_uri=self.uri, kind=kind, year=year
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
def derived_assertions(self) -> Iterable[DerivedAssertion]:
|
|
87
|
+
"""One :class:`DerivedAssertion` per derived assertion, each tagged with
|
|
88
|
+
the frontmatter key it came from so the indexer can resolve a source line:
|
|
89
|
+
biographical features → ``born`` / ``died``, free-text → ``features``,
|
|
90
|
+
roles → ``narrative_roles``. The identity assertion is file-level
|
|
91
|
+
(``source_field`` ``None``).
|
|
92
|
+
|
|
93
|
+
Overrides the declarative base because ``cross_refs`` cannot express this
|
|
94
|
+
fan-out: a single ``_feature_nodes`` tuple carries three distinct origins
|
|
95
|
+
(``born`` / ``died`` / ``features``), which only this class can untangle —
|
|
96
|
+
a biographical node is recognised by its ``kind``."""
|
|
97
|
+
yield DerivedAssertion(self.uri, self.uri, None)
|
|
98
|
+
for feature in self._feature_nodes:
|
|
99
|
+
field = {"birth": "born", "death": "died"}.get(feature.kind or "", "features")
|
|
100
|
+
yield DerivedAssertion(self.uri, feature.uri, field)
|
|
101
|
+
for role in self._role_nodes:
|
|
102
|
+
yield DerivedAssertion(self.uri, role.uri, "narrative_roles")
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
class Object(SluggedEntity):
|
|
106
|
+
"""A storyworld object (``golem:G16_Object``). Identity only in v0."""
|
|
107
|
+
|
|
108
|
+
golem_class: ClassVar[URIRef] = CLASS_IRI["Object"]
|
|
109
|
+
path_segment: ClassVar[str] = "object"
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"""Event module: narrative events and the psychological states they touch."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Iterable
|
|
6
|
+
from typing import ClassVar
|
|
7
|
+
|
|
8
|
+
from rdflib.namespace import RDF
|
|
9
|
+
from rdflib.term import URIRef
|
|
10
|
+
|
|
11
|
+
from bookwright.golem.base import CrossRef, GolemEntity, SluggedEntity, Triple
|
|
12
|
+
from bookwright.golem.modules.feature import Dimension
|
|
13
|
+
from bookwright.golem.namespaces import (
|
|
14
|
+
CLASS_IRI,
|
|
15
|
+
DURATION,
|
|
16
|
+
GENERICALLY_DEPENDENT_ON,
|
|
17
|
+
HAS_DIMENSION,
|
|
18
|
+
HAS_TYPE,
|
|
19
|
+
PARTICIPANT,
|
|
20
|
+
TEMPORAL_LOCATION,
|
|
21
|
+
TEMPORAL_RELATIONS,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class NarrativeEvent(SluggedEntity):
|
|
26
|
+
"""A narrative event (``golem:G5_Narrative_Event``).
|
|
27
|
+
|
|
28
|
+
Each participant is linked by one ``dlp:participant`` triple (FR-015). An event
|
|
29
|
+
may additionally carry a multi-year time **interval** (``begin`` / ``end`` years)
|
|
30
|
+
and any of the five qualitative temporal relations to other events, which the
|
|
31
|
+
``temporal`` validator (iteration 11) reasons over. The relations are ordinary
|
|
32
|
+
``cross_refs`` (one frozen ``TR:*`` predicate each); the interval needs a custom
|
|
33
|
+
``to_triples`` override because its typed-boundary + dimension shape is outside
|
|
34
|
+
what ``cross_refs`` can express (research D11). An event with no interval and no
|
|
35
|
+
relations behaves exactly as before — only its ``rdf:type`` and any participants.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
golem_class: ClassVar[URIRef] = CLASS_IRI["NarrativeEvent"]
|
|
39
|
+
path_segment: ClassVar[str] = "event"
|
|
40
|
+
cross_refs: ClassVar[tuple[CrossRef, ...]] = (
|
|
41
|
+
CrossRef("participants", PARTICIPANT, multi=True),
|
|
42
|
+
*(CrossRef(rel.name, rel.predicate, multi=True) for rel in TEMPORAL_RELATIONS),
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
participants: tuple[GolemEntity | URIRef, ...] = ()
|
|
46
|
+
begin: int | None = None
|
|
47
|
+
end: int | None = None
|
|
48
|
+
follows: tuple[GolemEntity | URIRef, ...] = ()
|
|
49
|
+
precedes: tuple[GolemEntity | URIRef, ...] = ()
|
|
50
|
+
overlaps: tuple[GolemEntity | URIRef, ...] = ()
|
|
51
|
+
includes: tuple[GolemEntity | URIRef, ...] = ()
|
|
52
|
+
included_in: tuple[GolemEntity | URIRef, ...] = ()
|
|
53
|
+
|
|
54
|
+
def to_triples(self) -> Iterable[Triple]:
|
|
55
|
+
"""The base emission (type + participants + the five relation edges) followed
|
|
56
|
+
by the closure-safe interval triples for any present begin/end boundary."""
|
|
57
|
+
yield from super().to_triples()
|
|
58
|
+
yield from self._interval_triples()
|
|
59
|
+
|
|
60
|
+
def _interval_triples(self) -> Iterable[Triple]:
|
|
61
|
+
if self.begin is None and self.end is None:
|
|
62
|
+
return
|
|
63
|
+
span_uri = URIRef(f"{self.uri}/time-span")
|
|
64
|
+
time_interval = CLASS_IRI["TimeInterval"]
|
|
65
|
+
yield (self.uri, DURATION, span_uri)
|
|
66
|
+
yield (span_uri, RDF.type, time_interval)
|
|
67
|
+
for kind, year in (("begin", self.begin), ("end", self.end)):
|
|
68
|
+
if year is None:
|
|
69
|
+
continue
|
|
70
|
+
boundary_uri = URIRef(f"{span_uri}/{kind}")
|
|
71
|
+
type_uri = URIRef(f"{self.uri_base}type/{kind}")
|
|
72
|
+
dimension = Dimension(uri_base=self.uri_base, feature_uri=boundary_uri, year=year)
|
|
73
|
+
yield (span_uri, TEMPORAL_LOCATION, boundary_uri)
|
|
74
|
+
yield (boundary_uri, RDF.type, time_interval)
|
|
75
|
+
yield (boundary_uri, HAS_TYPE, type_uri)
|
|
76
|
+
yield (type_uri, RDF.type, CLASS_IRI["Type"])
|
|
77
|
+
yield (boundary_uri, HAS_DIMENSION, dimension.uri)
|
|
78
|
+
yield from dimension.to_triples()
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class PsychologicalState(SluggedEntity):
|
|
82
|
+
"""A psychological state (``golem:G3_Psychological_State``).
|
|
83
|
+
|
|
84
|
+
When set, the state is ``dlp:generically-dependent-on`` its bearer (FR-015).
|
|
85
|
+
"""
|
|
86
|
+
|
|
87
|
+
golem_class: ClassVar[URIRef] = CLASS_IRI["PsychologicalState"]
|
|
88
|
+
path_segment: ClassVar[str] = "psychological-state"
|
|
89
|
+
cross_refs: ClassVar[tuple[CrossRef, ...]] = (CrossRef("bearer", GENERICALLY_DEPENDENT_ON),)
|
|
90
|
+
|
|
91
|
+
bearer: GolemEntity | URIRef | None = None
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
"""Feature module: character-scoped attribute carriers.
|
|
2
|
+
|
|
3
|
+
These typed nodes hang off a :class:`~bookwright.golem.modules.character.Character`
|
|
4
|
+
and carry the documented frontmatter that the identity-only model could not:
|
|
5
|
+
free-text features, biographical years (``born`` / ``died``), and narrative
|
|
6
|
+
roles. They are **not** narrative concepts — they are excluded from the
|
|
7
|
+
``CONCEPTS`` registry (SC-001) — but they emit only frozen GOLEM / CIDOC-CRM /
|
|
8
|
+
DOLCE terms (FR-020), and every node carries a deterministic, character-scoped
|
|
9
|
+
URI built from its owner's URI plus a fixed suffix (never a blank node, FR-021).
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from collections.abc import Iterable
|
|
15
|
+
from typing import ClassVar, Literal
|
|
16
|
+
|
|
17
|
+
from pydantic import model_validator
|
|
18
|
+
from rdflib.namespace import RDF, RDFS, XSD
|
|
19
|
+
from rdflib.term import Literal as RdfLiteral
|
|
20
|
+
from rdflib.term import URIRef
|
|
21
|
+
|
|
22
|
+
from bookwright.golem.base import GolemEntity, Triple
|
|
23
|
+
from bookwright.golem.namespaces import (
|
|
24
|
+
CLASS_IRI,
|
|
25
|
+
HAS_DIMENSION,
|
|
26
|
+
HAS_TYPE,
|
|
27
|
+
HAS_VALUE,
|
|
28
|
+
)
|
|
29
|
+
from bookwright.golem.slug import make_slug
|
|
30
|
+
|
|
31
|
+
BioKind = Literal["birth", "death"]
|
|
32
|
+
"""The two biographical feature kinds; each maps to a shared E55_Type individual."""
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def gyear_literal(year: int) -> RdfLiteral:
|
|
36
|
+
"""Format an integer year as a lexically valid ``xsd:gYear`` literal (FR-019).
|
|
37
|
+
|
|
38
|
+
XSD requires the year part to be at least four digits, with an optional
|
|
39
|
+
leading ``-`` for BCE years. Plain ``str(year)`` is invalid for years < 1000
|
|
40
|
+
or BCE (e.g. ``"800"``, ``"-44"``), which the iteration-10 temporal queries —
|
|
41
|
+
the whole reason for ``gYear`` over a plain string — would then reject. Pad
|
|
42
|
+
the magnitude to four digits and preserve the sign instead, so ``800`` →
|
|
43
|
+
``"0800"`` and ``-44`` → ``"-0044"`` while ``1828`` stays ``"1828"``.
|
|
44
|
+
"""
|
|
45
|
+
sign = "-" if year < 0 else ""
|
|
46
|
+
return RdfLiteral(f"{sign}{abs(year):04d}", datatype=XSD.gYear)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class Dimension(GolemEntity):
|
|
50
|
+
"""A measurement (``crm:E54_Dimension``) carrying a biographical year.
|
|
51
|
+
|
|
52
|
+
URI is ``{feature.uri}/dimension``; the year is emitted as an ``xsd:gYear``
|
|
53
|
+
literal (never ``xsd:integer`` or an untyped string — FR-019), which is what
|
|
54
|
+
makes iteration-10's temporal queries answerable.
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
golem_class: ClassVar[URIRef] = CLASS_IRI["Dimension"]
|
|
58
|
+
|
|
59
|
+
feature_uri: URIRef
|
|
60
|
+
year: int
|
|
61
|
+
|
|
62
|
+
def model_post_init(self, __context: object) -> None:
|
|
63
|
+
self._uri = URIRef(f"{self.feature_uri}/dimension")
|
|
64
|
+
|
|
65
|
+
def to_triples(self) -> Iterable[Triple]:
|
|
66
|
+
yield (self.uri, RDF.type, self.golem_class)
|
|
67
|
+
yield (self.uri, HAS_VALUE, gyear_literal(self.year))
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class CharacterFeature(GolemEntity):
|
|
71
|
+
"""A character feature (``golem:G17_Character_Feature``), one of two variants.
|
|
72
|
+
|
|
73
|
+
- **free-text** — supply ``label``; URI ``{character.uri}/feature/{slug(label)}``,
|
|
74
|
+
emits the type assertion + ``rdfs:label``.
|
|
75
|
+
- **biographical** — supply ``kind`` (``"birth"``/``"death"``) and ``year``;
|
|
76
|
+
URI ``{character.uri}/feature/bio/{kind}`` (the ``bio/`` sub-segment keeps
|
|
77
|
+
the birth/death token out of the free-text slug space, so the two variants
|
|
78
|
+
can never collide on one character), emits the type assertion, a
|
|
79
|
+
``crm:P2_has_type`` link to the shared ``{uri_base}type/{kind}`` E55_Type
|
|
80
|
+
individual, and a ``crm:P43_has_dimension`` link to its :class:`Dimension`.
|
|
81
|
+
|
|
82
|
+
Exactly one variant must be supplied. A free-text ``label`` that slugs to
|
|
83
|
+
empty raises :class:`~bookwright.golem.errors.EmptySlugError` (FR-021).
|
|
84
|
+
"""
|
|
85
|
+
|
|
86
|
+
golem_class: ClassVar[URIRef] = CLASS_IRI["CharacterFeature"]
|
|
87
|
+
|
|
88
|
+
character_uri: URIRef
|
|
89
|
+
label: str | None = None
|
|
90
|
+
kind: BioKind | None = None
|
|
91
|
+
year: int | None = None
|
|
92
|
+
|
|
93
|
+
@model_validator(mode="before")
|
|
94
|
+
@classmethod
|
|
95
|
+
def _exactly_one_variant(cls, data: dict[str, object]) -> dict[str, object]:
|
|
96
|
+
# Runs *before* identity construction in ``model_post_init`` (which
|
|
97
|
+
# builds the URI from the chosen variant), so the variant invariant is
|
|
98
|
+
# enforced by a real ``ValidationError`` rather than the type-narrowing
|
|
99
|
+
# assert at the bottom of this class. That assert is then never the line
|
|
100
|
+
# that rejects bad input, so it stays correct under ``python -O``. ``data``
|
|
101
|
+
# is always the raw kwargs dict: this frozen model is only ever built via
|
|
102
|
+
# ``CharacterFeature(...)``, never re-validated from an existing instance.
|
|
103
|
+
kind, label, year = data.get("kind"), data.get("label"), data.get("year")
|
|
104
|
+
if kind is not None:
|
|
105
|
+
# Biographical: a year is mandatory, a free-text label is forbidden.
|
|
106
|
+
if label is not None:
|
|
107
|
+
raise ValueError("biographical CharacterFeature must not also carry a `label`")
|
|
108
|
+
if year is None:
|
|
109
|
+
raise ValueError("biographical CharacterFeature requires a `year`")
|
|
110
|
+
else:
|
|
111
|
+
# Free-text: a label is mandatory; a stray `year` is forbidden rather
|
|
112
|
+
# than silently dropped, since only the biographical variant emits it.
|
|
113
|
+
if label is None:
|
|
114
|
+
raise ValueError("CharacterFeature requires either `label` or (`kind` + `year`)")
|
|
115
|
+
if year is not None:
|
|
116
|
+
raise ValueError("free-text CharacterFeature must not carry a `year`")
|
|
117
|
+
return data
|
|
118
|
+
|
|
119
|
+
def model_post_init(self, __context: object) -> None:
|
|
120
|
+
if self.kind is not None:
|
|
121
|
+
# Biographical features live under a `bio/` sub-segment: a free-text
|
|
122
|
+
# slug never contains `/`, so it can never collide with the
|
|
123
|
+
# birth/death token on the same character (FR-021).
|
|
124
|
+
self._uri = URIRef(f"{self.character_uri}/feature/bio/{self.kind}")
|
|
125
|
+
else:
|
|
126
|
+
assert self.label is not None # guaranteed by _exactly_one_variant
|
|
127
|
+
self._uri = URIRef(f"{self.character_uri}/feature/{make_slug(self.label)}")
|
|
128
|
+
|
|
129
|
+
def to_triples(self) -> Iterable[Triple]:
|
|
130
|
+
yield (self.uri, RDF.type, self.golem_class)
|
|
131
|
+
if self.kind is not None:
|
|
132
|
+
type_uri = URIRef(f"{self.uri_base}type/{self.kind}")
|
|
133
|
+
yield (self.uri, HAS_TYPE, type_uri)
|
|
134
|
+
yield (type_uri, RDF.type, CLASS_IRI["Type"])
|
|
135
|
+
assert self.year is not None # guaranteed by _exactly_one_variant
|
|
136
|
+
dimension = Dimension(uri_base=self.uri_base, feature_uri=self.uri, year=self.year)
|
|
137
|
+
yield (self.uri, HAS_DIMENSION, dimension.uri)
|
|
138
|
+
yield from dimension.to_triples()
|
|
139
|
+
else:
|
|
140
|
+
yield (self.uri, RDFS.label, RdfLiteral(self.label))
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
class CharacterRole(GolemEntity):
|
|
144
|
+
"""A character-scoped narrative role (``golem:G11_Narrative_Role``).
|
|
145
|
+
|
|
146
|
+
URI is ``{character.uri}/role/{slug(label)}``; emits the type assertion and
|
|
147
|
+
the role text on ``rdfs:label``. Distinct from the top-level ``NarrativeRole``
|
|
148
|
+
concept: this node is inlined under a character and is not in ``CONCEPTS``.
|
|
149
|
+
"""
|
|
150
|
+
|
|
151
|
+
golem_class: ClassVar[URIRef] = CLASS_IRI["NarrativeRole"]
|
|
152
|
+
|
|
153
|
+
character_uri: URIRef
|
|
154
|
+
label: str
|
|
155
|
+
|
|
156
|
+
def model_post_init(self, __context: object) -> None:
|
|
157
|
+
self._uri = URIRef(f"{self.character_uri}/role/{make_slug(self.label)}")
|
|
158
|
+
|
|
159
|
+
def to_triples(self) -> Iterable[Triple]:
|
|
160
|
+
yield (self.uri, RDF.type, self.golem_class)
|
|
161
|
+
yield (self.uri, RDFS.label, RdfLiteral(self.label))
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""Inference module: provenance for inferred attributes (CIDOC-CRM E13)."""
|
|
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, MintedEntity
|
|
10
|
+
from bookwright.golem.namespaces import (
|
|
11
|
+
ASSIGNED,
|
|
12
|
+
ASSIGNED_ATTRIBUTE_TO,
|
|
13
|
+
CLASS_IRI,
|
|
14
|
+
REFERS_TO,
|
|
15
|
+
USED_SPECIFIC_OBJECT,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class AttributeAssignment(MintedEntity):
|
|
20
|
+
"""A provenance record for an inferred attribute (``crm:E13_Attribute_Assignment``).
|
|
21
|
+
|
|
22
|
+
Constructed without a ``name``: its identity token is the time-ordered uuid7
|
|
23
|
+
minted by :class:`~bookwright.golem.base.MintedEntity`, so two assignments
|
|
24
|
+
created in sequence sort in creation order (FR-013, D3). The ``source`` path is
|
|
25
|
+
stored and emitted verbatim as an ``xsd:string`` literal (FR-009, D7);
|
|
26
|
+
``premise`` is omitted from the triples when ``None``.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
golem_class: ClassVar[URIRef] = CLASS_IRI["AttributeAssignment"]
|
|
30
|
+
path_segment: ClassVar[str] = "assertion"
|
|
31
|
+
cross_refs: ClassVar[tuple[CrossRef, ...]] = (
|
|
32
|
+
CrossRef("target", ASSIGNED_ATTRIBUTE_TO),
|
|
33
|
+
CrossRef("attribute", ASSIGNED),
|
|
34
|
+
CrossRef("source", USED_SPECIFIC_OBJECT, literal=True),
|
|
35
|
+
CrossRef("premise", REFERS_TO),
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
target: GolemEntity | URIRef
|
|
39
|
+
attribute: GolemEntity | URIRef
|
|
40
|
+
source: str
|
|
41
|
+
premise: GolemEntity | URIRef | None = None
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""Narrative module: units, their functions/roles, and ordered sequences."""
|
|
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, PROPER_PART, REFERS_TO
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class NarrativeUnit(SluggedEntity):
|
|
14
|
+
"""A narrative unit (``golem:G9_Narrative_Unit``).
|
|
15
|
+
|
|
16
|
+
Refers to each of its narrative functions and roles (``crm:P67_refers_to``).
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
golem_class: ClassVar[URIRef] = CLASS_IRI["NarrativeUnit"]
|
|
20
|
+
path_segment: ClassVar[str] = "narrative-unit"
|
|
21
|
+
cross_refs: ClassVar[tuple[CrossRef, ...]] = (
|
|
22
|
+
CrossRef("functions", REFERS_TO, multi=True),
|
|
23
|
+
CrossRef("roles", REFERS_TO, multi=True),
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
functions: tuple[GolemEntity | URIRef, ...] = ()
|
|
27
|
+
roles: tuple[GolemEntity | URIRef, ...] = ()
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class NarrativeFunction(SluggedEntity):
|
|
31
|
+
"""A narrative function (``golem:G10_Narrative_Function``). Identity only."""
|
|
32
|
+
|
|
33
|
+
golem_class: ClassVar[URIRef] = CLASS_IRI["NarrativeFunction"]
|
|
34
|
+
path_segment: ClassVar[str] = "narrative-function"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class NarrativeRole(SluggedEntity):
|
|
38
|
+
"""A narrative role (``golem:G11_Narrative_Role``). Identity only."""
|
|
39
|
+
|
|
40
|
+
golem_class: ClassVar[URIRef] = CLASS_IRI["NarrativeRole"]
|
|
41
|
+
path_segment: ClassVar[str] = "narrative-role"
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class NarrativeSequence(SluggedEntity):
|
|
45
|
+
"""A narrative sequence (``golem:G7_Narrative_Sequence``).
|
|
46
|
+
|
|
47
|
+
Emits one ``dlp:proper-part`` triple per member unit, in declared order
|
|
48
|
+
(FR-015). RDF is unordered; the ordering is the caller's tuple order.
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
golem_class: ClassVar[URIRef] = CLASS_IRI["NarrativeSequence"]
|
|
52
|
+
path_segment: ClassVar[str] = "narrative-sequence"
|
|
53
|
+
cross_refs: ClassVar[tuple[CrossRef, ...]] = (CrossRef("units", PROPER_PART, multi=True),)
|
|
54
|
+
|
|
55
|
+
units: tuple[GolemEntity | URIRef, ...] = ()
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
"""Provenance module: research entities (Source / Finding / Anchor).
|
|
2
|
+
|
|
3
|
+
The three research concepts of iteration 012 (design § 20). They reuse the GOLEM
|
|
4
|
+
machinery — frozen Pydantic identity, deterministic URIs, ``to_triples()`` — but
|
|
5
|
+
introduce **no new GOLEM/ontology class** (Constitution X, FR-001):
|
|
6
|
+
|
|
7
|
+
- :class:`Source` is typed purely via ``crm:E55_Type`` (``crm:P2_has_type`` →
|
|
8
|
+
a ``sources.ttl`` individual); it emits **no** ``rdf:type`` (research D2).
|
|
9
|
+
- :class:`Finding` and :class:`Anchor` reify on ``crm:E13_Attribute_Assignment``
|
|
10
|
+
(the same class the Inference module uses), staying distinguishable from inferred
|
|
11
|
+
assertions by their URI segment and their ``bw:`` predicates (FR-018, research D9).
|
|
12
|
+
|
|
13
|
+
All Bookwright (``bw:``) predicates and the source-type / reliability individuals
|
|
14
|
+
are declared in ``resources/vocabularies/sources.ttl``, never in the frozen
|
|
15
|
+
``golem.ttl``.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import datetime
|
|
21
|
+
from collections.abc import Iterable
|
|
22
|
+
from typing import ClassVar, Literal
|
|
23
|
+
|
|
24
|
+
from pydantic import field_validator
|
|
25
|
+
from rdflib.namespace import RDF, XSD
|
|
26
|
+
from rdflib.term import Literal as RdfLiteral
|
|
27
|
+
from rdflib.term import URIRef
|
|
28
|
+
|
|
29
|
+
from bookwright.golem.base import MintedEntity, SluggedEntity, Triple
|
|
30
|
+
from bookwright.golem.modules.feature import gyear_literal
|
|
31
|
+
from bookwright.golem.namespaces import (
|
|
32
|
+
ASSIGNED_ATTRIBUTE_TO,
|
|
33
|
+
BEGIN_OF_BEGIN,
|
|
34
|
+
BW_ACCESS_DATE,
|
|
35
|
+
BW_ASSERTED_BY,
|
|
36
|
+
BW_AUTHOR,
|
|
37
|
+
BW_CLAIM,
|
|
38
|
+
BW_CONSTRAINS,
|
|
39
|
+
BW_OPEN,
|
|
40
|
+
BW_ORIGINAL_LANGUAGE,
|
|
41
|
+
BW_ORIGINAL_QUOTE,
|
|
42
|
+
BW_PROMOTES,
|
|
43
|
+
BW_REFERENCE,
|
|
44
|
+
BW_RELIABILITY,
|
|
45
|
+
BW_RELIABILITY_JUSTIFICATION,
|
|
46
|
+
BW_SUPPORTED_BY,
|
|
47
|
+
BW_TRANSLATION,
|
|
48
|
+
CLASS_IRI,
|
|
49
|
+
E52_TIME_SPAN,
|
|
50
|
+
END_OF_END,
|
|
51
|
+
HAS_TIME_SPAN,
|
|
52
|
+
HAS_TYPE,
|
|
53
|
+
RELIABILITY_IRI,
|
|
54
|
+
SOURCE_TYPE_IRI,
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
SourceType = Literal[
|
|
58
|
+
"primaria", "secundaria", "oficial", "académica", "periodística", "testimonial"
|
|
59
|
+
]
|
|
60
|
+
"""The six controlled source-type values (FR-003). The `Literal` is the enforcement
|
|
61
|
+
point — an out-of-vocabulary value raises a `ValidationError` (research D4)."""
|
|
62
|
+
|
|
63
|
+
Reliability = Literal["alta", "media", "baja"]
|
|
64
|
+
"""The three controlled reliability values (FR-004)."""
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _string(value: str) -> RdfLiteral:
|
|
68
|
+
"""An ``xsd:string`` literal (the default datatype for every prose facet)."""
|
|
69
|
+
return RdfLiteral(value, datatype=XSD.string)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class Source(SluggedEntity):
|
|
73
|
+
"""A research source, typed via ``crm:E55_Type`` — **no** ``rdf:type`` (D2).
|
|
74
|
+
|
|
75
|
+
Its identity token is the ASCII slug of ``name`` (segment ``source``), like
|
|
76
|
+
:class:`~bookwright.golem.modules.character.Character`. ``type`` and
|
|
77
|
+
``reliability`` are controlled vocabularies enforced by ``Literal`` fields;
|
|
78
|
+
an out-of-vocabulary value is a ``ValidationError`` the reader turns into a
|
|
79
|
+
build-aborting :class:`~bookwright.io.errors.ResearchError` (FR-016).
|
|
80
|
+
|
|
81
|
+
``translation`` is set by the reader **only** when the source's
|
|
82
|
+
``original_language`` differs from the book language (research D6); the entity
|
|
83
|
+
itself is language-context-free.
|
|
84
|
+
"""
|
|
85
|
+
|
|
86
|
+
# A documented, *unemitted* placeholder: Source overrides ``to_triples`` and
|
|
87
|
+
# yields no ``(uri, rdf:type, golem_class)`` triple. Identity in the graph is
|
|
88
|
+
# ``?s crm:P2_has_type ?t . ?t a crm:E55_Type`` (research D2).
|
|
89
|
+
golem_class: ClassVar[URIRef] = CLASS_IRI["Type"]
|
|
90
|
+
path_segment: ClassVar[str] = "source"
|
|
91
|
+
|
|
92
|
+
reference: str
|
|
93
|
+
author: str
|
|
94
|
+
original_language: str
|
|
95
|
+
type: SourceType
|
|
96
|
+
reliability: Reliability
|
|
97
|
+
reliability_justification: str
|
|
98
|
+
access_date: datetime.date
|
|
99
|
+
original_quote: str
|
|
100
|
+
translation: str | None = None
|
|
101
|
+
|
|
102
|
+
@field_validator("reliability_justification")
|
|
103
|
+
@classmethod
|
|
104
|
+
def _non_empty_justification(cls, value: str) -> str:
|
|
105
|
+
if not value.strip():
|
|
106
|
+
raise ValueError("`reliability_justification` must not be empty")
|
|
107
|
+
return value
|
|
108
|
+
|
|
109
|
+
def to_triples(self) -> Iterable[Triple]:
|
|
110
|
+
"""Emit the source's provenance facets — typed via E55, never ``rdf:type``."""
|
|
111
|
+
yield (self.uri, HAS_TYPE, SOURCE_TYPE_IRI[self.type])
|
|
112
|
+
yield (self.uri, BW_RELIABILITY, RELIABILITY_IRI[self.reliability])
|
|
113
|
+
yield (self.uri, BW_RELIABILITY_JUSTIFICATION, _string(self.reliability_justification))
|
|
114
|
+
yield (self.uri, BW_REFERENCE, _string(self.reference))
|
|
115
|
+
yield (self.uri, BW_AUTHOR, _string(self.author))
|
|
116
|
+
yield (self.uri, BW_ORIGINAL_LANGUAGE, _string(self.original_language))
|
|
117
|
+
yield (self.uri, BW_ACCESS_DATE, RdfLiteral(self.access_date, datatype=XSD.date))
|
|
118
|
+
yield (self.uri, BW_ORIGINAL_QUOTE, _string(self.original_quote))
|
|
119
|
+
if self.translation is not None:
|
|
120
|
+
yield (self.uri, BW_TRANSLATION, _string(self.translation))
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
class Finding(MintedEntity):
|
|
124
|
+
"""A research finding, reified on ``crm:E13_Attribute_Assignment`` (FR-006).
|
|
125
|
+
|
|
126
|
+
Its identity token is the time-ordered uuid7 minted once by
|
|
127
|
+
:class:`~bookwright.golem.base.MintedEntity` (segment ``finding``). A **closed**
|
|
128
|
+
finding records a ``claim``, who asserts
|
|
129
|
+
it (``asserted_by``, default ``"author"``), the narrative entity it
|
|
130
|
+
``bears_on`` (reusing the Inference module's ``crm:P140_assigned_attribute_to``)
|
|
131
|
+
and one or more supporting ``sources``. An **open** finding (FR-008) is an
|
|
132
|
+
unresolved question: ``claim``/``bears_on``/``sources`` may all be empty and the
|
|
133
|
+
entity is still valid, emitting just ``rdf:type`` + ``bw:open true``.
|
|
134
|
+
|
|
135
|
+
``bw:assertedBy`` is emitted only alongside a ``claim`` — an open question with
|
|
136
|
+
no claim asserts nothing, so it carries no asserter (research D9).
|
|
137
|
+
"""
|
|
138
|
+
|
|
139
|
+
golem_class: ClassVar[URIRef] = CLASS_IRI["AttributeAssignment"]
|
|
140
|
+
path_segment: ClassVar[str] = "finding"
|
|
141
|
+
|
|
142
|
+
claim: str | None = None
|
|
143
|
+
asserted_by: str = "author"
|
|
144
|
+
bears_on: URIRef | None = None
|
|
145
|
+
sources: tuple[URIRef, ...] = ()
|
|
146
|
+
open: bool = False
|
|
147
|
+
|
|
148
|
+
def to_triples(self) -> Iterable[Triple]:
|
|
149
|
+
yield (self.uri, RDF.type, self.golem_class)
|
|
150
|
+
if self.claim is not None:
|
|
151
|
+
yield (self.uri, BW_CLAIM, _string(self.claim))
|
|
152
|
+
yield (self.uri, BW_ASSERTED_BY, _string(self.asserted_by))
|
|
153
|
+
if self.bears_on is not None:
|
|
154
|
+
yield (self.uri, ASSIGNED_ATTRIBUTE_TO, self.bears_on)
|
|
155
|
+
for source in self.sources:
|
|
156
|
+
yield (self.uri, BW_SUPPORTED_BY, source)
|
|
157
|
+
if self.open:
|
|
158
|
+
yield (self.uri, BW_OPEN, RdfLiteral(True))
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
class Anchor(MintedEntity):
|
|
162
|
+
"""A binding constraint promoting a :class:`Finding`, reified on E13 (FR-009).
|
|
163
|
+
|
|
164
|
+
Its identity token is the uuid7 minted by
|
|
165
|
+
:class:`~bookwright.golem.base.MintedEntity` (segment ``anchor``). It
|
|
166
|
+
``promotes`` the finding it derives from and ``constrains`` a narrative entity
|
|
167
|
+
(or the well-known untyped timeline IRI, research D10). When the named target
|
|
168
|
+
does not resolve in the bible, the reader builds the anchor with
|
|
169
|
+
``constrains=None`` and surfaces a warning — the ``bw:constrains`` triple is
|
|
170
|
+
simply omitted, the build still succeeds (research D12).
|
|
171
|
+
|
|
172
|
+
An optional time-span (``begin``/``end`` years, research D5) is emitted as a
|
|
173
|
+
``crm:E52_Time-Span`` sub-node under ``crm:P4_has_time-span``; an anchor with
|
|
174
|
+
neither bound emits no time-span (FR-010).
|
|
175
|
+
"""
|
|
176
|
+
|
|
177
|
+
golem_class: ClassVar[URIRef] = CLASS_IRI["AttributeAssignment"]
|
|
178
|
+
path_segment: ClassVar[str] = "anchor"
|
|
179
|
+
|
|
180
|
+
promotes: URIRef
|
|
181
|
+
constrains: URIRef | None = None
|
|
182
|
+
begin: int | None = None
|
|
183
|
+
end: int | None = None
|
|
184
|
+
|
|
185
|
+
def to_triples(self) -> Iterable[Triple]:
|
|
186
|
+
yield (self.uri, RDF.type, self.golem_class)
|
|
187
|
+
yield (self.uri, BW_PROMOTES, self.promotes)
|
|
188
|
+
if self.constrains is not None:
|
|
189
|
+
yield (self.uri, BW_CONSTRAINS, self.constrains)
|
|
190
|
+
if self.begin is not None or self.end is not None:
|
|
191
|
+
time_span = URIRef(f"{self.uri}/time-span")
|
|
192
|
+
yield (self.uri, HAS_TIME_SPAN, time_span)
|
|
193
|
+
yield (time_span, RDF.type, E52_TIME_SPAN)
|
|
194
|
+
if self.begin is not None:
|
|
195
|
+
yield (time_span, BEGIN_OF_BEGIN, gyear_literal(self.begin))
|
|
196
|
+
if self.end is not None:
|
|
197
|
+
yield (time_span, END_OF_END, gyear_literal(self.end))
|