@kontourai/survey 1.16.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 +166 -0
- package/dist/src/extraction-envelope.js +432 -0
- package/dist/src/index.d.ts +4 -0
- package/dist/src/index.js +2 -0
- 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 +2 -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
|
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import type { RawSource } from "./types.js";
|
|
2
|
+
import type { ClaimTargetHint, ReviewItem, ReviewValueType } from "./review-resource.js";
|
|
3
|
+
/** The upstream-owned portable extraction-result wire identifiers accepted by this adapter. */
|
|
4
|
+
export declare const portableExtractionResultFormat = "traverse-extraction-result";
|
|
5
|
+
export declare const portableExtractionResultVersion = 1;
|
|
6
|
+
export declare const extractionEnvelopeImportApiVersion = "survey.kontourai.io/v1alpha1";
|
|
7
|
+
export interface PortableExtractionOccurrence {
|
|
8
|
+
resolverVersion: "exact-occurrence-v1";
|
|
9
|
+
count: number;
|
|
10
|
+
selected: {
|
|
11
|
+
index: number;
|
|
12
|
+
start: number;
|
|
13
|
+
end: number;
|
|
14
|
+
};
|
|
15
|
+
selection: "source-order" | "occurrence-hint";
|
|
16
|
+
hintUsed: boolean;
|
|
17
|
+
ambiguous: boolean;
|
|
18
|
+
}
|
|
19
|
+
export interface PortableExtractionProposal {
|
|
20
|
+
fieldPath: string;
|
|
21
|
+
candidateValue: unknown;
|
|
22
|
+
confidence: number;
|
|
23
|
+
provenance: {
|
|
24
|
+
excerpt: string;
|
|
25
|
+
locator: string;
|
|
26
|
+
occurrence: PortableExtractionOccurrence;
|
|
27
|
+
};
|
|
28
|
+
extractor: string;
|
|
29
|
+
pathIndices?: number[];
|
|
30
|
+
inferenceType?: "explicit" | "inferred";
|
|
31
|
+
valueType?: ReviewValueType;
|
|
32
|
+
enumValues?: string[];
|
|
33
|
+
}
|
|
34
|
+
export type PortablePreparedArtifactState = {
|
|
35
|
+
status: "available" | "unavailable" | "storage-error";
|
|
36
|
+
requestedRef: string;
|
|
37
|
+
canonicalRef: string;
|
|
38
|
+
} | {
|
|
39
|
+
status: "identity-mismatch";
|
|
40
|
+
requestedRef: string;
|
|
41
|
+
canonicalRef: string;
|
|
42
|
+
} | {
|
|
43
|
+
status: "invalid-artifact";
|
|
44
|
+
reason: string;
|
|
45
|
+
canonicalRef: string;
|
|
46
|
+
} | {
|
|
47
|
+
status: "digest-mismatch";
|
|
48
|
+
requestedRef: string;
|
|
49
|
+
canonicalRef: string;
|
|
50
|
+
actualDigest: string;
|
|
51
|
+
actualContentLength: number;
|
|
52
|
+
};
|
|
53
|
+
export interface PortableExtractionResultEnvelope {
|
|
54
|
+
format: typeof portableExtractionResultFormat;
|
|
55
|
+
version: typeof portableExtractionResultVersion;
|
|
56
|
+
source: {
|
|
57
|
+
ref: string;
|
|
58
|
+
snapshotRef?: string;
|
|
59
|
+
};
|
|
60
|
+
result: {
|
|
61
|
+
proposals: PortableExtractionProposal[];
|
|
62
|
+
provider: string;
|
|
63
|
+
model?: string;
|
|
64
|
+
runId: string;
|
|
65
|
+
raw: {
|
|
66
|
+
tokensUsed?: number;
|
|
67
|
+
};
|
|
68
|
+
outcome: {
|
|
69
|
+
status: "success";
|
|
70
|
+
} | {
|
|
71
|
+
status: "partial";
|
|
72
|
+
reason: "cancelled" | "max-provider-calls" | "max-total-tokens";
|
|
73
|
+
} | {
|
|
74
|
+
status: "failure";
|
|
75
|
+
category: "invalid-config" | "invalid-task" | "preparation" | "provider" | "unexpected";
|
|
76
|
+
code: string;
|
|
77
|
+
};
|
|
78
|
+
warningClassifications?: Array<{
|
|
79
|
+
category: "provider" | "normalization" | "preparation" | "limit" | "storage" | "content" | "other";
|
|
80
|
+
code: string;
|
|
81
|
+
}>;
|
|
82
|
+
extractedAt: string;
|
|
83
|
+
providerCalls: number;
|
|
84
|
+
totalTokensUsed: number;
|
|
85
|
+
partial?: {
|
|
86
|
+
reason: "cancelled" | "max-provider-calls" | "max-total-tokens";
|
|
87
|
+
completedChunks: number;
|
|
88
|
+
remainingChunks: number;
|
|
89
|
+
tokenOvershoot?: number;
|
|
90
|
+
};
|
|
91
|
+
providerFailures?: Array<{
|
|
92
|
+
provider: string;
|
|
93
|
+
kind: "authentication" | "rate-limit" | "timeout" | "invalid-request" | "unavailable" | "unknown";
|
|
94
|
+
retryable: boolean;
|
|
95
|
+
}>;
|
|
96
|
+
taskDigest?: string;
|
|
97
|
+
exampleDigests?: string[];
|
|
98
|
+
pdfPageOffsets?: number[];
|
|
99
|
+
ocrDerived?: true;
|
|
100
|
+
preparedArtifact?: {
|
|
101
|
+
format: "traverse-prepared-artifact";
|
|
102
|
+
version: 1;
|
|
103
|
+
digest: string;
|
|
104
|
+
ref: string;
|
|
105
|
+
preparationMode: string;
|
|
106
|
+
preparationVersion: string;
|
|
107
|
+
contentLength: number;
|
|
108
|
+
sourceSnapshotRef?: string;
|
|
109
|
+
};
|
|
110
|
+
preparedArtifactState?: PortablePreparedArtifactState;
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
export interface ExtractionEnvelopeImportOptions {
|
|
114
|
+
/** Stable Survey import identity. Defaults to the upstream run id. */
|
|
115
|
+
importName?: string;
|
|
116
|
+
/** Producer namespace used to avoid cross-producer identity collisions. */
|
|
117
|
+
producerNamespace?: string;
|
|
118
|
+
sourceKind: RawSource["kind"];
|
|
119
|
+
/** Survey meaning is supplied at the boundary; it is not added to the upstream wire contract. */
|
|
120
|
+
claimTarget: (proposal: PortableExtractionProposal, index: number) => ClaimTargetHint;
|
|
121
|
+
}
|
|
122
|
+
export type ExtractionEnvelopeImportDiagnostic = {
|
|
123
|
+
kind: "artifact-unavailable";
|
|
124
|
+
status: "unavailable" | "storage-error" | "identity-mismatch" | "invalid-artifact";
|
|
125
|
+
artifactRef?: string;
|
|
126
|
+
message: string;
|
|
127
|
+
} | {
|
|
128
|
+
kind: "digest-mismatch";
|
|
129
|
+
artifactRef: string;
|
|
130
|
+
expectedDigest: string;
|
|
131
|
+
actualDigest: string;
|
|
132
|
+
message: string;
|
|
133
|
+
};
|
|
134
|
+
export interface ExtractionEnvelopeImport {
|
|
135
|
+
apiVersion: typeof extractionEnvelopeImportApiVersion;
|
|
136
|
+
kind: "ExtractionEnvelopeImport";
|
|
137
|
+
metadata: {
|
|
138
|
+
name: string;
|
|
139
|
+
producerNamespace: string;
|
|
140
|
+
};
|
|
141
|
+
spec: {
|
|
142
|
+
envelope: PortableExtractionResultEnvelope;
|
|
143
|
+
sourceKind: RawSource["kind"];
|
|
144
|
+
claimTargets: ClaimTargetHint[];
|
|
145
|
+
};
|
|
146
|
+
status: {
|
|
147
|
+
state: "grounded" | "unresolved";
|
|
148
|
+
diagnostics: ExtractionEnvelopeImportDiagnostic[];
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
export interface ExtractionEnvelopeImportResult {
|
|
152
|
+
record: ExtractionEnvelopeImport;
|
|
153
|
+
reviewItems: ReviewItem[];
|
|
154
|
+
}
|
|
155
|
+
export interface ExtractionEnvelopeResolutionIdentity {
|
|
156
|
+
evidenceId: string;
|
|
157
|
+
eventId: string;
|
|
158
|
+
}
|
|
159
|
+
/** Parse an untrusted upstream document and create Survey's durable import projection. */
|
|
160
|
+
export declare function importExtractionEnvelope(serialized: string | PortableExtractionResultEnvelope, options: ExtractionEnvelopeImportOptions): ExtractionEnvelopeImportResult;
|
|
161
|
+
export declare function buildReviewItemsFromExtractionEnvelopeImport(record: ExtractionEnvelopeImport): ReviewItem[];
|
|
162
|
+
export declare function exportExtractionEnvelopeImport(record: ExtractionEnvelopeImport): string;
|
|
163
|
+
export declare function reimportExtractionEnvelope(serialized: string): ExtractionEnvelopeImport;
|
|
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;
|
|
@@ -0,0 +1,432 @@
|
|
|
1
|
+
import { sha256Hex } from "./sha256.js";
|
|
2
|
+
import { reviewResourceApiVersion } from "./review-resource.js";
|
|
3
|
+
import { canonicalJson } from "./review-workbench/canonical.js";
|
|
4
|
+
/** The upstream-owned portable extraction-result wire identifiers accepted by this adapter. */
|
|
5
|
+
export const portableExtractionResultFormat = "traverse-extraction-result";
|
|
6
|
+
export const portableExtractionResultVersion = 1;
|
|
7
|
+
export const extractionEnvelopeImportApiVersion = "survey.kontourai.io/v1alpha1";
|
|
8
|
+
/** Parse an untrusted upstream document and create Survey's durable import projection. */
|
|
9
|
+
export function importExtractionEnvelope(serialized, options) {
|
|
10
|
+
let parsed;
|
|
11
|
+
if (typeof serialized === "string") {
|
|
12
|
+
try {
|
|
13
|
+
parsed = JSON.parse(serialized);
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
throw new Error("Portable extraction envelope is not valid JSON.");
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
else
|
|
20
|
+
parsed = serialized;
|
|
21
|
+
const envelope = validateEnvelope(parsed);
|
|
22
|
+
const claimTargets = envelope.result.proposals.map(options.claimTarget);
|
|
23
|
+
claimTargets.forEach(validateClaimTarget);
|
|
24
|
+
const name = options.importName ?? envelope.result.runId;
|
|
25
|
+
const producerNamespace = options.producerNamespace ?? envelope.result.provider;
|
|
26
|
+
nonEmpty(name, "Extraction envelope import name");
|
|
27
|
+
nonEmpty(producerNamespace, "Extraction envelope producer namespace");
|
|
28
|
+
const diagnostics = diagnosticsFor(envelope);
|
|
29
|
+
const record = {
|
|
30
|
+
apiVersion: extractionEnvelopeImportApiVersion,
|
|
31
|
+
kind: "ExtractionEnvelopeImport",
|
|
32
|
+
metadata: { name, producerNamespace },
|
|
33
|
+
spec: { envelope, sourceKind: options.sourceKind, claimTargets: cloneJson(claimTargets) },
|
|
34
|
+
status: { state: diagnostics.length ? "unresolved" : "grounded", diagnostics },
|
|
35
|
+
};
|
|
36
|
+
return { record, reviewItems: buildReviewItemsFromExtractionEnvelopeImport(record) };
|
|
37
|
+
}
|
|
38
|
+
export function buildReviewItemsFromExtractionEnvelopeImport(record) {
|
|
39
|
+
validateImport(record);
|
|
40
|
+
if (record.status.state !== "grounded")
|
|
41
|
+
return [];
|
|
42
|
+
return record.spec.envelope.result.proposals.map((proposal, index) => buildReviewItem(record, proposal, index));
|
|
43
|
+
}
|
|
44
|
+
export function exportExtractionEnvelopeImport(record) {
|
|
45
|
+
validateImport(record);
|
|
46
|
+
return canonicalJson(record);
|
|
47
|
+
}
|
|
48
|
+
export function reimportExtractionEnvelope(serialized) {
|
|
49
|
+
let parsed;
|
|
50
|
+
try {
|
|
51
|
+
parsed = JSON.parse(serialized);
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
throw new Error("Extraction envelope import is not valid JSON.");
|
|
55
|
+
}
|
|
56
|
+
validateImport(parsed);
|
|
57
|
+
return cloneJson(parsed);
|
|
58
|
+
}
|
|
59
|
+
export function createExtractionEnvelopeResolutionIdentity(record, proposalIndex) {
|
|
60
|
+
validateImport(record);
|
|
61
|
+
if (!Number.isSafeInteger(proposalIndex) || proposalIndex < 0 || proposalIndex >= record.spec.envelope.result.proposals.length) {
|
|
62
|
+
throw new Error(`Extraction envelope import ${record.metadata.name} has no proposal at index ${proposalIndex}.`);
|
|
63
|
+
}
|
|
64
|
+
const base = identityHash(identityInputs(record, record.spec.envelope.result.proposals[proposalIndex], proposalIndex));
|
|
65
|
+
const nonce = globalThis.crypto.randomUUID();
|
|
66
|
+
return { evidenceId: `survey.extraction.${base}.resolution-evidence.${nonce}`, eventId: `survey.extraction.${base}.resolution-event.${nonce}` };
|
|
67
|
+
}
|
|
68
|
+
function buildReviewItem(record, proposal, index) {
|
|
69
|
+
const envelope = record.spec.envelope;
|
|
70
|
+
const identity = identityHash(identityInputs(record, proposal, index));
|
|
71
|
+
const evidence = identityHash(evidenceInputs(record, proposal));
|
|
72
|
+
const target = record.spec.claimTargets[index];
|
|
73
|
+
const valueType = proposal.valueType ?? inferValueType(proposal.candidateValue);
|
|
74
|
+
const candidate = {
|
|
75
|
+
id: `extraction-envelope.${identity}.proposed`, role: "proposed", value: proposal.candidateValue,
|
|
76
|
+
confidence: proposal.confidence,
|
|
77
|
+
source: {
|
|
78
|
+
sourceRef: envelope.source.ref,
|
|
79
|
+
sourceId: envelope.source.snapshotRef ?? envelope.source.ref,
|
|
80
|
+
kind: record.spec.sourceKind,
|
|
81
|
+
observedAt: envelope.result.extractedAt,
|
|
82
|
+
...(envelope.result.preparedArtifact?.digest ? { checksum: envelope.result.preparedArtifact.digest } : {}),
|
|
83
|
+
locatorScheme: "text-span",
|
|
84
|
+
},
|
|
85
|
+
locator: { scheme: "text-span", locator: proposal.provenance.locator, excerpt: proposal.provenance.excerpt },
|
|
86
|
+
extraction: {
|
|
87
|
+
extractionId: `extraction-envelope.${identity}`,
|
|
88
|
+
target: proposal.fieldPath,
|
|
89
|
+
confidence: proposal.confidence,
|
|
90
|
+
extractor: proposal.extractor,
|
|
91
|
+
...(envelope.result.model ? { model: envelope.result.model } : {}),
|
|
92
|
+
},
|
|
93
|
+
claimTarget: target,
|
|
94
|
+
producer: { "survey.kontourai.io/extraction-envelope": {
|
|
95
|
+
importName: record.metadata.name, proposalIndex: index,
|
|
96
|
+
evidenceId: `survey.extraction.${evidence}.source-evidence`,
|
|
97
|
+
runId: envelope.result.runId, provider: envelope.result.provider,
|
|
98
|
+
...(envelope.result.model ? { model: envelope.result.model } : {}),
|
|
99
|
+
...(envelope.result.taskDigest ? { taskDigest: envelope.result.taskDigest } : {}),
|
|
100
|
+
...(envelope.result.exampleDigests ? { exampleDigests: envelope.result.exampleDigests } : {}),
|
|
101
|
+
valueType: { type: valueType, origin: proposal.inferenceType ?? "inferred" },
|
|
102
|
+
occurrence: proposal.provenance.occurrence,
|
|
103
|
+
attempt: { id: envelope.result.runId, providerCalls: envelope.result.providerCalls },
|
|
104
|
+
...(envelope.result.warningClassifications ? { warnings: envelope.result.warningClassifications } : {}),
|
|
105
|
+
outcome: envelope.result.outcome,
|
|
106
|
+
} },
|
|
107
|
+
};
|
|
108
|
+
return {
|
|
109
|
+
apiVersion: reviewResourceApiVersion, kind: "ReviewItem",
|
|
110
|
+
metadata: { name: `extraction-envelope.${identity}`, producer: { "survey.kontourai.io/extraction-envelope": {
|
|
111
|
+
importName: record.metadata.name,
|
|
112
|
+
evidenceId: `survey.extraction.${evidence}.source-evidence`,
|
|
113
|
+
source: envelope.source,
|
|
114
|
+
...(envelope.result.preparedArtifact ? { preparedArtifact: envelope.result.preparedArtifact } : {}),
|
|
115
|
+
} } },
|
|
116
|
+
spec: { target: proposal.fieldPath, candidates: [candidate], candidateSetStatus: "needs-review", valueDescriptor: { type: valueType }, editable: false },
|
|
117
|
+
status: { observedCandidateCount: 1 },
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
function identityInputs(record, proposal, index) {
|
|
121
|
+
return { producerNamespace: record.metadata.producerNamespace, importName: record.metadata.name, source: record.spec.envelope.source,
|
|
122
|
+
preparedArtifact: record.spec.envelope.result.preparedArtifact, runId: record.spec.envelope.result.runId,
|
|
123
|
+
proposalIndex: index, proposal, claimTarget: record.spec.claimTargets[index] };
|
|
124
|
+
}
|
|
125
|
+
function evidenceInputs(record, proposal) {
|
|
126
|
+
return { producerNamespace: record.metadata.producerNamespace, importName: record.metadata.name, source: record.spec.envelope.source,
|
|
127
|
+
sourceKind: record.spec.sourceKind, preparedArtifact: record.spec.envelope.result.preparedArtifact,
|
|
128
|
+
extractedAt: record.spec.envelope.result.extractedAt,
|
|
129
|
+
provenance: proposal.provenance };
|
|
130
|
+
}
|
|
131
|
+
function diagnosticsFor(envelope) {
|
|
132
|
+
const state = envelope.result.preparedArtifactState;
|
|
133
|
+
if (!state || state.status === "available")
|
|
134
|
+
return [];
|
|
135
|
+
if (state.status === "digest-mismatch")
|
|
136
|
+
return [{ kind: "digest-mismatch", artifactRef: state.canonicalRef,
|
|
137
|
+
expectedDigest: envelope.result.preparedArtifact.digest, actualDigest: state.actualDigest,
|
|
138
|
+
message: "Prepared artifact digest does not match the expected extraction artifact." }];
|
|
139
|
+
return [{ kind: "artifact-unavailable", status: state.status,
|
|
140
|
+
...(state.status === "invalid-artifact" ? { artifactRef: state.canonicalRef } : { artifactRef: state.requestedRef }),
|
|
141
|
+
message: `Prepared artifact resolution is ${state.status}.` }];
|
|
142
|
+
}
|
|
143
|
+
function validateImport(value) {
|
|
144
|
+
jsonSafe(value, "Extraction envelope import");
|
|
145
|
+
const record = obj(value, "Extraction envelope import");
|
|
146
|
+
exact(record, ["apiVersion", "kind", "metadata", "spec", "status"], "Extraction envelope import");
|
|
147
|
+
if (record.apiVersion !== extractionEnvelopeImportApiVersion || record.kind !== "ExtractionEnvelopeImport")
|
|
148
|
+
throw new Error("Invalid extraction envelope import resource identity.");
|
|
149
|
+
const metadata = obj(record.metadata, "metadata");
|
|
150
|
+
exact(metadata, ["name", "producerNamespace"], "metadata");
|
|
151
|
+
nonEmpty(metadata.name, "metadata.name");
|
|
152
|
+
nonEmpty(metadata.producerNamespace, "metadata.producerNamespace");
|
|
153
|
+
const spec = obj(record.spec, "spec");
|
|
154
|
+
exact(spec, ["envelope", "sourceKind", "claimTargets"], "spec");
|
|
155
|
+
const envelope = validateEnvelope(spec.envelope);
|
|
156
|
+
if (!RAW_SOURCE_KINDS.has(spec.sourceKind))
|
|
157
|
+
throw new Error("spec.sourceKind is invalid.");
|
|
158
|
+
const targets = array(spec.claimTargets, "spec.claimTargets");
|
|
159
|
+
targets.forEach(validateClaimTarget);
|
|
160
|
+
if (targets.length !== envelope.result.proposals.length)
|
|
161
|
+
throw new Error("spec.claimTargets must align with proposals.");
|
|
162
|
+
const status = obj(record.status, "status");
|
|
163
|
+
exact(status, ["state", "diagnostics"], "status");
|
|
164
|
+
const expected = diagnosticsFor(envelope);
|
|
165
|
+
if (canonicalJson(status.diagnostics) !== canonicalJson(expected) || status.state !== (expected.length ? "unresolved" : "grounded"))
|
|
166
|
+
throw new Error("Import status does not match envelope state.");
|
|
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
|
+
}
|
|
173
|
+
function validateEnvelope(input) {
|
|
174
|
+
jsonSafe(input, "Portable extraction envelope");
|
|
175
|
+
const e = obj(input, "envelope");
|
|
176
|
+
exact(e, ["format", "version", "source", "result"], "envelope");
|
|
177
|
+
if (e.format !== portableExtractionResultFormat || e.version !== portableExtractionResultVersion)
|
|
178
|
+
throw new Error("Unsupported portable extraction envelope format or version.");
|
|
179
|
+
const source = obj(e.source, "source");
|
|
180
|
+
exact(source, ["ref"], "source", ["snapshotRef"]);
|
|
181
|
+
safeReference(source.ref, "source.ref");
|
|
182
|
+
if (source.snapshotRef !== undefined)
|
|
183
|
+
safeReference(source.snapshotRef, "source.snapshotRef");
|
|
184
|
+
const r = obj(e.result, "result");
|
|
185
|
+
exact(r, ["proposals", "provider", "runId", "raw", "outcome", "extractedAt", "providerCalls", "totalTokensUsed"], "result", ["model", "warningClassifications", "partial", "providerFailures", "taskDigest", "exampleDigests", "pdfPageOffsets", "ocrDerived", "preparedArtifact", "preparedArtifactState"]);
|
|
186
|
+
stableIdentity(r.provider, "result.provider");
|
|
187
|
+
if (r.model !== undefined)
|
|
188
|
+
stableIdentity(r.model, "result.model");
|
|
189
|
+
if (typeof r.runId !== "string" || !RUN_ID.test(r.runId))
|
|
190
|
+
throw new Error("result.runId is invalid.");
|
|
191
|
+
wireNonEmpty(r.extractedAt, "result.extractedAt");
|
|
192
|
+
integer(r.providerCalls, "result.providerCalls");
|
|
193
|
+
integer(r.totalTokensUsed, "result.totalTokensUsed");
|
|
194
|
+
const raw = obj(r.raw, "result.raw");
|
|
195
|
+
exact(raw, [], "result.raw", ["tokensUsed"]);
|
|
196
|
+
if (raw.tokensUsed !== undefined)
|
|
197
|
+
integer(raw.tokensUsed, "result.raw.tokensUsed");
|
|
198
|
+
validateOutcome(r.outcome, r.partial);
|
|
199
|
+
if (r.warningClassifications !== undefined)
|
|
200
|
+
array(r.warningClassifications, "warnings").forEach(validateWarning);
|
|
201
|
+
if (r.providerFailures !== undefined)
|
|
202
|
+
array(r.providerFailures, "providerFailures").forEach(validateFailure);
|
|
203
|
+
optionalDigest(r.taskDigest, "result.taskDigest");
|
|
204
|
+
if (r.exampleDigests !== undefined)
|
|
205
|
+
array(r.exampleDigests, "exampleDigests").forEach((v) => digest(v, "exampleDigest"));
|
|
206
|
+
if (r.pdfPageOffsets !== undefined) {
|
|
207
|
+
const offsets = array(r.pdfPageOffsets, "pdfPageOffsets");
|
|
208
|
+
offsets.forEach((v) => integer(v, "pageOffset"));
|
|
209
|
+
if (offsets.some((v, i) => i > 0 && v <= offsets[i - 1]))
|
|
210
|
+
throw new Error("pdfPageOffsets must ascend strictly.");
|
|
211
|
+
}
|
|
212
|
+
if (r.ocrDerived !== undefined && r.ocrDerived !== true)
|
|
213
|
+
throw new Error("result.ocrDerived must be true.");
|
|
214
|
+
const artifact = r.preparedArtifact === undefined ? undefined : validateArtifact(r.preparedArtifact);
|
|
215
|
+
const state = r.preparedArtifactState === undefined ? undefined : validateArtifactState(r.preparedArtifactState, artifact);
|
|
216
|
+
if (source.snapshotRef !== undefined && artifact?.sourceSnapshotRef !== undefined && source.snapshotRef !== artifact.sourceSnapshotRef)
|
|
217
|
+
throw new Error("Source snapshot identity mismatch.");
|
|
218
|
+
const proposals = array(r.proposals, "result.proposals").map((p, i) => validateProposal(p, i, artifact?.contentLength));
|
|
219
|
+
return cloneJson({ ...e, source, result: { ...r, proposals, ...(artifact ? { preparedArtifact: artifact } : {}), ...(state ? { preparedArtifactState: state } : {}) } });
|
|
220
|
+
}
|
|
221
|
+
function validateProposal(input, index, contentLength) {
|
|
222
|
+
const p = obj(input, `proposal[${index}]`);
|
|
223
|
+
exact(p, ["fieldPath", "candidateValue", "confidence", "provenance", "extractor"], `proposal[${index}]`, ["pathIndices", "inferenceType", "valueType", "enumValues"]);
|
|
224
|
+
wireNonEmpty(p.fieldPath, "proposal.fieldPath");
|
|
225
|
+
stableIdentity(p.extractor, "proposal.extractor");
|
|
226
|
+
finite(p.confidence, "proposal.confidence", 0, 1);
|
|
227
|
+
const provenance = obj(p.provenance, "proposal.provenance");
|
|
228
|
+
exact(provenance, ["excerpt", "locator", "occurrence"], "proposal.provenance");
|
|
229
|
+
wireNonEmpty(provenance.excerpt, "proposal.provenance.excerpt");
|
|
230
|
+
if (typeof provenance.locator !== "string")
|
|
231
|
+
throw new Error("proposal locator must be chars:start-end.");
|
|
232
|
+
const match = /^chars:(0|[1-9]\d*)-(0|[1-9]\d*)$/.exec(provenance.locator);
|
|
233
|
+
if (!match)
|
|
234
|
+
throw new Error("proposal locator must be chars:start-end.");
|
|
235
|
+
const start = Number(match[1]), end = Number(match[2]);
|
|
236
|
+
if (end < start || end - start !== provenance.excerpt.length || (contentLength !== undefined && end > contentLength))
|
|
237
|
+
throw new Error("proposal locator/excerpt span is incoherent.");
|
|
238
|
+
validateOccurrence(provenance.occurrence, start, end);
|
|
239
|
+
if (p.pathIndices !== undefined)
|
|
240
|
+
array(p.pathIndices, "pathIndices").forEach((v) => integer(v, "pathIndex"));
|
|
241
|
+
if (p.inferenceType !== undefined && p.inferenceType !== "explicit" && p.inferenceType !== "inferred")
|
|
242
|
+
throw new Error("proposal inferenceType is invalid.");
|
|
243
|
+
if (p.valueType !== undefined && !VALUE_TYPES.has(p.valueType))
|
|
244
|
+
throw new Error("proposal valueType is invalid.");
|
|
245
|
+
if (p.enumValues !== undefined)
|
|
246
|
+
array(p.enumValues, "enumValues").forEach((v) => wellFormedString(v, "enumValue"));
|
|
247
|
+
return cloneJson(p);
|
|
248
|
+
}
|
|
249
|
+
function validateOccurrence(input, start, end) { const o = obj(input, "occurrence"); exact(o, ["resolverVersion", "count", "selected", "selection", "hintUsed", "ambiguous"], "occurrence"); if (o.resolverVersion !== "exact-occurrence-v1")
|
|
250
|
+
throw new Error("occurrence resolver version is invalid."); integer(o.count, "occurrence.count"); const s = obj(o.selected, "occurrence.selected"); exact(s, ["index", "start", "end"], "occurrence.selected"); integer(s.index, "selected.index"); integer(s.start, "selected.start"); integer(s.end, "selected.end"); if (s.start !== start || s.end !== end || s.index >= o.count || o.count < 1)
|
|
251
|
+
throw new Error("occurrence selection is incoherent."); if (o.selection !== "source-order" && o.selection !== "occurrence-hint")
|
|
252
|
+
throw new Error("occurrence selection is invalid."); if (typeof o.hintUsed !== "boolean" || typeof o.ambiguous !== "boolean")
|
|
253
|
+
throw new Error("occurrence flags must be booleans."); if (o.hintUsed !== (o.selection === "occurrence-hint"))
|
|
254
|
+
throw new Error("occurrence hintUsed does not match selection."); if (o.ambiguous !== (o.count > 1))
|
|
255
|
+
throw new Error("occurrence ambiguous does not match count."); }
|
|
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)
|
|
257
|
+
throw new Error("prepared artifact format is invalid."); digest(a.digest, "artifact.digest", false); if (!PREPARATION_MODES.has(a.preparationMode))
|
|
258
|
+
throw new Error("artifact.preparationMode is invalid."); nonEmpty(a.preparationVersion, "artifact.preparationVersion"); integer(a.contentLength, "artifact.contentLength"); if (a.sourceSnapshotRef !== undefined)
|
|
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)
|
|
260
|
+
throw new Error("prepared artifact ref does not match its identity binding."); return cloneJson(a); }
|
|
261
|
+
function validateArtifactState(input, artifact) { if (!artifact)
|
|
262
|
+
throw new Error("prepared artifact state requires prepared artifact."); const s = obj(input, "preparedArtifactState"); const status = s.status; if (status === "digest-mismatch") {
|
|
263
|
+
exact(s, ["status", "requestedRef", "canonicalRef", "actualDigest", "actualContentLength"], "preparedArtifactState");
|
|
264
|
+
digest(s.actualDigest, "actualDigest", false);
|
|
265
|
+
integer(s.actualContentLength, "actualContentLength");
|
|
266
|
+
}
|
|
267
|
+
else if (status === "invalid-artifact") {
|
|
268
|
+
exact(s, ["status", "reason", "canonicalRef"], "preparedArtifactState");
|
|
269
|
+
if (!ARTIFACT_INVALID_REASONS.has(s.reason))
|
|
270
|
+
throw new Error("prepared artifact invalid reason is invalid.");
|
|
271
|
+
}
|
|
272
|
+
else if (["available", "unavailable", "storage-error", "identity-mismatch"].includes(status))
|
|
273
|
+
exact(s, ["status", "requestedRef", "canonicalRef"], "preparedArtifactState");
|
|
274
|
+
else
|
|
275
|
+
throw new Error("prepared artifact state is invalid."); preparedReference(s.canonicalRef, "canonicalRef"); if (s.canonicalRef !== artifact.ref)
|
|
276
|
+
throw new Error("prepared artifact canonical ref mismatch."); if (s.requestedRef !== undefined) {
|
|
277
|
+
safeReference(s.requestedRef, "requestedRef");
|
|
278
|
+
if (status !== "identity-mismatch")
|
|
279
|
+
preparedReference(s.requestedRef, "requestedRef");
|
|
280
|
+
if (status === "identity-mismatch" ? s.requestedRef === s.canonicalRef : s.requestedRef !== s.canonicalRef)
|
|
281
|
+
throw new Error(`prepared artifact ${status} requestedRef relationship is invalid.`);
|
|
282
|
+
} return cloneJson(s); }
|
|
283
|
+
function validateOutcome(input, partial) { const o = obj(input, "outcome"); if (o.status === "success")
|
|
284
|
+
exact(o, ["status"], "outcome");
|
|
285
|
+
else if (o.status === "partial") {
|
|
286
|
+
exact(o, ["status", "reason"], "outcome");
|
|
287
|
+
if (!PARTIAL.has(o.reason))
|
|
288
|
+
throw new Error("partial reason is invalid.");
|
|
289
|
+
const p = obj(partial, "partial");
|
|
290
|
+
exact(p, ["reason", "completedChunks", "remainingChunks"], "partial", ["tokenOvershoot"]);
|
|
291
|
+
if (p.reason !== o.reason)
|
|
292
|
+
throw new Error("partial reason mismatch.");
|
|
293
|
+
integer(p.completedChunks, "completedChunks");
|
|
294
|
+
integer(p.remainingChunks, "remainingChunks");
|
|
295
|
+
if (p.tokenOvershoot !== undefined) {
|
|
296
|
+
integer(p.tokenOvershoot, "tokenOvershoot");
|
|
297
|
+
if (p.tokenOvershoot === 0)
|
|
298
|
+
throw new Error("tokenOvershoot must be positive.");
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
else if (o.status === "failure") {
|
|
302
|
+
exact(o, ["status", "category", "code"], "outcome");
|
|
303
|
+
if (!FAILURE_CATEGORIES.has(o.category))
|
|
304
|
+
throw new Error("failure category is invalid.");
|
|
305
|
+
stableIdentity(o.code, "failure code");
|
|
306
|
+
}
|
|
307
|
+
else
|
|
308
|
+
throw new Error("outcome status is invalid."); if (o.status !== "partial" && partial !== undefined)
|
|
309
|
+
throw new Error("partial requires partial outcome."); }
|
|
310
|
+
function validateWarning(v) { const w = obj(v, "warning"); exact(w, ["category", "code"], "warning"); if (!WARNING_CATEGORIES.has(w.category))
|
|
311
|
+
throw new Error("warning category invalid."); stableIdentity(w.code, "warning.code"); }
|
|
312
|
+
function validateFailure(v) { const f = obj(v, "providerFailure"); exact(f, ["provider", "kind", "retryable"], "providerFailure"); stableIdentity(f.provider, "failure.provider"); if (!FAILURE_KINDS.has(f.kind) || typeof f.retryable !== "boolean")
|
|
313
|
+
throw new Error("provider failure invalid."); }
|
|
314
|
+
function validateClaimTarget(v) { const t = obj(v, "claimTarget"); exact(t, ["subjectType", "subjectId", "facet", "claimType", "fieldOrBehavior", "impactLevel"], "claimTarget", ["claimId", "evidenceType", "evidenceMethod", "collectedBy", "derivedFrom"]); for (const key of ["subjectType", "subjectId", "facet", "claimType", "fieldOrBehavior"])
|
|
315
|
+
nonEmpty(t[key], `claimTarget.${key}`); if (!["low", "medium", "high", "critical"].includes(t.impactLevel))
|
|
316
|
+
throw new Error("claimTarget.impactLevel invalid."); for (const key of ["claimId", "evidenceType", "evidenceMethod", "collectedBy"])
|
|
317
|
+
optionalString(t[key], `claimTarget.${key}`); if (t.derivedFrom !== undefined)
|
|
318
|
+
array(t.derivedFrom, "claimTarget.derivedFrom").forEach((entry) => nonEmpty(entry, "claimTarget.derivedFrom entry")); }
|
|
319
|
+
function inferValueType(v) { if (Array.isArray(v))
|
|
320
|
+
return "array"; if (v === null || typeof v === "object")
|
|
321
|
+
return "object"; if (["string", "number", "boolean"].includes(typeof v))
|
|
322
|
+
return typeof v; return "string"; }
|
|
323
|
+
function obj(v, subject) { if (!v || typeof v !== "object" || Array.isArray(v))
|
|
324
|
+
throw new Error(`${subject} must be an object.`); return v; }
|
|
325
|
+
function array(v, subject) { if (!Array.isArray(v))
|
|
326
|
+
throw new Error(`${subject} must be an array.`); return v; }
|
|
327
|
+
function exact(v, required, subject, optional = []) { for (const k of required)
|
|
328
|
+
if (!Object.hasOwn(v, k))
|
|
329
|
+
throw new Error(`${subject}.${k} is required.`); for (const k of Object.keys(v))
|
|
330
|
+
if (![...required, ...optional].includes(k))
|
|
331
|
+
throw new Error(`${subject}.${k} is unexpected.`); }
|
|
332
|
+
function nonEmpty(v, subject) { if (typeof v !== "string" || !v.trim())
|
|
333
|
+
throw new Error(`${subject} must be a non-empty string.`); }
|
|
334
|
+
function wireNonEmpty(v, subject) { wellFormedString(v, subject); if (v.length === 0)
|
|
335
|
+
throw new Error(`${subject} must be non-empty.`); }
|
|
336
|
+
function wellFormedString(v, subject) { if (typeof v !== "string" || !isWellFormedUnicode(v))
|
|
337
|
+
throw new Error(`${subject} must be a well-formed string.`); }
|
|
338
|
+
function optionalString(v, subject) { if (v !== undefined)
|
|
339
|
+
nonEmpty(v, subject); }
|
|
340
|
+
function finite(v, subject, min, max) { if (typeof v !== "number" || !Number.isFinite(v) || v < min || v > max)
|
|
341
|
+
throw new Error(`${subject} is invalid.`); }
|
|
342
|
+
function integer(v, subject) { if (!Number.isSafeInteger(v) || v < 0)
|
|
343
|
+
throw new Error(`${subject} must be a non-negative safe integer.`); }
|
|
344
|
+
function digest(v, subject, prefixed = true) { if (typeof v !== "string" || !(prefixed ? /^sha256:[a-f0-9]{64}$/ : /^[a-f0-9]{64}$/).test(v))
|
|
345
|
+
throw new Error(`${subject} is invalid.`); }
|
|
346
|
+
function optionalDigest(v, subject) { if (v !== undefined)
|
|
347
|
+
digest(v, subject); }
|
|
348
|
+
function safeReference(v, subject) { wireNonEmpty(v, subject); if (referenceContainsAuthorization(v))
|
|
349
|
+
throw new Error(`${subject} contains authorization material.`); }
|
|
350
|
+
function referenceContainsAuthorization(value, depth = 0) { if (depth > 2 || /authorization\s*[:=]|bearer\s+[a-z0-9._~-]+/i.test(value))
|
|
351
|
+
return true; let parsed; try {
|
|
352
|
+
parsed = new URL(value);
|
|
353
|
+
}
|
|
354
|
+
catch {
|
|
355
|
+
return false;
|
|
356
|
+
} if (parsed.username || parsed.password)
|
|
357
|
+
return true; for (const [key, nested] of parsed.searchParams) {
|
|
358
|
+
if (/(?:^|[-_])(token|secret|password|passwd|api[-_]?key|authorization|signature|credential)(?:$|[-_])/i.test(key) || referenceContainsAuthorization(nested, depth + 1))
|
|
359
|
+
return true;
|
|
360
|
+
} return false; }
|
|
361
|
+
function stableIdentity(v, subject) { wireNonEmpty(v, subject); if (!STABLE_IDENTITY.test(v) || CREDENTIAL_IDENTITY.test(v) || referenceContainsAuthorization(v))
|
|
362
|
+
throw new Error(`${subject} must be a credential-free stable identity.`); }
|
|
363
|
+
function preparedReference(v, subject) { safeReference(v, subject); if (!/^traverse-prepared-artifact:v1:sha256:[a-f0-9]{64}$/.test(v))
|
|
364
|
+
throw new Error(`${subject} must be a prepared-artifact reference.`); }
|
|
365
|
+
function cloneJson(v) { return JSON.parse(JSON.stringify(v)); }
|
|
366
|
+
function identityHash(v) { return sha256Hex(canonicalJson(v)).slice(0, 20); }
|
|
367
|
+
function jsonSafe(v, subject, seen = new Set()) { if (v === null || typeof v === "boolean")
|
|
368
|
+
return; if (typeof v === "string") {
|
|
369
|
+
if (!isWellFormedUnicode(v))
|
|
370
|
+
throw new Error(`${subject} contains ill-formed Unicode.`);
|
|
371
|
+
return;
|
|
372
|
+
} if (typeof v === "number") {
|
|
373
|
+
if (!Number.isFinite(v) || Object.is(v, -0))
|
|
374
|
+
throw new Error(`${subject} contains a non-lossless number.`);
|
|
375
|
+
return;
|
|
376
|
+
} if (Array.isArray(v)) {
|
|
377
|
+
if (seen.has(v))
|
|
378
|
+
throw new Error(`${subject} contains a cycle.`);
|
|
379
|
+
const keys = Reflect.ownKeys(v);
|
|
380
|
+
const expected = new Set(["length", ...Array.from({ length: v.length }, (_, index) => String(index))]);
|
|
381
|
+
for (const key of keys) {
|
|
382
|
+
if (typeof key === "symbol" || !expected.has(key))
|
|
383
|
+
throw new Error(`${subject} is sparse or has unexpected array properties.`);
|
|
384
|
+
const descriptor = Object.getOwnPropertyDescriptor(v, key);
|
|
385
|
+
if (!descriptor || !("value" in descriptor))
|
|
386
|
+
throw new Error(`${subject} has an array accessor property.`);
|
|
387
|
+
if (key !== "length" && !descriptor.enumerable)
|
|
388
|
+
throw new Error(`${subject} has a non-enumerable array item.`);
|
|
389
|
+
}
|
|
390
|
+
if (keys.length !== expected.size)
|
|
391
|
+
throw new Error(`${subject} contains a sparse array slot.`);
|
|
392
|
+
seen.add(v);
|
|
393
|
+
for (let i = 0; i < v.length; i++) {
|
|
394
|
+
const descriptor = Object.getOwnPropertyDescriptor(v, String(i));
|
|
395
|
+
jsonSafe(descriptor.value, `${subject}[${i}]`, seen);
|
|
396
|
+
}
|
|
397
|
+
seen.delete(v);
|
|
398
|
+
return;
|
|
399
|
+
} if (typeof v !== "object" || Object.getPrototypeOf(v) !== Object.prototype)
|
|
400
|
+
throw new Error(`${subject} contains a non-JSON value.`); if (seen.has(v))
|
|
401
|
+
throw new Error(`${subject} contains a cycle.`); seen.add(v); for (const key of Reflect.ownKeys(v)) {
|
|
402
|
+
if (typeof key !== "string")
|
|
403
|
+
throw new Error(`${subject} contains a symbol key.`);
|
|
404
|
+
const d = Object.getOwnPropertyDescriptor(v, key);
|
|
405
|
+
if (!d?.enumerable || !("value" in d) || !isWellFormedUnicode(key))
|
|
406
|
+
throw new Error(`${subject}.${key} is not a lossless JSON property.`);
|
|
407
|
+
jsonSafe(d.value, `${subject}.${key}`, seen);
|
|
408
|
+
} seen.delete(v); }
|
|
409
|
+
function isWellFormedUnicode(value) { for (let index = 0; index < value.length; index += 1) {
|
|
410
|
+
const unit = value.charCodeAt(index);
|
|
411
|
+
if (unit >= 0xd800 && unit <= 0xdbff) {
|
|
412
|
+
if (index + 1 >= value.length)
|
|
413
|
+
return false;
|
|
414
|
+
const next = value.charCodeAt(index + 1);
|
|
415
|
+
if (next < 0xdc00 || next > 0xdfff)
|
|
416
|
+
return false;
|
|
417
|
+
index += 1;
|
|
418
|
+
}
|
|
419
|
+
else if (unit >= 0xdc00 && unit <= 0xdfff)
|
|
420
|
+
return false;
|
|
421
|
+
} return true; }
|
|
422
|
+
const RAW_SOURCE_KINDS = new Set(["uploaded-document", "web-page", "api-record", "manual-entry", "policy-standard", "inquiry-question", "agent-utterance", "system-schema"]);
|
|
423
|
+
const VALUE_TYPES = new Set(["string", "number", "boolean", "date", "enum", "array", "object"]);
|
|
424
|
+
const PARTIAL = new Set(["cancelled", "max-provider-calls", "max-total-tokens"]);
|
|
425
|
+
const FAILURE_CATEGORIES = new Set(["invalid-config", "invalid-task", "preparation", "provider", "unexpected"]);
|
|
426
|
+
const WARNING_CATEGORIES = new Set(["provider", "normalization", "preparation", "limit", "storage", "content", "other"]);
|
|
427
|
+
const FAILURE_KINDS = new Set(["authentication", "rate-limit", "timeout", "invalid-request", "unavailable", "unknown"]);
|
|
428
|
+
const PREPARATION_MODES = new Set(["text", "markdown", "transcript", "pdf-text", "image-ocr"]);
|
|
429
|
+
const ARTIFACT_INVALID_REASONS = new Set(["not-an-object", "invalid-format", "invalid-version", "invalid-digest", "invalid-ref", "invalid-preparation-mode", "invalid-preparation-version", "invalid-content-length", "invalid-source-snapshot-ref", "ill-formed-unicode", "invalid-resolved-text"]);
|
|
430
|
+
const STABLE_IDENTITY = /^[A-Za-z0-9][A-Za-z0-9._:@/+~-]{0,255}$/;
|
|
431
|
+
const CREDENTIAL_IDENTITY = /^(?:gh[pousr]_|sk-[A-Za-z0-9]|AKIA[A-Z0-9]|ASIA[A-Z0-9]|eyJ[A-Za-z0-9_-]+\.eyJ)/;
|
|
432
|
+
const RUN_ID = /^traverse-extraction-run:[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
|
package/dist/src/index.d.ts
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
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, 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";
|
|
7
|
+
export type { ExtractionEnvelopeImport, ExtractionEnvelopeImportDiagnostic, ExtractionEnvelopeImportOptions, ExtractionEnvelopeImportResult, ExtractionEnvelopeResolutionIdentity, PortableExtractionOccurrence, PortableExtractionProposal, PortableExtractionResultEnvelope, PortablePreparedArtifactState, } from "./extraction-envelope.js";
|
|
4
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";
|
|
5
9
|
export { candidateReviewRecord, candidateSetStatusFor, SurveyInputBuilder } from "./builder.js";
|
|
6
10
|
export type { CandidateReviewRecordInput, SurveyClaimRecord, SurveyInputBuilderArgs, SurveyObservationInput, } from "./builder.js";
|
package/dist/src/index.js
CHANGED
|
@@ -1,5 +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, validateExtractionEnvelopeImport, } from "./extraction-envelope.js";
|
|
4
|
+
export { buildExtractionInspectorModel, exportExtractionInspector, filterExtractionInspectorCandidates, } from "./review-workbench/extraction-inspector.js";
|
|
3
5
|
export { candidateReviewRecord, candidateSetStatusFor, SurveyInputBuilder } from "./builder.js";
|
|
4
6
|
export { reviewedCandidateResolution } from "./reviewed-candidate-resolution.js";
|
|
5
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
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kontourai/survey",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.18.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",
|
|
@@ -87,6 +87,7 @@
|
|
|
87
87
|
},
|
|
88
88
|
"devDependencies": {
|
|
89
89
|
"@anthropic-ai/sdk": "^0.54.0",
|
|
90
|
+
"@kontourai/traverse": "^0.19.0",
|
|
90
91
|
"@kontourai/ui": "^1.1.0",
|
|
91
92
|
"@playwright/test": "^1.60.0",
|
|
92
93
|
"@types/node": "^25.6.0",
|