okengine 0.9.1 → 0.10.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/AGENTS.md +1 -1
- package/manifest.v1.schema.json +9 -1
- package/package.json +3 -3
- package/site/content/docs/console/runs.mdx +10 -0
- package/site/content/docs/console/traces.mdx +4 -0
- package/site/content/docs/elements/ai.mdx +54 -32
- package/site/content/docs/elements/channel.mdx +97 -9
- package/site/content/docs/elements/clock.mdx +1 -0
- package/site/content/docs/elements/flow.mdx +42 -8
- package/site/content/docs/get-started/basic-usage.mdx +5 -1
- package/site/content/docs/get-started/installation.mdx +9 -5
- package/site/content/docs/meta.json +2 -0
- package/site/content/docs/providers/azure-redis.mdx +85 -0
- package/site/content/docs/providers/cockroachdb.mdx +86 -0
- package/site/content/docs/providers/digitalocean-caching.mdx +88 -0
- package/site/content/docs/providers/dragonfly-cloud.mdx +82 -0
- package/site/content/docs/providers/elasticache.mdx +92 -0
- package/site/content/docs/providers/index.mdx +101 -0
- package/site/content/docs/providers/memorystore.mdx +89 -0
- package/site/content/docs/providers/meta.json +20 -0
- package/site/content/docs/providers/neon.mdx +115 -0
- package/site/content/docs/providers/redis-cloud.mdx +93 -0
- package/site/content/docs/providers/supabase.mdx +117 -0
- package/site/content/docs/providers/upstash.mdx +91 -0
- package/site/content/docs/providers/yugabytedb.mdx +95 -0
- package/site/content/docs/recipes/caddy.mdx +128 -0
- package/site/content/docs/recipes/cockroachdb.mdx +127 -0
- package/site/content/docs/recipes/dragonfly.mdx +125 -0
- package/site/content/docs/recipes/index.mdx +146 -0
- package/site/content/docs/recipes/llama-cpp.mdx +153 -0
- package/site/content/docs/recipes/mailpit.mdx +136 -0
- package/site/content/docs/recipes/meilisearch.mdx +139 -0
- package/site/content/docs/recipes/meta.json +31 -0
- package/site/content/docs/recipes/ollama.mdx +142 -0
- package/site/content/docs/recipes/openbao.mdx +141 -0
- package/site/content/docs/recipes/pgdog.mdx +137 -0
- package/site/content/docs/recipes/postgres.mdx +136 -0
- package/site/content/docs/recipes/redis.mdx +130 -0
- package/site/content/docs/recipes/rustfs.mdx +148 -0
- package/site/content/docs/recipes/sglang.mdx +105 -0
- package/site/content/docs/recipes/supabase-docker.mdx +145 -0
- package/site/content/docs/recipes/timescale.mdx +129 -0
- package/site/content/docs/recipes/traefik.mdx +136 -0
- package/site/content/docs/recipes/valkey.mdx +121 -0
- package/site/content/docs/recipes/vllm.mdx +106 -0
- package/site/content/docs/recipes/yugabytedb.mdx +135 -0
- package/site/content/docs/reference/cli.md +42 -6
- package/site/content/docs/reference/configuration.mdx +5 -3
- package/site/content/docs/reference/environment-variables.mdx +12 -12
- package/site/content/docs/reference/fx.mdx +38 -4
- package/src/cli/ai-setup/ai-setup.test.ts +32 -12
- package/src/cli/ai-setup/apply.ts +65 -23
- package/src/cli/ai-setup/catalog.ts +1316 -35
- package/src/cli/ai-setup/detect-ollama.ts +47 -0
- package/src/cli/ai-setup/index.ts +44 -7
- package/src/cli/ai-setup/prompts.ts +430 -486
- package/src/cli/ai-setup/recommend.test.ts +150 -50
- package/src/cli/ai-setup/recommend.ts +118 -101
- package/src/cli/ask-seed.test.ts +68 -0
- package/src/cli/ask-seed.ts +79 -0
- package/src/cli/db-seed-redact.test.ts +25 -0
- package/src/cli/db-seed.ts +36 -7
- package/src/cli/db.test.ts +1 -1
- package/src/cli/db.ts +2 -2
- package/src/cli/dev-controls.test.ts +107 -0
- package/src/cli/dev-controls.ts +354 -0
- package/src/cli/dev-schema-sync.ts +12 -4
- package/src/cli/dev.test.ts +50 -30
- package/src/cli/dev.ts +607 -66
- package/src/cli/drizzle-env.test.ts +40 -6
- package/src/cli/drizzle-env.ts +22 -1
- package/src/cli/ensure-drizzle-config.ts +6 -4
- package/src/cli/hero-meta.test.ts +13 -1
- package/src/cli/hero-meta.ts +28 -6
- package/src/cli/index.ts +5 -0
- package/src/cli/registry.ts +24 -0
- package/src/cli/replay.test.ts +105 -0
- package/src/cli/replay.ts +206 -0
- package/src/compiler/extract.ts +3 -3
- package/src/config/index.ts +3 -3
- package/src/console/server/claim.ts +3 -1
- package/src/console/ui/dist/assets/{index-ClX0ZCe5.js → index-D-9KFce5.js} +2 -2
- package/src/console/ui/dist/assets/panel-overview-CW0CQEc2.js +1 -0
- package/src/console/ui/dist/assets/panel-runs-BxuyDI3x.js +1 -0
- package/src/console/ui/dist/assets/{panel-signals-Dsxdu_AR.js → panel-signals-BXofbj7v.js} +1 -1
- package/src/console/ui/dist/assets/{panel-store-hy7O8HOs.js → panel-store-B8mH-arU.js} +1 -1
- package/src/console/ui/dist/assets/{panel-traces-CBiAe5go.js → panel-traces-DSA-ZWnf.js} +1 -1
- package/src/console/ui/dist/index.html +1 -1
- package/src/console/ui/overview/slo.ts +31 -3
- package/src/console/ui/overview/types.ts +6 -0
- package/src/console/ui/runs/errors.test.ts +53 -0
- package/src/console/ui/runs/errors.ts +88 -0
- package/src/console/ui/runs/index.ts +8 -0
- package/src/console/ui/runs/search.ts +16 -0
- package/src/console/ui/shell/panels/runs/RunsPanel.tsx +72 -1
- package/src/docker/ai-model-status.test.ts +101 -0
- package/src/docker/ai-model-status.ts +276 -0
- package/src/docker/compose-health.test.ts +115 -0
- package/src/docker/compose-health.ts +234 -0
- package/src/docker/compose.ts +24 -8
- package/src/docker/derive.ts +35 -9
- package/src/docker/docker.test.ts +304 -12
- package/src/docker/helpers.ts +56 -0
- package/src/docker/index.ts +52 -0
- package/src/docker/ollama-pull.ts +232 -0
- package/src/docker/recipes/cockroach.ts +24 -0
- package/src/docker/recipes/index.ts +32 -0
- package/src/docker/recipes/llama-cpp.ts +273 -0
- package/src/docker/recipes/ollama.ts +19 -23
- package/src/docker/recipes/pgdog.ts +4 -1
- package/src/docker/recipes/postgres.ts +5 -18
- package/src/docker/recipes/sglang.ts +55 -0
- package/src/docker/recipes/supabase.ts +21 -0
- package/src/docker/recipes/timescale.ts +18 -0
- package/src/docker/recipes/vllm.ts +44 -0
- package/src/docker/recipes/yugabyte.ts +23 -0
- package/src/docker/stack-id.test.ts +22 -0
- package/src/docker/stack-id.ts +38 -4
- package/src/docker/types.ts +15 -0
- package/src/drivers/ai-ollama.integration.test.ts +7 -4
- package/src/drivers/signal-engine.ts +10 -0
- package/src/drivers/signal-postgres.ts +24 -2
- package/src/drivers/signal-types.ts +10 -0
- package/src/elements/channel/fallback.test.ts +91 -0
- package/src/elements/channel/hard-bounce.test.ts +82 -0
- package/src/elements/channel/injection.test.ts +153 -0
- package/src/elements/channel/locale-ar.test.ts +102 -0
- package/src/elements/channel/otp-delivery.ts +5 -2
- package/src/elements/channel/receipts-ledger.test.ts +100 -0
- package/src/elements/channel/runtime.ts +24 -1
- package/src/elements/channel/test-helpers.ts +60 -0
- package/src/elements/channel/whatsapp-compliance.test.ts +124 -0
- package/src/elements/store/cache.ts +1 -1
- package/src/elements/store/emit-drizzle.ts +43 -9
- package/src/elements/store/runtime.ts +1 -1
- package/src/elements/store/schema-decl.test.ts +2 -0
- package/src/elements/store/schema-decl.ts +15 -0
- package/src/elements/store/seed.ts +1 -1
- package/src/index.ts +1 -0
- package/src/kernel/app.ts +117 -10
- package/src/kernel/boot-bind/signal.ts +6 -2
- package/src/kernel/boot-bind/store.ts +2 -1
- package/src/kernel/boot.test.ts +6 -5
- package/src/kernel/boot.ts +8 -3
- package/src/kernel/compensate.test.ts +131 -0
- package/src/kernel/correlation.test.ts +108 -0
- package/src/kernel/effect-timing.test.ts +82 -0
- package/src/kernel/flow.ts +38 -0
- package/src/kernel/fx-runs.test.ts +49 -0
- package/src/kernel/fx.test.ts +14 -0
- package/src/kernel/fx.ts +111 -1
- package/src/manifest/types.ts +8 -2
- package/src/runs/collect.ts +3 -0
- package/src/runs/export-otlp.ts +64 -0
- package/src/runs/index.ts +16 -0
- package/src/runs/parquet.ts +9 -0
- package/src/runs/types.ts +5 -0
- package/src/runs/window.test.ts +68 -0
- package/src/runs/window.ts +148 -0
- package/src/runtime/boot-warn.test.ts +45 -0
- package/src/runtime/boot-warn.ts +17 -0
- package/src/runtime/dev-request-log.ts +5 -5
- package/src/term.test.ts +95 -10
- package/src/term.ts +369 -14
- package/src/console/ui/dist/assets/panel-overview-DONDzBd2.js +0 -1
- package/src/console/ui/dist/assets/panel-runs-D5zf-D9c.js +0 -1
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Optional OTLP metrics export shape — additive sink for WideEvents.
|
|
3
|
+
*
|
|
4
|
+
* Not a prerequisite for native alerting (Runs + Clock + Channel).
|
|
5
|
+
* Protocol-named (`otlp`); vendor choice stays in config / images.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { WideEvent } from "./types.ts";
|
|
9
|
+
|
|
10
|
+
/** One OTLP-shaped metric datapoint derived from a wide event. */
|
|
11
|
+
export interface OtlpRunMetric {
|
|
12
|
+
readonly name: "oke.runs" | "oke.run.duration_ms";
|
|
13
|
+
readonly kind: "counter" | "histogram";
|
|
14
|
+
readonly value: number;
|
|
15
|
+
readonly attributes: Readonly<Record<string, string>>;
|
|
16
|
+
readonly timeUnixMs: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Map a WideEvent onto OTLP-friendly metric datapoints.
|
|
21
|
+
*
|
|
22
|
+
* @param event - Appended run
|
|
23
|
+
*/
|
|
24
|
+
export function wideEventToOtlpMetrics(event: WideEvent): readonly OtlpRunMetric[] {
|
|
25
|
+
const attributes = {
|
|
26
|
+
flow: event.flow,
|
|
27
|
+
trigger: event.trigger,
|
|
28
|
+
outcome: event.error ? "error" : "ok",
|
|
29
|
+
...(event.error ? { error_code: event.error.code } : {}),
|
|
30
|
+
};
|
|
31
|
+
return [
|
|
32
|
+
{
|
|
33
|
+
name: "oke.runs",
|
|
34
|
+
kind: "counter",
|
|
35
|
+
value: 1,
|
|
36
|
+
attributes,
|
|
37
|
+
timeUnixMs: event.endedAt,
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
name: "oke.run.duration_ms",
|
|
41
|
+
kind: "histogram",
|
|
42
|
+
value: event.durationMs,
|
|
43
|
+
attributes: { flow: event.flow, trigger: event.trigger },
|
|
44
|
+
timeUnixMs: event.endedAt,
|
|
45
|
+
},
|
|
46
|
+
];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Pluggable sink for teams with an existing Grafana / Datadog stack. */
|
|
50
|
+
export type OtlpMetricsSink = (metrics: readonly OtlpRunMetric[]) => void | Promise<void>;
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Fan-out helper: call after `runs.record` when an OTLP sink is configured.
|
|
54
|
+
*
|
|
55
|
+
* @param event - Just-recorded wide event
|
|
56
|
+
* @param sink - Optional exporter
|
|
57
|
+
*/
|
|
58
|
+
export async function exportRunMetrics(
|
|
59
|
+
event: WideEvent,
|
|
60
|
+
sink: OtlpMetricsSink | undefined,
|
|
61
|
+
): Promise<void> {
|
|
62
|
+
if (!sink) return;
|
|
63
|
+
await sink(wideEventToOtlpMetrics(event));
|
|
64
|
+
}
|
package/src/runs/index.ts
CHANGED
|
@@ -68,5 +68,21 @@ export {
|
|
|
68
68
|
|
|
69
69
|
export { privacyErase, type PrivacyEraseOptions, type PrivacyEraseResult } from "./privacy.ts";
|
|
70
70
|
|
|
71
|
+
export {
|
|
72
|
+
evaluateSloBreaches,
|
|
73
|
+
parseLatencyMs,
|
|
74
|
+
percentile,
|
|
75
|
+
windowStatsForFlow,
|
|
76
|
+
type RunWindowStats,
|
|
77
|
+
type SloBreach,
|
|
78
|
+
} from "./window.ts";
|
|
79
|
+
|
|
80
|
+
export {
|
|
81
|
+
exportRunMetrics,
|
|
82
|
+
wideEventToOtlpMetrics,
|
|
83
|
+
type OtlpMetricsSink,
|
|
84
|
+
type OtlpRunMetric,
|
|
85
|
+
} from "./export-otlp.ts";
|
|
86
|
+
|
|
71
87
|
export { createRunsPostgresFake } from "./drivers/postgres.ts";
|
|
72
88
|
export { createRunsClickHouseFake } from "./drivers/clickhouse.ts";
|
package/src/runs/parquet.ts
CHANGED
|
@@ -42,6 +42,7 @@ export function wideEventToRow(event: WideEvent): ParquetRow {
|
|
|
42
42
|
build_version: event.buildVersion ?? null,
|
|
43
43
|
error_code: event.error?.code ?? null,
|
|
44
44
|
error_message: event.error?.message ?? null,
|
|
45
|
+
input: event.input === undefined ? null : JSON.stringify(event.input),
|
|
45
46
|
effects: JSON.stringify(event.effects),
|
|
46
47
|
logs: JSON.stringify(event.logs),
|
|
47
48
|
duration_ms: event.durationMs,
|
|
@@ -103,6 +104,14 @@ export function rowToWideEvent(row: Record<string, unknown>): WideEvent {
|
|
|
103
104
|
...(row.error_message != null ? { message: String(row.error_message) } : {}),
|
|
104
105
|
}
|
|
105
106
|
: null,
|
|
107
|
+
...(row.input != null && row.input !== ""
|
|
108
|
+
? {
|
|
109
|
+
input:
|
|
110
|
+
typeof row.input === "string"
|
|
111
|
+
? (JSON.parse(row.input) as unknown)
|
|
112
|
+
: (row.input as unknown),
|
|
113
|
+
}
|
|
114
|
+
: {}),
|
|
106
115
|
effects: parseJsonArray(row.effects) as WideEvent["effects"],
|
|
107
116
|
logs: parseJsonArray(row.logs) as WideEvent["logs"],
|
|
108
117
|
durationMs: Number(row.duration_ms ?? 0),
|
package/src/runs/types.ts
CHANGED
|
@@ -69,6 +69,11 @@ export interface WideEvent {
|
|
|
69
69
|
readonly buildVersion?: string;
|
|
70
70
|
/** Typed error when the flow failed. */
|
|
71
71
|
readonly error?: RunError | null;
|
|
72
|
+
/**
|
|
73
|
+
* Validated flow input snapshot for local replay (`oke replay`).
|
|
74
|
+
* Declared personal fields may be crypto-shredded into {@link archived}.
|
|
75
|
+
*/
|
|
76
|
+
readonly input?: unknown;
|
|
72
77
|
/** Effect ledger snapshot. */
|
|
73
78
|
readonly effects: readonly EffectEntry[];
|
|
74
79
|
/**
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import type { WideEvent } from "./types.ts";
|
|
3
|
+
import { evaluateSloBreaches, parseLatencyMs, windowStatsForFlow } from "./window.ts";
|
|
4
|
+
|
|
5
|
+
function ev(
|
|
6
|
+
partial: Partial<WideEvent> & Pick<WideEvent, "id" | "durationMs" | "startedAt">,
|
|
7
|
+
): WideEvent {
|
|
8
|
+
return {
|
|
9
|
+
flow: "checkout.create",
|
|
10
|
+
trigger: "http",
|
|
11
|
+
plane: "user",
|
|
12
|
+
gates: [],
|
|
13
|
+
cache: "none",
|
|
14
|
+
error: null,
|
|
15
|
+
effects: [],
|
|
16
|
+
logs: [],
|
|
17
|
+
endedAt: partial.startedAt + partial.durationMs,
|
|
18
|
+
dimensions: {},
|
|
19
|
+
...partial,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
describe("runs window stats", () => {
|
|
24
|
+
test("parseLatencyMs", () => {
|
|
25
|
+
expect(parseLatencyMs("200ms")).toBe(200);
|
|
26
|
+
expect(parseLatencyMs("1s")).toBe(1000);
|
|
27
|
+
expect(parseLatencyMs("bad")).toBeNull();
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test("computes p95 and error rate", () => {
|
|
31
|
+
const now = 10_000;
|
|
32
|
+
const events = [
|
|
33
|
+
ev({ id: "1", durationMs: 10, startedAt: now - 1000 }),
|
|
34
|
+
ev({ id: "2", durationMs: 20, startedAt: now - 900 }),
|
|
35
|
+
ev({ id: "3", durationMs: 30, startedAt: now - 800 }),
|
|
36
|
+
ev({ id: "4", durationMs: 40, startedAt: now - 700 }),
|
|
37
|
+
ev({
|
|
38
|
+
id: "5",
|
|
39
|
+
durationMs: 200,
|
|
40
|
+
startedAt: now - 600,
|
|
41
|
+
error: { code: "Timeout" },
|
|
42
|
+
}),
|
|
43
|
+
];
|
|
44
|
+
const stats = windowStatsForFlow(events, "checkout.create", now, 5 * 60_000);
|
|
45
|
+
expect(stats.total).toBe(5);
|
|
46
|
+
expect(stats.errors).toBe(1);
|
|
47
|
+
expect(stats.errorRate).toBeCloseTo(0.2);
|
|
48
|
+
expect(stats.p95Ms).toBe(200);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test("evaluateSloBreaches flags latency and availability", () => {
|
|
52
|
+
const stats = windowStatsForFlow(
|
|
53
|
+
[
|
|
54
|
+
ev({ id: "1", durationMs: 500, startedAt: 9000, error: { code: "X" } }),
|
|
55
|
+
ev({ id: "2", durationMs: 500, startedAt: 9100 }),
|
|
56
|
+
],
|
|
57
|
+
"checkout.create",
|
|
58
|
+
10_000,
|
|
59
|
+
5_000,
|
|
60
|
+
);
|
|
61
|
+
const breaches = evaluateSloBreaches(stats, {
|
|
62
|
+
availability: "99.9%",
|
|
63
|
+
latency: { p95: "100ms" },
|
|
64
|
+
});
|
|
65
|
+
expect(breaches.some((b) => b.kind === "availability")).toBe(true);
|
|
66
|
+
expect(breaches.some((b) => b.kind === "latency_p95")).toBe(true);
|
|
67
|
+
});
|
|
68
|
+
});
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rolling window metrics over WideEvents — shared by Console Overview and
|
|
3
|
+
* Flow-facing `fx.runs` checkers (native alerting without `fx.metric`).
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { WideEvent } from "./types.ts";
|
|
7
|
+
|
|
8
|
+
/** One flow's rolling stats over a time window. */
|
|
9
|
+
export interface RunWindowStats {
|
|
10
|
+
readonly flow: string;
|
|
11
|
+
readonly total: number;
|
|
12
|
+
readonly errors: number;
|
|
13
|
+
readonly errorRate: number;
|
|
14
|
+
readonly p50Ms: number;
|
|
15
|
+
readonly p95Ms: number;
|
|
16
|
+
readonly p99Ms: number;
|
|
17
|
+
readonly from: number;
|
|
18
|
+
readonly to: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Percentile of a sorted numeric array.
|
|
23
|
+
*
|
|
24
|
+
* @param sorted - Ascending durations
|
|
25
|
+
* @param p - Percentile in (0, 1]
|
|
26
|
+
*/
|
|
27
|
+
export function percentile(sorted: readonly number[], p: number): number {
|
|
28
|
+
if (sorted.length === 0) return 0;
|
|
29
|
+
if (sorted.length === 1) return sorted[0]!;
|
|
30
|
+
const idx = Math.min(sorted.length - 1, Math.max(0, Math.ceil(p * sorted.length) - 1));
|
|
31
|
+
return sorted[idx]!;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Parse a Manifest latency string like `200ms` / `1s` into milliseconds.
|
|
36
|
+
*
|
|
37
|
+
* @param raw - Latency threshold
|
|
38
|
+
*/
|
|
39
|
+
export function parseLatencyMs(raw: string | undefined): number | null {
|
|
40
|
+
if (!raw) return null;
|
|
41
|
+
const match = /^(\d+(?:\.\d+)?)\s*(ms|s|m)?$/i.exec(raw.trim());
|
|
42
|
+
if (!match) return null;
|
|
43
|
+
const n = Number(match[1]);
|
|
44
|
+
if (!Number.isFinite(n) || n < 0) return null;
|
|
45
|
+
const unit = (match[2] ?? "ms").toLowerCase();
|
|
46
|
+
if (unit === "s") return Math.round(n * 1000);
|
|
47
|
+
if (unit === "m") return Math.round(n * 60_000);
|
|
48
|
+
return Math.round(n);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Compute rolling stats for one flow over `[to - windowMs, to]`.
|
|
53
|
+
*
|
|
54
|
+
* @param events - Wide events
|
|
55
|
+
* @param flow - Flow name filter
|
|
56
|
+
* @param to - Window end (epoch-ms)
|
|
57
|
+
* @param windowMs - Window length
|
|
58
|
+
*/
|
|
59
|
+
export function windowStatsForFlow(
|
|
60
|
+
events: readonly WideEvent[],
|
|
61
|
+
flow: string,
|
|
62
|
+
to: number,
|
|
63
|
+
windowMs: number,
|
|
64
|
+
): RunWindowStats {
|
|
65
|
+
const from = to - Math.max(0, windowMs);
|
|
66
|
+
const mine = events.filter((e) => e.flow === flow && e.startedAt >= from && e.startedAt <= to);
|
|
67
|
+
const durations = mine.map((e) => e.durationMs).sort((a, b) => a - b);
|
|
68
|
+
const errors = mine.filter((e) => e.error != null).length;
|
|
69
|
+
const total = mine.length;
|
|
70
|
+
return {
|
|
71
|
+
flow,
|
|
72
|
+
total,
|
|
73
|
+
errors,
|
|
74
|
+
errorRate: total === 0 ? 0 : errors / total,
|
|
75
|
+
p50Ms: percentile(durations, 0.5),
|
|
76
|
+
p95Ms: percentile(durations, 0.95),
|
|
77
|
+
p99Ms: percentile(durations, 0.99),
|
|
78
|
+
from,
|
|
79
|
+
to,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Breach report for a declared latency / availability SLO. */
|
|
84
|
+
export interface SloBreach {
|
|
85
|
+
readonly flow: string;
|
|
86
|
+
readonly kind: "availability" | "latency_p95" | "latency_p99";
|
|
87
|
+
readonly threshold: number;
|
|
88
|
+
readonly observed: number;
|
|
89
|
+
readonly sampleCount: number;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Evaluate Manifest-style SLO thresholds against window stats.
|
|
94
|
+
*
|
|
95
|
+
* @param stats - Window stats
|
|
96
|
+
* @param slo - Declared availability + latency
|
|
97
|
+
*/
|
|
98
|
+
export function evaluateSloBreaches(
|
|
99
|
+
stats: RunWindowStats,
|
|
100
|
+
slo: {
|
|
101
|
+
readonly availability?: string;
|
|
102
|
+
readonly latency?: { readonly p95?: string; readonly p99?: string };
|
|
103
|
+
},
|
|
104
|
+
): SloBreach[] {
|
|
105
|
+
const out: SloBreach[] = [];
|
|
106
|
+
if (stats.total === 0) return out;
|
|
107
|
+
|
|
108
|
+
if (slo.availability) {
|
|
109
|
+
const match = /^(\d+(?:\.\d+)?)\s*%?$/.exec(slo.availability.trim());
|
|
110
|
+
if (match) {
|
|
111
|
+
const pct = Number(match[1]);
|
|
112
|
+
const tolerable = Math.max(0, 1 - pct / 100);
|
|
113
|
+
if (stats.errorRate > tolerable) {
|
|
114
|
+
out.push({
|
|
115
|
+
flow: stats.flow,
|
|
116
|
+
kind: "availability",
|
|
117
|
+
threshold: tolerable,
|
|
118
|
+
observed: stats.errorRate,
|
|
119
|
+
sampleCount: stats.total,
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const p95 = parseLatencyMs(slo.latency?.p95);
|
|
126
|
+
if (p95 != null && stats.p95Ms > p95) {
|
|
127
|
+
out.push({
|
|
128
|
+
flow: stats.flow,
|
|
129
|
+
kind: "latency_p95",
|
|
130
|
+
threshold: p95,
|
|
131
|
+
observed: stats.p95Ms,
|
|
132
|
+
sampleCount: stats.total,
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const p99 = parseLatencyMs(slo.latency?.p99);
|
|
137
|
+
if (p99 != null && stats.p99Ms > p99) {
|
|
138
|
+
out.push({
|
|
139
|
+
flow: stats.flow,
|
|
140
|
+
kind: "latency_p99",
|
|
141
|
+
threshold: p99,
|
|
142
|
+
observed: stats.p99Ms,
|
|
143
|
+
sampleCount: stats.total,
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return out;
|
|
148
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Boot honesty suppress flag for the `oke dev` Backend child.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { afterEach, describe, expect, test } from "bun:test";
|
|
6
|
+
import { emitBootWarn } from "./boot-warn.ts";
|
|
7
|
+
|
|
8
|
+
describe("emitBootWarn", () => {
|
|
9
|
+
const prev = process.env["OKE_SUPPRESS_BOOT_WARN"];
|
|
10
|
+
|
|
11
|
+
afterEach(() => {
|
|
12
|
+
if (prev === undefined) delete process.env["OKE_SUPPRESS_BOOT_WARN"];
|
|
13
|
+
else process.env["OKE_SUPPRESS_BOOT_WARN"] = prev;
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
test("forwards to console.warn by default", () => {
|
|
17
|
+
delete process.env["OKE_SUPPRESS_BOOT_WARN"];
|
|
18
|
+
const lines: string[] = [];
|
|
19
|
+
const original = console.warn;
|
|
20
|
+
console.warn = (...args: unknown[]) => {
|
|
21
|
+
lines.push(args.map(String).join(" "));
|
|
22
|
+
};
|
|
23
|
+
try {
|
|
24
|
+
emitBootWarn("oke boot: test notice");
|
|
25
|
+
expect(lines).toEqual(["oke boot: test notice"]);
|
|
26
|
+
} finally {
|
|
27
|
+
console.warn = original;
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test("no-ops when OKE_SUPPRESS_BOOT_WARN=1", () => {
|
|
32
|
+
process.env["OKE_SUPPRESS_BOOT_WARN"] = "1";
|
|
33
|
+
const lines: string[] = [];
|
|
34
|
+
const original = console.warn;
|
|
35
|
+
console.warn = (...args: unknown[]) => {
|
|
36
|
+
lines.push(args.map(String).join(" "));
|
|
37
|
+
};
|
|
38
|
+
try {
|
|
39
|
+
emitBootWarn("oke boot: hidden");
|
|
40
|
+
expect(lines).toEqual([]);
|
|
41
|
+
} finally {
|
|
42
|
+
console.warn = original;
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
});
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Boot honesty lines (`oke boot: …`) — once per process, suppressible in
|
|
3
|
+
* the `oke dev` Backend child so the parent Console owns the notice.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Emit a boot warning unless suppressed for this process.
|
|
8
|
+
*
|
|
9
|
+
* Set `OKE_SUPPRESS_BOOT_WARN=1` on the Backend child during `oke dev` so
|
|
10
|
+
* Signal / Channel / files process-local notices print once (parent only).
|
|
11
|
+
*
|
|
12
|
+
* @param message - Full warning text (usually prefixed `oke boot:`)
|
|
13
|
+
*/
|
|
14
|
+
export function emitBootWarn(message: string): void {
|
|
15
|
+
if (process.env["OKE_SUPPRESS_BOOT_WARN"] === "1") return;
|
|
16
|
+
console.warn(message);
|
|
17
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Dev-only request lines for `oke dev` (
|
|
2
|
+
* Dev-only request lines for `oke dev` (Backend / Console / MCP).
|
|
3
3
|
*
|
|
4
4
|
* Gated by `OKE_DEV_REQUEST_LOG=1`. Surfaces share one TTY; label + color
|
|
5
5
|
* keep streams readable without a multiplexer.
|
|
@@ -18,22 +18,22 @@ export function shouldLogDevRequests(): boolean {
|
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
/**
|
|
21
|
-
* Active surface for this async context, or `OKE_DEV_SURFACE`, or `
|
|
21
|
+
* Active surface for this async context, or `OKE_DEV_SURFACE`, or `Backend`.
|
|
22
22
|
*/
|
|
23
23
|
export function currentDevSurface(): DevLogSurface {
|
|
24
24
|
const fromAls = surfaceAls.getStore();
|
|
25
25
|
if (fromAls) return fromAls;
|
|
26
26
|
const fromEnv = process.env.OKE_DEV_SURFACE;
|
|
27
|
-
if (fromEnv === "
|
|
27
|
+
if (fromEnv === "Backend" || fromEnv === "Console" || fromEnv === "MCP") {
|
|
28
28
|
return fromEnv;
|
|
29
29
|
}
|
|
30
|
-
return "
|
|
30
|
+
return "Backend";
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
/**
|
|
34
34
|
* Run a handler tagged with a surface (Console wrap around `app.fetch`).
|
|
35
35
|
*
|
|
36
|
-
* @param surface -
|
|
36
|
+
* @param surface - Backend · Console · MCP
|
|
37
37
|
* @param fn - Async work
|
|
38
38
|
*/
|
|
39
39
|
export function runWithDevSurface<T>(
|
package/src/term.test.ts
CHANGED
|
@@ -5,7 +5,9 @@
|
|
|
5
5
|
import { describe, expect, test } from "bun:test";
|
|
6
6
|
import {
|
|
7
7
|
formatAppReadyLine,
|
|
8
|
+
formatBootWarn,
|
|
8
9
|
formatClaimNote,
|
|
10
|
+
formatCliChrome,
|
|
9
11
|
formatDevBanner,
|
|
10
12
|
formatDevHero,
|
|
11
13
|
formatDevLogSeparator,
|
|
@@ -13,6 +15,11 @@ import {
|
|
|
13
15
|
formatServiceLine,
|
|
14
16
|
formatRequestLine,
|
|
15
17
|
formatStackSummary,
|
|
18
|
+
countTermLines,
|
|
19
|
+
createAnchoredBoard,
|
|
20
|
+
createBootProgress,
|
|
21
|
+
createRewritableBlock,
|
|
22
|
+
formatStatusDot,
|
|
16
23
|
formatStatusLine,
|
|
17
24
|
termStyle,
|
|
18
25
|
} from "./term.ts";
|
|
@@ -38,9 +45,10 @@ describe("term", () => {
|
|
|
38
45
|
runtimeEnv: "local",
|
|
39
46
|
system: "darwin 25.4.0 · bun 1.3.14",
|
|
40
47
|
elements: [
|
|
41
|
-
{ element: "flow", detail: "
|
|
42
|
-
{ element: "store", detail: "sql postgres · kv redis" },
|
|
43
|
-
{ element: "signal", detail: "memory" },
|
|
48
|
+
{ element: "flow", detail: "", status: "ready" },
|
|
49
|
+
{ element: "store", detail: "sql postgres · kv redis", status: "ready" },
|
|
50
|
+
{ element: "signal", detail: "memory", status: "ready" },
|
|
51
|
+
{ element: "ai", detail: "openai-compatible · smollm2", status: "pending" },
|
|
44
52
|
],
|
|
45
53
|
});
|
|
46
54
|
expect(out).toContain("oke dev v0.2.4");
|
|
@@ -53,11 +61,65 @@ describe("term", () => {
|
|
|
53
61
|
expect(out).toContain("elements");
|
|
54
62
|
expect(out).toContain("store");
|
|
55
63
|
expect(out).toContain("postgres");
|
|
64
|
+
expect(out).toContain("● flow");
|
|
65
|
+
expect(out).toContain("● ai");
|
|
56
66
|
expect(out).not.toContain("on(Trigger)");
|
|
57
67
|
expect(out).not.toContain("O·K·E");
|
|
58
68
|
expect(out).not.toMatch(/\u001b\[/);
|
|
59
69
|
});
|
|
60
70
|
|
|
71
|
+
test("formatStatusDot colors by status", () => {
|
|
72
|
+
const s = termStyle(true);
|
|
73
|
+
expect(formatStatusDot("ready", true)).toBe(`${s.green}●${s.reset}`);
|
|
74
|
+
expect(formatStatusDot("pending", true)).toBe(`${s.yellow}●${s.reset}`);
|
|
75
|
+
expect(formatStatusDot("error", true)).toBe(`${s.red}●${s.reset}`);
|
|
76
|
+
expect(formatStatusDot("idle", true)).toBe(`${s.dim}●${s.reset}`);
|
|
77
|
+
expect(formatStatusDot("ready", false)).toBe("●");
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test("createRewritableBlock appends when disabled", () => {
|
|
81
|
+
let out = "";
|
|
82
|
+
const block = createRewritableBlock((t) => {
|
|
83
|
+
out += t;
|
|
84
|
+
}, false);
|
|
85
|
+
block.paint("a\nb\n");
|
|
86
|
+
block.paint("c\n");
|
|
87
|
+
expect(out).toBe("a\nb\nc\n");
|
|
88
|
+
expect(countTermLines("a\nb\n")).toBe(2);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test("createBootProgress replaces keyed rows then clears", () => {
|
|
92
|
+
let out = "";
|
|
93
|
+
const progress = createBootProgress((t) => {
|
|
94
|
+
out += t;
|
|
95
|
+
}, false);
|
|
96
|
+
progress.set("compose", "compose…\n");
|
|
97
|
+
progress.set("compose", "compose ok\n");
|
|
98
|
+
progress.set("vault", "vault ok\n");
|
|
99
|
+
// Non-TTY appends each set (including replacements).
|
|
100
|
+
expect(out).toContain("compose ok\n");
|
|
101
|
+
expect(out).toContain("vault ok\n");
|
|
102
|
+
progress.clear();
|
|
103
|
+
// clear is a no-op on the stream when rewrite is disabled
|
|
104
|
+
expect(out).toContain("vault ok\n");
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test("createAnchoredBoard wrapWrite tracks below output", () => {
|
|
108
|
+
let out = "";
|
|
109
|
+
const board = createAnchoredBoard((t) => {
|
|
110
|
+
out += t;
|
|
111
|
+
}, false);
|
|
112
|
+
board.paint("board\n");
|
|
113
|
+
const w = board.wrapWrite((t) => {
|
|
114
|
+
out += t;
|
|
115
|
+
});
|
|
116
|
+
w("below\n");
|
|
117
|
+
board.paint("board2\n"); // non-TTY: no rewrite append
|
|
118
|
+
expect(out).toContain("board\n");
|
|
119
|
+
expect(out).toContain("below\n");
|
|
120
|
+
board.stop();
|
|
121
|
+
});
|
|
122
|
+
|
|
61
123
|
test("formatServiceLine / formatAppReadyLine include URL", () => {
|
|
62
124
|
expect(formatServiceLine("Console", "http://127.0.0.1:6533", false)).toContain(
|
|
63
125
|
"http://127.0.0.1:6533",
|
|
@@ -65,7 +127,7 @@ describe("term", () => {
|
|
|
65
127
|
expect(formatAppReadyLine("http://127.0.0.1:6530", false)).toContain("http://127.0.0.1:6530");
|
|
66
128
|
});
|
|
67
129
|
|
|
68
|
-
test("formatDevHero keeps
|
|
130
|
+
test("formatDevHero keeps Backend Console MCP URLs", () => {
|
|
69
131
|
const out = formatDevHero({
|
|
70
132
|
appUrl: "http://127.0.0.1:6530",
|
|
71
133
|
consoleUrl: "http://127.0.0.1:6533",
|
|
@@ -73,12 +135,12 @@ describe("term", () => {
|
|
|
73
135
|
profile: "local",
|
|
74
136
|
runtimeEnv: "local",
|
|
75
137
|
system: "darwin 25.4.0 · bun 1.3.14",
|
|
76
|
-
elements: [{ element: "flow", detail: "
|
|
138
|
+
elements: [{ element: "flow", detail: "", status: "ready" }],
|
|
77
139
|
color: false,
|
|
78
140
|
});
|
|
79
141
|
expect(out).toContain("oke dev");
|
|
80
142
|
expect(out).toContain("██");
|
|
81
|
-
expect(out).toContain("
|
|
143
|
+
expect(out).toContain("Backend");
|
|
82
144
|
expect(out).toContain("Console");
|
|
83
145
|
expect(out).toContain("MCP");
|
|
84
146
|
expect(out).toContain("http://127.0.0.1:6530");
|
|
@@ -106,10 +168,27 @@ describe("term", () => {
|
|
|
106
168
|
expect(formatStatusLine("stack up (sql)", false)).toContain("stack up");
|
|
107
169
|
});
|
|
108
170
|
|
|
171
|
+
test("formatBootWarn wraps under Notice chrome", () => {
|
|
172
|
+
const out = formatBootWarn(
|
|
173
|
+
"oke boot: Channel suppression defaults to process-local memory for multi-instance",
|
|
174
|
+
false,
|
|
175
|
+
);
|
|
176
|
+
expect(out).toContain("Notice");
|
|
177
|
+
expect(out).toContain("process-local");
|
|
178
|
+
expect(out).not.toContain("oke boot:");
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
test("formatCliChrome routes boot and status lines", () => {
|
|
182
|
+
const out = formatCliChrome("oke db seed: ok\noke boot: hello world\n", false);
|
|
183
|
+
expect(out).toContain("oke db seed: ok");
|
|
184
|
+
expect(out).toContain("Notice");
|
|
185
|
+
expect(out).toContain("hello world");
|
|
186
|
+
});
|
|
187
|
+
|
|
109
188
|
test("formatRequestLine shows date, time, surface, flow, ms, status", () => {
|
|
110
189
|
const at = new Date(2026, 6, 26, 3, 11, 42);
|
|
111
190
|
const out = formatRequestLine({
|
|
112
|
-
surface: "
|
|
191
|
+
surface: "Backend",
|
|
113
192
|
method: "GET",
|
|
114
193
|
path: "/health",
|
|
115
194
|
flow: "main.health",
|
|
@@ -120,7 +199,7 @@ describe("term", () => {
|
|
|
120
199
|
});
|
|
121
200
|
expect(out).toContain("2026-07-26");
|
|
122
201
|
expect(out).toContain("03:11:42");
|
|
123
|
-
expect(out).toContain("
|
|
202
|
+
expect(out).toContain("Backend");
|
|
124
203
|
expect(out).toContain("200");
|
|
125
204
|
expect(out).not.toMatch(/\u001b\[/);
|
|
126
205
|
});
|
|
@@ -144,14 +223,20 @@ describe("term", () => {
|
|
|
144
223
|
const out = formatStackSummary({
|
|
145
224
|
project: "oke-dev-a3f791",
|
|
146
225
|
services: [
|
|
147
|
-
{ label: "postgres", hostPort: 15975 },
|
|
148
|
-
{ label: "redis", hostPort: 16975 },
|
|
226
|
+
{ label: "postgres", hostPort: 15975, status: "ready" },
|
|
227
|
+
{ label: "redis", hostPort: 16975, status: "ready" },
|
|
228
|
+
{ label: "ai", hostPort: 23975, detail: "gemma4:e4b-q4_K_M", status: "pending" },
|
|
149
229
|
],
|
|
150
230
|
appDrivers: ["postgres", "redis"],
|
|
151
231
|
color: false,
|
|
152
232
|
});
|
|
153
233
|
expect(out).toContain("Docker");
|
|
154
234
|
expect(out).toContain(":15975");
|
|
235
|
+
expect(out).toContain("gemma4:e4b-q4_K_M");
|
|
236
|
+
expect(out).toContain("● postgres");
|
|
237
|
+
expect(out).toContain("● ai");
|
|
238
|
+
// Blank │ row separates the Docker header from whatever sits above.
|
|
239
|
+
expect(out).toMatch(/│\n◇ {2}Docker/);
|
|
155
240
|
expect(out).not.toMatch(/\u001b\[/);
|
|
156
241
|
});
|
|
157
242
|
});
|