# Choosing a Parser Result This page explains the parser-result model the parser is trying to serve at a high level. It is intentionally written around the design goal first, not just around the current wrapper names. If you only remember one thing from this page, remember this: the parser is trying to give callers three main kinds of help, plus one more exploratory policy layer, and those should stay clearly separated. 1. A default tree family for callers who want a usable tree immediately and want recovery applied by default. 2. A conservative tree path for callers who want a tree immediately but do not want that applied recovery to survive in the final tree. 3. A findings-first `analyze()` path for callers who want parser facts first and want to decide later how to materialize them. 4. A policy path, still exploratory, where the caller wants to make some of those later materialization decisions itself. The first three paths are the core story, and all three are shipped today. The fourth path is worth exploring, but it should not be treated as equally settled yet. ## The simple version The intended result families look like this. ```text default tree family diagnostics optional default recovery already applied if the parser is not confident, leave it as text conservative tree diagnostics kept no applied recovery in the final tree collapse malformed committed regions back to text when needed analyze() findings lane diagnostics kept replayable findings kept recovery data listed, not applied caller materializes later, or stays at the event level policy lane starts from the same analyze() findings lane caller chooses some recovery outcomes explicitly still materializes through a tree policy step later ``` Today's public API already exposes the first two tree-first paths, and it does not fully expose the third yet. The current wrappers are called out near the end of this page, but they should not be the first thing the reader has to learn. ## Path 1: default tree family Use this when you want a usable tree immediately and want good defaults by default. What you get: - the cheapest tree-building path when you call `parse()` - default recovery applied in the final tree when the source gave strong enough structural evidence - optional diagnostics, depending on which wrapper you call What this family is trying to do: - keep clearly valid structure - stay fast when diagnostics are not needed - apply the parser's default tolerant recovery rules when the source clearly committed to a structure - treat uncertain or weakly signaled regions conservatively as text The important nuance is that no diagnostics should not be confused with no recovery. `parse()` still never throws, and it still keeps the default tolerant tree shape when the parser has enough evidence to do that honestly. This family is the path for callers who are effectively saying: ```text give me the normal tree keep good defaults and only make me pay for diagnostics if I ask for them ``` Today that family is split across three wrappers: ```ts const tree = parse(source); const withDiagnostics = parseWithDiagnostics(source); const withSummary = parseWithRecovery(source); ``` - `parse()` gives the default tree with no preserved diagnostics - `parseWithDiagnostics()` gives that same tree plus diagnostics - `parseWithRecovery()` gives that same tree plus diagnostics and a `recovered` summary boolean ## Path 2: conservative tree Use this when diagnostics matter and you do not want applied recovery to remain visible in the final tree. This is the path for callers who are effectively saying: ```text keep going show me the diagnostics but do not keep the applied recovery in the final tree ``` Today this path is: ```ts const result = parseStrictWithDiagnostics(source); ``` What you get: - diagnostics preserved in the result - a conservative tree that collapses malformed committed regions back to text when the source never justified keeping the recovered wrapper in the final tree This path is useful for: - inspection - linting - editor workflows that want to surface the parser's findings without keeping every recovered wrapper in the final tree ## Path 3: `analyze()` findings lane Use this when you want the fullest problem picture and do not want the parser applying recovery on your behalf in the final tree yet. This is the path for callers who are effectively saying: ```text show me everything you found do not apply the recovery for me yet I may want more than one materialization from the same parse ``` What this path gives you today: - diagnostics preserved in the result - replayable parser findings, not just a one-shot event generator - recovery data listed explicitly - no recovery applied on the caller's behalf - enough information to choose which repairs to keep, discard, or replace later at materialization time Current API: ```ts import { analyze, materialize, TreeMaterializationPolicy } from '@okikio/wikitext'; const findings = analyze(source); // inspect without materializing a tree for (const entry of findings.recovery ?? []) { console.log(entry.kind, entry.code, entry.policies); } // materialize once or many times from the same findings const tolerant = materialize(findings); const strict = materialize(findings, { policy: TreeMaterializationPolicy.SOURCE_STRICT, }); ``` The shapes live in `parse.ts`: ```ts interface AnalyzeOptions { readonly recovery?: boolean; } interface ParseRecovery { readonly kind: | 'missing-close' | 'unterminated-opener' | 'unclosed-table' | 'mismatched-exit' | 'orphan-exit' | 'eof-autoclose'; readonly code: string; readonly position: Position; readonly anchor: ParseDiagnosticAnchor; readonly node_type?: WikistNodeType; readonly policies: readonly TreeMaterializationPolicy[]; } interface ParseFindings { readonly source: TextSource; readonly events: readonly WikitextEvent[]; readonly diagnostics: readonly ParseDiagnostic[]; readonly recovery?: readonly ParseRecovery[]; } function analyze( source: TextSource, options?: AnalyzeOptions, ): ParseFindings; function materialize( findings: ParseFindings, options?: { readonly policy?: TreeMaterializationPolicy }, ): ParseOutput; ``` The ownership split is explicit: - `analyze()` owns parser facts - `materialize()` owns tree policy - the caller decides whether to materialize at all It also keeps the event-stream-first architecture intact. Trees stay consumers of parser findings, not the parser's only public truth. ## Path 4: policy lane Use this only when you need more control than the package-owned default and conservative materializers can give you. This path is still exploratory. It is for callers who are effectively saying: ```text show me the parser's recovery data I want to accept some of it and reject other parts myself ``` This path should not be a new parser truth. It should start from the same `analyze()` lane as Path 3. One concrete exploration could look like this. ```ts type RecoveryDecision = 'keep-structural' | 'collapse-to-text'; interface CustomMaterializeOptions { readonly policy: 'custom'; readonly resolve_recovery: ( recovery: ParseRecovery, ) => RecoveryDecision; } function materialize( findings: ParseFindings, options: CustomMaterializeOptions, ): ParseOutput; ``` That is attractive for: - editor tooling that wants product-specific repair rules - compatibility layers for different wiki profiles - lint or migration tools that want a conservative default with a few explicit exceptions But it is also where the API can become brittle too early. The main risks are: - freezing the recovery taxonomy before it is mature - exposing callback timing as if it were a stable parser contract - turning parser continuation details into user-configurable hooks too soon - making sessions and incremental reparsing harder to reason about That is why this path should stay exploratory for now. The likely order is: 1. make the `analyze()` lane real 2. make the default materializers explicit 3. only then decide whether a custom policy lane belongs in the public API ## Detailed examples The easiest way to make the paths concrete is to walk through the same kind of malformed input more than once. ## Example 1: ordinary valid markup ```text Paragraph with note ``` All four paths should agree on the basic structure here. - default tree family: keep the `reference` node - conservative tree: keep the same `reference` node too, because no recovery is needed - `analyze()` lane: keep the same parser facts, with no recovery list to apply - policy lane: same as the `analyze()` lane because there is no recovery choice to make ## Example 2: opener is complete, close tag is missing ```text Paragraph with note ``` This is the clearest HTML-like recovery case. The source has already made its intent obvious. - default tree family: keep the `reference` structure in the final tree - conservative tree: collapse that malformed committed region back to text - `analyze()` lane: preserve the same parser finding, list the missing close and possible repair, but do not apply that repair for the caller - policy lane: start from that same missing-close repair and let the caller choose whether the final tree keeps a structural `reference` node or collapses the region back to text ## Example 3: opener starts but never reaches `>` ```text Paragraph with