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
bookwright/__init__.py
ADDED
bookwright/__main__.py
ADDED
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,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
|