@kontourai/survey 1.17.0 → 1.18.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/README.md +9 -1
- package/dist/src/console/review-console-server.js +1 -1
- package/dist/src/extraction-envelope.d.ts +2 -0
- package/dist/src/extraction-envelope.js +9 -4
- package/dist/src/index.d.ts +3 -1
- package/dist/src/index.js +2 -1
- package/dist/src/review-workbench/extraction-inspector.d.ts +77 -0
- package/dist/src/review-workbench/extraction-inspector.js +198 -0
- package/dist/src/review-workbench/review-workbench-css.generated.js +19 -0
- package/dist/src/review-workbench/review-workbench-element.d.ts +4 -0
- package/dist/src/review-workbench/review-workbench-element.js +34 -0
- package/dist/src/review-workbench/review-workbench.css +19 -0
- package/dist/src/review-workbench/review-workbench.d.ts +1 -0
- package/dist/src/review-workbench/review-workbench.js +10 -6
- package/dist/src/review-workbench/review-workbench.standalone.css +19 -0
- package/dist/src/sha256.d.ts +2 -0
- package/dist/src/sha256.js +40 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -103,6 +103,8 @@ One observation, one chain: the page it came from, what the extractor read, who
|
|
|
103
103
|
|
|
104
104
|
Keep producer operational state outside Survey. Queue status, reviewer form state, retries, source caches, and product policy decisions belong in the producer's own data model. Survey carries only the portable evidence chain records needed by Surface.
|
|
105
105
|
|
|
106
|
+
Need Anthropic-backed `MappingProposer`/`UtteranceClaimExtractor` implementations for step 1? `@kontourai/survey/anthropic` ships production adapters as proposers-only — every output still goes through the normal review path. It's a separate subpath (not re-exported from the package root) so consumers without `@anthropic-ai/sdk` installed pay nothing.
|
|
107
|
+
|
|
106
108
|
When you build an `authorized-action` authorizing block outside the workbench, pair `buildAuthorizedActionAuthorizing` with `buildPromptRef({ module, component, version?, scheme? })` — `buildPromptRef` formats a well-formed, versioned `promptRef` (bare `"review-workbench/decision-card@v1"` or scheme-prefixed `"survey://<module>/<component>@v1"`) that `buildAuthorizedActionAuthorizing` accepts directly, instead of hand-formatting the string.
|
|
107
109
|
|
|
108
110
|
## Review lifecycle
|
|
@@ -201,12 +203,18 @@ npx survey-review-mcp --session path/to/session.json
|
|
|
201
203
|
|
|
202
204
|
Three tools: `survey_review_queue` (queue state), `survey_review_item` (item detail), and `survey_review_decide` (record a decision). Each queue and item call includes an embedded, fully self-contained review card with Accept / Hold / Reject / Could not confirm actions. See [docs/review-mcp.md](docs/review-mcp.md).
|
|
203
205
|
|
|
204
|
-
|
|
206
|
+
The embedded card is a fixed-size resource (a preferred frame of 420×560) that follows the host's light/dark `prefers-color-scheme`; it does not resize its layout at other breakpoints. For a card that adapts its layout to available width — stacking the current → proposed diff vertically and growing tap targets via CSS container queries below 620px/480px, with a viewport `@media` fallback — embed the [Review Workbench](#review-workbench-embed) directly instead.
|
|
205
207
|
|
|
206
208
|
Server code persisting browser-submitted review events should use `persistReviewSessionEvents`, then `deriveReviewSessionApplyResultForSnapshot` (or the composed `deriveServerReviewSessionApplyResult` from `@kontourai/survey/review-workbench/server-review-session`) before applying product policy — write results derive from pre-decision snapshots plus persisted events, never from browser-computed decisions.
|
|
207
209
|
|
|
208
210
|
The [Consumer Integration Guide](docs/consumer-integration-guide.md) covers the full path from `ReviewItem` construction through persisted review events, exported results, Surface projection, and the full `--k-*` theming token list. Test-covered examples are under [`examples/review-workbench/`](examples/review-workbench/). To run the standalone demo locally, see [Review Workbench Prototype](docs/review-workbench-prototype.md).
|
|
209
211
|
|
|
212
|
+
Portable extraction imports can also attach a source-linked, read-only inspector
|
|
213
|
+
to the same workbench. It verifies resolved artifact identity before showing
|
|
214
|
+
exact highlights, keeps unresolved material visibly non-grounded, and exports
|
|
215
|
+
with source text and excerpts redacted by default. See
|
|
216
|
+
[Extraction Envelope Import](docs/extraction-envelope-import.md#source-linked-inspection).
|
|
217
|
+
|
|
210
218
|
|
|
211
219
|
## Standalone review console
|
|
212
220
|
|
|
@@ -309,7 +309,7 @@ body {
|
|
|
309
309
|
</button>
|
|
310
310
|
</div>
|
|
311
311
|
</header>
|
|
312
|
-
<main id="review-workbench" class="workbench" data-testid="review-workbench"></main>
|
|
312
|
+
<main id="review-workbench" class="workbench survey-workbench-embed" data-testid="review-workbench"></main>
|
|
313
313
|
<script type="module">
|
|
314
314
|
import { mountReviewWorkbench, replayReviewSessionEvents, defaultReviewSessionName, buildReviewSessionEvents } from "${workbenchJsPath}";
|
|
315
315
|
|
|
@@ -162,3 +162,5 @@ export declare function buildReviewItemsFromExtractionEnvelopeImport(record: Ext
|
|
|
162
162
|
export declare function exportExtractionEnvelopeImport(record: ExtractionEnvelopeImport): string;
|
|
163
163
|
export declare function reimportExtractionEnvelope(serialized: string): ExtractionEnvelopeImport;
|
|
164
164
|
export declare function createExtractionEnvelopeResolutionIdentity(record: ExtractionEnvelopeImport, proposalIndex: number): ExtractionEnvelopeResolutionIdentity;
|
|
165
|
+
/** Validate and defensively clone a persisted or browser-delivered import. */
|
|
166
|
+
export declare function validateExtractionEnvelopeImport(value: unknown): ExtractionEnvelopeImport;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { sha256Hex } from "./sha256.js";
|
|
2
2
|
import { reviewResourceApiVersion } from "./review-resource.js";
|
|
3
3
|
import { canonicalJson } from "./review-workbench/canonical.js";
|
|
4
4
|
/** The upstream-owned portable extraction-result wire identifiers accepted by this adapter. */
|
|
@@ -62,7 +62,7 @@ export function createExtractionEnvelopeResolutionIdentity(record, proposalIndex
|
|
|
62
62
|
throw new Error(`Extraction envelope import ${record.metadata.name} has no proposal at index ${proposalIndex}.`);
|
|
63
63
|
}
|
|
64
64
|
const base = identityHash(identityInputs(record, record.spec.envelope.result.proposals[proposalIndex], proposalIndex));
|
|
65
|
-
const nonce = randomUUID();
|
|
65
|
+
const nonce = globalThis.crypto.randomUUID();
|
|
66
66
|
return { evidenceId: `survey.extraction.${base}.resolution-evidence.${nonce}`, eventId: `survey.extraction.${base}.resolution-event.${nonce}` };
|
|
67
67
|
}
|
|
68
68
|
function buildReviewItem(record, proposal, index) {
|
|
@@ -165,6 +165,11 @@ function validateImport(value) {
|
|
|
165
165
|
if (canonicalJson(status.diagnostics) !== canonicalJson(expected) || status.state !== (expected.length ? "unresolved" : "grounded"))
|
|
166
166
|
throw new Error("Import status does not match envelope state.");
|
|
167
167
|
}
|
|
168
|
+
/** Validate and defensively clone a persisted or browser-delivered import. */
|
|
169
|
+
export function validateExtractionEnvelopeImport(value) {
|
|
170
|
+
validateImport(value);
|
|
171
|
+
return cloneJson(value);
|
|
172
|
+
}
|
|
168
173
|
function validateEnvelope(input) {
|
|
169
174
|
jsonSafe(input, "Portable extraction envelope");
|
|
170
175
|
const e = obj(input, "envelope");
|
|
@@ -251,7 +256,7 @@ function validateOccurrence(input, start, end) { const o = obj(input, "occurrenc
|
|
|
251
256
|
function validateArtifact(input) { const a = obj(input, "preparedArtifact"); exact(a, ["format", "version", "digest", "ref", "preparationMode", "preparationVersion", "contentLength"], "preparedArtifact", ["sourceSnapshotRef"]); if (a.format !== "traverse-prepared-artifact" || a.version !== 1)
|
|
252
257
|
throw new Error("prepared artifact format is invalid."); digest(a.digest, "artifact.digest", false); if (!PREPARATION_MODES.has(a.preparationMode))
|
|
253
258
|
throw new Error("artifact.preparationMode is invalid."); nonEmpty(a.preparationVersion, "artifact.preparationVersion"); integer(a.contentLength, "artifact.contentLength"); if (a.sourceSnapshotRef !== undefined)
|
|
254
|
-
wireNonEmpty(a.sourceSnapshotRef, "artifact.sourceSnapshotRef"); const binding = JSON.stringify({ format: a.format, version: a.version, digest: a.digest, preparationMode: a.preparationMode, preparationVersion: a.preparationVersion, contentLength: a.contentLength, sourceSnapshotRef: a.sourceSnapshotRef ?? null }); const expectedRef = `traverse-prepared-artifact:v1:sha256:${
|
|
259
|
+
wireNonEmpty(a.sourceSnapshotRef, "artifact.sourceSnapshotRef"); const binding = JSON.stringify({ format: a.format, version: a.version, digest: a.digest, preparationMode: a.preparationMode, preparationVersion: a.preparationVersion, contentLength: a.contentLength, sourceSnapshotRef: a.sourceSnapshotRef ?? null }); const expectedRef = `traverse-prepared-artifact:v1:sha256:${sha256Hex(binding)}`; if (a.ref !== expectedRef)
|
|
255
260
|
throw new Error("prepared artifact ref does not match its identity binding."); return cloneJson(a); }
|
|
256
261
|
function validateArtifactState(input, artifact) { if (!artifact)
|
|
257
262
|
throw new Error("prepared artifact state requires prepared artifact."); const s = obj(input, "preparedArtifactState"); const status = s.status; if (status === "digest-mismatch") {
|
|
@@ -358,7 +363,7 @@ function stableIdentity(v, subject) { wireNonEmpty(v, subject); if (!STABLE_IDEN
|
|
|
358
363
|
function preparedReference(v, subject) { safeReference(v, subject); if (!/^traverse-prepared-artifact:v1:sha256:[a-f0-9]{64}$/.test(v))
|
|
359
364
|
throw new Error(`${subject} must be a prepared-artifact reference.`); }
|
|
360
365
|
function cloneJson(v) { return JSON.parse(JSON.stringify(v)); }
|
|
361
|
-
function identityHash(v) { return
|
|
366
|
+
function identityHash(v) { return sha256Hex(canonicalJson(v)).slice(0, 20); }
|
|
362
367
|
function jsonSafe(v, subject, seen = new Set()) { if (v === null || typeof v === "boolean")
|
|
363
368
|
return; if (typeof v === "string") {
|
|
364
369
|
if (!isWellFormedUnicode(v))
|
package/dist/src/index.d.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
export type { CandidateSetStatus, Candidate, CandidateSet, ClaimTarget, EscalationDimension, EscalationRecord, Extraction, Interpretation, LocatorScheme, ProvenanceResolution, RawSource, RawSourceKind, ReviewAuthorizing, ReviewAuthorizingAuthorizedAction, ReviewAuthorizingExchange, ReviewAuthorizingExplicitStatement, ReviewAuthorizingKind, ReviewOutcome, ReviewResolution, ReviewStatus, SurveyInput, } from "./types.js";
|
|
2
2
|
export { SURVEY_INPUT_CONTRACT_VERSION } from "./types.js";
|
|
3
3
|
export { reviewResourceApiVersion } from "./review-resource.js";
|
|
4
|
-
export { buildReviewItemsFromExtractionEnvelopeImport, createExtractionEnvelopeResolutionIdentity, exportExtractionEnvelopeImport, extractionEnvelopeImportApiVersion, importExtractionEnvelope, portableExtractionResultFormat, portableExtractionResultVersion, reimportExtractionEnvelope, } from "./extraction-envelope.js";
|
|
4
|
+
export { buildReviewItemsFromExtractionEnvelopeImport, createExtractionEnvelopeResolutionIdentity, exportExtractionEnvelopeImport, extractionEnvelopeImportApiVersion, importExtractionEnvelope, portableExtractionResultFormat, portableExtractionResultVersion, reimportExtractionEnvelope, validateExtractionEnvelopeImport, } from "./extraction-envelope.js";
|
|
5
|
+
export { buildExtractionInspectorModel, exportExtractionInspector, filterExtractionInspectorCandidates, } from "./review-workbench/extraction-inspector.js";
|
|
6
|
+
export type { ExtractionAlignmentState, ArtifactUnavailableCode, ExtractionInspectorCandidate, ExtractionInspectorEntry, ExtractionInspectorExportOptions, ExtractionInspectorFilters, ExtractionInspectorInput, ExtractionInspectorModel, ExtractionInspectorSource, ResolvedExtractionArtifact, } from "./review-workbench/extraction-inspector.js";
|
|
5
7
|
export type { ExtractionEnvelopeImport, ExtractionEnvelopeImportDiagnostic, ExtractionEnvelopeImportOptions, ExtractionEnvelopeImportResult, ExtractionEnvelopeResolutionIdentity, PortableExtractionOccurrence, PortableExtractionProposal, PortableExtractionResultEnvelope, PortablePreparedArtifactState, } from "./extraction-envelope.js";
|
|
6
8
|
export type { CandidateRole, ClaimTargetHint, ProducerPolicy, ExtractionReference, ResourceEnvelope, ResourceMetadata, ReviewActor, ReviewCandidate, ReviewDecision, ReviewDecisionMode, ReviewDecisionSpec, ReviewDecisionStatus, ReviewItem, ReviewItemSpec, ReviewItemStatus, ReviewLocator, ReviewResource, ReviewResourceApiVersion, ReviewResourceKind, ReviewSession, ReviewSessionEvent, ReviewSessionEventSpec, ReviewSessionEventStatus, ReviewSessionEventType, ReviewSessionSpec, ReviewSessionStatus, ReviewValueDescriptor, ReviewValueType, SourceReference, SurveyRecordProjectionHint, } from "./review-resource.js";
|
|
7
9
|
export { candidateReviewRecord, candidateSetStatusFor, SurveyInputBuilder } from "./builder.js";
|
package/dist/src/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export { SURVEY_INPUT_CONTRACT_VERSION } from "./types.js";
|
|
2
2
|
export { reviewResourceApiVersion } from "./review-resource.js";
|
|
3
|
-
export { buildReviewItemsFromExtractionEnvelopeImport, createExtractionEnvelopeResolutionIdentity, exportExtractionEnvelopeImport, extractionEnvelopeImportApiVersion, importExtractionEnvelope, portableExtractionResultFormat, portableExtractionResultVersion, reimportExtractionEnvelope, } from "./extraction-envelope.js";
|
|
3
|
+
export { buildReviewItemsFromExtractionEnvelopeImport, createExtractionEnvelopeResolutionIdentity, exportExtractionEnvelopeImport, extractionEnvelopeImportApiVersion, importExtractionEnvelope, portableExtractionResultFormat, portableExtractionResultVersion, reimportExtractionEnvelope, validateExtractionEnvelopeImport, } from "./extraction-envelope.js";
|
|
4
|
+
export { buildExtractionInspectorModel, exportExtractionInspector, filterExtractionInspectorCandidates, } from "./review-workbench/extraction-inspector.js";
|
|
4
5
|
export { candidateReviewRecord, candidateSetStatusFor, SurveyInputBuilder } from "./builder.js";
|
|
5
6
|
export { reviewedCandidateResolution } from "./reviewed-candidate-resolution.js";
|
|
6
7
|
export { reviewedCurrentProposedResolution } from "./reviewed-current-proposed-resolution.js";
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { type ExtractionEnvelopeImportResult } from "../extraction-envelope.js";
|
|
2
|
+
export type ExtractionAlignmentState = "aligned" | "excerpt-mismatch" | "artifact-unavailable" | "digest-mismatch";
|
|
3
|
+
export type ArtifactUnavailableCode = "not-found" | "storage-error" | "access-denied" | "invalid-artifact" | "unknown";
|
|
4
|
+
export type ResolvedExtractionArtifact = {
|
|
5
|
+
status: "available";
|
|
6
|
+
text: string;
|
|
7
|
+
actualDigest: string;
|
|
8
|
+
} | {
|
|
9
|
+
status: "unavailable";
|
|
10
|
+
code: ArtifactUnavailableCode;
|
|
11
|
+
} | {
|
|
12
|
+
status: "digest-mismatch";
|
|
13
|
+
actualDigest: string;
|
|
14
|
+
};
|
|
15
|
+
export interface ExtractionInspectorEntry {
|
|
16
|
+
/** Result returned by importExtractionEnvelope; includes authoritative ReviewItem identities. */
|
|
17
|
+
importResult: ExtractionEnvelopeImportResult;
|
|
18
|
+
artifact: ResolvedExtractionArtifact;
|
|
19
|
+
/** Optional producer-declared pass label, kept outside the upstream envelope. */
|
|
20
|
+
pass?: string;
|
|
21
|
+
}
|
|
22
|
+
export type ExtractionInspectorInput = ExtractionInspectorEntry | {
|
|
23
|
+
imports: ExtractionInspectorEntry[];
|
|
24
|
+
};
|
|
25
|
+
export interface ExtractionInspectorCandidate {
|
|
26
|
+
id: string;
|
|
27
|
+
sourceKey: string;
|
|
28
|
+
reviewItemName: string;
|
|
29
|
+
proposalIndex: number;
|
|
30
|
+
field: string;
|
|
31
|
+
provider: string;
|
|
32
|
+
model?: string;
|
|
33
|
+
attempt: string;
|
|
34
|
+
pass?: string;
|
|
35
|
+
valueType: string;
|
|
36
|
+
inferenceType: "explicit" | "inferred";
|
|
37
|
+
start: number;
|
|
38
|
+
end: number;
|
|
39
|
+
excerpt: string;
|
|
40
|
+
alignment: ExtractionAlignmentState;
|
|
41
|
+
}
|
|
42
|
+
export interface ExtractionInspectorSource {
|
|
43
|
+
key: string;
|
|
44
|
+
importName: string;
|
|
45
|
+
artifactRef?: string;
|
|
46
|
+
expectedDigest?: string;
|
|
47
|
+
actualDigest?: string;
|
|
48
|
+
artifactText?: string;
|
|
49
|
+
alignment: ExtractionAlignmentState;
|
|
50
|
+
message: string;
|
|
51
|
+
}
|
|
52
|
+
export interface ExtractionInspectorModel {
|
|
53
|
+
sources: ExtractionInspectorSource[];
|
|
54
|
+
candidates: ExtractionInspectorCandidate[];
|
|
55
|
+
}
|
|
56
|
+
export interface ExtractionInspectorFilters {
|
|
57
|
+
field?: string;
|
|
58
|
+
provider?: string;
|
|
59
|
+
model?: string;
|
|
60
|
+
attempt?: string;
|
|
61
|
+
pass?: string;
|
|
62
|
+
inferenceType?: "explicit" | "inferred";
|
|
63
|
+
alignment?: ExtractionAlignmentState;
|
|
64
|
+
}
|
|
65
|
+
export interface ExtractionInspectorExportOptions {
|
|
66
|
+
includePreparedText?: boolean;
|
|
67
|
+
includeExcerpts?: boolean;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Build a read-only view from one or more results already produced by Survey's
|
|
71
|
+
* public import boundary. The function rechecks the result/ReviewItem binding
|
|
72
|
+
* and fails closed if mutable caller data has drifted since import.
|
|
73
|
+
*/
|
|
74
|
+
export declare function buildExtractionInspectorModel(input: ExtractionInspectorInput): ExtractionInspectorModel;
|
|
75
|
+
export declare function filterExtractionInspectorCandidates(model: ExtractionInspectorModel, filters: ExtractionInspectorFilters): ExtractionInspectorCandidate[];
|
|
76
|
+
export declare function exportExtractionInspector(model: ExtractionInspectorModel, options?: ExtractionInspectorExportOptions): string;
|
|
77
|
+
export declare function mountExtractionInspector(container: HTMLElement, model: ExtractionInspectorModel): () => void;
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import { buildReviewItemsFromExtractionEnvelopeImport, validateExtractionEnvelopeImport } from "../extraction-envelope.js";
|
|
2
|
+
import { canonicalJson } from "./canonical.js";
|
|
3
|
+
import { sha256Hex } from "../sha256.js";
|
|
4
|
+
/**
|
|
5
|
+
* Build a read-only view from one or more results already produced by Survey's
|
|
6
|
+
* public import boundary. The function rechecks the result/ReviewItem binding
|
|
7
|
+
* and fails closed if mutable caller data has drifted since import.
|
|
8
|
+
*/
|
|
9
|
+
export function buildExtractionInspectorModel(input) {
|
|
10
|
+
const entries = "imports" in input ? input.imports : [input];
|
|
11
|
+
if (!Array.isArray(entries) || entries.length === 0)
|
|
12
|
+
throw new Error("Extraction inspector requires at least one validated import result.");
|
|
13
|
+
const sources = [];
|
|
14
|
+
const candidates = [];
|
|
15
|
+
const sourceKeys = new Set();
|
|
16
|
+
for (const [entryIndex, entry] of entries.entries()) {
|
|
17
|
+
if (!entry.importResult || typeof entry.importResult !== "object")
|
|
18
|
+
throw new Error("Invalid extraction import result.");
|
|
19
|
+
const record = validateExtractionEnvelopeImport(entry.importResult.record);
|
|
20
|
+
assertImportedResult(entry.importResult, record);
|
|
21
|
+
assertResolvedArtifact(entry.artifact);
|
|
22
|
+
const envelope = record.spec.envelope;
|
|
23
|
+
const prepared = envelope.result.preparedArtifact;
|
|
24
|
+
const sourceKey = `${record.metadata.producerNamespace}:${record.metadata.name}:${entryIndex}`;
|
|
25
|
+
if (sourceKeys.has(sourceKey))
|
|
26
|
+
throw new Error("Extraction inspector source identity collision.");
|
|
27
|
+
sourceKeys.add(sourceKey);
|
|
28
|
+
const source = sourceModel(sourceKey, record.metadata.name, prepared, record.status.state, entry.artifact);
|
|
29
|
+
sources.push(source);
|
|
30
|
+
const candidateStart = candidates.length;
|
|
31
|
+
envelope.result.proposals.forEach((proposal, proposalIndex) => {
|
|
32
|
+
const item = entry.importResult.reviewItems[proposalIndex];
|
|
33
|
+
if (!item)
|
|
34
|
+
return; // unresolved imports legitimately produce no ReviewItems
|
|
35
|
+
candidates.push(candidateModel(source, item.metadata.name, proposal, proposalIndex, envelope.result.provider, envelope.result.model, envelope.result.runId, entry.pass));
|
|
36
|
+
});
|
|
37
|
+
if (source.alignment === "excerpt-mismatch") {
|
|
38
|
+
delete source.artifactText;
|
|
39
|
+
for (let index = candidateStart; index < candidates.length; index += 1)
|
|
40
|
+
candidates[index].alignment = "excerpt-mismatch";
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return { sources, candidates };
|
|
44
|
+
}
|
|
45
|
+
function assertResolvedArtifact(artifact) {
|
|
46
|
+
if (!artifact || typeof artifact !== "object" || Array.isArray(artifact))
|
|
47
|
+
throw new Error("Invalid resolved extraction artifact.");
|
|
48
|
+
const keys = Object.keys(artifact).sort();
|
|
49
|
+
if (artifact.status === "available") {
|
|
50
|
+
if (keys.join(",") !== "actualDigest,status,text" || typeof artifact.text !== "string" || !/^[a-f0-9]{64}$/.test(artifact.actualDigest))
|
|
51
|
+
throw new Error("Invalid available extraction artifact.");
|
|
52
|
+
}
|
|
53
|
+
else if (artifact.status === "digest-mismatch") {
|
|
54
|
+
if (keys.join(",") !== "actualDigest,status" || !/^[a-f0-9]{64}$/.test(artifact.actualDigest))
|
|
55
|
+
throw new Error("Invalid digest-mismatch extraction artifact.");
|
|
56
|
+
}
|
|
57
|
+
else if (artifact.status === "unavailable") {
|
|
58
|
+
if (keys.join(",") !== "code,status" || !["not-found", "storage-error", "access-denied", "invalid-artifact", "unknown"].includes(artifact.code))
|
|
59
|
+
throw new Error("Invalid unavailable extraction artifact.");
|
|
60
|
+
}
|
|
61
|
+
else
|
|
62
|
+
throw new Error("Invalid resolved extraction artifact status.");
|
|
63
|
+
}
|
|
64
|
+
function assertImportedResult(result, record) {
|
|
65
|
+
if (!result || typeof result !== "object" || !result.record || !Array.isArray(result.reviewItems))
|
|
66
|
+
throw new Error("Invalid extraction import result.");
|
|
67
|
+
const { reviewItems } = result;
|
|
68
|
+
if (record.apiVersion !== "survey.kontourai.io/v1alpha1" || record.kind !== "ExtractionEnvelopeImport")
|
|
69
|
+
throw new Error("Invalid extraction import resource identity.");
|
|
70
|
+
if (!record.metadata?.name || !record.metadata.producerNamespace || !record.spec?.envelope?.result || !Array.isArray(record.spec.envelope.result.proposals))
|
|
71
|
+
throw new Error("Malformed extraction import result.");
|
|
72
|
+
const grounded = record.status?.state === "grounded";
|
|
73
|
+
if ((!grounded && reviewItems.length !== 0) || (grounded && reviewItems.length !== record.spec.envelope.result.proposals.length))
|
|
74
|
+
throw new Error("Extraction import ReviewItems do not match its grounding state.");
|
|
75
|
+
const canonicalItems = buildReviewItemsFromExtractionEnvelopeImport(record);
|
|
76
|
+
if (canonicalJson(reviewItems) !== canonicalJson(canonicalItems))
|
|
77
|
+
throw new Error("Extraction import ReviewItems do not match their canonical identities and bindings.");
|
|
78
|
+
reviewItems.forEach((item, index) => {
|
|
79
|
+
const proposal = record.spec.envelope.result.proposals[index];
|
|
80
|
+
const metadata = item.metadata?.producer?.["survey.kontourai.io/extraction-envelope"];
|
|
81
|
+
const candidate = item.spec?.candidates?.[0];
|
|
82
|
+
const binding = candidate?.producer?.["survey.kontourai.io/extraction-envelope"];
|
|
83
|
+
if (item.kind !== "ReviewItem" || !item.metadata.name || item.spec.candidates.length !== 1
|
|
84
|
+
|| metadata?.importName !== record.metadata.name || binding?.importName !== record.metadata.name
|
|
85
|
+
|| binding.proposalIndex !== index || binding.runId !== record.spec.envelope.result.runId || binding.provider !== record.spec.envelope.result.provider
|
|
86
|
+
|| item.spec.target !== proposal.fieldPath || candidate?.locator?.locator !== proposal.provenance.locator || candidate.locator.excerpt !== proposal.provenance.excerpt) {
|
|
87
|
+
throw new Error(`Extraction import ReviewItem ${index} is inconsistent with its validated proposal.`);
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
function sourceModel(key, importName, prepared, state, artifact) {
|
|
92
|
+
let alignment;
|
|
93
|
+
let message;
|
|
94
|
+
if (state !== "grounded" || artifact.status === "unavailable") {
|
|
95
|
+
alignment = "artifact-unavailable";
|
|
96
|
+
message = `Prepared artifact unavailable (${artifact.status === "unavailable" ? artifact.code : "invalid-artifact"}). Candidates are not grounded.`;
|
|
97
|
+
}
|
|
98
|
+
else if (artifact.status === "digest-mismatch" || !prepared || artifact.actualDigest !== prepared.digest
|
|
99
|
+
|| sha256Hex(artifact.text) !== artifact.actualDigest) {
|
|
100
|
+
alignment = "digest-mismatch";
|
|
101
|
+
message = "Prepared artifact digest does not match the extraction artifact. Candidates are not grounded.";
|
|
102
|
+
}
|
|
103
|
+
else if (artifact.text.length !== prepared.contentLength) {
|
|
104
|
+
alignment = "artifact-unavailable";
|
|
105
|
+
message = "Prepared artifact content has the wrong length. Candidates are not grounded.";
|
|
106
|
+
}
|
|
107
|
+
else {
|
|
108
|
+
alignment = "aligned";
|
|
109
|
+
message = "Prepared artifact identity verified. Exact source spans are available.";
|
|
110
|
+
}
|
|
111
|
+
return { key, importName, ...(prepared?.ref ? { artifactRef: prepared.ref } : {}), ...(prepared?.digest ? { expectedDigest: prepared.digest } : {}), ...("actualDigest" in artifact ? { actualDigest: artifact.actualDigest } : {}), ...(alignment === "aligned" && artifact.status === "available" ? { artifactText: artifact.text } : {}), alignment, message };
|
|
112
|
+
}
|
|
113
|
+
function candidateModel(source, reviewItemName, proposal, index, provider, model, attempt, pass) {
|
|
114
|
+
const match = /^chars:(\d+)-(\d+)$/.exec(proposal.provenance.locator);
|
|
115
|
+
if (!match)
|
|
116
|
+
throw new Error(`Extraction proposal ${index} has an invalid text span.`);
|
|
117
|
+
const start = Number(match[1]), end = Number(match[2]);
|
|
118
|
+
if (!Number.isSafeInteger(start) || !Number.isSafeInteger(end) || start < 0 || end < start)
|
|
119
|
+
throw new Error(`Extraction proposal ${index} has an invalid text span.`);
|
|
120
|
+
const alignment = source.alignment === "aligned" && source.artifactText.slice(start, end) !== proposal.provenance.excerpt ? "excerpt-mismatch" : source.alignment;
|
|
121
|
+
if (alignment === "excerpt-mismatch") {
|
|
122
|
+
source.alignment = alignment;
|
|
123
|
+
source.message = "One or more source spans do not match their recorded excerpts. Affected candidates are not grounded.";
|
|
124
|
+
}
|
|
125
|
+
return { id: `${source.key}:proposal:${index}`, sourceKey: source.key, reviewItemName, proposalIndex: index, field: proposal.fieldPath, provider, ...(model ? { model } : {}), attempt, ...(pass ? { pass } : {}), valueType: proposal.valueType ?? inferValueType(proposal.candidateValue), inferenceType: proposal.inferenceType ?? "inferred", start, end, excerpt: proposal.provenance.excerpt, alignment };
|
|
126
|
+
}
|
|
127
|
+
function inferValueType(value) { if (Array.isArray(value))
|
|
128
|
+
return "array"; if (value !== null && typeof value === "object")
|
|
129
|
+
return "object"; return typeof value === "number" ? "number" : typeof value === "boolean" ? "boolean" : "string"; }
|
|
130
|
+
export function filterExtractionInspectorCandidates(model, filters) {
|
|
131
|
+
return model.candidates.filter((c) => (!filters.field || c.field === filters.field) && (!filters.provider || c.provider === filters.provider) && (!filters.model || c.model === filters.model) && (!filters.attempt || c.attempt === filters.attempt) && (!filters.pass || c.pass === filters.pass) && (!filters.inferenceType || c.inferenceType === filters.inferenceType) && (!filters.alignment || c.alignment === filters.alignment));
|
|
132
|
+
}
|
|
133
|
+
export function exportExtractionInspector(model, options = {}) {
|
|
134
|
+
return canonicalJson({ apiVersion: "survey.kontourai.io/v1alpha1", kind: "ExtractionInspectorExport", spec: {
|
|
135
|
+
redaction: { preparedTextIncluded: options.includePreparedText === true, excerptsIncluded: options.includeExcerpts === true },
|
|
136
|
+
sources: model.sources.map(({ artifactText, message: _message, ...source }) => ({ ...source, preparedText: options.includePreparedText ? artifactText ?? null : "[redacted]" })),
|
|
137
|
+
candidates: model.candidates.map((candidate) => ({ ...candidate, excerpt: options.includeExcerpts ? candidate.excerpt : "[redacted]" })),
|
|
138
|
+
} });
|
|
139
|
+
}
|
|
140
|
+
export function mountExtractionInspector(container, model) {
|
|
141
|
+
const root = document.createElement("section");
|
|
142
|
+
root.className = "extraction-inspector";
|
|
143
|
+
root.setAttribute("aria-label", "Source-linked extraction inspector");
|
|
144
|
+
const options = (values) => [...new Set(values.filter((v) => Boolean(v)))].map((v) => `<option value="${escapeHtml(v)}">${escapeHtml(v)}</option>`).join("");
|
|
145
|
+
root.innerHTML = `<div class="inspector-heading"><div><p class="eyebrow">Source inspector</p><h2>Extraction evidence</h2></div><div class="inspector-postures" aria-live="polite"></div></div><div class="inspector-filters" aria-label="Extraction filters">${filterSelect("field", "Field", options(model.candidates.map(c => c.field)))}${filterSelect("provider", "Provider", options(model.candidates.map(c => c.provider)))}${filterSelect("model", "Model", options(model.candidates.map(c => c.model)))}${filterSelect("attempt", "Attempt", options(model.candidates.map(c => c.attempt)))}${filterSelect("pass", "Pass", options(model.candidates.map(c => c.pass)))}${filterSelect("inferenceType", "Type origin", '<option value="explicit">explicit</option><option value="inferred">inferred</option>')}${filterSelect("alignment", "Alignment", options(model.candidates.map(c => c.alignment)))}</div><div class="inspector-layout"><ol class="inspector-candidates" aria-label="Extraction candidates"></ol><div class="inspector-sources"></div></div>`;
|
|
146
|
+
container.appendChild(root);
|
|
147
|
+
const list = root.querySelector("ol"), sourcesRoot = root.querySelector(".inspector-sources"), postures = root.querySelector(".inspector-postures");
|
|
148
|
+
const filters = {};
|
|
149
|
+
const render = () => {
|
|
150
|
+
const visible = filterExtractionInspectorCandidates(model, filters);
|
|
151
|
+
list.innerHTML = visible.map(c => `<li><button type="button" class="inspector-candidate" id="candidate-${safeId(c.id)}" data-candidate-id="${escapeHtml(c.id)}" aria-controls="highlight-${safeId(c.id)}"><strong>${escapeHtml(c.field)}</strong><span>${escapeHtml(c.provider)}${c.model ? ` / ${escapeHtml(c.model)}` : ""}</span><span>${escapeHtml(c.inferenceType)} ${escapeHtml(c.valueType)} · ${escapeHtml(c.alignment)}</span></button></li>`).join("") || "<li>No candidates match these filters.</li>";
|
|
152
|
+
postures.innerHTML = model.sources.map(s => `<div class="inspector-posture ${s.alignment}" role="status"><strong>${escapeHtml(s.importName)}: ${escapeHtml(s.alignment)}</strong><span>${escapeHtml(s.message)}</span></div>`).join("");
|
|
153
|
+
sourcesRoot.innerHTML = model.sources.map(s => { const candidates = visible.filter(c => c.sourceKey === s.key); return `<div class="inspector-source" aria-label="Prepared source for ${escapeHtml(s.importName)}"><h3>${escapeHtml(s.importName)}</h3><pre tabindex="0">${s.artifactText === undefined ? `<span class="source-unavailable">${escapeHtml(s.message)}</span>` : renderSource(s.artifactText, candidates)}</pre></div>`; }).join("");
|
|
154
|
+
};
|
|
155
|
+
root.querySelectorAll("select").forEach(select => select.addEventListener("change", event => { event.stopPropagation(); const key = select.dataset.filter; if (select.value)
|
|
156
|
+
filters[key] = select.value;
|
|
157
|
+
else
|
|
158
|
+
delete filters[key]; render(); }));
|
|
159
|
+
const activateCandidate = (id) => { const candidate = model.candidates.find(c => c.id === id); if (!candidate)
|
|
160
|
+
return; root.dispatchEvent(new CustomEvent("survey-extraction-candidate-activate", { bubbles: true, composed: true, detail: { candidateId: id, reviewItemName: candidate.reviewItemName } })); };
|
|
161
|
+
root.addEventListener("click", event => { const candidate = event.target.closest("button[data-candidate-id]"); if (candidate) {
|
|
162
|
+
event.preventDefault();
|
|
163
|
+
event.stopPropagation();
|
|
164
|
+
activateCandidate(candidate.dataset.candidateId);
|
|
165
|
+
return;
|
|
166
|
+
} const highlight = event.target.closest("button[data-highlight-candidate-id]"); if (highlight) {
|
|
167
|
+
event.preventDefault();
|
|
168
|
+
event.stopPropagation();
|
|
169
|
+
root.querySelector(`#candidate-${CSS.escape(safeId(highlight.dataset.highlightCandidateId))}`)?.focus();
|
|
170
|
+
} });
|
|
171
|
+
root.addEventListener("keydown", event => { const button = event.target.closest("button[data-candidate-id]"); if (button && (event.key === "Enter" || event.key === " ")) {
|
|
172
|
+
event.preventDefault();
|
|
173
|
+
event.stopPropagation();
|
|
174
|
+
activateCandidate(button.dataset.candidateId);
|
|
175
|
+
} });
|
|
176
|
+
render();
|
|
177
|
+
return () => root.remove();
|
|
178
|
+
}
|
|
179
|
+
function renderSource(text, candidates) {
|
|
180
|
+
const boundaries = new Set([0, text.length]);
|
|
181
|
+
candidates.forEach(c => { boundaries.add(c.start); boundaries.add(c.end); });
|
|
182
|
+
const points = [...boundaries].sort((a, b) => a - b);
|
|
183
|
+
let html = "";
|
|
184
|
+
const starts = new Map();
|
|
185
|
+
candidates.forEach(c => starts.set(c.start, [...(starts.get(c.start) ?? []), c]));
|
|
186
|
+
for (let i = 0; i < points.length - 1; i++) {
|
|
187
|
+
const start = points[i], end = points[i + 1];
|
|
188
|
+
for (const c of starts.get(start) ?? [])
|
|
189
|
+
html += `<button type="button" class="highlight-anchor" id="highlight-${safeId(c.id)}" data-highlight-candidate-id="${escapeHtml(c.id)}" aria-label="Source highlight for ${escapeHtml(c.field)}; activate to return to candidate"></button>`;
|
|
190
|
+
const segment = escapeHtml(text.slice(start, end));
|
|
191
|
+
const active = candidates.filter(c => c.start < end && c.end > start);
|
|
192
|
+
html += active.length ? `<mark aria-label="Highlighted for ${active.map(c => escapeHtml(c.field)).join(", ")}">${segment}</mark>` : segment;
|
|
193
|
+
}
|
|
194
|
+
return html;
|
|
195
|
+
}
|
|
196
|
+
function filterSelect(key, label, choices) { return `<label>${escapeHtml(label)}<select data-filter="${key}"><option value="">All</option>${choices}</select></label>`; }
|
|
197
|
+
function safeId(value) { return value.replace(/[^a-zA-Z0-9_-]/g, "-"); }
|
|
198
|
+
function escapeHtml(text) { return text.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """); }
|
|
@@ -187,6 +187,25 @@ export const REVIEW_WORKBENCH_CSS = `/* Bundled, scoped Survey Review Workbench
|
|
|
187
187
|
color: var(--k-text);
|
|
188
188
|
}
|
|
189
189
|
|
|
190
|
+
/* Read-only evidence pane attached to the existing ReviewItem queue. */
|
|
191
|
+
.survey-workbench-embed .extraction-inspector{ container-type: inline-size; margin-top: 1rem; padding: 1rem; border: 1px solid var(--k-line); border-radius: var(--k-radius); background: var(--k-panel); color: var(--k-text); }
|
|
192
|
+
.survey-workbench-embed .inspector-heading, .survey-workbench-embed .inspector-layout{ display: grid; grid-template-columns: minmax(15rem, .7fr) minmax(0, 1.3fr); gap: 1rem; }
|
|
193
|
+
.survey-workbench-embed .inspector-heading h2{ margin: 0; }
|
|
194
|
+
.survey-workbench-embed .inspector-posture{ display: flex; flex-direction: column; padding: .75rem; border-radius: var(--k-radius-sm); background: var(--k-positive-wash); }
|
|
195
|
+
.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; }
|
|
196
|
+
.survey-workbench-embed .inspector-filters{ display: flex; flex-wrap: wrap; gap: .6rem; margin: 1rem 0; }
|
|
197
|
+
.survey-workbench-embed .inspector-filters label{ display: grid; gap: .25rem; font-size: .75rem; color: var(--k-text-muted); }
|
|
198
|
+
.survey-workbench-embed .inspector-filters select{ color: var(--k-text); background: var(--k-sunken); border: 1px solid var(--k-line); padding: .4rem; }
|
|
199
|
+
.survey-workbench-embed .inspector-candidates{ margin: 0; padding-left: 1.5rem; }
|
|
200
|
+
.survey-workbench-embed .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); }
|
|
201
|
+
.survey-workbench-embed .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; }
|
|
202
|
+
.survey-workbench-embed .inspector-source mark{ background: var(--k-brand-wash); color: var(--k-text); outline: 1px solid var(--k-brand); }
|
|
203
|
+
.survey-workbench-embed .inspector-candidate:focus{ outline: 3px solid var(--k-active); outline-offset: 2px; }
|
|
204
|
+
.survey-workbench-embed .highlight-anchor{ display: inline-block; width: 1px; height: 1em; }
|
|
205
|
+
.survey-workbench-embed .highlight-anchor:focus{ outline: 3px solid var(--k-active); }
|
|
206
|
+
.survey-workbench-embed .source-unavailable{ color: var(--k-negative); font-weight: 700; }
|
|
207
|
+
@container (max-width: 720px) { .inspector-heading, .inspector-layout { grid-template-columns: 1fr !important; } }
|
|
208
|
+
|
|
190
209
|
/* Re-assert the base structural tokens for light mode. themes.css's \`.theme-*\`
|
|
191
210
|
preset classes hardcode a dark --k-bg/--k-panel/etc (so the preset "just
|
|
192
211
|
works" without requiring [data-theme] to be set), and a bare class selector
|
|
@@ -34,6 +34,7 @@
|
|
|
34
34
|
*/
|
|
35
35
|
import { type ReviewQueueSessionState, type ReviewWorkbenchState } from "./review-workbench.js";
|
|
36
36
|
import type { ReviewPresentationAdapter } from "./review-presentation.js";
|
|
37
|
+
import { type ExtractionInspectorInput } from "./extraction-inspector.js";
|
|
37
38
|
export declare class SurveyReviewWorkbenchElement extends HTMLElement {
|
|
38
39
|
#private;
|
|
39
40
|
static readonly observedAttributes: string[];
|
|
@@ -44,6 +45,9 @@ export declare class SurveyReviewWorkbenchElement extends HTMLElement {
|
|
|
44
45
|
/** Optional presentation adapter for custom labels, value summaries, and links. */
|
|
45
46
|
get presentationAdapter(): ReviewPresentationAdapter | undefined;
|
|
46
47
|
set presentationAdapter(value: ReviewPresentationAdapter | undefined);
|
|
48
|
+
/** Optional read-only source pane attached to this workbench's existing review lifecycle. */
|
|
49
|
+
get extractionInspector(): ExtractionInspectorInput | null;
|
|
50
|
+
set extractionInspector(value: ExtractionInspectorInput | null | undefined);
|
|
47
51
|
connectedCallback(): void;
|
|
48
52
|
attributeChangedCallback(name: string, oldValue: string | null, newValue: string | null): void;
|
|
49
53
|
}
|
|
@@ -33,6 +33,7 @@
|
|
|
33
33
|
* src — URL to fetch a JSON-serialised ReviewQueueSessionState
|
|
34
34
|
*/
|
|
35
35
|
import { mountReviewWorkbench, } from "./review-workbench.js";
|
|
36
|
+
import { buildExtractionInspectorModel, mountExtractionInspector, } from "./extraction-inspector.js";
|
|
36
37
|
import { REVIEW_WORKBENCH_CSS } from "./review-workbench-css.generated.js";
|
|
37
38
|
/** @internal Second adopted stylesheet: host token defaults + inheritance delegation.
|
|
38
39
|
*
|
|
@@ -167,6 +168,8 @@ export class SurveyReviewWorkbenchElement extends HTMLElement {
|
|
|
167
168
|
static observedAttributes = ["theme", "color-scheme", "src"];
|
|
168
169
|
#session = null;
|
|
169
170
|
#presentationAdapter = undefined;
|
|
171
|
+
#extractionInspector = null;
|
|
172
|
+
#unmountInspector;
|
|
170
173
|
#root;
|
|
171
174
|
#mountRoot;
|
|
172
175
|
#mounted = false;
|
|
@@ -285,6 +288,18 @@ export class SurveyReviewWorkbenchElement extends HTMLElement {
|
|
|
285
288
|
this.#mountRoot = document.createElement("div");
|
|
286
289
|
this.#mountRoot.className = "workbench survey-workbench-embed";
|
|
287
290
|
this.#root.appendChild(this.#mountRoot);
|
|
291
|
+
this.#mountRoot.addEventListener("survey-extraction-candidate-activate", (event) => {
|
|
292
|
+
if (!this.#session || !("items" in this.#session))
|
|
293
|
+
return;
|
|
294
|
+
const detail = event.detail;
|
|
295
|
+
const item = this.#session.items.find((candidate) => candidate.metadata.name === detail.reviewItemName);
|
|
296
|
+
if (!item)
|
|
297
|
+
return;
|
|
298
|
+
if (this.#session.activeItemName !== item.metadata.name)
|
|
299
|
+
this.#session = { ...this.#session, activeItemName: item.metadata.name };
|
|
300
|
+
this.#remount();
|
|
301
|
+
queueMicrotask(() => this.#root.querySelector(`#highlight-${CSS.escape(detail.candidateId.replace(/[^a-zA-Z0-9_-]/g, "-"))}`)?.focus());
|
|
302
|
+
});
|
|
288
303
|
}
|
|
289
304
|
/** The review queue session to display. Setting this property re-mounts the workbench. */
|
|
290
305
|
get session() {
|
|
@@ -302,6 +317,21 @@ export class SurveyReviewWorkbenchElement extends HTMLElement {
|
|
|
302
317
|
this.#presentationAdapter = value;
|
|
303
318
|
this.#remount();
|
|
304
319
|
}
|
|
320
|
+
/** Optional read-only source pane attached to this workbench's existing review lifecycle. */
|
|
321
|
+
get extractionInspector() { return this.#extractionInspector; }
|
|
322
|
+
set extractionInspector(value) {
|
|
323
|
+
const next = value ?? null;
|
|
324
|
+
try {
|
|
325
|
+
if (next)
|
|
326
|
+
buildExtractionInspectorModel(next);
|
|
327
|
+
this.#extractionInspector = next;
|
|
328
|
+
this.#remount();
|
|
329
|
+
}
|
|
330
|
+
catch {
|
|
331
|
+
this.#extractionInspector = null;
|
|
332
|
+
this.#renderError("Extraction inspector input is invalid and was not rendered.");
|
|
333
|
+
}
|
|
334
|
+
}
|
|
305
335
|
connectedCallback() {
|
|
306
336
|
// Rescue a `session` set before the element was upgraded so the property
|
|
307
337
|
// assignment reaches the class accessor instead of being shadowed by an
|
|
@@ -375,6 +405,10 @@ export class SurveyReviewWorkbenchElement extends HTMLElement {
|
|
|
375
405
|
: {};
|
|
376
406
|
this.#applyThemeClasses();
|
|
377
407
|
mountReviewWorkbench(this.#mountRoot, this.#session, options);
|
|
408
|
+
this.#unmountInspector?.();
|
|
409
|
+
this.#unmountInspector = this.#extractionInspector
|
|
410
|
+
? mountExtractionInspector(this.#mountRoot, buildExtractionInspectorModel(this.#extractionInspector))
|
|
411
|
+
: undefined;
|
|
378
412
|
this.#mounted = true;
|
|
379
413
|
}
|
|
380
414
|
#renderEmpty() {
|
|
@@ -184,6 +184,25 @@
|
|
|
184
184
|
color: var(--k-text);
|
|
185
185
|
}
|
|
186
186
|
|
|
187
|
+
/* Read-only evidence pane attached to the existing ReviewItem queue. */
|
|
188
|
+
.survey-workbench-embed .extraction-inspector{ container-type: inline-size; margin-top: 1rem; padding: 1rem; border: 1px solid var(--k-line); border-radius: var(--k-radius); background: var(--k-panel); color: var(--k-text); }
|
|
189
|
+
.survey-workbench-embed .inspector-heading, .survey-workbench-embed .inspector-layout{ display: grid; grid-template-columns: minmax(15rem, .7fr) minmax(0, 1.3fr); gap: 1rem; }
|
|
190
|
+
.survey-workbench-embed .inspector-heading h2{ margin: 0; }
|
|
191
|
+
.survey-workbench-embed .inspector-posture{ display: flex; flex-direction: column; padding: .75rem; border-radius: var(--k-radius-sm); background: var(--k-positive-wash); }
|
|
192
|
+
.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; }
|
|
193
|
+
.survey-workbench-embed .inspector-filters{ display: flex; flex-wrap: wrap; gap: .6rem; margin: 1rem 0; }
|
|
194
|
+
.survey-workbench-embed .inspector-filters label{ display: grid; gap: .25rem; font-size: .75rem; color: var(--k-text-muted); }
|
|
195
|
+
.survey-workbench-embed .inspector-filters select{ color: var(--k-text); background: var(--k-sunken); border: 1px solid var(--k-line); padding: .4rem; }
|
|
196
|
+
.survey-workbench-embed .inspector-candidates{ margin: 0; padding-left: 1.5rem; }
|
|
197
|
+
.survey-workbench-embed .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); }
|
|
198
|
+
.survey-workbench-embed .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; }
|
|
199
|
+
.survey-workbench-embed .inspector-source mark{ background: var(--k-brand-wash); color: var(--k-text); outline: 1px solid var(--k-brand); }
|
|
200
|
+
.survey-workbench-embed .inspector-candidate:focus{ outline: 3px solid var(--k-active); outline-offset: 2px; }
|
|
201
|
+
.survey-workbench-embed .highlight-anchor{ display: inline-block; width: 1px; height: 1em; }
|
|
202
|
+
.survey-workbench-embed .highlight-anchor:focus{ outline: 3px solid var(--k-active); }
|
|
203
|
+
.survey-workbench-embed .source-unavailable{ color: var(--k-negative); font-weight: 700; }
|
|
204
|
+
@container (max-width: 720px) { .inspector-heading, .inspector-layout { grid-template-columns: 1fr !important; } }
|
|
205
|
+
|
|
187
206
|
/* Re-assert the base structural tokens for light mode. themes.css's `.theme-*`
|
|
188
207
|
preset classes hardcode a dark --k-bg/--k-panel/etc (so the preset "just
|
|
189
208
|
works" without requiring [data-theme] to be set), and a bare class selector
|
|
@@ -2,6 +2,7 @@ import { type ReviewQueueSessionState, type ReviewWorkbenchDecision, type Review
|
|
|
2
2
|
import { type ReviewSessionReplayIssue } from "./review-session-replay.js";
|
|
3
3
|
import { type ReviewPresentationAdapter } from "./review-presentation.js";
|
|
4
4
|
import { type ReviewCandidate, type ReviewDecision, type ReviewItem, type ReviewSession, type ReviewSessionEvent, type ReviewValueDescriptor } from "../review-resource.js";
|
|
5
|
+
export { buildExtractionInspectorModel, exportExtractionInspector, filterExtractionInspectorCandidates, mountExtractionInspector, type ExtractionAlignmentState, type ArtifactUnavailableCode, type ExtractionInspectorCandidate, type ExtractionInspectorEntry, type ExtractionInspectorExportOptions, type ExtractionInspectorFilters, type ExtractionInspectorInput, type ExtractionInspectorModel, type ExtractionInspectorSource, type ResolvedExtractionArtifact, } from "./extraction-inspector.js";
|
|
5
6
|
export { buildReviewSessionEvents, buildReviewSessionEvent, buildReviewSessionResource, candidateForDecision, currentReviewItem, currentReviewWorkbenchState, defaultReviewSessionName, deriveQueueRowStatus, initialReviewQueueSessionState, initialReviewWorkbenchState, nextUnresolvedItemName, replayReviewSessionEvents, reviewSessionSummary, reviewWorkbenchSessionStorageKey, selectedCandidateRole, workbenchDecisionDefinitions, type ReviewQueueRowStatus, type ReviewQueueSessionState, type ReviewSessionSummary, type ReviewWorkbenchDecision, type ReviewWorkbenchState, } from "./review-queue-session.js";
|
|
6
7
|
export { buildReviewCandidatePresentation, buildReviewItemPresentation, buildReviewResultPresentation, humanizeIdentifier, type ReviewCandidatePresentation, type ReviewCandidatePresentationContext, type ReviewItemPresentation, type ReviewItemPresentationContext, type ReviewPresentationAdapter, type ReviewPresentationLink, type ReviewResultPresentation, type ReviewTracePresentationContext, type ReviewTraceRef, type ReviewValuePresentationContext, } from "./review-presentation.js";
|
|
7
8
|
export { buildSurfaceProjectionPreview, type PreviewAuthorityTrace, type PreviewCandidateHistory, type PreviewClaim, type PreviewIntegrityPosture, type PreviewReviewEvent, type PreviewSourceAuthority, type PreviewSourceEvidence, type SurfaceProjectionPreview, } from "./review-surface-preview.js";
|
|
@@ -7,6 +7,7 @@ import { buildReviewCandidatePresentation, buildReviewItemPresentation, } from "
|
|
|
7
7
|
import { reviewResourceApiVersion, } from "../review-resource.js";
|
|
8
8
|
import { validateAuthorizing, buildAuthorizedActionAuthorizing } from "../review-authorizing.js";
|
|
9
9
|
import { humanizeIdentifier } from "./review-presentation.js";
|
|
10
|
+
export { buildExtractionInspectorModel, exportExtractionInspector, filterExtractionInspectorCandidates, mountExtractionInspector, } from "./extraction-inspector.js";
|
|
10
11
|
export { buildReviewSessionEvents, buildReviewSessionEvent, buildReviewSessionResource, candidateForDecision, currentReviewItem, currentReviewWorkbenchState, defaultReviewSessionName, deriveQueueRowStatus, initialReviewQueueSessionState, initialReviewWorkbenchState, nextUnresolvedItemName, replayReviewSessionEvents, reviewSessionSummary, reviewWorkbenchSessionStorageKey, selectedCandidateRole, workbenchDecisionDefinitions, } from "./review-queue-session.js";
|
|
11
12
|
export { buildReviewCandidatePresentation, buildReviewItemPresentation, buildReviewResultPresentation, humanizeIdentifier, } from "./review-presentation.js";
|
|
12
13
|
export { buildSurfaceProjectionPreview, } from "./review-surface-preview.js";
|
|
@@ -26,13 +27,14 @@ export function buildReviewDecision(state) {
|
|
|
26
27
|
reviewOutcomeId: candidate.projection?.reviewOutcomeId
|
|
27
28
|
?? `${state.item.metadata.name}:${state.decision}:review-outcome`,
|
|
28
29
|
};
|
|
30
|
+
const authorizing = buildDecisionCardAuthorizing(state);
|
|
29
31
|
const reviewDecision = {
|
|
30
32
|
apiVersion: reviewResourceApiVersion,
|
|
31
33
|
kind: "ReviewDecision",
|
|
32
34
|
metadata: {
|
|
33
35
|
name: `${state.item.metadata.name}-${state.decision}`,
|
|
34
|
-
labels: state.item.metadata.labels,
|
|
35
|
-
producer: state.item.metadata.producer,
|
|
36
|
+
...(state.item.metadata.labels ? { labels: state.item.metadata.labels } : {}),
|
|
37
|
+
...(state.item.metadata.producer ? { producer: state.item.metadata.producer } : {}),
|
|
36
38
|
},
|
|
37
39
|
spec: {
|
|
38
40
|
reviewItemName: state.item.metadata.name,
|
|
@@ -50,12 +52,14 @@ export function buildReviewDecision(state) {
|
|
|
50
52
|
},
|
|
51
53
|
reviewedAt: state.reviewedAt,
|
|
52
54
|
rationale: state.note,
|
|
53
|
-
authorizing:
|
|
54
|
-
projection,
|
|
55
|
-
|
|
55
|
+
...(authorizing !== undefined ? { authorizing } : {}),
|
|
56
|
+
...(projection !== undefined ? { projection } : {}),
|
|
57
|
+
...(state.decision === "accept-proposed" && state.editedValue !== undefined
|
|
58
|
+
? { editedValue: state.editedValue }
|
|
59
|
+
: {}),
|
|
56
60
|
},
|
|
57
61
|
status: {
|
|
58
|
-
|
|
62
|
+
...(candidate.projection?.claimId ? { appliedToClaimIds: [candidate.projection.claimId] } : {}),
|
|
59
63
|
},
|
|
60
64
|
};
|
|
61
65
|
assertReviewResolutionConsistency(`ReviewDecision ${reviewDecision.metadata.name}`, {
|
|
@@ -33,6 +33,25 @@
|
|
|
33
33
|
color: var(--k-text);
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
+
/* Read-only evidence pane attached to the existing ReviewItem queue. */
|
|
37
|
+
.extraction-inspector { container-type: inline-size; margin-top: 1rem; padding: 1rem; border: 1px solid var(--k-line); border-radius: var(--k-radius); background: var(--k-panel); color: var(--k-text); }
|
|
38
|
+
.inspector-heading, .inspector-layout { display: grid; grid-template-columns: minmax(15rem, .7fr) minmax(0, 1.3fr); gap: 1rem; }
|
|
39
|
+
.inspector-heading h2 { margin: 0; }
|
|
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-filters { display: flex; flex-wrap: wrap; gap: .6rem; margin: 1rem 0; }
|
|
43
|
+
.inspector-filters label { display: grid; gap: .25rem; font-size: .75rem; color: var(--k-text-muted); }
|
|
44
|
+
.inspector-filters select { color: var(--k-text); background: var(--k-sunken); border: 1px solid var(--k-line); padding: .4rem; }
|
|
45
|
+
.inspector-candidates { margin: 0; padding-left: 1.5rem; }
|
|
46
|
+
.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); }
|
|
47
|
+
.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; }
|
|
48
|
+
.inspector-source mark { background: var(--k-brand-wash); color: var(--k-text); outline: 1px solid var(--k-brand); }
|
|
49
|
+
.inspector-candidate:focus { outline: 3px solid var(--k-active); outline-offset: 2px; }
|
|
50
|
+
.highlight-anchor { display: inline-block; width: 1px; height: 1em; }
|
|
51
|
+
.highlight-anchor:focus { outline: 3px solid var(--k-active); }
|
|
52
|
+
.source-unavailable { color: var(--k-negative); font-weight: 700; }
|
|
53
|
+
@container (max-width: 720px) { .inspector-heading, .inspector-layout { grid-template-columns: 1fr !important; } }
|
|
54
|
+
|
|
36
55
|
/* Re-assert the base structural tokens for light mode. themes.css's `.theme-*`
|
|
37
56
|
preset classes hardcode a dark --k-bg/--k-panel/etc (so the preset "just
|
|
38
57
|
works" without requiring [data-theme] to be set), and a bare class selector
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/** Dependency-free synchronous SHA-256 for browser and Node parity. */
|
|
2
|
+
export function sha256Hex(text) {
|
|
3
|
+
const bytes = new TextEncoder().encode(text), bitLength = bytes.length * 8, length = Math.ceil((bytes.length + 9) / 64) * 64, data = new Uint8Array(length);
|
|
4
|
+
data.set(bytes);
|
|
5
|
+
data[bytes.length] = 0x80;
|
|
6
|
+
const view = new DataView(data.buffer);
|
|
7
|
+
view.setUint32(length - 8, Math.floor(bitLength / 0x100000000));
|
|
8
|
+
view.setUint32(length - 4, bitLength >>> 0);
|
|
9
|
+
const h = new Uint32Array([0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19]);
|
|
10
|
+
const k = new Uint32Array([0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5, 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174, 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da, 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967, 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, 0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070, 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3, 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2]), w = new Uint32Array(64), rotr = (v, n) => (v >>> n) | (v << (32 - n));
|
|
11
|
+
for (let offset = 0; offset < length; offset += 64) {
|
|
12
|
+
for (let i = 0; i < 16; i++)
|
|
13
|
+
w[i] = view.getUint32(offset + i * 4);
|
|
14
|
+
for (let i = 16; i < 64; i++) {
|
|
15
|
+
const x = rotr(w[i - 15], 7) ^ rotr(w[i - 15], 18) ^ (w[i - 15] >>> 3), y = rotr(w[i - 2], 17) ^ rotr(w[i - 2], 19) ^ (w[i - 2] >>> 10);
|
|
16
|
+
w[i] = (w[i - 16] + x + w[i - 7] + y) >>> 0;
|
|
17
|
+
}
|
|
18
|
+
let [a, b, c, d, e, f, g, z] = h;
|
|
19
|
+
for (let i = 0; i < 64; i++) {
|
|
20
|
+
const x = rotr(e, 6) ^ rotr(e, 11) ^ rotr(e, 25), ch = (e & f) ^ (~e & g), t1 = (z + x + ch + k[i] + w[i]) >>> 0, s0 = rotr(a, 2) ^ rotr(a, 13) ^ rotr(a, 22), maj = (a & b) ^ (a & c) ^ (b & c), t2 = (s0 + maj) >>> 0;
|
|
21
|
+
z = g;
|
|
22
|
+
g = f;
|
|
23
|
+
f = e;
|
|
24
|
+
e = (d + t1) >>> 0;
|
|
25
|
+
d = c;
|
|
26
|
+
c = b;
|
|
27
|
+
b = a;
|
|
28
|
+
a = (t1 + t2) >>> 0;
|
|
29
|
+
}
|
|
30
|
+
h[0] = (h[0] + a) >>> 0;
|
|
31
|
+
h[1] = (h[1] + b) >>> 0;
|
|
32
|
+
h[2] = (h[2] + c) >>> 0;
|
|
33
|
+
h[3] = (h[3] + d) >>> 0;
|
|
34
|
+
h[4] = (h[4] + e) >>> 0;
|
|
35
|
+
h[5] = (h[5] + f) >>> 0;
|
|
36
|
+
h[6] = (h[6] + g) >>> 0;
|
|
37
|
+
h[7] = (h[7] + z) >>> 0;
|
|
38
|
+
}
|
|
39
|
+
return [...h].map(v => v.toString(16).padStart(8, "0")).join("");
|
|
40
|
+
}
|
package/package.json
CHANGED