@oxygen-agent/cli 1.377.3 → 1.575.19

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 (95) hide show
  1. package/README.md +1 -1
  2. package/dist/column-run-notices.d.ts +11 -0
  3. package/dist/column-run-notices.js +37 -0
  4. package/dist/command-manifest.js +13 -8
  5. package/dist/help.js +78 -16
  6. package/dist/index.js +3450 -447
  7. package/dist/skills.js +106 -1
  8. package/node_modules/@oxygen/formula/dist/coerce.d.ts +8 -0
  9. package/node_modules/@oxygen/formula/dist/coerce.js +10 -0
  10. package/node_modules/@oxygen/formula/dist/evaluate.d.ts +31 -0
  11. package/node_modules/@oxygen/formula/dist/evaluate.js +248 -0
  12. package/node_modules/@oxygen/formula/dist/expression.d.ts +64 -0
  13. package/node_modules/@oxygen/formula/dist/expression.js +428 -0
  14. package/node_modules/@oxygen/formula/dist/formula-functions.d.ts +71 -0
  15. package/node_modules/@oxygen/formula/dist/formula-functions.js +1100 -0
  16. package/node_modules/@oxygen/formula/dist/index.d.ts +17 -0
  17. package/node_modules/@oxygen/formula/dist/index.js +17 -0
  18. package/node_modules/@oxygen/formula/dist/value-normalizers.d.ts +30 -0
  19. package/node_modules/@oxygen/formula/dist/value-normalizers.js +80 -0
  20. package/node_modules/@oxygen/formula/package.json +26 -0
  21. package/node_modules/@oxygen/recipe-sdk/dist/index.d.ts +30 -0
  22. package/node_modules/@oxygen/recipe-sdk/dist/index.js +2 -2
  23. package/node_modules/@oxygen/shared/dist/billing-anchors.d.ts +60 -0
  24. package/node_modules/@oxygen/shared/dist/billing-anchors.js +135 -0
  25. package/node_modules/@oxygen/shared/dist/billing.d.ts +99 -5
  26. package/node_modules/@oxygen/shared/dist/billing.js +185 -8
  27. package/node_modules/@oxygen/shared/dist/call-outcomes.d.ts +59 -0
  28. package/node_modules/@oxygen/shared/dist/call-outcomes.js +73 -0
  29. package/node_modules/@oxygen/shared/dist/cli-result.js +1 -0
  30. package/node_modules/@oxygen/shared/dist/credit-guidance.js +3 -1
  31. package/node_modules/@oxygen/shared/dist/crm-reply-events.d.ts +35 -0
  32. package/node_modules/@oxygen/shared/dist/crm-reply-events.js +31 -0
  33. package/node_modules/@oxygen/shared/dist/dial-guardrail-overrides.d.ts +50 -0
  34. package/node_modules/@oxygen/shared/dist/dial-guardrail-overrides.js +65 -0
  35. package/node_modules/@oxygen/shared/dist/directory.d.ts +1 -1
  36. package/node_modules/@oxygen/shared/dist/directory.js +1 -0
  37. package/node_modules/@oxygen/shared/dist/hosted-ai.d.ts +15 -0
  38. package/node_modules/@oxygen/shared/dist/hosted-ai.js +19 -0
  39. package/node_modules/@oxygen/shared/dist/index.d.ts +9 -0
  40. package/node_modules/@oxygen/shared/dist/index.js +9 -0
  41. package/node_modules/@oxygen/shared/dist/linkedin-quota-denial.d.ts +31 -0
  42. package/node_modules/@oxygen/shared/dist/linkedin-quota-denial.js +56 -0
  43. package/node_modules/@oxygen/shared/dist/linkedin-sequences.d.ts +5 -4
  44. package/node_modules/@oxygen/shared/dist/linkedin-sequences.js +5 -4
  45. package/node_modules/@oxygen/shared/dist/linkedin-url.d.ts +22 -0
  46. package/node_modules/@oxygen/shared/dist/linkedin-url.js +7 -4
  47. package/node_modules/@oxygen/shared/dist/log.js +41 -2
  48. package/node_modules/@oxygen/shared/dist/microsoft-consent-url.d.ts +7 -0
  49. package/node_modules/@oxygen/shared/dist/microsoft-consent-url.js +29 -0
  50. package/node_modules/@oxygen/shared/dist/object-storage.d.ts +31 -0
  51. package/node_modules/@oxygen/shared/dist/object-storage.js +61 -0
  52. package/node_modules/@oxygen/shared/dist/plan-limits.d.ts +636 -0
  53. package/node_modules/@oxygen/shared/dist/plan-limits.js +199 -0
  54. package/node_modules/@oxygen/shared/dist/pricing-sheet.d.ts +89 -23
  55. package/node_modules/@oxygen/shared/dist/pricing-sheet.js +88 -24
  56. package/node_modules/@oxygen/shared/dist/sequence-crm-events.d.ts +291 -0
  57. package/node_modules/@oxygen/shared/dist/sequence-crm-events.js +224 -0
  58. package/node_modules/@oxygen/shared/dist/sequence-template.d.ts +42 -1
  59. package/node_modules/@oxygen/shared/dist/sequence-template.js +0 -0
  60. package/node_modules/@oxygen/shared/dist/sequences.d.ts +287 -24
  61. package/node_modules/@oxygen/shared/dist/sequences.js +940 -60
  62. package/node_modules/@oxygen/shared/dist/spend-safety.d.ts +70 -0
  63. package/node_modules/@oxygen/shared/dist/spend-safety.js +106 -0
  64. package/node_modules/@oxygen/shared/dist/tags.d.ts +90 -1
  65. package/node_modules/@oxygen/shared/dist/tags.js +122 -6
  66. package/node_modules/@oxygen/shared/dist/version.d.ts +1 -1
  67. package/node_modules/@oxygen/shared/dist/version.js +1 -1
  68. package/node_modules/@oxygen/shared/dist/workflow-trigger-metadata.d.ts +1 -1
  69. package/node_modules/@oxygen/shared/dist/workflow-trigger-metadata.js +4 -0
  70. package/node_modules/@oxygen/shared/package.json +95 -0
  71. package/node_modules/@oxygen/workflows/dist/event-dispatch.d.ts +126 -0
  72. package/node_modules/@oxygen/workflows/dist/event-dispatch.js +173 -0
  73. package/node_modules/@oxygen/workflows/dist/graph/expression.d.ts +78 -0
  74. package/node_modules/@oxygen/workflows/dist/graph/expression.js +700 -0
  75. package/node_modules/@oxygen/workflows/dist/graph/index.d.ts +20 -0
  76. package/node_modules/@oxygen/workflows/dist/graph/index.js +20 -0
  77. package/node_modules/@oxygen/workflows/dist/graph/lint.d.ts +4 -0
  78. package/node_modules/@oxygen/workflows/dist/graph/lint.js +812 -0
  79. package/node_modules/@oxygen/workflows/dist/graph/manifest-schema.d.ts +501 -0
  80. package/node_modules/@oxygen/workflows/dist/graph/manifest-schema.js +200 -0
  81. package/node_modules/@oxygen/workflows/dist/graph/params.d.ts +86 -0
  82. package/node_modules/@oxygen/workflows/dist/graph/params.js +173 -0
  83. package/node_modules/@oxygen/workflows/dist/graph/remap.d.ts +48 -0
  84. package/node_modules/@oxygen/workflows/dist/graph/remap.js +213 -0
  85. package/node_modules/@oxygen/workflows/dist/graph/topology.d.ts +46 -0
  86. package/node_modules/@oxygen/workflows/dist/graph/topology.js +280 -0
  87. package/node_modules/@oxygen/workflows/dist/graph/types.d.ts +270 -0
  88. package/node_modules/@oxygen/workflows/dist/graph/types.js +93 -0
  89. package/node_modules/@oxygen/workflows/dist/index.d.ts +113 -1
  90. package/node_modules/@oxygen/workflows/dist/index.js +179 -13
  91. package/node_modules/@oxygen/workflows/dist/tool-effects.d.ts +1 -0
  92. package/node_modules/@oxygen/workflows/dist/tool-effects.js +19 -0
  93. package/node_modules/@oxygen/workflows/dist/usage-estimate.js +135 -4
  94. package/node_modules/@oxygen/workflows/package.json +4 -0
  95. package/package.json +7 -5
package/dist/skills.js CHANGED
@@ -10,6 +10,16 @@ import { resolveCmdShimInvocation } from "./windows-shim.js";
10
10
  const DEFAULT_SKILL_AGENTS = ["codex", "claude-code", "cursor"];
11
11
  const SKILL_INDEX_PATH = "/.well-known/skills/index.json";
12
12
  const SKILLS_ACCESS_API_PATH = "/api/cli/skills/access";
13
+ // Display names the vendored `npx skills` installer (1.5.x) uses in its own
14
+ // "Failed to install" console lines (`✗ <skill> → <displayName>: <reason>`).
15
+ // Only used to tell a genuine failure of an agent we actually requested apart
16
+ // from an agent name the installer swept in on its own — see
17
+ // classifyInstallerFailures below.
18
+ const AGENT_DISPLAY_NAMES = {
19
+ codex: "Codex",
20
+ "claude-code": "Claude Code",
21
+ cursor: "Cursor",
22
+ };
13
23
  /**
14
24
  * Resolves where to install skills from. Logged-in CLIs mint a short-lived
15
25
  * org-bound access URL (the skills surface is gated; anonymous serving is
@@ -136,6 +146,7 @@ options, runtime = {}) {
136
146
  // passed straight through as before.
137
147
  let stagingDir = null;
138
148
  const outputs = [];
149
+ const skippedTargets = [];
139
150
  try {
140
151
  let installSource = source.install_url;
141
152
  if (source.authenticated) {
@@ -174,6 +185,22 @@ options, runtime = {}) {
174
185
  exitCode: 1,
175
186
  });
176
187
  }
188
+ // A 0 exit code alone doesn't mean every requested agent succeeded (see
189
+ // classifyInstallerFailures) — check the installer's own per-target report.
190
+ const { failed, skipped } = classifyInstallerFailures(stdout, agents);
191
+ if (failed.length > 0) {
192
+ throw new OxygenError("skills_install_failed", "Unable to install Oxygen agent skills.", {
193
+ details: {
194
+ index_url: source.index_url,
195
+ authenticated: source.authenticated,
196
+ skill: target,
197
+ failed_targets: failed,
198
+ stdout: stdout.slice(0, 2000),
199
+ },
200
+ exitCode: 1,
201
+ });
202
+ }
203
+ skippedTargets.push(...skipped);
177
204
  outputs.push(stdout);
178
205
  }
179
206
  }
@@ -181,6 +208,10 @@ options, runtime = {}) {
181
208
  if (stagingDir)
182
209
  rmSync(stagingDir, { recursive: true, force: true });
183
210
  }
211
+ // Every requested agent that didn't throw above installed cleanly. Agents the
212
+ // installer swept in beyond what we requested are reported distinctly and
213
+ // never counted as a failure — see classifyInstallerFailures.
214
+ const skippedAgents = [...new Set(skippedTargets.map((entry) => entry.agent))];
184
215
  if (dependencySkills.length > 0) {
185
216
  process.stderr.write(`note: '${skill}' also installs its prerequisite skill(s): ${dependencySkills.join(", ")}.\n`);
186
217
  }
@@ -195,6 +226,15 @@ options, runtime = {}) {
195
226
  ...(dependencySkills.length > 0 ? { dependency_skills: dependencySkills } : {}),
196
227
  scope: options.project ? "project" : "global",
197
228
  output: outputs.join(""),
229
+ // Truthful counts a caller (human or scripting agent) can trust at a
230
+ // glance: a not-detected agent is never folded into `failed`.
231
+ agents_installed: agents,
232
+ installed_count: agents.length,
233
+ ...(skippedAgents.length > 0 ? { agents_skipped: skippedAgents } : {}),
234
+ skipped_count: skippedAgents.length,
235
+ // Any genuine failure threw above rather than reaching this return.
236
+ failed_count: 0,
237
+ summary: summarizeInstallOutcome(agents.length, skippedAgents.length, 0),
198
238
  };
199
239
  }
200
240
  /**
@@ -590,7 +630,17 @@ function installerAddArgs(indexUrl, agents, skill) {
590
630
  "skills",
591
631
  "add",
592
632
  indexUrl,
593
- "--agents",
633
+ // The installer's own parser only recognizes the singular `-a`/`--agent`
634
+ // flag (confirmed against `npx skills --help`, 1.5.x). The plural
635
+ // `--agents` this used to send is silently dropped as an unrecognized
636
+ // flag, so our explicit agent list never reaches the installer at all —
637
+ // it falls through to its own "no --agent given" auto-detect path, which
638
+ // (non-interactively, via --yes) sweeps in every agent sharing the
639
+ // universal `.agents/skills` convention (Codex, Cursor, but also things
640
+ // like PromptScript/Neovate/Pochi/Zenflow the user never asked for and
641
+ // that don't all support `--global`), producing a wall of unrelated
642
+ // "Failed" lines for agents this install was never targeting.
643
+ "--agent",
594
644
  ...agents,
595
645
  "--yes",
596
646
  "--skill",
@@ -598,6 +648,61 @@ function installerAddArgs(indexUrl, agents, skill) {
598
648
  "--full-depth",
599
649
  ];
600
650
  }
651
+ // The installer's exit code doesn't reflect write-level (skill, agent)
652
+ // failures — `npx skills add` (1.5.x) exits 0 even when individual targets
653
+ // failed, reporting them only in a "Failed to install N" console section
654
+ // (there is no --json mode for `add` and no per-target exit code). Reading
655
+ // that text is the only signal available: a failure naming one of the agents
656
+ // we actually requested is real and must still surface as a failure; a
657
+ // failure naming any other agent means the installer targeted something this
658
+ // install was never for (see installerAddArgs) — report that as skipped, not
659
+ // a failure, so an agent that merely isn't on this machine never reads as
660
+ // "something is broken."
661
+ /** "GitHub Copilot" and "github-copilot" are the same agent; compare them as one. */
662
+ function slugifyAgentLabel(label) {
663
+ return label.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
664
+ }
665
+ function classifyInstallerFailures(stdout, requestedAgents) {
666
+ const plain = stdout.replace(/\x1B\[[0-9;]*m/g, "");
667
+ // The installer labels each line with the agent's DISPLAY name ("GitHub
668
+ // Copilot") while we requested its slug ("github-copilot"). Matching through a
669
+ // hand-maintained display-name map silently misclassified every agent missing
670
+ // from it — ~27 of the installer's targets have displayName ≠ slug — turning a
671
+ // real permission error into a reassuring "skipped, not detected". Slugify both
672
+ // sides instead, so the comparison cannot rot as the installer adds agents.
673
+ const requestedNames = new Set(requestedAgents.flatMap((agent) => [slugifyAgentLabel(agent), slugifyAgentLabel(AGENT_DISPLAY_NAMES[agent] ?? agent)]));
674
+ const failed = [];
675
+ const skipped = [];
676
+ const lineRe = /✗\s+(.+?)\s+→\s+(.+?):\s+(.+)/g;
677
+ let match;
678
+ while ((match = lineRe.exec(plain))) {
679
+ const skillName = match[1]?.trim();
680
+ const agentLabel = match[2]?.trim();
681
+ const reason = match[3]?.trim();
682
+ if (!skillName || !agentLabel || !reason)
683
+ continue;
684
+ if (requestedNames.has(slugifyAgentLabel(agentLabel))) {
685
+ failed.push({ skill: skillName, agent: agentLabel, reason });
686
+ }
687
+ else {
688
+ skipped.push({ skill: skillName, agent: agentLabel });
689
+ }
690
+ }
691
+ return { failed, skipped };
692
+ }
693
+ // Truthful, non-alarming one-line summary for both human and --json readers:
694
+ // counts what actually happened instead of letting a "not detected" target
695
+ // read like a broken install.
696
+ function summarizeInstallOutcome(installedAgents, skippedAgents, failedAgents) {
697
+ const parts = [`installed for ${installedAgents} agent${installedAgents === 1 ? "" : "s"}`];
698
+ if (skippedAgents > 0) {
699
+ parts.push(`skipped ${skippedAgents} agent${skippedAgents === 1 ? "" : "s"} not detected on this machine`);
700
+ }
701
+ if (failedAgents > 0) {
702
+ parts.push(`${failedAgents} agent${failedAgents === 1 ? "" : "s"} failed`);
703
+ }
704
+ return `${parts.join("; ")}.`;
705
+ }
601
706
  function readWords(value) {
602
707
  const text = Array.isArray(value) ? value.join(" ") : value;
603
708
  return text
@@ -0,0 +1,8 @@
1
+ /**
2
+ * The one coercion helper the formula engine needs. Kept here (rather than
3
+ * imported from a host package) so the engine stays dependency-free and
4
+ * browser-safe; tenant-db re-exports it from its own `coerce.ts` so the
5
+ * existing internal import path keeps working.
6
+ */
7
+ /** Loose record check: any non-array object (class instances included). */
8
+ export declare function isRecord(value: unknown): value is Record<string, unknown>;
@@ -0,0 +1,10 @@
1
+ /**
2
+ * The one coercion helper the formula engine needs. Kept here (rather than
3
+ * imported from a host package) so the engine stays dependency-free and
4
+ * browser-safe; tenant-db re-exports it from its own `coerce.ts` so the
5
+ * existing internal import path keeps working.
6
+ */
7
+ /** Loose record check: any non-array object (class instances included). */
8
+ export function isRecord(value) {
9
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
10
+ }
@@ -0,0 +1,31 @@
1
+ import { type FormulaAst, type ParseOptions } from "./expression.js";
2
+ /**
3
+ * Expression evaluation. The engine never sees the host's data shape: every
4
+ * bare identifier goes through `FormulaScope.resolve`, so the same evaluator
5
+ * serves a tenant-db table row, a workflow node's inputs, and a browser
6
+ * preview pane with no host imports.
7
+ */
8
+ /** Cross-row corpus, keyed by the column key the corpus was loaded for. */
9
+ export type FormulaCrossRowCorpus = Record<string, readonly string[]>;
10
+ export type FormulaScope = {
11
+ /**
12
+ * Resolve a bare identifier to a value. Throw (with
13
+ * `formulaExpressionError`) when the identifier is not in scope — a typo'd
14
+ * reference must fail loudly rather than silently evaluate to null.
15
+ */
16
+ resolve(identifier: string): unknown;
17
+ /**
18
+ * Lenient read used only by cross-row functions, which take a column key
19
+ * rather than an in-scope identifier and treat an absent value as blank.
20
+ * Omit it and the strict `resolve` is used, with its throw absorbed.
21
+ */
22
+ resolveOptional?(identifier: string): unknown;
23
+ /** Cross-row corpus, required by `novelty_score`. */
24
+ corpus?: FormulaCrossRowCorpus;
25
+ /** The key being computed — carried for self-reference guards. */
26
+ selfKey?: string;
27
+ };
28
+ export declare function evaluateFormulaInScope(expression: string, scope: FormulaScope, options?: ParseOptions): unknown;
29
+ export declare function evaluateFormulaAst(node: FormulaAst, scope: FormulaScope): unknown;
30
+ /** Coerce a formula result to the boolean gate decision used for run conditions. */
31
+ export declare function isTruthyFormulaResult(value: unknown): boolean;
@@ -0,0 +1,248 @@
1
+ import { OxygenError } from "@oxygen/shared/cli-result";
2
+ import { isRecord } from "./coerce.js";
3
+ import { CROSS_ROW_FUNCTIONS, parseFormulaExpression, } from "./expression.js";
4
+ import { FORMULA_FUNCTION_REGISTRY, checkFormulaFunctionArity, compareFormulaValues, formulaExpressionError, formulaValuesEqual, readFormulaNumber, toFormulaBoolean, } from "./formula-functions.js";
5
+ export function evaluateFormulaInScope(expression, scope, options = {}) {
6
+ return evaluateFormulaAst(parseFormulaExpression(expression, options), scope);
7
+ }
8
+ export function evaluateFormulaAst(node, scope) {
9
+ switch (node.type) { // skipcq: JS-0047
10
+ case "literal":
11
+ return node.value;
12
+ case "identifier":
13
+ return scope.resolve(node.name);
14
+ case "unary":
15
+ return evaluateUnary(node.operator, evaluateFormulaAst(node.argument, scope));
16
+ case "binary":
17
+ return evaluateBinary(node.operator, () => evaluateFormulaAst(node.left, scope), () => evaluateFormulaAst(node.right, scope));
18
+ case "call": {
19
+ const normalized = node.name.toLowerCase();
20
+ if (CROSS_ROW_FUNCTIONS.has(normalized)) {
21
+ return evaluateCrossRowFunction({ name: normalized, args: node.args, scope });
22
+ }
23
+ const spec = FORMULA_FUNCTION_REGISTRY.get(normalized);
24
+ if (spec)
25
+ checkFormulaFunctionArity(spec, node.args.length);
26
+ if (spec?.lazy && spec.evaluateLazy) {
27
+ return spec.evaluateLazy(node.args.map((arg) => () => evaluateFormulaAst(arg, scope)));
28
+ }
29
+ if (spec?.evaluate) {
30
+ return spec.evaluate(node.args.map((arg) => evaluateFormulaAst(arg, scope)));
31
+ }
32
+ throw formulaExpressionError("Unsupported formula function.", {
33
+ function: node.name,
34
+ });
35
+ }
36
+ }
37
+ }
38
+ /** Coerce a formula result to the boolean gate decision used for run conditions. */
39
+ export function isTruthyFormulaResult(value) {
40
+ if (typeof value === "boolean")
41
+ return value;
42
+ if (typeof value === "number")
43
+ return value !== 0;
44
+ if (typeof value === "string") {
45
+ const normalized = value.trim().toLowerCase();
46
+ return normalized !== "" && normalized !== "false" && normalized !== "0" && normalized !== "no";
47
+ }
48
+ return value !== null && value !== undefined;
49
+ }
50
+ function evaluateCrossRowFunction(input) {
51
+ if (input.name !== "novelty_score") {
52
+ throw formulaExpressionError("Unsupported cross-row formula function.", {
53
+ function: input.name,
54
+ });
55
+ }
56
+ const firstArg = input.args[0];
57
+ if (input.args.length !== 1 || !firstArg) {
58
+ throw formulaExpressionError("novelty_score requires exactly one column argument.", {
59
+ function: input.name,
60
+ received: input.args.length,
61
+ });
62
+ }
63
+ const targetColumnKey = readCrossRowColumnArg(firstArg, input.name);
64
+ if (!input.scope.corpus) {
65
+ throw new OxygenError("novelty_score_corpus_unavailable", "novelty_score requires a sync columns run on the current table so the column corpus can be loaded.", {
66
+ details: {
67
+ column_key: targetColumnKey,
68
+ function: input.name,
69
+ hint: "Re-run with `oxygen columns run` (sync) instead of `--background` so the corpus is available.",
70
+ },
71
+ exitCode: 1,
72
+ });
73
+ }
74
+ const corpus = input.scope.corpus[targetColumnKey];
75
+ if (!corpus) {
76
+ throw new OxygenError("novelty_score_corpus_unavailable", `novelty_score corpus for column ${targetColumnKey} is empty or was not provided.`, {
77
+ details: { column_key: targetColumnKey, function: input.name },
78
+ exitCode: 1,
79
+ });
80
+ }
81
+ const cellValue = stringifyNoveltyValue(readOptionalValue(input.scope, targetColumnKey));
82
+ if (!cellValue)
83
+ return null;
84
+ const noveltyScore = computeNoveltyScore(cellValue, corpus);
85
+ return Math.round(noveltyScore * 10_000) / 10_000;
86
+ }
87
+ /**
88
+ * Cross-row functions take a column key, not an in-scope identifier: a column
89
+ * that exists in the corpus but not on this row scores as blank instead of
90
+ * failing the row. Scopes that can answer leniently supply `resolveOptional`;
91
+ * otherwise the strict resolver's unknown-identifier throw is absorbed here.
92
+ */
93
+ function readOptionalValue(scope, identifier) {
94
+ if (scope.resolveOptional)
95
+ return scope.resolveOptional(identifier);
96
+ try {
97
+ return scope.resolve(identifier);
98
+ }
99
+ catch {
100
+ return undefined;
101
+ }
102
+ }
103
+ function readCrossRowColumnArg(node, functionName) {
104
+ if (node.type === "identifier")
105
+ return node.name;
106
+ if (node.type === "literal" && typeof node.value === "string")
107
+ return node.value;
108
+ throw formulaExpressionError("Cross-row function requires a column key argument.", {
109
+ function: functionName,
110
+ });
111
+ }
112
+ function stringifyNoveltyValue(value) {
113
+ if (value === null || value === undefined)
114
+ return "";
115
+ if (typeof value === "string")
116
+ return value.trim();
117
+ if (typeof value === "number" || typeof value === "boolean")
118
+ return String(value).trim();
119
+ if (Array.isArray(value)) {
120
+ return value
121
+ .map((entry) => stringifyNoveltyValue(entry))
122
+ .filter((entry) => entry.length > 0)
123
+ .join(" ")
124
+ .trim();
125
+ }
126
+ if (isRecord(value)) {
127
+ const preferred = value.body ?? value.text ?? value.email ?? value.message;
128
+ if (typeof preferred === "string")
129
+ return preferred.trim();
130
+ return JSON.stringify(value);
131
+ }
132
+ return "";
133
+ }
134
+ const NOVELTY_SHINGLE_SIZE = 5;
135
+ function computeNoveltyScore(cellValue, corpus) {
136
+ const cellShingles = buildShingleSet(cellValue);
137
+ if (cellShingles.size === 0)
138
+ return 1;
139
+ let maxOverlap = 0;
140
+ let skippedSelfMatch = false;
141
+ for (const other of corpus) {
142
+ if (other === cellValue && !skippedSelfMatch) {
143
+ skippedSelfMatch = true;
144
+ continue;
145
+ }
146
+ const otherShingles = buildShingleSet(other);
147
+ if (otherShingles.size === 0)
148
+ continue;
149
+ const overlap = jaccardSimilarity(cellShingles, otherShingles);
150
+ if (overlap > maxOverlap)
151
+ maxOverlap = overlap;
152
+ }
153
+ const novelty = 1 - maxOverlap;
154
+ if (novelty < 0)
155
+ return 0;
156
+ if (novelty > 1)
157
+ return 1;
158
+ return novelty;
159
+ }
160
+ function buildShingleSet(value) {
161
+ const normalized = value
162
+ .toLowerCase()
163
+ .replace(/[^a-z0-9\s]/g, " ")
164
+ .replace(/\s+/g, " ")
165
+ .trim();
166
+ const tokens = normalized ? normalized.split(" ") : [];
167
+ const shingles = new Set();
168
+ if (tokens.length < NOVELTY_SHINGLE_SIZE) {
169
+ if (tokens.length > 0)
170
+ shingles.add(tokens.join(" "));
171
+ return shingles;
172
+ }
173
+ for (let i = 0; i <= tokens.length - NOVELTY_SHINGLE_SIZE; i += 1) {
174
+ shingles.add(tokens.slice(i, i + NOVELTY_SHINGLE_SIZE).join(" "));
175
+ }
176
+ return shingles;
177
+ }
178
+ function jaccardSimilarity(left, right) {
179
+ let intersection = 0;
180
+ const [smaller, larger] = left.size <= right.size ? [left, right] : [right, left];
181
+ for (const entry of smaller) {
182
+ if (larger.has(entry))
183
+ intersection += 1;
184
+ }
185
+ const union = left.size + right.size - intersection;
186
+ return union === 0 ? 0 : intersection / union;
187
+ }
188
+ function evaluateUnary(operator, value) {
189
+ switch (operator) {
190
+ case "+":
191
+ return readFormulaNumber(value, operator);
192
+ case "-":
193
+ return -readFormulaNumber(value, operator);
194
+ case "not":
195
+ return !toFormulaBoolean(value);
196
+ default:
197
+ throw formulaExpressionError("Unsupported unary operator.", { operator });
198
+ }
199
+ }
200
+ function evaluateBinary(// skipcq: JS-R1005
201
+ operator, leftThunk, rightThunk) {
202
+ if (operator === "and") {
203
+ return toFormulaBoolean(leftThunk()) && toFormulaBoolean(rightThunk());
204
+ }
205
+ if (operator === "or") {
206
+ return toFormulaBoolean(leftThunk()) || toFormulaBoolean(rightThunk());
207
+ }
208
+ const left = leftThunk();
209
+ const right = rightThunk();
210
+ switch (operator) {
211
+ case "=":
212
+ case "==":
213
+ return formulaValuesEqual(left, right);
214
+ case "!=":
215
+ case "<>":
216
+ return !formulaValuesEqual(left, right);
217
+ case "<":
218
+ return compareFormulaValues(left, right) < 0;
219
+ case "<=":
220
+ return compareFormulaValues(left, right) <= 0;
221
+ case ">":
222
+ return compareFormulaValues(left, right) > 0;
223
+ case ">=":
224
+ return compareFormulaValues(left, right) >= 0;
225
+ case "+":
226
+ return readFormulaNumber(left, operator) + readFormulaNumber(right, operator);
227
+ case "-":
228
+ return readFormulaNumber(left, operator) - readFormulaNumber(right, operator);
229
+ case "*":
230
+ return readFormulaNumber(left, operator) * readFormulaNumber(right, operator);
231
+ case "/": {
232
+ const divisor = readFormulaNumber(right, operator);
233
+ if (divisor === 0) {
234
+ throw formulaExpressionError("Formula division by zero.", { operator });
235
+ }
236
+ return readFormulaNumber(left, operator) / divisor;
237
+ }
238
+ case "%": {
239
+ const divisor = readFormulaNumber(right, operator);
240
+ if (divisor === 0) {
241
+ throw formulaExpressionError("Formula modulo by zero.", { operator });
242
+ }
243
+ return readFormulaNumber(left, operator) % divisor;
244
+ }
245
+ default:
246
+ throw formulaExpressionError("Unsupported binary operator.", { operator });
247
+ }
248
+ }
@@ -0,0 +1,64 @@
1
+ /** The parsed expression tree. Identifiers are resolved by a FormulaScope. */
2
+ export type FormulaAst = {
3
+ type: "literal";
4
+ value: unknown;
5
+ } | {
6
+ type: "identifier";
7
+ name: string;
8
+ } | {
9
+ type: "unary";
10
+ operator: string;
11
+ argument: FormulaAst;
12
+ } | {
13
+ type: "binary";
14
+ operator: string;
15
+ left: FormulaAst;
16
+ right: FormulaAst;
17
+ } | {
18
+ type: "call";
19
+ name: string;
20
+ args: FormulaAst[];
21
+ };
22
+ /**
23
+ * Functions evaluated against the whole column rather than the current scope.
24
+ * They are intercepted before registry dispatch (the registry entry carries
25
+ * metadata only) because they need a corpus the scope supplies.
26
+ */
27
+ export declare const CROSS_ROW_FUNCTIONS: ReadonlySet<string>;
28
+ /**
29
+ * How to tokenize. One flag today, and it is deliberately opt-in: a table formula
30
+ * must parse byte-identically to before this existed.
31
+ */
32
+ export type ParseOptions = {
33
+ /**
34
+ * Let an identifier carry `.field` and `[0]` segments, so `steps.enrich.email`
35
+ * is one identifier rather than a syntax error. Workflow expressions set this;
36
+ * table formulas do not, because a bare column name is their whole vocabulary and
37
+ * `a.b` there should stay the error it has always been.
38
+ */
39
+ paths?: boolean;
40
+ };
41
+ export declare function parseFormulaExpression(expression: string, options?: ParseOptions): FormulaAst;
42
+ /** Validate supported functions and arity on an already-parsed tree. */
43
+ export declare function validateFormulaAst(ast: FormulaAst): void;
44
+ /** Validate syntax, supported functions, and arity without evaluating any data. */
45
+ export declare function validateFormulaExpression(expression: string, options?: ParseOptions): void;
46
+ export declare function walkExpression(node: FormulaAst, visit: (node: FormulaAst) => void): void;
47
+ export declare function analyzeCrossRowDependencies(node: FormulaAst): {
48
+ functions: string[];
49
+ columnKeys: string[];
50
+ };
51
+ export declare function analyzeFormulaCrossRowDependencies(expression: string): {
52
+ functions: string[];
53
+ columnKeys: string[];
54
+ };
55
+ /**
56
+ * Every identifier the expression reads, plus its cross-row dependencies.
57
+ * Identifiers are bare names — the host decides what they bind to (a table
58
+ * column key today, a workflow node output tomorrow).
59
+ */
60
+ export declare function analyzeFormulaDependencies(expression: string, options?: ParseOptions): {
61
+ identifiers: string[];
62
+ crossRowFunctions: string[];
63
+ crossRowColumnKeys: string[];
64
+ };