@signal4/s4 2.6.1 → 2.8.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.
- package/README.md +60 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +407 -196
- package/dist/cli.js.map +1 -1
- package/dist/cli_args.d.ts +24 -0
- package/dist/cli_args.d.ts.map +1 -0
- package/dist/cli_args.js +130 -0
- package/dist/cli_args.js.map +1 -0
- package/dist/client.d.ts +103 -3
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +195 -23
- package/dist/client.js.map +1 -1
- package/dist/format.d.ts +103 -4
- package/dist/format.d.ts.map +1 -1
- package/dist/format.js +299 -64
- package/dist/format.js.map +1 -1
- package/dist/handler_types.d.ts +10 -0
- package/dist/handler_types.d.ts.map +1 -1
- package/dist/handler_types.js +25 -0
- package/dist/handler_types.js.map +1 -1
- package/dist/handlers/ads.js +4 -4
- package/dist/handlers/ads.js.map +1 -1
- package/dist/handlers/analyze.d.ts.map +1 -1
- package/dist/handlers/analyze.js +22 -6
- package/dist/handlers/analyze.js.map +1 -1
- package/dist/handlers/bookmarks.js +2 -2
- package/dist/handlers/bookmarks.js.map +1 -1
- package/dist/handlers/claim_detail.d.ts.map +1 -1
- package/dist/handlers/claim_detail.js +18 -10
- package/dist/handlers/claim_detail.js.map +1 -1
- package/dist/handlers/claims.d.ts.map +1 -1
- package/dist/handlers/claims.js +72 -27
- package/dist/handlers/claims.js.map +1 -1
- package/dist/handlers/count.d.ts +14 -0
- package/dist/handlers/count.d.ts.map +1 -0
- package/dist/handlers/count.js +158 -0
- package/dist/handlers/count.js.map +1 -0
- package/dist/handlers/explore.d.ts.map +1 -1
- package/dist/handlers/explore.js +56 -14
- package/dist/handlers/explore.js.map +1 -1
- package/dist/handlers/registry.d.ts.map +1 -1
- package/dist/handlers/registry.js +5 -1
- package/dist/handlers/registry.js.map +1 -1
- package/dist/handlers/reports.d.ts +6 -0
- package/dist/handlers/reports.d.ts.map +1 -1
- package/dist/handlers/reports.js +140 -26
- package/dist/handlers/reports.js.map +1 -1
- package/dist/handlers/search.d.ts.map +1 -1
- package/dist/handlers/search.js +80 -23
- package/dist/handlers/search.js.map +1 -1
- package/dist/handlers/timeline.d.ts.map +1 -1
- package/dist/handlers/timeline.js +14 -8
- package/dist/handlers/timeline.js.map +1 -1
- package/dist/http.js +25 -3
- package/dist/http.js.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/provenance.d.ts +29 -2
- package/dist/provenance.d.ts.map +1 -1
- package/dist/provenance.js +63 -4
- package/dist/provenance.js.map +1 -1
- package/dist/server_core.d.ts +8 -1
- package/dist/server_core.d.ts.map +1 -1
- package/dist/server_core.js +90 -8
- package/dist/server_core.js.map +1 -1
- package/dist/speed.d.ts +40 -0
- package/dist/speed.d.ts.map +1 -0
- package/dist/speed.js +214 -0
- package/dist/speed.js.map +1 -0
- package/dist/surface.d.ts +7 -0
- package/dist/surface.d.ts.map +1 -0
- package/dist/surface.js +19 -0
- package/dist/surface.js.map +1 -0
- package/dist/types.d.ts +249 -15
- package/dist/types.d.ts.map +1 -1
- package/package.json +3 -2
package/dist/format.js
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* any port. Canonical tokens, truncation markers, and footers all live
|
|
7
7
|
* here — do not invent per-tool vocabulary in handlers.
|
|
8
8
|
*/
|
|
9
|
+
import { hint as surfaceHint, getSurface } from "./surface.js";
|
|
9
10
|
const PUBLIC_URL = "https://api.signal4.ca";
|
|
10
11
|
// ---------------------------------------------------------------------------
|
|
11
12
|
// Political lean — single canonical bucket table (spec §5), shared with the
|
|
@@ -99,6 +100,65 @@ export function confToken(confidence) {
|
|
|
99
100
|
return "conf:unverified";
|
|
100
101
|
return `conf:${Math.round(confidence * 100)}%`;
|
|
101
102
|
}
|
|
103
|
+
// ---------------------------------------------------------------------------
|
|
104
|
+
// Speaker attribution
|
|
105
|
+
// ---------------------------------------------------------------------------
|
|
106
|
+
/**
|
|
107
|
+
* How a speaker renders, gated on the episode's host/guest roster (core#559).
|
|
108
|
+
*
|
|
109
|
+
* There are four states, and a consumer must be able to tell them apart:
|
|
110
|
+
*
|
|
111
|
+
* corroborated **Glenn Beck** — the roster backs the name
|
|
112
|
+
* unconfirmed **Unconfirmed — possibly Glenn Beck** — roster exists, name absent
|
|
113
|
+
* unchecked **Unconfirmed — possibly Glenn Beck** — no roster to check against
|
|
114
|
+
* no identity **Unresolved speaker** — a distinct voice, no name
|
|
115
|
+
*
|
|
116
|
+
* The hedge word leads so that a skim reads it. Putting the name first and the
|
|
117
|
+
* caveat in a token loses the caveat: an LLM consumer summarising the result
|
|
118
|
+
* writes "Glenn Beck said X" either way.
|
|
119
|
+
*
|
|
120
|
+
* The two hedged tiers deliberately share a label and differ in their token.
|
|
121
|
+
* They are the same instruction to the reader ("do not cite this name") for
|
|
122
|
+
* two different reasons, and the reason is machine-readable where it belongs.
|
|
123
|
+
*
|
|
124
|
+
* Fails closed: a backend that emits `identity_id` without `attribution` (any
|
|
125
|
+
* build older than core#559) is treated as `unchecked`. A stale deploy must not
|
|
126
|
+
* be able to turn assertions back on.
|
|
127
|
+
*/
|
|
128
|
+
export function speakerAttribution(spk) {
|
|
129
|
+
const s = spk ?? {};
|
|
130
|
+
const id = s.identity_id;
|
|
131
|
+
const name = s.name ?? s.primary_name;
|
|
132
|
+
if (id == null) {
|
|
133
|
+
// No identity — a diarization cluster. Any name string sitting here is a
|
|
134
|
+
// placeholder ("Unknown", "SPEAKER_01"), never something to print as a
|
|
135
|
+
// speaker: nothing links it to a person we can name.
|
|
136
|
+
return {
|
|
137
|
+
label: "Unresolved speaker",
|
|
138
|
+
tokens: ["speaker:unresolved (diarization cluster)"],
|
|
139
|
+
hedged: false,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
const shown = name ?? `Speaker ${id}`;
|
|
143
|
+
const attribution = s.attribution ?? "unchecked";
|
|
144
|
+
if (attribution === "corroborated") {
|
|
145
|
+
const by = s.corroborated_by ? ` (${s.corroborated_by})` : "";
|
|
146
|
+
return { label: shown, tokens: [`speaker:${id}`, `attribution:corroborated${by}`], hedged: false };
|
|
147
|
+
}
|
|
148
|
+
return {
|
|
149
|
+
label: `Unconfirmed — possibly ${shown}`,
|
|
150
|
+
tokens: [`speaker:${id}`, `attribution:${attribution}`],
|
|
151
|
+
hedged: true,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
/** Footer shown whenever a result set contains a hedged speaker name.
|
|
155
|
+
* Without it, "Unconfirmed — possibly X" is a label with no defined meaning. */
|
|
156
|
+
export const ATTRIBUTION_NOTE = "\n\n_Speaker names: `attribution:corroborated` = the identity is on that " +
|
|
157
|
+
"episode's host/guest list — safe to cite. `unconfirmed` = the episode has a " +
|
|
158
|
+
"host/guest list and this name is not on it. `unchecked` = no list exists for " +
|
|
159
|
+
"that episode. Do not cite an Unconfirmed name as the speaker; check it with " +
|
|
160
|
+
"`get_episode` (returns hosts/guests) or `get_segment_sentences`. `confidence` " +
|
|
161
|
+
"is not calibrated and is not a substitute for this._";
|
|
102
162
|
/** `trend:surging (+340%)` / `trend:new` / `trend:steady (n/a)` (spec §5). */
|
|
103
163
|
export function trendToken(direction, changePct) {
|
|
104
164
|
if (direction === "new")
|
|
@@ -143,7 +203,7 @@ export function truncateWithPointer(text, pointer, maxlen = QUOTE_BUDGET) {
|
|
|
143
203
|
const cut = clean.slice(0, maxlen);
|
|
144
204
|
const last = cut.lastIndexOf(" ");
|
|
145
205
|
const body = last > maxlen * 0.5 ? cut.slice(0, last) : cut;
|
|
146
|
-
return `${body}…[truncated — full text: ${pointer}]`;
|
|
206
|
+
return `${body}…[truncated — full text: ${surfaceHint(pointer)}]`;
|
|
147
207
|
}
|
|
148
208
|
/** Rewrite LLM `{seg_N}` / `{{seg_N}}` citations to the spec token `seg:N`. */
|
|
149
209
|
export function rewriteSegCitations(text) {
|
|
@@ -157,10 +217,10 @@ export function rewriteSegCitations(text) {
|
|
|
157
217
|
/** `showing 20 of 249 — offset:20 for more` (spec §7.2). */
|
|
158
218
|
export function pagingNote(opts) {
|
|
159
219
|
if (opts.countCapped) {
|
|
160
|
-
return `\nshowing ${opts.shown} of ${opts.total ?? "n/a"} (count_capped — true total unknown) — offset:${(opts.offset ?? 0) + opts.shown} for more
|
|
220
|
+
return surfaceHint(`\nshowing ${opts.shown} of ${opts.total ?? "n/a"} (count_capped — true total unknown) — offset:${(opts.offset ?? 0) + opts.shown} for more`);
|
|
161
221
|
}
|
|
162
222
|
if (opts.total != null && opts.total > opts.shown + (opts.offset ?? 0)) {
|
|
163
|
-
return `\nshowing ${opts.shown} of ${opts.total} — offset:${(opts.offset ?? 0) + opts.shown} for more
|
|
223
|
+
return surfaceHint(`\nshowing ${opts.shown} of ${opts.total} — offset:${(opts.offset ?? 0) + opts.shown} for more`);
|
|
164
224
|
}
|
|
165
225
|
return "";
|
|
166
226
|
}
|
|
@@ -169,7 +229,7 @@ export function nextCallHint(...calls) {
|
|
|
169
229
|
const real = calls.filter((c) => !!c);
|
|
170
230
|
if (!real.length)
|
|
171
231
|
return "";
|
|
172
|
-
return `\n→ ${real.join(" · ")}
|
|
232
|
+
return surfaceHint(`\n→ ${real.join(" · ")}`);
|
|
173
233
|
}
|
|
174
234
|
/** Assemble body + footers in spec order. */
|
|
175
235
|
function withFooters(body, footer, paging, hint) {
|
|
@@ -225,6 +285,11 @@ function clipLine(contentType, url) {
|
|
|
225
285
|
// ---------------------------------------------------------------------------
|
|
226
286
|
// Empty state
|
|
227
287
|
// ---------------------------------------------------------------------------
|
|
288
|
+
/** An empty page past the end of a list that had results: not "no matches" (signal4#329). */
|
|
289
|
+
export function pastEndMessage(offset, noun, depthNote = "") {
|
|
290
|
+
return `No more ${noun} past offset ${offset.toLocaleString("en-US")}: the list ends before it. `
|
|
291
|
+
+ `Earlier pages (offset 0 up) hold the matches${depthNote ? `; ${depthNote}` : ""}.`;
|
|
292
|
+
}
|
|
228
293
|
export function emptyStateSuggestions(params) {
|
|
229
294
|
const suggestions = [];
|
|
230
295
|
if (params.fullHistory) {
|
|
@@ -233,7 +298,11 @@ export function emptyStateSuggestions(params) {
|
|
|
233
298
|
suggestions.push("Verify the speaker name with find_speakers");
|
|
234
299
|
}
|
|
235
300
|
else {
|
|
236
|
-
|
|
301
|
+
// A caller who named their own dates does not want to hear about `days` —
|
|
302
|
+
// suggesting it implies we ignored the dates they passed (#554).
|
|
303
|
+
if (params.explicitRange)
|
|
304
|
+
suggestions.push("Try an earlier start_date, or full_history=true with a topic + speaker filter for the whole archive");
|
|
305
|
+
else if (params.days && params.days < 30)
|
|
237
306
|
suggestions.push("Try a wider window: days=30");
|
|
238
307
|
else if (params.days && params.days < 90)
|
|
239
308
|
suggestions.push("Try a wider window: days=90");
|
|
@@ -250,8 +319,13 @@ export function emptyStateSuggestions(params) {
|
|
|
250
319
|
let msg = `No matching content for "${query}"`;
|
|
251
320
|
if (params.channel_name)
|
|
252
321
|
msg += ` on channel "${params.channel_name}"`;
|
|
322
|
+
// Say the window we actually searched. "in the last 30 days" printed under a
|
|
323
|
+
// query that passed start_date=2025-01-01 is a lie about the search, and the
|
|
324
|
+
// one that made a client re-run correct searches (#554).
|
|
253
325
|
if (params.fullHistory)
|
|
254
326
|
msg += ` in the full corpus history`;
|
|
327
|
+
else if (params.explicitRange && params.startDate)
|
|
328
|
+
msg += ` between ${params.startDate} and ${params.endDate ?? "today"}`;
|
|
255
329
|
else if (params.days)
|
|
256
330
|
msg += ` in the last ${params.days} days`;
|
|
257
331
|
msg += ".";
|
|
@@ -262,49 +336,86 @@ export function emptyStateSuggestions(params) {
|
|
|
262
336
|
// ---------------------------------------------------------------------------
|
|
263
337
|
// Quotes
|
|
264
338
|
// ---------------------------------------------------------------------------
|
|
339
|
+
/** Quote excerpt budget in the compact search view. */
|
|
340
|
+
export const SEARCH_QUOTE_BUDGET = 300;
|
|
341
|
+
/** Real total if the API gave one; null/undefined means "unknown" and is never printed as a count. */
|
|
342
|
+
export function realTotal(data) {
|
|
343
|
+
return typeof data.total_results === "number" ? data.total_results : null;
|
|
344
|
+
}
|
|
345
|
+
/** `Showing 10 of 249 · sim ≥0.35 · more: --offset 10` — honest about unknown totals. */
|
|
346
|
+
export function resultsHeader(opts) {
|
|
347
|
+
const more = opts.hasMore ?? (opts.total != null && opts.total > opts.offset + opts.consumed);
|
|
348
|
+
const range = opts.offset > 0 ? `${opts.offset + 1}–${opts.offset + opts.consumed}` : `${opts.shown}`;
|
|
349
|
+
const parts = [`Showing ${range}${opts.total != null ? ` of ${opts.total.toLocaleString("en-US")}` : ""}${opts.noun ? ` ${opts.noun}` : ""}`];
|
|
350
|
+
if (opts.minSimilarity != null)
|
|
351
|
+
parts.push(`sim ≥${opts.minSimilarity}`);
|
|
352
|
+
if (more)
|
|
353
|
+
parts.push(surfaceHint(`more: offset:${opts.offset + opts.consumed}`));
|
|
354
|
+
else if (opts.depthReached)
|
|
355
|
+
parts.push(depthNote(opts.rankedDepth));
|
|
356
|
+
return parts.join(" · ");
|
|
357
|
+
}
|
|
358
|
+
/** A ranked search served its last page: more matches exist past it, but no deeper page. */
|
|
359
|
+
export function depthNote(rankedDepth) {
|
|
360
|
+
const at = rankedDepth ? ` at rank ${rankedDepth.toLocaleString("en-US")}` : "";
|
|
361
|
+
return `ranked results end${at}; mode:keyword or count_mentions group_by:episode list every match`;
|
|
362
|
+
}
|
|
363
|
+
/** One paging line for list tools: "showing 21–40 of 312 · next: offset:40".
|
|
364
|
+
* `param` names the offset parameter (observations_offset on get_claim). Empty when the
|
|
365
|
+
* whole list fits on the first page. (signal4#329) */
|
|
366
|
+
export function pageLine(opts) {
|
|
367
|
+
const param = opts.param ?? "offset";
|
|
368
|
+
if (!opts.hasMore && opts.offset === 0)
|
|
369
|
+
return "";
|
|
370
|
+
const range = opts.shown ? `${opts.offset + 1}–${opts.offset + opts.shown}` : `none past ${opts.offset}`;
|
|
371
|
+
const of = opts.total != null ? ` of ${opts.total.toLocaleString("en-US")}` : "";
|
|
372
|
+
const noun = opts.noun ? ` ${opts.noun}` : "";
|
|
373
|
+
const tail = opts.hasMore ? ` · next: ${param}:${opts.offset + opts.shown}`
|
|
374
|
+
: opts.depthNote ? ` · ${opts.depthNote}` : " · last page";
|
|
375
|
+
return surfaceHint(`\nshowing ${range}${of}${noun}${tail}`);
|
|
376
|
+
}
|
|
265
377
|
export function formatQuotes(data, footer = "", offset = 0) {
|
|
266
378
|
const quotes = dedupQuotes(data.quotes ?? []);
|
|
267
379
|
if (!quotes.length)
|
|
268
|
-
return "No quotes found matching your search criteria." + footer;
|
|
269
|
-
const
|
|
270
|
-
|
|
271
|
-
|
|
380
|
+
return (offset > 0 ? pastEndMessage(offset, "quotes") : "No quotes found matching your search criteria.") + footer;
|
|
381
|
+
const lines = [resultsHeader({
|
|
382
|
+
shown: quotes.length, total: realTotal(data), offset, consumed: (data.quotes ?? []).length,
|
|
383
|
+
hasMore: data.has_more, minSimilarity: data.min_similarity,
|
|
384
|
+
depthReached: data.depth_reached, rankedDepth: data.ranked_depth,
|
|
385
|
+
}) + "\n"];
|
|
386
|
+
// Set when any quote renders a hedged name, so the footer explaining what
|
|
387
|
+
// "Unconfirmed — possibly X" means appears only where it is needed.
|
|
388
|
+
let anyHedged = false;
|
|
272
389
|
for (let i = 0; i < quotes.length; i++) {
|
|
273
390
|
const q = quotes[i];
|
|
274
391
|
const spk = q.speaker ?? {};
|
|
275
|
-
const
|
|
276
|
-
|
|
277
|
-
|
|
392
|
+
const attr = speakerAttribution(spk);
|
|
393
|
+
if (attr.hedged)
|
|
394
|
+
anyHedged = true;
|
|
278
395
|
const sim = q.similarity;
|
|
279
396
|
const date = fmtDate(q.episode_date ?? q.publish_date);
|
|
280
397
|
const segId = q.segment_id;
|
|
281
398
|
const tStart = q.timestamp_start ?? q.start_time;
|
|
282
|
-
const
|
|
283
|
-
const tokens = [spkToken, confToken(spk.confidence)];
|
|
399
|
+
const tokens = [...attr.tokens, confToken(spk.confidence)];
|
|
284
400
|
if (spk.appearance_type)
|
|
285
401
|
tokens.push(`appearance:${spk.appearance_type}`);
|
|
286
402
|
if (sim != null)
|
|
287
403
|
tokens.push(`sim:${sim.toFixed(2)}`);
|
|
288
404
|
tokens.push(`date:${date}`);
|
|
289
405
|
if (tStart != null)
|
|
290
|
-
tokens.push(
|
|
406
|
+
tokens.push(`@${fmtTime(tStart)}`);
|
|
291
407
|
const lt = langToken(q.language);
|
|
292
408
|
if (lt)
|
|
293
409
|
tokens.push(lt);
|
|
294
|
-
if (q.channel_id != null)
|
|
295
|
-
tokens.push(`channel_id:${q.channel_id}`);
|
|
296
410
|
tokens.push(`channel:${q.channel_name ?? "n/a"}`);
|
|
297
|
-
lines.push(`${i + 1}. **${
|
|
411
|
+
lines.push(`${i + 1}. **${attr.label}** — ${tokens.join(" ")}`);
|
|
298
412
|
lines.push(` seg:${segId ?? "n/a"} episode:${q.content_id ?? "n/a"}`);
|
|
299
|
-
const clip = clipUrl(q.content_id, tStart ?? undefined, tEnd ?? undefined, q.clip_url);
|
|
300
|
-
lines.push(` ${clipLine(q.content_type, clip)}`);
|
|
301
413
|
const pointer = segId != null ? `get_segment seg:${segId}` : undefined;
|
|
302
|
-
lines.push(` > ${truncateWithPointer(q.text, pointer)}\n`);
|
|
414
|
+
lines.push(` > ${truncateWithPointer(q.text, pointer, SEARCH_QUOTE_BUDGET)}\n`);
|
|
303
415
|
}
|
|
304
416
|
const first = quotes[0];
|
|
305
417
|
const hint = nextCallHint(first?.segment_id != null ? `get_segment seg:${first.segment_id}` : undefined, first?.speaker?.identity_id != null ? `get_speaker speaker_id:${first.speaker.identity_id}` : undefined);
|
|
306
|
-
|
|
307
|
-
return withFooters(lines.join("\n"), footer, paging, hint);
|
|
418
|
+
return withFooters(lines.join("\n"), footer, "", hint) + (anyHedged ? ATTRIBUTION_NOTE : "");
|
|
308
419
|
}
|
|
309
420
|
// ---------------------------------------------------------------------------
|
|
310
421
|
// Claims
|
|
@@ -357,15 +468,42 @@ export function speakerResolutionNote(data) {
|
|
|
357
468
|
return "";
|
|
358
469
|
return "Note: speaker name did not resolve to a known speaker — 0 results may mean an unrecognized/variant spelling (ASR may render names phonetically), not that the speaker never said this. Try find_speakers to locate the canonical name.\n\n";
|
|
359
470
|
}
|
|
471
|
+
/** "12,345" or "100,000+" for a capped count. */
|
|
472
|
+
function countLabel(n, capped) {
|
|
473
|
+
return `${n.toLocaleString("en-US")}${capped ? "+" : ""}`;
|
|
474
|
+
}
|
|
475
|
+
/** What an entity name matched: the label, the other names folded in, how it was found. */
|
|
476
|
+
export function entityMatchLine(m) {
|
|
477
|
+
const parts = [`entity:${m.entity_id} "${m.name}"`];
|
|
478
|
+
if (m.how === "similar")
|
|
479
|
+
parts.push(`closest name to "${m.query}"`);
|
|
480
|
+
else if (m.how === "alias")
|
|
481
|
+
parts.push(`matched "${m.query}" as an alias`);
|
|
482
|
+
if (m.entity_ids.length > 1) {
|
|
483
|
+
const also = m.other_names.length ? `: ${m.other_names.slice(0, 3).map((n) => `"${n}"`).join(", ")}` : "";
|
|
484
|
+
parts.push(`${m.entity_ids.length} entity rows${also}`);
|
|
485
|
+
}
|
|
486
|
+
parts.push(`linked claims:${countLabel(m.linked_claims, m.linked_claims_capped)}`);
|
|
487
|
+
return parts.join(" · ");
|
|
488
|
+
}
|
|
489
|
+
/** Notice for an entity filter: what it matched, or that it matched nothing. */
|
|
490
|
+
export function entityResolutionNote(data) {
|
|
491
|
+
if (data.entity_resolved === false) {
|
|
492
|
+
return "Note: the entity name matched no known entity — 0 results means an unknown name, not that nothing was claimed. Try find_entities for the canonical name.\n\n";
|
|
493
|
+
}
|
|
494
|
+
return data.entity_match ? `entity filter: ${entityMatchLine(data.entity_match)}\n` : "";
|
|
495
|
+
}
|
|
360
496
|
export function formatClaims(data, footer = "", offset = 0) {
|
|
361
497
|
const claims = data.claims ?? [];
|
|
362
|
-
const note = speakerResolutionNote(data);
|
|
498
|
+
const note = speakerResolutionNote(data) + entityResolutionNote(data);
|
|
363
499
|
if (!claims.length)
|
|
364
|
-
return note + "No claims found matching your search criteria." + footer;
|
|
365
|
-
const total = data
|
|
366
|
-
const
|
|
367
|
-
|
|
368
|
-
|
|
500
|
+
return note + (offset > 0 ? pastEndMessage(offset, "claims") : "No claims found matching your search criteria.") + footer;
|
|
501
|
+
const total = realTotal(data);
|
|
502
|
+
const lines = [resultsHeader({
|
|
503
|
+
shown: claims.length, total, offset, consumed: claims.length,
|
|
504
|
+
hasMore: data.has_more, minSimilarity: data.min_similarity, noun: "claims",
|
|
505
|
+
depthReached: data.depth_reached, rankedDepth: data.ranked_depth,
|
|
506
|
+
}) + "\n"];
|
|
369
507
|
for (let i = 0; i < claims.length; i++) {
|
|
370
508
|
const c = claims[i];
|
|
371
509
|
lines.push(claimLine(c, i + 1));
|
|
@@ -384,19 +522,17 @@ export function formatClaims(data, footer = "", offset = 0) {
|
|
|
384
522
|
const obs = c.sample_observations ?? [];
|
|
385
523
|
if (obs.length) {
|
|
386
524
|
const o = obs[0];
|
|
387
|
-
const clip = clipUrl(o.content_id, o.start_time ?? undefined, o.end_time ?? undefined, o.clip_url);
|
|
388
525
|
const label = [o.speaker_name, o.channel_name, fmtDate(o.publish_date)].filter(Boolean).join(", ");
|
|
389
526
|
const pointer = o.segment_id != null ? `get_segment seg:${o.segment_id}` : undefined;
|
|
390
527
|
lines.push(` sample: "${truncateWithPointer(o.chunk_text, pointer)}" — ${label}`);
|
|
391
|
-
lines.push(` seg:${o.segment_id ?? "n/a"} episode:${o.content_id ?? "n/a"}
|
|
528
|
+
lines.push(` seg:${o.segment_id ?? "n/a"} episode:${o.content_id ?? "n/a"}`);
|
|
392
529
|
}
|
|
393
530
|
lines.push("");
|
|
394
531
|
}
|
|
395
532
|
if (anyNonEnglish(claims))
|
|
396
533
|
lines.push(NON_ENGLISH_NOTE, "");
|
|
397
|
-
const paging = pagingNote({ shown: claims.length, total, offset });
|
|
398
534
|
const hint = nextCallHint(`get_claim claim:${claims[0].claim_id} include_observations:true`);
|
|
399
|
-
return withFooters(note + lines.join("\n"), footer,
|
|
535
|
+
return withFooters(note + lines.join("\n"), footer, "", hint);
|
|
400
536
|
}
|
|
401
537
|
// ---------------------------------------------------------------------------
|
|
402
538
|
// Claims analysis (/api/v1/analysis/claims/*)
|
|
@@ -620,40 +756,51 @@ export function formatEntitySearch(data) {
|
|
|
620
756
|
const tokens = [`entity:${e.id}`];
|
|
621
757
|
if (e.entity_type)
|
|
622
758
|
tokens.push(`type:${e.entity_type}`);
|
|
623
|
-
tokens.push(`claims:${e.claim_count
|
|
759
|
+
tokens.push(`claims:${countLabel(e.claim_count, e.claim_count_capped)}`);
|
|
624
760
|
if (e.first_seen_date)
|
|
625
761
|
tokens.push(`first_seen:${fmtDate(e.first_seen_date)}`);
|
|
626
762
|
if (e.last_seen_date)
|
|
627
763
|
tokens.push(`last_seen:${fmtDate(e.last_seen_date)}`);
|
|
628
764
|
lines.push(`- **${e.name}** — ${tokens.join(" ")}`);
|
|
765
|
+
if (e.matched_ids && e.matched_ids.length > 1) {
|
|
766
|
+
const also = e.also_named?.length ? ` (also named ${e.also_named.slice(0, 3).map((n) => `"${n}"`).join(", ")})` : "";
|
|
767
|
+
lines.push(` "${data.query}" covers ${e.matched_ids.length} entity rows${also}; entity_claims uses them all`);
|
|
768
|
+
}
|
|
629
769
|
if (e.aliases?.length)
|
|
630
770
|
lines.push(` aliases:${e.aliases.slice(0, 5).join(",")}`);
|
|
631
771
|
}
|
|
632
|
-
|
|
633
|
-
|
|
772
|
+
lines.push("", "_claims = claims linked to the entity, single mentions included._");
|
|
773
|
+
const paging = pageLine({ offset: data.offset ?? 0, shown: data.entities.length, hasMore: !!data.has_more });
|
|
774
|
+
const hint = nextCallHint(`entity_claims entity:"${data.query}"`);
|
|
775
|
+
return lines.join("\n") + paging + hint;
|
|
634
776
|
}
|
|
635
777
|
export function formatEntityClaims(data, footer = "", offset = 0) {
|
|
636
778
|
if (!data.entity_id) {
|
|
637
|
-
|
|
779
|
+
if (data.timed_out) {
|
|
780
|
+
return `Timed out looking up "${data.entity}" — this is not "no claims". Retry (a repeat call is usually fast), or use search_claims with a topic and entity:"${data.entity}".` + footer;
|
|
781
|
+
}
|
|
782
|
+
return `No entity found matching "${data.entity}". Try find_entities for the canonical name, or search_claims with a topic.` + footer;
|
|
638
783
|
}
|
|
639
784
|
const lines = [`# Claims about ${data.entity}`];
|
|
640
785
|
const tokens = [`entity:${data.entity_id}`];
|
|
641
786
|
if (data.entity_type)
|
|
642
787
|
tokens.push(`type:${data.entity_type}`);
|
|
643
|
-
if (data.claim_count_rollup != null)
|
|
644
|
-
tokens.push(`claims:${data.claim_count_rollup}`);
|
|
645
|
-
if (data.channel_count != null)
|
|
646
|
-
tokens.push(`channels:${data.channel_count}`);
|
|
647
788
|
lines.push(tokens.join(" "));
|
|
789
|
+
if (data.match)
|
|
790
|
+
lines.push(`matched: ${entityMatchLine(data.match)}`);
|
|
648
791
|
if (data.aliases?.length)
|
|
649
792
|
lines.push(`aliases:${data.aliases.slice(0, 6).join(",")}`);
|
|
793
|
+
if (data.timed_out) {
|
|
794
|
+
lines.push("", `Timed out before the claims were ranked: ${data.entity} has too many linked claims to rank in the time limit with these filters. This is not "no claims". Retry, or narrow with collection or stance, or use search_claims with a topic and entity:"${data.entity}".`);
|
|
795
|
+
return lines.join("\n") + footer;
|
|
796
|
+
}
|
|
650
797
|
if (data.stance_distribution?.length) {
|
|
651
798
|
const total = data.stance_distribution.reduce((s, x) => s + x.count, 0) || 1;
|
|
652
799
|
const parts = data.stance_distribution
|
|
653
800
|
.map((s) => `${s.stance}:${s.count} (${Math.round((s.count / total) * 100)}%)`);
|
|
654
801
|
lines.push(`\nstance: ${parts.join(" ")}`);
|
|
655
802
|
}
|
|
656
|
-
lines.push(`\n## Top claims (${data.claims.length} of ${data.total_claims})`);
|
|
803
|
+
lines.push(`\n## Top claims (${data.claims.length} of ${data.total_claims.toLocaleString("en-US")} observed at least twice)`);
|
|
657
804
|
data.claims.forEach((c, i) => lines.push(claimLine(c, i + 1)));
|
|
658
805
|
if (anyNonEnglish(data.claims))
|
|
659
806
|
lines.push("", NON_ENGLISH_NOTE);
|
|
@@ -686,23 +833,27 @@ export function formatClaimGraph(data) {
|
|
|
686
833
|
const hint = nextCallHint(`get_claim claim:${data.claim.claim_id} include_observations:true`, next ? `claim_graph claim_id:${next.claim_id}` : undefined);
|
|
687
834
|
return lines.join("\n") + hint;
|
|
688
835
|
}
|
|
689
|
-
export function formatTopClaims(data, footer = "") {
|
|
836
|
+
export function formatTopClaims(data, footer = "", offset = 0) {
|
|
837
|
+
const start = data.offset ?? offset;
|
|
690
838
|
if (!data.claims?.length) {
|
|
839
|
+
if (start > 0)
|
|
840
|
+
return `No ${data.metric} claims past rank ${start} (the leaderboard is ${data.ranked_depth ?? 100} deep).` + footer;
|
|
691
841
|
return `No ${data.metric} claims found in the window. Try a wider days range or lower min_observations.` + footer;
|
|
692
842
|
}
|
|
693
843
|
const contested = data.metric === "contested";
|
|
694
844
|
const label = contested ? "Most contested claims" : "Most widely-spread claims";
|
|
695
845
|
const lines = [`# ${label}`, ""];
|
|
696
846
|
data.claims.forEach((c, i) => {
|
|
697
|
-
lines.push(claimLine(c, i + 1, { assertDeny: contested }));
|
|
847
|
+
lines.push(claimLine(c, start + i + 1, { assertDeny: contested }));
|
|
698
848
|
const speakers = (c.top_speakers ?? []).slice(0, 3);
|
|
699
849
|
if (speakers.length)
|
|
700
850
|
lines.push(` speakers: ${claimSpeakerParts(speakers, 3)}`);
|
|
701
851
|
});
|
|
702
852
|
if (anyNonEnglish(data.claims))
|
|
703
853
|
lines.push("", NON_ENGLISH_NOTE);
|
|
854
|
+
const paging = pageLine({ offset: start, shown: data.claims.length, hasMore: !!data.has_more });
|
|
704
855
|
const hint = nextCallHint(`get_claim claim:${data.claims[0].claim_id} include_observations:true`);
|
|
705
|
-
return withFooters(lines.join("\n"), footer,
|
|
856
|
+
return withFooters(lines.join("\n"), footer, paging, hint);
|
|
706
857
|
}
|
|
707
858
|
// ---------------------------------------------------------------------------
|
|
708
859
|
// Speaker
|
|
@@ -816,21 +967,28 @@ export function formatChannel(data) {
|
|
|
816
967
|
export function formatSegment(data) {
|
|
817
968
|
const segId = data.segment_id;
|
|
818
969
|
const spk = data.speaker ?? {};
|
|
819
|
-
|
|
820
|
-
|
|
970
|
+
// get_segment returns the primary speaker flat (speaker_name /
|
|
971
|
+
// canonical_speaker_id / attribution) and the per-speaker list separately;
|
|
972
|
+
// other producers of SegmentDetail nest it. Accept both, preferring the flat
|
|
973
|
+
// fields, which is the precedence formatSegment already had for the name.
|
|
974
|
+
const primary = {
|
|
975
|
+
...spk,
|
|
976
|
+
name: data.speaker_name ?? spk.name,
|
|
977
|
+
identity_id: spk.identity_id ?? data.canonical_speaker_id,
|
|
978
|
+
attribution: spk.attribution ?? data.attribution,
|
|
979
|
+
corroborated_by: spk.corroborated_by ?? data.corroborated_by,
|
|
980
|
+
};
|
|
981
|
+
const attr = speakerAttribution(primary);
|
|
982
|
+
const resolved = primary.identity_id != null;
|
|
821
983
|
const clip = clipUrl(data.content_id, data.start_time ?? undefined, data.end_time ?? undefined, data.clip_url);
|
|
822
|
-
const speakerTokens = [];
|
|
823
|
-
if (resolved)
|
|
824
|
-
speakerTokens.push(`speaker:${spk.identity_id}`);
|
|
825
|
-
else if (!spkName)
|
|
826
|
-
speakerTokens.push("speaker:unresolved (diarization cluster)");
|
|
984
|
+
const speakerTokens = [...attr.tokens];
|
|
827
985
|
if (spk.confidence !== undefined || resolved)
|
|
828
986
|
speakerTokens.push(confToken(spk.confidence));
|
|
829
987
|
if (spk.appearance_type)
|
|
830
988
|
speakerTokens.push(`appearance:${spk.appearance_type}`);
|
|
831
989
|
const lines = [
|
|
832
990
|
`**Segment ${segId ?? "n/a"}**`,
|
|
833
|
-
`- **Speaker:** ${
|
|
991
|
+
`- **Speaker:** ${attr.label}${speakerTokens.length ? ` — ${speakerTokens.join(" ")}` : ""}`,
|
|
834
992
|
`- **Channel:** ${data.channel_name ?? "n/a"}${data.channel_id != null ? ` — channel_id:${data.channel_id}` : ""}`,
|
|
835
993
|
`- **Episode:** ${data.episode_title ?? "n/a"} — episode:${data.content_id ?? "n/a"}`,
|
|
836
994
|
`- **Date:** date:${fmtDate(data.episode_date)}`,
|
|
@@ -840,7 +998,7 @@ export function formatSegment(data) {
|
|
|
840
998
|
lines.push(`- ${clipLine(data.content_type, clip)}`);
|
|
841
999
|
lines.push(`\n> ${data.text ?? ""}`);
|
|
842
1000
|
const hint = nextCallHint(segId != null ? `get_thread segment_id:${segId}` : undefined, data.content_id ? `get_episode content_id:${data.content_id}` : undefined);
|
|
843
|
-
return lines.join("\n") + hint;
|
|
1001
|
+
return lines.join("\n") + hint + (attr.hedged ? ATTRIBUTION_NOTE : "");
|
|
844
1002
|
}
|
|
845
1003
|
// ---------------------------------------------------------------------------
|
|
846
1004
|
// Analysis
|
|
@@ -1363,7 +1521,7 @@ function observationLine(o) {
|
|
|
1363
1521
|
` seg:${o.segment_id ?? "n/a"} episode:${o.content_id ?? "n/a"} ${clipLine(o.content_type, clip)}`,
|
|
1364
1522
|
];
|
|
1365
1523
|
}
|
|
1366
|
-
export function formatClaimDetail(detail, observations, related) {
|
|
1524
|
+
export function formatClaimDetail(detail, observations, related, obsOffset = 0) {
|
|
1367
1525
|
const c = detail.claim;
|
|
1368
1526
|
const lines = [];
|
|
1369
1527
|
lines.push(`# Claim claim:${c.claim_id}`);
|
|
@@ -1401,18 +1559,28 @@ export function formatClaimDetail(detail, observations, related) {
|
|
|
1401
1559
|
const obs = observations?.observations ?? c.sample_observations ?? [];
|
|
1402
1560
|
let firstObsSeg;
|
|
1403
1561
|
if (obs.length) {
|
|
1404
|
-
const label = observations ? `**Observations** (${observations.total} total)` : "**Sample Observations**";
|
|
1562
|
+
const label = observations ? `**Observations** (${observations.total.toLocaleString("en-US")} total)` : "**Sample Observations**";
|
|
1405
1563
|
lines.push(label);
|
|
1406
|
-
for (const o of obs.slice(0, 20)) {
|
|
1564
|
+
for (const o of observations ? obs : obs.slice(0, 20)) {
|
|
1407
1565
|
if (firstObsSeg == null && o.segment_id != null)
|
|
1408
1566
|
firstObsSeg = o.segment_id;
|
|
1409
1567
|
lines.push(...observationLine(o));
|
|
1410
1568
|
}
|
|
1411
|
-
|
|
1412
|
-
|
|
1569
|
+
// The hint used to say "page:N", a parameter get_claim never had (signal4#329).
|
|
1570
|
+
if (observations) {
|
|
1571
|
+
const page = pageLine({
|
|
1572
|
+
offset: obsOffset, shown: obs.length, total: observations.total,
|
|
1573
|
+
hasMore: obsOffset + obs.length < observations.total,
|
|
1574
|
+
param: "observations_offset", noun: "observations",
|
|
1575
|
+
}).trim();
|
|
1576
|
+
if (page)
|
|
1577
|
+
lines.push(page);
|
|
1413
1578
|
}
|
|
1414
1579
|
lines.push("");
|
|
1415
1580
|
}
|
|
1581
|
+
else if (observations && obsOffset > 0) {
|
|
1582
|
+
lines.push(`**Observations**: none past ${obsOffset.toLocaleString("en-US")} (${observations.total.toLocaleString("en-US")} total)`, "");
|
|
1583
|
+
}
|
|
1416
1584
|
if (related?.claims?.length) {
|
|
1417
1585
|
lines.push("**Related Claims:**");
|
|
1418
1586
|
for (const r of related.claims) {
|
|
@@ -1420,26 +1588,93 @@ export function formatClaimDetail(detail, observations, related) {
|
|
|
1420
1588
|
}
|
|
1421
1589
|
lines.push("");
|
|
1422
1590
|
}
|
|
1591
|
+
else if (related?.method === "timed_out" || related?.method === "unavailable") {
|
|
1592
|
+
lines.push("_Related claims: lookup timed out; the claim above is unaffected. Retry, or use search_claims with its summary as the topic._", "");
|
|
1593
|
+
}
|
|
1423
1594
|
const hint = nextCallHint(firstObsSeg != null ? `get_segment seg:${firstObsSeg}` : undefined, `claim_graph claim_id:${c.claim_id}`);
|
|
1424
1595
|
return lines.join("\n") + hint;
|
|
1425
1596
|
}
|
|
1426
1597
|
// ---------------------------------------------------------------------------
|
|
1427
1598
|
// Timeline
|
|
1428
1599
|
// ---------------------------------------------------------------------------
|
|
1429
|
-
|
|
1600
|
+
export const TIMELINE_MAX_ROWS = 60;
|
|
1601
|
+
function isoWeekStart(d) {
|
|
1602
|
+
const t = new Date(d.slice(0, 10) + "T00:00:00Z");
|
|
1603
|
+
const dow = (t.getUTCDay() + 6) % 7; // Monday = 0
|
|
1604
|
+
t.setUTCDate(t.getUTCDate() - dow);
|
|
1605
|
+
return t.toISOString().slice(0, 10);
|
|
1606
|
+
}
|
|
1607
|
+
/** Aggregate day buckets to week, then month, until at most `max` rows remain. */
|
|
1608
|
+
export function aggregateBuckets(buckets, granularity, max = TIMELINE_MAX_ROWS) {
|
|
1609
|
+
let rows = buckets.map((b) => ({ start: b.start.slice(0, 10), count: b.count }));
|
|
1610
|
+
let g = granularity;
|
|
1611
|
+
const fold = (key) => {
|
|
1612
|
+
const m = new Map();
|
|
1613
|
+
for (const r of rows) {
|
|
1614
|
+
const k = key(r.start);
|
|
1615
|
+
m.set(k, (m.get(k) ?? 0) + r.count);
|
|
1616
|
+
}
|
|
1617
|
+
rows = [...m.entries()].map(([start, count]) => ({ start, count })).sort((a, b) => a.start.localeCompare(b.start));
|
|
1618
|
+
};
|
|
1619
|
+
if (rows.length > max && g === "day") {
|
|
1620
|
+
fold(isoWeekStart);
|
|
1621
|
+
g = "week";
|
|
1622
|
+
}
|
|
1623
|
+
// Only fine buckets fold. month/quarter/year come bucketed by the server on calendar
|
|
1624
|
+
// boundaries; re-keying them here would relabel a quarter as a month.
|
|
1625
|
+
if (rows.length > max && g === "week") {
|
|
1626
|
+
fold((d) => d.slice(0, 7));
|
|
1627
|
+
g = "month";
|
|
1628
|
+
}
|
|
1629
|
+
return { rows, granularity: g };
|
|
1630
|
+
}
|
|
1631
|
+
/** MCP timeline: one summary line, then `date<TAB>count` rows (zero runs collapsed, ≤60 rows). */
|
|
1430
1632
|
export function formatTimeline(data, footer = "") {
|
|
1431
|
-
const
|
|
1432
|
-
if (!
|
|
1633
|
+
const raw = data.buckets ?? [];
|
|
1634
|
+
if (!raw.length)
|
|
1433
1635
|
return "No data found for this topic in the given time range." + footer;
|
|
1636
|
+
const { rows: buckets, granularity } = aggregateBuckets(raw, data.granularity);
|
|
1637
|
+
const total = buckets.reduce((n, b) => n + b.count, 0);
|
|
1638
|
+
const peak = buckets.reduce((best, b) => (b.count > best.count ? b : best), buckets[0]);
|
|
1639
|
+
const mean = total / buckets.length;
|
|
1434
1640
|
const lines = [
|
|
1435
|
-
`**"${data.topic}"** — ${data.total} segments
|
|
1641
|
+
`**"${data.topic}"** — ${data.total ?? total} segments${data.episodes != null ? ` · ${data.episodes} episodes · ${data.channels ?? 0} channels` : ""} · peak ${peak.start} (${peak.count}) · mean ${mean.toFixed(1)}/${granularity} · by ${granularity}${granularity !== data.granularity ? ` (aggregated from ${data.granularity})` : ""} · semantic estimate at ≥${data.min_similarity ?? 0.35}\n`,
|
|
1436
1642
|
"date\tcount",
|
|
1437
1643
|
];
|
|
1644
|
+
let zeroRun = [];
|
|
1645
|
+
const flush = () => {
|
|
1646
|
+
if (zeroRun.length > 3)
|
|
1647
|
+
lines.push(`${zeroRun[0].start}..${zeroRun[zeroRun.length - 1].start}\t0 (${zeroRun.length} empty)`);
|
|
1648
|
+
else
|
|
1649
|
+
for (const z of zeroRun)
|
|
1650
|
+
lines.push(`${z.start}\t0`);
|
|
1651
|
+
zeroRun = [];
|
|
1652
|
+
};
|
|
1438
1653
|
for (const b of buckets) {
|
|
1654
|
+
if (b.count === 0) {
|
|
1655
|
+
zeroRun.push(b);
|
|
1656
|
+
continue;
|
|
1657
|
+
}
|
|
1658
|
+
flush();
|
|
1439
1659
|
lines.push(`${b.start}\t${b.count}`);
|
|
1440
1660
|
}
|
|
1441
|
-
|
|
1661
|
+
flush();
|
|
1662
|
+
for (const note of data.notes ?? [])
|
|
1663
|
+
lines.push(`_Note: ${note}._`);
|
|
1442
1664
|
const hint = nextCallHint(`search_quotes topic:"${data.topic}" start_date:${peak.start.slice(0, 10)}`);
|
|
1443
1665
|
return withFooters(lines.join("\n"), footer, "", hint);
|
|
1444
1666
|
}
|
|
1667
|
+
/** Token-light collection listing: header, `key | channels | description`, aliases. */
|
|
1668
|
+
export function formatCollections(data) {
|
|
1669
|
+
const cols = (data.collections ?? []).filter((c) => c.enabled !== false);
|
|
1670
|
+
const surface = getSurface();
|
|
1671
|
+
const flag = surface === "cli" ? "--collection" : "collection";
|
|
1672
|
+
const lines = [`${cols.length} collections · omit ${flag} to search ${data.default ?? "everything"}`];
|
|
1673
|
+
for (const c of cols)
|
|
1674
|
+
lines.push(`${c.key} | ${c.channels ?? "?"} channels | ${truncate(c.description ?? c.label ?? "", 90)}`);
|
|
1675
|
+
const al = Object.entries(data.aliases ?? {});
|
|
1676
|
+
if (al.length)
|
|
1677
|
+
lines.push(`aliases: ${al.map(([a, k]) => `${a}=${k}`).join(", ")}`);
|
|
1678
|
+
return lines.join("\n");
|
|
1679
|
+
}
|
|
1445
1680
|
//# sourceMappingURL=format.js.map
|