Skip to main content

folio-resolve

The shared FOLIO source-text→concept matching engine. One pinned, MIT-licensed Python library that maps arbitrary source text (book headings, deposition transcripts, intake narratives, task titles) to concepts in FOLIO — the open legal ontology of 18,000+ concepts.

This is a lift-and-improve extraction. The matching intelligence already existed across folio-mapper, folio-enrich, and folio-insights — but it had diverged (enrich literally forked mapper's scorer) and was informally shared through a fragile sys.path hack. folio-resolve is the single source of truth those repos now pin, plus the capabilities the recorded failures demanded.


Why this exists

Three repos independently built the same engine and drifted apart:

  • folio-mapper wrote the canonical, word-order-invariant scorer + a 4-stage pipeline + a FAISS embedding index.
  • folio-enrich copied mapper's scorer (its search.py says "ported from folio-mapper" in the code) and added an Aho-Corasick entity ruler, a reconciler, a domain-prior judge, and a feature-rich annotate/feedback UI.
  • folio-insights sys.path-imports both siblings at runtime — documented as fragile.

A book-annotation review (Ch02) then surfaced failures that no amount of local patching would fix: place-names over-scoring (Slovenia → 99 units), homonyms (Action ≠ Auction), conjoined compound headings that match nothing, metadata tagged as substance, and pure-semantic maps with no shared label token (Presumptions → Litigation Burdens of Proof). Those became this library's new capabilities.

What it does

Capability Module Solves
Word-order-invariant relevance scoring scoring "arbitration rules" = "rules of arbitration"
Multi-strategy label search + legal expansions scoring, pipeline recall on paraphrases
Pure-Python Aho-Corasick entity ruler entity_ruler, matching trusted exact-label spans, no spaCy/C deps
Candidate reconciliation (ruler + LLM + semantic) reconciler one clean candidate set with provenance
Span decomposition (conjunction split + shared head) decompose "Findings of Fact and Conclusions of Law" → both siblings
Place-name / short-label gates gates kills Slovenia→99 and Presumptions→Northern Mariana Islands@90
Alias/homonym blocklist blocklist deterministic Action ≠ Auction guard
Metadata/front-matter exclusion sources never tag the copyright page
Multi-tag domain prior (auto-suggest + validate/add) domain_prior Defenses → Litigation Defenses; corpus carries many subjects
Shared label→IRI resolution (decompose-first, calibrated 0–100 bar, branch-carrying) resolve every consumer resolves identically; law→Delaware@90 no longer accepted
Score calibration (weak-band recalibration) calibration verdict-labeled score→P(correct) fit
Lemma-key index augmentation (build-time spaCy, cached JSON) lemma singular agreement reaches plural-labelled Agreements
LLM judge interface + domain-prior prompts judge context-aware disambiguation, verdict enforcement
Annotate primitives (confidence, per-tag verdicts, notes, reject/restore, insights) annotate the self-improving feedback loop, as a library
4-stage pipeline: filter → expand → rank → judge pipeline the build-once-use-many entry point

Personas

  • Book-extraction pipelines (folio-insights, books) — offline, CLI-first tagging of treatise text.
  • Intake / matter classifiers (alea-intake, mootloop) — narrative → FOLIO concepts for routing.
  • Interactive mappers (folio-mapper, clio-skills) — a UI over the library's ranked candidates.
  • The FOLIO ontology team — a reference implementation of "good" source-text→concept matching.

Use cases

  • Tag a chapter of a litigation treatise with FOLIO concepts, with a litigation domain prior so "Defenses" resolves to Litigation Defenses, not the generic sense.
  • Resolve a compound heading like "Proposed Findings of Fact and Conclusions of Law" to the two sibling concepts it actually names.
  • Suggest the subject tags for a corpus ("Personal Injury Depositions" → Personal Injury + Deposition) and let a human validate/add via a FOLIO taxonomy-tree picker.
  • Feed a reviewer's wrong verdict on a homonym straight into the alias blocklist so the mistake never recurs — the self-improving loop.

Install

uv add folio-resolve                 # core (pure-Python, only pydantic)
uv add "folio-resolve[folio]"        # + folio-python live ontology adapter
uv add "folio-resolve[embedding]"    # + sentence-transformers / faiss for the semantic path
uv add "folio-resolve[spacy]"        # + lemma-key index augmentation (build-time only)

The core is pure-Python — the scorer, decomposition, gates, blocklist, domain-prior, reconciler, calibration, and annotate primitives depend on nothing heavier than pydantic. FAISS, sentence-transformers, spaCy, and folio-python live behind Protocol seams with working pure-Python defaults, so the whole test suite runs with no model downloads and no network.

Lemma-key augmentation ([spacy] extra, v0.2.0)

The 2026-07 ruler shootout (bench/RESULTS.md) showed the recall edge folio-enrich's spaCy ruler appeared to have lives in its label index, not its engine: lemma keys let the singular surface agreement reach the plural-labelled concept Agreements (+200/200 lemma-gold hits, +698 corpus matches). v0.2.0 promotes that indexing here, engine-agnostic:

from folio_resolve import FOLIOEntityRuler, augment_labels

labels = provider.all_labels()                      # any OntologyProvider
labels = augment_labels(                            # adds lemma_preferred / lemma_alternative keys
    labels,
    cache_dir="~/.folio-resolve/lemmas",            # cached by ontology hash + LEMMA_VERSION
    ontology_hash=owl_content_hash,
    on_missing_spacy="skip",                        # no [spacy] extra -> un-augmented index, no crash
)
ruler = FOLIOEntityRuler()
ruler.load_patterns(labels)                         # pure-Python matching, zero heavy deps

spaCy is needed only at index-build time (computing what each label's lemma is; requires the [spacy] extra plus python -m spacy download en_core_web_sm). Steady-state consumers load the cached JSON lemma map and never import spaCy. Without the extra: the default raises a clear SpacyNotInstalledError; on_missing_spacy="skip" degrades to the un-augmented index. Use it whenever documents refer to concepts in the singular while the ontology labels them in the plural (FOLIO does, pervasively); skip it for exact-vocabulary corpora. Legal pluralia tantum (damages, proceedings, wills, …) are denylisted per-ontology via OntologySpec.behavior.lemma_denylist.

Judge transport hardening (v0.2.1)

parse_judge_json now survives everything models actually emit — ```json fences (a new exported strip_markdown_fences does the stripping), a non-list judged, non-dict rows, a non-object payload, None input, non-string reasoning, and an adjusted_score that is non-numeric ("high" → the row is dropped, not an exception) or out of range (clamped to 0-100 before verdict enforcement, so a "penalized" verdict cannot escape the scale). Surfaced by the folio-mapper migration: mapper is the donor of these verdict rules and had kept a local parse loop precisely because the library's was weaker than the code it was lifted from.

Consumer-driven API additions (v0.3.0)

Three gaps the alea-intake migration recorded (SCHEDULE.md row 4) are closed. All are additive — default behavior is bit-identical, pinned by a golden no-drift table in tests/test_scoring.py so folio-mapper's and folio-enrich's committed captures stay valid.

  • compute_relevance_score is type-defensive. Every text argument is coerced: a None preferred_label (folio-python returns one for concepts with no preferred label), a test double, a number, a non-str synonym entry — all read as absent instead of raising TypeError out of re.findall. A non-str label scores 0.0. Consumers can drop their boundary coercion shims. Same spirit as the v0.2.1 parse_judge_json hardening.

  • PlaceNameGate takes a consumer vocabulary. extra_tokens (place names, matched against label tokens and the whole label so multi-word names work), extra_markers (substring phrasings — ("city of", "republic of") catches City of Exampleton whatever it's called), and extra_branch_markers (extra governed branches). A place_tokens property exposes the merged vocabulary. This retires the parallel local backstops consumers had to carry:

    gate = PlaceNameGate(
        min_signals=2,
        extra_tokens={"macedonia", "rize", "europe", "north america"},
        extra_markers=("city of", "republic of", "province of"),
    )
    
  • The specificity penalty is weightable. compute_relevance_score(..., specificity_penalty=w) scales the "candidate is more specific than the query" haircut: 1.0 is the historical default, 0.0 disables it. Consumers whose queries are shorter than their targets by construction (a 1-2 word claim name against a 3-5 word FOLIO label — Habitability → Breach of Warranty of Habitability, scored 67.5 at full penalty) can damp it rather than fork the scorer. Consumers that map taxonomy nodes, where an over-specific target IS an error, leave it at 1.0.

Adopting PlaceNameGate is a per-path decision, not a per-repo one — see docs/migration/SCHEDULE.md.

Bring Your Own Key (BYOK)

folio-resolve is key-agnostic — it never reads an env var, instantiates a provider SDK, or makes a network call on its own. The zero-key deterministic core (ruler, scoring, decomposition, gates, blocklist, metadata exclusion, calibration, annotate) runs fully offline and free. Three optional stages accept a provider through a typing.Protocol seam you fill with an object you construct:

Stage Protocol Buys you Absent →
Judge Judge.complete(system, user) -> str context-aware disambiguation + verdict enforcement items pass through unjudged
Embeddings EmbeddingProvider (embed/embed_batch/dimension) semantic recall for no-shared-token maps local all-MiniLM-L6-v2 default (no key), or skipped
Domain-prior suggestions DomainPriorSuggester(ontology) auto-suggest corpus subject tags supply tags manually

You own the key, the vendor (OpenAI / Gemini / Anthropic / local), and the spend. The library ships the judge prompt builders and deterministic verdict enforcement; you supply only the raw model call. Graceful degradation is the default — no key means deterministic-only output with items marked unjudged, never a crash. Reference cost: ≈ $0.12 / chapter on gemini-2.5-flash-lite (~1,875 calls, ~652K tokens, 464 units). Full guide, env-var conventions, and a minimal wiring example per vendor: docs/BYOK.md.

Quick start

from folio_resolve import InMemoryOntology, Concept, MatchPipeline, DomainPrior

ontology = InMemoryOntology([
    Concept(iri="R-defenses", label="Litigation Defenses", branch="Objectives"),
    Concept(iri="R-arb", label="Arbitration Rules", branch="Service"),
])
pipe = MatchPipeline(ontology=ontology)

# word-order-invariant
pipe.match("rules of arbitration")        # -> Arbitration Rules

# domain prior threads a subject into the (optional) judge
prior = DomainPrior.from_manifest_subjects("treatise", [("R-lit", "Litigation")])
pipe.match("Defenses", domain_prior=prior)

Development

uv sync --extra dev
uv run pytest          # full suite, pure-Python, no network
uv run mypy src        # strict
uv run ruff check

License & attribution

MIT — see LICENSE. Every extracted component and dependency is logged in THIRD-PARTY.md. The migration schedule for consumer repos is in docs/migration/SCHEDULE.md.

Metadata

Release files for folio-resolve 0.3.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for folio-resolve 0.3.0
File Size Uploaded
folio_resolve-0.3.0.tar.gz 92.8 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for folio-resolve 0.3.0
File Interpreter ABI Platform
folio_resolve-0.3.0-py3-none-any.whl Python 3 none any Details

Total release size: 152.7 kB

Release files / folio_resolve-0.3.0.tar.gz

Download URL folio_resolve-0.3.0.tar.gz
Size 92.8 kB
Tags Source
SHA-256 checksum
How to use checksums
b9bdd82695b345f94c59827a5afa41143f8b13ef472a4a1216dae30a0ce0665b
BLAKE2b-256 checksum
How to use checksums
4f111958c1d0f7d32cd01cdba2b35f4d9108cb1110298b8bd97a968a154eb8f0
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Jul 30, 2026.

Transparency log

Release files / folio_resolve-0.3.0-py3-none-any.whl

Download URL folio_resolve-0.3.0-py3-none-any.whl
Size 59.9 kB
Tags Python 3
SHA-256 checksum
How to use checksums
9ef1fb8b948e664a0fee2448ac6041efb00cd703d3746146805bc0283112913c
BLAKE2b-256 checksum
How to use checksums
aab60991703d4c4ecc52490c6d57e1f1e697b046849e0f9c2204f7820199002b
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Jul 30, 2026.

Transparency log

Release history Release notifications | RSS feed

0.4.0

2 release files

0.3.1

2 release files

This release

0.3.0 This release

2 release files

0.2.0

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page