@warmdrift/kgauto-compiler 2.0.0-alpha.6 → 2.0.0-alpha.60

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/README.md +95 -0
  2. package/dist/brain-proxy.d.mts +113 -0
  3. package/dist/brain-proxy.d.ts +113 -0
  4. package/dist/brain-proxy.js +189 -0
  5. package/dist/brain-proxy.mjs +6 -0
  6. package/dist/chunk-IIMPJZNH.mjs +694 -0
  7. package/dist/chunk-IUWFML6Z.mjs +165 -0
  8. package/dist/chunk-NBO4R5PC.mjs +313 -0
  9. package/dist/chunk-P3TOAEG4.mjs +56 -0
  10. package/dist/chunk-QKXTMVCT.mjs +1470 -0
  11. package/dist/chunk-RO22VFIF.mjs +29 -0
  12. package/dist/chunk-RQ3BUUB6.mjs +190 -0
  13. package/dist/glassbox/index.d.mts +59 -0
  14. package/dist/glassbox/index.d.ts +59 -0
  15. package/dist/glassbox/index.js +312 -0
  16. package/dist/glassbox/index.mjs +12 -0
  17. package/dist/glassbox-routes/format.d.mts +24 -0
  18. package/dist/glassbox-routes/format.d.ts +24 -0
  19. package/dist/glassbox-routes/format.js +86 -0
  20. package/dist/glassbox-routes/format.mjs +18 -0
  21. package/dist/glassbox-routes/index.d.mts +191 -0
  22. package/dist/glassbox-routes/index.d.ts +191 -0
  23. package/dist/glassbox-routes/index.js +2714 -0
  24. package/dist/glassbox-routes/index.mjs +663 -0
  25. package/dist/glassbox-routes/react/index.d.mts +74 -0
  26. package/dist/glassbox-routes/react/index.d.ts +74 -0
  27. package/dist/glassbox-routes/react/index.js +819 -0
  28. package/dist/glassbox-routes/react/index.mjs +754 -0
  29. package/dist/index.d.mts +2175 -17
  30. package/dist/index.d.ts +2175 -17
  31. package/dist/index.js +7248 -1597
  32. package/dist/index.mjs +3428 -164
  33. package/dist/ir-B2h0GAEL.d.ts +1378 -0
  34. package/dist/ir-BC4uDL98.d.mts +1378 -0
  35. package/dist/key-health.d.mts +131 -0
  36. package/dist/key-health.d.ts +131 -0
  37. package/dist/key-health.js +215 -0
  38. package/dist/key-health.mjs +6 -0
  39. package/dist/profiles.d.mts +292 -2
  40. package/dist/profiles.d.ts +292 -2
  41. package/dist/profiles.js +1081 -16
  42. package/dist/profiles.mjs +9 -1
  43. package/dist/types-B-pzBJKf.d.mts +149 -0
  44. package/dist/types-CKuu7Clz.d.ts +149 -0
  45. package/dist/types-CRy90cjp.d.mts +131 -0
  46. package/dist/types-CsB2YASc.d.ts +131 -0
  47. package/package.json +52 -6
  48. package/dist/chunk-MBEI5UOM.mjs +0 -409
  49. package/dist/profiles-CQnLkQ7b.d.ts +0 -611
  50. package/dist/profiles-zm6diETo.d.mts +0 -611
package/README.md CHANGED
@@ -89,6 +89,101 @@ await record({
89
89
  });
90
90
  ```
91
91
 
92
+ ### `probeShadow()` — measure a swap candidate from an AI-SDK route (alpha.48)
93
+
94
+ `call()` runs a full-IR shadow probe internally (`shadowProbe` option). AI-SDK consumers who drive `streamText()` themselves never call `call()`, so they reach the same probe via the standalone `probeShadow()` export — fire it from `onFinish`, passing the served leg you already have, wrapped in `waitUntil()`/`after()` so it never blocks the response.
95
+
96
+ ```ts
97
+ import { probeShadow, configureBrain } from '@warmdrift/kgauto-compiler';
98
+ import { after } from 'next/server'; // or the platform's waitUntil()
99
+
100
+ configureBrain({ endpoint: 'https://your-app.com/api/kgauto/v2', apiKey: '...' });
101
+
102
+ const t0 = Date.now();
103
+ const result = streamText({
104
+ model, system, messages,
105
+ onFinish: ({ text, usage }) => {
106
+ after(() => probeShadow(ir, {
107
+ served: {
108
+ model: servedModelId,
109
+ responseText: text,
110
+ tokensIn: usage.promptTokens,
111
+ tokensOut: usage.completionTokens,
112
+ latencyMs: Date.now() - t0,
113
+ },
114
+ candidates: ['deepseek-v4-pro'], // what you'd consider swapping to
115
+ sampleRate: 0.05, // pin 1 for a dogfood window
116
+ }));
117
+ },
118
+ });
119
+ ```
120
+
121
+ kgauto runs each candidate on the **same IR** and persists a `probe_outcomes` row (`replay_source='inline-full-ir'`, `prompt_fidelity=1.0`, both latency legs). Phase 1 is `judge:'off'` — rows accumulate for an offline verdict rollup; inline Opus judging is Phase 2. Fire-and-forget: never throws into your `onFinish`.
122
+
123
+ #### Sync-mode latency protection (`call()` + `BrainConfig.sync`)
124
+
125
+ A `probeShadow()` consumer fires from `after()`, so the probe is always off the user-facing path — slow candidates are fine there (it's the right place to evaluate a slow reasoner). But a **`call()`-inline** consumer that sets `BrainConfig.sync: true` (PB-class Edge routes, so the brain write reliably lands before teardown — L-086) *awaits* the probe, putting the candidate's latency on the user's critical path. A `latency_tier='slow'` candidate (deepseek-v4-pro ~78–84s) then blows the route's time budget and the user sees an empty response. Two knobs on `shadowProbe` protect the sync path — **both are no-ops off the critical path** (non-sync `call()` and every `probeShadow()` consumer):
126
+
127
+ ```ts
128
+ await call(ir, {
129
+ shadowProbe: {
130
+ candidates: ['deepseek-v4-pro'],
131
+ sampleRate: 1,
132
+ maxLatencyMs: 15000, // default 15000. Sync-mode budget: if the probe
133
+ // overruns, abort the candidate, return the served
134
+ // response NOW, record outcome='aborted_latency_budget'.
135
+ skipSlowTierInSync: true, // default true. Don't even START a latency_tier='slow'
136
+ // candidate inline; record outcome='skipped_slow_tier_sync'.
137
+ },
138
+ });
139
+ ```
140
+
141
+ Aborted/skipped probes write a **diagnostic** `probe_outcomes` row (`outcome != 'completed'`, no candidate response, no verdict) so the offline rollup can tell "tried but too slow" from "never armed." A verdict rollup must filter `outcome='completed'`.
142
+
143
+ **The deeper rule: inline shadow-probe = FAST candidates only.** To evaluate a *slow* reasoner, use the offline/async probe path (the Phase-2 batch direction, off the response entirely) or `probeShadow()` from `after()` — never the inline sync probe.
144
+
145
+ ## Structured advisories API (alpha.29+)
146
+
147
+ kgauto's compile-time advisories (`caching-off-on-claude`, `tool-bloat`,
148
+ `archetype-perf-floor-breach`, etc.) used to vanish after the consumer read
149
+ the compile result. alpha.29 ships a structured query + resolution surface
150
+ so Admin UIs can render "what's open right now?" without log-scraping.
151
+
152
+ ```ts
153
+ import {
154
+ getActionableAdvisories,
155
+ markAdvisoryResolved,
156
+ } from '@warmdrift/kgauto-compiler';
157
+
158
+ // Query open advisories for this app
159
+ const open = await getActionableAdvisories({
160
+ appId: 'playbacksam',
161
+ brainEndpoint: process.env.KGAUTO_BRAIN_ENDPOINT!,
162
+ brainJwt: process.env.KGAUTO_BRAIN_JWT!,
163
+ brainAnonKey: process.env.KGAUTO_BRAIN_ANON_KEY!,
164
+ });
165
+
166
+ for (const a of open) {
167
+ console.log(`[${a.severity}] ${a.rule} — ${a.observationCount} firings since ${a.openedAt}`);
168
+ console.log(` ${a.message}`);
169
+ if (a.suggestedFix?.docsLink) console.log(` ↳ ${a.suggestedFix.docsLink}`);
170
+ }
171
+
172
+ // After the operator fixes the issue, mark it resolved:
173
+ const r = await markAdvisoryResolved({
174
+ id: open[0].id,
175
+ resolutionNote: 'wired historyCachePolicy in compile() — PR #99',
176
+ brainEndpoint: process.env.KGAUTO_BRAIN_ENDPOINT!,
177
+ brainJwt: process.env.KGAUTO_BRAIN_JWT!,
178
+ brainAnonKey: process.env.KGAUTO_BRAIN_ANON_KEY!,
179
+ });
180
+ if (!r.ok) console.error(r.reason);
181
+ ```
182
+
183
+ The view enforces server-side auto-resolution: advisories with no firing
184
+ in the last 14 days flip to `status='resolved'` automatically; the next
185
+ firing reopens the rule with a fresh stable `id`.
186
+
92
187
  ## Architecture
93
188
 
94
189
  ```
@@ -0,0 +1,113 @@
1
+ /**
2
+ * `@warmdrift/kgauto-compiler/brain-proxy` — consumer brain-forward factory.
3
+ *
4
+ * ## Why this exists (the 4-consumer silent-drop class, 2026-07-06 s62)
5
+ *
6
+ * The library's `record()` / `recordOutcome()` / `recordShadowProbe()` /
7
+ * advisory paths all POST to a consumer-hosted proxy that forwards the row to
8
+ * the shared Supabase brain (the consumer holds the service-role key; kgauto's
9
+ * library never sees it). Every consumer hand-rolled that proxy layer — and a
10
+ * 4-consumer audit found every one silently broken in a *different* shape:
11
+ *
12
+ * - tt-intelligence: middleware blocked 2 of 3 routes — server-to-server
13
+ * POSTs 307-redirected to /login (missing public-path allowlist entry).
14
+ * - inspire-central: the `/outcomes` proxy carried a column whitelist frozen
15
+ * at alpha.7 that stripped `fell_over_from` / `fallback_reason`
16
+ * (migration 018 columns) — Glass-Box fallback telemetry silently NULL.
17
+ * - playbacksam: the `/probe_outcomes` whitelist would strip alpha.53's
18
+ * `error_class` — every future migration column drops on the floor.
19
+ * - global-expansion: mounted 1 of 4 routes — advisory POSTs 404 silently.
20
+ *
21
+ * Same L-142 / L-117 silent-drop family, four independent shapes, all in the
22
+ * layer consumers hand-roll. This factory kills the *class*: kgauto owns the
23
+ * forward handler, the consumer mounts ONE catch-all route. No column
24
+ * whitelist — the body is forwarded verbatim (`{ ...body }`), so any future
25
+ * migration column passes through untouched. One middleware/public-path entry
26
+ * (`/api/kgauto/v2/`) covers all four segments, which structurally prevents
27
+ * the tt-intel middleware-drift failure.
28
+ *
29
+ * ## Mounting recipes
30
+ *
31
+ * ### (a) Next.js app-router catch-all — ONE file, ONE middleware entry
32
+ *
33
+ * // app/api/kgauto/v2/[segment]/route.ts
34
+ * import { createBrainForwardRoutes } from '@warmdrift/kgauto-compiler/brain-proxy';
35
+ * const routes = createBrainForwardRoutes({
36
+ * ingestSecret: process.env.KGAUTO_INGEST_SECRET!,
37
+ * brainUrl: process.env.KGAUTO_BRAIN_URL!, // https://xxxx.supabase.co
38
+ * serviceKey: process.env.KGAUTO_BRAIN_SERVICE_KEY!,
39
+ * });
40
+ * export const POST = (req: Request, { params }: { params: { segment: string } }) =>
41
+ * routes.handle(req, params.segment);
42
+ *
43
+ * // middleware.ts — ONE public-path entry covers ALL four segments.
44
+ * // This is the structural fix for the tt-intel drift: you cannot mount
45
+ * // 3-of-4 routes and forget the 4th, because the catch-all is one file
46
+ * // and the allowlist is one prefix.
47
+ * const PUBLIC_PATHS = [ ... '/api/kgauto/v2/' ];
48
+ *
49
+ * ### (b) Plain Vercel edge function, per segment
50
+ *
51
+ * // api/kgauto/probe_outcomes.ts
52
+ * export const config = { runtime: 'edge' };
53
+ * const routes = createBrainForwardRoutes({ ... });
54
+ * export default (req: Request) => routes.handle(req, 'probe_outcomes');
55
+ *
56
+ * The library appends the segment path to the consumer's `endpoint`, so the
57
+ * consumer's `KGAUTO_V2_BRAIN_URL` (the value passed to `configureBrain`)
58
+ * must be the base that resolves to `.../api/kgauto/v2` — i.e. the last path
59
+ * segment of each library POST (`outcomes`, `probe_outcomes`, …) is what
60
+ * `handle()` receives as `segment`.
61
+ *
62
+ * ## Prefer-header relay (alpha.56)
63
+ *
64
+ * `record()` sends `Prefer: return=representation` on its `/outcomes` POST and
65
+ * parses the inserted row id from the PROXY's response to fire the secondary
66
+ * `compile_outcome_advisories` POST. alpha.54/.55 hardcoded
67
+ * `Prefer: return=minimal` toward the brain and returned `{ok:true}` — so the
68
+ * id-parse found nothing and the advisory secondary silently never fired for
69
+ * factory-mounted consumers (PB + GE convergent finding, 2026-07-06). The
70
+ * factory now honors a caller `Prefer: return=representation` header: it is
71
+ * forwarded to the brain and the brain's response body (the inserted row(s))
72
+ * is relayed verbatim on success. Callers that don't send the header keep the
73
+ * `return=minimal` + `{ok:true}` behavior.
74
+ *
75
+ * ## Quality-segment handle resolution (alpha.55)
76
+ *
77
+ * `recordOutcome()` sends `outcome_id` as `CompileResult.handle` (a string),
78
+ * but `compile_outcome_quality.outcome_id` is a BIGINT FK →
79
+ * `compile_outcomes(id)`. A verbatim forward would 400 every handle-shaped
80
+ * verdict (tt-intel + IC convergent finding, 2026-07-06). On the
81
+ * `compile_outcome_quality` segment the factory resolves non-numeric
82
+ * `outcome_id` values to their BIGINT row ids via
83
+ * `compile_outcomes?handle=eq.<v>` (latest row wins) before forwarding.
84
+ * Numeric ids pass through untouched; an unresolvable handle returns
85
+ * `404 handle_not_found` so the library's route-404 observability names the
86
+ * failure instead of the brain's opaque FK error.
87
+ */
88
+ interface BrainForwardConfig {
89
+ /** Bearer token consumers' callers must present (usually KGAUTO_INGEST_SECRET). */
90
+ ingestSecret: string;
91
+ /** Brain Supabase project URL (e.g. https://xxxx.supabase.co). */
92
+ brainUrl: string;
93
+ /** Brain service-role key (server-side only). */
94
+ serviceKey: string;
95
+ /** Optional fetch impl for tests. */
96
+ fetchImpl?: typeof fetch;
97
+ }
98
+ interface BrainForwardRoutes {
99
+ /**
100
+ * Web-standard handler. `segment` is the last path segment of the library's
101
+ * POST URL (e.g. 'outcomes', 'probe_outcomes'). In a Next.js catch-all this
102
+ * is `params.segment`. Never throws — every error path returns a `Response`.
103
+ */
104
+ handle(req: Request, segment: string): Promise<Response>;
105
+ /**
106
+ * The path segments this factory serves. Consumers can use it for route
107
+ * generation, allowlist construction, or tests.
108
+ */
109
+ segments: readonly string[];
110
+ }
111
+ declare function createBrainForwardRoutes(config: BrainForwardConfig): BrainForwardRoutes;
112
+
113
+ export { type BrainForwardConfig, type BrainForwardRoutes, createBrainForwardRoutes };
@@ -0,0 +1,113 @@
1
+ /**
2
+ * `@warmdrift/kgauto-compiler/brain-proxy` — consumer brain-forward factory.
3
+ *
4
+ * ## Why this exists (the 4-consumer silent-drop class, 2026-07-06 s62)
5
+ *
6
+ * The library's `record()` / `recordOutcome()` / `recordShadowProbe()` /
7
+ * advisory paths all POST to a consumer-hosted proxy that forwards the row to
8
+ * the shared Supabase brain (the consumer holds the service-role key; kgauto's
9
+ * library never sees it). Every consumer hand-rolled that proxy layer — and a
10
+ * 4-consumer audit found every one silently broken in a *different* shape:
11
+ *
12
+ * - tt-intelligence: middleware blocked 2 of 3 routes — server-to-server
13
+ * POSTs 307-redirected to /login (missing public-path allowlist entry).
14
+ * - inspire-central: the `/outcomes` proxy carried a column whitelist frozen
15
+ * at alpha.7 that stripped `fell_over_from` / `fallback_reason`
16
+ * (migration 018 columns) — Glass-Box fallback telemetry silently NULL.
17
+ * - playbacksam: the `/probe_outcomes` whitelist would strip alpha.53's
18
+ * `error_class` — every future migration column drops on the floor.
19
+ * - global-expansion: mounted 1 of 4 routes — advisory POSTs 404 silently.
20
+ *
21
+ * Same L-142 / L-117 silent-drop family, four independent shapes, all in the
22
+ * layer consumers hand-roll. This factory kills the *class*: kgauto owns the
23
+ * forward handler, the consumer mounts ONE catch-all route. No column
24
+ * whitelist — the body is forwarded verbatim (`{ ...body }`), so any future
25
+ * migration column passes through untouched. One middleware/public-path entry
26
+ * (`/api/kgauto/v2/`) covers all four segments, which structurally prevents
27
+ * the tt-intel middleware-drift failure.
28
+ *
29
+ * ## Mounting recipes
30
+ *
31
+ * ### (a) Next.js app-router catch-all — ONE file, ONE middleware entry
32
+ *
33
+ * // app/api/kgauto/v2/[segment]/route.ts
34
+ * import { createBrainForwardRoutes } from '@warmdrift/kgauto-compiler/brain-proxy';
35
+ * const routes = createBrainForwardRoutes({
36
+ * ingestSecret: process.env.KGAUTO_INGEST_SECRET!,
37
+ * brainUrl: process.env.KGAUTO_BRAIN_URL!, // https://xxxx.supabase.co
38
+ * serviceKey: process.env.KGAUTO_BRAIN_SERVICE_KEY!,
39
+ * });
40
+ * export const POST = (req: Request, { params }: { params: { segment: string } }) =>
41
+ * routes.handle(req, params.segment);
42
+ *
43
+ * // middleware.ts — ONE public-path entry covers ALL four segments.
44
+ * // This is the structural fix for the tt-intel drift: you cannot mount
45
+ * // 3-of-4 routes and forget the 4th, because the catch-all is one file
46
+ * // and the allowlist is one prefix.
47
+ * const PUBLIC_PATHS = [ ... '/api/kgauto/v2/' ];
48
+ *
49
+ * ### (b) Plain Vercel edge function, per segment
50
+ *
51
+ * // api/kgauto/probe_outcomes.ts
52
+ * export const config = { runtime: 'edge' };
53
+ * const routes = createBrainForwardRoutes({ ... });
54
+ * export default (req: Request) => routes.handle(req, 'probe_outcomes');
55
+ *
56
+ * The library appends the segment path to the consumer's `endpoint`, so the
57
+ * consumer's `KGAUTO_V2_BRAIN_URL` (the value passed to `configureBrain`)
58
+ * must be the base that resolves to `.../api/kgauto/v2` — i.e. the last path
59
+ * segment of each library POST (`outcomes`, `probe_outcomes`, …) is what
60
+ * `handle()` receives as `segment`.
61
+ *
62
+ * ## Prefer-header relay (alpha.56)
63
+ *
64
+ * `record()` sends `Prefer: return=representation` on its `/outcomes` POST and
65
+ * parses the inserted row id from the PROXY's response to fire the secondary
66
+ * `compile_outcome_advisories` POST. alpha.54/.55 hardcoded
67
+ * `Prefer: return=minimal` toward the brain and returned `{ok:true}` — so the
68
+ * id-parse found nothing and the advisory secondary silently never fired for
69
+ * factory-mounted consumers (PB + GE convergent finding, 2026-07-06). The
70
+ * factory now honors a caller `Prefer: return=representation` header: it is
71
+ * forwarded to the brain and the brain's response body (the inserted row(s))
72
+ * is relayed verbatim on success. Callers that don't send the header keep the
73
+ * `return=minimal` + `{ok:true}` behavior.
74
+ *
75
+ * ## Quality-segment handle resolution (alpha.55)
76
+ *
77
+ * `recordOutcome()` sends `outcome_id` as `CompileResult.handle` (a string),
78
+ * but `compile_outcome_quality.outcome_id` is a BIGINT FK →
79
+ * `compile_outcomes(id)`. A verbatim forward would 400 every handle-shaped
80
+ * verdict (tt-intel + IC convergent finding, 2026-07-06). On the
81
+ * `compile_outcome_quality` segment the factory resolves non-numeric
82
+ * `outcome_id` values to their BIGINT row ids via
83
+ * `compile_outcomes?handle=eq.<v>` (latest row wins) before forwarding.
84
+ * Numeric ids pass through untouched; an unresolvable handle returns
85
+ * `404 handle_not_found` so the library's route-404 observability names the
86
+ * failure instead of the brain's opaque FK error.
87
+ */
88
+ interface BrainForwardConfig {
89
+ /** Bearer token consumers' callers must present (usually KGAUTO_INGEST_SECRET). */
90
+ ingestSecret: string;
91
+ /** Brain Supabase project URL (e.g. https://xxxx.supabase.co). */
92
+ brainUrl: string;
93
+ /** Brain service-role key (server-side only). */
94
+ serviceKey: string;
95
+ /** Optional fetch impl for tests. */
96
+ fetchImpl?: typeof fetch;
97
+ }
98
+ interface BrainForwardRoutes {
99
+ /**
100
+ * Web-standard handler. `segment` is the last path segment of the library's
101
+ * POST URL (e.g. 'outcomes', 'probe_outcomes'). In a Next.js catch-all this
102
+ * is `params.segment`. Never throws — every error path returns a `Response`.
103
+ */
104
+ handle(req: Request, segment: string): Promise<Response>;
105
+ /**
106
+ * The path segments this factory serves. Consumers can use it for route
107
+ * generation, allowlist construction, or tests.
108
+ */
109
+ segments: readonly string[];
110
+ }
111
+ declare function createBrainForwardRoutes(config: BrainForwardConfig): BrainForwardRoutes;
112
+
113
+ export { type BrainForwardConfig, type BrainForwardRoutes, createBrainForwardRoutes };
@@ -0,0 +1,189 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/brain-proxy.ts
21
+ var brain_proxy_exports = {};
22
+ __export(brain_proxy_exports, {
23
+ createBrainForwardRoutes: () => createBrainForwardRoutes
24
+ });
25
+ module.exports = __toCommonJS(brain_proxy_exports);
26
+ var SEGMENT_TO_TABLE = {
27
+ outcomes: "compile_outcomes",
28
+ compile_outcome_advisories: "compile_outcome_advisories",
29
+ compile_outcome_quality: "compile_outcome_quality",
30
+ probe_outcomes: "probe_outcomes"
31
+ };
32
+ var KNOWN_SEGMENTS = Object.keys(SEGMENT_TO_TABLE);
33
+ var JSON_HEADERS = { "Content-Type": "application/json" };
34
+ function jsonResponse(status, body) {
35
+ return new Response(JSON.stringify(body), { status, headers: JSON_HEADERS });
36
+ }
37
+ function bearerOf(req) {
38
+ const header = req.headers.get("Authorization") ?? "";
39
+ const match = /^Bearer\s+(.+)$/i.exec(header);
40
+ return match?.[1]?.trim() ?? "";
41
+ }
42
+ function requireString(name, value) {
43
+ if (typeof value !== "string" || value.length === 0) {
44
+ throw new Error(`createBrainForwardRoutes: ${name} is required`);
45
+ }
46
+ return value;
47
+ }
48
+ function isNumericOutcomeId(value) {
49
+ return typeof value === "number" || typeof value === "string" && /^\d+$/.test(value);
50
+ }
51
+ function createBrainForwardRoutes(config) {
52
+ const ingestSecret = requireString("ingestSecret", config.ingestSecret);
53
+ const brainUrl = requireString("brainUrl", config.brainUrl).replace(/\/+$/, "");
54
+ const serviceKey = requireString("serviceKey", config.serviceKey);
55
+ const fetchFn = config.fetchImpl ?? fetch;
56
+ async function resolveQualityOutcomeIds(body) {
57
+ const rows = Array.isArray(body) ? body : [body];
58
+ const handles = [
59
+ ...new Set(
60
+ rows.map((r) => r?.outcome_id).filter(
61
+ (v) => typeof v === "string" && !isNumericOutcomeId(v)
62
+ )
63
+ )
64
+ ];
65
+ if (handles.length === 0) return body;
66
+ const list = handles.map((h) => `"${h.replace(/"/g, "")}"`).join(",");
67
+ const url = `${brainUrl}/rest/v1/compile_outcomes?select=id,handle&handle=in.(${encodeURIComponent(list)})&order=id.desc`;
68
+ let idByHandle;
69
+ try {
70
+ const res = await fetchFn(url, {
71
+ headers: { apikey: serviceKey, Authorization: `Bearer ${serviceKey}` }
72
+ });
73
+ if (!res.ok) {
74
+ const text = await res.text().catch(() => "<no body>");
75
+ return jsonResponse(502, {
76
+ error: "handle-resolution-failed",
77
+ table: "compile_outcome_quality",
78
+ status: res.status,
79
+ detail: text
80
+ });
81
+ }
82
+ const found = await res.json();
83
+ idByHandle = /* @__PURE__ */ new Map();
84
+ for (const r of found) {
85
+ if (!idByHandle.has(r.handle)) idByHandle.set(r.handle, r.id);
86
+ }
87
+ } catch (err) {
88
+ return jsonResponse(502, {
89
+ error: "handle-resolution-failed",
90
+ table: "compile_outcome_quality",
91
+ detail: err instanceof Error ? err.message : String(err)
92
+ });
93
+ }
94
+ const unresolved = handles.filter((h) => !idByHandle.has(h));
95
+ if (unresolved.length > 0) {
96
+ return jsonResponse(404, {
97
+ error: "handle_not_found",
98
+ table: "compile_outcome_quality",
99
+ unresolved
100
+ });
101
+ }
102
+ const rewritten = rows.map(
103
+ (r) => typeof r?.outcome_id === "string" && idByHandle.has(r.outcome_id) ? { ...r, outcome_id: idByHandle.get(r.outcome_id) } : r
104
+ );
105
+ return Array.isArray(body) ? rewritten : rewritten[0];
106
+ }
107
+ async function handle(req, segment) {
108
+ try {
109
+ if (req.method !== "POST") {
110
+ return jsonResponse(405, {
111
+ error: "method-not-allowed",
112
+ method: req.method,
113
+ allowed: ["POST"]
114
+ });
115
+ }
116
+ const table = SEGMENT_TO_TABLE[segment];
117
+ if (!table) {
118
+ return jsonResponse(404, {
119
+ error: "unknown-brain-forward-segment",
120
+ segment,
121
+ known: KNOWN_SEGMENTS
122
+ });
123
+ }
124
+ if (bearerOf(req) !== ingestSecret) {
125
+ return jsonResponse(401, { error: "unauthorized" });
126
+ }
127
+ let body;
128
+ try {
129
+ body = await req.json();
130
+ } catch {
131
+ return jsonResponse(400, { error: "invalid-json" });
132
+ }
133
+ let row = Array.isArray(body) ? body : { ...body };
134
+ if (table === "compile_outcome_quality") {
135
+ const outcome = await resolveQualityOutcomeIds(row);
136
+ if (outcome instanceof Response) return outcome;
137
+ row = outcome;
138
+ }
139
+ const wantsRepresentation = /return=representation/i.test(
140
+ req.headers.get("Prefer") ?? ""
141
+ );
142
+ let brainRes;
143
+ try {
144
+ brainRes = await fetchFn(`${brainUrl}/rest/v1/${table}`, {
145
+ method: "POST",
146
+ headers: {
147
+ apikey: serviceKey,
148
+ Authorization: `Bearer ${serviceKey}`,
149
+ "Content-Type": "application/json",
150
+ Prefer: wantsRepresentation ? "return=representation" : "return=minimal"
151
+ },
152
+ body: JSON.stringify(row)
153
+ });
154
+ } catch (err) {
155
+ return jsonResponse(502, {
156
+ error: "brain-unreachable",
157
+ table,
158
+ detail: err instanceof Error ? err.message : String(err)
159
+ });
160
+ }
161
+ if (brainRes.ok) {
162
+ if (wantsRepresentation) {
163
+ const text2 = await brainRes.text().catch(() => "");
164
+ if (text2.length > 0) {
165
+ return new Response(text2, { status: 201, headers: JSON_HEADERS });
166
+ }
167
+ }
168
+ return jsonResponse(201, { ok: true });
169
+ }
170
+ const text = await brainRes.text().catch(() => "<no body>");
171
+ return jsonResponse(brainRes.status, {
172
+ error: "brain-write-failed",
173
+ table,
174
+ status: brainRes.status,
175
+ detail: text
176
+ });
177
+ } catch (err) {
178
+ return jsonResponse(500, {
179
+ error: "brain-forward-internal-error",
180
+ detail: err instanceof Error ? err.message : String(err)
181
+ });
182
+ }
183
+ }
184
+ return { handle, segments: KNOWN_SEGMENTS };
185
+ }
186
+ // Annotate the CommonJS export names for ESM import in node:
187
+ 0 && (module.exports = {
188
+ createBrainForwardRoutes
189
+ });
@@ -0,0 +1,6 @@
1
+ import {
2
+ createBrainForwardRoutes
3
+ } from "./chunk-IUWFML6Z.mjs";
4
+ export {
5
+ createBrainForwardRoutes
6
+ };