okengine 0.19.2 → 0.19.4

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 (50) hide show
  1. package/package.json +1 -1
  2. package/site/content/docs/elements/channel/index.mdx +1 -1
  3. package/site/content/docs/elements/clock/index.mdx +106 -17
  4. package/site/content/docs/elements/clock/schedules.mdx +39 -11
  5. package/site/content/docs/elements/flow/consumers.mdx +129 -86
  6. package/site/content/docs/elements/flow/index.mdx +3 -3
  7. package/site/content/docs/elements/flow/routing.mdx +11 -8
  8. package/site/content/docs/elements/signal/broadcast.mdx +15 -14
  9. package/site/content/docs/elements/signal/index.mdx +99 -36
  10. package/site/content/docs/elements/signal/live.mdx +5 -13
  11. package/site/content/docs/elements/signal/once.mdx +88 -28
  12. package/site/content/docs/reference/errors.mdx +41 -27
  13. package/site/content/docs/understand/the-architecture.mdx +2 -2
  14. package/src/cli/competitor-mention-removal.test.ts +3 -3
  15. package/src/compiler/extract.test.ts +237 -15
  16. package/src/compiler/extract.ts +75 -4
  17. package/src/compiler/flow-path.test.ts +23 -0
  18. package/src/compiler/flow-path.ts +17 -1
  19. package/src/console/ui-next/dist/assets/{access-page-DceEWH9u.js → access-page-rO2HLPab.js} +1 -1
  20. package/src/console/ui-next/dist/assets/{agent-disclosure-tC9s2VFd.js → agent-disclosure-ae8w8JLx.js} +1 -1
  21. package/src/console/ui-next/dist/assets/{cache-glyph-C-naNQSR.js → cache-glyph-C-uHdh5d.js} +1 -1
  22. package/src/console/ui-next/dist/assets/{call-pii-button-DnZ_MlDn.js → call-pii-button-BJH54w4s.js} +1 -1
  23. package/src/console/ui-next/dist/assets/{collapsible-RekgR6Qz.js → collapsible-CaE8cs9p.js} +1 -1
  24. package/src/console/ui-next/dist/assets/{duration-tone-DugtWBS0.js → duration-tone-DdOQkQVK.js} +1 -1
  25. package/src/console/ui-next/dist/assets/{flows-page-DVmn1ZuQ.js → flows-page-DWeBszQQ.js} +1 -1
  26. package/src/console/ui-next/dist/assets/{highlighted-json-CvBGaiSD.js → highlighted-json-B2gBsC4B.js} +1 -1
  27. package/src/console/ui-next/dist/assets/{http-method-D7_OXbdC.js → http-method-BnMl0dJt.js} +1 -1
  28. package/src/console/ui-next/dist/assets/{index-DH0K2f6N.js → index-CBAP48v5.js} +3 -3
  29. package/src/console/ui-next/dist/assets/index-VxoEz295.css +2 -0
  30. package/src/console/ui-next/dist/assets/{observability-page-qJzF2nSN.js → observability-page-DVpcJEnW.js} +1 -1
  31. package/src/console/ui-next/dist/assets/{replica-lag-Vqk0pUBA.js → replica-lag-BUQf2Nf6.js} +1 -1
  32. package/src/console/ui-next/dist/assets/{request-meta-BtShi4sG.js → request-meta-CPkGDQgT.js} +1 -1
  33. package/src/console/ui-next/dist/assets/{store-page-CMsYH_vH.js → store-page-CwS_cY4V.js} +1 -1
  34. package/src/console/ui-next/dist/assets/{trace-detail-sheet-ycFB2uua.js → trace-detail-sheet-DNX7Y2bd.js} +1 -1
  35. package/src/console/ui-next/dist/assets/{tree-expand-toggle-iG1jcWgU.js → tree-expand-toggle-Bhdk27aF.js} +1 -1
  36. package/src/console/ui-next/dist/assets/{units-page-Bavchke4.js → units-page-CsBh0XbI.js} +1 -1
  37. package/src/console/ui-next/dist/assets/{vault-page-DxUFhiZI.js → vault-page-CsTlPtK9.js} +1 -1
  38. package/src/console/ui-next/dist/index.html +2 -2
  39. package/src/kernel/app.ts +87 -1
  40. package/src/kernel/errors-flow-name.ts +20 -4
  41. package/src/kernel/errors-once-signal.ts +25 -0
  42. package/src/kernel/errors.registry.test.ts +12 -2
  43. package/src/kernel/flow-name.test.ts +105 -20
  44. package/src/kernel/flow.ts +3 -3
  45. package/src/kernel/on.ts +0 -5
  46. package/src/kernel/once-signal.test.ts +71 -0
  47. package/src/kernel/stamp-http.test.ts +2 -1
  48. package/src/kernel/stamp-http.ts +5 -7
  49. package/src/kernel/unit.ts +1 -2
  50. package/src/console/ui-next/dist/assets/index-D0zS5rKO.css +0 -2
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Nameless Flow inherits Signal/Clock trigger names; duplicates fail OKE1070.
2
+ * Nameless Signal/Clock consumers fail OKE1072; duplicate names fail OKE1070.
3
3
  */
4
4
 
5
5
  import { beforeEach, describe, expect, test } from "bun:test";
@@ -7,7 +7,7 @@ import { clock, resetClocks } from "../elements/clock/declare.ts";
7
7
  import { resetSignals, signal } from "../elements/signal/declare.ts";
8
8
  import { oke } from "./app.ts";
9
9
  import { OkeError } from "./errors.ts";
10
- import { FLOW_NAME_DUPLICATE } from "./errors-flow-name.ts";
10
+ import { FLOW_NAME_DUPLICATE, FLOW_UNNAMED } from "./errors-flow-name.ts";
11
11
  import { flow, resetFlowSeq } from "./flow.ts";
12
12
  import { on, resetBindings } from "./on.ts";
13
13
  import { stampFlowName } from "./stamp-http.ts";
@@ -20,28 +20,28 @@ beforeEach(() => {
20
20
  resetClocks();
21
21
  });
22
22
 
23
- describe("on() — Signal/Clock name inheritance", () => {
24
- test("nameless flow inherits inline signal.once name", () => {
23
+ describe("on() — Signal/Clock names stay explicit or tree-stamped", () => {
24
+ test("nameless flow does not inherit an inline signal.once name", () => {
25
25
  const bound = on(
26
26
  signal.once("link-clicked"),
27
27
  flow({
28
28
  do: () => ({ ok: true }),
29
29
  }),
30
30
  );
31
- expect(bound.name).toBe("link-clicked");
31
+ expect(bound.name).toBe("");
32
32
  });
33
33
 
34
- test("nameless flow inherits clock.every name", () => {
34
+ test("nameless flow does not inherit a clock.every name", () => {
35
35
  const bound = on(
36
36
  clock.every("cleanup", "10m"),
37
37
  flow({
38
38
  do: () => ({ ok: true }),
39
39
  }),
40
40
  );
41
- expect(bound.name).toBe("cleanup");
41
+ expect(bound.name).toBe("");
42
42
  });
43
43
 
44
- test("explicit flow name wins over the trigger name", () => {
44
+ test("explicit flow name is independent of the trigger name", () => {
45
45
  const bound = on(
46
46
  signal.once("order-placed"),
47
47
  flow("orders.fulfill", { do: () => ({ ok: true }) }),
@@ -59,40 +59,88 @@ describe("on() — Signal/Clock name inheritance", () => {
59
59
  expect(bound.name).toBe("");
60
60
  });
61
61
 
62
- test("file-tree stamp overwrites a trigger-inherited name", () => {
62
+ test("file-tree stamp fills a nameless Signal consumer", () => {
63
63
  const bound = on(
64
64
  signal.once("note-created"),
65
65
  flow({
66
66
  do: () => ({ ok: true }),
67
67
  }),
68
68
  );
69
- expect(bound.name).toBe("note-created");
69
+ expect(bound.name).toBe("");
70
70
  stampFlowName(bound, "notes.onCreated");
71
71
  expect(bound.name).toBe("notes.onCreated");
72
72
  });
73
73
  });
74
74
 
75
- describe("oke — FLOW_NAME_DUPLICATE OKE1070", () => {
76
- test("two nameless inheritances of the same trigger name fail at construction", () => {
77
- const ping = signal.once("health.ping");
78
- on(ping, flow({ do: () => ({ a: true }) }));
79
- on(ping, flow({ do: () => ({ b: true }) }));
75
+ describe("oke — FLOW_UNNAMED OKE1072", () => {
76
+ test("nameless flow({ do }) on signal.once fails OKE1072", () => {
77
+ on(
78
+ signal.once("link-clicked"),
79
+ flow({
80
+ do: () => ({ ok: true }),
81
+ }),
82
+ );
80
83
  try {
81
84
  oke({ name: "t", autoBoot: false });
82
- expect.unreachable("oke() should throw OKE1070");
85
+ expect.unreachable("oke() should throw OKE1072");
83
86
  } catch (err) {
84
87
  expect(err).toBeInstanceOf(OkeError);
85
88
  const okeErr = err as OkeError;
86
- expect(okeErr.code).toBe(1070);
87
- expect(okeErr.code).toBe(FLOW_NAME_DUPLICATE.code);
88
- expect(okeErr.message).toMatch(/OKE1070/);
89
+ expect(okeErr.code).toBe(1072);
90
+ expect(okeErr.code).toBe(FLOW_UNNAMED.code);
91
+ expect(okeErr.message).toMatch(/OKE1072/);
92
+ expect(okeErr.causeText).toContain("signal");
93
+ expect(okeErr.causeText).toContain("link-clicked");
94
+ expect(okeErr.message).toContain("src/flows/<unit>/");
95
+ expect(okeErr.message).toContain("export const");
89
96
  }
90
97
  });
91
98
 
99
+ test("nameless flow({ do }) on signal.broadcast fails OKE1072", () => {
100
+ on(
101
+ signal.broadcast("catalog.changed"),
102
+ flow({
103
+ do: () => ({ ok: true }),
104
+ }),
105
+ );
106
+ expect(() => oke({ name: "t", autoBoot: false })).toThrow(/OKE1072/);
107
+ });
108
+
109
+ test("nameless flow({ do }) on signal.live fails OKE1072", () => {
110
+ on(
111
+ signal.live("order-status", { optional: true }),
112
+ flow({
113
+ do: () => ({ ok: true }),
114
+ }),
115
+ );
116
+ expect(() => oke({ name: "t", autoBoot: false })).toThrow(/OKE1072/);
117
+ });
118
+
119
+ test("nameless flow({ do }) on clock.every fails OKE1072", () => {
120
+ on(
121
+ clock.every("cleanup", "10m"),
122
+ flow({
123
+ do: () => ({ ok: true }),
124
+ }),
125
+ );
126
+ expect(() => oke({ name: "t", autoBoot: false })).toThrow(/OKE1072/);
127
+ });
128
+ });
129
+
130
+ describe("oke — FLOW_NAME_DUPLICATE OKE1070", () => {
92
131
  test("two explicit flows with the same name fail OKE1070", () => {
93
132
  on(signal.once("a"), flow("shared", { do: () => 1 }));
94
133
  on(clock.every("b", "1h"), flow("shared", { do: () => 2 }));
95
- expect(() => oke({ name: "t", autoBoot: false })).toThrow(/OKE1070/);
134
+ try {
135
+ oke({ name: "t", autoBoot: false });
136
+ expect.unreachable("oke() should throw OKE1070");
137
+ } catch (err) {
138
+ expect(err).toBeInstanceOf(OkeError);
139
+ const okeErr = err as OkeError;
140
+ expect(okeErr.code).toBe(1070);
141
+ expect(okeErr.code).toBe(FLOW_NAME_DUPLICATE.code);
142
+ expect(okeErr.message).toMatch(/OKE1070/);
143
+ }
96
144
  });
97
145
 
98
146
  test("the same flow object bound twice is not a duplicate", () => {
@@ -101,4 +149,41 @@ describe("oke — FLOW_NAME_DUPLICATE OKE1070", () => {
101
149
  on(signal.once("order-placed"), shared);
102
150
  expect(() => oke({ name: "t", autoBoot: false })).not.toThrow();
103
151
  });
152
+
153
+ test("two differently-named flows on the same clock do not fail OKE1070", () => {
154
+ const tick = clock.every("metrics.tick", "1h");
155
+ on(tick, flow("ops.sweep", { do: () => 1 }));
156
+ on(tick, flow("ops.report", { do: () => 2 }));
157
+ expect(() => oke({ name: "t", autoBoot: false })).not.toThrow();
158
+ });
159
+
160
+ test("docs Clock Inline (explicit name) boots", () => {
161
+ on(
162
+ clock.every("health.pingExternal", "30s"),
163
+ flow("health.pingExternal", {
164
+ plane: "operator",
165
+ do: async () => ({ ok: true }),
166
+ }),
167
+ );
168
+ expect(() => oke({ name: "t", autoBoot: false })).not.toThrow();
169
+ });
170
+
171
+ test("docs Clock named-export (shared schedule) boots", () => {
172
+ const tickClock = clock.every("metrics.tick", "1h");
173
+ on(
174
+ tickClock,
175
+ flow("ops.sweep", {
176
+ plane: "operator",
177
+ do: async () => ({ ok: true }),
178
+ }),
179
+ );
180
+ on(
181
+ tickClock,
182
+ flow("ops.report", {
183
+ plane: "operator",
184
+ do: async () => ({ ok: true }),
185
+ }),
186
+ );
187
+ expect(() => oke({ name: "t", autoBoot: false })).not.toThrow();
188
+ });
104
189
  });
@@ -240,9 +240,9 @@ export const flowBrand: unique symbol = Symbol("oke.flow");
240
240
  * {@link call} / `mcp.tool`, not here.
241
241
  *
242
242
  * A nameless `flow({ do })` is stamped `unit.export` by the file-tree
243
- * generator, {@link unit}, or `.adopt()`. Bound to a named Signal or Clock
244
- * with no explicit / tree / export name, it inherits the trigger's name.
245
- * Explicit `flow("notes.get", {…})` still wins.
243
+ * generator, {@link unit}, or `.adopt()`. Explicit `flow("notes.get", {…})`
244
+ * still wins. Signal / Clock consumers with no explicit or tree-derived name
245
+ * fail **OKE1072** at `oke()` / extract — they do not inherit the trigger name.
246
246
  *
247
247
  * @param options - Runtime options and handler (name stamped later)
248
248
  */
package/src/kernel/on.ts CHANGED
@@ -203,11 +203,6 @@ export function on(
203
203
  list.push(normalized);
204
204
  // Stamp runtime carrier for the first bound trigger (type follows BoundTriggerOf).
205
205
  (flowDef as { $trigger: Trigger }).$trigger = normalized;
206
- // `$n` = name inherited from this Signal/Clock (file-tree / unit() may overwrite).
207
- if ((normalized.kind === "signal" || normalized.kind === "clock") && !flowDef.name) {
208
- (flowDef as { name: string; $n?: boolean }).name = normalized.name;
209
- (flowDef as { $n?: boolean }).$n = true;
210
- }
211
206
  stampExposureContract(flowDef as AnyFlowDef, normalized, triggerOrMount);
212
207
  bindings.push({ trigger: normalized, flow: flowDef as AnyFlowDef });
213
208
  return flowDef;
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Once-signal uniqueness (OKE1071) — two different Flows on `signal.once`.
3
+ */
4
+
5
+ import { beforeEach, describe, expect, test } from "bun:test";
6
+ import { resetClocks } from "../elements/clock/declare.ts";
7
+ import { resetSignals, signal } from "../elements/signal/declare.ts";
8
+ import { oke } from "./app.ts";
9
+ import { OkeError } from "./errors.ts";
10
+ import { ONCE_SIGNAL_MULTI_FLOW } from "./errors-once-signal.ts";
11
+ import { flow, resetFlowSeq } from "./flow.ts";
12
+ import { on, resetBindings } from "./on.ts";
13
+
14
+ beforeEach(() => {
15
+ resetBindings();
16
+ resetFlowSeq();
17
+ resetSignals();
18
+ resetClocks();
19
+ });
20
+
21
+ describe("oke — ONCE_SIGNAL_MULTI_FLOW OKE1071", () => {
22
+ test("two differently-named flows on the same once signal fail OKE1071", () => {
23
+ const orderPlaced = signal.once("orders.placed");
24
+ on(orderPlaced, flow("orders.charge", { do: () => ({ a: true }) }));
25
+ on(orderPlaced, flow("orders.ship", { do: () => ({ b: true }) }));
26
+ try {
27
+ oke({ name: "t", autoBoot: false });
28
+ expect.unreachable("oke() should throw OKE1071");
29
+ } catch (err) {
30
+ expect(err).toBeInstanceOf(OkeError);
31
+ const okeErr = err as OkeError;
32
+ expect(okeErr.code).toBe(1071);
33
+ expect(okeErr.code).toBe(ONCE_SIGNAL_MULTI_FLOW.code);
34
+ expect(okeErr.message).toMatch(/OKE1071/);
35
+ expect(okeErr.causeText).toContain("orders.placed");
36
+ expect(okeErr.causeText).toContain("orders.charge");
37
+ expect(okeErr.causeText).toContain("orders.ship");
38
+ expect(okeErr.fix).toMatch(/signal\.broadcast/);
39
+ }
40
+ });
41
+
42
+ test("the same scenario with signal.broadcast does not fail", () => {
43
+ const catalogChanged = signal.broadcast("catalog.changed");
44
+ on(catalogChanged, flow("cache.invalidate", { do: () => ({ a: true }) }));
45
+ on(catalogChanged, flow("search.reindex", { do: () => ({ b: true }) }));
46
+ expect(() => oke({ name: "t", autoBoot: false })).not.toThrow();
47
+ });
48
+
49
+ test("two differently-named flows on signal.live do not fail OKE1071", () => {
50
+ const status = signal.live("order-status", { optional: true });
51
+ on(status, flow("orders.mirrorA", { do: () => ({ a: true }) }));
52
+ on(status, flow("orders.mirrorB", { do: () => ({ b: true }) }));
53
+ expect(() => oke({ name: "t", autoBoot: false })).not.toThrow();
54
+ });
55
+
56
+ test("a single flow bound to once boots with zero warnings", () => {
57
+ const warnings: string[] = [];
58
+ const orig = console.warn;
59
+ console.warn = (...args: unknown[]) => {
60
+ warnings.push(args.map(String).join(" "));
61
+ };
62
+ try {
63
+ const emailTask = signal.once("tasks.email");
64
+ on(emailTask, flow("workers.email", { do: () => ({ ok: true }) }));
65
+ expect(() => oke({ name: "t", autoBoot: false })).not.toThrow();
66
+ expect(warnings).toEqual([]);
67
+ } finally {
68
+ console.warn = orig;
69
+ }
70
+ });
71
+ });
@@ -52,13 +52,14 @@ describe("stampFlowName", () => {
52
52
  expect(f.name).toBe("notes.get");
53
53
  });
54
54
 
55
- test("file-tree stamp overwrites a trigger-inherited name", () => {
55
+ test("file-tree stamp fills a nameless Signal consumer", () => {
56
56
  const bound = on(
57
57
  signal.once("note-created"),
58
58
  flow({
59
59
  do: () => ({ ok: true }),
60
60
  }),
61
61
  );
62
+ expect(bound.name).toBe("");
62
63
  stampFlowName(bound, "notes.onCreated");
63
64
  expect(bound.name).toBe("notes.onCreated");
64
65
  });
@@ -39,21 +39,20 @@ export function stampHttpPath<F extends AnyFlowDef, P extends string>(
39
39
  type FlowNameCarrier = {
40
40
  name: string;
41
41
  unit: string | undefined;
42
- $n?: boolean;
43
42
  };
44
43
 
45
44
  /**
46
- * True when a Flow name is still a placeholder: empty, synthetic `flow_*`,
47
- * or inherited from a Signal/Clock trigger (`$n`). File-tree / `unit()` may overwrite.
45
+ * True when a Flow name is still a placeholder: empty or synthetic `flow_*`.
46
+ * File-tree / `unit()` may overwrite.
48
47
  *
49
48
  * @param flow - Flow name carrier
50
49
  */
51
- export function isUnsetFlowName(flow: { readonly name: string; readonly $n?: boolean }): boolean {
52
- return !flow.name || flow.name.startsWith("flow_") || flow.$n === true;
50
+ export function isUnsetFlowName(flow: { readonly name: string }): boolean {
51
+ return !flow.name || flow.name.startsWith("flow_");
53
52
  }
54
53
 
55
54
  /**
56
- * Stamp `unit.export` onto a nameless (or `flow_*` / trigger-inherited) Flow.
55
+ * Stamp `unit.export` onto a nameless (or `flow_*`) Flow.
57
56
  *
58
57
  * Explicit `flow("notes.get")` wins. Fills `unit` when missing.
59
58
  *
@@ -65,7 +64,6 @@ export function stampFlowName<F extends AnyFlowDef>(flow: F, name: string): F {
65
64
  const f = flow as FlowNameCarrier;
66
65
  if (isUnsetFlowName(f)) {
67
66
  f.name = name;
68
- f.$n = false;
69
67
  }
70
68
  if (!f.unit) {
71
69
  const resolved = f.name || name;
@@ -42,8 +42,7 @@ export function unit<const T extends Record<string, AnyFlowDef>>(
42
42
  for (const [exportName, flowDef] of Object.entries(flows)) {
43
43
  const f = flowDef as FlowDef;
44
44
  if (isUnsetFlowName(f)) {
45
- (f as { name: string; $n?: boolean }).name = `${name}.${exportName}`;
46
- (f as { $n?: boolean }).$n = false;
45
+ (f as { name: string }).name = `${name}.${exportName}`;
47
46
  }
48
47
  if (!f.unit) {
49
48
  (f as { unit: string }).unit = name;