sourcebound 1.2.1__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 (71) hide show
  1. clean_docs/__init__.py +26 -0
  2. clean_docs/__main__.py +3 -0
  3. clean_docs/accessibility.py +182 -0
  4. clean_docs/adapters/__init__.py +1 -0
  5. clean_docs/adapters/event_capture.py +56 -0
  6. clean_docs/adapters/mdx_dependencies.json +714 -0
  7. clean_docs/adapters/mdx_parser.mjs +29992 -0
  8. clean_docs/applicability.py +330 -0
  9. clean_docs/audit.py +1120 -0
  10. clean_docs/bootstrap.py +507 -0
  11. clean_docs/capabilities.py +200 -0
  12. clean_docs/changed.py +452 -0
  13. clean_docs/claims.py +840 -0
  14. clean_docs/cli.py +1612 -0
  15. clean_docs/context.py +307 -0
  16. clean_docs/corpus.py +377 -0
  17. clean_docs/demo.py +369 -0
  18. clean_docs/doctor.py +184 -0
  19. clean_docs/emit/__init__.py +4 -0
  20. clean_docs/emit/llms_txt.py +102 -0
  21. clean_docs/emit/stepwise.py +168 -0
  22. clean_docs/engine.py +324 -0
  23. clean_docs/errors.py +20 -0
  24. clean_docs/evaluation.py +867 -0
  25. clean_docs/execution.py +138 -0
  26. clean_docs/explain.py +123 -0
  27. clean_docs/extractors/__init__.py +19 -0
  28. clean_docs/extractors/command.py +51 -0
  29. clean_docs/extractors/inventory.py +176 -0
  30. clean_docs/extractors/json_pointer.py +84 -0
  31. clean_docs/extractors/python_literal.py +104 -0
  32. clean_docs/extractors/static.py +111 -0
  33. clean_docs/feedback.py +1390 -0
  34. clean_docs/impact.py +1624 -0
  35. clean_docs/improvements.py +1178 -0
  36. clean_docs/inventory.py +474 -0
  37. clean_docs/isolation.py +157 -0
  38. clean_docs/manifest.py +898 -0
  39. clean_docs/mdx.py +272 -0
  40. clean_docs/migration.py +121 -0
  41. clean_docs/models.py +194 -0
  42. clean_docs/outcomes.py +296 -0
  43. clean_docs/performance.py +123 -0
  44. clean_docs/phrasing.py +448 -0
  45. clean_docs/plugins.py +249 -0
  46. clean_docs/policy.py +536 -0
  47. clean_docs/projections.py +255 -0
  48. clean_docs/regions.py +75 -0
  49. clean_docs/release.py +232 -0
  50. clean_docs/renderers.py +57 -0
  51. clean_docs/residue.py +311 -0
  52. clean_docs/review_contracts.py +862 -0
  53. clean_docs/review_ledger.py +297 -0
  54. clean_docs/review_limits.py +9 -0
  55. clean_docs/sensitivity.py +602 -0
  56. clean_docs/snapshot.py +212 -0
  57. clean_docs/standard.py +281 -0
  58. clean_docs/standards/default.json +309 -0
  59. clean_docs/standards/exemplars.md +87 -0
  60. clean_docs/standards/v0-migrations.json +26 -0
  61. clean_docs/symbols.py +47 -0
  62. clean_docs/templates.py +96 -0
  63. clean_docs/verdict.py +1397 -0
  64. clean_docs/visuals.py +346 -0
  65. clean_docs/write_gate.py +170 -0
  66. sourcebound-1.2.1.dist-info/LICENSE +21 -0
  67. sourcebound-1.2.1.dist-info/METADATA +109 -0
  68. sourcebound-1.2.1.dist-info/RECORD +71 -0
  69. sourcebound-1.2.1.dist-info/WHEEL +5 -0
  70. sourcebound-1.2.1.dist-info/entry_points.txt +2 -0
  71. sourcebound-1.2.1.dist-info/top_level.txt +1 -0
clean_docs/mdx.py ADDED
@@ -0,0 +1,272 @@
1
+ from __future__ import annotations
2
+
3
+ import hashlib
4
+ import json
5
+ import os
6
+ import re
7
+ import shutil
8
+ import subprocess
9
+ import tempfile
10
+ from collections.abc import Mapping
11
+ from dataclasses import dataclass
12
+ from importlib.resources import files
13
+ from pathlib import Path
14
+ from typing import Any
15
+
16
+
17
+ MDX_PARSE_SCHEMA = "sourcebound.mdx-parse.v1"
18
+ MDX_PARSE_BATCH_SCHEMA = "sourcebound.mdx-parse-batch.v1"
19
+ MDX_PARSE_REQUEST_SCHEMA = "sourcebound.mdx-parse-request.v1"
20
+ MDX_PARSER_ID = "@mdx-js/mdx@3.1.1"
21
+ MDX_CONTROL = re.compile(
22
+ r"^(?P<indent>\s*)\{/\*\s*(?P<body>sourcebound:.*?)\s*\*/\}\s*$"
23
+ )
24
+
25
+
26
+ class MdxParserError(RuntimeError):
27
+ """The first-party MDX parser could not establish a structural result."""
28
+
29
+
30
+ @dataclass(frozen=True)
31
+ class MdxLink:
32
+ line: int
33
+ column: int
34
+ url: str
35
+
36
+
37
+ @dataclass(frozen=True)
38
+ class MdxNode:
39
+ type: str
40
+ start_line: int
41
+ start_column: int
42
+ start_byte: int
43
+ end_line: int
44
+ end_column: int
45
+ end_byte: int
46
+ name: str | None = None
47
+ url: str | None = None
48
+ depth: int | None = None
49
+ text: str | None = None
50
+ language: str | None = None
51
+ meta: str | None = None
52
+ alt: str | None = None
53
+
54
+
55
+ @dataclass(frozen=True)
56
+ class MdxDocument:
57
+ digest: str
58
+ masked_text: str
59
+ links: tuple[MdxLink, ...]
60
+ nodes: tuple[MdxNode, ...]
61
+ parser: str = MDX_PARSER_ID
62
+
63
+ def policy_text(self, source: str) -> str:
64
+ source_lines = source.splitlines(keepends=True)
65
+ masked_lines = self.masked_text.splitlines(keepends=True)
66
+ if len(source_lines) != len(masked_lines):
67
+ raise MdxParserError("MDX parser changed the document line structure")
68
+ for index, line in enumerate(source_lines):
69
+ content = line.rstrip("\r\n")
70
+ ending = line[len(content):]
71
+ match = MDX_CONTROL.match(content)
72
+ if match:
73
+ masked_lines[index] = (
74
+ f"{match.group('indent')}<!-- {match.group('body').strip()} -->"
75
+ f"{ending}"
76
+ )
77
+ return "".join(masked_lines)
78
+
79
+
80
+ def parser_path() -> Path:
81
+ return Path(str(files("clean_docs.adapters").joinpath("mdx_parser.mjs")))
82
+
83
+
84
+ def parser_availability() -> tuple[bool, str]:
85
+ node = shutil.which("node")
86
+ if node is None:
87
+ return False, "Node.js executable not found"
88
+ try:
89
+ version_process = subprocess.run(
90
+ [node, "--version"],
91
+ text=True,
92
+ capture_output=True,
93
+ timeout=5,
94
+ check=False,
95
+ )
96
+ except (OSError, subprocess.SubprocessError) as exc:
97
+ return False, f"cannot inspect Node.js runtime: {exc}"
98
+ match = re.fullmatch(r"v(\d+)\.\d+\.\d+\s*", version_process.stdout)
99
+ if version_process.returncode != 0 or match is None:
100
+ return False, "Node.js runtime did not report a semantic version"
101
+ if int(match.group(1)) < 20:
102
+ return False, f"Node.js 20 or newer is required; found {version_process.stdout.strip()}"
103
+ bundled = parser_path()
104
+ if not bundled.is_file():
105
+ return False, "bundled MDX parser not found"
106
+ return True, f"{MDX_PARSER_ID} via {node}"
107
+
108
+
109
+ def _integer(value: Any, field: str, *, minimum: int = 0) -> int:
110
+ if isinstance(value, bool) or not isinstance(value, int) or value < minimum:
111
+ raise MdxParserError(f"MDX parser returned invalid {field}")
112
+ return value
113
+
114
+
115
+ def _string(value: Any, field: str) -> str:
116
+ if not isinstance(value, str):
117
+ raise MdxParserError(f"MDX parser returned invalid {field}")
118
+ return value
119
+
120
+
121
+ def _node(raw: Any) -> MdxNode:
122
+ if not isinstance(raw, dict):
123
+ raise MdxParserError("MDX parser returned an invalid node")
124
+ start = raw.get("start")
125
+ end = raw.get("end")
126
+ if not isinstance(start, dict) or not isinstance(end, dict):
127
+ raise MdxParserError("MDX parser returned a node without positions")
128
+ name = raw.get("name")
129
+ url = raw.get("url")
130
+ text = raw.get("text")
131
+ language = raw.get("language")
132
+ meta = raw.get("meta")
133
+ alt = raw.get("alt")
134
+ depth = raw.get("depth")
135
+ if name is not None and not isinstance(name, str):
136
+ raise MdxParserError("MDX parser returned an invalid node name")
137
+ if url is not None and not isinstance(url, str):
138
+ raise MdxParserError("MDX parser returned an invalid node URL")
139
+ if text is not None and not isinstance(text, str):
140
+ raise MdxParserError("MDX parser returned invalid heading text")
141
+ if language is not None and not isinstance(language, str):
142
+ raise MdxParserError("MDX parser returned invalid code language")
143
+ if meta is not None and not isinstance(meta, str):
144
+ raise MdxParserError("MDX parser returned invalid code metadata")
145
+ if alt is not None and not isinstance(alt, str):
146
+ raise MdxParserError("MDX parser returned invalid image alternative text")
147
+ if depth is not None:
148
+ depth = _integer(depth, "heading depth", minimum=1)
149
+ return MdxNode(
150
+ type=_string(raw.get("type"), "node type"),
151
+ start_line=_integer(start.get("line"), "start line", minimum=1),
152
+ start_column=_integer(start.get("column"), "start column", minimum=1),
153
+ start_byte=_integer(start.get("byte"), "start byte"),
154
+ end_line=_integer(end.get("line"), "end line", minimum=1),
155
+ end_column=_integer(end.get("column"), "end column", minimum=1),
156
+ end_byte=_integer(end.get("byte"), "end byte"),
157
+ name=name,
158
+ url=url,
159
+ depth=depth,
160
+ text=text,
161
+ language=language,
162
+ meta=meta,
163
+ alt=alt,
164
+ )
165
+
166
+
167
+ def _parse_payload(payload: Any, text: str) -> MdxDocument:
168
+ if not isinstance(payload, dict) or payload.get("schema") != MDX_PARSE_SCHEMA:
169
+ raise MdxParserError("MDX parser returned an unsupported document schema")
170
+ if payload.get("parser") != MDX_PARSER_ID:
171
+ raise MdxParserError("MDX parser identity does not match the packaged adapter")
172
+ digest = hashlib.sha256(text.encode("utf-8")).hexdigest()
173
+ if payload.get("digest") != digest:
174
+ raise MdxParserError("MDX parser input digest does not match")
175
+ masked = _string(payload.get("masked"), "masked document")
176
+ if masked.count("\n") != text.count("\n"):
177
+ raise MdxParserError("MDX parser changed the document line structure")
178
+ raw_links = payload.get("links")
179
+ raw_nodes = payload.get("nodes")
180
+ if not isinstance(raw_links, list) or not isinstance(raw_nodes, list):
181
+ raise MdxParserError("MDX parser omitted semantic results")
182
+ links: list[MdxLink] = []
183
+ for raw in raw_links:
184
+ if not isinstance(raw, dict):
185
+ raise MdxParserError("MDX parser returned an invalid link")
186
+ links.append(
187
+ MdxLink(
188
+ line=_integer(raw.get("line"), "link line", minimum=1),
189
+ column=_integer(raw.get("column"), "link column", minimum=1),
190
+ url=_string(raw.get("url"), "link URL"),
191
+ )
192
+ )
193
+ return MdxDocument(
194
+ digest=digest,
195
+ masked_text=masked,
196
+ links=tuple(links),
197
+ nodes=tuple(_node(raw) for raw in raw_nodes),
198
+ )
199
+
200
+
201
+ def parse_mdx_documents(
202
+ documents: Mapping[str, str],
203
+ ) -> tuple[dict[str, MdxDocument], dict[str, str]]:
204
+ available, detail = parser_availability()
205
+ if not available:
206
+ raise MdxParserError(detail)
207
+ environment = {
208
+ "LC_ALL": "C",
209
+ "PATH": os.environ.get("PATH", ""),
210
+ "TZ": "UTC",
211
+ }
212
+ with tempfile.TemporaryDirectory(prefix="sourcebound-mdx-") as working:
213
+ try:
214
+ process = subprocess.run(
215
+ ["node", "--no-warnings", str(parser_path())],
216
+ input=json.dumps(
217
+ {
218
+ "schema": MDX_PARSE_REQUEST_SCHEMA,
219
+ "documents": [
220
+ {"id": identifier, "text": text}
221
+ for identifier, text in sorted(documents.items())
222
+ ],
223
+ },
224
+ separators=(",", ":"),
225
+ ),
226
+ text=True,
227
+ capture_output=True,
228
+ cwd=working,
229
+ env=environment,
230
+ timeout=20,
231
+ check=False,
232
+ )
233
+ except (OSError, subprocess.SubprocessError) as exc:
234
+ raise MdxParserError(f"MDX parser failed: {exc}") from exc
235
+ if process.returncode != 0:
236
+ detail = process.stderr.strip() or "parser exited without a diagnostic"
237
+ raise MdxParserError(f"MDX parser failed: {detail}")
238
+ try:
239
+ payload = json.loads(process.stdout)
240
+ except json.JSONDecodeError as exc:
241
+ raise MdxParserError("MDX parser returned invalid JSON") from exc
242
+ if not isinstance(payload, dict) or payload.get("schema") != MDX_PARSE_BATCH_SCHEMA:
243
+ raise MdxParserError("MDX parser returned an unsupported batch schema")
244
+ raw_results = payload.get("documents")
245
+ if not isinstance(raw_results, list):
246
+ raise MdxParserError("MDX parser omitted batch results")
247
+ parsed: dict[str, MdxDocument] = {}
248
+ errors: dict[str, str] = {}
249
+ for raw in raw_results:
250
+ if not isinstance(raw, dict) or not isinstance(raw.get("id"), str):
251
+ raise MdxParserError("MDX parser returned an invalid batch item")
252
+ identifier = raw["id"]
253
+ if identifier not in documents or identifier in parsed or identifier in errors:
254
+ raise MdxParserError("MDX parser returned an unexpected batch item")
255
+ if raw.get("ok") is True:
256
+ parsed[identifier] = _parse_payload(raw.get("result"), documents[identifier])
257
+ elif raw.get("ok") is False and isinstance(raw.get("error"), str):
258
+ errors[identifier] = raw["error"]
259
+ else:
260
+ raise MdxParserError("MDX parser returned an invalid batch state")
261
+ if set(parsed) | set(errors) != set(documents):
262
+ raise MdxParserError("MDX parser did not account for every document")
263
+ return parsed, errors
264
+
265
+
266
+ def parse_mdx(text: str) -> MdxDocument:
267
+ parsed, errors = parse_mdx_documents({"document.mdx": text})
268
+ if errors:
269
+ raise MdxParserError(
270
+ f"MDX syntax is not structurally valid: {errors['document.mdx']}"
271
+ )
272
+ return parsed["document.mdx"]
@@ -0,0 +1,121 @@
1
+ """Migrate prior manifests with an exact backup and rollback path."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import difflib
6
+ import hashlib
7
+ from dataclasses import asdict, dataclass
8
+ from pathlib import Path
9
+ from typing import Any
10
+
11
+ import yaml
12
+
13
+ from clean_docs.errors import ConfigurationError
14
+ from clean_docs.regions import atomic_write
15
+
16
+
17
+ @dataclass(frozen=True)
18
+ class MigrationPlan:
19
+ source_version: int
20
+ target_version: int
21
+ original_sha256: str
22
+ migrated_sha256: str
23
+ original: str
24
+ migrated: str
25
+ diff: str
26
+
27
+ def as_dict(self) -> dict[str, object]:
28
+ payload = asdict(self)
29
+ payload["schema"] = "sourcebound.manifest-migration.v1"
30
+ payload["backup"] = ".sourcebound.yml.v0.bak"
31
+ return payload
32
+
33
+
34
+ def _mapping(value: Any) -> dict[str, Any]:
35
+ if not isinstance(value, dict):
36
+ raise ConfigurationError("manifest migration source must be a mapping")
37
+ return value
38
+
39
+
40
+ def build_migration_plan(path: Path) -> MigrationPlan:
41
+ try:
42
+ original = path.read_text(encoding="utf-8")
43
+ except OSError as exc:
44
+ raise ConfigurationError(f"cannot read manifest {path}: {exc}") from exc
45
+ try:
46
+ raw = _mapping(yaml.safe_load(original))
47
+ except yaml.YAMLError as exc:
48
+ raise ConfigurationError(f"invalid YAML in {path}: {exc}") from exc
49
+ source_version = raw.get("version")
50
+ if source_version not in {0, 1}:
51
+ raise ConfigurationError("manifest migration requires source version 0 or 1")
52
+ migrated_data = dict(raw)
53
+ target_version = source_version + 1
54
+ migrated_data["version"] = target_version
55
+ if source_version == 1:
56
+ execution = migrated_data.get("execution")
57
+ if isinstance(execution, dict):
58
+ allowed = execution.get("allowed_commands")
59
+ if isinstance(allowed, dict):
60
+ for command in allowed.values():
61
+ if isinstance(command, dict):
62
+ command.pop("network", None)
63
+ migrated = yaml.safe_dump(migrated_data, sort_keys=False, allow_unicode=True)
64
+ diff = "".join(
65
+ difflib.unified_diff(
66
+ original.splitlines(keepends=True),
67
+ migrated.splitlines(keepends=True),
68
+ fromfile=path.name,
69
+ tofile=f"{path.name} (version {target_version})",
70
+ )
71
+ )
72
+ return MigrationPlan(
73
+ source_version,
74
+ target_version,
75
+ hashlib.sha256(original.encode()).hexdigest(),
76
+ hashlib.sha256(migrated.encode()).hexdigest(),
77
+ original,
78
+ migrated,
79
+ diff,
80
+ )
81
+
82
+
83
+ def backup_path(path: Path, source_version: int = 0) -> Path:
84
+ return path.with_name(path.name + f".v{source_version}.bak")
85
+
86
+
87
+ def apply_migration(path: Path, plan: MigrationPlan) -> Path:
88
+ backup = backup_path(path, plan.source_version)
89
+ if backup.exists():
90
+ raise ConfigurationError(f"migration backup already exists: {backup}")
91
+ try:
92
+ current = path.read_text(encoding="utf-8")
93
+ except OSError as exc:
94
+ raise ConfigurationError(f"cannot read manifest {path}: {exc}") from exc
95
+ if hashlib.sha256(current.encode()).hexdigest() != plan.original_sha256:
96
+ raise ConfigurationError("manifest changed after migration planning")
97
+ atomic_write(backup, plan.original)
98
+ try:
99
+ atomic_write(path, plan.migrated)
100
+ except OSError:
101
+ atomic_write(path, plan.original)
102
+ raise
103
+ return backup
104
+
105
+
106
+ def rollback_migration(path: Path) -> None:
107
+ backups = sorted(path.parent.glob(path.name + ".v*.bak"))
108
+ if len(backups) != 1:
109
+ raise ConfigurationError(
110
+ f"manifest rollback requires one backup, found {len(backups)}"
111
+ )
112
+ backup = backups[0]
113
+ try:
114
+ original = backup.read_text(encoding="utf-8")
115
+ except OSError as exc:
116
+ raise ConfigurationError(f"cannot read migration backup {backup}: {exc}") from exc
117
+ atomic_write(path, original)
118
+ try:
119
+ backup.unlink()
120
+ except OSError as exc:
121
+ raise ConfigurationError(f"cannot remove migration backup {backup}: {exc}") from exc
clean_docs/models.py ADDED
@@ -0,0 +1,194 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from pathlib import Path
5
+ from typing import Any
6
+
7
+
8
+ PLUGIN_API_VERSION = 1
9
+ PLUGIN_INTERFACES = frozenset({"discoverer", "extractor", "policy", "renderer"})
10
+
11
+
12
+ @dataclass(frozen=True)
13
+ class Source:
14
+ path: Path
15
+ symbol: str | None = None
16
+ pointer: str | None = None
17
+ glob: str | None = None
18
+
19
+
20
+ @dataclass(frozen=True)
21
+ class RegionBinding:
22
+ id: str
23
+ doc: Path
24
+ region: str
25
+ extractor: str
26
+ source: Source
27
+ renderer: str
28
+ columns: tuple[str, ...]
29
+ language: str | None = None
30
+
31
+
32
+ @dataclass(frozen=True)
33
+ class Assertion:
34
+ path: str
35
+ operator: str
36
+ expected: Any
37
+ prose: str | None = None
38
+
39
+
40
+ @dataclass(frozen=True)
41
+ class ClaimBinding:
42
+ id: str
43
+ doc: Path
44
+ anchor: str
45
+ extractor: str
46
+ command: str
47
+ assertion: Assertion
48
+
49
+
50
+ @dataclass(frozen=True)
51
+ class SymbolBinding:
52
+ id: str
53
+ doc: Path
54
+ anchor: str
55
+ source: Source
56
+
57
+
58
+ Binding = RegionBinding | ClaimBinding | SymbolBinding
59
+
60
+
61
+ @dataclass(frozen=True)
62
+ class SourceClaimCheck:
63
+ id: str
64
+ kind: str
65
+ doc: Path
66
+ anchor: str
67
+ subject: str
68
+ source: Path
69
+ locator: str
70
+
71
+
72
+ @dataclass(frozen=True)
73
+ class CommandSpec:
74
+ id: str
75
+ argv: tuple[str, ...]
76
+ timeout_seconds: int
77
+
78
+
79
+ @dataclass(frozen=True)
80
+ class PluginSpec:
81
+ id: str
82
+ api_version: int
83
+ interfaces: tuple[str, ...]
84
+ argv: tuple[str, ...]
85
+ timeout_seconds: int
86
+
87
+
88
+ @dataclass(frozen=True)
89
+ class LlmsTxtProjection:
90
+ output: Path
91
+ title: str | None = None
92
+ summary: str | None = None
93
+ include: tuple[Path, ...] = ()
94
+
95
+
96
+ @dataclass(frozen=True)
97
+ class ContextBundleProjection:
98
+ id: str
99
+ output: Path
100
+ include: tuple[Path, ...]
101
+
102
+
103
+ @dataclass(frozen=True)
104
+ class StaticDemoProjection:
105
+ output: Path
106
+ evidence: Path
107
+
108
+
109
+ @dataclass(frozen=True)
110
+ class VisualProjection:
111
+ id: str
112
+ source: Path
113
+ human_output: Path
114
+ agent_output: Path
115
+
116
+
117
+ @dataclass(frozen=True)
118
+ class ReviewLocator:
119
+ id: str
120
+ path: Path
121
+ extractor: str
122
+ locator: str
123
+
124
+
125
+ @dataclass(frozen=True)
126
+ class ReviewContract:
127
+ id: str
128
+ mode: str
129
+ sources: tuple[ReviewLocator, ...]
130
+ targets: tuple[ReviewLocator, ...]
131
+
132
+
133
+ @dataclass(frozen=True)
134
+ class PublicDisposition:
135
+ """A narrowly scoped, documented disposition for one historical finding."""
136
+
137
+ base: str
138
+ kind: str
139
+ subject: str
140
+ documentation: Path
141
+ replacement: str
142
+ reason: str
143
+
144
+
145
+ @dataclass(frozen=True)
146
+ class ProjectionConfig:
147
+ llms_txt: LlmsTxtProjection | None = None
148
+ bundles: tuple[ContextBundleProjection, ...] = ()
149
+ demo: StaticDemoProjection | None = None
150
+ visuals: tuple[VisualProjection, ...] = ()
151
+
152
+
153
+ @dataclass(frozen=True)
154
+ class Manifest:
155
+ path: Path
156
+ version: int
157
+ bindings: tuple[Binding, ...]
158
+ commands: tuple[CommandSpec, ...] = ()
159
+ plugins: tuple[PluginSpec, ...] = ()
160
+ projections: ProjectionConfig | None = None
161
+ source_claim_checks: tuple[SourceClaimCheck, ...] = ()
162
+ review_contracts: tuple[ReviewContract, ...] = ()
163
+ public_dispositions: tuple[PublicDisposition, ...] = ()
164
+ deprecations: tuple[str, ...] = ()
165
+
166
+
167
+ @dataclass(frozen=True)
168
+ class Provenance:
169
+ ref: str
170
+ path: str
171
+ locator: str
172
+ extractor: str
173
+ digest: str
174
+
175
+
176
+ @dataclass(frozen=True)
177
+ class EvidenceValue:
178
+ kind: str
179
+ value: Any
180
+ provenance: Provenance
181
+
182
+
183
+ @dataclass(frozen=True)
184
+ class BindingResult:
185
+ binding_id: str
186
+ doc: str
187
+ changed: bool
188
+ expected: str
189
+ observed: str
190
+ diff: str
191
+ provenance: Provenance
192
+ binding_type: str = "region"
193
+ state: str | None = None
194
+ prose_checked: bool = False