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,89 @@
|
|
|
1
|
+
"""``RdflibIndexer`` — the v0 default engine (wraps :class:`rdflib.Graph`).
|
|
2
|
+
|
|
3
|
+
Owns Turtle serialization and prefix binding so FR-015 (short prefixes) holds
|
|
4
|
+
regardless of which engine the manifest selects (R5). The build command feeds
|
|
5
|
+
``entity.to_triples()`` through :meth:`add_triple`; ``graph query`` runs SPARQL
|
|
6
|
+
through :meth:`query`.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from collections.abc import Iterable
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
from rdflib import Graph
|
|
16
|
+
from rdflib.query import ResultRow
|
|
17
|
+
from rdflib.term import Literal, URIRef
|
|
18
|
+
|
|
19
|
+
from bookwright.golem.namespaces import bind_prefixes
|
|
20
|
+
|
|
21
|
+
from .errors import GraphLoadError, InvalidQueryError
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class RdflibIndexer:
|
|
25
|
+
"""An :class:`~bookwright.indexers.base.Indexer` backed by ``rdflib``."""
|
|
26
|
+
|
|
27
|
+
def __init__(self, graph: Graph | None = None) -> None:
|
|
28
|
+
self._graph = graph if graph is not None else Graph()
|
|
29
|
+
bind_prefixes(self._graph)
|
|
30
|
+
|
|
31
|
+
# --- ingestion ----------------------------------------------------------
|
|
32
|
+
|
|
33
|
+
def add_triple(
|
|
34
|
+
self,
|
|
35
|
+
s: URIRef | str,
|
|
36
|
+
p: URIRef | str,
|
|
37
|
+
o: URIRef | Literal | str | int | float,
|
|
38
|
+
) -> None:
|
|
39
|
+
"""Add one triple. IRI-like ``str`` subjects/predicates coerce to ``URIRef``;
|
|
40
|
+
objects that are already rdflib terms pass through, scalars become literals."""
|
|
41
|
+
subject = s if isinstance(s, URIRef) else URIRef(s)
|
|
42
|
+
predicate = p if isinstance(p, URIRef) else URIRef(p)
|
|
43
|
+
if isinstance(o, (URIRef, Literal)):
|
|
44
|
+
obj: URIRef | Literal = o
|
|
45
|
+
else:
|
|
46
|
+
obj = Literal(o)
|
|
47
|
+
self._graph.add((subject, predicate, obj))
|
|
48
|
+
|
|
49
|
+
# --- persistence --------------------------------------------------------
|
|
50
|
+
|
|
51
|
+
def load(self, ttl_path: Path) -> None:
|
|
52
|
+
"""Parse the Turtle at ``ttl_path`` into the engine's graph.
|
|
53
|
+
|
|
54
|
+
A malformed file raises :class:`GraphLoadError` (a clean envelope) rather
|
|
55
|
+
than letting an rdflib parse error escape as a raw traceback.
|
|
56
|
+
"""
|
|
57
|
+
try:
|
|
58
|
+
self._graph.parse(str(ttl_path), format="turtle")
|
|
59
|
+
except Exception as exc: # rdflib raises a variety of parse errors
|
|
60
|
+
raise GraphLoadError(str(ttl_path), str(exc)) from exc
|
|
61
|
+
|
|
62
|
+
def save(self, ttl_path: Path) -> None:
|
|
63
|
+
"""Serialize the graph to Turtle (short prefixes), creating parent dirs."""
|
|
64
|
+
ttl_path.parent.mkdir(parents=True, exist_ok=True)
|
|
65
|
+
self._graph.serialize(destination=str(ttl_path), format="turtle")
|
|
66
|
+
|
|
67
|
+
# --- querying -----------------------------------------------------------
|
|
68
|
+
|
|
69
|
+
def query(self, sparql: str) -> Iterable[dict[str, Any]]:
|
|
70
|
+
"""Run a SELECT/ASK; return one dict per row (projected var → ``str``).
|
|
71
|
+
|
|
72
|
+
The result is fully materialized inside the ``try`` so a malformed query
|
|
73
|
+
raises :class:`InvalidQueryError` with **no partial rows** yielded first
|
|
74
|
+
(FR-016).
|
|
75
|
+
"""
|
|
76
|
+
try:
|
|
77
|
+
result = self._graph.query(sparql)
|
|
78
|
+
rows: list[dict[str, Any]] = [
|
|
79
|
+
{str(var): str(value) for var, value in row.asdict().items()}
|
|
80
|
+
for row in result
|
|
81
|
+
if isinstance(row, ResultRow)
|
|
82
|
+
]
|
|
83
|
+
except Exception as exc: # rdflib raises a variety of parse errors
|
|
84
|
+
raise InvalidQueryError(str(exc)) from exc
|
|
85
|
+
return rows
|
|
86
|
+
|
|
87
|
+
def count(self) -> int:
|
|
88
|
+
"""Return the number of triples currently held."""
|
|
89
|
+
return len(self._graph)
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
"""Public API for the ``bookwright.integrations`` layer (iteration 3).
|
|
2
|
+
|
|
3
|
+
Importing this package eagerly populates ``INTEGRATION_REGISTRY`` via
|
|
4
|
+
``_register_builtins()`` (FR-002). Downstream consumers (iteration 4's
|
|
5
|
+
``bookwright init``, iteration 9's skills materializer) need only::
|
|
6
|
+
|
|
7
|
+
from bookwright.integrations import get, list_keys, parse_options
|
|
8
|
+
|
|
9
|
+
The public surface (re-exported in ``__all__``) is contractual; renaming
|
|
10
|
+
any symbol below is a breaking change for iterations 4+.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from bookwright.integrations.base import SkillsIntegration
|
|
16
|
+
from bookwright.integrations.claude import ClaudeIntegration
|
|
17
|
+
from bookwright.integrations.constants import (
|
|
18
|
+
SKILL_DESCRIPTION_MAX_LENGTH,
|
|
19
|
+
SKILL_NAME_MAX_LENGTH,
|
|
20
|
+
SKILL_PLACEHOLDER_MARKER_NAME,
|
|
21
|
+
)
|
|
22
|
+
from bookwright.integrations.errors import (
|
|
23
|
+
DuplicateRegistrationError,
|
|
24
|
+
InvalidIntegrationError,
|
|
25
|
+
InvalidOptionDeclarationError,
|
|
26
|
+
MalformedOptionError,
|
|
27
|
+
SkillLintError,
|
|
28
|
+
SkillMaterializationError,
|
|
29
|
+
UnknownIntegrationError,
|
|
30
|
+
UnknownOptionError,
|
|
31
|
+
)
|
|
32
|
+
from bookwright.integrations.generic import GenericIntegration
|
|
33
|
+
from bookwright.integrations.options import IntegrationOption, parse_options
|
|
34
|
+
|
|
35
|
+
INTEGRATION_REGISTRY: dict[str, type[SkillsIntegration]] = {}
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _fqcn(cls: type) -> str:
|
|
39
|
+
"""Fully-qualified class name, used in DuplicateRegistrationError payloads."""
|
|
40
|
+
|
|
41
|
+
return f"{cls.__module__}.{cls.__qualname__}"
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _register(cls: type[SkillsIntegration]) -> None:
|
|
45
|
+
"""Register one integration class under its declared ``cls.key``.
|
|
46
|
+
|
|
47
|
+
Re-registering the same class is a no-op (FR-002 idempotency under
|
|
48
|
+
``importlib.reload``). Registering a *different* class under an
|
|
49
|
+
existing key raises ``DuplicateRegistrationError`` naming both
|
|
50
|
+
classes (FR-005, research R5). Registering a class whose ``key``
|
|
51
|
+
is empty (the base-class sentinel) raises ``InvalidIntegrationError``
|
|
52
|
+
(R13) — subclasses MUST override ``key`` with a non-empty string.
|
|
53
|
+
Registering a class whose ``default_skills_dir`` is empty raises
|
|
54
|
+
``InvalidIntegrationError`` (R25) for the same reason: an empty
|
|
55
|
+
default would surface as a misleading ``resolves_to_project_root``
|
|
56
|
+
error in ``setup()`` because ``Path("")`` collapses to ``Path(".")``.
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
new_fqcn = _fqcn(cls)
|
|
60
|
+
if not cls.key:
|
|
61
|
+
raise InvalidIntegrationError(rule="empty_key", value=new_fqcn)
|
|
62
|
+
if not cls.default_skills_dir:
|
|
63
|
+
# R25 — symmetric guard to R13. Without this, a forgetful subclass
|
|
64
|
+
# that overrides `key` but leaves `default_skills_dir` at the
|
|
65
|
+
# base sentinel ("") would register successfully and then fail
|
|
66
|
+
# at setup() time with `resolves_to_project_root` (because
|
|
67
|
+
# `Path("") == Path(".")` collapses to project_root), pointing
|
|
68
|
+
# the author at the wrong layer.
|
|
69
|
+
raise InvalidIntegrationError(rule="empty_default_skills_dir", value=new_fqcn)
|
|
70
|
+
|
|
71
|
+
existing = INTEGRATION_REGISTRY.get(cls.key)
|
|
72
|
+
if existing is None:
|
|
73
|
+
INTEGRATION_REGISTRY[cls.key] = cls
|
|
74
|
+
return
|
|
75
|
+
|
|
76
|
+
# R12 — compare by fully-qualified class name, not by identity. After
|
|
77
|
+
# `importlib.reload(bookwright.integrations.claude)` the reloaded
|
|
78
|
+
# `ClaudeIntegration` is a NEW class object with different identity
|
|
79
|
+
# from the one already in the registry; relying on `is` would raise
|
|
80
|
+
# DuplicateRegistrationError spuriously, breaking the FR-002 reload
|
|
81
|
+
# idempotency promise. FQCN equality preserves the duplicate guard
|
|
82
|
+
# for genuinely different classes (different module/qualname) that
|
|
83
|
+
# collide on `key`.
|
|
84
|
+
# R27 — compute existing FQCN once and reuse for both the equality
|
|
85
|
+
# check and the error payload, instead of recomputing on each call
|
|
86
|
+
# site.
|
|
87
|
+
existing_fqcn = _fqcn(existing)
|
|
88
|
+
if existing is cls or existing_fqcn == new_fqcn:
|
|
89
|
+
INTEGRATION_REGISTRY[cls.key] = cls # rebind to the reloaded class
|
|
90
|
+
return
|
|
91
|
+
raise DuplicateRegistrationError(
|
|
92
|
+
value=cls.key,
|
|
93
|
+
existing=existing_fqcn,
|
|
94
|
+
new=new_fqcn,
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _register_builtins() -> None:
|
|
99
|
+
"""Populate ``INTEGRATION_REGISTRY`` with the two v0 built-ins.
|
|
100
|
+
|
|
101
|
+
Future contributors add a single ``_register(NewIntegration)`` line
|
|
102
|
+
here (per quickstart "Adding a new integration"). No edit to
|
|
103
|
+
``base.py``, ``claude/``, or ``generic/`` is permitted — this is
|
|
104
|
+
enforced mechanically by ``tests/integrations/test_plugin_contract.py``
|
|
105
|
+
(FR-031).
|
|
106
|
+
"""
|
|
107
|
+
|
|
108
|
+
_register(ClaudeIntegration)
|
|
109
|
+
_register(GenericIntegration)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def get(key: str) -> type[SkillsIntegration]:
|
|
113
|
+
"""Look up an integration class by its short key (FR-003)."""
|
|
114
|
+
|
|
115
|
+
cls = INTEGRATION_REGISTRY.get(key)
|
|
116
|
+
if cls is None:
|
|
117
|
+
raise UnknownIntegrationError(value=key, valid=list_keys())
|
|
118
|
+
return cls
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def list_keys() -> list[str]:
|
|
122
|
+
"""Return registered integration keys, alphabetically sorted (FR-004)."""
|
|
123
|
+
|
|
124
|
+
return sorted(INTEGRATION_REGISTRY.keys())
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
_register_builtins()
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
__all__ = [
|
|
131
|
+
# NOTE: `INTEGRATION_REGISTRY` is intentionally NOT exposed in __all__
|
|
132
|
+
# (R17). The dict remains importable from the module namespace for
|
|
133
|
+
# the in-tree test snapshot fixture and the registry-mutation guard
|
|
134
|
+
# tests, but external consumers MUST go through `get`, `list_keys`,
|
|
135
|
+
# and `_register` — direct dict assignment bypasses FR-005's
|
|
136
|
+
# duplicate-detection guard and the R13 empty-key guard.
|
|
137
|
+
"SKILL_DESCRIPTION_MAX_LENGTH",
|
|
138
|
+
"SKILL_NAME_MAX_LENGTH",
|
|
139
|
+
"SKILL_PLACEHOLDER_MARKER_NAME",
|
|
140
|
+
"ClaudeIntegration",
|
|
141
|
+
"DuplicateRegistrationError",
|
|
142
|
+
"GenericIntegration",
|
|
143
|
+
"IntegrationOption",
|
|
144
|
+
"InvalidIntegrationError",
|
|
145
|
+
"InvalidOptionDeclarationError",
|
|
146
|
+
"MalformedOptionError",
|
|
147
|
+
"SkillLintError",
|
|
148
|
+
"SkillMaterializationError",
|
|
149
|
+
"SkillsIntegration",
|
|
150
|
+
"UnknownIntegrationError",
|
|
151
|
+
"UnknownOptionError",
|
|
152
|
+
"get",
|
|
153
|
+
"list_keys",
|
|
154
|
+
"parse_options",
|
|
155
|
+
]
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"""``SkillsIntegration`` — the single operative v0 base class for integrations.
|
|
2
|
+
|
|
3
|
+
Subclasses live under ``bookwright.integrations.<key>/`` and override:
|
|
4
|
+
- ``key``, ``config``, ``default_skills_dir`` (always),
|
|
5
|
+
- ``supports_*`` flags (when the agent supports the capability),
|
|
6
|
+
- ``options()`` (when the integration declares ``--integration-options``
|
|
7
|
+
flags), and
|
|
8
|
+
- ``resolve_skills_dir()`` (when the resolved dir depends on
|
|
9
|
+
``parsed_options``).
|
|
10
|
+
|
|
11
|
+
``setup()`` is implemented once here; no v0 subclass overrides it.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
from collections.abc import Mapping
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
from types import MappingProxyType
|
|
19
|
+
from typing import TYPE_CHECKING, ClassVar
|
|
20
|
+
|
|
21
|
+
from bookwright.integrations.errors import MalformedOptionError
|
|
22
|
+
from bookwright.integrations.materialize import generate_skill_md, iter_command_sources
|
|
23
|
+
from bookwright.integrations.options import IntegrationOption
|
|
24
|
+
from bookwright.io.fs import NullLedger, mkdir_tracked
|
|
25
|
+
|
|
26
|
+
if TYPE_CHECKING:
|
|
27
|
+
from bookwright.core.manifest import Manifest
|
|
28
|
+
from bookwright.io.fs import FileLedger
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class SkillsIntegration:
|
|
32
|
+
"""Base contract every Bookwright v0 integration implements."""
|
|
33
|
+
|
|
34
|
+
# Sentinel defaults — every concrete subclass MUST override `key`,
|
|
35
|
+
# `config`, and `default_skills_dir`. The empty-string sentinel on
|
|
36
|
+
# `key` is caught by `_register` (R13). `config`'s default is a
|
|
37
|
+
# frozen `MappingProxyType` (R19) so a forgetful subclass that
|
|
38
|
+
# accidentally writes `cls.config['x'] = 'y'` raises TypeError
|
|
39
|
+
# instead of silently mutating the shared base dict and polluting
|
|
40
|
+
# every other forgetful subclass.
|
|
41
|
+
key: ClassVar[str] = ""
|
|
42
|
+
config: ClassVar[Mapping[str, str | bool]] = MappingProxyType({})
|
|
43
|
+
default_skills_dir: ClassVar[str] = ""
|
|
44
|
+
|
|
45
|
+
supports_dynamic_context: ClassVar[bool] = False
|
|
46
|
+
supports_subagents: ClassVar[bool] = False
|
|
47
|
+
supports_tool_restrictions: ClassVar[bool] = False
|
|
48
|
+
|
|
49
|
+
@classmethod
|
|
50
|
+
def options(cls) -> list[IntegrationOption]:
|
|
51
|
+
"""Return the integration's declared ``--integration-options`` flags.
|
|
52
|
+
|
|
53
|
+
Default: empty list (FR-013). Override to declare flags.
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
return []
|
|
57
|
+
|
|
58
|
+
def resolve_skills_dir(
|
|
59
|
+
self,
|
|
60
|
+
parsed_options: Mapping[str, object] | None = None,
|
|
61
|
+
) -> Path:
|
|
62
|
+
"""Return the project-relative skills directory for this integration.
|
|
63
|
+
|
|
64
|
+
Default implementation returns ``Path(self.default_skills_dir)`` and
|
|
65
|
+
ignores ``parsed_options`` (FR-023). Subclasses that declare options
|
|
66
|
+
affecting the resolved directory (e.g., ``GenericIntegration``'s
|
|
67
|
+
``--skills-dir``) MUST override.
|
|
68
|
+
|
|
69
|
+
``Mapping`` (not ``dict``) is used in the signature so callers may
|
|
70
|
+
pass the narrower ``dict[str, str | bool]`` returned by
|
|
71
|
+
``parse_options`` without an explicit cast (``dict`` is invariant).
|
|
72
|
+
"""
|
|
73
|
+
|
|
74
|
+
del parsed_options
|
|
75
|
+
return Path(self.default_skills_dir)
|
|
76
|
+
|
|
77
|
+
def setup(
|
|
78
|
+
self,
|
|
79
|
+
project_root: Path,
|
|
80
|
+
manifest: Manifest,
|
|
81
|
+
parsed_options: Mapping[str, object] | None = None,
|
|
82
|
+
*,
|
|
83
|
+
ledger: FileLedger | None = None,
|
|
84
|
+
) -> None:
|
|
85
|
+
"""Materialize one ``SKILL.md`` per source command under the resolved dir.
|
|
86
|
+
|
|
87
|
+
Shared by every v0 integration (no subclass overrides it — the only
|
|
88
|
+
per-integration variation is already behind ``resolve_skills_dir`` and
|
|
89
|
+
the capability flags). For each packaged source command, delegate to
|
|
90
|
+
``generate_skill_md``; idempotent per-``SKILL.md`` (FR-014); never writes
|
|
91
|
+
outside the resolved dir (FR-017).
|
|
92
|
+
|
|
93
|
+
``ledger`` is the rollback-recording ``FileLedger`` (``init`` passes its
|
|
94
|
+
live ``BackupLedger``); when omitted it defaults to a ``NullLedger`` so
|
|
95
|
+
``setup()`` is standalone-callable. Every materialized path is recorded
|
|
96
|
+
through it (FR-019). A ``SkillLintError``/``SkillMaterializationError``
|
|
97
|
+
from any command propagates, aborting this integration (FR-016).
|
|
98
|
+
"""
|
|
99
|
+
|
|
100
|
+
# `manifest` is part of the iteration-9 contract; unused in v0 body.
|
|
101
|
+
del manifest
|
|
102
|
+
|
|
103
|
+
ledger = ledger or NullLedger()
|
|
104
|
+
resolved = self.resolve_skills_dir(parsed_options)
|
|
105
|
+
target = (project_root / resolved).resolve()
|
|
106
|
+
root = project_root.resolve()
|
|
107
|
+
if target == root:
|
|
108
|
+
# `--skills-dir=`, `--skills-dir .`, `--skills-dir ./`, etc. all
|
|
109
|
+
# collapse the target into project_root itself. Rejected as a
|
|
110
|
+
# separate rule from `escapes_project_root` so the JSON envelope
|
|
111
|
+
# can distinguish "lands AT root" from "lands OUTSIDE root" (R6).
|
|
112
|
+
raise MalformedOptionError(rule="resolves_to_project_root", value=str(resolved))
|
|
113
|
+
if not target.is_relative_to(root):
|
|
114
|
+
raise MalformedOptionError(rule="escapes_project_root", value=str(resolved))
|
|
115
|
+
mkdir_tracked(target, ledger)
|
|
116
|
+
for command_path in iter_command_sources():
|
|
117
|
+
generate_skill_md(command_path, target, self, ledger=ledger)
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""``ClaudeIntegration`` — Claude Code integration (FR-007, FR-010, FR-013)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import ClassVar
|
|
6
|
+
|
|
7
|
+
from bookwright.integrations.base import SkillsIntegration
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ClaudeIntegration(SkillsIntegration):
|
|
11
|
+
"""Integration for the Claude Code agent (.claude/skills layout).
|
|
12
|
+
|
|
13
|
+
Inherits the base ``setup()`` body and the base ``resolve_skills_dir``
|
|
14
|
+
(which already returns ``Path(default_skills_dir)``); no overrides are
|
|
15
|
+
necessary in v0.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
key: ClassVar[str] = "claude"
|
|
19
|
+
default_skills_dir: ClassVar[str] = ".claude/skills"
|
|
20
|
+
config: ClassVar[dict[str, str | bool]] = {
|
|
21
|
+
"name": "Claude Code",
|
|
22
|
+
"install_url": "https://docs.claude.com/claude-code",
|
|
23
|
+
"requires_cli": True,
|
|
24
|
+
"context_file": "CLAUDE.md",
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
supports_dynamic_context: ClassVar[bool] = True
|
|
28
|
+
supports_subagents: ClassVar[bool] = True
|
|
29
|
+
supports_tool_restrictions: ClassVar[bool] = True
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""Agent Skills (agentskills.io) compliance constants for the integrations layer.
|
|
2
|
+
|
|
3
|
+
Single source of truth for the numeric caps, the inherited default license,
|
|
4
|
+
and the dynamic-context injection allowlist consumed by iteration 9's
|
|
5
|
+
``SKILL.md`` materializer and linter (FR-003, FR-005, FR-013, FR-015).
|
|
6
|
+
|
|
7
|
+
Structural invariant from FR-003 — *the directory name MUST equal the
|
|
8
|
+
``name`` field inside its ``SKILL.md``* — is not numeric and therefore is not
|
|
9
|
+
encoded as a constant; it is enforced by iteration 9's materializer/linter and
|
|
10
|
+
documented here so future readers do not duplicate the rule.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from typing import Final
|
|
16
|
+
|
|
17
|
+
SKILL_NAME_MAX_LENGTH: Final[int] = 64
|
|
18
|
+
SKILL_DESCRIPTION_MAX_LENGTH: Final[int] = 1024
|
|
19
|
+
|
|
20
|
+
#: Tier-2 ``SKILL.md`` body budget (R6/FR-015). Bodies are copied unchanged from
|
|
21
|
+
#: already-budget-passing iteration-8 sources, so this is a regression guard.
|
|
22
|
+
SKILL_BODY_MAX_TOKENS: Final[int] = 5000
|
|
23
|
+
|
|
24
|
+
#: License inherited by a materialized skill when its source declares no
|
|
25
|
+
#: ``license`` (FR-005/A-002). The single source of truth for the design default.
|
|
26
|
+
DEFAULT_SKILL_LICENSE: Final[str] = "Apache-2.0"
|
|
27
|
+
|
|
28
|
+
#: Deny-by-default allowlist of project-file *read* commands permitted inside a
|
|
29
|
+
#: `` !`…` `` dynamic-context injection (the FR-013 invariant). File-read only —
|
|
30
|
+
#: ``ls``/``find`` (which *list*, not *read a file*) are deliberately excluded to
|
|
31
|
+
#: stay faithful to FR-013 ("reads a project file").
|
|
32
|
+
INJECTION_READ_COMMANDS: Final[frozenset[str]] = frozenset({"cat", "head", "tail"})
|
|
33
|
+
|
|
34
|
+
#: DEPRECATED (iteration 9): the iteration-3 placeholder-marker filename. The
|
|
35
|
+
#: marker is **no longer written** — ``setup()`` now materializes real
|
|
36
|
+
#: ``SKILL.md`` files. Retained only so the deprecated symbol stays importable
|
|
37
|
+
#: for the legacy-cleanup tests; do not write this file in new code.
|
|
38
|
+
SKILL_PLACEHOLDER_MARKER_NAME: Final[str] = ".bookwright-skills-placeholder"
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""Authoritative `SKILL.md` `description` table (R1/R3, FR-004).
|
|
2
|
+
|
|
3
|
+
A pure, dependency-free data module: `SKILL_DESCRIPTIONS` maps each command name
|
|
4
|
+
to its bilingual ES/EN trigger-bearing description, and `get_description` is the
|
|
5
|
+
single authoritative lookup (with source-frontmatter fallback) where the
|
|
6
|
+
1024-char cap is asserted in **one** place.
|
|
7
|
+
|
|
8
|
+
In v0 the table mirrors the iteration-8 source frontmatter `description` verbatim
|
|
9
|
+
under a CI equality gate (SC-009): the dict is the authoritative read seam (where
|
|
10
|
+
the cap lives and where future per-skill tuning would land), while the gate makes
|
|
11
|
+
any divergence from the source an explicit, reviewed change — never silent drift.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
from bookwright.integrations.constants import SKILL_DESCRIPTION_MAX_LENGTH
|
|
17
|
+
|
|
18
|
+
SKILL_DESCRIPTIONS: dict[str, str] = {
|
|
19
|
+
"bookwright-constitution": 'Define la constitución narrativa del libro: voz, tono, pacto con el lector, líneas rojas e invariantes de coherencia — el paso de configuración que va ANTES de la biblia. Build the book\'s narrative constitution: voice, tone, reader pact, red lines and coherence invariants — the setup step BEFORE the bible. Úsalo cuando el autor quiera fijar el tono, la voz o las reglas de su obra ("define el tono", "set the tone/voice", "establece las bases"). NO genera fichas de personajes ni localizaciones: eso es bookwright-bible, que va después.',
|
|
20
|
+
"bookwright-bible": 'Genera la biblia del proyecto en una sola pasada: fichas de personajes, escenarios y localizaciones, cronología, relaciones, temas, glosario y subtramas — DESPUÉS de tener la constitución. Build the project bible in a single pass: character, setting and location sheets, timeline, relationships, themes, glossary and subplots — AFTER the constitution exists. Úsalo cuando el autor pida "fichas de mis personajes y localizaciones" / "character and location sheets", "puebla la biblia" / "build the bible". NO sirve para definir el tono o la voz: eso es bookwright-constitution, que va antes.',
|
|
21
|
+
"bookwright-outline": 'Construye el esqueleto narrativo de la obra: arcos de personaje, estructura por actos y capítulos, y una sinopsis inicial, a partir de la constitución y la biblia. Build the book\'s narrative skeleton: character arcs, act/chapter structure and an initial synopsis, from the constitution and bible. Úsalo cuando el autor quiera "estructurar la trama", "diseñar los arcos" / "outline the plot", "design the arcs and structure". Trabaja al nivel de capítulos y arcos, no de escenas concretas (eso es bookwright-scenes).',
|
|
22
|
+
"bookwright-scenes": 'Desglosa la estructura en una lista de escenas concretas, cada una con su función narrativa, personajes presentes, lugar y beats. Break the structure into a concrete scene list, each carrying its narrative function, characters present, location and beats. Úsalo cuando el autor quiera "desglosar los capítulos en escenas", "preparar la lista de escenas" / "break chapters into scenes", "plan the scene list" antes de redactar. Planifica escenas; NO redacta su prosa (eso es bookwright-draft).',
|
|
23
|
+
"bookwright-draft": 'Redacta la prosa de una escena concreta (indicada por su scene_id) en el capítulo correcto del manuscrito, respetando la voz, la focalización y las restricciones de la constitución y la biblia. Draft the prose of a specific scene (given by its scene_id) into the correct manuscript chapter, honoring the voice, focalization and constraints from the constitution and bible. Úsalo cuando el autor diga "escribe/redacta la escena X" / "draft/write scene X". Es el único comando que produce prosa de manuscrito.',
|
|
24
|
+
"bookwright-synopsis": 'Actualiza la sinopsis del proyecto: una versión corta (250–350 palabras) y una larga (1000–2000 palabras) que reflejan el estado actual de la trama. Update the project synopsis: a short version (250–350 words) and a long one (1000–2000 words) reflecting the current state of the plot. Úsalo cuando el autor pida "actualiza/genera la sinopsis", "resume la novela" / "update/write the synopsis", "summarize the plot". Regenera ambos resúmenes en cualquier momento del proyecto.',
|
|
25
|
+
"bookwright-clarify": 'Revisa los artefactos del proyecto y devuelve una lista de preguntas abiertas que el autor debería resolver antes de seguir. Review the project artifacts and return a list of open questions the author should resolve before continuing. Úsalo cuando el autor pregunte "¿qué me falta por aclarar antes de seguir?", "¿qué dudas quedan?" / "what\'s still unclear?", "what do I need to decide next?". Es de solo lectura. Pregunta por DUDAS abiertas, NO comprueba la completitud de un artefacto concreto (eso es bookwright-checklist).',
|
|
26
|
+
"bookwright-analyze": 'Revisa la consistencia cruzada PRE-redacción entre constitución, biblia, outline y escenas, y reporta contradicciones antes de empezar a escribir. Check PRE-draft cross-artifact consistency among constitution, bible, outline and scenes, reporting contradictions before any prose is written. Úsalo cuando el autor pregunte "¿es coherente mi planificación antes de redactar?" / "is my planning consistent before I start drafting?". Es de solo lectura y trabaja en fase PRE-draft. NO compara el manuscrito con la biblia (eso es post-draft: bookwright-continuity).',
|
|
27
|
+
"bookwright-continuity": 'Revisa la consistencia POST-redacción del manuscrito frente a la biblia: cumplimiento de la biblia, coherencia de los arcos de personaje y de la línea de tiempo. Check POST-draft continuity of the manuscript against the bible: bible compliance, character-arc consistency and timeline coherence. Úsalo cuando el autor pida "revisa si mi manuscrito es coherente con la biblia" / "check my manuscript against the bible". Es de solo lectura y trabaja en fase POST-draft. NO revisa la planificación antes de redactar (eso es pre-draft: bookwright-analyze).',
|
|
28
|
+
"bookwright-checklist": 'Comprueba si UN artefacto concreto está completo: todas sus secciones presentes, sin marcadores [PENDING: …] sin resolver y sin placeholders vacíos. Check whether ONE named artifact is complete: all sections present, no unresolved [PENDING: …] markers, no empty placeholders. Úsalo cuando el autor pregunte "¿está completa mi constitución / esta ficha?" / "is this artifact complete?". Es de solo lectura. Mide COMPLETITUD de un artefacto, NO recoge las dudas abiertas del proyecto (eso es bookwright-clarify).',
|
|
29
|
+
"bookwright-research": 'Investiga un tema del mundo real y lo documenta como hallazgos con procedencia completa (fuentes, citas en lengua original, fiabilidad) en bible/research/, marcando qué hallazgos son anclas que restringen la ficción. Research a real-world topic and document it as findings with full provenance (sources, original-language quotes, reliability) under bible/research/, marking which findings are binding anchors on the fiction. Úsalo cuando el autor pida "investiga <tema>", "documenta <tema> con fuentes", "preséntame fuentes sobre <tema>" / "research <topic>", "find sources on <topic>". NO verifica prosa ya escrita contra sus fuentes (eso es bookwright-verify, posterior) ni puebla fichas de personajes o localizaciones (eso es bookwright-bible).',
|
|
30
|
+
"bookwright-verify": 'Verifica el manuscrito ya redactado contra las anclas de investigación: detecta pasajes que contradicen lo investigado — anacronismos, errores de procedimiento (algo ilegal o imposible en la ambientación) e inexactitudes culturales o lingüísticas. Verify the drafted manuscript against the research anchors: flag passages that contradict the research — anachronisms, procedural errors (something illegal or impossible in the setting) and cultural or linguistic inaccuracies. Úsalo cuando el autor pida "verifica si mi manuscrito contradice lo investigado" / "check my manuscript against my research". Es de solo lectura y trabaja en fase POST-draft. NO compara el manuscrito con la biblia (eso es bookwright-continuity) ni audita la integridad estructural de las anclas (eso es el validator factual_anchor).',
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def get_description(name: str, fallback: str) -> str:
|
|
35
|
+
"""Authoritative lookup with source-frontmatter fallback (R3, FR-004).
|
|
36
|
+
|
|
37
|
+
Returns `SKILL_DESCRIPTIONS[name]` when present, else `fallback` (the source
|
|
38
|
+
frontmatter description). The 1024-char cap is *enforced* at runtime by
|
|
39
|
+
``lint_skill_md`` Rule 3 (which survives ``python -O``); the ``assert`` below
|
|
40
|
+
is only a developer-time tripwire on the static table — over-cap is a coding
|
|
41
|
+
error caught by tests (the SC-009 equality gate), never a user-data case.
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
result = SKILL_DESCRIPTIONS.get(name, fallback)
|
|
45
|
+
assert len(result) < SKILL_DESCRIPTION_MAX_LENGTH, (
|
|
46
|
+
f"description for {name!r} is {len(result)} chars, >= cap {SKILL_DESCRIPTION_MAX_LENGTH}"
|
|
47
|
+
)
|
|
48
|
+
return result
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
"""Structured exception family for the integrations layer (FR-035, FR-036).
|
|
2
|
+
|
|
3
|
+
Every public error inherits from the private `_IntegrationError` base — which now
|
|
4
|
+
inherits the shared `BookwrightError` — and exposes:
|
|
5
|
+
- a class-level ``code: str`` (immutable identifier),
|
|
6
|
+
- a ``message: str`` attribute (human-readable, also passed to
|
|
7
|
+
``Exception.__init__``),
|
|
8
|
+
- the structured fields under the canonical envelope's ``details`` (the
|
|
9
|
+
single ``to_json()`` is owned by `BookwrightError`; see
|
|
10
|
+
``specs/018-unified-error-envelope/contracts/error-envelope.md``).
|
|
11
|
+
|
|
12
|
+
The ``init --json`` / ``integration use --json`` consumers read the canonical
|
|
13
|
+
``to_json()`` body; renaming any ``details`` field below is a breaking change.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
from bookwright.errors import BookwrightError
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class _IntegrationError(BookwrightError):
|
|
22
|
+
"""Private base for all structured errors raised by the integrations layer.
|
|
23
|
+
|
|
24
|
+
Subclasses MUST set a non-empty class-level ``code`` and assign their
|
|
25
|
+
structured fields on ``self`` in ``__init__`` (e.g., ``self.rule``,
|
|
26
|
+
``self.value``), then end ``__init__`` with
|
|
27
|
+
``super().__init__(message, {<public attrs>})`` so the inherited
|
|
28
|
+
``BookwrightError.to_json()`` emits the canonical envelope. The base is
|
|
29
|
+
abstract: it declares no ``code`` and is never serialized directly.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class UnknownIntegrationError(_IntegrationError):
|
|
34
|
+
"""Raised by ``get(key)`` when ``key`` is not in ``INTEGRATION_REGISTRY``."""
|
|
35
|
+
|
|
36
|
+
code = "unknown_integration"
|
|
37
|
+
|
|
38
|
+
def __init__(self, *, value: str | None, valid: list[str]) -> None:
|
|
39
|
+
self.value = value
|
|
40
|
+
self.valid = list(valid)
|
|
41
|
+
message = f"unknown integration: {value!r}; valid: [{', '.join(self.valid)}]"
|
|
42
|
+
super().__init__(message, {"value": value, "valid": self.valid})
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class UnknownOptionError(_IntegrationError):
|
|
46
|
+
"""Raised by ``parse_options`` for a flag the integration does not declare."""
|
|
47
|
+
|
|
48
|
+
code = "unknown_option"
|
|
49
|
+
|
|
50
|
+
def __init__(self, *, integration: str, value: str, valid: list[str]) -> None:
|
|
51
|
+
self.integration = integration
|
|
52
|
+
self.value = value
|
|
53
|
+
self.valid = list(valid)
|
|
54
|
+
message = (
|
|
55
|
+
f"unknown option {value} for integration {integration!r}; "
|
|
56
|
+
f"valid: [{', '.join(self.valid)}]"
|
|
57
|
+
)
|
|
58
|
+
super().__init__(
|
|
59
|
+
message,
|
|
60
|
+
{"integration": integration, "value": value, "valid": self.valid},
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class MalformedOptionError(_IntegrationError):
|
|
65
|
+
"""Raised by ``parse_options`` on a structural rule violation in user input."""
|
|
66
|
+
|
|
67
|
+
code = "malformed_option"
|
|
68
|
+
|
|
69
|
+
def __init__(self, *, rule: str, value: str) -> None:
|
|
70
|
+
self.rule = rule
|
|
71
|
+
self.value = value
|
|
72
|
+
message = f"malformed option {value!r}: {rule}"
|
|
73
|
+
super().__init__(message, {"rule": rule, "value": value})
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class DuplicateRegistrationError(_IntegrationError):
|
|
77
|
+
"""Raised by ``_register`` when a *different* class is bound to an existing key."""
|
|
78
|
+
|
|
79
|
+
code = "duplicate_registration"
|
|
80
|
+
|
|
81
|
+
def __init__(self, *, value: str, existing: str, new: str) -> None:
|
|
82
|
+
self.value = value
|
|
83
|
+
self.existing = existing
|
|
84
|
+
self.new = new
|
|
85
|
+
message = (
|
|
86
|
+
f"duplicate integration registration for key {value!r}: "
|
|
87
|
+
f"already registered as {existing}, refusing to replace with {new}"
|
|
88
|
+
)
|
|
89
|
+
super().__init__(message, {"value": value, "existing": existing, "new": new})
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class InvalidOptionDeclarationError(_IntegrationError):
|
|
93
|
+
"""Raised when an ``IntegrationOption`` descriptor itself is malformed.
|
|
94
|
+
|
|
95
|
+
Programming-error guard for FR-015: surfaced the first time
|
|
96
|
+
``parse_options`` introspects an integration's ``options()``. Never
|
|
97
|
+
user-facing — the offending integration class needs to be fixed.
|
|
98
|
+
"""
|
|
99
|
+
|
|
100
|
+
code = "invalid_option_declaration"
|
|
101
|
+
|
|
102
|
+
def __init__(self, *, rule: str, value: str) -> None:
|
|
103
|
+
self.rule = rule
|
|
104
|
+
self.value = value
|
|
105
|
+
message = (
|
|
106
|
+
f"invalid option declaration ({rule}): {value!r}; "
|
|
107
|
+
"this is a programming error in the integration's options()"
|
|
108
|
+
)
|
|
109
|
+
super().__init__(message, {"rule": rule, "value": value})
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
class InvalidIntegrationError(_IntegrationError):
|
|
113
|
+
"""Raised by ``_register`` when an integration class is malformed.
|
|
114
|
+
|
|
115
|
+
Programming-error guard (R13): catches integrations that forgot to
|
|
116
|
+
override the base ``SkillsIntegration`` sentinel defaults
|
|
117
|
+
(e.g., ``cls.key = ""``). Never user-facing — the offending
|
|
118
|
+
integration class needs to be fixed.
|
|
119
|
+
"""
|
|
120
|
+
|
|
121
|
+
code = "invalid_integration"
|
|
122
|
+
|
|
123
|
+
def __init__(self, *, rule: str, value: str) -> None:
|
|
124
|
+
self.rule = rule
|
|
125
|
+
self.value = value
|
|
126
|
+
message = (
|
|
127
|
+
f"invalid integration ({rule}): {value!r}; "
|
|
128
|
+
"this is a programming error in the integration class"
|
|
129
|
+
)
|
|
130
|
+
super().__init__(message, {"rule": rule, "value": value})
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
class SkillLintError(_IntegrationError):
|
|
134
|
+
"""Raised by ``lint_skill_md`` on the first agentskills.io violation (FR-015).
|
|
135
|
+
|
|
136
|
+
Post-write: a freshly generated skill is linted right after it is written, and
|
|
137
|
+
``generate_skill_md`` deletes the offending skill dir before this error escapes
|
|
138
|
+
(FR-016 — "no invalid SKILL.md on disk"). Note that ``generate_skill_md`` does
|
|
139
|
+
NOT re-lint a *pre-existing* ``SKILL.md``: idempotency (FR-014) skips it
|
|
140
|
+
untouched, so re-validation of hand-edited skills is the job of the standalone
|
|
141
|
+
``lint_skill_md`` call (and the iteration-11 validation system that reuses it),
|
|
142
|
+
not of ``setup()``.
|
|
143
|
+
"""
|
|
144
|
+
|
|
145
|
+
code = "skill_lint_failed"
|
|
146
|
+
|
|
147
|
+
def __init__(self, *, skill: str, rule: str, detail: str) -> None:
|
|
148
|
+
self.skill = skill
|
|
149
|
+
self.rule = rule
|
|
150
|
+
self.detail = detail
|
|
151
|
+
message = f"skill {skill!r} failed lint rule {rule!r}: {detail}"
|
|
152
|
+
super().__init__(message, {"skill": skill, "rule": rule, "detail": detail})
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
class SkillMaterializationError(_IntegrationError):
|
|
156
|
+
"""Raised by ``generate_skill_md`` on a pre-write authoring error (FR-010/FR-020).
|
|
157
|
+
|
|
158
|
+
``rule`` ∈ {``dangling_reference``, ``name_frontmatter_mismatch``,
|
|
159
|
+
``residual_token``}. All are detected *before* the first filesystem write, so a
|
|
160
|
+
rejected source leaves **zero** on-disk state (nothing to clean up).
|
|
161
|
+
"""
|
|
162
|
+
|
|
163
|
+
code = "skill_materialization_failed"
|
|
164
|
+
|
|
165
|
+
def __init__(self, *, skill: str, rule: str, detail: str) -> None:
|
|
166
|
+
self.skill = skill
|
|
167
|
+
self.rule = rule
|
|
168
|
+
self.detail = detail
|
|
169
|
+
message = f"skill {skill!r} materialization failed ({rule}): {detail}"
|
|
170
|
+
super().__init__(message, {"skill": skill, "rule": rule, "detail": detail})
|