@openparser/lineage 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +89 -0
- package/LICENSE +201 -0
- package/README.md +211 -0
- package/dist/activity.d.ts +74 -0
- package/dist/activity.d.ts.map +1 -0
- package/dist/approval.d.ts +24 -0
- package/dist/approval.d.ts.map +1 -0
- package/dist/builder.d.ts +48 -0
- package/dist/builder.d.ts.map +1 -0
- package/dist/common.d.ts +34 -0
- package/dist/common.d.ts.map +1 -0
- package/dist/confidence.d.ts +61 -0
- package/dist/confidence.d.ts.map +1 -0
- package/dist/derivation.d.ts +163 -0
- package/dist/derivation.d.ts.map +1 -0
- package/dist/document.d.ts +412 -0
- package/dist/document.d.ts.map +1 -0
- package/dist/entity.d.ts +126 -0
- package/dist/entity.d.ts.map +1 -0
- package/dist/fields.d.ts +76 -0
- package/dist/fields.d.ts.map +1 -0
- package/dist/index.d.ts +21 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +870 -0
- package/dist/labels.d.ts +11 -0
- package/dist/labels.d.ts.map +1 -0
- package/dist/profile/grounding.d.ts +34 -0
- package/dist/profile/grounding.d.ts.map +1 -0
- package/dist/profile/index.d.ts +14 -0
- package/dist/profile/index.d.ts.map +1 -0
- package/dist/profile/index.js +1298 -0
- package/dist/profile/labels.d.ts +10 -0
- package/dist/profile/labels.d.ts.map +1 -0
- package/dist/profile/recognition.d.ts +70 -0
- package/dist/profile/recognition.d.ts.map +1 -0
- package/dist/profile/transform-validation.d.ts +250 -0
- package/dist/profile/transform-validation.d.ts.map +1 -0
- package/dist/prov.d.ts +40 -0
- package/dist/prov.d.ts.map +1 -0
- package/dist/relation.d.ts +27 -0
- package/dist/relation.d.ts.map +1 -0
- package/dist/traverse.d.ts +12 -0
- package/dist/traverse.d.ts.map +1 -0
- package/dist/validate.d.ts +14 -0
- package/dist/validate.d.ts.map +1 -0
- package/lineage.schema.json +625 -0
- package/package.json +71 -0
- package/src/activity.ts +65 -0
- package/src/approval.ts +28 -0
- package/src/builder.ts +129 -0
- package/src/common.ts +58 -0
- package/src/confidence.ts +72 -0
- package/src/derivation.ts +81 -0
- package/src/document.ts +82 -0
- package/src/entity.ts +87 -0
- package/src/fields.ts +163 -0
- package/src/index.ts +123 -0
- package/src/labels.ts +13 -0
- package/src/profile/grounding.ts +74 -0
- package/src/profile/index.ts +67 -0
- package/src/profile/labels.ts +28 -0
- package/src/profile/recognition.ts +149 -0
- package/src/profile/transform-validation.ts +1379 -0
- package/src/prov.ts +205 -0
- package/src/relation.ts +32 -0
- package/src/traverse.ts +91 -0
- package/src/validate.ts +221 -0
package/src/fields.ts
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import type { JsonValue } from './common';
|
|
2
|
+
import type { ConfidenceAssertion, ConfidenceKind } from './confidence';
|
|
3
|
+
import type { LineageDocument } from './document';
|
|
4
|
+
import type { Entity } from './entity';
|
|
5
|
+
import { entityAncestors } from './traverse';
|
|
6
|
+
|
|
7
|
+
/** How a field's confidence came about, or that none was asserted at all. */
|
|
8
|
+
export type ConfidenceOrigin = ConfidenceKind | 'not_reported';
|
|
9
|
+
|
|
10
|
+
export type LineageField = {
|
|
11
|
+
id: string;
|
|
12
|
+
pointer: string;
|
|
13
|
+
/**
|
|
14
|
+
* Display-only dotted rendering of `pointer`. Lossy: a property whose name
|
|
15
|
+
* contains a dot is indistinguishable from nesting. Use `pointer` as the key.
|
|
16
|
+
*/
|
|
17
|
+
dottedPath: string;
|
|
18
|
+
value: JsonValue | undefined;
|
|
19
|
+
confidence: ConfidenceAssertion | undefined;
|
|
20
|
+
confidenceOrigin: ConfidenceOrigin;
|
|
21
|
+
entity: Entity;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export type LineageFieldTrace = {
|
|
25
|
+
field: LineageField;
|
|
26
|
+
evidence: Array<{ id: string; entity: Entity }>;
|
|
27
|
+
activities: string[];
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
/** RFC 6901 pointer for an output entity. `path` is the only field locator. */
|
|
31
|
+
function fieldPointer(entity: Entity): string {
|
|
32
|
+
return entity.path ?? '';
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Most relevant confidence assertion: extraction-scoped, else the first. */
|
|
36
|
+
function fieldConfidence(entity: Entity): ConfidenceAssertion | undefined {
|
|
37
|
+
const assertions = entity.confidence;
|
|
38
|
+
if (!assertions || assertions.length === 0) return undefined;
|
|
39
|
+
return assertions.find((assertion) => assertion.scope === 'extraction') ?? assertions[0];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* RFC 6901 pointer to dotted display path, unescaping `~1` and `~0`.
|
|
44
|
+
*
|
|
45
|
+
* Display only. A property whose name contains a dot is indistinguishable from
|
|
46
|
+
* nesting, so never use the result as a key.
|
|
47
|
+
*/
|
|
48
|
+
export function dottedPathFromPointer(pointer: string): string {
|
|
49
|
+
if (pointer === '' || pointer === '/') return '';
|
|
50
|
+
return pointer
|
|
51
|
+
.replace(/^\//, '')
|
|
52
|
+
.split('/')
|
|
53
|
+
.map((segment) => segment.replace(/~1/g, '/').replace(/~0/g, '~'))
|
|
54
|
+
.join('.');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Dotted display path back to an RFC 6901 pointer, escaping `~` and `/`.
|
|
59
|
+
*
|
|
60
|
+
* Only sound for paths `dottedPathFromPointer` could have produced: a property
|
|
61
|
+
* name containing a dot round-trips as nesting. Prefer carrying the pointer.
|
|
62
|
+
*/
|
|
63
|
+
export function pointerFromDottedPath(dottedPath: string): string {
|
|
64
|
+
if (dottedPath === '') return '';
|
|
65
|
+
return `/${dottedPath
|
|
66
|
+
.split('.')
|
|
67
|
+
.map((segment) => segment.replaceAll('~', '~0').replaceAll('/', '~1'))
|
|
68
|
+
.join('/')}`;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* The entity holding a field's current value at `pointer`.
|
|
73
|
+
*
|
|
74
|
+
* Precedence is explicit and does not use `Object.entries` insertion order:
|
|
75
|
+
* 1. An entity at that path listed in `doc.outputs` — after review, `outputs`
|
|
76
|
+
* is the current field set (plus a root snapshot / completion decision,
|
|
77
|
+
* which do not share field paths). If several outputs match, the earliest
|
|
78
|
+
* in `doc.outputs` wins.
|
|
79
|
+
* 2. Otherwise a `kind: 'decision'` entity at that path (the producer-settled
|
|
80
|
+
* value on documents that never went through review). If several decisions
|
|
81
|
+
* match, the lexicographically smallest id wins.
|
|
82
|
+
* 3. Otherwise the lexicographically smallest id among remaining matches.
|
|
83
|
+
*
|
|
84
|
+
* `path` is the only field locator, so this is the single supported way to go
|
|
85
|
+
* from a pointer to an entity id — ids themselves are not a stable contract.
|
|
86
|
+
*/
|
|
87
|
+
export function fieldEntityByPointer(
|
|
88
|
+
doc: LineageDocument,
|
|
89
|
+
pointer: string
|
|
90
|
+
): { id: string; entity: Entity } | undefined {
|
|
91
|
+
const matching = Object.entries(doc.entities).filter(([, entity]) => entity.path === pointer);
|
|
92
|
+
if (matching.length === 0) return undefined;
|
|
93
|
+
|
|
94
|
+
const matchingIds = new Set(matching.map(([id]) => id));
|
|
95
|
+
const outputId = doc.outputs.find((id) => matchingIds.has(id));
|
|
96
|
+
if (outputId) {
|
|
97
|
+
const entity = doc.entities[outputId];
|
|
98
|
+
if (entity) return { id: outputId, entity };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const ranked = matching.toSorted(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0));
|
|
102
|
+
const decision = ranked.find(([, entity]) => entity.kind === 'decision');
|
|
103
|
+
const chosen = decision ?? ranked[0];
|
|
104
|
+
return chosen ? { id: chosen[0], entity: chosen[1] } : undefined;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function toField(id: string, entity: Entity): LineageField {
|
|
108
|
+
const pointer = fieldPointer(entity);
|
|
109
|
+
const confidence = fieldConfidence(entity);
|
|
110
|
+
return {
|
|
111
|
+
id,
|
|
112
|
+
pointer,
|
|
113
|
+
dottedPath: dottedPathFromPointer(pointer),
|
|
114
|
+
value: entity.value,
|
|
115
|
+
confidence,
|
|
116
|
+
confidenceOrigin: confidence ? confidence.kind : 'not_reported',
|
|
117
|
+
entity,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** Output field entities as fields, in `doc.outputs` order.
|
|
122
|
+
*
|
|
123
|
+
* Whole-document snapshots (`path === ''`) and other pathless outputs are
|
|
124
|
+
* omitted — they are the whole result (or a run-level decision), not a field.
|
|
125
|
+
*/
|
|
126
|
+
export function fields(doc: LineageDocument): LineageField[] {
|
|
127
|
+
const result: LineageField[] = [];
|
|
128
|
+
for (const id of doc.outputs) {
|
|
129
|
+
const entity = doc.entities[id];
|
|
130
|
+
if (!entity) continue;
|
|
131
|
+
if (fieldPointer(entity) === '') continue;
|
|
132
|
+
result.push(toField(id, entity));
|
|
133
|
+
}
|
|
134
|
+
return result;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Trace a field by JSON Pointer, including upstream evidence and activities.
|
|
139
|
+
*
|
|
140
|
+
* Whether that evidence is *sufficient* is a question for whoever produced the
|
|
141
|
+
* document, since it turns on their own attributes; see the OpenParser profile
|
|
142
|
+
* for one answer.
|
|
143
|
+
*/
|
|
144
|
+
export function fieldTrace(doc: LineageDocument, pointer: string): LineageFieldTrace | undefined {
|
|
145
|
+
const field = fields(doc).find((candidate) => candidate.pointer === pointer);
|
|
146
|
+
if (!field) return undefined;
|
|
147
|
+
|
|
148
|
+
const ancestorIds = entityAncestors(doc, field.id);
|
|
149
|
+
const evidence = ancestorIds
|
|
150
|
+
.filter((id) => {
|
|
151
|
+
const entity = doc.entities[id];
|
|
152
|
+
return entity?.kind === 'collection' || entity?.kind === 'evidence';
|
|
153
|
+
})
|
|
154
|
+
.sort()
|
|
155
|
+
.map((id) => ({ id, entity: doc.entities[id]! }));
|
|
156
|
+
|
|
157
|
+
const relevant = new Set([field.id, ...ancestorIds]);
|
|
158
|
+
const activities = [
|
|
159
|
+
...new Set(doc.derivations.filter((d) => relevant.has(d.output)).map((d) => d.activity)),
|
|
160
|
+
].sort();
|
|
161
|
+
|
|
162
|
+
return { field, evidence, activities };
|
|
163
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public `@openparser/lineage` surface for `lineage@1` complete data-derivation DAGs.
|
|
3
|
+
*
|
|
4
|
+
* Provider- and domain-agnostic provenance documents grounded in W3C PROV-DM
|
|
5
|
+
* semantics with ergonomic JSON. Root export only — no subpath exports.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export {
|
|
9
|
+
ACTIVITY_STATUSES,
|
|
10
|
+
ActivitySchema,
|
|
11
|
+
ActivityStatusSchema,
|
|
12
|
+
AgentSchema,
|
|
13
|
+
AssociationSchema,
|
|
14
|
+
ImplementationSchema,
|
|
15
|
+
type Activity,
|
|
16
|
+
type ActivityStatus,
|
|
17
|
+
type Agent,
|
|
18
|
+
type Association,
|
|
19
|
+
type Implementation,
|
|
20
|
+
} from './activity';
|
|
21
|
+
|
|
22
|
+
export { ApprovalAssertionSchema, type ApprovalAssertion } from './approval';
|
|
23
|
+
|
|
24
|
+
export { LineageBuilder } from './builder';
|
|
25
|
+
|
|
26
|
+
export {
|
|
27
|
+
AttributesSchema,
|
|
28
|
+
JsonPointerSchema,
|
|
29
|
+
JsonValueSchema,
|
|
30
|
+
LINEAGE_FORMAT,
|
|
31
|
+
LineageFormatSchema,
|
|
32
|
+
LineageIdSchema,
|
|
33
|
+
TimestampSchema,
|
|
34
|
+
jsonValuesEqual,
|
|
35
|
+
type Attributes,
|
|
36
|
+
type JsonPointer,
|
|
37
|
+
type JsonValue,
|
|
38
|
+
type LineageFormat,
|
|
39
|
+
type LineageId,
|
|
40
|
+
type Timestamp,
|
|
41
|
+
} from './common';
|
|
42
|
+
|
|
43
|
+
export {
|
|
44
|
+
CONFIDENCE_KINDS,
|
|
45
|
+
ConfidenceAssertionSchema,
|
|
46
|
+
ConfidenceKindSchema,
|
|
47
|
+
ConfidenceScaleSchema,
|
|
48
|
+
ConfidenceSourceSchema,
|
|
49
|
+
type ConfidenceAssertion,
|
|
50
|
+
type ConfidenceKind,
|
|
51
|
+
type ConfidenceScale,
|
|
52
|
+
type ConfidenceSource,
|
|
53
|
+
} from './confidence';
|
|
54
|
+
|
|
55
|
+
export {
|
|
56
|
+
DERIVATION_EFFECTS,
|
|
57
|
+
DerivationEffectSchema,
|
|
58
|
+
DerivationInputSchema,
|
|
59
|
+
DerivationSchema,
|
|
60
|
+
MAX_DERIVATION_INPUTS,
|
|
61
|
+
TransformationSchema,
|
|
62
|
+
derivationId,
|
|
63
|
+
type Derivation,
|
|
64
|
+
type DerivationEffect,
|
|
65
|
+
type DerivationInput,
|
|
66
|
+
type Transformation,
|
|
67
|
+
} from './derivation';
|
|
68
|
+
|
|
69
|
+
export {
|
|
70
|
+
LineageDocumentSchema,
|
|
71
|
+
LineageDocumentShapeSchema,
|
|
72
|
+
parseLineageDocument,
|
|
73
|
+
safeParseLineageDocument,
|
|
74
|
+
type LineageDocument,
|
|
75
|
+
} from './document';
|
|
76
|
+
|
|
77
|
+
export {
|
|
78
|
+
AttributionSchema,
|
|
79
|
+
DigestSchema,
|
|
80
|
+
ENTITY_KINDS,
|
|
81
|
+
EntityKindSchema,
|
|
82
|
+
EntitySchema,
|
|
83
|
+
LocatorSchema,
|
|
84
|
+
SelectorSchema,
|
|
85
|
+
type Attribution,
|
|
86
|
+
type Digest,
|
|
87
|
+
type Entity,
|
|
88
|
+
type EntityKind,
|
|
89
|
+
type Locator,
|
|
90
|
+
type Selector,
|
|
91
|
+
} from './entity';
|
|
92
|
+
|
|
93
|
+
export {
|
|
94
|
+
LINEAGE_PROV_NAMESPACE,
|
|
95
|
+
lineageIdToProvQualifiedName,
|
|
96
|
+
toProvJson,
|
|
97
|
+
type ProvJsonDocument,
|
|
98
|
+
} from './prov';
|
|
99
|
+
|
|
100
|
+
export {
|
|
101
|
+
RELATION_TYPES,
|
|
102
|
+
RelationSchema,
|
|
103
|
+
RelationTypeSchema,
|
|
104
|
+
type Relation,
|
|
105
|
+
type RelationType,
|
|
106
|
+
} from './relation';
|
|
107
|
+
|
|
108
|
+
export {
|
|
109
|
+
dottedPathFromPointer,
|
|
110
|
+
fieldEntityByPointer,
|
|
111
|
+
fieldTrace,
|
|
112
|
+
fields,
|
|
113
|
+
pointerFromDottedPath,
|
|
114
|
+
type ConfidenceOrigin,
|
|
115
|
+
type LineageField,
|
|
116
|
+
type LineageFieldTrace,
|
|
117
|
+
} from './fields';
|
|
118
|
+
|
|
119
|
+
export { activityLabel } from './labels';
|
|
120
|
+
|
|
121
|
+
export { entityAncestors, entityDescendants, topologicalEntityOrder } from './traverse';
|
|
122
|
+
|
|
123
|
+
export { LineageValidationError, validateLineageGraph } from './validate';
|
package/src/labels.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { Activity } from './activity';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Display label for an activity: an explicit `name` when a producer set one,
|
|
5
|
+
* otherwise the raw `type`.
|
|
6
|
+
*
|
|
7
|
+
* `type` is an open string. Core `lineage@1` does not map vendor activity types
|
|
8
|
+
* to presentation copy — OpenParser-friendly names live in
|
|
9
|
+
* `@openparser/lineage/openparser`.
|
|
10
|
+
*/
|
|
11
|
+
export function activityLabel(activity: Activity): string {
|
|
12
|
+
return activity.name ?? activity.type;
|
|
13
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whether a field's evidence is sufficient, as the OpenParser profile defines it.
|
|
3
|
+
*
|
|
4
|
+
* `lineage@1` records what a value was derived from; it takes no position on when
|
|
5
|
+
* that is enough, because "enough" depends on what the producer was able to
|
|
6
|
+
* resolve. OpenParser answers it with one attribute on the producer derivation —
|
|
7
|
+
* the source ids the model named that could not be resolved to a region — so the
|
|
8
|
+
* rule reads a `openparser:` key and belongs here rather than in the protocol.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { Derivation } from '../derivation';
|
|
12
|
+
import type { LineageDocument } from '../document';
|
|
13
|
+
import { fieldTrace } from '../fields';
|
|
14
|
+
import { entityAncestors } from '../traverse';
|
|
15
|
+
|
|
16
|
+
const DROPPED_SOURCES_ATTR = 'openparser:droppedSourceIds';
|
|
17
|
+
|
|
18
|
+
/** Whether a field is backed by source evidence, partially backed, or unbacked. */
|
|
19
|
+
export type GroundingStatus = 'grounded' | 'partial' | 'ungrounded';
|
|
20
|
+
|
|
21
|
+
export type FieldGrounding = {
|
|
22
|
+
status: GroundingStatus;
|
|
23
|
+
/** Source ids the producer cited but could not resolve to a region. */
|
|
24
|
+
droppedSourceIds: string[];
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/** Source ids the producer named but could not resolve to a region. */
|
|
28
|
+
export function droppedSourceIds(producer: Derivation | undefined): string[] {
|
|
29
|
+
const dropped = producer?.attributes?.[DROPPED_SOURCES_ATTR];
|
|
30
|
+
return Array.isArray(dropped) ? dropped.filter((id): id is string => typeof id === 'string') : [];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Grounding is read off the graph rather than stored: the evidence a value
|
|
35
|
+
* actually reaches, less the sources its producer could not resolve.
|
|
36
|
+
*
|
|
37
|
+
* Callers that already walked to the evidence pass the count, since how far a
|
|
38
|
+
* given consumer follows the graph is its own business.
|
|
39
|
+
*/
|
|
40
|
+
export function groundingStatus(options: {
|
|
41
|
+
evidenceCount: number;
|
|
42
|
+
droppedSourceIds: string[];
|
|
43
|
+
}): GroundingStatus {
|
|
44
|
+
if (options.evidenceCount === 0) return 'ungrounded';
|
|
45
|
+
return options.droppedSourceIds.length > 0 ? 'partial' : 'grounded';
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function collectDroppedSourceIds(doc: LineageDocument, entityIds: Iterable<string>): string[] {
|
|
49
|
+
const relevant = new Set(entityIds);
|
|
50
|
+
const seen = new Set<string>();
|
|
51
|
+
const dropped: string[] = [];
|
|
52
|
+
for (const derivation of doc.derivations) {
|
|
53
|
+
if (!relevant.has(derivation.output)) continue;
|
|
54
|
+
for (const id of droppedSourceIds(derivation)) {
|
|
55
|
+
if (seen.has(id)) continue;
|
|
56
|
+
seen.add(id);
|
|
57
|
+
dropped.push(id);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return dropped;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Grounding for the field at a JSON Pointer, walking the graph to its evidence. */
|
|
64
|
+
export function fieldGrounding(doc: LineageDocument, pointer: string): FieldGrounding | undefined {
|
|
65
|
+
const trace = fieldTrace(doc, pointer);
|
|
66
|
+
if (!trace) return undefined;
|
|
67
|
+
|
|
68
|
+
const ancestorIds = entityAncestors(doc, trace.field.id);
|
|
69
|
+
const dropped = collectDroppedSourceIds(doc, [trace.field.id, ...ancestorIds]);
|
|
70
|
+
return {
|
|
71
|
+
status: groundingStatus({ evidenceCount: trace.evidence.length, droppedSourceIds: dropped }),
|
|
72
|
+
droppedSourceIds: dropped,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The OpenParser profile of `lineage@1`, published as `@openparser/lineage/openparser`.
|
|
3
|
+
*
|
|
4
|
+
* `lineage@1` is a vendor-neutral record of where values came from. Anything that
|
|
5
|
+
* depends on how OpenParser in particular scores an OCR word, or on an
|
|
6
|
+
* `openparser:` attribute, lives here — so the protocol surface stays readable as
|
|
7
|
+
* a protocol, and a producer that is not OpenParser is never asked to adopt our
|
|
8
|
+
* conventions to use it.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export { activityLabel } from './labels';
|
|
12
|
+
|
|
13
|
+
export {
|
|
14
|
+
droppedSourceIds,
|
|
15
|
+
fieldGrounding,
|
|
16
|
+
groundingStatus,
|
|
17
|
+
type FieldGrounding,
|
|
18
|
+
type GroundingStatus,
|
|
19
|
+
} from './grounding';
|
|
20
|
+
|
|
21
|
+
export {
|
|
22
|
+
CITED_RECOGNITION_METHOD,
|
|
23
|
+
CONTEXT_BLEND_WEIGHT,
|
|
24
|
+
REGION_RECOGNITION_METHOD,
|
|
25
|
+
aggregateCitedWordConfidences,
|
|
26
|
+
annotateWordsWithCitation,
|
|
27
|
+
citedSpanInText,
|
|
28
|
+
downwardBlendCitedMinimum,
|
|
29
|
+
type CitedWordAggregate,
|
|
30
|
+
type ScoredWord,
|
|
31
|
+
} from './recognition';
|
|
32
|
+
|
|
33
|
+
export {
|
|
34
|
+
DATE_SOURCE_CATALOG_ID,
|
|
35
|
+
DATE_SOURCE_CATALOG_V1,
|
|
36
|
+
DATE_SOURCE_CATALOG_VERSION,
|
|
37
|
+
FORMAT_ONLY_DATE_TARGETS,
|
|
38
|
+
MAX_TRANSFORM_INTENT_PARAMETER_BYTES,
|
|
39
|
+
MAX_TRANSFORM_PROOF_BYTES,
|
|
40
|
+
TRANSFORM_DOCS_ANCHORS,
|
|
41
|
+
TRANSFORM_OPERATIONS,
|
|
42
|
+
TRANSFORM_PROOF_ATTRIBUTE,
|
|
43
|
+
TRANSFORM_PROOF_OPERAND_KEYS,
|
|
44
|
+
TRANSFORM_VALIDATORS,
|
|
45
|
+
TRANSFORM_VERIFICATION_DOCS,
|
|
46
|
+
TransformClaimSchema,
|
|
47
|
+
TransformIntentSchema,
|
|
48
|
+
TransformProofClassSchema,
|
|
49
|
+
TransformProofSchema,
|
|
50
|
+
dateTimeFormatIssue,
|
|
51
|
+
formatOnlyDateTimeParameters,
|
|
52
|
+
isTrustedTransformDocsHref,
|
|
53
|
+
parseTransformProof,
|
|
54
|
+
transformValidatorDescriptor,
|
|
55
|
+
trustedDateSourceFormats,
|
|
56
|
+
trustedIntentParametersFitProof,
|
|
57
|
+
trustedTransformDocsHref,
|
|
58
|
+
verifyTransform,
|
|
59
|
+
type DateTimeKind,
|
|
60
|
+
type TransformClaim,
|
|
61
|
+
type TransformIntent,
|
|
62
|
+
type TransformOperation,
|
|
63
|
+
type TransformProof,
|
|
64
|
+
type TransformProofClass,
|
|
65
|
+
type TransformValidatorDescriptor,
|
|
66
|
+
type TransformVerificationResult,
|
|
67
|
+
} from './transform-validation';
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { Activity } from '../activity';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Human labels for the activity types this profile emits.
|
|
5
|
+
*
|
|
6
|
+
* Documents do not carry generated display strings, so this is where a label is
|
|
7
|
+
* recovered from `activity.type`. Keeping it here rather than in each dashboard
|
|
8
|
+
* means one mapping to translate or correct, and it stays available to anyone
|
|
9
|
+
* reading a document through this library.
|
|
10
|
+
*/
|
|
11
|
+
const ACTIVITY_LABELS: Record<string, string> = {
|
|
12
|
+
ocr: 'Parse',
|
|
13
|
+
extract: 'Schema-constrained extraction',
|
|
14
|
+
transform: 'Transform',
|
|
15
|
+
grounding_resolution: 'Grounding resolution and validation',
|
|
16
|
+
'review.confirm': 'Human review',
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Display label for an OpenParser activity: an explicit `name` when a producer
|
|
21
|
+
* set one, otherwise a label for known types, otherwise the raw type.
|
|
22
|
+
*
|
|
23
|
+
* `type` is an open string, so unknown types render as themselves rather than
|
|
24
|
+
* being hidden behind a placeholder.
|
|
25
|
+
*/
|
|
26
|
+
export function activityLabel(activity: Activity): string {
|
|
27
|
+
return activity.name ?? ACTIVITY_LABELS[activity.type] ?? activity.type;
|
|
28
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How a value's recognition score is derived from the OCR words behind it.
|
|
3
|
+
*
|
|
4
|
+
* This is a profile rule, not part of `lineage@1`: the protocol says a score has
|
|
5
|
+
* a scale, a scope, and a named method, and stays silent on what any particular
|
|
6
|
+
* method computes. It ships beside the protocol because a producer and a consumer
|
|
7
|
+
* that disagree here disagree about what a number means. A region cited by two
|
|
8
|
+
* fields is read once but quoted differently by each, so scoring the region and
|
|
9
|
+
* scoring a value are different questions — and answering the second with the
|
|
10
|
+
* first is how a crisply scanned number ends up wearing its neighbor's bad
|
|
11
|
+
* handwriting.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/** An OCR token with its recognition score, and whether a value was read from it. */
|
|
15
|
+
export type ScoredWord = {
|
|
16
|
+
id?: string;
|
|
17
|
+
text: string;
|
|
18
|
+
confidence: number;
|
|
19
|
+
cited?: boolean;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export const CITED_RECOGNITION_METHOD = 'cited_minimum_context_downblend';
|
|
23
|
+
|
|
24
|
+
/** Scoring a whole region, because no word within it could be narrowed to. */
|
|
25
|
+
export const REGION_RECOGNITION_METHOD = 'region_word_minimum';
|
|
26
|
+
|
|
27
|
+
/** How far the surrounding text may pull a cited score down. */
|
|
28
|
+
export const CONTEXT_BLEND_WEIGHT = 0.15;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Blend a cited-word minimum with the surrounding context: weaker context may
|
|
32
|
+
* only pull the score down, and never below what the blend allows. A noisy
|
|
33
|
+
* neighborhood is evidence that a value might be misread, not a verdict that it
|
|
34
|
+
* was.
|
|
35
|
+
*/
|
|
36
|
+
export function downwardBlendCitedMinimum(
|
|
37
|
+
citedMin: number,
|
|
38
|
+
contextMean: number | undefined
|
|
39
|
+
): number {
|
|
40
|
+
if (contextMean === undefined) return citedMin;
|
|
41
|
+
const blended = citedMin * (1 - CONTEXT_BLEND_WEIGHT) + contextMean * CONTEXT_BLEND_WEIGHT;
|
|
42
|
+
return Math.min(citedMin, blended);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export type CitedWordAggregate = {
|
|
46
|
+
score: number;
|
|
47
|
+
minimum: number;
|
|
48
|
+
maximum: number;
|
|
49
|
+
sampleCount: number;
|
|
50
|
+
method: typeof CITED_RECOGNITION_METHOD | typeof REGION_RECOGNITION_METHOD;
|
|
51
|
+
contextMean?: number;
|
|
52
|
+
contextSampleCount?: number;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Score one value from the words it was read from, discounted by the words
|
|
57
|
+
* around it. With nothing marked cited there is nothing to narrow to, so every
|
|
58
|
+
* word counts, the result is the region's own weakest token, and `method` says
|
|
59
|
+
* so — a score over the whole region must not claim to have narrowed to a value.
|
|
60
|
+
*/
|
|
61
|
+
export function aggregateCitedWordConfidences(
|
|
62
|
+
words: readonly ScoredWord[]
|
|
63
|
+
): CitedWordAggregate | undefined {
|
|
64
|
+
if (words.length === 0) return undefined;
|
|
65
|
+
const cited = words.filter((word) => word.cited !== false);
|
|
66
|
+
const context = words.filter((word) => word.cited === false);
|
|
67
|
+
const active = cited.length > 0 ? cited : words;
|
|
68
|
+
const activeScores = active.map((word) => word.confidence);
|
|
69
|
+
const contextMean =
|
|
70
|
+
cited.length > 0 && context.length > 0
|
|
71
|
+
? context.reduce((sum, word) => sum + word.confidence, 0) / context.length
|
|
72
|
+
: undefined;
|
|
73
|
+
const citedMin = Math.min(...activeScores);
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
score: downwardBlendCitedMinimum(citedMin, contextMean),
|
|
77
|
+
minimum: citedMin,
|
|
78
|
+
maximum: Math.max(...activeScores),
|
|
79
|
+
sampleCount: active.length,
|
|
80
|
+
method: cited.length > 0 ? CITED_RECOGNITION_METHOD : REGION_RECOGNITION_METHOD,
|
|
81
|
+
...(contextMean !== undefined ? { contextMean, contextSampleCount: context.length } : {}),
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** First candidate found in the text wins; pass candidates in priority order. */
|
|
86
|
+
export function citedSpanInText(
|
|
87
|
+
regionText: string,
|
|
88
|
+
candidates: ReadonlyArray<string | undefined>
|
|
89
|
+
): { start: number; end: number } {
|
|
90
|
+
if (!regionText) return { start: -1, end: -1 };
|
|
91
|
+
for (const candidate of candidates) {
|
|
92
|
+
const quote = candidate?.trim();
|
|
93
|
+
if (!quote) continue;
|
|
94
|
+
let start = regionText.indexOf(quote);
|
|
95
|
+
if (start < 0) start = regionText.toLowerCase().indexOf(quote.toLowerCase());
|
|
96
|
+
if (start >= 0) return { start, end: start + quote.length };
|
|
97
|
+
}
|
|
98
|
+
return { start: -1, end: -1 };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function wordOverlapsSpan(
|
|
102
|
+
regionText: string,
|
|
103
|
+
word: ScoredWord,
|
|
104
|
+
cursor: number,
|
|
105
|
+
span: { start: number; end: number }
|
|
106
|
+
): { overlaps: boolean; nextCursor: number } {
|
|
107
|
+
if (!word.text) return { overlaps: false, nextCursor: cursor };
|
|
108
|
+
const at = regionText.indexOf(word.text, cursor);
|
|
109
|
+
if (at < 0) return { overlaps: false, nextCursor: cursor };
|
|
110
|
+
const wordEnd = at + word.text.length;
|
|
111
|
+
return {
|
|
112
|
+
overlaps: span.start >= 0 && at < span.end && wordEnd > span.start,
|
|
113
|
+
nextCursor: wordEnd,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Mark the words a value was read from within a region.
|
|
119
|
+
*
|
|
120
|
+
* `extractedText` is what the extraction produced, which is always text present
|
|
121
|
+
* in the document: for a value returned as-is it is the value itself, and for one
|
|
122
|
+
* the model went on to rewrite — `2024-05-30` from `MAY 30, 2024` — it is the text
|
|
123
|
+
* that was actually read. Searching for the value instead would find nothing in
|
|
124
|
+
* the second case. `lineageWordTexts`/`lineageWordIds` name words a lineage
|
|
125
|
+
* document already attributes to the value, used when no span matches.
|
|
126
|
+
*/
|
|
127
|
+
export function annotateWordsWithCitation<T extends ScoredWord>(
|
|
128
|
+
regionText: string,
|
|
129
|
+
words: readonly T[],
|
|
130
|
+
options?: {
|
|
131
|
+
extractedText?: string;
|
|
132
|
+
lineageWordTexts?: ReadonlySet<string>;
|
|
133
|
+
lineageWordIds?: ReadonlySet<string>;
|
|
134
|
+
}
|
|
135
|
+
): Array<T & { cited: boolean }> {
|
|
136
|
+
const span = citedSpanInText(regionText, [options?.extractedText]);
|
|
137
|
+
const hasQuoteSpan = span.start >= 0;
|
|
138
|
+
let cursor = 0;
|
|
139
|
+
|
|
140
|
+
return words.map((word) => {
|
|
141
|
+
const { overlaps, nextCursor } = wordOverlapsSpan(regionText, word, cursor, span);
|
|
142
|
+
cursor = nextCursor;
|
|
143
|
+
const inLineage = Boolean(
|
|
144
|
+
(word.id && options?.lineageWordIds?.has(word.id)) ||
|
|
145
|
+
options?.lineageWordTexts?.has(word.text)
|
|
146
|
+
);
|
|
147
|
+
return { ...word, cited: hasQuoteSpan ? overlaps : inLineage };
|
|
148
|
+
});
|
|
149
|
+
}
|