@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.
@@ -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
- .survey-workbench-embed .inspector-posture.digest-mismatch, .survey-workbench-embed .inspector-posture.artifact-unavailable, .survey-workbench-embed .inspector-posture.excerpt-mismatch{ background: var(--k-negative-wash); color: var(--k-negative); border: 2px solid currentColor; }
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
- .survey-workbench-embed .inspector-posture.digest-mismatch, .survey-workbench-embed .inspector-posture.artifact-unavailable, .survey-workbench-embed .inspector-posture.excerpt-mismatch{ background: var(--k-negative-wash); color: var(--k-negative); border: 2px solid currentColor; }
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
- .inspector-posture.digest-mismatch, .inspector-posture.artifact-unavailable, .inspector-posture.excerpt-mismatch { background: var(--k-negative-wash); color: var(--k-negative); border: 2px solid currentColor; }
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kontourai/survey",
3
- "version": "5.0.0",
3
+ "version": "6.1.0",
4
4
  "description": "Producer-side source, extraction, candidate, and review contracts for projecting verified claims into Surface.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",