@opencxh/domain 1.274.0 → 1.275.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 (34) hide show
  1. package/dist/entities/ai-account/capabilities.d.ts +4 -0
  2. package/dist/entities/ai-account/index.d.ts +3 -0
  3. package/dist/entities/ai-account/modality.d.ts +26 -0
  4. package/dist/entities/ai-account/modality.test.d.ts +1 -0
  5. package/dist/entities/ai-account/routing.d.ts +71 -0
  6. package/dist/entities/ai-account/routing.test.d.ts +1 -0
  7. package/dist/entities/ai-account/system-one.d.ts +109 -0
  8. package/dist/entities/ai-account/system-one.test.d.ts +1 -0
  9. package/dist/entities/ai-account/types.d.ts +12 -5
  10. package/dist/entities/ai-message/types.d.ts +7 -0
  11. package/dist/entities/ai-settings/types.d.ts +9 -0
  12. package/dist/entities/eval/index.d.ts +2 -0
  13. package/dist/entities/eval/runner.d.ts +14 -0
  14. package/dist/entities/eval/runner.test.d.ts +1 -0
  15. package/dist/entities/eval/types.d.ts +46 -0
  16. package/dist/entities/inbox/types.d.ts +4 -4
  17. package/dist/entities/interaction/attention.d.ts +12 -13
  18. package/dist/entities/kb/index.d.ts +1 -0
  19. package/dist/entities/kb/keys.d.ts +2 -0
  20. package/dist/entities/rule/bind.d.ts +8 -0
  21. package/dist/entities/rule/bind.test.d.ts +1 -0
  22. package/dist/entities/rule/cron.d.ts +40 -0
  23. package/dist/entities/rule/cron.test.d.ts +1 -0
  24. package/dist/entities/rule/eval.d.ts +31 -0
  25. package/dist/entities/rule/index.d.ts +4 -0
  26. package/dist/entities/rule/match.d.ts +8 -0
  27. package/dist/entities/rule/outcome.d.ts +42 -0
  28. package/dist/entities/rule/outcome.test.d.ts +1 -0
  29. package/dist/entities/rule/types.d.ts +44 -0
  30. package/dist/index.cjs +20 -20
  31. package/dist/index.d.ts +1 -0
  32. package/dist/index.js +1990 -1736
  33. package/dist/platform/resource-source.d.ts +5 -0
  34. package/package.json +1 -1
@@ -38,6 +38,10 @@ export declare const AI_CAPABILITIES: readonly [{
38
38
  readonly id: "embedding";
39
39
  readonly defaultFlag: "isDefaultEmbedding";
40
40
  readonly label: "Embeddings";
41
+ }, {
42
+ readonly id: "decision";
43
+ readonly defaultFlag: "isDefaultDecision";
44
+ readonly label: "Beslissingen";
41
45
  }];
42
46
  /** What an account can be the default for, and what a vendor can claim. One vocabulary. */
43
47
  export type AiCapability = (typeof AI_CAPABILITIES)[number]["id"];
@@ -1,3 +1,6 @@
1
1
  export * from './capabilities';
2
+ export * from './modality';
3
+ export * from './routing';
4
+ export * from './system-one';
2
5
  export * from './types';
3
6
  export * from './vendors';
@@ -0,0 +1,26 @@
1
+ /**
2
+ * What one **model** can take in and give back.
3
+ *
4
+ * The question used to be asked per *vendor* (`aiVendorAcceptsAttachment`), and that is wrong in
5
+ * both directions: within one vendor models differ — a small fast model and its flagship do not
6
+ * read the same things — and it covered input only, so `tts` stayed an account flag with a form
7
+ * and no adapter behind it.
8
+ *
9
+ * The vocabulary is the source's, not ours (`architecture.input_modalities` on OpenRouter), and
10
+ * deliberately so: it is synced hourly and a translation table here would be a second place to
11
+ * maintain the same fact. That is why these are plain strings.
12
+ */
13
+ export interface ModelModality {
14
+ in: string[];
15
+ out: string[];
16
+ }
17
+ /**
18
+ * Does this model accept this kind — or is it simply not in the catalog?
19
+ *
20
+ * **Three states, on purpose.** `undefined` means "the catalog does not know this model", which a
21
+ * caller must be able to tell from a "no": the answer then falls back to what the vendor
22
+ * declares, rather than silently refusing every attachment on a self-hosted or brand-new model.
23
+ */
24
+ export declare function modelAcceptsAttachment(modality: ModelModality | undefined, kind: string): boolean | undefined;
25
+ /** Can this model speak? The honest question behind the `tts` flag. */
26
+ export declare function modelProduces(modality: ModelModality | undefined, out: string): boolean;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Which model answers this turn — decided per request instead of once per profile.
3
+ *
4
+ * Today nobody chooses: the model stands on the profile, or it is the account's default. Every
5
+ * question gets the same brain, whether it is *"mark this conversation resolved"* or *"write the
6
+ * quote letter"*. That is slow on the cheap questions and expensive on all of them.
7
+ *
8
+ * The routing rules are **rules**, not a model: the shape of a request is known without asking
9
+ * anyone. A hop to a classifier here would cost ~100 ms and a call to decide something that is
10
+ * free to decide.
11
+ */
12
+ export declare const MODEL_TIERS: readonly ["fast", "standard", "deep"];
13
+ export type ModelTier = (typeof MODEL_TIERS)[number];
14
+ /** Which account and model answer at one level. Both, because a tier can live on another vendor. */
15
+ export interface TierBinding {
16
+ accountId: string;
17
+ model: string;
18
+ }
19
+ export type ModelTiers = Partial<Record<ModelTier, TierBinding>>;
20
+ /**
21
+ * What this turn looks like, in the things that are knowable **before** the model is asked.
22
+ *
23
+ * Deliberately no context size, though the plan names it. The size is only known after the turn
24
+ * is assembled, and the assembly needs the provider, which needs the account, which is what this
25
+ * decides. The rule it would serve (a small classify goes to `fast`) is carried by
26
+ * {@link RoutingRequest.expectsSchema}, and that is the case it was written for.
27
+ */
28
+ export interface RoutingRequest {
29
+ /** `ask` reads, `draft` prepares, `agent` may act. Absent counts as `agent`. */
30
+ mode?: string;
31
+ /** How many tools the profile puts on the table. Zero is the cheap case. */
32
+ toolCount?: number;
33
+ hasAttachments?: boolean;
34
+ /** A structured answer against a schema: classify, extract, judge. */
35
+ expectsSchema?: boolean;
36
+ /** This turn repairs a failed one. Measured, not guessed — see the control-repair trace. */
37
+ repair?: boolean;
38
+ /** The ceiling the role allows. A cap, never a per-turn choice. */
39
+ maxTier?: ModelTier;
40
+ /** `soft` = a budget passed its warning threshold. `hard` never reaches here; it refuses. */
41
+ budgetPressure?: "none" | "soft";
42
+ }
43
+ export interface RoutingDecision {
44
+ tier: ModelTier;
45
+ /** Why, in the order the rules fired. Lands in the run so a bill can be read back. */
46
+ reasons: string[];
47
+ }
48
+ /**
49
+ * The tier for one request.
50
+ *
51
+ * Order matters and is the whole design: pick a base from the shape, escalate a repair, then
52
+ * **degrade under budget pressure rather than refuse**, and finally clamp to whatever ceiling the
53
+ * role set. Degrading last-but-one is deliberate — a cheaper answer is nearly always better than
54
+ * no answer, and today a full budget is a closed door.
55
+ */
56
+ export declare function resolveTier(request: RoutingRequest): RoutingDecision;
57
+ export interface Route extends RoutingDecision {
58
+ accountId: string;
59
+ model: string;
60
+ /** False when no tier was bound and the profile's own choice stands. */
61
+ routed: boolean;
62
+ }
63
+ /**
64
+ * The tier, resolved to an account and a model.
65
+ *
66
+ * **An unconfigured organisation must change nothing.** Tiers are opt-in, so an empty map means
67
+ * the profile's own account and model answer exactly as they did — which is also why the walk
68
+ * downward exists: a half-filled ladder (`fast` and `deep`, nothing between) should still route
69
+ * rather than fall back to the profile and quietly ignore what was configured.
70
+ */
71
+ export declare function resolveRoute(tiers: ModelTiers | undefined, request: RoutingRequest, fallback: TierBinding): Route;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,109 @@
1
+ /**
2
+ * The System One contract: ask a model a **question about a state**, get a number back.
3
+ *
4
+ * A different shape of model from every other one in this platform. A chat model is handed a
5
+ * prompt and writes text; this one is handed a state plus a set of typed questions and answers
6
+ * each of them as a probability distribution. It generates nothing — the docs say so outright —
7
+ * which is exactly why it is worth having: *"does this answer the question?"* is a yes/no with a
8
+ * degree, and a chat model asked that writes a paragraph about it.
9
+ *
10
+ * **Questions come as a set, and that is the contract rather than an optimisation.** Measured by
11
+ * the vendor: thirteen questions in one call against a 54k-character document were 12.2× cheaper
12
+ * and 10× faster than thirteen calls, and each question is scored on its own — batching adds no
13
+ * cross-talk. Anything that asks one question at a time here is paying twelve times over.
14
+ */
15
+ /** What a `true` and a `false` mean for this question. Optional; the instructions usually say it. */
16
+ export interface NoulCriteria {
17
+ true?: string;
18
+ false?: string;
19
+ }
20
+ export interface NoulQuestion {
21
+ type: "noul";
22
+ instructions: string;
23
+ criteria?: NoulCriteria;
24
+ }
25
+ /** Options as `{ name: description }`. A `null` description means "the name says it". */
26
+ export interface ChoiceQuestion {
27
+ type: "choice";
28
+ instructions: string;
29
+ criteria: Record<string, string | null>;
30
+ }
31
+ /** An ordered rubric, lowest first. */
32
+ export interface ScoreQuestion {
33
+ type: "score";
34
+ instructions: string;
35
+ criteria: string[];
36
+ }
37
+ export type SystemOneQuestion = NoulQuestion | ChoiceQuestion | ScoreQuestion;
38
+ /**
39
+ * P(yes), 0..1 — the answer and the certainty in one number.
40
+ *
41
+ * **No separate `confidence`, and that is deliberate on the vendor's side**: a yes/no has two
42
+ * outcomes, so one number describes the whole distribution. 0.5 is not "unsure about yes", it is
43
+ * "yes and no weighed the same".
44
+ */
45
+ export interface NoulAnswer {
46
+ type: "noul";
47
+ noul: number;
48
+ }
49
+ export interface ChoiceAnswer {
50
+ type: "choice";
51
+ choice: string;
52
+ confidence: number;
53
+ probabilities: Record<string, number>;
54
+ }
55
+ export interface ScoreAnswer {
56
+ type: "score";
57
+ score: number;
58
+ confidence: number;
59
+ legend?: Record<string, string>;
60
+ probabilities?: Record<string, number>;
61
+ }
62
+ export type SystemOneAnswer = NoulAnswer | ChoiceAnswer | ScoreAnswer;
63
+ export interface SystemOneRequest {
64
+ /** The thing being judged. A string, or a labelled object — the labels are read. */
65
+ state: unknown;
66
+ model?: string;
67
+ questions: Record<string, SystemOneQuestion>;
68
+ }
69
+ export interface SystemOneResponse {
70
+ model: string;
71
+ answers: Record<string, SystemOneAnswer>;
72
+ usage?: {
73
+ input_tokens?: number;
74
+ output_tokens?: number;
75
+ };
76
+ }
77
+ /** Vendor limits, so a request is refused here rather than as a 422 nobody reads. */
78
+ export declare const MAX_CHOICE_OPTIONS = 255;
79
+ export declare const MIN_SCORE_LEVELS = 2;
80
+ export declare const MAX_SCORE_LEVELS = 10;
81
+ /**
82
+ * What is wrong with this question set, or `null`.
83
+ *
84
+ * Checked before the call because the alternative is a 422 with a vendor's wording in a log, and
85
+ * because these are the mistakes a caller makes while writing a question rather than at runtime.
86
+ */
87
+ export declare function validateQuestions(questions: Record<string, SystemOneQuestion>): string | null;
88
+ export type DecisionBand = "act" | "check" | "refuse";
89
+ /**
90
+ * What to *do* with a number, in three bands.
91
+ *
92
+ * The vendor is explicit that the thresholds are domain-specific and that you should start
93
+ * conservatively — so they are parameters, not constants, and the default is deliberately timid.
94
+ * The three bands are the vendor's own architecture advice: act, act-but-confirm, do not act.
95
+ *
96
+ * **`check` is the honest middle and must not be collapsed.** Two bands force every uncertain
97
+ * answer into either a silent action or a dead end; the middle is where a human gets asked, which
98
+ * is the entire reason a calibrated number is worth more than a label.
99
+ */
100
+ export declare function decisionBand(value: number, act?: number, check?: number): DecisionBand;
101
+ /**
102
+ * Questions this model should not be asked.
103
+ *
104
+ * Straight from the vendor's own jaggedness page, and worth having in code rather than in a
105
+ * doc: counting, arithmetic, raw numeric values and **dates** are named failure modes — it
106
+ * "reads dates as text, not as ordered quantities". Every one of those has a deterministic
107
+ * answer in this codebase already, so asking a model is both worse and more expensive.
108
+ */
109
+ export declare const SYSTEM_ONE_NON_GOALS: readonly ["counting or arithmetic", "comparing dates or durations", "judging whether two numbers are close", "generating text"];
@@ -0,0 +1 @@
1
+ export {};
@@ -1,4 +1,5 @@
1
- export interface AiAccount {
1
+ import { AiDefaultFlags } from './capabilities';
2
+ interface AiAccountFields {
2
3
  id: string;
3
4
  organizationId: string;
4
5
  vendor: string;
@@ -9,8 +10,14 @@ export interface AiAccount {
9
10
  * is ignored.
10
11
  */
11
12
  baseUrl?: string;
12
- isDefaultLlm?: boolean;
13
- isDefaultStt?: boolean;
14
- isDefaultTts?: boolean;
15
- isDefaultEmbedding?: boolean;
16
13
  }
14
+ /**
15
+ * The default flags, **derived** from the capability catalog rather than listed again.
16
+ *
17
+ * They were written out by hand here, which is the drift the catalog exists to stop: adding
18
+ * `decision` gave every other place a fifth checkbox and this interface a fourth, so the form
19
+ * offered a flag the type did not have. One list, everything else follows.
20
+ */
21
+ export interface AiAccount extends AiAccountFields, Partial<AiDefaultFlags> {
22
+ }
23
+ export {};
@@ -97,6 +97,13 @@ export interface AIMessage {
97
97
  toolsTotal?: number;
98
98
  /** Which mode this turn ran as. Absent = `"agent"`: every row written before modes existed. */
99
99
  mode?: AssistantMode;
100
+ /**
101
+ * Why this model answered — one line per routing rule that fired.
102
+ *
103
+ * Absent when nothing was routed, which is every organisation that bound no tiers. A reason for
104
+ * a choice nobody made would read as a decision.
105
+ */
106
+ routing?: string[];
100
107
  }
101
108
  export type AIMessageFeedback = "up" | "down";
102
109
  /**
@@ -1,3 +1,4 @@
1
+ import { ModelTiers } from '../ai-account/routing';
1
2
  /**
2
3
  * What an organisation configures for the AI app as a whole — not per profile, not per user.
3
4
  *
@@ -5,6 +6,14 @@
5
6
  * setting nobody can find.
6
7
  */
7
8
  export interface AiOrgSettings {
9
+ /**
10
+ * Which account and model answer at `fast`, `standard` and `deep`.
11
+ *
12
+ * **Opt-in and empty by default.** An organisation that configures nothing keeps exactly what
13
+ * it has: the profile's own model answers every turn. Only once a tier is bound does the router
14
+ * (`resolveRoute`) get anything to choose between.
15
+ */
16
+ tiers?: ModelTiers;
8
17
  /**
9
18
  * May the AI app listen in on conversations (speech to text)?
10
19
  *
@@ -0,0 +1,2 @@
1
+ export * from './runner';
2
+ export * from './types';
@@ -0,0 +1,14 @@
1
+ import { EvalCase, EvalCaseResult, EvalRun } from './types';
2
+ /**
3
+ * Run a suite. The judge is injected, which is the whole reason this is one function.
4
+ *
5
+ * A deterministic suite (rule correctness, a classification against a known answer) judges by
6
+ * comparison and costs nothing; a judged suite asks a model. Both are the same loop, and keeping
7
+ * that loop in one place is what makes "put this in CI" a decision about the judge rather than a
8
+ * second implementation.
9
+ *
10
+ * **Sequential, not parallel.** A judged suite is a batch of model calls, and firing forty at
11
+ * once is how a rate limit turns a measurement into a red build. A deterministic judge is fast
12
+ * enough that the difference does not register.
13
+ */
14
+ export declare function runEvalSuite<I, E>(suite: string, target: string, cases: readonly EvalCase<I, E>[], judge: (testCase: EvalCase<I, E>) => Promise<EvalCaseResult> | EvalCaseResult, now?: number): Promise<EvalRun>;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Measuring whether this platform is any good at the things it decides.
3
+ *
4
+ * Today that exists once, as a test file inside the playbook folder: a handful of cases, opt-in,
5
+ * never in CI. That is not small-but-good, it is the wrong shape — it measures one kind of thing
6
+ * (a playbook prompt) while seven things need exactly the same machinery: prompt quality, the
7
+ * accuracy of a classification, **rule correctness**, retrieval, tool selection, whether an agent
8
+ * turn reached its control move, and live answer quality.
9
+ *
10
+ * Two design rules live in the shape itself. A case carries either an `expected` value **or** a
11
+ * `rubric`, because those are the two ways anything here can be judged: exactly, or by opinion —
12
+ * and an opinion needs a judge, which costs money and has to be calibrated. And a case is
13
+ * something you can *harvest*: every correction in the feedback loop is by definition a labelled
14
+ * case, while a golden set written by hand is out of date within a quarter.
15
+ */
16
+ export interface EvalCase<Input = unknown, Expected = unknown> {
17
+ id: string;
18
+ /** What kind of thing is judged: `rule`, `classify`, `retrieval`, `tool-selection`, … */
19
+ subject: string;
20
+ input: Input;
21
+ /** The exact answer, when there is one. Mutually exclusive with {@link EvalCase.rubric}. */
22
+ expected?: Expected;
23
+ /** What "good" means, for a case only a judge can settle. */
24
+ rubric?: string;
25
+ tags?: string[];
26
+ }
27
+ export interface EvalCaseResult {
28
+ caseId: string;
29
+ ok: boolean;
30
+ /** What actually happened, in the case's own terms. Present on a failure; optional otherwise. */
31
+ got?: unknown;
32
+ /** Why it failed, in a sentence a person can act on. */
33
+ note?: string;
34
+ }
35
+ export interface EvalRun {
36
+ suite: string;
37
+ /** What was measured: a model id, a rule set version, a prompt. */
38
+ target: string;
39
+ at: number;
40
+ /** Passed over total, 0..1. `1` on an empty suite — nothing failed. */
41
+ score: number;
42
+ perCase: EvalCaseResult[];
43
+ }
44
+ export declare function summariseEval(suite: string, target: string, perCase: readonly EvalCaseResult[], at?: number): EvalRun;
45
+ /** The failures, as lines. What a CI log should print and nothing more. */
46
+ export declare function failureLines(run: EvalRun): string[];
@@ -11,10 +11,10 @@ export interface Inbox {
11
11
  /**
12
12
  * Hours a first reply may take in this inbox before a conversation counts as at risk.
13
13
  *
14
- * Absent = the ladder per priority ({@link SLA_HOURS_BY_PRIORITY}), which is what every
15
- * organisation silently ran on: the onboarding step promised a norm per inbox, ticked itself
16
- * off on `ticketEnabled`, and nothing anywhere stored an agreed number. So the Focus badge and
17
- * Assist' SLA nudge were a hardcoded opinion.
14
+ * **Absent = no service target at all**, not a default. A ladder per priority used to stand in
15
+ * (1/4/8/24) and that was a hardcoded opinion: every inbox, and every conversation outside one,
16
+ * silently carried a promise nobody made. Deliberately not being in an SLA — personal mail —
17
+ * is the common case, so it is the one that needs no configuration.
18
18
  *
19
19
  * One number and not four (one per priority): an inbox usually *is* the agreement ("support:
20
20
  * within 4 hours"), and four fields per inbox is four things to keep consistent while the
@@ -1,4 +1,4 @@
1
- import { AttentionCtx, ParkMark, Priority, Scored } from '../../platform/attention';
1
+ import { AttentionCtx, ParkMark, Scored } from '../../platform/attention';
2
2
  import { Interaction } from './types';
3
3
  /** The only statuses that can ask for attention; `closed` and `snoozed` never compete. */
4
4
  export declare const ATTENTION_STATUSES: readonly ["open"];
@@ -15,11 +15,6 @@ export declare const ATTENTION_STATUSES: readonly ["open"];
15
15
  * larger than any positive contribution, so bulk always ranks below every real conversation.
16
16
  */
17
17
  export declare const BULK_PENALTY = 1000;
18
- /**
19
- * Hours a first response is "allowed" per priority before an open interaction counts as
20
- * SLA-at-risk. Also consumed by the Mijn dag KPI and the wait heuristic.
21
- */
22
- export declare const SLA_HOURS_BY_PRIORITY: Record<Priority, number>;
23
18
  /**
24
19
  * How long you wait for a reply before the conversation asks for attention again. Without this
25
20
  * bound a thread the customer never replied to would stay out of sight forever.
@@ -48,16 +43,17 @@ export declare function isReminderActive(reminder: ParkMark | undefined, i: Inte
48
43
  /** Whole hours an open interaction has been waiting (never negative). */
49
44
  export declare function waitHoursOf(i: Interaction, now: number): number;
50
45
  /**
51
- * The first-reply budget for this conversation, in hours.
46
+ * The first-reply budget for this conversation in hours, or `undefined` when nobody agreed one.
52
47
  *
53
- * The inbox's own norm wins when it has one; otherwise the ladder per priority. One helper
54
- * rather than the lookup at each call site, because three readers ask this question (the Focus
55
- * badge, "Mijn dag", and Assist' nudge tag) and a fourth answer would be a fourth opinion.
48
+ * **There is deliberately no fallback.** A ladder per priority used to fill this in (1/4/8/24),
49
+ * which invented an agreement nobody made: a personal mailbox got an eight-hour promise for the
50
+ * sole reason that its priority was unset. "No service target" is a valid state — most mail is
51
+ * in it — and it has to look different from a target someone actually configured.
56
52
  *
57
53
  * A non-positive stored value is treated as absent: "0 hours" is not an agreement, it is an
58
54
  * empty field that made it through a form.
59
55
  */
60
- export declare function slaHoursFor(i: Interaction, slaByInbox?: Record<string, number>): number;
56
+ export declare function slaHoursFor(i: Interaction, slaByInbox?: Record<string, number>): number | undefined;
61
57
  /**
62
58
  * Index inboxes the way {@link slaHoursFor} wants them: only the ones that named a norm.
63
59
  *
@@ -71,8 +67,11 @@ export declare function slaHoursByInbox(inboxes: readonly {
71
67
  /**
72
68
  * True when an open interaction has waited past its first-reply budget.
73
69
  *
74
- * `slaByInbox` maps inbox id -> agreed hours; a caller that has no inboxes at hand may leave it
75
- * out and gets the priority ladder, which is exactly the old behaviour.
70
+ * No budget, no risk. A conversation in an inbox that named no norm cannot be late, because
71
+ * there is nothing to be late for — so the answer is `false`, not "unknown". The one reader is a
72
+ * badge, and a badge that fires on an agreement that does not exist is the bug this removed.
73
+ *
74
+ * `slaByInbox` maps inbox id -> agreed hours; leave it out and nothing is ever at risk.
76
75
  */
77
76
  export declare function isSlaAtRisk(i: Interaction, now: number, slaByInbox?: Record<string, number>): boolean;
78
77
  /**
@@ -1,4 +1,5 @@
1
1
  export * from './help-center';
2
+ export * from './keys';
2
3
  export * from './locales';
3
4
  export * from './tree';
4
5
  export * from './types';
@@ -0,0 +1,2 @@
1
+ /** The scope kind of one article: `article:<id>`. One definition for the claim and the owner. */
2
+ export declare const ARTICLE_SCOPE_KIND = "article";
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Fill `{{path}}` references in action params from what the rule saw (`{ event, subject }`).
3
+ *
4
+ * A param that is *only* a reference keeps the referenced value's type (`"{{subject.id}}"` → the
5
+ * id, a number stays a number); a reference inside longer text is stringified. An unresolvable
6
+ * path becomes `""` — the same "empty is not an instruction" the tools already read.
7
+ */
8
+ export declare function bindParams(params: Record<string, unknown>, context: Record<string, unknown>): Record<string, unknown>;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Does this cron expression name this minute?
3
+ *
4
+ * Five fields, standard order: `minute hour day-of-month month day-of-week`. Each field is `*`,
5
+ * a number, a range (`a-b`), a step (`*` slash `n`), or a comma-separated list of those.
6
+ *
7
+ * **Hand-written and not a library.** It is thirty lines, it has to run inside QuickJS, and the
8
+ * alternative is a dependency in `packages/domain` — which carries none, on purpose. What it
9
+ * deliberately does **not** support is the part of cron nobody writes by hand: `L`, `W`, `#`,
10
+ * named months and weekdays, and seconds. An expression using one of those is refused rather
11
+ * than quietly matching nothing, which is the failure mode a scheduled rule cannot afford — a
12
+ * rule that never fires looks exactly like a rule whose condition is never true.
13
+ */
14
+ export interface ParsedCron {
15
+ minute: Set<number>;
16
+ hour: Set<number>;
17
+ dayOfMonth: Set<number>;
18
+ month: Set<number>;
19
+ dayOfWeek: Set<number>;
20
+ /** True when either day field was left as `*` — see {@link cronMatches}. */
21
+ anyDayOfMonth: boolean;
22
+ anyDayOfWeek: boolean;
23
+ }
24
+ /** `null` when the expression is not one this matcher can honour. */
25
+ export declare function parseCron(expression: string): ParsedCron | null;
26
+ /** Is this expression one this matcher can honour at all? */
27
+ export declare function isSupportedCron(expression: string): boolean;
28
+ /**
29
+ * Does this expression name the minute `date` falls in?
30
+ *
31
+ * The day rule is cron's one genuine oddity and it is kept: when **both** day fields are
32
+ * restricted they are OR-ed, not AND-ed — `0 9 1 * 1` means "the first of the month, and every
33
+ * Monday", not "a Monday that is the first". Every cron implementation does this, and quietly
34
+ * doing the intuitive thing instead would make a rule fire on a handful of days a year.
35
+ *
36
+ * Evaluated in **UTC**, because that is what the tick that calls it runs in. A rule author who
37
+ * means nine in the morning in Amsterdam writes the UTC hour; naming a timezone per rule is a
38
+ * field this type does not have yet, and guessing the organisation's is worse than the omission.
39
+ */
40
+ export declare function cronMatches(expression: string, date: Date): boolean;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,31 @@
1
+ import { EvalCase, EvalCaseResult } from '../eval/types';
2
+ import { Rule, RuleEvent } from './types';
3
+ /**
4
+ * The first eval suite, and the one that is free: **does this rule set do what its author said?**
5
+ *
6
+ * Deterministic, no model, no network — which is what makes it the suite that belongs in CI. The
7
+ * whole question is whether `matchRules` plus a condition produces the actions somebody expected,
8
+ * and every part of that is already pure.
9
+ *
10
+ * This is the check that would have caught the shipped starter template pointing at
11
+ * `subject.lastActivityPreview.text`: a field no conversation has, so the rule compared
12
+ * `undefined`, never fired, and said nothing about why. It was found by hand, once. A case says
13
+ * "this event, this rule, these actions" and it stays found.
14
+ */
15
+ export interface RuleCaseInput {
16
+ event: RuleEvent;
17
+ rules: Rule[];
18
+ }
19
+ export interface RuleCaseExpected {
20
+ /** The actions that should fire, in order. An empty list means "this must not fire". */
21
+ actions: string[];
22
+ }
23
+ export type RuleEvalCase = EvalCase<RuleCaseInput, RuleCaseExpected>;
24
+ /**
25
+ * Judge one rule case.
26
+ *
27
+ * The evaluator is injected and the caller is expected to pass **the production one**. A suite
28
+ * with its own condition semantics measures its own semantics; the point is to measure what will
29
+ * actually run.
30
+ */
31
+ export declare function judgeRuleCase(testCase: RuleEvalCase, evaluate: (condition: unknown, context: Record<string, unknown>) => boolean): EvalCaseResult;
@@ -1,4 +1,8 @@
1
+ export * from './bind';
1
2
  export * from './condition';
3
+ export * from './cron';
4
+ export * from './eval';
2
5
  export * from './match';
6
+ export * from './outcome';
3
7
  export * from './trigger-source';
4
8
  export * from './types';
@@ -1,4 +1,12 @@
1
1
  import { Rule, RuleEvent } from './types';
2
+ /**
3
+ * The event name a clock tick carries.
4
+ *
5
+ * A reserved name and not a kind of its own, so the tick travels through the same matcher, the
6
+ * same guards and the same idempotency key as everything else. `@` cannot occur in an
7
+ * `<app>:<resource>:<verb>` name, so no app can ever deliver it by accident.
8
+ */
9
+ export declare const SCHEDULE_EVENT = "@schedule";
2
10
  /**
3
11
  * How deep a chain of rules may go before it is refused.
4
12
  *
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Is what a rule wrote still standing?
3
+ *
4
+ * The feedback loop's first real signal, and it needs no new write path: every run already
5
+ * carries `priors` — the previous value of each field it wrote, plus the params it wrote them
6
+ * with — because undo needs exactly that. Asking the same question an hour later turns it into a
7
+ * measurement.
8
+ *
9
+ * **Why this matters more than an approval rate.** The trust ladder promotes on *observed
10
+ * unchanged approval*, and approving is a ritual — people click yes. A correction is a real
11
+ * objection, and so is an undo. An automatic action that could be reversed in one click and never
12
+ * is, is a stronger promotion signal than any approval percentage; a burst of corrections is an
13
+ * immediate demotion signal.
14
+ */
15
+ export type RunOutcome = "kept" | "corrected" | "undone" | "unknown";
16
+ /** The half of an action's prior this question needs. `before` is undo's business, not this one. */
17
+ export interface OutcomePrior {
18
+ action: string;
19
+ params: Record<string, unknown>;
20
+ /** Which params were content — a field the subject owns — rather than addressing. */
21
+ fields: string[];
22
+ }
23
+ /**
24
+ * Which fields no longer hold what the action put there.
25
+ *
26
+ * Shared with the undo, deliberately: "changed since" decides both whether an undo may run and
27
+ * whether this run counts as corrected, and two definitions of that would eventually disagree —
28
+ * with the undo overwriting somebody's change while the measurement called it kept.
29
+ *
30
+ * A field the subject does not report at all (`undefined`) is **not** drift. That is a reader
31
+ * that did not return the column, not a person who changed it, and counting it would turn every
32
+ * thin read into a wave of false corrections.
33
+ */
34
+ export declare function driftedFields(prior: OutcomePrior, current: Record<string, unknown> | undefined): string[];
35
+ /**
36
+ * The outcome of one run.
37
+ *
38
+ * `unknown` when the subject cannot be read — it was deleted, or its kind has no reader. Kept
39
+ * apart from `kept` on purpose: "nobody objected" and "we could not look" are different facts,
40
+ * and folding the second into the first is how a trust ladder promotes on silence.
41
+ */
42
+ export declare function runOutcome(priors: readonly OutcomePrior[], current: Record<string, unknown> | undefined, undoneAt?: number): RunOutcome;
@@ -0,0 +1 @@
1
+ export {};