@opencxh/domain 1.265.1 → 1.270.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.
- package/dist/entities/rule/index.d.ts +2 -0
- package/dist/entities/rule/match.d.ts +49 -0
- package/dist/entities/rule/match.test.d.ts +1 -0
- package/dist/entities/rule/types.d.ts +103 -0
- package/dist/index.cjs +19 -19
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1240 -1197
- package/package.json +1 -1
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { Rule, RuleEvent } from './types';
|
|
2
|
+
/**
|
|
3
|
+
* How deep a chain of rules may go before it is refused.
|
|
4
|
+
*
|
|
5
|
+
* A rule writes, a write is an event, an event triggers rules. Async that is a slow loop; **in a
|
|
6
|
+
* write path it is a stack**, and it takes down the request that started it. Three hops is enough
|
|
7
|
+
* for anything anyone meant to build and short enough that a mistake stops being one.
|
|
8
|
+
*/
|
|
9
|
+
export declare const MAX_RULE_DEPTH = 3;
|
|
10
|
+
/** A rule that fires, with the key that makes firing it twice a no-op. */
|
|
11
|
+
export interface RuleMatch {
|
|
12
|
+
rule: Rule;
|
|
13
|
+
/**
|
|
14
|
+
* `<eventId>:<ruleId>:<version>`. A write path can run again — a retry, a redelivery — and the
|
|
15
|
+
* effect must land once. The version is in there on purpose: against an edited rule the same
|
|
16
|
+
* event is a different decision, and that one is allowed to happen.
|
|
17
|
+
*/
|
|
18
|
+
idempotencyKey: string;
|
|
19
|
+
}
|
|
20
|
+
export interface RuleMatchResult {
|
|
21
|
+
matched: RuleMatch[];
|
|
22
|
+
/**
|
|
23
|
+
* Why each rule that did not fire did not fire, in words.
|
|
24
|
+
*
|
|
25
|
+
* The same reason `triggerMismatch` returns a sentence rather than a boolean: "why did nothing
|
|
26
|
+
* happen" is the question an admin actually asks, and a silent empty list is the worst possible
|
|
27
|
+
* answer to it.
|
|
28
|
+
*/
|
|
29
|
+
skipped: {
|
|
30
|
+
ruleId: string;
|
|
31
|
+
reason: string;
|
|
32
|
+
}[];
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Which rules fire for this event.
|
|
36
|
+
*
|
|
37
|
+
* Pure, and the evaluator is injected — `packages/domain` carries no dependencies, and the
|
|
38
|
+
* JSONLogic implementation lives with the executor. That also makes every guard below testable
|
|
39
|
+
* without a store, which is the point: each one is a rule about *not* doing something, and those
|
|
40
|
+
* are exactly the ones nobody notices are broken.
|
|
41
|
+
*/
|
|
42
|
+
export declare function matchRules(event: RuleEvent, rules: readonly Rule[], evaluate: (condition: unknown, context: Record<string, unknown>) => boolean): RuleMatchResult;
|
|
43
|
+
/**
|
|
44
|
+
* The event a rule's own write produces, stamped so the guards above can see it coming.
|
|
45
|
+
*
|
|
46
|
+
* One function so no call site has to remember both fields: forgetting `depth` makes the ceiling
|
|
47
|
+
* unreachable, and forgetting `causedByRule` makes a rule able to answer itself.
|
|
48
|
+
*/
|
|
49
|
+
export declare function causedBy(event: RuleEvent, ruleId: string): Pick<RuleEvent, "causedByRule" | "depth">;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { OwnerScope } from '../scope/types';
|
|
2
|
+
/**
|
|
3
|
+
* When something starts. One vocabulary for every executor.
|
|
4
|
+
*
|
|
5
|
+
* There were five of these, and they had to agree without anything saying so: `PlaybookTrigger`,
|
|
6
|
+
* `webhook.triggers[]` (four fixed strings), the sync tick, and twenty-one hand-rolled cron jobs.
|
|
7
|
+
*
|
|
8
|
+
* **Four arms, not five.** `stream` is deliberately absent. It existed once, as a field-less
|
|
9
|
+
* `{ kind: "stream" }` on `PlaybookTrigger`, and was removed with a reason that generalises:
|
|
10
|
+
* *"Definitions belong separated by their runtime, not by their editor."* A live read lane has no
|
|
11
|
+
* run, no lock and no gate, so it is not an automation with a trigger — it is its own thing
|
|
12
|
+
* (`LiveLens`), and it carries its cadence itself.
|
|
13
|
+
*/
|
|
14
|
+
export type Trigger =
|
|
15
|
+
/** Something happened. `name` is what the bus delivers: `<app>:<resource>:<verb>`. */
|
|
16
|
+
{
|
|
17
|
+
kind: "event";
|
|
18
|
+
name: string;
|
|
19
|
+
}
|
|
20
|
+
/** The clock. */
|
|
21
|
+
| {
|
|
22
|
+
kind: "schedule";
|
|
23
|
+
cron: string;
|
|
24
|
+
}
|
|
25
|
+
/** A person pressed start. */
|
|
26
|
+
| {
|
|
27
|
+
kind: "manual";
|
|
28
|
+
}
|
|
29
|
+
/** Work was handed to an agent. `targetKind` is declared by the app that owns that work. */
|
|
30
|
+
| {
|
|
31
|
+
kind: "assignment";
|
|
32
|
+
targetKind: string;
|
|
33
|
+
};
|
|
34
|
+
/**
|
|
35
|
+
* One thing a rule does.
|
|
36
|
+
*
|
|
37
|
+
* **`action` is always `tool:<namespaced>`.** The action library is the tool federation — the
|
|
38
|
+
* same 62 descriptors the assistant uses, with the model taken out from in front. A second
|
|
39
|
+
* library would duplicate what a descriptor already carries: a JSON-Schema, a scope gate, an
|
|
40
|
+
* `effect` classification and an approval hook.
|
|
41
|
+
*/
|
|
42
|
+
export interface RuleAction {
|
|
43
|
+
action: string;
|
|
44
|
+
params: Record<string, unknown>;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* *If this, then that* — without a model.
|
|
48
|
+
*
|
|
49
|
+
* The deterministic kind of automation. Seven table-stakes gaps have this exact shape (routing,
|
|
50
|
+
* auto-assign on inbound, binding an SLA profile, escalation, a macro that does more than paste
|
|
51
|
+
* text, status rules in work, "quote accepted so the deal is won"), and none of them needs a
|
|
52
|
+
* model. Today every one of them would have to go through an LLM turn.
|
|
53
|
+
*
|
|
54
|
+
* What makes it a *rule* and not a workflow is the executor, not the shape: a rule runs
|
|
55
|
+
* synchronously in the write path of the app that owns the subject, so a conversation is routed
|
|
56
|
+
* and assigned **before** it becomes visible rather than jumping a beat later.
|
|
57
|
+
*/
|
|
58
|
+
export interface Rule {
|
|
59
|
+
id: string;
|
|
60
|
+
organizationId: string;
|
|
61
|
+
ownerScope: OwnerScope;
|
|
62
|
+
name: string;
|
|
63
|
+
/** What this rule is for, in the author's own words. */
|
|
64
|
+
description?: string;
|
|
65
|
+
enabled: boolean;
|
|
66
|
+
/**
|
|
67
|
+
* Only `event` runs today. The other arms exist because the vocabulary is shared; a rule on a
|
|
68
|
+
* schedule waits for the cadence work in a later wave.
|
|
69
|
+
*/
|
|
70
|
+
trigger: Trigger;
|
|
71
|
+
/**
|
|
72
|
+
* JSONLogic over `{ event, subject }` — **and nothing else**. Absent = always.
|
|
73
|
+
*
|
|
74
|
+
* That the context is only those two is the load-bearing constraint, not an omission. Give a
|
|
75
|
+
* rule a fan-out ("is this customer a VIP", "how many open items has this team") and the write
|
|
76
|
+
* path of every inbound message becomes as slow as the slowest app that answers — and then the
|
|
77
|
+
* whole reason it runs synchronously is gone. Anything needing a lookup is a workflow.
|
|
78
|
+
*/
|
|
79
|
+
when?: unknown;
|
|
80
|
+
then: RuleAction[];
|
|
81
|
+
/**
|
|
82
|
+
* Bumped on every edit. Part of the idempotency key: re-running an event against an edited
|
|
83
|
+
* rule is a different decision, and should be allowed to happen once.
|
|
84
|
+
*/
|
|
85
|
+
version: number;
|
|
86
|
+
}
|
|
87
|
+
/** What a rule is offered. The event, the row it is about, and where it came from. */
|
|
88
|
+
export interface RuleEvent {
|
|
89
|
+
/** `<app>:<resource>:<verb>`, as the bus delivers it. */
|
|
90
|
+
name: string;
|
|
91
|
+
/** Stable id of this occurrence. Half of the idempotency key. */
|
|
92
|
+
id: string;
|
|
93
|
+
payload: Record<string, unknown>;
|
|
94
|
+
/** The row this is about, as the owning app already has it in hand. */
|
|
95
|
+
subject?: Record<string, unknown>;
|
|
96
|
+
/**
|
|
97
|
+
* Set when this event was itself produced by a rule. Both halves of the re-entrancy guard
|
|
98
|
+
* hang off this: a rule may not react to its own output, and a chain has a ceiling.
|
|
99
|
+
*/
|
|
100
|
+
causedByRule?: string;
|
|
101
|
+
/** How many rule hops produced this event. Absent = zero, a genuine first cause. */
|
|
102
|
+
depth?: number;
|
|
103
|
+
}
|