bookwright-cli 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- bookwright/__init__.py +3 -0
- bookwright/__main__.py +6 -0
- bookwright/cli.py +19 -0
- bookwright/commands/__init__.py +0 -0
- bookwright/commands/_envelope.py +36 -0
- bookwright/commands/check.py +75 -0
- bookwright/commands/graph/__init__.py +23 -0
- bookwright/commands/graph/build.py +157 -0
- bookwright/commands/graph/envelope.py +26 -0
- bookwright/commands/graph/query.py +98 -0
- bookwright/commands/init/__init__.py +5 -0
- bookwright/commands/init/conflict.py +107 -0
- bookwright/commands/init/envelope.py +322 -0
- bookwright/commands/init/git.py +96 -0
- bookwright/commands/init/main.py +263 -0
- bookwright/commands/init/resolve.py +193 -0
- bookwright/commands/init/scaffold.py +242 -0
- bookwright/commands/init/validate.py +172 -0
- bookwright/commands/integration/__init__.py +22 -0
- bookwright/commands/integration/use.py +120 -0
- bookwright/commands/validate.py +160 -0
- bookwright/commands/version.py +35 -0
- bookwright/core/__init__.py +35 -0
- bookwright/core/_blocks.py +239 -0
- bookwright/core/_build.py +154 -0
- bookwright/core/_research_block.py +56 -0
- bookwright/core/_translate.py +90 -0
- bookwright/core/errors.py +127 -0
- bookwright/core/iso639_1.py +200 -0
- bookwright/core/manifest.py +343 -0
- bookwright/errors.py +47 -0
- bookwright/golem/__init__.py +71 -0
- bookwright/golem/base.py +200 -0
- bookwright/golem/errors.py +29 -0
- bookwright/golem/modules/__init__.py +1 -0
- bookwright/golem/modules/character.py +109 -0
- bookwright/golem/modules/event.py +91 -0
- bookwright/golem/modules/feature.py +161 -0
- bookwright/golem/modules/inference.py +41 -0
- bookwright/golem/modules/narrative.py +55 -0
- bookwright/golem/modules/provenance.py +197 -0
- bookwright/golem/modules/relationship.py +38 -0
- bookwright/golem/modules/setting.py +30 -0
- bookwright/golem/namespaces.py +332 -0
- bookwright/golem/serialize.py +25 -0
- bookwright/golem/slug.py +22 -0
- bookwright/indexers/__init__.py +47 -0
- bookwright/indexers/base.py +55 -0
- bookwright/indexers/errors.py +80 -0
- bookwright/indexers/rdflib_indexer.py +89 -0
- bookwright/integrations/__init__.py +155 -0
- bookwright/integrations/base.py +117 -0
- bookwright/integrations/claude/__init__.py +29 -0
- bookwright/integrations/constants.py +38 -0
- bookwright/integrations/descriptions.py +48 -0
- bookwright/integrations/errors.py +170 -0
- bookwright/integrations/generic/__init__.py +56 -0
- bookwright/integrations/lint.py +160 -0
- bookwright/integrations/materialize.py +202 -0
- bookwright/integrations/options.py +203 -0
- bookwright/io/__init__.py +1 -0
- bookwright/io/bible.py +500 -0
- bookwright/io/errors.py +98 -0
- bookwright/io/frontmatter.py +61 -0
- bookwright/io/fs.py +226 -0
- bookwright/io/manuscript.py +15 -0
- bookwright/io/project.py +21 -0
- bookwright/io/report.py +107 -0
- bookwright/io/research.py +427 -0
- bookwright/resources/__init__.py +1 -0
- bookwright/resources/commands/bookwright-analyze.md +66 -0
- bookwright/resources/commands/bookwright-bible.md +96 -0
- bookwright/resources/commands/bookwright-checklist.md +67 -0
- bookwright/resources/commands/bookwright-clarify.md +65 -0
- bookwright/resources/commands/bookwright-constitution.md +79 -0
- bookwright/resources/commands/bookwright-continuity.md +70 -0
- bookwright/resources/commands/bookwright-draft.md +74 -0
- bookwright/resources/commands/bookwright-outline.md +71 -0
- bookwright/resources/commands/bookwright-research.md +107 -0
- bookwright/resources/commands/bookwright-scenes.md +66 -0
- bookwright/resources/commands/bookwright-synopsis.md +67 -0
- bookwright/resources/commands/bookwright-verify.md +136 -0
- bookwright/resources/commands/references/golem-character.md +65 -0
- bookwright/resources/commands/references/golem-events-timeline.md +56 -0
- bookwright/resources/commands/references/golem-relationships.md +53 -0
- bookwright/resources/commands/references/greimas-actants.md +57 -0
- bookwright/resources/commands/references/pending-protocol.md +72 -0
- bookwright/resources/commands/references/propp-functions.md +54 -0
- bookwright/resources/commands/references/research-format.md +136 -0
- bookwright/resources/project/.bookwright/cache/.gitkeep +0 -0
- bookwright/resources/project/.bookwright/schema/.gitkeep +0 -0
- bookwright/resources/project/.bookwright/templates/.gitkeep +0 -0
- bookwright/resources/project/.gitignore +23 -0
- bookwright/resources/project/README.md.j2 +40 -0
- bookwright/resources/project/__init__.py +6 -0
- bookwright/resources/project/bible/characters/.gitkeep +0 -0
- bookwright/resources/project/bible/constitution.md.j2 +74 -0
- bookwright/resources/project/bible/glossary.md +36 -0
- bookwright/resources/project/bible/locations/.gitkeep +0 -0
- bookwright/resources/project/bible/pov-structure.md +43 -0
- bookwright/resources/project/bible/relationships.md +36 -0
- bookwright/resources/project/bible/research/_index.md +28 -0
- bookwright/resources/project/bible/research/sources.md +23 -0
- bookwright/resources/project/bible/settings/.gitkeep +0 -0
- bookwright/resources/project/bible/subplots.md +35 -0
- bookwright/resources/project/bible/themes.md +36 -0
- bookwright/resources/project/bible/timeline.md +38 -0
- bookwright/resources/project/manuscript/.gitkeep +0 -0
- bookwright/resources/project/outline/arcs.md +34 -0
- bookwright/resources/project/outline/scenes.md +31 -0
- bookwright/resources/project/outline/structure.md +35 -0
- bookwright/resources/project/outline/synopsis.md +25 -0
- bookwright/resources/schemas/__init__.py +19 -0
- bookwright/resources/schemas/golem-1.1/VERSION +1 -0
- bookwright/resources/schemas/golem-1.1/golem.ttl +1947 -0
- bookwright/resources/schemas/golem-1.1/version.json +8 -0
- bookwright/resources/templates/__init__.py +1 -0
- bookwright/resources/templates/bible/character.md.tmpl +63 -0
- bookwright/resources/templates/bible/location.md.tmpl +37 -0
- bookwright/resources/templates/bible/research/_index.md.tmpl +25 -0
- bookwright/resources/templates/bible/research/sources.md.tmpl +21 -0
- bookwright/resources/templates/bible/research/tema.md.tmpl +37 -0
- bookwright/resources/templates/bible/setting.md.tmpl +38 -0
- bookwright/resources/templates/manifest.template.toml +79 -0
- bookwright/resources/templates/manuscript/chapter.md.tmpl +36 -0
- bookwright/resources/templates/scenes/scene.md.tmpl +37 -0
- bookwright/resources/vocabularies/__init__.py +6 -0
- bookwright/resources/vocabularies/greimas.ttl +4 -0
- bookwright/resources/vocabularies/propp.ttl +4 -0
- bookwright/resources/vocabularies/sources.ttl +82 -0
- bookwright/validation/__init__.py +33 -0
- bookwright/validation/anchor_queries.py +223 -0
- bookwright/validation/base.py +233 -0
- bookwright/validation/queries.py +197 -0
- bookwright/validation/registry.py +185 -0
- bookwright/validation/report.py +106 -0
- bookwright/validation/runner.py +65 -0
- bookwright/validation/validators/__init__.py +9 -0
- bookwright/validation/validators/character_presence.py +202 -0
- bookwright/validation/validators/factual_anchor.py +291 -0
- bookwright/validation/validators/focalization.py +152 -0
- bookwright/validation/validators/setting_continuity.py +100 -0
- bookwright/validation/validators/temporal.py +277 -0
- bookwright_cli-0.2.0.dist-info/METADATA +218 -0
- bookwright_cli-0.2.0.dist-info/RECORD +149 -0
- bookwright_cli-0.2.0.dist-info/WHEEL +4 -0
- bookwright_cli-0.2.0.dist-info/entry_points.txt +2 -0
- bookwright_cli-0.2.0.dist-info/licenses/LICENSE +202 -0
- bookwright_cli-0.2.0.dist-info/licenses/NOTICE +14 -0
|
@@ -0,0 +1,427 @@
|
|
|
1
|
+
"""Discover ``bible/research/`` files and map their front-matter to provenance entities.
|
|
2
|
+
|
|
3
|
+
The research analogue of :mod:`bookwright.io.bible` (design § 20.5/§ 20.7). It reads
|
|
4
|
+
``sources.md`` (the Source registry), every ``<topic>.md`` (findings + anchors) and
|
|
5
|
+
``_index.md`` (global open questions), and turns their YAML front-matter into the
|
|
6
|
+
frozen :class:`~bookwright.golem.modules.provenance.Source` /
|
|
7
|
+
:class:`~bookwright.golem.modules.provenance.Finding` /
|
|
8
|
+
:class:`~bookwright.golem.modules.provenance.Anchor` entities ``graph build`` then
|
|
9
|
+
serializes.
|
|
10
|
+
|
|
11
|
+
Fault model (deliberately **stricter** than the bible mapper — research D7): a
|
|
12
|
+
vocabulary violation, a missing required Source facet, a non-open finding without a
|
|
13
|
+
``claim``/``sources``, an ``anchors[].promotes`` naming an unknown finding, a
|
|
14
|
+
translation-rule violation, or malformed YAML raises
|
|
15
|
+
:class:`~bookwright.io.errors.ResearchError` and aborts the build with no graph. An
|
|
16
|
+
unresolved ``bears_on``/``constrains`` *narrative* target is the one **soft** miss
|
|
17
|
+
(D12): the link triple is omitted and a :class:`ResearchWarning` is recorded, the
|
|
18
|
+
build still succeeding — existence/kind checking is the iteration-15 validator's job.
|
|
19
|
+
An **absent or empty** ``bible/research/`` yields zero entities and never raises
|
|
20
|
+
(FR-015, SC-005).
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
from collections.abc import Mapping
|
|
26
|
+
from dataclasses import dataclass, field
|
|
27
|
+
from pathlib import Path
|
|
28
|
+
from typing import Any
|
|
29
|
+
|
|
30
|
+
import yaml
|
|
31
|
+
from pydantic import ValidationError
|
|
32
|
+
from rdflib.term import URIRef
|
|
33
|
+
|
|
34
|
+
from bookwright.golem import Anchor, Finding, Source
|
|
35
|
+
from bookwright.golem.base import GolemEntity
|
|
36
|
+
from bookwright.golem.errors import EmptySlugError
|
|
37
|
+
from bookwright.golem.namespaces import RELIABILITY_IRI, SOURCE_TYPE_IRI
|
|
38
|
+
from bookwright.golem.slug import make_slug
|
|
39
|
+
|
|
40
|
+
from .errors import ResearchError
|
|
41
|
+
from .frontmatter import parse_frontmatter
|
|
42
|
+
|
|
43
|
+
SOURCES_FILE = "sources.md"
|
|
44
|
+
INDEX_FILE = "_index.md"
|
|
45
|
+
|
|
46
|
+
# Every facet a Source declares (research-format.md). ``translation`` is governed by
|
|
47
|
+
# the language rule (§ D6), not listed as plainly required here.
|
|
48
|
+
_SOURCE_FACETS = (
|
|
49
|
+
"name",
|
|
50
|
+
"reference",
|
|
51
|
+
"author",
|
|
52
|
+
"original_language",
|
|
53
|
+
"type",
|
|
54
|
+
"reliability",
|
|
55
|
+
"reliability_justification",
|
|
56
|
+
"access_date",
|
|
57
|
+
"original_quote",
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@dataclass(frozen=True)
|
|
62
|
+
class ResearchWarning:
|
|
63
|
+
"""A soft, unresolved ``bears_on``/``constrains`` narrative target (research D12).
|
|
64
|
+
|
|
65
|
+
The link triple was omitted; the build still succeeds. ``field`` is
|
|
66
|
+
``"bears_on"`` or ``"constrains"``; ``name`` is the target that did not resolve
|
|
67
|
+
in the bible ``entity_index``.
|
|
68
|
+
"""
|
|
69
|
+
|
|
70
|
+
relpath: str
|
|
71
|
+
field: str
|
|
72
|
+
name: str
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@dataclass(frozen=True)
|
|
76
|
+
class ResearchResult:
|
|
77
|
+
"""The outcome of mapping a project's ``bible/research/`` to provenance entities."""
|
|
78
|
+
|
|
79
|
+
sources: tuple[Source, ...] = ()
|
|
80
|
+
findings: tuple[Finding, ...] = ()
|
|
81
|
+
anchors: tuple[Anchor, ...] = ()
|
|
82
|
+
files_processed: int = 0
|
|
83
|
+
warnings: tuple[ResearchWarning, ...] = ()
|
|
84
|
+
|
|
85
|
+
@property
|
|
86
|
+
def entities(self) -> tuple[GolemEntity, ...]:
|
|
87
|
+
return (*self.sources, *self.findings, *self.anchors)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
@dataclass
|
|
91
|
+
class _Accumulator:
|
|
92
|
+
"""Mutable state threaded through the mapping passes."""
|
|
93
|
+
|
|
94
|
+
project_root: Path
|
|
95
|
+
uri_base: str
|
|
96
|
+
book_language: str
|
|
97
|
+
bible_index: Mapping[str, URIRef]
|
|
98
|
+
timeline_uri: URIRef
|
|
99
|
+
sources: list[Source] = field(default_factory=list)
|
|
100
|
+
findings: list[Finding] = field(default_factory=list)
|
|
101
|
+
anchors: list[Anchor] = field(default_factory=list)
|
|
102
|
+
warnings: list[ResearchWarning] = field(default_factory=list)
|
|
103
|
+
files_processed: int = 0
|
|
104
|
+
source_index: dict[str, URIRef] = field(default_factory=dict)
|
|
105
|
+
|
|
106
|
+
def result(self) -> ResearchResult:
|
|
107
|
+
return ResearchResult(
|
|
108
|
+
sources=tuple(self.sources),
|
|
109
|
+
findings=tuple(self.findings),
|
|
110
|
+
anchors=tuple(self.anchors),
|
|
111
|
+
files_processed=self.files_processed,
|
|
112
|
+
warnings=tuple(self.warnings),
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def map_research( # noqa: PLR0913 — the six parameters are the fixed contract surface (research-io.md)
|
|
117
|
+
project_root: Path,
|
|
118
|
+
research_dir: Path,
|
|
119
|
+
uri_base: str,
|
|
120
|
+
book_language: str,
|
|
121
|
+
bible_index: Mapping[str, URIRef],
|
|
122
|
+
timeline_uri: URIRef,
|
|
123
|
+
) -> ResearchResult:
|
|
124
|
+
"""Map ``research_dir`` to provenance entities (see module docstring).
|
|
125
|
+
|
|
126
|
+
Files are processed in a deterministic order: ``sources.md`` first (so finding
|
|
127
|
+
source references resolve in one pass), then the topic files in sorted order,
|
|
128
|
+
then ``_index.md`` (global open questions).
|
|
129
|
+
"""
|
|
130
|
+
acc = _Accumulator(
|
|
131
|
+
project_root=project_root,
|
|
132
|
+
uri_base=uri_base,
|
|
133
|
+
book_language=book_language,
|
|
134
|
+
bible_index=bible_index,
|
|
135
|
+
timeline_uri=timeline_uri,
|
|
136
|
+
)
|
|
137
|
+
if not research_dir.is_dir():
|
|
138
|
+
return acc.result()
|
|
139
|
+
|
|
140
|
+
sources_path = research_dir / SOURCES_FILE
|
|
141
|
+
if sources_path.is_file():
|
|
142
|
+
_map_sources(acc, sources_path)
|
|
143
|
+
|
|
144
|
+
for path in sorted(research_dir.glob("*.md")):
|
|
145
|
+
if path.name in {SOURCES_FILE, INDEX_FILE}:
|
|
146
|
+
continue
|
|
147
|
+
_map_topic(acc, path)
|
|
148
|
+
|
|
149
|
+
index_path = research_dir / INDEX_FILE
|
|
150
|
+
if index_path.is_file():
|
|
151
|
+
_map_index(acc, index_path)
|
|
152
|
+
|
|
153
|
+
return acc.result()
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
# --- sources.md -------------------------------------------------------------
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def _map_sources(acc: _Accumulator, path: Path) -> None:
|
|
160
|
+
relpath = _relpath(acc, path)
|
|
161
|
+
acc.files_processed += 1
|
|
162
|
+
metadata = _load(acc, path, relpath)
|
|
163
|
+
raw_sources = metadata.get("sources", [])
|
|
164
|
+
if not isinstance(raw_sources, list):
|
|
165
|
+
raise ResearchError(relpath, f"`sources` must be a list in {relpath}")
|
|
166
|
+
for raw in raw_sources:
|
|
167
|
+
if not isinstance(raw, dict):
|
|
168
|
+
raise ResearchError(relpath, f"each `sources` item must be a mapping in {relpath}")
|
|
169
|
+
source = _build_source(acc, raw, relpath)
|
|
170
|
+
slug = make_slug(source.name)
|
|
171
|
+
if slug in acc.source_index:
|
|
172
|
+
raise ResearchError(
|
|
173
|
+
relpath,
|
|
174
|
+
f"duplicate source name {source.name!r} (slug {slug!r}) in {relpath}",
|
|
175
|
+
source.name,
|
|
176
|
+
)
|
|
177
|
+
acc.source_index[slug] = source.uri
|
|
178
|
+
acc.sources.append(source)
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def _build_source(acc: _Accumulator, raw: dict[str, Any], relpath: str) -> Source:
|
|
182
|
+
"""Validate one source mapping and build the frozen :class:`Source` (D4/D6)."""
|
|
183
|
+
for facet in _SOURCE_FACETS:
|
|
184
|
+
if facet not in raw:
|
|
185
|
+
raise ResearchError(
|
|
186
|
+
relpath, f"source is missing required `{facet}` in {relpath}", facet
|
|
187
|
+
)
|
|
188
|
+
_reject_unknown_vocab(raw, relpath)
|
|
189
|
+
try:
|
|
190
|
+
source = Source(uri_base=acc.uri_base, **raw)
|
|
191
|
+
except ValidationError as exc:
|
|
192
|
+
raise ResearchError(relpath, f"invalid source in {relpath}: {_first_error(exc)}") from exc
|
|
193
|
+
except EmptySlugError as exc:
|
|
194
|
+
raise ResearchError(
|
|
195
|
+
relpath, f"source `name` is empty or unsluggable in {relpath}", exc.name
|
|
196
|
+
) from exc
|
|
197
|
+
return _apply_translation_rule(acc, source, relpath)
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def _reject_unknown_vocab(raw: dict[str, Any], relpath: str) -> None:
|
|
201
|
+
"""Name the offending value for an out-of-vocabulary ``type``/``reliability``."""
|
|
202
|
+
type_value = raw.get("type")
|
|
203
|
+
if type_value not in SOURCE_TYPE_IRI:
|
|
204
|
+
raise ResearchError(
|
|
205
|
+
relpath, f"unknown source type {type_value!r} in {relpath}", str(type_value)
|
|
206
|
+
)
|
|
207
|
+
reliability = raw.get("reliability")
|
|
208
|
+
if reliability not in RELIABILITY_IRI:
|
|
209
|
+
raise ResearchError(
|
|
210
|
+
relpath, f"unknown reliability {reliability!r} in {relpath}", str(reliability)
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def _apply_translation_rule(acc: _Accumulator, source: Source, relpath: str) -> Source:
|
|
215
|
+
"""Enforce the language-driven translation rule (research D6, SC-004).
|
|
216
|
+
|
|
217
|
+
When the source language differs from the book's, ``translation`` is required;
|
|
218
|
+
when they match, any supplied translation is dropped (never emitted).
|
|
219
|
+
"""
|
|
220
|
+
if source.original_language != acc.book_language:
|
|
221
|
+
if source.translation is None or not source.translation.strip():
|
|
222
|
+
raise ResearchError(
|
|
223
|
+
relpath,
|
|
224
|
+
f"source {source.name!r} needs a `translation` (language "
|
|
225
|
+
f"{source.original_language!r} ≠ book {acc.book_language!r}) in {relpath}",
|
|
226
|
+
source.name,
|
|
227
|
+
)
|
|
228
|
+
return source
|
|
229
|
+
if source.translation is not None:
|
|
230
|
+
return source.model_copy(update={"translation": None})
|
|
231
|
+
return source
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
# --- <topic>.md & _index.md -------------------------------------------------
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def _map_topic(acc: _Accumulator, path: Path) -> None:
|
|
238
|
+
relpath = _relpath(acc, path)
|
|
239
|
+
acc.files_processed += 1
|
|
240
|
+
metadata = _load(acc, path, relpath)
|
|
241
|
+
finding_ids = _map_findings(acc, metadata.get("findings"), relpath, open_only=False)
|
|
242
|
+
_map_anchors(acc, metadata.get("anchors"), relpath, finding_ids)
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def _map_index(acc: _Accumulator, path: Path) -> None:
|
|
246
|
+
relpath = _relpath(acc, path)
|
|
247
|
+
acc.files_processed += 1
|
|
248
|
+
metadata = _load(acc, path, relpath)
|
|
249
|
+
_map_findings(acc, metadata.get("open_questions"), relpath, open_only=True)
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def _map_findings(
|
|
253
|
+
acc: _Accumulator, raw_findings: Any, relpath: str, *, open_only: bool
|
|
254
|
+
) -> dict[str, URIRef]:
|
|
255
|
+
"""Build the findings in one file; return its in-file ``id`` → URI map."""
|
|
256
|
+
finding_ids: dict[str, URIRef] = {}
|
|
257
|
+
if raw_findings is None:
|
|
258
|
+
return finding_ids
|
|
259
|
+
if not isinstance(raw_findings, list):
|
|
260
|
+
raise ResearchError(relpath, f"`findings` must be a list in {relpath}")
|
|
261
|
+
for raw in raw_findings:
|
|
262
|
+
if not isinstance(raw, dict):
|
|
263
|
+
raise ResearchError(relpath, f"each finding must be a mapping in {relpath}")
|
|
264
|
+
identifier, finding = _build_finding(acc, raw, relpath, open_only=open_only)
|
|
265
|
+
if identifier in finding_ids:
|
|
266
|
+
raise ResearchError(
|
|
267
|
+
relpath, f"duplicate finding id {identifier!r} in {relpath}", identifier
|
|
268
|
+
)
|
|
269
|
+
finding_ids[identifier] = finding.uri
|
|
270
|
+
acc.findings.append(finding)
|
|
271
|
+
return finding_ids
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def _build_finding(
|
|
275
|
+
acc: _Accumulator, raw: dict[str, Any], relpath: str, *, open_only: bool
|
|
276
|
+
) -> tuple[str, Finding]:
|
|
277
|
+
identifier = raw.get("id")
|
|
278
|
+
if not isinstance(identifier, str) or not identifier.strip():
|
|
279
|
+
raise ResearchError(relpath, f"a finding is missing its `id` in {relpath}")
|
|
280
|
+
is_open = bool(raw.get("open", False)) or open_only
|
|
281
|
+
claim = raw.get("claim")
|
|
282
|
+
sources = _resolve_sources(acc, raw.get("sources"), relpath)
|
|
283
|
+
if not is_open and (not isinstance(claim, str) or not claim.strip() or not sources):
|
|
284
|
+
raise ResearchError(
|
|
285
|
+
relpath,
|
|
286
|
+
f"finding {identifier!r} needs a `claim` and at least one `source` in {relpath}",
|
|
287
|
+
identifier,
|
|
288
|
+
)
|
|
289
|
+
bears_on = _resolve_narrative(acc, raw.get("bears_on"), "bears_on", relpath)
|
|
290
|
+
finding = Finding(
|
|
291
|
+
uri_base=acc.uri_base,
|
|
292
|
+
claim=claim if isinstance(claim, str) and claim.strip() else None,
|
|
293
|
+
asserted_by=str(raw.get("asserted_by", "author")),
|
|
294
|
+
bears_on=bears_on,
|
|
295
|
+
sources=sources,
|
|
296
|
+
open=is_open,
|
|
297
|
+
)
|
|
298
|
+
return identifier, finding
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def _resolve_sources(acc: _Accumulator, raw_sources: Any, relpath: str) -> tuple[URIRef, ...]:
|
|
302
|
+
if raw_sources is None:
|
|
303
|
+
return ()
|
|
304
|
+
if not isinstance(raw_sources, list):
|
|
305
|
+
raise ResearchError(relpath, f"`sources` must be a list of source names in {relpath}")
|
|
306
|
+
resolved: list[URIRef] = []
|
|
307
|
+
for name in raw_sources:
|
|
308
|
+
try:
|
|
309
|
+
slug = make_slug(str(name))
|
|
310
|
+
except EmptySlugError:
|
|
311
|
+
slug = None
|
|
312
|
+
uri = acc.source_index.get(slug) if slug is not None else None
|
|
313
|
+
if uri is None:
|
|
314
|
+
raise ResearchError(relpath, f"unknown source {name!r} in {relpath}", str(name))
|
|
315
|
+
resolved.append(uri)
|
|
316
|
+
return tuple(resolved)
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
def _map_anchors(
|
|
320
|
+
acc: _Accumulator, raw_anchors: Any, relpath: str, finding_ids: Mapping[str, URIRef]
|
|
321
|
+
) -> None:
|
|
322
|
+
if raw_anchors is None:
|
|
323
|
+
return
|
|
324
|
+
if not isinstance(raw_anchors, list):
|
|
325
|
+
raise ResearchError(relpath, f"`anchors` must be a list in {relpath}")
|
|
326
|
+
for raw in raw_anchors:
|
|
327
|
+
if not isinstance(raw, dict):
|
|
328
|
+
raise ResearchError(relpath, f"each anchor must be a mapping in {relpath}")
|
|
329
|
+
acc.anchors.append(_build_anchor(acc, raw, relpath, finding_ids))
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
def _build_anchor(
|
|
333
|
+
acc: _Accumulator, raw: dict[str, Any], relpath: str, finding_ids: Mapping[str, URIRef]
|
|
334
|
+
) -> Anchor:
|
|
335
|
+
promotes_id = raw.get("promotes")
|
|
336
|
+
if not isinstance(promotes_id, str) or promotes_id not in finding_ids:
|
|
337
|
+
raise ResearchError(
|
|
338
|
+
relpath,
|
|
339
|
+
f"anchor `promotes` an unknown finding {promotes_id!r} in {relpath}",
|
|
340
|
+
str(promotes_id),
|
|
341
|
+
)
|
|
342
|
+
if "constrains" not in raw:
|
|
343
|
+
raise ResearchError(relpath, f"anchor is missing required `constrains` in {relpath}")
|
|
344
|
+
constrains = _resolve_constrains(acc, raw.get("constrains"), relpath)
|
|
345
|
+
begin, end = _resolve_span(raw, relpath)
|
|
346
|
+
return Anchor(
|
|
347
|
+
uri_base=acc.uri_base,
|
|
348
|
+
promotes=finding_ids[promotes_id],
|
|
349
|
+
constrains=constrains,
|
|
350
|
+
begin=begin,
|
|
351
|
+
end=end,
|
|
352
|
+
)
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
def _resolve_constrains(acc: _Accumulator, raw: Any, relpath: str) -> URIRef | None:
|
|
356
|
+
if isinstance(raw, str) and raw.strip() == "timeline":
|
|
357
|
+
return acc.timeline_uri
|
|
358
|
+
return _resolve_narrative(acc, raw, "constrains", relpath)
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
def _resolve_narrative(acc: _Accumulator, raw: Any, field_name: str, relpath: str) -> URIRef | None:
|
|
362
|
+
"""Resolve a narrative target via the bible index; a miss is a soft warning (D12)."""
|
|
363
|
+
if raw is None:
|
|
364
|
+
return None
|
|
365
|
+
name = str(raw)
|
|
366
|
+
try:
|
|
367
|
+
slug = make_slug(name)
|
|
368
|
+
except EmptySlugError:
|
|
369
|
+
slug = None
|
|
370
|
+
uri = acc.bible_index.get(slug) if slug is not None else None
|
|
371
|
+
if uri is None:
|
|
372
|
+
acc.warnings.append(ResearchWarning(relpath=relpath, field=field_name, name=name))
|
|
373
|
+
return None
|
|
374
|
+
return uri
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
def _resolve_span(raw: dict[str, Any], relpath: str) -> tuple[int | None, int | None]:
|
|
378
|
+
"""Coerce ``begin``/``end``/``date`` to year ints (``date`` is begin == end)."""
|
|
379
|
+
begin = _coerce_year(raw.get("begin"), "begin", relpath)
|
|
380
|
+
end = _coerce_year(raw.get("end"), "end", relpath)
|
|
381
|
+
date = _coerce_year(raw.get("date"), "date", relpath)
|
|
382
|
+
if date is not None:
|
|
383
|
+
if begin is not None or end is not None:
|
|
384
|
+
raise ResearchError(
|
|
385
|
+
relpath, f"anchor `date` is mutually exclusive with `begin`/`end` in {relpath}"
|
|
386
|
+
)
|
|
387
|
+
return date, date
|
|
388
|
+
return begin, end
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
def _coerce_year(value: Any, key: str, relpath: str) -> int | None:
|
|
392
|
+
if value is None:
|
|
393
|
+
return None
|
|
394
|
+
if isinstance(value, bool) or not isinstance(value, int):
|
|
395
|
+
raise ResearchError(
|
|
396
|
+
relpath, f"anchor `{key}` must be an integer year in {relpath}", str(value)
|
|
397
|
+
)
|
|
398
|
+
return value
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
# --- shared -----------------------------------------------------------------
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
def _relpath(acc: _Accumulator, path: Path) -> str:
|
|
405
|
+
return path.relative_to(acc.project_root).as_posix()
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
def _load(acc: _Accumulator, path: Path, relpath: str) -> dict[str, Any]:
|
|
409
|
+
"""Read and parse a research file's front-matter; malformed YAML is fatal (D7)."""
|
|
410
|
+
try:
|
|
411
|
+
text = path.read_text(encoding="utf-8")
|
|
412
|
+
except (OSError, UnicodeDecodeError) as exc:
|
|
413
|
+
raise ResearchError(relpath, f"cannot read {relpath}: {exc}") from exc
|
|
414
|
+
try:
|
|
415
|
+
return parse_frontmatter(text).metadata
|
|
416
|
+
except yaml.YAMLError as exc:
|
|
417
|
+
raise ResearchError(relpath, f"malformed YAML front-matter in {relpath}: {exc}") from exc
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
def _first_error(exc: ValidationError) -> str:
|
|
421
|
+
"""A compact, value-naming summary of the first pydantic validation error."""
|
|
422
|
+
errors = exc.errors()
|
|
423
|
+
if not errors: # pragma: no cover — a pydantic ValidationError always carries ≥1 error
|
|
424
|
+
return str(exc)
|
|
425
|
+
first = errors[0]
|
|
426
|
+
location = ".".join(str(part) for part in first.get("loc", ()))
|
|
427
|
+
return f"{location}: {first.get('msg', '')}".strip(": ")
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Bundled non-code resources (templates, static data)."""
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: bookwright-analyze
|
|
3
|
+
description: >-
|
|
4
|
+
Revisa la consistencia cruzada PRE-redacción entre constitución, biblia,
|
|
5
|
+
outline y escenas, y reporta contradicciones antes de empezar a escribir. Check
|
|
6
|
+
PRE-draft cross-artifact consistency among constitution, bible, outline and
|
|
7
|
+
scenes, reporting contradictions before any prose is written. Úsalo cuando el
|
|
8
|
+
autor pregunte "¿es coherente mi planificación antes de redactar?" / "is my
|
|
9
|
+
planning consistent before I start drafting?". Es de solo lectura y trabaja en
|
|
10
|
+
fase PRE-draft. NO compara el manuscrito con la biblia (eso es post-draft:
|
|
11
|
+
bookwright-continuity).
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
# /bookwright-analyze
|
|
15
|
+
|
|
16
|
+
## Rol
|
|
17
|
+
|
|
18
|
+
Eres un editor de consistencia. Tu tarea es **detectar contradicciones** entre
|
|
19
|
+
los artefactos de planificación **antes** de que se redacte una sola escena, y
|
|
20
|
+
reportarlas sin tocar nada.
|
|
21
|
+
|
|
22
|
+
## Input
|
|
23
|
+
|
|
24
|
+
`{ARGS}` — foco opcional (p. ej. "céntrate en la cronología"). La base son los
|
|
25
|
+
cuatro artefactos de planificación.
|
|
26
|
+
|
|
27
|
+
## Procedimiento
|
|
28
|
+
|
|
29
|
+
1. Lee `bible/constitution.md`, el conjunto de la biblia (`bible/`),
|
|
30
|
+
`outline/arcs.md`, `outline/structure.md` y `outline/scenes.md`.
|
|
31
|
+
2. Coteja entre artefactos: ¿los arcos respetan las invariantes de la
|
|
32
|
+
constitución? ¿las escenas usan personajes y lugares que existen en la biblia?
|
|
33
|
+
¿la estructura y los arcos convergen? ¿la cronología de eventos es compatible
|
|
34
|
+
con el orden estructural?
|
|
35
|
+
3. Si falta alguno de los cuatro artefactos (proyecto vacío o pre-draft
|
|
36
|
+
incompleto), repórtalo como "prerrequisito ausente", no falles de forma opaca.
|
|
37
|
+
4. Redacta los hallazgos como una lista de **inconsistencias**, cada una con los
|
|
38
|
+
artefactos implicados y una sugerencia de resolución.
|
|
39
|
+
|
|
40
|
+
## Output
|
|
41
|
+
|
|
42
|
+
Un reporte en prosa de inconsistencias cruzadas pre-draft, agrupadas por gravedad.
|
|
43
|
+
**No escribe nada** en el proyecto.
|
|
44
|
+
|
|
45
|
+
## Archivos a leer
|
|
46
|
+
|
|
47
|
+
- `bible/constitution.md`, el conjunto de `bible/`, `outline/arcs.md`,
|
|
48
|
+
`outline/structure.md`, `outline/scenes.md`.
|
|
49
|
+
|
|
50
|
+
## Archivos a escribir
|
|
51
|
+
|
|
52
|
+
- Ninguno. Este comando es de **solo lectura**: no escribe nada en el proyecto;
|
|
53
|
+
solo emite un reporte.
|
|
54
|
+
|
|
55
|
+
## Información faltante
|
|
56
|
+
|
|
57
|
+
Si alguno de los cuatro artefactos no existe todavía, repórtalo como
|
|
58
|
+
"prerrequisito ausente" e indica qué comando lo genera, en vez de analizar sobre
|
|
59
|
+
material inventado. No marques `[PENDING: …]` (no escribe archivos).
|
|
60
|
+
|
|
61
|
+
## Qué NO hacer
|
|
62
|
+
|
|
63
|
+
- No escribas ni modifiques ningún archivo: solo reporta.
|
|
64
|
+
- No analices el manuscrito frente a la biblia: eso es post-draft,
|
|
65
|
+
`bookwright-continuity`.
|
|
66
|
+
- No resuelvas tú las contradicciones reescribiendo artefactos: solo señálalas.
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: bookwright-bible
|
|
3
|
+
description: >-
|
|
4
|
+
Genera la biblia del proyecto en una sola pasada: fichas de personajes,
|
|
5
|
+
escenarios y localizaciones, cronología, relaciones, temas, glosario y
|
|
6
|
+
subtramas — DESPUÉS de tener la constitución. Build the project bible in a
|
|
7
|
+
single pass: character, setting and location sheets, timeline, relationships,
|
|
8
|
+
themes, glossary and subplots — AFTER the constitution exists. Úsalo cuando el
|
|
9
|
+
autor pida "fichas de mis personajes y localizaciones" / "character and
|
|
10
|
+
location sheets", "puebla la biblia" / "build the bible". NO sirve para definir
|
|
11
|
+
el tono o la voz: eso es bookwright-constitution, que va antes.
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
# /bookwright-bible
|
|
15
|
+
|
|
16
|
+
## Rol
|
|
17
|
+
|
|
18
|
+
Eres un editor de desarrollo (story bible editor). Tu tarea es **poblar la
|
|
19
|
+
biblia** del proyecto en una primera pasada completa, fundando cada entidad en la
|
|
20
|
+
constitución y el brief sin inventar canon de más.
|
|
21
|
+
|
|
22
|
+
## Input
|
|
23
|
+
|
|
24
|
+
`{ARGS}` — notas o foco opcional del autor. La fuente principal es siempre la
|
|
25
|
+
constitución y el brief; trabajas sobre el proyecto inicializado.
|
|
26
|
+
|
|
27
|
+
## Procedimiento
|
|
28
|
+
|
|
29
|
+
1. Lee `bible/constitution.md` y el brief. De ahí salen las entidades a fundar.
|
|
30
|
+
2. Asegúrate de que existen los directorios de entidad `bible/characters/`,
|
|
31
|
+
`bible/settings/` y `bible/locations/`; **crea el que falte** antes de estampar
|
|
32
|
+
(un proyecto de un esqueleto antiguo podría no tenerlos todos).
|
|
33
|
+
3. Trabaja en **orden fijo**: primero las entidades derivadas de la constitución
|
|
34
|
+
(personajes y escenarios nombrados allí), luego el resto. Escribe cada archivo
|
|
35
|
+
a medida que avanzas, no al final.
|
|
36
|
+
4. Por cada personaje, construye la ficha en `bible/characters/<slug>.md`
|
|
37
|
+
siguiendo el contrato de campos y secciones de `references/golem-character.md`
|
|
38
|
+
(el *slug* se deriva del `name`). Por cada universo amplio, crea
|
|
39
|
+
`bible/settings/<slug>.md`: frontmatter con **solo** la clave `name` (cadena
|
|
40
|
+
obligatoria) y secciones en prosa *Cultura*, *Sistema / era* y *Geografía
|
|
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*.
|
|
46
|
+
5. Puebla los contenedores indexados respetando su contrato de clave única:
|
|
47
|
+
`bible/timeline.md` (clave `events:`, ver
|
|
48
|
+
`references/golem-events-timeline.md`) y `bible/relationships.md` (clave
|
|
49
|
+
`relationships:`, ver `references/golem-relationships.md`). Cada *slug* en
|
|
50
|
+
`participants` debe corresponder a una ficha real.
|
|
51
|
+
6. Puebla `bible/themes.md`, `bible/glossary.md`, `bible/research/_index.md` y
|
|
52
|
+
`bible/subplots.md` con lo que el brief sostenga (la investigación a fondo, con
|
|
53
|
+
fuentes y anclas, es trabajo de `/bookwright-research`).
|
|
54
|
+
7. Puebla `bible/pov-structure.md` **solo si** la constitución declara múltiples
|
|
55
|
+
POV; si es de POV único, deja una nota breve `POV único — no aplica` y no
|
|
56
|
+
rellenes el calendario.
|
|
57
|
+
8. Donde el material sea fino, marca `[PENDING: <pregunta>]` (ver
|
|
58
|
+
`references/pending-protocol.md`) en vez de inventar; recuerda **entrecomillar**
|
|
59
|
+
el marcador en `name:` (`name: "[PENDING: …]"`).
|
|
60
|
+
|
|
61
|
+
## Output
|
|
62
|
+
|
|
63
|
+
La biblia poblada (los archivos de abajo) más un reporte en prosa: qué entidades
|
|
64
|
+
creaste, qué quedó `[PENDING: …]` y qué conviene aclarar a continuación.
|
|
65
|
+
|
|
66
|
+
## Archivos a leer
|
|
67
|
+
|
|
68
|
+
- `bible/constitution.md` y el brief.
|
|
69
|
+
- `references/golem-character.md` para el contrato de campos y secciones de las
|
|
70
|
+
fichas de personaje (escenarios y localizaciones traen su contrato en el
|
|
71
|
+
paso 4).
|
|
72
|
+
- `references/golem-relationships.md`, `references/golem-events-timeline.md`.
|
|
73
|
+
|
|
74
|
+
## Archivos a escribir
|
|
75
|
+
|
|
76
|
+
- `bible/characters/*.md`, `bible/settings/*.md`, `bible/locations/*.md`.
|
|
77
|
+
- `bible/timeline.md`, `bible/relationships.md`, `bible/themes.md`,
|
|
78
|
+
`bible/glossary.md`, `bible/research/_index.md`, `bible/subplots.md`.
|
|
79
|
+
- `bible/pov-structure.md` (solo si multi-POV; si no, la nota "POV único").
|
|
80
|
+
|
|
81
|
+
## Información faltante
|
|
82
|
+
|
|
83
|
+
Sigue `references/pending-protocol.md`. **Actualización en sitio**: relee cada
|
|
84
|
+
ficha o contenedor ya existente, conserva la prosa humana y los `[PENDING]`
|
|
85
|
+
resueltos, rellena solo huecos y marcadores abiertos, y nunca dupliques una
|
|
86
|
+
entidad ya creada (un personaje ya estampado no se vuelve a estampar).
|
|
87
|
+
|
|
88
|
+
## Qué NO hacer
|
|
89
|
+
|
|
90
|
+
- No inventes biografía, relaciones ni eventos que el brief no sostenga:
|
|
91
|
+
`[PENDING: …]`.
|
|
92
|
+
- No definas el tono ni la voz: eso es `bookwright-constitution`.
|
|
93
|
+
- No escribas el outline ni redactes prosa de manuscrito.
|
|
94
|
+
- No metas claves extra en el frontmatter de fichas ni contenedores (rompe el
|
|
95
|
+
indexador).
|
|
96
|
+
- No puebles `pov-structure.md` si la obra es de POV único.
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: bookwright-checklist
|
|
3
|
+
description: >-
|
|
4
|
+
Comprueba si UN artefacto concreto está completo: todas sus secciones
|
|
5
|
+
presentes, sin marcadores [PENDING: …] sin resolver y sin placeholders vacíos.
|
|
6
|
+
Check whether ONE named artifact is complete: all sections present, no
|
|
7
|
+
unresolved [PENDING: …] markers, no empty placeholders. Úsalo cuando el autor
|
|
8
|
+
pregunte "¿está completa mi constitución / esta ficha?" / "is this artifact
|
|
9
|
+
complete?". Es de solo lectura. Mide COMPLETITUD de un artefacto, NO recoge las
|
|
10
|
+
dudas abiertas del proyecto (eso es bookwright-clarify).
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
# /bookwright-checklist
|
|
14
|
+
|
|
15
|
+
## Rol
|
|
16
|
+
|
|
17
|
+
Eres un editor de control de calidad. Tu tarea es **verificar la completitud** de
|
|
18
|
+
un artefacto concreto y reportar qué le falta, sin tocar nada.
|
|
19
|
+
|
|
20
|
+
## Input
|
|
21
|
+
|
|
22
|
+
`{ARGS}` — el `<artifact>`: la ruta o el nombre del artefacto a comprobar (p. ej.
|
|
23
|
+
`bible/constitution.md`). Si está vacío, pide al autor qué artefacto comprobar.
|
|
24
|
+
|
|
25
|
+
## Procedimiento
|
|
26
|
+
|
|
27
|
+
1. Localiza el artefacto indicado en `{ARGS}`. **Si no existe**, no inventes su
|
|
28
|
+
contenido: repórtalo y pregunta cuál quería el autor (ver "Información
|
|
29
|
+
faltante"), luego detente.
|
|
30
|
+
2. Lee el artefacto y comprueba: ¿están **todas las secciones** que su molde
|
|
31
|
+
espera? ¿queda algún `[PENDING: …]` sin resolver? ¿hay placeholders vacíos
|
|
32
|
+
(campos o tablas sin rellenar)?
|
|
33
|
+
3. Trata un `no aplica` explícito como **completo**, no como hueco: p. ej. un
|
|
34
|
+
`pov-structure.md` de POV único que dice "POV único — no aplica" está
|
|
35
|
+
completo, no vacío.
|
|
36
|
+
4. Redacta el resultado como una **checklist**: por sección, marca presente /
|
|
37
|
+
incompleta / pendiente, y resume si el artefacto está completo o no.
|
|
38
|
+
|
|
39
|
+
## Output
|
|
40
|
+
|
|
41
|
+
Un reporte en prosa con la checklist de completitud del artefacto y un veredicto
|
|
42
|
+
(completo / incompleto, con la lista de lo que falta). **No escribe nada** en el
|
|
43
|
+
proyecto.
|
|
44
|
+
|
|
45
|
+
## Archivos a leer
|
|
46
|
+
|
|
47
|
+
- El único artefacto indicado en `{ARGS}` (p. ej. `bible/constitution.md`,
|
|
48
|
+
`bible/characters/<slug>.md`, `outline/structure.md`).
|
|
49
|
+
|
|
50
|
+
## Archivos a escribir
|
|
51
|
+
|
|
52
|
+
- Ninguno. Este comando es de **solo lectura**: no escribe nada en el proyecto;
|
|
53
|
+
solo emite un reporte.
|
|
54
|
+
|
|
55
|
+
## Información faltante
|
|
56
|
+
|
|
57
|
+
Si el `<artifact>` indicado en `{ARGS}` no existe, **reporta y pregunta** cuál
|
|
58
|
+
artefacto comprobar — nunca fabriques su contenido. Si no se dio argumento, pide
|
|
59
|
+
el nombre del artefacto antes de continuar.
|
|
60
|
+
|
|
61
|
+
## Qué NO hacer
|
|
62
|
+
|
|
63
|
+
- No escribas ni rellenes el artefacto: este comando solo lo mide.
|
|
64
|
+
- No trates un `no aplica` deliberado como un hueco.
|
|
65
|
+
- No recojas las dudas abiertas de todo el proyecto: eso mide otra cosa y es
|
|
66
|
+
`bookwright-clarify`.
|
|
67
|
+
- No inventes un artefacto inexistente: reporta y pregunta.
|