@kontourai/survey 0.4.3 → 0.4.4

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.
Files changed (32) hide show
  1. package/README.md +211 -9
  2. package/dist/examples/review-workbench/downstream-public-directory-adapter.d.ts +3 -0
  3. package/dist/examples/review-workbench/downstream-public-directory-adapter.js +262 -0
  4. package/dist/examples/review-workbench/review-queue-session.d.ts +54 -0
  5. package/dist/examples/review-workbench/review-queue-session.js +122 -0
  6. package/dist/examples/review-workbench/review-surface-preview.d.ts +55 -0
  7. package/dist/examples/review-workbench/review-surface-preview.js +104 -0
  8. package/dist/examples/review-workbench/review-workbench-data.d.ts +261 -0
  9. package/dist/examples/review-workbench/review-workbench-data.js +199 -0
  10. package/dist/examples/review-workbench/review-workbench.d.ts +7 -0
  11. package/dist/examples/review-workbench/review-workbench.js +488 -0
  12. package/dist/fixtures/downstream-public-directory-proposal.d.ts +100 -0
  13. package/dist/fixtures/downstream-public-directory-proposal.js +59 -0
  14. package/dist/fixtures/public-directory-review-resource.d.ts +156 -0
  15. package/dist/fixtures/public-directory-review-resource.js +157 -0
  16. package/dist/fixtures/regulated-document-review-resource.d.ts +121 -0
  17. package/dist/fixtures/regulated-document-review-resource.js +131 -0
  18. package/dist/src/builder.d.ts +4 -1
  19. package/dist/src/builder.js +7 -0
  20. package/dist/src/index.d.ts +8 -4
  21. package/dist/src/index.js +4 -2
  22. package/dist/src/learning-projections.d.ts +19 -0
  23. package/dist/src/learning-projections.js +77 -0
  24. package/dist/src/raw-source.d.ts +15 -0
  25. package/dist/src/raw-source.js +22 -0
  26. package/dist/src/review-proof.d.ts +28 -0
  27. package/dist/src/review-proof.js +58 -1
  28. package/dist/src/review-resource.d.ts +111 -0
  29. package/dist/src/review-resource.js +1 -0
  30. package/dist/src/to-surface.js +226 -13
  31. package/dist/src/types.d.ts +17 -1
  32. 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, console projections, and downstream transparency.
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 `manualEntrySource`. Producer-provided `id` values are preserved; otherwise
117
- Survey derives a stable id from source kind and `sourceRef`. Bare checksum
118
- values are normalized to `sha256:<value>`, while already-prefixed checksum
119
- values are preserved. Producer metadata is copied through to Surface evidence.
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 listing is
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 product facts. The vertical product owns that domain logic.
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 to the Surface verification event `notes`
670
- so the reviewer chain sees the signal without having to read into the rationale.
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,32 @@ 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 currently
875
+ emits `learning.comfort-zone` from structured `ReviewOutcome.withinComfortZone
876
+ === false` data and `learning.escalation` from unresolved `EscalationRecord`s,
877
+ including unattached records that producer tooling can route but Surface cannot
878
+ attach to a claim event.
879
+
880
+ These projections are producer/review workflow and evaluation signals. They are
881
+ not claims about truth or veracity, not Surface claim status, not evidence, and
882
+ not verification events. Calling `buildSurveyLearningProjections` does not alter
883
+ `buildSurveyTrustInput`, trust status derivation, or escalation event projection.
884
+
683
885
  ## Product Boundary
684
886
 
685
887
  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;