Packages

A blazingly fast Elixir library for repairing malformed JSON using binary pattern matching. Handles LLM outputs, legacy data, and broken JSON with intelligent context-aware fixes.

Current section

14 Versions

Jump to

Compare versions

5 files changed
+116 additions
-27 deletions
  @@ -7,6 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7 7
8 8 ## [Unreleased]
9 9
10 + ## [0.1.9] - 2025-10-28
11 +
12 + ### Fixed
13 + - **Multi-item arrays**: Layer 2 now closes missing braces and brackets between array elements, avoiding mismatched delimiter swaps during comma handling and restoring proper validation (#8).
14 +
15 + ### Added
16 + - **Regression coverage**: Added LF and CRLF variants of the reporter's samples to ensure multi-element arrays with missing terminators stay repaired.
17 +
10 18 ## [0.1.8] - 2025-10-27
11 19
12 20 ### Added
  @@ -340,7 +348,8 @@ This is a **100% rewrite** - all previous code has been replaced with the new la
340 348 - Minimal memory overhead (< 8KB for repairs)
341 349 - All operations pass performance thresholds
342 350
343 - [Unreleased]: https://github.com/nshkrdotcom/json_remedy/compare/v0.1.8...HEAD
351 + [Unreleased]: https://github.com/nshkrdotcom/json_remedy/compare/v0.1.9...HEAD
352 + [0.1.9]: https://github.com/nshkrdotcom/json_remedy/compare/v0.1.8...v0.1.9
344 353 [0.1.8]: https://github.com/nshkrdotcom/json_remedy/compare/v0.1.7...v0.1.8
345 354 [0.1.7]: https://github.com/nshkrdotcom/json_remedy/compare/v0.1.6...v0.1.7
346 355 [0.1.6]: https://github.com/nshkrdotcom/json_remedy/compare/v0.1.5...v0.1.6
  @@ -74,6 +74,7 @@ Runs **before** the main layer pipeline to handle complex patterns that would ot
74 74
75 75 ### 🏗️ **Structural Repairs (Layer 2)**
76 76 - **Missing closing delimiters**: `{"name": "Alice"``{"name": "Alice"}`
77 + - **Array element terminators**: Multi-item arrays recover missing braces/brackets between elements *(v0.1.9)*
77 78 - **Extra delimiters**: `{"name": "Alice"}}}``{"name": "Alice"}`
78 79 - **Mismatched delimiters**: `[{"name": "Alice"}]` → proper structure
79 80 - **Missing opening braces**: `["key": "value"]``[{"key": "value"}]`
  @@ -159,7 +160,7 @@ Add JsonRemedy to your `mix.exs`:
159 160 ```elixir
160 161 def deps do
161 162 [
162 - {:json_remedy, "~> 0.1.8"}
163 + {:json_remedy, "~> 0.1.9"}
163 164 ]
164 165 end
165 166 ```
  @@ -2,7 +2,7 @@
2 2 [{<<"Documentation">>,<<"https://hexdocs.pm/json_remedy">>},
3 3 {<<"GitHub">>,<<"https://github.com/nshkrdotcom/json_remedy">>}]}.
4 4 {<<"name">>,<<"json_remedy">>}.
5 - {<<"version">>,<<"0.1.8">>}.
5 + {<<"version">>,<<"0.1.9">>}.
6 6 {<<"description">>,
7 7 <<"A blazingly fast Elixir library for repairing malformed JSON using binary pattern matching. Handles LLM outputs, legacy data, and broken JSON with intelligent context-aware fixes.">>}.
8 8 {<<"elixir">>,<<"~> 1.14">>}.
  @@ -137,6 +137,9 @@ defmodule JsonRemedy.Layer2.StructuralRepair do
137 137 "]" ->
138 138 handle_closing_bracket(state, char)
139 139
140 + "," ->
141 + handle_comma(state, char)
142 +
140 143 _ ->
141 144 # Regular character, just add to result
142 145 %{state | result_chars: [char | state.result_chars]}
  @@ -288,6 +291,12 @@ defmodule JsonRemedy.Layer2.StructuralRepair do
288 291 end
289 292 end
290 293
294 + @spec handle_comma(state :: state_map(), char :: <<_::8>>) :: state_map()
295 + defp handle_comma(state, char) do
296 + adjusted_state = maybe_close_contexts_before_separator(state)
297 + %{adjusted_state | result_chars: [char | adjusted_state.result_chars]}
298 + end
299 +
291 300 @spec determine_state_from_stack(stack :: [context_frame()]) :: parser_state()
292 301 defp determine_state_from_stack([]), do: :root
293 302 defp determine_state_from_stack([%{type: :brace} | _]), do: :object
  @@ -344,29 +353,7 @@ defmodule JsonRemedy.Layer2.StructuralRepair do
344 353 state.context_stack
345 354 |> Enum.reduce({[], []}, fn context_frame, {chars_acc, repairs_acc} ->
346 355 {close_char, repair} =
347 - case context_frame.type do
348 - :brace ->
349 - repair = %{
350 - layer: :structural_repair,
351 - action: "added missing closing brace",
352 - position: state.position + length(chars_acc),
353 - original: nil,
354 - replacement: "}"
355 - }
356 -
357 - {"}", repair}
358 -
359 - :bracket ->
360 - repair = %{
361 - layer: :structural_repair,
362 - action: "added missing closing bracket",
363 - position: state.position + length(chars_acc),
364 - original: nil,
365 - replacement: "]"
366 - }
367 -
368 - {"]", repair}
369 - end
356 + closing_info(context_frame.type, state.position + length(chars_acc))
370 357
371 358 {[close_char | chars_acc], [repair | repairs_acc]}
372 359 end)
  @@ -380,6 +367,98 @@ defmodule JsonRemedy.Layer2.StructuralRepair do
380 367 }
381 368 end
382 369
370 + @spec maybe_close_contexts_before_separator(state_map()) :: state_map()
371 + defp maybe_close_contexts_before_separator(state) do
372 + next_char = next_significant_char(state)
373 +
374 + cond do
375 + next_char in [nil, "\""] ->
376 + state
377 +
378 + requires_array_boundary_closure?(state.context_stack) ->
379 + close_contexts_until_array(state)
380 +
381 + true ->
382 + state
383 + end
384 + end
385 +
386 + @spec next_significant_char(state_map()) :: String.t() | nil
387 + defp next_significant_char(state) do
388 + remaining_length = String.length(state.input) - state.position - 1
389 +
390 + if remaining_length <= 0 do
391 + nil
392 + else
393 + state.input
394 + |> String.slice(state.position + 1, remaining_length)
395 + |> String.graphemes()
396 + |> Enum.find(fn char ->
397 + char not in [" ", "\t", "\n", "\r"]
398 + end)
399 + end
400 + end
401 +
402 + @spec requires_array_boundary_closure?([context_frame()]) :: boolean()
403 + defp requires_array_boundary_closure?(stack) do
404 + case Enum.find_index(stack, &match?(%{type: :bracket}, &1)) do
405 + nil -> false
406 + 0 -> false
407 + _ -> true
408 + end
409 + end
410 +
411 + @spec close_contexts_until_array(state_map()) :: state_map()
412 + defp close_contexts_until_array(state) do
413 + {to_close, remaining} =
414 + Enum.split_while(state.context_stack, fn %{type: type} -> type != :bracket end)
415 +
416 + if Enum.empty?(to_close) do
417 + state
418 + else
419 + {result_chars, repairs} =
420 + Enum.with_index(to_close)
421 + |> Enum.reduce({state.result_chars, state.repairs}, fn {%{type: type}, idx},
422 + {chars_acc, repairs_acc} ->
423 + {close_char, repair} = closing_info(type, state.position + idx)
424 + {[close_char | chars_acc], [repair | repairs_acc]}
425 + end)
426 +
427 + %{
428 + state
429 + | context_stack: remaining,
430 + current_state: determine_state_from_stack(remaining),
431 + result_chars: result_chars,
432 + repairs: repairs
433 + }
434 + end
435 + end
436 +
437 + @spec closing_info(delimiter_type(), non_neg_integer()) :: {String.t(), repair_action()}
438 + defp closing_info(:brace, position) do
439 + repair = %{
440 + layer: :structural_repair,
441 + action: "added missing closing brace",
442 + position: position,
443 + original: nil,
444 + replacement: "}"
445 + }
446 +
447 + {"}", repair}
448 + end
449 +
450 + defp closing_info(:bracket, position) do
451 + repair = %{
452 + layer: :structural_repair,
453 + action: "added missing closing bracket",
454 + position: position,
455 + original: nil,
456 + replacement: "]"
457 + }
458 +
459 + {"]", repair}
460 + end
461 +
383 462 # LayerBehaviour callback implementations
384 463
385 464 @doc """
  @@ -1,7 +1,7 @@
1 1 defmodule JsonRemedy.MixProject do
2 2 use Mix.Project
3 3
4 - @version "0.1.8"
4 + @version "0.1.9"
5 5 @source_url "https://github.com/nshkrdotcom/json_remedy"
6 6
7 7 def project do