@kontourai/survey 0.4.3 → 0.4.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 +218 -9
- package/dist/examples/review-workbench/downstream-public-directory-adapter.d.ts +3 -0
- package/dist/examples/review-workbench/downstream-public-directory-adapter.js +262 -0
- package/dist/examples/review-workbench/review-queue-session.d.ts +54 -0
- package/dist/examples/review-workbench/review-queue-session.js +122 -0
- package/dist/examples/review-workbench/review-surface-preview.d.ts +55 -0
- package/dist/examples/review-workbench/review-surface-preview.js +104 -0
- package/dist/examples/review-workbench/review-workbench-data.d.ts +261 -0
- package/dist/examples/review-workbench/review-workbench-data.js +199 -0
- package/dist/examples/review-workbench/review-workbench.d.ts +7 -0
- package/dist/examples/review-workbench/review-workbench.js +488 -0
- package/dist/fixtures/downstream-public-directory-proposal.d.ts +100 -0
- package/dist/fixtures/downstream-public-directory-proposal.js +59 -0
- package/dist/fixtures/public-directory-review-resource.d.ts +156 -0
- package/dist/fixtures/public-directory-review-resource.js +157 -0
- package/dist/fixtures/regulated-document-review-resource.d.ts +121 -0
- package/dist/fixtures/regulated-document-review-resource.js +131 -0
- package/dist/src/builder.d.ts +4 -1
- package/dist/src/builder.js +7 -0
- package/dist/src/index.d.ts +8 -4
- package/dist/src/index.js +4 -2
- package/dist/src/learning-projections.d.ts +19 -0
- package/dist/src/learning-projections.js +132 -0
- package/dist/src/raw-source.d.ts +15 -0
- package/dist/src/raw-source.js +22 -0
- package/dist/src/review-proof.d.ts +28 -0
- package/dist/src/review-proof.js +58 -1
- package/dist/src/review-resource.d.ts +111 -0
- package/dist/src/review-resource.js +1 -0
- package/dist/src/to-surface.js +226 -13
- package/dist/src/types.d.ts +17 -1
- package/package.json +10 -4
package/README.md
CHANGED
|
@@ -11,7 +11,7 @@ ingestion platform:
|
|
|
11
11
|
- `buildSurveyTrustInput` projects those records into `@kontourai/surface`
|
|
12
12
|
`TrustInput`;
|
|
13
13
|
- Surface owns Claim, Subject, Claim Type, Evidence, Status, Claim Dependency,
|
|
14
|
-
TrustInput, trust reporting,
|
|
14
|
+
TrustInput, trust reporting, and public reporting surfaces.
|
|
15
15
|
|
|
16
16
|
The first success criterion is that generic corrected-document and public-field
|
|
17
17
|
fixtures can pass through Survey and produce valid Surface reports without
|
|
@@ -64,6 +64,45 @@ const trustInput = validateTrustInput(buildSurveyTrustInput(surveyInput));
|
|
|
64
64
|
const report = buildTrustReport(trustInput);
|
|
65
65
|
```
|
|
66
66
|
|
|
67
|
+
## Contributor checks
|
|
68
|
+
|
|
69
|
+
Install the repo-owned Git hooks once per clone:
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
npm run setup:repo-hooks
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
The setup command is idempotent. It sets this repo's local `core.hooksPath` to
|
|
76
|
+
`.githooks` and does not require global Git configuration.
|
|
77
|
+
|
|
78
|
+
Use the same checks directly when you want to validate hook drift or package
|
|
79
|
+
health before pushing:
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
npm run validate:repo-hooks
|
|
83
|
+
npm run verify
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
The committed pre-push hook runs both commands from the repo root.
|
|
87
|
+
|
|
88
|
+
## Producer validation path
|
|
89
|
+
|
|
90
|
+
Survey producers validate through public `@kontourai/survey` and
|
|
91
|
+
`@kontourai/surface` contracts:
|
|
92
|
+
|
|
93
|
+
1. Build Survey observations with source, extraction, candidate, review, and
|
|
94
|
+
claim records.
|
|
95
|
+
2. Call `buildSurveyTrustInput` to project the Survey records into Surface
|
|
96
|
+
`TrustInput`.
|
|
97
|
+
3. Call Surface `validateTrustInput` on the projected input.
|
|
98
|
+
4. Optionally call public Surface report APIs such as `buildTrustReport` to
|
|
99
|
+
inspect claims, evidence, status, gaps, and metadata.
|
|
100
|
+
|
|
101
|
+
Keep producer operational state outside Survey. Queue status, reviewer form
|
|
102
|
+
state, retries, source caches, and product policy decisions belong in the
|
|
103
|
+
producer's own data model. Survey carries only the portable source,
|
|
104
|
+
extraction, candidate, review, and claim projection records needed by Surface.
|
|
105
|
+
|
|
67
106
|
## Raw sources
|
|
68
107
|
|
|
69
108
|
Use raw-source helpers when a producer wants Survey to shape source identity
|
|
@@ -113,10 +152,103 @@ const surveyInput = new SurveyInputBuilder({ source: "example-producer:run-1" })
|
|
|
113
152
|
```
|
|
114
153
|
|
|
115
154
|
Survey exports `uploadedDocumentSource`, `apiRecordSource`, `webPageSource`,
|
|
116
|
-
and `
|
|
117
|
-
Survey derives a stable id from source kind and
|
|
118
|
-
values are normalized to `sha256:<value>`, while
|
|
119
|
-
values are preserved. Producer metadata is copied
|
|
155
|
+
`manualEntrySource`, and `policyStandardSource`. Producer-provided `id` values
|
|
156
|
+
are preserved; otherwise Survey derives a stable id from source kind and
|
|
157
|
+
`sourceRef`. Bare checksum values are normalized to `sha256:<value>`, while
|
|
158
|
+
already-prefixed checksum values are preserved. Producer metadata is copied
|
|
159
|
+
through to Surface evidence.
|
|
160
|
+
|
|
161
|
+
Use `policyStandardSource` when the observed material is the applied standard
|
|
162
|
+
itself. It records `inlineText`, `standardVersion`, and optional `paragraphRef`
|
|
163
|
+
on the `RawSource` and projects to Surface `policy_rule` evidence by default.
|
|
164
|
+
Survey only preserves the producer-applied standard text/version; it does not
|
|
165
|
+
decide whether that standard is correct for the producer's domain.
|
|
166
|
+
|
|
167
|
+
## Interpretation records
|
|
168
|
+
|
|
169
|
+
Use `addInterpretation` when a producer records how an actor read a
|
|
170
|
+
`policy-standard` paragraph for one claim. Interpretations are flat provenance
|
|
171
|
+
records with an `appliesTo` edge to a claim and an `anchorsTo` edge to a
|
|
172
|
+
policy-standard raw source; they are not nested claim derivations or rejection
|
|
173
|
+
reasons.
|
|
174
|
+
|
|
175
|
+
```ts
|
|
176
|
+
import {
|
|
177
|
+
apiRecordSource,
|
|
178
|
+
buildSurveyTrustInput,
|
|
179
|
+
fieldObservation,
|
|
180
|
+
policyStandardSource,
|
|
181
|
+
SurveyInputBuilder,
|
|
182
|
+
} from "@kontourai/survey";
|
|
183
|
+
|
|
184
|
+
const observedAt = new Date().toISOString();
|
|
185
|
+
const standard = policyStandardSource({
|
|
186
|
+
id: "source.example.policy-standard.rule-1",
|
|
187
|
+
sourceRef: "policy-standard://example/rules/2026#rule-1",
|
|
188
|
+
observedAt,
|
|
189
|
+
inlineText: "A producer reading must cite the applied rule paragraph.",
|
|
190
|
+
standardVersion: "2026.1",
|
|
191
|
+
paragraphRef: "rule-1",
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
const surveyInput = new SurveyInputBuilder({ source: "example-producer:run-1" })
|
|
195
|
+
.addRawSource(standard)
|
|
196
|
+
.addObservation(fieldObservation({
|
|
197
|
+
id: "observation.example.policy-application",
|
|
198
|
+
field: "policyApplication.status",
|
|
199
|
+
value: "DOCUMENTED",
|
|
200
|
+
rawSource: apiRecordSource({
|
|
201
|
+
id: "source.example.application-record",
|
|
202
|
+
sourceRef: "example-records://application/application-1",
|
|
203
|
+
observedAt,
|
|
204
|
+
checksum: "application-1",
|
|
205
|
+
}),
|
|
206
|
+
extraction: {
|
|
207
|
+
target: "policyApplication.status",
|
|
208
|
+
locator: "json:$.policyApplication.status",
|
|
209
|
+
extractor: "example-extractor",
|
|
210
|
+
extractedAt: observedAt,
|
|
211
|
+
},
|
|
212
|
+
claim: {
|
|
213
|
+
id: "claim.example.policy-application",
|
|
214
|
+
subjectType: "example.application",
|
|
215
|
+
subjectId: "application-1",
|
|
216
|
+
surface: "example.review",
|
|
217
|
+
claimType: "policy-application.status",
|
|
218
|
+
impactLevel: "medium",
|
|
219
|
+
collectedBy: "example-extractor",
|
|
220
|
+
},
|
|
221
|
+
}))
|
|
222
|
+
.addInterpretation({
|
|
223
|
+
id: "interpretation.example.rule-1",
|
|
224
|
+
appliesToClaimId: "claim.example.policy-application",
|
|
225
|
+
anchorsToSourceId: standard.id,
|
|
226
|
+
ruleLocator: "text:paragraph=rule-1",
|
|
227
|
+
reading: "The producer read rule 1 as applying to the claim.",
|
|
228
|
+
actor: "producer-operator",
|
|
229
|
+
recordedAt: observedAt,
|
|
230
|
+
})
|
|
231
|
+
.build();
|
|
232
|
+
|
|
233
|
+
const trustInput = buildSurveyTrustInput(surveyInput);
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
Projection emits a normal Surface verification event with
|
|
237
|
+
`method: "survey-interpretation"`, the existing `claimId`, and anchor
|
|
238
|
+
`evidenceIds`. Because current Surface verification events reject unsupported
|
|
239
|
+
keys, typed edge details are preserved on the projected claim at
|
|
240
|
+
`metadata.survey.interpretations[]`. The anchor evidence uses
|
|
241
|
+
`evidenceType: "policy_rule"`, `method: "anchoring"`, the interpretation
|
|
242
|
+
`ruleLocator` as `sourceLocator`, and the policy-standard text/version metadata.
|
|
243
|
+
|
|
244
|
+
## Review resources
|
|
245
|
+
|
|
246
|
+
Survey also exports producer-neutral `ReviewItem`, `ReviewCandidate`, and
|
|
247
|
+
`ReviewDecision` TypeScript resource shapes for review UI and adapter fixtures.
|
|
248
|
+
They use `apiVersion`, `kind`, `metadata`, `spec`, and `status` fields while
|
|
249
|
+
mapping back to the existing Survey record layer. See
|
|
250
|
+
[`docs/review-resource-contract.md`](docs/review-resource-contract.md) for
|
|
251
|
+
field ownership, mapping hints, and the `ReviewSession` non-goal.
|
|
120
252
|
|
|
121
253
|
## Field observations
|
|
122
254
|
|
|
@@ -272,10 +404,10 @@ const trustInput = buildSurveyTrustInput(surveyInput);
|
|
|
272
404
|
`sourceOfAuthorityObservation` remains available as the lower-level object
|
|
273
405
|
factory when a producer already has the full observation input assembled.
|
|
274
406
|
|
|
275
|
-
Contextual claims such as "this submission is compliant" or "this
|
|
407
|
+
Contextual claims such as "this submission is compliant" or "this record is
|
|
276
408
|
eligible for a specific requester" are not source-of-authority observations.
|
|
277
409
|
They are Surface claims with Claim Dependencies on source-of-authority claims
|
|
278
|
-
and other
|
|
410
|
+
and other producer facts. The producer owns that domain logic.
|
|
279
411
|
|
|
280
412
|
For the reusable producer workflow, including manual confirmation state,
|
|
281
413
|
source references, Survey review outcomes, and Surface report boundaries, see
|
|
@@ -295,6 +427,13 @@ This is useful for corrected documents, source-of-truth choices, and review
|
|
|
295
427
|
queues where losing candidates should remain visible for transparency rather
|
|
296
428
|
than disappearing from the trust trail.
|
|
297
429
|
|
|
430
|
+
Candidates may include an optional `rejectionReason` when a producer wants to
|
|
431
|
+
record why a non-selected alternative was superseded or rejected. Survey
|
|
432
|
+
preserves that producer-provided rationale on the candidate and projects it to
|
|
433
|
+
Surface claim `metadata.survey.candidate.rejectionReason` for that candidate
|
|
434
|
+
while preserving producer-provided `metadata.survey` keys. Survey does not rank
|
|
435
|
+
candidates, choose winners, or define rejection policy.
|
|
436
|
+
|
|
298
437
|
## Reviewed current/proposed resolutions
|
|
299
438
|
|
|
300
439
|
Use `reviewedCurrentProposedResolution` when a producer has exactly two
|
|
@@ -477,6 +616,12 @@ shared candidate set while rejecting conflicting duplicate ids. Duplicate
|
|
|
477
616
|
conflict checks assume Survey records are JSON-shaped data, which is the same
|
|
478
617
|
shape expected by Surface validation and reports.
|
|
479
618
|
|
|
619
|
+
If an observation candidate includes `rejectionReason`, `candidateReviewRecord`
|
|
620
|
+
preserves it in the shared candidate set. Use this only for producer-authored
|
|
621
|
+
rationale about a candidate that the producer already treats as non-selected,
|
|
622
|
+
superseded, or rejected; it does not affect selected candidate behavior or
|
|
623
|
+
status projection.
|
|
624
|
+
|
|
480
625
|
A candidate set with status `"conflict"` represents a Survey-side Candidate
|
|
481
626
|
Conflict before review has resolved which candidate should win. When no review
|
|
482
627
|
outcome overrides it, `buildSurveyTrustInput` projects the claim to Surface
|
|
@@ -517,6 +662,30 @@ so producers can store or recompute the exact canonical proof material used for
|
|
|
517
662
|
the anchor. Producer metadata is not part of the canonical payload; any
|
|
518
663
|
non-portable context belongs outside the hash, such as anchor metadata.
|
|
519
664
|
|
|
665
|
+
The canonical payload is the portable review proof contract. It contains:
|
|
666
|
+
|
|
667
|
+
| Field | Purpose |
|
|
668
|
+
| --- | --- |
|
|
669
|
+
| `schemaVersion` / `proof.schema` / `proof.schemaVersion` | Stable Survey review proof schema identity. |
|
|
670
|
+
| `proof.packageName` / `proof.packageVersion` | Review proof contract identity. `proof.packageVersion` is the proof contract version, not the npm package release version. Package releases do not change canonical proof hashes unless this explicit proof contract version or another canonical field changes. |
|
|
671
|
+
| `proof.issuer` | Survey producer identity, derived from the claim collector. |
|
|
672
|
+
| `proof.producer` | Extraction producer identity, derived from the extractor id. |
|
|
673
|
+
| `proof.issuedAt` | Proof envelope time, derived from review time, then claim update time, then extraction time. |
|
|
674
|
+
| `proof.subject` | Claim identity: claim id, candidate set id, reviewed candidate id, subject, surface, claim type, and field/behavior. If the claim also names a candidate id, it must match the reviewed candidate id. |
|
|
675
|
+
| `proof.sourcePayload` / `rawSource.checksum` | Source payload identity, ref, and producer-supplied checksum when present. |
|
|
676
|
+
| `extraction` | Extracted target, value, locator, excerpt, extractor, confidence, and extraction time. |
|
|
677
|
+
| `candidate` / `candidateSet` | Candidate identity/value plus the ordered candidate set, selected candidate, status, and rationale. |
|
|
678
|
+
| `reviewOutcome` | Review decision/status, actor, review time, rationale, and evidence ids. |
|
|
679
|
+
| `claim` | Projected claim identity, status/value, impact, evidence method, derivation links, collector, actor, and event method. |
|
|
680
|
+
|
|
681
|
+
To recompute the anchor value, rebuild the same canonical payload from the
|
|
682
|
+
reviewed Survey records, call `canonicalReviewProofJson(payload)`, and compute
|
|
683
|
+
SHA-256 over that JSON. The result should equal
|
|
684
|
+
`claim.currentIntegrityAnchor.value`. The Surface anchor remains generic:
|
|
685
|
+
`kind: "hash"`, `algorithm: "sha256"`, `verificationStatus: "unverified"`, no
|
|
686
|
+
Survey-specific anchor metadata, and a source/time pointer for display. Claims
|
|
687
|
+
without a selected review outcome are not anchored by `{ reviewProofs: true }`.
|
|
688
|
+
|
|
520
689
|
When the Surface projection proof option is enabled, `buildSurveyTrustInput`
|
|
521
690
|
will attach the same kind of anchor to the projected reviewed claim:
|
|
522
691
|
|
|
@@ -529,6 +698,12 @@ trail in the canonical payload. It does not authenticate an actor, sign the
|
|
|
529
698
|
payload, or prove the real-world truth of the claim. Non-goals include JWT/JWS
|
|
530
699
|
signing, key management, a transparency log, and any veracity guarantee.
|
|
531
700
|
|
|
701
|
+
JWT-adjacent words in the payload are process-envelope vocabulary, not a v0 JWT
|
|
702
|
+
implementation. `issuer` identifies the Survey producer for recomputation,
|
|
703
|
+
`subject` identifies the claim being reviewed, and `issuedAt` records the review
|
|
704
|
+
proof time. Audience restrictions, expiry, cryptographic signing, key discovery,
|
|
705
|
+
and legal non-repudiation are deferred concepts for a future signed envelope.
|
|
706
|
+
|
|
532
707
|
## Computed values
|
|
533
708
|
|
|
534
709
|
Computed values are normal `ClaimTarget` entries in `claims`. Producers should
|
|
@@ -666,8 +841,9 @@ in Surface with a `candidate-escalation` event.
|
|
|
666
841
|
Use `withinComfortZone: false` on a `ReviewOutcome` when the reviewer is
|
|
667
842
|
recording a decision outside their domain expertise or is flagging that the
|
|
668
843
|
conclusion requires a different authority to confirm. The flag and optional
|
|
669
|
-
`comfortZoneNote` are carried forward
|
|
670
|
-
|
|
844
|
+
`comfortZoneNote` are carried forward as structured Survey metadata on the
|
|
845
|
+
projected Surface claim at `metadata.survey.comfortZone`. Verification event
|
|
846
|
+
`notes` carry the normal review or candidate-set rationale only.
|
|
671
847
|
|
|
672
848
|
```ts
|
|
673
849
|
reviewOutcome: {
|
|
@@ -680,6 +856,39 @@ reviewOutcome: {
|
|
|
680
856
|
},
|
|
681
857
|
```
|
|
682
858
|
|
|
859
|
+
## Learning projections
|
|
860
|
+
|
|
861
|
+
Use `buildSurveyLearningProjections(input)` when producer or review tooling needs
|
|
862
|
+
workflow/evaluation signals without changing Surface `TrustInput`.
|
|
863
|
+
|
|
864
|
+
```ts
|
|
865
|
+
import {
|
|
866
|
+
buildSurveyLearningProjections,
|
|
867
|
+
buildSurveyTrustInput,
|
|
868
|
+
} from "@kontourai/survey";
|
|
869
|
+
|
|
870
|
+
const learning = buildSurveyLearningProjections(surveyInput);
|
|
871
|
+
const trustInput = buildSurveyTrustInput(surveyInput);
|
|
872
|
+
```
|
|
873
|
+
|
|
874
|
+
Learning projections are product-neutral `learning.*` records. Survey emits
|
|
875
|
+
`learning.rejected-candidate` from structured candidate rejection data such as
|
|
876
|
+
non-empty `Candidate.rejectionReason` values or a candidate-specific
|
|
877
|
+
`ReviewOutcome.status === "rejected"` outcome with rationale. When both exist,
|
|
878
|
+
Survey emits one rejected-candidate projection enriched with candidate and
|
|
879
|
+
review outcome context.
|
|
880
|
+
Ordinary rejected candidates do not emit `learning.comfort-zone`.
|
|
881
|
+
|
|
882
|
+
Survey also emits `learning.comfort-zone` from structured
|
|
883
|
+
`ReviewOutcome.withinComfortZone === false` data and `learning.escalation` from
|
|
884
|
+
unresolved `EscalationRecord`s, including unattached records that producer
|
|
885
|
+
tooling can route but Surface cannot attach to a claim event.
|
|
886
|
+
|
|
887
|
+
These projections are producer/review workflow and evaluation signals. They are
|
|
888
|
+
not claims about truth or veracity, not Surface claim status, not evidence, and
|
|
889
|
+
not verification events. Calling `buildSurveyLearningProjections` does not alter
|
|
890
|
+
`buildSurveyTrustInput`, trust status derivation, or escalation event projection.
|
|
891
|
+
|
|
683
892
|
## Product Boundary
|
|
684
893
|
|
|
685
894
|
Survey does not crawl pages, parse PDFs, rank candidates, decide review policy,
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import { type ReviewItem } from "../../src/review-resource.js";
|
|
2
|
+
import type { DownstreamPublicDirectoryProposal } from "../../fixtures/downstream-public-directory-proposal.js";
|
|
3
|
+
export declare function downstreamPublicDirectoryProposalToReviewItem(proposal: DownstreamPublicDirectoryProposal, field?: string): ReviewItem;
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
import { reviewResourceApiVersion } from "../../src/review-resource.js";
|
|
2
|
+
export function downstreamPublicDirectoryProposalToReviewItem(proposal, field = firstProposedField(proposal)) {
|
|
3
|
+
const diff = proposal.proposedChanges[field];
|
|
4
|
+
if (!diff) {
|
|
5
|
+
throw new Error(`Proposal ${proposal.id} does not include proposed field ${field}.`);
|
|
6
|
+
}
|
|
7
|
+
const reviewState = reviewStateForProposal(proposal.status, field);
|
|
8
|
+
const currentCandidate = currentReviewCandidate(proposal, field, diff, reviewState.selectedRole);
|
|
9
|
+
const proposedCandidate = proposedReviewCandidate(proposal, field, diff, reviewState.selectedRole);
|
|
10
|
+
return {
|
|
11
|
+
apiVersion: reviewResourceApiVersion,
|
|
12
|
+
kind: "ReviewItem",
|
|
13
|
+
metadata: reviewItemMetadata(proposal, field),
|
|
14
|
+
spec: {
|
|
15
|
+
target: field,
|
|
16
|
+
candidateSetStatus: reviewState.candidateSetStatus,
|
|
17
|
+
...optionalSelectedCandidateId(reviewState.selectedCandidateId),
|
|
18
|
+
rationale: proposal.reviewerNotes ?? downstreamDecisionRationale(proposal.status),
|
|
19
|
+
candidates: [currentCandidate, proposedCandidate],
|
|
20
|
+
producerPolicy: {
|
|
21
|
+
owner: "downstream-adapter",
|
|
22
|
+
sourceShape: "public-directory-current-proposed-proposal",
|
|
23
|
+
proposalStatus: proposal.status,
|
|
24
|
+
...rejectionPolicyForProposal(proposal.status),
|
|
25
|
+
approvedFields: proposal.appliedFields,
|
|
26
|
+
feedbackTags: proposal.feedbackTags,
|
|
27
|
+
opaquePolicyMetadata: {
|
|
28
|
+
priority: proposal.priority,
|
|
29
|
+
crawlTrigger: proposal.crawlTrigger,
|
|
30
|
+
communityScope: proposal.communitySlug,
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
projection: selectedProjection(reviewState.selectedRole, currentCandidate, proposedCandidate),
|
|
34
|
+
},
|
|
35
|
+
status: {
|
|
36
|
+
observedCandidateCount: 2,
|
|
37
|
+
...optionalSelectedCandidateId(reviewState.selectedCandidateId),
|
|
38
|
+
reviewDecisionName: proposal.reviewedAt
|
|
39
|
+
? `public-directory-${proposal.publicRecordId}-${field}-${proposal.id}-decision`
|
|
40
|
+
: undefined,
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
function reviewItemMetadata(proposal, field) {
|
|
45
|
+
return {
|
|
46
|
+
name: `public-directory-${proposal.publicRecordId}-${field}-${proposal.id}`,
|
|
47
|
+
uid: proposal.id,
|
|
48
|
+
labels: {
|
|
49
|
+
domain: "public-directory",
|
|
50
|
+
field,
|
|
51
|
+
},
|
|
52
|
+
annotations: {
|
|
53
|
+
"survey.kontourai.io/adapter": "downstream-public-directory",
|
|
54
|
+
"survey.kontourai.io/source-shape": "sanitized-copied-proposal",
|
|
55
|
+
},
|
|
56
|
+
producer: {
|
|
57
|
+
displayName: proposal.recordName ?? "Example Public Record",
|
|
58
|
+
slug: proposal.recordSlug ?? proposal.publicRecordId,
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
function firstProposedField(proposal) {
|
|
63
|
+
const [field] = Object.keys(proposal.proposedChanges);
|
|
64
|
+
if (!field) {
|
|
65
|
+
throw new Error(`Proposal ${proposal.id} does not include proposed changes.`);
|
|
66
|
+
}
|
|
67
|
+
return field;
|
|
68
|
+
}
|
|
69
|
+
function currentFieldSource(proposal, field) {
|
|
70
|
+
const fieldSources = proposal.recordData?.fieldSources;
|
|
71
|
+
if (!isRecord(fieldSources)) {
|
|
72
|
+
return {};
|
|
73
|
+
}
|
|
74
|
+
const source = fieldSources[field];
|
|
75
|
+
return isRecord(source)
|
|
76
|
+
? {
|
|
77
|
+
excerpt: typeof source.excerpt === "string" ? source.excerpt : undefined,
|
|
78
|
+
sourceUrl: typeof source.sourceUrl === "string" ? source.sourceUrl : undefined,
|
|
79
|
+
approvedAt: typeof source.approvedAt === "string" ? source.approvedAt : undefined,
|
|
80
|
+
}
|
|
81
|
+
: {};
|
|
82
|
+
}
|
|
83
|
+
function currentReviewCandidate(proposal, field, diff, selectedRole) {
|
|
84
|
+
const currentSource = currentFieldSource(proposal, field);
|
|
85
|
+
const observedAt = currentSource.approvedAt ?? proposal.reviewedAt ?? proposal.createdAt;
|
|
86
|
+
return reviewCandidate({
|
|
87
|
+
proposal,
|
|
88
|
+
field,
|
|
89
|
+
diff,
|
|
90
|
+
role: "current",
|
|
91
|
+
value: diff.old,
|
|
92
|
+
sourceId: `public-directory:source:${proposal.publicRecordId}:${field}:current`,
|
|
93
|
+
sourceRef: currentSource.sourceUrl ?? `current-record:${proposal.publicRecordId}:${field}`,
|
|
94
|
+
observedAt,
|
|
95
|
+
excerpt: currentSource.excerpt ?? `Current ${field} value from the reviewed public record.`,
|
|
96
|
+
extractor: "downstream-current-record",
|
|
97
|
+
extractedAt: observedAt,
|
|
98
|
+
confidence: 1,
|
|
99
|
+
sourceRank: selectedRole === "current" ? 1 : 2,
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
function proposedReviewCandidate(proposal, field, diff, selectedRole) {
|
|
103
|
+
return reviewCandidate({
|
|
104
|
+
proposal,
|
|
105
|
+
field,
|
|
106
|
+
diff,
|
|
107
|
+
role: "proposed",
|
|
108
|
+
value: diff.new,
|
|
109
|
+
sourceId: `public-directory:source:${proposal.publicRecordId}:${field}:${proposal.id}`,
|
|
110
|
+
sourceRef: diff.sourceUrl ?? proposal.sourceUrl,
|
|
111
|
+
observedAt: proposal.createdAt,
|
|
112
|
+
excerpt: diff.excerpt ?? `Proposed ${field} value from downstream extraction.`,
|
|
113
|
+
extractor: proposal.extractionModel,
|
|
114
|
+
extractedAt: proposal.createdAt,
|
|
115
|
+
confidence: diff.confidence,
|
|
116
|
+
sourceRank: selectedRole === "proposed" ? 1 : 2,
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
function reviewCandidate(args) {
|
|
120
|
+
const id = candidateId(args.field, args.role);
|
|
121
|
+
const extractionId = `public-directory:extraction:${args.proposal.publicRecordId}:${args.field}:${args.proposal.id}:${args.role}`;
|
|
122
|
+
const claimId = candidateClaimId(args);
|
|
123
|
+
return {
|
|
124
|
+
id,
|
|
125
|
+
role: args.role,
|
|
126
|
+
value: args.value,
|
|
127
|
+
confidence: args.confidence,
|
|
128
|
+
sourceRank: args.sourceRank,
|
|
129
|
+
source: candidateSource(args),
|
|
130
|
+
locator: candidateLocator(args),
|
|
131
|
+
extraction: candidateExtraction(args, extractionId),
|
|
132
|
+
claimTarget: candidateClaimTarget(args, claimId),
|
|
133
|
+
projection: candidateProjection(args, id, extractionId, claimId),
|
|
134
|
+
producer: candidateProducerMetadata(args),
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
function reviewStateForProposal(status, field) {
|
|
138
|
+
if (status === "APPROVED") {
|
|
139
|
+
return {
|
|
140
|
+
candidateSetStatus: "resolved",
|
|
141
|
+
selectedRole: "proposed",
|
|
142
|
+
selectedCandidateId: candidateId(field, "proposed"),
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
if (status === "REJECTED") {
|
|
146
|
+
return {
|
|
147
|
+
candidateSetStatus: "resolved",
|
|
148
|
+
selectedRole: "current",
|
|
149
|
+
selectedCandidateId: candidateId(field, "current"),
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
return {
|
|
153
|
+
candidateSetStatus: "needs-review",
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
function optionalSelectedCandidateId(selectedCandidateId) {
|
|
157
|
+
return selectedCandidateId ? { selectedCandidateId } : {};
|
|
158
|
+
}
|
|
159
|
+
function rejectionPolicyForProposal(status) {
|
|
160
|
+
return status === "REJECTED"
|
|
161
|
+
? { rejectionSemantics: "selected-current-keeps-existing-value-and-rejects-proposed-candidate" }
|
|
162
|
+
: {};
|
|
163
|
+
}
|
|
164
|
+
function selectedProjection(selectedRole, currentCandidate, proposedCandidate) {
|
|
165
|
+
if (selectedRole === "current") {
|
|
166
|
+
return currentCandidate.projection;
|
|
167
|
+
}
|
|
168
|
+
if (selectedRole === "proposed") {
|
|
169
|
+
return proposedCandidate.projection;
|
|
170
|
+
}
|
|
171
|
+
return undefined;
|
|
172
|
+
}
|
|
173
|
+
function candidateSource(args) {
|
|
174
|
+
return {
|
|
175
|
+
sourceId: args.sourceId,
|
|
176
|
+
sourceRef: args.sourceRef,
|
|
177
|
+
kind: args.sourceRef.startsWith("http") ? "web-page" : "manual-entry",
|
|
178
|
+
observedAt: args.observedAt,
|
|
179
|
+
fetchedAt: args.role === "proposed" ? args.proposal.crawlCompletedAt ?? args.observedAt : args.observedAt,
|
|
180
|
+
locatorScheme: args.sourceRef.startsWith("http") ? "html" : "structured-field",
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
function candidateLocator(args) {
|
|
184
|
+
return {
|
|
185
|
+
scheme: args.sourceRef.startsWith("http") ? "html" : "structured-field",
|
|
186
|
+
locator: `field:${args.field}`,
|
|
187
|
+
excerpt: args.excerpt,
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
function candidateExtraction(args, extractionId) {
|
|
191
|
+
return {
|
|
192
|
+
extractionId,
|
|
193
|
+
target: args.field,
|
|
194
|
+
confidence: args.confidence,
|
|
195
|
+
extractor: args.extractor,
|
|
196
|
+
extractedAt: args.extractedAt,
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
function candidateClaimTarget(args, claimId) {
|
|
200
|
+
return {
|
|
201
|
+
claimId,
|
|
202
|
+
subjectType: "public-record.entity",
|
|
203
|
+
subjectId: args.proposal.publicRecordId,
|
|
204
|
+
surface: "public-directory.profile",
|
|
205
|
+
claimType: args.role === "current" ? "public-data.field" : "public-data.field-candidate",
|
|
206
|
+
fieldOrBehavior: args.field,
|
|
207
|
+
impactLevel: "medium",
|
|
208
|
+
evidenceType: "source_excerpt",
|
|
209
|
+
evidenceMethod: args.role === "current" ? "attestation" : "extraction",
|
|
210
|
+
collectedBy: args.extractor,
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
function candidateProjection(args, candidateIdValue, extractionId, claimId) {
|
|
214
|
+
return {
|
|
215
|
+
rawSourceId: args.sourceId,
|
|
216
|
+
extractionId,
|
|
217
|
+
candidateSetId: `public-directory:candidates:${args.proposal.publicRecordId}:${args.field}:${args.proposal.id}`,
|
|
218
|
+
candidateId: candidateIdValue,
|
|
219
|
+
reviewOutcomeId: rejectedCurrentReviewOutcomeId(args),
|
|
220
|
+
claimId,
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
function candidateProducerMetadata(args) {
|
|
224
|
+
const selectedWhenRejected = rejectedCurrentReviewOutcomeId(args) ? { selectedWhenRejected: true } : {};
|
|
225
|
+
return {
|
|
226
|
+
proposalId: args.proposal.id,
|
|
227
|
+
proposalStatus: args.proposal.status,
|
|
228
|
+
previousValue: args.diff.old,
|
|
229
|
+
diffMode: args.diff.mode,
|
|
230
|
+
...selectedWhenRejected,
|
|
231
|
+
sourceAuthority: {
|
|
232
|
+
authorityClass: "public-directory-listing",
|
|
233
|
+
declaredBy: args.role === "current" ? "Downstream reviewed record" : "Downstream extraction pipeline",
|
|
234
|
+
scope: `${args.field} field on ${args.proposal.publicRecordId}`,
|
|
235
|
+
},
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
function candidateClaimId(args) {
|
|
239
|
+
return args.role === "current"
|
|
240
|
+
? `public-directory.${args.proposal.publicRecordId}.${args.field}.current`
|
|
241
|
+
: `public-directory.${args.proposal.publicRecordId}.${args.field}.${args.proposal.id}.proposed`;
|
|
242
|
+
}
|
|
243
|
+
function rejectedCurrentReviewOutcomeId(args) {
|
|
244
|
+
return args.role === "current" && args.proposal.status === "REJECTED"
|
|
245
|
+
? `public-directory:review:${args.proposal.publicRecordId}:${args.field}:${args.proposal.id}:keep-current`
|
|
246
|
+
: undefined;
|
|
247
|
+
}
|
|
248
|
+
function candidateId(field, role) {
|
|
249
|
+
return `public-directory:candidate:${field}:${role}`;
|
|
250
|
+
}
|
|
251
|
+
function downstreamDecisionRationale(status) {
|
|
252
|
+
if (status === "APPROVED") {
|
|
253
|
+
return "Downstream reviewer accepted the proposed value.";
|
|
254
|
+
}
|
|
255
|
+
if (status === "REJECTED") {
|
|
256
|
+
return "Downstream reviewer rejected the proposed value and kept the current value.";
|
|
257
|
+
}
|
|
258
|
+
return "Downstream proposal is awaiting review.";
|
|
259
|
+
}
|
|
260
|
+
function isRecord(value) {
|
|
261
|
+
return typeof value === "object" && value !== null;
|
|
262
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { type ReviewCandidate, type ReviewItem } from "../../src/review-resource.js";
|
|
2
|
+
export type ReviewWorkbenchDecision = "accept-proposed" | "keep-current" | "reject-proposed";
|
|
3
|
+
export type ReviewQueueRowStatus = "pending" | "in-review" | "resolved" | "rejected" | "escalated";
|
|
4
|
+
export interface ReviewWorkbenchState {
|
|
5
|
+
readonly item: ReviewItem;
|
|
6
|
+
readonly note: string;
|
|
7
|
+
readonly decision?: ReviewWorkbenchDecision;
|
|
8
|
+
readonly reviewedAt: string;
|
|
9
|
+
readonly actorId: string;
|
|
10
|
+
}
|
|
11
|
+
export interface ReviewQueueSessionState {
|
|
12
|
+
readonly items: readonly ReviewItem[];
|
|
13
|
+
readonly activeItemName: string;
|
|
14
|
+
readonly notesByItemName: Readonly<Record<string, string>>;
|
|
15
|
+
readonly decisionsByItemName: Readonly<Record<string, ReviewWorkbenchDecision>>;
|
|
16
|
+
readonly reviewedAt: string;
|
|
17
|
+
readonly actorId: string;
|
|
18
|
+
}
|
|
19
|
+
export interface ReviewSessionSummary {
|
|
20
|
+
readonly accepted: number;
|
|
21
|
+
readonly keptCurrent: number;
|
|
22
|
+
readonly rejected: number;
|
|
23
|
+
readonly escalated: number;
|
|
24
|
+
readonly unresolved: number;
|
|
25
|
+
}
|
|
26
|
+
export declare const workbenchDecisionDefinitions: {
|
|
27
|
+
"accept-proposed": {
|
|
28
|
+
label: string;
|
|
29
|
+
effect: string;
|
|
30
|
+
candidateRole: "proposed";
|
|
31
|
+
status: "verified";
|
|
32
|
+
};
|
|
33
|
+
"keep-current": {
|
|
34
|
+
label: string;
|
|
35
|
+
effect: string;
|
|
36
|
+
candidateRole: "current";
|
|
37
|
+
status: "verified";
|
|
38
|
+
};
|
|
39
|
+
"reject-proposed": {
|
|
40
|
+
label: string;
|
|
41
|
+
effect: string;
|
|
42
|
+
candidateRole: "proposed";
|
|
43
|
+
status: "rejected";
|
|
44
|
+
};
|
|
45
|
+
};
|
|
46
|
+
export declare function initialReviewWorkbenchState(item?: ReviewItem): ReviewWorkbenchState;
|
|
47
|
+
export declare function initialReviewQueueSessionState(items?: readonly ReviewItem[]): ReviewQueueSessionState;
|
|
48
|
+
export declare function currentReviewWorkbenchState(session: ReviewQueueSessionState): ReviewWorkbenchState;
|
|
49
|
+
export declare function currentReviewItem(session: ReviewQueueSessionState): ReviewItem;
|
|
50
|
+
export declare function deriveQueueRowStatus(item: ReviewItem, session: ReviewQueueSessionState): ReviewQueueRowStatus;
|
|
51
|
+
export declare function nextUnresolvedItemName(session: ReviewQueueSessionState): string | undefined;
|
|
52
|
+
export declare function reviewSessionSummary(session: ReviewQueueSessionState): ReviewSessionSummary;
|
|
53
|
+
export declare function candidateForDecision(item: ReviewItem, decision: ReviewWorkbenchDecision): ReviewCandidate;
|
|
54
|
+
export declare function selectedCandidateRole(state: ReviewWorkbenchState): ReviewCandidate["role"] | undefined;
|