@kontourai/survey 7.0.0 → 8.1.0
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/LICENSE +201 -0
- package/README.md +2 -1
- package/dist/src/calibration.d.ts +15 -0
- package/dist/src/calibration.js +11 -0
- package/dist/src/canonical-reviewed-trust-input.d.ts +10 -0
- package/dist/src/canonical-reviewed-trust-input.js +15 -0
- package/dist/src/console/review-console-server.d.ts +20 -0
- package/dist/src/console/review-console-server.js +75 -4
- package/dist/src/decision-carry-forward.d.ts +157 -0
- package/dist/src/decision-carry-forward.js +295 -0
- package/dist/src/extraction-envelope.d.ts +4 -3
- package/dist/src/extraction-envelope.js +3 -3
- package/dist/src/field-states.d.ts +100 -0
- package/dist/src/field-states.js +169 -0
- package/dist/src/index.d.ts +3 -1
- package/dist/src/index.js +2 -0
- package/dist/src/mcp/review-mcp.js +44 -13
- package/dist/src/review-resource.d.ts +29 -0
- package/dist/src/review-session-file.d.ts +6 -0
- package/dist/src/review-session-file.js +2 -0
- package/dist/src/review-workbench/audit-sample.d.ts +32 -0
- package/dist/src/review-workbench/audit-sample.js +53 -0
- package/dist/src/review-workbench/extraction-inspector.d.ts +1 -1
- package/dist/src/review-workbench/extraction-inspector.js +1 -1
- package/dist/src/review-workbench/field-state-view.d.ts +15 -0
- package/dist/src/review-workbench/field-state-view.js +84 -0
- package/dist/src/review-workbench/review-queue-session.d.ts +46 -1
- package/dist/src/review-workbench/review-queue-session.js +74 -3
- package/dist/src/review-workbench/review-session-replay.d.ts +1 -1
- package/dist/src/review-workbench/review-session-replay.js +15 -1
- package/dist/src/review-workbench/review-workbench-css.generated.d.ts +9 -0
- package/dist/src/review-workbench/review-workbench-css.generated.js +453 -43
- package/dist/src/review-workbench/review-workbench-element.js +68 -212
- package/dist/src/review-workbench/review-workbench.css +386 -42
- package/dist/src/review-workbench/review-workbench.d.ts +13 -1
- package/dist/src/review-workbench/review-workbench.js +41 -15
- package/dist/src/review-workbench/review-workbench.standalone.css +37 -23
- package/dist/src/review-workbench/vendor/kontourai-ui/tokens/themes.css +189 -10
- package/dist/src/review-workbench/vendor/kontourai-ui/tokens/tokens.css +156 -5
- package/package.json +13 -8
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { foldCandidateVerifications } from "./candidate-verification.js";
|
|
2
|
+
import { decisionReference, decisionResolution, validateDecisionCarryForward, validateDecisionSupersession, } from "./decision-carry-forward.js";
|
|
3
|
+
import { buildReviewItemsFromExtractionEnvelopeImport, excerptMismatchProposalIndices, validateExtractionEnvelopeImport, } from "./extraction-envelope.js";
|
|
4
|
+
import { canonicalJson } from "./review-workbench/canonical.js";
|
|
5
|
+
/**
|
|
6
|
+
* Per-field content and lifecycle states (#294), derived from records Survey
|
|
7
|
+
* holds and never stored as labels.
|
|
8
|
+
*
|
|
9
|
+
* Only states with a producer ship. `not_found` (the source was read in full
|
|
10
|
+
* and holds no value for the field) needs per-field chunk coverage and a
|
|
11
|
+
* per-field dropped-proposal count from the extractor, which no producer
|
|
12
|
+
* reports yet, so a field with no candidate in a complete run has no content
|
|
13
|
+
* state at all. Absence of a state never means verified.
|
|
14
|
+
*/
|
|
15
|
+
/** The claim-metadata key under which the Surface projection carries a field's states. */
|
|
16
|
+
export const FIELD_STATE_METADATA_KEY = "survey.kontourai.io/field-state";
|
|
17
|
+
/**
|
|
18
|
+
* Derives the content and lifecycle state of every field in the given
|
|
19
|
+
* imports. Pure: reads only its input. Each state appears only when the input
|
|
20
|
+
* that produces it is present: no verifications, no `unsupported`; no
|
|
21
|
+
* supersessions, no `superseded`.
|
|
22
|
+
*/
|
|
23
|
+
export function deriveFieldStates(input) {
|
|
24
|
+
const decisionsByItem = new Map();
|
|
25
|
+
for (const decision of input.decisions ?? []) {
|
|
26
|
+
const name = decision.spec.reviewItemName;
|
|
27
|
+
if (decisionsByItem.has(name))
|
|
28
|
+
throw new Error(`ReviewItem ${name} has more than one decision; field states read one decision per item.`);
|
|
29
|
+
decisionsByItem.set(name, decision);
|
|
30
|
+
}
|
|
31
|
+
const carriedByItem = new Map();
|
|
32
|
+
for (const raw of input.carryForwards ?? []) {
|
|
33
|
+
const record = validateDecisionCarryForward(raw);
|
|
34
|
+
if (carriedByItem.has(record.reviewItemName))
|
|
35
|
+
throw new Error(`ReviewItem ${record.reviewItemName} has more than one carried-forward decision.`);
|
|
36
|
+
carriedByItem.set(record.reviewItemName, record);
|
|
37
|
+
}
|
|
38
|
+
const supersededDigests = new Set((input.supersessions ?? []).map((raw) => validateDecisionSupersession(raw).priorDecision.digest));
|
|
39
|
+
const verifications = input.verifications;
|
|
40
|
+
const states = [];
|
|
41
|
+
for (const entry of input.imports) {
|
|
42
|
+
const record = validateExtractionEnvelopeImport(entry.record);
|
|
43
|
+
const importName = record.metadata.name;
|
|
44
|
+
const outcome = record.spec.envelope.result.outcome.status;
|
|
45
|
+
const incompleteRun = outcome === "partial" || outcome === "failure";
|
|
46
|
+
const unresolvedImport = record.status.state === "unresolved";
|
|
47
|
+
const baseSignals = (excluded) => ({
|
|
48
|
+
...(incompleteRun ? { incompleteRun: true } : {}),
|
|
49
|
+
...(excluded > 0 ? { excludedProposals: excluded } : {}),
|
|
50
|
+
...(unresolvedImport ? { unresolvedImport: true } : {}),
|
|
51
|
+
});
|
|
52
|
+
const excludedBySlot = new Map();
|
|
53
|
+
for (const index of excerptMismatchProposalIndices(record)) {
|
|
54
|
+
const key = slotKey(proposalSlot(record, index));
|
|
55
|
+
excludedBySlot.set(key, (excludedBySlot.get(key) ?? 0) + 1);
|
|
56
|
+
}
|
|
57
|
+
const seen = new Set();
|
|
58
|
+
for (const item of buildReviewItemsFromExtractionEnvelopeImport(record)) {
|
|
59
|
+
const slot = itemSlot(record, item);
|
|
60
|
+
const key = slotKey(slot);
|
|
61
|
+
seen.add(key);
|
|
62
|
+
const lifecycle = lifecycleFor(item, decisionsByItem, carriedByItem, supersededDigests);
|
|
63
|
+
states.push({
|
|
64
|
+
importName,
|
|
65
|
+
slot,
|
|
66
|
+
reviewItemName: item.metadata.name,
|
|
67
|
+
content: contentFor(item, verifications),
|
|
68
|
+
...lifecycle,
|
|
69
|
+
signals: baseSignals(excludedBySlot.get(key) ?? 0),
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
// A slot whose every proposal was excluded has no item; it still appears,
|
|
73
|
+
// so a dropped proposal never vanishes from the field's record.
|
|
74
|
+
for (const [key, count] of excludedBySlot) {
|
|
75
|
+
if (seen.has(key))
|
|
76
|
+
continue;
|
|
77
|
+
seen.add(key);
|
|
78
|
+
const index = [...excerptMismatchProposalIndices(record)].find((proposalIndex) => slotKey(proposalSlot(record, proposalIndex)) === key);
|
|
79
|
+
// Proposals were made, so the field was read: `excluded`, never `not_covered`.
|
|
80
|
+
states.push({ importName, slot: proposalSlot(record, index), content: "excluded", signals: baseSignals(count) });
|
|
81
|
+
}
|
|
82
|
+
for (const expected of entry.expectedFields ?? []) {
|
|
83
|
+
const slot = normalizeSlot(expected);
|
|
84
|
+
const key = slotKey(slot);
|
|
85
|
+
if (seen.has(key))
|
|
86
|
+
continue;
|
|
87
|
+
seen.add(key);
|
|
88
|
+
states.push({ importName, slot, ...(incompleteRun ? { content: "not_covered" } : {}), signals: baseSignals(0) });
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return states;
|
|
92
|
+
}
|
|
93
|
+
/** The lifecycle a workbench decision gives its item, as `deriveFieldStates` reads it. */
|
|
94
|
+
export function lifecycleForDecision(decision) {
|
|
95
|
+
if (!decision)
|
|
96
|
+
return "pending";
|
|
97
|
+
return decisionResolution(decision) ?? "pending";
|
|
98
|
+
}
|
|
99
|
+
/** The claim metadata the Surface projection carries for one field state. */
|
|
100
|
+
export function fieldStateClaimMetadata(state) {
|
|
101
|
+
return {
|
|
102
|
+
schemaVersion: 1,
|
|
103
|
+
...(state.content !== undefined ? { content: state.content } : {}),
|
|
104
|
+
...(state.lifecycle !== undefined ? { lifecycle: state.lifecycle } : {}),
|
|
105
|
+
...(state.decisionBasis !== undefined ? { decisionBasis: state.decisionBasis } : {}),
|
|
106
|
+
...(Object.keys(state.signals).length ? { signals: { ...state.signals } } : {}),
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
function lifecycleFor(item, decisionsByItem, carriedByItem, supersededDigests) {
|
|
110
|
+
const decision = decisionsByItem.get(item.metadata.name);
|
|
111
|
+
if (decision) {
|
|
112
|
+
const superseded = supersededDigests.has(decisionReference(decision).digest);
|
|
113
|
+
return { lifecycle: superseded ? "superseded" : lifecycleForDecision(decision), decisionName: decision.metadata.name, decisionBasis: "affirmed" };
|
|
114
|
+
}
|
|
115
|
+
const carried = carriedByItem.get(item.metadata.name);
|
|
116
|
+
if (carried) {
|
|
117
|
+
if (carried.candidateId !== undefined && !item.spec.candidates.some((candidate) => candidate.id === carried.candidateId)) {
|
|
118
|
+
throw new Error(`Carried-forward decision for ${item.metadata.name} names candidate ${carried.candidateId}, which the item does not hold.`);
|
|
119
|
+
}
|
|
120
|
+
const superseded = supersededDigests.has(carried.priorDecision.digest);
|
|
121
|
+
return { lifecycle: superseded ? "superseded" : carried.resolution, decisionName: carried.priorDecision.name, decisionBasis: "carried-forward" };
|
|
122
|
+
}
|
|
123
|
+
return { lifecycle: "pending" };
|
|
124
|
+
}
|
|
125
|
+
function contentFor(item, verifications) {
|
|
126
|
+
if (verifications !== undefined && item.spec.candidates.every((candidate) => candidateUnsupported(candidate, verifications)))
|
|
127
|
+
return "unsupported";
|
|
128
|
+
return item.spec.candidates.length > 1 ? "conflicting" : "value";
|
|
129
|
+
}
|
|
130
|
+
/** A candidate with a contradicted or not-addressed record for its current value, and no supported one. */
|
|
131
|
+
function candidateUnsupported(candidate, verifications) {
|
|
132
|
+
const { applicable } = foldCandidateVerifications({ candidateId: candidate.id, value: candidate.value }, verifications);
|
|
133
|
+
const verdicts = applicable.filter((record) => record.result !== "abstain");
|
|
134
|
+
return verdicts.length > 0 && verdicts.every((record) => record.result === "contradicted" || record.result === "not-addressed");
|
|
135
|
+
}
|
|
136
|
+
function itemSlot(record, item) {
|
|
137
|
+
const binding = item.metadata.producer?.["survey.kontourai.io/extraction-envelope"];
|
|
138
|
+
const first = binding?.proposalIndices?.[0];
|
|
139
|
+
if (first === undefined)
|
|
140
|
+
throw new Error(`ReviewItem ${item.metadata.name} carries no proposal binding.`);
|
|
141
|
+
return proposalSlot(record, first);
|
|
142
|
+
}
|
|
143
|
+
function proposalSlot(record, index) {
|
|
144
|
+
const target = record.spec.claimTargets[index];
|
|
145
|
+
const proposal = record.spec.envelope.result.proposals[index];
|
|
146
|
+
return normalizeSlot({
|
|
147
|
+
subjectType: target.subjectType, subjectId: target.subjectId, facet: target.facet, claimType: target.claimType,
|
|
148
|
+
fieldOrBehavior: target.fieldOrBehavior, ...(target.claimId !== undefined ? { claimId: target.claimId } : {}),
|
|
149
|
+
...(proposal.pathIndices !== undefined ? { pathIndices: proposal.pathIndices } : {}),
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
function normalizeSlot(slot) {
|
|
153
|
+
for (const key of ["subjectType", "subjectId", "facet", "claimType", "fieldOrBehavior"]) {
|
|
154
|
+
if (typeof slot[key] !== "string" || slot[key].trim() === "")
|
|
155
|
+
throw new Error(`Field slot ${key} must be a non-empty string.`);
|
|
156
|
+
}
|
|
157
|
+
return {
|
|
158
|
+
subjectType: slot.subjectType, subjectId: slot.subjectId, facet: slot.facet, claimType: slot.claimType, fieldOrBehavior: slot.fieldOrBehavior,
|
|
159
|
+
...(slot.claimId !== undefined ? { claimId: slot.claimId } : {}),
|
|
160
|
+
...(slot.pathIndices !== undefined ? { pathIndices: [...slot.pathIndices] } : {}),
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
/** The same identity the importer groups a claim slot by. */
|
|
164
|
+
function slotKey(slot) {
|
|
165
|
+
return canonicalJson({
|
|
166
|
+
subjectType: slot.subjectType, subjectId: slot.subjectId, facet: slot.facet, claimType: slot.claimType,
|
|
167
|
+
fieldOrBehavior: slot.fieldOrBehavior, claimId: slot.claimId ?? null, pathIndices: slot.pathIndices ?? null,
|
|
168
|
+
});
|
|
169
|
+
}
|
package/dist/src/index.d.ts
CHANGED
|
@@ -9,7 +9,7 @@ export { resolvePortablePdfRegion } from "./pdf-layout.js";
|
|
|
9
9
|
export type { PortablePdfBoundingBox, PortablePdfLayout, PortablePdfPageGeometry, PortablePdfRegionContext, PortablePdfTable, PortablePdfTableCell, PortablePdfTextElement, PortablePdfTextRange, } from "./pdf-layout.js";
|
|
10
10
|
export type { ExtractionAlignmentState, ArtifactUnavailableCode, BuiltExtractionInspectorCandidate, BuiltExtractionInspectorModel, ExtractionInspectorCandidate, ExtractionInspectorEntry, ExtractionInspectorExportOptions, ExtractionInspectorFilters, ExtractionInspectorInput, ExtractionInspectorModel, ExtractionInspectorSource, ResolvedExtractionArtifact, } from "./review-workbench/extraction-inspector.js";
|
|
11
11
|
export type { ExtractionEnvelopeImport, ExtractionEnvelopeImportDiagnostic, ExtractionEnvelopeImportOptions, ExtractionEnvelopeImportProvenance, ExtractionEnvelopeImportResult, ExtractionEnvelopeResolutionIdentity, PortableExtractionCoverageEntry, PortableExtractionEvidenceMatch, PortableExtractionPartialReason, PortableExtractionOccurrence, PortableExtractionProducedBy, PortableExtractionProposal, PortableExtractionResultEnvelope, PortablePreparedArtifactState, } from "./extraction-envelope.js";
|
|
12
|
-
export type { CandidateRole, ClaimTargetHint, ProducerPolicy, ExtractionReference, ResourceEnvelope, ResourceMetadata, ReviewActor, ReviewCandidate, ReviewDecision, ReviewDecisionMode, ReviewDecisionSpec, ReviewDecisionStatus, ReviewItem, ReviewItemSpec, ReviewItemStatus, ReviewLocator, ReviewResource, ReviewResourceApiVersion, ReviewResourceKind, ReviewSession, ReviewSessionEvent, ReviewSessionEventSpec, ReviewSessionEventStatus, ReviewSessionEventType, ReviewSessionSpec, ReviewSessionStatus, ReviewValueDescriptor, ReviewValueType, SourceReference, SurveyRecordProjectionHint, } from "./review-resource.js";
|
|
12
|
+
export type { CandidateRole, ClaimTargetHint, ProducerPolicy, ExtractionReference, ResourceEnvelope, ResourceMetadata, ReviewActor, ReviewCandidate, ReviewDecision, ReviewDecisionMode, ReviewDecisionSpec, ReviewSessionPresentation, ReviewSessionSampling, ReviewDecisionStatus, ReviewItem, ReviewItemSpec, ReviewItemStatus, ReviewLocator, ReviewResource, ReviewResourceApiVersion, ReviewResourceKind, ReviewSession, ReviewSessionEvent, ReviewSessionEventSpec, ReviewSessionEventStatus, ReviewSessionEventType, ReviewSessionSpec, ReviewSessionStatus, ReviewValueDescriptor, ReviewValueType, SourceReference, SurveyRecordProjectionHint, } from "./review-resource.js";
|
|
13
13
|
export { toSurfaceReviewedExtractionDecision, toSurfaceReviewedExtractionImport, toSurfaceReviewedExtractionItem, } from "./surface-reviewed-extraction.js";
|
|
14
14
|
export { candidateReviewRecord, candidateSetStatusFor, SurveyInputBuilder } from "./builder.js";
|
|
15
15
|
export type { CandidateReviewRecordInput, SurveyClaimRecord, SurveyInputBuilderArgs, SurveyObservationInput, } from "./builder.js";
|
|
@@ -58,3 +58,5 @@ export { deriveOversightMetrics, mergeTrustBundleWithOversightMetrics, oversight
|
|
|
58
58
|
export type { AggregateOversightMetrics, DeriveOversightMetricsOptions, OversightMetrics, OversightMetricsClaimsSubject, OversightQualityClaim, ReviewerOversightMetrics, } from "./oversight-metrics.js";
|
|
59
59
|
export { calibrationToClaims, deriveCalibration, mergeTrustBundleWithCalibration, } from "./calibration.js";
|
|
60
60
|
export type { CalibrationBin, CalibrationClaim, CalibrationClaimsSubject, CalibrationGroup, CalibrationInput, CalibrationMetrics, CalibrationSample, DeriveCalibrationOptions, } from "./calibration.js";
|
|
61
|
+
export { deriveFieldStates, fieldStateClaimMetadata, lifecycleForDecision, FIELD_STATE_METADATA_KEY, type DeriveFieldStatesInput, type FieldContentState, type FieldLifecycleState, type FieldSlot, type FieldState, type FieldStateImport, type FieldStateSignals, } from "./field-states.js";
|
|
62
|
+
export { buildDecisionSupersession, decisionCarryForwardKind, decisionReference, decisionResolution, decisionSupersessionKind, defaultCarryForwardPolicy, splitRoundForCarryForward, validateDecisionCarryForward, validateDecisionSupersession, type BuildDecisionSupersessionInput, type CarriedForwardItem, type CarriedResolution, type CarryForwardPolicy, type DecisionBasis, type DecisionCarryForward, type DecisionReference, type DecisionSupersession, type NeedsReviewItem, type NeedsReviewReason, type PriorRoundDecision, type RoundReviewItem, type SplitRoundForCarryForwardInput, type SplitRoundForCarryForwardResult, type SupersessionReason, } from "./decision-carry-forward.js";
|
package/dist/src/index.js
CHANGED
|
@@ -28,3 +28,5 @@ export { confidenceBasisForReview, defineProductVocabulary, stableId } from "./v
|
|
|
28
28
|
export { currentProposedReviewItem } from "./current-proposed-review-item.js";
|
|
29
29
|
export { deriveOversightMetrics, mergeTrustBundleWithOversightMetrics, oversightMetricsToClaims, } from "./oversight-metrics.js";
|
|
30
30
|
export { calibrationToClaims, deriveCalibration, mergeTrustBundleWithCalibration, } from "./calibration.js";
|
|
31
|
+
export { deriveFieldStates, fieldStateClaimMetadata, lifecycleForDecision, FIELD_STATE_METADATA_KEY, } from "./field-states.js";
|
|
32
|
+
export { buildDecisionSupersession, decisionCarryForwardKind, decisionReference, decisionResolution, decisionSupersessionKind, defaultCarryForwardPolicy, splitRoundForCarryForward, validateDecisionCarryForward, validateDecisionSupersession, } from "./decision-carry-forward.js";
|
|
@@ -3,7 +3,7 @@ import { resolve, dirname } from "node:path";
|
|
|
3
3
|
import { McpServer } from "@modelcontextprotocol/server";
|
|
4
4
|
import { serveStdio } from "@modelcontextprotocol/server/stdio";
|
|
5
5
|
import { z } from "zod";
|
|
6
|
-
import { attestReviewQueueExtraction, buildReviewSessionEvents, conflictSelectionIssue, currentReviewItem, decisionSelectsNoCandidate, deriveQueueRowStatus, nextUnresolvedItemName, reviewSessionSummary, workbenchDecisionDefinitions, } from "../review-workbench/review-workbench.js";
|
|
6
|
+
import { attestReviewQueueExtraction, buildReviewSessionEvents, conflictSelectionIssue, currentReviewItem, decisionSelectsNoCandidate, deriveQueueRowStatus, isScoreBlind, nextUnresolvedItemName, reviewSessionSummary, workbenchDecisionDefinitions, } from "../review-workbench/review-workbench.js";
|
|
7
7
|
import { createServerReviewSessionRecord, currentSessionState, deriveServerReviewSessionApplyResult, } from "../review-workbench/server-review-session.js";
|
|
8
8
|
import { buildReviewItemPresentation, candidateVerificationNotes, excludedProposalsSentence, excludedProposalsUnreadableSentence } from "../review-workbench/review-presentation.js";
|
|
9
9
|
import { appendReviewSessionEvents, readReviewSessionFile, storedReviewSessionName, updateReviewSessionFile, } from "../review-session-file.js";
|
|
@@ -41,6 +41,14 @@ function attestedQueue(file) {
|
|
|
41
41
|
function attestationLines(attestation) {
|
|
42
42
|
return attestation.state === "unverified" ? [attestation.message, ``] : [];
|
|
43
43
|
}
|
|
44
|
+
/**
|
|
45
|
+
* A score-blind session shows no confidence and no verifier result on any
|
|
46
|
+
* surface (text, data or card), so the decision is made without them.
|
|
47
|
+
*/
|
|
48
|
+
const SCORE_BLIND_NOTICE = "Score-blind review: confidence and verifier results are hidden for this whole session. Decide from the source and its excerpt.";
|
|
49
|
+
function scoreBlindLines(state) {
|
|
50
|
+
return isScoreBlind(state) ? [`${state.sampling?.kind === "random-audit" ? "Random audit sample. " : ""}${SCORE_BLIND_NOTICE}`, ``] : [];
|
|
51
|
+
}
|
|
44
52
|
// ---- Queue helpers -------------------------------------------------------
|
|
45
53
|
function queueSummaryText(snapshot, events, attestation) {
|
|
46
54
|
const current = currentSessionState(snapshot, events);
|
|
@@ -57,6 +65,7 @@ function queueSummaryText(snapshot, events, attestation) {
|
|
|
57
65
|
});
|
|
58
66
|
return [
|
|
59
67
|
...attestationLines(attestation),
|
|
68
|
+
...scoreBlindLines(current),
|
|
60
69
|
`Review queue: ${resolved}/${total} resolved`,
|
|
61
70
|
`Active item: ${activeItem.metadata.name} (${activeItem.spec.target})`,
|
|
62
71
|
...(nextItem ? [`Next unresolved: ${nextItem}`] : ["All items resolved."]),
|
|
@@ -77,8 +86,11 @@ function itemDetailText(item, snapshot, events, attestation) {
|
|
|
77
86
|
const proposedCandidates = item.spec.candidates.filter((c) => c.role === "proposed");
|
|
78
87
|
const valueStr = (v) => typeof v === "string" ? v : JSON.stringify(v);
|
|
79
88
|
const confStr = (c) => c !== undefined ? `${Math.round(c * 100)}%` : "unknown";
|
|
89
|
+
const blind = isScoreBlind(current);
|
|
90
|
+
const confidenceLine = (c) => blind ? [] : [` confidence: ${confStr(c)}`];
|
|
80
91
|
const lines = [
|
|
81
92
|
...attestationLines(attestation),
|
|
93
|
+
...scoreBlindLines(current),
|
|
82
94
|
`Item: ${item.metadata.name}`,
|
|
83
95
|
`Target: ${item.spec.target}`,
|
|
84
96
|
`Status: ${status}`,
|
|
@@ -87,7 +99,7 @@ function itemDetailText(item, snapshot, events, attestation) {
|
|
|
87
99
|
...(note ? [`Note: ${note}`] : []),
|
|
88
100
|
``,
|
|
89
101
|
`Current value: ${valueStr(currentCandidate?.value ?? "(none)")}`,
|
|
90
|
-
|
|
102
|
+
...confidenceLine(currentCandidate?.extraction?.confidence ?? currentCandidate?.confidence),
|
|
91
103
|
` source: ${currentCandidate?.source?.sourceRef ?? "none"}`,
|
|
92
104
|
...(currentCandidate?.locator?.excerpt ? [` excerpt: ${currentCandidate.locator.excerpt}`] : []),
|
|
93
105
|
``,
|
|
@@ -100,12 +112,12 @@ function itemDetailText(item, snapshot, events, attestation) {
|
|
|
100
112
|
...(proposedCandidates.length === 0 ? [`Proposed value: (none)`] : proposedCandidates.flatMap((candidate) => [
|
|
101
113
|
`Proposed value: ${valueStr(candidate.value)}${chosenId === undefined ? "" : candidate.id === chosenId ? " [chosen]" : " [not chosen]"}`,
|
|
102
114
|
...(proposedCandidates.length > 1 ? [` candidateId: ${candidate.id}`] : []),
|
|
103
|
-
|
|
115
|
+
...confidenceLine(candidate.extraction?.confidence ?? candidate.confidence),
|
|
104
116
|
` source: ${candidate.source?.sourceRef ?? "none"}`,
|
|
105
117
|
...(candidate.locator?.excerpt ? [` excerpt: ${candidate.locator.excerpt}`] : []),
|
|
106
118
|
])),
|
|
107
119
|
...extractionImportLines(item),
|
|
108
|
-
...candidateVerificationNotes(item, editedValueFor(item, current)).flatMap((entry) => [``, `Verification: ${entry.sentence}`]),
|
|
120
|
+
...(blind ? [] : candidateVerificationNotes(item, editedValueFor(item, current)).flatMap((entry) => [``, `Verification: ${entry.sentence}`])),
|
|
109
121
|
];
|
|
110
122
|
if (item.spec.rationale) {
|
|
111
123
|
lines.push(``, `Rationale: ${item.spec.rationale}`);
|
|
@@ -113,6 +125,13 @@ function itemDetailText(item, snapshot, events, attestation) {
|
|
|
113
125
|
return lines.join("\n");
|
|
114
126
|
}
|
|
115
127
|
/** The reviewer's edit when the item's decision accepts one; verifier records are read against it. */
|
|
128
|
+
/** The session's presentation and sampling, as a client should read them. */
|
|
129
|
+
function sessionConditions(snapshot) {
|
|
130
|
+
return {
|
|
131
|
+
...(snapshot.presentation !== undefined ? { presentation: snapshot.presentation } : {}),
|
|
132
|
+
...(snapshot.sampling !== undefined ? { sampling: snapshot.sampling } : {}),
|
|
133
|
+
};
|
|
134
|
+
}
|
|
116
135
|
function editedValueFor(item, state) {
|
|
117
136
|
return state.decisionsByItemName[item.metadata.name] === "accept-proposed" ? state.editedValuesByItemName?.[item.metadata.name] : undefined;
|
|
118
137
|
}
|
|
@@ -157,8 +176,10 @@ function buildReviewCardHtml(item, snapshot, events, attestation) {
|
|
|
157
176
|
const status = deriveQueueRowStatus(item, current);
|
|
158
177
|
const valueStr = (v) => typeof v === "string" ? v : JSON.stringify(v, null, 2);
|
|
159
178
|
const confStr = (c) => c !== undefined ? `${Math.round(c * 100)}%` : "—";
|
|
179
|
+
const blind = isScoreBlind(current);
|
|
180
|
+
const confHtml = (c) => blind ? "" : `<div class="conf">confidence ${confStr(c)}</div>`;
|
|
160
181
|
const currentValue = valueStr(currentCandidate?.value ?? "—");
|
|
161
|
-
const currentConf =
|
|
182
|
+
const currentConf = confHtml(currentCandidate?.extraction?.confidence ?? currentCandidate?.confidence);
|
|
162
183
|
const currentSource = currentCandidate?.source?.sourceRef ?? "—";
|
|
163
184
|
const currentExcerpt = currentCandidate?.locator?.excerpt ?? "";
|
|
164
185
|
const proposedCard = (candidate, label) => {
|
|
@@ -174,7 +195,7 @@ function buildReviewCardHtml(item, snapshot, events, attestation) {
|
|
|
174
195
|
return `<div class="card is-proposed"${conflict && candidate ? ` data-candidate-id="${escapeHtml(candidate.id)}"` : ""}>
|
|
175
196
|
<div class="card-label">${escapeHtml(label)}</div>
|
|
176
197
|
<div class="value">${value.includes("\n") ? `<pre>${escapeHtml(value)}</pre>` : escapeHtml(value)}</div>
|
|
177
|
-
|
|
198
|
+
${confHtml(candidate?.extraction?.confidence ?? candidate?.confidence)}
|
|
178
199
|
<div class="source-ref">${escapeHtml(candidate?.source?.sourceRef ?? "—")}</div>
|
|
179
200
|
${excerpt ? `<div class="excerpt">${escapeHtml(excerpt)}</div>` : ""}
|
|
180
201
|
${choice}
|
|
@@ -187,7 +208,7 @@ function buildReviewCardHtml(item, snapshot, events, attestation) {
|
|
|
187
208
|
const itemPresentation = buildReviewItemPresentation(item);
|
|
188
209
|
const excludedNote = excludedProposalsSentence(itemPresentation.excludedProposals);
|
|
189
210
|
const unreadableNote = excludedProposalsUnreadableSentence(itemPresentation.excludedProposalsUnreadable);
|
|
190
|
-
const verificationNotes = candidateVerificationNotes(item, editedValueFor(item, current));
|
|
211
|
+
const verificationNotes = blind ? [] : candidateVerificationNotes(item, editedValueFor(item, current));
|
|
191
212
|
const decisionBadge = decision
|
|
192
213
|
? `<span class="badge badge-${decision === "accept-proposed" || decision === "select-proposed" ? "accept" : decision === "reject-proposed" ? "reject" : "hold"}">${escapeHtml(decision === "select-proposed" ? `Chose 1 of ${proposedCandidates.length} values` : workbenchDecisionDefinitions[decision].label)}</span>`
|
|
193
214
|
: `<span class="badge badge-pending">${escapeHtml(status)}</span>`;
|
|
@@ -207,7 +228,7 @@ function buildReviewCardHtml(item, snapshot, events, attestation) {
|
|
|
207
228
|
--k-line-strong: rgba(150,180,210,0.22);
|
|
208
229
|
--k-text: #eef3f8;
|
|
209
230
|
--k-text-muted: #aebccb;
|
|
210
|
-
--k-text-faint: #
|
|
231
|
+
--k-text-faint: #75889d;
|
|
211
232
|
--k-brand: #5ce0c6;
|
|
212
233
|
--k-positive: #34d399;
|
|
213
234
|
--k-caution: #f3b14b;
|
|
@@ -215,7 +236,7 @@ function buildReviewCardHtml(item, snapshot, events, attestation) {
|
|
|
215
236
|
--k-active: #7aa2ff;
|
|
216
237
|
--k-radius-sm: 9px;
|
|
217
238
|
--k-radius-md: 14px;
|
|
218
|
-
--k-font-ui: "Hanken Grotesk",ui-sans-serif,system-ui,-apple-system,sans-serif;
|
|
239
|
+
--k-font-ui: "Hanken Grotesk",ui-sans-serif,system-ui,-apple-system,"Segoe UI",sans-serif;
|
|
219
240
|
--k-font-mono: "IBM Plex Mono",ui-monospace,SFMono-Regular,Menlo,monospace;
|
|
220
241
|
}
|
|
221
242
|
@media (prefers-color-scheme: light) {
|
|
@@ -228,7 +249,11 @@ function buildReviewCardHtml(item, snapshot, events, attestation) {
|
|
|
228
249
|
--k-line-strong: rgba(36,40,46,0.20);
|
|
229
250
|
--k-text: #202124;
|
|
230
251
|
--k-text-muted: #5b626b;
|
|
231
|
-
--k-text-faint: #
|
|
252
|
+
--k-text-faint: #6a707b;
|
|
253
|
+
/* The dark brand (#5ce0c6) reads at ~1.6:1 on the light canvas; the eyebrow
|
|
254
|
+
and source link use --k-brand as text, so light mode takes @kontourai/ui's
|
|
255
|
+
survey light brand, retinted in 1.18 to clear 4.5:1. */
|
|
256
|
+
--k-brand: #107e6d;
|
|
232
257
|
--k-positive: #168257;
|
|
233
258
|
--k-caution: #8a5a00;
|
|
234
259
|
--k-negative: #c83b3b;
|
|
@@ -276,6 +301,7 @@ h1{font-size:15px;font-weight:700;margin:0 0 4px}
|
|
|
276
301
|
</head>
|
|
277
302
|
<body>
|
|
278
303
|
${attestation.state === "unverified" ? `<p class="notice" id="unverified-queue-note">${escapeHtml(attestation.message)}</p>` : ""}
|
|
304
|
+
${blind ? `<p class="notice" id="score-blind-note">${escapeHtml(scoreBlindLines(current)[0])}</p>` : ""}
|
|
279
305
|
<p class="eyebrow">Survey Review</p>
|
|
280
306
|
<h1>${escapeHtml(item.spec.target)}</h1>
|
|
281
307
|
<div class="meta">
|
|
@@ -289,7 +315,7 @@ ${attestation.state === "unverified" ? `<p class="notice" id="unverified-queue-n
|
|
|
289
315
|
<div class="card">
|
|
290
316
|
<div class="card-label">Current</div>
|
|
291
317
|
<div class="value">${currentValue.includes("\n") ? `<pre>${escapeHtml(currentValue)}</pre>` : escapeHtml(currentValue)}</div>
|
|
292
|
-
|
|
318
|
+
${currentConf}
|
|
293
319
|
<div class="source-ref">${escapeHtml(currentSource)}</div>
|
|
294
320
|
${currentExcerpt ? `<div class="excerpt">${escapeHtml(currentExcerpt)}</div>` : ""}
|
|
295
321
|
</div>
|
|
@@ -376,6 +402,7 @@ async function toolQueue(options) {
|
|
|
376
402
|
const text = queueSummaryText(snapshot, events, attestation);
|
|
377
403
|
const queueData = {
|
|
378
404
|
queueAttestation: attestation.state,
|
|
405
|
+
...sessionConditions(snapshot),
|
|
379
406
|
items: snapshot.items.map((item) => {
|
|
380
407
|
const current = currentSessionState(snapshot, events);
|
|
381
408
|
const selectedCandidateId = current.decisionsByItemName[item.metadata.name] === "select-proposed"
|
|
@@ -413,8 +440,10 @@ async function toolItem(itemName, options) {
|
|
|
413
440
|
const text = itemDetailText(item, snapshot, events, attestation);
|
|
414
441
|
const decision = current.decisionsByItemName[item.metadata.name];
|
|
415
442
|
const selectedCandidateId = decision === "select-proposed" ? current.selectedCandidateIdsByItemName?.[item.metadata.name] : undefined;
|
|
443
|
+
const blind = isScoreBlind(current);
|
|
416
444
|
const itemData = {
|
|
417
445
|
queueAttestation: attestation.state,
|
|
446
|
+
...sessionConditions(snapshot),
|
|
418
447
|
name: item.metadata.name,
|
|
419
448
|
target: item.spec.target,
|
|
420
449
|
status: deriveQueueRowStatus(item, current),
|
|
@@ -431,14 +460,14 @@ async function toolItem(itemName, options) {
|
|
|
431
460
|
};
|
|
432
461
|
})(),
|
|
433
462
|
candidates: (() => {
|
|
434
|
-
const notes = candidateVerificationNotes(item, editedValueFor(item, current));
|
|
463
|
+
const notes = blind ? [] : candidateVerificationNotes(item, editedValueFor(item, current));
|
|
435
464
|
return item.spec.candidates.map((c, index) => {
|
|
436
465
|
const note = notes.find((entry) => entry.candidateIndex === index);
|
|
437
466
|
return {
|
|
438
467
|
id: c.id,
|
|
439
468
|
role: c.role,
|
|
440
469
|
value: c.value,
|
|
441
|
-
confidence: c.extraction?.confidence ?? c.confidence,
|
|
470
|
+
...(blind ? {} : { confidence: c.extraction?.confidence ?? c.confidence }),
|
|
442
471
|
sourceRef: c.source?.sourceRef,
|
|
443
472
|
excerpt: c.locator?.excerpt,
|
|
444
473
|
...(selectedCandidateId !== undefined && c.role === "proposed" ? { chosen: c.id === selectedCandidateId } : {}),
|
|
@@ -466,6 +495,8 @@ async function toolDecide(itemName, mcpDecision, note, attemptEvidenceIds, optio
|
|
|
466
495
|
// Read, validate and write inside the shared session lock so a concurrent
|
|
467
496
|
// decide or console save cannot interleave and drop this decision (#281).
|
|
468
497
|
const { snapshot, sessionWithDecision, newEvents, attestation } = await updateReviewSessionFile(options.sessionPath, (file) => {
|
|
498
|
+
if (file.producerApplication !== undefined)
|
|
499
|
+
throw new DomainError("This review was already sent to its workflow; open a new review session");
|
|
469
500
|
const { snapshot, events } = file;
|
|
470
501
|
const attestation = attestedQueue(file);
|
|
471
502
|
const current = currentSessionState(snapshot, events);
|
|
@@ -207,7 +207,36 @@ export interface ReviewDecisionSpec {
|
|
|
207
207
|
* decisions recorded before choosing between conflicting values existed.
|
|
208
208
|
*/
|
|
209
209
|
unselectedCandidateIds?: string[];
|
|
210
|
+
/**
|
|
211
|
+
* The presentation of the session the decision was made in, copied from the
|
|
212
|
+
* session (never supplied by the reviewer). `{ scoreBlind: true }` means the
|
|
213
|
+
* reviewer saw no confidence and no verifier result before deciding.
|
|
214
|
+
* Absent on decisions from sessions that declare none.
|
|
215
|
+
*/
|
|
216
|
+
presentation?: ReviewSessionPresentation;
|
|
217
|
+
/** How the session's items were chosen, copied from the session. */
|
|
218
|
+
sampling?: ReviewSessionSampling;
|
|
210
219
|
}
|
|
220
|
+
/**
|
|
221
|
+
* How a session presents items to its reviewer, fixed by the session (never
|
|
222
|
+
* by the reviewer). `scoreBlind` hides every proposer confidence and every
|
|
223
|
+
* verifier result on every decision surface for the whole session, so the
|
|
224
|
+
* decision is made without them.
|
|
225
|
+
*/
|
|
226
|
+
export interface ReviewSessionPresentation {
|
|
227
|
+
readonly scoreBlind: boolean;
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* How the session's items were chosen: the ordinary review `queue`, or a
|
|
231
|
+
* seeded `random-audit` draw at `rate` (see `drawRandomAuditSample`).
|
|
232
|
+
*/
|
|
233
|
+
export type ReviewSessionSampling = {
|
|
234
|
+
readonly kind: "queue";
|
|
235
|
+
} | {
|
|
236
|
+
readonly kind: "random-audit";
|
|
237
|
+
readonly rate: number;
|
|
238
|
+
readonly seed: string;
|
|
239
|
+
};
|
|
211
240
|
export interface ReviewDecisionStatus {
|
|
212
241
|
appliedToClaimIds?: string[];
|
|
213
242
|
}
|
|
@@ -21,6 +21,12 @@ export interface ReviewSessionFileContent {
|
|
|
21
21
|
* from an import is presented as an unverified queue.
|
|
22
22
|
*/
|
|
23
23
|
readonly extractionImport?: unknown;
|
|
24
|
+
/** Presentation receipt after a producer consumed this session; further appends refuse. */
|
|
25
|
+
readonly producerApplication?: {
|
|
26
|
+
readonly reference: string;
|
|
27
|
+
readonly summary: string;
|
|
28
|
+
readonly revision: string;
|
|
29
|
+
};
|
|
24
30
|
}
|
|
25
31
|
export interface ReviewSessionFileLockOptions {
|
|
26
32
|
/** How long to wait for a busy lock before failing. Defaults to 10s. */
|
|
@@ -304,6 +304,8 @@ export function storedReviewSessionName(content, fallback = defaultReviewSession
|
|
|
304
304
|
* rewritten, so decision reversals and note changes stay on record.
|
|
305
305
|
*/
|
|
306
306
|
export function appendReviewSessionEvents(content, appended) {
|
|
307
|
+
if (content.producerApplication !== undefined)
|
|
308
|
+
throw new Error("This review was already sent to its workflow; open a new review session");
|
|
307
309
|
const sessionName = storedReviewSessionName(content);
|
|
308
310
|
const start = content.events.length;
|
|
309
311
|
const renumbered = [...appended]
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { ReviewItem } from "../review-resource.js";
|
|
2
|
+
import { type ReviewQueueSessionState } from "./review-queue-session.js";
|
|
3
|
+
export interface RandomAuditSampleOptions {
|
|
4
|
+
/** Inclusion probability for each item, greater than 0 and at most 1. */
|
|
5
|
+
readonly rate: number;
|
|
6
|
+
/** Any non-empty string. The same seed and items always draw the same sample. */
|
|
7
|
+
readonly seed: string;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Draws a seeded random audit sample from `items`, for example the items an
|
|
11
|
+
* auto-accept threshold would have accepted without review.
|
|
12
|
+
*
|
|
13
|
+
* Each item is included on its own when a SHA-256 of `(seed, item name)`,
|
|
14
|
+
* read as a number in [0, 1), is below `rate`. The draw is deterministic, does
|
|
15
|
+
* not depend on the order of `items`, and an item's inclusion does not change
|
|
16
|
+
* when other items are added or removed. Items keep their input order. Item
|
|
17
|
+
* names must be unique.
|
|
18
|
+
*/
|
|
19
|
+
export declare function drawRandomAuditSample(items: readonly ReviewItem[], options: RandomAuditSampleOptions): ReviewItem[];
|
|
20
|
+
/**
|
|
21
|
+
* Draws a random audit sample (see {@link drawRandomAuditSample}) and opens it
|
|
22
|
+
* as a score-blind review session: the session hides every confidence and
|
|
23
|
+
* verifier result, and each decision made in it records
|
|
24
|
+
* `presentation: { scoreBlind: true }` and
|
|
25
|
+
* `sampling: { kind: "random-audit", rate, seed }`.
|
|
26
|
+
*
|
|
27
|
+
* Throws when the draw selects no item, since a session needs one.
|
|
28
|
+
*/
|
|
29
|
+
export declare function openRandomAuditSession(items: readonly ReviewItem[], options: RandomAuditSampleOptions & {
|
|
30
|
+
readonly reviewedAt?: string;
|
|
31
|
+
readonly actorId?: string;
|
|
32
|
+
}): ReviewQueueSessionState;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { sha256Hex } from "../sha256.js";
|
|
2
|
+
import { canonicalJson } from "./canonical.js";
|
|
3
|
+
import { assertAuditRate, assertAuditSeed, initialReviewQueueSessionState, } from "./review-queue-session.js";
|
|
4
|
+
/**
|
|
5
|
+
* Draws a seeded random audit sample from `items`, for example the items an
|
|
6
|
+
* auto-accept threshold would have accepted without review.
|
|
7
|
+
*
|
|
8
|
+
* Each item is included on its own when a SHA-256 of `(seed, item name)`,
|
|
9
|
+
* read as a number in [0, 1), is below `rate`. The draw is deterministic, does
|
|
10
|
+
* not depend on the order of `items`, and an item's inclusion does not change
|
|
11
|
+
* when other items are added or removed. Items keep their input order. Item
|
|
12
|
+
* names must be unique.
|
|
13
|
+
*/
|
|
14
|
+
export function drawRandomAuditSample(items, options) {
|
|
15
|
+
assertAuditRate(options.rate);
|
|
16
|
+
assertAuditSeed(options.seed);
|
|
17
|
+
const names = new Set();
|
|
18
|
+
for (const item of items) {
|
|
19
|
+
if (names.has(item.metadata.name)) {
|
|
20
|
+
throw new Error(`Audit sample items must have unique names; ${item.metadata.name} appears twice.`);
|
|
21
|
+
}
|
|
22
|
+
names.add(item.metadata.name);
|
|
23
|
+
}
|
|
24
|
+
return items.filter((item) => auditDraw(options.seed, item.metadata.name) < options.rate);
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Draws a random audit sample (see {@link drawRandomAuditSample}) and opens it
|
|
28
|
+
* as a score-blind review session: the session hides every confidence and
|
|
29
|
+
* verifier result, and each decision made in it records
|
|
30
|
+
* `presentation: { scoreBlind: true }` and
|
|
31
|
+
* `sampling: { kind: "random-audit", rate, seed }`.
|
|
32
|
+
*
|
|
33
|
+
* Throws when the draw selects no item, since a session needs one.
|
|
34
|
+
*/
|
|
35
|
+
export function openRandomAuditSession(items, options) {
|
|
36
|
+
const sample = drawRandomAuditSample(items, options);
|
|
37
|
+
if (sample.length === 0) {
|
|
38
|
+
throw new Error(`The random audit sample at rate ${options.rate} drew no items from ${items.length}; use a higher rate or more items.`);
|
|
39
|
+
}
|
|
40
|
+
const base = initialReviewQueueSessionState(sample);
|
|
41
|
+
return {
|
|
42
|
+
...base,
|
|
43
|
+
...(options.reviewedAt !== undefined ? { reviewedAt: options.reviewedAt } : {}),
|
|
44
|
+
...(options.actorId !== undefined ? { actorId: options.actorId } : {}),
|
|
45
|
+
presentation: { scoreBlind: true },
|
|
46
|
+
sampling: { kind: "random-audit", rate: options.rate, seed: options.seed },
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
/** A deterministic number in [0, 1) from the first 52 bits of a SHA-256. */
|
|
50
|
+
function auditDraw(seed, itemName) {
|
|
51
|
+
const hex = sha256Hex(canonicalJson(["survey.random-audit.v1", seed, itemName])).slice(0, 13);
|
|
52
|
+
return Number.parseInt(hex, 16) / 2 ** 52;
|
|
53
|
+
}
|
|
@@ -88,7 +88,7 @@ export interface ExtractionInspectorSource {
|
|
|
88
88
|
}>;
|
|
89
89
|
/**
|
|
90
90
|
* Whether the import checked excerpts against the prepared artifact
|
|
91
|
-
* (`status.provenance
|
|
91
|
+
* (`status.provenance`, required on every import record). Always set by
|
|
92
92
|
* {@link buildExtractionInspectorModel}; optional for hand-authored models.
|
|
93
93
|
*/
|
|
94
94
|
importProvenance?: "verified" | "unverified";
|
|
@@ -25,7 +25,7 @@ export function buildExtractionInspectorModel(input) {
|
|
|
25
25
|
if (sourceKeys.has(sourceKey))
|
|
26
26
|
throw new Error("Extraction inspector source identity collision.");
|
|
27
27
|
sourceKeys.add(sourceKey);
|
|
28
|
-
const source = sourceModel(sourceKey, record.metadata.name, prepared, record.status.diagnostics, entry.artifact, envelope.result.ocrDerived, envelope.result.proposals, record.status.provenance
|
|
28
|
+
const source = sourceModel(sourceKey, record.metadata.name, prepared, record.status.diagnostics, entry.artifact, envelope.result.ocrDerived, envelope.result.proposals, record.status.provenance);
|
|
29
29
|
sources.push(source);
|
|
30
30
|
const candidateStart = candidates.length;
|
|
31
31
|
const itemByProposalIndex = new Map();
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { FieldState } from "../field-states.js";
|
|
2
|
+
import { type ReviewQueueSessionState } from "./review-queue-session.js";
|
|
3
|
+
/**
|
|
4
|
+
* The field-state panel: one row per field the extraction was asked for or
|
|
5
|
+
* proposed, with its derived content state, its lifecycle, and its signals.
|
|
6
|
+
* Fields with no review card (not read, only excluded proposals, no proposal)
|
|
7
|
+
* are listed too, so a run that lost content never reads as complete.
|
|
8
|
+
*
|
|
9
|
+
* The lifecycle of a field in the queue is read from the live session, so it
|
|
10
|
+
* follows the reviewer's decisions. Only what the session cannot know comes
|
|
11
|
+
* from the derived states: a decision carried forward from an earlier round,
|
|
12
|
+
* and a superseded one. In a score-blind session a content state derived from
|
|
13
|
+
* verifier records is shown as the state its candidates alone give.
|
|
14
|
+
*/
|
|
15
|
+
export declare function renderFieldStatesHtml(states: readonly FieldState[], session: ReviewQueueSessionState): string;
|