@kontourai/survey 5.0.0 → 6.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/dist/src/candidate-verification.d.ts +175 -0
- package/dist/src/candidate-verification.js +334 -0
- package/dist/src/extraction-envelope.d.ts +66 -0
- package/dist/src/extraction-envelope.js +176 -7
- package/dist/src/index.d.ts +5 -3
- package/dist/src/index.js +2 -1
- package/dist/src/mcp/review-mcp.js +42 -8
- package/dist/src/review-resource.d.ts +9 -0
- package/dist/src/review-workbench/audit-rows.d.ts +1 -1
- package/dist/src/review-workbench/audit-rows.js +1 -0
- package/dist/src/review-workbench/extraction-inspector.d.ts +18 -14
- package/dist/src/review-workbench/extraction-inspector.js +29 -35
- package/dist/src/review-workbench/review-presentation.d.ts +61 -0
- package/dist/src/review-workbench/review-presentation.js +108 -0
- package/dist/src/review-workbench/review-workbench-css.generated.js +10 -2
- package/dist/src/review-workbench/review-workbench.css +10 -2
- package/dist/src/review-workbench/review-workbench.js +55 -1
- package/dist/src/review-workbench/review-workbench.standalone.css +10 -2
- package/package.json +1 -1
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type CandidateVerificationStatus, type SupportAbstainReason, type SupportVerificationMethod, type SupportVerificationResult } from "../candidate-verification.js";
|
|
1
2
|
import { type ReviewCandidate, type ReviewItem } from "../review-resource.js";
|
|
2
3
|
import type { InterpretationAnswerImpact, InterpretationReadingKind } from "../types.js";
|
|
3
4
|
import { type ReviewWorkbenchResult } from "./review-workbench.js";
|
|
@@ -50,6 +51,25 @@ export interface ReviewItemPresentation {
|
|
|
50
51
|
readonly reviewItemLink?: ReviewPresentationLink;
|
|
51
52
|
readonly traceRefs: readonly ReviewTraceRef[];
|
|
52
53
|
readonly candidates: readonly ReviewCandidatePresentation[];
|
|
54
|
+
/**
|
|
55
|
+
* Proposals for this claim that an envelope import left out because the
|
|
56
|
+
* source text at their cited span is not their excerpt. Unverifiable, not
|
|
57
|
+
* disproven: every decision surface shows them. Empty for other items.
|
|
58
|
+
*/
|
|
59
|
+
readonly excludedProposals: readonly ExcludedProposalPresentation[];
|
|
60
|
+
/**
|
|
61
|
+
* For an envelope-imported item, whether its import checked excerpts
|
|
62
|
+
* against the prepared artifact. Absent for other items, and for an item
|
|
63
|
+
* whose envelope binding is missing, which cannot claim either.
|
|
64
|
+
*/
|
|
65
|
+
readonly excerptVerification?: "verified" | "unverified";
|
|
66
|
+
}
|
|
67
|
+
export interface ExcludedProposalPresentation {
|
|
68
|
+
readonly proposalIndex: number;
|
|
69
|
+
readonly value: unknown;
|
|
70
|
+
readonly valueText: string;
|
|
71
|
+
readonly locator: string;
|
|
72
|
+
readonly excerpt: string;
|
|
53
73
|
}
|
|
54
74
|
export interface ReviewResultPresentation {
|
|
55
75
|
readonly result: ReviewWorkbenchResult;
|
|
@@ -106,6 +126,47 @@ export interface InterpretationReadingPresentation {
|
|
|
106
126
|
* authored record under a label nothing derived.
|
|
107
127
|
*/
|
|
108
128
|
export declare function buildInterpretationReadingPresentation(source: InterpretationReadingSource): InterpretationReadingPresentation;
|
|
129
|
+
/**
|
|
130
|
+
* One sentence naming every excluded proposal, for surfaces that speak in
|
|
131
|
+
* text (the MCP item, the recorded decision prompt). Undefined when none.
|
|
132
|
+
*/
|
|
133
|
+
export declare function excludedProposalsSentence(excluded: readonly ExcludedProposalPresentation[]): string | undefined;
|
|
134
|
+
export interface CandidateVerificationRecordPresentation {
|
|
135
|
+
readonly recordId: string;
|
|
136
|
+
readonly verifierId: string;
|
|
137
|
+
readonly verifierVersion: string;
|
|
138
|
+
readonly method: SupportVerificationMethod;
|
|
139
|
+
readonly result: SupportVerificationResult;
|
|
140
|
+
readonly abstainReason?: SupportAbstainReason;
|
|
141
|
+
readonly createdAt: string;
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* What the verifier records on one candidate say about the value under
|
|
145
|
+
* review. Only candidates that carry `verifications` get a note, so a
|
|
146
|
+
* candidate no verifier looked at shows nothing rather than a verdict.
|
|
147
|
+
*/
|
|
148
|
+
export interface CandidateVerificationNote {
|
|
149
|
+
/** Position in `item.spec.candidates`; ids are not assumed unique. */
|
|
150
|
+
readonly candidateIndex: number;
|
|
151
|
+
readonly candidateId: string;
|
|
152
|
+
readonly subjectLabel: string;
|
|
153
|
+
/** As {@link foldCandidateVerifications}: `not-evaluated`, `abstained` (only abstentions apply) or `evaluated`. */
|
|
154
|
+
readonly status: CandidateVerificationStatus;
|
|
155
|
+
readonly records: readonly CandidateVerificationRecordPresentation[];
|
|
156
|
+
/** Valid records bound to another value, candidate or input. */
|
|
157
|
+
readonly inapplicableCount: number;
|
|
158
|
+
/** Records that failed validation (edited, inconsistent or malformed); never shown as verdicts. */
|
|
159
|
+
readonly rejectedCount: number;
|
|
160
|
+
readonly sentence: string;
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Verifier notes for every candidate that carries records, read against the
|
|
164
|
+
* value each surface is deciding on: the candidate's own value, or the
|
|
165
|
+
* reviewer's edit when an accept carries one (records on the proposed value do
|
|
166
|
+
* not apply to an edited value). Records are validated here, so an inconsistent
|
|
167
|
+
* record is counted and ignored on every surface alike.
|
|
168
|
+
*/
|
|
169
|
+
export declare function candidateVerificationNotes(item: ReviewItem, editedValue?: unknown): CandidateVerificationNote[];
|
|
109
170
|
export declare function buildReviewItemPresentation(item: ReviewItem, adapter?: ReviewPresentationAdapter): ReviewItemPresentation;
|
|
110
171
|
export declare function buildReviewCandidatePresentation(item: ReviewItem, candidate: ReviewCandidate, adapter?: ReviewPresentationAdapter, targetLabel?: string): ReviewCandidatePresentation;
|
|
111
172
|
export declare function buildReviewResultPresentation(result: ReviewWorkbenchResult, item: ReviewItem | undefined, adapter?: ReviewPresentationAdapter): ReviewResultPresentation;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { foldCandidateVerifications } from "../candidate-verification.js";
|
|
1
2
|
import { findSoleCandidateById } from "../review-resource.js";
|
|
2
3
|
import { formatValue } from "./review-surface-preview.js";
|
|
3
4
|
const INTERPRETATION_KIND_LABELS = {
|
|
@@ -49,6 +50,105 @@ export function buildInterpretationReadingPresentation(source) {
|
|
|
49
50
|
provenanceLabel: "Authored judgment",
|
|
50
51
|
};
|
|
51
52
|
}
|
|
53
|
+
const EXTRACTION_ENVELOPE_PRODUCER = "survey.kontourai.io/extraction-envelope";
|
|
54
|
+
/**
|
|
55
|
+
* The envelope-import binding an item carries, or undefined when it has none.
|
|
56
|
+
* A binding names its import and proposals, and every candidate points back
|
|
57
|
+
* at the same import; anything less cannot vouch for what the import checked.
|
|
58
|
+
*/
|
|
59
|
+
function extractionEnvelopeBinding(item) {
|
|
60
|
+
const metadata = item.metadata?.producer?.[EXTRACTION_ENVELOPE_PRODUCER];
|
|
61
|
+
if (!metadata || typeof metadata !== "object" || Array.isArray(metadata))
|
|
62
|
+
return undefined;
|
|
63
|
+
const binding = metadata;
|
|
64
|
+
if (typeof binding.importName !== "string" || !binding.importName || !Array.isArray(binding.proposalIndices) || binding.proposalIndices.length === 0)
|
|
65
|
+
return undefined;
|
|
66
|
+
const bound = item.spec.candidates.length > 0 && item.spec.candidates.every((candidate) => {
|
|
67
|
+
const own = candidate.producer?.[EXTRACTION_ENVELOPE_PRODUCER];
|
|
68
|
+
return own?.importName === binding.importName;
|
|
69
|
+
});
|
|
70
|
+
return bound ? binding : undefined;
|
|
71
|
+
}
|
|
72
|
+
/** The well-formed `excludedProposals` entries of a bound envelope item; malformed entries are ignored, never rendered. */
|
|
73
|
+
function excludedProposalsOf(item, binding, adapter) {
|
|
74
|
+
if (!binding || !Array.isArray(binding.excludedProposals))
|
|
75
|
+
return [];
|
|
76
|
+
return binding.excludedProposals.flatMap((entry) => {
|
|
77
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry))
|
|
78
|
+
return [];
|
|
79
|
+
const e = entry;
|
|
80
|
+
if (!Number.isSafeInteger(e.proposalIndex) || typeof e.locator !== "string" || typeof e.excerpt !== "string" || !("value" in e))
|
|
81
|
+
return [];
|
|
82
|
+
const proposed = item.spec.candidates.find((candidate) => candidate.role === "proposed");
|
|
83
|
+
const valueText = (proposed ? adapter.summarizeValue?.(e.value, { item, candidate: proposed, value: e.value }) : undefined) ?? formatValue(e.value);
|
|
84
|
+
return [{ proposalIndex: e.proposalIndex, value: e.value, valueText, locator: e.locator, excerpt: e.excerpt }];
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* One sentence naming every excluded proposal, for surfaces that speak in
|
|
89
|
+
* text (the MCP item, the recorded decision prompt). Undefined when none.
|
|
90
|
+
*/
|
|
91
|
+
export function excludedProposalsSentence(excluded) {
|
|
92
|
+
if (excluded.length === 0)
|
|
93
|
+
return undefined;
|
|
94
|
+
const named = excluded.map((entry) => `${entry.valueText} (proposal ${entry.proposalIndex}, ${entry.locator})`).join(", ");
|
|
95
|
+
return `${excluded.length === 1 ? "Another proposed value was" : `${excluded.length} other proposed values were`} excluded at import because the source text at the cited span is not the excerpt: ${named}. Unverifiable is not disproven.`;
|
|
96
|
+
}
|
|
97
|
+
const VERIFICATION_RESULT_TEXT = {
|
|
98
|
+
supported: "supported",
|
|
99
|
+
contradicted: "contradicted",
|
|
100
|
+
"not-addressed": "not addressed by the evidence",
|
|
101
|
+
};
|
|
102
|
+
/**
|
|
103
|
+
* Verifier notes for every candidate that carries records, read against the
|
|
104
|
+
* value each surface is deciding on: the candidate's own value, or the
|
|
105
|
+
* reviewer's edit when an accept carries one (records on the proposed value do
|
|
106
|
+
* not apply to an edited value). Records are validated here, so an inconsistent
|
|
107
|
+
* record is counted and ignored on every surface alike.
|
|
108
|
+
*/
|
|
109
|
+
export function candidateVerificationNotes(item, editedValue) {
|
|
110
|
+
const proposed = item.spec.candidates.filter((candidate) => candidate.role === "proposed");
|
|
111
|
+
return item.spec.candidates.flatMap((candidate, candidateIndex) => {
|
|
112
|
+
if (candidate.verifications === undefined)
|
|
113
|
+
return [];
|
|
114
|
+
const edited = editedValue !== undefined && proposed.length === 1 && candidate === proposed[0];
|
|
115
|
+
const subjectLabel = edited
|
|
116
|
+
? "the edited value"
|
|
117
|
+
: candidate.role === "current"
|
|
118
|
+
? "the current value"
|
|
119
|
+
: candidate.role === "proposed"
|
|
120
|
+
? proposed.length > 1 ? `proposed value ${proposed.indexOf(candidate) + 1} of ${proposed.length}` : "the proposed value"
|
|
121
|
+
: `candidate ${candidate.id}`;
|
|
122
|
+
const records = Array.isArray(candidate.verifications) ? candidate.verifications : [candidate.verifications];
|
|
123
|
+
const fold = foldCandidateVerifications({ candidateId: candidate.id, value: edited ? editedValue : candidate.value }, records);
|
|
124
|
+
const presented = fold.applicable.map((record) => ({
|
|
125
|
+
recordId: record.id,
|
|
126
|
+
verifierId: record.verifier.id,
|
|
127
|
+
verifierVersion: record.verifier.version,
|
|
128
|
+
method: record.method,
|
|
129
|
+
result: record.result,
|
|
130
|
+
...(record.abstainReason ? { abstainReason: record.abstainReason } : {}),
|
|
131
|
+
createdAt: record.createdAt,
|
|
132
|
+
}));
|
|
133
|
+
const said = presented.map((record) => `${record.verifierId} ${record.verifierVersion} (${record.method}) ${record.result === "abstain" ? `abstained: ${record.abstainReason}` : `said ${VERIFICATION_RESULT_TEXT[record.result]}`}`);
|
|
134
|
+
const plural = (count, one, many) => `${count} ${count === 1 ? one : many}`;
|
|
135
|
+
const sentence = [
|
|
136
|
+
said.length ? `Verifier records for ${subjectLabel} (what a verifier said: not proof, not a review decision): ${said.join("; ")}.` : `No verifier record applies to ${subjectLabel}.`,
|
|
137
|
+
...(fold.inapplicable.length ? [`${plural(fold.inapplicable.length, "record was", "records were")} made for a different value or evidence and ${fold.inapplicable.length === 1 ? "does" : "do"} not apply.`] : []),
|
|
138
|
+
...(fold.rejected.length ? [`${plural(fold.rejected.length, "record", "records")} failed validation and ${fold.rejected.length === 1 ? "was" : "were"} ignored.`] : []),
|
|
139
|
+
].join(" ");
|
|
140
|
+
return [{
|
|
141
|
+
candidateIndex,
|
|
142
|
+
candidateId: candidate.id,
|
|
143
|
+
subjectLabel,
|
|
144
|
+
status: fold.status,
|
|
145
|
+
records: presented,
|
|
146
|
+
inapplicableCount: fold.inapplicable.length,
|
|
147
|
+
rejectedCount: fold.rejected.length,
|
|
148
|
+
sentence,
|
|
149
|
+
}];
|
|
150
|
+
});
|
|
151
|
+
}
|
|
52
152
|
export function buildReviewItemPresentation(item, adapter = {}) {
|
|
53
153
|
const context = { item };
|
|
54
154
|
const targetLabel = adapter.labelForTarget?.(item.spec.target, context) ?? humanizeIdentifier(item.spec.target);
|
|
@@ -61,6 +161,14 @@ export function buildReviewItemPresentation(item, adapter = {}) {
|
|
|
61
161
|
reviewItemLink: adapter.linkForReviewItem?.(item, context),
|
|
62
162
|
traceRefs: traceRefsForReviewItem(item, adapter),
|
|
63
163
|
candidates: item.spec.candidates.map((candidate) => buildReviewCandidatePresentation(item, candidate, adapter, targetLabel)),
|
|
164
|
+
...extractionPresentation(item, adapter),
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
function extractionPresentation(item, adapter) {
|
|
168
|
+
const binding = extractionEnvelopeBinding(item);
|
|
169
|
+
return {
|
|
170
|
+
excludedProposals: excludedProposalsOf(item, binding, adapter),
|
|
171
|
+
...(binding ? { excerptVerification: binding.excerptVerification === "verified" ? "verified" : "unverified" } : {}),
|
|
64
172
|
};
|
|
65
173
|
}
|
|
66
174
|
export function buildReviewCandidatePresentation(item, candidate, adapter = {}, targetLabel = adapter.labelForTarget?.(item.spec.target, { item }) ?? humanizeIdentifier(item.spec.target)) {
|
|
@@ -246,9 +246,10 @@ export const REVIEW_WORKBENCH_CSS = `/* Bundled, scoped Survey Review Workbench
|
|
|
246
246
|
.survey-workbench-embed .inspector-heading, .survey-workbench-embed .inspector-layout{ display: grid; grid-template-columns: minmax(15rem, .7fr) minmax(0, 1.3fr); gap: 1rem; }
|
|
247
247
|
.survey-workbench-embed .inspector-heading h2{ margin: 0; }
|
|
248
248
|
.survey-workbench-embed .inspector-posture{ display: flex; flex-direction: column; padding: .75rem; border-radius: var(--k-radius-sm); background: var(--k-positive-wash); }
|
|
249
|
-
|
|
250
|
-
.survey-workbench-embed .inspector-posture.extraction-failed{ background: var(--k-negative-wash); color: var(--k-negative); border: 2px solid currentColor; }
|
|
249
|
+
/* Caution first: an artifact failure on the same source must win the tie and stay negative. */
|
|
251
250
|
.survey-workbench-embed .inspector-posture.extraction-incomplete{ background: var(--k-caution-wash); color: var(--k-caution); border: 2px solid currentColor; }
|
|
251
|
+
.survey-workbench-embed .inspector-posture.digest-mismatch, .survey-workbench-embed .inspector-posture.artifact-unavailable, .survey-workbench-embed .inspector-posture.excerpt-mismatch, .survey-workbench-embed .inspector-posture.proposals-excluded{ background: var(--k-negative-wash); color: var(--k-negative); border: 2px solid currentColor; }
|
|
252
|
+
.survey-workbench-embed .inspector-posture.extraction-failed{ background: var(--k-negative-wash); color: var(--k-negative); border: 2px solid currentColor; }
|
|
252
253
|
.survey-workbench-embed .inspector-filters{ display: flex; flex-wrap: wrap; gap: .6rem; margin: 1rem 0; }
|
|
253
254
|
.survey-workbench-embed .inspector-filters label{ display: grid; gap: .25rem; font-size: .75rem; color: var(--k-text-muted); }
|
|
254
255
|
.survey-workbench-embed .inspector-filters select, .survey-workbench-embed .inspector-filters input{ color: var(--k-text); background: var(--k-sunken); border: 1px solid var(--k-line); padding: .4rem; }
|
|
@@ -834,6 +835,13 @@ export const REVIEW_WORKBENCH_CSS = `/* Bundled, scoped Survey Review Workbench
|
|
|
834
835
|
font-size: 12px;
|
|
835
836
|
}
|
|
836
837
|
|
|
838
|
+
.survey-workbench-embed .excerpt-verification,
|
|
839
|
+
.survey-workbench-embed .support-verification{
|
|
840
|
+
margin: 0;
|
|
841
|
+
font-size: 12px;
|
|
842
|
+
color: var(--k-muted);
|
|
843
|
+
}
|
|
844
|
+
|
|
837
845
|
/* decisions */
|
|
838
846
|
|
|
839
847
|
/* The decision row is the point of the card, so it sits on its own line,
|
|
@@ -243,9 +243,10 @@
|
|
|
243
243
|
.survey-workbench-embed .inspector-heading, .survey-workbench-embed .inspector-layout{ display: grid; grid-template-columns: minmax(15rem, .7fr) minmax(0, 1.3fr); gap: 1rem; }
|
|
244
244
|
.survey-workbench-embed .inspector-heading h2{ margin: 0; }
|
|
245
245
|
.survey-workbench-embed .inspector-posture{ display: flex; flex-direction: column; padding: .75rem; border-radius: var(--k-radius-sm); background: var(--k-positive-wash); }
|
|
246
|
-
|
|
247
|
-
.survey-workbench-embed .inspector-posture.extraction-failed{ background: var(--k-negative-wash); color: var(--k-negative); border: 2px solid currentColor; }
|
|
246
|
+
/* Caution first: an artifact failure on the same source must win the tie and stay negative. */
|
|
248
247
|
.survey-workbench-embed .inspector-posture.extraction-incomplete{ background: var(--k-caution-wash); color: var(--k-caution); border: 2px solid currentColor; }
|
|
248
|
+
.survey-workbench-embed .inspector-posture.digest-mismatch, .survey-workbench-embed .inspector-posture.artifact-unavailable, .survey-workbench-embed .inspector-posture.excerpt-mismatch, .survey-workbench-embed .inspector-posture.proposals-excluded{ background: var(--k-negative-wash); color: var(--k-negative); border: 2px solid currentColor; }
|
|
249
|
+
.survey-workbench-embed .inspector-posture.extraction-failed{ background: var(--k-negative-wash); color: var(--k-negative); border: 2px solid currentColor; }
|
|
249
250
|
.survey-workbench-embed .inspector-filters{ display: flex; flex-wrap: wrap; gap: .6rem; margin: 1rem 0; }
|
|
250
251
|
.survey-workbench-embed .inspector-filters label{ display: grid; gap: .25rem; font-size: .75rem; color: var(--k-text-muted); }
|
|
251
252
|
.survey-workbench-embed .inspector-filters select, .survey-workbench-embed .inspector-filters input{ color: var(--k-text); background: var(--k-sunken); border: 1px solid var(--k-line); padding: .4rem; }
|
|
@@ -831,6 +832,13 @@
|
|
|
831
832
|
font-size: 12px;
|
|
832
833
|
}
|
|
833
834
|
|
|
835
|
+
.survey-workbench-embed .excerpt-verification,
|
|
836
|
+
.survey-workbench-embed .support-verification{
|
|
837
|
+
margin: 0;
|
|
838
|
+
font-size: 12px;
|
|
839
|
+
color: var(--k-muted);
|
|
840
|
+
}
|
|
841
|
+
|
|
834
842
|
/* decisions */
|
|
835
843
|
|
|
836
844
|
/* The decision row is the point of the card, so it sits on its own line,
|
|
@@ -6,7 +6,7 @@ import { buildSurfaceProjectionPreview, formatValue, } from "./review-surface-pr
|
|
|
6
6
|
import { buildReviewCandidatePresentation, buildReviewItemPresentation, } from "./review-presentation.js";
|
|
7
7
|
import { findSoleCandidateById, reviewResourceApiVersion, } from "../review-resource.js";
|
|
8
8
|
import { validateAuthorizing, buildAuthorizedActionAuthorizing } from "../review-authorizing.js";
|
|
9
|
-
import { humanizeIdentifier } from "./review-presentation.js";
|
|
9
|
+
import { candidateVerificationNotes, excludedProposalsSentence, humanizeIdentifier } from "./review-presentation.js";
|
|
10
10
|
import { editedValueFromEditorText, isIsoCalendarDate, parsePlainDecimal } from "./edited-value.js";
|
|
11
11
|
import { createAuditFactTrace, } from "./audit-rows.js";
|
|
12
12
|
export { reviewAuditRowKeys } from "./audit-rows.js";
|
|
@@ -129,6 +129,14 @@ function buildDecisionCardAuthorizing(state) {
|
|
|
129
129
|
* followed by the selected decision label so the block is self-contained.
|
|
130
130
|
*/
|
|
131
131
|
function decisionCardRenderedPrompt(state, targetLabel) {
|
|
132
|
+
// The card shows proposals excluded at import; the recorded prompt must say
|
|
133
|
+
// the reviewer was told about them.
|
|
134
|
+
const excluded = excludedProposalsSentence(buildReviewItemPresentation(state.item).excludedProposals);
|
|
135
|
+
// Likewise the verifier records the card showed, read against the same value.
|
|
136
|
+
const verification = candidateVerificationNotes(state.item, state.decision === "accept-proposed" ? state.editedValue : undefined).map((note) => note.sentence);
|
|
137
|
+
return [decisionCardBasePrompt(state, targetLabel), ...(excluded ? [excluded] : []), ...verification].join(" ");
|
|
138
|
+
}
|
|
139
|
+
function decisionCardBasePrompt(state, targetLabel) {
|
|
132
140
|
const currentCandidate = state.item.spec.candidates.find((c) => c.role === "current");
|
|
133
141
|
const proposedCandidates = state.item.spec.candidates.filter((c) => c.role === "proposed");
|
|
134
142
|
// A conflict card lists every proposed value and offers reject-all or could
|
|
@@ -693,6 +701,8 @@ function renderFieldCard(item, session, presentationAdapter) {
|
|
|
693
701
|
? renderConflictingProposals(item, proposedCandidates, presentationAdapter, presentation.targetLabel)
|
|
694
702
|
: "<p class=\"field-value\">No proposed value is available for this field.</p>"}
|
|
695
703
|
${proposed ? renderProvenanceRow(item, proposed, presentationAdapter) : ""}
|
|
704
|
+
${renderExtractionImportNotes(presentation)}
|
|
705
|
+
${renderVerificationNotes(candidateVerificationNotes(item, decision === "accept-proposed" ? editedValue : undefined))}
|
|
696
706
|
<div class="decide">
|
|
697
707
|
${keepDecision === undefined ? "" : `<button class="btn keep" type="button" data-testid="keep-current" data-item-name="${escapeHtml(item.metadata.name)}">${keepLabel}</button>`}
|
|
698
708
|
${proposed ? `<button class="btn use" type="button" data-testid="use-proposed" data-item-name="${escapeHtml(item.metadata.name)}">Use proposed</button>
|
|
@@ -924,6 +934,49 @@ function renderProvenanceRow(item, proposed, presentationAdapter) {
|
|
|
924
934
|
</div>
|
|
925
935
|
`;
|
|
926
936
|
}
|
|
937
|
+
/**
|
|
938
|
+
* What an envelope import says about this field's evidence: whether its
|
|
939
|
+
* excerpts were checked against the prepared source text, and any rival
|
|
940
|
+
* proposals the import left out because their excerpt was not at their span.
|
|
941
|
+
* Such a proposal is unverifiable, not disproven, so the reviewer sees it
|
|
942
|
+
* before accepting the value that remained. Empty for items from other
|
|
943
|
+
* producers.
|
|
944
|
+
*/
|
|
945
|
+
function renderExtractionImportNotes(presentation) {
|
|
946
|
+
const { excerptVerification: verification, excludedProposals: excluded } = presentation;
|
|
947
|
+
if (verification === undefined && excluded.length === 0)
|
|
948
|
+
return "";
|
|
949
|
+
const excludedHtml = excluded.length === 0 ? "" : `
|
|
950
|
+
<div class="noprov" data-testid="excluded-proposals" role="note">
|
|
951
|
+
${WARNING_SVG}
|
|
952
|
+
<span class="tag">${excluded.length} excluded</span>
|
|
953
|
+
<span>${excluded.length === 1 ? "Another proposal for this field was" : `${excluded.length} other proposals for this field were`} left out because the source text at the cited span is not the excerpt:
|
|
954
|
+
${excluded.map((entry) => `<span data-testid="excluded-proposal" data-proposal-index="${escapeHtml(String(entry.proposalIndex))}"><q>${escapeHtml(entry.valueText)}</q> (proposal ${escapeHtml(String(entry.proposalIndex))}, ${escapeHtml(entry.locator)})</span>`).join(", ")}.
|
|
955
|
+
Unverifiable is not disproven: check the source before deciding.</span>
|
|
956
|
+
</div>`;
|
|
957
|
+
const verificationHtml = verification === undefined ? "" : `
|
|
958
|
+
<p class="excerpt-verification" data-testid="excerpt-verification" data-verified="${verification === "verified" ? "true" : "false"}">${verification === "verified"
|
|
959
|
+
? "Excerpts checked against the prepared source text at import."
|
|
960
|
+
: "Excerpts not checked against the prepared source text at import."}</p>`;
|
|
961
|
+
return `
|
|
962
|
+
<div class="prov import-notes">${excludedHtml}${verificationHtml}
|
|
963
|
+
</div>
|
|
964
|
+
`;
|
|
965
|
+
}
|
|
966
|
+
/**
|
|
967
|
+
* What verifier records say about the value on this card. A record is what a
|
|
968
|
+
* verifier said, not a review decision; records bound to another value and
|
|
969
|
+
* records that failed validation are counted, never shown as verdicts.
|
|
970
|
+
*/
|
|
971
|
+
function renderVerificationNotes(notes) {
|
|
972
|
+
if (notes.length === 0)
|
|
973
|
+
return "";
|
|
974
|
+
return `
|
|
975
|
+
<div class="prov import-notes">${notes.map((note) => `
|
|
976
|
+
<p class="support-verification" data-testid="support-verification" data-candidate-id="${escapeHtml(note.candidateId)}" data-status="${note.status}">${escapeHtml(note.sentence)}</p>`).join("")}
|
|
977
|
+
</div>
|
|
978
|
+
`;
|
|
979
|
+
}
|
|
927
980
|
function renderConfidenceMeter(confidence) {
|
|
928
981
|
const clamped = Math.max(0, Math.min(1, confidence));
|
|
929
982
|
const pct = Math.round(clamped * 100);
|
|
@@ -980,6 +1033,7 @@ function renderAuditDetails(item, current, proposed, session, presentationAdapte
|
|
|
980
1033
|
${proposed?.extraction.model ? fieldItem("model", "Model", proposed.extraction.model) : ""}
|
|
981
1034
|
${proposed ? placementItem(trace, { of: proposed.id, property: "extraction.extractor" }, "extractor", "Extractor", proposed.extraction.extractor ?? "unknown") : ""}
|
|
982
1035
|
${proposed ? fieldItem("extracted-at", "Extracted at", proposed.extraction.extractedAt ?? "unknown") : ""}
|
|
1036
|
+
${buildReviewItemPresentation(item, presentationAdapter).excludedProposals.map((entry) => fieldItem("excluded-proposal", `Excluded proposal ${entry.proposalIndex}`, `${entry.valueText} at ${entry.locator}: excerpt not at the cited span`)).join("")}
|
|
983
1037
|
</dl>
|
|
984
1038
|
${preview
|
|
985
1039
|
? `<div class="preview-section-grid">${renderSurfacePreviewSections(preview, trace)}</div><p class="preview-disclaimer preview-disclaimer-footer">${escapeHtml(preview.postureDisclaimer)}</p>`
|
|
@@ -38,9 +38,10 @@
|
|
|
38
38
|
.inspector-heading, .inspector-layout { display: grid; grid-template-columns: minmax(15rem, .7fr) minmax(0, 1.3fr); gap: 1rem; }
|
|
39
39
|
.inspector-heading h2 { margin: 0; }
|
|
40
40
|
.inspector-posture { display: flex; flex-direction: column; padding: .75rem; border-radius: var(--k-radius-sm); background: var(--k-positive-wash); }
|
|
41
|
-
|
|
42
|
-
.inspector-posture.extraction-failed { background: var(--k-negative-wash); color: var(--k-negative); border: 2px solid currentColor; }
|
|
41
|
+
/* Caution first: an artifact failure on the same source must win the tie and stay negative. */
|
|
43
42
|
.inspector-posture.extraction-incomplete { background: var(--k-caution-wash); color: var(--k-caution); border: 2px solid currentColor; }
|
|
43
|
+
.inspector-posture.digest-mismatch, .inspector-posture.artifact-unavailable, .inspector-posture.excerpt-mismatch, .inspector-posture.proposals-excluded { background: var(--k-negative-wash); color: var(--k-negative); border: 2px solid currentColor; }
|
|
44
|
+
.inspector-posture.extraction-failed { background: var(--k-negative-wash); color: var(--k-negative); border: 2px solid currentColor; }
|
|
44
45
|
.inspector-filters { display: flex; flex-wrap: wrap; gap: .6rem; margin: 1rem 0; }
|
|
45
46
|
.inspector-filters label { display: grid; gap: .25rem; font-size: .75rem; color: var(--k-text-muted); }
|
|
46
47
|
.inspector-filters select, .inspector-filters input { color: var(--k-text); background: var(--k-sunken); border: 1px solid var(--k-line); padding: .4rem; }
|
|
@@ -626,6 +627,13 @@ h1, h2, h3, p {
|
|
|
626
627
|
font-size: 12px;
|
|
627
628
|
}
|
|
628
629
|
|
|
630
|
+
.excerpt-verification,
|
|
631
|
+
.support-verification {
|
|
632
|
+
margin: 0;
|
|
633
|
+
font-size: 12px;
|
|
634
|
+
color: var(--k-muted);
|
|
635
|
+
}
|
|
636
|
+
|
|
629
637
|
/* decisions */
|
|
630
638
|
|
|
631
639
|
/* The decision row is the point of the card, so it sits on its own line,
|
package/package.json
CHANGED