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.
Files changed (149) hide show
  1. bookwright/__init__.py +3 -0
  2. bookwright/__main__.py +6 -0
  3. bookwright/cli.py +19 -0
  4. bookwright/commands/__init__.py +0 -0
  5. bookwright/commands/_envelope.py +36 -0
  6. bookwright/commands/check.py +75 -0
  7. bookwright/commands/graph/__init__.py +23 -0
  8. bookwright/commands/graph/build.py +157 -0
  9. bookwright/commands/graph/envelope.py +26 -0
  10. bookwright/commands/graph/query.py +98 -0
  11. bookwright/commands/init/__init__.py +5 -0
  12. bookwright/commands/init/conflict.py +107 -0
  13. bookwright/commands/init/envelope.py +322 -0
  14. bookwright/commands/init/git.py +96 -0
  15. bookwright/commands/init/main.py +263 -0
  16. bookwright/commands/init/resolve.py +193 -0
  17. bookwright/commands/init/scaffold.py +242 -0
  18. bookwright/commands/init/validate.py +172 -0
  19. bookwright/commands/integration/__init__.py +22 -0
  20. bookwright/commands/integration/use.py +120 -0
  21. bookwright/commands/validate.py +160 -0
  22. bookwright/commands/version.py +35 -0
  23. bookwright/core/__init__.py +35 -0
  24. bookwright/core/_blocks.py +239 -0
  25. bookwright/core/_build.py +154 -0
  26. bookwright/core/_research_block.py +56 -0
  27. bookwright/core/_translate.py +90 -0
  28. bookwright/core/errors.py +127 -0
  29. bookwright/core/iso639_1.py +200 -0
  30. bookwright/core/manifest.py +343 -0
  31. bookwright/errors.py +47 -0
  32. bookwright/golem/__init__.py +71 -0
  33. bookwright/golem/base.py +200 -0
  34. bookwright/golem/errors.py +29 -0
  35. bookwright/golem/modules/__init__.py +1 -0
  36. bookwright/golem/modules/character.py +109 -0
  37. bookwright/golem/modules/event.py +91 -0
  38. bookwright/golem/modules/feature.py +161 -0
  39. bookwright/golem/modules/inference.py +41 -0
  40. bookwright/golem/modules/narrative.py +55 -0
  41. bookwright/golem/modules/provenance.py +197 -0
  42. bookwright/golem/modules/relationship.py +38 -0
  43. bookwright/golem/modules/setting.py +30 -0
  44. bookwright/golem/namespaces.py +332 -0
  45. bookwright/golem/serialize.py +25 -0
  46. bookwright/golem/slug.py +22 -0
  47. bookwright/indexers/__init__.py +47 -0
  48. bookwright/indexers/base.py +55 -0
  49. bookwright/indexers/errors.py +80 -0
  50. bookwright/indexers/rdflib_indexer.py +89 -0
  51. bookwright/integrations/__init__.py +155 -0
  52. bookwright/integrations/base.py +117 -0
  53. bookwright/integrations/claude/__init__.py +29 -0
  54. bookwright/integrations/constants.py +38 -0
  55. bookwright/integrations/descriptions.py +48 -0
  56. bookwright/integrations/errors.py +170 -0
  57. bookwright/integrations/generic/__init__.py +56 -0
  58. bookwright/integrations/lint.py +160 -0
  59. bookwright/integrations/materialize.py +202 -0
  60. bookwright/integrations/options.py +203 -0
  61. bookwright/io/__init__.py +1 -0
  62. bookwright/io/bible.py +500 -0
  63. bookwright/io/errors.py +98 -0
  64. bookwright/io/frontmatter.py +61 -0
  65. bookwright/io/fs.py +226 -0
  66. bookwright/io/manuscript.py +15 -0
  67. bookwright/io/project.py +21 -0
  68. bookwright/io/report.py +107 -0
  69. bookwright/io/research.py +427 -0
  70. bookwright/resources/__init__.py +1 -0
  71. bookwright/resources/commands/bookwright-analyze.md +66 -0
  72. bookwright/resources/commands/bookwright-bible.md +96 -0
  73. bookwright/resources/commands/bookwright-checklist.md +67 -0
  74. bookwright/resources/commands/bookwright-clarify.md +65 -0
  75. bookwright/resources/commands/bookwright-constitution.md +79 -0
  76. bookwright/resources/commands/bookwright-continuity.md +70 -0
  77. bookwright/resources/commands/bookwright-draft.md +74 -0
  78. bookwright/resources/commands/bookwright-outline.md +71 -0
  79. bookwright/resources/commands/bookwright-research.md +107 -0
  80. bookwright/resources/commands/bookwright-scenes.md +66 -0
  81. bookwright/resources/commands/bookwright-synopsis.md +67 -0
  82. bookwright/resources/commands/bookwright-verify.md +136 -0
  83. bookwright/resources/commands/references/golem-character.md +65 -0
  84. bookwright/resources/commands/references/golem-events-timeline.md +56 -0
  85. bookwright/resources/commands/references/golem-relationships.md +53 -0
  86. bookwright/resources/commands/references/greimas-actants.md +57 -0
  87. bookwright/resources/commands/references/pending-protocol.md +72 -0
  88. bookwright/resources/commands/references/propp-functions.md +54 -0
  89. bookwright/resources/commands/references/research-format.md +136 -0
  90. bookwright/resources/project/.bookwright/cache/.gitkeep +0 -0
  91. bookwright/resources/project/.bookwright/schema/.gitkeep +0 -0
  92. bookwright/resources/project/.bookwright/templates/.gitkeep +0 -0
  93. bookwright/resources/project/.gitignore +23 -0
  94. bookwright/resources/project/README.md.j2 +40 -0
  95. bookwright/resources/project/__init__.py +6 -0
  96. bookwright/resources/project/bible/characters/.gitkeep +0 -0
  97. bookwright/resources/project/bible/constitution.md.j2 +74 -0
  98. bookwright/resources/project/bible/glossary.md +36 -0
  99. bookwright/resources/project/bible/locations/.gitkeep +0 -0
  100. bookwright/resources/project/bible/pov-structure.md +43 -0
  101. bookwright/resources/project/bible/relationships.md +36 -0
  102. bookwright/resources/project/bible/research/_index.md +28 -0
  103. bookwright/resources/project/bible/research/sources.md +23 -0
  104. bookwright/resources/project/bible/settings/.gitkeep +0 -0
  105. bookwright/resources/project/bible/subplots.md +35 -0
  106. bookwright/resources/project/bible/themes.md +36 -0
  107. bookwright/resources/project/bible/timeline.md +38 -0
  108. bookwright/resources/project/manuscript/.gitkeep +0 -0
  109. bookwright/resources/project/outline/arcs.md +34 -0
  110. bookwright/resources/project/outline/scenes.md +31 -0
  111. bookwright/resources/project/outline/structure.md +35 -0
  112. bookwright/resources/project/outline/synopsis.md +25 -0
  113. bookwright/resources/schemas/__init__.py +19 -0
  114. bookwright/resources/schemas/golem-1.1/VERSION +1 -0
  115. bookwright/resources/schemas/golem-1.1/golem.ttl +1947 -0
  116. bookwright/resources/schemas/golem-1.1/version.json +8 -0
  117. bookwright/resources/templates/__init__.py +1 -0
  118. bookwright/resources/templates/bible/character.md.tmpl +63 -0
  119. bookwright/resources/templates/bible/location.md.tmpl +37 -0
  120. bookwright/resources/templates/bible/research/_index.md.tmpl +25 -0
  121. bookwright/resources/templates/bible/research/sources.md.tmpl +21 -0
  122. bookwright/resources/templates/bible/research/tema.md.tmpl +37 -0
  123. bookwright/resources/templates/bible/setting.md.tmpl +38 -0
  124. bookwright/resources/templates/manifest.template.toml +79 -0
  125. bookwright/resources/templates/manuscript/chapter.md.tmpl +36 -0
  126. bookwright/resources/templates/scenes/scene.md.tmpl +37 -0
  127. bookwright/resources/vocabularies/__init__.py +6 -0
  128. bookwright/resources/vocabularies/greimas.ttl +4 -0
  129. bookwright/resources/vocabularies/propp.ttl +4 -0
  130. bookwright/resources/vocabularies/sources.ttl +82 -0
  131. bookwright/validation/__init__.py +33 -0
  132. bookwright/validation/anchor_queries.py +223 -0
  133. bookwright/validation/base.py +233 -0
  134. bookwright/validation/queries.py +197 -0
  135. bookwright/validation/registry.py +185 -0
  136. bookwright/validation/report.py +106 -0
  137. bookwright/validation/runner.py +65 -0
  138. bookwright/validation/validators/__init__.py +9 -0
  139. bookwright/validation/validators/character_presence.py +202 -0
  140. bookwright/validation/validators/factual_anchor.py +291 -0
  141. bookwright/validation/validators/focalization.py +152 -0
  142. bookwright/validation/validators/setting_continuity.py +100 -0
  143. bookwright/validation/validators/temporal.py +277 -0
  144. bookwright_cli-0.2.0.dist-info/METADATA +218 -0
  145. bookwright_cli-0.2.0.dist-info/RECORD +149 -0
  146. bookwright_cli-0.2.0.dist-info/WHEEL +4 -0
  147. bookwright_cli-0.2.0.dist-info/entry_points.txt +2 -0
  148. bookwright_cli-0.2.0.dist-info/licenses/LICENSE +202 -0
  149. bookwright_cli-0.2.0.dist-info/licenses/NOTICE +14 -0
bookwright/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ """Bookwright — Spec-driven authoring toolkit."""
2
+
3
+ __version__ = "0.2.0"
bookwright/__main__.py ADDED
@@ -0,0 +1,6 @@
1
+ """Allow `python -m bookwright` to invoke the CLI."""
2
+
3
+ from bookwright.cli import app
4
+
5
+ if __name__ == "__main__":
6
+ app()
bookwright/cli.py ADDED
@@ -0,0 +1,19 @@
1
+ """Bookwright CLI entry point."""
2
+
3
+ import typer
4
+
5
+ from bookwright.commands import check, graph, init, integration, validate, version
6
+
7
+ app = typer.Typer(
8
+ name="bookwright",
9
+ help="Bookwright — Spec-driven authoring toolkit.",
10
+ no_args_is_help=True,
11
+ add_completion=False,
12
+ )
13
+
14
+ app.command("version")(version.run)
15
+ app.command("check")(check.run)
16
+ app.command("init", context_settings=init.CONTEXT_SETTINGS)(init.run)
17
+ app.command("validate")(validate.run)
18
+ app.add_typer(graph.app, name="graph")
19
+ app.add_typer(integration.app, name="integration")
File without changes
@@ -0,0 +1,36 @@
1
+ """Shared command-layer envelope helper (review R1).
2
+
3
+ Several agent-facing commands catch a ``ManifestError`` at their ``--json``
4
+ boundary and remap it to the contract's single ``invalid_manifest`` code. Rather
5
+ than re-build the ``{status,code,message}`` skeleton by hand in each command
6
+ module, the remap routes through the base ``BookwrightError.to_json`` — the one
7
+ place the envelope skeleton lives — exactly as ``commands.validate._UsageError``
8
+ already does for the same case.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ from typing import Any
14
+
15
+ from bookwright.errors import BookwrightError
16
+
17
+ #: The contract code every caught ``ManifestError`` collapses to at a ``--json``
18
+ #: boundary. Single-sourced here so the two remap sites — this module and
19
+ #: ``commands.validate._UsageError`` — cannot drift to different literals.
20
+ INVALID_MANIFEST_CODE = "invalid_manifest"
21
+
22
+
23
+ class _InvalidManifestError(BookwrightError):
24
+ """A caught ``ManifestError`` re-coded to the contract's ``invalid_manifest``.
25
+
26
+ Mirrors ``commands.validate._UsageError(INVALID_MANIFEST_CODE, ...)``: the
27
+ remap is expressed as a ``BookwrightError`` whose canonical ``to_json()``
28
+ builds the envelope, never a hand-rolled dict.
29
+ """
30
+
31
+ code = INVALID_MANIFEST_CODE
32
+
33
+
34
+ def invalid_manifest_payload(exc: Exception) -> dict[str, Any]:
35
+ """The ``invalid_manifest`` error envelope for a caught ``ManifestError``."""
36
+ return _InvalidManifestError(str(exc)).to_json()
@@ -0,0 +1,75 @@
1
+ """`bookwright check` — verify the running interpreter and runtime dependencies."""
2
+
3
+ import importlib
4
+ import json
5
+ import sys
6
+ from typing import TypedDict
7
+
8
+ import typer
9
+ from rich.console import Console
10
+
11
+ RUNTIME_MODULES: tuple[str, ...] = (
12
+ "typer",
13
+ "rich",
14
+ "rdflib",
15
+ "pydantic",
16
+ "tomlkit",
17
+ "jinja2",
18
+ "slugify",
19
+ "platformdirs",
20
+ "uuid_utils",
21
+ "yaml",
22
+ "packaging",
23
+ )
24
+
25
+
26
+ class CheckResult(TypedDict, total=False):
27
+ name: str
28
+ status: str
29
+ detail: str
30
+
31
+
32
+ def _python_version_check() -> CheckResult:
33
+ found = f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}"
34
+ if sys.version_info >= (3, 11): # noqa: UP036
35
+ return {"name": "python_version", "status": "ok", "detail": found}
36
+ return {
37
+ "name": "python_version",
38
+ "status": "fail",
39
+ "detail": f"found {found}, requires >=3.11",
40
+ }
41
+
42
+
43
+ def _dependency_check(module_name: str) -> CheckResult:
44
+ try:
45
+ importlib.import_module(module_name)
46
+ except ImportError as exc:
47
+ return {
48
+ "name": f"dependency:{module_name}",
49
+ "status": "fail",
50
+ "detail": str(exc),
51
+ }
52
+ return {"name": f"dependency:{module_name}", "status": "ok"}
53
+
54
+
55
+ def run(
56
+ json_output: bool = typer.Option(
57
+ False, "--json", help="Emit a single JSON document on stdout."
58
+ ),
59
+ ) -> None:
60
+ """Verify Python version (>=3.11) and that all declared deps are importable."""
61
+ checks: list[CheckResult] = [_python_version_check()]
62
+ for module_name in RUNTIME_MODULES:
63
+ checks.append(_dependency_check(module_name))
64
+ ok = all(c["status"] == "ok" for c in checks)
65
+ payload = {"ok": ok, "checks": checks}
66
+ if json_output:
67
+ sys.stdout.write(json.dumps(payload, separators=(",", ":")) + "\n")
68
+ else:
69
+ console = Console()
70
+ for check in checks:
71
+ tag = "OK " if check["status"] == "ok" else "FAIL"
72
+ detail = check.get("detail", "")
73
+ suffix = f" — {detail}" if detail else ""
74
+ console.print(f"{tag} {check['name']}{suffix}")
75
+ raise typer.Exit(code=0 if ok else 1)
@@ -0,0 +1,23 @@
1
+ """The ``bookwright graph`` Typer sub-app.
2
+
3
+ ``build`` and ``query`` live in their own modules (Principle IV) and register
4
+ their callbacks here. The app is wired into the root CLI in
5
+ :mod:`bookwright.cli` via ``app.add_typer(graph.app, name="graph")``.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import typer
11
+
12
+ app = typer.Typer(
13
+ name="graph",
14
+ help="Build and query the project's GOLEM graph.",
15
+ no_args_is_help=True,
16
+ add_completion=False,
17
+ )
18
+
19
+ # `build` and `query` register their callbacks on `app` at import
20
+ # time; importing them here keeps the sub-app self-contained. The `as` redirect
21
+ # marks the imports as intentional re-exports (registration side effect).
22
+ from . import build as build # noqa: E402
23
+ from . import query as query # noqa: E402
@@ -0,0 +1,157 @@
1
+ """`bookwright graph build` — read the bible, build the graph, write Turtle.
2
+
3
+ Locates the project, resolves the manifest-selected engine, maps every
4
+ recognised bible file to GOLEM entities (with CIDOC provenance), and serializes
5
+ to ``bible/graph.ttl``. Fault model (R7, cli-graph.md): missing project /
6
+ directory / unknown engine → exit 2; slug collision → exit 3 (no graph); ≥ 1
7
+ skipped file → exit 4 (graph still written); clean build → exit 0.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import typer
13
+ from rich.console import Console
14
+
15
+ from bookwright.core.errors import ManifestError
16
+ from bookwright.core.manifest import Manifest
17
+ from bookwright.golem.namespaces import timeline_uri
18
+ from bookwright.indexers import UnknownIndexerError, resolve_indexer
19
+ from bookwright.io.bible import build_provenance, map_bible
20
+ from bookwright.io.errors import (
21
+ MissingDirectoryError,
22
+ ProjectNotFoundError,
23
+ ResearchError,
24
+ SlugCollisionError,
25
+ )
26
+ from bookwright.io.manuscript import manuscript_present
27
+ from bookwright.io.project import find_project_root
28
+ from bookwright.io.report import BuildReport, ResearchTargetWarning
29
+ from bookwright.io.research import map_research
30
+
31
+ from .._envelope import invalid_manifest_payload
32
+ from . import app
33
+ from .envelope import emit_error, emit_json
34
+
35
+ EXIT_CONFIG = 2
36
+ EXIT_COLLISION = 3
37
+
38
+
39
+ @app.command("build")
40
+ def run(
41
+ force: bool = typer.Option(
42
+ False, "--force", help="Rebuild from scratch, ignoring any cache (v0: no-op)."
43
+ ),
44
+ json_output: bool = typer.Option(
45
+ False, "--json", help="Emit the build report as one JSON document on stdout."
46
+ ),
47
+ ) -> None:
48
+ """Build the project graph from the bible and write ``bible/graph.ttl``."""
49
+ console = Console(stderr=True)
50
+ try:
51
+ report = _build()
52
+ except ManifestError as exc:
53
+ emit_error(invalid_manifest_payload(exc), json_output)
54
+ raise typer.Exit(EXIT_CONFIG) from exc
55
+ except (
56
+ ProjectNotFoundError,
57
+ MissingDirectoryError,
58
+ UnknownIndexerError,
59
+ ResearchError,
60
+ ) as exc:
61
+ emit_error(exc.to_json(), json_output)
62
+ raise typer.Exit(EXIT_CONFIG) from exc
63
+ except SlugCollisionError as exc:
64
+ emit_error(exc.to_json(), json_output)
65
+ raise typer.Exit(EXIT_COLLISION) from exc
66
+
67
+ if json_output:
68
+ emit_json(report.to_json())
69
+ else:
70
+ _print_summary(console, report)
71
+ raise typer.Exit(report.exit_code)
72
+
73
+
74
+ def _build() -> BuildReport:
75
+ """Run the build, returning the report. Raises the fault-model exceptions."""
76
+ project_root = find_project_root()
77
+ manifest = Manifest.load(project_root / "manifest.toml")
78
+
79
+ bible_dir = project_root / manifest.paths.bible
80
+ manuscript_dir = project_root / manifest.paths.manuscript
81
+ if not bible_dir.is_dir():
82
+ raise MissingDirectoryError("bible", str(bible_dir))
83
+ if not manuscript_present(manuscript_dir):
84
+ raise MissingDirectoryError("manuscript", str(manuscript_dir))
85
+
86
+ engine_cls = resolve_indexer(manifest.bookwright.indexer)
87
+ engine = engine_cls()
88
+
89
+ uri_base = manifest.bookwright.uri_base
90
+ result = map_bible(project_root, bible_dir, uri_base)
91
+
92
+ for mapped in result.mapped:
93
+ for triple in mapped.entity.to_triples():
94
+ engine.add_triple(*triple)
95
+ for assignment in build_provenance(mapped, uri_base):
96
+ for triple in assignment.to_triples():
97
+ engine.add_triple(*triple)
98
+
99
+ # Research pass: map bible/research/ and feed its triples into the same engine
100
+ # (one graph, one save — research D8). Research entities are already E13
101
+ # reifications, so they are NOT routed through build_provenance.
102
+ research = map_research(
103
+ project_root,
104
+ bible_dir / "research",
105
+ uri_base,
106
+ manifest.book.language,
107
+ result.entity_index,
108
+ timeline_uri(uri_base),
109
+ )
110
+ for entity in research.entities:
111
+ for triple in entity.to_triples():
112
+ engine.add_triple(*triple)
113
+
114
+ graph_rel = manifest.paths.graph
115
+ engine.save(project_root / graph_rel)
116
+
117
+ return BuildReport(
118
+ files_processed=result.files_processed + research.files_processed,
119
+ entities=len(result.entities) + len(research.entities),
120
+ triples=engine.count(),
121
+ graph_path=graph_rel,
122
+ skipped=tuple(result.skipped),
123
+ unknown_keys=tuple(result.unknown_keys),
124
+ unresolved_participants=tuple(result.unresolved_participants),
125
+ sources=len(research.sources),
126
+ findings=len(research.findings),
127
+ anchors=len(research.anchors),
128
+ research_warnings=tuple(
129
+ ResearchTargetWarning(path=w.relpath, field=w.field, name=w.name)
130
+ for w in research.warnings
131
+ ),
132
+ )
133
+
134
+
135
+ def _print_summary(console: Console, report: BuildReport) -> None:
136
+ """Write the human build summary to stderr (one line per metric)."""
137
+ console.print(
138
+ f"processed {report.files_processed} files, "
139
+ f"{report.entities} entities, {report.triples} triples → {report.graph_path}"
140
+ )
141
+ if report.skipped:
142
+ console.print(f"skipped {len(report.skipped)} file(s):")
143
+ for item in report.skipped:
144
+ console.print(f" - {item.path}: {item.reason}")
145
+ if report.unknown_keys:
146
+ console.print(f"{len(report.unknown_keys)} unknown frontmatter key(s) ignored")
147
+ if report.unresolved_participants:
148
+ console.print(f"{len(report.unresolved_participants)} unresolved participant reference(s)")
149
+ if report.sources or report.findings or report.anchors:
150
+ console.print(
151
+ f"research: {report.sources} source(s), "
152
+ f"{report.findings} finding(s), {report.anchors} anchor(s)"
153
+ )
154
+ if report.research_warnings:
155
+ console.print(f"{len(report.research_warnings)} unresolved research target(s):")
156
+ for warning in report.research_warnings:
157
+ console.print(f" - {warning.path}: {warning.field} '{warning.name}' not in bible")
@@ -0,0 +1,26 @@
1
+ """JSON success/error envelopes for the ``graph`` sub-commands (Principle IX).
2
+
3
+ Single-line ``json.dumps(payload, separators=(",", ":")) + "\\n"`` to stdout,
4
+ mirroring the pattern in :mod:`bookwright.commands.version`. Human prose and
5
+ progress go to stderr via a ``Console(stderr=True)`` owned by each command.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import json
11
+ import sys
12
+ from typing import Any
13
+
14
+
15
+ def emit_json(payload: dict[str, Any]) -> None:
16
+ """Write exactly one JSON document to stdout (the only thing on stdout)."""
17
+ sys.stdout.write(json.dumps(payload, separators=(",", ":")) + "\n")
18
+
19
+
20
+ def emit_error(payload: dict[str, Any], json_output: bool) -> None:
21
+ """Surface an error envelope: one JSON doc on stdout under ``--json``, else a
22
+ single ``bookwright: error: <message>`` line on stderr (Principle IX)."""
23
+ if json_output:
24
+ emit_json(payload)
25
+ else:
26
+ sys.stderr.write(f"bookwright: error: {payload['message']}\n")
@@ -0,0 +1,98 @@
1
+ """`bookwright graph query` — load ``bible/graph.ttl`` and run a SPARQL query.
2
+
3
+ Read-only. Renders a ``rich`` table for humans (stdout) or, under ``--json``, a
4
+ single ``{"status":"ok","results":[...],"count":N}`` document (Principle IX).
5
+ Fault model (cli-graph.md): missing project / graph / unknown engine → exit 2;
6
+ malformed SPARQL → exit 3 with no partial rows.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from typing import Any
12
+
13
+ import typer
14
+ from rich.console import Console
15
+ from rich.table import Table
16
+
17
+ from bookwright.core.errors import ManifestError
18
+ from bookwright.core.manifest import Manifest
19
+ from bookwright.indexers import (
20
+ GraphLoadError,
21
+ GraphNotBuiltError,
22
+ InvalidQueryError,
23
+ UnknownIndexerError,
24
+ resolve_indexer,
25
+ )
26
+ from bookwright.io.errors import ProjectNotFoundError
27
+ from bookwright.io.project import find_project_root
28
+
29
+ from .._envelope import invalid_manifest_payload
30
+ from . import app
31
+ from .envelope import emit_error, emit_json
32
+
33
+ EXIT_CONFIG = 2
34
+ EXIT_INVALID_QUERY = 3
35
+
36
+
37
+ @app.command("query")
38
+ def run(
39
+ sparql: str = typer.Argument(..., help="The SPARQL query to run against the graph."),
40
+ json_output: bool = typer.Option(
41
+ False, "--json", help="Emit results as one JSON document on stdout."
42
+ ),
43
+ ) -> None:
44
+ """Run ``sparql`` against the project graph and print the rows."""
45
+ try:
46
+ rows = _query(sparql)
47
+ except ManifestError as exc:
48
+ emit_error(invalid_manifest_payload(exc), json_output)
49
+ raise typer.Exit(EXIT_CONFIG) from exc
50
+ except (
51
+ ProjectNotFoundError,
52
+ GraphNotBuiltError,
53
+ GraphLoadError,
54
+ UnknownIndexerError,
55
+ ) as exc:
56
+ emit_error(exc.to_json(), json_output)
57
+ raise typer.Exit(EXIT_CONFIG) from exc
58
+ except InvalidQueryError as exc:
59
+ emit_error(exc.to_json(), json_output)
60
+ raise typer.Exit(EXIT_INVALID_QUERY) from exc
61
+
62
+ if json_output:
63
+ emit_json({"status": "ok", "results": rows, "count": len(rows)})
64
+ else:
65
+ _render_table(rows)
66
+
67
+
68
+ def _query(sparql: str) -> list[dict[str, Any]]:
69
+ """Load the graph and run the query, returning fully materialized rows."""
70
+ project_root = find_project_root()
71
+ manifest = Manifest.load(project_root / "manifest.toml")
72
+
73
+ engine_cls = resolve_indexer(manifest.bookwright.indexer)
74
+ engine = engine_cls()
75
+
76
+ graph_path = project_root / manifest.paths.graph
77
+ if not graph_path.is_file():
78
+ raise GraphNotBuiltError(manifest.paths.graph)
79
+ engine.load(graph_path)
80
+
81
+ return list(engine.query(sparql))
82
+
83
+
84
+ def _render_table(rows: list[dict[str, Any]]) -> None:
85
+ """Render rows as a ``rich`` table on stdout (an empty note when no matches)."""
86
+ console = Console()
87
+ if not rows:
88
+ Console(stderr=True).print("(no results)")
89
+ return
90
+ columns: list[str] = []
91
+ for row in rows:
92
+ for key in row:
93
+ if key not in columns:
94
+ columns.append(key)
95
+ table = Table(*columns)
96
+ for row in rows:
97
+ table.add_row(*(row.get(column, "") for column in columns))
98
+ console.print(table)
@@ -0,0 +1,5 @@
1
+ """Public surface for the `bookwright init` subcommand package."""
2
+
3
+ from .main import CONTEXT_SETTINGS, run
4
+
5
+ __all__ = ["CONTEXT_SETTINGS", "run"]
@@ -0,0 +1,107 @@
1
+ """Pre-scaffold conflict checks and ledger seeding for ``bookwright init``.
2
+
3
+ Owns FR-026 / FR-027 / FR-028 / FR-029 — the name-collision matrices for
4
+ ``named`` and ``here`` modes — plus the tiny ``BackupLedger`` seeder that
5
+ records the project root if ``init`` created it.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from pathlib import Path
11
+
12
+ import typer
13
+
14
+ from . import resolve
15
+ from .envelope import emit_error
16
+ from .scaffold import BackupLedger
17
+
18
+
19
+ def apply_named_conflict_matrix(
20
+ target: Path,
21
+ force: bool,
22
+ *,
23
+ json_output: bool,
24
+ ) -> None:
25
+ """FR-026 / FR-027 / FR-028 — refuse with structured codes when conflicting."""
26
+
27
+ if (target / ".bookwright").exists():
28
+ emit_error(
29
+ code="already_initialized",
30
+ message=(
31
+ f"directory {str(target)!r} is already a Bookwright project (found .bookwright/)"
32
+ ),
33
+ details={"target": str(target)},
34
+ exit_code=3,
35
+ json_output=json_output,
36
+ rolled_back=False,
37
+ )
38
+ if not target.exists():
39
+ return
40
+ if any(target.iterdir()) and not force:
41
+ emit_error(
42
+ code="target_not_empty",
43
+ message=(
44
+ f"directory {target.name!r} is not empty; "
45
+ "use --force to overwrite or --here to initialise in place"
46
+ ),
47
+ details={"target": str(target)},
48
+ exit_code=4,
49
+ json_output=json_output,
50
+ rolled_back=False,
51
+ )
52
+
53
+
54
+ def apply_here_conflict_matrix(
55
+ target: Path,
56
+ force: bool,
57
+ *,
58
+ json_output: bool,
59
+ ) -> None:
60
+ """FR-028 / FR-029 / interactive prompt for ``--here``."""
61
+
62
+ if (target / ".bookwright").exists():
63
+ emit_error(
64
+ code="already_initialized",
65
+ message=(
66
+ f"directory {str(target)!r} is already a Bookwright project (found .bookwright/)"
67
+ ),
68
+ details={"target": str(target)},
69
+ exit_code=3,
70
+ json_output=json_output,
71
+ rolled_back=False,
72
+ )
73
+ if not any(target.iterdir()):
74
+ return
75
+ if force:
76
+ return
77
+ if not resolve.is_interactive() or json_output:
78
+ emit_error(
79
+ code="non_interactive_here",
80
+ message="--here in a non-empty directory requires --force in non-interactive runs",
81
+ details={"target": str(target), "modern": "--force"},
82
+ exit_code=4,
83
+ json_output=json_output,
84
+ rolled_back=False,
85
+ )
86
+ confirmed = typer.confirm(
87
+ f"bookwright: directory {str(target)!r} is not empty. Overwrite name collisions?",
88
+ default=False,
89
+ )
90
+ if not confirmed:
91
+ emit_error(
92
+ code="user_declined_overwrite",
93
+ message="aborted by user",
94
+ details={"target": str(target)},
95
+ exit_code=4,
96
+ json_output=json_output,
97
+ rolled_back=False,
98
+ )
99
+
100
+
101
+ def seed_backup_ledger(project_root: Path, cleanup_project_root: bool) -> BackupLedger:
102
+ """Return a ledger; record the project root itself if we created it."""
103
+
104
+ ledger = BackupLedger(project_root)
105
+ if cleanup_project_root:
106
+ ledger.record_new_directory(project_root)
107
+ return ledger