@wrenautomation/birdwatch 0.0.0-stage → 0.30.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (141) hide show
  1. package/LICENSE.md +61 -0
  2. package/README.md +320 -2
  3. package/dist/app/cli.d.ts +4 -0
  4. package/dist/app/cli.js +149 -0
  5. package/dist/app/cli.js.map +1 -0
  6. package/dist/app/engine-cli.d.ts +44 -0
  7. package/dist/app/engine-cli.js +596 -0
  8. package/dist/app/engine-cli.js.map +1 -0
  9. package/dist/app/index.d.ts +6 -0
  10. package/dist/app/index.js +7 -0
  11. package/dist/app/index.js.map +1 -0
  12. package/dist/app/serve.d.ts +14 -0
  13. package/dist/app/serve.js +102 -0
  14. package/dist/app/serve.js.map +1 -0
  15. package/dist/document/body.d.ts +6 -0
  16. package/dist/document/body.js +18 -0
  17. package/dist/document/body.js.map +1 -0
  18. package/dist/document/check.d.ts +35 -0
  19. package/dist/document/check.js +68 -0
  20. package/dist/document/check.js.map +1 -0
  21. package/dist/document/extract.d.ts +81 -0
  22. package/dist/document/extract.js +226 -0
  23. package/dist/document/extract.js.map +1 -0
  24. package/dist/document/index.d.ts +11 -0
  25. package/dist/document/index.js +12 -0
  26. package/dist/document/index.js.map +1 -0
  27. package/dist/document/model.d.ts +10 -0
  28. package/dist/document/model.js +2 -0
  29. package/dist/document/model.js.map +1 -0
  30. package/dist/document/readers.d.ts +24 -0
  31. package/dist/document/readers.js +87 -0
  32. package/dist/document/readers.js.map +1 -0
  33. package/dist/document/types.d.ts +99 -0
  34. package/dist/document/types.js +93 -0
  35. package/dist/document/types.js.map +1 -0
  36. package/dist/engine/agent.d.ts +475 -0
  37. package/dist/engine/agent.js +990 -0
  38. package/dist/engine/agent.js.map +1 -0
  39. package/dist/engine/canonical.d.ts +4 -0
  40. package/dist/engine/canonical.js +55 -0
  41. package/dist/engine/canonical.js.map +1 -0
  42. package/dist/engine/context.d.ts +9 -0
  43. package/dist/engine/context.js +141 -0
  44. package/dist/engine/context.js.map +1 -0
  45. package/dist/engine/cost.d.ts +13 -0
  46. package/dist/engine/cost.js +31 -0
  47. package/dist/engine/cost.js.map +1 -0
  48. package/dist/engine/index.d.ts +16 -0
  49. package/dist/engine/index.js +17 -0
  50. package/dist/engine/index.js.map +1 -0
  51. package/dist/engine/judge.d.ts +158 -0
  52. package/dist/engine/judge.js +323 -0
  53. package/dist/engine/judge.js.map +1 -0
  54. package/dist/engine/people.d.ts +34 -0
  55. package/dist/engine/people.js +125 -0
  56. package/dist/engine/people.js.map +1 -0
  57. package/dist/engine/plan.d.ts +6 -0
  58. package/dist/engine/plan.js +7 -0
  59. package/dist/engine/plan.js.map +1 -0
  60. package/dist/engine/plans/index.d.ts +19 -0
  61. package/dist/engine/plans/index.js +104 -0
  62. package/dist/engine/plans/index.js.map +1 -0
  63. package/dist/engine/ports.d.ts +76 -0
  64. package/dist/engine/ports.js +28 -0
  65. package/dist/engine/ports.js.map +1 -0
  66. package/dist/engine/recall.d.ts +42 -0
  67. package/dist/engine/recall.js +71 -0
  68. package/dist/engine/recall.js.map +1 -0
  69. package/dist/engine/state.d.ts +96 -0
  70. package/dist/engine/state.js +162 -0
  71. package/dist/engine/state.js.map +1 -0
  72. package/dist/engine/steps.d.ts +98 -0
  73. package/dist/engine/steps.js +435 -0
  74. package/dist/engine/steps.js.map +1 -0
  75. package/dist/engine/tree.d.ts +76 -0
  76. package/dist/engine/tree.js +148 -0
  77. package/dist/engine/tree.js.map +1 -0
  78. package/dist/engine/types.d.ts +250 -0
  79. package/dist/engine/types.js +168 -0
  80. package/dist/engine/types.js.map +1 -0
  81. package/dist/engine/view.d.ts +138 -0
  82. package/dist/engine/view.js +188 -0
  83. package/dist/engine/view.js.map +1 -0
  84. package/dist/index.d.ts +15 -0
  85. package/dist/index.js +16 -0
  86. package/dist/index.js.map +1 -0
  87. package/dist/ledger/files.d.ts +39 -0
  88. package/dist/ledger/files.js +113 -0
  89. package/dist/ledger/files.js.map +1 -0
  90. package/dist/ledger/ids.d.ts +4 -0
  91. package/dist/ledger/ids.js +9 -0
  92. package/dist/ledger/ids.js.map +1 -0
  93. package/dist/ledger/index.d.ts +11 -0
  94. package/dist/ledger/index.js +12 -0
  95. package/dist/ledger/index.js.map +1 -0
  96. package/dist/ledger/ledger.d.ts +46 -0
  97. package/dist/ledger/ledger.js +105 -0
  98. package/dist/ledger/ledger.js.map +1 -0
  99. package/dist/ledger/memory.d.ts +19 -0
  100. package/dist/ledger/memory.js +19 -0
  101. package/dist/ledger/memory.js.map +1 -0
  102. package/dist/ledger/records.d.ts +1424 -0
  103. package/dist/ledger/records.js +292 -0
  104. package/dist/ledger/records.js.map +1 -0
  105. package/dist/sources/arxiv.d.ts +105 -0
  106. package/dist/sources/arxiv.js +321 -0
  107. package/dist/sources/arxiv.js.map +1 -0
  108. package/dist/sources/exa.d.ts +19 -0
  109. package/dist/sources/exa.js +59 -0
  110. package/dist/sources/exa.js.map +1 -0
  111. package/dist/sources/fetcher.d.ts +35 -0
  112. package/dist/sources/fetcher.js +67 -0
  113. package/dist/sources/fetcher.js.map +1 -0
  114. package/dist/sources/google.d.ts +16 -0
  115. package/dist/sources/google.js +64 -0
  116. package/dist/sources/google.js.map +1 -0
  117. package/dist/sources/index.d.ts +17 -0
  118. package/dist/sources/index.js +18 -0
  119. package/dist/sources/index.js.map +1 -0
  120. package/dist/sources/junk.d.ts +10 -0
  121. package/dist/sources/junk.js +16 -0
  122. package/dist/sources/junk.js.map +1 -0
  123. package/dist/sources/ladder.d.ts +47 -0
  124. package/dist/sources/ladder.js +158 -0
  125. package/dist/sources/ladder.js.map +1 -0
  126. package/dist/sources/ledger.d.ts +7 -0
  127. package/dist/sources/ledger.js +35 -0
  128. package/dist/sources/ledger.js.map +1 -0
  129. package/dist/sources/paste.d.ts +8 -0
  130. package/dist/sources/paste.js +22 -0
  131. package/dist/sources/paste.js.map +1 -0
  132. package/dist/sources/searxng.d.ts +11 -0
  133. package/dist/sources/searxng.js +38 -0
  134. package/dist/sources/searxng.js.map +1 -0
  135. package/dist/sources/sites.d.ts +53 -0
  136. package/dist/sources/sites.js +285 -0
  137. package/dist/sources/sites.js.map +1 -0
  138. package/dist/sources/ytdlp.d.ts +43 -0
  139. package/dist/sources/ytdlp.js +117 -0
  140. package/dist/sources/ytdlp.js.map +1 -0
  141. package/package.json +61 -3
@@ -0,0 +1,990 @@
1
+ /**
2
+ * The engine driven by an agent: each step as one call over a run's ledger. Claude Code (or any
3
+ * agent) plans, triages, reviews and stops in its own head; the engine does the fixed work
4
+ * (search, open, extract through the quote gate, check) and keeps the records. Every record the
5
+ * agent writes carries its id and, on a decision, a why; the person's words come through it
6
+ * marked `from: human`. The budget is a guard rail: the brief shows it, nothing cuts a query or
7
+ * stops a round by it.
8
+ */
9
+ import { isTransient } from "@wrenautomation/models";
10
+ import { gateDrafts } from "@wrenautomation/read/study";
11
+ import { gateFindings, patternOf } from "../document/index.js";
12
+ import { LedgerRecord, SOURCE_CLASSES, } from "../ledger/records.js";
13
+ import { canonical } from "./canonical.js";
14
+ import { context } from "./context.js";
15
+ import { costOf } from "./cost.js";
16
+ import { classGuess, judgeStats, pickOf, yesOf } from "./judge.js";
17
+ import { peopleView, profileKey, unfollowedOf, } from "./people.js";
18
+ import { ageOf, defaultPlanFile, PlanFile, QueryTree, rulesOf, treeOf, } from "./plan.js";
19
+ import { checkCount, classesOf, echoOf, fold, kept, personRound, queriesOf, sourcesOf, spreadSample, } from "./state.js";
20
+ import { checkSample, extractDocuments, fetchStep, openAfter, planAsk, readHits, } from "./steps.js";
21
+ import { budgetUnder, lineageOf, linkTo, runTree, treeReadsOf, treeRoom, } from "./tree.js";
22
+ export const shown = (r) => {
23
+ if (r.type !== "document")
24
+ return r;
25
+ const { text, ...rest } = r;
26
+ return { ...rest, chars: text.length };
27
+ };
28
+ const message = (err) => (err instanceof Error ? err.message : String(err));
29
+ async function stateOf(deps, run) {
30
+ const state = fold(run, await deps.ledger.trace(run));
31
+ if (!state.records.length)
32
+ throw new Error(`run ${run}: no records`);
33
+ return state;
34
+ }
35
+ /** The round's snapshot is every step's precondition. */
36
+ function planned(state) {
37
+ if (state.snapshot?.round !== state.round)
38
+ throw new Error(`run ${state.run}: round ${state.round} has no plan: birdwatch plan --run first`);
39
+ }
40
+ const tool = (deps) => ({ from: "tool", by: deps.writer });
41
+ /** One step over the run: the fold, the step, its `done`, the fold again. */
42
+ async function stepRun(deps, run, step, fn) {
43
+ const state = await stateOf(deps, run);
44
+ planned(state);
45
+ const before = new Set(state.records.map((r) => r.id));
46
+ const ctx = context(deps, run, state.round, step);
47
+ let result;
48
+ try {
49
+ result = await fn(ctx, state);
50
+ }
51
+ catch (err) {
52
+ if (isTransient(err))
53
+ throw err;
54
+ result = { ok: false, note: message(err) };
55
+ }
56
+ await ctx.write({ type: "done", step, ok: result.ok, ...(result.note ? { note: result.note } : {}) }, tool(deps));
57
+ const after = await deps.ledger.trace(run);
58
+ return {
59
+ run,
60
+ round: state.round,
61
+ step,
62
+ ok: result.ok,
63
+ ...(result.note ? { note: result.note } : {}),
64
+ wrote: after.filter((r) => !before.has(r.id)).map(shown),
65
+ };
66
+ }
67
+ /**
68
+ * Opens a round: writes its snapshot (the ask, with what is known and what the last round left
69
+ * open, the allow-list, the budget) and the plan step's `done`. No queries: the agent's come
70
+ * through `search`, never capped.
71
+ */
72
+ export async function planRound(deps, input) {
73
+ const run = input.run ?? deps.ids();
74
+ const state = fold(run, await deps.ledger.trace(run));
75
+ if (input.run && !state.records.length)
76
+ throw new Error(`run ${run}: no records`);
77
+ const round = state.round;
78
+ if (state.snapshot?.round === round)
79
+ throw new Error(state.next
80
+ ? `run ${run}: round ${round} is planned`
81
+ : `run ${run}: stopped after round ${round}; birdwatch add edge again opens the next`);
82
+ const last = state.snapshot?.ask;
83
+ const base = last ?? input.ask;
84
+ if (!base)
85
+ throw new Error("a new run needs an ask");
86
+ for (const name of input.planFile.sources)
87
+ if (!deps.sources.has(name))
88
+ throw new Error(`no source named ${name}`);
89
+ if (input.parent && state.snapshot)
90
+ throw new Error("--parent: only a new run");
91
+ const link = input.parent ? await linkTo(deps, input.parent, input.planFile) : null;
92
+ const parent = link?.parent ?? state.snapshot?.parent;
93
+ const { known, premises, open } = await planAsk(deps, state, base, round, parent);
94
+ const ask = {
95
+ text: base.text,
96
+ kind: base.kind,
97
+ known,
98
+ premises,
99
+ steer: [...new Set([...(last?.steer ?? []), ...state.steers.map((s) => s.text)])],
100
+ open,
101
+ criteria: [...(base.criteria ?? [])],
102
+ ...(base.site ? { site: base.site } : {}),
103
+ };
104
+ const yieldRows = await deps.ledger.yield();
105
+ const ctx = context(deps, run, round, "plan");
106
+ await ctx.write({
107
+ type: "snapshot",
108
+ ask,
109
+ sources: input.planFile.sources,
110
+ opens: (input.opens ?? state.snapshot?.opens ?? []).filter((n) => !input.planFile.sources.includes(n)),
111
+ budget: await budgetUnder(deps, parent, input.planFile.budget),
112
+ known: ask.known,
113
+ planFile: { id: input.planFile.id, version: input.planFile.version },
114
+ model: input.model,
115
+ yieldRows: yieldRows.length,
116
+ ...(parent ? { parent } : { treeReads: treeReadsOf(input.planFile, input.planFile.budget) }),
117
+ }, tool(deps));
118
+ const note = [
119
+ `round ${round} over ${input.planFile.sources.join(", ")}; search next`,
120
+ ...(link?.warnings ?? []),
121
+ ].join("; ");
122
+ await ctx.write({ type: "done", step: "plan", ok: true, note }, tool(deps));
123
+ const after = await deps.ledger.trace(run);
124
+ return {
125
+ run,
126
+ round,
127
+ step: "plan",
128
+ ok: true,
129
+ note,
130
+ wrote: after.filter((r) => r.round === round && r.step === "plan").map(shown),
131
+ };
132
+ }
133
+ /** Writes the agent's queries, then runs the fetch step: every searcher on every unsearched query of the round. */
134
+ export async function search(deps, run, queries, who) {
135
+ const state = await stateOf(deps, run);
136
+ planned(state);
137
+ const kind = state.snapshot.ask.kind;
138
+ const ctx = context(deps, run, state.round, "plan");
139
+ for (const q of queries) {
140
+ const terms = q.terms.replace(/\s+/g, " ").trim();
141
+ if (!terms)
142
+ throw new Error("a query needs terms");
143
+ if (!q.why.trim())
144
+ throw new Error(`query "${terms}": why?`);
145
+ await ctx.write({
146
+ type: "query",
147
+ kind,
148
+ terms,
149
+ why: q.why,
150
+ ...(q.site ? { site: q.site } : {}),
151
+ ...(q.facet ? { facet: q.facet } : {}),
152
+ }, who);
153
+ }
154
+ return stepRun(deps, run, "fetch", fetchStep);
155
+ }
156
+ /**
157
+ * What a person does after a good page: its channel's or author's own posts, or pages like it.
158
+ * Writes a query with `follow`, then runs the fetch step: its hits go through the same calls,
159
+ * reads and checks, and the tree's read cap bounds them.
160
+ */
161
+ export async function follow(deps, run, input, who) {
162
+ const state = await stateOf(deps, run);
163
+ planned(state);
164
+ const snapshot = state.snapshot;
165
+ if (!input.why.trim())
166
+ throw new Error("follow: why?");
167
+ const doc = input.document ? state.documents.find((d) => d.id === input.document) : undefined;
168
+ if (input.document && !doc)
169
+ throw new Error(`no document ${input.document}`);
170
+ const url = input.url ?? doc?.url;
171
+ if (!url)
172
+ throw new Error("follow: a URL or a document");
173
+ const ctx = context(deps, run, state.round, "plan");
174
+ const reach = [...snapshot.sources, ...snapshot.opens];
175
+ const joining = (input.with ?? []).filter((n) => !reach.includes(n));
176
+ for (const n of joining) {
177
+ const s = deps.sources.get(n);
178
+ if (!s)
179
+ throw new Error(`no source named ${n}`);
180
+ if (!s.follow)
181
+ throw new Error(`${n} follows nothing`);
182
+ }
183
+ const names = [...reach, ...joining];
184
+ const followers = names.filter((n) => deps.sources.get(n)?.follows?.(input.as, url));
185
+ if (!followers.length) {
186
+ const here = [...deps.sources.values()].filter((s) => s.follows?.(input.as, url));
187
+ throw new Error(`follow: no source in the run lists ${input.as} of ${url}${here.length ? `; --with ${here.map((s) => s.name).join(" ")}` : ""}`);
188
+ }
189
+ if (joining.length) {
190
+ const { id: _id, at: _at, from: _from, by: _by, ...rest } = snapshot;
191
+ await ctx.write({ ...rest, opens: [...snapshot.opens, ...joining] }, who);
192
+ }
193
+ await ctx.write({
194
+ type: "query",
195
+ kind: snapshot.ask.kind,
196
+ terms: `${input.as} of ${url}`,
197
+ why: input.why,
198
+ follow: { url, as: input.as, ...(doc ? { document: doc.id } : {}) },
199
+ }, who);
200
+ return stepRun(deps, run, "fetch", fetchStep);
201
+ }
202
+ /** The round's queries by facet and try, with what each wrote and what triage kept: rebuilt from the records. */
203
+ export function treeView(state, round, guide) {
204
+ const hitsOf = new Map();
205
+ const againOf = new Map();
206
+ for (const h of state.hits)
207
+ for (const q of h.queries)
208
+ hitsOf.set(q, (hitsOf.get(q) ?? 0) + 1);
209
+ for (const f of state.founds)
210
+ againOf.set(f.query, (againOf.get(f.query) ?? 0) + 1);
211
+ const facets = [];
212
+ const queryFacet = new Map();
213
+ for (const q of state.queries) {
214
+ if (q.round !== round || !q.facet)
215
+ continue;
216
+ queryFacet.set(q.id, q.facet);
217
+ let facet = facets.find((f) => f.name === q.facet);
218
+ if (!facet) {
219
+ facet = {
220
+ name: q.facet,
221
+ tries: [],
222
+ hits: 0,
223
+ kept: 0,
224
+ uncalled: 0,
225
+ enough: false,
226
+ needs: [],
227
+ fit: 0,
228
+ guessed: 0,
229
+ classes: {},
230
+ lacks: false,
231
+ };
232
+ facets.push(facet);
233
+ }
234
+ for (const n of q.needs ?? [])
235
+ if (!facet.needs.includes(n))
236
+ facet.needs.push(n);
237
+ const tier = q.tier ?? 1;
238
+ let t = facet.tries.find((x) => x.tier === tier);
239
+ if (!t) {
240
+ t = { tier, queries: [], hits: 0 };
241
+ facet.tries.push(t);
242
+ }
243
+ const hits = hitsOf.get(q.id) ?? 0;
244
+ t.queries.push({ id: q.id, terms: q.terms, hits, again: againOf.get(q.id) ?? 0 });
245
+ t.hits += hits;
246
+ facet.hits += hits;
247
+ }
248
+ // A hit counts for every facet whose query found it: a page two facets reached is kept for both.
249
+ const facetsOf = new Map();
250
+ for (const [hit, queries] of queriesOf(state)) {
251
+ const names = new Set([...queries].flatMap((q) => queryFacet.get(q) ?? []));
252
+ facetsOf.set(hit, names);
253
+ const call = state.called.get(hit);
254
+ const guess = call === "keep" ? classGuess(state, hit) : undefined;
255
+ for (const name of names) {
256
+ const facet = facets.find((f) => f.name === name);
257
+ if (call === "keep") {
258
+ facet.kept++;
259
+ if (guess)
260
+ facet.guessed++;
261
+ if (guess && facet.needs.includes(guess))
262
+ facet.fit++;
263
+ }
264
+ else if (!call)
265
+ facet.uncalled++;
266
+ }
267
+ }
268
+ const yielded = new Set(state.findings.filter((f) => kept(state, f)).map((f) => f.document));
269
+ for (const d of state.documents) {
270
+ if (!yielded.has(d.id))
271
+ continue;
272
+ const c = state.classOf.get(d.id) ?? "unclassed";
273
+ for (const name of facetsOf.get(d.hit) ?? []) {
274
+ const facet = facets.find((f) => f.name === name);
275
+ facet.classes[c] = (facet.classes[c] ?? 0) + 1;
276
+ }
277
+ }
278
+ for (const f of facets) {
279
+ f.tries.sort((a, b) => a.tier - b.tier);
280
+ f.lacks =
281
+ f.needs.length > 0 &&
282
+ Object.keys(f.classes).length > 0 &&
283
+ !f.needs.some((n) => f.classes[n]);
284
+ // A facet that needs classes is enough only with one kept hit of them, once any class is
285
+ // known; after the reads, only with a read page of them.
286
+ f.enough = f.kept >= guide.thin && (!f.needs.length || !f.guessed || f.fit > 0) && !f.lacks;
287
+ }
288
+ const served = new Set(facets.map((f) => f.name));
289
+ const dead = state.dead.map((d) => d.facet);
290
+ return {
291
+ thin: guide.thin,
292
+ facets,
293
+ uncovered: guide.facets.map((f) => f.name).filter((n) => !served.has(n) && !dead.includes(n)),
294
+ dead,
295
+ };
296
+ }
297
+ /**
298
+ * The guide a run's tree runs under: the kind's stored plan file, else its default. An ask with
299
+ * premises adds a facet that tests them, so the tree says when no query did.
300
+ */
301
+ async function guideOf(deps, ask) {
302
+ const guide = treeOf((await deps.ledger.readPlan?.(ask.kind)) ?? defaultPlanFile(ask.kind));
303
+ return ask.premises?.length ? { ...guide, facets: [...guide.facets, PREMISE_FACET] } : guide;
304
+ }
305
+ const PREMISE_FACET = {
306
+ name: "premise",
307
+ why: "test what the ask or the known list takes as given: does it hold?",
308
+ };
309
+ /**
310
+ * One tier of the round's query tree: for each facet in the tree, its next try, in one fetch.
311
+ * A facet with no query yet this round runs its first try. A facet that ran one waits for
312
+ * triage (a call on each of its hits); then, under the guide's `thin` in kept hits, it runs the
313
+ * next try; at `thin` it is done; out of tries, it is a gap. So the agent runs the same tree
314
+ * again after each triage, and the tree falls through on what the agent kept, never on what a
315
+ * searcher returned (every searcher returns a page). `all` runs every facet's next try at once.
316
+ * A page two facets reached is a `found` on the first facet's hit, and the brief ranks it above
317
+ * one facet's. Never capped: the agent wrote the tree.
318
+ */
319
+ export async function searchTree(deps, run, tree, who, options = {}) {
320
+ QueryTree.parse(tree);
321
+ const first = await stateOf(deps, run);
322
+ planned(first);
323
+ const ask = first.snapshot.ask;
324
+ const round = first.round;
325
+ const guide = await guideOf(deps, ask);
326
+ const names = new Set();
327
+ for (const f of tree.facets) {
328
+ if (names.has(f.name))
329
+ throw new Error(`facet "${f.name}" twice`);
330
+ names.add(f.name);
331
+ for (const n of f.needs ?? [])
332
+ if (!SOURCE_CLASSES.includes(n))
333
+ throw new Error(`facet "${f.name}" needs ${n}: not one of ${SOURCE_CLASSES.join(", ")}`);
334
+ }
335
+ const before = treeView(first, round, guide);
336
+ const wrote = [];
337
+ const lines = [];
338
+ const ctx = context(deps, run, round, "plan");
339
+ const ids = [];
340
+ for (const facet of tree.facets) {
341
+ if (before.dead.includes(facet.name)) {
342
+ lines.push(`${facet.name}: dead`);
343
+ continue;
344
+ }
345
+ const ran = before.facets.find((f) => f.name === facet.name);
346
+ const sites = new Set(facet.tries.flat().map((l) => l.site ?? ""));
347
+ if (sites.size === 1 && !sites.has(""))
348
+ lines.push(`${facet.name}: every try pins ${[...sites][0]}, one source class at most`);
349
+ if (ran?.lacks)
350
+ lines.push(`${facet.name}: needs ${ran.needs.join(" or ")}, read pages are ${Object.keys(ran.classes).join(", ")}`);
351
+ const tier = (ran?.tries.at(-1)?.tier ?? 0) + 1;
352
+ const next = facet.tries[tier - 1];
353
+ if (ran && !options.all) {
354
+ if (ran.enough) {
355
+ lines.push(`${facet.name}: ${ran.kept} kept, enough`);
356
+ continue;
357
+ }
358
+ if (ran.uncalled) {
359
+ lines.push(`${facet.name}: ${ran.uncalled} uncalled, triage first`);
360
+ continue;
361
+ }
362
+ }
363
+ if (!next) {
364
+ const note = `facet "${facet.name}": ${ran?.kept ?? 0} kept in ${tier - 1} ${tier === 2 ? "try" : "tries"}, none left`;
365
+ if (ran && !ran.enough && !first.gaps.some((g) => g.round === round && g.note === note)) {
366
+ const fetch = context(deps, run, round, "fetch");
367
+ const id = await fetch.write({ type: "gap", note }, { from: "tool", by: deps.writer });
368
+ const trace = await deps.ledger.trace(run);
369
+ wrote.push(shown(trace.find((r) => r.id === id)));
370
+ }
371
+ lines.push(`${facet.name}: out of tries`);
372
+ continue;
373
+ }
374
+ for (const leaf of next) {
375
+ const terms = leaf.terms.replace(/\s+/g, " ").trim();
376
+ if (!terms)
377
+ throw new Error(`facet "${facet.name}": a query needs terms`);
378
+ if (!leaf.why.trim())
379
+ throw new Error(`query "${terms}": why?`);
380
+ ids.push(await ctx.write({
381
+ type: "query",
382
+ kind: ask.kind,
383
+ terms,
384
+ why: leaf.why,
385
+ facet: facet.name,
386
+ tier,
387
+ ...(leaf.site ? { site: leaf.site } : {}),
388
+ ...(facet.needs?.length ? { needs: facet.needs } : {}),
389
+ }, who));
390
+ }
391
+ const unfit = ran && ran.kept >= before.thin && !ran.enough
392
+ ? `, none of them ${ran.needs.join(" or ")}`
393
+ : "";
394
+ lines.push(`${facet.name}: try ${tier}${ran ? ` (${ran.kept} kept${unfit})` : ""}`);
395
+ }
396
+ if (!ids.length) {
397
+ const end = await stateOf(deps, run);
398
+ return {
399
+ run,
400
+ round,
401
+ step: "fetch",
402
+ ok: false,
403
+ wrote,
404
+ note: `nothing to search; ${lines.join("; ")}`,
405
+ tree: treeView(end, round, guide),
406
+ };
407
+ }
408
+ const trace = await deps.ledger.trace(run);
409
+ for (const id of ids)
410
+ wrote.push(shown(trace.find((r) => r.id === id)));
411
+ const did = await stepRun(deps, run, "fetch", fetchStep);
412
+ wrote.push(...did.wrote);
413
+ const end = await stateOf(deps, run);
414
+ const view = treeView(end, round, guide);
415
+ const uncovered = view.uncovered.length ? `; not covered: ${view.uncovered.join(", ")}` : "";
416
+ return {
417
+ ...did,
418
+ wrote,
419
+ note: `${did.note ?? ""}; ${lines.join("; ")}${uncovered}`,
420
+ tree: view,
421
+ };
422
+ }
423
+ /** Kept hits of any round with no document yet. */
424
+ export const unread = (state) => {
425
+ const read = new Set(state.documents.map((d) => d.hit));
426
+ return state.hits.filter((h) => state.called.get(h.id) === "keep" && !read.has(h.id));
427
+ };
428
+ /**
429
+ * Keeps the hits named (and writes a hit per URL named), then runs the read step over every
430
+ * kept hit of the run that has no document. No names: the reads alone.
431
+ */
432
+ export async function open(deps, run, input, who) {
433
+ const state = await stateOf(deps, run);
434
+ planned(state);
435
+ const named = [...(input.hits ?? [])];
436
+ if ((named.length || input.urls?.length) && !input.why?.trim())
437
+ throw new Error("open: why read these?");
438
+ const ids = new Set(state.hits.map((h) => h.id));
439
+ for (const id of named)
440
+ if (!ids.has(id))
441
+ throw new Error(`no hit ${id}`);
442
+ const ctx = context(deps, run, state.round, "triage");
443
+ const byKey = new Map(state.hits.map((h) => [h.key, h.id]));
444
+ for (const url of input.urls ?? []) {
445
+ const key = canonical(url);
446
+ const have = byKey.get(key);
447
+ if (have) {
448
+ named.push(have);
449
+ continue;
450
+ }
451
+ const id = await ctx.write({ type: "hit", url, key, queries: [], sources: [] }, who);
452
+ byKey.set(key, id);
453
+ named.push(id);
454
+ }
455
+ for (const id of new Set(named))
456
+ await ctx.write({ type: "call", on: id, verdict: "keep", why: input.why }, who);
457
+ const snapshot = state.snapshot;
458
+ const joining = (input.with ?? []).filter((n) => !snapshot?.sources.includes(n) && !snapshot?.opens.includes(n));
459
+ if (joining.length && snapshot) {
460
+ for (const n of joining) {
461
+ const s = deps.sources.get(n);
462
+ if (!s)
463
+ throw new Error(`no source named ${n}`);
464
+ if (!s.open)
465
+ throw new Error(`${n} opens nothing: only an open source joins a read`);
466
+ }
467
+ const { id: _id, at: _at, from: _from, by: _by, ...rest } = snapshot;
468
+ await ctx.write({ ...rest, opens: [...snapshot.opens, ...joining] }, who);
469
+ }
470
+ return stepRun(deps, run, "read", (c, s) => readHits(c, s, unread(s)));
471
+ }
472
+ /** Documents of any round the extractor has no verdict on. */
473
+ export const unjudged = (state) => {
474
+ const judged = new Set(state.verdicts.map((v) => v.document));
475
+ return state.documents.filter((d) => !judged.has(d.id));
476
+ };
477
+ /** The extract step over the documents named, else every one of the run without a verdict. */
478
+ export async function extract(deps, run, documents) {
479
+ return stepRun(deps, run, "extract", (c, s) => {
480
+ const docs = documents
481
+ ? documents.map((id) => {
482
+ const d = s.documents.find((x) => x.id === id);
483
+ if (!d)
484
+ throw new Error(`no document ${id}`);
485
+ return d;
486
+ })
487
+ : unjudged(s);
488
+ return extractDocuments(c, s, docs);
489
+ });
490
+ }
491
+ /** Kept findings the run's patterns rest on that no check has read. */
492
+ export const uncheckedOf = (state, ids) => {
493
+ const checked = new Set(state.checks.map((c) => c.on));
494
+ const byId = new Map(state.findings.map((f) => [f.id, f]));
495
+ return ids.filter((id) => {
496
+ const f = byId.get(id);
497
+ return !!f?.proof.quote && kept(state, f) && !checked.has(id);
498
+ });
499
+ };
500
+ /** Findings the judge says credit another named source. */
501
+ export const citesOf = (state, ids) => ids.filter((id) => (yesOf(state, id, "cites") ?? 0) > 0.5);
502
+ /**
503
+ * The check over the findings named; else, with `patterns`, every unchecked finding a pattern
504
+ * rests on; else a sample spread over documents: `n`, or the budget's `checks` or a tenth of
505
+ * the kept findings, whichever is more.
506
+ */
507
+ export async function check(deps, run, input = {}) {
508
+ return stepRun(deps, run, "check", (c, s) => {
509
+ const byId = new Map(s.findings.map((f) => [f.id, f]));
510
+ const named = (ids) => ids.map((id) => {
511
+ const f = byId.get(id);
512
+ if (!f)
513
+ throw new Error(`no finding ${id}`);
514
+ return f;
515
+ });
516
+ const reach = s.findings.filter((f) => f.proof.quote && kept(s, f)).length;
517
+ const sample = input.findings
518
+ ? named(input.findings)
519
+ : input.patterns
520
+ ? named(uncheckedOf(s, [...new Set(s.patterns.flatMap((p) => p.findings))]))
521
+ : spreadSample(s, input.n ?? checkCount(reach, s.snapshot?.budget.checks ?? 3));
522
+ return checkSample(c, s, sample);
523
+ });
524
+ }
525
+ /**
526
+ * One record in the agent's or the person's name. A query, finding or pattern lands in the
527
+ * current round; a call, edge, steer or gap in the round it is about (the one just reviewed,
528
+ * between rounds). A finding goes through the quote gate: its quote must be on the document,
529
+ * word for word, or the add fails with the gate's why. A pattern names kept findings; its
530
+ * source count is counted here, never claimed. No `step`: a hand-written record has none.
531
+ */
532
+ export async function add(deps, run, a, who) {
533
+ const state = await stateOf(deps, run);
534
+ const round = a.type === "query" ||
535
+ a.type === "finding" ||
536
+ a.type === "pattern" ||
537
+ a.type === "check" ||
538
+ a.type === "draft" ||
539
+ a.type === "class" ||
540
+ a.type === "dead" ||
541
+ a.type === "person"
542
+ ? state.round
543
+ : personRound(state);
544
+ const warnings = [];
545
+ const write = async (record) => {
546
+ const id = deps.ids();
547
+ const full = LedgerRecord.parse({
548
+ ...record,
549
+ id,
550
+ run,
551
+ round,
552
+ at: deps.clock().toISOString(),
553
+ from: who.from,
554
+ by: who.by,
555
+ });
556
+ await deps.ledger.append(full);
557
+ return full;
558
+ };
559
+ const why = (text, what) => {
560
+ if (!text.trim())
561
+ throw new Error(`${what}: why?`);
562
+ return text;
563
+ };
564
+ switch (a.type) {
565
+ case "query": {
566
+ if (!state.snapshot)
567
+ throw new Error(`run ${run}: no plan yet`);
568
+ const terms = a.terms.replace(/\s+/g, " ").trim();
569
+ if (!terms)
570
+ throw new Error("a query needs terms");
571
+ return {
572
+ record: await write({
573
+ type: "query",
574
+ kind: state.snapshot.ask.kind,
575
+ terms,
576
+ why: why(a.why, `query "${terms}"`),
577
+ ...(a.site ? { site: a.site } : {}),
578
+ ...(a.facet ? { facet: a.facet } : {}),
579
+ }),
580
+ warnings,
581
+ };
582
+ }
583
+ case "finding": {
584
+ const doc = state.documents.find((d) => d.id === a.document);
585
+ if (!doc)
586
+ throw new Error(`no document ${a.document}`);
587
+ const answer = { findings: [{ text: a.text, quote: a.quote }] };
588
+ const gate = gateFindings(answer, doc);
589
+ const f = gate.findings[0];
590
+ if (!f) {
591
+ const why = gate.dropped[0]?.why ?? "empty";
592
+ throw new Error(why === "quote_short"
593
+ ? "finding dropped at the gate: quote_short, the quote needs 5 words: take the line with its label or the next line"
594
+ : `finding dropped at the gate: ${why}`);
595
+ }
596
+ return {
597
+ record: await write({
598
+ type: "finding",
599
+ ...f,
600
+ document: doc.id,
601
+ ...(doc.date ? { date: doc.date } : {}),
602
+ }),
603
+ warnings,
604
+ };
605
+ }
606
+ case "check": {
607
+ const f = state.findings.find((x) => x.id === a.on);
608
+ if (!f)
609
+ throw new Error(`no finding ${a.on}`);
610
+ const record = await write({ type: "check", on: a.on, ok: a.ok, why: why(a.why, "check") });
611
+ if (!a.ok)
612
+ await write({ type: "call", on: a.on, verdict: "drop", why: a.why });
613
+ return { record, warnings };
614
+ }
615
+ case "call": {
616
+ const hit = state.hits.find((h) => h.id === a.on);
617
+ const finding = hit ? undefined : state.findings.find((f) => f.id === a.on);
618
+ if (!hit && !finding)
619
+ throw new Error(`no hit or finding ${a.on}`);
620
+ // Last call wins: a flip is said out loud; a person's call outranks the agent's.
621
+ const last = state.calls.filter((c) => c.on === a.on).at(-1);
622
+ if (last && last.verdict !== a.verdict) {
623
+ if (last.from === "human" && who.from === "ai")
624
+ throw new Error(`${a.on}: ${last.by} called it ${last.verdict} (${last.why}); a person's call stands over the agent's`);
625
+ warnings.push(`flips ${last.by}'s ${last.verdict} from round ${last.round} (${last.why}): the last call wins`);
626
+ }
627
+ return {
628
+ record: await write({
629
+ type: "call",
630
+ on: a.on,
631
+ verdict: a.verdict,
632
+ why: why(a.why, "call"),
633
+ }),
634
+ warnings,
635
+ };
636
+ }
637
+ case "edge": {
638
+ if (a.next === "back" && !a.to)
639
+ throw new Error("edge back: to which step?");
640
+ const budget = state.snapshot?.budget;
641
+ if (a.next === "again" && budget && round >= budget.rounds)
642
+ warnings.push(`round ${round + 1} is past the budget of ${budget.rounds}: a guard rail, not a stop`);
643
+ return {
644
+ record: await write({
645
+ type: "edge",
646
+ next: a.next,
647
+ ...(a.to ? { to: a.to } : {}),
648
+ why: why(a.why, "edge"),
649
+ }),
650
+ warnings,
651
+ };
652
+ }
653
+ case "steer":
654
+ if (who.from !== "human")
655
+ throw new Error("a steer is the person's word: --human");
656
+ if (!a.text.trim())
657
+ throw new Error("steer: say what");
658
+ return { record: await write({ type: "steer", text: a.text }), warnings };
659
+ case "gap":
660
+ if (!a.note.trim())
661
+ throw new Error("gap: say what is missing");
662
+ return {
663
+ record: await write({ type: "gap", note: a.note, ...(a.on ? { on: a.on } : {}) }),
664
+ warnings,
665
+ };
666
+ case "pattern": {
667
+ const byId = new Map(state.findings.map((f) => [f.id, f]));
668
+ const on = [...new Set(a.findings)].map((id) => {
669
+ const f = byId.get(id);
670
+ if (!f)
671
+ throw new Error(`no finding ${id}`);
672
+ if (!kept(state, f))
673
+ throw new Error(`finding ${id} is dropped: a pattern rests on kept findings`);
674
+ return f;
675
+ });
676
+ const pattern = patternOf(a.text, on);
677
+ const classes = classesOf(state, pattern.findings);
678
+ if (sourcesOf(classes) < 2)
679
+ warnings.push("one source: a claim, not a pattern yet");
680
+ const cites = citesOf(state, pattern.findings);
681
+ if (echoOf(classes))
682
+ warnings.push(`vendor echo: every classed source is a vendor; vendors cite each other${cites.length ? `; the judge says ${cites.join(" ")} credit another source: read that source` : ""}`);
683
+ const unchecked = uncheckedOf(state, pattern.findings);
684
+ if (unchecked.length)
685
+ warnings.push(`unchecked: ${unchecked.join(" ")}; run check --patterns`);
686
+ return { record: await write({ type: "pattern", ...pattern }), warnings };
687
+ }
688
+ case "dead": {
689
+ const facet = a.facet.trim();
690
+ if (!facet)
691
+ throw new Error("dead: which facet?");
692
+ if (state.dead.some((d) => d.facet === facet))
693
+ throw new Error(`facet "${facet}" is dead already`);
694
+ return { record: await write({ type: "dead", facet, why: why(a.why, "dead") }), warnings };
695
+ }
696
+ case "person": {
697
+ const name = a.name.trim();
698
+ if (!name)
699
+ throw new Error("person: a name");
700
+ const profiles = [...new Set(a.profiles.map((u) => u.trim()).filter(Boolean))];
701
+ for (const u of profiles)
702
+ if (!profileKey(u))
703
+ throw new Error(`person: not a URL: ${u}`);
704
+ if (!profiles.length && !a.aka.length)
705
+ throw new Error("person: a profile URL or a name they post under");
706
+ return {
707
+ record: await write({
708
+ type: "person",
709
+ name,
710
+ profiles,
711
+ aka: [...new Set(a.aka.map((n) => n.trim()).filter(Boolean))],
712
+ why: why(a.why, "person"),
713
+ }),
714
+ warnings,
715
+ };
716
+ }
717
+ case "class": {
718
+ const target = state.documents.some((d) => d.id === a.on) || state.findings.some((f) => f.id === a.on);
719
+ if (!target)
720
+ throw new Error(`no document or finding ${a.on}`);
721
+ return {
722
+ record: await write({ type: "class", on: a.on, class: a.class, why: why(a.why, "class") }),
723
+ warnings,
724
+ };
725
+ }
726
+ case "draft": {
727
+ if (!a.for.trim())
728
+ throw new Error("draft: say what it is for");
729
+ // The quote gate numbers its claims: each kept finding is one, a pattern stands for its findings.
730
+ const keptOnes = state.findings.filter((f) => kept(state, f));
731
+ const claims = keptOnes.map((f, i) => ({
732
+ n: i + 1,
733
+ angle: 0,
734
+ claim: f.text,
735
+ quote: f.proof.quote ?? "",
736
+ source_url: f.proof.url,
737
+ }));
738
+ const nOf = new Map(keptOnes.map((f, i) => [f.id, i + 1]));
739
+ const onPattern = new Map(state.patterns.map((p) => [p.id, p.findings]));
740
+ const ns = (id) => (onPattern.get(id) ?? [id]).map((f) => nOf.get(f) ?? 0);
741
+ const spec = { key: "draft", ask: a.for };
742
+ const items = [];
743
+ const dropped = [];
744
+ // One item at a time: the gate's cap is a model's, not the agent's.
745
+ for (const item of a.items) {
746
+ const cites = [...new Set(item.cites ?? [])];
747
+ const out = gateDrafts({ items: [{ title: item.title ?? "", body: item.body ?? "", cites: cites.flatMap(ns) }] }, spec, claims);
748
+ const ok = out.kept[0];
749
+ if (ok)
750
+ items.push({ title: ok.title, body: ok.body, cites });
751
+ else
752
+ for (const d of out.dropped)
753
+ dropped.push(d);
754
+ }
755
+ for (const d of dropped)
756
+ warnings.push(`item "${d.title}" dropped at the gate: ${d.why}`);
757
+ if (!items.length)
758
+ throw new Error(`draft: every item failed the gate (${dropped.map((d) => d.why).join(", ") || "no items"}): cite kept findings or patterns, and take each number from a cited quote`);
759
+ return { record: await write({ type: "draft", for: a.for, items, dropped }), warnings };
760
+ }
761
+ }
762
+ }
763
+ /**
764
+ * The judge's guesses over a bar, written in the agent's name with its why and the judge's
765
+ * number: a class on each document with none, or a keep or drop on each uncalled hit by how
766
+ * likely it serves the ask. The agent sets the bar and owns the records; the judge decides none.
767
+ */
768
+ export async function accept(deps, run, a, who) {
769
+ const state = await stateOf(deps, run);
770
+ if (!a.why.trim())
771
+ throw new Error(`accept ${a.take}: why?`);
772
+ const bar = a.take === "class" ? a.min : a.take === "keep" ? a.over : a.under;
773
+ if (!(bar >= 0 && bar <= 1))
774
+ throw new Error(`accept ${a.take}: the bar is a probability, 0 to 1`);
775
+ const wrote = [];
776
+ let skipped = 0;
777
+ if (a.take === "class") {
778
+ for (const d of state.documents) {
779
+ if (state.classOf.has(d.id))
780
+ continue;
781
+ const g = pickOf(state, d.id, "class");
782
+ if (!g)
783
+ continue;
784
+ if (g.p < a.min || !SOURCE_CLASSES.includes(g.pick)) {
785
+ skipped++;
786
+ continue;
787
+ }
788
+ const why = `${a.why} (judge: ${g.pick} ${g.p.toFixed(2)})`;
789
+ wrote.push((await add(deps, run, { type: "class", on: d.id, class: g.pick, why }, who))
790
+ .record);
791
+ }
792
+ return { wrote, skipped };
793
+ }
794
+ for (const h of state.hits) {
795
+ if (state.called.has(h.id))
796
+ continue;
797
+ const yes = yesOf(state, h.id, "serves");
798
+ if (yes === undefined)
799
+ continue;
800
+ if (a.take === "keep" ? yes <= a.over : yes >= a.under) {
801
+ skipped++;
802
+ continue;
803
+ }
804
+ const why = `${a.why} (judge: serves ${yes.toFixed(2)})`;
805
+ wrote.push((await add(deps, run, { type: "call", on: h.id, verdict: a.take, why }, who)).record);
806
+ }
807
+ return { wrote, skipped };
808
+ }
809
+ /**
810
+ * The next version of a plan file: the current one with the changes, version plus one. Pure:
811
+ * the ledger writes it (`birdwatch learn`), at the end of a run the agent and the person agreed
812
+ * was good. The sources stay an allow-list a run narrows, never widens; `learn` is how it widens.
813
+ */
814
+ export function tunePlan(current, changes) {
815
+ const next = PlanFile.parse({
816
+ ...current,
817
+ ...(changes.sources ? { sources: changes.sources } : {}),
818
+ budget: { ...current.budget, ...(changes.budget ?? {}) },
819
+ ...(changes.age !== undefined ? { age: changes.age } : {}),
820
+ ...(changes.rules ? { rules: changes.rules } : {}),
821
+ ...(changes.tree ? { tree: changes.tree } : {}),
822
+ ...(changes.runs ? { runs: changes.runs } : {}),
823
+ version: current.version + 1,
824
+ });
825
+ if (JSON.stringify({ ...next, version: 0 }) === JSON.stringify({ ...current, version: 0 }))
826
+ throw new Error("learn: nothing changed");
827
+ return next;
828
+ }
829
+ const mean = (xs) => xs.reduce((a, x) => a + x, 0) / xs.length;
830
+ /** An uncalled hit's place in the brief: facets it serves, then the judge's serves and criteria. */
831
+ const hitRank = (h) => h.facets.length +
832
+ (h.judge?.serves ?? 0) +
833
+ (h.judge?.criteria ? mean(Object.values(h.judge.criteria)) : 0);
834
+ /** A page's place in the extract list: its criteria's mean, else worth; unjudged last. */
835
+ const pageRank = (d) => d.criteria ? mean(Object.values(d.criteria)) : (d.worth ?? -1);
836
+ /** The brief, as JSON: everything the agent needs to plan, triage, review and stop. */
837
+ export async function brief(deps, run) {
838
+ const state = await stateOf(deps, run);
839
+ const s = state.snapshot;
840
+ const round = state.round;
841
+ const urlOf = new Map(state.documents.map((d) => [d.id, d.url]));
842
+ const sources = s?.sources ?? [];
843
+ const yieldRows = (await deps.ledger.yield()).filter((r) => r.source === undefined || sources.includes(r.source));
844
+ const gapLine = (g) => `${g.step ?? g.by}: ${g.note}`;
845
+ const facetOf = new Map(state.queries.flatMap((q) => (q.facet ? [[q.id, q.facet]] : [])));
846
+ const qOf = queriesOf(state);
847
+ const facetsOf = (hit) => [...new Set([...(qOf.get(hit) ?? [])].flatMap((q) => facetOf.get(q) ?? []))].sort();
848
+ const family = await runTree(deps, run);
849
+ const criteria = s?.ask.criteria ?? [];
850
+ const criteriaOf = (id) => {
851
+ const out = Object.fromEntries(criteria.flatMap((c) => {
852
+ const yes = yesOf(state, id, c.name);
853
+ return yes === undefined ? [] : [[c.name, yes]];
854
+ }));
855
+ return Object.keys(out).length ? out : undefined;
856
+ };
857
+ const kindFile = s
858
+ ? ((await deps.ledger.readPlan?.(s.ask.kind)) ?? defaultPlanFile(s.ask.kind))
859
+ : null;
860
+ const tree = s && state.queries.some((q) => q.round === round && q.facet)
861
+ ? treeView(state, round, await guideOf(deps, s.ask))
862
+ : null;
863
+ return {
864
+ run,
865
+ round,
866
+ next: state.next,
867
+ note: state.note,
868
+ ask: s?.ask ?? null,
869
+ budget: s?.budget ?? null,
870
+ sources,
871
+ planFile: s?.planFile ?? null,
872
+ guide: kindFile ? { age: ageOf(kindFile) ?? null, rules: rulesOf(kindFile) } : null,
873
+ steer: state.steers.map((x) => x.text),
874
+ open: round > 1 ? openAfter(state, round - 1) : [],
875
+ gaps: state.gaps.filter((g) => g.round === round).map(gapLine),
876
+ findings: {
877
+ kept: state.findings
878
+ .filter((f) => kept(state, f))
879
+ .map((f) => ({
880
+ id: f.id,
881
+ round: f.round,
882
+ text: f.text,
883
+ ...(f.proof.quote ? { quote: f.proof.quote } : {}),
884
+ url: f.proof.url,
885
+ ...(f.date ? { date: f.date } : {}),
886
+ ...(f.fixes ? { fixes: f.fixes } : {}),
887
+ })),
888
+ dropped: state.findings.filter((f) => !kept(state, f)).length,
889
+ },
890
+ checks: state.checks.map(({ on, ok, why }) => ({ on, ok, why })),
891
+ patterns: state.patterns.map(({ id, round, text, findings }) => {
892
+ const classes = classesOf(state, findings);
893
+ return {
894
+ id,
895
+ round,
896
+ text,
897
+ findings,
898
+ sources: sourcesOf(classes),
899
+ classes,
900
+ echo: echoOf(classes),
901
+ unchecked: uncheckedOf(state, findings),
902
+ cites: citesOf(state, findings),
903
+ };
904
+ }),
905
+ parent: s?.parent ?? null,
906
+ children: family.children,
907
+ treeCost: family.children.length ? family.cost : null,
908
+ lineage: (await lineageOf(deps, run)).map((x) => ({
909
+ run: x.run,
910
+ ask: x.ask.text,
911
+ ...(x.parent ? { why: x.parent.why } : {}),
912
+ })),
913
+ treeRoom: await treeRoom(deps, run),
914
+ dead: state.dead.map(({ facet, why }) => ({ facet, why })),
915
+ people: peopleView(state),
916
+ unfollowed: unfollowedOf(state),
917
+ draft: (() => {
918
+ const d = state.drafts.at(-1);
919
+ return d ? { id: d.id, round: d.round, for: d.for, items: d.items } : null;
920
+ })(),
921
+ verdicts: state.verdicts.map((v) => ({
922
+ document: v.document,
923
+ url: urlOf.get(v.document) ?? v.document,
924
+ quality: v.quality,
925
+ ...(v.gap ? { gap: v.gap } : {}),
926
+ })),
927
+ tree,
928
+ hits: {
929
+ uncalled: state.hits
930
+ .filter((h) => !state.called.has(h.id))
931
+ .map(({ type: _t, run: _r, step: _s, at: _a, from: _f, by: _b, key: _k, ...view }) => {
932
+ const serves = yesOf(state, view.id, "serves");
933
+ const side = pickOf(state, view.id, "side");
934
+ const cls = pickOf(state, view.id, "class");
935
+ const met = criteriaOf(view.id);
936
+ return {
937
+ ...view,
938
+ facets: facetsOf(view.id),
939
+ ...(serves !== undefined
940
+ ? {
941
+ judge: {
942
+ serves,
943
+ side: side?.pick ?? "",
944
+ class: cls?.pick ?? "",
945
+ ...(met ? { criteria: met } : {}),
946
+ },
947
+ }
948
+ : {}),
949
+ };
950
+ })
951
+ .sort((a, b) => hitRank(b) - hitRank(a)),
952
+ toRead: unread(state).map(({ id, url }) => ({ id, url })),
953
+ read: state.documents.length,
954
+ total: state.hits.length,
955
+ },
956
+ documents: {
957
+ toExtract: unjudged(state)
958
+ .map(({ id, url }) => {
959
+ const worth = yesOf(state, id, "worth");
960
+ const met = criteriaOf(id);
961
+ return {
962
+ id,
963
+ url,
964
+ ...(worth !== undefined ? { worth } : {}),
965
+ ...(met ? { criteria: met } : {}),
966
+ };
967
+ })
968
+ .sort((a, b) => pageRank(b) - pageRank(a)),
969
+ unclassed: state.documents
970
+ .filter((d) => !state.classOf.has(d.id) &&
971
+ state.findings.some((f) => f.document === d.id && kept(state, f)))
972
+ .map(({ id, url }) => {
973
+ const g = pickOf(state, id, "class");
974
+ return { id, url, ...(g ? { guess: { class: g.pick, p: g.p } } : {}) };
975
+ }),
976
+ total: state.documents.length,
977
+ },
978
+ edges: state.edges.map(({ round, next, to, why, by }) => ({
979
+ round,
980
+ next,
981
+ ...(to ? { to } : {}),
982
+ why,
983
+ by,
984
+ })),
985
+ yield: yieldRows,
986
+ judge: judgeStats(state, deps.judge?.id ?? state.judges.at(-1)?.by ?? null),
987
+ cost: costOf(state.spends),
988
+ };
989
+ }
990
+ //# sourceMappingURL=agent.js.map