@wrenai/genbi 0.0.1

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 (286) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +141 -0
  3. package/bin/genbi.mjs +31 -0
  4. package/dist/assets/echarts-LTFC-ysd.js +40 -0
  5. package/dist/assets/index-Bo--QMUN.js +441 -0
  6. package/dist/assets/index-Xs4q7pNo.css +1 -0
  7. package/dist/index.html +14 -0
  8. package/dist/wrenai_logo.png +0 -0
  9. package/dist/wrenai_logo_white.png +0 -0
  10. package/dist-server/harness/auth/detect.d.ts +18 -0
  11. package/dist-server/harness/auth/detect.js +37 -0
  12. package/dist-server/harness/auth/index.d.ts +4 -0
  13. package/dist-server/harness/auth/index.js +2 -0
  14. package/dist-server/harness/auth/probe.d.ts +18 -0
  15. package/dist-server/harness/auth/probe.js +52 -0
  16. package/dist-server/harness/auth/types.d.ts +42 -0
  17. package/dist-server/harness/auth/types.js +9 -0
  18. package/dist-server/harness/bundle/loader.d.ts +32 -0
  19. package/dist-server/harness/bundle/loader.js +43 -0
  20. package/dist-server/harness/bundle/schema.d.ts +199 -0
  21. package/dist-server/harness/bundle/schema.js +127 -0
  22. package/dist-server/harness/bundle/version.d.ts +10 -0
  23. package/dist-server/harness/bundle/version.js +43 -0
  24. package/dist-server/harness/capability/gate.d.ts +6 -0
  25. package/dist-server/harness/capability/gate.js +29 -0
  26. package/dist-server/harness/capability/registry.d.ts +6 -0
  27. package/dist-server/harness/capability/registry.js +17 -0
  28. package/dist-server/harness/cli-args.d.ts +123 -0
  29. package/dist-server/harness/cli-args.js +247 -0
  30. package/dist-server/harness/cli.d.ts +2 -0
  31. package/dist-server/harness/cli.js +151 -0
  32. package/dist-server/harness/compile/cache.d.ts +23 -0
  33. package/dist-server/harness/compile/cache.js +109 -0
  34. package/dist-server/harness/compile/compose-profile.d.ts +34 -0
  35. package/dist-server/harness/compile/compose-profile.js +95 -0
  36. package/dist-server/harness/compile/errors.d.ts +22 -0
  37. package/dist-server/harness/compile/errors.js +41 -0
  38. package/dist-server/harness/compile/fingerprint.d.ts +22 -0
  39. package/dist-server/harness/compile/fingerprint.js +84 -0
  40. package/dist-server/harness/compile/index.d.ts +8 -0
  41. package/dist-server/harness/compile/index.js +7 -0
  42. package/dist-server/harness/compile/pipeline.d.ts +66 -0
  43. package/dist-server/harness/compile/pipeline.js +176 -0
  44. package/dist-server/harness/compile/resolve-binary.d.ts +55 -0
  45. package/dist-server/harness/compile/resolve-binary.js +133 -0
  46. package/dist-server/harness/compile/types.d.ts +107 -0
  47. package/dist-server/harness/compile/types.js +1 -0
  48. package/dist-server/harness/compile/warble-identity.d.ts +12 -0
  49. package/dist-server/harness/compile/warble-identity.js +33 -0
  50. package/dist-server/harness/compliance/gate.d.ts +35 -0
  51. package/dist-server/harness/compliance/gate.js +48 -0
  52. package/dist-server/harness/compliance/index.d.ts +2 -0
  53. package/dist-server/harness/compliance/index.js +1 -0
  54. package/dist-server/harness/compliance/types.d.ts +16 -0
  55. package/dist-server/harness/compliance/types.js +1 -0
  56. package/dist-server/harness/events/emitter.d.ts +34 -0
  57. package/dist-server/harness/events/emitter.js +25 -0
  58. package/dist-server/harness/events/index.d.ts +4 -0
  59. package/dist-server/harness/events/index.js +2 -0
  60. package/dist-server/harness/events/sse.d.ts +20 -0
  61. package/dist-server/harness/events/sse.js +48 -0
  62. package/dist-server/harness/events/types.d.ts +131 -0
  63. package/dist-server/harness/events/types.js +1 -0
  64. package/dist-server/harness/exec/errors.d.ts +16 -0
  65. package/dist-server/harness/exec/errors.js +29 -0
  66. package/dist-server/harness/exec/index.d.ts +5 -0
  67. package/dist-server/harness/exec/index.js +2 -0
  68. package/dist-server/harness/exec/local.d.ts +32 -0
  69. package/dist-server/harness/exec/local.js +134 -0
  70. package/dist-server/harness/exec/policy.d.ts +12 -0
  71. package/dist-server/harness/exec/policy.js +1 -0
  72. package/dist-server/harness/exec/types.d.ts +63 -0
  73. package/dist-server/harness/exec/types.js +1 -0
  74. package/dist-server/harness/guardrails/index.d.ts +2 -0
  75. package/dist-server/harness/guardrails/index.js +1 -0
  76. package/dist-server/harness/guardrails/policy.d.ts +45 -0
  77. package/dist-server/harness/guardrails/policy.js +57 -0
  78. package/dist-server/harness/index.d.ts +37 -0
  79. package/dist-server/harness/index.js +19 -0
  80. package/dist-server/harness/loop/errors.d.ts +53 -0
  81. package/dist-server/harness/loop/errors.js +72 -0
  82. package/dist-server/harness/loop/executor.d.ts +123 -0
  83. package/dist-server/harness/loop/executor.js +380 -0
  84. package/dist-server/harness/loop/guard.d.ts +16 -0
  85. package/dist-server/harness/loop/guard.js +1 -0
  86. package/dist-server/harness/loop/index.d.ts +5 -0
  87. package/dist-server/harness/loop/index.js +3 -0
  88. package/dist-server/harness/native-producer/index.d.ts +109 -0
  89. package/dist-server/harness/native-producer/index.js +302 -0
  90. package/dist-server/harness/npm-package-resolve.d.ts +28 -0
  91. package/dist-server/harness/npm-package-resolve.js +47 -0
  92. package/dist-server/harness/providers/adapters/anthropic.d.ts +7 -0
  93. package/dist-server/harness/providers/adapters/anthropic.js +6 -0
  94. package/dist-server/harness/providers/adapters/mock.d.ts +4 -0
  95. package/dist-server/harness/providers/adapters/mock.js +5 -0
  96. package/dist-server/harness/providers/adapters/openai-compatible.d.ts +10 -0
  97. package/dist-server/harness/providers/adapters/openai-compatible.js +10 -0
  98. package/dist-server/harness/providers/binding.d.ts +23 -0
  99. package/dist-server/harness/providers/binding.js +13 -0
  100. package/dist-server/harness/providers/errors.d.ts +6 -0
  101. package/dist-server/harness/providers/errors.js +12 -0
  102. package/dist-server/harness/providers/index.d.ts +18 -0
  103. package/dist-server/harness/providers/index.js +22 -0
  104. package/dist-server/harness/providers/registry.d.ts +16 -0
  105. package/dist-server/harness/providers/registry.js +24 -0
  106. package/dist-server/harness/render/envelope.d.ts +145 -0
  107. package/dist-server/harness/render/envelope.js +428 -0
  108. package/dist-server/harness/render/errors.d.ts +33 -0
  109. package/dist-server/harness/render/errors.js +46 -0
  110. package/dist-server/harness/render/index.d.ts +5 -0
  111. package/dist-server/harness/render/index.js +3 -0
  112. package/dist-server/harness/render/validate.d.ts +13 -0
  113. package/dist-server/harness/render/validate.js +88 -0
  114. package/dist-server/harness/route/adapter-spec.d.ts +40 -0
  115. package/dist-server/harness/route/adapter-spec.js +61 -0
  116. package/dist-server/harness/route/agent-sdk-cli.d.ts +30 -0
  117. package/dist-server/harness/route/agent-sdk-cli.js +86 -0
  118. package/dist-server/harness/route/agent-sdk-manifest.d.ts +31 -0
  119. package/dist-server/harness/route/agent-sdk-manifest.js +56 -0
  120. package/dist-server/harness/route/artifact-content.d.ts +53 -0
  121. package/dist-server/harness/route/artifact-content.js +83 -0
  122. package/dist-server/harness/route/chat-event-mapper.d.ts +95 -0
  123. package/dist-server/harness/route/chat-event-mapper.js +164 -0
  124. package/dist-server/harness/route/codex-ask-events.d.ts +25 -0
  125. package/dist-server/harness/route/codex-ask-events.js +395 -0
  126. package/dist-server/harness/route/codex-ask.d.ts +15 -0
  127. package/dist-server/harness/route/codex-ask.js +242 -0
  128. package/dist-server/harness/route/codex-local-cli.d.ts +20 -0
  129. package/dist-server/harness/route/codex-local-cli.js +78 -0
  130. package/dist-server/harness/route/codex-local-manifest.d.ts +22 -0
  131. package/dist-server/harness/route/codex-local-manifest.js +180 -0
  132. package/dist-server/harness/route/describe.d.ts +54 -0
  133. package/dist-server/harness/route/describe.js +84 -0
  134. package/dist-server/harness/route/dispatched.d.ts +138 -0
  135. package/dist-server/harness/route/dispatched.js +339 -0
  136. package/dist-server/harness/route/in-process.d.ts +43 -0
  137. package/dist-server/harness/route/in-process.js +106 -0
  138. package/dist-server/harness/route/index.d.ts +19 -0
  139. package/dist-server/harness/route/index.js +12 -0
  140. package/dist-server/harness/route/profile-source.d.ts +37 -0
  141. package/dist-server/harness/route/profile-source.js +76 -0
  142. package/dist-server/harness/route/route.d.ts +33 -0
  143. package/dist-server/harness/route/route.js +114 -0
  144. package/dist-server/harness/route/tier-binding.d.ts +36 -0
  145. package/dist-server/harness/route/tier-binding.js +63 -0
  146. package/dist-server/harness/route/types.d.ts +286 -0
  147. package/dist-server/harness/route/types.js +1 -0
  148. package/dist-server/harness/session/errors.d.ts +4 -0
  149. package/dist-server/harness/session/errors.js +7 -0
  150. package/dist-server/harness/session/gate.d.ts +11 -0
  151. package/dist-server/harness/session/gate.js +13 -0
  152. package/dist-server/harness/session/index.d.ts +5 -0
  153. package/dist-server/harness/session/index.js +3 -0
  154. package/dist-server/harness/session/run.d.ts +60 -0
  155. package/dist-server/harness/session/run.js +333 -0
  156. package/dist-server/harness/session/types.d.ts +29 -0
  157. package/dist-server/harness/session/types.js +1 -0
  158. package/dist-server/harness/setup/codex-events.d.ts +12 -0
  159. package/dist-server/harness/setup/codex-events.js +108 -0
  160. package/dist-server/harness/setup/runner.d.ts +497 -0
  161. package/dist-server/harness/setup/runner.js +1314 -0
  162. package/dist-server/harness/tools/errors.d.ts +107 -0
  163. package/dist-server/harness/tools/errors.js +141 -0
  164. package/dist-server/harness/tools/index.d.ts +13 -0
  165. package/dist-server/harness/tools/index.js +8 -0
  166. package/dist-server/harness/tools/mcp.d.ts +26 -0
  167. package/dist-server/harness/tools/mcp.js +15 -0
  168. package/dist-server/harness/tools/native.d.ts +179 -0
  169. package/dist-server/harness/tools/native.js +322 -0
  170. package/dist-server/harness/tools/parse.d.ts +14 -0
  171. package/dist-server/harness/tools/parse.js +18 -0
  172. package/dist-server/harness/tools/resolve-wren-binary.d.ts +12 -0
  173. package/dist-server/harness/tools/resolve-wren-binary.js +21 -0
  174. package/dist-server/harness/tools/resolve.d.ts +51 -0
  175. package/dist-server/harness/tools/resolve.js +79 -0
  176. package/dist-server/harness/tools/setup-native.d.ts +84 -0
  177. package/dist-server/harness/tools/setup-native.js +353 -0
  178. package/dist-server/harness/tools/wiring.d.ts +10 -0
  179. package/dist-server/harness/tools/wiring.js +16 -0
  180. package/dist-server/providers/setup.provider.yaml +31 -0
  181. package/dist-server/providers/wren.provider.yaml +33 -0
  182. package/dist-server/server/adopt.d.ts +87 -0
  183. package/dist-server/server/adopt.js +263 -0
  184. package/dist-server/server/app.d.ts +6 -0
  185. package/dist-server/server/app.js +2521 -0
  186. package/dist-server/server/auth-choice.d.ts +16 -0
  187. package/dist-server/server/auth-choice.js +36 -0
  188. package/dist-server/server/bin.d.ts +2 -0
  189. package/dist-server/server/bin.js +601 -0
  190. package/dist-server/server/bootstrap-project-binding.d.ts +31 -0
  191. package/dist-server/server/bootstrap-project-binding.js +71 -0
  192. package/dist-server/server/clarify.d.ts +10 -0
  193. package/dist-server/server/clarify.js +17 -0
  194. package/dist-server/server/codex-setup-mcp.d.ts +2 -0
  195. package/dist-server/server/codex-setup-mcp.js +147 -0
  196. package/dist-server/server/compose.d.ts +89 -0
  197. package/dist-server/server/compose.js +271 -0
  198. package/dist-server/server/conn-config.d.ts +30 -0
  199. package/dist-server/server/conn-config.js +253 -0
  200. package/dist-server/server/context-files.d.ts +12 -0
  201. package/dist-server/server/context-files.js +117 -0
  202. package/dist-server/server/context-map.d.ts +30 -0
  203. package/dist-server/server/context-map.js +95 -0
  204. package/dist-server/server/context-source.d.ts +68 -0
  205. package/dist-server/server/context-source.js +85 -0
  206. package/dist-server/server/db.d.ts +862 -0
  207. package/dist-server/server/db.js +1839 -0
  208. package/dist-server/server/enrichment-runner.d.ts +184 -0
  209. package/dist-server/server/enrichment-runner.js +213 -0
  210. package/dist-server/server/enrichment-verify.d.ts +73 -0
  211. package/dist-server/server/enrichment-verify.js +290 -0
  212. package/dist-server/server/enrichment.d.ts +232 -0
  213. package/dist-server/server/enrichment.js +199 -0
  214. package/dist-server/server/env-detect.d.ts +8 -0
  215. package/dist-server/server/env-detect.js +17 -0
  216. package/dist-server/server/env-field-hints.d.ts +46 -0
  217. package/dist-server/server/env-field-hints.js +48 -0
  218. package/dist-server/server/fold.d.ts +133 -0
  219. package/dist-server/server/fold.js +407 -0
  220. package/dist-server/server/harness-profile-sources.d.ts +3 -0
  221. package/dist-server/server/harness-profile-sources.js +24 -0
  222. package/dist-server/server/harness.d.ts +19 -0
  223. package/dist-server/server/harness.js +439 -0
  224. package/dist-server/server/impact.d.ts +18 -0
  225. package/dist-server/server/impact.js +85 -0
  226. package/dist-server/server/interactive-terminal.d.ts +175 -0
  227. package/dist-server/server/interactive-terminal.js +336 -0
  228. package/dist-server/server/native-artifacts.d.ts +1004 -0
  229. package/dist-server/server/native-artifacts.js +564 -0
  230. package/dist-server/server/native-dispatch-registry.d.ts +42 -0
  231. package/dist-server/server/native-dispatch-registry.js +45 -0
  232. package/dist-server/server/native-producer-fence.d.ts +14 -0
  233. package/dist-server/server/native-producer-fence.js +48 -0
  234. package/dist-server/server/native-session-resume.d.ts +6 -0
  235. package/dist-server/server/native-session-resume.js +103 -0
  236. package/dist-server/server/native-session-workspace.d.ts +40 -0
  237. package/dist-server/server/native-session-workspace.js +175 -0
  238. package/dist-server/server/native-sessions.d.ts +374 -0
  239. package/dist-server/server/native-sessions.js +1517 -0
  240. package/dist-server/server/native-wren-runtime.d.ts +20 -0
  241. package/dist-server/server/native-wren-runtime.js +259 -0
  242. package/dist-server/server/node-pty-host.d.ts +31 -0
  243. package/dist-server/server/node-pty-host.js +100 -0
  244. package/dist-server/server/route-intent.d.ts +40 -0
  245. package/dist-server/server/route-intent.js +23 -0
  246. package/dist-server/server/runtime-binding.d.ts +52 -0
  247. package/dist-server/server/runtime-binding.js +258 -0
  248. package/dist-server/server/source-catalog.d.ts +84 -0
  249. package/dist-server/server/source-catalog.js +221 -0
  250. package/dist-server/server/source-driver.d.ts +35 -0
  251. package/dist-server/server/source-driver.js +124 -0
  252. package/dist-server/server/spa.d.ts +2 -0
  253. package/dist-server/server/spa.js +51 -0
  254. package/dist-server/server/subscription-model-catalog.d.ts +27 -0
  255. package/dist-server/server/subscription-model-catalog.js +203 -0
  256. package/dist-server/server/turn.d.ts +249 -0
  257. package/dist-server/server/turn.js +1074 -0
  258. package/dist-server/server/wire-types.d.ts +799 -0
  259. package/dist-server/server/wire-types.js +1 -0
  260. package/dist-server/server/wren-home.d.ts +19 -0
  261. package/dist-server/server/wren-home.js +33 -0
  262. package/dist-server/server/wren-profiles.d.ts +43 -0
  263. package/dist-server/server/wren-profiles.js +123 -0
  264. package/package.json +93 -0
  265. package/profiles/genbi-default/context/binding.yml +9 -0
  266. package/profiles/genbi-default/ir.golden.json +506 -0
  267. package/profiles/genbi-default/profile.yml +16 -0
  268. package/profiles/genbi-enrich-context/components/apply_enrichment/component.yml +42 -0
  269. package/profiles/genbi-enrich-context/components/draft_enrichment/component.yml +30 -0
  270. package/profiles/genbi-enrich-context/components/draft_enrichment/steps/draft.md +34 -0
  271. package/profiles/genbi-enrich-context/components/inspect_context/component.yml +32 -0
  272. package/profiles/genbi-enrich-context/components/inspect_context/steps/inspect.md +13 -0
  273. package/profiles/genbi-enrich-context/context/binding.yml +4 -0
  274. package/profiles/genbi-enrich-context/ir.golden.json +302 -0
  275. package/profiles/genbi-enrich-context/profile.yml +15 -0
  276. package/profiles/genbi-monitor/context/binding.yml +7 -0
  277. package/profiles/genbi-monitor/ir.golden.json +188 -0
  278. package/profiles/genbi-monitor/profile.yml +27 -0
  279. package/profiles/genbi-setup/components/build_context/component.yml +44 -0
  280. package/profiles/genbi-setup/components/build_context/steps/build.md +14 -0
  281. package/profiles/genbi-setup/components/connect_source/component.yml +51 -0
  282. package/profiles/genbi-setup/components/connect_source/steps/connect.md +23 -0
  283. package/profiles/genbi-setup/context/binding.yml +8 -0
  284. package/profiles/genbi-setup/ir.golden.json +140 -0
  285. package/profiles/genbi-setup/profile.yml +18 -0
  286. package/profiles/genbi-setup/raw/schema.json +5 -0
@@ -0,0 +1,290 @@
1
+ /**
2
+ * Refutes an enrichment proposal before a human is asked to approve it.
3
+ *
4
+ * Everything capable of refuting a proposal — `wren context validate`, `wren
5
+ * context build`, planning its references through the MDL — used to run inside
6
+ * apply, downstream of the approval. So the person was asked to approve
7
+ * something no machine had checked, and the machine checked it afterwards.
8
+ * That ordering does not scale: enrichment output is routinely larger than
9
+ * anyone will read, so approval degrades into a rubber stamp — which is worse
10
+ * than no gate, because it looks like review happened.
11
+ *
12
+ * The checks here need no correct answer to compare against. The engine is the
13
+ * ground truth: a measure over a column that does not exist cannot be planned,
14
+ * whatever anyone believes about it.
15
+ *
16
+ * MECHANISM. A proposal is not on disk, and must not be written into the bound
17
+ * project to find out whether it is sound. The host copies the project to a
18
+ * temporary directory, writes the proposal there, runs the ladder, and discards
19
+ * the copy. The bound project is never touched, so there is nothing to roll
20
+ * back — not even when a check throws.
21
+ *
22
+ * The shadow is host-owned. Giving the agent write access to a project copy is
23
+ * giving it write access that can leak to the real one.
24
+ *
25
+ * WHY THE HOST RUNS THESE ITSELF. The agent holds read-only execution and can
26
+ * run the same commands; it should, because doing so reduces the rounds needed
27
+ * here. But its report is not evidence. "The model says validate passed" is the
28
+ * same failure as the model-authored hashes a live draft once produced, which
29
+ * is why proposal hashing is host-owned too.
30
+ */
31
+ import { execFile } from "node:child_process";
32
+ import { cp, lstat, mkdtemp, mkdir, rm, writeFile } from "node:fs/promises";
33
+ import { tmpdir } from "node:os";
34
+ import path from "node:path";
35
+ export const VERIFICATION_LADDER = ["sink", "grammar", "validate", "build", "dry_plan", "dry_run"];
36
+ /**
37
+ * The Wren keys each change kind's draft must be written in. The first live
38
+ * draft to reach this stage used `model`/`columns`/`metrics`/`relationships` —
39
+ * plausible-looking, and not Wren's grammar at all.
40
+ */
41
+ const REQUIRED_KEYS = {
42
+ new_cube: ["base_object"],
43
+ new_view: ["statement"],
44
+ };
45
+ /** Keys that indicate a different product's grammar rather than Wren's. */
46
+ const FOREIGN_KEYS = {
47
+ new_cube: ["columns", "metrics", "model"],
48
+ };
49
+ /**
50
+ * The first path component of `sink` that is a symlink, if any. Checked inside
51
+ * the shadow, where `cp` has already reproduced the bound project's own links.
52
+ */
53
+ async function symlinkComponent(root, sink) {
54
+ const parts = sink.split("/").filter((part) => part.length > 0);
55
+ let current = root;
56
+ for (const part of parts) {
57
+ current = path.join(current, part);
58
+ try {
59
+ if ((await lstat(current)).isSymbolicLink())
60
+ return path.relative(root, current);
61
+ }
62
+ catch {
63
+ return undefined; // does not exist yet — nothing to follow
64
+ }
65
+ }
66
+ return undefined;
67
+ }
68
+ /**
69
+ * Every call below runs with `cwd` set to the shadow copy, but `wren` does
70
+ * not resolve its project from cwd alone: `WREN_PROJECT_HOME`, when set,
71
+ * wins over cwd-based discovery (see `discover_project_path()` in
72
+ * `core/wren/src/wren/context.py`), and an explicit `--mdl`/`--path` would
73
+ * win over both. If this process's own ambient environment ever carries
74
+ * `WREN_PROJECT_HOME` — as GenBI itself sets it for native session children
75
+ * (`server/native-sessions.ts`, `server/bin.ts`) — every step below would
76
+ * silently validate/build/query the BOUND project instead of the shadow,
77
+ * because `execFile` inherits the parent's env by default. Pin it here,
78
+ * explicitly, to the one project this call is actually allowed to touch:
79
+ * the shadow root passed in as `cwd`. This does not depend on whether the
80
+ * BFF happens to run with `WREN_PROJECT_HOME` unset today.
81
+ *
82
+ * `WREN_HOME` (the `~/.wren` profile/config store, a different variable) is
83
+ * deliberately left ambient rather than pinned: it is a read-only source of
84
+ * real connection credentials, and `dry-run`/`cube query` need the real
85
+ * profile to reach the real data source for the warehouse step — pointing
86
+ * it at a throwaway location would break verification, not sandbox it, and
87
+ * nothing in this ladder writes through it. Checked: `context validate`,
88
+ * `context build`, `dry-plan`, `cube query --sql-only`, and `dry-run` take
89
+ * no other ambient project-selecting input than `WREN_PROJECT_HOME`/cwd/
90
+ * `~/.wren/config.yml`'s `default_project` (per the CLI's own `--path`
91
+ * help text and `discover_project_path()`), and none of the five writes to
92
+ * `WREN_HOME` or any other location outside the project directory it is
93
+ * given.
94
+ */
95
+ function runWren(args, cwd) {
96
+ return new Promise((resolve) => {
97
+ execFile("wren", args, { cwd, maxBuffer: 32 * 1024 * 1024, env: { ...process.env, WREN_PROJECT_HOME: cwd } }, (error, stdout, stderr) => {
98
+ resolve({ stdout, stderr, error: error });
99
+ });
100
+ });
101
+ }
102
+ /** Trims a command's output into something safe and readable to carry in a verdict. */
103
+ function firstMeaningfulLine(...streams) {
104
+ for (const stream of streams) {
105
+ const line = stream
106
+ .split(/\r?\n/)
107
+ .map((entry) => entry.trim())
108
+ .find((entry) => entry.length > 0);
109
+ if (line !== undefined)
110
+ return line.slice(0, 400);
111
+ }
112
+ return "no output";
113
+ }
114
+ /**
115
+ * A cheap structural read of a YAML draft. Not a YAML parser: it only needs to
116
+ * know which top-level keys are present, and pulling in a parser to answer that
117
+ * would be most of a dependency spent on one question. Matches this package's
118
+ * existing convention of small single-purpose scans (see `server/adopt.ts`).
119
+ */
120
+ function topLevelKeys(draft) {
121
+ const keys = new Set();
122
+ for (const line of draft.split(/\r?\n/)) {
123
+ const match = /^([A-Za-z_][\w-]*):/.exec(line);
124
+ if (match)
125
+ keys.add(match[1].toLowerCase());
126
+ }
127
+ return keys;
128
+ }
129
+ function checkGrammar(operation) {
130
+ const keys = topLevelKeys(operation.draft);
131
+ if (keys.size === 0)
132
+ return undefined; // not a keyed document (e.g. knowledge markdown)
133
+ const foreign = (FOREIGN_KEYS[operation.changeKind] ?? []).filter((key) => keys.has(key));
134
+ if (foreign.length > 0) {
135
+ return `uses ${foreign.map((key) => `"${key}"`).join(", ")}, which is not Wren's grammar for ${operation.changeKind}; expected ${(REQUIRED_KEYS[operation.changeKind] ?? []).map((key) => `"${key}"`).join(", ")}`;
136
+ }
137
+ const missing = (REQUIRED_KEYS[operation.changeKind] ?? []).filter((key) => !keys.has(key));
138
+ return missing.length > 0 ? `is missing required ${missing.map((key) => `"${key}"`).join(", ")} for ${operation.changeKind}` : undefined;
139
+ }
140
+ /** Measure names declared in a cube draft, for the generated query. */
141
+ function draftMeasureNames(draft) {
142
+ const names = [];
143
+ let inMeasures = false;
144
+ for (const line of draft.split(/\r?\n/)) {
145
+ if (/^measures:\s*$/.test(line)) {
146
+ inMeasures = true;
147
+ continue;
148
+ }
149
+ if (/^[A-Za-z_]/.test(line)) {
150
+ inMeasures = false;
151
+ continue;
152
+ }
153
+ if (!inMeasures)
154
+ continue;
155
+ const name = /^\s*-?\s*name:\s*"?([A-Za-z_][\w-]*)"?\s*$/.exec(line)?.[1];
156
+ if (name !== undefined)
157
+ names.push(name);
158
+ }
159
+ return names;
160
+ }
161
+ function probeFor(operation) {
162
+ const name = /^(?:cubes|views|models)\/([a-z0-9_-]+)\//.exec(operation.sink)?.[1];
163
+ if (name === undefined)
164
+ return undefined;
165
+ if (operation.changeKind === "new_cube") {
166
+ const measures = draftMeasureNames(operation.draft);
167
+ return measures.length > 0 ? { kind: "cube", cube: name, measures } : undefined;
168
+ }
169
+ return { kind: "relation", sql: `SELECT * FROM "${name}" LIMIT 0` };
170
+ }
171
+ /**
172
+ * There is deliberately no opt-out for the data-source step.
173
+ *
174
+ * An earlier version carried an `includeWarehouseDryRun` flag documented as
175
+ * "off by default". Once cubes had to be checked through `dry-run` — nothing
176
+ * local resolves their expressions — the flag stopped being read while its
177
+ * documentation went on promising a round trip could be avoided. An advertised
178
+ * safety lever that does nothing is worse than none, so it is gone.
179
+ *
180
+ * What is true, in one place: verifying a `new_cube` always reaches the data
181
+ * source, via `dry-run`, which parses and validates without returning rows.
182
+ * Every other change kind is verified entirely locally. A caller that cannot
183
+ * reach its source cannot verify a cube, and the honest report for that is a
184
+ * refutation or `unavailable` from the attempt itself — not a flag that skips
185
+ * the check and calls the result verified.
186
+ */
187
+ /**
188
+ * Runs the ladder against a shadow copy of the bound project. Stops at the
189
+ * first refutation, so a proposal refuted locally never reaches the warehouse.
190
+ *
191
+ * The verdict carries the proposal hash and project revision it was earned
192
+ * against: a verdict must not survive onto edited content or a moved project.
193
+ */
194
+ export async function verifyProposal(proposal, binding, options = {}) {
195
+ const run = options.run ?? runWren;
196
+ const bound = { proposalHash: proposal.hash, projectRevision: proposal.projectRevision };
197
+ const stepsRun = [];
198
+ // Steps 1-2 need no project at all, so they run before paying for a copy.
199
+ for (const operation of proposal.operations) {
200
+ const grammar = checkGrammar(operation);
201
+ if (grammar !== undefined) {
202
+ return { status: "refuted", ...bound, stepsRun: ["sink", "grammar"], refutation: { operationId: operation.id, step: "grammar", reason: `draft ${grammar}` } };
203
+ }
204
+ }
205
+ stepsRun.push("sink", "grammar");
206
+ let shadow;
207
+ try {
208
+ shadow = await mkdtemp(path.join(tmpdir(), "wren-enrich-verify-"));
209
+ const project = path.join(shadow, "project");
210
+ // `.env` comes along: the ladder's later steps resolve the project's own
211
+ // connection the way any other wren invocation in that directory would.
212
+ await cp(binding.path, project, { recursive: true });
213
+ for (const operation of proposal.operations) {
214
+ const sinkPath = path.join(project, operation.sink);
215
+ // `canonicalizeProposal` already constrained every sink to a fixed shape,
216
+ // so this cannot escape; assert it anyway, because a path that escapes a
217
+ // temp directory is not a failure worth discovering later.
218
+ if (!path.resolve(sinkPath).startsWith(path.resolve(project) + path.sep)) {
219
+ return { status: "refuted", ...bound, stepsRun, refutation: { operationId: operation.id, step: "sink", reason: `sink "${operation.sink}" resolves outside the project` } };
220
+ }
221
+ // The lexical check above never touches the filesystem, so it passes for a
222
+ // path whose components are symlinks. `cp` does not dereference them, so a
223
+ // symlink in the bound project is copied AS a symlink and a later write
224
+ // would follow it out of the shadow — defeating the guarantee this module
225
+ // states. Refuse rather than follow.
226
+ const escaping = await symlinkComponent(project, operation.sink);
227
+ if (escaping !== undefined) {
228
+ return { status: "refuted", ...bound, stepsRun, refutation: { operationId: operation.id, step: "sink", reason: `sink "${operation.sink}" passes through a symlink ("${escaping}"), which a write would follow out of the project` } };
229
+ }
230
+ await mkdir(path.dirname(sinkPath), { recursive: true });
231
+ await writeFile(sinkPath, operation.draft.endsWith("\n") ? operation.draft : `${operation.draft}\n`, "utf-8");
232
+ }
233
+ const validate = await run(["context", "validate"], project);
234
+ stepsRun.push("validate");
235
+ if (validate.error !== null) {
236
+ if (validate.error.code === "ENOENT")
237
+ return { status: "unavailable", ...bound, reason: "the wren CLI is not on PATH" };
238
+ return { status: "refuted", ...bound, stepsRun, refutation: { operationId: proposal.operations[0]?.id ?? "", step: "validate", reason: firstMeaningfulLine(validate.stderr, validate.stdout) } };
239
+ }
240
+ const build = await run(["context", "build"], project);
241
+ stepsRun.push("build");
242
+ if (build.error !== null) {
243
+ return { status: "refuted", ...bound, stepsRun, refutation: { operationId: proposal.operations[0]?.id ?? "", step: "build", reason: firstMeaningfulLine(build.stderr, build.stdout) } };
244
+ }
245
+ for (const operation of proposal.operations) {
246
+ const probe = probeFor(operation);
247
+ if (probe === undefined)
248
+ continue; // nothing queryable (e.g. a knowledge append)
249
+ if (probe.kind === "relation") {
250
+ const plan = await run(["dry-plan", "--sql", probe.sql], project);
251
+ if (!stepsRun.includes("dry_plan"))
252
+ stepsRun.push("dry_plan");
253
+ if (plan.error !== null) {
254
+ return { status: "refuted", ...bound, stepsRun, refutation: { operationId: operation.id, step: "dry_plan", reason: firstMeaningfulLine(plan.stderr, plan.stdout) } };
255
+ }
256
+ continue;
257
+ }
258
+ // Cube: generate its query, then have the source parse it. Both steps are
259
+ // needed — generation alone accepts an invented column.
260
+ const generated = await run(["cube", "query", "--cube", probe.cube, "--measures", probe.measures.join(","), "--sql-only"], project);
261
+ if (!stepsRun.includes("dry_plan"))
262
+ stepsRun.push("dry_plan");
263
+ if (generated.error !== null) {
264
+ return { status: "refuted", ...bound, stepsRun, refutation: { operationId: operation.id, step: "dry_plan", reason: firstMeaningfulLine(generated.stderr, generated.stdout) } };
265
+ }
266
+ const sql = generated.stdout.split(/\r?\n/).map((line) => line.trim()).filter((line) => line.length > 0).pop();
267
+ if (sql === undefined) {
268
+ return { status: "unavailable", ...bound, reason: `could not generate a query for cube "${probe.cube}"` };
269
+ }
270
+ // Unlike the relation probe, this one reaches the data source: nothing
271
+ // local resolves a cube's expressions, so the alternative is not checking
272
+ // cubes at all. `dry-run` parses and validates without returning rows.
273
+ const dry = await run(["dry-run", "--sql", sql], project);
274
+ if (!stepsRun.includes("dry_run"))
275
+ stepsRun.push("dry_run");
276
+ if (dry.error !== null) {
277
+ return { status: "refuted", ...bound, stepsRun, refutation: { operationId: operation.id, step: "dry_run", reason: firstMeaningfulLine(dry.stderr, dry.stdout) } };
278
+ }
279
+ }
280
+ return { status: "verified", ...bound, stepsRun };
281
+ }
282
+ catch (error) {
283
+ // An unrunnable ladder is reported as such, never as a pass.
284
+ return { status: "unavailable", ...bound, reason: error.message };
285
+ }
286
+ finally {
287
+ if (shadow !== undefined)
288
+ await rm(shadow, { recursive: true, force: true }).catch(() => undefined);
289
+ }
290
+ }
@@ -0,0 +1,232 @@
1
+ export type EnrichmentMode = "grill" | "autopilot";
2
+ export type EnrichmentRisk = "low" | "high" | "conflict" | "ambiguous";
3
+ export type EnrichmentDecision = "accept" | "edit" | "skip";
4
+ export type EnrichmentRunStatus = "drafting" | "awaiting_decision" | "awaiting_approval" | "ready" | "completed" | "cancelled" | "reconcile_required" | "failed";
5
+ /**
6
+ * Deliberately has no kind for "append a description to a model" (a change
7
+ * that targets the same `models/<name>/metadata.yml` sink as `mdl_metric`
8
+ * and `calculated_column`, but is neither). Adding one is not a host-only
9
+ * change: `apply_enrichment` (the warble component that applies an approved
10
+ * operation) already has to know a distinct per-`changeKind` merge shape for
11
+ * every kind that shares that sink, and a description append would be a
12
+ * third, different shape into the same file. That component's guardrails
13
+ * are locked and out of scope here, so until it grows the case, a
14
+ * model-description append has no valid `changeKind` and is refused by
15
+ * `canonicalizeProposal` like any other invalid draft — not silently
16
+ * reclassified as `mdl_metric`/`calculated_column`, which would misroute it.
17
+ */
18
+ export type EnrichmentChangeKind = "knowledge_append" | "new_cube" | "new_view" | "new_relationship" | "mdl_metric" | "calculated_column" | "conflict" | "ambiguous";
19
+ export type EnrichmentOperationState = "awaiting_decision" | "awaiting_approval" | "ready" | "ready_to_reapply" | "applying" | "applied" | "skipped" | "reconcile_required";
20
+ /**
21
+ * Model-authored confidence is display-only. It never participates in risk,
22
+ * approval, or Auto-pilot eligibility. Numbers remain accepted internally so
23
+ * existing ledgers and callers can be read during the transition to labels.
24
+ */
25
+ export type EnrichmentConfidence = string | number;
26
+ /** Safe, sink-scoped proposal metadata. Never carry source material or SDK ids. */
27
+ export interface EnrichmentOperation {
28
+ readonly id: string;
29
+ readonly sink: string;
30
+ readonly risk: EnrichmentRisk;
31
+ readonly summary: string;
32
+ readonly confidence: EnrichmentConfidence;
33
+ readonly changeKind: EnrichmentChangeKind;
34
+ readonly draft: string;
35
+ }
36
+ export interface EnrichmentProposal {
37
+ readonly id: string;
38
+ readonly hash: string;
39
+ readonly projectRevision: string;
40
+ readonly operations: readonly EnrichmentOperation[];
41
+ }
42
+ /**
43
+ * The host's view of the currently bound project. Paths alone are not an
44
+ * authority: a symlink can be retargeted and the same spelling can name a
45
+ * different directory. `identity` is deliberately a portable textual form of
46
+ * the platform's device/inode pair, while `path` is the canonical realpath.
47
+ */
48
+ export interface EnrichmentBinding {
49
+ readonly path: string;
50
+ readonly identity: string;
51
+ readonly generation: number;
52
+ readonly revision: string;
53
+ }
54
+ export type UnversionedEnrichmentBinding = Omit<EnrichmentBinding, "generation">;
55
+ /**
56
+ * The subset of a binding that identifies *which directory* is bound,
57
+ * independent of whether it has ever been built. Binding a project (Setup's
58
+ * connect step, adopt, the context step's temporary healthcheck bind) is a
59
+ * foundation operation that must succeed regardless of build state, so it
60
+ * may only ever produce this — never a revision.
61
+ */
62
+ export type ProjectIdentity = Omit<UnversionedEnrichmentBinding, "revision">;
63
+ /** Untrusted runtime draft: host ignores its ids, hash, revision and risk. */
64
+ export interface EnrichmentDraftOperation {
65
+ readonly sink: string;
66
+ readonly changeKind: EnrichmentChangeKind;
67
+ readonly summary: string;
68
+ readonly draft: string;
69
+ readonly confidence?: unknown;
70
+ readonly id?: string;
71
+ readonly risk?: EnrichmentRisk;
72
+ }
73
+ export interface EnrichmentDraft {
74
+ readonly operations: readonly EnrichmentDraftOperation[];
75
+ readonly id?: string;
76
+ readonly hash?: string;
77
+ readonly projectRevision?: string;
78
+ }
79
+ /**
80
+ * A runner's own live answer to "can you serve `draft()` right now, and if
81
+ * not, why not". `reason` is a short, stable, machine-readable code (never
82
+ * free-form prose): it must already be safe to hand to a browser as-is, so
83
+ * it must never carry a runner/dispatcher path, an SDK session id, or any
84
+ * provider identity beyond what the caller's own runtime configuration
85
+ * already chose. The host route (`server/app.ts`) only forwards whatever
86
+ * `readiness()` returns; it never re-derives it from the runtime config
87
+ * itself, so a runner that grows a new refusal reason surfaces it here
88
+ * without the route needing to change.
89
+ */
90
+ export interface EnrichmentRunnerReadiness {
91
+ readonly available: boolean;
92
+ readonly reason?: string;
93
+ }
94
+ export interface EnrichmentRunner {
95
+ /**
96
+ * A runner only drafts a typed proposal. It must not mutate a project: the
97
+ * host validates and applies accepted operations through its completion
98
+ * ledger. The production constructor deliberately does not provide one
99
+ * until the dispatcher exposes this callback contract.
100
+ */
101
+ draft(input: {
102
+ readonly projectPath: string;
103
+ readonly mode: EnrichmentMode;
104
+ readonly projectRevision: string;
105
+ }): Promise<unknown>;
106
+ /**
107
+ * Optional live readiness check, independent of `draft()`: it must return
108
+ * without dispatching, without a live model turn, and without any other
109
+ * cost or side effect -- a pure read of whatever the runner already
110
+ * consults to decide whether to refuse (e.g. the current `AuthChoice`).
111
+ * Omitted only by test doubles that don't model refusal; such a runner is
112
+ * treated as always available, matching this capability's behavior before
113
+ * `readiness()` existed.
114
+ */
115
+ readiness?(): EnrichmentRunnerReadiness;
116
+ }
117
+ /** Trusted deterministic apply seam owned and invoked by the host. */
118
+ export interface EnrichmentApplyRunner {
119
+ apply(input: {
120
+ readonly projectPath: string;
121
+ readonly projectRevision: string;
122
+ readonly proposalHash: string;
123
+ readonly operation: EnrichmentOperation;
124
+ readonly idempotencyKey: string;
125
+ readonly fence: number;
126
+ }): Promise<{
127
+ readonly validationDigest: string;
128
+ readonly buildDigest: string;
129
+ }>;
130
+ reconcile(input: {
131
+ readonly idempotencyKey: string;
132
+ readonly fence: number;
133
+ }): Promise<{
134
+ readonly state: "applied" | "not_applied" | "still_unknown";
135
+ readonly validationDigest?: string;
136
+ readonly buildDigest?: string;
137
+ }>;
138
+ }
139
+ /** The host never sends proposal text to an approval provider. */
140
+ export interface EnrichmentApprovalRequest {
141
+ readonly runId: string;
142
+ readonly binding: EnrichmentBinding;
143
+ readonly proposalHash: string;
144
+ readonly operation: {
145
+ readonly id: string;
146
+ readonly sink: string;
147
+ readonly risk: EnrichmentRisk;
148
+ readonly changeKind: EnrichmentChangeKind;
149
+ readonly hash: string;
150
+ };
151
+ }
152
+ /** A trusted host callback returns an opaque, short-lived authorization record. */
153
+ export interface EnrichmentApprovalAttestation {
154
+ readonly evidenceRef: string;
155
+ readonly nonce: string;
156
+ readonly expiresAt: string;
157
+ readonly binding: EnrichmentBinding;
158
+ readonly proposalHash: string;
159
+ readonly operationHash: string;
160
+ }
161
+ export interface EnrichmentApprovalProvider {
162
+ attest(input: EnrichmentApprovalRequest): Promise<EnrichmentApprovalAttestation>;
163
+ }
164
+ export declare class EnrichmentContractError extends Error {
165
+ }
166
+ /**
167
+ * Identifies a project directory without requiring it to be built. This is
168
+ * the only resolution `bindProject` may use: binding must succeed for a
169
+ * freshly connected, not-yet-compiled project, so it can never read or
170
+ * require `target/mdl.json`.
171
+ */
172
+ export declare function resolveProjectIdentity(projectPath: string): ProjectIdentity;
173
+ /**
174
+ * Identifies a project directory *and* proves it is built. Only enrichment
175
+ * call sites may use this: they need a concrete revision to lock a run
176
+ * against or to fence an in-flight operation, and it is legitimate for them
177
+ * to refuse (via `EnrichmentContractError`) when the project isn't built.
178
+ */
179
+ export declare function resolveEnrichmentBinding(projectPath: string): UnversionedEnrichmentBinding;
180
+ export declare function sameEnrichmentBinding(left: EnrichmentBinding, right: EnrichmentBinding): boolean;
181
+ export declare function computeBoundProjectRevision(projectPath: string): string;
182
+ /**
183
+ * Converts untrusted model confidence into bounded UI text without making it
184
+ * an operation-validity gate. Unsafe, missing, or structured values become a
185
+ * neutral label instead of costing an otherwise valid proposal.
186
+ */
187
+ export declare function normalizeEnrichmentConfidence(value: unknown): string;
188
+ export declare function hashEnrichmentOperation(operation: Pick<EnrichmentOperation, "id" | "sink" | "risk" | "summary" | "draft" | "changeKind">): string;
189
+ export declare function canonicalizeProposal(draft: unknown, revision: string): EnrichmentProposal;
190
+ export declare function requiresApproval(mode: EnrichmentMode, risk: EnrichmentRisk): boolean;
191
+ /**
192
+ * The host-mediated MCP tool a native `context_enrichment` session uses to
193
+ * hand a drafted proposal to the host. The agent never writes into the bound
194
+ * project directly: this tool only carries the proposal *document* through
195
+ * to the same canonicalize -> shadow-verify -> finalize pipeline `POST
196
+ * /api/context/enrichment/start` already uses (see `server/app.ts`'s
197
+ * `settleEnrichmentDraft`). The schema below is deliberately permissive --
198
+ * `canonicalizeProposal` above is the single source of truth for shape and
199
+ * content validation, so this schema must not duplicate or drift from it.
200
+ */
201
+ export declare const NATIVE_ENRICHMENT_SUBMIT_MCP_TOOL_NAME = "submit_context_proposal";
202
+ export declare const NATIVE_ENRICHMENT_SUBMIT_INPUT_SCHEMA: {
203
+ readonly type: "object";
204
+ readonly additionalProperties: false;
205
+ readonly required: readonly ["operations"];
206
+ readonly properties: {
207
+ readonly operations: {
208
+ readonly type: "array";
209
+ readonly minItems: 1;
210
+ readonly items: {
211
+ readonly type: "object";
212
+ readonly additionalProperties: false;
213
+ readonly required: readonly ["sink", "changeKind", "summary", "draft"];
214
+ readonly properties: {
215
+ readonly sink: {
216
+ readonly type: "string";
217
+ };
218
+ readonly changeKind: {
219
+ readonly enum: readonly ["knowledge_append", "new_cube", "new_view", "new_relationship", "mdl_metric", "calculated_column", "conflict", "ambiguous"];
220
+ };
221
+ readonly summary: {
222
+ readonly type: "string";
223
+ };
224
+ readonly draft: {
225
+ readonly type: "string";
226
+ };
227
+ readonly confidence: {};
228
+ };
229
+ };
230
+ };
231
+ };
232
+ };