@kontourai/survey 0.4.23 → 0.5.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 +86 -872
- package/dist/example-data/corrected-document-candidates.d.ts +2 -0
- package/dist/{fixtures → example-data}/corrected-document-candidates.js +3 -3
- package/dist/{fixtures → example-data}/downstream-public-directory-proposal.d.ts +1 -1
- package/dist/{fixtures → example-data}/downstream-public-directory-proposal.js +1 -1
- package/dist/{fixtures → example-data}/public-directory-review-resource.d.ts +2 -2
- package/dist/{fixtures → example-data}/public-directory-review-resource.js +2 -2
- package/dist/example-data/public-field-review.d.ts +2 -0
- package/dist/{fixtures → example-data}/public-field-review.js +2 -2
- package/dist/{fixtures → example-data}/regulated-document-review-resource.d.ts +1 -1
- package/dist/{fixtures → example-data}/regulated-document-review-resource.js +3 -3
- package/dist/examples/public-field-observation.js +4 -4
- package/dist/examples/review-workbench/downstream-public-directory-adapter.d.ts +1 -1
- package/dist/examples/review-workbench/facility-credential-consumer.d.ts +2 -2
- package/dist/examples/review-workbench/facility-credential-consumer.js +8 -8
- package/dist/examples/review-workbench/server-apply-consumer.d.ts +1 -1
- package/dist/examples/review-workbench/server-apply-consumer.js +3 -3
- package/dist/src/agent-utterance.d.ts +166 -0
- package/dist/src/agent-utterance.js +373 -0
- package/dist/src/anthropic.d.ts +104 -0
- package/dist/src/anthropic.js +383 -0
- package/dist/src/index.d.ts +10 -2
- package/dist/src/index.js +5 -1
- package/dist/src/inquiry-mapping.d.ts +256 -0
- package/dist/src/inquiry-mapping.js +385 -0
- package/dist/src/review-workbench/review-queue-session.js +3 -3
- package/dist/src/review-workbench/review-surface-preview.js +1 -1
- package/dist/src/review-workbench/review-workbench-data.d.ts +4 -4
- package/dist/src/review-workbench/review-workbench-data.js +14 -14
- package/dist/src/schema-mapping.d.ts +196 -0
- package/dist/src/schema-mapping.js +486 -0
- package/dist/src/to-flow-artifact.d.ts +41 -0
- package/dist/src/to-flow-artifact.js +31 -0
- package/dist/src/to-surface.d.ts +3 -3
- package/dist/src/to-surface.js +1 -1
- package/dist/src/types.d.ts +2 -2
- package/package.json +25 -9
- package/dist/fixtures/corrected-document-candidates.d.ts +0 -2
- package/dist/fixtures/public-field-review.d.ts +0 -2
|
@@ -0,0 +1,385 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Inquiry mapping — ADR 0003 step 5.
|
|
3
|
+
*
|
|
4
|
+
* This module implements the "memoize the mapping, never the answer" principle.
|
|
5
|
+
* A MappingProposal is a reviewable record produced by a pluggable MappingProposer.
|
|
6
|
+
* Proposals flow through Survey's existing candidate → review machinery.
|
|
7
|
+
* The durable artifact after review is an InquiryMapping.
|
|
8
|
+
* Answers always recompute live from the TrustBundle.
|
|
9
|
+
*
|
|
10
|
+
* Nothing here silently decides. Exact canonical-form matching is the only thing
|
|
11
|
+
* that resolves without review. A proposer may suggest that a question maps to a
|
|
12
|
+
* registered claim or rule — but every suggestion lands as a MappingProposal with
|
|
13
|
+
* provenance before it counts. (ADR 0003 §4)
|
|
14
|
+
*
|
|
15
|
+
* Integration point: resolveQuestion is the entry point for consumers checking
|
|
16
|
+
* whether a cached mapping already covers a question. Flow-agent hook wiring
|
|
17
|
+
* (connecting this to an agent's output pipeline) is out of scope for this module
|
|
18
|
+
* and lives in the flow-agents repo.
|
|
19
|
+
*/
|
|
20
|
+
import { resolveInquiry } from "@kontourai/surface";
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
// Question normalization
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
/**
|
|
25
|
+
* Deterministic normalization for question strings.
|
|
26
|
+
*
|
|
27
|
+
* Rules:
|
|
28
|
+
* - Lowercase
|
|
29
|
+
* - Collapse internal whitespace runs to a single space
|
|
30
|
+
* - Trim leading/trailing whitespace
|
|
31
|
+
* - Strip terminal punctuation (. ? ! , ;) from the end
|
|
32
|
+
*
|
|
33
|
+
* This is exact normalized-text memoization, not semantic matching.
|
|
34
|
+
* Two questions that differ only in case, whitespace, or trailing punctuation
|
|
35
|
+
* are considered the same question. Questions with different wording but the
|
|
36
|
+
* same intent are NOT matched here; a MappingProposer handles that.
|
|
37
|
+
*/
|
|
38
|
+
export function normalizeQuestion(question) {
|
|
39
|
+
return question
|
|
40
|
+
.toLowerCase()
|
|
41
|
+
.replace(/\s+/g, " ")
|
|
42
|
+
.trim()
|
|
43
|
+
.replace(/[.?!,;]+$/u, "");
|
|
44
|
+
}
|
|
45
|
+
// ---------------------------------------------------------------------------
|
|
46
|
+
// Proposal → CandidateSet projection
|
|
47
|
+
// ---------------------------------------------------------------------------
|
|
48
|
+
/**
|
|
49
|
+
* Project an array of proposals for a single question into Survey's existing
|
|
50
|
+
* Candidate / CandidateSet shapes so they flow through the existing review
|
|
51
|
+
* machinery rather than a parallel system.
|
|
52
|
+
*
|
|
53
|
+
* Status rules:
|
|
54
|
+
* - All proposals agree on the same target/ruleId → "needs-review"
|
|
55
|
+
* - Proposals disagree (more than one distinct resolved target/rule) → "conflict"
|
|
56
|
+
* - Empty proposals → "needs-review" with empty candidates
|
|
57
|
+
*
|
|
58
|
+
* The CandidateSet target is the normalized question; each Candidate carries
|
|
59
|
+
* the proposal id as extractionId and the proposal metadata.
|
|
60
|
+
*/
|
|
61
|
+
export function proposalsToCandidateSet(question, proposals) {
|
|
62
|
+
const normalized = normalizeQuestion(question);
|
|
63
|
+
const candidates = proposals.map((proposal) => ({
|
|
64
|
+
id: `mapping-candidate.${proposal.id}`,
|
|
65
|
+
extractionId: proposal.id,
|
|
66
|
+
value: proposal.proposedTarget ?? proposal.proposedRuleId ?? null,
|
|
67
|
+
confidence: proposal.confidence,
|
|
68
|
+
metadata: {
|
|
69
|
+
mappingProposal: {
|
|
70
|
+
proposalId: proposal.id,
|
|
71
|
+
proposedTarget: proposal.proposedTarget,
|
|
72
|
+
proposedRuleId: proposal.proposedRuleId,
|
|
73
|
+
confidence: proposal.confidence,
|
|
74
|
+
rationale: proposal.rationale,
|
|
75
|
+
excerpt: proposal.excerpt,
|
|
76
|
+
proposedBy: proposal.proposedBy,
|
|
77
|
+
proposedAt: proposal.proposedAt,
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
}));
|
|
81
|
+
// Determine status: conflict if proposals disagree on target/rule
|
|
82
|
+
const status = proposals.length > 1 && proposalsDisagree(proposals) ? "conflict" : "needs-review";
|
|
83
|
+
const candidateSet = {
|
|
84
|
+
id: `mapping-candidate-set.${normalized}`,
|
|
85
|
+
target: normalized,
|
|
86
|
+
candidates,
|
|
87
|
+
status,
|
|
88
|
+
metadata: {
|
|
89
|
+
inquiryMapping: {
|
|
90
|
+
question,
|
|
91
|
+
normalizedQuestion: normalized,
|
|
92
|
+
kind: "inquiry-question",
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
};
|
|
96
|
+
return { candidateSet, candidates };
|
|
97
|
+
}
|
|
98
|
+
function proposalsDisagree(proposals) {
|
|
99
|
+
const keys = new Set(proposals.map((p) => p.proposedTarget
|
|
100
|
+
? `target:${p.proposedTarget.subjectType}/${p.proposedTarget.subjectId}/${p.proposedTarget.fieldOrBehavior}`
|
|
101
|
+
: `rule:${p.proposedRuleId}`));
|
|
102
|
+
return keys.size > 1;
|
|
103
|
+
}
|
|
104
|
+
// ---------------------------------------------------------------------------
|
|
105
|
+
// Review outcome → InquiryMapping
|
|
106
|
+
// ---------------------------------------------------------------------------
|
|
107
|
+
/**
|
|
108
|
+
* Turn a Survey ReviewOutcome on a mapping candidate set into a durable
|
|
109
|
+
* InquiryMapping record.
|
|
110
|
+
*
|
|
111
|
+
* The candidateSet must have been built by proposalsToCandidateSet.
|
|
112
|
+
* The reviewOutcome's candidateId must match one of the candidates.
|
|
113
|
+
*/
|
|
114
|
+
export function applyMappingReview(candidateSet, reviewOutcome) {
|
|
115
|
+
const normalized = candidateSet.target;
|
|
116
|
+
// Find the reviewed candidate
|
|
117
|
+
const candidateId = reviewOutcome.candidateId ?? candidateSet.selectedCandidateId ?? candidateSet.candidates[0]?.id;
|
|
118
|
+
const candidate = candidateSet.candidates.find((c) => c.id === candidateId);
|
|
119
|
+
if (!candidate) {
|
|
120
|
+
throw new Error(`applyMappingReview: no candidate found for id ${candidateId ?? "<none>"}`);
|
|
121
|
+
}
|
|
122
|
+
const meta = candidate.metadata?.mappingProposal;
|
|
123
|
+
const proposalId = meta?.proposalId ?? candidate.extractionId;
|
|
124
|
+
const status = reviewOutcome.status === "verified" || reviewOutcome.status === "assumed" || reviewOutcome.status === "rejected"
|
|
125
|
+
? reviewOutcome.status
|
|
126
|
+
: "rejected";
|
|
127
|
+
return {
|
|
128
|
+
id: `inquiry-mapping.${normalized}`,
|
|
129
|
+
normalizedQuestion: normalized,
|
|
130
|
+
target: meta?.proposedTarget,
|
|
131
|
+
ruleId: meta?.proposedRuleId,
|
|
132
|
+
status: status,
|
|
133
|
+
reviewedBy: reviewOutcome.actor ?? "unknown",
|
|
134
|
+
reviewedAt: reviewOutcome.reviewedAt ?? new Date().toISOString(),
|
|
135
|
+
rationale: reviewOutcome.rationale,
|
|
136
|
+
withinComfortZone: reviewOutcome.withinComfortZone,
|
|
137
|
+
proposalId,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Apply an auto-accept policy to a list of proposals, returning InquiryMappings.
|
|
142
|
+
*
|
|
143
|
+
* Proposals at or above minConfidence → status "assumed", withinComfortZone: true
|
|
144
|
+
* Proposals below minConfidence → return a "needs-review" mapping (not yet durable)
|
|
145
|
+
*
|
|
146
|
+
* Only non-conflicting proposals are auto-accepted. If proposals disagree, they
|
|
147
|
+
* need human review regardless of confidence.
|
|
148
|
+
*
|
|
149
|
+
* Returns an array of InquiryMappings (only for accepted proposals).
|
|
150
|
+
*/
|
|
151
|
+
export function applyAutoAcceptPolicy(proposals, policy) {
|
|
152
|
+
if (proposals.length === 0)
|
|
153
|
+
return [];
|
|
154
|
+
// If proposals disagree, none can be auto-accepted
|
|
155
|
+
if (proposals.length > 1 && proposalsDisagree(proposals))
|
|
156
|
+
return [];
|
|
157
|
+
return proposals
|
|
158
|
+
.filter((p) => p.confidence >= policy.minConfidence)
|
|
159
|
+
.map((proposal) => ({
|
|
160
|
+
id: `inquiry-mapping.auto.${normalizeQuestion(proposal.question)}`,
|
|
161
|
+
normalizedQuestion: normalizeQuestion(proposal.question),
|
|
162
|
+
target: proposal.proposedTarget,
|
|
163
|
+
ruleId: proposal.proposedRuleId,
|
|
164
|
+
status: "assumed",
|
|
165
|
+
reviewedBy: "auto-accept-policy",
|
|
166
|
+
reviewedAt: proposal.proposedAt,
|
|
167
|
+
rationale: `Auto-accepted: confidence ${proposal.confidence} >= threshold ${policy.minConfidence}. ${proposal.rationale}`,
|
|
168
|
+
withinComfortZone: true,
|
|
169
|
+
proposalId: proposal.id,
|
|
170
|
+
}));
|
|
171
|
+
}
|
|
172
|
+
// ---------------------------------------------------------------------------
|
|
173
|
+
// Mapping lookup
|
|
174
|
+
// ---------------------------------------------------------------------------
|
|
175
|
+
/**
|
|
176
|
+
* Look up an InquiryMapping for a question by exact normalized-text match.
|
|
177
|
+
*
|
|
178
|
+
* Rejected mappings are remembered but never resolve: this function returns
|
|
179
|
+
* undefined for rejected mappings so callers treat the question as a miss.
|
|
180
|
+
* To check whether a question was previously rejected (and should not be
|
|
181
|
+
* re-proposed), call lookupRejectedMapping.
|
|
182
|
+
*/
|
|
183
|
+
export function lookupMapping(mappings, question) {
|
|
184
|
+
const normalized = normalizeQuestion(question);
|
|
185
|
+
const match = mappings.find((m) => m.normalizedQuestion === normalized);
|
|
186
|
+
if (!match)
|
|
187
|
+
return undefined;
|
|
188
|
+
// Rejected mappings are remembered but never resolve
|
|
189
|
+
if (match.status === "rejected")
|
|
190
|
+
return undefined;
|
|
191
|
+
return match;
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Check whether a question was previously rejected.
|
|
195
|
+
* Rejected mappings prevent re-proposing: if this returns a mapping, the
|
|
196
|
+
* question should not be sent to a proposer again without human escalation.
|
|
197
|
+
*/
|
|
198
|
+
export function lookupRejectedMapping(mappings, question) {
|
|
199
|
+
const normalized = normalizeQuestion(question);
|
|
200
|
+
return mappings.find((m) => m.normalizedQuestion === normalized && m.status === "rejected");
|
|
201
|
+
}
|
|
202
|
+
// ---------------------------------------------------------------------------
|
|
203
|
+
// Live question resolution
|
|
204
|
+
// ---------------------------------------------------------------------------
|
|
205
|
+
/**
|
|
206
|
+
* Resolve a natural-language question against a TrustBundle.
|
|
207
|
+
*
|
|
208
|
+
* On mapping hit (verified or assumed): constructs a Surface Inquiry from the
|
|
209
|
+
* mapped target/rule and returns resolveInquiry(...) — answers always recompute
|
|
210
|
+
* live from the current bundle state; the mapping is memoized, not the answer.
|
|
211
|
+
*
|
|
212
|
+
* On miss (no mapping, or rejected mapping): returns an InquiryRecord with
|
|
213
|
+
* outcome "unsupported" so the gap is honest and recordable.
|
|
214
|
+
*
|
|
215
|
+
* This function is the clean integration point for consumers. Flow-agent hook
|
|
216
|
+
* wiring (connecting this to an agent's output pipeline) lives in the
|
|
217
|
+
* flow-agents repo.
|
|
218
|
+
*/
|
|
219
|
+
export function resolveQuestion(bundle, question, options) {
|
|
220
|
+
const { mappings, rules, now, askedBy } = options;
|
|
221
|
+
const normalized = normalizeQuestion(question);
|
|
222
|
+
const mapping = lookupMapping(mappings, question);
|
|
223
|
+
const askedAt = (now ?? new Date()).toISOString();
|
|
224
|
+
const inquiryId = `inquiry.${normalized}.${askedAt}`;
|
|
225
|
+
if (!mapping) {
|
|
226
|
+
// No mapping or rejected: return honest unsupported record
|
|
227
|
+
return {
|
|
228
|
+
id: inquiryId,
|
|
229
|
+
inquiry: {
|
|
230
|
+
id: inquiryId,
|
|
231
|
+
question,
|
|
232
|
+
askedBy,
|
|
233
|
+
askedAt,
|
|
234
|
+
},
|
|
235
|
+
outcome: "unsupported",
|
|
236
|
+
resolutionPath: { claimIds: [] },
|
|
237
|
+
inputSnapshot: [],
|
|
238
|
+
statusFunctionVersion: "1",
|
|
239
|
+
resolvedAt: askedAt,
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
// Build a Surface Inquiry from the mapping
|
|
243
|
+
const inquiry = {
|
|
244
|
+
id: inquiryId,
|
|
245
|
+
question,
|
|
246
|
+
target: mapping.target,
|
|
247
|
+
askedBy,
|
|
248
|
+
askedAt,
|
|
249
|
+
metadata: {
|
|
250
|
+
inquiryMappingId: mapping.id,
|
|
251
|
+
normalizedQuestion: normalized,
|
|
252
|
+
},
|
|
253
|
+
};
|
|
254
|
+
return resolveInquiry(bundle, inquiry, { now, rules });
|
|
255
|
+
}
|
|
256
|
+
// ---------------------------------------------------------------------------
|
|
257
|
+
// Review workbench integration
|
|
258
|
+
// ---------------------------------------------------------------------------
|
|
259
|
+
/**
|
|
260
|
+
* Build ReviewItem records for the existing review workbench from a list of
|
|
261
|
+
* mapping candidate sets.
|
|
262
|
+
*
|
|
263
|
+
* Follow the existing ReviewItem contract exactly. This helper produces
|
|
264
|
+
* ReviewItem payloads so mapping proposals can be reviewed through the same
|
|
265
|
+
* workbench as other Survey candidates.
|
|
266
|
+
*/
|
|
267
|
+
export function buildMappingReviewItems(candidateSets) {
|
|
268
|
+
return candidateSets.map(({ candidateSet, candidates }) => ({
|
|
269
|
+
apiVersion: "survey.kontourai.io/v1alpha1",
|
|
270
|
+
kind: "ReviewItem",
|
|
271
|
+
metadata: {
|
|
272
|
+
name: candidateSet.id,
|
|
273
|
+
labels: { "survey.kontourai.io/kind": "inquiry-mapping" },
|
|
274
|
+
},
|
|
275
|
+
spec: {
|
|
276
|
+
target: candidateSet.target,
|
|
277
|
+
candidates: candidates.map((candidate) => {
|
|
278
|
+
const meta = candidate.metadata?.mappingProposal;
|
|
279
|
+
const targetOrRule = meta?.proposedTarget
|
|
280
|
+
? `${meta.proposedTarget.subjectType}/${meta.proposedTarget.subjectId}/${meta.proposedTarget.fieldOrBehavior}`
|
|
281
|
+
: `rule:${meta?.proposedRuleId ?? "unknown"}`;
|
|
282
|
+
return {
|
|
283
|
+
id: candidate.id,
|
|
284
|
+
role: "proposed",
|
|
285
|
+
value: candidate.value,
|
|
286
|
+
confidence: candidate.confidence,
|
|
287
|
+
source: {
|
|
288
|
+
sourceRef: `inquiry-question:${candidateSet.target}`,
|
|
289
|
+
kind: "inquiry-question",
|
|
290
|
+
observedAt: meta?.proposedAt ?? new Date().toISOString(),
|
|
291
|
+
locatorScheme: "text",
|
|
292
|
+
},
|
|
293
|
+
extraction: {
|
|
294
|
+
target: targetOrRule,
|
|
295
|
+
confidence: meta?.confidence,
|
|
296
|
+
extractor: meta?.proposedBy ?? "unknown",
|
|
297
|
+
extractedAt: meta?.proposedAt ?? new Date().toISOString(),
|
|
298
|
+
},
|
|
299
|
+
claimTarget: {
|
|
300
|
+
subjectType: meta?.proposedTarget?.subjectType ?? "inquiry",
|
|
301
|
+
subjectId: meta?.proposedTarget?.subjectId ?? candidateSet.target,
|
|
302
|
+
surface: "inquiry.mapping",
|
|
303
|
+
claimType: "inquiry-mapping",
|
|
304
|
+
fieldOrBehavior: meta?.proposedTarget?.fieldOrBehavior ?? meta?.proposedRuleId ?? "unknown",
|
|
305
|
+
impactLevel: "low",
|
|
306
|
+
},
|
|
307
|
+
projection: {
|
|
308
|
+
candidateSetId: candidateSet.id,
|
|
309
|
+
candidateId: candidate.id,
|
|
310
|
+
},
|
|
311
|
+
};
|
|
312
|
+
}),
|
|
313
|
+
candidateSetStatus: candidateSet.status,
|
|
314
|
+
rationale: candidateSet.rationale,
|
|
315
|
+
},
|
|
316
|
+
status: {
|
|
317
|
+
observedCandidateCount: candidates.length,
|
|
318
|
+
},
|
|
319
|
+
}));
|
|
320
|
+
}
|
|
321
|
+
// ---------------------------------------------------------------------------
|
|
322
|
+
// Reference proposer (deterministic, for tests — not for production use)
|
|
323
|
+
// ---------------------------------------------------------------------------
|
|
324
|
+
/**
|
|
325
|
+
* Reference MappingProposer for tests.
|
|
326
|
+
*
|
|
327
|
+
* REFERENCE IMPLEMENTATION ONLY — not suitable for production matching.
|
|
328
|
+
*
|
|
329
|
+
* Matching strategy: a question maps to a claim if it contains both the
|
|
330
|
+
* claim's subjectId and fieldOrBehavior as token substrings (case-insensitive,
|
|
331
|
+
* space-delimited token match). This is intentionally simple and transparent
|
|
332
|
+
* so tests can be deterministic.
|
|
333
|
+
*/
|
|
334
|
+
export const referenceMappingProposer = {
|
|
335
|
+
name: "reference-mapping-proposer",
|
|
336
|
+
propose(question, context) {
|
|
337
|
+
const q = question.toLowerCase();
|
|
338
|
+
const proposals = [];
|
|
339
|
+
const now = new Date().toISOString();
|
|
340
|
+
if (context.bundle) {
|
|
341
|
+
for (const claim of context.bundle.claims) {
|
|
342
|
+
const subjectTokens = claim.subjectId.toLowerCase().split(/[\s\-_.:/]+/);
|
|
343
|
+
const fieldTokens = claim.fieldOrBehavior.toLowerCase().split(/[\s\-_.:/]+/);
|
|
344
|
+
const subjectMatch = subjectTokens.some((tok) => tok.length > 2 && q.includes(tok));
|
|
345
|
+
const fieldMatch = fieldTokens.some((tok) => tok.length > 2 && q.includes(tok));
|
|
346
|
+
if (subjectMatch && fieldMatch) {
|
|
347
|
+
const proposalId = `proposal.ref.${claim.subjectId}.${claim.fieldOrBehavior}.${Date.now()}`;
|
|
348
|
+
proposals.push({
|
|
349
|
+
id: proposalId,
|
|
350
|
+
question,
|
|
351
|
+
proposedTarget: {
|
|
352
|
+
subjectType: claim.subjectType,
|
|
353
|
+
subjectId: claim.subjectId,
|
|
354
|
+
fieldOrBehavior: claim.fieldOrBehavior,
|
|
355
|
+
qualifiers: claim.qualifiers,
|
|
356
|
+
},
|
|
357
|
+
confidence: 0.75,
|
|
358
|
+
rationale: `Reference proposer: question contains subjectId token and fieldOrBehavior token for claim ${claim.id}.`,
|
|
359
|
+
proposedBy: "reference-mapping-proposer",
|
|
360
|
+
proposedAt: now,
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
if (context.rules) {
|
|
366
|
+
for (const rule of context.rules) {
|
|
367
|
+
const ruleTokens = rule.name.toLowerCase().split(/[\s\-_.:/]+/);
|
|
368
|
+
const ruleMatch = ruleTokens.some((tok) => tok.length > 2 && q.includes(tok));
|
|
369
|
+
if (ruleMatch) {
|
|
370
|
+
const proposalId = `proposal.ref.rule.${rule.id}.${Date.now()}`;
|
|
371
|
+
proposals.push({
|
|
372
|
+
id: proposalId,
|
|
373
|
+
question,
|
|
374
|
+
proposedRuleId: rule.id,
|
|
375
|
+
confidence: 0.65,
|
|
376
|
+
rationale: `Reference proposer: question contains rule name token for rule ${rule.id}.`,
|
|
377
|
+
proposedBy: "reference-mapping-proposer",
|
|
378
|
+
proposedAt: now,
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
return proposals;
|
|
384
|
+
},
|
|
385
|
+
};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { publicDirectoryReviewItemExample, reviewWorkbenchQueueExamples } from "./review-workbench-data.js";
|
|
2
2
|
import { reviewResourceApiVersion, } from "../../src/review-resource.js";
|
|
3
3
|
export const reviewWorkbenchSessionStorageKey = "kontourai.survey.review-workbench.session-events.v1";
|
|
4
4
|
export const defaultReviewSessionName = "review-workbench-session";
|
|
@@ -22,7 +22,7 @@ export const workbenchDecisionDefinitions = {
|
|
|
22
22
|
status: "rejected",
|
|
23
23
|
},
|
|
24
24
|
};
|
|
25
|
-
export function initialReviewWorkbenchState(item =
|
|
25
|
+
export function initialReviewWorkbenchState(item = publicDirectoryReviewItemExample) {
|
|
26
26
|
return {
|
|
27
27
|
item,
|
|
28
28
|
note: "",
|
|
@@ -31,7 +31,7 @@ export function initialReviewWorkbenchState(item = publicDirectoryReviewItemFixt
|
|
|
31
31
|
actorId: "review-workbench-operator",
|
|
32
32
|
};
|
|
33
33
|
}
|
|
34
|
-
export function initialReviewQueueSessionState(items =
|
|
34
|
+
export function initialReviewQueueSessionState(items = reviewWorkbenchQueueExamples) {
|
|
35
35
|
return {
|
|
36
36
|
items,
|
|
37
37
|
activeItemName: items[0]?.metadata.name ?? "",
|
|
@@ -109,7 +109,7 @@ function portableAuthorityTrace(value) {
|
|
|
109
109
|
return {
|
|
110
110
|
status: "provided",
|
|
111
111
|
label: "Portable authority trace provided",
|
|
112
|
-
detail: `${value.length} authority trace entr${value.length === 1 ? "y" : "ies"} supplied by the
|
|
112
|
+
detail: `${value.length} authority trace entr${value.length === 1 ? "y" : "ies"} supplied by the example.`,
|
|
113
113
|
};
|
|
114
114
|
}
|
|
115
115
|
return {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type ReviewItem } from "../review-resource.js";
|
|
2
|
-
export declare const
|
|
2
|
+
export declare const publicDirectoryReviewItemExample: {
|
|
3
3
|
apiVersion: "survey.kontourai.io/v1alpha1";
|
|
4
4
|
kind: "ReviewItem";
|
|
5
5
|
metadata: {
|
|
@@ -129,7 +129,7 @@ export declare const publicDirectoryReviewItemFixture: {
|
|
|
129
129
|
reviewDecisionName: string;
|
|
130
130
|
};
|
|
131
131
|
};
|
|
132
|
-
export declare const
|
|
132
|
+
export declare const regulatedRuleConflictReviewItemExample: {
|
|
133
133
|
apiVersion: "survey.kontourai.io/v1alpha1";
|
|
134
134
|
kind: "ReviewItem";
|
|
135
135
|
metadata: {
|
|
@@ -267,7 +267,7 @@ export declare const regulatedRuleConflictReviewItemFixture: {
|
|
|
267
267
|
selectedCandidateId: string;
|
|
268
268
|
};
|
|
269
269
|
};
|
|
270
|
-
export declare const
|
|
270
|
+
export declare const facilityCredentialReviewItemExample: {
|
|
271
271
|
apiVersion: "survey.kontourai.io/v1alpha1";
|
|
272
272
|
kind: "ReviewItem";
|
|
273
273
|
metadata: {
|
|
@@ -425,7 +425,7 @@ export declare const facilityCredentialReviewItemFixture: {
|
|
|
425
425
|
selectedCandidateId: string;
|
|
426
426
|
};
|
|
427
427
|
};
|
|
428
|
-
export declare const
|
|
428
|
+
export declare const reviewWorkbenchQueueExamples: (ReviewItem | {
|
|
429
429
|
apiVersion: "survey.kontourai.io/v1alpha1";
|
|
430
430
|
kind: "ReviewItem";
|
|
431
431
|
metadata: {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { reviewResourceApiVersion } from "../review-resource.js";
|
|
2
|
-
// Browser-safe mirror of
|
|
3
|
-
// scripts/check-review-workbench.cjs fails if this data drifts from the canonical
|
|
4
|
-
export const
|
|
2
|
+
// Browser-safe mirror of example-data/public-directory-review-resource.ts.
|
|
3
|
+
// scripts/check-review-workbench.cjs fails if this data drifts from the canonical example.
|
|
4
|
+
export const publicDirectoryReviewItemExample = {
|
|
5
5
|
apiVersion: reviewResourceApiVersion,
|
|
6
6
|
kind: "ReviewItem",
|
|
7
7
|
metadata: {
|
|
@@ -131,7 +131,7 @@ export const publicDirectoryReviewItemFixture = {
|
|
|
131
131
|
reviewDecisionName: "public-directory-availability-operator-decision",
|
|
132
132
|
},
|
|
133
133
|
};
|
|
134
|
-
export const
|
|
134
|
+
export const regulatedRuleConflictReviewItemExample = {
|
|
135
135
|
apiVersion: reviewResourceApiVersion,
|
|
136
136
|
kind: "ReviewItem",
|
|
137
137
|
metadata: {
|
|
@@ -267,7 +267,7 @@ export const regulatedRuleConflictReviewItemFixture = {
|
|
|
267
267
|
selectedCandidateId: "regulated-rule-conflict-standard-threshold:candidate:current",
|
|
268
268
|
},
|
|
269
269
|
};
|
|
270
|
-
export const
|
|
270
|
+
export const facilityCredentialReviewItemExample = {
|
|
271
271
|
apiVersion: reviewResourceApiVersion,
|
|
272
272
|
kind: "ReviewItem",
|
|
273
273
|
metadata: {
|
|
@@ -424,21 +424,21 @@ export const facilityCredentialReviewItemFixture = {
|
|
|
424
424
|
};
|
|
425
425
|
function queueFixture(name, target, currentValue, proposedValue, candidateSetStatus, feedbackTags) {
|
|
426
426
|
return {
|
|
427
|
-
...
|
|
427
|
+
...publicDirectoryReviewItemExample,
|
|
428
428
|
metadata: {
|
|
429
|
-
...
|
|
429
|
+
...publicDirectoryReviewItemExample.metadata,
|
|
430
430
|
name,
|
|
431
431
|
},
|
|
432
432
|
spec: {
|
|
433
|
-
...
|
|
433
|
+
...publicDirectoryReviewItemExample.spec,
|
|
434
434
|
target,
|
|
435
435
|
selectedCandidateId: `${name}:candidate:current`,
|
|
436
436
|
candidateSetStatus,
|
|
437
437
|
producerPolicy: {
|
|
438
438
|
feedbackTags,
|
|
439
439
|
},
|
|
440
|
-
rationale: `
|
|
441
|
-
candidates:
|
|
440
|
+
rationale: `Example-backed local review queue item for ${target}.`,
|
|
441
|
+
candidates: publicDirectoryReviewItemExample.spec.candidates.map((candidate) => {
|
|
442
442
|
const role = candidate.role ?? "candidate";
|
|
443
443
|
const value = role === "proposed" ? proposedValue : currentValue;
|
|
444
444
|
return {
|
|
@@ -475,17 +475,17 @@ function queueFixture(name, target, currentValue, proposedValue, candidateSetSta
|
|
|
475
475
|
}),
|
|
476
476
|
},
|
|
477
477
|
status: {
|
|
478
|
-
observedCandidateCount:
|
|
478
|
+
observedCandidateCount: publicDirectoryReviewItemExample.status?.observedCandidateCount,
|
|
479
479
|
selectedCandidateId: `${name}:candidate:current`,
|
|
480
480
|
reviewDecisionName: candidateSetStatus === "resolved" ? `${name}-resolved-decision` : undefined,
|
|
481
481
|
},
|
|
482
482
|
};
|
|
483
483
|
}
|
|
484
|
-
export const
|
|
484
|
+
export const reviewWorkbenchQueueExamples = [
|
|
485
485
|
queueFixture("public-directory-hours", "hours", "Weekdays 9am-5pm", "Weekdays 8am-6pm", "needs-review", ["hours-change", "crawler-suggested"]),
|
|
486
486
|
queueFixture("public-directory-phone", "phoneNumber", "+1-555-0100", "+1-555-0199", "needs-review", ["contact-field", "source-conflict"]),
|
|
487
|
-
|
|
487
|
+
publicDirectoryReviewItemExample,
|
|
488
488
|
queueFixture("public-directory-address", "streetAddress", "100 Main Street", "102 Main Street", "needs-review", ["address-change", "producer-escalation-candidate"]),
|
|
489
489
|
queueFixture("public-directory-license", "licenseStatus", "ACTIVE", "EXPIRED", "escalated", ["licensing", "manual-review-required"]),
|
|
490
|
-
|
|
490
|
+
regulatedRuleConflictReviewItemExample,
|
|
491
491
|
];
|