bookwright-cli 0.3.1__tar.gz → 0.3.2__tar.gz

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 (164) hide show
  1. {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/PKG-INFO +1 -1
  2. {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/__init__.py +1 -1
  3. {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/golem/deferrals.py +2 -6
  4. bookwright_cli-0.3.2/src/bookwright/io/_bible_builders.py +268 -0
  5. {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/io/bible.py +81 -197
  6. {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/resources/commands/bookwright-bible.md +5 -4
  7. {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/.gitignore +0 -0
  8. {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/LICENSE +0 -0
  9. {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/NOTICE +0 -0
  10. {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/README.md +0 -0
  11. {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/pyproject.toml +0 -0
  12. {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/__main__.py +0 -0
  13. {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/cli.py +0 -0
  14. {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/commands/__init__.py +0 -0
  15. {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/commands/_envelope.py +0 -0
  16. {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/commands/_graph.py +0 -0
  17. {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/commands/_project.py +0 -0
  18. {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/commands/check.py +0 -0
  19. {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/commands/focus/__init__.py +0 -0
  20. {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/commands/focus/clear.py +0 -0
  21. {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/commands/focus/errors.py +0 -0
  22. {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/commands/focus/set_.py +0 -0
  23. {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/commands/focus/show.py +0 -0
  24. {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/commands/graph/__init__.py +0 -0
  25. {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/commands/graph/build.py +0 -0
  26. {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/commands/graph/query.py +0 -0
  27. {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/commands/init/__init__.py +0 -0
  28. {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/commands/init/conflict.py +0 -0
  29. {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/commands/init/envelope.py +0 -0
  30. {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/commands/init/git.py +0 -0
  31. {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/commands/init/main.py +0 -0
  32. {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/commands/init/resolve.py +0 -0
  33. {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/commands/init/scaffold.py +0 -0
  34. {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/commands/init/validate.py +0 -0
  35. {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/commands/integration/__init__.py +0 -0
  36. {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/commands/integration/use.py +0 -0
  37. {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/commands/status.py +0 -0
  38. {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/commands/validate.py +0 -0
  39. {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/commands/version.py +0 -0
  40. {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/core/__init__.py +0 -0
  41. {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/core/_blocks.py +0 -0
  42. {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/core/_build.py +0 -0
  43. {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/core/_focus_block.py +0 -0
  44. {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/core/_research_block.py +0 -0
  45. {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/core/_translate.py +0 -0
  46. {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/core/errors.py +0 -0
  47. {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/core/iso639_1.py +0 -0
  48. {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/core/manifest.py +0 -0
  49. {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/errors.py +0 -0
  50. {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/golem/__init__.py +0 -0
  51. {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/golem/base.py +0 -0
  52. {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/golem/errors.py +0 -0
  53. {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/golem/modules/__init__.py +0 -0
  54. {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/golem/modules/character.py +0 -0
  55. {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/golem/modules/event.py +0 -0
  56. {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/golem/modules/feature.py +0 -0
  57. {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/golem/modules/inference.py +0 -0
  58. {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/golem/modules/narrative.py +0 -0
  59. {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/golem/modules/provenance.py +0 -0
  60. {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/golem/modules/relationship.py +0 -0
  61. {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/golem/modules/setting.py +0 -0
  62. {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/golem/namespaces.py +0 -0
  63. {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/golem/serialize.py +0 -0
  64. {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/golem/slug.py +0 -0
  65. {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/indexers/__init__.py +0 -0
  66. {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/indexers/base.py +0 -0
  67. {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/indexers/errors.py +0 -0
  68. {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/indexers/rdflib_indexer.py +0 -0
  69. {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/integrations/__init__.py +0 -0
  70. {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/integrations/base.py +0 -0
  71. {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/integrations/claude/__init__.py +0 -0
  72. {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/integrations/constants.py +0 -0
  73. {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/integrations/descriptions.py +0 -0
  74. {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/integrations/errors.py +0 -0
  75. {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/integrations/generic/__init__.py +0 -0
  76. {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/integrations/lint.py +0 -0
  77. {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/integrations/materialize.py +0 -0
  78. {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/integrations/options.py +0 -0
  79. {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/io/__init__.py +0 -0
  80. {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/io/_research_identity.py +0 -0
  81. {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/io/errors.py +0 -0
  82. {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/io/frontmatter.py +0 -0
  83. {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/io/fs.py +0 -0
  84. {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/io/manuscript.py +0 -0
  85. {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/io/project.py +0 -0
  86. {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/io/report.py +0 -0
  87. {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/io/research.py +0 -0
  88. {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/resources/__init__.py +0 -0
  89. {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/resources/commands/bookwright-analyze.md +0 -0
  90. {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/resources/commands/bookwright-checklist.md +0 -0
  91. {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/resources/commands/bookwright-clarify.md +0 -0
  92. {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/resources/commands/bookwright-constitution.md +0 -0
  93. {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/resources/commands/bookwright-continuity.md +0 -0
  94. {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/resources/commands/bookwright-draft.md +0 -0
  95. {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/resources/commands/bookwright-outline.md +0 -0
  96. {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/resources/commands/bookwright-research.md +0 -0
  97. {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/resources/commands/bookwright-scenes.md +0 -0
  98. {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/resources/commands/bookwright-synopsis.md +0 -0
  99. {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/resources/commands/bookwright-verify.md +0 -0
  100. {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/resources/commands/references/golem-character.md +0 -0
  101. {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/resources/commands/references/golem-events-timeline.md +0 -0
  102. {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/resources/commands/references/golem-relationships.md +0 -0
  103. {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/resources/commands/references/greimas-actants.md +0 -0
  104. {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/resources/commands/references/pending-protocol.md +0 -0
  105. {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/resources/commands/references/propp-functions.md +0 -0
  106. {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/resources/commands/references/research-format.md +0 -0
  107. {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/resources/project/.bookwright/cache/.gitkeep +0 -0
  108. {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/resources/project/.bookwright/schema/.gitkeep +0 -0
  109. {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/resources/project/.bookwright/templates/.gitkeep +0 -0
  110. {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/resources/project/.gitignore +0 -0
  111. {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/resources/project/README.md.j2 +0 -0
  112. {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/resources/project/__init__.py +0 -0
  113. {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/resources/project/bible/characters/.gitkeep +0 -0
  114. {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/resources/project/bible/constitution.md.j2 +0 -0
  115. {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/resources/project/bible/glossary.md +0 -0
  116. {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/resources/project/bible/locations/.gitkeep +0 -0
  117. {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/resources/project/bible/pov-structure.md +0 -0
  118. {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/resources/project/bible/relationships.md +0 -0
  119. {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/resources/project/bible/research/_index.md +0 -0
  120. {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/resources/project/bible/research/sources.md +0 -0
  121. {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/resources/project/bible/settings/.gitkeep +0 -0
  122. {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/resources/project/bible/subplots.md +0 -0
  123. {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/resources/project/bible/themes.md +0 -0
  124. {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/resources/project/bible/timeline.md +0 -0
  125. {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/resources/project/manuscript/.gitkeep +0 -0
  126. {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/resources/project/outline/arcs.md +0 -0
  127. {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/resources/project/outline/scenes.md +0 -0
  128. {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/resources/project/outline/structure.md +0 -0
  129. {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/resources/project/outline/synopsis.md +0 -0
  130. {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/resources/schemas/__init__.py +0 -0
  131. {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/resources/schemas/golem-1.1/VERSION +0 -0
  132. {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/resources/schemas/golem-1.1/golem.ttl +0 -0
  133. {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/resources/schemas/golem-1.1/version.json +0 -0
  134. {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/resources/templates/__init__.py +0 -0
  135. {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/resources/templates/bible/character.md.tmpl +0 -0
  136. {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/resources/templates/bible/location.md.tmpl +0 -0
  137. {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/resources/templates/bible/research/_index.md.tmpl +0 -0
  138. {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/resources/templates/bible/research/sources.md.tmpl +0 -0
  139. {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/resources/templates/bible/research/tema.md.tmpl +0 -0
  140. {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/resources/templates/bible/setting.md.tmpl +0 -0
  141. {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/resources/templates/manifest.template.toml +0 -0
  142. {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/resources/templates/manuscript/chapter.md.tmpl +0 -0
  143. {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/resources/templates/scenes/scene.md.tmpl +0 -0
  144. {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/resources/vocabularies/__init__.py +0 -0
  145. {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/resources/vocabularies/greimas.ttl +0 -0
  146. {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/resources/vocabularies/propp.ttl +0 -0
  147. {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/resources/vocabularies/sources.ttl +0 -0
  148. {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/status/__init__.py +0 -0
  149. {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/status/model.py +0 -0
  150. {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/status/queries.py +0 -0
  151. {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/status/rules.py +0 -0
  152. {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/validation/__init__.py +0 -0
  153. {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/validation/anchor_queries.py +0 -0
  154. {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/validation/base.py +0 -0
  155. {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/validation/queries.py +0 -0
  156. {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/validation/registry.py +0 -0
  157. {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/validation/report.py +0 -0
  158. {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/validation/runner.py +0 -0
  159. {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/validation/validators/__init__.py +0 -0
  160. {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/validation/validators/character_presence.py +0 -0
  161. {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/validation/validators/factual_anchor.py +0 -0
  162. {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/validation/validators/focalization.py +0 -0
  163. {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/validation/validators/setting_continuity.py +0 -0
  164. {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/validation/validators/temporal.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: bookwright-cli
3
- Version: 0.3.1
3
+ Version: 0.3.2
4
4
  Summary: Spec-driven authoring toolkit for novels, essays, and memoirs.
5
5
  Project-URL: Homepage, https://github.com/jmorenobl/bookwright
6
6
  Project-URL: Repository, https://github.com/jmorenobl/bookwright
@@ -1,3 +1,3 @@
1
1
  """Bookwright — Spec-driven authoring toolkit."""
2
2
 
3
- __version__ = "0.3.1"
3
+ __version__ = "0.3.2"
@@ -1,6 +1,6 @@
1
1
  """The deferral registry: which GOLEM concepts are modelled but not yet fed.
2
2
 
3
- Seven of the thirteen :data:`bookwright.golem.CONCEPTS` have no authored-text
3
+ Six of the thirteen :data:`bookwright.golem.CONCEPTS` have no authored-text
4
4
  ingestion path today — they are *modelled* (a frozen class, a ``CLASS_IRI``
5
5
  entry) but never *materialized* by any builder over ``bible/*.md``. This module
6
6
  names them explicitly so the gap is a written contract rather than silent.
@@ -34,10 +34,6 @@ class DeferralNote(NamedTuple):
34
34
 
35
35
 
36
36
  DEFERRED_CONCEPTS: dict[str, DeferralNote] = {
37
- "NarrativeLocation": DeferralNote(
38
- reason="scaffold/skill write bible/locations/ but nothing ingests it (G13)",
39
- target_version="v0.3.x",
40
- ),
41
37
  "Object": DeferralNote(
42
38
  reason="no builder, no bible/objects/, no skill (G16)",
43
39
  target_version="v0.3.x",
@@ -63,5 +59,5 @@ DEFERRED_CONCEPTS: dict[str, DeferralNote] = {
63
59
  target_version="undecided",
64
60
  ),
65
61
  }
66
- """Concept name → deferral note. Exactly seven entries, each key a ``CONCEPTS``
62
+ """Concept name → deferral note. Exactly six entries, each key a ``CONCEPTS``
67
63
  member; the orphan set the parity test derives must equal this dict's keys."""
@@ -0,0 +1,268 @@
1
+ """Concrete builders, coercers, and the mapper's context/result records.
2
+
3
+ A behavior-preserving extraction from :mod:`bookwright.io.bible` (iteration 025):
4
+ ``bible.py`` keeps the orchestration — discovering files and wiring per-concept
5
+ passes — while this module holds the leaf builders that turn one frontmatter
6
+ mapping into a GOLEM entity, the value coercers they share, and the mutable
7
+ context / immutable result records threaded through the whole map.
8
+
9
+ The dependency is **one-way**: ``bible.py`` imports these names; this module
10
+ imports nothing from ``bible.py`` (no cycle — Principle IX). It depends only on
11
+ ``golem``, ``io.errors``, ``io.report``, and the stdlib.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ from collections.abc import Callable
17
+ from dataclasses import dataclass, field
18
+ from pathlib import Path
19
+ from typing import Any
20
+
21
+ from rdflib.term import URIRef
22
+
23
+ from bookwright.golem import Character, EmptySlugError, NarrativeEvent, NarrativeLocation
24
+ from bookwright.golem.base import GolemEntity
25
+ from bookwright.golem.namespaces import TEMPORAL_RELATIONS
26
+ from bookwright.golem.slug import make_slug
27
+
28
+ from .errors import InvalidFrontmatterError, SlugCollisionError
29
+ from .report import SkippedFile, UnknownKey, UnresolvedParticipant
30
+
31
+ # The five qualitative temporal relations an event may declare (each a list of
32
+ # event names resolved against the timeline's own event index — research D11).
33
+ # Derived from the single source of truth so the keys never drift from the model;
34
+ # ``bible.py`` imports it to assemble ``EVENT_ITEM_KEYS``.
35
+ RELATION_KEYS: tuple[str, ...] = tuple(rel.name for rel in TEMPORAL_RELATIONS)
36
+
37
+ # A directory builder maps ``(frontmatter, relpath) → entity``; a collection
38
+ # builder maps an ``_ItemContext`` (name, resolved participants, the raw item, and
39
+ # the collection's own name→URI index) → entity. Typed so ``mypy --strict`` checks
40
+ # every call site (rather than the previous ``Any`` escape hatch).
41
+ _Builder = Callable[[dict[str, Any], str], GolemEntity]
42
+ _ItemBuilder = Callable[["_ItemContext"], GolemEntity]
43
+
44
+
45
+ @dataclass(frozen=True)
46
+ class MappedEntity:
47
+ """One constructed entity paired with the source needed for provenance (R6)."""
48
+
49
+ entity: GolemEntity
50
+ relpath: str
51
+ key_lines: dict[str, int]
52
+
53
+
54
+ @dataclass
55
+ class MapResult:
56
+ """The outcome of mapping a project's bible to GOLEM entities."""
57
+
58
+ mapped: list[MappedEntity] = field(default_factory=list)
59
+ files_processed: int = 0
60
+ skipped: list[SkippedFile] = field(default_factory=list)
61
+ unknown_keys: list[UnknownKey] = field(default_factory=list)
62
+ unresolved_participants: list[UnresolvedParticipant] = field(default_factory=list)
63
+ # ``make_slug(name) → URI`` for every character, setting, event and location — the
64
+ # research ``bears_on``/``constrains`` targets (D11), distinct from participant
65
+ # ``slug_index``.
66
+ entity_index: dict[str, URIRef] = field(default_factory=dict)
67
+
68
+ @property
69
+ def entities(self) -> list[GolemEntity]:
70
+ return [m.entity for m in self.mapped]
71
+
72
+
73
+ class _Collisions:
74
+ """Tracks ``(concept, slug) → relpath`` to detect identifier collisions (FR-014)."""
75
+
76
+ def __init__(self) -> None:
77
+ self._seen: dict[tuple[str, str], str] = {}
78
+
79
+ def record(self, concept: str, slug: str, relpath: str) -> None:
80
+ prior = self._seen.get((concept, slug))
81
+ if prior is not None and prior != relpath:
82
+ raise SlugCollisionError(slug, prior, relpath)
83
+ self._seen[(concept, slug)] = relpath
84
+
85
+
86
+ @dataclass
87
+ class _MapContext:
88
+ """The mutable state every mapping helper shares (R3).
89
+
90
+ Bundling ``project_root``, the accumulating ``result``, the collision
91
+ tracker, and the resolution indices into one object keeps each helper's
92
+ signature small — the four used to be threaded positionally through every
93
+ function. ``settings_index`` is the settings-scoped name→URI index a
94
+ location's ``setting:`` resolves against (iteration 025), kept separate from
95
+ the character-scoped ``slug_index`` and the research ``entity_index``.
96
+ """
97
+
98
+ project_root: Path
99
+ result: MapResult
100
+ collisions: _Collisions
101
+ slug_index: dict[str, URIRef]
102
+ settings_index: dict[str, URIRef] = field(default_factory=dict)
103
+
104
+
105
+ @dataclass(frozen=True)
106
+ class _ItemContext:
107
+ """Everything a collection builder needs for one item (R3)."""
108
+
109
+ ctx: _MapContext
110
+ item: dict[str, Any]
111
+ name: str
112
+ participants: tuple[URIRef, ...]
113
+ relpath: str
114
+ item_index: dict[str, URIRef]
115
+
116
+
117
+ def _require_name(metadata: dict[str, Any]) -> str:
118
+ name = metadata.get("name")
119
+ if not isinstance(name, str) or not name.strip():
120
+ raise InvalidFrontmatterError("", "missing or empty `name`")
121
+ return name
122
+
123
+
124
+ def _coerce_year(value: Any, field_name: str) -> int | None:
125
+ if value is None:
126
+ return None
127
+ if isinstance(value, bool) or not isinstance(value, int):
128
+ raise InvalidFrontmatterError("", f"`{field_name}` must be an integer year")
129
+ return value
130
+
131
+
132
+ def _coerce_str_list(value: Any, field_name: str) -> tuple[str, ...]:
133
+ if value is None:
134
+ return ()
135
+ if not isinstance(value, list) or not all(isinstance(item, str) for item in value):
136
+ raise InvalidFrontmatterError("", f"`{field_name}` must be a list of strings")
137
+ return tuple(value)
138
+
139
+
140
+ def _build_character(uri_base: str, metadata: dict[str, Any]) -> Character:
141
+ name = _require_name(metadata)
142
+ born = _coerce_year(metadata.get("born"), "born")
143
+ died = _coerce_year(metadata.get("died"), "died")
144
+ features = _coerce_str_list(metadata.get("features"), "features")
145
+ roles = _coerce_str_list(metadata.get("narrative_roles"), "narrative_roles")
146
+ return Character(
147
+ uri_base=uri_base,
148
+ name=name,
149
+ born=born,
150
+ died=died,
151
+ features=features,
152
+ narrative_roles=roles,
153
+ )
154
+
155
+
156
+ def _build_event(uri_base: str, ic: _ItemContext) -> NarrativeEvent:
157
+ """Construct a ``NarrativeEvent`` from a timeline item: interval + relations."""
158
+ begin, end = _resolve_interval(ic)
159
+ relations = {
160
+ key: _resolve_refs(ic.ctx, ic.item.get(key), ic.item_index, ic.name, ic.relpath)
161
+ for key in RELATION_KEYS
162
+ }
163
+ return NarrativeEvent(
164
+ uri_base=uri_base,
165
+ name=ic.name,
166
+ participants=ic.participants,
167
+ begin=begin,
168
+ end=end,
169
+ **relations,
170
+ )
171
+
172
+
173
+ def _resolve_interval(ic: _ItemContext) -> tuple[int | None, int | None]:
174
+ """Coerce ``begin`` / ``end`` / ``date`` to int years, enforcing exclusivity.
175
+
176
+ ``date`` is a single-year shorthand (``begin == end``). Supplying ``date``
177
+ alongside ``begin``/``end`` is a soft warning (``date`` ignored), like an
178
+ unknown key — never an abort.
179
+ """
180
+ begin = _coerce_year(ic.item.get("begin"), "begin")
181
+ end = _coerce_year(ic.item.get("end"), "end")
182
+ date = _coerce_year(ic.item.get("date"), "date")
183
+ if date is not None:
184
+ if begin is not None or end is not None:
185
+ # Mutually exclusive: keep begin/end, drop date, flag it softly.
186
+ ic.ctx.result.unknown_keys.append(UnknownKey(path=ic.relpath, key="date"))
187
+ else:
188
+ return date, date
189
+ return begin, end
190
+
191
+
192
+ def _resolve_refs(
193
+ ctx: _MapContext,
194
+ raw: Any,
195
+ index: dict[str, URIRef],
196
+ entity_name: str,
197
+ relpath: str,
198
+ ) -> tuple[URIRef, ...]:
199
+ """Resolve a list of names against ``index`` (characters or sibling events).
200
+
201
+ A non-list value, or a name absent from the index, is surfaced as an
202
+ ``UnresolvedParticipant`` soft warning (no abort); the owning entity is built.
203
+ """
204
+ if raw is None:
205
+ return ()
206
+ if not isinstance(raw, list):
207
+ ctx.result.unresolved_participants.append(
208
+ UnresolvedParticipant(path=relpath, entity=entity_name, name=str(raw))
209
+ )
210
+ return ()
211
+ resolved: list[URIRef] = []
212
+ for ref in raw:
213
+ if not isinstance(ref, str):
214
+ continue
215
+ uri = index.get(make_slug(ref))
216
+ if uri is None:
217
+ ctx.result.unresolved_participants.append(
218
+ UnresolvedParticipant(path=relpath, entity=entity_name, name=ref)
219
+ )
220
+ continue
221
+ resolved.append(uri)
222
+ return tuple(resolved)
223
+
224
+
225
+ def _resolve_setting(
226
+ ctx: _MapContext, metadata: dict[str, Any], entity_name: str, relpath: str
227
+ ) -> URIRef | None:
228
+ """Resolve an optional ``setting:`` against the settings-scoped index (FR-003/004).
229
+
230
+ Absent / ``None`` / blank-or-whitespace → no edge. A non-string is unusable
231
+ frontmatter (``InvalidFrontmatterError`` → the file is skipped). A present name
232
+ that resolves yields the setting's URI (the ``dlp:generic-location`` target); a
233
+ present name that does not resolve — including one that slugs to nothing — is a
234
+ soft miss recorded as an ``UnresolvedParticipant`` (the location is still built,
235
+ no edge, no abort).
236
+ """
237
+ value = metadata.get("setting")
238
+ if value is None:
239
+ return None
240
+ if not isinstance(value, str):
241
+ raise InvalidFrontmatterError("", "`setting` must be a string")
242
+ if not value.strip():
243
+ return None
244
+ try:
245
+ slug = make_slug(value)
246
+ except EmptySlugError:
247
+ slug = None
248
+ uri = ctx.settings_index.get(slug) if slug is not None else None
249
+ if uri is None:
250
+ ctx.result.unresolved_participants.append(
251
+ UnresolvedParticipant(path=relpath, entity=entity_name, name=value)
252
+ )
253
+ return None
254
+ return uri
255
+
256
+
257
+ def _build_location(
258
+ uri_base: str, ctx: _MapContext, metadata: dict[str, Any], relpath: str
259
+ ) -> NarrativeLocation:
260
+ """Build a ``NarrativeLocation`` (G13) from ``name`` + optional ``setting`` (FR-001/002)."""
261
+ name = _require_name(metadata)
262
+ # Validate the name slugs *before* resolving ``setting:``, so an unsluggable name
263
+ # aborts (→ the file is skipped) without ``_resolve_setting`` first recording a
264
+ # stray unresolved-participant warning for a file that produces no entity. Keeps
265
+ # the invariant that a skipped file appears only under ``skipped``.
266
+ make_slug(name)
267
+ setting = _resolve_setting(ctx, metadata, name, relpath)
268
+ return NarrativeLocation(uri_base=uri_base, name=name, setting=setting)
@@ -5,12 +5,19 @@ frontmatter values straight to the iteration-5 constructors — it never builds
5
5
  feature/role/dimension nodes itself (data-model § 0/§ 3). It collects soft
6
6
  warnings (``unknown_keys``, ``unresolved_participants``), skips files whose
7
7
  frontmatter is unusable (FR-013), and raises on a slug collision (FR-014).
8
+
9
+ This module owns the orchestration: discovering files, wiring the per-concept
10
+ passes, and emitting provenance. The leaf builders, coercers, and the
11
+ context/result records live in the sibling :mod:`._bible_builders` (a one-way
12
+ dependency — this module imports from it, never the reverse). The public surface
13
+ (``map_bible``, ``build_provenance``, ``MapResult``, ``MappedEntity``) is
14
+ re-exported here so existing imports keep resolving.
8
15
  """
9
16
 
10
17
  from __future__ import annotations
11
18
 
12
19
  from collections.abc import Callable, Iterable
13
- from dataclasses import dataclass, field
20
+ from dataclasses import dataclass
14
21
  from pathlib import Path
15
22
  from typing import Any
16
23
 
@@ -19,100 +26,79 @@ from rdflib.term import URIRef
19
26
 
20
27
  from bookwright.golem import (
21
28
  AttributeAssignment,
22
- Character,
23
29
  EmptySlugError,
24
- NarrativeEvent,
25
30
  Setting,
26
31
  SocialRelationship,
27
32
  )
28
33
  from bookwright.golem.base import GolemEntity
29
- from bookwright.golem.namespaces import TEMPORAL_RELATIONS
30
34
  from bookwright.golem.slug import make_slug
31
35
 
32
- from .errors import InvalidFrontmatterError, SlugCollisionError
36
+ from ._bible_builders import (
37
+ RELATION_KEYS,
38
+ MappedEntity,
39
+ MapResult,
40
+ _build_character,
41
+ _build_event,
42
+ _build_location,
43
+ _Builder,
44
+ _coerce_str_list,
45
+ _coerce_year,
46
+ _Collisions,
47
+ _ItemBuilder,
48
+ _ItemContext,
49
+ _MapContext,
50
+ _require_name,
51
+ _resolve_interval,
52
+ _resolve_refs,
53
+ _resolve_setting,
54
+ )
55
+ from .errors import InvalidFrontmatterError
33
56
  from .frontmatter import Frontmatter, parse_frontmatter
34
- from .report import SkippedFile, UnknownKey, UnresolvedParticipant
57
+ from .report import SkippedFile, UnknownKey
58
+
59
+ __all__ = [
60
+ "CHARACTER_KEYS",
61
+ "EVENT_ITEM_KEYS",
62
+ "ITEM_KEYS",
63
+ "LOCATION_KEYS",
64
+ "RELATIONSHIPS_TOP_KEYS",
65
+ "RELATION_KEYS",
66
+ "SETTING_KEYS",
67
+ "TIMELINE_TOP_KEYS",
68
+ "MapResult",
69
+ "MappedEntity",
70
+ "_Collisions",
71
+ "_ItemContext",
72
+ # Re-exported from ._bible_builders so ``from bookwright.io.bible import …`` keeps working.
73
+ "_MapContext",
74
+ "_build_character",
75
+ "_build_event",
76
+ "_build_location",
77
+ "_coerce_str_list",
78
+ "_coerce_year",
79
+ "_require_name",
80
+ "_resolve_interval",
81
+ "_resolve_refs",
82
+ "_resolve_setting",
83
+ "build_provenance",
84
+ "map_bible",
85
+ ]
35
86
 
36
87
  CHARACTER_KEYS = frozenset({"name", "born", "died", "features", "narrative_roles"})
37
88
  SETTING_KEYS = frozenset({"name"})
89
+ LOCATION_KEYS = frozenset({"name", "setting"})
38
90
  ITEM_KEYS = frozenset({"name", "participants"})
39
- # The five qualitative temporal relations an event may declare (each a list of
40
- # event names resolved against the timeline's own event index — research D11).
41
- # Derived from the single source of truth so the keys never drift from the model.
42
- RELATION_KEYS: tuple[str, ...] = tuple(rel.name for rel in TEMPORAL_RELATIONS)
43
91
  # Events additionally accept an interval (``begin`` / ``end`` years, or the
44
- # ``date`` single-year shorthand) plus the relation keys.
92
+ # ``date`` single-year shorthand) plus the relation keys (defined in
93
+ # ``._bible_builders`` alongside ``_build_event``).
45
94
  EVENT_ITEM_KEYS = frozenset({"name", "participants", "begin", "end", "date", *RELATION_KEYS})
46
95
  TIMELINE_TOP_KEYS = frozenset({"events"})
47
96
  RELATIONSHIPS_TOP_KEYS = frozenset({"relationships"})
48
97
 
49
- # A directory builder maps ``(frontmatter, relpath) → entity``; a collection
50
- # builder maps an ``_ItemContext`` (name, resolved participants, the raw item, and
51
- # the collection's own name→URI index) → entity. Typed so ``mypy --strict`` checks
52
- # every call site (rather than the previous ``Any`` escape hatch).
53
- _Builder = Callable[[dict[str, Any], str], GolemEntity]
54
- _ItemBuilder = Callable[["_ItemContext"], GolemEntity]
55
-
56
-
57
- @dataclass(frozen=True)
58
- class MappedEntity:
59
- """One constructed entity paired with the source needed for provenance (R6)."""
60
-
61
- entity: GolemEntity
62
- relpath: str
63
- key_lines: dict[str, int]
64
-
65
-
66
- @dataclass
67
- class MapResult:
68
- """The outcome of mapping a project's bible to GOLEM entities."""
69
-
70
- mapped: list[MappedEntity] = field(default_factory=list)
71
- files_processed: int = 0
72
- skipped: list[SkippedFile] = field(default_factory=list)
73
- unknown_keys: list[UnknownKey] = field(default_factory=list)
74
- unresolved_participants: list[UnresolvedParticipant] = field(default_factory=list)
75
- # ``make_slug(name) → URI`` for every character, setting and event — the research
76
- # ``bears_on``/``constrains`` targets (D11), distinct from participant ``slug_index``.
77
- entity_index: dict[str, URIRef] = field(default_factory=dict)
78
-
79
- @property
80
- def entities(self) -> list[GolemEntity]:
81
- return [m.entity for m in self.mapped]
82
-
83
-
84
- class _Collisions:
85
- """Tracks ``(concept, slug) → relpath`` to detect identifier collisions (FR-014)."""
86
-
87
- def __init__(self) -> None:
88
- self._seen: dict[tuple[str, str], str] = {}
89
-
90
- def record(self, concept: str, slug: str, relpath: str) -> None:
91
- prior = self._seen.get((concept, slug))
92
- if prior is not None and prior != relpath:
93
- raise SlugCollisionError(slug, prior, relpath)
94
- self._seen[(concept, slug)] = relpath
95
-
96
-
97
- @dataclass
98
- class _MapContext:
99
- """The mutable state every mapping helper shares (R3).
100
-
101
- Bundling ``project_root``, the accumulating ``result``, the collision
102
- tracker, and the ``slug → URI`` index into one object keeps each helper's
103
- signature small — the four used to be threaded positionally through every
104
- function.
105
- """
106
-
107
- project_root: Path
108
- result: MapResult
109
- collisions: _Collisions
110
- slug_index: dict[str, URIRef]
111
-
112
98
 
113
99
  @dataclass(frozen=True)
114
100
  class _DirSpec:
115
- """Per-concept config for a one-entity-per-file directory (characters/settings)."""
101
+ """Per-concept config for a one-entity-per-file directory (characters/settings/locations)."""
116
102
 
117
103
  directory: Path
118
104
  concept: str
@@ -122,6 +108,9 @@ class _DirSpec:
122
108
  # Whether built entities feed the research ``entity_index`` (D11) — separate from
123
109
  # ``index`` so a setting joins it without changing participant resolution.
124
110
  into_entity_index: bool = False
111
+ # Whether built entities feed the settings-scoped ``settings_index`` a location's
112
+ # ``setting:`` resolves against (iteration 025) — only the settings pass sets it.
113
+ into_settings_index: bool = False
125
114
 
126
115
 
127
116
  @dataclass(frozen=True)
@@ -142,25 +131,14 @@ class _CollectionSpec:
142
131
  into_entity_index: bool = False
143
132
 
144
133
 
145
- @dataclass(frozen=True)
146
- class _ItemContext:
147
- """Everything a collection builder needs for one item (R3)."""
148
-
149
- ctx: _MapContext
150
- item: dict[str, Any]
151
- name: str
152
- participants: tuple[URIRef, ...]
153
- relpath: str
154
- item_index: dict[str, URIRef]
155
-
156
-
157
134
  def map_bible(project_root: Path, bible_dir: Path, uri_base: str) -> MapResult:
158
135
  """Map every recognised bible file under ``bible_dir`` to GOLEM entities.
159
136
 
160
- Characters and settings are one-entity-per-file; ``timeline.md`` /
137
+ Characters, settings and locations are one-entity-per-file; ``timeline.md`` /
161
138
  ``relationships.md`` are single collection files. Characters are constructed
162
139
  first so ``events:`` / ``relationships:`` participants resolve against a
163
- ``slug → URI`` index in a single pass.
140
+ ``slug → URI`` index in a single pass; settings are mapped **before** locations
141
+ so a location's ``setting:`` resolves against the settings-scoped index.
164
142
  """
165
143
  ctx = _MapContext(
166
144
  project_root=project_root,
@@ -189,6 +167,18 @@ def map_bible(project_root: Path, bible_dir: Path, uri_base: str) -> MapResult:
189
167
  allowed_keys=SETTING_KEYS,
190
168
  index=False,
191
169
  into_entity_index=True,
170
+ into_settings_index=True,
171
+ ),
172
+ )
173
+ _map_single_dir(
174
+ ctx,
175
+ _DirSpec(
176
+ directory=bible_dir / "locations",
177
+ concept="NarrativeLocation",
178
+ builder=lambda meta, rp: _build_location(uri_base, ctx, meta, rp),
179
+ allowed_keys=LOCATION_KEYS,
180
+ index=False,
181
+ into_entity_index=True,
192
182
  ),
193
183
  )
194
184
  _map_collection(
@@ -244,13 +234,6 @@ def _relpath(path: Path, project_root: Path) -> str:
244
234
  return path.relative_to(project_root).as_posix()
245
235
 
246
236
 
247
- def _require_name(metadata: dict[str, Any]) -> str:
248
- name = metadata.get("name")
249
- if not isinstance(name, str) or not name.strip():
250
- raise InvalidFrontmatterError("", "missing or empty `name`")
251
- return name
252
-
253
-
254
237
  def _record_unknown_keys(
255
238
  ctx: _MapContext, metadata: dict[str, Any], allowed: frozenset[str], relpath: str
256
239
  ) -> None:
@@ -259,38 +242,6 @@ def _record_unknown_keys(
259
242
  ctx.result.unknown_keys.append(UnknownKey(path=relpath, key=key))
260
243
 
261
244
 
262
- def _build_character(uri_base: str, metadata: dict[str, Any]) -> Character:
263
- name = _require_name(metadata)
264
- born = _coerce_year(metadata.get("born"), "born")
265
- died = _coerce_year(metadata.get("died"), "died")
266
- features = _coerce_str_list(metadata.get("features"), "features")
267
- roles = _coerce_str_list(metadata.get("narrative_roles"), "narrative_roles")
268
- return Character(
269
- uri_base=uri_base,
270
- name=name,
271
- born=born,
272
- died=died,
273
- features=features,
274
- narrative_roles=roles,
275
- )
276
-
277
-
278
- def _coerce_year(value: Any, field_name: str) -> int | None:
279
- if value is None:
280
- return None
281
- if isinstance(value, bool) or not isinstance(value, int):
282
- raise InvalidFrontmatterError("", f"`{field_name}` must be an integer year")
283
- return value
284
-
285
-
286
- def _coerce_str_list(value: Any, field_name: str) -> tuple[str, ...]:
287
- if value is None:
288
- return ()
289
- if not isinstance(value, list) or not all(isinstance(item, str) for item in value):
290
- raise InvalidFrontmatterError("", f"`{field_name}` must be a list of strings")
291
- return tuple(value)
292
-
293
-
294
245
  def _map_single_dir(ctx: _MapContext, spec: _DirSpec) -> None:
295
246
  if not spec.directory.is_dir():
296
247
  return
@@ -317,6 +268,8 @@ def _map_single_dir(ctx: _MapContext, spec: _DirSpec) -> None:
317
268
  ctx.slug_index[_slug_of(entity)] = entity.uri
318
269
  if spec.into_entity_index:
319
270
  ctx.result.entity_index[_slug_of(entity)] = entity.uri
271
+ if spec.into_settings_index:
272
+ ctx.settings_index[_slug_of(entity)] = entity.uri
320
273
  ctx.result.mapped.append(
321
274
  MappedEntity(entity=entity, relpath=relpath, key_lines=frontmatter.key_lines)
322
275
  )
@@ -409,75 +362,6 @@ def _map_collection_item(
409
362
  )
410
363
 
411
364
 
412
- def _build_event(uri_base: str, ic: _ItemContext) -> NarrativeEvent:
413
- """Construct a ``NarrativeEvent`` from a timeline item: interval + relations."""
414
- begin, end = _resolve_interval(ic)
415
- relations = {
416
- key: _resolve_refs(ic.ctx, ic.item.get(key), ic.item_index, ic.name, ic.relpath)
417
- for key in RELATION_KEYS
418
- }
419
- return NarrativeEvent(
420
- uri_base=uri_base,
421
- name=ic.name,
422
- participants=ic.participants,
423
- begin=begin,
424
- end=end,
425
- **relations,
426
- )
427
-
428
-
429
- def _resolve_interval(ic: _ItemContext) -> tuple[int | None, int | None]:
430
- """Coerce ``begin`` / ``end`` / ``date`` to int years, enforcing exclusivity.
431
-
432
- ``date`` is a single-year shorthand (``begin == end``). Supplying ``date``
433
- alongside ``begin``/``end`` is a soft warning (``date`` ignored), like an
434
- unknown key — never an abort.
435
- """
436
- begin = _coerce_year(ic.item.get("begin"), "begin")
437
- end = _coerce_year(ic.item.get("end"), "end")
438
- date = _coerce_year(ic.item.get("date"), "date")
439
- if date is not None:
440
- if begin is not None or end is not None:
441
- # Mutually exclusive: keep begin/end, drop date, flag it softly.
442
- ic.ctx.result.unknown_keys.append(UnknownKey(path=ic.relpath, key="date"))
443
- else:
444
- return date, date
445
- return begin, end
446
-
447
-
448
- def _resolve_refs(
449
- ctx: _MapContext,
450
- raw: Any,
451
- index: dict[str, URIRef],
452
- entity_name: str,
453
- relpath: str,
454
- ) -> tuple[URIRef, ...]:
455
- """Resolve a list of names against ``index`` (characters or sibling events).
456
-
457
- A non-list value, or a name absent from the index, is surfaced as an
458
- ``UnresolvedParticipant`` soft warning (no abort); the owning entity is built.
459
- """
460
- if raw is None:
461
- return ()
462
- if not isinstance(raw, list):
463
- ctx.result.unresolved_participants.append(
464
- UnresolvedParticipant(path=relpath, entity=entity_name, name=str(raw))
465
- )
466
- return ()
467
- resolved: list[URIRef] = []
468
- for ref in raw:
469
- if not isinstance(ref, str):
470
- continue
471
- uri = index.get(make_slug(ref))
472
- if uri is None:
473
- ctx.result.unresolved_participants.append(
474
- UnresolvedParticipant(path=relpath, entity=entity_name, name=ref)
475
- )
476
- continue
477
- resolved.append(uri)
478
- return tuple(resolved)
479
-
480
-
481
365
  def _safe_parse(ctx: _MapContext, path: Path, relpath: str) -> Frontmatter | None:
482
366
  try:
483
367
  text = path.read_text(encoding="utf-8")
@@ -39,10 +39,11 @@ constitución y el brief; trabajas sobre el proyecto inicializado.
39
39
  `bible/settings/<slug>.md`: frontmatter con **solo** la clave `name` (cadena
40
40
  obligatoria) y secciones en prosa *Cultura*, *Sistema / era* y *Geografía
41
41
  amplia* — es el universo narrativo amplio (región, era, cultura), no un lugar
42
- concreto. Por cada lugar concreto, crea `bible/locations/<slug>.md`: **no se
43
- indexa en v0**, así que va sin frontmatter ingerido; ánclalo en los cinco
44
- sentidos con secciones *Qué se ve / oye / huele / toca* y *Atmósfera
45
- dominante*.
42
+ concreto. Por cada lugar concreto, crea `bible/locations/<slug>.md`: frontmatter
43
+ con `name:` (cadena obligatoria; el *slug* deriva de él) y `setting:` opcional
44
+ el `name` de un `bible/settings/` hermano, que enlaza la localización a su
45
+ universo vía `dlp:generic-location`. Ánclalo además en los cinco sentidos con
46
+ secciones en prosa *Qué se ve / oye / huele / toca* y *Atmósfera dominante*.
46
47
  5. Puebla los contenedores indexados respetando su contrato de clave única:
47
48
  `bible/timeline.md` (clave `events:`, ver
48
49
  `references/golem-events-timeline.md`) y `bible/relationships.md` (clave