@oxygen-agent/cli 1.717.11 → 1.739.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.
Files changed (44) hide show
  1. package/README.md +1 -1
  2. package/dist/command-manifest.js +27 -12
  3. package/dist/index.js +243 -72
  4. package/dist/skills.js +2 -2
  5. package/node_modules/@oxygen/shared/dist/axiom-field-budget.d.ts +99 -0
  6. package/node_modules/@oxygen/shared/dist/axiom-field-budget.js +112 -17
  7. package/node_modules/@oxygen/shared/dist/cli-result.js +2 -0
  8. package/node_modules/@oxygen/shared/dist/crm-activity-events.d.ts +20 -0
  9. package/node_modules/@oxygen/shared/dist/crm-activity-events.js +5 -0
  10. package/node_modules/@oxygen/shared/dist/future-signup-events.d.ts +27 -0
  11. package/node_modules/@oxygen/shared/dist/future-signup-events.js +90 -0
  12. package/node_modules/@oxygen/shared/dist/future-signup-lifecycle-projection.d.ts +320 -0
  13. package/node_modules/@oxygen/shared/dist/future-signup-lifecycle-projection.js +1233 -0
  14. package/node_modules/@oxygen/shared/dist/index.d.ts +7 -1
  15. package/node_modules/@oxygen/shared/dist/index.js +14 -1
  16. package/node_modules/@oxygen/shared/dist/notetaker-events.d.ts +4 -0
  17. package/node_modules/@oxygen/shared/dist/notetaker-events.js +23 -0
  18. package/node_modules/@oxygen/shared/dist/plain-support-events.d.ts +40 -0
  19. package/node_modules/@oxygen/shared/dist/plain-support-events.js +43 -0
  20. package/node_modules/@oxygen/shared/dist/redaction.js +6 -1
  21. package/node_modules/@oxygen/shared/dist/user-capability-routing.d.ts +2 -0
  22. package/node_modules/@oxygen/shared/dist/user-capability-routing.js +23 -0
  23. package/node_modules/@oxygen/shared/dist/version.d.ts +1 -1
  24. package/node_modules/@oxygen/shared/dist/version.js +1 -1
  25. package/node_modules/@oxygen/shared/dist/webhook-headers.d.ts +10 -0
  26. package/node_modules/@oxygen/shared/dist/webhook-headers.js +48 -0
  27. package/node_modules/@oxygen/shared/dist/workspace-event-catalog.d.ts +31 -1
  28. package/node_modules/@oxygen/shared/dist/workspace-event-catalog.js +112 -1
  29. package/node_modules/@oxygen/workflows/dist/graph/expression.js +7 -5
  30. package/node_modules/@oxygen/workflows/dist/graph/lint.d.ts +9 -0
  31. package/node_modules/@oxygen/workflows/dist/graph/lint.js +409 -7
  32. package/node_modules/@oxygen/workflows/dist/graph/manifest-schema.d.ts +513 -0
  33. package/node_modules/@oxygen/workflows/dist/graph/manifest-schema.js +104 -1
  34. package/node_modules/@oxygen/workflows/dist/graph/params.d.ts +2 -0
  35. package/node_modules/@oxygen/workflows/dist/graph/params.js +15 -0
  36. package/node_modules/@oxygen/workflows/dist/graph/remap.js +12 -3
  37. package/node_modules/@oxygen/workflows/dist/graph/topology.d.ts +9 -0
  38. package/node_modules/@oxygen/workflows/dist/graph/topology.js +33 -1
  39. package/node_modules/@oxygen/workflows/dist/graph/types.d.ts +123 -16
  40. package/node_modules/@oxygen/workflows/dist/graph/types.js +45 -0
  41. package/node_modules/@oxygen/workflows/dist/index.d.ts +94 -2
  42. package/node_modules/@oxygen/workflows/dist/index.js +169 -26
  43. package/node_modules/@oxygen/workflows/dist/portable.js +3 -1
  44. package/package.json +1 -1
package/dist/skills.js CHANGED
@@ -2,7 +2,7 @@ import { spawnSync } from "node:child_process";
2
2
  import { existsSync, mkdirSync, mkdtempSync, readdirSync, rmSync, writeFileSync } from "node:fs";
3
3
  import { homedir, tmpdir } from "node:os";
4
4
  import { dirname, join, resolve } from "node:path";
5
- import { OxygenError, inferCapabilityRoute, serializeCapabilityRoute, toFailure, } from "@oxygen/shared";
5
+ import { OxygenError, inferUserCapabilityRoute, serializeCapabilityRoute, toFailure, } from "@oxygen/shared";
6
6
  import { defaultApiUrl, loadCredentials, normalizeApiUrl } from "./credentials.js";
7
7
  import { requestOxygen } from "./http-client.js";
8
8
  import { isRecord, readOption } from "./util.js";
@@ -98,7 +98,7 @@ export async function searchAgentSkills(query, options, runtime = {}) {
98
98
  }
99
99
  const normalized = query.trim();
100
100
  const tokens = tokenizeSkillQuery(normalized);
101
- const route = inferCapabilityRoute(normalized);
101
+ const route = inferUserCapabilityRoute(normalized);
102
102
  const requestedLimit = Number.parseInt(readOption(options.limit) ?? "5", 10);
103
103
  const limit = Number.isFinite(requestedLimit) ? Math.min(10, Math.max(1, requestedLimit)) : 5;
104
104
  const matches = index.skills
@@ -19,6 +19,13 @@
19
19
  * under `worker_fields`. New instrumentation can then be added freely without
20
20
  * anyone having to remember that a new key can take down log shipping globally.
21
21
  *
22
+ * WITH ONE HOLE, found the hard way on 2026-08-15. Packing protects the dataset
23
+ * from the CONTENTS, not from the CONTAINER: `worker_fields` is itself a column,
24
+ * and a dataset with no free column cannot have it. Registering the map field on
25
+ * the already-full `oxygen-logs` therefore turned a slow leak into a total ingest
26
+ * outage — 41,372 records/day, ~60x worse than the leak it replaced. `AxiomOverflowMode`
27
+ * below is the escape hatch; freeing a column slot is the actual fix.
28
+ *
22
29
  * THE ALLOWLIST IS A LOAD-BEARING CONTRACT IN BOTH DIRECTIONS.
23
30
  *
24
31
  * - A field that a monitor, dashboard, or checked-in query reads **flat** must be
@@ -34,6 +41,39 @@
34
41
  * Both directions are asserted by `scripts/ci/log-field-budget.mjs` against the
35
42
  * real seed and dashboard JSON, so a change to either side fails loudly instead
36
43
  * of quietly blinding a monitor.
44
+ *
45
+ * THERE IS A THIRD DIRECTION, and it is the one that took production down: a
46
+ * name in this set that the DATASET has no column for. Allowlisted means flat by
47
+ * definition, so such a name cannot fall back into the map — it demands a
48
+ * brand-new column, and a dataset with no slot to allocate one rejects the whole
49
+ * ingest batch with HTTP 400. So membership here is not "a field we read flat";
50
+ * it is "a field we read flat AND the dataset can actually hold". The live half
51
+ * of `scripts/ci/log-field-budget.mjs` checks that second half against Axiom.
52
+ *
53
+ * REMOVED 2026-08-15 — `chunk_id`, `item_id`, `digest`. Prod `oxygen-logs` had no
54
+ * column for any of the three and no slot left to create one, so every record
55
+ * carrying one was rejected 400 and DROPPED. That was still ~116 ship failures
56
+ * per 25 minutes after the `worker_fields` map field itself had been repaired on
57
+ * the Axiom side, and it hit the Next.js server-error path hardest, through
58
+ * `digest`.
59
+ *
60
+ * The warning above — that dropping a name silently moves it into the overflow
61
+ * and can zero a query that reads it flat — applies to exactly this removal, so
62
+ * it was CHECKED rather than assumed, in both directions:
63
+ *
64
+ * - Nothing reads them FLAT. Every `aplQuery` in docs/observability/monitors.json
65
+ * was parsed and all dashboard JSON grepped: zero matches for the three as flat
66
+ * fields (the apparent hits are the English word "digest" in description prose).
67
+ * - Nothing reads them out of `worker_fields` either, so the move breaks no query
68
+ * in the opposite direction.
69
+ *
70
+ * Packed, they stay queryable as `['worker_fields']['digest']` — a longer access
71
+ * path, the same value, still typed. `digest` matters most: it is the only join
72
+ * key from a browser error report back to its `next.request_error` row (see the
73
+ * error-shape group below), and that correlation still works through the map
74
+ * field. The trade is explicit and worth stating plainly: keeping them flat meant
75
+ * the ENTIRE record was dropped, which loses the digest anyway plus everything
76
+ * travelling with it. Packed is strictly better than dropped.
37
77
  */
38
78
  /**
39
79
  * Fields that stay top-level in `oxygen-logs`.
@@ -64,8 +104,67 @@ export declare const AXIOM_STABLE_FIELDS: ReadonlySet<string>;
64
104
  * ordinary flat fields and the budget is defeated silently. Create it with:
65
105
  * POST /v2/datasets/{dataset}/mapfields {"name": "worker_fields"}
66
106
  * `scripts/ops/axiom-mapfields.mjs` does this idempotently for every log dataset.
107
+ *
108
+ * AND IT MUST BE ABLE TO EXIST — see `AxiomOverflowMode` below. A map field still
109
+ * occupies ONE column slot, so registering it on an already-full dataset does not
110
+ * create it; Axiom rejects every ingest that carries it instead.
67
111
  */
68
112
  export declare const AXIOM_OVERFLOW_MAP_FIELD = "worker_fields";
113
+ /**
114
+ * How overflow is attached to a record.
115
+ *
116
+ * `map` is correct and is the default. `message` exists because of a failure mode
117
+ * the map-field design did not anticipate, measured in production 2026-08-15:
118
+ *
119
+ * {"code":400,"message":"adding 'worker_fields' to dataset fields would
120
+ * exceed the column limit of 1025"}
121
+ *
122
+ * `oxygen-logs` was at 1,026 columns. A map field's CONTENTS are free, but the map
123
+ * field itself is still a column, and there was no slot for it. So Axiom refused to
124
+ * create it and rejected every batch carrying it — 19,029 rejected batches and
125
+ * 41,372 lost records in 24h, roughly 60x the 412-batches-per-7-days leak the map
126
+ * field was introduced to fix. Registering the map field on a full dataset had
127
+ * converted "silently absorb overflow as new columns" into "drop everything".
128
+ *
129
+ * The degraded mode restores the shape this module shipped BEFORE the map field:
130
+ * a JSON string in `message` under the same `worker_fields` key, so
131
+ * `parse_json(tostring(message)).worker_fields.x` — the access path every
132
+ * pre-2026-08-08 row and every legacy query already uses — keeps working.
133
+ *
134
+ * Deliberately process-level and one-way. A shipper flips it on the first
135
+ * column-limit rejection and every later record in that process packs the same
136
+ * way; nothing flips it back, because a dataset does not grow a free column while
137
+ * the process is running. Fresh processes start in `map` again, so the moment the
138
+ * dataset has headroom the next deploy silently returns to the better shape with
139
+ * no code change.
140
+ *
141
+ * IT IS A LAST RESORT, NOT A HOME. While degraded, every monitor reading
142
+ * `['worker_fields']['x']` resolves to null — the silent-blinding failure this
143
+ * module's header warns about. Readers that must survive both shapes coalesce
144
+ * them; see `docs/observability/queries.apl.md`. Losing the flat shape beats
145
+ * losing the record.
146
+ */
147
+ export type AxiomOverflowMode = "map" | "message";
148
+ /** The mode the next `packAxiomLogRecord` call will use. */
149
+ export declare function getAxiomOverflowMode(): AxiomOverflowMode;
150
+ /**
151
+ * Does this Axiom 400 body mean "the map field will not fit"?
152
+ *
153
+ * Matched on the stable half of Axiom's message rather than the whole string: the
154
+ * field name and the limit number both vary, and a shipper that only recognised
155
+ * `worker_fields`/`1025` verbatim would go back to dropping everything the day
156
+ * either changed.
157
+ */
158
+ export declare function isColumnLimitRejection(body: string | null | undefined): boolean;
159
+ /**
160
+ * Switches this process to `message` packing.
161
+ *
162
+ * Returns true only on the transition, so a caller can retry the rejected batch
163
+ * and log the degradation exactly once instead of once per batch forever.
164
+ */
165
+ export declare function degradeAxiomOverflowToMessage(): boolean;
166
+ /** Test-only: restores the default so cases cannot leak mode into each other. */
167
+ export declare function resetAxiomOverflowMode(): void;
69
168
  /**
70
169
  * Bound on the serialized overflow, so one pathological record cannot push a whole
71
170
  * batch past Axiom's request limits. 64 KB is far above any real log line and far
@@ -19,6 +19,13 @@
19
19
  * under `worker_fields`. New instrumentation can then be added freely without
20
20
  * anyone having to remember that a new key can take down log shipping globally.
21
21
  *
22
+ * WITH ONE HOLE, found the hard way on 2026-08-15. Packing protects the dataset
23
+ * from the CONTENTS, not from the CONTAINER: `worker_fields` is itself a column,
24
+ * and a dataset with no free column cannot have it. Registering the map field on
25
+ * the already-full `oxygen-logs` therefore turned a slow leak into a total ingest
26
+ * outage — 41,372 records/day, ~60x worse than the leak it replaced. `AxiomOverflowMode`
27
+ * below is the escape hatch; freeing a column slot is the actual fix.
28
+ *
22
29
  * THE ALLOWLIST IS A LOAD-BEARING CONTRACT IN BOTH DIRECTIONS.
23
30
  *
24
31
  * - A field that a monitor, dashboard, or checked-in query reads **flat** must be
@@ -34,6 +41,39 @@
34
41
  * Both directions are asserted by `scripts/ci/log-field-budget.mjs` against the
35
42
  * real seed and dashboard JSON, so a change to either side fails loudly instead
36
43
  * of quietly blinding a monitor.
44
+ *
45
+ * THERE IS A THIRD DIRECTION, and it is the one that took production down: a
46
+ * name in this set that the DATASET has no column for. Allowlisted means flat by
47
+ * definition, so such a name cannot fall back into the map — it demands a
48
+ * brand-new column, and a dataset with no slot to allocate one rejects the whole
49
+ * ingest batch with HTTP 400. So membership here is not "a field we read flat";
50
+ * it is "a field we read flat AND the dataset can actually hold". The live half
51
+ * of `scripts/ci/log-field-budget.mjs` checks that second half against Axiom.
52
+ *
53
+ * REMOVED 2026-08-15 — `chunk_id`, `item_id`, `digest`. Prod `oxygen-logs` had no
54
+ * column for any of the three and no slot left to create one, so every record
55
+ * carrying one was rejected 400 and DROPPED. That was still ~116 ship failures
56
+ * per 25 minutes after the `worker_fields` map field itself had been repaired on
57
+ * the Axiom side, and it hit the Next.js server-error path hardest, through
58
+ * `digest`.
59
+ *
60
+ * The warning above — that dropping a name silently moves it into the overflow
61
+ * and can zero a query that reads it flat — applies to exactly this removal, so
62
+ * it was CHECKED rather than assumed, in both directions:
63
+ *
64
+ * - Nothing reads them FLAT. Every `aplQuery` in docs/observability/monitors.json
65
+ * was parsed and all dashboard JSON grepped: zero matches for the three as flat
66
+ * fields (the apparent hits are the English word "digest" in description prose).
67
+ * - Nothing reads them out of `worker_fields` either, so the move breaks no query
68
+ * in the opposite direction.
69
+ *
70
+ * Packed, they stay queryable as `['worker_fields']['digest']` — a longer access
71
+ * path, the same value, still typed. `digest` matters most: it is the only join
72
+ * key from a browser error report back to its `next.request_error` row (see the
73
+ * error-shape group below), and that correlation still works through the map
74
+ * field. The trade is explicit and worth stating plainly: keeping them flat meant
75
+ * the ENTIRE record was dropped, which loses the digest anyway plus everything
76
+ * travelling with it. Packed is strictly better than dropped.
37
77
  */
38
78
  /**
39
79
  * Fields that stay top-level in `oxygen-logs`.
@@ -66,8 +106,9 @@ export const AXIOM_STABLE_FIELDS = new Set([
66
106
  "operation",
67
107
  "tenant_database_id",
68
108
  "run_id",
69
- "chunk_id",
70
- "item_id",
109
+ // `chunk_id` and `item_id` used to sit here. Removed 2026-08-15 because prod
110
+ // had no column for either and no slot to make one — see the header. They now
111
+ // ride in `worker_fields`, where they cost no column at all.
71
112
  "worker_id",
72
113
  "conversation_id",
73
114
  // --- Error shape (errorFields) -------------------------------------------
@@ -75,9 +116,11 @@ export const AXIOM_STABLE_FIELDS = new Set([
75
116
  "error_name",
76
117
  "error_message",
77
118
  "error_code",
78
- // Next.js stamps `digest` on a serialized server error; it is the only value
79
- // that joins a browser error report back to its `next.request_error` row.
80
- "digest",
119
+ // `digest` used to sit here. Next.js stamps it on a serialized server error and
120
+ // it is still the only value that joins a browser error report back to its
121
+ // `next.request_error` row — but prod could not allocate a column for it, so a
122
+ // flat `digest` meant the whole record died and the join key with it. Removed
123
+ // 2026-08-15; the join now reads `tostring(['worker_fields']['digest'])`.
81
124
  // --- Primitive identity --------------------------------------------------
82
125
  "workflow_id",
83
126
  "workflow_run_id",
@@ -195,8 +238,51 @@ export const AXIOM_STABLE_FIELDS = new Set([
195
238
  * ordinary flat fields and the budget is defeated silently. Create it with:
196
239
  * POST /v2/datasets/{dataset}/mapfields {"name": "worker_fields"}
197
240
  * `scripts/ops/axiom-mapfields.mjs` does this idempotently for every log dataset.
241
+ *
242
+ * AND IT MUST BE ABLE TO EXIST — see `AxiomOverflowMode` below. A map field still
243
+ * occupies ONE column slot, so registering it on an already-full dataset does not
244
+ * create it; Axiom rejects every ingest that carries it instead.
198
245
  */
199
246
  export const AXIOM_OVERFLOW_MAP_FIELD = "worker_fields";
247
+ /**
248
+ * The pre-existing column the overflow degrades into when the map field cannot be
249
+ * created. Owned by the Vercel drain (it is where Lane B parks the whole app JSON),
250
+ * which is exactly why it is safe here: it already exists on every log dataset, so
251
+ * writing it costs no new column on a dataset that has none left to give.
252
+ */
253
+ const AXIOM_OVERFLOW_MESSAGE_FIELD = "message";
254
+ let overflowMode = "map";
255
+ /** The mode the next `packAxiomLogRecord` call will use. */
256
+ export function getAxiomOverflowMode() {
257
+ return overflowMode;
258
+ }
259
+ /**
260
+ * Does this Axiom 400 body mean "the map field will not fit"?
261
+ *
262
+ * Matched on the stable half of Axiom's message rather than the whole string: the
263
+ * field name and the limit number both vary, and a shipper that only recognised
264
+ * `worker_fields`/`1025` verbatim would go back to dropping everything the day
265
+ * either changed.
266
+ */
267
+ export function isColumnLimitRejection(body) {
268
+ return typeof body === "string" && /would exceed the column limit/i.test(body);
269
+ }
270
+ /**
271
+ * Switches this process to `message` packing.
272
+ *
273
+ * Returns true only on the transition, so a caller can retry the rejected batch
274
+ * and log the degradation exactly once instead of once per batch forever.
275
+ */
276
+ export function degradeAxiomOverflowToMessage() {
277
+ if (overflowMode === "message")
278
+ return false;
279
+ overflowMode = "message";
280
+ return true;
281
+ }
282
+ /** Test-only: restores the default so cases cannot leak mode into each other. */
283
+ export function resetAxiomOverflowMode() {
284
+ overflowMode = "map";
285
+ }
200
286
  /**
201
287
  * Bound on the serialized overflow, so one pathological record cannot push a whole
202
288
  * batch past Axiom's request limits. 64 KB is far above any real log line and far
@@ -246,17 +332,26 @@ export function packAxiomLogRecord(record) {
246
332
  }
247
333
  if (Object.keys(overflow).length === 0)
248
334
  return stable;
249
- // Nested under the map field as a real object — NOT stringified. Axiom stores
250
- // map contents without allocating a column per key, and keeps them queryable
251
- // and typed.
252
- stable[AXIOM_OVERFLOW_MAP_FIELD] =
253
- JSON.stringify(overflow).length <= MAX_PACKED_MESSAGE_LENGTH
254
- ? overflow
255
- : {
256
- // Never silently truncate to nothing: the key NAMES alone are usually
257
- // enough to identify which call site produced the oversized record.
258
- _truncated: true,
259
- _keys: Object.keys(overflow),
260
- };
335
+ const bounded = JSON.stringify(overflow).length <= MAX_PACKED_MESSAGE_LENGTH
336
+ ? overflow
337
+ : {
338
+ // Never silently truncate to nothing: the key NAMES alone are usually
339
+ // enough to identify which call site produced the oversized record.
340
+ _truncated: true,
341
+ _keys: Object.keys(overflow),
342
+ };
343
+ if (overflowMode === "map") {
344
+ // Nested under the map field as a real object — NOT stringified. Axiom stores
345
+ // map contents without allocating a column per key, and keeps them queryable
346
+ // and typed.
347
+ stable[AXIOM_OVERFLOW_MAP_FIELD] = bounded;
348
+ return stable;
349
+ }
350
+ // Degraded: the map field has no column to live in. Same payload, same inner
351
+ // key, stringified into a column that already exists. A caller's own `message`
352
+ // field is not lost — it is non-allowlisted, so it is inside `bounded` already.
353
+ stable[AXIOM_OVERFLOW_MESSAGE_FIELD] = JSON.stringify({
354
+ [AXIOM_OVERFLOW_MAP_FIELD]: bounded,
355
+ });
261
356
  return stable;
262
357
  }
@@ -70,6 +70,8 @@ export function exitCodeForErrorCode(code) {
70
70
  }
71
71
  if (code === "not_found" || code.endsWith("_not_found"))
72
72
  return 4;
73
+ if (code.endsWith("_requires_approval"))
74
+ return 7;
73
75
  if (code === "timeout" || code.endsWith("_timeout"))
74
76
  return 8;
75
77
  if (code.startsWith("invalid_")
@@ -74,6 +74,26 @@ export declare const CRM_ACTIVITY_EVENT_DEFINITIONS: readonly [{
74
74
  readonly key: "signup";
75
75
  readonly label: "Signup";
76
76
  readonly group: "Signals";
77
+ }, {
78
+ readonly key: "product_activated";
79
+ readonly label: "Product activated";
80
+ readonly group: "Product lifecycle";
81
+ }, {
82
+ readonly key: "trial_started";
83
+ readonly label: "Free trial started";
84
+ readonly group: "Product lifecycle";
85
+ }, {
86
+ readonly key: "plan_paid";
87
+ readonly label: "Paid plan started";
88
+ readonly group: "Product lifecycle";
89
+ }, {
90
+ readonly key: "customer_lost";
91
+ readonly label: "Customer lost";
92
+ readonly group: "Product lifecycle";
93
+ }, {
94
+ readonly key: "workspace_deleted";
95
+ readonly label: "Workspace deleted";
96
+ readonly group: "Product lifecycle";
77
97
  }, {
78
98
  readonly key: "website_visit";
79
99
  readonly label: "Website visit";
@@ -24,6 +24,11 @@ export const CRM_ACTIVITY_EVENT_DEFINITIONS = [
24
24
  { key: "sync_pushed", label: "CRM sync pushed", group: "CRM operations" },
25
25
  { key: "sync_pulled", label: "CRM sync pulled", group: "CRM operations" },
26
26
  { key: "signup", label: "Signup", group: "Signals" },
27
+ { key: "product_activated", label: "Product activated", group: "Product lifecycle" },
28
+ { key: "trial_started", label: "Free trial started", group: "Product lifecycle" },
29
+ { key: "plan_paid", label: "Paid plan started", group: "Product lifecycle" },
30
+ { key: "customer_lost", label: "Customer lost", group: "Product lifecycle" },
31
+ { key: "workspace_deleted", label: "Workspace deleted", group: "Product lifecycle" },
27
32
  { key: "website_visit", label: "Website visit", group: "Signals" },
28
33
  { key: "profile_view", label: "Profile view", group: "Signals" },
29
34
  { key: "post_reaction", label: "Post reaction", group: "Signals" },
@@ -0,0 +1,27 @@
1
+ export declare const FUTURE_SIGNUP_CONTRACT_VERSION = "future_signup_crm_activation.v1";
2
+ export declare const FUTURE_SIGNUP_ACTIVATION_TAXONOMY_VERSION = "activation_commands.v1";
3
+ export declare const FUTURE_SIGNUP_LIFECYCLE_CONTRACT_VERSION = "future_signup_lifecycle.v1";
4
+ export declare const FUTURE_SIGNUP_LIFECYCLE_PROJECTION_CONTRACT_VERSION = "future_signup_lifecycle_projection.v1";
5
+ export declare const FUTURE_SIGNUP_CRM_DESTINATION = "oxygen_company_crm";
6
+ export declare const FUTURE_SIGNUP_ACTIVATION_COMMANDS: readonly ["agent create", "crm activity log", "crm assert", "crm object add-attr", "crm object create", "crm relationships define", "crm relationships upsert", "knowledge page upsert", "publishing posts create", "sequences create", "tables create", "tables duplicate", "tables import-csv", "tables import-file", "tables import-staged", "tables import-url", "tables insert", "tables relate", "tables update", "tables upsert", "workflows apply"];
7
+ export declare function isFutureSignupActivationCommand(command: string): boolean;
8
+ export declare function futureSignupJourneyId(clerkUserId: string): string;
9
+ export declare function futureSignupFactId(clerkUserId: string): string;
10
+ export declare function futureSignupOperationFactId(operationEventId: string): string;
11
+ export declare function futureSignupActivationFactId(clerkUserId: string): string;
12
+ export declare function futureSignupTrialStartedFactId(stripeSubscriptionId: string): string;
13
+ export declare function futureSignupPaidFactId(stripeInvoiceId: string): string;
14
+ export declare function futureSignupLostFactId(stripeSubscriptionId: string, lossKind: "paid_churn" | "trial_non_conversion"): string;
15
+ export declare function futureSignupSubscriptionEndedFactId(stripeSubscriptionId: string, terminationKind: "replaced" | "duplicate" | "unqualified"): string;
16
+ export declare function futureSignupWorkspaceDeletedFactId(clerkOrgId: string): string;
17
+ /** Immutable cause for membership, completeness, or Company-identity projection changes. */
18
+ export declare function futureSignupLifecycleProjectionRefreshFactId(clerkOrgId: string, sourceEventId: string): string;
19
+ export declare function futureSignupLifecycleProjectionFactId(sourceFactId: string): string;
20
+ export declare function futureSignupLifecycleAggregateId(clerkOrgId: string): string;
21
+ export declare function futureSignupLifecycleEventId(factId: string, factRevision: number): string;
22
+ export declare function isAtOrAfterFutureSignupCutover(input: {
23
+ clerkUserId: string;
24
+ signupAt: Date;
25
+ cutoverAt: Date;
26
+ clerkUserIdTiebreaker?: string;
27
+ }): boolean;
@@ -0,0 +1,90 @@
1
+ export const FUTURE_SIGNUP_CONTRACT_VERSION = "future_signup_crm_activation.v1";
2
+ export const FUTURE_SIGNUP_ACTIVATION_TAXONOMY_VERSION = "activation_commands.v1";
3
+ export const FUTURE_SIGNUP_LIFECYCLE_CONTRACT_VERSION = "future_signup_lifecycle.v1";
4
+ export const FUTURE_SIGNUP_LIFECYCLE_PROJECTION_CONTRACT_VERSION = "future_signup_lifecycle_projection.v1";
5
+ export const FUTURE_SIGNUP_CRM_DESTINATION = "oxygen_company_crm";
6
+ export const FUTURE_SIGNUP_ACTIVATION_COMMANDS = [
7
+ "agent create",
8
+ "crm activity log",
9
+ "crm assert",
10
+ "crm object add-attr",
11
+ "crm object create",
12
+ "crm relationships define",
13
+ "crm relationships upsert",
14
+ "knowledge page upsert",
15
+ "publishing posts create",
16
+ "sequences create",
17
+ "tables create",
18
+ "tables duplicate",
19
+ "tables import-csv",
20
+ "tables import-file",
21
+ "tables import-staged",
22
+ "tables import-url",
23
+ "tables insert",
24
+ "tables relate",
25
+ "tables update",
26
+ "tables upsert",
27
+ "workflows apply",
28
+ ];
29
+ const ACTIVATION_COMMAND_SET = new Set(FUTURE_SIGNUP_ACTIVATION_COMMANDS);
30
+ export function isFutureSignupActivationCommand(command) {
31
+ return ACTIVATION_COMMAND_SET.has(command.trim().toLowerCase());
32
+ }
33
+ export function futureSignupJourneyId(clerkUserId) {
34
+ return `future_signup:v1:clerk_user:${requiredId(clerkUserId, "clerkUserId")}`;
35
+ }
36
+ export function futureSignupFactId(clerkUserId) {
37
+ return futureSignupJourneyId(clerkUserId);
38
+ }
39
+ export function futureSignupOperationFactId(operationEventId) {
40
+ return `product_operation:v1:${requiredId(operationEventId, "operationEventId")}`;
41
+ }
42
+ export function futureSignupActivationFactId(clerkUserId) {
43
+ return `product_activated:v1:${futureSignupFactId(clerkUserId)}`;
44
+ }
45
+ export function futureSignupTrialStartedFactId(stripeSubscriptionId) {
46
+ return `future_signup_lifecycle:trial_started:v1:${requiredId(stripeSubscriptionId, "stripeSubscriptionId")}`;
47
+ }
48
+ export function futureSignupPaidFactId(stripeInvoiceId) {
49
+ return `future_signup_lifecycle:paid:v1:${requiredId(stripeInvoiceId, "stripeInvoiceId")}`;
50
+ }
51
+ export function futureSignupLostFactId(stripeSubscriptionId, lossKind) {
52
+ return `future_signup_lifecycle:lost:v1:${requiredId(stripeSubscriptionId, "stripeSubscriptionId")}:${lossKind}`;
53
+ }
54
+ export function futureSignupSubscriptionEndedFactId(stripeSubscriptionId, terminationKind) {
55
+ return `future_signup_lifecycle:subscription_ended:v1:${requiredId(stripeSubscriptionId, "stripeSubscriptionId")}:${terminationKind}`;
56
+ }
57
+ export function futureSignupWorkspaceDeletedFactId(clerkOrgId) {
58
+ return `future_signup_lifecycle:workspace_deleted:v1:${requiredId(clerkOrgId, "clerkOrgId")}`;
59
+ }
60
+ /** Immutable cause for membership, completeness, or Company-identity projection changes. */
61
+ export function futureSignupLifecycleProjectionRefreshFactId(clerkOrgId, sourceEventId) {
62
+ return `future_signup_lifecycle:projection_refreshed:v1:${requiredId(clerkOrgId, "clerkOrgId")}:${requiredId(sourceEventId, "sourceEventId")}`;
63
+ }
64
+ export function futureSignupLifecycleProjectionFactId(sourceFactId) {
65
+ return `lifecycle_projection:v1:${requiredId(sourceFactId, "sourceFactId")}`;
66
+ }
67
+ export function futureSignupLifecycleAggregateId(clerkOrgId) {
68
+ return `lifecycle_aggregate:v1:clerk_org:${requiredId(clerkOrgId, "clerkOrgId")}`;
69
+ }
70
+ export function futureSignupLifecycleEventId(factId, factRevision) {
71
+ if (!Number.isInteger(factRevision) || factRevision < 1) {
72
+ throw new TypeError("factRevision must be a positive integer.");
73
+ }
74
+ return `${requiredId(factId, "factId")}:r${factRevision}`;
75
+ }
76
+ export function isAtOrAfterFutureSignupCutover(input) {
77
+ const signupMs = input.signupAt.getTime();
78
+ const cutoverMs = input.cutoverAt.getTime();
79
+ if (!Number.isFinite(signupMs) || !Number.isFinite(cutoverMs))
80
+ return false;
81
+ if (signupMs !== cutoverMs)
82
+ return signupMs > cutoverMs;
83
+ return input.clerkUserId.localeCompare(input.clerkUserIdTiebreaker ?? "") >= 0;
84
+ }
85
+ function requiredId(value, field) {
86
+ const normalized = value.trim();
87
+ if (!normalized)
88
+ throw new TypeError(`${field} is required.`);
89
+ return normalized;
90
+ }