wotann 0.5.39 → 0.5.40

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 (130) hide show
  1. package/dist/autopilot/completion-oracle.d.ts +71 -0
  2. package/dist/autopilot/completion-oracle.d.ts.map +1 -1
  3. package/dist/autopilot/completion-oracle.js +69 -2
  4. package/dist/autopilot/completion-oracle.js.map +1 -1
  5. package/dist/context/compaction-floor.d.ts +100 -0
  6. package/dist/context/compaction-floor.d.ts.map +1 -0
  7. package/dist/context/compaction-floor.js +94 -0
  8. package/dist/context/compaction-floor.js.map +1 -0
  9. package/dist/daemon/kairos-rpc.d.ts +1 -0
  10. package/dist/daemon/kairos-rpc.d.ts.map +1 -1
  11. package/dist/daemon/kairos-rpc.js +24 -0
  12. package/dist/daemon/kairos-rpc.js.map +1 -1
  13. package/dist/daemon/rpc-handlers/ports-rpc.d.ts +100 -0
  14. package/dist/daemon/rpc-handlers/ports-rpc.d.ts.map +1 -0
  15. package/dist/daemon/rpc-handlers/ports-rpc.js +278 -0
  16. package/dist/daemon/rpc-handlers/ports-rpc.js.map +1 -0
  17. package/dist/hooks/agentmemory-event-taxonomy.d.ts +144 -0
  18. package/dist/hooks/agentmemory-event-taxonomy.d.ts.map +1 -0
  19. package/dist/hooks/agentmemory-event-taxonomy.js +85 -0
  20. package/dist/hooks/agentmemory-event-taxonomy.js.map +1 -0
  21. package/dist/hooks/built-in.d.ts.map +1 -1
  22. package/dist/hooks/built-in.js +7 -3
  23. package/dist/hooks/built-in.js.map +1 -1
  24. package/dist/index.js +2 -1
  25. package/dist/index.js.map +1 -1
  26. package/dist/lib.d.ts +1 -1
  27. package/dist/lib.d.ts.map +1 -1
  28. package/dist/lib.js +1 -1
  29. package/dist/lib.js.map +1 -1
  30. package/dist/memory/injection-scanner.d.ts +78 -0
  31. package/dist/memory/injection-scanner.d.ts.map +1 -0
  32. package/dist/memory/injection-scanner.js +204 -0
  33. package/dist/memory/injection-scanner.js.map +1 -0
  34. package/dist/middleware/ttsr.d.ts +79 -3
  35. package/dist/middleware/ttsr.d.ts.map +1 -1
  36. package/dist/middleware/ttsr.js +136 -16
  37. package/dist/middleware/ttsr.js.map +1 -1
  38. package/dist/orchestration/ports-bridge.d.ts +325 -0
  39. package/dist/orchestration/ports-bridge.d.ts.map +1 -0
  40. package/dist/orchestration/ports-bridge.js +712 -0
  41. package/dist/orchestration/ports-bridge.js.map +1 -0
  42. package/dist/orchestration/textgrad-refinement.d.ts +123 -0
  43. package/dist/orchestration/textgrad-refinement.d.ts.map +1 -0
  44. package/dist/orchestration/textgrad-refinement.js +111 -0
  45. package/dist/orchestration/textgrad-refinement.js.map +1 -0
  46. package/dist/prompt/engine.d.ts +15 -0
  47. package/dist/prompt/engine.d.ts.map +1 -1
  48. package/dist/prompt/engine.js +20 -7
  49. package/dist/prompt/engine.js.map +1 -1
  50. package/dist/prompt/modules/capabilities.d.ts.map +1 -1
  51. package/dist/prompt/modules/capabilities.js +1 -0
  52. package/dist/prompt/modules/capabilities.js.map +1 -1
  53. package/dist/prompt/modules/caveman.d.ts.map +1 -1
  54. package/dist/prompt/modules/caveman.js +1 -0
  55. package/dist/prompt/modules/caveman.js.map +1 -1
  56. package/dist/prompt/modules/conventions.d.ts.map +1 -1
  57. package/dist/prompt/modules/conventions.js +1 -0
  58. package/dist/prompt/modules/conventions.js.map +1 -1
  59. package/dist/prompt/modules/identity.d.ts.map +1 -1
  60. package/dist/prompt/modules/identity.js +1 -0
  61. package/dist/prompt/modules/identity.js.map +1 -1
  62. package/dist/prompt/modules/index.d.ts +29 -0
  63. package/dist/prompt/modules/index.d.ts.map +1 -1
  64. package/dist/prompt/modules/index.js +51 -9
  65. package/dist/prompt/modules/index.js.map +1 -1
  66. package/dist/prompt/modules/llms-txt.d.ts.map +1 -1
  67. package/dist/prompt/modules/llms-txt.js +2 -4
  68. package/dist/prompt/modules/llms-txt.js.map +1 -1
  69. package/dist/prompt/modules/safety.d.ts.map +1 -1
  70. package/dist/prompt/modules/safety.js +1 -0
  71. package/dist/prompt/modules/safety.js.map +1 -1
  72. package/dist/prompt/modules/security.d.ts.map +1 -1
  73. package/dist/prompt/modules/security.js +1 -0
  74. package/dist/prompt/modules/security.js.map +1 -1
  75. package/dist/prompt/modules/skills.d.ts.map +1 -1
  76. package/dist/prompt/modules/skills.js +1 -0
  77. package/dist/prompt/modules/skills.js.map +1 -1
  78. package/dist/prompt/modules/tools.d.ts.map +1 -1
  79. package/dist/prompt/modules/tools.js +1 -0
  80. package/dist/prompt/modules/tools.js.map +1 -1
  81. package/dist/prompt/modules/user.d.ts.map +1 -1
  82. package/dist/prompt/modules/user.js +1 -0
  83. package/dist/prompt/modules/user.js.map +1 -1
  84. package/dist/providers/credential-pool.d.ts +45 -1
  85. package/dist/providers/credential-pool.d.ts.map +1 -1
  86. package/dist/providers/credential-pool.js +94 -1
  87. package/dist/providers/credential-pool.js.map +1 -1
  88. package/dist/providers/sticky-rotation-wire.d.ts +133 -0
  89. package/dist/providers/sticky-rotation-wire.d.ts.map +1 -0
  90. package/dist/providers/sticky-rotation-wire.js +185 -0
  91. package/dist/providers/sticky-rotation-wire.js.map +1 -0
  92. package/dist/runtime-hooks/ttsr-rule.d.ts +82 -0
  93. package/dist/runtime-hooks/ttsr-rule.d.ts.map +1 -0
  94. package/dist/runtime-hooks/ttsr-rule.js +207 -0
  95. package/dist/runtime-hooks/ttsr-rule.js.map +1 -0
  96. package/dist/runtime-hooks/ttsr-runner.d.ts +129 -0
  97. package/dist/runtime-hooks/ttsr-runner.d.ts.map +1 -0
  98. package/dist/runtime-hooks/ttsr-runner.js +193 -0
  99. package/dist/runtime-hooks/ttsr-runner.js.map +1 -0
  100. package/dist/runtime-hooks/ttsr-scope.d.ts +115 -0
  101. package/dist/runtime-hooks/ttsr-scope.d.ts.map +1 -0
  102. package/dist/runtime-hooks/ttsr-scope.js +378 -0
  103. package/dist/runtime-hooks/ttsr-scope.js.map +1 -0
  104. package/dist/sandbox/unified-exec.d.ts.map +1 -1
  105. package/dist/sandbox/unified-exec.js +6 -1
  106. package/dist/sandbox/unified-exec.js.map +1 -1
  107. package/dist/skills/cli-anything.d.ts.map +1 -1
  108. package/dist/skills/cli-anything.js +4 -1
  109. package/dist/skills/cli-anything.js.map +1 -1
  110. package/dist/skills/limits.d.ts +86 -0
  111. package/dist/skills/limits.d.ts.map +1 -0
  112. package/dist/skills/limits.js +140 -0
  113. package/dist/skills/limits.js.map +1 -0
  114. package/dist/skills/loader.d.ts +9 -0
  115. package/dist/skills/loader.d.ts.map +1 -1
  116. package/dist/skills/loader.js +29 -1
  117. package/dist/skills/loader.js.map +1 -1
  118. package/dist/storage/session-artifacts.d.ts +105 -0
  119. package/dist/storage/session-artifacts.d.ts.map +1 -0
  120. package/dist/storage/session-artifacts.js +198 -0
  121. package/dist/storage/session-artifacts.js.map +1 -0
  122. package/dist/tools/workspace-pack.d.ts +97 -0
  123. package/dist/tools/workspace-pack.d.ts.map +1 -0
  124. package/dist/tools/workspace-pack.js +228 -0
  125. package/dist/tools/workspace-pack.js.map +1 -0
  126. package/dist/tui/composer-history.d.ts +99 -0
  127. package/dist/tui/composer-history.d.ts.map +1 -0
  128. package/dist/tui/composer-history.js +169 -0
  129. package/dist/tui/composer-history.js.map +1 -0
  130. package/package.json +1 -1
@@ -0,0 +1,712 @@
1
+ /**
2
+ * ports-bridge — business-level bridge functions for the 4 zombie ports.
3
+ *
4
+ * The audit finding #8 surfaced that four port primitives shipped under
5
+ * `src/ports/*` are library-only — they're re-exported from `src/lib.ts`
6
+ * but no internal consumer reaches them. They were never callable from
7
+ * the daemon's RPC surface, because each requires dependency injection
8
+ * (converse callback, search provider, graph store, dom adapter) that
9
+ * a JSON-RPC frame cannot transport.
10
+ *
11
+ * This module is the missing seam:
12
+ *
13
+ * - Each port is a *library primitive*. The bridge constructs the
14
+ * dependency-injected primitive INTERNALLY from business-level inputs
15
+ * (a chatdev waterfall spec + query, a research query, a text blob to
16
+ * ingest into a memory graph, a DOM snapshot to locate against).
17
+ *
18
+ * - The daemon RPC handlers (src/daemon/rpc-handlers/ports-rpc.ts) call
19
+ * these bridge functions one-to-one. They are the *only* in-process
20
+ * consumer; the bridge does not call them back. No cycle.
21
+ *
22
+ * - Each bridge function follows the QB#3 honest-stub policy: if a real
23
+ * dependency cannot be reached (no LLM provider for chatdev, no
24
+ * search provider for deer-flow), the bridge returns an `ok:false`
25
+ * envelope with a verbatim `reason` string. It NEVER silently
26
+ * succeeds with mock data.
27
+ *
28
+ * - For mem0-graph and scrapling-adaptive the in-memory backends are
29
+ * always available, so the bridge wires those by default and only
30
+ * surfaces failure when the input itself is malformed.
31
+ *
32
+ * QB bars honoured:
33
+ * QB#3 honest stub — no silent success
34
+ * QB#7 zero module-level mutable state — every bridge call constructs
35
+ * its own primitives
36
+ * QB#10 errors surface verbatim
37
+ * QB#13 no direct env reads except through the audit-friendly
38
+ * `WOTANN_*` knobs documented per-function
39
+ * QB#19 every type/exports is consumed by at least one handler
40
+ *
41
+ * Imports use ESM `.js` suffixes; strict TypeScript with ES2022 target.
42
+ */
43
+ import { promises as fsp } from "node:fs";
44
+ import { dirname } from "node:path";
45
+ import { runWaterfall, defaultWaterfall, fixtureConverse, DEFAULT_ROLES, } from "../ports/chatdev-waterfall.js";
46
+ import { runResearch, fixturePipeline, } from "../ports/deer-flow-research.js";
47
+ import { ingestText, neighborhood, createInMemoryGraphStore, } from "../ports/mem0-graph-memory.js";
48
+ import { adaptiveLocate, createInMemoryFingerprintStore, } from "../ports/scrapling-adaptive.js";
49
+ import { resolveWotannHomeSubdir } from "../utils/wotann-home.js";
50
+ import { writeFileAtomic } from "../utils/atomic-io.js";
51
+ /**
52
+ * Build a converse function that drives one phase through the runtime.
53
+ * On any provider error it surfaces the message verbatim (QB#10).
54
+ */
55
+ function buildRuntimeConverse(runtime) {
56
+ return async (rolePair, inputArtifact, charter) => {
57
+ const phaseId = `${rolePair.initiator.id}->${rolePair.responder.id}`;
58
+ const systemPrompt = [
59
+ `You are pairing the roles ${rolePair.initiator.name} and ${rolePair.responder.name}.`,
60
+ `Their joint charter:`,
61
+ charter,
62
+ `Refine the incoming artifact and respond with the refined artifact only.`,
63
+ `Do not invent new sections; thread the artifact forward.`,
64
+ ].join("\n");
65
+ let assistantText;
66
+ try {
67
+ assistantText = await runtime.complete(inputArtifact, systemPrompt);
68
+ }
69
+ catch (err) {
70
+ const msg = err instanceof Error ? err.message : String(err);
71
+ return {
72
+ phaseId,
73
+ ok: false,
74
+ reason: `runtime converse threw: ${msg}`,
75
+ transcript: [
76
+ {
77
+ speakerRoleId: rolePair.initiator.id,
78
+ text: inputArtifact,
79
+ meta: { kind: "runtime-input" },
80
+ },
81
+ ],
82
+ artifact: inputArtifact,
83
+ };
84
+ }
85
+ return {
86
+ phaseId,
87
+ ok: true,
88
+ reason: null,
89
+ transcript: [
90
+ {
91
+ speakerRoleId: rolePair.initiator.id,
92
+ text: inputArtifact,
93
+ meta: { kind: "runtime-input" },
94
+ },
95
+ {
96
+ speakerRoleId: rolePair.responder.id,
97
+ text: assistantText,
98
+ meta: { kind: "runtime-output" },
99
+ },
100
+ ],
101
+ artifact: assistantText.length > 0 ? assistantText : inputArtifact,
102
+ };
103
+ };
104
+ }
105
+ function resolveRoleById(id) {
106
+ for (const r of Object.values(DEFAULT_ROLES)) {
107
+ if (r.id === id)
108
+ return r;
109
+ }
110
+ return null;
111
+ }
112
+ /**
113
+ * Bridge: run a ChatDev waterfall.
114
+ *
115
+ * @param spec Optional phase override. When absent the default 5-phase
116
+ * waterfall (Demand -> Design -> Implement -> Review -> Test)
117
+ * is used. Phase role ids must reference DEFAULT_ROLES.
118
+ * @param query The seed artifact handed to the first phase.
119
+ * @param options Runtime ref + fixture override flag.
120
+ */
121
+ export async function runChatDevWaterfall(spec, query, options = {}) {
122
+ if (typeof query !== "string" || query.trim().length === 0) {
123
+ return { ok: false, reason: "query must be a non-empty string" };
124
+ }
125
+ const useRuntime = !options.forceFixture && options.runtime !== undefined;
126
+ // QB#3: when a runtime is requested but unreachable we never silently
127
+ // swap in fixtures — the caller asked for runtime mode. forceFixture
128
+ // is the explicit opt-in for fixture mode.
129
+ if (options.runtime !== undefined && options.forceFixture) {
130
+ // explicit fixture mode wins — caller asked for it
131
+ }
132
+ const converse = useRuntime
133
+ ? buildRuntimeConverse(options.runtime)
134
+ : fixtureConverse();
135
+ let phases;
136
+ if (spec?.phases && spec.phases.length > 0) {
137
+ const built = [];
138
+ for (const p of spec.phases) {
139
+ const initiator = resolveRoleById(p.initiatorId);
140
+ const responder = resolveRoleById(p.responderId);
141
+ if (!initiator || !responder) {
142
+ return {
143
+ ok: false,
144
+ reason: `unknown role id in phase '${p.id}': initiatorId='${p.initiatorId}', responderId='${p.responderId}'. Valid: ${Object.keys(DEFAULT_ROLES).join(", ")}`,
145
+ };
146
+ }
147
+ built.push({
148
+ id: p.id,
149
+ description: p.description,
150
+ initiator,
151
+ responder,
152
+ converse,
153
+ });
154
+ }
155
+ phases = built;
156
+ }
157
+ else {
158
+ // Default waterfall — replace the fixture converser on every phase
159
+ // with the runtime-backed one when runtime mode is active.
160
+ const base = defaultWaterfall();
161
+ phases = base.map((ph) => ({ ...ph, converse }));
162
+ }
163
+ const waterfallOptions = spec?.globalCharter
164
+ ? { seed: query, globalCharter: spec.globalCharter }
165
+ : { seed: query };
166
+ const result = await runWaterfall(phases, waterfallOptions);
167
+ return {
168
+ ok: true,
169
+ result,
170
+ mode: useRuntime ? "runtime" : "fixture",
171
+ };
172
+ }
173
+ /**
174
+ * Build a planner that asks the runtime to decompose the question into
175
+ * sub-questions and parses a JSON array out of the response. If parsing
176
+ * fails we fall back to one heuristic sub-question (the original
177
+ * question) — that's honest: we tried and got a malformed response, so
178
+ * we still do one researcher pass.
179
+ */
180
+ function buildRuntimePlanner(runtime, maxSubQuestions) {
181
+ return async (query) => {
182
+ const systemPrompt = [
183
+ `You are decomposing a research query into sub-questions.`,
184
+ `Respond ONLY with a JSON array of up to ${maxSubQuestions} sub-questions.`,
185
+ `Format: [{ "id": "subq-1", "text": "...", "rationale": "..." }, ...]`,
186
+ ].join("\n");
187
+ let response;
188
+ try {
189
+ response = await runtime.complete(query.question, systemPrompt);
190
+ }
191
+ catch (err) {
192
+ const msg = err instanceof Error ? err.message : String(err);
193
+ throw new Error(`planner runtime failed: ${msg}`);
194
+ }
195
+ const parsed = tryParseSubQuestions(response, maxSubQuestions);
196
+ if (parsed.length > 0)
197
+ return parsed;
198
+ return [
199
+ {
200
+ id: "subq-1",
201
+ text: query.question,
202
+ rationale: `runtime planner returned no parseable sub-questions; using original question. Raw: ${response.slice(0, 200)}`,
203
+ },
204
+ ];
205
+ };
206
+ }
207
+ function tryParseSubQuestions(raw, max) {
208
+ // Extract first JSON array between brackets.
209
+ const match = raw.match(/\[[\s\S]*\]/);
210
+ if (!match)
211
+ return [];
212
+ let parsed;
213
+ try {
214
+ parsed = JSON.parse(match[0]);
215
+ }
216
+ catch {
217
+ return [];
218
+ }
219
+ if (!Array.isArray(parsed))
220
+ return [];
221
+ const out = [];
222
+ for (let i = 0; i < parsed.length && out.length < max; i++) {
223
+ const entry = parsed[i];
224
+ if (typeof entry !== "object" || entry === null)
225
+ continue;
226
+ const obj = entry;
227
+ const text = typeof obj["text"] === "string" ? obj["text"] : null;
228
+ if (!text || text.length === 0)
229
+ continue;
230
+ const id = typeof obj["id"] === "string" && obj["id"].length > 0 ? obj["id"] : `subq-${out.length + 1}`;
231
+ const rationale = typeof obj["rationale"] === "string" ? obj["rationale"] : "";
232
+ out.push({ id, text, rationale });
233
+ }
234
+ return out;
235
+ }
236
+ /**
237
+ * Build a researcher that gathers evidence via the SearchProvider and
238
+ * (optionally) summarises via the runtime. If the runtime is absent
239
+ * the summary is empty — honest: we didn't have an LLM to write one.
240
+ */
241
+ function buildResearcher(searchProvider, runtime) {
242
+ return async (subQuestion) => {
243
+ let hits;
244
+ try {
245
+ hits = await searchProvider(subQuestion.text);
246
+ }
247
+ catch (err) {
248
+ const msg = err instanceof Error ? err.message : String(err);
249
+ return {
250
+ subQuestionId: subQuestion.id,
251
+ evidence: [],
252
+ summary: "",
253
+ error: `search provider threw: ${msg}`,
254
+ };
255
+ }
256
+ const evidence = hits.map((h) => ({
257
+ source: h.url,
258
+ snippet: h.snippet,
259
+ meta: {
260
+ title: h.title,
261
+ ...(h.sourceEngine ? { engine: h.sourceEngine } : {}),
262
+ },
263
+ }));
264
+ let summary = "";
265
+ if (runtime !== undefined && evidence.length > 0) {
266
+ const systemPrompt = [
267
+ `You are summarising research evidence for ONE sub-question.`,
268
+ `Cite each piece by index. Be terse — 3-5 sentences max.`,
269
+ ].join("\n");
270
+ const prompt = [
271
+ `Sub-question: ${subQuestion.text}`,
272
+ `Evidence:`,
273
+ ...evidence.map((e, i) => `[${i + 1}] ${e.source}: ${e.snippet}`),
274
+ ].join("\n");
275
+ try {
276
+ summary = await runtime.complete(prompt, systemPrompt);
277
+ }
278
+ catch (err) {
279
+ const msg = err instanceof Error ? err.message : String(err);
280
+ return {
281
+ subQuestionId: subQuestion.id,
282
+ evidence,
283
+ summary: "",
284
+ error: `summariser runtime failed: ${msg}`,
285
+ };
286
+ }
287
+ }
288
+ return {
289
+ subQuestionId: subQuestion.id,
290
+ evidence,
291
+ summary,
292
+ error: null,
293
+ };
294
+ };
295
+ }
296
+ /**
297
+ * Build a reporter that synthesises findings into a final report. With
298
+ * no runtime ref the synthesis is a deterministic concatenation of the
299
+ * findings' summaries — honest stub.
300
+ */
301
+ function buildReporter(runtime) {
302
+ return async (query, _subQuestions, findings) => {
303
+ const allCitations = [];
304
+ for (const f of findings) {
305
+ for (const c of f.evidence)
306
+ allCitations.push(c);
307
+ }
308
+ if (!runtime) {
309
+ const synthesis = findings
310
+ .filter((f) => f.summary.length > 0 || f.evidence.length > 0)
311
+ .map((f) => f.summary || `(${f.evidence.length} pieces of evidence)`)
312
+ .join("\n\n");
313
+ return { synthesis, citations: allCitations };
314
+ }
315
+ const systemPrompt = [
316
+ `You are writing the final research report.`,
317
+ `Cite each piece of evidence by its [source] index.`,
318
+ `Structure: brief intro, then per-aspect findings, then closing summary.`,
319
+ ].join("\n");
320
+ const prompt = [
321
+ `Research question: ${query.question}`,
322
+ `Findings:`,
323
+ ...findings.map((f, i) => `## Finding ${i + 1}\n${f.summary || "(no summary)"}`),
324
+ ].join("\n\n");
325
+ try {
326
+ const synthesis = await runtime.complete(prompt, systemPrompt);
327
+ return { synthesis, citations: allCitations };
328
+ }
329
+ catch (err) {
330
+ const msg = err instanceof Error ? err.message : String(err);
331
+ throw new Error(`reporter runtime failed: ${msg}`);
332
+ }
333
+ };
334
+ }
335
+ /**
336
+ * Bridge: run the deer-flow research pipeline.
337
+ *
338
+ * Requires (at minimum) a `searchProvider` for evidence gathering. With
339
+ * just the search provider the bridge still produces a complete report:
340
+ * the planner returns one sub-question (the user query), the researcher
341
+ * returns the raw hits as evidence, and the reporter concatenates the
342
+ * raw evidence. With a `runtime` ref the planner+reporter become
343
+ * LLM-driven and the researcher gains an evidence-summarisation step.
344
+ *
345
+ * When neither is supplied AND `forceFixture` is false, returns ok:false
346
+ * with reason "no search provider supplied". This is QB#3 — research
347
+ * without retrieval is hallucination.
348
+ */
349
+ export async function runDeerFlowResearch(query, options = {}) {
350
+ if (typeof query !== "string" || query.trim().length === 0) {
351
+ return { ok: false, reason: "query must be a non-empty string" };
352
+ }
353
+ const maxSubQuestions = options.maxSubQuestions ?? 3;
354
+ if (!Number.isFinite(maxSubQuestions) || maxSubQuestions <= 0) {
355
+ return { ok: false, reason: "maxSubQuestions must be a positive integer" };
356
+ }
357
+ const useFixture = options.forceFixture === true || options.searchProvider === undefined;
358
+ if (useFixture && !options.forceFixture) {
359
+ return {
360
+ ok: false,
361
+ reason: "no search provider supplied. Pass `searchProvider` (a SearchProvider callback) or set `forceFixture:true` to run the deterministic placeholder pipeline.",
362
+ };
363
+ }
364
+ let pipeline;
365
+ if (options.forceFixture === true) {
366
+ pipeline = fixturePipeline();
367
+ }
368
+ else {
369
+ const searchProvider = options.searchProvider;
370
+ pipeline = {
371
+ planner: options.runtime
372
+ ? buildRuntimePlanner(options.runtime, maxSubQuestions)
373
+ : (q) => [
374
+ {
375
+ id: "subq-1",
376
+ text: q.question,
377
+ rationale: "no runtime planner; using original question",
378
+ },
379
+ ],
380
+ researcher: buildResearcher(searchProvider, options.runtime),
381
+ reporter: buildReporter(options.runtime),
382
+ };
383
+ }
384
+ const researchQuery = {
385
+ question: query,
386
+ maxSubQuestions,
387
+ };
388
+ const outcome = await runResearch(researchQuery, pipeline);
389
+ return {
390
+ ok: true,
391
+ outcome,
392
+ mode: options.forceFixture === true ? "fixture" : "runtime",
393
+ };
394
+ }
395
+ async function loadGraphSnapshot(path) {
396
+ try {
397
+ const raw = await fsp.readFile(path, "utf8");
398
+ const parsed = JSON.parse(raw);
399
+ if (!parsed || typeof parsed !== "object")
400
+ return null;
401
+ const entities = Array.isArray(parsed.entities) ? parsed.entities : [];
402
+ const edges = Array.isArray(parsed.edges) ? parsed.edges : [];
403
+ return {
404
+ entities: entities.filter((e) => typeof e === "object" &&
405
+ e !== null &&
406
+ typeof e.id === "string" &&
407
+ typeof e.type === "string"),
408
+ edges: edges.filter((e) => typeof e === "object" &&
409
+ e !== null &&
410
+ typeof e.from === "string" &&
411
+ typeof e.to === "string" &&
412
+ typeof e.relation === "string"),
413
+ };
414
+ }
415
+ catch {
416
+ return null;
417
+ }
418
+ }
419
+ function rehydrateStore(store, snapshot) {
420
+ for (const e of snapshot.entities)
421
+ store.addEntity(e);
422
+ for (const ed of snapshot.edges)
423
+ store.addEdge(ed);
424
+ }
425
+ function snapshotStore(store) {
426
+ const entities = store.getEntities();
427
+ const edges = [];
428
+ for (const e of entities) {
429
+ for (const out of store.getEdgesFrom(e.id))
430
+ edges.push(out);
431
+ }
432
+ return { entities, edges };
433
+ }
434
+ /**
435
+ * Bridge: ingest text into a mem0-style graph memory.
436
+ *
437
+ * Uses `createInMemoryGraphStore` by default (no persistence). When
438
+ * `persist:true` the store is rehydrated from + flushed back to
439
+ * `${WOTANN_HOME}/mem-graph.json`. Returns the extracted triples plus a
440
+ * 2-hop neighborhood walk for every subject as a stable read view.
441
+ */
442
+ export async function ingestTextToMemGraph(text, options = {}) {
443
+ if (typeof text !== "string") {
444
+ return { ok: false, reason: "text must be a string" };
445
+ }
446
+ if (text.trim().length === 0) {
447
+ return {
448
+ ok: true,
449
+ triples: [],
450
+ entitiesAdded: 0,
451
+ edgesAdded: 0,
452
+ neighborhoods: [],
453
+ persisted: false,
454
+ };
455
+ }
456
+ const store = createInMemoryGraphStore();
457
+ const persist = options.persist === true;
458
+ const path = options.persistencePath ?? resolveWotannHomeSubdir("mem-graph.json");
459
+ if (persist) {
460
+ const snapshot = await loadGraphSnapshot(path);
461
+ if (snapshot)
462
+ rehydrateStore(store, snapshot);
463
+ }
464
+ const ingestOptions = options.turnId !== undefined && options.llm !== undefined
465
+ ? { turnId: options.turnId, llm: options.llm }
466
+ : options.turnId !== undefined
467
+ ? { turnId: options.turnId }
468
+ : options.llm !== undefined
469
+ ? { llm: options.llm }
470
+ : {};
471
+ const result = await ingestText(store, text, ingestOptions);
472
+ if (!result.ok) {
473
+ return { ok: false, reason: result.reason };
474
+ }
475
+ // Build 2-hop neighborhood for each subject. Deduplicate by entity id.
476
+ const seen = new Set();
477
+ const neighborhoods = [];
478
+ for (const t of result.triples) {
479
+ if (seen.has(t.subject.id))
480
+ continue;
481
+ seen.add(t.subject.id);
482
+ neighborhoods.push({
483
+ entityId: t.subject.id,
484
+ edges: neighborhood(store, t.subject.id, 2),
485
+ });
486
+ }
487
+ let persisted = false;
488
+ if (persist) {
489
+ try {
490
+ const out = snapshotStore(store);
491
+ const json = JSON.stringify(out, null, 2);
492
+ const parent = dirname(path);
493
+ await fsp.mkdir(parent, { recursive: true });
494
+ writeFileAtomic(path, json);
495
+ persisted = true;
496
+ }
497
+ catch (err) {
498
+ const msg = err instanceof Error ? err.message : String(err);
499
+ return { ok: false, reason: `persist failed: ${msg}` };
500
+ }
501
+ }
502
+ return {
503
+ ok: true,
504
+ triples: result.triples,
505
+ entitiesAdded: result.entitiesAdded,
506
+ edgesAdded: result.edgesAdded,
507
+ neighborhoods,
508
+ persisted,
509
+ };
510
+ }
511
+ function buildBridgeTree(snapshot) {
512
+ let counter = 0;
513
+ const all = [];
514
+ function build(raw, parent, siblingIndex) {
515
+ const node = {
516
+ id: counter++,
517
+ tag: raw.tag,
518
+ classes: Array.isArray(raw.classes) ? [...raw.classes] : [],
519
+ text: typeof raw.text === "string" ? raw.text : "",
520
+ parent,
521
+ siblingIndex,
522
+ children: [],
523
+ };
524
+ all.push(node);
525
+ if (Array.isArray(raw.children)) {
526
+ raw.children.forEach((c, i) => {
527
+ node.children.push(build(c, node, i));
528
+ });
529
+ }
530
+ return node;
531
+ }
532
+ const root = build(snapshot.root, null, 0);
533
+ return { root, all };
534
+ }
535
+ /**
536
+ * Tiny CSS-shaped selector: supports `tag`, `.class`, `tag.class`, and
537
+ * descendant combinators ` ` (space). No pseudo-classes, no attribute
538
+ * selectors. Sufficient for the bridge's locate-by-fingerprint use case;
539
+ * stronger selectors are a future port-side change.
540
+ */
541
+ function querySelectorAll(nodes, selector) {
542
+ const trimmed = selector.trim();
543
+ if (trimmed.length === 0)
544
+ return [];
545
+ const parts = trimmed.split(/\s+/);
546
+ function matchOne(part, node) {
547
+ // Split into tag + classes
548
+ const tagMatch = part.match(/^([a-zA-Z][a-zA-Z0-9-]*)?/);
549
+ const tag = tagMatch?.[1] ?? "";
550
+ const classes = part.match(/\.([a-zA-Z0-9_-]+)/g);
551
+ if (tag.length > 0 && node.tag !== tag)
552
+ return false;
553
+ if (classes) {
554
+ for (const c of classes) {
555
+ const className = c.slice(1);
556
+ if (!node.classes.includes(className))
557
+ return false;
558
+ }
559
+ }
560
+ return true;
561
+ }
562
+ function isAncestor(ancestor, descendant) {
563
+ let p = descendant.parent;
564
+ while (p) {
565
+ if (p === ancestor)
566
+ return true;
567
+ p = p.parent;
568
+ }
569
+ return false;
570
+ }
571
+ // Match the last part directly, then walk back through earlier parts.
572
+ const lastPart = parts[parts.length - 1] ?? "";
573
+ const candidates = nodes.filter((n) => matchOne(lastPart, n));
574
+ if (parts.length === 1)
575
+ return candidates;
576
+ return candidates.filter((c) => {
577
+ let current = c;
578
+ for (let i = parts.length - 2; i >= 0; i--) {
579
+ const part = parts[i];
580
+ if (part === undefined)
581
+ return false;
582
+ // Walk up looking for an ancestor matching part[i].
583
+ let found = null;
584
+ let p = current?.parent ?? null;
585
+ while (p) {
586
+ if (matchOne(part, p)) {
587
+ found = p;
588
+ break;
589
+ }
590
+ p = p.parent;
591
+ }
592
+ if (!found)
593
+ return false;
594
+ current = found;
595
+ }
596
+ return true;
597
+ });
598
+ }
599
+ function buildAdapter(root, allNodes) {
600
+ return {
601
+ query: (selector) => querySelectorAll(allNodes, selector),
602
+ queryFrom: (rootNode, selector) => {
603
+ const subset = [];
604
+ function walk(n) {
605
+ subset.push(n);
606
+ for (const c of n.children)
607
+ walk(c);
608
+ }
609
+ walk(rootNode);
610
+ return querySelectorAll(subset, selector);
611
+ },
612
+ getTag: (n) => n.tag,
613
+ getClasses: (n) => n.classes,
614
+ getText: (n) => n.text,
615
+ getParent: (n) => n.parent,
616
+ getSiblingIndex: (n) => n.siblingIndex,
617
+ walkAll: () => allNodes,
618
+ };
619
+ }
620
+ /**
621
+ * Bridge: adaptively locate a node by label within a DOM snapshot.
622
+ *
623
+ * The caller submits:
624
+ * - a DomSnapshot (a JSON tree of nodes)
625
+ * - a query label
626
+ * - either:
627
+ * * a `selector` string, in which case the bridge runs adaptiveLocate
628
+ * (which saves a fingerprint AND returns the located node), OR
629
+ * * just a label, in which case the bridge runs relocate against
630
+ * a previously-saved fingerprint. Since the store is per-call,
631
+ * the snapshot must include `seeds` to pre-populate the store.
632
+ *
633
+ * Returns the located node's tag, classes, and a text preview, along
634
+ * with the similarity score and whether the original selector was used.
635
+ *
636
+ * Per-call state: the FingerprintStore is created fresh inside this
637
+ * function (QB#7). For session-scoped fingerprints, a follow-up RPC
638
+ * surface can pass a long-lived store ref — that's deferred to the
639
+ * scrapling-adaptive port itself, not this bridge.
640
+ */
641
+ export async function adaptiveLocateOnDom(domSnapshot, query, options = {}) {
642
+ if (!domSnapshot || typeof domSnapshot !== "object" || !domSnapshot.root) {
643
+ return { ok: false, reason: "domSnapshot must include a root node" };
644
+ }
645
+ if (!query || typeof query.label !== "string" || query.label.length === 0) {
646
+ return { ok: false, reason: "query.label must be a non-empty string" };
647
+ }
648
+ const { all } = buildBridgeTree(domSnapshot);
649
+ if (all.length === 0) {
650
+ return { ok: false, reason: "snapshot produced zero nodes" };
651
+ }
652
+ const root = all[0];
653
+ if (!root) {
654
+ return { ok: false, reason: "snapshot produced zero nodes" };
655
+ }
656
+ const adapter = buildAdapter(root, all);
657
+ const store = createInMemoryFingerprintStore();
658
+ // Pre-seed the store with any caller-supplied (label, selector) pairs.
659
+ if (domSnapshot.seeds) {
660
+ for (const seed of domSnapshot.seeds) {
661
+ const result = adaptiveLocate(adapter, store, seed.label, seed.selector, {});
662
+ if (!result.ok) {
663
+ // Seeds that miss don't kill the whole call — they just don't
664
+ // contribute fingerprints. We log the miss in the reason field
665
+ // only if the eventual locate fails.
666
+ void result;
667
+ }
668
+ }
669
+ }
670
+ const relocateOptions = options.minScore !== undefined ? { minScore: options.minScore } : {};
671
+ const result = query.selector
672
+ ? adaptiveLocate(adapter, store, query.label, query.selector, relocateOptions)
673
+ : (() => {
674
+ const saved = store.load(query.label);
675
+ if (!saved) {
676
+ return {
677
+ ok: false,
678
+ reason: `no selector supplied and no fingerprint saved under label "${query.label}". Provide a selector or pre-seed via domSnapshot.seeds.`,
679
+ };
680
+ }
681
+ return adaptiveLocate(adapter, store, query.label, saved.originalSelector, relocateOptions);
682
+ })();
683
+ if (!result.ok) {
684
+ return { ok: false, reason: result.reason };
685
+ }
686
+ const text = result.node.text;
687
+ return {
688
+ ok: true,
689
+ label: query.label,
690
+ node: {
691
+ tag: result.node.tag,
692
+ classes: result.node.classes,
693
+ textPreview: text.length > 80 ? `${text.slice(0, 77)}...` : text,
694
+ },
695
+ score: result.score,
696
+ viaOriginalSelector: result.viaOriginalSelector,
697
+ };
698
+ }
699
+ // ── Test-only exports (QB#14 real-code-path) ─────────────
700
+ export const _testOnly = {
701
+ tryParseSubQuestions,
702
+ buildRuntimeConverse,
703
+ buildRuntimePlanner,
704
+ buildResearcher,
705
+ buildReporter,
706
+ buildBridgeTree,
707
+ buildAdapter,
708
+ querySelectorAll,
709
+ loadGraphSnapshot,
710
+ snapshotStore,
711
+ };
712
+ //# sourceMappingURL=ports-bridge.js.map