@vertesia/common 1.5.0-dev.20260714.072725Z → 1.5.0-dev.20260722.120446Z

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.
Files changed (127) hide show
  1. package/lib/apikey.d.ts +1 -0
  2. package/lib/apikey.d.ts.map +1 -1
  3. package/lib/apikey.js.map +1 -1
  4. package/lib/apps.d.ts +500 -34
  5. package/lib/apps.d.ts.map +1 -1
  6. package/lib/apps.js +63 -70
  7. package/lib/apps.js.map +1 -1
  8. package/lib/audit-trail.d.ts +61 -1
  9. package/lib/audit-trail.d.ts.map +1 -1
  10. package/lib/audit-trail.js +15 -0
  11. package/lib/audit-trail.js.map +1 -1
  12. package/lib/data-platform.d.ts +121 -5
  13. package/lib/data-platform.d.ts.map +1 -1
  14. package/lib/index.d.ts +7 -0
  15. package/lib/index.d.ts.map +1 -1
  16. package/lib/index.js +7 -0
  17. package/lib/index.js.map +1 -1
  18. package/lib/interaction.d.ts +19 -1
  19. package/lib/interaction.d.ts.map +1 -1
  20. package/lib/interaction.js.map +1 -1
  21. package/lib/json-schema.d.ts +1 -1
  22. package/lib/json-schema.d.ts.map +1 -1
  23. package/lib/platform-event.d.ts +79 -3
  24. package/lib/platform-event.d.ts.map +1 -1
  25. package/lib/platform-event.js.map +1 -1
  26. package/lib/project.d.ts +119 -20
  27. package/lib/project.d.ts.map +1 -1
  28. package/lib/project.js +65 -0
  29. package/lib/project.js.map +1 -1
  30. package/lib/query.d.ts +6 -0
  31. package/lib/query.d.ts.map +1 -1
  32. package/lib/refs.d.ts +1 -0
  33. package/lib/refs.d.ts.map +1 -1
  34. package/lib/schema-for-extraction.d.ts +21 -0
  35. package/lib/schema-for-extraction.d.ts.map +1 -0
  36. package/lib/schema-for-extraction.js +205 -0
  37. package/lib/schema-for-extraction.js.map +1 -0
  38. package/lib/store/agent-run.d.ts +2 -0
  39. package/lib/store/agent-run.d.ts.map +1 -1
  40. package/lib/store/conversation-state.d.ts +39 -0
  41. package/lib/store/conversation-state.d.ts.map +1 -1
  42. package/lib/store/conversation-state.js +3 -0
  43. package/lib/store/conversation-state.js.map +1 -1
  44. package/lib/store/doc-analyzer.d.ts +10 -67
  45. package/lib/store/doc-analyzer.d.ts.map +1 -1
  46. package/lib/store/dsl-workflow.d.ts +1 -0
  47. package/lib/store/dsl-workflow.d.ts.map +1 -1
  48. package/lib/store/dsl-workflow.js.map +1 -1
  49. package/lib/store/grounded-extraction.d.ts +146 -0
  50. package/lib/store/grounded-extraction.d.ts.map +1 -0
  51. package/lib/store/grounded-extraction.js +8 -0
  52. package/lib/store/grounded-extraction.js.map +1 -0
  53. package/lib/store/index.d.ts +1 -0
  54. package/lib/store/index.d.ts.map +1 -1
  55. package/lib/store/index.js +1 -0
  56. package/lib/store/index.js.map +1 -1
  57. package/lib/store/store.d.ts +307 -1
  58. package/lib/store/store.d.ts.map +1 -1
  59. package/lib/store/store.js +438 -0
  60. package/lib/store/store.js.map +1 -1
  61. package/lib/store/workflow.d.ts +3 -0
  62. package/lib/store/workflow.d.ts.map +1 -1
  63. package/lib/store/workflow.js.map +1 -1
  64. package/lib/user.d.ts +14 -0
  65. package/lib/user.d.ts.map +1 -1
  66. package/lib/user.js +31 -0
  67. package/lib/user.js.map +1 -1
  68. package/lib/vertesia-common.js +2 -2
  69. package/lib/vertesia-common.js.map +1 -1
  70. package/lib/view-configuration-validation.d.ts +15 -0
  71. package/lib/view-configuration-validation.d.ts.map +1 -0
  72. package/lib/view-configuration-validation.js +63 -0
  73. package/lib/view-configuration-validation.js.map +1 -0
  74. package/lib/view-query-validation.d.ts +13 -0
  75. package/lib/view-query-validation.d.ts.map +1 -0
  76. package/lib/view-query-validation.js +266 -0
  77. package/lib/view-query-validation.js.map +1 -0
  78. package/lib/view-validation-helpers.d.ts +15 -0
  79. package/lib/view-validation-helpers.d.ts.map +1 -0
  80. package/lib/view-validation-helpers.js +25 -0
  81. package/lib/view-validation-helpers.js.map +1 -0
  82. package/lib/views-schema.d.ts +1992 -0
  83. package/lib/views-schema.d.ts.map +1 -0
  84. package/lib/views-schema.js +674 -0
  85. package/lib/views-schema.js.map +1 -0
  86. package/lib/views-validation.d.ts +21 -0
  87. package/lib/views-validation.d.ts.map +1 -0
  88. package/lib/views-validation.js +164 -0
  89. package/lib/views-validation.js.map +1 -0
  90. package/lib/views.d.ts +381 -0
  91. package/lib/views.d.ts.map +1 -0
  92. package/lib/views.js +41 -0
  93. package/lib/views.js.map +1 -0
  94. package/package.json +5 -4
  95. package/src/apikey.ts +1 -0
  96. package/src/apps.test.ts +9 -1
  97. package/src/apps.ts +583 -90
  98. package/src/audit-trail.ts +83 -0
  99. package/src/data-platform.ts +129 -5
  100. package/src/index.ts +12 -0
  101. package/src/interaction.ts +20 -1
  102. package/src/json-schema.ts +0 -1
  103. package/src/platform-event.ts +92 -2
  104. package/src/project.test.ts +44 -0
  105. package/src/project.ts +205 -22
  106. package/src/query.ts +6 -0
  107. package/src/refs.ts +1 -0
  108. package/src/roles.test.ts +32 -0
  109. package/src/schema-for-extraction.test.ts +191 -0
  110. package/src/schema-for-extraction.ts +231 -0
  111. package/src/store/agent-run.ts +2 -0
  112. package/src/store/conversation-state.ts +47 -0
  113. package/src/store/doc-analyzer.ts +10 -76
  114. package/src/store/dsl-workflow.ts +1 -0
  115. package/src/store/grounded-extraction.ts +154 -0
  116. package/src/store/index.ts +1 -0
  117. package/src/store/store.ts +778 -1
  118. package/src/store/workflow.ts +3 -0
  119. package/src/user.ts +46 -0
  120. package/src/view-configuration-validation.ts +74 -0
  121. package/src/view-query-validation.test.ts +21 -0
  122. package/src/view-query-validation.ts +319 -0
  123. package/src/view-validation-helpers.ts +28 -0
  124. package/src/views-schema.test.ts +364 -0
  125. package/src/views-schema.ts +689 -0
  126. package/src/views-validation.ts +234 -0
  127. package/src/views.ts +484 -0
package/src/project.ts CHANGED
@@ -1,7 +1,9 @@
1
1
  import type { JSONSchemaType } from 'ajv';
2
2
  import type { SupportedIntegrations } from './integrations.js';
3
+ import type { ContentTypeIntakePolicy, IntakeVisionDetail, IntakeVisionProfileSettings } from './store/store.js';
3
4
  import type { WorkflowRunStatus } from './store/workflow.js';
4
5
  import type { AccountRef } from './user.js';
6
+ import { ELASTICSEARCH_FIELD_PATH_PATTERN } from './view-validation-helpers.js';
5
7
 
6
8
  export interface ICreateProjectPayload {
7
9
  name: string;
@@ -125,6 +127,7 @@ export const SYSTEM_INTERACTION_CATEGORIES: Record<string, SystemInteractionCate
125
127
  Mediator: SystemInteractionCategory.non_applicable,
126
128
  AnalyzeConversation: SystemInteractionCategory.analysis,
127
129
  GetAgentConversationTopic: SystemInteractionCategory.analysis,
130
+ ContentSearchAgent: SystemInteractionCategory.analysis,
128
131
  StudioAssistant: SystemInteractionCategory.agent,
129
132
  };
130
133
 
@@ -269,7 +272,70 @@ export const BrowserUseProjectConfigurationSchema: JSONSchemaType<BrowserUseProj
269
272
  export type ProjectSearchTier = 'standard' | 'performance';
270
273
  export type ElasticsearchBackend = 'serverless' | 'hosted';
271
274
 
275
+ /**
276
+ * Fast pre-conversion type identification (the "sniff") for untyped documents.
277
+ * The sniff classifies a document from cheap local evidence (first/last page text,
278
+ * a low-res first-page image, office docProps) BEFORE any conversion, so a
279
+ * high-confidence match can apply the type's intake policy — including skipping
280
+ * conversion — without paying for it first.
281
+ */
282
+ export interface ProjectIntakeSniffConfiguration {
283
+ /**
284
+ * Enable the pre-conversion sniff for untyped documents. Defaults to true.
285
+ * Can be overridden per run with the `sniffEnabled` workflow var.
286
+ */
287
+ enabled?: boolean;
288
+
289
+ /**
290
+ * Confidence at or above which the sniffed type is committed and its full policy applied
291
+ * (including conversion-skip). 0..1, defaults to 0.85.
292
+ */
293
+ high_confidence?: number;
294
+
295
+ /**
296
+ * Confidence at or above which the sniffed type is treated as provisional: the document
297
+ * still converts and the post-conversion selector confirms on neutral evidence.
298
+ * 0..1, defaults to 0.6. Below this the sniff result is advisory provenance only.
299
+ */
300
+ medium_confidence?: number;
301
+
302
+ /**
303
+ * Minimum page count for the sniff LLM call. Below this, conversion is cheap and full
304
+ * converted text is better selection evidence, so intake uses the standard
305
+ * convert-then-select path. Documents with unknown page counts are sniffed.
306
+ * Defaults to 5; 0 means always sniff.
307
+ */
308
+ min_pages?: number;
309
+ }
310
+
272
311
  export interface ProjectIntakeConfiguration {
312
+ /**
313
+ * Master switch for the standard intake pipeline. When false, StandardIntake exits as a
314
+ * no-op WITHOUT touching object status (objects stay in `created`, identifiable as
315
+ * unprocessed). Defaults to true.
316
+ */
317
+ enabled?: boolean;
318
+
319
+ /**
320
+ * Fast pre-conversion type identification for untyped documents. Absent means enabled
321
+ * with platform default thresholds.
322
+ */
323
+ sniff?: ProjectIntakeSniffConfiguration;
324
+
325
+ /**
326
+ * Project-level intake policy defaults. Same shape as the per-content-type policy; a
327
+ * type's `intake` block wins field-by-field over these defaults, which in turn win over
328
+ * the legacy flat fields below. `identification` is type-specific and ignored here.
329
+ */
330
+ default_policy?: ContentTypeIntakePolicy;
331
+
332
+ /**
333
+ * Project overrides for the platform vision detail profiles used by intake visual
334
+ * extraction (`low`/`standard`/`high`). Partial: omitted profiles or fields inherit the
335
+ * platform defaults. Types reference detail NAMES only; the profile settings live here.
336
+ */
337
+ vision_profiles?: Partial<Record<IntakeVisionDetail, Partial<IntakeVisionProfileSettings>>>;
338
+
273
339
  /**
274
340
  * Generate table-of-content sections during standard document intake.
275
341
  * Defaults to false.
@@ -331,28 +397,7 @@ export interface ProjectConfiguration {
331
397
  * Indexing configuration for this project.
332
398
  * Controls whether indexing and querying are enabled at the project level.
333
399
  */
334
- indexing?: {
335
- /**
336
- * Enable indexing for content objects in this project.
337
- * When enabled, content changes trigger indexing workflows.
338
- * Defaults to true - indexing is always on when ES infrastructure is available.
339
- */
340
- enabled?: boolean;
341
-
342
- /**
343
- * Search tier for this project.
344
- * standard uses the regional hosted Elasticsearch deployment.
345
- * performance uses the regional serverless Elasticsearch project.
346
- * Defaults to standard when omitted.
347
- */
348
- search_tier?: ProjectSearchTier;
349
-
350
- /**
351
- * Elasticsearch backend override for this project.
352
- * Prefer search_tier for project configuration unless an explicit backend override is needed.
353
- */
354
- backend?: ElasticsearchBackend;
355
- };
400
+ indexing?: ProjectIndexingConfiguration;
356
401
 
357
402
  /**
358
403
  * Standard content intake behavior.
@@ -382,6 +427,142 @@ export interface ProjectConfiguration {
382
427
  pdf_template_object_id?: string;
383
428
  }
384
429
 
430
+ /**
431
+ * Elasticsearch field types that may be explicitly assigned to content-object
432
+ * properties. Paths are relative to the object's `properties` field.
433
+ */
434
+ export type ProjectSearchPropertyType = 'keyword' | 'text' | 'boolean' | 'long' | 'double' | 'date';
435
+
436
+ /**
437
+ * Explicit search mapping for one content-object property.
438
+ *
439
+ * Changing a mapping requires a full reindex. Existing Elasticsearch fields
440
+ * cannot change type in place.
441
+ */
442
+ export interface ProjectSearchPropertyMapping {
443
+ type: ProjectSearchPropertyType;
444
+
445
+ /** Elasticsearch date format. Valid only when type is `date`. */
446
+ format?: string;
447
+
448
+ /** Maximum indexed string length. Valid only when type is `keyword`. */
449
+ ignore_above?: number;
450
+
451
+ /**
452
+ * Skip malformed values instead of rejecting the whole document. Valid only
453
+ * for long, double, and date mappings.
454
+ */
455
+ ignore_malformed?: boolean;
456
+ }
457
+
458
+ export const PROJECT_SEARCH_PROPERTY_TYPES: readonly ProjectSearchPropertyType[] = [
459
+ 'keyword',
460
+ 'text',
461
+ 'boolean',
462
+ 'long',
463
+ 'double',
464
+ 'date',
465
+ ];
466
+
467
+ const MAX_PROJECT_SEARCH_PROPERTY_MAPPINGS = 200;
468
+ const MAX_KEYWORD_IGNORE_ABOVE = 8191;
469
+
470
+ /**
471
+ * Validate property mappings at API and index-creation boundaries.
472
+ *
473
+ * Returns user-facing issue strings instead of throwing so callers can map the
474
+ * result to the error type appropriate for their boundary.
475
+ */
476
+ export function validateProjectSearchPropertyMappings(value: unknown): string[] {
477
+ if (value === undefined) return [];
478
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
479
+ return ['indexing.property_mappings must be an object keyed by property path'];
480
+ }
481
+
482
+ const entries = Object.entries(value as Record<string, unknown>);
483
+ const issues: string[] = [];
484
+ if (entries.length > MAX_PROJECT_SEARCH_PROPERTY_MAPPINGS) {
485
+ issues.push(`indexing.property_mappings must contain at most ${MAX_PROJECT_SEARCH_PROPERTY_MAPPINGS} fields`);
486
+ }
487
+
488
+ const supportedTypes = new Set<string>(PROJECT_SEARCH_PROPERTY_TYPES);
489
+ for (const [path, rawMapping] of entries) {
490
+ const field = `indexing.property_mappings.${path}`;
491
+ if (!ELASTICSEARCH_FIELD_PATH_PATTERN.test(path)) {
492
+ issues.push(`${field} must be a dot-separated path containing only letters, numbers, and underscores`);
493
+ }
494
+ if (!rawMapping || typeof rawMapping !== 'object' || Array.isArray(rawMapping)) {
495
+ issues.push(`${field} must be an object`);
496
+ continue;
497
+ }
498
+ const mapping = rawMapping as Record<string, unknown>;
499
+ const extraKeys = Object.keys(mapping).filter(
500
+ (key) => !['type', 'format', 'ignore_above', 'ignore_malformed'].includes(key),
501
+ );
502
+ if (extraKeys.length > 0) {
503
+ issues.push(`${field} contains unsupported option(s): ${extraKeys.join(', ')}`);
504
+ }
505
+ if (typeof mapping.type !== 'string' || !supportedTypes.has(mapping.type)) {
506
+ issues.push(`${field}.type must be one of: ${PROJECT_SEARCH_PROPERTY_TYPES.join(', ')}`);
507
+ }
508
+ if (mapping.format !== undefined && (mapping.type !== 'date' || typeof mapping.format !== 'string')) {
509
+ issues.push(`${field}.format is supported only for date mappings`);
510
+ }
511
+ if (
512
+ mapping.ignore_above !== undefined &&
513
+ (mapping.type !== 'keyword' ||
514
+ !Number.isInteger(mapping.ignore_above) ||
515
+ (mapping.ignore_above as number) < 1 ||
516
+ (mapping.ignore_above as number) > MAX_KEYWORD_IGNORE_ABOVE)
517
+ ) {
518
+ issues.push(
519
+ `${field}.ignore_above is supported only for keyword mappings and must be an integer from 1 to ${MAX_KEYWORD_IGNORE_ABOVE}`,
520
+ );
521
+ }
522
+ if (
523
+ mapping.ignore_malformed !== undefined &&
524
+ (!['long', 'double', 'date'].includes(String(mapping.type)) ||
525
+ typeof mapping.ignore_malformed !== 'boolean')
526
+ ) {
527
+ issues.push(`${field}.ignore_malformed is supported only for long, double, and date mappings`);
528
+ }
529
+ }
530
+ return issues;
531
+ }
532
+
533
+ export interface ProjectIndexingConfiguration {
534
+ /**
535
+ * Enable indexing for content objects in this project.
536
+ * When enabled, content changes trigger indexing workflows.
537
+ * Defaults to true - indexing is always on when ES infrastructure is available.
538
+ */
539
+ enabled?: boolean;
540
+
541
+ /**
542
+ * Search tier for this project.
543
+ * standard uses the regional hosted Elasticsearch deployment.
544
+ * performance uses the regional serverless Elasticsearch project.
545
+ * Defaults to standard when omitted.
546
+ */
547
+ search_tier?: ProjectSearchTier;
548
+
549
+ /**
550
+ * Elasticsearch backend override for this project.
551
+ * Prefer search_tier for project configuration unless an explicit backend override is needed.
552
+ */
553
+ backend?: ElasticsearchBackend;
554
+
555
+ /**
556
+ * Explicit mappings for selected content-object property paths.
557
+ *
558
+ * Keys are dot-separated paths relative to `properties`, for example
559
+ * `order_total` or `customer.account_number`. Unlisted fields are mapped
560
+ * dynamically from their JSON values. Changing this value requires a full
561
+ * reindex.
562
+ */
563
+ property_mappings?: Record<string, ProjectSearchPropertyMapping>;
564
+ }
565
+
385
566
  // export interface ProjectConfigurationEmbeddings {
386
567
  // environment: string;
387
568
  // max_tokens: number;
@@ -937,6 +1118,8 @@ export interface IndexConfiguration {
937
1118
  };
938
1119
  /** ISO 639-1 language code for text analysis */
939
1120
  language?: string;
1121
+ /** Explicit mappings for selected content-object property paths. */
1122
+ property_mappings?: Record<string, ProjectSearchPropertyMapping>;
940
1123
  field_mappings?: Record<string, unknown>;
941
1124
  project_embeddings_config?: {
942
1125
  text?: EmbeddingTypeConfig;
package/src/query.ts CHANGED
@@ -88,6 +88,12 @@ export interface RunSearchQuery extends SimpleSearchQuery {
88
88
  model?: string;
89
89
  status?: ExecutionRunStatus;
90
90
  tags?: string[];
91
+ /**
92
+ * Tags to exclude. Runs carrying any of these tags are filtered out of the results,
93
+ * counts, and facet buckets. Combined with `tags` (which requires all of the listed
94
+ * tags) as an additional `$nin` constraint on the same field.
95
+ */
96
+ exclude_tags?: string[];
91
97
  query?: string;
92
98
  default_query_path?: string;
93
99
  parent?: string[];
package/src/refs.ts CHANGED
@@ -21,6 +21,7 @@ export interface ResourceRef {
21
21
  id: string;
22
22
  name: string;
23
23
  type: string;
24
+ email?: string;
24
25
  description?: string;
25
26
  version?: number;
26
27
  status?: string;
@@ -0,0 +1,32 @@
1
+ import { describe, expect, test } from 'vitest';
2
+ import { SystemRoles } from './project.js';
3
+ import { AbacScopes } from './roles/types.js';
4
+
5
+ describe('shared role vocabulary', () => {
6
+ test('exports all system roles', () => {
7
+ expect(Object.values(SystemRoles).sort()).toEqual(
8
+ [
9
+ 'admin',
10
+ 'app_member',
11
+ 'application',
12
+ 'auditor',
13
+ 'automation',
14
+ 'billing',
15
+ 'consumer',
16
+ 'content_processor',
17
+ 'content_superadmin',
18
+ 'developer',
19
+ 'executor',
20
+ 'manager',
21
+ 'member',
22
+ 'owner',
23
+ 'reader',
24
+ 'support',
25
+ ].sort(),
26
+ );
27
+ });
28
+
29
+ test('exports ABAC scopes used by role wire types', () => {
30
+ expect(AbacScopes).toEqual(['document', 'collection', 'task']);
31
+ });
32
+ });
@@ -0,0 +1,191 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import {
3
+ isExtractableSchemaNode,
4
+ mergePreservingNonExtractable,
5
+ schemaForExtraction,
6
+ } from './schema-for-extraction.js';
7
+
8
+ describe('schemaForExtraction', () => {
9
+ it('drops top-level properties marked x-extract: false and cleans required', () => {
10
+ const schema = {
11
+ type: 'object',
12
+ properties: {
13
+ po_number: { type: 'string' },
14
+ match: { type: 'string', 'x-extract': false },
15
+ match_reason: { type: 'string', 'x-extract': false },
16
+ },
17
+ required: ['po_number', 'match'],
18
+ };
19
+ const filtered = schemaForExtraction(schema);
20
+ expect(Object.keys(filtered.properties as object).sort()).toEqual(['po_number']);
21
+ expect(filtered.required).toEqual(['po_number']);
22
+ expect((filtered.properties as Record<string, unknown>).po_number).toEqual({ type: 'string' });
23
+ });
24
+
25
+ it('drops nested and array-item properties with x-extract: false', () => {
26
+ const schema = {
27
+ type: 'object',
28
+ properties: {
29
+ line_items: {
30
+ type: 'array',
31
+ items: {
32
+ type: 'object',
33
+ properties: {
34
+ part_number: { type: 'string' },
35
+ match: { type: 'string', 'x-extract': false },
36
+ quantity: { type: 'string' },
37
+ },
38
+ required: ['part_number', 'match'],
39
+ },
40
+ },
41
+ },
42
+ };
43
+ const filtered = schemaForExtraction(schema);
44
+ const items = (filtered.properties as Record<string, Record<string, unknown>>).line_items.items as Record<
45
+ string,
46
+ unknown
47
+ >;
48
+ expect(Object.keys(items.properties as object).sort()).toEqual(['part_number', 'quantity']);
49
+ expect(items.required).toEqual(['part_number']);
50
+ });
51
+
52
+ it('treats missing x-extract as extractable', () => {
53
+ expect(isExtractableSchemaNode({ type: 'string' })).toBe(true);
54
+ expect(isExtractableSchemaNode({ type: 'string', 'x-extract': true })).toBe(true);
55
+ expect(isExtractableSchemaNode({ type: 'string', 'x-extract': false })).toBe(false);
56
+ });
57
+
58
+ it('filters $defs and drops properties that reference non-extractable definitions', () => {
59
+ const schema = {
60
+ type: 'object',
61
+ $defs: {
62
+ hidden: { type: 'string', 'x-extract': false },
63
+ visible: {
64
+ type: 'object',
65
+ properties: {
66
+ value: { type: 'string' },
67
+ match: { type: 'string', 'x-extract': false },
68
+ },
69
+ required: ['value', 'match'],
70
+ },
71
+ },
72
+ properties: {
73
+ hidden: { $ref: '#/$defs/hidden' },
74
+ visible: { $ref: '#/$defs/visible' },
75
+ },
76
+ required: ['hidden', 'visible'],
77
+ };
78
+
79
+ const filtered = schemaForExtraction(schema);
80
+
81
+ expect(filtered.$defs.hidden).toBeUndefined();
82
+ expect(filtered.$defs.visible).toEqual({
83
+ type: 'object',
84
+ properties: { value: { type: 'string' } },
85
+ required: ['value'],
86
+ });
87
+ expect(filtered.properties).toEqual({ visible: { $ref: '#/$defs/visible' } });
88
+ expect(filtered.required).toEqual(['visible']);
89
+ });
90
+
91
+ it('drops non-extractable anyOf branches', () => {
92
+ const schema = {
93
+ type: 'object',
94
+ properties: {
95
+ value: {
96
+ anyOf: [{ type: 'string', 'x-extract': false }, { type: 'number' }],
97
+ },
98
+ },
99
+ };
100
+
101
+ const filtered = schemaForExtraction(schema);
102
+
103
+ expect((filtered.properties.value as { anyOf: unknown[] }).anyOf).toEqual([{ type: 'number' }]);
104
+ });
105
+ });
106
+
107
+ describe('mergePreservingNonExtractable', () => {
108
+ it('preserves non-extractable top-level values over model output', () => {
109
+ const schema = {
110
+ type: 'object',
111
+ properties: {
112
+ po_number: { type: 'string' },
113
+ match: { type: 'string', 'x-extract': false },
114
+ },
115
+ };
116
+ const existing = { po_number: 'OLD', match: 'erp-hit' };
117
+ const extracted = { po_number: 'NEW', match: 'model-invented' };
118
+ expect(mergePreservingNonExtractable(existing, extracted, schema)).toEqual({
119
+ po_number: 'NEW',
120
+ match: 'erp-hit',
121
+ });
122
+ });
123
+
124
+ it('clears extractable fields omitted by the latest extraction', () => {
125
+ const schema = {
126
+ type: 'object',
127
+ properties: {
128
+ po_number: { type: 'string' },
129
+ total: { type: 'number' },
130
+ match: { type: 'string', 'x-extract': false },
131
+ },
132
+ };
133
+ const existing = { po_number: 'STALE', total: 42, match: 'erp-hit' };
134
+ const extracted = { total: 50 };
135
+
136
+ expect(mergePreservingNonExtractable(existing, extracted, schema)).toEqual({
137
+ total: 50,
138
+ match: 'erp-hit',
139
+ });
140
+ });
141
+
142
+ it('does not preserve non-extractable array item fields by index', () => {
143
+ const schema = {
144
+ type: 'object',
145
+ properties: {
146
+ line_items: {
147
+ type: 'array',
148
+ items: {
149
+ type: 'object',
150
+ properties: {
151
+ part: { type: 'string' },
152
+ match: { type: 'string', 'x-extract': false },
153
+ },
154
+ },
155
+ },
156
+ },
157
+ };
158
+ const existing = {
159
+ line_items: [
160
+ { part: 'A', match: 'hit-a' },
161
+ { part: 'B', match: 'hit-b' },
162
+ ],
163
+ };
164
+ const extracted = {
165
+ line_items: [{ part: 'B2' }],
166
+ };
167
+ expect(mergePreservingNonExtractable(existing, extracted, schema)).toEqual({
168
+ line_items: [{ part: 'B2' }],
169
+ });
170
+ });
171
+
172
+ it('preserves top-level values referenced through non-extractable $defs', () => {
173
+ const schema = {
174
+ type: 'object',
175
+ $defs: {
176
+ match: { type: 'string', 'x-extract': false },
177
+ },
178
+ properties: {
179
+ po_number: { type: 'string' },
180
+ match: { $ref: '#/$defs/match' },
181
+ },
182
+ };
183
+ const existing = { po_number: 'OLD', match: 'erp-hit' };
184
+ const extracted = { po_number: 'NEW', match: 'model-invented' };
185
+
186
+ expect(mergePreservingNonExtractable(existing, extracted, schema)).toEqual({
187
+ po_number: 'NEW',
188
+ match: 'erp-hit',
189
+ });
190
+ });
191
+ });