agent-json-reliability 0.1.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.
@@ -0,0 +1,128 @@
1
+ Metadata-Version: 2.4
2
+ Name: agent-json-reliability
3
+ Version: 0.1.0
4
+ Summary: Deterministic JSON repair and JSON Schema validation for AI agents and software. No LLM required.
5
+ Author: Agent JSON Reliability contributors
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/toninovo4249-ai/agent-json-reliability
8
+ Keywords: json,json-schema,json-repair,ai-agents,mcp,structured-output
9
+ Classifier: Development Status :: 4 - Beta
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Topic :: Software Development :: Libraries
13
+ Requires-Python: >=3.10
14
+ Description-Content-Type: text/markdown
15
+ License-File: LICENSE
16
+ Requires-Dist: fastapi>=0.115.0
17
+ Requires-Dist: uvicorn>=0.30.0
18
+ Requires-Dist: jsonschema>=4.23.0
19
+ Dynamic: license-file
20
+
21
+ # Agent JSON Reliability
22
+
23
+ STATUS=BETA version `0.1.0`
24
+
25
+ AI agents and software frequently emit malformed JSON: markdown fences, trailing commas, single quotes, `True`/`False`/`None`, and extra prose around one object.
26
+
27
+ **Agent JSON Reliability** is a deterministic pipeline for that failure mode:
28
+
29
+ 1. **inspect** — diagnose whether the text is JSON and which failures are present
30
+ 2. **safe repair** — apply only structural, unambiguous fixes
31
+ 3. **validate** — optional JSON Schema check on the result
32
+ 4. **structured diagnostics** — `valid_original`, `repaired`, `valid_final`, `schema_valid`, `unsafe_or_ambiguous`, `changes`, `errors`
33
+
34
+ This is not a generic jsonschema wrapper. The primary value is turning **malformed agent output** into reliable structured data **without an LLM** and **without inventing semantic values**.
35
+
36
+ When repair would require guessing (truncated objects, missing values, competing JSON documents), the service **refuses**. That is correct behavior.
37
+
38
+ Primary HTTP endpoint: `POST /v1/json/reliable`
39
+
40
+ MCP tools (same core functions, no duplicate implementation): `json_reliable` / `reliable_json`, `validate_json`, `repair_json`, `inspect_json`
41
+
42
+ <!-- mcp-name: io.github.toninovo4249-ai/agent-json-reliability -->
43
+
44
+ ## Before / after (safe repair)
45
+
46
+ Malformed agent output:
47
+
48
+ ```json
49
+ {
50
+ "text": "```json\n{\"name\":\"alice\",\"age\":30,}\n```"
51
+ }
52
+ ```
53
+
54
+ Expected result (semantically equivalent valid JSON):
55
+
56
+ - `valid_original=false`
57
+ - `repaired=true`
58
+ - `valid_final=true`
59
+ - `unsafe_or_ambiguous=false`
60
+ - `json` → `{"name":"alice","age":30}`
61
+
62
+ If a schema is supplied and satisfied: `schema_valid=true`.
63
+
64
+ ## Safe refusal (ambiguous)
65
+
66
+ Truncated input such as `{"user":` is JSON-intended but not deterministically repairable without inventing keys or values.
67
+
68
+ Expected:
69
+
70
+ - HTTP `200` (documented safe response)
71
+ - `valid_final=false`
72
+ - `unsafe_or_ambiguous=true` **or** `repaired=false`
73
+
74
+ Do not treat a correct refusal as a product failure.
75
+
76
+ ## Run locally (no Hunter)
77
+
78
+ ```bash
79
+ python -m pip install -r requirements.txt
80
+ python serve.py
81
+ ```
82
+
83
+ Binds `127.0.0.1:8770` by default. Set `PUBLIC_BASE_URL` to an HTTPS origin only when you expose the process yourself. Do not commit a temporary tunnel hostname as the canonical URL.
84
+
85
+ ## HTTP examples
86
+
87
+ See `examples/curl.md`, `examples/python.py`, `examples/javascript.js`.
88
+
89
+ Remote examples use `${PUBLIC_BASE_URL}`. Local default is `http://127.0.0.1:8770`.
90
+
91
+ Share URLs (optional, unverified telemetry tags):
92
+
93
+ - `/?source=github`
94
+ - `/?source=mcp-registry`
95
+ - `/?source=api-directory`
96
+
97
+ ## MCP (stdio — durable)
98
+
99
+ Package transport does not depend on a temporary public URL.
100
+
101
+ ```json
102
+ {
103
+ "mcpServers": {
104
+ "agent-json-reliability": {
105
+ "command": "python",
106
+ "args": ["-m", "products.gateway.mcp_stdio"]
107
+ }
108
+ }
109
+ }
110
+ ```
111
+
112
+ After you publish to PyPI (free): `uvx agent-json-reliability`.
113
+
114
+ Remote HTTP MCP (`POST /mcp`) is for a running instance. Prefer stdio/package for Official MCP Registry metadata.
115
+
116
+ ## Machine discovery
117
+
118
+ - `GET /.well-known/agent-services.json`
119
+ - `GET /openapi.json`
120
+ - `GET /llms.txt`
121
+ - `GET /AGENTS.md`
122
+ - `POST /mcp` JSON-RPC (`tools/list`, `tools/call`)
123
+
124
+ ## What this package does not include
125
+
126
+ No Hunter market database, no private reports, no Windows user paths, no collectors, no wallets, no x402 payment.
127
+
128
+ Free beta: no auth, no payment.
@@ -0,0 +1,30 @@
1
+ serve.py,sha256=q5a-jhErzovqZ-XGMRrUV8Eepy5SNJ0WfmFQaJZmBWo,958
2
+ agent_json_reliability-0.1.0.dist-info/licenses/LICENSE,sha256=3dC8g3pbUlohK3QJEGaAn12Qg30ciDaELSTd0BXIuGE,516
3
+ products/__init__.py,sha256=HuOKG1roMpOoMIYohAffe21pBS3DEVoTt4mF4nmSMEo,70
4
+ products/agent_json_reliability/__init__.py,sha256=wMoZTPy4uhabZCyc4xe0qX_6Q17qvU4zlG4CHtjfh0Q,298
5
+ products/agent_json_reliability/inspect.py,sha256=LcymxHNKlv-H6gxyovl_yhwva8QJDUW6El75nBSWnBc,2196
6
+ products/agent_json_reliability/reliable.py,sha256=8gGopZG6EMW3hGgWh_QOBRVzMWAPVc7LjVhR0Ui5Q4M,2592
7
+ products/agent_json_reliability/repair.py,sha256=P1XTciJbVlqYmcwYJCZ6vyCYHZ9od91MdLInoUI8Oi0,7395
8
+ products/agent_json_reliability/validate.py,sha256=ffBiHE6leHlGqTWXVunD2qSjeVsCsmBOSF3c7LN4VAU,7030
9
+ products/beta/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10
+ products/beta/app.py,sha256=rhJ7m-l_0CaVnyEknfDL_ZI2VQB1puC0SLCG3bhMksA,12702
11
+ products/beta/batch.py,sha256=Lg3ZKxOcENZyJMKv0NQlGNF_MxWjbEVlzzTEOg3doAI,917
12
+ products/beta/catalog.py,sha256=94Gmxi-HQHjBcn1WX1aB8VFxNNAVDmctMuKjJf2sdow,2080
13
+ products/beta/identity.py,sha256=ouXpqNQ4pjcBhzc9dYk6znBUe2y4pO7mSLQzmKwZGl8,4343
14
+ products/beta/landing.py,sha256=EReK7hINodUIPPe-Y2ixEfizXA48JUrgekZTc_Gfbkc,4669
15
+ products/beta/security.py,sha256=6YJbw4ahcR_G8n1pq04ndS3gP9AeWunaZRfxJQphdvc,2597
16
+ products/beta/selfcheck.py,sha256=G_iF5LqAReqLyKImScXurc5Ud99zmGfUcCyqCL5AqhU,1011
17
+ products/beta/settings.py,sha256=W6yfqC2yyj_-xulhPF3Ery8tO8f5ChlpuAUVvzF9h6U,2973
18
+ products/beta/store.py,sha256=kLwTrL-iNFG2pLyML8ApTWDJyYdfU-U0sW7YnLvwcSE,4732
19
+ products/common/__init__.py,sha256=rJlpYCJdYduuCGz_7RftJexTnJF3WrdekL4yzG1glko,175
20
+ products/common/timing.py,sha256=8F3x3x7n5jrp1e-l2SEjWaOPq9FOCREkipJINnf7Wv0,313
21
+ products/gateway/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
22
+ products/gateway/manifest.py,sha256=sDFQsA71EKrF4sFrSS98oe5IpsTEQiLm7hBUCqf1isc,6993
23
+ products/gateway/mcp_server.py,sha256=YmSLbjr9D6Ej8YubSPj0aOKjHqlnjqM1QmZj_4vVKis,2732
24
+ products/gateway/mcp_stdio.py,sha256=B0japGfz3QKOgYrfNXtj-wXWcoUKrW6OFjlLAlylh3I,1508
25
+ products/gateway/rate_limit.py,sha256=LwcIyqk7BWTxAaJWhm5uTrXaWo1AOD_WYbpNSRGADsg,885
26
+ agent_json_reliability-0.1.0.dist-info/METADATA,sha256=V4eMP8eTvNfaqV4Fith5UUaksZE8N_omc8aGVeQtxZI,4252
27
+ agent_json_reliability-0.1.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
28
+ agent_json_reliability-0.1.0.dist-info/entry_points.txt,sha256=nr7RzsB4dHKHEQjtA1M1ieHro5HRfdOwTlNTwUxrS0M,117
29
+ agent_json_reliability-0.1.0.dist-info/top_level.txt,sha256=WdV6O4or29KOsiwfII1hnZH5pdU0DnRVfpa1hp1Vdtc,15
30
+ agent_json_reliability-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,3 @@
1
+ [console_scripts]
2
+ agent-json-reliability = products.gateway.mcp_stdio:main
3
+ agent-json-reliability-serve = serve:main
@@ -0,0 +1,5 @@
1
+ MIT License
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4
+
5
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND.
@@ -0,0 +1,2 @@
1
+ products
2
+ serve
products/__init__.py ADDED
@@ -0,0 +1 @@
1
+ """Local zero-cost product prototypes. Independent of collectors."""
@@ -0,0 +1,7 @@
1
+ from __future__ import annotations
2
+
3
+ from products.agent_json_reliability.inspect import inspect_json
4
+ from products.agent_json_reliability.repair import repair_json
5
+ from products.agent_json_reliability.validate import validate_json
6
+
7
+ __all__ = ["validate_json", "repair_json", "inspect_json"]
@@ -0,0 +1,62 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import re
5
+ from typing import Any
6
+
7
+ from products.agent_json_reliability.repair import repair_json
8
+ from products.common.timing import Clock
9
+
10
+ _FAILURES = [
11
+ ("markdown_fence", re.compile(r"```")),
12
+ ("trailing_comma", re.compile(r",\s*[}\]]")),
13
+ ("single_quotes", re.compile(r"'")),
14
+ ("python_literals", re.compile(r"\b(True|False|None)\b")),
15
+ ("comments", re.compile(r"//|/\*")),
16
+ ("leading_prose", None),
17
+ ("truncated", None),
18
+ ]
19
+
20
+
21
+ def inspect_json(text: str) -> dict[str, Any]:
22
+ with Clock() as sw:
23
+ if not isinstance(text, str):
24
+ text = str(text)
25
+ raw = text.strip()
26
+ valid = False
27
+ err_pos = None
28
+ try:
29
+ json.loads(raw)
30
+ valid = True
31
+ except json.JSONDecodeError as e:
32
+ err_pos = e.pos
33
+ types: list[str] = []
34
+ if re.search(r"```", raw):
35
+ types.append("markdown_fence")
36
+ if re.search(r",\s*[}\]]", raw):
37
+ types.append("trailing_comma")
38
+ if "'" in raw and '"' not in raw:
39
+ types.append("single_quotes")
40
+ if re.search(r"\b(True|False|None)\b", raw):
41
+ types.append("python_literals")
42
+ if "//" in raw or "/*" in raw:
43
+ types.append("comments")
44
+ stripped = raw.lstrip()
45
+ if stripped and stripped[0] not in "{[":
46
+ types.append("leading_prose")
47
+ if raw.count("{") > raw.count("}") or raw.count("[") > raw.count("]"):
48
+ types.append("truncated")
49
+ if re.search(r"\}\s*\{", raw):
50
+ types.append("multiple_objects")
51
+ likely = valid or bool(re.search(r"[\{\[]", raw))
52
+ rep = repair_json(raw)
53
+ complexity = min(100, raw.count("{") + raw.count("[") + raw.count(":") + len(raw) // 200)
54
+ return {
55
+ "valid_json": valid,
56
+ "likely_json": likely,
57
+ "error_position": err_pos,
58
+ "detected_failure_types": types,
59
+ "repair_possible": bool(rep.get("repaired") or valid),
60
+ "estimated_complexity": complexity,
61
+ "processing_ms": sw.ms,
62
+ }
@@ -0,0 +1,66 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ from products.agent_json_reliability.inspect import inspect_json
6
+ from products.agent_json_reliability.repair import repair_json
7
+ from products.agent_json_reliability.validate import validate_json
8
+ from products.common.timing import Clock
9
+
10
+
11
+ def reliable_json(text: str, schema: dict | None = None, mode: str = "safe") -> dict[str, Any]:
12
+ """inspect → safe repair if needed → optional schema validate. Same primitives as HTTP tools."""
13
+ with Clock() as sw:
14
+ insp = inspect_json(text)
15
+ valid_original = bool(insp.get("valid_json"))
16
+ changes: list[str] = []
17
+ repaired = False
18
+ payload: Any = None
19
+ unsafe = False
20
+ if valid_original:
21
+ import json
22
+
23
+ payload = json.loads(text.strip().lstrip("\ufeff"))
24
+ else:
25
+ rep = repair_json(text, mode)
26
+ repaired = bool(rep.get("repaired"))
27
+ payload = rep.get("json")
28
+ changes = list(rep.get("changes") or [])
29
+ unsafe = bool(rep.get("unsafe_or_ambiguous"))
30
+ valid_final = payload is not None
31
+ schema_valid = None
32
+ errors: list = []
33
+ if valid_final and schema is not None:
34
+ v = validate_json(payload, schema)
35
+ schema_valid = v.get("schema_valid")
36
+ errors = v.get("errors") or []
37
+ valid_final = bool(schema_valid)
38
+ elif not valid_final:
39
+ errors = [{"message": "unrepaired_or_invalid"}]
40
+ return {
41
+ "valid_original": valid_original,
42
+ "repaired": repaired,
43
+ "json": payload,
44
+ "valid_final": valid_final and payload is not None and (schema_valid is not False),
45
+ "schema_valid": schema_valid,
46
+ "unsafe_or_ambiguous": unsafe,
47
+ "changes": changes,
48
+ "errors": errors,
49
+ "inspect": insp,
50
+ "processing_ms": sw.ms,
51
+ "outcome_class": _outcome(valid_original, repaired, payload, schema_valid, errors),
52
+ }
53
+
54
+
55
+ def _outcome(valid_original, repaired, payload, schema_valid, errors) -> str:
56
+ if schema_valid is False:
57
+ return "SCHEMA_INVALID"
58
+ if schema_valid is True:
59
+ return "SCHEMA_VALID"
60
+ if valid_original:
61
+ return "ORIGINAL_VALID"
62
+ if repaired and payload is not None:
63
+ return "SAFE_REPAIR_SUCCESS"
64
+ if any((e.get("message") or "").startswith("schema") for e in errors or []):
65
+ return "INVALID_SCHEMA"
66
+ return "SAFE_REPAIR_REFUSED"
@@ -0,0 +1,237 @@
1
+ from __future__ import annotations
2
+
3
+ import ast
4
+ import json
5
+ import re
6
+ from typing import Any
7
+
8
+ from products.common.timing import Clock
9
+
10
+ _FENCE = re.compile(r"```(?:json|JSON)?\s*([\s\S]*?)```", re.MULTILINE)
11
+ _TRAIL_COMMA = re.compile(r",(\s*[}\]])")
12
+ _COMMENT_LINE = re.compile(r"(^|[^:])//.*?$", re.MULTILINE)
13
+ _COMMENT_BLOCK = re.compile(r"/\*[\s\S]*?\*/")
14
+
15
+
16
+ def _extract_fenced(text: str, changes: list[str]) -> str:
17
+ m = _FENCE.search(text)
18
+ if m:
19
+ changes.append("strip_markdown_fence")
20
+ return m.group(1).strip()
21
+ return text
22
+
23
+
24
+ def _extract_balanced(text: str, changes: list[str]) -> str:
25
+ starts = [i for i, ch in enumerate(text) if ch in "{["]
26
+ if not starts:
27
+ return text.strip()
28
+ first = starts[0]
29
+ chunk = text[first:]
30
+ if first > 0 and text[:first].strip():
31
+ changes.append("strip_leading_prose")
32
+ depth_obj = depth_arr = 0
33
+ in_str = False
34
+ esc = False
35
+ last = -1
36
+ for i, ch in enumerate(chunk):
37
+ if in_str:
38
+ if esc:
39
+ esc = False
40
+ elif ch == "\\":
41
+ esc = True
42
+ elif ch == '"':
43
+ in_str = False
44
+ continue
45
+ if ch == '"':
46
+ in_str = True
47
+ continue
48
+ if ch == "{":
49
+ depth_obj += 1
50
+ elif ch == "}":
51
+ depth_obj -= 1
52
+ elif ch == "[":
53
+ depth_arr += 1
54
+ elif ch == "]":
55
+ depth_arr -= 1
56
+ if depth_obj == 0 and depth_arr == 0 and ch in "}]":
57
+ last = i
58
+ break
59
+ if last >= 0:
60
+ tail = chunk[last + 1 :]
61
+ st = tail.strip()
62
+ if st.startswith("{") or st.startswith("["):
63
+ changes.append("multiple_json_values")
64
+ return chunk
65
+ if st:
66
+ changes.append("strip_trailing_prose")
67
+ return chunk[: last + 1]
68
+ return chunk
69
+
70
+
71
+ def _strip_comments(text: str, changes: list[str]) -> str:
72
+ if "//" not in text and "/*" not in text:
73
+ return text
74
+ nxt = _COMMENT_BLOCK.sub("", text)
75
+ nxt = _COMMENT_LINE.sub(lambda m: m.group(1), nxt)
76
+ if nxt != text:
77
+ changes.append("strip_comments")
78
+ return nxt
79
+
80
+
81
+ def _py_literals(text: str, changes: list[str]) -> str:
82
+ nxt = re.sub(r"\bTrue\b", "true", text)
83
+ nxt = re.sub(r"\bFalse\b", "false", nxt)
84
+ nxt = re.sub(r"\bNone\b", "null", nxt)
85
+ if nxt != text:
86
+ changes.append("python_literals")
87
+ return nxt
88
+
89
+
90
+ def _trailing_commas(text: str, changes: list[str]) -> str:
91
+ nxt, n = _TRAIL_COMMA.subn(r"\1", text)
92
+ if n:
93
+ changes.append("strip_trailing_commas")
94
+ return nxt
95
+
96
+
97
+ def _close_truncated(text: str, changes: list[str]) -> str | None:
98
+ """Close extra { [ only when the remainder is empty of required values."""
99
+ in_str = False
100
+ esc = False
101
+ stack: list[str] = []
102
+ last_sig = ""
103
+ for ch in text:
104
+ if in_str:
105
+ if esc:
106
+ esc = False
107
+ elif ch == "\\":
108
+ esc = True
109
+ elif ch == '"':
110
+ in_str = False
111
+ continue
112
+ if ch == '"':
113
+ in_str = True
114
+ last_sig = '"'
115
+ continue
116
+ if ch in "{[":
117
+ stack.append("}" if ch == "{" else "]")
118
+ last_sig = ch
119
+ elif ch in "}]":
120
+ if not stack or stack[-1] != ch:
121
+ return None
122
+ stack.pop()
123
+ last_sig = ch
124
+ elif ch == ":":
125
+ last_sig = ":"
126
+ elif ch == ",":
127
+ last_sig = ","
128
+ elif not ch.isspace():
129
+ last_sig = "v"
130
+ if in_str:
131
+ return None
132
+ if not stack:
133
+ return None
134
+ if last_sig in {":"}:
135
+ return None # would invent a value
136
+ closed = text + "".join(reversed(stack))
137
+ changes.append("close_truncated_brackets")
138
+ return closed
139
+
140
+
141
+ def _try_load(text: str) -> Any:
142
+ return json.loads(text)
143
+
144
+
145
+ def _literal_eval_jsonish(text: str) -> Any | None:
146
+ try:
147
+ val = ast.literal_eval(text)
148
+ except (SyntaxError, ValueError, MemoryError):
149
+ return None
150
+ if isinstance(val, (dict, list, str, int, float, bool)) or val is None:
151
+ return val
152
+ return None
153
+
154
+
155
+ def repair_json(text: str, mode: str = "safe") -> dict[str, Any]:
156
+ with Clock() as sw:
157
+ changes: list[str] = []
158
+ if not isinstance(text, str):
159
+ return _fail(sw, "text_not_string", changes)
160
+ if len(text.encode("utf-8")) > 262_144:
161
+ return _fail(sw, "too_large", changes)
162
+ raw = text.strip().lstrip("\ufeff")
163
+ if raw != text.strip() or text[:1] == "\ufeff" or text != text.strip():
164
+ changes.append("strip_bom_or_whitespace")
165
+ if not raw:
166
+ return _fail(sw, "empty", changes)
167
+
168
+ try:
169
+ parsed = _try_load(raw)
170
+ return _ok(parsed, bool(changes), changes, "high", False, sw)
171
+ except json.JSONDecodeError:
172
+ pass
173
+
174
+ work = _extract_fenced(raw, changes)
175
+ work = _extract_balanced(work, changes)
176
+ if "multiple_json_values" in changes:
177
+ return _fail(sw, "multiple_json_values", changes, unsafe=True)
178
+ work = _strip_comments(work, changes)
179
+
180
+ looks_py = ("True" in work or "False" in work or "None" in work or ("'" in work and '"' not in work))
181
+ if looks_py:
182
+ lit = _literal_eval_jsonish(work)
183
+ if lit is not None:
184
+ changes.append("python_literal_eval")
185
+ return _ok(lit, True, changes, "medium", False, sw)
186
+
187
+ work = _py_literals(work, changes)
188
+ work = _trailing_commas(work, changes)
189
+
190
+ try:
191
+ parsed = _try_load(work)
192
+ return _ok(parsed, True, changes, "high", False, sw)
193
+ except json.JSONDecodeError:
194
+ pass
195
+
196
+ closed = _close_truncated(work, changes)
197
+ if closed is not None:
198
+ closed = _trailing_commas(closed, changes)
199
+ try:
200
+ parsed = _try_load(closed)
201
+ return _ok(parsed, True, changes, "medium", False, sw)
202
+ except json.JSONDecodeError:
203
+ changes[:] = [c for c in changes if c != "close_truncated_brackets"]
204
+
205
+ decoder = json.JSONDecoder()
206
+ try:
207
+ first, idx = decoder.raw_decode(work)
208
+ rest = work[idx:].strip()
209
+ if rest:
210
+ return _fail(sw, "multiple_json_values", changes, unsafe=True)
211
+ return _ok(first, True, changes, "medium", False, sw)
212
+ except json.JSONDecodeError:
213
+ pass
214
+
215
+ return _fail(sw, "unambiguous_repair_impossible", changes, unsafe=True)
216
+
217
+
218
+ def _ok(parsed, repaired, changes, confidence, unsafe, sw):
219
+ return {
220
+ "repaired": repaired,
221
+ "json": parsed,
222
+ "changes": changes,
223
+ "confidence": confidence,
224
+ "unsafe_or_ambiguous": unsafe,
225
+ "processing_ms": sw.ms,
226
+ }
227
+
228
+
229
+ def _fail(sw, reason: str, changes: list[str], unsafe: bool = False):
230
+ return {
231
+ "repaired": False,
232
+ "json": None,
233
+ "changes": changes + [reason],
234
+ "confidence": "low",
235
+ "unsafe_or_ambiguous": True if unsafe else False,
236
+ "processing_ms": sw.ms,
237
+ }