@clivly/core 0.2.0 → 0.3.0-next.2

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.
@@ -1,13 +1,35 @@
1
+ import { Scored } from "./mapping-score.cjs";
2
+
1
3
  //#region src/entity-heuristics.d.ts
2
4
  declare const TARGET_CONCEPTS: readonly ["contact", "company", "deal", "activity", "conversation", "note"];
3
5
  type TargetConcept = (typeof TARGET_CONCEPTS)[number];
4
6
  declare function suggestCursorField(columnKeys: string[]): string | null;
5
- declare function suggestConcept(tableName: string): TargetConcept | null;
6
7
  declare function matchesConcept(tableName: string, concept: TargetConcept): {
7
8
  exact: boolean;
8
9
  } | null;
9
- declare function suggestFieldMap(concept: TargetConcept, columns: string[]): Record<string, string>;
10
- declare function requiredFieldsFor(concept: TargetConcept): string[];
10
+ interface DiscoveredColumnMeta {
11
+ isForeignKey?: boolean;
12
+ isPrimaryKey?: boolean;
13
+ name: string;
14
+ nullable?: boolean;
15
+ references?: {
16
+ table: string;
17
+ column: string;
18
+ };
19
+ type?: string;
20
+ }
21
+ interface ScoredTable {
22
+ columns: string[];
23
+ columnsMeta?: DiscoveredColumnMeta[];
24
+ name: string;
25
+ rowCount?: number;
26
+ }
27
+ declare function scoreConcepts(table: ScoredTable): Scored<TargetConcept>[];
28
+ declare function topConcept(table: ScoredTable): {
29
+ best: Scored<TargetConcept>;
30
+ ambiguous: boolean;
31
+ } | null;
32
+ declare function requiredFieldsFor(concept: TargetConcept | "custom"): string[];
11
33
  declare function validateMapping(requiredFields: string[], fieldMap: Record<string, string>): {
12
34
  valid: boolean;
13
35
  missing: string[];
@@ -28,16 +50,19 @@ interface RelationshipSuggestion {
28
50
  name: string;
29
51
  via: SuggestedVia;
30
52
  }
31
- interface HeuristicTable {
32
- columns: string[];
33
- name: string;
34
- }
35
53
  /**
36
- * Suggest contact→company relationships for a contact-concept source table by
37
- * inspecting FK column names across the discovered schema. Detects all three
38
- * DSL shapes: FK-on-contact, FK-on-company, and a join table. Suggestions are
39
- * candidates only the developer confirms.
54
+ * Suggest contact→company relationships. When the source table carries FK
55
+ * metadata (`references`), the target table is a *fact*, not a name guess —
56
+ * that scores highest. Without metadata (v1 hosts) we fall back to the legacy
57
+ * FK-name hints at a lower tier.
40
58
  */
41
- declare function suggestRelationships(source: HeuristicTable, allTables: HeuristicTable[]): RelationshipSuggestion[];
59
+ declare function scoreRelationships(source: ScoredTable, allTables: ScoredTable[]): Scored<RelationshipSuggestion>[];
60
+ interface FieldSuggestion {
61
+ column: string;
62
+ field: string;
63
+ required: boolean;
64
+ }
65
+ declare function scoreFieldMap(concept: TargetConcept, table: ScoredTable): Scored<FieldSuggestion>[];
66
+ declare function toFieldMap(scored: Scored<FieldSuggestion>[]): Record<string, string>;
42
67
  //#endregion
43
- export { RelationshipSuggestion, SuggestedVia, TARGET_CONCEPTS, TargetConcept, matchesConcept, requiredFieldsFor, suggestConcept, suggestCursorField, suggestDiscriminatorColumns, suggestFieldMap, suggestRelationships, validateMapping };
68
+ export { DiscoveredColumnMeta, FieldSuggestion, RelationshipSuggestion, ScoredTable, SuggestedVia, TARGET_CONCEPTS, TargetConcept, matchesConcept, requiredFieldsFor, scoreConcepts, scoreFieldMap, scoreRelationships, suggestCursorField, suggestDiscriminatorColumns, toFieldMap, topConcept, validateMapping };
@@ -1,13 +1,35 @@
1
+ import { Scored } from "./mapping-score.mjs";
2
+
1
3
  //#region src/entity-heuristics.d.ts
2
4
  declare const TARGET_CONCEPTS: readonly ["contact", "company", "deal", "activity", "conversation", "note"];
3
5
  type TargetConcept = (typeof TARGET_CONCEPTS)[number];
4
6
  declare function suggestCursorField(columnKeys: string[]): string | null;
5
- declare function suggestConcept(tableName: string): TargetConcept | null;
6
7
  declare function matchesConcept(tableName: string, concept: TargetConcept): {
7
8
  exact: boolean;
8
9
  } | null;
9
- declare function suggestFieldMap(concept: TargetConcept, columns: string[]): Record<string, string>;
10
- declare function requiredFieldsFor(concept: TargetConcept): string[];
10
+ interface DiscoveredColumnMeta {
11
+ isForeignKey?: boolean;
12
+ isPrimaryKey?: boolean;
13
+ name: string;
14
+ nullable?: boolean;
15
+ references?: {
16
+ table: string;
17
+ column: string;
18
+ };
19
+ type?: string;
20
+ }
21
+ interface ScoredTable {
22
+ columns: string[];
23
+ columnsMeta?: DiscoveredColumnMeta[];
24
+ name: string;
25
+ rowCount?: number;
26
+ }
27
+ declare function scoreConcepts(table: ScoredTable): Scored<TargetConcept>[];
28
+ declare function topConcept(table: ScoredTable): {
29
+ best: Scored<TargetConcept>;
30
+ ambiguous: boolean;
31
+ } | null;
32
+ declare function requiredFieldsFor(concept: TargetConcept | "custom"): string[];
11
33
  declare function validateMapping(requiredFields: string[], fieldMap: Record<string, string>): {
12
34
  valid: boolean;
13
35
  missing: string[];
@@ -28,16 +50,19 @@ interface RelationshipSuggestion {
28
50
  name: string;
29
51
  via: SuggestedVia;
30
52
  }
31
- interface HeuristicTable {
32
- columns: string[];
33
- name: string;
34
- }
35
53
  /**
36
- * Suggest contact→company relationships for a contact-concept source table by
37
- * inspecting FK column names across the discovered schema. Detects all three
38
- * DSL shapes: FK-on-contact, FK-on-company, and a join table. Suggestions are
39
- * candidates only the developer confirms.
54
+ * Suggest contact→company relationships. When the source table carries FK
55
+ * metadata (`references`), the target table is a *fact*, not a name guess —
56
+ * that scores highest. Without metadata (v1 hosts) we fall back to the legacy
57
+ * FK-name hints at a lower tier.
40
58
  */
41
- declare function suggestRelationships(source: HeuristicTable, allTables: HeuristicTable[]): RelationshipSuggestion[];
59
+ declare function scoreRelationships(source: ScoredTable, allTables: ScoredTable[]): Scored<RelationshipSuggestion>[];
60
+ interface FieldSuggestion {
61
+ column: string;
62
+ field: string;
63
+ required: boolean;
64
+ }
65
+ declare function scoreFieldMap(concept: TargetConcept, table: ScoredTable): Scored<FieldSuggestion>[];
66
+ declare function toFieldMap(scored: Scored<FieldSuggestion>[]): Record<string, string>;
42
67
  //#endregion
43
- export { RelationshipSuggestion, SuggestedVia, TARGET_CONCEPTS, TargetConcept, matchesConcept, requiredFieldsFor, suggestConcept, suggestCursorField, suggestDiscriminatorColumns, suggestFieldMap, suggestRelationships, validateMapping };
68
+ export { DiscoveredColumnMeta, FieldSuggestion, RelationshipSuggestion, ScoredTable, SuggestedVia, TARGET_CONCEPTS, TargetConcept, matchesConcept, requiredFieldsFor, scoreConcepts, scoreFieldMap, scoreRelationships, suggestCursorField, suggestDiscriminatorColumns, toFieldMap, topConcept, validateMapping };
@@ -1,3 +1,5 @@
1
+ import { MATERIALIZING_CONCEPTS } from "./entity-config.mjs";
2
+ import { CONCEPT_TIERS, FIELD_TIERS, RELATIONSHIP_TIERS, W, isAmbiguous, isNumericLike, isTextLike, isTimestampLike, tierFor } from "./mapping-score.mjs";
1
3
  //#region src/entity-heuristics.ts
2
4
  const TARGET_CONCEPTS = [
3
5
  "contact",
@@ -115,7 +117,7 @@ const CONCEPT_FIELDS = {
115
117
  {
116
118
  field: "name",
117
119
  candidates: ["name", "title"],
118
- required: false
120
+ required: true
119
121
  },
120
122
  {
121
123
  field: "value",
@@ -207,12 +209,7 @@ function suggestCursorField(columnKeys) {
207
209
  }
208
210
  return null;
209
211
  }
210
- function suggestConcept(tableName) {
211
- const normalized = tableName.toLowerCase();
212
- for (const { concept, names } of TABLE_PATTERNS) if (names.includes(normalized)) return concept;
213
- for (const { concept, names } of TABLE_PATTERNS) if (names.some((name) => normalized.includes(name))) return concept;
214
- return null;
215
- }
212
+ const MATERIALIZING = new Set(MATERIALIZING_CONCEPTS);
216
213
  function matchesConcept(tableName, concept) {
217
214
  const normalized = tableName.toLowerCase();
218
215
  const entry = TABLE_PATTERNS.find((p) => p.concept === concept);
@@ -221,19 +218,64 @@ function matchesConcept(tableName, concept) {
221
218
  if (entry.names.some((name) => normalized.includes(name))) return { exact: false };
222
219
  return null;
223
220
  }
224
- function suggestFieldMap(concept, columns) {
225
- const lowerToActual = new Map(columns.map((c) => [c.toLowerCase(), c]));
226
- const map = {};
227
- for (const { field, candidates } of CONCEPT_FIELDS[concept]) for (const candidate of candidates) {
228
- const match = lowerToActual.get(candidate);
229
- if (match) {
230
- map[field] = match;
231
- break;
221
+ function metaFor(table, column) {
222
+ return table.columnsMeta?.find((c) => c.name === column);
223
+ }
224
+ function hasDefiningColumn(concept, columns) {
225
+ const lower = new Set(columns.map((c) => c.toLowerCase()));
226
+ const required = CONCEPT_FIELDS[concept].filter((f) => f.required);
227
+ return required.length > 0 && required.every((f) => f.candidates.some((c) => lower.has(c)));
228
+ }
229
+ function scoreConcepts(table) {
230
+ const out = [];
231
+ for (const concept of TARGET_CONCEPTS) {
232
+ if (!MATERIALIZING.has(concept)) continue;
233
+ const nameMatch = matchesConcept(table.name, concept);
234
+ if (!nameMatch) continue;
235
+ let score = 0;
236
+ const reasons = [];
237
+ if (nameMatch.exact) {
238
+ score += W.nameExact;
239
+ reasons.push(`name matches "${table.name}"`);
240
+ } else {
241
+ score += W.nameSubstring;
242
+ reasons.push(`name looks like ${concept}`);
232
243
  }
244
+ if (hasDefiningColumn(concept, table.columns)) {
245
+ score += W.definingColumn;
246
+ reasons.push(`has the columns a ${concept} needs`);
247
+ }
248
+ if (table.columnsMeta?.some((c) => c.isPrimaryKey)) {
249
+ score += W.hasPrimaryKey;
250
+ reasons.push("has primary key");
251
+ }
252
+ if (table.rowCount !== void 0) if (table.rowCount === 0) {
253
+ score += W.emptyTable;
254
+ reasons.push("table is empty");
255
+ } else {
256
+ score += W.hasRows;
257
+ reasons.push(`${table.rowCount.toLocaleString("en-US")} rows`);
258
+ }
259
+ out.push({
260
+ value: concept,
261
+ score,
262
+ tier: tierFor(score, CONCEPT_TIERS),
263
+ reasons
264
+ });
233
265
  }
234
- return map;
266
+ return out.sort((a, b) => b.score - a.score);
267
+ }
268
+ function topConcept(table) {
269
+ const ranked = scoreConcepts(table);
270
+ const best = ranked[0];
271
+ if (!best) return null;
272
+ return {
273
+ best,
274
+ ambiguous: isAmbiguous(ranked)
275
+ };
235
276
  }
236
277
  function requiredFieldsFor(concept) {
278
+ if (concept === "custom") return [];
237
279
  return CONCEPT_FIELDS[concept].filter((f) => f.required).map((f) => f.field);
238
280
  }
239
281
  function validateMapping(requiredFields, fieldMap) {
@@ -261,14 +303,6 @@ function suggestDiscriminatorColumns(columns) {
261
303
  }
262
304
  return out;
263
305
  }
264
- const CONTACT_FK_HINTS = [
265
- "user_id",
266
- "owner_id",
267
- "contact_id",
268
- "member_id",
269
- "person_id",
270
- "created_by"
271
- ];
272
306
  const COMPANY_FK_HINTS = [
273
307
  "organization_id",
274
308
  "organisation_id",
@@ -286,50 +320,112 @@ function firstMatchingColumn(columns, hints) {
286
320
  return null;
287
321
  }
288
322
  /**
289
- * Suggest contact→company relationships for a contact-concept source table by
290
- * inspecting FK column names across the discovered schema. Detects all three
291
- * DSL shapes: FK-on-contact, FK-on-company, and a join table. Suggestions are
292
- * candidates only the developer confirms.
323
+ * Suggest contact→company relationships. When the source table carries FK
324
+ * metadata (`references`), the target table is a *fact*, not a name guess —
325
+ * that scores highest. Without metadata (v1 hosts) we fall back to the legacy
326
+ * FK-name hints at a lower tier.
293
327
  */
294
- function suggestRelationships(source, allTables) {
295
- const companyTables = allTables.filter((t) => t.name !== source.name && suggestConcept(t.name) === "company");
328
+ function scoreRelationships(source, allTables) {
329
+ const companyTables = allTables.filter((t) => t.name !== source.name && topConcept(t)?.best.value === "company");
296
330
  if (companyTables.length === 0) return [];
297
- const suggestions = [];
298
- const contactFk = firstMatchingColumn(source.columns, COMPANY_FK_HINTS);
299
- if (contactFk) suggestions.push({
300
- name: "company",
301
- entityTable: companyTables[0]?.name ?? "",
302
- via: {
303
- fkOn: "contact",
304
- column: `${source.name}.${contactFk}`
305
- }
306
- });
307
- for (const company of companyTables) {
308
- const companyFk = firstMatchingColumn(company.columns, CONTACT_FK_HINTS);
309
- if (companyFk) suggestions.push({
310
- name: "company",
311
- entityTable: company.name,
312
- via: {
313
- fkOn: "company",
314
- column: `${company.name}.${companyFk}`
315
- }
331
+ const companyNames = new Set(companyTables.map((t) => t.name.toLowerCase()));
332
+ const out = [];
333
+ const seenTargets = /* @__PURE__ */ new Set();
334
+ for (const col of source.columnsMeta ?? []) {
335
+ const ref = col.references;
336
+ if (!(ref && companyNames.has(ref.table.toLowerCase()))) continue;
337
+ if (seenTargets.has(ref.table.toLowerCase())) continue;
338
+ seenTargets.add(ref.table.toLowerCase());
339
+ out.push({
340
+ value: {
341
+ name: "company",
342
+ entityTable: ref.table,
343
+ via: {
344
+ fkOn: "contact",
345
+ column: `${source.name}.${col.name}`
346
+ }
347
+ },
348
+ score: W.fkResolved,
349
+ tier: tierFor(W.fkResolved, RELATIONSHIP_TIERS),
350
+ reasons: [`foreign key → ${ref.table}.${ref.column}`]
316
351
  });
317
352
  }
318
- for (const table of allTables) {
319
- if (table.name === source.name) continue;
320
- const localKey = firstMatchingColumn(table.columns, CONTACT_FK_HINTS);
321
- const foreignKey = firstMatchingColumn(table.columns, COMPANY_FK_HINTS);
322
- if (localKey && foreignKey) suggestions.push({
353
+ if (source.columnsMeta?.length) return out.sort((a, b) => b.score - a.score);
354
+ const contactFk = firstMatchingColumn(source.columns, COMPANY_FK_HINTS);
355
+ if (contactFk) out.push({
356
+ value: {
323
357
  name: "company",
324
- entityTable: companyTables.find((c) => c.name !== table.name)?.name ?? companyTables[0]?.name ?? "",
358
+ entityTable: companyTables[0]?.name ?? "",
325
359
  via: {
326
- through: table.name,
327
- localKey,
328
- foreignKey
360
+ fkOn: "contact",
361
+ column: `${source.name}.${contactFk}`
329
362
  }
363
+ },
364
+ score: W.fkNameHint,
365
+ tier: tierFor(W.fkNameHint, RELATIONSHIP_TIERS),
366
+ reasons: [`column "${contactFk}" looks like a company link`]
367
+ });
368
+ return out.sort((a, b) => b.score - a.score);
369
+ }
370
+ const FIELD_TYPE_EXPECTATION = {
371
+ name: "text",
372
+ email: "text",
373
+ domain: "text",
374
+ stage: "text",
375
+ status: "text",
376
+ subject: "text",
377
+ body: "text",
378
+ summary: "text",
379
+ type: "text",
380
+ value: "numeric",
381
+ createdAt: "timestamp"
382
+ };
383
+ function typeFits(field, type) {
384
+ const expectation = FIELD_TYPE_EXPECTATION[field];
385
+ if (!(expectation && type)) return null;
386
+ if (expectation === "text") return isTextLike(type);
387
+ if (expectation === "numeric") return isNumericLike(type);
388
+ return isTimestampLike(type);
389
+ }
390
+ function scoreFieldMap(concept, table) {
391
+ const lowerToActual = new Map(table.columns.map((c) => [c.toLowerCase(), c]));
392
+ const out = [];
393
+ for (const { field, candidates, required } of CONCEPT_FIELDS[concept]) for (const candidate of candidates) {
394
+ const column = lowerToActual.get(candidate);
395
+ if (!column) continue;
396
+ let score = W.fieldNameMatch;
397
+ const reasons = [`name matches "${column}"`];
398
+ const meta = metaFor(table, column);
399
+ const fits = typeFits(field, meta?.type);
400
+ if (fits === true) {
401
+ score += W.typeCompatible;
402
+ reasons.push(`type ${meta?.type}`);
403
+ } else if (fits === false) {
404
+ score += W.typeMismatch;
405
+ reasons.push(`type ${meta?.type} does not fit ${field}`);
406
+ }
407
+ if (required && meta?.nullable === false) {
408
+ score += W.requiredNotNull;
409
+ reasons.push("not null");
410
+ }
411
+ out.push({
412
+ value: {
413
+ field,
414
+ column,
415
+ required
416
+ },
417
+ score,
418
+ tier: tierFor(score, FIELD_TIERS),
419
+ reasons
330
420
  });
421
+ break;
331
422
  }
332
- return suggestions;
423
+ return out;
424
+ }
425
+ function toFieldMap(scored) {
426
+ const map = {};
427
+ for (const { value } of scored) map[value.field] = value.column;
428
+ return map;
333
429
  }
334
430
  //#endregion
335
- export { TARGET_CONCEPTS, matchesConcept, requiredFieldsFor, suggestConcept, suggestCursorField, suggestDiscriminatorColumns, suggestFieldMap, suggestRelationships, validateMapping };
431
+ export { TARGET_CONCEPTS, matchesConcept, requiredFieldsFor, scoreConcepts, scoreFieldMap, scoreRelationships, suggestCursorField, suggestDiscriminatorColumns, toFieldMap, topConcept, validateMapping };
package/dist/index.cjs CHANGED
@@ -8,6 +8,7 @@ const require_sync_engine = require("./sync-engine.cjs");
8
8
  exports.CANONICAL_FIELDS = require_entity_config.CANONICAL_FIELDS;
9
9
  exports.CRM_CONCEPTS = require_entity_config.CRM_CONCEPTS;
10
10
  exports.ClivlyConfigError = require_entity_config.ClivlyConfigError;
11
+ exports.MATERIALIZING_CONCEPTS = require_entity_config.MATERIALIZING_CONCEPTS;
11
12
  exports.authCustom = require_auth_adapter.authCustom;
12
13
  exports.buildFilterFromDiscriminator = require_mapping_form.buildFilterFromDiscriminator;
13
14
  exports.buildRelationships = require_mapping_form.buildRelationships;
package/dist/index.d.cts CHANGED
@@ -1,9 +1,9 @@
1
- import { Activity, ActivityType, Company, Contact, ContactFilters, ContactStatus, CreateActivityInput, CreateCompanyInput, CreateContactInput, CreateDealInput, CreateTaskInput, CrmRole, Deal, DealStage, Note, Task, TaskEntityType, TaskFilters, TaskPriority, TaskStatus, UpdateContactInput, UpdateDealInput, UpdateTaskInput } from "./types.cjs";
1
+ import { Activity, ActivityType, Company, Contact, ContactFilters, ContactStatus, CreateActivityInput, CreateCompanyInput, CreateContactInput, CreateDealInput, CreateTaskInput, CrmRole, Deal, DealStage, Note, RecordSource, Task, TaskEntityType, TaskFilters, TaskPriority, TaskStatus, UpdateContactInput, UpdateDealInput, UpdateTaskInput } from "./types.cjs";
2
2
  import { CRMAdapter } from "./adapter.cjs";
3
3
  import { ClivlyAuthAdapter, ClivlyUser, CrmMembership, MembershipResolver, authCustom } from "./auth-adapter.cjs";
4
- import { CANONICAL_FIELDS, CRM_CONCEPTS, ClivlyConfigError, ClivlyEntitiesConfig, ClivlyEntityConfig, ConfigValidationError, ConfigValidationResult, CrmConcept, DiscoveredSchemaTable, FilterExpr, FilterValue, RelationshipSpec, RelationshipVia, RelationshipsMap, defineClivlyConfig, entitiesConfigSchema, filterSchema, relationshipSchema, relationshipsSchema, validateEntitiesConfig } from "./entity-config.cjs";
4
+ import { CANONICAL_FIELDS, CRM_CONCEPTS, ClivlyConfigError, ClivlyEntitiesConfig, ClivlyEntityConfig, ConfigValidationError, ConfigValidationResult, CrmConcept, DiscoveredSchemaTable, FilterExpr, FilterValue, MATERIALIZING_CONCEPTS, MaterializingConcept, RelationshipSpec, RelationshipVia, RelationshipsMap, defineClivlyConfig, entitiesConfigSchema, filterSchema, relationshipSchema, relationshipsSchema, validateEntitiesConfig } from "./entity-config.cjs";
5
5
  import { RelationshipCandidate, buildFilterFromDiscriminator, buildRelationships, relationshipViaSignature } from "./mapping-form.cjs";
6
6
  import { EntityMappingRow, mappingsToEntitiesConfig } from "./mappings-config.cjs";
7
7
  import { CompileOptions, CompiledView, ExplainResult, compileEntityView, compileEntityViews, explainEntitiesConfig } from "./view-compiler.cjs";
8
8
  import { EntitySyncResult, MirrorRecord, PersistAction, SourceRow, SyncAction, SyncResult, SyncStore, reconcile, runSync } from "./sync-engine.cjs";
9
- export { Activity, ActivityType, CANONICAL_FIELDS, type CRMAdapter, CRM_CONCEPTS, type ClivlyAuthAdapter, ClivlyConfigError, type ClivlyEntitiesConfig, type ClivlyEntityConfig, type ClivlyUser, Company, type CompileOptions, type CompiledView, type ConfigValidationError, type ConfigValidationResult, Contact, ContactFilters, ContactStatus, CreateActivityInput, CreateCompanyInput, CreateContactInput, CreateDealInput, CreateTaskInput, type CrmConcept, type CrmMembership, CrmRole, Deal, DealStage, type DiscoveredSchemaTable, type EntityMappingRow, type EntitySyncResult, type ExplainResult, type FilterExpr, type FilterValue, type MembershipResolver, type MirrorRecord, Note, type PersistAction, type RelationshipCandidate, type RelationshipSpec, type RelationshipVia, type RelationshipsMap, type SourceRow, type SyncAction, type SyncResult, type SyncStore, Task, TaskEntityType, TaskFilters, TaskPriority, TaskStatus, UpdateContactInput, UpdateDealInput, UpdateTaskInput, authCustom, buildFilterFromDiscriminator, buildRelationships, compileEntityView, compileEntityViews, defineClivlyConfig, entitiesConfigSchema, explainEntitiesConfig, filterSchema, mappingsToEntitiesConfig, reconcile, relationshipSchema, relationshipViaSignature, relationshipsSchema, runSync, validateEntitiesConfig };
9
+ export { Activity, ActivityType, CANONICAL_FIELDS, type CRMAdapter, CRM_CONCEPTS, type ClivlyAuthAdapter, ClivlyConfigError, type ClivlyEntitiesConfig, type ClivlyEntityConfig, type ClivlyUser, Company, type CompileOptions, type CompiledView, type ConfigValidationError, type ConfigValidationResult, Contact, ContactFilters, ContactStatus, CreateActivityInput, CreateCompanyInput, CreateContactInput, CreateDealInput, CreateTaskInput, type CrmConcept, type CrmMembership, CrmRole, Deal, DealStage, type DiscoveredSchemaTable, type EntityMappingRow, type EntitySyncResult, type ExplainResult, type FilterExpr, type FilterValue, MATERIALIZING_CONCEPTS, type MaterializingConcept, type MembershipResolver, type MirrorRecord, Note, type PersistAction, RecordSource, type RelationshipCandidate, type RelationshipSpec, type RelationshipVia, type RelationshipsMap, type SourceRow, type SyncAction, type SyncResult, type SyncStore, Task, TaskEntityType, TaskFilters, TaskPriority, TaskStatus, UpdateContactInput, UpdateDealInput, UpdateTaskInput, authCustom, buildFilterFromDiscriminator, buildRelationships, compileEntityView, compileEntityViews, defineClivlyConfig, entitiesConfigSchema, explainEntitiesConfig, filterSchema, mappingsToEntitiesConfig, reconcile, relationshipSchema, relationshipViaSignature, relationshipsSchema, runSync, validateEntitiesConfig };
package/dist/index.d.mts CHANGED
@@ -1,9 +1,9 @@
1
- import { Activity, ActivityType, Company, Contact, ContactFilters, ContactStatus, CreateActivityInput, CreateCompanyInput, CreateContactInput, CreateDealInput, CreateTaskInput, CrmRole, Deal, DealStage, Note, Task, TaskEntityType, TaskFilters, TaskPriority, TaskStatus, UpdateContactInput, UpdateDealInput, UpdateTaskInput } from "./types.mjs";
1
+ import { Activity, ActivityType, Company, Contact, ContactFilters, ContactStatus, CreateActivityInput, CreateCompanyInput, CreateContactInput, CreateDealInput, CreateTaskInput, CrmRole, Deal, DealStage, Note, RecordSource, Task, TaskEntityType, TaskFilters, TaskPriority, TaskStatus, UpdateContactInput, UpdateDealInput, UpdateTaskInput } from "./types.mjs";
2
2
  import { CRMAdapter } from "./adapter.mjs";
3
3
  import { ClivlyAuthAdapter, ClivlyUser, CrmMembership, MembershipResolver, authCustom } from "./auth-adapter.mjs";
4
- import { CANONICAL_FIELDS, CRM_CONCEPTS, ClivlyConfigError, ClivlyEntitiesConfig, ClivlyEntityConfig, ConfigValidationError, ConfigValidationResult, CrmConcept, DiscoveredSchemaTable, FilterExpr, FilterValue, RelationshipSpec, RelationshipVia, RelationshipsMap, defineClivlyConfig, entitiesConfigSchema, filterSchema, relationshipSchema, relationshipsSchema, validateEntitiesConfig } from "./entity-config.mjs";
4
+ import { CANONICAL_FIELDS, CRM_CONCEPTS, ClivlyConfigError, ClivlyEntitiesConfig, ClivlyEntityConfig, ConfigValidationError, ConfigValidationResult, CrmConcept, DiscoveredSchemaTable, FilterExpr, FilterValue, MATERIALIZING_CONCEPTS, MaterializingConcept, RelationshipSpec, RelationshipVia, RelationshipsMap, defineClivlyConfig, entitiesConfigSchema, filterSchema, relationshipSchema, relationshipsSchema, validateEntitiesConfig } from "./entity-config.mjs";
5
5
  import { RelationshipCandidate, buildFilterFromDiscriminator, buildRelationships, relationshipViaSignature } from "./mapping-form.mjs";
6
6
  import { EntityMappingRow, mappingsToEntitiesConfig } from "./mappings-config.mjs";
7
7
  import { CompileOptions, CompiledView, ExplainResult, compileEntityView, compileEntityViews, explainEntitiesConfig } from "./view-compiler.mjs";
8
8
  import { EntitySyncResult, MirrorRecord, PersistAction, SourceRow, SyncAction, SyncResult, SyncStore, reconcile, runSync } from "./sync-engine.mjs";
9
- export { Activity, ActivityType, CANONICAL_FIELDS, type CRMAdapter, CRM_CONCEPTS, type ClivlyAuthAdapter, ClivlyConfigError, type ClivlyEntitiesConfig, type ClivlyEntityConfig, type ClivlyUser, Company, type CompileOptions, type CompiledView, type ConfigValidationError, type ConfigValidationResult, Contact, ContactFilters, ContactStatus, CreateActivityInput, CreateCompanyInput, CreateContactInput, CreateDealInput, CreateTaskInput, type CrmConcept, type CrmMembership, CrmRole, Deal, DealStage, type DiscoveredSchemaTable, type EntityMappingRow, type EntitySyncResult, type ExplainResult, type FilterExpr, type FilterValue, type MembershipResolver, type MirrorRecord, Note, type PersistAction, type RelationshipCandidate, type RelationshipSpec, type RelationshipVia, type RelationshipsMap, type SourceRow, type SyncAction, type SyncResult, type SyncStore, Task, TaskEntityType, TaskFilters, TaskPriority, TaskStatus, UpdateContactInput, UpdateDealInput, UpdateTaskInput, authCustom, buildFilterFromDiscriminator, buildRelationships, compileEntityView, compileEntityViews, defineClivlyConfig, entitiesConfigSchema, explainEntitiesConfig, filterSchema, mappingsToEntitiesConfig, reconcile, relationshipSchema, relationshipViaSignature, relationshipsSchema, runSync, validateEntitiesConfig };
9
+ export { Activity, ActivityType, CANONICAL_FIELDS, type CRMAdapter, CRM_CONCEPTS, type ClivlyAuthAdapter, ClivlyConfigError, type ClivlyEntitiesConfig, type ClivlyEntityConfig, type ClivlyUser, Company, type CompileOptions, type CompiledView, type ConfigValidationError, type ConfigValidationResult, Contact, ContactFilters, ContactStatus, CreateActivityInput, CreateCompanyInput, CreateContactInput, CreateDealInput, CreateTaskInput, type CrmConcept, type CrmMembership, CrmRole, Deal, DealStage, type DiscoveredSchemaTable, type EntityMappingRow, type EntitySyncResult, type ExplainResult, type FilterExpr, type FilterValue, MATERIALIZING_CONCEPTS, type MaterializingConcept, type MembershipResolver, type MirrorRecord, Note, type PersistAction, RecordSource, type RelationshipCandidate, type RelationshipSpec, type RelationshipVia, type RelationshipsMap, type SourceRow, type SyncAction, type SyncResult, type SyncStore, Task, TaskEntityType, TaskFilters, TaskPriority, TaskStatus, UpdateContactInput, UpdateDealInput, UpdateTaskInput, authCustom, buildFilterFromDiscriminator, buildRelationships, compileEntityView, compileEntityViews, defineClivlyConfig, entitiesConfigSchema, explainEntitiesConfig, filterSchema, mappingsToEntitiesConfig, reconcile, relationshipSchema, relationshipViaSignature, relationshipsSchema, runSync, validateEntitiesConfig };
package/dist/index.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  import { authCustom } from "./auth-adapter.mjs";
2
- import { CANONICAL_FIELDS, CRM_CONCEPTS, ClivlyConfigError, defineClivlyConfig, entitiesConfigSchema, filterSchema, relationshipSchema, relationshipsSchema, validateEntitiesConfig } from "./entity-config.mjs";
2
+ import { CANONICAL_FIELDS, CRM_CONCEPTS, ClivlyConfigError, MATERIALIZING_CONCEPTS, defineClivlyConfig, entitiesConfigSchema, filterSchema, relationshipSchema, relationshipsSchema, validateEntitiesConfig } from "./entity-config.mjs";
3
3
  import { buildFilterFromDiscriminator, buildRelationships, relationshipViaSignature } from "./mapping-form.mjs";
4
4
  import { mappingsToEntitiesConfig } from "./mappings-config.mjs";
5
5
  import { compileEntityView, compileEntityViews, explainEntitiesConfig } from "./view-compiler.mjs";
6
6
  import { reconcile, runSync } from "./sync-engine.mjs";
7
- export { CANONICAL_FIELDS, CRM_CONCEPTS, ClivlyConfigError, authCustom, buildFilterFromDiscriminator, buildRelationships, compileEntityView, compileEntityViews, defineClivlyConfig, entitiesConfigSchema, explainEntitiesConfig, filterSchema, mappingsToEntitiesConfig, reconcile, relationshipSchema, relationshipViaSignature, relationshipsSchema, runSync, validateEntitiesConfig };
7
+ export { CANONICAL_FIELDS, CRM_CONCEPTS, ClivlyConfigError, MATERIALIZING_CONCEPTS, authCustom, buildFilterFromDiscriminator, buildRelationships, compileEntityView, compileEntityViews, defineClivlyConfig, entitiesConfigSchema, explainEntitiesConfig, filterSchema, mappingsToEntitiesConfig, reconcile, relationshipSchema, relationshipViaSignature, relationshipsSchema, runSync, validateEntitiesConfig };
@@ -0,0 +1,97 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ //#region src/mapping-score.ts
3
+ const CONCEPT_TIERS = {
4
+ high: 70,
5
+ medium: 40
6
+ };
7
+ const FIELD_TIERS = {
8
+ high: 70,
9
+ medium: 40
10
+ };
11
+ const RELATIONSHIP_TIERS = {
12
+ high: 60,
13
+ medium: 30
14
+ };
15
+ /** Two concept candidates closer than this are treated as ambiguous. */
16
+ const AMBIGUITY_MARGIN = 15;
17
+ /** Signal weights. Tuned against fixtures; adjust here, not at call sites. */
18
+ const W = {
19
+ nameExact: 50,
20
+ nameSubstring: 25,
21
+ definingColumn: 20,
22
+ hasPrimaryKey: 10,
23
+ hasRows: 10,
24
+ emptyTable: -20,
25
+ fieldNameMatch: 50,
26
+ typeCompatible: 30,
27
+ typeMismatch: -30,
28
+ requiredNotNull: 10,
29
+ fkResolved: 60,
30
+ fkNameHint: 30,
31
+ joinTable: 20
32
+ };
33
+ function tierFor(score, t) {
34
+ if (score >= t.high) return "High";
35
+ if (score >= t.medium) return "Medium";
36
+ return "Low";
37
+ }
38
+ const TEXT_TYPES = [
39
+ "text",
40
+ "varchar",
41
+ "char",
42
+ "citext",
43
+ "uuid",
44
+ "string"
45
+ ];
46
+ const NUMERIC_TYPES = [
47
+ "numeric",
48
+ "decimal",
49
+ "int",
50
+ "integer",
51
+ "bigint",
52
+ "smallint",
53
+ "float",
54
+ "double",
55
+ "real",
56
+ "money"
57
+ ];
58
+ const TIMESTAMP_TYPES = [
59
+ "timestamp",
60
+ "timestamptz",
61
+ "date",
62
+ "datetime"
63
+ ];
64
+ function matchesType(type, list) {
65
+ if (!type) return false;
66
+ const normalized = type.toLowerCase();
67
+ return list.some((t) => normalized.startsWith(t));
68
+ }
69
+ function isTextLike(type) {
70
+ return matchesType(type, TEXT_TYPES);
71
+ }
72
+ function isNumericLike(type) {
73
+ return matchesType(type, NUMERIC_TYPES);
74
+ }
75
+ function isTimestampLike(type) {
76
+ return matchesType(type, TIMESTAMP_TYPES);
77
+ }
78
+ /**
79
+ * True when the top two candidates are too close to call. Callers must never
80
+ * one-click apply an ambiguous result — the user picks.
81
+ */
82
+ function isAmbiguous(ranked) {
83
+ const [first, second] = ranked;
84
+ if (!(first && second)) return false;
85
+ return first.score - second.score < 15;
86
+ }
87
+ //#endregion
88
+ exports.AMBIGUITY_MARGIN = AMBIGUITY_MARGIN;
89
+ exports.CONCEPT_TIERS = CONCEPT_TIERS;
90
+ exports.FIELD_TIERS = FIELD_TIERS;
91
+ exports.RELATIONSHIP_TIERS = RELATIONSHIP_TIERS;
92
+ exports.W = W;
93
+ exports.isAmbiguous = isAmbiguous;
94
+ exports.isNumericLike = isNumericLike;
95
+ exports.isTextLike = isTextLike;
96
+ exports.isTimestampLike = isTimestampLike;
97
+ exports.tierFor = tierFor;
@@ -0,0 +1,49 @@
1
+ //#region src/mapping-score.d.ts
2
+ type Tier = "High" | "Medium" | "Low";
3
+ interface Scored<T> {
4
+ /** Human-readable chips, e.g. `type text`, `name matches "contacts"`. */
5
+ reasons: string[];
6
+ /** Internal — used for ranking and the apply threshold. Never rendered. */
7
+ score: number;
8
+ tier: Tier;
9
+ value: T;
10
+ }
11
+ /** Tier cut-offs: score >= high → High; >= medium → Medium; else Low. */
12
+ interface Thresholds {
13
+ high: number;
14
+ medium: number;
15
+ }
16
+ declare const CONCEPT_TIERS: Thresholds;
17
+ declare const FIELD_TIERS: Thresholds;
18
+ declare const RELATIONSHIP_TIERS: Thresholds;
19
+ /** Two concept candidates closer than this are treated as ambiguous. */
20
+ declare const AMBIGUITY_MARGIN = 15;
21
+ /** Signal weights. Tuned against fixtures; adjust here, not at call sites. */
22
+ declare const W: {
23
+ readonly nameExact: 50;
24
+ readonly nameSubstring: 25;
25
+ readonly definingColumn: 20;
26
+ readonly hasPrimaryKey: 10;
27
+ readonly hasRows: 10;
28
+ readonly emptyTable: -20;
29
+ readonly fieldNameMatch: 50;
30
+ readonly typeCompatible: 30;
31
+ readonly typeMismatch: -30;
32
+ readonly requiredNotNull: 10;
33
+ readonly fkResolved: 60;
34
+ readonly fkNameHint: 30;
35
+ readonly joinTable: 20;
36
+ };
37
+ declare function tierFor(score: number, t: Thresholds): Tier;
38
+ declare function isTextLike(type: string | undefined): boolean;
39
+ declare function isNumericLike(type: string | undefined): boolean;
40
+ declare function isTimestampLike(type: string | undefined): boolean;
41
+ /**
42
+ * True when the top two candidates are too close to call. Callers must never
43
+ * one-click apply an ambiguous result — the user picks.
44
+ */
45
+ declare function isAmbiguous(ranked: Array<{
46
+ score: number;
47
+ }>): boolean;
48
+ //#endregion
49
+ export { AMBIGUITY_MARGIN, CONCEPT_TIERS, FIELD_TIERS, RELATIONSHIP_TIERS, Scored, Thresholds, Tier, W, isAmbiguous, isNumericLike, isTextLike, isTimestampLike, tierFor };
@@ -0,0 +1,49 @@
1
+ //#region src/mapping-score.d.ts
2
+ type Tier = "High" | "Medium" | "Low";
3
+ interface Scored<T> {
4
+ /** Human-readable chips, e.g. `type text`, `name matches "contacts"`. */
5
+ reasons: string[];
6
+ /** Internal — used for ranking and the apply threshold. Never rendered. */
7
+ score: number;
8
+ tier: Tier;
9
+ value: T;
10
+ }
11
+ /** Tier cut-offs: score >= high → High; >= medium → Medium; else Low. */
12
+ interface Thresholds {
13
+ high: number;
14
+ medium: number;
15
+ }
16
+ declare const CONCEPT_TIERS: Thresholds;
17
+ declare const FIELD_TIERS: Thresholds;
18
+ declare const RELATIONSHIP_TIERS: Thresholds;
19
+ /** Two concept candidates closer than this are treated as ambiguous. */
20
+ declare const AMBIGUITY_MARGIN = 15;
21
+ /** Signal weights. Tuned against fixtures; adjust here, not at call sites. */
22
+ declare const W: {
23
+ readonly nameExact: 50;
24
+ readonly nameSubstring: 25;
25
+ readonly definingColumn: 20;
26
+ readonly hasPrimaryKey: 10;
27
+ readonly hasRows: 10;
28
+ readonly emptyTable: -20;
29
+ readonly fieldNameMatch: 50;
30
+ readonly typeCompatible: 30;
31
+ readonly typeMismatch: -30;
32
+ readonly requiredNotNull: 10;
33
+ readonly fkResolved: 60;
34
+ readonly fkNameHint: 30;
35
+ readonly joinTable: 20;
36
+ };
37
+ declare function tierFor(score: number, t: Thresholds): Tier;
38
+ declare function isTextLike(type: string | undefined): boolean;
39
+ declare function isNumericLike(type: string | undefined): boolean;
40
+ declare function isTimestampLike(type: string | undefined): boolean;
41
+ /**
42
+ * True when the top two candidates are too close to call. Callers must never
43
+ * one-click apply an ambiguous result — the user picks.
44
+ */
45
+ declare function isAmbiguous(ranked: Array<{
46
+ score: number;
47
+ }>): boolean;
48
+ //#endregion
49
+ export { AMBIGUITY_MARGIN, CONCEPT_TIERS, FIELD_TIERS, RELATIONSHIP_TIERS, Scored, Thresholds, Tier, W, isAmbiguous, isNumericLike, isTextLike, isTimestampLike, tierFor };