@wrenai/genbi 0.0.1 → 0.0.3

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/README.md CHANGED
@@ -8,8 +8,13 @@ The app does not write SQL itself. A question goes to the Wren agent harness,
8
8
  which answers with a structured envelope — the blocks to render, and whether the
9
9
  answer was verified — and the UI renders that.
10
10
 
11
- This README is for working on the app from source: getting it running, and the
12
- daily build/test/typecheck loop. Three companion docs cover the rest:
11
+ To just run it, `npx @wrenai/genbi` is the whole story — see
12
+ [Getting started](#getting-started) below, or [INSTALL.md](./INSTALL.md) for
13
+ what it needs and how to configure it.
14
+
15
+ The rest of this README is for working on the app **from source**: getting it
16
+ running, and the daily build/test/typecheck loop. Three companion docs cover
17
+ the rest:
13
18
 
14
19
  - [RUNNING.md](./RUNNING.md) — a live, BFF-backed dev loop against your own
15
20
  Warble build, and checking that the pieces still fit together.
@@ -20,10 +25,10 @@ daily build/test/typecheck loop. Three companion docs cover the rest:
20
25
 
21
26
  ## Developer preview
22
27
 
23
- `@wrenai/genbi` is at `0.0.0` and has not been published to npm. Nothing here is
24
- under a compatibility commitment yet — commands, environment variables and
25
- interfaces move without notice. The Codex runtime is supported in code but has
26
- not been exercised end to end; Claude is the one that is. See
28
+ `@wrenai/genbi` is published at `0.0.3`. Nothing here is under a compatibility
29
+ commitment yet — commands, environment variables and interfaces move without
30
+ notice. The Codex runtime is supported in code but has not been exercised end
31
+ to end; Claude is the one that is. See
27
32
  [MAINTAINING.md](./MAINTAINING.md#versioning) for what the version number does
28
33
  and does not promise.
29
34
 
@@ -40,8 +45,24 @@ pinned `@warble/*` packages are already pulled in by `pnpm install` — see
40
45
 
41
46
  ## Getting started
42
47
 
43
- This app lives in a pnpm workspace, so install JavaScript dependencies from
44
- the repo root:
48
+ ### Just run it
49
+
50
+ ```bash
51
+ WREN_HARNESS_WORKSPACE_ROOT=/absolute/path/to/an/empty/directory npx @wrenai/genbi
52
+ ```
53
+
54
+ That fetches the published package, downloads the Warble binary for your
55
+ platform on install, and serves the built UI and the BFF from one process on
56
+ `http://127.0.0.1:4787`. You still need a provider CLI you are logged in to
57
+ (`claude` or `codex`) and the `wren` CLI on `PATH`.
58
+ [INSTALL.md](./INSTALL.md) covers the prerequisites, what happens on first
59
+ start, and every other environment variable.
60
+
61
+ ### From source
62
+
63
+ Everything below is the development loop, and is only worth setting up if you
64
+ are changing the app. It lives in a pnpm workspace, so install JavaScript
65
+ dependencies from the repo root:
45
66
 
46
67
  ```bash
47
68
  corepack enable
@@ -95,6 +95,17 @@ export interface NativeVendorReadiness {
95
95
  export interface NativeProducerReadiness {
96
96
  readonly available: boolean;
97
97
  readonly category?: "native_session_producer_incompatible";
98
+ /**
99
+ * Why the preflight rejected the producer. Safe to send: every field is a
100
+ * fixed string plus a content digest of the resolved binary — the phase name,
101
+ * a `result=` token, and (for a Wren runtime rejection) one of that resolver's
102
+ * fixed sentences. None of them interpolate a path or process output.
103
+ *
104
+ * It is here because the category alone named a state without a cause, so an
105
+ * operator reading the Sessions view could not tell a missing binary from an
106
+ * incompatible one, and neither could a bug report.
107
+ */
108
+ readonly diagnostic?: string;
98
109
  }
99
110
  export interface NativePurposeReadiness {
100
111
  readonly scopeKind: "bootstrap" | "bound_project";
@@ -196,13 +207,30 @@ export interface NativeProducerIdentity {
196
207
  readonly executable: string;
197
208
  readonly identity: string;
198
209
  }
210
+ /** One vendor's own preflight outcome. A vendor nobody selected must not speak for one that was. */
211
+ export interface NativeProducerVendorProbe {
212
+ readonly available: boolean;
213
+ /** Browser-safe: a fixed sentence, never a path or process output. */
214
+ readonly reason?: string;
215
+ /** Fixed fields plus a content digest; never a command path or process output. */
216
+ readonly diagnostic: string;
217
+ }
199
218
  export interface NativeProducerPreflightResult {
219
+ /** True when at least one vendor is usable — never a verdict on the vendor in use. */
200
220
  readonly available: boolean;
201
221
  readonly category?: typeof NATIVE_PRODUCER_CATEGORY;
202
222
  /** Server-only resolved executable and content identity for the eventual launch. */
203
223
  readonly producer?: NativeProducerIdentity;
204
224
  /** Server-only: fixed fields plus a content digest; never a command path or process output. */
205
225
  readonly diagnostic: string;
226
+ /**
227
+ * Per-vendor outcomes. The preflight probes every vendor so that readiness can
228
+ * answer for the one actually configured: the Codex leg requires a native Wren
229
+ * runtime the resolver can trace to the server's own repository, which an
230
+ * installed package can never satisfy, and a single boolean over both vendors
231
+ * let that failure take Claude down with it.
232
+ */
233
+ readonly vendors: Readonly<Record<NativeVendor, NativeProducerVendorProbe>>;
206
234
  }
207
235
  /**
208
236
  * Exercises the exact, file-only native dispatch boundary against the
@@ -310,6 +310,24 @@ function configuredIrPath(irPath) {
310
310
  return false;
311
311
  }
312
312
  }
313
+ /**
314
+ * Why a vendor's preflight rejected the producer. Each is a fixed sentence
315
+ * chosen here — none is built from a path, a command, or process output — so
316
+ * `nativeSessionLaunchFailure` may pass them through unchanged.
317
+ */
318
+ const NATIVE_PRODUCER_REASONS = new Set([
319
+ "the Warble binary could not be resolved",
320
+ "the Warble binary could not be identified",
321
+ "the Warble binary could not be started",
322
+ "the Warble binary did not respond",
323
+ "the Warble binary rejected the dispatch probe",
324
+ "the Warble binary does not support the native session flags this build requires",
325
+ "the Warble binary could not be started for this vendor",
326
+ "the Warble binary did not respond for this vendor",
327
+ "Warble could not dispatch this profile for this vendor",
328
+ "Warble emitted a launch spec this build cannot run",
329
+ "the preflight could not create its temporary workspace",
330
+ ]);
313
331
  /** A deliberately small public classification; never expose process or filesystem detail. */
314
332
  export function nativeSessionLaunchFailure(error) {
315
333
  const message = error instanceof Error ? error.message : "";
@@ -319,8 +337,15 @@ export function nativeSessionLaunchFailure(error) {
319
337
  return message;
320
338
  if (message === "native session materialization prerequisites are unavailable")
321
339
  return message;
322
- if (message === "native Wren runtime is unavailable")
323
- return "native session materialization prerequisites are unavailable";
340
+ // Every NativeWrenRuntimeError message is `native Wren runtime is unavailable: `
341
+ // followed by one of that resolver's fixed literals; not one of them
342
+ // interpolates a path or process output, so the whole family may pass through.
343
+ // Matching the bare sentence, as this did, stopped matching anything once the
344
+ // resolver started naming which link in the chain broke.
345
+ if (message.startsWith("native Wren runtime is unavailable"))
346
+ return message;
347
+ if (NATIVE_PRODUCER_REASONS.has(message))
348
+ return message;
324
349
  if (message === "native session producer is incompatible")
325
350
  return message;
326
351
  if (message === "native session launch artifacts are unavailable" || message === "native session launch specification is invalid" || message === "native session launch specification is incompatible")
@@ -348,12 +373,25 @@ const NATIVE_HELP_MARKERS = [
348
373
  "--native-mcp",
349
374
  "--out",
350
375
  ];
351
- function nativeProducerResult(available, identity, phase, detail, producer) {
376
+ function producerDiagnostic(identity, phase, detail) {
377
+ return `identity=${identity ?? "unavailable"} phase=${phase} ${detail}`;
378
+ }
379
+ /** A failure before any vendor-specific work rules out every vendor, for the same reason. */
380
+ function everyVendorUnavailable(probe) {
381
+ const vendors = {};
382
+ for (const vendor of NATIVE_PROBE_TARGETS)
383
+ vendors[vendor] = probe;
384
+ return vendors;
385
+ }
386
+ /** A shared-phase outcome: it decided nothing vendor-specific, so it speaks for all of them. */
387
+ function nativeProducerResult(available, identity, phase, detail, reason, producer) {
388
+ const diagnostic = producerDiagnostic(identity, phase, detail);
352
389
  return {
353
390
  available,
354
391
  ...(available ? {} : { category: NATIVE_PRODUCER_CATEGORY }),
355
- ...(available && producer ? { producer } : {}),
356
- diagnostic: `identity=${identity ?? "unavailable"} phase=${phase} ${detail}`,
392
+ ...(producer ? { producer } : {}),
393
+ diagnostic,
394
+ vendors: everyVendorUnavailable(available ? { available: true, diagnostic } : { available: false, reason, diagnostic }),
357
395
  };
358
396
  }
359
397
  function executableIdentity(executable) {
@@ -439,26 +477,34 @@ function runNativeProbe(command, args) {
439
477
  export async function probeNativeSessionProducer(input) {
440
478
  const executable = localExecutablePath(input.warbleBin);
441
479
  if (!executable)
442
- return nativeProducerResult(false, undefined, "resolve", "result=binary_not_found");
480
+ return nativeProducerResult(false, undefined, "resolve", "result=binary_not_found", "the Warble binary could not be resolved");
443
481
  const identity = executableIdentity(executable);
444
482
  if (!identity)
445
- return nativeProducerResult(false, undefined, "identity", "result=identity_unavailable");
483
+ return nativeProducerResult(false, undefined, "identity", "result=identity_unavailable", "the Warble binary could not be identified");
446
484
  const producer = { executable, identity };
447
485
  const help = await runNativeProbe(executable, ["dispatch", "--help"]);
448
486
  if (help.spawnFailed)
449
- return nativeProducerResult(false, identity, "dispatch_help", "result=spawn_failed");
487
+ return nativeProducerResult(false, identity, "dispatch_help", "result=spawn_failed", "the Warble binary could not be started");
450
488
  if (help.timedOut)
451
- return nativeProducerResult(false, identity, "dispatch_help", "result=timed_out");
489
+ return nativeProducerResult(false, identity, "dispatch_help", "result=timed_out", "the Warble binary did not respond");
452
490
  if (help.exitCode !== 0)
453
- return nativeProducerResult(false, identity, "dispatch_help", `result=exit_code_${help.exitCode ?? "signal"}`);
491
+ return nativeProducerResult(false, identity, "dispatch_help", `result=exit_code_${help.exitCode ?? "signal"}`, "the Warble binary rejected the dispatch probe");
454
492
  const missingMarkers = NATIVE_HELP_MARKERS.filter((marker) => !help.output.includes(marker));
455
493
  if (missingMarkers.length > 0)
456
- return nativeProducerResult(false, identity, "dispatch_help", `result=missing_markers_${missingMarkers.join(",")}`);
457
- for (const purpose of NATIVE_PURPOSES) {
458
- const irPath = input.irPaths[purpose];
459
- if (!irPath || !configuredIrPath(irPath))
460
- continue;
461
- for (const vendor of NATIVE_PROBE_TARGETS) {
494
+ return nativeProducerResult(false, identity, "dispatch_help", `result=missing_markers_${missingMarkers.join(",")}`, "the Warble binary does not support the native session flags this build requires");
495
+ // Vendor is the outer loop on purpose. It used to be the inner one, and the
496
+ // first failing pair returned for the whole preflight — so a Codex rejection
497
+ // reported the producer incompatible even for a Claude-only install, which is
498
+ // exactly what an installed package produces.
499
+ const vendors = {};
500
+ for (const vendor of NATIVE_PROBE_TARGETS) {
501
+ let outcome = { available: true, diagnostic: producerDiagnostic(identity, `complete_${vendor}`, "result=compatible") };
502
+ for (const purpose of NATIVE_PURPOSES) {
503
+ const irPath = input.irPaths[purpose];
504
+ if (!irPath || !configuredIrPath(irPath))
505
+ continue;
506
+ const phase = `dispatch_${purpose}_${vendor}`;
507
+ const fail = (detail, reason) => ({ available: false, reason, diagnostic: producerDiagnostic(identity, phase, detail) });
462
508
  let root;
463
509
  try {
464
510
  root = mkdtempSync(path.join(os.tmpdir(), "genbi-native-preflight-"));
@@ -478,32 +524,54 @@ export async function probeNativeSessionProducer(input) {
478
524
  writeFileSync(scopePath, JSON.stringify({ version: "3", kind: NATIVE_DISPATCH_REGISTRY[purpose].scopeKind, scope_id: scopeId, cwd: outputRoot, entry: nativeEntryFor(purpose, vendor), ...(purpose === "setup" ? { bootstrap_root: bootstrapRoot } : {}), ...(wrenRuntime ? { wren_runtime: wrenRuntime } : {}), ...(binding ? { binding } : {}) }), { encoding: "utf8", mode: 0o600, flag: "wx" });
479
525
  writeFileSync(mcpPath, JSON.stringify({ version: "1", url: "http://127.0.0.1:0/api/native-sessions/mcp", credential: "native-preflight-nonsecret" }), { encoding: "utf8", mode: 0o600, flag: "wx" });
480
526
  const dispatched = await runNativeProbe(executable, ["dispatch", irPath, "--target", targetForProvider(vendor), "--purpose", purpose, "--native-scope", scopePath, "--native-mcp", mcpPath, "--out", outputRoot]);
481
- const phase = `dispatch_${purpose}_${vendor}`;
482
- if (dispatched.spawnFailed)
483
- return nativeProducerResult(false, identity, phase, "result=spawn_failed");
484
- if (dispatched.timedOut)
485
- return nativeProducerResult(false, identity, phase, "result=timed_out");
486
- if (dispatched.exitCode !== 0)
487
- return nativeProducerResult(false, identity, phase, `result=exit_code_${dispatched.exitCode ?? "signal"}`);
527
+ if (dispatched.spawnFailed) {
528
+ outcome = fail("result=spawn_failed", "the Warble binary could not be started for this vendor");
529
+ break;
530
+ }
531
+ if (dispatched.timedOut) {
532
+ outcome = fail("result=timed_out", "the Warble binary did not respond for this vendor");
533
+ break;
534
+ }
535
+ if (dispatched.exitCode !== 0) {
536
+ outcome = fail(`result=exit_code_${dispatched.exitCode ?? "signal"}`, "Warble could not dispatch this profile for this vendor");
537
+ break;
538
+ }
488
539
  try {
489
540
  readNativeLaunchSpec(outputRoot, purpose, vendor, scopeId, undefined, { version: "1", url: "http://127.0.0.1:0/api/native-sessions/mcp", credential: "native-preflight-nonsecret" });
490
541
  }
491
542
  catch {
492
- return nativeProducerResult(false, identity, phase, "result=launch_spec_incompatible");
543
+ outcome = fail("result=launch_spec_incompatible", "Warble emitted a launch spec this build cannot run");
544
+ break;
493
545
  }
494
546
  }
495
547
  catch (error) {
496
- if (error instanceof NativeWrenRuntimeError)
497
- return nativeProducerResult(false, identity, `dispatch_${purpose}_${vendor}`, `result=wren_runtime_unavailable (${error.message})`);
498
- return nativeProducerResult(false, identity, `dispatch_${purpose}_${vendor}`, "result=preflight_workspace_unavailable");
548
+ outcome = error instanceof NativeWrenRuntimeError
549
+ ? fail(`result=wren_runtime_unavailable (${error.message})`, error.message)
550
+ : fail("result=preflight_workspace_unavailable", "the preflight could not create its temporary workspace");
551
+ break;
499
552
  }
500
553
  finally {
501
554
  if (root)
502
555
  rmSync(root, { recursive: true, force: true });
503
556
  }
504
557
  }
505
- }
506
- return nativeProducerResult(true, identity, "complete", "result=compatible", producer);
558
+ vendors[vendor] = outcome;
559
+ }
560
+ const usable = NATIVE_PROBE_TARGETS.filter((vendor) => vendors[vendor].available);
561
+ const available = usable.length > 0;
562
+ // When nothing is usable the summary must still carry each vendor's own cause;
563
+ // collapsing them to "no_compatible_vendor" would lose the only line that says
564
+ // what actually broke.
565
+ const vendorResults = NATIVE_PROBE_TARGETS
566
+ .map((vendor) => `${vendor}=${vendors[vendor].diagnostic.split(" result=")[1] ?? "unknown"}`)
567
+ .join(" ");
568
+ return {
569
+ available,
570
+ ...(available ? {} : { category: NATIVE_PRODUCER_CATEGORY }),
571
+ producer,
572
+ diagnostic: producerDiagnostic(identity, "complete", available ? `result=compatible_${usable.join("+")}` : `result=no_compatible_vendor ${vendorResults}`),
573
+ vendors,
574
+ };
507
575
  }
508
576
  function parseNativeSetupRecoveryReport(value) {
509
577
  const report = record(value);
@@ -1139,6 +1207,10 @@ export class NativeSessionService {
1139
1207
  ? nativeSessionStateBaseAvailableForProject(this.materializationState(), configuredRoot)
1140
1208
  : purpose === "setup" && projectAvailable && nativeSessionStateBaseAvailable(this.materializationState());
1141
1209
  const workspaceUnavailable = !projectAvailable || !stateAvailable;
1210
+ // Only the configured vendor's own probe may block this purpose. Probing
1211
+ // both and collapsing them into one boolean let an unselected vendor veto
1212
+ // the selected one.
1213
+ const selectedProducer = producer && dispatch ? producer.vendors[dispatch.provider] : undefined;
1142
1214
  const reason = runtimeCorrection
1143
1215
  ?? (!dispatch && !legacyFixture
1144
1216
  ? "native sessions require a saved Runtime & authentication binding"
@@ -1146,8 +1218,8 @@ export class NativeSessionService {
1146
1218
  ? purpose === "setup" ? "native setup sessions require a workspace root" : "native sessions require a current bound project"
1147
1219
  : workspaceUnavailable
1148
1220
  ? "native session workspace is unavailable"
1149
- : producer && !producer.available
1150
- ? "native session producer is incompatible"
1221
+ : selectedProducer && !selectedProducer.available
1222
+ ? selectedProducer.reason ?? "native session producer is incompatible"
1151
1223
  : !irPath || !(this.options.dispatch !== undefined || configuredIrPath(irPath)) || !legacyProducerAvailable()
1152
1224
  ? "native session materialization prerequisites are unavailable"
1153
1225
  : mcpReadiness && !mcpReadiness.available
@@ -1160,6 +1232,17 @@ export class NativeSessionService {
1160
1232
  ? `the ${executable} interactive CLI is not available on this machine`
1161
1233
  : undefined);
1162
1234
  const vendorReadiness = {};
1235
+ // Production readiness now reports every vendor, so the UI can say which
1236
+ // one is unusable and why instead of only that "the producer" is.
1237
+ if (!legacyFixture && producer)
1238
+ for (const vendor of NATIVE_VENDORS) {
1239
+ const probe = producer.vendors[vendor];
1240
+ vendorReadiness[vendor] = probe?.available === false
1241
+ ? { available: false, reason: probe.reason ?? "native session producer is incompatible" }
1242
+ : !executableAvailable(vendor)
1243
+ ? { available: false, reason: `the ${vendor === "claude" ? "claude" : "codex"} interactive CLI is not available on this machine` }
1244
+ : { available: true };
1245
+ }
1163
1246
  if (legacyFixture)
1164
1247
  for (const vendor of NATIVE_VENDORS) {
1165
1248
  const legacyExecutable = vendor === "claude" ? "claude" : "codex";
@@ -1174,7 +1257,16 @@ export class NativeSessionService {
1174
1257
  scopeKind,
1175
1258
  profile: definition.profile,
1176
1259
  ...(dispatch ? { target: dispatch.target, targetLabel: dispatch.targetLabel } : {}),
1177
- ...(producer ? { producer: producer.available ? { available: true } : { available: false, category: NATIVE_PRODUCER_CATEGORY } } : {}),
1260
+ // With a vendor selected this answers for that vendor; with none selected
1261
+ // it falls back to "is any vendor usable", rather than claiming health
1262
+ // no vendor could back.
1263
+ ...(producer
1264
+ ? {
1265
+ producer: (selectedProducer ? selectedProducer.available : producer.available)
1266
+ ? { available: true, diagnostic: (selectedProducer ?? producer).diagnostic }
1267
+ : { available: false, category: NATIVE_PRODUCER_CATEGORY, diagnostic: (selectedProducer ?? producer).diagnostic },
1268
+ }
1269
+ : {}),
1178
1270
  available: legacyAvailable,
1179
1271
  ...(unavailable ? { reason: unavailable } : {}),
1180
1272
  vendors: vendorReadiness,
@@ -1222,8 +1314,10 @@ export class NativeSessionService {
1222
1314
  ? configuredIr
1223
1315
  : await this.options.resolveDispatchIr(purpose, binding);
1224
1316
  const producer = this.legacyFixtureMode() ? undefined : await this.productionProducerPreflight();
1225
- if (producer && (!producer.available || !producer.producer))
1226
- throw new InteractiveLaunchError("native session producer is incompatible");
1317
+ const launchProducer = producer ? producer.vendors[dispatchDefinition.provider] : undefined;
1318
+ if (producer && (!launchProducer?.available || !producer.producer)) {
1319
+ throw new InteractiveLaunchError(launchProducer?.reason ?? "native session producer is incompatible");
1320
+ }
1227
1321
  if (producer?.producer)
1228
1322
  assertNativeProducerIdentity(producer.producer);
1229
1323
  const scopeId = resumed?.row.scopeId ?? newId("native-scope");
@@ -41,10 +41,20 @@ export declare function collectBundleTierNames(bundle: {
41
41
  /** Reads the tier contract directly from a compiled Warble IR document. */
42
42
  export declare function collectIrTierNames(ir: unknown): string[];
43
43
  /**
44
- * Compiles the canonical profile source without rebinding its context and
45
- * extracts its tier contract from IR. This is intentionally independent of
46
- * auth choice and of any user project: the Setup form must discover the rows
47
- * before either exists.
44
+ * Reads the profile's tier contract, which the Setup form needs before an auth
45
+ * choice or a user project exists.
46
+ *
47
+ * A tier name is a static property of the profile — it comes from each
48
+ * `llm_calls[].tier` — so nothing here depends on what project is bound. The
49
+ * profile ships its own compiled IR next to it, so the contract is read from
50
+ * that and no Warble process runs at all.
51
+ *
52
+ * Compiling instead is the fallback, and only for a profile source that has no
53
+ * golden beside it (`WREN_HARNESS_PROFILE` pointed at someone's own profile
54
+ * tree). It is a fallback rather than the default because `warble compile`
55
+ * requires a resolvable context binding, and the shipped profiles are bound to
56
+ * a path that only resolves inside a checkout of this repository: in an
57
+ * installed package that compile fails, and the Setup form has no rows.
48
58
  */
49
59
  export declare function compileUnboundProfileTierNames(options: {
50
60
  readonly profileSource: string;
@@ -4,7 +4,7 @@
4
4
  * reads an API key only while constructing an in-memory in-process adapter spec.
5
5
  */
6
6
  import { createHash } from "node:crypto";
7
- import { mkdirSync, writeFileSync } from "node:fs";
7
+ import { existsSync, mkdirSync, writeFileSync } from "node:fs";
8
8
  import { mkdtemp, readFile, rm } from "node:fs/promises";
9
9
  import { tmpdir } from "node:os";
10
10
  import path from "node:path";
@@ -233,12 +233,26 @@ export function collectIrTierNames(ir) {
233
233
  return [...seen];
234
234
  }
235
235
  /**
236
- * Compiles the canonical profile source without rebinding its context and
237
- * extracts its tier contract from IR. This is intentionally independent of
238
- * auth choice and of any user project: the Setup form must discover the rows
239
- * before either exists.
236
+ * Reads the profile's tier contract, which the Setup form needs before an auth
237
+ * choice or a user project exists.
238
+ *
239
+ * A tier name is a static property of the profile — it comes from each
240
+ * `llm_calls[].tier` — so nothing here depends on what project is bound. The
241
+ * profile ships its own compiled IR next to it, so the contract is read from
242
+ * that and no Warble process runs at all.
243
+ *
244
+ * Compiling instead is the fallback, and only for a profile source that has no
245
+ * golden beside it (`WREN_HARNESS_PROFILE` pointed at someone's own profile
246
+ * tree). It is a fallback rather than the default because `warble compile`
247
+ * requires a resolvable context binding, and the shipped profiles are bound to
248
+ * a path that only resolves inside a checkout of this repository: in an
249
+ * installed package that compile fails, and the Setup form has no rows.
240
250
  */
241
251
  export async function compileUnboundProfileTierNames(options) {
252
+ const goldenPath = path.join(path.resolve(options.profileSource), "ir.golden.json");
253
+ if (existsSync(goldenPath)) {
254
+ return collectIrTierNames(JSON.parse(await readFile(goldenPath, "utf8")));
255
+ }
242
256
  const workDir = await mkdtemp(path.join(tmpdir(), "wren-harness-runtime-tiers-"));
243
257
  try {
244
258
  const irPath = path.join(workDir, "ir.json");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wrenai/genbi",
3
- "version": "0.0.1",
3
+ "version": "0.0.3",
4
4
  "description": "Wren GenBI — the web UI for WrenAI: ask questions of your data and get dashboards back",
5
5
  "license": "Apache-2.0",
6
6
  "keywords": [