@kontourai/survey 0.1.3 → 0.1.5
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/README.md +162 -0
- package/dist/src/builder.d.ts +16 -0
- package/dist/src/builder.js +95 -2
- package/dist/src/field-observation.d.ts +20 -0
- package/dist/src/field-observation.js +16 -0
- package/dist/src/index.d.ts +4 -2
- package/dist/src/index.js +2 -1
- package/dist/src/observation-helper.d.ts +21 -0
- package/dist/src/observation-helper.js +63 -0
- package/dist/src/repeated-observation.js +8 -34
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -64,6 +64,71 @@ const trustInput = validateTrustInput(buildSurveyTrustInput(surveyInput));
|
|
|
64
64
|
const report = buildTrustReport(trustInput);
|
|
65
65
|
```
|
|
66
66
|
|
|
67
|
+
## Field observations
|
|
68
|
+
|
|
69
|
+
Use `fieldObservation` when a producer wants to describe one scalar field value
|
|
70
|
+
without hand-assembling the repeated source, extraction, candidate, review, and
|
|
71
|
+
claim defaults. The helper returns a normal `SurveyObservationInput`, so it
|
|
72
|
+
works with `SurveyInputBuilder.addObservation` and the same Surface projection
|
|
73
|
+
path.
|
|
74
|
+
|
|
75
|
+
```ts
|
|
76
|
+
import {
|
|
77
|
+
buildSurveyTrustInput,
|
|
78
|
+
fieldObservation,
|
|
79
|
+
SurveyInputBuilder,
|
|
80
|
+
} from "@kontourai/survey";
|
|
81
|
+
|
|
82
|
+
const surveyInput = new SurveyInputBuilder({
|
|
83
|
+
source: "example-producer:run-1",
|
|
84
|
+
})
|
|
85
|
+
.addObservation(fieldObservation({
|
|
86
|
+
id: "entity-123.status.current",
|
|
87
|
+
field: "registrationStatus",
|
|
88
|
+
value: "ACTIVE",
|
|
89
|
+
rawSource: {
|
|
90
|
+
kind: "api-record",
|
|
91
|
+
sourceRef: "example-records://entity/entity-123",
|
|
92
|
+
observedAt: new Date().toISOString(),
|
|
93
|
+
locatorScheme: "structured-field",
|
|
94
|
+
},
|
|
95
|
+
extraction: {
|
|
96
|
+
confidence: 0.97,
|
|
97
|
+
locator: "json:$.registrationStatus",
|
|
98
|
+
extractor: "example-extractor",
|
|
99
|
+
extractedAt: new Date().toISOString(),
|
|
100
|
+
},
|
|
101
|
+
reviewOutcome: {
|
|
102
|
+
status: "verified",
|
|
103
|
+
actor: "records-operator",
|
|
104
|
+
reviewedAt: new Date().toISOString(),
|
|
105
|
+
},
|
|
106
|
+
claim: {
|
|
107
|
+
subjectType: "public-record.entity",
|
|
108
|
+
subjectId: "entity-123",
|
|
109
|
+
surface: "example.profile",
|
|
110
|
+
claimType: "public-data.field",
|
|
111
|
+
status: "verified",
|
|
112
|
+
impactLevel: "medium",
|
|
113
|
+
collectedBy: "example-extractor",
|
|
114
|
+
},
|
|
115
|
+
metadata: {
|
|
116
|
+
producerField: "registration_status",
|
|
117
|
+
},
|
|
118
|
+
}))
|
|
119
|
+
.build();
|
|
120
|
+
|
|
121
|
+
const trustInput = buildSurveyTrustInput(surveyInput);
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
`fieldObservation` sets `extraction.target` and `claim.fieldOrBehavior` from
|
|
125
|
+
`field` when omitted, uses the scalar as both the extraction and claim value,
|
|
126
|
+
and adds neutral helper metadata at
|
|
127
|
+
`metadata.survey.field = { representation: "scalar" }`. Producer metadata is
|
|
128
|
+
preserved. Producers still own scalar semantics, validation, candidate ranking,
|
|
129
|
+
review policy, and whether a value should be verified, proposed, rejected, or
|
|
130
|
+
assumed.
|
|
131
|
+
|
|
67
132
|
## Repeated observations
|
|
68
133
|
|
|
69
134
|
Use `repeatedObservation` when a producer wants to describe a repeated field or
|
|
@@ -133,6 +198,103 @@ Producer metadata is preserved. Producers still own item semantics,
|
|
|
133
198
|
validation, candidate ranking, review policy, and whether a value should be
|
|
134
199
|
verified, proposed, rejected, or assumed.
|
|
135
200
|
|
|
201
|
+
## Candidate review records
|
|
202
|
+
|
|
203
|
+
Use `candidateReviewRecord` when a producer has multiple candidate observations
|
|
204
|
+
for the same target and wants Survey to assemble the shared candidate set,
|
|
205
|
+
candidate links, and optional review outcome.
|
|
206
|
+
|
|
207
|
+
```ts
|
|
208
|
+
import {
|
|
209
|
+
candidateReviewRecord,
|
|
210
|
+
fieldObservation,
|
|
211
|
+
SurveyInputBuilder,
|
|
212
|
+
} from "@kontourai/survey";
|
|
213
|
+
|
|
214
|
+
const observations = [
|
|
215
|
+
fieldObservation({
|
|
216
|
+
id: "entity-123.status.registry",
|
|
217
|
+
field: "registrationStatus",
|
|
218
|
+
value: "ACTIVE",
|
|
219
|
+
rawSource: {
|
|
220
|
+
kind: "api-record",
|
|
221
|
+
sourceRef: "example-records://entity/entity-123",
|
|
222
|
+
observedAt: new Date().toISOString(),
|
|
223
|
+
locatorScheme: "structured-field",
|
|
224
|
+
},
|
|
225
|
+
extraction: {
|
|
226
|
+
confidence: 0.97,
|
|
227
|
+
locator: "json:$.registrationStatus",
|
|
228
|
+
extractor: "example-extractor",
|
|
229
|
+
extractedAt: new Date().toISOString(),
|
|
230
|
+
},
|
|
231
|
+
candidate: { id: "candidate.registry", confidence: 0.97 },
|
|
232
|
+
claim: {
|
|
233
|
+
id: "claim.entity-123.status.registry",
|
|
234
|
+
subjectType: "public-record.entity",
|
|
235
|
+
subjectId: "entity-123",
|
|
236
|
+
surface: "example.profile",
|
|
237
|
+
claimType: "public-data.field",
|
|
238
|
+
status: "verified",
|
|
239
|
+
impactLevel: "medium",
|
|
240
|
+
collectedBy: "example-extractor",
|
|
241
|
+
},
|
|
242
|
+
}),
|
|
243
|
+
fieldObservation({
|
|
244
|
+
id: "entity-123.status.archive",
|
|
245
|
+
field: "registrationStatus",
|
|
246
|
+
value: "INACTIVE",
|
|
247
|
+
rawSource: {
|
|
248
|
+
kind: "web-page",
|
|
249
|
+
sourceRef: "https://records.example.test/entity-123",
|
|
250
|
+
observedAt: new Date().toISOString(),
|
|
251
|
+
locatorScheme: "html",
|
|
252
|
+
},
|
|
253
|
+
extraction: {
|
|
254
|
+
confidence: 0.71,
|
|
255
|
+
locator: "css:#registration-status",
|
|
256
|
+
extractor: "example-crawler",
|
|
257
|
+
extractedAt: new Date().toISOString(),
|
|
258
|
+
},
|
|
259
|
+
candidate: { id: "candidate.archive", confidence: 0.71 },
|
|
260
|
+
claim: {
|
|
261
|
+
id: "claim.entity-123.status.archive",
|
|
262
|
+
subjectType: "public-record.entity",
|
|
263
|
+
subjectId: "entity-123",
|
|
264
|
+
surface: "example.profile",
|
|
265
|
+
claimType: "public-data.field",
|
|
266
|
+
status: "superseded",
|
|
267
|
+
impactLevel: "medium",
|
|
268
|
+
collectedBy: "example-crawler",
|
|
269
|
+
},
|
|
270
|
+
}),
|
|
271
|
+
];
|
|
272
|
+
|
|
273
|
+
const surveyInput = new SurveyInputBuilder({ source: "example-producer:run-1" })
|
|
274
|
+
.addClaimRecords(candidateReviewRecord({
|
|
275
|
+
id: "candidate-set.entity-123.registration-status",
|
|
276
|
+
target: "registrationStatus",
|
|
277
|
+
selectedCandidateId: "candidate.registry",
|
|
278
|
+
status: "resolved",
|
|
279
|
+
rationale: "Registry source wins over archive source.",
|
|
280
|
+
reviewOutcome: {
|
|
281
|
+
status: "verified",
|
|
282
|
+
actor: "records-operator",
|
|
283
|
+
reviewedAt: new Date().toISOString(),
|
|
284
|
+
},
|
|
285
|
+
observations,
|
|
286
|
+
}))
|
|
287
|
+
.build();
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
`candidateReviewRecord` does not choose the winning candidate or status. The
|
|
291
|
+
producer still supplies candidate ids, selected candidate id, claim ids, review
|
|
292
|
+
status, rationale, and all domain policy. Survey only assembles the generic
|
|
293
|
+
record graph and tolerates repeated references to identical raw sources or the
|
|
294
|
+
shared candidate set while rejecting conflicting duplicate ids. Duplicate
|
|
295
|
+
conflict checks assume Survey records are JSON-shaped data, which is the same
|
|
296
|
+
shape expected by Surface validation and reports.
|
|
297
|
+
|
|
136
298
|
## Product Boundary
|
|
137
299
|
|
|
138
300
|
Survey does not crawl pages, parse PDFs, rank candidates, decide review policy,
|
package/dist/src/builder.d.ts
CHANGED
|
@@ -37,6 +37,19 @@ export interface SurveyObservationInput {
|
|
|
37
37
|
id?: string;
|
|
38
38
|
};
|
|
39
39
|
}
|
|
40
|
+
export interface CandidateReviewRecordInput {
|
|
41
|
+
id: string;
|
|
42
|
+
target: string;
|
|
43
|
+
observations: SurveyObservationInput[];
|
|
44
|
+
selectedCandidateId?: string;
|
|
45
|
+
status?: CandidateSet["status"];
|
|
46
|
+
rationale?: string;
|
|
47
|
+
metadata?: Record<string, unknown>;
|
|
48
|
+
reviewOutcome?: Omit<ReviewOutcome, "id" | "candidateSetId"> & {
|
|
49
|
+
id?: string;
|
|
50
|
+
candidateId?: string;
|
|
51
|
+
};
|
|
52
|
+
}
|
|
40
53
|
export declare class SurveyInputBuilder {
|
|
41
54
|
private readonly source;
|
|
42
55
|
private readonly generatedAt;
|
|
@@ -58,4 +71,7 @@ export declare class SurveyInputBuilder {
|
|
|
58
71
|
addObservation(observation: SurveyObservationInput): this;
|
|
59
72
|
addObservations(observations: SurveyObservationInput[]): this;
|
|
60
73
|
build(): SurveyInput;
|
|
74
|
+
private addRecordCandidateSet;
|
|
75
|
+
private addRecordRawSource;
|
|
61
76
|
}
|
|
77
|
+
export declare function candidateReviewRecord(input: CandidateReviewRecordInput): SurveyClaimRecord[];
|
package/dist/src/builder.js
CHANGED
|
@@ -36,9 +36,9 @@ export class SurveyInputBuilder {
|
|
|
36
36
|
return this;
|
|
37
37
|
}
|
|
38
38
|
addClaimRecord(record) {
|
|
39
|
-
this.
|
|
39
|
+
this.addRecordRawSource(record.rawSource);
|
|
40
40
|
this.addExtraction(record.extraction);
|
|
41
|
-
this.
|
|
41
|
+
this.addRecordCandidateSet(record.candidateSet);
|
|
42
42
|
if (record.reviewOutcome)
|
|
43
43
|
this.addReviewOutcome(record.reviewOutcome);
|
|
44
44
|
this.addClaim(record.claim);
|
|
@@ -69,6 +69,77 @@ export class SurveyInputBuilder {
|
|
|
69
69
|
derivedClaims: [...this.derivedClaims.values()],
|
|
70
70
|
};
|
|
71
71
|
}
|
|
72
|
+
addRecordCandidateSet(candidateSet) {
|
|
73
|
+
addIdempotent(this.candidateSets, candidateSet, "candidate set");
|
|
74
|
+
}
|
|
75
|
+
addRecordRawSource(rawSource) {
|
|
76
|
+
addIdempotent(this.rawSources, rawSource, "raw source");
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
export function candidateReviewRecord(input) {
|
|
80
|
+
if (input.observations.length === 0) {
|
|
81
|
+
throw new Error(`Candidate review record ${input.id} needs at least one observation`);
|
|
82
|
+
}
|
|
83
|
+
const records = input.observations.map((observation) => observationToClaimRecord(observation));
|
|
84
|
+
const candidateSetId = input.id;
|
|
85
|
+
const selectedCandidateId = input.selectedCandidateId ?? input.reviewOutcome?.candidateId;
|
|
86
|
+
const candidates = records.map((record) => record.candidateSet.candidates[0]);
|
|
87
|
+
assertUniqueCandidateIds(candidates, candidateSetId);
|
|
88
|
+
assertCandidateIdExists(candidates, selectedCandidateId, candidateSetId, "selected");
|
|
89
|
+
if (input.selectedCandidateId && input.reviewOutcome?.candidateId && input.selectedCandidateId !== input.reviewOutcome.candidateId) {
|
|
90
|
+
throw new Error(`Candidate review record ${candidateSetId} has conflicting selected and review candidate ids`);
|
|
91
|
+
}
|
|
92
|
+
if (input.reviewOutcome && !selectedCandidateId) {
|
|
93
|
+
throw new Error(`Candidate review record ${candidateSetId} needs a selected candidate id for review outcome`);
|
|
94
|
+
}
|
|
95
|
+
const candidateSet = {
|
|
96
|
+
id: candidateSetId,
|
|
97
|
+
target: input.target,
|
|
98
|
+
candidates,
|
|
99
|
+
selectedCandidateId,
|
|
100
|
+
status: input.status ?? candidateSetStatusFor(input.reviewOutcome?.status),
|
|
101
|
+
rationale: input.rationale,
|
|
102
|
+
metadata: input.metadata,
|
|
103
|
+
};
|
|
104
|
+
const reviewCandidateId = input.reviewOutcome?.candidateId ?? selectedCandidateId;
|
|
105
|
+
const reviewOutcome = input.reviewOutcome
|
|
106
|
+
? {
|
|
107
|
+
id: input.reviewOutcome.id ?? `${candidateSetId}.review`,
|
|
108
|
+
candidateSetId,
|
|
109
|
+
...input.reviewOutcome,
|
|
110
|
+
candidateId: reviewCandidateId,
|
|
111
|
+
}
|
|
112
|
+
: undefined;
|
|
113
|
+
assertCandidateIdExists(candidates, reviewCandidateId, candidateSetId, "review");
|
|
114
|
+
return records.map((record) => {
|
|
115
|
+
const candidate = record.candidateSet.candidates[0];
|
|
116
|
+
return {
|
|
117
|
+
...record,
|
|
118
|
+
candidateSet,
|
|
119
|
+
reviewOutcome: candidate.id === reviewCandidateId ? reviewOutcome : undefined,
|
|
120
|
+
claim: {
|
|
121
|
+
...record.claim,
|
|
122
|
+
candidateSetId,
|
|
123
|
+
candidateId: candidate.id,
|
|
124
|
+
},
|
|
125
|
+
};
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
function assertUniqueCandidateIds(candidates, candidateSetId) {
|
|
129
|
+
const seen = new Set();
|
|
130
|
+
for (const candidate of candidates) {
|
|
131
|
+
if (seen.has(candidate.id)) {
|
|
132
|
+
throw new Error(`Candidate review record ${candidateSetId} has duplicate candidate id: ${candidate.id}`);
|
|
133
|
+
}
|
|
134
|
+
seen.add(candidate.id);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
function assertCandidateIdExists(candidates, candidateId, candidateSetId, role) {
|
|
138
|
+
if (!candidateId)
|
|
139
|
+
return;
|
|
140
|
+
if (!candidates.some((candidate) => candidate.id === candidateId)) {
|
|
141
|
+
throw new Error(`Candidate review record ${candidateSetId} does not contain ${role} candidate ${candidateId}`);
|
|
142
|
+
}
|
|
72
143
|
}
|
|
73
144
|
function observationToClaimRecord(observation) {
|
|
74
145
|
const ids = observationIds(observation.id, observation);
|
|
@@ -138,3 +209,25 @@ function addUnique(map, item, label) {
|
|
|
138
209
|
throw new Error(`Duplicate ${label} id: ${item.id}`);
|
|
139
210
|
map.set(item.id, item);
|
|
140
211
|
}
|
|
212
|
+
function addIdempotent(map, item, label) {
|
|
213
|
+
const existing = map.get(item.id);
|
|
214
|
+
if (existing) {
|
|
215
|
+
if (stableStringify(existing) !== stableStringify(item)) {
|
|
216
|
+
throw new Error(`Conflicting ${label} id: ${item.id}`);
|
|
217
|
+
}
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
map.set(item.id, item);
|
|
221
|
+
}
|
|
222
|
+
function stableStringify(value) {
|
|
223
|
+
return JSON.stringify(sortValue(value));
|
|
224
|
+
}
|
|
225
|
+
function sortValue(value) {
|
|
226
|
+
if (Array.isArray(value))
|
|
227
|
+
return value.map(sortValue);
|
|
228
|
+
if (!value || typeof value !== "object")
|
|
229
|
+
return value;
|
|
230
|
+
return Object.fromEntries(Object.entries(value)
|
|
231
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
232
|
+
.map(([key, item]) => [key, sortValue(item)]));
|
|
233
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { SurveyObservationInput } from "./builder.js";
|
|
2
|
+
export interface FieldObservationInput<TValue> {
|
|
3
|
+
id: string;
|
|
4
|
+
field: string;
|
|
5
|
+
value: TValue;
|
|
6
|
+
rawSource: SurveyObservationInput["rawSource"];
|
|
7
|
+
extraction: Omit<SurveyObservationInput["extraction"], "target" | "value" | "excerpt"> & {
|
|
8
|
+
target?: string;
|
|
9
|
+
excerpt?: string | null;
|
|
10
|
+
};
|
|
11
|
+
reviewOutcome?: SurveyObservationInput["reviewOutcome"];
|
|
12
|
+
claim: Omit<SurveyObservationInput["claim"], "fieldOrBehavior" | "value"> & {
|
|
13
|
+
fieldOrBehavior?: string;
|
|
14
|
+
};
|
|
15
|
+
candidate?: SurveyObservationInput["candidate"];
|
|
16
|
+
candidateSet?: SurveyObservationInput["candidateSet"];
|
|
17
|
+
representation?: "scalar";
|
|
18
|
+
metadata?: Record<string, unknown>;
|
|
19
|
+
}
|
|
20
|
+
export declare function fieldObservation<TValue>(input: FieldObservationInput<TValue>): SurveyObservationInput;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { buildObservation } from "./observation-helper.js";
|
|
2
|
+
export function fieldObservation(input) {
|
|
3
|
+
const representation = input.representation ?? "scalar";
|
|
4
|
+
return buildObservation({
|
|
5
|
+
...input,
|
|
6
|
+
surveyMetadata: {
|
|
7
|
+
field: { representation },
|
|
8
|
+
},
|
|
9
|
+
defaultExcerpt: `${input.field}: ${valueSummary(input.value)}`,
|
|
10
|
+
});
|
|
11
|
+
}
|
|
12
|
+
function valueSummary(value) {
|
|
13
|
+
if (value === null || value === undefined)
|
|
14
|
+
return "<empty>";
|
|
15
|
+
return String(value);
|
|
16
|
+
}
|
package/dist/src/index.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
export type { CandidateSetStatus, Candidate, CandidateSet, ClaimTarget, DerivedClaimTarget, Extraction, LocatorScheme, RawSource, RawSourceKind, ReviewOutcome, ReviewStatus, SurveyInput, } from "./types.js";
|
|
2
|
-
export { SurveyInputBuilder } from "./builder.js";
|
|
3
|
-
export type { SurveyClaimRecord, SurveyInputBuilderArgs, SurveyObservationInput } from "./builder.js";
|
|
2
|
+
export { candidateReviewRecord, SurveyInputBuilder } from "./builder.js";
|
|
3
|
+
export type { CandidateReviewRecordInput, SurveyClaimRecord, SurveyInputBuilderArgs, SurveyObservationInput, } from "./builder.js";
|
|
4
4
|
export { buildSurveyTrustInput } from "./to-surface.js";
|
|
5
|
+
export { fieldObservation } from "./field-observation.js";
|
|
6
|
+
export type { FieldObservationInput } from "./field-observation.js";
|
|
5
7
|
export { repeatedObservation } from "./repeated-observation.js";
|
|
6
8
|
export type { RepeatedObservationInput } from "./repeated-observation.js";
|
package/dist/src/index.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
-
export { SurveyInputBuilder } from "./builder.js";
|
|
1
|
+
export { candidateReviewRecord, SurveyInputBuilder } from "./builder.js";
|
|
2
2
|
export { buildSurveyTrustInput } from "./to-surface.js";
|
|
3
|
+
export { fieldObservation } from "./field-observation.js";
|
|
3
4
|
export { repeatedObservation } from "./repeated-observation.js";
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { SurveyObservationInput } from "./builder.js";
|
|
2
|
+
export interface BuildObservationInput<TValue> {
|
|
3
|
+
id: string;
|
|
4
|
+
field: string;
|
|
5
|
+
value: TValue;
|
|
6
|
+
rawSource: SurveyObservationInput["rawSource"];
|
|
7
|
+
extraction: Omit<SurveyObservationInput["extraction"], "target" | "value" | "excerpt"> & {
|
|
8
|
+
target?: string;
|
|
9
|
+
excerpt?: string | null;
|
|
10
|
+
};
|
|
11
|
+
reviewOutcome?: SurveyObservationInput["reviewOutcome"];
|
|
12
|
+
claim: Omit<SurveyObservationInput["claim"], "fieldOrBehavior" | "value"> & {
|
|
13
|
+
fieldOrBehavior?: string;
|
|
14
|
+
};
|
|
15
|
+
candidate?: SurveyObservationInput["candidate"];
|
|
16
|
+
candidateSet?: SurveyObservationInput["candidateSet"];
|
|
17
|
+
metadata?: Record<string, unknown>;
|
|
18
|
+
surveyMetadata: Record<string, unknown>;
|
|
19
|
+
defaultExcerpt: string;
|
|
20
|
+
}
|
|
21
|
+
export declare function buildObservation<TValue>(input: BuildObservationInput<TValue>): SurveyObservationInput;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
export function buildObservation(input) {
|
|
2
|
+
return {
|
|
3
|
+
id: input.id,
|
|
4
|
+
rawSource: input.rawSource,
|
|
5
|
+
extraction: {
|
|
6
|
+
...input.extraction,
|
|
7
|
+
target: input.extraction.target ?? input.field,
|
|
8
|
+
value: input.value,
|
|
9
|
+
excerpt: input.extraction.excerpt ?? input.defaultExcerpt,
|
|
10
|
+
},
|
|
11
|
+
candidate: input.candidate,
|
|
12
|
+
candidateSet: input.candidateSet,
|
|
13
|
+
reviewOutcome: input.reviewOutcome,
|
|
14
|
+
claim: {
|
|
15
|
+
...input.claim,
|
|
16
|
+
fieldOrBehavior: input.claim.fieldOrBehavior ?? input.field,
|
|
17
|
+
value: input.value,
|
|
18
|
+
metadata: mergeObservationMetadata(input.claim.metadata, input.metadata, input.surveyMetadata),
|
|
19
|
+
},
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
function mergeObservationMetadata(claimMetadata, metadata, surveyMetadata) {
|
|
23
|
+
const claimSurvey = claimMetadata?.survey && isRecord(claimMetadata.survey) ? claimMetadata.survey : {};
|
|
24
|
+
const survey = metadata?.survey && isRecord(metadata.survey) ? metadata.survey : {};
|
|
25
|
+
return {
|
|
26
|
+
...claimMetadata,
|
|
27
|
+
...metadata,
|
|
28
|
+
survey: {
|
|
29
|
+
...claimSurvey,
|
|
30
|
+
...survey,
|
|
31
|
+
...mergeNestedRecords(claimSurvey, survey, surveyMetadata),
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
function mergeNestedRecords(claimSurvey, survey, surveyMetadata) {
|
|
36
|
+
const merged = {};
|
|
37
|
+
const keys = new Set([
|
|
38
|
+
...Object.keys(claimSurvey),
|
|
39
|
+
...Object.keys(survey),
|
|
40
|
+
...Object.keys(surveyMetadata),
|
|
41
|
+
]);
|
|
42
|
+
for (const key of keys) {
|
|
43
|
+
const claimValue = claimSurvey[key];
|
|
44
|
+
const metadataValue = survey[key];
|
|
45
|
+
const helperValue = surveyMetadata[key];
|
|
46
|
+
if (isRecord(claimValue) && isRecord(metadataValue) && isRecord(helperValue)) {
|
|
47
|
+
merged[key] = { ...claimValue, ...metadataValue, ...helperValue };
|
|
48
|
+
}
|
|
49
|
+
else if (isRecord(claimValue) && isRecord(helperValue) && metadataValue === undefined) {
|
|
50
|
+
merged[key] = { ...claimValue, ...helperValue };
|
|
51
|
+
}
|
|
52
|
+
else if (isRecord(metadataValue) && isRecord(helperValue)) {
|
|
53
|
+
merged[key] = { ...metadataValue, ...helperValue };
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
merged[key] = helperValue ?? metadataValue ?? claimValue;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return merged;
|
|
60
|
+
}
|
|
61
|
+
function isRecord(value) {
|
|
62
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
63
|
+
}
|
|
@@ -1,42 +1,16 @@
|
|
|
1
|
+
import { buildObservation } from "./observation-helper.js";
|
|
1
2
|
export function repeatedObservation(input) {
|
|
2
3
|
const representation = input.representation ?? "aggregate-array";
|
|
3
4
|
const value = [...input.value];
|
|
4
|
-
return {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
...input.extraction,
|
|
9
|
-
target: input.extraction.target ?? input.field,
|
|
10
|
-
value,
|
|
11
|
-
excerpt: input.extraction.excerpt ?? `${input.field}: ${value.length} item(s)`,
|
|
12
|
-
},
|
|
13
|
-
candidate: input.candidate,
|
|
14
|
-
candidateSet: input.candidateSet,
|
|
15
|
-
reviewOutcome: input.reviewOutcome,
|
|
16
|
-
claim: {
|
|
17
|
-
...input.claim,
|
|
18
|
-
fieldOrBehavior: input.claim.fieldOrBehavior ?? input.field,
|
|
19
|
-
value,
|
|
20
|
-
metadata: repeatedMetadata(input.claim.metadata, input.metadata, representation, value.length),
|
|
21
|
-
},
|
|
22
|
-
};
|
|
23
|
-
}
|
|
24
|
-
function repeatedMetadata(claimMetadata, metadata, representation, itemCount) {
|
|
25
|
-
const claimSurvey = claimMetadata?.survey && isRecord(claimMetadata.survey) ? claimMetadata.survey : {};
|
|
26
|
-
const survey = metadata?.survey && isRecord(metadata.survey) ? metadata.survey : {};
|
|
27
|
-
return {
|
|
28
|
-
...claimMetadata,
|
|
29
|
-
...metadata,
|
|
30
|
-
survey: {
|
|
31
|
-
...claimSurvey,
|
|
32
|
-
...survey,
|
|
5
|
+
return buildObservation({
|
|
6
|
+
...input,
|
|
7
|
+
value,
|
|
8
|
+
surveyMetadata: {
|
|
33
9
|
repeated: {
|
|
34
10
|
representation,
|
|
35
|
-
itemCount,
|
|
11
|
+
itemCount: value.length,
|
|
36
12
|
},
|
|
37
13
|
},
|
|
38
|
-
|
|
39
|
-
}
|
|
40
|
-
function isRecord(value) {
|
|
41
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
14
|
+
defaultExcerpt: `${input.field}: ${value.length} item(s)`,
|
|
15
|
+
});
|
|
42
16
|
}
|
package/package.json
CHANGED