Extraction proposals#
This subject previously had provenance only in frozen ADR history
(0001-proposals-only.md) with no living
decision ratified under the topic-keyed decision registry. This update
ratifies the first living decision for the subject: inferenceType, a
per-field grounding-honesty classification that refines what an
ExtractionProposal's provenance means, without changing ADR 0001's
proposals-only identity or its chars:<start>-<end> provenance contract.
Decision#
TargetFieldSchema.inferenceType?: "explicit" | "inferred"(caller-set, 100% optional)."explicit"— the value should appear verbatim in the source text; offset-verification of the VALUE itself (not just the excerpt) is meaningful, and adapters may instruct the provider to copy it verbatim rather than paraphrase/reformat it."inferred"— the value is derived/normalized/classified from the source (e.g. computed, reworded, or categorized); the excerpt still grounds the proposal, but the value itself can never be offset-verified against the source text. Absent means unspecified — today's behavior, no classification implied either way.- Carry-through onto
ExtractionProposal.inferenceType.extract()'snormalizeChunkProposalsattachesinferenceTypefrom the MATCHED (post-normalization)targetSchemaentry — looked up by the declared path, the same lookuppathIndices's indexed-path recovery already uses, so an indexed-array proposal (e.g. provider-emitted"schedules[0].startDate"recovered against a declared"schedules[].startDate") still resolves the tag correctly — ONLY when that entry declares it; the key is entirely absent ("inferenceType" in proposal === false) otherwise. This mirrors the existingpathIndicesconditional-attach idiom exactly. - Anthropic adapter prompt guidance,
description-string only.buildExtractionTool's per-field line gains one extra sentence sourced fromf.inferenceType:"explicit"appends a verbatim-copy instruction;"inferred"appends a derived/normalized-value instruction that still requires a grounding excerpt;undefinedappends nothing.input_schema(thefieldPath/value/confidence/excerpt/locatorshape andrequiredlist) is byte-identical before and after — this is prompt guidance, not a schema change, so no client-side parsing of tool output changes. - Carry-through + prompt guidance ONLY — no stricter verification this
slice.
extract()gains zero new drop/warning/clamp logic tied toinferenceType. An"explicit"-tagged field whose provider-returnedcandidateValuedoes not literally match the excerpt is proposed exactly as it would be today — reviewed by the caller, not gated by Traverse (see "Out of scope" below for why).
Enumerated observable deltas (everything a consumer could notice, positive or negative):
TargetFieldSchemagains one new optional key. A schema object that never sets it is unaffected — no default value materializes, the key is simply absent, same as today.ExtractionProposalgains one new optional key, populated ONLY when the proposal's matched schema field declaredinferenceType. For every existing caller/schema (untagged), no proposal ever gains this key —"inferenceType" in proposalisfalse,JSON.stringify(proposal)output is unchanged,Object.keys(proposal)is unchanged.buildExtractionTool()'s returnedAnthropicTool.descriptionstring gains one extra sentence per field ONLY for fields that declareinferenceType; an untagged field's rendered line is byte-identical to today.input_schemais completely unchanged.- No change to
extract()'s drop/warning/clamp semantics for ANY field, tagged or not (no new warning strings, no new drop conditions). A tagged"explicit"field whose provider-returned value doesn't verbatim-match the excerpt is proposed exactly as it would be today. - No change to
ExtractionResult's shape,warningscontent/count, orproviderCalls/totalTokensUsedaccounting for any existing caller. - No change to the Survey compat fixture's typecheck status (optional
field; existing
toExtraction()mapping remains valid). - Downstream: a caller who reads
proposal.inferenceTypecan now render an honest "offset-grounded value" vs. "derived value, excerpt-grounded only" badge; a caller who does not read it observes nothing different at all.
Evidence annotations (evidenceMatch)#
extract() sets evidenceMatch on every proposal it returns: deterministic,
versioned facts computed from the proposal's own value, schema entry and
excerpt. It is an annotation, not a verdict. No proposal is dropped, warned
about, clamped or reordered because of it, the [AC6] test above is
unchanged, and a provider-supplied evidenceMatch is ignored. Traverse
computes only these deterministic checks; checking a value with a model is
not Traverse's job, and no Traverse confidence is derived from them.
evidenceMatch: {
checkerVersion: "evidence-match-v4";
schema: "ok" | "type-mismatch" | "enum-mismatch" | "format-invalid";
valueInExcerpt: "match" | "mismatch" | "not-evaluated" | "not-applicable";
tokenBoundary?: boolean;
}
schemais exact. The value's JSON type must be the declaredtype(the same predicate task examples use); anenumstring outsideenumValuesisenum-mismatch; adatestring that is not an ISO-8601 calendar date in extended format (YYYY,YYYY-MM,YYYY-MM-DD, optionallyTHH:MM[:SS[.fff]]andZor±HH:MM, each component range-checked) isformat-invalid. Anenumthat declares noenumValueshas no set to be outside of and is checked as a string.valueInExcerptruns only for fields declaredinferenceType: "explicit"with a scalar type. Inferred and unclassified fields, andarray/objectfields, arenot-applicable: only an explicit value is meant to appear in the source. The rule for every type is containment on token boundaries after a fixed normalization; equality is not required, so"open"matches"Status: Open".matchmeans the value was found in the excerpt, not that the excerpt supports it. The checks are built so that a doubtful case isnot-evaluatedrather than a falsematch:string/enum: NFKC and lower case. Whitespace and punctuation fold away, except for punctuation that can change meaning:+ - # %attached to a word or digit, and. ,between two of them.matchneeds the value, with that punctuation, as a whole run of the excerpt ("open"does not match"Reopened";"C++"matches"C++."). When only the words agree and that punctuation or a sign differs ("A+"vs"Grade: A-","C++"vs"C#","-5"vs"5","1.5"vs"1,5"), the result isnot-evaluated. One exception: a phone-style value (only digits, spaces and( ) . - /, at least seven digits in at least three groups, no leading-) compares by its digit groups, so"303.555.1234"matches"(303) 555-1234". A signed value or one with a single decimal separator ("-1234567","1234567.89") does not.- Negation, for string, enum, number and boolean values: when a cue
(
not,no,never,without,cannot,nor, or a word ending inn't) is in the three words before an occurrence of the value, the result isnot-evaluated("open"vs"no longer open",5vs"not 5").No.orNo:directly before a digit is read as the abbreviation for number, not as the word no ("Ticket No. 5"); plainnobefore a digit stays a negation (3vs"There are no 3-bedroom units"). Dates are not checked for negation. number: written numbers in the excerpt, each optionally signed and led by a currency symbol, with comma thousands separators and a point decimal, not touching a letter or digit (3is not read from2023). Dates, times and fractions written in digits (2026-06-09,10:30,1/2) are skipped, so6is not read from a date, and a number isnot-evaluatedrather thanmatchwhen only such a form contains it.matchwhen a number equals the value. A number written at another scale or sign convention (45%, accounting(5),$4.2 million) neither matches nor mismatches: when no plainly written number equals the value and one of these is present, the result isnot-evaluated.boolean: the words yes/true and no/false.matchonly when the excerpt has the value's word, not negated, and no word of the other polarity;mismatchonly in the reverse case."This is not true","no longer yes"and"Yes, no refunds"arenot-evaluated.date: the value must beYYYY-MM-DD; the excerpt is read for2026-06-09,June 9, 2026and9 June 2026(English month names, full or abbreviated, optional ordinal and comma). Numeric forms like06/09/2026are ambiguous between day-first and month-first and are not read.- A value or excerpt the normalizer cannot read (a number written in
words, no yes/no word, no readable date, a value with no word
characters, a value of the wrong type) is
not-evaluated, nevermismatch.
tokenBoundaryis false when the excerpt starts or ends inside a word of the prepared text (the excerpt"3"located inside"2023").
Known false matches that remain. The checks are lexical, so match can
still appear where the excerpt does not support the value:
- a negation more than three words before the value, or after it
(
"open, but not this week"); - contrastive or hypothetical wording (
"formerly open","if open"); - exclusion wording (
"excludes C"matches"C"); - enum members that differ only in case (
"Open"and"open"fold together); - a number in a different currency (
45matches"€45"when the value meant dollars); - digit groups read with different separators (
"1.234.567"matches"Total 1,234.567"); - a sign on the excerpt side of a phone-style value: the sign check covers
only the value (
"303-555-1234"matches"-303-555-1234").
Changing any rule so that some input gets a different result changes
checkerVersion, released or not. evidence-match-v1 to
evidence-match-v3 were never released; evidence-match-v4 is the first
released version.
Recommended consumer policy. A schema result other than ok can block
by default, because it is exact. valueInExcerpt and tokenBoundary are
annotations until the value-in-excerpt check's false-mismatch rate has been
measured on a labelled set; after that a consumer may choose to block on
mismatch. Do not read match as support: it says only that the value is
written in the excerpt. not-evaluated and not-applicable say nothing about the value.
The consumer decides what blocks; Traverse does not.
The portable envelope carries evidenceMatch as an optional proposal key
(see portable-extraction-result-envelope.md).
Value typing, value normalization and several values per field#
Found by running a real model end to end (issue #199).
The tool schema types each value by its own field. Every bundled adapter
sends an item schema with one variant per distinct value type, each naming the
field paths that take it: number and boolean as those JSON types, enum
as a string restricted to its enumValues, date as a string described as
ISO 8601, array/object as those JSON types. A number field therefore
cannot be answered with a string where the provider enforces the schema. The
Relay and OpenAI adapters send the strict structured-output dialect (every key
required, optional ones nullable, no additional properties); OpenAI falls back
to a non-strict tool, and Relay refuses, when a target is array or object,
whose nested shape TargetFieldSchema does not declare. Outside strict mode a
variant holding an array path (schedules[].startDate) leaves fieldPath
open, so an indexed answer stays possible.
Each variant repeats the item schema (about 0.75 KB), and every distinct enum
value set needs its own. At most MAX_ENUM_VALUE_VARIANTS (8) distinct enum
sets, in schema order, get a variant. Enum fields past the cap are typed as a
plain string: their values stay in the tool description and
evidenceMatch.schema still checks them. The adapter then returns the warning
provider tool schema left enum values unrestricted for N field(s) … naming
them (envelope code provider/provider-warning), which extract() keeps
once per run. The schema is therefore at most 8 enum variants plus one per
other value type, whatever the number of fields.
extract() rewrites a value into its declared type in two lossless cases,
and records each one. Not every provider enforces a schema, and a custom
provider has none, so the core also normalizes:
string-to-number: anumberfield answered with the canonical decimal spelling of a number ("2.1"becomes2.1). The trimmed string must match an optional-, digits and at most one point, andString(Number(s))must equal it. So a trailing zero in the fraction is refused ("1.000"may be a grouped 1000,"2.10"a version), as are"1,234","007","2.1 kg","$5","1e3","-0"and a number too long for a double. A refused string keepstype-mismatch.date-to-iso: adatefield answered with a written English date, the whole value being21 March 2013orMarch 21, 2013(full or abbreviated month, optional ordinal and comma), becomes2013-03-21. Numeric forms (03/04/2013) are day/month ambiguous and are left as written, as are a partial (March 2013) or impossible date and a year outside 1000 to 2999.
A rewritten proposal carries valueNormalization: { kind, from } in process,
where from is the string the provider returned, and the run gets one warning
per rewrite naming the field and the original
(coerced string value "2.1" to number for "<field>",
normalized date value "21 March 2013" to ISO 8601 for "<field>").
candidateValue is the rewritten value and evidenceMatch describes it. A
proposal without valueNormalization carries exactly what the provider
returned. Anything not rewritten is left for evidenceMatch.schema to report
as type-mismatch or format-invalid.
The portable envelope does not carry valueNormalization. Released
readers (Survey 8.0.0, Fieldwork 0.13.0, Traverse 4.0.0) reject an unknown
proposal key, so adding one would break every one of them. In the envelope a
rewrite appears only as the warning's existing classification,
normalization/proposal-normalization; the field and original are in
ExtractionResult.warnings and on the in-process proposal. A per-proposal
envelope key is deferred until readers accept it. A test imports such an
envelope under the published Survey reader.
The rewrite runs before occurrence resolution and dedup, so "2.1" and 2.1
for one span are one proposal. Which duplicate survives does not depend on
provider order: a proposal whose value came back in its declared type wins
over a rewritten one, and between two rewritten ones the smaller original (by
code unit) wins. Rewrite warnings are written after dedup, for returned
proposals only, so there is never a warning for a record no proposal carries.
checkerVersion stays evidence-match-v4. The checker's rules are
unchanged: schemaMatch("21 March 2013", date) is still format-invalid and
schemaMatch("2.1", number) still type-mismatch. What changed is the value
extract() hands the checker. The envelope validator recomputes
evidenceMatch from the stored candidateValue, valueType and excerpt, so
an envelope written before this change and one written after it are both
consistent under v4. A bump would name a rule change that did not happen and
would make every stored v4 envelope fail validation.
An explicit date field's prompt line asks for the stated date in ISO form
rather than verbatim; the two could not both be asked for. The excerpt keeps
the source wording.
One proposal per distinct value. The prompt asks for one proposal per
distinct value the content states for a field, not one per field: two
different values for one field are two proposals, so a reviewer sees the
conflict, and a value stated more than once is one proposal. The rule is
written in the tool description, in the proposals array's schema description
and in the message, because a runtime that projects the tool as a
structured-output schema never shows the model the tool description. Dedup is
unchanged: it collapses only the same field, value and span, so distinct
values always survive it. Traverse does not pick between them.
Out of scope#
- A stricter,
inferenceType === "explicit"-gated "candidateValue must be groundable in the excerpt" check that drops proposals. The annotation above records the comparison without dropping anything ("Evidence annotations", issue #170); a gate remains the consumer's choice. The original reasons for not building a dropping check here still hold:- Format-aware equivalence is heuristic. The normalizers above cover a
fixed set of forms and report anything else as
not-evaluated; their false-mismatch rate on real pages has not been measured, so dropping onmismatchcould remove well-grounded values. - The fields most likely to be tagged
"explicit"in practice (address/ zip/contact fields, money amounts) are exactly the ones most prone to formatting drift between a provider's returned value and the raw excerpt text. A naive strict check would produce new, silent-seeming false-positive drops for genuinely well-grounded explicit values whose provider-returned form merely differs in punctuation/format from the raw excerpt — regressing real extractions rather than only "being stricter where it's supposed to be." (See the regression test intests/extract.test.ts's"inferenceType carry-through"suite,"[AC6] adds no new drop/warning/clamp condition...", which pins this exact no-op behavior for a reformatted phone number.) - A gate needs a labelled evaluation set first; until one exists, blocking
on
valueInExcerptwould turn an annotation into a value-dropping gate with unmeasured error. - Downstream rendering of the explicit/inferred distinction in a review UI (e.g. Survey-side) is separate work, not attempted here.
- Format-aware equivalence is heuristic. The normalizers above cover a
fixed set of forms and report anything else as