@thejob/schema 2.5.0 → 2.7.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/CLAUDE.md +6 -0
- package/dist/index.cjs +1063 -568
- package/dist/index.d.cts +602 -7
- package/dist/index.d.ts +602 -7
- package/dist/index.js +1002 -552
- package/package.json +1 -1
- package/src/index.ts +11 -0
- package/src/job/job.schema.ts +28 -5
- package/src/taxonomy/taxonomy-filter.schema.ts +84 -0
- package/src/taxonomy/taxonomy.constant.ts +127 -0
- package/src/taxonomy/taxonomy.schema.ts +404 -0
|
@@ -0,0 +1,404 @@
|
|
|
1
|
+
import { array, boolean, number, object, string } from "yup";
|
|
2
|
+
import {
|
|
3
|
+
CONCEPT_ID_PATTERN,
|
|
4
|
+
CONCEPT_TYPE_PATTERN,
|
|
5
|
+
SCHEDULE_MAX_HOURS,
|
|
6
|
+
SCHEDULE_MIN_HOURS,
|
|
7
|
+
SupportedAuditStatuses,
|
|
8
|
+
SupportedConceptSources,
|
|
9
|
+
SupportedPrefilterRules,
|
|
10
|
+
SupportedProposalKinds,
|
|
11
|
+
SupportedProposalMessageRoles,
|
|
12
|
+
SupportedProposalStatuses,
|
|
13
|
+
SupportedReleaseKinds,
|
|
14
|
+
SupportedReleaseStatuses,
|
|
15
|
+
SupportedResolverVerdicts,
|
|
16
|
+
SupportedTaxonomyJobKinds,
|
|
17
|
+
SupportedTaxonomyNodeKinds,
|
|
18
|
+
SupportedTaxonomySources,
|
|
19
|
+
SupportedUnmatchedReasons,
|
|
20
|
+
SupportedUnmatchedResolutions,
|
|
21
|
+
TAXONOMY_NODE_ID_PATTERN,
|
|
22
|
+
VOCABULARY_ID_PATTERN,
|
|
23
|
+
} from "./taxonomy.constant.js";
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* The documents thejob-taxonomy-service stores and serves.
|
|
27
|
+
*
|
|
28
|
+
* Timestamps are epoch milliseconds (numbers), and ids are the service's own
|
|
29
|
+
* (`conceptId`, `nodeId`, ...), so none of these extend `DbDefaultSchema`.
|
|
30
|
+
*
|
|
31
|
+
* Optional fields really are absent on some documents, which is why they are
|
|
32
|
+
* `.optional()` rather than defaulted: several are resume markers or audit
|
|
33
|
+
* stamps whose ABSENCE carries meaning ("not looked at yet").
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
const conceptId = () => string().matches(CONCEPT_ID_PATTERN, "conceptId must look like <vocabulary>_<slug>");
|
|
37
|
+
const vocabularyId = () => string().matches(VOCABULARY_ID_PATTERN, "vocabulary must match [a-z][a-z0-9-]{0,31}");
|
|
38
|
+
const nodeId = () => string().matches(TAXONOMY_NODE_ID_PATTERN, "node ids look like CAT_01_TECH");
|
|
39
|
+
const strings = () => array(string().trim().min(1).required());
|
|
40
|
+
|
|
41
|
+
// ─── Vocabularies ────────────────────────────────────────────────────────────
|
|
42
|
+
|
|
43
|
+
/** One concept type a vocabulary allows, e.g. `programming_language`. */
|
|
44
|
+
export const VocabularyTypeSchema = object({
|
|
45
|
+
id: string().matches(CONCEPT_TYPE_PATTERN, "type ids match [a-z][a-z0-9_]*").required().label("Type ID"),
|
|
46
|
+
/** Shown to the model when it classifies a concept; include examples. */
|
|
47
|
+
description: string().defined().label("Description"),
|
|
48
|
+
}).label("Vocabulary Type");
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* A kind of concept the service holds (skills, languages, roles). Adding one is
|
|
52
|
+
* data (an API call), never a code change: every prompt is built from these
|
|
53
|
+
* fields.
|
|
54
|
+
*/
|
|
55
|
+
export const VocabularySchema = object({
|
|
56
|
+
vocabularyId: vocabularyId().required().label("Vocabulary ID"),
|
|
57
|
+
name: string().trim().min(1).required().label("Name"),
|
|
58
|
+
/** What one concept is called in prompts and messages: "skill", "role". */
|
|
59
|
+
noun: string().trim().min(1).required().label("Noun"),
|
|
60
|
+
nounPlural: string().trim().min(1).required().label("Noun (plural)"),
|
|
61
|
+
/** What belongs in this vocabulary, for prompts. */
|
|
62
|
+
description: string().trim().min(1).required().label("Description"),
|
|
63
|
+
/** Where its terms are written, for the "swap it in" test: "a job posting". */
|
|
64
|
+
appearsIn: string().trim().min(1).required().label("Appears In"),
|
|
65
|
+
/** Things that are NOT concepts of this vocabulary, for prompts. */
|
|
66
|
+
outOfScope: string().trim().min(1).required().label("Out of Scope"),
|
|
67
|
+
types: array(VocabularyTypeSchema.required())
|
|
68
|
+
.min(1)
|
|
69
|
+
.required()
|
|
70
|
+
.test("unique-type-ids", "type ids must be unique", (types) =>
|
|
71
|
+
!types || new Set(types.map((t) => t.id)).size === types.length,
|
|
72
|
+
)
|
|
73
|
+
.label("Types"),
|
|
74
|
+
/** The type a concept gets when nothing more specific is known. One of `types`. */
|
|
75
|
+
defaultType: string()
|
|
76
|
+
.required()
|
|
77
|
+
.test("default-type-listed", "defaultType must be one of the type ids", function (value) {
|
|
78
|
+
const types = (this.parent as { types?: { id: string }[] }).types ?? [];
|
|
79
|
+
return types.some((t) => t.id === value);
|
|
80
|
+
})
|
|
81
|
+
.label("Default Type"),
|
|
82
|
+
prefilter: array(string().oneOf(SupportedPrefilterRules).required()).default([]).label("Prefilter"),
|
|
83
|
+
/**
|
|
84
|
+
* Words that qualify a term without changing what it names ("fluent",
|
|
85
|
+
* "native"), dropped from its canonical lookup form in THIS vocabulary only.
|
|
86
|
+
*/
|
|
87
|
+
qualifiers: strings().optional().label("Qualifiers"),
|
|
88
|
+
/** Extra instructions per model task; each is optional. */
|
|
89
|
+
guidance: object({
|
|
90
|
+
describe: string().optional(),
|
|
91
|
+
classify: string().optional(),
|
|
92
|
+
aliases: string().optional(),
|
|
93
|
+
verify: string().optional(),
|
|
94
|
+
place: string().optional(),
|
|
95
|
+
}).label("Guidance"),
|
|
96
|
+
source: string().oneOf(SupportedTaxonomySources).required().label("Source"),
|
|
97
|
+
createdAt: number().required().label("Created At"),
|
|
98
|
+
updatedAt: number().required().label("Updated At"),
|
|
99
|
+
}).label("Vocabulary");
|
|
100
|
+
|
|
101
|
+
// ─── Backbone ────────────────────────────────────────────────────────────────
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* One node of a vocabulary's 3-level backbone (category > subcategory > group),
|
|
105
|
+
* stored flat with `parentId` and `path` so any level can be read directly.
|
|
106
|
+
*/
|
|
107
|
+
export const TaxonomyNodeSchema = object({
|
|
108
|
+
nodeId: nodeId().required().label("Node ID"),
|
|
109
|
+
vocabulary: vocabularyId().required().label("Vocabulary"),
|
|
110
|
+
kind: string().oneOf(SupportedTaxonomyNodeKinds).required().label("Kind"),
|
|
111
|
+
name: string().trim().min(1).required().label("Name"),
|
|
112
|
+
/** Groups carry a description; categories and subcategories usually do not. */
|
|
113
|
+
description: string().optional().label("Description"),
|
|
114
|
+
aliases: strings().default([]).label("Aliases"),
|
|
115
|
+
/** null for categories (the roots). */
|
|
116
|
+
parentId: string().nullable().defined().label("Parent ID"),
|
|
117
|
+
/** Ancestor ids from the root down to and including this node. */
|
|
118
|
+
path: array(string().required()).default([]).label("Path"),
|
|
119
|
+
source: string().oneOf(SupportedTaxonomySources).optional().label("Source"),
|
|
120
|
+
createdAt: number().required().label("Created At"),
|
|
121
|
+
updatedAt: number().required().label("Updated At"),
|
|
122
|
+
}).label("Taxonomy Node");
|
|
123
|
+
|
|
124
|
+
// ─── Concepts ────────────────────────────────────────────────────────────────
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Where a concept sits in its vocabulary's backbone. A concept may have several
|
|
128
|
+
* (the same name filed under several categories merges into one concept) or none
|
|
129
|
+
* (live but uncategorized, waiting for a placement).
|
|
130
|
+
*/
|
|
131
|
+
export const ConceptMembershipSchema = object({
|
|
132
|
+
categoryId: string().required().label("Category ID"),
|
|
133
|
+
categoryName: string().required().label("Category Name"),
|
|
134
|
+
subcategoryId: string().required().label("Subcategory ID"),
|
|
135
|
+
subcategoryName: string().required().label("Subcategory Name"),
|
|
136
|
+
groupId: string().required().label("Group ID"),
|
|
137
|
+
groupName: string().required().label("Group Name"),
|
|
138
|
+
/** The source record this membership came from; '' when written by hand. */
|
|
139
|
+
sourceId: string().default("").label("Source ID"),
|
|
140
|
+
}).label("Concept Membership");
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* A canonical concept: what `/normalize` resolves free text onto.
|
|
144
|
+
*
|
|
145
|
+
* Which keys a concept answers to is NOT here: the service keeps them in its
|
|
146
|
+
* own `matchKeys` store. `aliases` is alias TEXT for people to read.
|
|
147
|
+
*/
|
|
148
|
+
export const ConceptSchema = object({
|
|
149
|
+
/** `<vocabulary>_<slug>`, e.g. `skills_power-bi`. Primary key. */
|
|
150
|
+
conceptId: conceptId().required().label("Concept ID"),
|
|
151
|
+
vocabulary: vocabularyId().required().label("Vocabulary"),
|
|
152
|
+
name: string().trim().min(1).max(200).required().label("Name"),
|
|
153
|
+
/** One of the vocabulary's type ids; checked against the vocabulary by the service. */
|
|
154
|
+
type: string().trim().matches(CONCEPT_TYPE_PATTERN, "type must be a type id").required().label("Type"),
|
|
155
|
+
description: string().trim().max(2000).default("").label("Description"),
|
|
156
|
+
aliases: strings().default([]).label("Aliases"),
|
|
157
|
+
memberships: array(ConceptMembershipSchema.required()).default([]).label("Memberships"),
|
|
158
|
+
/** Always concepts of the same vocabulary. */
|
|
159
|
+
relatedConceptIds: strings().default([]).label("Related Concepts"),
|
|
160
|
+
source: string().oneOf(SupportedConceptSources).required().label("Source"),
|
|
161
|
+
createdAt: number().required().label("Created At"),
|
|
162
|
+
updatedAt: number().required().label("Updated At"),
|
|
163
|
+
/** Resume marker of `retype-concepts`: set once the type was examined. */
|
|
164
|
+
retypedAt: number().optional().label("Retyped At"),
|
|
165
|
+
/** Resume marker of `generate-aliases`: set even when no alias was added. */
|
|
166
|
+
aliasedAt: number().optional().label("Aliased At"),
|
|
167
|
+
/** Set while `description` is a stand-in; removed once a real one is written. */
|
|
168
|
+
descriptionPending: boolean().isTrue().optional().label("Description Pending"),
|
|
169
|
+
}).label("Concept");
|
|
170
|
+
|
|
171
|
+
// ─── Misses ──────────────────────────────────────────────────────────────────
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* A term callers sent that the taxonomy could not resolve: the ranked work list
|
|
175
|
+
* for growing it. Never expired; `resolution` closes the loop.
|
|
176
|
+
*/
|
|
177
|
+
export const UnmatchedTermSchema = object({
|
|
178
|
+
vocabulary: vocabularyId().required().label("Vocabulary"),
|
|
179
|
+
/** The raw input as first received. */
|
|
180
|
+
term: string().required().label("Term"),
|
|
181
|
+
/** The dedupe key, so "React JS" and "react.js" share a row. */
|
|
182
|
+
normalizedTerm: string().required().label("Normalized Term"),
|
|
183
|
+
count: number().integer().min(0).required().label("Count"),
|
|
184
|
+
firstSeenAt: number().required().label("First Seen At"),
|
|
185
|
+
lastSeenAt: number().required().label("Last Seen At"),
|
|
186
|
+
/** Which callers sent it, capped. */
|
|
187
|
+
sources: array(
|
|
188
|
+
object({
|
|
189
|
+
service: string().required(),
|
|
190
|
+
context: string().optional(),
|
|
191
|
+
}).required(),
|
|
192
|
+
)
|
|
193
|
+
.default([])
|
|
194
|
+
.label("Sources"),
|
|
195
|
+
resolution: string().oneOf(SupportedUnmatchedResolutions).required().label("Resolution"),
|
|
196
|
+
resolvedConceptId: string().optional().label("Resolved Concept"),
|
|
197
|
+
/** `ambiguous`: the concept exists; the caller gave no context that picked one. */
|
|
198
|
+
reason: string().oneOf(SupportedUnmatchedReasons).optional().label("Reason"),
|
|
199
|
+
/** The concepts an ambiguous term could mean. */
|
|
200
|
+
candidates: array(string().required()).optional().label("Candidates"),
|
|
201
|
+
/** Who rejected it when it was not a human: `prefilter:<rule>` or `model`. */
|
|
202
|
+
rejectedBy: string().optional().label("Rejected By"),
|
|
203
|
+
}).label("Unmatched Term");
|
|
204
|
+
|
|
205
|
+
// ─── Proposals (the review queue) ────────────────────────────────────────────
|
|
206
|
+
|
|
207
|
+
/** A scored backbone group a draft could be placed in. */
|
|
208
|
+
export const ProposalCandidateSchema = object({
|
|
209
|
+
groupId: string().required(),
|
|
210
|
+
groupName: string().required(),
|
|
211
|
+
subcategoryId: string().required(),
|
|
212
|
+
subcategoryName: string().required(),
|
|
213
|
+
categoryId: string().required(),
|
|
214
|
+
categoryName: string().required(),
|
|
215
|
+
score: number().required(),
|
|
216
|
+
nearestMembers: array(string().required()).default([]),
|
|
217
|
+
}).label("Proposal Candidate");
|
|
218
|
+
|
|
219
|
+
/** One turn of a draft's refinement chat. */
|
|
220
|
+
export const ProposalMessageSchema = object({
|
|
221
|
+
role: string().oneOf(SupportedProposalMessageRoles).required(),
|
|
222
|
+
/** The operator's instruction, or the model's note on what it changed and why. */
|
|
223
|
+
content: string().defined(),
|
|
224
|
+
/** Which model answered. Absent on operator turns. */
|
|
225
|
+
model: string().optional(),
|
|
226
|
+
at: number().required(),
|
|
227
|
+
}).label("Proposal Message");
|
|
228
|
+
|
|
229
|
+
/** A home that does not exist yet, named level by level. "" means no opinion on that level. */
|
|
230
|
+
export const SuggestedHomeSchema = object({
|
|
231
|
+
categoryName: string().defined(),
|
|
232
|
+
subcategoryName: string().defined(),
|
|
233
|
+
groupName: string().defined(),
|
|
234
|
+
}).label("Suggested Home");
|
|
235
|
+
|
|
236
|
+
/** The resolver's verdict on the existing concepts it was shown for a miss. */
|
|
237
|
+
export const ResolverVerificationSchema = object({
|
|
238
|
+
verdict: string().oneOf(SupportedResolverVerdicts).required(),
|
|
239
|
+
/** The shown concept the verdict is about; null for `new_concept` / `out_of_scope`. */
|
|
240
|
+
conceptId: string().nullable().defined(),
|
|
241
|
+
evidence: string().defined(),
|
|
242
|
+
/** What the judge took the term to mean, abbreviations expanded. */
|
|
243
|
+
means: string().optional(),
|
|
244
|
+
/** For `same`: did the independent confirm step agree? */
|
|
245
|
+
consistent: boolean().optional(),
|
|
246
|
+
confirm: object({
|
|
247
|
+
answer: string().oneOf(["yes", "no"] as const).required(),
|
|
248
|
+
evidence: string().defined(),
|
|
249
|
+
})
|
|
250
|
+
.optional()
|
|
251
|
+
.default(undefined),
|
|
252
|
+
}).label("Resolver Verification");
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* A draft between a miss and the taxonomy. Nothing in a draft is matchable;
|
|
256
|
+
* approving it is the only way it reaches the concepts.
|
|
257
|
+
*/
|
|
258
|
+
export const ConceptProposalSchema = object({
|
|
259
|
+
proposalId: string().required().label("Proposal ID"),
|
|
260
|
+
vocabulary: vocabularyId().required().label("Vocabulary"),
|
|
261
|
+
/** The miss this closes; `normalizedTerm` links back to its unmatched row. */
|
|
262
|
+
term: string().required().label("Term"),
|
|
263
|
+
normalizedTerm: string().required().label("Normalized Term"),
|
|
264
|
+
/** Demand at proposal time, so the queue ranks without a join. */
|
|
265
|
+
count: number().integer().min(0).required().label("Count"),
|
|
266
|
+
/** Other misses folded into this draft because they name the same new concept. */
|
|
267
|
+
mergedTerms: array(
|
|
268
|
+
object({
|
|
269
|
+
term: string().required(),
|
|
270
|
+
normalizedTerm: string().required(),
|
|
271
|
+
count: number().integer().min(0).required(),
|
|
272
|
+
}).required(),
|
|
273
|
+
)
|
|
274
|
+
.optional()
|
|
275
|
+
.label("Merged Terms"),
|
|
276
|
+
/** Absent on drafts that predate the field: read as `concept`. */
|
|
277
|
+
kind: string().oneOf(SupportedProposalKinds).optional().label("Kind"),
|
|
278
|
+
/** For `alias`: the concept the term names. For `placement`: the concept to place. */
|
|
279
|
+
targetConceptId: string().optional().label("Target Concept"),
|
|
280
|
+
targetName: string().optional().label("Target Name"),
|
|
281
|
+
|
|
282
|
+
// The draft concept, editable until approved. Empty strings are valid here:
|
|
283
|
+
// an alias draft carries no draft concept of its own.
|
|
284
|
+
name: string().defined().label("Name"),
|
|
285
|
+
type: string().defined().label("Type"),
|
|
286
|
+
description: string().defined().label("Description"),
|
|
287
|
+
aliases: array(string().required()).default([]).label("Aliases"),
|
|
288
|
+
/** Empty when no group fits: approved that way, the concept goes live uncategorized. */
|
|
289
|
+
memberships: array(ConceptMembershipSchema.required()).default([]).label("Memberships"),
|
|
290
|
+
suggestedHome: SuggestedHomeSchema.nullable().optional().default(undefined).label("Suggested Home"),
|
|
291
|
+
/** Resolved to concepts that exist; names that matched nothing are dropped. */
|
|
292
|
+
relatedConceptIds: array(string().required()).default([]).label("Related Concepts"),
|
|
293
|
+
|
|
294
|
+
/** Every placement candidate considered, best first. */
|
|
295
|
+
candidates: array(ProposalCandidateSchema.required()).default([]).label("Candidates"),
|
|
296
|
+
needsReview: boolean().required().label("Needs Review"),
|
|
297
|
+
reason: string().optional().label("Reason"),
|
|
298
|
+
verification: ResolverVerificationSchema.optional().default(undefined).label("Verification"),
|
|
299
|
+
/** What the auto-accept gate decided for an alias verdict. */
|
|
300
|
+
autoAccept: object({
|
|
301
|
+
accepted: boolean().required(),
|
|
302
|
+
reason: string().optional(),
|
|
303
|
+
tier: string().optional(),
|
|
304
|
+
calibrationId: string().optional(),
|
|
305
|
+
releaseId: string().optional(),
|
|
306
|
+
})
|
|
307
|
+
.optional()
|
|
308
|
+
.default(undefined)
|
|
309
|
+
.label("Auto Accept"),
|
|
310
|
+
|
|
311
|
+
/** Which model drafted it. */
|
|
312
|
+
model: string().defined().label("Model"),
|
|
313
|
+
/** The refinement conversation, oldest first. */
|
|
314
|
+
messages: array(ProposalMessageSchema.required()).default([]).label("Messages"),
|
|
315
|
+
status: string().oneOf(SupportedProposalStatuses).required().label("Status"),
|
|
316
|
+
/** Set when approved: the concept this became. */
|
|
317
|
+
resolvedConceptId: string().optional().label("Resolved Concept"),
|
|
318
|
+
createdAt: number().required().label("Created At"),
|
|
319
|
+
updatedAt: number().required().label("Updated At"),
|
|
320
|
+
}).label("Concept Proposal");
|
|
321
|
+
|
|
322
|
+
// ─── Releases and audits ─────────────────────────────────────────────────────
|
|
323
|
+
|
|
324
|
+
/** A group of key writes that is undone together. Every key write belongs to one. */
|
|
325
|
+
export const ReleaseSchema = object({
|
|
326
|
+
releaseId: string().required().label("Release ID"),
|
|
327
|
+
kind: string().oneOf(SupportedReleaseKinds).required().label("Kind"),
|
|
328
|
+
status: string().oneOf(SupportedReleaseStatuses).required().label("Status"),
|
|
329
|
+
createdBy: string().required().label("Created By"),
|
|
330
|
+
createdAt: number().required().label("Created At"),
|
|
331
|
+
note: string().optional().label("Note"),
|
|
332
|
+
rolledBackAt: number().optional().label("Rolled Back At"),
|
|
333
|
+
rolledBackBy: string().optional().label("Rolled Back By"),
|
|
334
|
+
}).label("Release");
|
|
335
|
+
|
|
336
|
+
/** A sampled auto-accepted key, waiting for (or carrying) a human's verdict. */
|
|
337
|
+
export const AuditSchema = object({
|
|
338
|
+
auditId: string().required().label("Audit ID"),
|
|
339
|
+
releaseId: string().required().label("Release ID"),
|
|
340
|
+
key: string().required().label("Key"),
|
|
341
|
+
term: string().required().label("Term"),
|
|
342
|
+
conceptId: string().required().label("Concept ID"),
|
|
343
|
+
tier: string().optional().label("Tier"),
|
|
344
|
+
status: string().oneOf(SupportedAuditStatuses).required().label("Status"),
|
|
345
|
+
createdAt: number().required().label("Created At"),
|
|
346
|
+
resolvedAt: number().optional().label("Resolved At"),
|
|
347
|
+
resolvedBy: string().optional().label("Resolved By"),
|
|
348
|
+
}).label("Audit");
|
|
349
|
+
|
|
350
|
+
// ─── Operations ──────────────────────────────────────────────────────────────
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* Operator-editable runtime settings and kill switches. One document; every
|
|
354
|
+
* replica obeys it within seconds.
|
|
355
|
+
*/
|
|
356
|
+
export const ResolverSettingsSchema = object({
|
|
357
|
+
autoAccept: object({
|
|
358
|
+
/** Stop switch for WRITING auto keys. Off until a calibration passes. */
|
|
359
|
+
enabled: boolean().required(),
|
|
360
|
+
/** Minimum calibrated precision (Wilson 95% lower bound) for a tier to auto-accept. */
|
|
361
|
+
target: number().min(0.5).lessThan(1).required(),
|
|
362
|
+
maxPerRun: number().integer().min(0).required(),
|
|
363
|
+
maxPerDay: number().integer().min(0).required(),
|
|
364
|
+
/** Share of each auto release sampled into the audit queue (0..1). */
|
|
365
|
+
auditRate: number().min(0).max(1).required(),
|
|
366
|
+
}).label("Auto Accept"),
|
|
367
|
+
/** Stop switch for SERVING auto keys: false hides them from lookups at once. */
|
|
368
|
+
serveAutoKeys: boolean().required().label("Serve Auto Keys"),
|
|
369
|
+
/** Minimum Meilisearch ranking score (0..1) for a fuzzy hit to be considered. */
|
|
370
|
+
fuzzyMinScore: number().min(0).max(1).required().label("Fuzzy Min Score"),
|
|
371
|
+
/** Per-client limit on public search, per minute per replica. 0 disables it. */
|
|
372
|
+
searchRateLimitPerMin: number().integer().min(0).required().label("Search Rate Limit"),
|
|
373
|
+
/** Master switch for scheduled maintenance jobs. */
|
|
374
|
+
scheduledJobs: boolean().required().label("Scheduled Jobs"),
|
|
375
|
+
updatedAt: number().optional().label("Updated At"),
|
|
376
|
+
updatedBy: string().optional().label("Updated By"),
|
|
377
|
+
/** Why auto-accept was last switched off automatically. */
|
|
378
|
+
trippedReason: string().optional().label("Tripped Reason"),
|
|
379
|
+
}).label("Resolver Settings");
|
|
380
|
+
|
|
381
|
+
/** How often one maintenance job runs on the schedule. */
|
|
382
|
+
export const TaxonomyScheduleSchema = object({
|
|
383
|
+
kind: string().oneOf(SupportedTaxonomyJobKinds).required().label("Job"),
|
|
384
|
+
enabled: boolean().required().label("Enabled"),
|
|
385
|
+
everyHours: number().min(SCHEDULE_MIN_HOURS).max(SCHEDULE_MAX_HOURS).required().label("Every (hours)"),
|
|
386
|
+
/** Last time the scheduler fired this job. */
|
|
387
|
+
lastRunAt: number().optional().label("Last Run At"),
|
|
388
|
+
updatedAt: number().required().label("Updated At"),
|
|
389
|
+
}).label("Taxonomy Schedule");
|
|
390
|
+
|
|
391
|
+
// ─── Normalize ───────────────────────────────────────────────────────────────
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* Optional caller context for `/normalize`, used ONLY to choose between the
|
|
395
|
+
* meanings of an ambiguous term. It never changes an unambiguous answer.
|
|
396
|
+
*/
|
|
397
|
+
export const NormalizeContextSchema = object({
|
|
398
|
+
/** The title of the record the terms come from. */
|
|
399
|
+
title: string().max(200).optional(),
|
|
400
|
+
/** Backbone node ids (category, subcategory or group). */
|
|
401
|
+
categoryIds: array(nodeId().required()).max(10).optional(),
|
|
402
|
+
/** Other terms from the same record, in the same vocabulary. */
|
|
403
|
+
siblings: array(string().max(200).required()).max(50).optional(),
|
|
404
|
+
}).label("Normalize Context");
|