Streaming JSON repair and partial parsing in pure MoonBit
| Module | zhanshishan/jsonmend |
| Version | 0.5.0 |
| License | Apache-2.0 |
| Preferred target | wasm (also builds for native and js) |
| Prebuilt binaries | Release v0.5.0 — Linux CLI, wasm probe, browser bundle, SHA256SUMS |
| Registry | mooncakes.io zhanshishan/jsonmend |
let plan = @jsonmend.analyze(text,
options=@jsonmend.Options::for_agent(),
schema=@jsonmend.Schema::parse("{!query:string,!limit:integer}"))
plan.decision // Safe | CosmeticRepair | LossyRepair | Ambiguous | Invalid
plan.is_executable() // the only question a gate has to ask
plan.repairs // rule, action, position, confidence class, information loss
plan.prompt() // ready to feed back to the model when it is not executable
plan.to_json() // machine channel: decision/exactness/value/retry/repairsanalyze never raises — a refusal is part of the return value — and costs 1.00–1.03× a plain
repair call.moon add zhanshishan/jsonmendimport {
"zhanshishan/jsonmend" @jsonmend,
}try {
let result = @jsonmend.repair("{'a': 1,}") // RepairResult
println(result.text) // {"a":1}
println(result.repairs.length()) // number of repair records
} catch {
e => println(e.to_string())
}try {
let p = @jsonmend.PartialParser::new()
p.feed("{\"name\":\"Mo")
let s1 = p.snapshot()
match s1.value {
Some(v) => println(v.stringify()) // {"name":"Mo"}
None => println("<none>")
}
println(s1.completeness is @jsonmend.Partial) // true
p.feed("n\"}")
let done = p.finish()
println(done.text) // {"name":"Mon"}
println(done.completeness is @jsonmend.Complete) // true
} catch {
e => println(e.to_string())
}try {
let p = @jsonmend.PartialParser::new(options={
..@jsonmend.Options::default(),
incomplete_number: @jsonmend.TrimToValid,
pending_key: @jsonmend.NullKey,
})
p.feed_bytes(b"[1, 1.")
let s = p.snapshot()
// s.value is [1,1]: the unfinished number is trimmed to its longest valid prefix.
p.feed_bytes(b"5]")
let done = p.finish()
println(done.text) // [1,1.5]
} catch {
e => println(e.to_string())
}== first attempt
model : {"city":["Paris" "London"]}
gate : E011 adjacent_strings - the gate cannot pick a reading
refused : the gate refused to pick a reading
prompt :
JSONMend refused the arguments: E011 R006 InsertComma at line 1 column 18 (offset 17).
Reason: two adjacent strings may be two elements or one concatenated string
Ambiguity: adjacent_strings
Re-emit the arguments as valid JSON with exactly one reading.
== re-emitted
model : {"city":["Paris","London"]}
execute : {"city":["Paris","London"]}| Field | Type | Default | Meaning |
|---|---|---|---|
| mode | Mode | Lenient | Lenient records repairs; Strict raises E010 at the first one. |
| max_depth | Int32 | 512 | Maximum container nesting; exceeding it raises E020. |
| max_input_len | Int32 | 67108864 | Maximum input length in UTF-16 code units; exceeding it raises E021. |
| max_token_len | Int32 | 16777216 | Maximum length of one token in code units; exceeding it raises E022. |
| duplicate_keys | DuplicateKeys | KeepLast | KeepLast, KeepFirst, or AsError for duplicate object keys (R018). |
| incomplete_number | IncompleteNumber | Drop | Snapshot policy for an unfinished number: Drop or TrimToValid. |
| pending_key | PendingKey | OmitKey | Snapshot policy for a dangling key: OmitKey or NullKey. |
| non_finite | NonFinite | AsNull | NaN/Infinity/-Infinity become AsNull, AsString, or AsError (R012). |
| strip_fence | Bool | true | Strip Markdown code fences (R001). |
| collect_repairs | Bool | true | Record repairs in the result; when false the array stays empty, but strict mode and disabled rules still raise. |
| want_text | Bool | true | Produce repaired text in FinalResult.text; when false only the Json value is built. |
| skip_prose | Bool | false | Skip leading prose before the first { or [ (R021). |
| quote_lookahead | Bool | false | Enable quote-ambiguity lookahead for missing commas between strings (R003). |
| multi_root | MultiRoot | KeepFirst | Policy for content after the first root value: KeepFirst, AsArray, or AsError (R016). |
| rules | RuleSet | RuleSet::all() | Per-rule switches; disabling a rule makes that code raise E010. |
| indent | Int32 | 0 | Output indentation in spaces; 0 renders compact JSON. |
| max_repairs | Int32 | 10000 | Maximum number of repair records kept; beyond it repairs still apply but the result sets repairs_truncated. |
| reject_ambiguous | Bool | false | Refuse a repair that admits a second reading (E011) instead of applying it. Options::for_execution() sets it. |
| max_loss | Exactness | Lossy | Highest acceptable exactness. The first record that exceeds it raises E010 with that code and position. Options::for_execution() sets Cosmetic. |
| collect_deltas | Bool | false | Collect incremental deltas; read them with PartialParser::drain_deltas(). Cannot be combined with duplicate_keys: KeepFirst or incomplete_number: TrimToValid. |
| Code | Name | Meaning |
|---|---|---|
| R001 | StripFence | Remove a Markdown code fence. |
| R002 | StripComment | Remove a line or block comment. |
| R003 | NormalizeQuote | Normalize a quote to a double quote. |
| R004 | QuoteBareWord | Quote a bare word (Info in key position, Warning in value position). |
| R005 | RemoveExtraComma | Remove a trailing, leading, or repeated comma. |
| R006 | InsertComma | Insert a missing comma. |
| R007 | InsertColon | Insert a missing colon. |
| R008 | FillNull | Fill null for a missing value. |
| R009 | CloseContainer | Close an unclosed array or object. |
| R010 | CloseString | Close an unclosed string. |
| R011 | FixNumber | Normalize or trim a number. |
| R012 | ConvertLiteral | Convert or complete a literal (True, None, undefined, non-finite values). |
| R013 | EscapeControl | Escape a control character inside a string. |
| R014 | FixEscape | Repair an invalid or truncated escape, or a lone surrogate. |
| R015 | MismatchedCloser | Replace a closer whose type does not match the open container. |
| R016 | DropUnexpected | Drop unrecognized or surplus content, including trailing content after the root value. |
| R017 | DropExtraCloser | Drop a surplus closer. |
| R018 | MergeDuplicateKey | Merge a duplicate key according to duplicate_keys. |
| R019 | ConcatStrings | Merge adjacent string concatenation (["a" + "b"] becomes ["ab"]). |
| R020 | DropEllipsis | Drop an ... placeholder in a value position. |
| R021 | SkipProse | Skip leading prose before the JSON value. |
| R022 | NormalizeLiteral | Re-case a literal (True, NULL) without changing which value it denotes. Info when only the case changes; Warning when characters are also added or dropped (R012). |
| Code | Error | Raised when |
|---|---|---|
| E001 | EmptyInput | The input is empty or whitespace only. |
| E002 | NoJsonFound | No JSON value can be found (payload is the offset). |
| E010 | StrictViolation | Strict mode, or a disabled rule, rejects a repair (payload is the code, position, and message). |
| E020 | DepthLimit | Nesting exceeds max_depth. |
| E021 | SizeLimit | Input exceeds max_input_len. |
| E022 | TokenLimit | A token exceeds max_token_len. |
| E030 | InvalidState | feed after finish, or finish called twice or after a fatal error. |
| E040 | InvalidUtf8 | Invalid or truncated UTF-8 in feed_bytes (payload is the byte offset). |
| E050 | InvalidOption | An option value is out of range, or a combination is invalid (collect_deltas with KeepFirst/TrimToValid). |
| E060 | InvalidEvent | jsonmend/llm received an event payload it cannot interpret. |
moon build cmd/jsonmend --target native --release
echo "{'a': 1,}" | ./_build/native/release/build/cmd/jsonmend/jsonmend.exejsonmend [选项] [文件]
读取文件或标准输入,把修复后的 JSON 写到标准输出。
--strict 严格模式:需要修复就报错(退出码 5)
--check 只检查,不输出;需要修复时退出码 1
--report 把修复记录写到标准错误
--indent N 输出缩进空格数,默认紧凑输出
--multi-root MODE first、array 或 error,默认 first
--skip-prose 跳过 JSON 之前的说明文字
--quote-lookahead 启用引号歧义前瞻
--max-repairs N 最多记录多少条修复记录,默认 10000
--max-loss LEVEL 精确度上限:exact、cosmetic 或 lossy,默认 lossy
--for-execution 执行档:等价于 --max-loss cosmetic --reject-ambiguous
--reject-ambiguous 拒绝有第二种读法的修复,报 E011(退出码 5)
--retry-hint 拒绝时在标准错误上打印可直接回灌模型的提示
--chunk N 分块喂入:每次喂 N 个码元(≤1048576),用来验证流式路径(默认一次喂完)
--explain 在标准错误上逐条解释修复,并打印精确度小结
--batch 逐行读取 {"id","input"} JSON,逐行输出结果 JSON
与 --chunk N 同用可把每条输入按 N 字节分块喂入
jsonmend readiness [选项] [文件]
打印字段就绪曲线:每个顶级字段在输入消耗到多少时已定稿。
--step N 每 N 个码元观察一次,默认按长度自适应
jsonmend llm [选项] [文件]
把模型的事件流喂给 provider 适配器,每个工具调用输出一行结果 JSON。
--provider NAME anthropic(默认)或 openai
--sse 输入是 SSE 文本(默认按每行一个事件 JSON 读取)
--text 读取文本增量(delta.content / text_delta)而不是工具调用参数(配 --skip-prose 可跳过前言)
--follow 每次增量都输出一行,而不是只在结束时输出
-h, --help 显示帮助
--version 显示版本
文件为 `-` 或缺省时读取标准输入。
退出码:0 成功(可能有修复);1 --check 发现需要修复;2 用法错误;
3 输入无效;4 资源限制;5 严格模式违规;70 内部错误。| Flag | Meaning |
|---|---|
| --strict | Strict mode: any needed repair is an error (exit code 5). |
| --check | Check only, no output; exit code 1 when repairs are needed. |
| --report | Write the repair records to standard error. |
| --indent N | Output indentation in spaces (0 to 64); the default is compact output. |
| --multi-root MODE | first, array, or error; the default is first. |
| --skip-prose | Skip prose before the JSON value. |
| --quote-lookahead | Enable quote-ambiguity lookahead. |
| --max-repairs N | Maximum number of repair records to keep (default 10000); a truncated run also prints a note to standard error. |
| --max-loss LEVEL | Exactness ceiling: exact, cosmetic, or lossy (default). Exceeding it exits with code 5. |
| --explain | Print every record with severity, position and a caret snippet, plus an exactness summary, to standard error. |
| --batch | Read {"id","input"} objects line by line and write one result object per line. |
| llm (subcommand) | Feed a provider event stream through the adapter: jsonmend llm [--provider anthropic\|openai] [--sse] [--follow] [file]. |
| -h, --help | Show help. |
| --version | Show the version (jsonmend 0.5.0). |
| --policy NAME | Safety profile: permissive, interactive, execution, agent or strict. |
| --schema DECL | Skeleton declaration such as {!query:string,!limit:integer}; with explain it also prints the syntax-ready and schema-ready fractions. |
| --json | With explain: the machine-readable plan instead of the text block. |
| file, or - | Read a file; - or an omitted argument reads standard input. |
| Code | Meaning |
|---|---|
| 0 | Success; the output may still contain repairs. |
| 1 | --check found that repairs are needed. |
| 2 | Usage error (unknown flag, bad option value, or E050). |
| 3 | Invalid input (E001/E002/E040/E060, or an unreadable file). |
| 4 | Resource limit reached (E020/E021/E022). |
| 5 | Strict-mode violation (E010, including a --max-loss breach). |
| 70 | Internal error (E030). |
cd code
moon test # default backend (wasm-gc)
moon test --target native # host backend
moon test --target js # JavaScript backend
bash tests/cli_smoke.sh # 62 command line assertions
bash tests/examples.sh # example inputs vs code/examples/cases/*.expected
node ../code/tests/demo_smoke.mjs # the committed browser bundle still answers
python3 tests/diff/run_diff.py --cli ./_build/native/release/build/cmd/jsonmend/jsonmend.exe
python3 tests/eval/run_eval.py --cli ./_build/native/release/build/cmd/jsonmend/jsonmend.exe
python3 tests/eval/run_eval_paths.py --cli ./_build/native/release/build/cmd/jsonmend/jsonmend.exe
python3 tests/eval/check_amb_guard.py # the adversarial suite's guard still fires
python3 tests/eval/run_ambiguity.py --cli ./_build/native/release/build/cmd/jsonmend/jsonmend.exe
python3 tests/eval/refcompare.py --cli ./_build/native/release/build/cmd/jsonmend/jsonmend.exe
python3 tests/gen_llm_fixtures.py --check
python3 tests/memory_baseline.py --cli ./_build/native/release/build/cmd/jsonmend/jsonmend.exe
python3 tests/eval/run_independent.py # vendored nst/JSONTestSuite, independent of our own corpus
python3 tests/eval/run_readiness.py --cli ./_build/native/release/build/cmd/jsonmend/jsonmend.exe --check
moon run examples/agent_loop --target native --release| Suite | Size | Result (0.5.0) |
|---|---|---|
| Golden cases (spec appendices A and B) | 126 | pass |
| Documented streaming cases (S-001…S-021) + chunk invariance | 21 + invariance checks | pass |
| Property and fuzz tests (TC-P-01…TC-P-06) | 8 groups × 400 cases (JSONMEND_LONGRUN raises it; nightly runs 1 000 000) | pass, no counterexample |
| Exactness labels | hand-written table + 240 property cases + the max_loss gate | pass |
| Incremental deltas (replay == snapshot) | 400 generated + 400 mutated documents | pass |
| LLM adapters (both providers, 9 generated fixtures + documented event shapes) | 30 tests | pass |
| Adapter path against the ground-truth corpus | 1200 cases × (both providers × tool args and text) | 1200/1200 on all four (docs/EVAL_PATHS.md) |
| Adversarial ambiguity suite amb-1 | 24 handwritten cases, frozen fingerprint | 10/10 flagged and refused, 14/14 controls pass, 0 failures (docs/EVAL_AMBIGUITY.md) |
| Reference libraries on the same corpus | 1200 cases × (jsonrepair 3.15.0, json_repair 0.64.0) | JSONMend silent wrong values 0, jsonrepair 122, json_repair 189 (docs/EVAL_REFERENCE.md) |
| Chunked paths against the ground-truth corpus | 1200 cases × chunk 1/3/7/random | 1200/1200 each, 0 regressions |
| Safety profiles, repair plans, schemas (new in 0.5.0) | 34 tests (7 + 22 + 5) | pass |
| Independent third-party corpus (nst/JSONTestSuite) | 318 files (95 y_ / 188 n_ / 35 i_) | y_ false repairs 2/95 (both duplicate keys), n_ valid values 165/188 vs json_repair 166/188 and jsonrepair 144/188 (docs/EVAL_INDEPENDENT.md) |
| Schema readiness table | 7 scenarios | 4 with a gap (up to 713‰), 3 where both fractions arrive at the end (docs/READINESS.md) |
| Public surface (every R code and E code) | 22 + 12 codes checked field by field | pass |
| Ground-truth evaluation (protocol frozen first) | 1200 cases (19 mutation families + 1 control × 60) | recovery 100.00%, 0 mislabelled, 4/4 pre-registered criteria PASS |
| Command line smoke (exit codes 0/1/2/3/4/5/70) | 62 assertions, 14 of them explain | pass |
| Differential corpus against jsonrepair / json-repair | 402 cases | 202 divergences, all documented, 0 unexplained |
| Line coverage of the library | 2312/2651 lines (root), 337/391 (llm) | 87.2% / 86.2% (moon coverage cannot instrument the cmd package, which calls quit(); the new modules widen the denominator, plan.mbt at 71.5% is the weakest file) |
moon run bench --target native --release
moon run bench --target js --releasepub(all) suberror MendError {
EmptyInput
NoJsonFound(Int)
StrictViolation(RepairCode, Position, String)
AmbiguousRepair(RepairCode, Position, Ambiguity)
DepthLimit(Int, Int)
SizeLimit(Int, Int)
TokenLimit(Int, Int)
InvalidState(String)
InvalidUtf8(Int)
InvalidOption(String)
InvalidEvent(String)
SchemaError(String)
}pub(all) enum Ambiguity {
Unambiguous
BareWord
AdjacentStrings
ConcatOperator
TrailingContent
}pub(all) enum Completeness {
Empty
Partial
Complete
}impl Eq for Completenesspub(all) enum DeltaOp {
Set
AppendText
Close
}pub(all) enum DuplicateKeys {
KeepLast
KeepFirst
AsError
}impl Eq for DuplicateKeyspub(all) enum Exactness {
Exact
Cosmetic
Lossy
}pub(all) enum ExecutionDecision {
Safe
CosmeticRepair
LossyRepair
Ambiguous
Invalid
}impl Eq for ExecutionDecisionfn ExecutionDecision::worse_of(self : ExecutionDecision, other : ExecutionDecision) -> ExecutionDecisionpub(all) struct FieldReady {
key : String
permille : Int
}impl Eq for FieldReadypub struct FieldTracker {
// private fields
}pub(all) struct FinalResult {
text : String
value : Json
repairs : Array[Repair]
completeness : Completeness
repairs_truncated : Bool
exactness : Exactness
ambiguous : Bool
}pub(all) enum IncompleteNumber {
Drop
TrimToValid
}impl Eq for IncompleteNumberpub(all) enum Mode {
Lenient
Strict
}pub(all) enum MultiRoot {
KeepFirst
AsArray
AsError
}pub(all) enum NonFinite {
AsNull
AsError
AsString
}pub(all) struct Options {
mode : Mode
max_depth : Int
max_input_len : Int
max_token_len : Int
duplicate_keys : DuplicateKeys
incomplete_number : IncompleteNumber
pending_key : PendingKey
non_finite : NonFinite
strip_fence : Bool
collect_repairs : Bool
want_text : Bool
skip_prose : Bool
quote_lookahead : Bool
multi_root : MultiRoot
rules : RuleSet
indent : Int
max_repairs : Int
max_loss : Exactness
collect_deltas : Bool
reject_ambiguous : Bool
}pub(all) struct ParseResult {
value : Json
repairs : Array[Repair]
completeness : Completeness
repairs_truncated : Bool
exactness : Exactness
ambiguous : Bool
}pub struct PartialParser {
// private fields
}fn PartialParser::feed_bytes_with(self : PartialParser, bytes : Bytes, on_delta : (Delta) -> Unit) -> Unit raise MendErrorfn PartialParser::feed_with(self : PartialParser, chunk : String, on_delta : (Delta) -> Unit) -> Unit raise MendErrorpub(all) enum PathSegment {
Key(String)
Index(Int)
}impl Eq for PathSegmentpub(all) enum PendingKey {
OmitKey
NullKey
}impl Eq for PendingKeypub(all) enum PlanAction {
Remove
Insert
Replace
Normalize
Merge
Wrap
}impl Eq for PlanActionpub(all) struct PlanOp {
rule : String
name : String
action : PlanAction
position : Position
severity : Severity
ambiguity : Ambiguity
confidence_class : String
confidence : Double
}pub(all) struct Position {
offset : Int
line : Int
column : Int
}pub(all) enum ReadinessEvent {
FieldReady(String)
FieldInvalid(String, String)
FieldIncomplete(String, String)
RootReady
RootInvalid(Int)
}pub(all) struct Repair {
code : RepairCode
position : Position
severity : Severity
message : String
ambiguity : Ambiguity
}pub(all) enum RepairCode {
StripFence
StripComment
NormalizeQuote
QuoteBareWord
RemoveExtraComma
InsertComma
InsertColon
FillNull
CloseContainer
CloseString
FixNumber
ConvertLiteral
EscapeControl
FixEscape
MismatchedCloser
DropUnexpected
DropExtraCloser
MergeDuplicateKey
ConcatStrings
DropEllipsis
SkipProse
NormalizeLiteral
}impl Eq for RepairCodepub(all) struct RepairPlan {
text : String
value : Json?
repairs : Array[PlanOp]
repairs_truncated : Bool
exactness : Exactness
valid_json : Bool
ambiguous : Bool
completeness : Completeness
decision : ExecutionDecision
retry_hint : RetryHint?
error_code : String
error_message : String
readiness_permille : Int
schema_total : Int
schema_satisfied : Int
schema_missing : Array[String]
}pub(all) struct RepairResult {
text : String
repairs : Array[Repair]
completeness : Completeness
repairs_truncated : Bool
exactness : Exactness
ambiguous : Bool
}pub(all) struct RuleSet {
// private fields
}pub(all) struct SafetyPolicy {
profile : String
mode : Mode
max_loss : Exactness
reject_ambiguous : Bool
duplicate_keys : DuplicateKeys
incomplete_number : IncompleteNumber
pending_key : PendingKey
multi_root : MultiRoot
max_depth : Int
max_input_len : Int
rules : RuleSet
}impl Eq for SchemaMemberpub(all) struct SchemaMismatch {
path : String
expected : String
got : String
}pub(all) struct SchemaVerdict {
total : Int
satisfied : Int
missing : Array[String]
mismatched : Array[SchemaMismatch]
ok : Bool
}pub(all) struct SemanticReadiness {
syntax_permille : Int
schema_permille : Int
settled_permille : Int
step : Int
verdict : SchemaVerdict
}pub struct SemanticWatcher {
settled_count : Int
concluded : Bool
conflict : Bool
checked : Bool
events : Array[ReadinessEvent]
bytes : Int
settled_at : Int
syntax_at : Int
// private fields
}pub(all) enum Severity {
Info
Warning
}pub(all) enum Slot {
KeySlot
ValueSlot
}pub(all) struct Snapshot {
value : Json?
completeness : Completeness
open_path : Array[PathSegment]
exactness : Exactness
ambiguous : Bool
}fn auto_step(n : Int, step : Int) -> Intfn field_readiness(input : String, step? : Int, options? : Options) -> Array[FieldReady] raise MendErrorfn hint_context(text : String, offset : Int, radius : Int) -> (String, Int)fn is_valid_json(text : String) -> Boolfn semantic_readiness(input : String, schema : Schema, step? : Int, options? : Options) -> SemanticReadiness raise MendErrorfn syntax_readiness_permille(input : String, step? : Int) -> Intfn watch_readiness(input : String, schema : Schema, chunk? : Int, options? : Options) -> Array[ReadinessEventAt] raise MendErrorInstall
Download zipStreaming JSON repair and partial parsing in pure MoonBit