@clivly/core 0.8.0 → 0.9.1
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 +182 -16
- package/dist/drizzle.cjs +82 -1
- package/dist/drizzle.d.cts +23 -2
- package/dist/drizzle.d.mts +23 -2
- package/dist/drizzle.mjs +81 -2
- package/dist/entity-config-8Fgr6IOH.mjs +666 -0
- package/dist/entity-config-CLxI8dVb.d.cts +556 -0
- package/dist/entity-config-CLxI8dVb.d.mts +556 -0
- package/dist/entity-config-Dh8aEcgj.cjs +779 -0
- package/dist/entity-config.cjs +13 -349
- package/dist/entity-config.d.cts +2 -295
- package/dist/entity-config.d.mts +2 -295
- package/dist/entity-config.mjs +2 -339
- package/dist/entity-heuristics.cjs +28 -8
- package/dist/entity-heuristics.d.cts +2 -12
- package/dist/entity-heuristics.d.mts +2 -12
- package/dist/entity-heuristics.mjs +28 -8
- package/dist/env-file.cjs +59 -0
- package/dist/env-file.d.cts +23 -0
- package/dist/env-file.d.mts +23 -0
- package/dist/env-file.mjs +55 -0
- package/dist/index.cjs +83 -2
- package/dist/index.d.cts +72 -2
- package/dist/index.d.mts +72 -2
- package/dist/index.mjs +69 -3
- package/dist/integration-state.cjs +242 -0
- package/dist/integration-state.d.cts +88 -0
- package/dist/integration-state.d.mts +88 -0
- package/dist/integration-state.mjs +232 -0
- package/dist/mapping-form.d.cts +1 -1
- package/dist/mapping-form.d.mts +1 -1
- package/dist/mappings-config.cjs +32 -6
- package/dist/mappings-config.d.cts +7 -1
- package/dist/mappings-config.d.mts +7 -1
- package/dist/mappings-config.mjs +32 -6
- package/dist/projection-BXBkh7qp.cjs +98 -0
- package/dist/projection-C_EMRmZ8.mjs +75 -0
- package/dist/sync-engine.cjs +9 -2
- package/dist/sync-engine.d.cts +1 -1
- package/dist/sync-engine.d.mts +1 -1
- package/dist/sync-engine.mjs +9 -2
- package/dist/view-compiler-BpNI8Ttw.cjs +299 -0
- package/dist/view-compiler-CcqsGxv4.mjs +270 -0
- package/dist/view-compiler.cjs +6 -188
- package/dist/view-compiler.d.cts +12 -2
- package/dist/view-compiler.d.mts +12 -2
- package/dist/view-compiler.mjs +2 -185
- package/package.json +21 -1
package/dist/index.mjs
CHANGED
|
@@ -1,7 +1,73 @@
|
|
|
1
1
|
import { authCustom } from "./auth-adapter.mjs";
|
|
2
|
-
import {
|
|
2
|
+
import { i as projectionSchema, n as PROJECTION_KEY_PATTERN, r as identityRefs, t as BASE_REF } from "./projection-C_EMRmZ8.mjs";
|
|
3
|
+
import { _ as coversUniqueKey, a as MATERIALIZING_CONCEPTS, c as filterSchema, d as relationshipsSchema, f as validateEntitiesConfig, g as columnMetaFor, h as joinFansOut, i as ClivlyConfigError, m as fanOutJoins, n as CRM_CONCEPTS, o as defineClivlyConfig, p as validateProjection, s as entitiesConfigSchema, t as CANONICAL_FIELDS, u as relationshipSchema, v as hasRichMeta, y as primaryKeyColumns } from "./entity-config-8Fgr6IOH.mjs";
|
|
3
4
|
import { buildFilterFromDiscriminator, buildRelationships, relationshipViaSignature } from "./mapping-form.mjs";
|
|
4
5
|
import { mappingsToEntitiesConfig } from "./mappings-config.mjs";
|
|
5
|
-
import {
|
|
6
|
+
import { i as explainEntitiesConfig, n as compileEntityView, r as compileEntityViews, t as DEFAULT_VIEW_PREFIX } from "./view-compiler-CcqsGxv4.mjs";
|
|
6
7
|
import { reconcile, runSync } from "./sync-engine.mjs";
|
|
7
|
-
|
|
8
|
+
//#region src/projection-draft.ts
|
|
9
|
+
function emptyProjectionDraft(baseTable) {
|
|
10
|
+
return {
|
|
11
|
+
baseTable,
|
|
12
|
+
entityKey: "",
|
|
13
|
+
fieldBindings: {},
|
|
14
|
+
identity: { kind: "base_pk" },
|
|
15
|
+
joins: []
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* The ON conditions implied by a foreign key between the two tables, so the
|
|
20
|
+
* builder can pre-fill a join instead of making the user hand-pick columns.
|
|
21
|
+
* Returns [] when no FK relates them — the user then picks manually.
|
|
22
|
+
*/
|
|
23
|
+
function suggestJoinConditions(baseTable, joinTable, schema, joinKey) {
|
|
24
|
+
const join = schema.find((t) => t.name === joinTable);
|
|
25
|
+
const conditions = [];
|
|
26
|
+
for (const column of join?.columnsMeta ?? []) if (column.references?.table === baseTable) conditions.push({
|
|
27
|
+
left: {
|
|
28
|
+
table: joinKey,
|
|
29
|
+
column: column.name
|
|
30
|
+
},
|
|
31
|
+
op: "eq",
|
|
32
|
+
right: {
|
|
33
|
+
table: BASE_REF,
|
|
34
|
+
column: column.references.column
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
return conditions;
|
|
38
|
+
}
|
|
39
|
+
/** A draft becomes a projection once it has a valid key and ≥1 binding. */
|
|
40
|
+
function draftToProjection(draft) {
|
|
41
|
+
if (!PROJECTION_KEY_PATTERN.test(draft.entityKey)) return null;
|
|
42
|
+
if (Object.keys(draft.fieldBindings).length === 0) return null;
|
|
43
|
+
return {
|
|
44
|
+
version: 1,
|
|
45
|
+
kind: "projection",
|
|
46
|
+
entityKey: draft.entityKey,
|
|
47
|
+
baseTable: draft.baseTable,
|
|
48
|
+
joins: draft.joins,
|
|
49
|
+
fieldBindings: draft.fieldBindings,
|
|
50
|
+
identity: draft.identity
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
function draftErrors(draft, schema) {
|
|
54
|
+
const projection = draftToProjection(draft);
|
|
55
|
+
if (!projection) return [{
|
|
56
|
+
path: "projection",
|
|
57
|
+
message: "Name the object and map a field."
|
|
58
|
+
}];
|
|
59
|
+
return validateProjection(projection, schema, "projection");
|
|
60
|
+
}
|
|
61
|
+
/** Per-join fan-out, for the builder's inline cardinality hints. */
|
|
62
|
+
function describeCardinality(draft, schema) {
|
|
63
|
+
const projection = draftToProjection(draft);
|
|
64
|
+
if (!projection) return [];
|
|
65
|
+
const tablesByName = new Map(schema.map((t) => [t.name, t]));
|
|
66
|
+
const fanning = new Set(fanOutJoins(projection, tablesByName).map((j) => j.key));
|
|
67
|
+
return projection.joins.map((join) => ({
|
|
68
|
+
joinKey: join.key,
|
|
69
|
+
fansOut: fanning.has(join.key)
|
|
70
|
+
}));
|
|
71
|
+
}
|
|
72
|
+
//#endregion
|
|
73
|
+
export { BASE_REF, CANONICAL_FIELDS, CRM_CONCEPTS, ClivlyConfigError, DEFAULT_VIEW_PREFIX, MATERIALIZING_CONCEPTS, authCustom, buildFilterFromDiscriminator, buildRelationships, columnMetaFor, compileEntityView, compileEntityViews, coversUniqueKey, defineClivlyConfig, describeCardinality, draftErrors, draftToProjection, emptyProjectionDraft, entitiesConfigSchema, explainEntitiesConfig, fanOutJoins, filterSchema, hasRichMeta, identityRefs, joinFansOut, mappingsToEntitiesConfig, primaryKeyColumns, projectionSchema, reconcile, relationshipSchema, relationshipViaSignature, relationshipsSchema, runSync, suggestJoinConditions, validateEntitiesConfig, validateProjection };
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
//#region src/integration-state.ts
|
|
3
|
+
const INTEGRATION_STEPS = [
|
|
4
|
+
"credential",
|
|
5
|
+
"application",
|
|
6
|
+
"schema",
|
|
7
|
+
"mapping",
|
|
8
|
+
"trigger",
|
|
9
|
+
"sync",
|
|
10
|
+
"widget"
|
|
11
|
+
];
|
|
12
|
+
const INTEGRATION_STEP_LABELS = {
|
|
13
|
+
credential: "Secure credential",
|
|
14
|
+
application: "Application connected",
|
|
15
|
+
schema: "Schema discovered",
|
|
16
|
+
mapping: "Entities mapped",
|
|
17
|
+
trigger: "Clivly can start a sync",
|
|
18
|
+
sync: "First sync observed",
|
|
19
|
+
widget: "Chat widget installed"
|
|
20
|
+
};
|
|
21
|
+
/** Each step's prerequisites. Ordering is enforced by a test, not convention. */
|
|
22
|
+
const STEP_PREREQUISITES = {
|
|
23
|
+
credential: [],
|
|
24
|
+
application: ["credential"],
|
|
25
|
+
schema: ["application"],
|
|
26
|
+
mapping: ["schema"],
|
|
27
|
+
trigger: ["application"],
|
|
28
|
+
sync: ["mapping", "trigger"],
|
|
29
|
+
widget: ["application"]
|
|
30
|
+
};
|
|
31
|
+
function stepIndex(id) {
|
|
32
|
+
return INTEGRATION_STEPS.indexOf(id);
|
|
33
|
+
}
|
|
34
|
+
/** A heartbeat older than this means the app stopped checking in. */
|
|
35
|
+
const HEARTBEAT_STALE_MS = 600 * 1e3;
|
|
36
|
+
function formatAge(ms) {
|
|
37
|
+
if (ms < 6e4) return `${Math.max(1, Math.round(ms / 1e3))}s ago`;
|
|
38
|
+
if (ms < 36e5) return `${Math.round(ms / 6e4)}m ago`;
|
|
39
|
+
return `${Math.round(ms / 36e5)}h ago`;
|
|
40
|
+
}
|
|
41
|
+
function heartbeatAge(facts) {
|
|
42
|
+
return facts.lastHeartbeatAt === null ? null : facts.now - facts.lastHeartbeatAt;
|
|
43
|
+
}
|
|
44
|
+
/** Is the raw condition behind each step met, ignoring prerequisites? */
|
|
45
|
+
function isSatisfied(id, facts) {
|
|
46
|
+
const age = heartbeatAge(facts);
|
|
47
|
+
switch (id) {
|
|
48
|
+
case "credential": return facts.hasCredential;
|
|
49
|
+
case "application": return age !== null && age < HEARTBEAT_STALE_MS;
|
|
50
|
+
case "schema": return facts.discoveredTableCount > 0;
|
|
51
|
+
case "mapping": return facts.mappedEntityCount > 0;
|
|
52
|
+
case "trigger": return facts.triggerConfigured;
|
|
53
|
+
case "sync": return facts.lastRun?.ok === true;
|
|
54
|
+
default: return facts.widgetInstalled;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
function evidenceFor(id, facts) {
|
|
58
|
+
const age = heartbeatAge(facts);
|
|
59
|
+
if (id === "application" && age !== null) return `heartbeat ${formatAge(age)}`;
|
|
60
|
+
if (id === "schema" && facts.discoveredTableCount > 0) return `found ${facts.discoveredTableCount} tables`;
|
|
61
|
+
if (id === "mapping" && facts.mappedEntityCount > 0) return `${facts.mappedEntityCount} entity mapped`;
|
|
62
|
+
if (id === "trigger" && facts.triggerConfigured) return "trigger URL and secret set";
|
|
63
|
+
if (id === "sync" && facts.lastRun) return `last run ${formatAge(facts.now - facts.lastRun.at)}`;
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
function problemFor(id, facts) {
|
|
67
|
+
if (id === "sync" && facts.lastRun && !facts.lastRun.ok) return {
|
|
68
|
+
code: "sync_failed",
|
|
69
|
+
message: facts.lastRun.message ?? "The last sync run failed.",
|
|
70
|
+
fix: "Open the run log to see the host response."
|
|
71
|
+
};
|
|
72
|
+
if (id === "application") {
|
|
73
|
+
const age = heartbeatAge(facts);
|
|
74
|
+
if (age !== null && age >= HEARTBEAT_STALE_MS) return {
|
|
75
|
+
code: "heartbeat_stale",
|
|
76
|
+
message: `Your app stopped checking in ${formatAge(age)}.`,
|
|
77
|
+
fix: "Start your app, then run `npx clivly status`."
|
|
78
|
+
};
|
|
79
|
+
if (age === null && facts.everConnected) return {
|
|
80
|
+
code: "heartbeat_stale",
|
|
81
|
+
message: "Your application stopped checking in.",
|
|
82
|
+
fix: "Start your app, then run `npx clivly status`."
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Walk the ladder once. A step is `blocked` when an earlier step it needs has
|
|
89
|
+
* not been satisfied — a prerequisite statement, not a verdict about this step
|
|
90
|
+
* — and only the first unsatisfied step is `active`, so exactly one root cause
|
|
91
|
+
* ever surfaces.
|
|
92
|
+
*/
|
|
93
|
+
function resolveSteps(facts) {
|
|
94
|
+
const satisfied = /* @__PURE__ */ new Set();
|
|
95
|
+
const steps = [];
|
|
96
|
+
let activeTaken = false;
|
|
97
|
+
for (const id of INTEGRATION_STEPS) {
|
|
98
|
+
const unmet = STEP_PREREQUISITES[id].find((need) => !satisfied.has(need));
|
|
99
|
+
const label = INTEGRATION_STEP_LABELS[id];
|
|
100
|
+
if (isSatisfied(id, facts)) {
|
|
101
|
+
satisfied.add(id);
|
|
102
|
+
steps.push({
|
|
103
|
+
id,
|
|
104
|
+
label,
|
|
105
|
+
state: "done",
|
|
106
|
+
blockedBy: null,
|
|
107
|
+
evidence: evidenceFor(id, facts),
|
|
108
|
+
problem: null
|
|
109
|
+
});
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
if (unmet !== void 0) {
|
|
113
|
+
steps.push({
|
|
114
|
+
id,
|
|
115
|
+
label,
|
|
116
|
+
state: "blocked",
|
|
117
|
+
blockedBy: unmet,
|
|
118
|
+
evidence: null,
|
|
119
|
+
problem: null
|
|
120
|
+
});
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
const problem = problemFor(id, facts);
|
|
124
|
+
let state = "active";
|
|
125
|
+
if (problem) state = "failed";
|
|
126
|
+
else if (id === "widget") state = "skipped";
|
|
127
|
+
const claimsAttention = state === "active" || state === "failed";
|
|
128
|
+
steps.push({
|
|
129
|
+
id,
|
|
130
|
+
label,
|
|
131
|
+
state: activeTaken && state === "active" ? "blocked" : state,
|
|
132
|
+
blockedBy: null,
|
|
133
|
+
evidence: evidenceFor(id, facts),
|
|
134
|
+
problem: activeTaken ? null : problem
|
|
135
|
+
});
|
|
136
|
+
if (claimsAttention) activeTaken = true;
|
|
137
|
+
}
|
|
138
|
+
return steps;
|
|
139
|
+
}
|
|
140
|
+
const STEP_STATUS_WHEN_ACTIVE = {
|
|
141
|
+
credential: "not_configured",
|
|
142
|
+
application: "awaiting_connection",
|
|
143
|
+
schema: "connected",
|
|
144
|
+
mapping: "mapping_required",
|
|
145
|
+
trigger: "trigger_required",
|
|
146
|
+
sync: "ready_to_sync",
|
|
147
|
+
widget: "healthy"
|
|
148
|
+
};
|
|
149
|
+
/**
|
|
150
|
+
* Collapse the ladder to the single status the surfaces switch on. Live
|
|
151
|
+
* conditions — a run in flight, a failure, a heartbeat gone quiet — win over
|
|
152
|
+
* ladder position, because they describe the integration as it is now rather
|
|
153
|
+
* than how far setup once got.
|
|
154
|
+
*/
|
|
155
|
+
function deriveStatus(steps, facts) {
|
|
156
|
+
if (facts.syncInFlight) return "syncing";
|
|
157
|
+
if (steps.find((step) => step.id === "application")?.problem?.code === "heartbeat_stale") return "disconnected";
|
|
158
|
+
if (steps.some((step) => step.state === "failed")) return "error";
|
|
159
|
+
const blocking = steps.find((step) => step.state === "active" || step.state === "blocked");
|
|
160
|
+
if (blocking) return STEP_STATUS_WHEN_ACTIVE[blocking.id];
|
|
161
|
+
return facts.warnings.length > 0 ? "warning" : "healthy";
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* The command that installs Clivly into a host app. Exported because the connect
|
|
165
|
+
* dialog prints it too, and it must be the same string as the one the ladder
|
|
166
|
+
* recommends — that pairing is what C-05 broke.
|
|
167
|
+
*/
|
|
168
|
+
const INSTALL_COMMAND = "npx clivly init";
|
|
169
|
+
const NEXT_BY_STEP = {
|
|
170
|
+
credential: {
|
|
171
|
+
label: "Create a credential",
|
|
172
|
+
command: null,
|
|
173
|
+
envFile: null
|
|
174
|
+
},
|
|
175
|
+
application: {
|
|
176
|
+
label: "Install Clivly in your app",
|
|
177
|
+
command: INSTALL_COMMAND,
|
|
178
|
+
envFile: ".env.local"
|
|
179
|
+
},
|
|
180
|
+
schema: {
|
|
181
|
+
label: "Check what the SDK reported",
|
|
182
|
+
command: "npx clivly status",
|
|
183
|
+
envFile: null
|
|
184
|
+
},
|
|
185
|
+
mapping: {
|
|
186
|
+
label: "Choose the table holding your people",
|
|
187
|
+
command: null,
|
|
188
|
+
envFile: null
|
|
189
|
+
},
|
|
190
|
+
trigger: {
|
|
191
|
+
label: "Let Clivly start a sync",
|
|
192
|
+
command: null,
|
|
193
|
+
envFile: null
|
|
194
|
+
},
|
|
195
|
+
sync: {
|
|
196
|
+
label: "Run the first sync",
|
|
197
|
+
command: "npx clivly sync",
|
|
198
|
+
envFile: null
|
|
199
|
+
},
|
|
200
|
+
widget: {
|
|
201
|
+
label: "Install the chat widget",
|
|
202
|
+
command: null,
|
|
203
|
+
envFile: null
|
|
204
|
+
}
|
|
205
|
+
};
|
|
206
|
+
/**
|
|
207
|
+
* THE one producer of `next.command`. Six surfaces previously each invented
|
|
208
|
+
* their own "run this next" string, and one of them named a read-only command
|
|
209
|
+
* as the fix for a write problem (C-05). If you need a command for the
|
|
210
|
+
* next-action slot, call this — do not write another one.
|
|
211
|
+
*/
|
|
212
|
+
function nextAction(steps, status) {
|
|
213
|
+
if (status === "healthy" || status === "syncing") return null;
|
|
214
|
+
const blocking = steps.find((step) => step.state === "failed") ?? steps.find((step) => step.state === "active");
|
|
215
|
+
if (!blocking) return null;
|
|
216
|
+
if (blocking.problem?.code === "heartbeat_stale") return {
|
|
217
|
+
label: "Reconnect your app",
|
|
218
|
+
command: "npx clivly status",
|
|
219
|
+
envFile: null
|
|
220
|
+
};
|
|
221
|
+
return NEXT_BY_STEP[blocking.id];
|
|
222
|
+
}
|
|
223
|
+
function computeIntegrationState(facts) {
|
|
224
|
+
const steps = resolveSteps(facts);
|
|
225
|
+
const status = deriveStatus(steps, facts);
|
|
226
|
+
return {
|
|
227
|
+
status,
|
|
228
|
+
steps,
|
|
229
|
+
next: nextAction(steps, status)
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
//#endregion
|
|
233
|
+
exports.INSTALL_COMMAND = INSTALL_COMMAND;
|
|
234
|
+
exports.INTEGRATION_STEPS = INTEGRATION_STEPS;
|
|
235
|
+
exports.INTEGRATION_STEP_LABELS = INTEGRATION_STEP_LABELS;
|
|
236
|
+
exports.STEP_PREREQUISITES = STEP_PREREQUISITES;
|
|
237
|
+
exports.computeIntegrationState = computeIntegrationState;
|
|
238
|
+
exports.deriveStatus = deriveStatus;
|
|
239
|
+
exports.formatAge = formatAge;
|
|
240
|
+
exports.nextAction = nextAction;
|
|
241
|
+
exports.resolveSteps = resolveSteps;
|
|
242
|
+
exports.stepIndex = stepIndex;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
//#region src/integration-state.d.ts
|
|
2
|
+
declare const INTEGRATION_STEPS: readonly ["credential", "application", "schema", "mapping", "trigger", "sync", "widget"];
|
|
3
|
+
type StepId = (typeof INTEGRATION_STEPS)[number];
|
|
4
|
+
/** `blocked` means "prerequisite unmet", never "broken" — see C-19. */
|
|
5
|
+
type StepState = "done" | "active" | "blocked" | "failed" | "skipped";
|
|
6
|
+
type IntegrationStatus = "not_configured" | "sdk_installed" | "awaiting_connection" | "connected" | "schema_discovered" | "mapping_required" | "trigger_required" | "ready_to_sync" | "syncing" | "healthy" | "warning" | "error" | "disconnected";
|
|
7
|
+
declare const INTEGRATION_STEP_LABELS: Record<StepId, string>;
|
|
8
|
+
/** Each step's prerequisites. Ordering is enforced by a test, not convention. */
|
|
9
|
+
declare const STEP_PREREQUISITES: Record<StepId, readonly StepId[]>;
|
|
10
|
+
declare function stepIndex(id: StepId): number;
|
|
11
|
+
interface RunFact {
|
|
12
|
+
at: number;
|
|
13
|
+
message?: string;
|
|
14
|
+
ok: boolean;
|
|
15
|
+
}
|
|
16
|
+
interface IntegrationFacts {
|
|
17
|
+
discoveredTableCount: number;
|
|
18
|
+
/**
|
|
19
|
+
* Has this app ever checked in? Distinguishes "never installed" from "was
|
|
20
|
+
* working, now silent" — the same absent heartbeat means different things,
|
|
21
|
+
* and only one of them is diagnosed rather than set up.
|
|
22
|
+
*/
|
|
23
|
+
everConnected: boolean;
|
|
24
|
+
hasCredential: boolean;
|
|
25
|
+
lastHeartbeatAt: number | null;
|
|
26
|
+
lastRun: RunFact | null;
|
|
27
|
+
mappedEntityCount: number;
|
|
28
|
+
now: number;
|
|
29
|
+
syncInFlight: boolean;
|
|
30
|
+
/** A trigger URL and secret both exist, so Clivly can ask the app to sync. */
|
|
31
|
+
triggerConfigured: boolean;
|
|
32
|
+
warnings: string[];
|
|
33
|
+
widgetInstalled: boolean;
|
|
34
|
+
}
|
|
35
|
+
interface StepProblem {
|
|
36
|
+
code: string;
|
|
37
|
+
fix: string;
|
|
38
|
+
message: string;
|
|
39
|
+
}
|
|
40
|
+
interface IntegrationStep {
|
|
41
|
+
blockedBy: StepId | null;
|
|
42
|
+
evidence: string | null;
|
|
43
|
+
id: StepId;
|
|
44
|
+
label: string;
|
|
45
|
+
problem: StepProblem | null;
|
|
46
|
+
state: StepState;
|
|
47
|
+
}
|
|
48
|
+
declare function formatAge(ms: number): string;
|
|
49
|
+
/**
|
|
50
|
+
* Walk the ladder once. A step is `blocked` when an earlier step it needs has
|
|
51
|
+
* not been satisfied — a prerequisite statement, not a verdict about this step
|
|
52
|
+
* — and only the first unsatisfied step is `active`, so exactly one root cause
|
|
53
|
+
* ever surfaces.
|
|
54
|
+
*/
|
|
55
|
+
declare function resolveSteps(facts: IntegrationFacts): IntegrationStep[];
|
|
56
|
+
/**
|
|
57
|
+
* Collapse the ladder to the single status the surfaces switch on. Live
|
|
58
|
+
* conditions — a run in flight, a failure, a heartbeat gone quiet — win over
|
|
59
|
+
* ladder position, because they describe the integration as it is now rather
|
|
60
|
+
* than how far setup once got.
|
|
61
|
+
*/
|
|
62
|
+
declare function deriveStatus(steps: IntegrationStep[], facts: IntegrationFacts): IntegrationStatus;
|
|
63
|
+
interface NextAction {
|
|
64
|
+
command: string | null;
|
|
65
|
+
envFile: string | null;
|
|
66
|
+
label: string;
|
|
67
|
+
}
|
|
68
|
+
interface IntegrationState {
|
|
69
|
+
next: NextAction | null;
|
|
70
|
+
status: IntegrationStatus;
|
|
71
|
+
steps: IntegrationStep[];
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* The command that installs Clivly into a host app. Exported because the connect
|
|
75
|
+
* dialog prints it too, and it must be the same string as the one the ladder
|
|
76
|
+
* recommends — that pairing is what C-05 broke.
|
|
77
|
+
*/
|
|
78
|
+
declare const INSTALL_COMMAND = "npx clivly init";
|
|
79
|
+
/**
|
|
80
|
+
* THE one producer of `next.command`. Six surfaces previously each invented
|
|
81
|
+
* their own "run this next" string, and one of them named a read-only command
|
|
82
|
+
* as the fix for a write problem (C-05). If you need a command for the
|
|
83
|
+
* next-action slot, call this — do not write another one.
|
|
84
|
+
*/
|
|
85
|
+
declare function nextAction(steps: IntegrationStep[], status: IntegrationStatus): NextAction | null;
|
|
86
|
+
declare function computeIntegrationState(facts: IntegrationFacts): IntegrationState;
|
|
87
|
+
//#endregion
|
|
88
|
+
export { INSTALL_COMMAND, INTEGRATION_STEPS, INTEGRATION_STEP_LABELS, IntegrationFacts, IntegrationState, IntegrationStatus, IntegrationStep, NextAction, RunFact, STEP_PREREQUISITES, StepId, StepProblem, StepState, computeIntegrationState, deriveStatus, formatAge, nextAction, resolveSteps, stepIndex };
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
//#region src/integration-state.d.ts
|
|
2
|
+
declare const INTEGRATION_STEPS: readonly ["credential", "application", "schema", "mapping", "trigger", "sync", "widget"];
|
|
3
|
+
type StepId = (typeof INTEGRATION_STEPS)[number];
|
|
4
|
+
/** `blocked` means "prerequisite unmet", never "broken" — see C-19. */
|
|
5
|
+
type StepState = "done" | "active" | "blocked" | "failed" | "skipped";
|
|
6
|
+
type IntegrationStatus = "not_configured" | "sdk_installed" | "awaiting_connection" | "connected" | "schema_discovered" | "mapping_required" | "trigger_required" | "ready_to_sync" | "syncing" | "healthy" | "warning" | "error" | "disconnected";
|
|
7
|
+
declare const INTEGRATION_STEP_LABELS: Record<StepId, string>;
|
|
8
|
+
/** Each step's prerequisites. Ordering is enforced by a test, not convention. */
|
|
9
|
+
declare const STEP_PREREQUISITES: Record<StepId, readonly StepId[]>;
|
|
10
|
+
declare function stepIndex(id: StepId): number;
|
|
11
|
+
interface RunFact {
|
|
12
|
+
at: number;
|
|
13
|
+
message?: string;
|
|
14
|
+
ok: boolean;
|
|
15
|
+
}
|
|
16
|
+
interface IntegrationFacts {
|
|
17
|
+
discoveredTableCount: number;
|
|
18
|
+
/**
|
|
19
|
+
* Has this app ever checked in? Distinguishes "never installed" from "was
|
|
20
|
+
* working, now silent" — the same absent heartbeat means different things,
|
|
21
|
+
* and only one of them is diagnosed rather than set up.
|
|
22
|
+
*/
|
|
23
|
+
everConnected: boolean;
|
|
24
|
+
hasCredential: boolean;
|
|
25
|
+
lastHeartbeatAt: number | null;
|
|
26
|
+
lastRun: RunFact | null;
|
|
27
|
+
mappedEntityCount: number;
|
|
28
|
+
now: number;
|
|
29
|
+
syncInFlight: boolean;
|
|
30
|
+
/** A trigger URL and secret both exist, so Clivly can ask the app to sync. */
|
|
31
|
+
triggerConfigured: boolean;
|
|
32
|
+
warnings: string[];
|
|
33
|
+
widgetInstalled: boolean;
|
|
34
|
+
}
|
|
35
|
+
interface StepProblem {
|
|
36
|
+
code: string;
|
|
37
|
+
fix: string;
|
|
38
|
+
message: string;
|
|
39
|
+
}
|
|
40
|
+
interface IntegrationStep {
|
|
41
|
+
blockedBy: StepId | null;
|
|
42
|
+
evidence: string | null;
|
|
43
|
+
id: StepId;
|
|
44
|
+
label: string;
|
|
45
|
+
problem: StepProblem | null;
|
|
46
|
+
state: StepState;
|
|
47
|
+
}
|
|
48
|
+
declare function formatAge(ms: number): string;
|
|
49
|
+
/**
|
|
50
|
+
* Walk the ladder once. A step is `blocked` when an earlier step it needs has
|
|
51
|
+
* not been satisfied — a prerequisite statement, not a verdict about this step
|
|
52
|
+
* — and only the first unsatisfied step is `active`, so exactly one root cause
|
|
53
|
+
* ever surfaces.
|
|
54
|
+
*/
|
|
55
|
+
declare function resolveSteps(facts: IntegrationFacts): IntegrationStep[];
|
|
56
|
+
/**
|
|
57
|
+
* Collapse the ladder to the single status the surfaces switch on. Live
|
|
58
|
+
* conditions — a run in flight, a failure, a heartbeat gone quiet — win over
|
|
59
|
+
* ladder position, because they describe the integration as it is now rather
|
|
60
|
+
* than how far setup once got.
|
|
61
|
+
*/
|
|
62
|
+
declare function deriveStatus(steps: IntegrationStep[], facts: IntegrationFacts): IntegrationStatus;
|
|
63
|
+
interface NextAction {
|
|
64
|
+
command: string | null;
|
|
65
|
+
envFile: string | null;
|
|
66
|
+
label: string;
|
|
67
|
+
}
|
|
68
|
+
interface IntegrationState {
|
|
69
|
+
next: NextAction | null;
|
|
70
|
+
status: IntegrationStatus;
|
|
71
|
+
steps: IntegrationStep[];
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* The command that installs Clivly into a host app. Exported because the connect
|
|
75
|
+
* dialog prints it too, and it must be the same string as the one the ladder
|
|
76
|
+
* recommends — that pairing is what C-05 broke.
|
|
77
|
+
*/
|
|
78
|
+
declare const INSTALL_COMMAND = "npx clivly init";
|
|
79
|
+
/**
|
|
80
|
+
* THE one producer of `next.command`. Six surfaces previously each invented
|
|
81
|
+
* their own "run this next" string, and one of them named a read-only command
|
|
82
|
+
* as the fix for a write problem (C-05). If you need a command for the
|
|
83
|
+
* next-action slot, call this — do not write another one.
|
|
84
|
+
*/
|
|
85
|
+
declare function nextAction(steps: IntegrationStep[], status: IntegrationStatus): NextAction | null;
|
|
86
|
+
declare function computeIntegrationState(facts: IntegrationFacts): IntegrationState;
|
|
87
|
+
//#endregion
|
|
88
|
+
export { INSTALL_COMMAND, INTEGRATION_STEPS, INTEGRATION_STEP_LABELS, IntegrationFacts, IntegrationState, IntegrationStatus, IntegrationStep, NextAction, RunFact, STEP_PREREQUISITES, StepId, StepProblem, StepState, computeIntegrationState, deriveStatus, formatAge, nextAction, resolveSteps, stepIndex };
|