@kontourai/survey 2.2.4 → 2.4.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 +3 -1
- package/dist/src/index.js +1 -0
- 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/queue-binding.d.ts +127 -0
- package/dist/src/review-workbench/queue-binding.js +324 -0
- 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 +3 -1
- package/dist/src/review-workbench/review-workbench.js +173 -60
- package/dist/src/review-workbench/review-workbench.standalone.css +45 -11
- package/dist/src/review-workbench/server-review-session.d.ts +14 -0
- package/dist/src/review-workbench/server-review-session.js +7 -0
- package/package.json +2 -1
|
@@ -4,9 +4,12 @@ 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";
|
|
12
|
+
export { assertReviewQueueAgainstExtractionImport, assertReviewQueueBinding, bindReviewQueue, hashReviewQueueSnapshot, UnattestedExtractionQueueError, UnattestedReviewQueueError, validateReviewQueueAgainstExtractionImport, validateReviewQueueBinding, } from "./queue-binding.js";
|
|
10
13
|
export { buildExtractionInspectorModel, exportExtractionInspector, filterExtractionInspectorCandidates, mountExtractionInspector, } from "./extraction-inspector.js";
|
|
11
14
|
export { buildReviewSessionEvents, buildReviewSessionEvent, buildReviewSessionResource, candidateForDecision, keepActionDecision, currentReviewItem, currentReviewWorkbenchState, defaultReviewSessionName, deriveQueueRowStatus, initialReviewQueueSessionState, initialReviewWorkbenchState, nextUnresolvedItemName, replayReviewSessionEvents, reviewSessionSummary, reviewWorkbenchSessionStorageKey, selectedCandidateRole, workbenchDecisionDefinitions, } from "./review-queue-session.js";
|
|
12
15
|
export { buildReviewCandidatePresentation, buildReviewItemPresentation, buildReviewResultPresentation, humanizeIdentifier, } from "./review-presentation.js";
|
|
@@ -251,7 +254,7 @@ function buildReviewApplyActionContext(result, itemByName, issues) {
|
|
|
251
254
|
};
|
|
252
255
|
}
|
|
253
256
|
function matchingSelectedCandidate(item, result) {
|
|
254
|
-
const candidate = item
|
|
257
|
+
const candidate = findSoleCandidateById(item, result.selectedCandidateId);
|
|
255
258
|
return candidate
|
|
256
259
|
&& candidate.role === result.selectedCandidateRole
|
|
257
260
|
&& structuralEqual(candidate.value, result.selectedValue)
|
|
@@ -660,6 +663,14 @@ function renderFieldCard(item, session, presentationAdapter) {
|
|
|
660
663
|
</label>
|
|
661
664
|
<button class="btn unconfirmed" type="button" data-testid="could-not-confirm" data-item-name="${escapeHtml(item.metadata.name)}">Could not confirm</button>` : ""}
|
|
662
665
|
</div>
|
|
666
|
+
<!--
|
|
667
|
+
A control's precondition message belongs where the control is. The
|
|
668
|
+
reason "Could not confirm" needs lives in the collapsed audit
|
|
669
|
+
accordion, so writing the failure only into that textarea's validation
|
|
670
|
+
bubble put it somewhere the reviewer could not see at the moment they
|
|
671
|
+
clicked, and the button read as permanently dead (kontourai/survey#208).
|
|
672
|
+
-->
|
|
673
|
+
<span class="derr" data-testid="decision-error" role="alert" hidden></span>
|
|
663
674
|
<div class="decided">
|
|
664
675
|
<span class="chip ${state}" data-testid="decided-chip">${chipLabel(state, hasCurrentValue)}</span>
|
|
665
676
|
<button class="undo" type="button" data-testid="undo-decision" data-item-name="${escapeHtml(item.metadata.name)}">Change</button>
|
|
@@ -886,6 +897,10 @@ function renderAuditDetails(item, current, proposed, session, presentationAdapte
|
|
|
886
897
|
};
|
|
887
898
|
const reviewDecisionPayload = buildReviewDecision(state);
|
|
888
899
|
const preview = buildSurfaceProjectionPreview(item, reviewDecisionPayload, presentationAdapter);
|
|
900
|
+
// Seeded with what the card face already shows, so the audit surface never
|
|
901
|
+
// reprints it. Passed down through every section: the sections render in DOM
|
|
902
|
+
// order, so "first placement wins" is also "highest-context placement wins".
|
|
903
|
+
const trace = createAuditFactTrace(proposed ? [{ of: proposed.id, property: "locator.excerpt", value: proposed.locator?.excerpt }] : []);
|
|
889
904
|
return `
|
|
890
905
|
<details class="audit-details" data-testid="audit-details">
|
|
891
906
|
<summary>Audit details</summary>
|
|
@@ -897,17 +912,17 @@ function renderAuditDetails(item, current, proposed, session, presentationAdapte
|
|
|
897
912
|
${definition ? `<p class="field-value"><span class="field-label">Decision effect</span> ${escapeHtml(definition.effect)}</p>` : ""}
|
|
898
913
|
${renderProducerFeedbackTags(item)}
|
|
899
914
|
<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") : ""}
|
|
915
|
+
${current ? fieldItem("current-candidate-id", "Current candidate ID", current.id) : ""}
|
|
916
|
+
${proposed ? fieldItem("proposed-candidate-id", "Proposed candidate ID", proposed.id) : ""}
|
|
917
|
+
${proposed ? fieldItem("claim-id", "Claim ID", proposed.claimTarget.claimId ?? proposed.claimTarget.fieldOrBehavior) : ""}
|
|
918
|
+
${proposed ? placementItem(trace, { of: proposed.id, property: "source.sourceId" }, "raw-source-id", "Raw Source ID", proposed.source.sourceId ?? proposed.source.sourceRef) : ""}
|
|
919
|
+
${proposed?.locator ? fieldItem("locator", "Locator", proposed.locator.locator ?? proposed.locator.scheme) : ""}
|
|
920
|
+
${proposed?.extraction.model ? fieldItem("model", "Model", proposed.extraction.model) : ""}
|
|
921
|
+
${proposed ? placementItem(trace, { of: proposed.id, property: "extraction.extractor" }, "extractor", "Extractor", proposed.extraction.extractor ?? "unknown") : ""}
|
|
922
|
+
${proposed ? fieldItem("extracted-at", "Extracted at", proposed.extraction.extractedAt ?? "unknown") : ""}
|
|
908
923
|
</dl>
|
|
909
924
|
${preview
|
|
910
|
-
? `<div class="preview-section-grid">${renderSurfacePreviewSections(preview)}</div><p class="preview-disclaimer preview-disclaimer-footer">${escapeHtml(preview.postureDisclaimer)}</p>`
|
|
925
|
+
? `<div class="preview-section-grid">${renderSurfacePreviewSections(preview, trace)}</div><p class="preview-disclaimer preview-disclaimer-footer">${escapeHtml(preview.postureDisclaimer)}</p>`
|
|
911
926
|
: "<p class=\"preview-disclaimer\">No decision recorded yet — pick an option above to preview the saved record.</p>"}
|
|
912
927
|
${reviewDecisionPayload
|
|
913
928
|
? `<details class="reference-details">
|
|
@@ -949,57 +964,83 @@ function renderFooterTally(session) {
|
|
|
949
964
|
</footer>
|
|
950
965
|
`;
|
|
951
966
|
}
|
|
952
|
-
function renderSurfacePreviewSections(preview) {
|
|
967
|
+
function renderSurfacePreviewSections(preview, trace) {
|
|
968
|
+
// Every row below describes the SELECTED candidate. When the reviewer kept the
|
|
969
|
+
// current value, that is a different record from the one the ID stack above
|
|
970
|
+
// describes, so none of these are repeat placements and all of them render —
|
|
971
|
+
// which is the point of keying on fact identity rather than on strings.
|
|
972
|
+
const selected = preview.canonicalClaim.candidateId;
|
|
953
973
|
return [
|
|
954
974
|
renderCandidateHistory(preview),
|
|
955
|
-
renderSourceEvidence(preview),
|
|
975
|
+
renderSourceEvidence(preview, trace, selected),
|
|
956
976
|
renderReviewEvent(preview),
|
|
957
|
-
renderIntegrityPosture(preview),
|
|
977
|
+
renderIntegrityPosture(preview, trace, selected),
|
|
958
978
|
renderAuthorityTrace(preview),
|
|
959
979
|
].join("");
|
|
960
980
|
}
|
|
961
981
|
function renderReviewEvent(preview) {
|
|
982
|
+
// A could-not-confirm resolution projects no review event; five rows reading
|
|
983
|
+
// "unknown / not recorded / pending / not provided" report an absence the
|
|
984
|
+
// decided chip on the card face already states (kontourai/survey#207).
|
|
985
|
+
if (!preview.reviewEvent) {
|
|
986
|
+
return "";
|
|
987
|
+
}
|
|
962
988
|
return `
|
|
963
989
|
<section class="preview-section" data-testid="surface-review-event">
|
|
964
990
|
<h3>${escapeHtml("Review event")}</h3>
|
|
965
991
|
<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
|
|
992
|
+
${fieldItem("actor", "Actor", preview.reviewEvent.actor)}
|
|
993
|
+
${fieldItem("reviewed-at", "Reviewed at", preview.reviewEvent.reviewedAt)}
|
|
994
|
+
${fieldItem("status", "Status", preview.reviewEvent.status)}
|
|
995
|
+
${fieldItem("rationale", "Rationale", preview.reviewEvent.rationale, "rationale-clamp")}
|
|
996
|
+
${fieldItem("outcome", "Outcome", preview.reviewEvent.reviewOutcomeId)}
|
|
971
997
|
</dl>
|
|
972
998
|
</section>
|
|
973
999
|
`;
|
|
974
1000
|
}
|
|
975
|
-
function renderIntegrityPosture(preview) {
|
|
1001
|
+
function renderIntegrityPosture(preview, trace, selected) {
|
|
976
1002
|
return renderPreviewSection("Integrity posture", "surface-integrity-posture", [
|
|
977
|
-
|
|
978
|
-
],
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
]);
|
|
1003
|
+
{ key: "checksum", label: "Checksum", value: preview.integrityPosture.checksum },
|
|
1004
|
+
], [
|
|
1005
|
+
{ key: "candidate-set-id", label: "Candidate set ID", value: preview.integrityPosture.candidateSetId },
|
|
1006
|
+
{ key: "raw-source-id", label: "Raw source ID", value: preview.integrityPosture.rawSourceId, fact: { of: selected, property: "source.sourceId" } },
|
|
1007
|
+
{ key: "extraction-id", label: "Extraction ID", value: preview.integrityPosture.extractionId, fact: { of: selected, property: "extraction.extractionId" } },
|
|
1008
|
+
], trace);
|
|
983
1009
|
}
|
|
984
1010
|
function renderAuthorityTrace(preview) {
|
|
1011
|
+
// An absent portable authority trace is the common case, and the two rows
|
|
1012
|
+
// that said so were the same sentence on every card of every queue. The
|
|
1013
|
+
// posture statement it repeated lives in the footer disclaimer; the data
|
|
1014
|
+
// stays on SurfaceProjectionPreview for hosts that project it.
|
|
1015
|
+
if (preview.authorityTrace.status === "empty") {
|
|
1016
|
+
return "";
|
|
1017
|
+
}
|
|
985
1018
|
return renderPreviewSection("Authority trace", "surface-authority-trace", [
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
]
|
|
1019
|
+
{ key: "authority-trace-status", label: "Status", value: preview.authorityTrace.label },
|
|
1020
|
+
{ key: "authority-trace-detail", label: "Detail", value: preview.authorityTrace.detail },
|
|
1021
|
+
]);
|
|
989
1022
|
}
|
|
990
1023
|
function renderCandidateHistory(preview) {
|
|
1024
|
+
// Nothing unselected is not history; the row that said so carried no fact.
|
|
991
1025
|
if (preview.candidateHistory.length === 0) {
|
|
992
|
-
return
|
|
1026
|
+
return "";
|
|
993
1027
|
}
|
|
994
1028
|
const VISIBLE_COUNT = 3;
|
|
995
1029
|
const total = preview.candidateHistory.length;
|
|
996
1030
|
const visibleCandidates = preview.candidateHistory.slice(0, VISIBLE_COUNT);
|
|
997
1031
|
const overflowCandidates = preview.candidateHistory.slice(VISIBLE_COUNT);
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
const
|
|
1032
|
+
// `historyLabel` is the section's own heading, so a "History: Unselected
|
|
1033
|
+
// candidate history" row above every value restated the <h3> once per
|
|
1034
|
+
// candidate. The label stays on PreviewCandidateHistory for hosts that
|
|
1035
|
+
// project it without a heading of their own.
|
|
1036
|
+
const renderHistoryRows = (candidates) => candidates.map((candidate) => fieldItem("history-value", "Value", candidate.value)).join("");
|
|
1037
|
+
// Deliberately NOT deduplicated. With two candidates this repeats the ID
|
|
1038
|
+
// stack's "Current candidate ID", but it is the only row that names WHICH
|
|
1039
|
+
// unselected candidate a value belongs to, it stops being a repeat the moment
|
|
1040
|
+
// there are three, and suppressing it would make this section's disclosure
|
|
1041
|
+
// appear and disappear with the candidate count — structure a host would then
|
|
1042
|
+
// have to key on. Row-level suppression is what `data-audit-row` is for.
|
|
1043
|
+
const referenceRows = preview.candidateHistory.map((candidate) => fieldItem("candidate-id", "Candidate ID", candidate.candidateId)).join("");
|
|
1003
1044
|
const overflowHtml = overflowCandidates.length > 0
|
|
1004
1045
|
? `<div class="history-overflow">${renderHistoryRows(overflowCandidates)}</div>`
|
|
1005
1046
|
: "";
|
|
@@ -1019,53 +1060,75 @@ function renderCandidateHistory(preview) {
|
|
|
1019
1060
|
</dl>
|
|
1020
1061
|
${expanderHtml}
|
|
1021
1062
|
</div>
|
|
1022
|
-
|
|
1063
|
+
<details class="reference-details">
|
|
1023
1064
|
<summary>IDs and trace links</summary>
|
|
1024
1065
|
<dl class="field-stack compact">${referenceRows}</dl>
|
|
1025
|
-
</details
|
|
1066
|
+
</details>
|
|
1026
1067
|
</section>
|
|
1027
1068
|
`;
|
|
1028
1069
|
}
|
|
1029
|
-
function renderSourceEvidence(preview) {
|
|
1030
|
-
|
|
1070
|
+
function renderSourceEvidence(preview, trace, selected) {
|
|
1071
|
+
// The excerpt is quoted on the card face for the PROPOSED candidate. A
|
|
1072
|
+
// decision that selected some other candidate has a different excerpt, and it
|
|
1073
|
+
// renders here.
|
|
1074
|
+
const clampedExcerptHtml = trace.isRepeatPlacement({ of: selected, property: "locator.excerpt" }, preview.sourceEvidence.excerpt)
|
|
1075
|
+
? ""
|
|
1076
|
+
: fieldItemClamped("excerpt", "Excerpt", preview.sourceEvidence.excerpt, "excerpt");
|
|
1031
1077
|
return `
|
|
1032
1078
|
<section class="preview-section" data-testid="surface-source-evidence">
|
|
1033
1079
|
<h3>${escapeHtml("Raw Source")}</h3>
|
|
1034
1080
|
<dl class="field-stack compact">
|
|
1035
|
-
${fieldItem("Source Reference", preview.sourceEvidence.sourceRef)}
|
|
1081
|
+
${fieldItem("source-reference", "Source Reference", preview.sourceEvidence.sourceRef)}
|
|
1036
1082
|
${clampedExcerptHtml}
|
|
1037
|
-
${
|
|
1038
|
-
${fieldItem("Observed", preview.sourceEvidence.observedAt)}
|
|
1083
|
+
${placementItem(trace, { of: selected, property: "extraction.extractor" }, "extractor", "Extractor", preview.sourceEvidence.extractor)}
|
|
1084
|
+
${fieldItem("observed", "Observed", preview.sourceEvidence.observedAt)}
|
|
1039
1085
|
${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),
|
|
1086
|
+
fieldItem("source-authority-class", "Source authority class", preview.sourceEvidence.sourceAuthority.authorityClass),
|
|
1087
|
+
fieldItem("declared-by", "Declared by", preview.sourceEvidence.sourceAuthority.declaredBy),
|
|
1088
|
+
fieldItem("authority-scope", "Authority scope", preview.sourceEvidence.sourceAuthority.scope),
|
|
1043
1089
|
].join("") : ""}
|
|
1044
1090
|
</dl>
|
|
1045
1091
|
${renderReferenceDetails([
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
])}
|
|
1092
|
+
{ key: "raw-source-id", label: "Raw Source ID", value: preview.sourceEvidence.sourceId, fact: { of: selected, property: "source.sourceId" } },
|
|
1093
|
+
{ key: "extraction-id", label: "Extraction ID", value: preview.sourceEvidence.extractionId, fact: { of: selected, property: "extraction.extractionId" } },
|
|
1094
|
+
], trace)}
|
|
1049
1095
|
</section>
|
|
1050
1096
|
`;
|
|
1051
1097
|
}
|
|
1052
|
-
function
|
|
1098
|
+
function renderPreviewRows(rows, trace) {
|
|
1099
|
+
return rows
|
|
1100
|
+
.map((row) => (row.fact && trace
|
|
1101
|
+
? placementItem(trace, row.fact, row.key, row.label, row.value)
|
|
1102
|
+
: fieldItem(row.key, row.label, row.value)))
|
|
1103
|
+
.join("");
|
|
1104
|
+
}
|
|
1105
|
+
function renderPreviewSection(title, testId, rows, references = [], trace) {
|
|
1053
1106
|
return `
|
|
1054
|
-
<section class="preview-section
|
|
1107
|
+
<section class="preview-section" data-testid="${testId}">
|
|
1055
1108
|
<h3>${escapeHtml(title)}</h3>
|
|
1056
1109
|
<dl class="field-stack compact">
|
|
1057
|
-
${rows
|
|
1110
|
+
${renderPreviewRows(rows, trace)}
|
|
1058
1111
|
</dl>
|
|
1059
|
-
${references.length > 0 ? renderReferenceDetails(references) : ""}
|
|
1112
|
+
${references.length > 0 ? renderReferenceDetails(references, trace) : ""}
|
|
1060
1113
|
</section>
|
|
1061
1114
|
`;
|
|
1062
1115
|
}
|
|
1063
|
-
|
|
1116
|
+
/**
|
|
1117
|
+
* The per-section "IDs and trace links" list.
|
|
1118
|
+
*
|
|
1119
|
+
* Every call site includes at least one reference that is a first placement and
|
|
1120
|
+
* so can never be suppressed — Raw Source keeps its extraction id, Integrity
|
|
1121
|
+
* posture its candidate set id, and the history section's candidate ids are
|
|
1122
|
+
* exempt outright. The disclosure therefore does not appear and disappear with
|
|
1123
|
+
* the data, which is what a host selecting on structure would otherwise be
|
|
1124
|
+
* exposed to. Keep that property when adding a reference list.
|
|
1125
|
+
*/
|
|
1126
|
+
function renderReferenceDetails(references, trace) {
|
|
1064
1127
|
return `
|
|
1065
1128
|
<details class="reference-details">
|
|
1066
1129
|
<summary>IDs and trace links</summary>
|
|
1067
1130
|
<dl class="field-stack compact">
|
|
1068
|
-
${references
|
|
1131
|
+
${renderPreviewRows(references, trace)}
|
|
1069
1132
|
</dl>
|
|
1070
1133
|
</details>
|
|
1071
1134
|
`;
|
|
@@ -1253,6 +1316,24 @@ function queueSessionFromStartState(startState) {
|
|
|
1253
1316
|
actorId: startState.actorId,
|
|
1254
1317
|
};
|
|
1255
1318
|
}
|
|
1319
|
+
const COULD_NOT_CONFIRM_REASON_REQUIRED = "A reason is required when you could not confirm.";
|
|
1320
|
+
/**
|
|
1321
|
+
* Opens every collapsed region between `control` and the card before anything
|
|
1322
|
+
* tries to focus it or anchor a validation bubble on it.
|
|
1323
|
+
*
|
|
1324
|
+
* The browser will not show a validation message on, or move focus into, a
|
|
1325
|
+
* control inside a closed `<details>` — the call silently does nothing. Any
|
|
1326
|
+
* decision path that blocks on an input the reviewer cannot currently see must
|
|
1327
|
+
* go through here, or the control it guards reads as dead (kontourai/survey#208,
|
|
1328
|
+
* after #201 and #203 in the same family).
|
|
1329
|
+
*/
|
|
1330
|
+
function revealControl(control) {
|
|
1331
|
+
let ancestor = control?.parentElement?.closest?.("details") ?? null;
|
|
1332
|
+
while (ancestor) {
|
|
1333
|
+
ancestor.open = true;
|
|
1334
|
+
ancestor = ancestor.parentElement?.closest?.("details") ?? null;
|
|
1335
|
+
}
|
|
1336
|
+
}
|
|
1256
1337
|
function bindFieldCardInteractions(root, controller) {
|
|
1257
1338
|
root.querySelectorAll("[data-testid='use-proposed']").forEach((button) => {
|
|
1258
1339
|
button.addEventListener("click", () => {
|
|
@@ -1312,12 +1393,25 @@ function bindFieldCardInteractions(root, controller) {
|
|
|
1312
1393
|
const note = controller.currentSession().notesByItemName[itemName]?.trim() ?? "";
|
|
1313
1394
|
const field = button.closest("[data-testid='review-field']");
|
|
1314
1395
|
const textarea = field?.querySelector("[data-testid='reviewer-note']");
|
|
1396
|
+
const errorEl = field?.querySelector("[data-testid='decision-error']");
|
|
1315
1397
|
if (!note) {
|
|
1316
|
-
|
|
1317
|
-
|
|
1398
|
+
// Say it where the button is, then open the way to the input that
|
|
1399
|
+
// satisfies it. Focusing (and validating) a control inside a collapsed
|
|
1400
|
+
// <details> is a no-op the reviewer never sees (kontourai/survey#208).
|
|
1401
|
+
if (errorEl) {
|
|
1402
|
+
errorEl.textContent = COULD_NOT_CONFIRM_REASON_REQUIRED;
|
|
1403
|
+
errorEl.hidden = false;
|
|
1404
|
+
}
|
|
1405
|
+
textarea?.setCustomValidity?.(COULD_NOT_CONFIRM_REASON_REQUIRED);
|
|
1406
|
+
revealControl(textarea);
|
|
1318
1407
|
textarea?.focus();
|
|
1408
|
+
textarea?.reportValidity?.();
|
|
1319
1409
|
return;
|
|
1320
1410
|
}
|
|
1411
|
+
if (errorEl) {
|
|
1412
|
+
errorEl.textContent = "";
|
|
1413
|
+
errorEl.hidden = true;
|
|
1414
|
+
}
|
|
1321
1415
|
textarea?.setCustomValidity?.("");
|
|
1322
1416
|
controller.setDecision(itemName, "could-not-confirm");
|
|
1323
1417
|
controller.renderCurrentState();
|
|
@@ -1332,6 +1426,13 @@ function bindFieldCardInteractions(root, controller) {
|
|
|
1332
1426
|
root.querySelectorAll("[data-testid='reviewer-note']").forEach((textarea) => {
|
|
1333
1427
|
textarea.addEventListener("input", () => {
|
|
1334
1428
|
textarea.setCustomValidity?.("");
|
|
1429
|
+
const decisionError = textarea
|
|
1430
|
+
.closest("[data-testid='review-field']")
|
|
1431
|
+
?.querySelector("[data-testid='decision-error']");
|
|
1432
|
+
if (decisionError) {
|
|
1433
|
+
decisionError.textContent = "";
|
|
1434
|
+
decisionError.hidden = true;
|
|
1435
|
+
}
|
|
1335
1436
|
const itemName = textarea.dataset.itemName ?? "";
|
|
1336
1437
|
controller.updateReviewerNote(itemName, textarea.value);
|
|
1337
1438
|
refreshAuditPayloadForItem(textarea, controller, itemName);
|
|
@@ -1403,9 +1504,9 @@ function bindHistoryExpanders(root) {
|
|
|
1403
1504
|
});
|
|
1404
1505
|
});
|
|
1405
1506
|
}
|
|
1406
|
-
function fieldItemClamped(label, value, extraClass = "") {
|
|
1507
|
+
function fieldItemClamped(key, label, value, extraClass = "") {
|
|
1407
1508
|
return `
|
|
1408
|
-
<div class="kv ${extraClass}">
|
|
1509
|
+
<div class="kv ${extraClass}" data-audit-row="${key}">
|
|
1409
1510
|
<dt class="field-label">${escapeHtml(label)}</dt>
|
|
1410
1511
|
<div class="excerpt-clamp" data-clamp>
|
|
1411
1512
|
<dd class="field-value">${escapeHtml(value)}</dd>
|
|
@@ -1414,14 +1515,26 @@ function fieldItemClamped(label, value, extraClass = "") {
|
|
|
1414
1515
|
</div>
|
|
1415
1516
|
`;
|
|
1416
1517
|
}
|
|
1417
|
-
function fieldItem(label, value, extraClass = "") {
|
|
1518
|
+
function fieldItem(key, label, value, extraClass = "") {
|
|
1418
1519
|
return `
|
|
1419
|
-
<div class="kv ${extraClass}">
|
|
1520
|
+
<div class="kv ${extraClass}" data-audit-row="${key}">
|
|
1420
1521
|
<dt class="field-label">${escapeHtml(label)}</dt>
|
|
1421
1522
|
<dd class="field-value">${escapeHtml(value)}</dd>
|
|
1422
1523
|
</div>
|
|
1423
1524
|
`;
|
|
1424
1525
|
}
|
|
1526
|
+
/**
|
|
1527
|
+
* A row for a fact the card renders in more than one place. Rendered only if
|
|
1528
|
+
* this card has not already printed the SAME property of the SAME record with
|
|
1529
|
+
* the same value; see {@link AuditFactTrace}.
|
|
1530
|
+
*
|
|
1531
|
+
* Only the four genuinely repeated placements go through here. Every other row
|
|
1532
|
+
* has exactly one home and is rendered with {@link fieldItem} directly, so no
|
|
1533
|
+
* row can ever be suppressed by a fact it has nothing to do with.
|
|
1534
|
+
*/
|
|
1535
|
+
function placementItem(trace, fact, key, label, value, extraClass = "") {
|
|
1536
|
+
return trace.isRepeatPlacement(fact, value) ? "" : fieldItem(key, label, value, extraClass);
|
|
1537
|
+
}
|
|
1425
1538
|
function escapeHtml(value) {
|
|
1426
1539
|
return formatValue(value)
|
|
1427
1540
|
.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
|
}
|
|
@@ -2,6 +2,7 @@ import { type DeriveReviewSessionApplyResultForSnapshotResult, type MapReviewWor
|
|
|
2
2
|
import { type ReviewDecisionModeIssue } from "./producer-decision-mode.js";
|
|
3
3
|
import { type ReviewSessionReplayIssue } from "./review-session-replay.js";
|
|
4
4
|
import type { ReviewDecision, ReviewSessionEvent } from "../review-resource.js";
|
|
5
|
+
import { type ReviewQueueBinding } from "./queue-binding.js";
|
|
5
6
|
import { type ReviewQueueSessionState } from "./review-queue-session.js";
|
|
6
7
|
export interface ServerReviewSessionRecord {
|
|
7
8
|
readonly sessionName: string;
|
|
@@ -56,6 +57,19 @@ export interface DeriveServerReviewSessionApplyResultOptions {
|
|
|
56
57
|
readonly currentSnapshot?: ReviewQueueSessionState;
|
|
57
58
|
readonly currentEventCount?: number;
|
|
58
59
|
readonly requiredResolvedItems?: ReviewSessionApplyResolutionRequirement;
|
|
60
|
+
/**
|
|
61
|
+
* The queue binding taken when this session opened (see
|
|
62
|
+
* ./queue-binding.ts). When present, the apply derivation refuses unless the
|
|
63
|
+
* record's snapshot — and the caller's current snapshot, when supplied —
|
|
64
|
+
* still matches the bound bytes and the bound item set, both directions.
|
|
65
|
+
*
|
|
66
|
+
* This is the check the record's own `snapshotHash` cannot provide: a record
|
|
67
|
+
* rebuilt from a mutated snapshot carries a hash of the mutated bytes and
|
|
68
|
+
* agrees with itself. The binding's authority is that it was written earlier,
|
|
69
|
+
* at queue construction, and carried unchanged — so it MUST come from the
|
|
70
|
+
* consumer's storage, not be recomputed at call time.
|
|
71
|
+
*/
|
|
72
|
+
readonly binding?: ReviewQueueBinding;
|
|
59
73
|
}
|
|
60
74
|
export declare function createServerReviewSessionRecord(options: CreateServerReviewSessionRecordOptions): ServerReviewSessionRecord;
|
|
61
75
|
export declare function hashReviewSessionSnapshot(snapshot: ReviewQueueSessionState): string;
|
|
@@ -2,6 +2,7 @@ import { createHash } from "node:crypto";
|
|
|
2
2
|
import { deriveReviewSessionApplyResultForSnapshot, mapReviewWorkbenchResultsToApplyActions, ReviewApplyActionMappingError, } from "./review-workbench.js";
|
|
3
3
|
import { validateReviewDecisionMode, } from "./producer-decision-mode.js";
|
|
4
4
|
import { validateReviewSessionEventsForSnapshot, } from "./review-session-replay.js";
|
|
5
|
+
import { assertReviewQueueBinding } from "./queue-binding.js";
|
|
5
6
|
import { replayReviewSessionEvents } from "./review-queue-session.js";
|
|
6
7
|
import { canonicalJson } from "./canonical.js";
|
|
7
8
|
export class StaleServerReviewSessionError extends Error {
|
|
@@ -92,6 +93,12 @@ export function assertServerReviewSessionEvents(record, events) {
|
|
|
92
93
|
}
|
|
93
94
|
}
|
|
94
95
|
export function deriveServerReviewSessionApplyResult(options) {
|
|
96
|
+
if (options.binding) {
|
|
97
|
+
assertReviewQueueBinding(options.binding, options.record.snapshot, { sessionName: options.record.sessionName });
|
|
98
|
+
if (options.currentSnapshot) {
|
|
99
|
+
assertReviewQueueBinding(options.binding, options.currentSnapshot, { sessionName: options.record.sessionName });
|
|
100
|
+
}
|
|
101
|
+
}
|
|
95
102
|
assertServerReviewSessionFreshness(options.record, options.record.snapshot);
|
|
96
103
|
if (options.currentSnapshot) {
|
|
97
104
|
assertServerReviewSessionFreshness(options.record, options.currentSnapshot, options.currentEventCount);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kontourai/survey",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.4.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",
|
|
@@ -55,6 +55,7 @@
|
|
|
55
55
|
"verify": "npm run check:content-boundary && npm run check:decisions && npm run typecheck && npm test && npm run check:review-workbench-assets && npm run check:review-workbench && npm run test:browser && npm run test:browser:concurrent",
|
|
56
56
|
"check:content-boundary": "node --test tests/content-boundary-script.test.cjs && node scripts/check-content-boundary.cjs",
|
|
57
57
|
"check:decisions": "node scripts/check-decisions.cjs check",
|
|
58
|
+
"check:guards": "node scripts/check-guards.mjs",
|
|
58
59
|
"gen:decisions-index": "node scripts/check-decisions.cjs gen-index",
|
|
59
60
|
"freeze:adrs": "node scripts/freeze-adrs.mjs",
|
|
60
61
|
"sync:review-workbench-assets": "node scripts/sync-review-workbench-assets.cjs",
|