The Document API
Rules read documents through bbox.Doc(), a curated facade designed for rule authors. It
is the API to write against.
Document rules run through kis rules, with the document beside each
input (--with-bbox) or named on the command line (-b page=invoice.bbox). rules.svc serves no
documents today: a scope whose rules name bbox does not load there.
Four things to learn
Section titled “Four things to learn”A rule reads a document with an entry, a scope, a verb, and a coercion. That is the whole shape:
bbox.Doc() .Top(10) .Has("Closing Disclosure")// entry scope verb
out.Set("amount", bbox.Doc().Right("Loan Amount").Currency());// entry verb coercionScopes narrow, verbs ask or fetch, coercions turn a hit into a typed value. Most rules need one of each.
Matching is fuzzy and case-insensitive by default
Section titled “Matching is fuzzy and case-insensitive by default”You do not configure matching. It is loose out of the box.
It is easy to under-read this as a convenience. It is not. It removes a category of work.
bbox.Doc().Has("closing disclosure") // matches "CLOSING DISCLOSURE"bbox.Doc().Has("SELLER'S TRANSACTION") // also matches "SELLERS TRANSACTION"Case, spacing and the apostrophe/punctuation variants that OCR produces are absorbed. You write the phrase as a human would say it, once.
The bbox.Get methods made this the author’s problem. Every call carried tolerance numbers:
.FindIPhraseFuzzy("Loan Terms", 2, 4, 0, 0).FuzzyTextNear("Name", "right", 1, 2, 0.3)Nobody can tell you what 2, 4, 0, 0 should be for a phrase they have not seen, and a matcher that
does not absorb the apostrophe forces a rule to list both SELLER'S TRANSACTION and
SELLERS TRANSACTION.
The facade derives the tolerance from the phrase instead. A surface you choose from can be used correctly by a person and by a small language model; a surface you tune cannot. That was a design constraint, not a nicety.
Opting out
Section titled “Opting out”When you genuinely need strictness, a document code, an identifier, a legal string where a near-miss is wrong:
| Modifier | Effect |
|---|---|
Exact() | Turns off loose matching for that chain. Case is still ignored |
MatchCase() | Case-sensitive, and implies Exact() |
bbox.Doc().Exact().Has("FORM-1099-B")bbox.Doc().MatchCase().Has("SECTION A")MatchCase() implies Exact() deliberately: case-sensitive-but-still-fuzzy is not a
combination that means anything useful, so the facade does not offer it.
Reach for these when a false positive costs you something. Leave them off otherwise, the
default is right far more often than not, and every Exact() is a phrase you have promised to
keep spelled exactly that way.
bbox.Doc() // the documentbbox.Doc("page") // a specific loaded document, by nameThe name is optional because it is almost always ceremony. With no argument, bbox.Doc() resolves
the only loaded document, or the one named page when several are loaded. Supply the name only
when a rule genuinely works across more than one loaded document.
Scopes
Section titled “Scopes”Each returns a narrowed Doc, so they chain.
| Scope | Narrows to |
|---|---|
Top(pct) / Bottom(pct) | A band measured from the top or bottom, as a percentage |
Band(fromPct, toPct) | An arbitrary horizontal band |
Region(x0, y0, x1, y1) | An explicit rectangle, in percent |
Page(n) | One page |
Section(heading) | From a heading to the end of its section |
Between(start, end) | Between two phrases |
Until(phrase) | From here until a phrase |
Line() / Lines(n) | The current line, or the next n |
Exact() / MatchCase() | Opt out of loose matching: see Matching |
Top(10) reads better than a rectangle and is what most rules meant. There is no fixed
“header”: choose the depth that fits the form.
Presence verbs
Section titled “Presence verbs”| Verb | Returns |
|---|---|
Has(phrase) | bool |
HasAll(phrases...) | bool. Every phrase present |
HasAny(phrases...) | bool: at least one |
HasPattern(pattern) | bool |
Count(phrase) / CountOf(pattern) | int |
Checked(label) | bool: checkbox state |
Label-relative reads
Section titled “Label-relative reads”The four directional verbs read a value positioned relative to a label, and each returns a
Value:
bbox.Doc().Right("Loan Amount")bbox.Doc().Below("Total")bbox.Doc().Left("Balance")bbox.Doc().Above("Signature")Anchor chains
Section titled “Anchor chains”Extra arguments form a chain: each anchor is located below the one before it, and the read happens in the verb’s direction from the last.
bbox.Doc().Right("Date Issued", "Closing Date")bbox.Doc().Below("Current name", "First name")The chain replaces the string anchor paths of the bbox.Get methods, "Date Issued<below>|Closing Date"
and "A |> B". Two things improve by making them arguments rather than syntax. Each label is
separately checkable, so a failing chain reports which link broke instead of returning an empty
string. And there is no string grammar to get subtly wrong.
Coercions
Section titled “Coercions”A Value becomes typed by asking:
| Coercion | Produces |
|---|---|
Str() | The text, as a string |
Num() | A number. Separators and a leading $ are stripped; 0 when the text is not a number |
Text() | The text, as a Value |
Number() | The first number in the text, as a Value |
Currency() | The amount with symbols and separators removed, as a Value; accounting negatives such as (1,234.56) keep their sign |
Date() | The date as YYYY-MM-DD; a miss when it does not parse |
Percent() | The percentage as a bare number |
YesNo() | Yes or No from affirmative or negative text, including Y and N |
TitleCase() | Title-cased text |
Match(pattern) | The first substring matching the pattern |
out.Set takes a Value directly, out.Set("amount", bbox.Doc().Right("Loan Amount").Currency()),
and carries its confidence along as an attribute. A read that missed writes nothing: the key is
absent from the output.
And for inspecting the read itself:
| Method | Use |
|---|---|
Found() / Missing() | Whether anything matched |
Or(fallback) | A default when missing |
Confidence() | The match confidence, 0 on a miss |
Why() | On a hit, what matched as the label, on which page, at what distance; on a miss, why it missed |
Reason() | Why a read missed, such as label not found: Loan Amount; empty on a hit |
Why() is the one to reach for when a rule silently produces nothing. A Value that did not
match knows which anchor in its chain failed.
Tables, forms and rows
Section titled “Tables, forms and rows”| Entry | Returns | For |
|---|---|---|
Table() / TableWith(text) | Table | The document’s table, or one containing text |
Form(name) / HasForm(name) | Form | A named form |
Rows(startHeading, endHeading) | Rows | A column-aligned section between two headings |
Cell(rowLabel, colLabel) | Value | One cell by its labels |
Table offers Row, RowWith, Column, Cell, Headers, Records, RowCount,
ColumnCount. Rows, for column-aligned sections that are not real tables, offers Col,
ColNth, ColRightOf, ColUnder, SplitOn, Strip, Records, Count.
Records() on either gives you the rows as maps, which is usually what you want to hand to
out.Set.
Typography and structure
Section titled “Typography and structure”| Method | Use |
|---|---|
Title() / IsTitle(phrase) | The document title |
Headings() / IsHeading(phrase) / HeadingCount() | Heading structure |
IsBigText(phrase) | Whether text is visually prominent |
FontSize(phrase) / BodyFontSize() | Type sizes |
LineWith(phrase) / FirstLine() / LineCount() | Lines |
PageCount() / TableCount() | Counts |
AllText() | Everything, as one value |
Typography signals are how you distinguish a heading that happens to read like body text from one that is actually set as a heading.
What this replaced
Section titled “What this replaced”The same classification rule, before and after:
// With bbox.Get: six lines, twelve magic numbers, two presence idiomsbbox.Get("page").InRegionPct(0.0, 0.0, 60.0, 10.0) .ContainingIPhraseFuzzy("Closing Disclosure", 2, 4).Count() > 0 &&bbox.Get("page").FindIPhraseFuzzy("Loan Terms", 2, 4, 0, 0).Len() > 0 &&bbox.Get("page").FindIPhraseFuzzy("Projected Payments", 2, 4, 0, 0).Len() > 0 &&bbox.Get("page").FindIPhraseFuzzy("SELLER'S TRANSACTION", 2, 4, 0, 0).Len() == 0 &&bbox.Get("page").FindIPhraseFuzzy("SELLERS TRANSACTION", 2, 4, 0, 0).Len() == 0// With bbox.Doc()bbox.Doc().Top(10).Has("Closing Disclosure") &&bbox.Doc().HasAll("Loan Terms", "Projected Payments") &&!bbox.Doc().Has("SELLER'S TRANSACTION")The second negative check disappears entirely: loose matching absorbs the apostrophe variant, so there is nothing to enumerate.
The tuning numbers are gone too, and that is the deeper point. 2, 4, 0, 0 were fuzz
thresholds an author had to tune; the facade derives them. A surface you choose from is
one both a person and a small model can use correctly, a surface you tune is not.
Migration
Section titled “Migration”| Instead of | Write |
|---|---|
bbox.Get("page") | bbox.Doc() |
.InRegionPct(0,0,100,20) | .Top(20) |
.InRegionPct(0,0,100,100) | nothing. It was a no-op |
.ContainingIPhraseFuzzy(p, 2, 4).Count() > 0 | .Has(p) |
.FindIPhraseFuzzy(p, 2, 4, 0, 0).Len() > 0 | .Has(p) |
.FuzzyTextNear(l, "right", …) | .Right(l) |
.FuzzyTextNearCapped(l, …) | .Right(l) |
.GetOCRTextSimple("A |> B") | .Below("A", "B") |
"A<below>|B" chains | .Below("A", "B") |
.IsCheckboxChecked(l) | .Checked(l) |
.ExtractColumnAlignedSection(…) | .Rows(start, end) |
.SafeExtractForm(n) / .HasForm(n) | .Form(n) / .HasForm(n) |
Existing rules do not need rewriting to keep working. Migrate when you touch a rule for another reason.
The runtime half
Section titled “The runtime half”The facade fixes what a rule reads. On the writing side, a rule leaves the agenda once it fires,
so it needs no Retract; a later rule sees an earlier rule’s out write, so it needs no
Changed; and dotted output keys nest. Together they are why the example above is three lines
rather than fourteen. See
Writing rules.
See also
Section titled “See also”- Built-in functions: the full callable surface
- Writing rules: rule structure and the output envelope
- The document model: what the spatial model contains