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.
- {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/PKG-INFO +1 -1
- {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/__init__.py +1 -1
- {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/golem/deferrals.py +2 -6
- bookwright_cli-0.3.2/src/bookwright/io/_bible_builders.py +268 -0
- {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/io/bible.py +81 -197
- {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/resources/commands/bookwright-bible.md +5 -4
- {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/.gitignore +0 -0
- {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/LICENSE +0 -0
- {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/NOTICE +0 -0
- {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/README.md +0 -0
- {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/pyproject.toml +0 -0
- {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/__main__.py +0 -0
- {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/cli.py +0 -0
- {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/commands/__init__.py +0 -0
- {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/commands/_envelope.py +0 -0
- {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/commands/_graph.py +0 -0
- {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/commands/_project.py +0 -0
- {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/commands/check.py +0 -0
- {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/commands/focus/__init__.py +0 -0
- {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/commands/focus/clear.py +0 -0
- {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/commands/focus/errors.py +0 -0
- {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/commands/focus/set_.py +0 -0
- {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/commands/focus/show.py +0 -0
- {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/commands/graph/__init__.py +0 -0
- {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/commands/graph/build.py +0 -0
- {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/commands/graph/query.py +0 -0
- {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/commands/init/__init__.py +0 -0
- {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/commands/init/conflict.py +0 -0
- {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/commands/init/envelope.py +0 -0
- {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/commands/init/git.py +0 -0
- {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/commands/init/main.py +0 -0
- {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/commands/init/resolve.py +0 -0
- {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/commands/init/scaffold.py +0 -0
- {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/commands/init/validate.py +0 -0
- {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/commands/integration/__init__.py +0 -0
- {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/commands/integration/use.py +0 -0
- {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/commands/status.py +0 -0
- {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/commands/validate.py +0 -0
- {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/commands/version.py +0 -0
- {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/core/__init__.py +0 -0
- {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/core/_blocks.py +0 -0
- {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/core/_build.py +0 -0
- {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/core/_focus_block.py +0 -0
- {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/core/_research_block.py +0 -0
- {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/core/_translate.py +0 -0
- {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/core/errors.py +0 -0
- {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/core/iso639_1.py +0 -0
- {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/core/manifest.py +0 -0
- {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/errors.py +0 -0
- {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/golem/__init__.py +0 -0
- {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/golem/base.py +0 -0
- {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/golem/errors.py +0 -0
- {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/golem/modules/__init__.py +0 -0
- {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/golem/modules/character.py +0 -0
- {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/golem/modules/event.py +0 -0
- {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/golem/modules/feature.py +0 -0
- {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/golem/modules/inference.py +0 -0
- {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/golem/modules/narrative.py +0 -0
- {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/golem/modules/provenance.py +0 -0
- {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/golem/modules/relationship.py +0 -0
- {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/golem/modules/setting.py +0 -0
- {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/golem/namespaces.py +0 -0
- {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/golem/serialize.py +0 -0
- {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/golem/slug.py +0 -0
- {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/indexers/__init__.py +0 -0
- {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/indexers/base.py +0 -0
- {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/indexers/errors.py +0 -0
- {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/indexers/rdflib_indexer.py +0 -0
- {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/integrations/__init__.py +0 -0
- {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/integrations/base.py +0 -0
- {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/integrations/claude/__init__.py +0 -0
- {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/integrations/constants.py +0 -0
- {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/integrations/descriptions.py +0 -0
- {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/integrations/errors.py +0 -0
- {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/integrations/generic/__init__.py +0 -0
- {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/integrations/lint.py +0 -0
- {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/integrations/materialize.py +0 -0
- {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/integrations/options.py +0 -0
- {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/io/__init__.py +0 -0
- {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/io/_research_identity.py +0 -0
- {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/io/errors.py +0 -0
- {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/io/frontmatter.py +0 -0
- {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/io/fs.py +0 -0
- {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/io/manuscript.py +0 -0
- {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/io/project.py +0 -0
- {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/io/report.py +0 -0
- {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/io/research.py +0 -0
- {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/resources/__init__.py +0 -0
- {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/resources/commands/bookwright-analyze.md +0 -0
- {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/resources/commands/bookwright-checklist.md +0 -0
- {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/resources/commands/bookwright-clarify.md +0 -0
- {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/resources/commands/bookwright-constitution.md +0 -0
- {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/resources/commands/bookwright-continuity.md +0 -0
- {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/resources/commands/bookwright-draft.md +0 -0
- {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/resources/commands/bookwright-outline.md +0 -0
- {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/resources/commands/bookwright-research.md +0 -0
- {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/resources/commands/bookwright-scenes.md +0 -0
- {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/resources/commands/bookwright-synopsis.md +0 -0
- {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/resources/commands/bookwright-verify.md +0 -0
- {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/resources/commands/references/golem-character.md +0 -0
- {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/resources/commands/references/golem-events-timeline.md +0 -0
- {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/resources/commands/references/golem-relationships.md +0 -0
- {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/resources/commands/references/greimas-actants.md +0 -0
- {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/resources/commands/references/pending-protocol.md +0 -0
- {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/resources/commands/references/propp-functions.md +0 -0
- {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/resources/commands/references/research-format.md +0 -0
- {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/resources/project/.bookwright/cache/.gitkeep +0 -0
- {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/resources/project/.bookwright/schema/.gitkeep +0 -0
- {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/resources/project/.bookwright/templates/.gitkeep +0 -0
- {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/resources/project/.gitignore +0 -0
- {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/resources/project/README.md.j2 +0 -0
- {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/resources/project/__init__.py +0 -0
- {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/resources/project/bible/characters/.gitkeep +0 -0
- {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/resources/project/bible/constitution.md.j2 +0 -0
- {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/resources/project/bible/glossary.md +0 -0
- {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/resources/project/bible/locations/.gitkeep +0 -0
- {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/resources/project/bible/pov-structure.md +0 -0
- {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/resources/project/bible/relationships.md +0 -0
- {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/resources/project/bible/research/_index.md +0 -0
- {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/resources/project/bible/research/sources.md +0 -0
- {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/resources/project/bible/settings/.gitkeep +0 -0
- {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/resources/project/bible/subplots.md +0 -0
- {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/resources/project/bible/themes.md +0 -0
- {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/resources/project/bible/timeline.md +0 -0
- {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/resources/project/manuscript/.gitkeep +0 -0
- {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/resources/project/outline/arcs.md +0 -0
- {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/resources/project/outline/scenes.md +0 -0
- {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/resources/project/outline/structure.md +0 -0
- {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/resources/project/outline/synopsis.md +0 -0
- {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/resources/schemas/__init__.py +0 -0
- {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/resources/schemas/golem-1.1/VERSION +0 -0
- {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/resources/schemas/golem-1.1/golem.ttl +0 -0
- {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/resources/schemas/golem-1.1/version.json +0 -0
- {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/resources/templates/__init__.py +0 -0
- {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/resources/templates/bible/character.md.tmpl +0 -0
- {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/resources/templates/bible/location.md.tmpl +0 -0
- {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/resources/templates/bible/research/_index.md.tmpl +0 -0
- {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/resources/templates/bible/research/sources.md.tmpl +0 -0
- {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/resources/templates/bible/research/tema.md.tmpl +0 -0
- {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/resources/templates/bible/setting.md.tmpl +0 -0
- {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/resources/templates/manifest.template.toml +0 -0
- {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/resources/templates/manuscript/chapter.md.tmpl +0 -0
- {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/resources/templates/scenes/scene.md.tmpl +0 -0
- {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/resources/vocabularies/__init__.py +0 -0
- {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/resources/vocabularies/greimas.ttl +0 -0
- {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/resources/vocabularies/propp.ttl +0 -0
- {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/resources/vocabularies/sources.ttl +0 -0
- {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/status/__init__.py +0 -0
- {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/status/model.py +0 -0
- {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/status/queries.py +0 -0
- {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/status/rules.py +0 -0
- {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/validation/__init__.py +0 -0
- {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/validation/anchor_queries.py +0 -0
- {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/validation/base.py +0 -0
- {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/validation/queries.py +0 -0
- {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/validation/registry.py +0 -0
- {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/validation/report.py +0 -0
- {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/validation/runner.py +0 -0
- {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/validation/validators/__init__.py +0 -0
- {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/validation/validators/character_presence.py +0 -0
- {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/validation/validators/factual_anchor.py +0 -0
- {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/validation/validators/focalization.py +0 -0
- {bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/validation/validators/setting_continuity.py +0 -0
- {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.
|
|
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,6 +1,6 @@
|
|
|
1
1
|
"""The deferral registry: which GOLEM concepts are modelled but not yet fed.
|
|
2
2
|
|
|
3
|
-
|
|
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
|
|
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
|
|
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 .
|
|
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
|
|
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
|
|
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")
|
{bookwright_cli-0.3.1 → bookwright_cli-0.3.2}/src/bookwright/resources/commands/bookwright-bible.md
RENAMED
|
@@ -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`:
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|