@kontourai/survey 2.2.3 → 2.3.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/index.d.ts +1 -1
- package/dist/src/review-resource.d.ts +13 -0
- package/dist/src/review-resource.js +21 -0
- package/dist/src/review-workbench/audit-rows.d.ts +72 -0
- package/dist/src/review-workbench/audit-rows.js +133 -0
- package/dist/src/review-workbench/extraction-inspector.d.ts +67 -2
- package/dist/src/review-workbench/extraction-inspector.js +233 -22
- package/dist/src/review-workbench/review-presentation.js +28 -1
- package/dist/src/review-workbench/review-queue-session.d.ts +11 -0
- package/dist/src/review-workbench/review-queue-session.js +13 -1
- package/dist/src/review-workbench/review-surface-preview.js +2 -1
- package/dist/src/review-workbench/review-workbench-css.generated.js +45 -11
- package/dist/src/review-workbench/review-workbench-element.js +24 -1
- package/dist/src/review-workbench/review-workbench.css +45 -11
- package/dist/src/review-workbench/review-workbench.d.ts +2 -1
- package/dist/src/review-workbench/review-workbench.js +172 -60
- package/dist/src/review-workbench/review-workbench.standalone.css +45 -11
- package/package.json +1 -1
|
@@ -4,9 +4,11 @@ import { candidateForDecision, keepActionDecision, buildReviewSessionEvent, buil
|
|
|
4
4
|
import { validateReviewSessionEventsForSnapshot, } from "./review-session-replay.js";
|
|
5
5
|
import { buildSurfaceProjectionPreview, formatValue, } from "./review-surface-preview.js";
|
|
6
6
|
import { buildReviewCandidatePresentation, buildReviewItemPresentation, } from "./review-presentation.js";
|
|
7
|
-
import { reviewResourceApiVersion, } from "../review-resource.js";
|
|
7
|
+
import { findSoleCandidateById, reviewResourceApiVersion, } from "../review-resource.js";
|
|
8
8
|
import { validateAuthorizing, buildAuthorizedActionAuthorizing } from "../review-authorizing.js";
|
|
9
9
|
import { humanizeIdentifier } from "./review-presentation.js";
|
|
10
|
+
import { createAuditFactTrace, } from "./audit-rows.js";
|
|
11
|
+
export { reviewAuditRowKeys } from "./audit-rows.js";
|
|
10
12
|
export { buildExtractionInspectorModel, exportExtractionInspector, filterExtractionInspectorCandidates, mountExtractionInspector, } from "./extraction-inspector.js";
|
|
11
13
|
export { buildReviewSessionEvents, buildReviewSessionEvent, buildReviewSessionResource, candidateForDecision, keepActionDecision, currentReviewItem, currentReviewWorkbenchState, defaultReviewSessionName, deriveQueueRowStatus, initialReviewQueueSessionState, initialReviewWorkbenchState, nextUnresolvedItemName, replayReviewSessionEvents, reviewSessionSummary, reviewWorkbenchSessionStorageKey, selectedCandidateRole, workbenchDecisionDefinitions, } from "./review-queue-session.js";
|
|
12
14
|
export { buildReviewCandidatePresentation, buildReviewItemPresentation, buildReviewResultPresentation, humanizeIdentifier, } from "./review-presentation.js";
|
|
@@ -251,7 +253,7 @@ function buildReviewApplyActionContext(result, itemByName, issues) {
|
|
|
251
253
|
};
|
|
252
254
|
}
|
|
253
255
|
function matchingSelectedCandidate(item, result) {
|
|
254
|
-
const candidate = item
|
|
256
|
+
const candidate = findSoleCandidateById(item, result.selectedCandidateId);
|
|
255
257
|
return candidate
|
|
256
258
|
&& candidate.role === result.selectedCandidateRole
|
|
257
259
|
&& structuralEqual(candidate.value, result.selectedValue)
|
|
@@ -660,6 +662,14 @@ function renderFieldCard(item, session, presentationAdapter) {
|
|
|
660
662
|
</label>
|
|
661
663
|
<button class="btn unconfirmed" type="button" data-testid="could-not-confirm" data-item-name="${escapeHtml(item.metadata.name)}">Could not confirm</button>` : ""}
|
|
662
664
|
</div>
|
|
665
|
+
<!--
|
|
666
|
+
A control's precondition message belongs where the control is. The
|
|
667
|
+
reason "Could not confirm" needs lives in the collapsed audit
|
|
668
|
+
accordion, so writing the failure only into that textarea's validation
|
|
669
|
+
bubble put it somewhere the reviewer could not see at the moment they
|
|
670
|
+
clicked, and the button read as permanently dead (kontourai/survey#208).
|
|
671
|
+
-->
|
|
672
|
+
<span class="derr" data-testid="decision-error" role="alert" hidden></span>
|
|
663
673
|
<div class="decided">
|
|
664
674
|
<span class="chip ${state}" data-testid="decided-chip">${chipLabel(state, hasCurrentValue)}</span>
|
|
665
675
|
<button class="undo" type="button" data-testid="undo-decision" data-item-name="${escapeHtml(item.metadata.name)}">Change</button>
|
|
@@ -886,6 +896,10 @@ function renderAuditDetails(item, current, proposed, session, presentationAdapte
|
|
|
886
896
|
};
|
|
887
897
|
const reviewDecisionPayload = buildReviewDecision(state);
|
|
888
898
|
const preview = buildSurfaceProjectionPreview(item, reviewDecisionPayload, presentationAdapter);
|
|
899
|
+
// Seeded with what the card face already shows, so the audit surface never
|
|
900
|
+
// reprints it. Passed down through every section: the sections render in DOM
|
|
901
|
+
// order, so "first placement wins" is also "highest-context placement wins".
|
|
902
|
+
const trace = createAuditFactTrace(proposed ? [{ of: proposed.id, property: "locator.excerpt", value: proposed.locator?.excerpt }] : []);
|
|
889
903
|
return `
|
|
890
904
|
<details class="audit-details" data-testid="audit-details">
|
|
891
905
|
<summary>Audit details</summary>
|
|
@@ -897,17 +911,17 @@ function renderAuditDetails(item, current, proposed, session, presentationAdapte
|
|
|
897
911
|
${definition ? `<p class="field-value"><span class="field-label">Decision effect</span> ${escapeHtml(definition.effect)}</p>` : ""}
|
|
898
912
|
${renderProducerFeedbackTags(item)}
|
|
899
913
|
<dl class="field-stack compact">
|
|
900
|
-
${current ? fieldItem("Current candidate ID", current.id) : ""}
|
|
901
|
-
${proposed ? fieldItem("Proposed candidate ID", proposed.id) : ""}
|
|
902
|
-
${proposed ? fieldItem("Claim ID", proposed.claimTarget.claimId ?? proposed.claimTarget.fieldOrBehavior) : ""}
|
|
903
|
-
${proposed ?
|
|
904
|
-
${proposed?.locator ? fieldItem("Locator", proposed.locator.locator ?? proposed.locator.scheme) : ""}
|
|
905
|
-
${proposed?.extraction.model ? fieldItem("Model", proposed.extraction.model) : ""}
|
|
906
|
-
${proposed ?
|
|
907
|
-
${proposed ? fieldItem("Extracted at", proposed.extraction.extractedAt ?? "unknown") : ""}
|
|
914
|
+
${current ? fieldItem("current-candidate-id", "Current candidate ID", current.id) : ""}
|
|
915
|
+
${proposed ? fieldItem("proposed-candidate-id", "Proposed candidate ID", proposed.id) : ""}
|
|
916
|
+
${proposed ? fieldItem("claim-id", "Claim ID", proposed.claimTarget.claimId ?? proposed.claimTarget.fieldOrBehavior) : ""}
|
|
917
|
+
${proposed ? placementItem(trace, { of: proposed.id, property: "source.sourceId" }, "raw-source-id", "Raw Source ID", proposed.source.sourceId ?? proposed.source.sourceRef) : ""}
|
|
918
|
+
${proposed?.locator ? fieldItem("locator", "Locator", proposed.locator.locator ?? proposed.locator.scheme) : ""}
|
|
919
|
+
${proposed?.extraction.model ? fieldItem("model", "Model", proposed.extraction.model) : ""}
|
|
920
|
+
${proposed ? placementItem(trace, { of: proposed.id, property: "extraction.extractor" }, "extractor", "Extractor", proposed.extraction.extractor ?? "unknown") : ""}
|
|
921
|
+
${proposed ? fieldItem("extracted-at", "Extracted at", proposed.extraction.extractedAt ?? "unknown") : ""}
|
|
908
922
|
</dl>
|
|
909
923
|
${preview
|
|
910
|
-
? `<div class="preview-section-grid">${renderSurfacePreviewSections(preview)}</div><p class="preview-disclaimer preview-disclaimer-footer">${escapeHtml(preview.postureDisclaimer)}</p>`
|
|
924
|
+
? `<div class="preview-section-grid">${renderSurfacePreviewSections(preview, trace)}</div><p class="preview-disclaimer preview-disclaimer-footer">${escapeHtml(preview.postureDisclaimer)}</p>`
|
|
911
925
|
: "<p class=\"preview-disclaimer\">No decision recorded yet — pick an option above to preview the saved record.</p>"}
|
|
912
926
|
${reviewDecisionPayload
|
|
913
927
|
? `<details class="reference-details">
|
|
@@ -949,57 +963,83 @@ function renderFooterTally(session) {
|
|
|
949
963
|
</footer>
|
|
950
964
|
`;
|
|
951
965
|
}
|
|
952
|
-
function renderSurfacePreviewSections(preview) {
|
|
966
|
+
function renderSurfacePreviewSections(preview, trace) {
|
|
967
|
+
// Every row below describes the SELECTED candidate. When the reviewer kept the
|
|
968
|
+
// current value, that is a different record from the one the ID stack above
|
|
969
|
+
// describes, so none of these are repeat placements and all of them render —
|
|
970
|
+
// which is the point of keying on fact identity rather than on strings.
|
|
971
|
+
const selected = preview.canonicalClaim.candidateId;
|
|
953
972
|
return [
|
|
954
973
|
renderCandidateHistory(preview),
|
|
955
|
-
renderSourceEvidence(preview),
|
|
974
|
+
renderSourceEvidence(preview, trace, selected),
|
|
956
975
|
renderReviewEvent(preview),
|
|
957
|
-
renderIntegrityPosture(preview),
|
|
976
|
+
renderIntegrityPosture(preview, trace, selected),
|
|
958
977
|
renderAuthorityTrace(preview),
|
|
959
978
|
].join("");
|
|
960
979
|
}
|
|
961
980
|
function renderReviewEvent(preview) {
|
|
981
|
+
// A could-not-confirm resolution projects no review event; five rows reading
|
|
982
|
+
// "unknown / not recorded / pending / not provided" report an absence the
|
|
983
|
+
// decided chip on the card face already states (kontourai/survey#207).
|
|
984
|
+
if (!preview.reviewEvent) {
|
|
985
|
+
return "";
|
|
986
|
+
}
|
|
962
987
|
return `
|
|
963
988
|
<section class="preview-section" data-testid="surface-review-event">
|
|
964
989
|
<h3>${escapeHtml("Review event")}</h3>
|
|
965
990
|
<dl class="field-stack compact">
|
|
966
|
-
${fieldItem("Actor", preview.reviewEvent
|
|
967
|
-
${fieldItem("Reviewed at", preview.reviewEvent
|
|
968
|
-
${fieldItem("Status", preview.reviewEvent
|
|
969
|
-
${fieldItem("Rationale", preview.reviewEvent
|
|
970
|
-
${fieldItem("Outcome", preview.reviewEvent
|
|
991
|
+
${fieldItem("actor", "Actor", preview.reviewEvent.actor)}
|
|
992
|
+
${fieldItem("reviewed-at", "Reviewed at", preview.reviewEvent.reviewedAt)}
|
|
993
|
+
${fieldItem("status", "Status", preview.reviewEvent.status)}
|
|
994
|
+
${fieldItem("rationale", "Rationale", preview.reviewEvent.rationale, "rationale-clamp")}
|
|
995
|
+
${fieldItem("outcome", "Outcome", preview.reviewEvent.reviewOutcomeId)}
|
|
971
996
|
</dl>
|
|
972
997
|
</section>
|
|
973
998
|
`;
|
|
974
999
|
}
|
|
975
|
-
function renderIntegrityPosture(preview) {
|
|
1000
|
+
function renderIntegrityPosture(preview, trace, selected) {
|
|
976
1001
|
return renderPreviewSection("Integrity posture", "surface-integrity-posture", [
|
|
977
|
-
|
|
978
|
-
],
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
]);
|
|
1002
|
+
{ key: "checksum", label: "Checksum", value: preview.integrityPosture.checksum },
|
|
1003
|
+
], [
|
|
1004
|
+
{ key: "candidate-set-id", label: "Candidate set ID", value: preview.integrityPosture.candidateSetId },
|
|
1005
|
+
{ key: "raw-source-id", label: "Raw source ID", value: preview.integrityPosture.rawSourceId, fact: { of: selected, property: "source.sourceId" } },
|
|
1006
|
+
{ key: "extraction-id", label: "Extraction ID", value: preview.integrityPosture.extractionId, fact: { of: selected, property: "extraction.extractionId" } },
|
|
1007
|
+
], trace);
|
|
983
1008
|
}
|
|
984
1009
|
function renderAuthorityTrace(preview) {
|
|
1010
|
+
// An absent portable authority trace is the common case, and the two rows
|
|
1011
|
+
// that said so were the same sentence on every card of every queue. The
|
|
1012
|
+
// posture statement it repeated lives in the footer disclaimer; the data
|
|
1013
|
+
// stays on SurfaceProjectionPreview for hosts that project it.
|
|
1014
|
+
if (preview.authorityTrace.status === "empty") {
|
|
1015
|
+
return "";
|
|
1016
|
+
}
|
|
985
1017
|
return renderPreviewSection("Authority trace", "surface-authority-trace", [
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
]
|
|
1018
|
+
{ key: "authority-trace-status", label: "Status", value: preview.authorityTrace.label },
|
|
1019
|
+
{ key: "authority-trace-detail", label: "Detail", value: preview.authorityTrace.detail },
|
|
1020
|
+
]);
|
|
989
1021
|
}
|
|
990
1022
|
function renderCandidateHistory(preview) {
|
|
1023
|
+
// Nothing unselected is not history; the row that said so carried no fact.
|
|
991
1024
|
if (preview.candidateHistory.length === 0) {
|
|
992
|
-
return
|
|
1025
|
+
return "";
|
|
993
1026
|
}
|
|
994
1027
|
const VISIBLE_COUNT = 3;
|
|
995
1028
|
const total = preview.candidateHistory.length;
|
|
996
1029
|
const visibleCandidates = preview.candidateHistory.slice(0, VISIBLE_COUNT);
|
|
997
1030
|
const overflowCandidates = preview.candidateHistory.slice(VISIBLE_COUNT);
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
const
|
|
1031
|
+
// `historyLabel` is the section's own heading, so a "History: Unselected
|
|
1032
|
+
// candidate history" row above every value restated the <h3> once per
|
|
1033
|
+
// candidate. The label stays on PreviewCandidateHistory for hosts that
|
|
1034
|
+
// project it without a heading of their own.
|
|
1035
|
+
const renderHistoryRows = (candidates) => candidates.map((candidate) => fieldItem("history-value", "Value", candidate.value)).join("");
|
|
1036
|
+
// Deliberately NOT deduplicated. With two candidates this repeats the ID
|
|
1037
|
+
// stack's "Current candidate ID", but it is the only row that names WHICH
|
|
1038
|
+
// unselected candidate a value belongs to, it stops being a repeat the moment
|
|
1039
|
+
// there are three, and suppressing it would make this section's disclosure
|
|
1040
|
+
// appear and disappear with the candidate count — structure a host would then
|
|
1041
|
+
// have to key on. Row-level suppression is what `data-audit-row` is for.
|
|
1042
|
+
const referenceRows = preview.candidateHistory.map((candidate) => fieldItem("candidate-id", "Candidate ID", candidate.candidateId)).join("");
|
|
1003
1043
|
const overflowHtml = overflowCandidates.length > 0
|
|
1004
1044
|
? `<div class="history-overflow">${renderHistoryRows(overflowCandidates)}</div>`
|
|
1005
1045
|
: "";
|
|
@@ -1019,53 +1059,75 @@ function renderCandidateHistory(preview) {
|
|
|
1019
1059
|
</dl>
|
|
1020
1060
|
${expanderHtml}
|
|
1021
1061
|
</div>
|
|
1022
|
-
|
|
1062
|
+
<details class="reference-details">
|
|
1023
1063
|
<summary>IDs and trace links</summary>
|
|
1024
1064
|
<dl class="field-stack compact">${referenceRows}</dl>
|
|
1025
|
-
</details
|
|
1065
|
+
</details>
|
|
1026
1066
|
</section>
|
|
1027
1067
|
`;
|
|
1028
1068
|
}
|
|
1029
|
-
function renderSourceEvidence(preview) {
|
|
1030
|
-
|
|
1069
|
+
function renderSourceEvidence(preview, trace, selected) {
|
|
1070
|
+
// The excerpt is quoted on the card face for the PROPOSED candidate. A
|
|
1071
|
+
// decision that selected some other candidate has a different excerpt, and it
|
|
1072
|
+
// renders here.
|
|
1073
|
+
const clampedExcerptHtml = trace.isRepeatPlacement({ of: selected, property: "locator.excerpt" }, preview.sourceEvidence.excerpt)
|
|
1074
|
+
? ""
|
|
1075
|
+
: fieldItemClamped("excerpt", "Excerpt", preview.sourceEvidence.excerpt, "excerpt");
|
|
1031
1076
|
return `
|
|
1032
1077
|
<section class="preview-section" data-testid="surface-source-evidence">
|
|
1033
1078
|
<h3>${escapeHtml("Raw Source")}</h3>
|
|
1034
1079
|
<dl class="field-stack compact">
|
|
1035
|
-
${fieldItem("Source Reference", preview.sourceEvidence.sourceRef)}
|
|
1080
|
+
${fieldItem("source-reference", "Source Reference", preview.sourceEvidence.sourceRef)}
|
|
1036
1081
|
${clampedExcerptHtml}
|
|
1037
|
-
${
|
|
1038
|
-
${fieldItem("Observed", preview.sourceEvidence.observedAt)}
|
|
1082
|
+
${placementItem(trace, { of: selected, property: "extraction.extractor" }, "extractor", "Extractor", preview.sourceEvidence.extractor)}
|
|
1083
|
+
${fieldItem("observed", "Observed", preview.sourceEvidence.observedAt)}
|
|
1039
1084
|
${preview.sourceEvidence.sourceAuthority ? [
|
|
1040
|
-
fieldItem("Source authority class", preview.sourceEvidence.sourceAuthority.authorityClass),
|
|
1041
|
-
fieldItem("Declared by", preview.sourceEvidence.sourceAuthority.declaredBy),
|
|
1042
|
-
fieldItem("Authority scope", preview.sourceEvidence.sourceAuthority.scope),
|
|
1085
|
+
fieldItem("source-authority-class", "Source authority class", preview.sourceEvidence.sourceAuthority.authorityClass),
|
|
1086
|
+
fieldItem("declared-by", "Declared by", preview.sourceEvidence.sourceAuthority.declaredBy),
|
|
1087
|
+
fieldItem("authority-scope", "Authority scope", preview.sourceEvidence.sourceAuthority.scope),
|
|
1043
1088
|
].join("") : ""}
|
|
1044
1089
|
</dl>
|
|
1045
1090
|
${renderReferenceDetails([
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
])}
|
|
1091
|
+
{ key: "raw-source-id", label: "Raw Source ID", value: preview.sourceEvidence.sourceId, fact: { of: selected, property: "source.sourceId" } },
|
|
1092
|
+
{ key: "extraction-id", label: "Extraction ID", value: preview.sourceEvidence.extractionId, fact: { of: selected, property: "extraction.extractionId" } },
|
|
1093
|
+
], trace)}
|
|
1049
1094
|
</section>
|
|
1050
1095
|
`;
|
|
1051
1096
|
}
|
|
1052
|
-
function
|
|
1097
|
+
function renderPreviewRows(rows, trace) {
|
|
1098
|
+
return rows
|
|
1099
|
+
.map((row) => (row.fact && trace
|
|
1100
|
+
? placementItem(trace, row.fact, row.key, row.label, row.value)
|
|
1101
|
+
: fieldItem(row.key, row.label, row.value)))
|
|
1102
|
+
.join("");
|
|
1103
|
+
}
|
|
1104
|
+
function renderPreviewSection(title, testId, rows, references = [], trace) {
|
|
1053
1105
|
return `
|
|
1054
|
-
<section class="preview-section
|
|
1106
|
+
<section class="preview-section" data-testid="${testId}">
|
|
1055
1107
|
<h3>${escapeHtml(title)}</h3>
|
|
1056
1108
|
<dl class="field-stack compact">
|
|
1057
|
-
${rows
|
|
1109
|
+
${renderPreviewRows(rows, trace)}
|
|
1058
1110
|
</dl>
|
|
1059
|
-
${references.length > 0 ? renderReferenceDetails(references) : ""}
|
|
1111
|
+
${references.length > 0 ? renderReferenceDetails(references, trace) : ""}
|
|
1060
1112
|
</section>
|
|
1061
1113
|
`;
|
|
1062
1114
|
}
|
|
1063
|
-
|
|
1115
|
+
/**
|
|
1116
|
+
* The per-section "IDs and trace links" list.
|
|
1117
|
+
*
|
|
1118
|
+
* Every call site includes at least one reference that is a first placement and
|
|
1119
|
+
* so can never be suppressed — Raw Source keeps its extraction id, Integrity
|
|
1120
|
+
* posture its candidate set id, and the history section's candidate ids are
|
|
1121
|
+
* exempt outright. The disclosure therefore does not appear and disappear with
|
|
1122
|
+
* the data, which is what a host selecting on structure would otherwise be
|
|
1123
|
+
* exposed to. Keep that property when adding a reference list.
|
|
1124
|
+
*/
|
|
1125
|
+
function renderReferenceDetails(references, trace) {
|
|
1064
1126
|
return `
|
|
1065
1127
|
<details class="reference-details">
|
|
1066
1128
|
<summary>IDs and trace links</summary>
|
|
1067
1129
|
<dl class="field-stack compact">
|
|
1068
|
-
${references
|
|
1130
|
+
${renderPreviewRows(references, trace)}
|
|
1069
1131
|
</dl>
|
|
1070
1132
|
</details>
|
|
1071
1133
|
`;
|
|
@@ -1253,6 +1315,24 @@ function queueSessionFromStartState(startState) {
|
|
|
1253
1315
|
actorId: startState.actorId,
|
|
1254
1316
|
};
|
|
1255
1317
|
}
|
|
1318
|
+
const COULD_NOT_CONFIRM_REASON_REQUIRED = "A reason is required when you could not confirm.";
|
|
1319
|
+
/**
|
|
1320
|
+
* Opens every collapsed region between `control` and the card before anything
|
|
1321
|
+
* tries to focus it or anchor a validation bubble on it.
|
|
1322
|
+
*
|
|
1323
|
+
* The browser will not show a validation message on, or move focus into, a
|
|
1324
|
+
* control inside a closed `<details>` — the call silently does nothing. Any
|
|
1325
|
+
* decision path that blocks on an input the reviewer cannot currently see must
|
|
1326
|
+
* go through here, or the control it guards reads as dead (kontourai/survey#208,
|
|
1327
|
+
* after #201 and #203 in the same family).
|
|
1328
|
+
*/
|
|
1329
|
+
function revealControl(control) {
|
|
1330
|
+
let ancestor = control?.parentElement?.closest?.("details") ?? null;
|
|
1331
|
+
while (ancestor) {
|
|
1332
|
+
ancestor.open = true;
|
|
1333
|
+
ancestor = ancestor.parentElement?.closest?.("details") ?? null;
|
|
1334
|
+
}
|
|
1335
|
+
}
|
|
1256
1336
|
function bindFieldCardInteractions(root, controller) {
|
|
1257
1337
|
root.querySelectorAll("[data-testid='use-proposed']").forEach((button) => {
|
|
1258
1338
|
button.addEventListener("click", () => {
|
|
@@ -1312,12 +1392,25 @@ function bindFieldCardInteractions(root, controller) {
|
|
|
1312
1392
|
const note = controller.currentSession().notesByItemName[itemName]?.trim() ?? "";
|
|
1313
1393
|
const field = button.closest("[data-testid='review-field']");
|
|
1314
1394
|
const textarea = field?.querySelector("[data-testid='reviewer-note']");
|
|
1395
|
+
const errorEl = field?.querySelector("[data-testid='decision-error']");
|
|
1315
1396
|
if (!note) {
|
|
1316
|
-
|
|
1317
|
-
|
|
1397
|
+
// Say it where the button is, then open the way to the input that
|
|
1398
|
+
// satisfies it. Focusing (and validating) a control inside a collapsed
|
|
1399
|
+
// <details> is a no-op the reviewer never sees (kontourai/survey#208).
|
|
1400
|
+
if (errorEl) {
|
|
1401
|
+
errorEl.textContent = COULD_NOT_CONFIRM_REASON_REQUIRED;
|
|
1402
|
+
errorEl.hidden = false;
|
|
1403
|
+
}
|
|
1404
|
+
textarea?.setCustomValidity?.(COULD_NOT_CONFIRM_REASON_REQUIRED);
|
|
1405
|
+
revealControl(textarea);
|
|
1318
1406
|
textarea?.focus();
|
|
1407
|
+
textarea?.reportValidity?.();
|
|
1319
1408
|
return;
|
|
1320
1409
|
}
|
|
1410
|
+
if (errorEl) {
|
|
1411
|
+
errorEl.textContent = "";
|
|
1412
|
+
errorEl.hidden = true;
|
|
1413
|
+
}
|
|
1321
1414
|
textarea?.setCustomValidity?.("");
|
|
1322
1415
|
controller.setDecision(itemName, "could-not-confirm");
|
|
1323
1416
|
controller.renderCurrentState();
|
|
@@ -1332,6 +1425,13 @@ function bindFieldCardInteractions(root, controller) {
|
|
|
1332
1425
|
root.querySelectorAll("[data-testid='reviewer-note']").forEach((textarea) => {
|
|
1333
1426
|
textarea.addEventListener("input", () => {
|
|
1334
1427
|
textarea.setCustomValidity?.("");
|
|
1428
|
+
const decisionError = textarea
|
|
1429
|
+
.closest("[data-testid='review-field']")
|
|
1430
|
+
?.querySelector("[data-testid='decision-error']");
|
|
1431
|
+
if (decisionError) {
|
|
1432
|
+
decisionError.textContent = "";
|
|
1433
|
+
decisionError.hidden = true;
|
|
1434
|
+
}
|
|
1335
1435
|
const itemName = textarea.dataset.itemName ?? "";
|
|
1336
1436
|
controller.updateReviewerNote(itemName, textarea.value);
|
|
1337
1437
|
refreshAuditPayloadForItem(textarea, controller, itemName);
|
|
@@ -1403,9 +1503,9 @@ function bindHistoryExpanders(root) {
|
|
|
1403
1503
|
});
|
|
1404
1504
|
});
|
|
1405
1505
|
}
|
|
1406
|
-
function fieldItemClamped(label, value, extraClass = "") {
|
|
1506
|
+
function fieldItemClamped(key, label, value, extraClass = "") {
|
|
1407
1507
|
return `
|
|
1408
|
-
<div class="kv ${extraClass}">
|
|
1508
|
+
<div class="kv ${extraClass}" data-audit-row="${key}">
|
|
1409
1509
|
<dt class="field-label">${escapeHtml(label)}</dt>
|
|
1410
1510
|
<div class="excerpt-clamp" data-clamp>
|
|
1411
1511
|
<dd class="field-value">${escapeHtml(value)}</dd>
|
|
@@ -1414,14 +1514,26 @@ function fieldItemClamped(label, value, extraClass = "") {
|
|
|
1414
1514
|
</div>
|
|
1415
1515
|
`;
|
|
1416
1516
|
}
|
|
1417
|
-
function fieldItem(label, value, extraClass = "") {
|
|
1517
|
+
function fieldItem(key, label, value, extraClass = "") {
|
|
1418
1518
|
return `
|
|
1419
|
-
<div class="kv ${extraClass}">
|
|
1519
|
+
<div class="kv ${extraClass}" data-audit-row="${key}">
|
|
1420
1520
|
<dt class="field-label">${escapeHtml(label)}</dt>
|
|
1421
1521
|
<dd class="field-value">${escapeHtml(value)}</dd>
|
|
1422
1522
|
</div>
|
|
1423
1523
|
`;
|
|
1424
1524
|
}
|
|
1525
|
+
/**
|
|
1526
|
+
* A row for a fact the card renders in more than one place. Rendered only if
|
|
1527
|
+
* this card has not already printed the SAME property of the SAME record with
|
|
1528
|
+
* the same value; see {@link AuditFactTrace}.
|
|
1529
|
+
*
|
|
1530
|
+
* Only the four genuinely repeated placements go through here. Every other row
|
|
1531
|
+
* has exactly one home and is rendered with {@link fieldItem} directly, so no
|
|
1532
|
+
* row can ever be suppressed by a fact it has nothing to do with.
|
|
1533
|
+
*/
|
|
1534
|
+
function placementItem(trace, fact, key, label, value, extraClass = "") {
|
|
1535
|
+
return trace.isRepeatPlacement(fact, value) ? "" : fieldItem(key, label, value, extraClass);
|
|
1536
|
+
}
|
|
1425
1537
|
function escapeHtml(value) {
|
|
1426
1538
|
return formatValue(value)
|
|
1427
1539
|
.replaceAll("&", "&")
|
|
@@ -48,11 +48,37 @@
|
|
|
48
48
|
.inspector-pager button:disabled, .queue-pager button:disabled { opacity: .45; }
|
|
49
49
|
.inspector-candidates { margin: 0; padding-left: 1.5rem; }
|
|
50
50
|
.inspector-candidate { width: 100%; display: grid; gap: .2rem; text-align: left; padding: .65rem; color: var(--k-text); background: transparent; border: 1px solid var(--k-line); }
|
|
51
|
-
.inspector-source pre { white-space: pre-wrap; overflow-wrap: anywhere; margin: 0; padding: 1rem; background: var(--k-sunken); border: 1px solid var(--k-line); min-height: 8rem; }
|
|
51
|
+
.inspector-source pre { white-space: pre-wrap; overflow-wrap: anywhere; margin: 0; padding: 1rem; background: var(--k-sunken); border: 1px solid var(--k-line); min-height: 8rem; line-height: 2.2; }
|
|
52
52
|
.inspector-source mark { background: var(--k-brand-wash); color: var(--k-text); outline: 1px solid var(--k-brand); }
|
|
53
53
|
.inspector-candidate:focus { outline: 3px solid var(--k-active); outline-offset: 2px; }
|
|
54
|
-
|
|
55
|
-
.
|
|
54
|
+
/* An inert link target, one per candidate in the model, so a host's
|
|
55
|
+
`href="#<highlightElementId>"` always resolves. It is not a control and must
|
|
56
|
+
not behave like one: no size, no tab stop, nothing painted. */
|
|
57
|
+
.highlight-anchor {
|
|
58
|
+
display: inline;
|
|
59
|
+
width: 0;
|
|
60
|
+
height: 0;
|
|
61
|
+
overflow: hidden;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/* The highlight IS the return control — the only part of this surface a reader
|
|
65
|
+
can see, so it is the thing to aim at. Full phrase width, already visibly
|
|
66
|
+
marked, one tab stop per painted highlight. */
|
|
67
|
+
.source-highlight {
|
|
68
|
+
cursor: pointer;
|
|
69
|
+
border-radius: 2px;
|
|
70
|
+
/* Vertical padding on an inline box grows the hit area without moving the
|
|
71
|
+
line box, and the prepared text's line-height below leaves room for it, so
|
|
72
|
+
the target reaches ~24px tall without lines overlapping each other. The
|
|
73
|
+
phrase itself supplies the width. */
|
|
74
|
+
padding: 5px 3px;
|
|
75
|
+
margin: 0 -3px;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
.source-highlight:focus-visible {
|
|
79
|
+
outline: 3px solid var(--k-active);
|
|
80
|
+
outline-offset: 1px;
|
|
81
|
+
}
|
|
56
82
|
.source-unavailable { color: var(--k-negative); font-weight: 700; }
|
|
57
83
|
@container (max-width: 720px) { .inspector-heading, .inspector-layout { grid-template-columns: 1fr !important; } }
|
|
58
84
|
|
|
@@ -485,6 +511,22 @@ h1, h2, h3, p {
|
|
|
485
511
|
display: none;
|
|
486
512
|
}
|
|
487
513
|
|
|
514
|
+
/* Why a decision was refused, next to the button that refused it. The input
|
|
515
|
+
that satisfies the precondition can be inside the collapsed audit accordion,
|
|
516
|
+
so the message cannot live only with the input (kontourai/survey#208). */
|
|
517
|
+
.derr {
|
|
518
|
+
display: block;
|
|
519
|
+
margin-top: 6px;
|
|
520
|
+
text-align: right;
|
|
521
|
+
font-size: 12px;
|
|
522
|
+
font-weight: 600;
|
|
523
|
+
color: var(--k-negative);
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
.derr[hidden] {
|
|
527
|
+
display: none;
|
|
528
|
+
}
|
|
529
|
+
|
|
488
530
|
/* confidence + provenance */
|
|
489
531
|
|
|
490
532
|
.prov {
|
|
@@ -926,14 +968,6 @@ h1, h2, h3, p {
|
|
|
926
968
|
text-transform: uppercase;
|
|
927
969
|
}
|
|
928
970
|
|
|
929
|
-
.preview-section.is-neutral {
|
|
930
|
-
background: var(--k-raised);
|
|
931
|
-
}
|
|
932
|
-
|
|
933
|
-
.preview-section.is-neutral h3 {
|
|
934
|
-
color: var(--k-faint);
|
|
935
|
-
}
|
|
936
|
-
|
|
937
971
|
.reference-details {
|
|
938
972
|
margin-top: 8px;
|
|
939
973
|
}
|
package/package.json
CHANGED