@kontourai/survey 1.1.0 → 1.1.1

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.
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Normalize a value into a deterministic, order-independent form: object keys
3
+ * sorted, `undefined`-valued keys dropped, Dates rendered as ISO strings.
4
+ */
5
+ export declare function canonicalize(value: unknown): unknown;
6
+ /** Deterministic JSON string for hashing or structural comparison. */
7
+ export declare function canonicalJson(value: unknown): string;
@@ -0,0 +1,32 @@
1
+ // Single canonical JSON serializer shared by the review-workbench structural
2
+ // comparison (`structuralEqual`) and the server-side snapshot hashing, so the
3
+ // same object always produces the same canonical string and hash.
4
+ //
5
+ // Previously two near-identical copies lived in review-workbench.ts and
6
+ // server-review-session.ts and disagreed on Date handling — the workbench copy
7
+ // turned a Date into `{}` (no own enumerable keys), while the server copy used
8
+ // the ISO string. That desynced `structuralEqual` from snapshot freshness/replay
9
+ // hashes for any object carrying a Date (audit 2026-06-28, ops#24).
10
+ /**
11
+ * Normalize a value into a deterministic, order-independent form: object keys
12
+ * sorted, `undefined`-valued keys dropped, Dates rendered as ISO strings.
13
+ */
14
+ export function canonicalize(value) {
15
+ if (value instanceof Date) {
16
+ return value.toISOString();
17
+ }
18
+ if (Array.isArray(value)) {
19
+ return value.map((entry) => canonicalize(entry));
20
+ }
21
+ if (value && typeof value === "object") {
22
+ const entries = Object.entries(value)
23
+ .filter(([, entryValue]) => entryValue !== undefined)
24
+ .sort(([left], [right]) => left.localeCompare(right));
25
+ return Object.fromEntries(entries.map(([key, entryValue]) => [key, canonicalize(entryValue)]));
26
+ }
27
+ return value;
28
+ }
29
+ /** Deterministic JSON string for hashing or structural comparison. */
30
+ export function canonicalJson(value) {
31
+ return JSON.stringify(canonicalize(value));
32
+ }
@@ -22,7 +22,7 @@ export function buildReviewCandidatePresentation(item, candidate, adapter = {},
22
22
  roleLabel: adapter.labelForCandidateRole?.(candidate.role, context) ?? defaultCandidateRoleLabel(candidate.role),
23
23
  valueLabel: targetLabel,
24
24
  valueText: adapter.summarizeValue?.(candidate.value, { ...context, value: candidate.value }) ?? formatValue(candidate.value),
25
- sourceLabel: "Source",
25
+ sourceLabel: "Source Reference",
26
26
  sourceText: sourceLink?.label ?? sourceRef,
27
27
  sourceLink,
28
28
  traceRefs: traceRefsForCandidate(item, candidate, adapter),
@@ -78,7 +78,7 @@ function traceRefsForCandidate(item, candidate, adapter) {
78
78
  kind: "claim",
79
79
  },
80
80
  {
81
- label: "Source ID",
81
+ label: "Raw Source ID",
82
82
  value: candidate.source.sourceId ?? candidate.source.sourceRef,
83
83
  kind: "source",
84
84
  },
@@ -92,7 +92,7 @@ function buildIntegrityPosture(item, candidate, projection) {
92
92
  };
93
93
  }
94
94
  function postureDisclaimer() {
95
- return "Survey records source and review posture for projection; it does not validate real-world truth.";
95
+ return "Survey records Raw Source, Source Reference, and review posture for projection; it does not validate real-world truth.";
96
96
  }
97
97
  function sourceAuthorityFromProducer(value) {
98
98
  if (!isRecord(value)) {
@@ -115,7 +115,7 @@ function portableAuthorityTrace(value) {
115
115
  return {
116
116
  status: "empty",
117
117
  label: "Empty / not provided",
118
- detail: "No portable authority trace is present. SourceAuthority metadata is shown as source evidence and is not promoted into authorityTrace.",
118
+ detail: "No portable authority trace is present. SourceAuthority metadata is shown with Raw Source and Source Reference posture and is not promoted into authorityTrace.",
119
119
  };
120
120
  }
121
121
  function isRecord(value) {
@@ -1,3 +1,4 @@
1
+ import { canonicalJson } from "./canonical.js";
1
2
  import { candidateForDecision, buildReviewSessionEvent, buildReviewSessionEvents, buildReviewSessionResource, currentReviewItem, currentReviewWorkbenchState, defaultReviewSessionName, deriveQueueRowStatus, initialReviewQueueSessionState, nextUnresolvedItemName, replayReviewSessionEvents, reviewSessionSummary, reviewWorkbenchSessionStorageKey, selectedCandidateRole, workbenchDecisionDefinitions, } from "./review-queue-session.js";
2
3
  import { validateReviewSessionEventsForSnapshot, } from "./review-session-replay.js";
3
4
  import { buildSurfaceProjectionPreview, formatValue, } from "./review-surface-preview.js";
@@ -229,19 +230,7 @@ function matchingSelectedCandidate(item, result) {
229
230
  : undefined;
230
231
  }
231
232
  function structuralEqual(left, right) {
232
- return JSON.stringify(canonicalJson(left)) === JSON.stringify(canonicalJson(right));
233
- }
234
- function canonicalJson(value) {
235
- if (!value || typeof value !== "object") {
236
- return value;
237
- }
238
- if (Array.isArray(value)) {
239
- return value.map(canonicalJson);
240
- }
241
- return Object.fromEntries(Object.entries(value)
242
- .filter(([, entry]) => entry !== undefined)
243
- .sort(([left], [right]) => left.localeCompare(right))
244
- .map(([key, entry]) => [key, canonicalJson(entry)]));
233
+ return canonicalJson(left) === canonicalJson(right);
245
234
  }
246
235
  export function buildReviewWorkbenchResultsFromSession(session) {
247
236
  return session.items.flatMap((item) => {
@@ -653,7 +642,7 @@ function renderReviewFocus(state, presentationAdapter) {
653
642
  </div>
654
643
  <dl class="focus-evidence">
655
644
  ${fieldItem("Target", presentation.targetLabel)}
656
- ${fieldItem("Proposed source", proposed.source.sourceRef)}
645
+ ${fieldItem("Proposed Source Reference", proposed.source.sourceRef)}
657
646
  ${fieldItemClamped("Proposed excerpt", proposed.locator?.excerpt ?? "none", "excerpt")}
658
647
  </dl>
659
648
  </section>
@@ -745,7 +734,7 @@ function renderPendingSurfacePreview() {
745
734
  <span class="surface-summary-label">Surface preview</span>
746
735
  <span class="state-label">pending</span>
747
736
  </summary>
748
- <p class="preview-disclaimer">Choose a review decision to preview the source and review posture that would project toward Surface.</p>
737
+ <p class="preview-disclaimer">Choose a review decision to preview the Raw Source, Source Reference, and review posture that would project toward Surface.</p>
749
738
  </details>
750
739
  `;
751
740
  }
@@ -852,12 +841,12 @@ function renderCandidateHistory(preview) {
852
841
  function renderSourceEvidence(preview) {
853
842
  // Excerpt uses a 3-line clamp wrapper with a toggle button.
854
843
  const clampedExcerptHtml = fieldItemClamped("Excerpt", preview.sourceEvidence.excerpt, "excerpt");
855
- // Build source evidence section with clamped excerpt inline
844
+ // Build Raw Source section with clamped excerpt inline.
856
845
  return `
857
846
  <section class="preview-section" data-testid="surface-source-evidence">
858
- <h3>${escapeHtml("Source evidence")}</h3>
847
+ <h3>${escapeHtml("Raw Source")}</h3>
859
848
  <dl class="field-stack compact">
860
- ${fieldItem("Source URL", preview.sourceEvidence.sourceRef)}
849
+ ${fieldItem("Source Reference", preview.sourceEvidence.sourceRef)}
861
850
  ${clampedExcerptHtml}
862
851
  ${fieldItem("Extractor", preview.sourceEvidence.extractor)}
863
852
  ${fieldItem("Observed", preview.sourceEvidence.observedAt)}
@@ -868,7 +857,7 @@ function renderSourceEvidence(preview) {
868
857
  ].join("") : ""}
869
858
  </dl>
870
859
  ${renderReferenceDetails([
871
- ["Source ID", preview.sourceEvidence.sourceId],
860
+ ["Raw Source ID", preview.sourceEvidence.sourceId],
872
861
  ["Extraction ID", preview.sourceEvidence.extractionId],
873
862
  ])}
874
863
  </section>
@@ -1194,7 +1183,7 @@ function renderCandidateCard(candidate, state, presentationAdapter) {
1194
1183
  <p class="field-value">${escapeHtml(presentation.valueText)}</p>
1195
1184
  </div>
1196
1185
  <dl class="field-stack">
1197
- ${fieldItem("Source URL", presentation.sourceText)}
1186
+ ${fieldItem(presentation.sourceLabel, presentation.sourceText)}
1198
1187
  ${fieldItem("Locator", candidate.locator?.locator ?? candidate.locator?.scheme ?? "none")}
1199
1188
  ${fieldItemClamped("Excerpt", candidate.locator?.excerpt ?? "none", "excerpt")}
1200
1189
  ${fieldItem("Extraction confidence", confidence === undefined ? "unknown" : formatConfidence(confidence))}
@@ -1,6 +1,7 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { deriveReviewSessionApplyResultForSnapshot, } from "./review-workbench.js";
3
3
  import { validateReviewSessionEventsForSnapshot, } from "./review-session-replay.js";
4
+ import { canonicalJson } from "./canonical.js";
4
5
  export class StaleServerReviewSessionError extends Error {
5
6
  name = "StaleServerReviewSessionError";
6
7
  issues;
@@ -107,24 +108,6 @@ function staleIssuesForComparison(comparison) {
107
108
  }
108
109
  return issues;
109
110
  }
110
- function canonicalJson(value) {
111
- return JSON.stringify(canonicalize(value));
112
- }
113
- function canonicalize(value) {
114
- if (value instanceof Date) {
115
- return value.toISOString();
116
- }
117
- if (Array.isArray(value)) {
118
- return value.map((entry) => canonicalize(entry));
119
- }
120
- if (value && typeof value === "object") {
121
- const entries = Object.entries(value)
122
- .filter(([, entryValue]) => entryValue !== undefined)
123
- .sort(([left], [right]) => left.localeCompare(right));
124
- return Object.fromEntries(entries.map(([key, entryValue]) => [key, canonicalize(entryValue)]));
125
- }
126
- return value;
127
- }
128
111
  function sha256(value) {
129
112
  return createHash("sha256").update(value).digest("hex");
130
113
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kontourai/survey",
3
- "version": "1.1.0",
3
+ "version": "1.1.1",
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",