context-engineering-core 0.12.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. context_engine/__init__.py +15 -0
  2. context_engine/__main__.py +3 -0
  3. context_engine/_sqlite.py +19 -0
  4. context_engine/_windows_security.py +125 -0
  5. context_engine/_windows_storage.py +110 -0
  6. context_engine/adapters.py +148 -0
  7. context_engine/answers.py +90 -0
  8. context_engine/budget.py +93 -0
  9. context_engine/cli.py +222 -0
  10. context_engine/config.py +193 -0
  11. context_engine/context.py +116 -0
  12. context_engine/demo.py +98 -0
  13. context_engine/errors.py +91 -0
  14. context_engine/evaluation/__init__.py +1 -0
  15. context_engine/evaluation/commands.py +90 -0
  16. context_engine/evaluation/corpus.py +247 -0
  17. context_engine/evaluation/data/freeze.json +26 -0
  18. context_engine/evaluation/data/ground_truth.json +287 -0
  19. context_engine/evaluation/data/pin_retention.json +8 -0
  20. context_engine/evaluation/data/protocol.json +122 -0
  21. context_engine/evaluation/data/scenario.json +1226 -0
  22. context_engine/evaluation/data/summary_fixture.json +7 -0
  23. context_engine/evaluation/execution.py +538 -0
  24. context_engine/evaluation/fixtures.py +32 -0
  25. context_engine/evaluation/grading.py +62 -0
  26. context_engine/evaluation/harness.py +165 -0
  27. context_engine/evaluation/heldout/__init__.py +260 -0
  28. context_engine/evaluation/heldout/__main__.py +40 -0
  29. context_engine/evaluation/heldout/data/ground_truth.json +18 -0
  30. context_engine/evaluation/heldout/data/manifest.json +9 -0
  31. context_engine/evaluation/heldout/data/protocol.json +14 -0
  32. context_engine/evaluation/heldout/data/scenarios.json +18 -0
  33. context_engine/evaluation/heldout/data/scoring_manifest.json +4 -0
  34. context_engine/evaluation/heldout/data/scoring_protocol.json +12 -0
  35. context_engine/evaluation/heldout/scoring.py +176 -0
  36. context_engine/evaluation/protocol.py +264 -0
  37. context_engine/evaluation/reporting.py +294 -0
  38. context_engine/evaluation/results.py +331 -0
  39. context_engine/evaluation/run_commands.py +146 -0
  40. context_engine/evaluation/strategies.py +261 -0
  41. context_engine/inspection.py +266 -0
  42. context_engine/layers/__init__.py +18 -0
  43. context_engine/layers/cap.py +99 -0
  44. context_engine/layers/common.py +162 -0
  45. context_engine/layers/pin.py +68 -0
  46. context_engine/layers/retrieve.py +233 -0
  47. context_engine/layers/summarize.py +202 -0
  48. context_engine/layers/window.py +29 -0
  49. context_engine/mcp_server.py +267 -0
  50. context_engine/memory/__init__.py +5 -0
  51. context_engine/memory/codec.py +91 -0
  52. context_engine/memory/demo.py +90 -0
  53. context_engine/memory/store.py +770 -0
  54. context_engine/models.py +384 -0
  55. context_engine/pipeline.py +383 -0
  56. context_engine/providers/__init__.py +1 -0
  57. context_engine/providers/client.py +301 -0
  58. context_engine/providers/commands.py +138 -0
  59. context_engine/providers/contracts.py +324 -0
  60. context_engine/providers/credentials.py +66 -0
  61. context_engine/providers/memory.py +112 -0
  62. context_engine/providers/store.py +408 -0
  63. context_engine/providers/transport.py +195 -0
  64. context_engine/providers/windows_credentials.py +23 -0
  65. context_engine/py.typed +0 -0
  66. context_engine/service/__init__.py +1 -0
  67. context_engine/service/__main__.py +68 -0
  68. context_engine/service/app.py +404 -0
  69. context_engine/service/auth.py +132 -0
  70. context_engine/service/client.py +52 -0
  71. context_engine/service/control.py +134 -0
  72. context_engine/service/demo.py +151 -0
  73. context_engine/service/integrations.py +28 -0
  74. context_engine/service/schemas.py +59 -0
  75. context_engine/service/worker.py +59 -0
  76. context_engine/service/workers.py +156 -0
  77. context_engine/summaries.py +196 -0
  78. context_engine/tokens.py +171 -0
  79. context_engine/work.py +46 -0
  80. context_engineering_core-0.12.0.dist-info/METADATA +203 -0
  81. context_engineering_core-0.12.0.dist-info/RECORD +84 -0
  82. context_engineering_core-0.12.0.dist-info/WHEEL +4 -0
  83. context_engineering_core-0.12.0.dist-info/entry_points.txt +3 -0
  84. context_engineering_core-0.12.0.dist-info/licenses/LICENSE +202 -0
@@ -0,0 +1,91 @@
1
+ """Structured, content-free errors safe for default diagnostic output."""
2
+
3
+ from types import MappingProxyType
4
+ from typing import ClassVar
5
+
6
+
7
+ class ContextEngineError(Exception):
8
+ code: ClassVar[str] = "context_engine_error"
9
+
10
+ def __init__(self, message: str, **details: int | str) -> None:
11
+ super().__init__(message)
12
+ self.details = MappingProxyType(dict(details))
13
+
14
+ def to_dict(self) -> dict:
15
+ return {"code": self.code, "message": str(self), "details": dict(self.details)}
16
+
17
+
18
+ class ConfigurationError(ContextEngineError):
19
+ code = "invalid_configuration"
20
+
21
+
22
+ class ContractError(ContextEngineError):
23
+ code = "invalid_contract"
24
+
25
+
26
+ class TokenizerUnavailable(ContextEngineError):
27
+ code = "tokenizer_unavailable"
28
+
29
+
30
+ class RequiredContextTooLarge(ContextEngineError):
31
+ code = "required_context_too_large"
32
+
33
+
34
+ class LayerAllocationError(ContextEngineError):
35
+ code = "layer_allocation_exceeds_budget"
36
+
37
+
38
+ class PromptTooLarge(ContextEngineError):
39
+ code = "prompt_too_large"
40
+
41
+
42
+ class LayerStateError(ContextEngineError):
43
+ code = "invalid_layer_state"
44
+
45
+
46
+ class CapBudgetError(ContextEngineError):
47
+ code = "cap_budget_too_small"
48
+
49
+
50
+ class SummaryLeakageError(ContextEngineError):
51
+ code = "summary_leakage"
52
+
53
+
54
+ class InspectionError(ContextEngineError):
55
+ code = "invalid_inspection_input"
56
+
57
+
58
+ class SummaryPolicyError(ContextEngineError):
59
+ code = "summary_policy_error"
60
+
61
+
62
+ class BenchmarkError(ContextEngineError):
63
+ code = "invalid_benchmark"
64
+
65
+
66
+ class ProviderError(ContextEngineError):
67
+ code = "provider_error"
68
+
69
+
70
+ class StorageError(ContextEngineError):
71
+ code = "runtime_storage_error"
72
+
73
+
74
+ class QuotaError(ContextEngineError):
75
+ code = "quota_exhausted"
76
+
77
+
78
+ class CacheError(ContextEngineError):
79
+ code = "invalid_replay_cache"
80
+
81
+
82
+ class MemoryConflict(ContextEngineError):
83
+ code = "memory_revision_conflict"
84
+
85
+
86
+ class MemoryIntegrityError(ContextEngineError):
87
+ code = "memory_integrity_error"
88
+
89
+
90
+ class WorkCancelled(ContextEngineError):
91
+ code = "work_cancelled"
@@ -0,0 +1 @@
1
+ """Fixture validation only; the complete benchmark harness arrives at C04."""
@@ -0,0 +1,90 @@
1
+ """Bounded offline owner demonstrations, not a model benchmark runner."""
2
+
3
+ from copy import deepcopy
4
+ from dataclasses import asdict
5
+
6
+ from ..models import canonical_json
7
+ from .corpus import Bundle, load_bundle
8
+ from .harness import prepare_probe
9
+ from .protocol import load_freeze, make_manifest, preflight
10
+ from .results import assess_targets, record_response, statistics, validate_run
11
+
12
+
13
+ def altered_bundle(bundle: Bundle, name: str, value: dict) -> Bundle:
14
+ return Bundle(
15
+ tuple((key, canonical_json(value) if key == name else raw) for key, raw in bundle.parts)
16
+ )
17
+
18
+
19
+ def corruption_demo(bundle, frozen, manifest, records) -> dict:
20
+ """Synthetic fault injection on detached copies; never rewrite frozen resources."""
21
+ result = {}
22
+ for label, name, mutate in (
23
+ ("malformed_fixture", "scenario", lambda data: data["history"].pop()),
24
+ ("summary_answer_leak", "summary_fixture", lambda data: data.update(content="shard-19")),
25
+ ("configuration_drift", "protocol", lambda data: data["cap"].update(max_tokens=36)),
26
+ ):
27
+ value = bundle.get(name)
28
+ mutate(value)
29
+ gates = preflight(altered_bundle(bundle, name, value), frozen)
30
+ result[label] = {
31
+ "status": "INVALID" if any(g.status == "FAIL" for g in gates) else "UNEXPECTED_PASS",
32
+ "failed_gates": [g.gate for g in gates if g.status == "FAIL"],
33
+ }
34
+ bad = deepcopy(records)
35
+ bad[-1]["request"]["messages"][-1]["content"] = "Tampered question"
36
+ result["changed_request"] = validate_run(bundle, frozen, manifest, bad)
37
+ bad = deepcopy(records)
38
+ bad[-1] = record_response(
39
+ bundle, bad[-1], answer="synthetic partial response", finish_reason="length", origin="test"
40
+ )
41
+ result["truncated_test_response"] = validate_run(bundle, frozen, manifest, bad)
42
+ return result
43
+
44
+
45
+ def benchmark_check(
46
+ *, fact="shard", budget=900, model=None, show_content=False, data_dir=None
47
+ ) -> dict:
48
+ bundle, frozen = load_bundle(data_dir), load_freeze()
49
+ gates = preflight(bundle, frozen)
50
+ if any(g.status == "FAIL" for g in gates):
51
+ return {"status": "INVALID", "gates": [asdict(g) for g in gates], "inference_calls": 0}
52
+ model = model or bundle.get("protocol")["models"][0]
53
+ manifest = make_manifest(
54
+ bundle, frozen, facts=[fact], budgets=[budget], variants=["A0", "A5"], models=[model]
55
+ )
56
+ records = [prepare_probe(bundle, frozen, manifest, slot["probe_id"]) for slot in manifest.slots]
57
+ validation = validate_run(bundle, frozen, manifest, records)
58
+ exported = (
59
+ records
60
+ if show_content
61
+ else [{k: v for k, v in row.items() if k != "request"} for row in records]
62
+ )
63
+ return {
64
+ **validation,
65
+ "run_id": manifest.run_id,
66
+ "fixture_hashes": bundle.hashes,
67
+ "protocol_approval": bundle.get("protocol")["approval"],
68
+ "inference_calls": 0,
69
+ "content_included": show_content,
70
+ "probes": exported,
71
+ "statistics": statistics(manifest, records),
72
+ "quality_acceptance": assess_targets(manifest, records, validation),
73
+ "deliberately_invalid": corruption_demo(bundle, frozen, manifest, records),
74
+ }
75
+
76
+
77
+ def benchmark_plan(*, data_dir=None) -> dict:
78
+ bundle, frozen = load_bundle(data_dir), load_freeze()
79
+ manifest = make_manifest(bundle, frozen)
80
+ completion = bundle.get("protocol")["generation"]["max_completion_tokens"]
81
+ return {
82
+ "status": "PLANNED",
83
+ "run_id": manifest.run_id,
84
+ "manifest": manifest.data,
85
+ "slots": list(manifest.slots),
86
+ "planned_slots": len(manifest.slots),
87
+ "conservative_tokens_before_retries": sum(s["budget"] + completion for s in manifest.slots),
88
+ "inference_calls": 0,
89
+ "spending_authorized": False,
90
+ }
@@ -0,0 +1,247 @@
1
+ """Frozen, packaged synthetic resources; ground truth never enters an inspection job."""
2
+
3
+ import hashlib
4
+ import json
5
+ from dataclasses import dataclass
6
+ from datetime import datetime
7
+ from importlib import resources
8
+ from pathlib import Path
9
+
10
+ from ..config import Settings
11
+ from ..errors import BenchmarkError, ContextEngineError
12
+ from ..inspection import parse_job
13
+ from ..models import canonical_json
14
+ from .grading import normalize, present
15
+
16
+ PARTS = ("scenario", "ground_truth", "summary_fixture", "protocol", "pin_retention")
17
+ MAX_BYTES = 2_000_000
18
+
19
+
20
+ def digest(value) -> str:
21
+ return hashlib.sha256(canonical_json(value).encode()).hexdigest()
22
+
23
+
24
+ def strict_json(raw: str, *, maximum: int = MAX_BYTES) -> dict:
25
+ def unique(pairs):
26
+ result = {}
27
+ for key, value in pairs:
28
+ if key in result:
29
+ raise BenchmarkError("Duplicate benchmark JSON key")
30
+ result[key] = value
31
+ return result
32
+
33
+ def nonfinite(_):
34
+ raise BenchmarkError("Nonfinite benchmark JSON number")
35
+
36
+ try:
37
+ if len(raw.encode("utf-8")) > maximum:
38
+ raise BenchmarkError("Benchmark document exceeds byte limit")
39
+ parsed = json.loads(raw, object_pairs_hook=unique, parse_constant=nonfinite)
40
+ if not isinstance(parsed, dict):
41
+ raise BenchmarkError("Benchmark document must be an object")
42
+ canonical_json(parsed)
43
+ return parsed
44
+ except (UnicodeError, ValueError, TypeError, RecursionError):
45
+ raise BenchmarkError("Malformed benchmark JSON") from None
46
+
47
+
48
+ @dataclass(frozen=True)
49
+ class Bundle:
50
+ parts: tuple[tuple[str, str], ...]
51
+
52
+ def __post_init__(self):
53
+ if not isinstance(self.parts, tuple) or tuple(name for name, _ in self.parts) != PARTS:
54
+ raise BenchmarkError("Incomplete benchmark bundle")
55
+ for _, raw in self.parts:
56
+ strict_json(raw)
57
+
58
+ def get(self, name: str) -> dict:
59
+ return strict_json(dict(self.parts)[name]) # Fresh objects, no mutable shared fixtures.
60
+
61
+ @property
62
+ def hashes(self) -> dict:
63
+ return {name: hashlib.sha256(raw.encode("utf-8")).hexdigest() for name, raw in self.parts}
64
+
65
+ def job(self, question: str):
66
+ scenario = self.get("scenario")
67
+ scenario.pop("scenario_id")
68
+ scenario["question"] = question
69
+ return parse_job(scenario, Settings.from_env({}))
70
+
71
+ @property
72
+ def facts(self) -> tuple[dict, ...]:
73
+ return tuple(self.get("ground_truth")["facts"])
74
+
75
+ def fact(self, identifier: str) -> dict:
76
+ for fact in self.facts:
77
+ if fact["fact_id"] == identifier:
78
+ return fact
79
+ raise BenchmarkError("Unknown benchmark fact")
80
+
81
+
82
+ def read_resource(name: str, directory: Path | None = None) -> str:
83
+ path = directory / name if directory else resources.files(__package__).joinpath("data", name)
84
+ try:
85
+ with path.open("rb") as handle:
86
+ raw = handle.read(MAX_BYTES + 1)
87
+ if len(raw) > MAX_BYTES:
88
+ raise BenchmarkError("Benchmark document exceeds byte limit")
89
+ return raw.decode("utf-8")
90
+ except (OSError, UnicodeError, ValueError):
91
+ raise BenchmarkError("Cannot read benchmark resource") from None
92
+
93
+
94
+ def load_bundle(directory: Path | None = None) -> Bundle:
95
+ return Bundle(tuple((name, read_resource(name + ".json", directory)) for name in PARTS))
96
+
97
+
98
+ def aliases(fact: dict) -> tuple[str, ...]:
99
+ return (fact["expected"], *fact["aliases"])
100
+
101
+
102
+ def check_fixture(bundle: Bundle) -> None:
103
+ """V1 schema, cardinality, relationships, plant locations and unique metadata."""
104
+ try:
105
+ scenario, truth = bundle.get("scenario"), bundle.get("ground_truth")
106
+ summary, protocol = bundle.get("summary_fixture"), bundle.get("protocol")
107
+ if any(
108
+ type(part["schema_version"]) is not int or part["schema_version"] != 1
109
+ for part in (scenario, truth, summary, protocol, bundle.get("pin_retention"))
110
+ ):
111
+ raise BenchmarkError("Unsupported benchmark schema")
112
+ if set(truth) != {"schema_version", "scenario_id", "facts"}:
113
+ raise BenchmarkError("Unexpected ground-truth fields")
114
+ if any(
115
+ part["scenario_id"] != scenario["scenario_id"] for part in (truth, summary, protocol)
116
+ ):
117
+ raise BenchmarkError("Scenario identity mismatch")
118
+ job = bundle.job("Fixture validation")
119
+ from ..layers.summarize import SummarySnapshot
120
+ from ..models import Pin
121
+
122
+ if set(summary) != {
123
+ "schema_version",
124
+ "scenario_id",
125
+ "policy_version",
126
+ "created_at",
127
+ "content",
128
+ }:
129
+ raise BenchmarkError("Unexpected summary fields")
130
+ snapshot = SummarySnapshot.from_history(
131
+ scope=job.arguments["pinned_facts"].scope,
132
+ content=summary["content"],
133
+ history=job.arguments["history"],
134
+ policy_version=summary["policy_version"],
135
+ created_at=datetime.fromisoformat(summary["created_at"]),
136
+ )
137
+ if snapshot.created_at > job.arguments["at"]:
138
+ raise BenchmarkError("Frozen summary is not available at evaluation time")
139
+ fixture = bundle.get("pin_retention")
140
+ if set(fixture) != {"schema_version", "fixture_id", "question", "key", "value", "origin"}:
141
+ raise BenchmarkError("Unexpected separate pin fixture fields")
142
+ Pin(
143
+ scope=job.arguments["pinned_facts"].scope,
144
+ key=fixture["key"],
145
+ value=fixture["value"],
146
+ origin=fixture["origin"],
147
+ effective_at=job.arguments["at"],
148
+ )
149
+ if (
150
+ not isinstance(fixture["question"], str)
151
+ or not fixture["question"].strip()
152
+ or present(fixture["question"], (fixture["value"],))
153
+ ):
154
+ raise BenchmarkError("Invalid separate pin question")
155
+ # Constructed carrier validation catches duplicate/mixed source identity without inference.
156
+ from ..context import ContextCarrier
157
+ from ..models import Message, Role
158
+
159
+ history = job.arguments["history"]
160
+ ContextCarrier(
161
+ job.arguments["pinned_facts"].scope,
162
+ history,
163
+ history,
164
+ job.arguments["pinned_facts"],
165
+ Message("fixture-question", Role.USER, "Q"),
166
+ )
167
+ if len(history) != 100 or [t.turn_id for t in history] != [f"t{i}" for i in range(1, 101)]:
168
+ raise BenchmarkError("Expected ordered 100-turn fixture")
169
+ if len(bundle.facts) != 31 or len({f["fact_id"] for f in bundle.facts}) != 31:
170
+ raise BenchmarkError("Expected exactly 31 unique facts")
171
+ turns = {t.turn_id: t for t in history}
172
+ seen_answers, seen_questions = set(), set()
173
+ fields = {
174
+ "fact_id",
175
+ "expected",
176
+ "aliases",
177
+ "source_turn_id",
178
+ "source_message_id",
179
+ "question",
180
+ "zone",
181
+ }
182
+ for fact in bundle.facts:
183
+ if set(fact) != fields or not isinstance(fact["aliases"], list):
184
+ raise BenchmarkError("Unexpected fact fields")
185
+ if any(
186
+ not isinstance(fact[k], str) or not fact[k].strip() for k in fields - {"aliases"}
187
+ ):
188
+ raise BenchmarkError("Invalid fact text")
189
+ values = aliases(fact)
190
+ if any(not isinstance(value, str) or not normalize(value) for value in values):
191
+ raise BenchmarkError("Invalid answer alias")
192
+ normalized = {normalize(value) for value in values}
193
+ if len(normalized) != len(values) or seen_answers & normalized:
194
+ raise BenchmarkError("Ambiguous answer aliases")
195
+ seen_answers |= normalized
196
+ if normalize(fact["question"]) in seen_questions:
197
+ raise BenchmarkError("Duplicate probe question")
198
+ seen_questions.add(normalize(fact["question"]))
199
+ turn = turns[fact["source_turn_id"]]
200
+ message = next(m for m in turn.messages if m.message_id == fact["source_message_id"])
201
+ if not present(message.content, (fact["expected"],)):
202
+ raise BenchmarkError("Planted value missing from declared source")
203
+ locations = [
204
+ (t.turn_id, m.message_id)
205
+ for t in history
206
+ for m in t.messages
207
+ if present(m.content, values)
208
+ ]
209
+ if locations != [(turn.turn_id, message.message_id)]:
210
+ raise BenchmarkError("Fact appears outside its declared source")
211
+ number = int(turn.turn_id[1:])
212
+ if fact["zone"] != ("early" if number <= 33 else "middle" if number <= 66 else "late"):
213
+ raise BenchmarkError("Incorrect source age zone")
214
+ flagship = bundle.fact("shard")
215
+ if (flagship["source_turn_id"], flagship["expected"], flagship["question"]) != (
216
+ "t82",
217
+ "shard-19",
218
+ "Which partition ended up carrying the blame?",
219
+ ):
220
+ raise BenchmarkError("Flagship source contract changed")
221
+ except BenchmarkError:
222
+ raise
223
+ except (ContextEngineError, KeyError, TypeError, ValueError, StopIteration, OverflowError):
224
+ raise BenchmarkError("Malformed benchmark fixture") from None
225
+
226
+
227
+ def check_leakage(bundle: Bundle) -> None:
228
+ """V2 primary input has no evaluator fields, answer pins, question/summary leakage."""
229
+ from ..layers.summarize import SummarySnapshot
230
+ from .fixtures import validate_frozen_summary
231
+
232
+ job = bundle.job("Leakage validation")
233
+ scenario, summary = bundle.get("scenario"), bundle.get("summary_fixture")
234
+ values = tuple(value for fact in bundle.facts for value in aliases(fact))
235
+ if scenario["pins"] or bundle.get("protocol")["primary_pins"] != "empty":
236
+ raise BenchmarkError("Primary corpus must have no answer pins")
237
+ for content in [scenario["system"], *(fact["question"] for fact in bundle.facts)]:
238
+ if present(content, values):
239
+ raise BenchmarkError("Answer leakage in system or probe question")
240
+ snapshot = SummarySnapshot.from_history(
241
+ scope=job.arguments["pinned_facts"].scope,
242
+ content=summary["content"],
243
+ history=job.arguments["history"],
244
+ policy_version=summary["policy_version"],
245
+ created_at=job.arguments["at"],
246
+ )
247
+ validate_frozen_summary(snapshot, values)
@@ -0,0 +1,26 @@
1
+ {
2
+ "fixture_hashes": {
3
+ "ground_truth": "c319c47c0548ced89dc68deba183c3028c35aad2aabd756db8b3bbdfe4c064e6",
4
+ "pin_retention": "febe00b1b71016fd679a3d99a2bccdb901ff9b956f0933d1003578c06faf5f3b",
5
+ "protocol": "789585023df970bcfea2723b5b7994a1251f0aa512f6ee5820b8ad7353eb571c",
6
+ "scenario": "19f3bc62987b9b2253eed316e903c4f860b445b441d9151573588bce283bae54",
7
+ "summary_fixture": "b3c1b76bb330b52493e5150647747bad77d5de3a55d05d032bb5703be1c70196"
8
+ },
9
+ "runtime": {
10
+ "code_hash": "59eeb4d28e6b3106a18317d5b6acee255f0b22d5e5143e3e7bb74b1146a39ff4",
11
+ "dependencies": {
12
+ "certifi": "2026.10.23",
13
+ "charset-normalizer": "3.5.1",
14
+ "idna": "3.19",
15
+ "numpy": "2.5.3",
16
+ "rank-bm25": "0.2.2",
17
+ "regex": "2026.9.3",
18
+ "requests": "2.34.2",
19
+ "tiktoken": "0.14.0",
20
+ "urllib3": "2.8.0"
21
+ },
22
+ "package_version": "0.12.0",
23
+ "python": "3.12.13"
24
+ },
25
+ "schema_version": 1
26
+ }