Sign in

    jsonmend

    Streaming JSON repair and partial parsing in pure MoonBit

    json
    repair
    streaming
    llm
    parser
    Download zip
    Version
    0.6.1
    License
    Apache-2.0
    Last updated
    4 hours ago
    Downloads
    6

    #JSONMend

    Streaming JSON repair and partial parsing in pure MoonBit.

    Modulezhanshishan/jsonmend
    Version0.5.0
    LicenseApache-2.0
    Preferred targetwasm (also builds for native and js)
    Prebuilt binariesRelease v0.5.0 — Linux CLI, wasm probe, browser bundle, SHA256SUMS
    Registrymooncakes.io zhanshishan/jsonmend

    #Features

    #0.5.0: "may I execute this" in one call

    • SafetyPolicy: five named profiles — permissive, interactive, execution, agent, strict — over the safety-relevant Options. Options::with_policy replaces only the safety knobs and keeps the protocol ones (delta stream, prose handling), and SafetyPolicy::of_options reads a set of options back to the profile it is (or None).
    • analyze returns the whole plan:

      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/repairs

      analyze never raises — a refusal is part of the return value — and costs 1.00–1.03× a plain repair call.
    • Schema + semantic_readiness: declare {!query:string,!limit:integer} and get two fractions for the same stream — syntax ready and schema ready — plus the members that are missing or of the wrong kind.
    • Self-healing loop example: code/examples/agent_loop feeds plan.prompt() back to a scripted "model" and retries; four episodes cover E011 ambiguity, E010 truncation, E070 skeleton mismatch, and giving up when a retry makes no progress.
    • Live demo: static/demo/live.html replays four real scenarios frame by frame (truncated → retry → executed; ambiguous → retry → executed; single reading → executed; long tail → ready at 19%).

    #Everything else

    • One-shot repair with repair: repaired text, a list of repair records, and the completeness of the input.
    • One-shot parsing with parse: the repaired input as a Json tree plus the same records.
    • Streaming partial parsing with PartialParser: feed (or feed_bytes) any number of chunks, take snapshot() at any time, then finish().
    • Repair records for every change: stable code (R001…R022), position (UTF-16 offset plus 1-based line/column), severity, and a fixed English message.
    • Exactness labels: every result carries Exact / Cosmetic / Lossy, derived from the severities of the records. Options::max_loss turns the first repair that exceeds the budget into a fatal E010, so "repair, then execute the arguments" cannot happen silently.
    • Ambiguity gate: a repair that admits a second, equally self-consistent reading (["a" "b"], ["a"+"b"], [fast], {"a":1} {"b":2}) is recorded with an Ambiguity class and refused with E011 under Options::reject_ambiguous / Options::for_execution(), before the exactness gate. 10/10 adversarial cases are flagged and refused, while both reference libraries answer all 10 with a silent value (docs/EVAL_AMBIGUITY.md).
    • Reject as feedback: MendError::retry_hint (and --retry-hint) turns a refusal into a prompt fragment — error code, repair code, position, the second reading, a bounded context window with a caret — that can be fed straight back to the model that produced the input.
    • Field readiness (FR-29): field_readiness reports, for every top-level member, the input fraction at which its value is already final, and FieldTracker does the same from the delta stream, so a caller can stop the generation as soon as the fields it needs are settled. The decile curve replaces the "95% coverage" figure that an object made structurally uninformative.
    • Incremental deltas: with Options::collect_deltas, drain_deltas() returns Set / AppendText / Close operations with RFC 6901 JSON Pointer paths, so a consumer can patch its own view instead of re-reading snapshots.
    • LLM provider adapters in the jsonmend/llm sub-package (experimental): Anthropic fine-grained tool streaming and OpenAI-compatible tool-call deltas, an SSE framer, and a text stream that reads the JSON out of delta.text / delta.content. The end of a call is reported as Complete, MaxTokens, Interrupted or ProviderError with the provider's own stop_reason verbatim; ping and unknown event types are counted and ignored. jsonmend llm on the command line.
    • Strict mode (Mode::Strict) turns the first repair into a fatal E010, and per-rule switches disable individual rules the same way.
    • Resource limits: nesting depth, total input length, and single-token length (E020/E021/E022).
    • Policies for duplicate keys, non-finite numbers, multiple root values, Markdown fences, leading prose, ambiguous quotes, and output indentation.
    • Multi-backend: the library is pure MoonBit with no regex and no recursion; bench runs on native and js.
    • CLI and browser demo: cmd/jsonmend for the command line, examples/web for the browser.

    #Why you need it

    LLM applications stream JSON token by token, and the stream is routinely cut off mid-value, wrapped in a Markdown fence, written with single quotes, or padded with prose and comments. A strict parser rejects the whole payload, so the application shows nothing until the end — and sometimes never. JSONMend keeps the last confirmed prefix available after every chunk and repairs the flaws along the way, so a partial result is usable immediately and the final result is valid JSON. The same engine drives both the one-shot and the streaming paths, so they cannot drift apart.

    #Install

    Add the dependency with the MoonBit toolchain:

    moon add zhanshishan/jsonmend

    Then import the package in your moon.pkg with the alias used throughout this document:

    import { "zhanshishan/jsonmend" @jsonmend, }

    moon add records the dependency in the module manifest (this repository uses code/moon.mod). The exact registry syntax and the current toolchain command follow the mooncakes.io instructions; verify them against the released package page.

    #Quick start

    repair raises MendError, so wrap the calls in try/catch (the three statements below are the whole example):

    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())
    }

    Use @jsonmend.parse instead when you want the Json tree rather than the text; it returns the same repairs and completeness alongside a value. repair returns a RepairResult with the fields text, repairs, and completeness.

    #Streaming example

    PartialParser accepts any number of chunks. A chunk may split any token, including an escape sequence or a surrogate pair.

    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())
    }

    feed_bytes takes raw UTF-8 and can be mixed with feed; invalid or truncated UTF-8 raises E040. Options are passed by keyword:

    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())
    }

    snapshot() returns a Snapshot whose fields are value (Json?), completeness, and open_path; finish() returns a FinalResult with text, value, repairs, and completeness. snapshot() never raises and never mutates the parser; finish() may be called once and raises E030 on a second call or after a fatal error.

    #End-to-end example: refuse, feed back, then execute

    examples/agent_gate/ is a runnable program (moon run examples/agent_gate --target native --release) that walks the whole loop for a tool call whose arguments admit two readings:

    == 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"]}

    Options::for_execution() checks ambiguity before exactness: repairs such as ["Paris" "London"] and "Par" + "is" keep the value the same but admit two readings, so only the ambiguity gate stops them, while a truncation such as "Par is E010. Either refusal turns into a prompt the model can be given again (RetryHint::to_prompt()). Without the gate the same input still repairs, with ambiguous set to true — the gate is opt-in.

    #Options

    Every field of Options. @jsonmend.Options::default() is the documented default set.

    FieldTypeDefaultMeaning
    modeModeLenientLenient records repairs; Strict raises E010 at the first one.
    max_depthInt32512Maximum container nesting; exceeding it raises E020.
    max_input_lenInt3267108864Maximum input length in UTF-16 code units; exceeding it raises E021.
    max_token_lenInt3216777216Maximum length of one token in code units; exceeding it raises E022.
    duplicate_keysDuplicateKeysKeepLastKeepLast, KeepFirst, or AsError for duplicate object keys (R018).
    incomplete_numberIncompleteNumberDropSnapshot policy for an unfinished number: Drop or TrimToValid.
    pending_keyPendingKeyOmitKeySnapshot policy for a dangling key: OmitKey or NullKey.
    non_finiteNonFiniteAsNullNaN/Infinity/-Infinity become AsNull, AsString, or AsError (R012).
    strip_fenceBooltrueStrip Markdown code fences (R001).
    collect_repairsBooltrueRecord repairs in the result; when false the array stays empty, but strict mode and disabled rules still raise.
    want_textBooltrueProduce repaired text in FinalResult.text; when false only the Json value is built.
    skip_proseBoolfalseSkip leading prose before the first { or [ (R021).
    quote_lookaheadBoolfalseEnable quote-ambiguity lookahead for missing commas between strings (R003).
    multi_rootMultiRootKeepFirstPolicy for content after the first root value: KeepFirst, AsArray, or AsError (R016).
    rulesRuleSetRuleSet::all()Per-rule switches; disabling a rule makes that code raise E010.
    indentInt320Output indentation in spaces; 0 renders compact JSON.
    max_repairsInt3210000Maximum number of repair records kept; beyond it repairs still apply but the result sets repairs_truncated.
    reject_ambiguousBoolfalseRefuse a repair that admits a second reading (E011) instead of applying it. Options::for_execution() sets it.
    max_lossExactnessLossyHighest acceptable exactness. The first record that exceeds it raises E010 with that code and position. Options::for_execution() sets Cosmetic.
    collect_deltasBoolfalseCollect incremental deltas; read them with PartialParser::drain_deltas(). Cannot be combined with duplicate_keys: KeepFirst or incomplete_number: TrimToValid.

    Invalid values (max_depth <= 0, max_input_len <= 0, max_token_len <= 0, max_repairs <= 0, indent < 0) raise E050 from Options::validate, which repair, parse, and PartialParser::new call for you.

    #Repair rules overview

    Every change is one record whose code is stable. The full dictionary, with examples and the requirement each rule implements, is in docs/RULES.md.

    CodeNameMeaning
    R001StripFenceRemove a Markdown code fence.
    R002StripCommentRemove a line or block comment.
    R003NormalizeQuoteNormalize a quote to a double quote.
    R004QuoteBareWordQuote a bare word (Info in key position, Warning in value position).
    R005RemoveExtraCommaRemove a trailing, leading, or repeated comma.
    R006InsertCommaInsert a missing comma.
    R007InsertColonInsert a missing colon.
    R008FillNullFill null for a missing value.
    R009CloseContainerClose an unclosed array or object.
    R010CloseStringClose an unclosed string.
    R011FixNumberNormalize or trim a number.
    R012ConvertLiteralConvert or complete a literal (True, None, undefined, non-finite values).
    R013EscapeControlEscape a control character inside a string.
    R014FixEscapeRepair an invalid or truncated escape, or a lone surrogate.
    R015MismatchedCloserReplace a closer whose type does not match the open container.
    R016DropUnexpectedDrop unrecognized or surplus content, including trailing content after the root value.
    R017DropExtraCloserDrop a surplus closer.
    R018MergeDuplicateKeyMerge a duplicate key according to duplicate_keys.
    R019ConcatStringsMerge adjacent string concatenation (["a" + "b"] becomes ["ab"]).
    R020DropEllipsisDrop an ... placeholder in a value position.
    R021SkipProseSkip leading prose before the JSON value.
    R022NormalizeLiteralRe-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).

    #Repair records and error codes

    Two signal families are kept apart: repair records are informational and travel with the result; fatal errors abort processing and are raised as MendError.

    A Repair carries code : RepairCode, position : Position, severity : Severity, and message : String. repair, parse, and finish also return repairs_truncated : Bool, which is true when the record array hit max_repairs. Position.offset counts UTF-16 code units from 0, while line and column are 1-based. Severity::Info means the value is unchanged and only the notation was normalized; Severity::Warning means content may have been invented, dropped, or altered. Messages are fixed templates and never embed user input.

    CodeErrorRaised when
    E001EmptyInputThe input is empty or whitespace only.
    E002NoJsonFoundNo JSON value can be found (payload is the offset).
    E010StrictViolationStrict mode, or a disabled rule, rejects a repair (payload is the code, position, and message).
    E020DepthLimitNesting exceeds max_depth.
    E021SizeLimitInput exceeds max_input_len.
    E022TokenLimitA token exceeds max_token_len.
    E030InvalidStatefeed after finish, or finish called twice or after a fatal error.
    E040InvalidUtf8Invalid or truncated UTF-8 in feed_bytes (payload is the byte offset).
    E050InvalidOptionAn option value is out of range, or a combination is invalid (collect_deltas with KeepFirst/TrimToValid).
    E060InvalidEventjsonmend/llm received an event payload it cannot interpret.

    RepairCode::code, MendError::code, and MendError::to_string produce the stable strings (R005, E020, and a one-line diagnostic respectively). The full reference is in docs/ERRORS.md.

    #Snapshot semantics

    snapshot() returns a non-destructive view of the value parsed so far, without raising and without changing parser state:

    • Stable prefix. Values that are already closed — complete strings, numbers, literals, and containers — never change in later snapshots. Only the last unfinished element can change.
    • Monotonicity. The snapshot sequence grows monotonically. Choosing TrimToValid deliberately breaks monotonicity for unfinished numbers (a snapshot showing 12 can later become 123); this is documented behaviour, not a bug.
    • Unfinished number. With the default incomplete_number = Drop, an unfinished number is not shown at all. With TrimToValid, the longest valid prefix is shown, or nothing if there is no valid prefix.
    • Unfinished key. A key whose closing quote has not arrived never appears. A dangling key (key plus colon, no value) is omitted with the default pending_key = OmitKey, or shown as "key": null with NullKey.
    • Unfinished string. The received prefix appears; an incomplete escape sequence is held back until it is complete.
    • Literal prefix. A partial literal such as tr is not shown in a snapshot; it is completed at finish.
    • Open containers. An open array or object appears with the content confirmed so far, and completeness is Partial.
    • open_path locates the value currently being built as Array[PathSegment], where a segment is Key(String) or Index(Int32). Synthetic frames, such as the array wrapper added by multi_root = AsArray, are skipped.

    A snapshot differs from the final result by design: the preview omits what is not yet decided, while finish closes containers (R009), closes strings (R010), trims numbers (R011), completes literals (R012), and fills a dangling key with null (R008). A value omitted from a preview can therefore appear in the finished result.

    #Command line and demo

    Build and run the CLI:

    moon build cmd/jsonmend --target native --release echo "{'a': 1,}" | ./_build/native/release/build/cmd/jsonmend/jsonmend.exe

    The help text of cmd/jsonmend/args.mbt::help_text() is quoted verbatim (it is in Chinese):

    jsonmend [选项] [文件] 读取文件或标准输入,把修复后的 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 内部错误。

    Flags and their meanings:

    FlagMeaning
    --strictStrict mode: any needed repair is an error (exit code 5).
    --checkCheck only, no output; exit code 1 when repairs are needed.
    --reportWrite the repair records to standard error.
    --indent NOutput indentation in spaces (0 to 64); the default is compact output.
    --multi-root MODEfirst, array, or error; the default is first.
    --skip-proseSkip prose before the JSON value.
    --quote-lookaheadEnable quote-ambiguity lookahead.
    --max-repairs NMaximum number of repair records to keep (default 10000); a truncated run also prints a note to standard error.
    --max-loss LEVELExactness ceiling: exact, cosmetic, or lossy (default). Exceeding it exits with code 5.
    --explainPrint every record with severity, position and a caret snippet, plus an exactness summary, to standard error.
    --batchRead {"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, --helpShow help.
    --versionShow the version (jsonmend 0.5.0).
    --policy NAMESafety profile: permissive, interactive, execution, agent or strict.
    --schema DECLSkeleton declaration such as {!query:string,!limit:integer}; with explain it also prints the syntax-ready and schema-ready fractions.
    --jsonWith explain: the machine-readable plan instead of the text block.
    file, or -Read a file; - or an omitted argument reads standard input.

    --indent=N, --multi-root=MODE, and --max-repairs=N are accepted as equivalent to the space-separated forms.

    Exit codes:

    CodeMeaning
    0Success; the output may still contain repairs.
    1--check found that repairs are needed.
    2Usage error (unknown flag, bad option value, or E050).
    3Invalid input (E001/E002/E040/E060, or an unreadable file).
    4Resource limit reached (E020/E021/E022).
    5Strict-mode violation (E010, including a --max-loss breach).
    70Internal error (E030).

    The browser demo lives in examples/web; the built page is static/demo/index.html and can be opened directly from disk. The package exports three plain String-in/String-out entry points — repair_text, report_text, and snapshot_text (value, completeness, and open path for a prefix) — and registers them on the host page through globalThis.jsonmendRegister*, so the page contains no second implementation. Rebuild the bundle with bash static/demo/build.sh; the steps and the headless check are in docs/REPRODUCE.md.

    #Testing

    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

    SuiteSizeResult (0.5.0)
    Golden cases (spec appendices A and B)126pass
    Documented streaming cases (S-001…S-021) + chunk invariance21 + invariance checkspass
    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 labelshand-written table + 240 property cases + the max_loss gatepass
    Incremental deltas (replay == snapshot)400 generated + 400 mutated documentspass
    LLM adapters (both providers, 9 generated fixtures + documented event shapes)30 testspass
    Adapter path against the ground-truth corpus1200 cases × (both providers × tool args and text)1200/1200 on all four (docs/EVAL_PATHS.md)
    Adversarial ambiguity suite amb-124 handwritten cases, frozen fingerprint10/10 flagged and refused, 14/14 controls pass, 0 failures (docs/EVAL_AMBIGUITY.md)
    Reference libraries on the same corpus1200 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 corpus1200 cases × chunk 1/3/7/random1200/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 table7 scenarios4 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 fieldpass
    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 explainpass
    Differential corpus against jsonrepair / json-repair402 cases202 divergences, all documented, 0 unexplained
    Line coverage of the library2312/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)

    Cross-backend results are identical: 129 tests pass on wasm-gc, native and JavaScript. .github/workflows/ci.yml runs the same commands on every push, so the numbers above are re-derived rather than trusted. Reproduction steps, thresholds and the known shortfalls are in docs/REPRODUCE.md and docs/TRACEABILITY.md. The evaluation protocol and its revision history are in docs/EVAL_REGISTRATION.md and docs/EVAL.md; how AI tooling was used is in docs/AI_USAGE.md.

    #Differences from jsonrepair

    JSONMend is not a port of jsonrepair and does not aim for byte-identical output. Several differences are deliberate, documented policy choices: trailing content after the root value keeps the first value by default instead of merging it, Markdown fences are stripped by default, duplicate keys keep the last value, unfinished numbers are dropped from snapshots, and strict mode plus per-rule switches have no direct equivalent. The 0.3.0 changes did not move the report by a single byte: exactness labels and rule renumbering do not change any value. A differential harness runs jsonrepair (TypeScript/Node) and json-repair (Python) over a corpus of hand-written and mutated inputs, compares parsed values, and classifies every divergence. In the committed 0.3.0 report, all 202 divergences had a documented cause and none was unexplained. The method, the classification, and the reference-library versions are in docs/DIVERGENCES.md; rule-to-requirement mapping is in docs/TRACEABILITY.md.

    #Performance

    Field readiness curve

    The figure is produced by code/tests/readiness_figure.py from the real output of jsonmend readiness: the left panel is a tool-call document (name 188, arguments 858, id 981, median 858/1000) and the right panel is the decile curve of a flat 200-member record (0 10 21 30 41 51 61 71 80 90 100). The dashed line is the ideal baseline — a field final when its own share of the input has been read. This replaces the previous "95% coverage" figure, which an object made structurally uninformative (it sat at 95.3% regardless of quality). The limitation is on the figure too: the metric covers top-level members only, so arguments cannot settle before its container closes, which is the only reason the left panel leaves the diagonal.

    bench/main.mbt measures one-shot repair throughput over valid, flawed, deeply nested, and long-string corpora, plus parse-only cost, scaling at n/2n/4n, and streaming overhead as a function of chunk size and snapshot frequency:

    moon run bench --target native --release moon run bench --target js --release

    On a 2026 Apple-silicon laptop, native measured 41–42 MB/s (valid), 83–87 MB/s (flawed) and 594 MB/s (one long string); js measured 22–39 MB/s. The same run also measures two things that matter for streaming: the first visible value appears at 0.7% of the document (median over tool-call payloads), and enabling collect_deltas costs 1.40–1.46 × the baseline time on a 1 MB document — that number is a known shortfall, not a win.

    These figures are observations, not a guarantee: throughput depends on the input shape, chunk size, snapshot frequency, and the toolchain version. Memory (13.7 × input; Node is 10.6 × and Python 14.3 × on the same file, so the 4 × budget itself is unrealistic) and artifact size are measured separately from the process. The full method, tables and decision records are in docs/BENCHMARK.md, and docs/REPRODUCE.md explains how to rerun the benchmarks.

    #Known limitations

    • The FR-16 event interface is a thin wrapper. feed_with / feed_bytes_with iterate drain_deltas, and the callback runs inside feed, so a callback must not feed the same parser again. It is not a separate push interface (the reasoning is in docs/ONEPAGE.md).
    • Only ... in a value position is dropped. R020 handles [1,...,3] and {"a":1,...}; an ellipsis anywhere else is not treated as a placeholder.
    • A truncated literal prefix is completed regardless of the following character. For example, a bare tru followed by an unexpected character is completed to true and the unexpected character is handled separately.
    • incomplete_number and pending_key only affect snapshots. They do not change finish: the final result trims the number (R011) and fills a dangling key with null (R008) under their own rules.
    • Lone surrogates differ by backend. On the wasm backend, \uD800-style lone surrogates survive in the output; on the other backends the InvalidUtf8 (E040) path substitutes U+FFFD.
    • The jsonmend/llm and delta sub-packages are experimental. Their APIs may change in a minor release; the core repair / parse / PartialParser surface is the stable one.
    • The CLI is native-only. cmd/jsonmend/moon.pkg declares supported_targets = "native".
    • Recovery guarantees are heuristic, not a conformance claim. JSONMend is not a JSON standard, and it does not guarantee a particular interpretation for every malformed input; the differential report is the evidence for the behaviour it does document.
    • Json numbers follow the core representation. Very large integers may lose precision in the tree, although the repaired text keeps the number's original digits.
    • quote_lookahead is off by default. When enabled, a sequence of strings that is missing commas is biased toward being read as string content.
    • Repair records are capped by max_repairs (default 10000). Further repairs are applied but not recorded, and the result sets repairs_truncated; the CLI prints a note to standard error.
    • Incremental deltas are correct but not cheap. Each change allocates one Delta plus a path array, so a 1 MB document produces ~195 000 records and takes 1.40–1.46 × the baseline. In the agent scenario (4 KB, 3-code-unit chunks) the whole stream is 0.115 ms, so the overhead is not on the critical path and the compact buffer is no longer the next thing to build.
    • --skip-prose only skips prose before a container root. A scalar root preceded by prose (Sure!\n42) still raises E002.
    • Memory is 13.7 × the input (177.9 MB for a 12.95 MB document). On the same input Node's JSON.parse is 10.6 × and Python's json.loads is 14.3 ×, so a 4 × budget is not realistic for any tree-building parser, Node included. Reaching it requires a "render text without building a tree" mode (docs/MEMORY.md).
    • The per-feed p99 is only an upper bound. The runtime clock is millisecond-resolution and exposes no allocation counter, so the scenario benchmark reports an amortised µs/feed plus the count of feeds that reached the clock resolution (docs/BENCHMARK.md).
    • The readiness curve covers top-level members only. A nested member ({"arguments":{…}}) is ready only when its container closes; the curve costs ~1.8 ms/KiB, and early stopping saves ~0 on flat records by construction.
    • Text mode does not skip prose by default. --skip-prose starts a value only at { or [, so it would skip a scalar root (0, 42, false); prose followed by a scalar root still raises E002.
    • The JS backend still allocates one Position per code unit. The n/2n/4n ratio therefore reaches 2.3–2.8 against a 2.3 target. Absolute throughput is well above the 5 MB/s floor, so this is recorded as a decision rather than fixed.
    • Parser instances are not thread-safe. Use one instance per input stream.

    #Roadmap

    • : zhanshishan/jsonmend 0.3.0, 0.4.0 and 0.5.0 are published (moon publish exited 0 for 0.5.0).
    • : static/demo/live.html.
    • : Schema + semantic_readiness.
    • Constraints in the declaration: the skeleton language names types only, not minimum / enum / maxLength.
    • Make semantic_readiness incremental: today it replays the document (32.7× repair); the delta-driven FieldTracker path could compute the same numbers as members settle.
    • Make the delta stream cheap: encode operations into a reusable buffer and share path prefixes, targeting ≤ 1.11 × the baseline instead of 1.40–1.46 × (lower priority now that the scenario benchmark shows it is off the critical path).
    • : done as the feed_with thin wrapper.
    • Record real provider streams to replace the documented event shapes (needs credentials; tests/record_stream.py is ready).
    • Extend the readiness curve to nested members and give it a zero-sampling, delta-driven implementation.
    • Add a text-only mode (want_value = false). The cross-implementation baseline shows the 4 × budget is unrealistic for any tree builder (Node itself is 10.6 ×), so this needs the metric changed first.
    • Add a fourth panel to the browser demo that plays a recorded LLM event stream, so the label and the growing arguments are visible without a terminal.
    • Split the engine's current position from its recorded positions to remove the per-code-unit allocation on the JS backend.
    • Widen the differential corpus further and add memory-budget tests for the streaming path.
    • Keep docs/RULES.md, docs/ERRORS.md, docs/DIVERGENCES.md, docs/BENCHMARK.md, and docs/TRACEABILITY.md in sync with the code.

    #Contributing

    • CONTRIBUTING.md (part of the release checklist) covers the development environment, how to run the tests, commit conventions, and how to add a repair rule — a rule change must update docs/RULES.md, docs/TRACEABILITY.md, and the golden tests together.
    • Issue templates cover defects (input, options, expected versus actual output, and backend) and new-rule proposals.
    • SECURITY.md (part of the release checklist) describes how to report a problem: this library processes untrusted input.

    #License and acknowledgements

    JSONMend is released under the Apache License 2.0 (license = "Apache-2.0" in code/moon.mod). A LICENSE/NOTICE pair is part of the release checklist.

    • jsonrepair (TypeScript) and json-repair (Python) are used as behavioural oracles in the differential report. They are used only for comparison; no code from either project is vendored.
    • The MoonBit core library (moonbitlang/core/json, moonbitlang/core/string) provides the Json type and string handling.

    See docs/ONEPAGE.md for a one-page project summary.

    MendError

    pub(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)
    }

    Fatal error. Every variant carries the data needed to diagnose the input without re-running the parser.

    MendError::code

    fn MendError::code(self : MendError) -> String

    Stable error code string, e.g. E010.

    MendError::retry_hint

    fn MendError::retry_hint(self : MendError, input? : String, radius? : Int) -> RetryHint?

    Convenience: the hint of a refusal, or None.

    MendError::to_string

    fn MendError::to_string(self : MendError) -> String

    One-line diagnostic, e.g. E020 DepthLimit: limit 3 exceeded at offset 3.

    Ambiguity

    pub(all) enum Ambiguity {
    Unambiguous
    BareWord
    AdjacentStrings
    ConcatOperator
    TrailingContent
    }

    Whether a repair admits a second, equally self-consistent reading (FR-27).

    Unambiguous means the JSON grammar leaves no other reading of the input. Every other variant names the concrete alternative reading, which a consumer that is about to execute the value must refuse: Options::reject_ambiguous turns a non-Unambiguous repair into E011.

    The marker is orthogonal to Severity: ["a" "b"] is Info (nothing was invented, both characters were already there) yet structurally ambiguous.
    impl Eq for Ambiguity

    Ambiguity::equal

    fn Ambiguity::equal(self : Ambiguity, other : Ambiguity) -> Bool

    Explicit Eq extension for Ambiguity.

    Ambiguity::is_ambiguous

    fn Ambiguity::is_ambiguous(self : Ambiguity) -> Bool

    Whether the two readings of an ambiguity can be told apart from the input alone. Always false today: that is what makes it an ambiguity.

    Ambiguity::message

    fn Ambiguity::message(self : Ambiguity) -> String

    Fixed English message of an ambiguity, for E011 and RetryHint.

    Ambiguity::name

    fn Ambiguity::name(self : Ambiguity) -> String

    Stable lowercase name of an ambiguity, e.g. adjacent_strings.

    Ambiguity::not_equal

    fn Ambiguity::not_equal(x : Ambiguity, y : Ambiguity) -> Bool

    Explicit Eq extension for Ambiguity.

    Completeness

    pub(all) enum Completeness {
    Empty
    Partial
    Complete
    }

    How complete a value is.
    impl Eq for Completeness

    Completeness::equal

    fn Completeness::equal(self : Completeness, other : Completeness) -> Bool

    Explicit Eq extension for Completeness.

    Completeness::not_equal

    fn Completeness::not_equal(x : Completeness, y : Completeness) -> Bool

    Explicit Eq extension for Completeness.

    Delta

    pub(all) struct Delta {
    path : Array[PathSegment]
    op : DeltaOp
    value : Json?
    text : String
    }

    One incremental change to the value built so far.

    Delta::pointer

    fn Delta::pointer(self : Delta) -> String

    Render a path as an RFC 6901 JSON Pointer, e.g. /items/0/name.

    DeltaOp

    pub(all) enum DeltaOp {
    Set
    AppendText
    Close
    }

    Operation of one Delta (FR-18). The names follow JSON Patch (RFC 6902) where an equivalent exists; AppendText has no RFC 6902 counterpart.
    impl Eq for DeltaOp

    DeltaOp::equal

    fn DeltaOp::equal(self : DeltaOp, other : DeltaOp) -> Bool

    Explicit Eq extension for DeltaOp.

    DeltaOp::name

    fn DeltaOp::name(self : DeltaOp) -> String

    DeltaOp as a stable lowercase word, e.g. append_text.

    DeltaOp::not_equal

    fn DeltaOp::not_equal(x : DeltaOp, y : DeltaOp) -> Bool

    Explicit Eq extension for DeltaOp.

    DuplicateKeys

    pub(all) enum DuplicateKeys {
    KeepLast
    KeepFirst
    AsError
    }

    Policy for duplicate object keys (BR-19).
    impl Eq for DuplicateKeys

    DuplicateKeys::equal

    fn DuplicateKeys::equal(self : DuplicateKeys, other : DuplicateKeys) -> Bool

    Explicit Eq extension for DuplicateKeys.

    DuplicateKeys::not_equal

    fn DuplicateKeys::not_equal(x : DuplicateKeys, y : DuplicateKeys) -> Bool

    Explicit Eq extension for DuplicateKeys.

    Exactness

    pub(all) enum Exactness {
    Exact
    Cosmetic
    Lossy
    }

    How much of a result was invented rather than read from the input (FR-17).

    The three levels are ordered: Exact is the safest, Lossy the riskiest. Options::max_loss compares against this order.

    • Exact — no repair was recorded, so the value is exactly what the input said (a valid document, possibly with trailing whitespace).
    • Cosmetic — only notation-level repairs (Info severity) were recorded: quotes normalised, a comment or a code fence removed, a missing comma or colon inserted, a control character escaped. No value was guessed.
    • Lossy — at least one Warning severity repair was recorded: a value was invented, dropped, truncated or replaced.
    impl Eq for Exactness

    Exactness::equal

    fn Exactness::equal(self : Exactness, other : Exactness) -> Bool

    Explicit Eq extension for Exactness.

    Exactness::name

    fn Exactness::name(self : Exactness) -> String

    Stable lowercase name, e.g. exact.

    Exactness::not_equal

    fn Exactness::not_equal(x : Exactness, y : Exactness) -> Bool

    Explicit Eq extension for Exactness.

    Exactness::rank

    fn Exactness::rank(self : Exactness) -> Int

    Risk rank of an exactness level: Exact 0, Cosmetic 1, Lossy 2.

    Exactness::within

    fn Exactness::within(self : Exactness, limit : Exactness) -> Bool

    Whether self is at most as risky as limit.

    ExecutionDecision

    pub(all) enum ExecutionDecision {
    Safe
    CosmeticRepair
    LossyRepair
    Ambiguous
    Invalid
    }

    What a caller may do with a result (FR-34).

    ExecutionDecision::equal

    fn ExecutionDecision::equal(self : ExecutionDecision, other : ExecutionDecision) -> Bool

    Explicit Eq extension for ExecutionDecision.

    ExecutionDecision::is_safe

    fn ExecutionDecision::is_safe(self : ExecutionDecision) -> Bool

    Whether the caller may execute: Safe and CosmeticRepair only.

    ExecutionDecision::label

    fn ExecutionDecision::label(self : ExecutionDecision) -> String

    Upper-case label for a human, e.g. SAFE TO EXECUTE.

    ExecutionDecision::name

    fn ExecutionDecision::name(self : ExecutionDecision) -> String

    Stable lowercase name, e.g. lossy_repair.

    ExecutionDecision::not_equal

    fn ExecutionDecision::not_equal(x : ExecutionDecision, y : ExecutionDecision) -> Bool

    Explicit Eq extension for ExecutionDecision.

    ExecutionDecision::rank

    fn ExecutionDecision::rank(self : ExecutionDecision) -> Int

    The risk of a decision, used to order two decisions (FR-34): Safe 0, CosmeticRepair 1, LossyRepair 2, Ambiguous 3, Invalid 4.

    ExecutionDecision::worse_of

    The riskier of two decisions. Used to combine the parse decision with the schema verdict without inventing a sixth variant.

    FieldReady

    pub(all) struct FieldReady {
    key : String
    permille : Int
    }

    Readiness point of one top-level member.
    impl Eq for FieldReady

    FieldReady::equal

    fn FieldReady::equal(self : FieldReady, other : FieldReady) -> Bool

    Explicit Eq extension for FieldReady.

    FieldReady::not_equal

    fn FieldReady::not_equal(x : FieldReady, y : FieldReady) -> Bool

    Explicit Eq extension for FieldReady.

    FieldTracker

    pub struct FieldTracker {
    // private fields
    }

    Watches a set of top-level members of the object a model is writing and reports when each one can no longer change, so the caller can cancel the generation (FR-29).

    The rule is read off the delta contract rather than guessed:

    • Close on /<key> — a container member is finmap;
    • Set on /<key> with a completed scalar — a number, literal, or a non-empty string is finmap (an empty string is the placeholder that announces a string slot, followed by AppendText);
    • any later delta about a different first-level member — the member before it cannot change any more.

    FieldTracker::all_ready

    fn FieldTracker::all_ready(self : FieldTracker) -> Bool

    Whether every watched member is finmap.

    FieldTracker::feed

    fn FieldTracker::feed(self : FieldTracker, d : Delta) -> Unit

    Feed one delta.

    FieldTracker::new

    fn FieldTracker::new(keys : Array[String]) -> FieldTracker

    A tracker for keys.

    FieldTracker::ready

    fn FieldTracker::ready(self : FieldTracker, key : String) -> Bool

    Whether the named member is finmap.

    FieldTracker::ready_count

    fn FieldTracker::ready_count(self : FieldTracker) -> Int

    How many watched members are finmap.

    FieldTracker::ready_keys

    fn FieldTracker::ready_keys(self : FieldTracker) -> Array[String]

    Names of the watched members that are finmap, in the order they were ready.

    FinalResult

    pub(all) struct FinalResult {
    text : String
    value : Json
    repairs : Array[Repair]
    completeness : Completeness
    repairs_truncated : Bool
    exactness : Exactness
    ambiguous : Bool
    }

    Result of PartialParser::finish.

    IncompleteNumber

    pub(all) enum IncompleteNumber {
    Drop
    TrimToValid
    }

    How an unterminated number appears in a snapshot (FR-11).

    IncompleteNumber::equal

    fn IncompleteNumber::equal(self : IncompleteNumber, other : IncompleteNumber) -> Bool

    Explicit Eq extension for IncompleteNumber.

    IncompleteNumber::not_equal

    fn IncompleteNumber::not_equal(x : IncompleteNumber, y : IncompleteNumber) -> Bool

    Explicit Eq extension for IncompleteNumber.

    Mode

    pub(all) enum Mode {
    Lenient
    Strict
    }

    Repair mode. Strict turns the first repair record into E010.
    impl Eq for Mode

    Mode::equal

    fn Mode::equal(self : Mode, other : Mode) -> Bool

    Explicit Eq extension for Mode.

    Mode::not_equal

    fn Mode::not_equal(x : Mode, y : Mode) -> Bool

    Explicit Eq extension for Mode.

    MultiRoot

    pub(all) enum MultiRoot {
    KeepFirst
    AsArray
    AsError
    }

    Policy for content after the first root value (FR-15).
    impl Eq for MultiRoot

    MultiRoot::equal

    fn MultiRoot::equal(self : MultiRoot, other : MultiRoot) -> Bool

    Explicit Eq extension for MultiRoot.

    MultiRoot::not_equal

    fn MultiRoot::not_equal(x : MultiRoot, y : MultiRoot) -> Bool

    Explicit Eq extension for MultiRoot.

    NonFinite

    pub(all) enum NonFinite {
    AsNull
    AsError
    AsString
    }

    How NaN, Infinity and -Infinity are converted (BR-12).
    impl Eq for NonFinite

    NonFinite::equal

    fn NonFinite::equal(self : NonFinite, other : NonFinite) -> Bool

    Explicit Eq extension for NonFinite.

    NonFinite::not_equal

    fn NonFinite::not_equal(x : NonFinite, y : NonFinite) -> Bool

    Explicit Eq extension for NonFinite.

    Options

    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
    }

    All options. Defaults are the ones documented in D04 4.6.

    Options::default

    fn Options::default() -> Options

    Documented defaults: lenient, depth 512, 64 Mi code units, duplicate keys keep the last value, snapshots drop unfinished numbers, all rules on.

    Options::for_agent

    fn Options::for_agent() -> Options

    Profile for an agent loop that streams a tool call and hands a refusal back to the model (FR-28, FR-30): the execution policy's safety knobs plus the protocol an agent channel needs — the delta stream (collect_deltas: true, from for_llm()) so a consumer can follow the value as it is written, and skip_prose: true because a model that answers in a chat channel often prefixes the arguments with a sentence and the prefix is not part of the arguments.

    The safety knobs are the agent profile's; what a streaming agent adds is protocol, so this is SafetyPolicy::for_agent() with the delta stream and prose handling turned on. There is exactly one definition of the agent profile, and SafetyPolicy::of_options reads it back.

    Options::for_execution

    fn Options::for_execution() -> Options

    Profile for a caller that will execute the parsed value (FR-17, FR-27): notation repairs are accepted, but a repair that admits a second reading raises E011 and anything that invented, dropped or truncated a value raises E010. Distinct from strict(), which rejects even a normalised quote, and from lenient(), which accepts both.

    Options::for_llm

    fn Options::for_llm() -> Options

    Profile for consuming a large language model's tool-call arguments (FR-18, FR-19): the documented defaults plus collect_deltas, so that PartialParser::drain_deltas is available. duplicate_keys stays KeepLast and incomplete_number stays Drop, which are the two settings a delta stream can express.

    Options::for_text

    fn Options::for_text() -> Options

    Profile for a caller whose only output is the repaired text — a pipe, a log line, a file rewrite (FR-30). The value is what is wanted and nothing else is: no repair records are kept (they cost an allocation per repair and an array that grows with the input) and the text is compact.

    This is a preset, not a separate engine mode: the tree is still built internally, because the duplicate-key policy, + concatenation and the fill_null replay all read the value that was already accepted (see docs/RULES.md §2.4 for why the value cannot simply be dropped).

    Options::lenient

    fn Options::lenient() -> Options

    The documented defaults: lenient, every rule on, no exactness gate, no delta tracking. Equivalent to Options::default().

    Options::strict

    fn Options::strict() -> Options

    Reject any input that needs a repair: the first recorded repair becomes E010. Equivalent to { ..Options::default(), mode: Strict }.

    Options::validate

    fn Options::validate(self : Options) -> Unit raise MendError

    Validate option values, raising E050 for anything out of range.

    Options::with_policy

    fn Options::with_policy(self : Options, policy : SafetyPolicy) -> Options

    The same policy applied to self, keeping every protocol field (deltas, prose, indent, repair log, limits the caller set by hand) and replacing only the safety knobs.

    ParseResult

    pub(all) struct ParseResult {
    value : Json
    repairs : Array[Repair]
    completeness : Completeness
    repairs_truncated : Bool
    exactness : Exactness
    ambiguous : Bool
    }

    Result of parse.

    PartialParser

    pub struct PartialParser {
    // private fields
    }

    Streaming partial parser. feed any number of chunks, then finish; the one-shot functions are built on exactly this type.

    PartialParser::analyze

    fn PartialParser::analyze(self : PartialParser, input? : String, schema? : Schema) -> RepairPlan

    The same analysis for a stream that was fed incrementally (FR-33).

    input is optional and only used to quote the window around a refused position; the parser does not keep the text it consumed.

    PartialParser::drain_deltas

    fn PartialParser::drain_deltas(self : PartialParser) -> Array[Delta] raise MendError

    drain_deltas on the parser that the stream produced, keeping the existing documentation in one place.

    PartialParser::feed

    fn PartialParser::feed(self : PartialParser, chunk : String) -> Unit raise MendError

    Feed one chunk. A chunk may split any token, including a surrogate pair.

    PartialParser::feed_bytes

    fn PartialParser::feed_bytes(self : PartialParser, bytes : Bytes) -> Unit raise MendError

    Feed raw UTF-8 bytes. Invalid or truncated UTF-8 raises E040.

    PartialParser::feed_bytes_with

    fn PartialParser::feed_bytes_with(self : PartialParser, bytes : Bytes, on_delta : (Delta) -> Unit) -> Unit raise MendError

    The same, for byte input (FR-12, FR-16).

    PartialParser::feed_with

    fn PartialParser::feed_with(self : PartialParser, chunk : String, on_delta : (Delta) -> Unit) -> Unit raise MendError

    FR-16, the event interface, as a thin wrapper over drain_deltas.

    on_delta is called once per change, in the order drain_deltas would return them, immediately after the change happened. This is deliberately not a separate engine: it is the same log, consumed as it is produced, because the callback is what a caller integrating with an event loop needs and a second implementation would be a second place for the two to diverge.

    The callback runs while the parser is inside feed, so it must not feed the parser again; callbacks that need to do so should collect the deltas and act after feed returns.

    PartialParser::finish

    fn PartialParser::finish(self : PartialParser) -> FinalResult raise MendError

    Finish parsing: close open containers, produce the repaired text and the value. Calling it twice, or after a fatal error, raises E030.

    PartialParser::new

    fn PartialParser::new(options? : Options) -> PartialParser raise MendError

    Create a parser. Invalid options raise E050.

    PartialParser::snapshot

    fn PartialParser::snapshot(self : PartialParser) -> Snapshot

    Snapshot the value parsed so far. Never raises, never mutates the parser.

    The returned value may share structure with the parser's cache, so it must be treated as immutable; use snapshot_copy when the caller needs to mutate it (FR-13).

    PartialParser::snapshot_copy

    fn PartialParser::snapshot_copy(self : PartialParser) -> Snapshot

    A deep copy of the snapshot: safe to mutate without disturbing the parser or any earlier snapshot (FR-13).

    PathSegment

    pub(all) enum PathSegment {
    Key(String)
    Index(Int)
    }

    One step of Snapshot::open_path.
    impl Eq for PathSegment

    PathSegment::equal

    fn PathSegment::equal(self : PathSegment, other : PathSegment) -> Bool

    Explicit Eq extension for PathSegment.

    PathSegment::not_equal

    fn PathSegment::not_equal(x : PathSegment, y : PathSegment) -> Bool

    Explicit Eq extension for PathSegment.

    PendingKey

    pub(all) enum PendingKey {
    OmitKey
    NullKey
    }

    How a dangling key (key with colon but no value) appears in a snapshot.
    impl Eq for PendingKey

    PendingKey::equal

    fn PendingKey::equal(self : PendingKey, other : PendingKey) -> Bool

    Explicit Eq extension for PendingKey.

    PendingKey::not_equal

    fn PendingKey::not_equal(x : PendingKey, y : PendingKey) -> Bool

    Explicit Eq extension for PendingKey.

    PlanAction

    pub(all) enum PlanAction {
    Remove
    Insert
    Replace
    Normalize
    Merge
    Wrap
    }

    What a repair did to the text (FR-33).
    impl Eq for PlanAction

    PlanAction::equal

    fn PlanAction::equal(self : PlanAction, other : PlanAction) -> Bool

    Explicit Eq extension for PlanAction.

    PlanAction::name

    fn PlanAction::name(self : PlanAction) -> String

    Stable lowercase name, e.g. remove.

    PlanAction::not_equal

    fn PlanAction::not_equal(x : PlanAction, y : PlanAction) -> Bool

    Explicit Eq extension for PlanAction.

    PlanOp

    pub(all) struct PlanOp {
    rule : String
    name : String
    action : PlanAction
    position : Position
    severity : Severity
    ambiguity : Ambiguity
    confidence_class : String
    confidence : Double
    }

    One repair, described as an operation on the input (FR-33).
    impl Eq for PlanOp

    PlanOp::equal

    fn PlanOp::equal(self : PlanOp, other : PlanOp) -> Bool

    Explicit Eq extension for PlanOp.

    PlanOp::information_loss

    fn PlanOp::information_loss(self : PlanOp) -> Bool

    Whether the operation threw information away: true for a Warning record.

    PlanOp::not_equal

    fn PlanOp::not_equal(x : PlanOp, y : PlanOp) -> Bool

    Explicit Eq extension for PlanOp.

    PlanOp::of_repair

    fn PlanOp::of_repair(r : Repair) -> PlanOp

    One repair record as a plan operation (FR-33).

    Position

    pub(all) struct Position {
    offset : Int
    line : Int
    column : Int
    }

    A position inside the input. offset counts UTF-16 code units from 0, line and column are 1-based (BR-20).
    impl Eq for Position

    Position::equal

    fn Position::equal(self : Position, other : Position) -> Bool

    Explicit Eq extension for Position.

    Position::not_equal

    fn Position::not_equal(x : Position, y : Position) -> Bool

    Explicit Eq extension for Position.

    ReadinessEvent

    pub(all) enum ReadinessEvent {
    FieldReady(String)
    FieldInvalid(String, String)
    FieldIncomplete(String, String)
    RootReady
    RootInvalid(Int)
    }

    What the watcher learned while the document was arriving (FR-39).

    ReadinessEvent::is_verdict

    fn ReadinessEvent::is_verdict(self : ReadinessEvent) -> Bool

    Whether the event is a verdict rather than a progress note.

    ReadinessEvent::kind

    fn ReadinessEvent::kind(self : ReadinessEvent) -> String

    Stable lowercase name of an event kind, for output and for tests.

    ReadinessEvent::path

    fn ReadinessEvent::path(self : ReadinessEvent) -> String

    Path the event is about; "" for the root events.

    ReadinessEvent::to_json

    fn ReadinessEvent::to_json(self : ReadinessEvent) -> String

    JSON form of one event. Field names are part of the contract.

    ReadinessEventAt

    pub(all) struct ReadinessEventAt {
    event : ReadinessEvent
    permille : Int
    }

    One event with the input fraction at which it fired.

    Repair

    pub(all) struct Repair {
    code : RepairCode
    position : Position
    severity : Severity
    message : String
    ambiguity : Ambiguity
    }

    One repair record: what was changed, where, how serious it is, and whether the change admits a second reading.

    RepairCode

    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
    }

    Repair codes. The string form (R001 … R022) is stable: a code never changes meaning once released, and codes are only ever appended.
    impl Eq for RepairCode

    RepairCode::code

    fn RepairCode::code(self : RepairCode) -> String

    Stable string form of a repair code, e.g. R005.

    RepairCode::equal

    fn RepairCode::equal(self : RepairCode, other : RepairCode) -> Bool

    Explicit Eq extension for RepairCode.

    RepairCode::message

    fn RepairCode::message(self : RepairCode) -> String

    Fixed English message template of a repair code. Messages never embed user input, so they cannot be used to inject content into logs (D09 9.7).

    RepairCode::name

    fn RepairCode::name(self : RepairCode) -> String

    Human readable name of a repair code, e.g. RemoveExtraComma.

    RepairCode::not_equal

    fn RepairCode::not_equal(x : RepairCode, y : RepairCode) -> Bool

    Explicit Eq extension for RepairCode.

    RepairCode::number

    fn RepairCode::number(self : RepairCode) -> Int

    Stable numeric index of a repair code (1 … 21), used by rule switches.

    RepairCode::severity

    fn RepairCode::severity(self : RepairCode) -> Severity

    Default severity of a repair code. QuoteBareWord is context dependent and is recorded as Warning when it quotes a value (see BR-07a).

    RepairCode::severity_in

    fn RepairCode::severity_in(self : RepairCode, slot : Slot) -> Severity

    Severity of a code in a given slot. Only QuoteBareWord differs: quoting a member name is a notation fix, quoting a value invents a string (BR-07a).

    RepairPlan

    pub(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]
    }

    The full analysis of one input: the repairs, the risk, the decision and the refusal, if any (FR-33, FR-33).

    RepairPlan::ambiguity_kind

    fn RepairPlan::ambiguity_kind(self : RepairPlan) -> String

    Which kind of second reading the plan admits (Q-26).

    semantic — two values became one (merge), so two consumers that disagree about repeated member names would end up with different values; the plan cannot be executed without choosing for them. syntactic — the notation could be read two ways, but the value is the same under both readings. none — one reading only.

    RepairPlan::execution

    fn RepairPlan::execution(self : RepairPlan) -> String

    allowed when the plan may be handed to a tool, blocked otherwise.

    RepairPlan::explain_text

    fn RepairPlan::explain_text(self : RepairPlan, input? : String) -> String

    Render self as the jsonmend explain block (FR-33).

    input is optional; when it is passed it is echoed under Input: truncated to one line, so the block is self-contained in a log.

    RepairPlan::is_executable

    fn RepairPlan::is_executable(self : RepairPlan) -> Bool

    Whether the caller may execute the plan (FR-34).

    RepairPlan::normalized

    fn RepairPlan::normalized(self : RepairPlan) -> Bool

    Whether the value JSONMend hands over was produced by rewriting the input: true as soon as any repair was recorded, including the ones that only normalise notation.

    RepairPlan::prompt

    fn RepairPlan::prompt(self : RepairPlan) -> String

    Feedback ready to be sent back to the model that produced the input (FR-28, FR-33).

    A refusal carries a RetryHint, which names the position and quotes the window. A plan that was accepted but is not executable — a lossy repair under permissive() — has no hint, because nothing was refused; the prompt is then derived from the rules that fired, which is the same instruction a refusal would have produced. A plan that was accepted and is executable returns a sentence saying so, so that a caller never has to branch.

    RepairPlan::suggestions

    fn RepairPlan::suggestions(self : RepairPlan) -> Array[String]

    What a model should do differently, one sentence per rule that fired (FR-33). Ordered by first occurrence and de-duplicated, because a truncated document fires R010 and R009 and the two sentences are one instruction.

    RepairPlan::summary

    fn RepairPlan::summary(self : RepairPlan) -> String

    A one line summary for logs: lossy_repair exactness=lossy repairs=2.

    RepairPlan::to_json

    fn RepairPlan::to_json(self : RepairPlan) -> String

    Machine readable form of the plan, for jsonmend explain --json and for the browser demo. Field names are part of the contract.

    RepairResult

    pub(all) struct RepairResult {
    text : String
    repairs : Array[Repair]
    completeness : Completeness
    repairs_truncated : Bool
    exactness : Exactness
    ambiguous : Bool
    }

    Result of repair.

    RetryHint

    pub(all) struct RetryHint {
    error_code : String
    repair_code : String
    repair_name : String
    position : Position
    ambiguity : Ambiguity
    reason : String
    context : String
    caret : Int
    }

    One actionable refusal, ready to be fed back to the model that produced the arguments (FR-28).
    impl Eq for RetryHint

    RetryHint::equal

    fn RetryHint::equal(self : RetryHint, other : RetryHint) -> Bool

    Explicit Eq extension for RetryHint.

    RetryHint::not_equal

    fn RetryHint::not_equal(x : RetryHint, y : RetryHint) -> Bool

    Explicit Eq extension for RetryHint.

    RetryHint::of_error

    fn RetryHint::of_error(e : MendError, input? : String, radius? : Int) -> RetryHint?

    Build the hint for a refusal. Returns None for every error that is not a refusal of a repair (EmptyInput, SizeLimit, …), because those are not something the model can fix by re-emitting its arguments.

    input is optional: without it the hint still names the position, but it cannot quote the surrounding text.

    RetryHint::to_prompt

    fn RetryHint::to_prompt(self : RetryHint) -> String

    Render the hint as a prompt fragment (FR-28). Stable across releases; the wording is part of the contract, so it is covered by a test.

    The rendering never contains the whole input, only the window around the refused position, so a caller can put it in a retry message as is.

    RetryHint::to_string

    fn RetryHint::to_string(self : RetryHint) -> String

    One line form, for --report and logs.

    RuleSet

    pub(all) struct RuleSet {
    // private fields
    }

    Per-rule switches. Disabling a rule makes that single code raise E010 instead of being recorded (D09 9.5).

    RuleSet::all

    fn RuleSet::all() -> RuleSet

    All rules enabled.

    RuleSet::disable

    fn RuleSet::disable(self : RuleSet, code : RepairCode) -> RuleSet

    A copy of this rule set with one rule disabled.

    RuleSet::enable

    fn RuleSet::enable(self : RuleSet, code : RepairCode) -> RuleSet

    A copy of this rule set with one rule enabled.

    RuleSet::is_enabled

    fn RuleSet::is_enabled(self : RuleSet, code : RepairCode) -> Bool

    Whether a repair code may be reported without raising E010.

    RuleSet::same_as

    fn RuleSet::same_as(self : RuleSet, other : RuleSet) -> Bool

    Whether two rule sets enable exactly the same rules (FR-30). Used to decide whether an options value still matches a named safety profile.

    SafetyPolicy

    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
    }

    A named set of safety-relevant settings (FR-32).

    Every field here can also be set on Options directly; the policy exists so that the intent is part of the value and can be printed in a log line (SafetyPolicy::name).

    SafetyPolicy::for_agent

    fn SafetyPolicy::for_agent() -> SafetyPolicy

    An agent loop, where a refusal is cheap because the refusal is handed back to the model that produced the input (FR-28).

    In safety terms this is for_execution() plus a resource bound: a tool call is a handful of arguments, so nesting is capped at 64 and the input at one million code units, while the general defaults are 512 and 64 MiB. What an agent additionally needs is protocol, not policy, which is why the streaming preset is Options::for_agent() — this policy plus for_llm()'s delta stream and prose handling.

    SafetyPolicy::for_execution

    fn SafetyPolicy::for_execution() -> SafetyPolicy

    Nothing human sits between the parse and the effect: notation repairs pass, a repair that changed a value raises E010 and a repair with a second reading raises E011 (FR-27).

    This is the policy behind Options::for_execution().

    SafetyPolicy::interactive

    fn SafetyPolicy::interactive() -> SafetyPolicy

    A human reads the result and can supply the missing judgement: notation repairs pass, a repair that invented, dropped or truncated a value is refused (max_loss: Cosmetic), and a second reading is reported through RepairResult::ambiguous instead of being refused, because a person is allowed to choose.

    SafetyPolicy::name

    fn SafetyPolicy::name(self : SafetyPolicy) -> String

    The profile name, e.g. execution.

    SafetyPolicy::of_options

    fn SafetyPolicy::of_options(options : Options) -> String?

    The policy a set of options corresponds to, or None when the options are not one of the five documented profiles.

    Two options values match the same profile when every safety knob is equal; protocol knobs are ignored, because that is what makes them protocol. The comparison exists so that --policy reporting cannot drift: the CLI prints the profile the options it built actually are, not the word the user typed.

    SafetyPolicy::permissive

    fn SafetyPolicy::permissive() -> SafetyPolicy

    The three profiles that differ in what they accept, from least to most careful. permissive is the documented default; interactive refuses a repair that invented content but lets a human judge a second reading; execution refuses both.

    SafetyPolicy::strict

    fn SafetyPolicy::strict() -> SafetyPolicy

    Refuse every repair, including a normalised quote: Mode::Strict turns the first recorded repair into E010, whatever its severity. This is the policy behind Options::strict().

    max_loss is left where the permissive profile puts it, because under Strict it is never consulted: the refusal happens first. Changing it would make the profile look stricter than it is while changing nothing.

    SafetyPolicy::to_options

    fn SafetyPolicy::to_options(self : SafetyPolicy) -> Options

    The policy as a fresh Options value: the documented protocol defaults with this policy's safety knobs.

    Schema

    pub(all) enum Schema {
    Any
    Null
    Bool
    Num
    Int
    Str
    Arr(Schema)
    Obj(Array[SchemaMember])
    }

    The kinds a skeleton declaration can name (FR-36).

    Schema::accepts

    fn Schema::accepts(self : Schema, v : Json) -> Bool

    Whether v has the declared kind (FR-36).

    Schema::at_path

    fn Schema::at_path(self : Schema, path : String) -> Schema?

    The declaration a dotted path points at, or None (FR-36, FR-39).

    A path is spelled the way Schema::required_paths produces it: member names joined with ., where a numeric step selects an array element. A member name that itself contains a . is therefore ambiguous; the declaration language has no way to write one, so required_paths cannot produce one either, and the two agree by construction.

    Schema::evaluate

    fn Schema::evaluate(self : Schema, value : Json) -> SchemaVerdict

    Walk value against schema (FR-36). path is the dotted prefix of the node, empty at the root.

    Schema::field

    fn Schema::field(self : Schema, key : String) -> SchemaMember?

    The declared schema of member key, or None.

    Schema::member_keys

    fn Schema::member_keys(self : Schema) -> Array[String]

    Member names declared under parent, in declaration order.

    Schema::name

    fn Schema::name(self : Schema) -> String

    Stable name of a kind, as it is written in the text form.

    Schema::parse

    fn Schema::parse(text : String) -> Schema raise MendError

    Parse the text form of a skeleton (FR-36). Raises E070 with the offset of the problem; the text is never partially accepted.

    Schema::required_paths

    fn Schema::required_paths(self : Schema) -> Array[String]

    The dotted paths of the required members of a skeleton, in declaration order (FR-36).

    A required object contributes its own path and, recursively, the required members inside it. An optional member contributes nothing: a member that may be absent cannot hold up readiness. An array is one path, because the number of elements a model will emit is not declared.

    Schema::stringify

    fn Schema::stringify(self : Schema) -> String

    The text form of a schema, the inverse of Schema::parse.

    SchemaMember

    pub(all) struct SchemaMember {
    key : String
    required : Bool
    schema : Schema
    }

    One declared object member (FR-36).
    impl Eq for SchemaMember

    SchemaMember::equal

    fn SchemaMember::equal(self : SchemaMember, other : SchemaMember) -> Bool

    Explicit Eq extension for SchemaMember.

    SchemaMember::not_equal

    fn SchemaMember::not_equal(x : SchemaMember, y : SchemaMember) -> Bool

    Explicit Eq extension for SchemaMember.

    SchemaMismatch

    pub(all) struct SchemaMismatch {
    path : String
    expected : String
    got : String
    }

    One declared member whose value does not have the declared kind (FR-36).

    SchemaVerdict

    pub(all) struct SchemaVerdict {
    total : Int
    satisfied : Int
    missing : Array[String]
    mismatched : Array[SchemaMismatch]
    ok : Bool
    }

    The verdict of one value against a skeleton (FR-36).

    SemanticReadiness

    pub(all) struct SemanticReadiness {
    syntax_permille : Int
    schema_permille : Int
    settled_permille : Int
    step : Int
    verdict : SchemaVerdict
    }

    How much of a skeleton was already final, and when (FR-36).

    SemanticWatcher

    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
    }

    Streams a skeleton verdict: feed the model's chunks, drain the events (FR-39).

    The watcher owns its own PartialParser and forces collect_deltas on, so the caller does not have to know that the mechanism is the delta stream. duplicate_keys KeepFirst is refused (E050): a delta stream cannot retract a value it already announced, and this type reads the stream.

    SemanticWatcher::all_settled

    fn SemanticWatcher::all_settled(self : SemanticWatcher) -> Bool

    Whether every followed path is final.

    SemanticWatcher::bytes

    fn SemanticWatcher::bytes(self : SemanticWatcher) -> Int

    Code units fed so far.

    SemanticWatcher::concluded

    fn SemanticWatcher::concluded(self : SemanticWatcher) -> Bool

    Whether the root verdict has been reached: RootReady or RootInvalid.

    SemanticWatcher::conflict

    fn SemanticWatcher::conflict(self : SemanticWatcher) -> Bool

    Whether a duplicate key re-opened a member after it had been decided. The events of that member were true when they were emitted; a caller that cares must re-read the final document (analyze reports the duplicate as R018).

    SemanticWatcher::drain_events

    Events produced since the previous drain.

    SemanticWatcher::feed

    fn SemanticWatcher::feed(self : SemanticWatcher, chunk : String) -> Unit raise MendError

    Feed one chunk of the model's output.

    Raises what the parser raises: a profile with reject_ambiguous or max_loss set refuses a document mid-stream instead of at the end, which is exactly the reason to watch rather than to measure afterwards.

    SemanticWatcher::finish

    Close the document: drain what the model never got to and decide.

    SemanticWatcher::new

    fn SemanticWatcher::new(schema : Schema, options? : Options) -> SemanticWatcher raise MendError

    Build a watcher for schema.

    Only the required paths whose declaration is decided by their own value are followed: a required object with required members is decided by those members, which are followed instead, and the whole skeleton is checked once they are all in.

    SemanticWatcher::ready_count

    fn SemanticWatcher::ready_count(self : SemanticWatcher) -> Int

    Required paths that are final and hold a value of the declared kind.

    SemanticWatcher::settled_at

    fn SemanticWatcher::settled_at(self : SemanticWatcher) -> Int

    Code units consumed when the last followed path became final, or -1 while that has not happened.

    SemanticWatcher::syntax_at

    fn SemanticWatcher::syntax_at(self : SemanticWatcher) -> Int

    Code units consumed when the root value became final, or -1 while the document still needs more input to be readable at all.

    SemanticWatcher::total_required

    fn SemanticWatcher::total_required(self : SemanticWatcher) -> Int

    How many paths the watcher follows.

    SemanticWatcher::verdict

    The verdict of what has arrived so far, against the whole skeleton. Never raises: an unfinished document is simply not ok yet.

    SemanticWatcher::watched_paths

    fn SemanticWatcher::watched_paths(self : SemanticWatcher) -> Array[String]

    The paths the watcher follows, in declaration order.

    Severity

    pub(all) enum Severity {
    Info
    Warning
    }

    Severity of a repair record.

    Info means the value is unchanged (only the notation was normalised); Warning means content may have been invented, dropped or altered.
    impl Eq for Severity

    Severity::equal

    fn Severity::equal(self : Severity, other : Severity) -> Bool

    Explicit Eq extension for Severity.

    Severity::not_equal

    fn Severity::not_equal(x : Severity, y : Severity) -> Bool

    Explicit Eq extension for Severity.

    Slot

    pub(all) enum Slot {
    KeySlot
    ValueSlot
    }

    Where a repaired token sits in the grammar (BR-07a).
    impl Eq for Slot

    Slot::equal

    fn Slot::equal(self : Slot, other : Slot) -> Bool

    Explicit Eq extension for Slot.

    Slot::not_equal

    fn Slot::not_equal(x : Slot, y : Slot) -> Bool

    Explicit Eq extension for Slot.

    Snapshot

    pub(all) struct Snapshot {
    value : Json?
    completeness : Completeness
    open_path : Array[PathSegment]
    exactness : Exactness
    ambiguous : Bool
    }

    A non-destructive view of the value parsed so far.

    advice_for

    fn advice_for(op : PlanOp) -> String

    The instruction one operation implies (FR-33).

    A second reading is more specific than the rule that produced it: R006 between two elements usually means "you forgot a comma", but when the two strings may equally be one concatenated string the useful sentence is the ambiguity, not the rule.

    analyze

    fn analyze(input : String, options? : Options, readiness? : Int, schema? : Schema) -> RepairPlan

    Analyse input under options (FR-33, FR-33).

    Never raises: a refusal is a plan whose decision is not executable and whose retry_hint carries the feedback. readiness is the sampling step of the syntax readiness measurement; 0 (the default) skips it, because it costs O(n / step) partial parses and most callers do not need it.

    auto_step

    fn auto_step(n : Int, step : Int) -> Int

    Sampling step: step when positive, otherwise one that samples a document about a thousand times.

    copy_json

    fn copy_json(v : Json) -> Json

    Deep copy of a Json value.

    decide

    fn decide(repairs : Int, exactness : Exactness, ambiguous : Bool) -> ExecutionDecision

    The decision for a result that was produced: repairs is the number of recorded records, because exactness alone cannot tell a clean document from one that needed a cosmetic repair (both are Exact).

    decide_error

    fn decide_error(code : String) -> ExecutionDecision

    The decision for a value that was refused: E011 is an ambiguity, every other refusal of a repair (E010) is a lossy one, and a fatal error that is not a repair refusal is Invalid.

    decided_at_permille

    fn decided_at_permille(input : String, schema : Schema, chunk? : Int, options? : Options) -> Int raise MendError

    The input fraction at which input was decided, or -1 when it never was (FR-39).

    RootReady and RootInvalid both count: what the caller needs to know is that the answer is final, not that the answer is yes. "" when the input is decided only at the end.

    early_stop_saving

    fn early_stop_saving(docs : Array[String], step? : Int) -> Int raise MendError

    Average share of the input, in per mille, that a consumer never has to read if it stops the generation as soon as every field of a document is ready (FR-29).

    The baseline is 0 by definition: waiting for the end of the document reads all of it. A document whose root is not an object contributes 0.

    exactness_of

    fn exactness_of(repairs : Array[Repair]) -> Exactness

    Aggregate the exactness of a result from its repair records (FR-17).

    The level is the highest risk among the records: no records is Exact, any Warning is Lossy, otherwise Cosmetic.

    field_readiness

    fn field_readiness(input : String, step? : Int, options? : Options) -> Array[FieldReady] raise MendError

    Readiness curve of one document (FR-29).

    step is the number of code units fed between two observations; 0 (the default) picks a step that samples the document about a thousand times, so the result does not depend on the document length.

    Since 0.6.0 this listens to the delta stream instead of re-reading the document: a top-level member is ready at the offset of its own final delta, which is one pass with one drain per step rather than one partial parse and one snapshot per step (FR-39). On a 100 KiB payload that is ~30x.

    A document whose root is not an object has no fields and yields [].

    has_ambiguity

    fn has_ambiguity(repairs : Array[Repair]) -> Bool

    Whether any record in repairs is ambiguous (FR-27).

    hint_context

    fn hint_context(text : String, offset : Int, radius : Int) -> (String, Int)

    Window of text around offset: radius code units on each side, with the offset itself always inside (FR-28). Never embeds the input in a structured field, so the caller chooses what to show.

    is_valid_json

    fn is_valid_json(text : String) -> Bool

    Whether text is valid JSON according to the strict core parser. This is the validation used by the test suite to prove that every repaired output is legal JSON; it performs no repair of its own.

    kind_of

    fn kind_of(v : Json) -> String

    The JSON kind of a value, for a mismatch message.

    lookup_path

    fn lookup_path(v : Json, path : String) -> Json?

    Look up a dotted path in a value, where a numeric step is an array index (FR-36). "" is the value itself.

    parse

    fn parse(input : String, options? : Options) -> ParseResult raise MendError

    Repair input and return the resulting Json value.

    plan_action

    fn plan_action(code : RepairCode) -> PlanAction

    The action a rule performs (FR-33). Total by construction: adding a RepairCode without a case here is a compile error.

    readiness_deciles

    fn readiness_deciles(docs : Array[String], step? : Int) -> Array[Int] raise MendError

    The readiness curve over docs, as eleven percentages: entry d is the share (0…100) of fields whose value was already finmap when d tenths of the input had been consumed (FR-29). Entry 0 is 0 by construction, entry 10 is 100.

    readiness_events_json

    fn readiness_events_json(events : Array[ReadinessEvent]) -> String

    JSON array of events.

    readiness_median

    fn readiness_median(docs : Array[String], step? : Int) -> Int raise MendError

    Median readiness point, in per mille, over every field of every document (FR-29). Documents without fields are ignored.

    repair

    fn repair(input : String, options? : Options) -> RepairResult raise MendError

    Repair input in one shot. Returns the repaired text, the repair records and Complete.

    semantic_readiness

    fn semantic_readiness(input : String, schema : Schema, step? : Int, options? : Options) -> SemanticReadiness raise MendError

    Syntax readiness and schema readiness of one document against one skeleton (FR-36, FR-39).

    Since 0.6.0 this is a thin report over SemanticWatcher: the document is fed in step-sized pieces and the numbers are read off the events, so the cost is one pass plus one delta drain per piece instead of one partial parse and one snapshot per piece. The result is the same kind of measurement — a fraction of the input — but it no longer rebuilds the value tree a thousand times, and settled_permille is new: the moment the answer became final even when the answer is no.

    syntax_readiness_permille

    fn syntax_readiness_permille(input : String, step? : Int) -> Int

    The first input fraction at which the document was already the value it ends up as (FR-29, FR-36). -1 when the input cannot be parsed at all.

    This is the "JSON syntax ready" number of the readiness report: it is 1000% semantics but per mille of the input, i.e. 1000 means "not before the end".

    watch_readiness

    fn watch_readiness(input : String, schema : Schema, chunk? : Int, options? : Options) -> Array[ReadinessEventAt] raise MendError

    Watch input in chunk-sized pieces and return every event (FR-39). The events are the same the streaming API produces; this form exists so that an audit can print them with a position.