scoutline 0.22.0 → 0.24.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 +108 -26
- package/dist/capabilities/investigation.d.ts +134 -0
- package/dist/capabilities/investigation.d.ts.map +1 -0
- package/dist/capabilities/investigation.js +278 -0
- package/dist/capabilities/investigation.js.map +1 -0
- package/dist/capabilities/search.d.ts +18 -4
- package/dist/capabilities/search.d.ts.map +1 -1
- package/dist/commands/config.d.ts.map +1 -1
- package/dist/commands/config.js +14 -5
- package/dist/commands/config.js.map +1 -1
- package/dist/commands/crawl.d.ts.map +1 -1
- package/dist/commands/crawl.js +2 -1
- package/dist/commands/crawl.js.map +1 -1
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/doctor.js +2 -1
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/init.d.ts +42 -1
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +42 -5
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/investigate.d.ts +232 -0
- package/dist/commands/investigate.d.ts.map +1 -0
- package/dist/commands/investigate.js +838 -0
- package/dist/commands/investigate.js.map +1 -0
- package/dist/commands/map.d.ts.map +1 -1
- package/dist/commands/map.js +2 -1
- package/dist/commands/map.js.map +1 -1
- package/dist/commands/quota.d.ts.map +1 -1
- package/dist/commands/quota.js +2 -2
- package/dist/commands/quota.js.map +1 -1
- package/dist/commands/read.d.ts.map +1 -1
- package/dist/commands/read.js +2 -1
- package/dist/commands/read.js.map +1 -1
- package/dist/commands/repo.js +1 -1
- package/dist/commands/research.d.ts.map +1 -1
- package/dist/commands/research.js +2 -1
- package/dist/commands/research.js.map +1 -1
- package/dist/commands/search.d.ts +54 -6
- package/dist/commands/search.d.ts.map +1 -1
- package/dist/commands/search.js +273 -25
- package/dist/commands/search.js.map +1 -1
- package/dist/commands/vision.d.ts.map +1 -1
- package/dist/commands/vision.js +15 -2
- package/dist/commands/vision.js.map +1 -1
- package/dist/index.d.ts +93 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +737 -14
- package/dist/index.js.map +1 -1
- package/dist/lib/code-mode.d.ts +12 -0
- package/dist/lib/code-mode.d.ts.map +1 -1
- package/dist/lib/code-mode.js +18 -10
- package/dist/lib/code-mode.js.map +1 -1
- package/dist/lib/config-store.d.ts +45 -1
- package/dist/lib/config-store.d.ts.map +1 -1
- package/dist/lib/config-store.js +70 -1
- package/dist/lib/config-store.js.map +1 -1
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/config.js +5 -1
- package/dist/lib/config.js.map +1 -1
- package/dist/lib/errors.d.ts +7 -1
- package/dist/lib/errors.d.ts.map +1 -1
- package/dist/lib/errors.js +8 -2
- package/dist/lib/errors.js.map +1 -1
- package/dist/lib/execution.d.ts +14 -5
- package/dist/lib/execution.d.ts.map +1 -1
- package/dist/lib/execution.js +25 -6
- package/dist/lib/execution.js.map +1 -1
- package/dist/lib/investigate-claims.d.ts +90 -0
- package/dist/lib/investigate-claims.d.ts.map +1 -0
- package/dist/lib/investigate-claims.js +188 -0
- package/dist/lib/investigate-claims.js.map +1 -0
- package/dist/lib/investigate-extract.d.ts +35 -0
- package/dist/lib/investigate-extract.d.ts.map +1 -0
- package/dist/lib/investigate-extract.js +101 -0
- package/dist/lib/investigate-extract.js.map +1 -0
- package/dist/lib/investigate-planner.d.ts +58 -0
- package/dist/lib/investigate-planner.d.ts.map +1 -0
- package/dist/lib/investigate-planner.js +141 -0
- package/dist/lib/investigate-planner.js.map +1 -0
- package/dist/lib/mcp-client.d.ts +7 -0
- package/dist/lib/mcp-client.d.ts.map +1 -1
- package/dist/lib/mcp-client.js +11 -1
- package/dist/lib/mcp-client.js.map +1 -1
- package/dist/lib/parse-zoned-instant.d.ts +13 -0
- package/dist/lib/parse-zoned-instant.d.ts.map +1 -0
- package/dist/lib/parse-zoned-instant.js +17 -0
- package/dist/lib/parse-zoned-instant.js.map +1 -0
- package/dist/lib/quota-mapping.d.ts +4 -2
- package/dist/lib/quota-mapping.d.ts.map +1 -1
- package/dist/lib/quota-mapping.js +25 -2
- package/dist/lib/quota-mapping.js.map +1 -1
- package/dist/lib/redact.d.ts +3 -2
- package/dist/lib/redact.d.ts.map +1 -1
- package/dist/lib/redact.js +90 -24
- package/dist/lib/redact.js.map +1 -1
- package/dist/lib/timeout.d.ts +28 -0
- package/dist/lib/timeout.d.ts.map +1 -0
- package/dist/lib/timeout.js +30 -0
- package/dist/lib/timeout.js.map +1 -0
- package/dist/lib/url.d.ts +10 -4
- package/dist/lib/url.d.ts.map +1 -1
- package/dist/lib/url.js +21 -6
- package/dist/lib/url.js.map +1 -1
- package/dist/providers/arxiv/client.d.ts +4 -1
- package/dist/providers/arxiv/client.d.ts.map +1 -1
- package/dist/providers/arxiv/client.js +11 -3
- package/dist/providers/arxiv/client.js.map +1 -1
- package/dist/providers/bocha/adapter.d.ts +43 -0
- package/dist/providers/bocha/adapter.d.ts.map +1 -0
- package/dist/providers/bocha/adapter.js +197 -0
- package/dist/providers/bocha/adapter.js.map +1 -0
- package/dist/providers/bocha/client.d.ts +51 -0
- package/dist/providers/bocha/client.d.ts.map +1 -0
- package/dist/providers/bocha/client.js +104 -0
- package/dist/providers/bocha/client.js.map +1 -0
- package/dist/providers/bocha/credentials.d.ts +12 -0
- package/dist/providers/bocha/credentials.d.ts.map +1 -0
- package/dist/providers/bocha/credentials.js +29 -0
- package/dist/providers/bocha/credentials.js.map +1 -0
- package/dist/providers/bocha/diagnostics.d.ts +17 -0
- package/dist/providers/bocha/diagnostics.d.ts.map +1 -0
- package/dist/providers/bocha/diagnostics.js +33 -0
- package/dist/providers/bocha/diagnostics.js.map +1 -0
- package/dist/providers/brave/adapter.d.ts.map +1 -1
- package/dist/providers/brave/adapter.js +4 -3
- package/dist/providers/brave/adapter.js.map +1 -1
- package/dist/providers/brave/client.d.ts +1 -0
- package/dist/providers/brave/client.d.ts.map +1 -1
- package/dist/providers/brave/client.js +4 -6
- package/dist/providers/brave/client.js.map +1 -1
- package/dist/providers/brave/credentials.d.ts +2 -0
- package/dist/providers/brave/credentials.d.ts.map +1 -1
- package/dist/providers/brave/credentials.js +2 -0
- package/dist/providers/brave/credentials.js.map +1 -1
- package/dist/providers/catalog.d.ts +34 -0
- package/dist/providers/catalog.d.ts.map +1 -0
- package/dist/providers/catalog.js +57 -0
- package/dist/providers/catalog.js.map +1 -0
- package/dist/providers/crossref/adapter.js +2 -2
- package/dist/providers/crossref/adapter.js.map +1 -1
- package/dist/providers/crossref/client.d.ts +4 -1
- package/dist/providers/crossref/client.d.ts.map +1 -1
- package/dist/providers/crossref/client.js +11 -3
- package/dist/providers/crossref/client.js.map +1 -1
- package/dist/providers/crossref/diagnostics.d.ts +1 -1
- package/dist/providers/crossref/diagnostics.js +1 -1
- package/dist/providers/crossref/diagnostics.js.map +1 -1
- package/dist/providers/europepmc/adapter.js +2 -2
- package/dist/providers/europepmc/adapter.js.map +1 -1
- package/dist/providers/europepmc/client.d.ts +4 -1
- package/dist/providers/europepmc/client.d.ts.map +1 -1
- package/dist/providers/europepmc/client.js +11 -3
- package/dist/providers/europepmc/client.js.map +1 -1
- package/dist/providers/exa/client.d.ts +1 -0
- package/dist/providers/exa/client.d.ts.map +1 -1
- package/dist/providers/exa/client.js +3 -2
- package/dist/providers/exa/client.js.map +1 -1
- package/dist/providers/exa/credentials.d.ts +2 -0
- package/dist/providers/exa/credentials.d.ts.map +1 -1
- package/dist/providers/exa/credentials.js +2 -0
- package/dist/providers/exa/credentials.js.map +1 -1
- package/dist/providers/firecrawl/adapter.d.ts.map +1 -1
- package/dist/providers/firecrawl/adapter.js +2 -1
- package/dist/providers/firecrawl/adapter.js.map +1 -1
- package/dist/providers/firecrawl/client.d.ts +1 -0
- package/dist/providers/firecrawl/client.d.ts.map +1 -1
- package/dist/providers/firecrawl/client.js +3 -2
- package/dist/providers/firecrawl/client.js.map +1 -1
- package/dist/providers/firecrawl/credentials.d.ts +2 -0
- package/dist/providers/firecrawl/credentials.d.ts.map +1 -1
- package/dist/providers/firecrawl/credentials.js +2 -0
- package/dist/providers/firecrawl/credentials.js.map +1 -1
- package/dist/providers/firecrawl/quota.d.ts.map +1 -1
- package/dist/providers/firecrawl/quota.js +5 -2
- package/dist/providers/firecrawl/quota.js.map +1 -1
- package/dist/providers/jina/adapter.d.ts.map +1 -1
- package/dist/providers/jina/adapter.js +6 -4
- package/dist/providers/jina/adapter.js.map +1 -1
- package/dist/providers/jina/client.d.ts +2 -0
- package/dist/providers/jina/client.d.ts.map +1 -1
- package/dist/providers/jina/client.js +6 -5
- package/dist/providers/jina/client.js.map +1 -1
- package/dist/providers/jina/credentials.d.ts +2 -0
- package/dist/providers/jina/credentials.d.ts.map +1 -1
- package/dist/providers/jina/credentials.js +2 -0
- package/dist/providers/jina/credentials.js.map +1 -1
- package/dist/providers/kagi/adapter.d.ts +44 -0
- package/dist/providers/kagi/adapter.d.ts.map +1 -0
- package/dist/providers/kagi/adapter.js +188 -0
- package/dist/providers/kagi/adapter.js.map +1 -0
- package/dist/providers/kagi/client.d.ts +41 -0
- package/dist/providers/kagi/client.d.ts.map +1 -0
- package/dist/providers/kagi/client.js +100 -0
- package/dist/providers/kagi/client.js.map +1 -0
- package/dist/providers/kagi/credentials.d.ts +15 -0
- package/dist/providers/kagi/credentials.d.ts.map +1 -0
- package/dist/providers/kagi/credentials.js +35 -0
- package/dist/providers/kagi/credentials.js.map +1 -0
- package/dist/providers/kagi/diagnostics.d.ts +17 -0
- package/dist/providers/kagi/diagnostics.d.ts.map +1 -0
- package/dist/providers/kagi/diagnostics.js +33 -0
- package/dist/providers/kagi/diagnostics.js.map +1 -0
- package/dist/providers/linkup/client.d.ts +1 -0
- package/dist/providers/linkup/client.d.ts.map +1 -1
- package/dist/providers/linkup/client.js +3 -2
- package/dist/providers/linkup/client.js.map +1 -1
- package/dist/providers/linkup/credentials.d.ts +2 -0
- package/dist/providers/linkup/credentials.d.ts.map +1 -1
- package/dist/providers/linkup/credentials.js +2 -0
- package/dist/providers/linkup/credentials.js.map +1 -1
- package/dist/providers/minimax/adapter.d.ts.map +1 -1
- package/dist/providers/minimax/adapter.js +2 -2
- package/dist/providers/minimax/adapter.js.map +1 -1
- package/dist/providers/minimax/coding-plan-client.d.ts +1 -0
- package/dist/providers/minimax/coding-plan-client.d.ts.map +1 -1
- package/dist/providers/minimax/coding-plan-client.js +3 -2
- package/dist/providers/minimax/coding-plan-client.js.map +1 -1
- package/dist/providers/minimax/quota-client.d.ts +1 -0
- package/dist/providers/minimax/quota-client.d.ts.map +1 -1
- package/dist/providers/minimax/quota-client.js +3 -2
- package/dist/providers/minimax/quota-client.js.map +1 -1
- package/dist/providers/openalex/client.d.ts +2 -0
- package/dist/providers/openalex/client.d.ts.map +1 -1
- package/dist/providers/openalex/client.js +10 -3
- package/dist/providers/openalex/client.js.map +1 -1
- package/dist/providers/openalex/diagnostics.d.ts +1 -1
- package/dist/providers/openalex/diagnostics.js +1 -1
- package/dist/providers/openalex/diagnostics.js.map +1 -1
- package/dist/providers/parallel/client.d.ts +1 -0
- package/dist/providers/parallel/client.d.ts.map +1 -1
- package/dist/providers/parallel/client.js +3 -2
- package/dist/providers/parallel/client.js.map +1 -1
- package/dist/providers/parallel/credentials.d.ts +2 -0
- package/dist/providers/parallel/credentials.d.ts.map +1 -1
- package/dist/providers/parallel/credentials.js +2 -0
- package/dist/providers/parallel/credentials.js.map +1 -1
- package/dist/providers/perplexity/client.d.ts +2 -0
- package/dist/providers/perplexity/client.d.ts.map +1 -1
- package/dist/providers/perplexity/client.js +5 -4
- package/dist/providers/perplexity/client.js.map +1 -1
- package/dist/providers/perplexity/credentials.d.ts +2 -0
- package/dist/providers/perplexity/credentials.d.ts.map +1 -1
- package/dist/providers/perplexity/credentials.js +2 -0
- package/dist/providers/perplexity/credentials.js.map +1 -1
- package/dist/providers/pubmed/client.d.ts +2 -0
- package/dist/providers/pubmed/client.d.ts.map +1 -1
- package/dist/providers/pubmed/client.js +10 -3
- package/dist/providers/pubmed/client.js.map +1 -1
- package/dist/providers/registry.d.ts.map +1 -1
- package/dist/providers/registry.js +10 -1
- package/dist/providers/registry.js.map +1 -1
- package/dist/providers/searchapi/adapter.d.ts +54 -0
- package/dist/providers/searchapi/adapter.d.ts.map +1 -0
- package/dist/providers/searchapi/adapter.js +310 -0
- package/dist/providers/searchapi/adapter.js.map +1 -0
- package/dist/providers/searchapi/client.d.ts +73 -0
- package/dist/providers/searchapi/client.d.ts.map +1 -0
- package/dist/providers/searchapi/client.js +196 -0
- package/dist/providers/searchapi/client.js.map +1 -0
- package/dist/providers/searchapi/credentials.d.ts +46 -0
- package/dist/providers/searchapi/credentials.d.ts.map +1 -0
- package/dist/providers/searchapi/credentials.js +71 -0
- package/dist/providers/searchapi/credentials.js.map +1 -0
- package/dist/providers/searchapi/diagnostics.d.ts +48 -0
- package/dist/providers/searchapi/diagnostics.d.ts.map +1 -0
- package/dist/providers/searchapi/diagnostics.js +70 -0
- package/dist/providers/searchapi/diagnostics.js.map +1 -0
- package/dist/providers/searchapi/quota.d.ts +60 -0
- package/dist/providers/searchapi/quota.d.ts.map +1 -0
- package/dist/providers/searchapi/quota.js +125 -0
- package/dist/providers/searchapi/quota.js.map +1 -0
- package/dist/providers/spider/client.d.ts +2 -0
- package/dist/providers/spider/client.d.ts.map +1 -1
- package/dist/providers/spider/client.js +16 -6
- package/dist/providers/spider/client.js.map +1 -1
- package/dist/providers/spider/credentials.d.ts +2 -0
- package/dist/providers/spider/credentials.d.ts.map +1 -1
- package/dist/providers/spider/credentials.js +2 -0
- package/dist/providers/spider/credentials.js.map +1 -1
- package/dist/providers/tavily/client.d.ts +1 -0
- package/dist/providers/tavily/client.d.ts.map +1 -1
- package/dist/providers/tavily/client.js +3 -2
- package/dist/providers/tavily/client.js.map +1 -1
- package/dist/providers/tavily/credentials.d.ts +2 -0
- package/dist/providers/tavily/credentials.d.ts.map +1 -1
- package/dist/providers/tavily/credentials.js +2 -0
- package/dist/providers/tavily/credentials.js.map +1 -1
- package/dist/providers/types.d.ts +31 -2
- package/dist/providers/types.d.ts.map +1 -1
- package/dist/providers/types.js +3 -0
- package/dist/providers/types.js.map +1 -1
- package/dist/providers/you/client.d.ts +4 -0
- package/dist/providers/you/client.d.ts.map +1 -1
- package/dist/providers/you/client.js +4 -5
- package/dist/providers/you/client.js.map +1 -1
- package/dist/providers/you/credentials.d.ts +2 -0
- package/dist/providers/you/credentials.d.ts.map +1 -1
- package/dist/providers/you/credentials.js +3 -3
- package/dist/providers/you/credentials.js.map +1 -1
- package/dist/providers/zai/adapter.d.ts.map +1 -1
- package/dist/providers/zai/adapter.js +219 -4
- package/dist/providers/zai/adapter.js.map +1 -1
- package/dist/providers/zai/credentials.d.ts +2 -0
- package/dist/providers/zai/credentials.d.ts.map +1 -1
- package/dist/providers/zai/credentials.js +2 -0
- package/dist/providers/zai/credentials.js.map +1 -1
- package/dist/providers/zai/layout-parsing.d.ts +76 -0
- package/dist/providers/zai/layout-parsing.d.ts.map +1 -0
- package/dist/providers/zai/layout-parsing.js +151 -0
- package/dist/providers/zai/layout-parsing.js.map +1 -0
- package/dist/providers/zai/media.d.ts +19 -0
- package/dist/providers/zai/media.d.ts.map +1 -1
- package/dist/providers/zai/media.js +61 -0
- package/dist/providers/zai/media.js.map +1 -1
- package/dist/providers/zai/monitor-client.d.ts +1 -0
- package/dist/providers/zai/monitor-client.d.ts.map +1 -1
- package/dist/providers/zai/monitor-client.js +3 -2
- package/dist/providers/zai/monitor-client.js.map +1 -1
- package/dist/providers/zai/quota.d.ts +4 -0
- package/dist/providers/zai/quota.d.ts.map +1 -1
- package/dist/providers/zai/quota.js +16 -1
- package/dist/providers/zai/quota.js.map +1 -1
- package/package.json +1 -1
- package/skills/scoutline/SKILL.md +163 -45
- package/skills/scoutline/references/advanced.md +12 -10
|
@@ -0,0 +1,838 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Investigation orchestrator (investigate-pipeline lane, Ticket T4;
|
|
3
|
+
* docs/plans/investigate-pipeline DESIGN.md D3, PRD AC-3/AC-4/AC-9/
|
|
4
|
+
* AC-10; ADR-0013).
|
|
5
|
+
*
|
|
6
|
+
* Thin, data-returning composition of the seams `search` and `read`
|
|
7
|
+
* already export — no bespoke merge fork, no local fan-out copy, no
|
|
8
|
+
* transport of its own:
|
|
9
|
+
*
|
|
10
|
+
* 1. planSubQueries (T2, injected loadContextText — no filesystem).
|
|
11
|
+
* 2. resolveFanoutPlan over the resolved provider pin (AC-1 tiers,
|
|
12
|
+
* the same inputs handleSearch passes).
|
|
13
|
+
* 3. Grid execution through the exported seams: fan-out mode runs
|
|
14
|
+
* executeFanoutPlan; single mode runs the exported search() with
|
|
15
|
+
* {merge: N > 1} over the escaped-pipe join of the sub-queries
|
|
16
|
+
* (the exact context-mode join precedent, index.ts handleSearch).
|
|
17
|
+
* 4. Top --sources distinct sources = the first K rows of the merged
|
|
18
|
+
* FormattedResult[] (mergeResults already collapsed near-dup
|
|
19
|
+
* clusters to representatives — a near-dup pair IS one row).
|
|
20
|
+
* 5. Reads: bounded-concurrency pool (default 4) over the reader
|
|
21
|
+
* capability seam via executeReaderOperation, one client per
|
|
22
|
+
* read (descriptor.create per read), closed in `finally`.
|
|
23
|
+
* 6. extractPassages (T3) per read result.
|
|
24
|
+
* 7. EvidencePack assembly per the T1 types.
|
|
25
|
+
*
|
|
26
|
+
* `--synthesize` (T7, PRD AC-7, DESIGN D6, ADR-0013 §2) is the explicit
|
|
27
|
+
* Z.AI-only escape hatch and is ADDITIVE-ONLY BY CONSTRUCTION: the pack
|
|
28
|
+
* is fully assembled (searches, reads, extraction, coverage) BEFORE the
|
|
29
|
+
* synthesis dep is ever called, so the brief can only ever be ADDED as
|
|
30
|
+
* the LAST key — a failed synthesis is the invocation's terminal error
|
|
31
|
+
* and never degrades, shrinks, or reorders the pack. The command holds
|
|
32
|
+
* no transport: construction is handler-seam wiring, exactly like every
|
|
33
|
+
* other capability, and the dep is injected.
|
|
34
|
+
*
|
|
35
|
+
* Reader supplier selection mirrors handleRead: the FIRST descriptor
|
|
36
|
+
* in registry order whose injected `readerCapabilityFor` resolves a
|
|
37
|
+
* ReaderCapability serves every read (the same first-configured-capable
|
|
38
|
+
* order read uses; cross-provider fallback per failed read is the
|
|
39
|
+
* index.ts handler seam's business, T6). A supplier that rejects a
|
|
40
|
+
* source terminally classifies as `reader-failed:<code>`; a supplier
|
|
41
|
+
* that cannot serve the capability at all classifies as
|
|
42
|
+
* `no-reader-supplier`. Unread rows carry reason codes only
|
|
43
|
+
* (redacted, house rule); the pool continues past them;
|
|
44
|
+
* `sourcesRead` counts successes only.
|
|
45
|
+
*
|
|
46
|
+
* Consumption linearity: every billable arm and read attempt records
|
|
47
|
+
* exactly one event (N×M + K) because the shared executors emit per
|
|
48
|
+
* invoke and the orchestrator never double-reads a URL.
|
|
49
|
+
*
|
|
50
|
+
* `--isolated` is ACCEPTED, never rejected: the pid-segment cache
|
|
51
|
+
* behavior is the main() handler seam's job (T6) — the command has no
|
|
52
|
+
* cache-directory logic of its own; the injected cache IS the
|
|
53
|
+
* (possibly isolated) production cache. Journal entry/marker WRITING
|
|
54
|
+
* lives at the index.ts descriptor seam (journalingDescriptors /
|
|
55
|
+
* captureServingDescriptors): this command consumes the injected
|
|
56
|
+
* descriptor list verbatim, so capture-wrapped descriptors keep
|
|
57
|
+
* stamping servedFrom/cacheKey cells the journal hook consumes — the
|
|
58
|
+
* T4-level guarantee (asserted by the seam-passthrough test); journal
|
|
59
|
+
* wiring itself is deferred to T6 (journal.ts / index.ts untouched).
|
|
60
|
+
*/
|
|
61
|
+
import { createHash } from "node:crypto";
|
|
62
|
+
import { buildProviderCacheKey } from "../lib/cache.js";
|
|
63
|
+
import { executeReaderOperation } from "../lib/execution.js";
|
|
64
|
+
import { UnsupportedCapabilityError, ValidationError } from "../lib/errors.js";
|
|
65
|
+
import { redactSecrets } from "../lib/redact.js";
|
|
66
|
+
import { applyBudget } from "../lib/output-budget.js";
|
|
67
|
+
import { persistCompaction } from "../lib/output-budget-persistence.js";
|
|
68
|
+
import { executeFanoutPlan, resolveFanoutPlan, search, } from "./search.js";
|
|
69
|
+
import { deriveTemplateTopic, planSubQueries } from "../lib/investigate-planner.js";
|
|
70
|
+
import { extractPassages } from "../lib/investigate-extract.js";
|
|
71
|
+
import { splitClaims, matchClaimsToEvidence, MAX_VERIFY_CLAIMS } from "../lib/investigate-claims.js";
|
|
72
|
+
import { SHARED_PROVIDER_FLAG_IDS } from "../providers/catalog.js";
|
|
73
|
+
/**
|
|
74
|
+
* Passage-quote cap for the brief prompt. Bounded so the prompt cannot
|
|
75
|
+
* grow with the read pool: 20 quotes ≈ the first few sources' passages,
|
|
76
|
+
* well inside the Z.AI chat context even at the passage cap (5 per
|
|
77
|
+
* source). Deterministic (first N in pack order), never sampled.
|
|
78
|
+
*/
|
|
79
|
+
export const SYNTHESIS_QUOTE_CAP = 20;
|
|
80
|
+
/**
|
|
81
|
+
* `investigate --help` (T6/T7). The command's rejected-flag contract is
|
|
82
|
+
* part of the surface: --depth/--arms/--budget-tokens DO NOT EXIST (PRD
|
|
83
|
+
* AC-1 — the rejection is the feature), and --context-stdin is
|
|
84
|
+
* deliberately not investigate's (search-only spelling; pipes and
|
|
85
|
+
* --context cover the sub-query sources).
|
|
86
|
+
*/
|
|
87
|
+
export const INVESTIGATE_HELP = `
|
|
88
|
+
Investigate Command - Local investigation pipeline (EvidencePack)
|
|
89
|
+
|
|
90
|
+
Usage: scoutline investigate <question> [options]
|
|
91
|
+
|
|
92
|
+
Plans sub-queries, fans out search, merges by fusion, reads the top
|
|
93
|
+
sources, and extracts deterministic passages into an EvidencePack
|
|
94
|
+
(schemaVersion 1). The pack is data, not prose: agents consume it
|
|
95
|
+
directly; text output modes fall back to JSON. Warm re-runs replay the
|
|
96
|
+
response cache (coverage.cacheHits reflects it).
|
|
97
|
+
|
|
98
|
+
Provider selection (precedence: explicit flag, then SCOUTLINE_PROVIDER, then zai):
|
|
99
|
+
--provider <${SHARED_PROVIDER_FLAG_IDS}> Select the search provider. A
|
|
100
|
+
comma-list or \`all\` fans out over every listed arm (search's
|
|
101
|
+
activation tiers, verbatim); a single id runs one arm;
|
|
102
|
+
\`scoutline config set fanout true\` (no pin) is a standing fan-out.
|
|
103
|
+
|
|
104
|
+
Cost: the run bills N sub-queries × M arms searches + up to K sources
|
|
105
|
+
(per-source reader supplier attempts can bill more than one read) —
|
|
106
|
+
one stderr notice states the exact arithmetic before any billable work.
|
|
107
|
+
|
|
108
|
+
Sub-query planning (precedence: pipes > --context > template):
|
|
109
|
+
Pipes An unescaped \`|\` in the question splits it into explicit
|
|
110
|
+
sub-queries (the --merge grammar; escape with \\| for a
|
|
111
|
+
literal pipe). Capped at 8 — a longer split fails loud
|
|
112
|
+
with VALIDATION_ERROR (never silently truncated). Wins over
|
|
113
|
+
--context with a stderr notice.
|
|
114
|
+
--context <path> Read a local notes file and derive up to 8
|
|
115
|
+
sub-queries (headings/questions), exactly like search.
|
|
116
|
+
Template Deterministic transforms of the bare question (original,
|
|
117
|
+
key terms, overview/evidence/criticism), deduped, capped at 5.
|
|
118
|
+
|
|
119
|
+
Verify mode (--verify, ADR-0013 follow-up):
|
|
120
|
+
The statement splits into sentence-claims (the extract grammar's
|
|
121
|
+
terminators; '|' is literal text); each claim is searched VERBATIM
|
|
122
|
+
(no per-claim controls); deterministic term matching links claims to
|
|
123
|
+
the extracted passages. Verdicts per claim:
|
|
124
|
+
corroborated >= 1 matching passage, 0 negation cues
|
|
125
|
+
contradicted >= 1 matching passage carrying a negation cue
|
|
126
|
+
unresolved no matching passages
|
|
127
|
+
HINT-GRADE DISCLOSURE: 'contradicted' is a negation-cue heuristic
|
|
128
|
+
(fixed, versioned cue list) — NOT semantic contradiction. Semantic
|
|
129
|
+
judgment stays with the calling agent; the per-claim negationCues
|
|
130
|
+
count exists for exactly that re-judgment. Claim text and verdicts
|
|
131
|
+
are never cut by --max-chars (evidence pointers drop last, whole).
|
|
132
|
+
|
|
133
|
+
Options:
|
|
134
|
+
--provider <ids> Comma-list, \`all\`, or a single id (fan-out tiers
|
|
135
|
+
above).
|
|
136
|
+
--context <path> Local notes file deriving sub-queries (max 256 KiB;
|
|
137
|
+
never leaves the machine — only the derived
|
|
138
|
+
sub-query strings are searched).
|
|
139
|
+
--sources <n> How many distinct post-cluster sources to read
|
|
140
|
+
(positive integer; default 5).
|
|
141
|
+
--max-chars <n> Fit the pack in ~<n> chars (passages trim first —
|
|
142
|
+
quotes truncate, charRange adjusts — then late
|
|
143
|
+
sources drop; question/subQueries/coverage are
|
|
144
|
+
never cut; the full untrimmed pack is saved to the
|
|
145
|
+
artifacts store — recover with
|
|
146
|
+
"scoutline history show").
|
|
147
|
+
--verify Claim-corroboration mode: the positional becomes a
|
|
148
|
+
STATEMENT; sentence-claims (<= 8, fail-loud) are the
|
|
149
|
+
sub-query grid verbatim; the pack gains a verify
|
|
150
|
+
block (per claim: verdict + evidence pointers +
|
|
151
|
+
negation-cue count). Cannot combine with --context
|
|
152
|
+
(verify owns planning).
|
|
153
|
+
--synthesize Attach an ADDITIVE \`brief\` (Z.AI chat) to the pack.
|
|
154
|
+
Absent by default. Z.AI-only: it ignores --provider
|
|
155
|
+
(a stderr notice fires when another provider is
|
|
156
|
+
pinned) and always synthesizes through Z.AI. The
|
|
157
|
+
pack is assembled first, so the brief only ever
|
|
158
|
+
ADDS a key — a synthesis failure is this run's
|
|
159
|
+
terminal error, never a degraded pack.
|
|
160
|
+
--no-cache Skip the response cache for this run's searches
|
|
161
|
+
and reads.
|
|
162
|
+
--no-journal Skip the research journal entries for this run's
|
|
163
|
+
underlying search/read ops.
|
|
164
|
+
--save [<path>] Save the pack as a clean report (global flag;
|
|
165
|
+
master copy + optional export; refuses an
|
|
166
|
+
existing target without --save-force).
|
|
167
|
+
--isolated Process-isolated state (accepted; no stateful
|
|
168
|
+
directory exists — pure cache replay on re-run).
|
|
169
|
+
|
|
170
|
+
Not investigate's flags (rejected with VALIDATION_ERROR — by design):
|
|
171
|
+
--depth There is no depth axis; planning is deterministic
|
|
172
|
+
(pipes > context > template).
|
|
173
|
+
--arms The arm set IS the provider pin (--provider
|
|
174
|
+
comma-list / all); there is no separate control.
|
|
175
|
+
--budget-tokens --max-chars is the budget (chars, not tokens).
|
|
176
|
+
--context-stdin Search-only spelling; use --context <path> (or
|
|
177
|
+
pipes in the question) instead.
|
|
178
|
+
|
|
179
|
+
Standard global options apply (--output-format/-O, --save-format,
|
|
180
|
+
--save-force, --provider before the command, --no-fallback,
|
|
181
|
+
--isolated).
|
|
182
|
+
|
|
183
|
+
Output formats (--output-format / -O):
|
|
184
|
+
data Raw EvidencePack JSON (default)
|
|
185
|
+
json Envelope-wrapped {success, data, timestamp}
|
|
186
|
+
pretty Pretty-printed json
|
|
187
|
+
compact / markdown / refs / tty Fall back to JSON — the pack is
|
|
188
|
+
data, not prose.
|
|
189
|
+
|
|
190
|
+
Examples:
|
|
191
|
+
scoutline investigate "rust async runtime benchmarks"
|
|
192
|
+
scoutline investigate "rust async | rust tokio" # explicit pipes
|
|
193
|
+
scoutline --provider tavily,exa investigate "alpha | beta"
|
|
194
|
+
scoutline investigate "vector dbs" --context notes.md --sources 3
|
|
195
|
+
scoutline investigate "k8s cost" --max-chars 4000 # budgeted pack
|
|
196
|
+
scoutline investigate "wasm runtimes" --synthesize # + Z.AI brief
|
|
197
|
+
scoutline investigate "X is fast. Y lags." --verify # claim verdicts
|
|
198
|
+
|
|
199
|
+
Default JSON shape (EvidencePack, schemaVersion 1):
|
|
200
|
+
{
|
|
201
|
+
"schemaVersion": 1,
|
|
202
|
+
"question": "...",
|
|
203
|
+
"subQueries": ["..."],
|
|
204
|
+
"sources": [
|
|
205
|
+
{
|
|
206
|
+
"url": "https://...",
|
|
207
|
+
"finalUrl": "https://...",
|
|
208
|
+
"title": "Page title",
|
|
209
|
+
"fetchedAt": "2026-09-20T00:00:00.000Z",
|
|
210
|
+
"provider": "tavily",
|
|
211
|
+
"contentFormat": "markdown",
|
|
212
|
+
"contentSha256": "<sha256 of the utf-8 content>",
|
|
213
|
+
"passages": [{ "quote": "...", "charRange": [0, 42] }]
|
|
214
|
+
}
|
|
215
|
+
],
|
|
216
|
+
"coverage": {
|
|
217
|
+
"subQueries": 2,
|
|
218
|
+
"armsUsed": 2,
|
|
219
|
+
"sourcesConsidered": 5,
|
|
220
|
+
"sourcesRead": 5,
|
|
221
|
+
"cacheHits": 0,
|
|
222
|
+
"unread": [{ "url": "https://...", "reason": "reader-failed:API_ERROR" }]
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
`.trim();
|
|
226
|
+
// ---------------------------------------------------------------------------
|
|
227
|
+
// Output Budget ladder (T5, DESIGN D7, PRD AC-8)
|
|
228
|
+
// ---------------------------------------------------------------------------
|
|
229
|
+
/**
|
|
230
|
+
* One passage-trim step: truncate every quote FROM THE END to half
|
|
231
|
+
* its length and ADJUST charRange to the truncated slice so the
|
|
232
|
+
* round-trip pin `content.slice(...charRange) === quote` survives
|
|
233
|
+
* every pass. NO omission marker is appended: the pin demands quote
|
|
234
|
+
* be an exact slice of the paired content, so any added `…` would
|
|
235
|
+
* break it (the read ladder's marker idiom does not apply here).
|
|
236
|
+
* A quote too short to halve stays unchanged (rule exhausts; the
|
|
237
|
+
* source-drop rule takes over) — and empty quotes never exist, so a
|
|
238
|
+
* budgeted pack still decodes. url/title/fetchedAt/provider/hashes
|
|
239
|
+
* are never touched. `ponytail:` marker-less trim is the pin-driven
|
|
240
|
+
* minimum; a marker would need pinless budgeted quotes (schema
|
|
241
|
+
* change) — revisit only if budgeted-quote readability ever matters.
|
|
242
|
+
*/
|
|
243
|
+
const trimPassagesRule = {
|
|
244
|
+
name: "trim-passages",
|
|
245
|
+
apply: (envelope) => {
|
|
246
|
+
const pack = envelope;
|
|
247
|
+
return {
|
|
248
|
+
...pack,
|
|
249
|
+
sources: pack.sources.map((source) => ({
|
|
250
|
+
...source,
|
|
251
|
+
passages: source.passages.map((passage) => {
|
|
252
|
+
const half = Math.floor(passage.quote.length / 2);
|
|
253
|
+
if (half <= 0)
|
|
254
|
+
return passage;
|
|
255
|
+
return {
|
|
256
|
+
quote: passage.quote.slice(0, half),
|
|
257
|
+
// charRange adjusts to the truncated slice — the pin holds.
|
|
258
|
+
charRange: [passage.charRange[0], passage.charRange[0] + half],
|
|
259
|
+
};
|
|
260
|
+
}),
|
|
261
|
+
})),
|
|
262
|
+
};
|
|
263
|
+
},
|
|
264
|
+
};
|
|
265
|
+
/**
|
|
266
|
+
* One late-source drop step: the LAST source drops whole (search's
|
|
267
|
+
* drop-lowest-rank analog). Fix-round M1: when a verify block rides
|
|
268
|
+
* the pack, evidence pointers at the dropped index are scrubbed HERE
|
|
269
|
+
* (per claim, whole) — otherwise the pack ships dangling pointers
|
|
270
|
+
* (decode's index guard checks non-negative only) that only the later
|
|
271
|
+
* pointer-drop rule would remove, leaving a window of budgets with
|
|
272
|
+
* unrecoverable references. Question-mode packs (no verify block)
|
|
273
|
+
* take the byte-identical pre-fix path — the scrub is verify-only.
|
|
274
|
+
*/
|
|
275
|
+
const dropLastSourceRule = {
|
|
276
|
+
name: "drop-late-source",
|
|
277
|
+
apply: (envelope) => {
|
|
278
|
+
const pack = envelope;
|
|
279
|
+
if (pack.sources.length <= 0)
|
|
280
|
+
return pack;
|
|
281
|
+
const droppedIndex = pack.sources.length - 1;
|
|
282
|
+
const sources = pack.sources.slice(0, -1);
|
|
283
|
+
if (pack.verify === undefined) {
|
|
284
|
+
return { ...pack, sources };
|
|
285
|
+
}
|
|
286
|
+
return {
|
|
287
|
+
...pack,
|
|
288
|
+
sources,
|
|
289
|
+
verify: {
|
|
290
|
+
...pack.verify,
|
|
291
|
+
claims: pack.verify.claims.map((claim) => claim.evidence.some((p) => p.sourceIndex === droppedIndex)
|
|
292
|
+
? {
|
|
293
|
+
...claim,
|
|
294
|
+
evidence: claim.evidence.filter((p) => p.sourceIndex !== droppedIndex),
|
|
295
|
+
}
|
|
296
|
+
: claim),
|
|
297
|
+
},
|
|
298
|
+
};
|
|
299
|
+
},
|
|
300
|
+
};
|
|
301
|
+
/**
|
|
302
|
+
* investigate-verify lane (DESIGN D6, PRD AC-7): the LATE pointer
|
|
303
|
+
* drop — evidence pointers drop per claim, WHOLE (a claim keeps its
|
|
304
|
+
* full pointer set or none; never a truncated list). Claim text,
|
|
305
|
+
* verdicts, and cue counts are never cut (expressed by omission —
|
|
306
|
+
* this rule touches only `evidence`). Runs after the source drop so
|
|
307
|
+
* pointer elimination is the last loss before the floor.
|
|
308
|
+
*
|
|
309
|
+
* ponytail: near the floor this rule is COARSE — it clears every
|
|
310
|
+
* claim's pointers in one step rather than shedding one claim's at a
|
|
311
|
+
* time, so budgets just above the floor can overshoot down to a
|
|
312
|
+
* pointer-free pack. Fine-grained per-claim shedding (or per-pointer,
|
|
313
|
+
* cheapest-first) is the upgrade path if a consumer ever needs to
|
|
314
|
+
* keep SOME pointers at extreme budgets; nothing today reads pointers
|
|
315
|
+
* partially.
|
|
316
|
+
*/
|
|
317
|
+
const dropEvidencePointersRule = {
|
|
318
|
+
name: "drop-evidence-pointers",
|
|
319
|
+
apply: (envelope) => {
|
|
320
|
+
const pack = envelope;
|
|
321
|
+
if (pack.verify === undefined)
|
|
322
|
+
return pack;
|
|
323
|
+
if (pack.verify.claims.every((claim) => claim.evidence.length === 0))
|
|
324
|
+
return pack;
|
|
325
|
+
return {
|
|
326
|
+
...pack,
|
|
327
|
+
verify: {
|
|
328
|
+
...pack.verify,
|
|
329
|
+
claims: pack.verify.claims.map((claim) => ({ ...claim, evidence: [] })),
|
|
330
|
+
},
|
|
331
|
+
};
|
|
332
|
+
},
|
|
333
|
+
};
|
|
334
|
+
/**
|
|
335
|
+
* INVESTIGATE_LADDER (D7 budget order; verify lane D6 extension):
|
|
336
|
+
* passages trim FIRST (quote truncate, charRange adjusts — the
|
|
337
|
+
* round-trip pin survives), then LATE sources drop whole, then —
|
|
338
|
+
* verify packs only — evidence pointers drop per claim (whole).
|
|
339
|
+
* question/subQueries/coverage and verify claim text/verdicts/cue
|
|
340
|
+
* counts are never cut — expressed by omission (no rule touches
|
|
341
|
+
* them).
|
|
342
|
+
*/
|
|
343
|
+
export const INVESTIGATE_LADDER = [
|
|
344
|
+
trimPassagesRule,
|
|
345
|
+
dropLastSourceRule,
|
|
346
|
+
dropEvidencePointersRule,
|
|
347
|
+
];
|
|
348
|
+
const DEFAULT_SOURCES = 5;
|
|
349
|
+
const DEFAULT_READ_CONCURRENCY = 4;
|
|
350
|
+
// ---------------------------------------------------------------------------
|
|
351
|
+
// Option validation (trust boundary)
|
|
352
|
+
// ---------------------------------------------------------------------------
|
|
353
|
+
function validateOptions(options) {
|
|
354
|
+
// Review fix #4: every provided field validates UNCONDITIONALLY —
|
|
355
|
+
// no early return may shield a later field's check.
|
|
356
|
+
if (typeof options.sources === "number") {
|
|
357
|
+
if (!Number.isInteger(options.sources) || options.sources <= 0) {
|
|
358
|
+
throw new ValidationError(`--sources must be a positive integer (got ${options.sources}).`);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
// T5 (D7): strict positive-integer --max-chars (parseBriefMaxChars
|
|
362
|
+
// class) — validated at the boundary so a bad value is
|
|
363
|
+
// VALIDATION_ERROR regardless of provider state.
|
|
364
|
+
if (options.maxChars !== undefined) {
|
|
365
|
+
const value = options.maxChars;
|
|
366
|
+
if (!Number.isFinite(value) || !Number.isInteger(value) || value < 1) {
|
|
367
|
+
throw new ValidationError("--max-chars must be a positive integer");
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
return typeof options.sources === "number" ? options.sources : DEFAULT_SOURCES;
|
|
371
|
+
}
|
|
372
|
+
/**
|
|
373
|
+
* Output Budget seam (T5, D7): walk INVESTIGATE_LADDER over the pack;
|
|
374
|
+
* on a fired budget persist the FULL untrimmed pack through
|
|
375
|
+
* persistCompaction (mirrored save shape — post-redaction,
|
|
376
|
+
* pre-compaction `result`, MANDATORY log entry with
|
|
377
|
+
* presentation-flag-free args) and stamp `compaction {budget, ref}`
|
|
378
|
+
* into the returned payload. No flag → identity (the zero-diff
|
|
379
|
+
* invariant). Returns a NEW CommandResult; never mutates the input.
|
|
380
|
+
*/
|
|
381
|
+
async function applyInvestigateOutputBudget(result, maxChars, options) {
|
|
382
|
+
if (maxChars === undefined || result.kind !== "data")
|
|
383
|
+
return result;
|
|
384
|
+
const outcome = applyBudget(result.data, maxChars, INVESTIGATE_LADDER);
|
|
385
|
+
if (outcome.compaction === undefined)
|
|
386
|
+
return result;
|
|
387
|
+
const redactedEnvelope = redactSecrets(result.data, options.deps.secrets);
|
|
388
|
+
const compaction = await persistCompaction(redactedEnvelope, outcome.compaction, {
|
|
389
|
+
command: "investigate",
|
|
390
|
+
args: options.args,
|
|
391
|
+
provider: options.providerRouting,
|
|
392
|
+
outputFormat: "data",
|
|
393
|
+
}, {
|
|
394
|
+
env: options.deps.env,
|
|
395
|
+
now: options.deps.now ?? Date.now,
|
|
396
|
+
onNotice: options.context.notice,
|
|
397
|
+
});
|
|
398
|
+
options.context.notice(`output budget: ${maxChars} chars — full untrimmed envelope saved (${compaction.ref})`);
|
|
399
|
+
return {
|
|
400
|
+
kind: "data",
|
|
401
|
+
data: {
|
|
402
|
+
...outcome.projection,
|
|
403
|
+
compaction,
|
|
404
|
+
},
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
/**
|
|
408
|
+
* Serve one read through the shared cache, trying suppliers in
|
|
409
|
+
* registry order (review fix #2 — AC-4 "provider fallback"). One
|
|
410
|
+
* client per ATTEMPT: `deps.readerCapabilityFor` resolves the
|
|
411
|
+
* supplier's capability per call (`descriptor.create` per attempt —
|
|
412
|
+
* `create` is side-effect-free metadata capture; the operation's
|
|
413
|
+
* invoke owns and closes its transport inside executeReaderOperation,
|
|
414
|
+
* so no transport outlives the call).
|
|
415
|
+
*
|
|
416
|
+
* Advance-to-next-supplier on ANY thrown error — an
|
|
417
|
+
* UnsupportedCapabilityError (supplier cannot serve the capability)
|
|
418
|
+
* and terminal failures alike (executeReaderOperation has already
|
|
419
|
+
* exhausted its internal retry for transient classes by the time the
|
|
420
|
+
* error escapes, so every escape is supplier-exhausted). ALL suppliers
|
|
421
|
+
* exhausted → the LAST supplier's reason code surfaces (most
|
|
422
|
+
* informative: the deepest attempt). Every attempt bills exactly one
|
|
423
|
+
* consumption event (executor behavior — fallback attempts are
|
|
424
|
+
* billable reads, consistent with the usage-ledger "retries count as
|
|
425
|
+
* attempts" doctrine; K in the N×M+K notice counts attempts).
|
|
426
|
+
*
|
|
427
|
+
* cacheHits instrumentation: a warm serve is a read-only cache `get()`
|
|
428
|
+
* on the FIRST supplier's partition key that decodes non-null through
|
|
429
|
+
* the operation's own decoder. ponytail: conservative undercount when
|
|
430
|
+
* a fallback supplier serves warm (first-supplier probe only); widen
|
|
431
|
+
* to per-attempt probes if a fallback-heavy workload needs exact hits.
|
|
432
|
+
* Boundary: legacy read-through candidates are miss-then-set serves
|
|
433
|
+
* and count as misses (honest warm-serve count only).
|
|
434
|
+
*/
|
|
435
|
+
async function serveRead(url, suppliers, deps, noCache, cacheHits) {
|
|
436
|
+
if (suppliers.length === 0) {
|
|
437
|
+
return { url, reason: "no-reader-supplier", warm: false };
|
|
438
|
+
}
|
|
439
|
+
let lastReason = "no-reader-supplier";
|
|
440
|
+
for (const descriptor of suppliers) {
|
|
441
|
+
const capability = deps.readerCapabilityFor(descriptor);
|
|
442
|
+
if (capability === undefined) {
|
|
443
|
+
// Not a reader supplier at all — selection pre-filters these;
|
|
444
|
+
// kept as a guard for hand-built descriptor lists.
|
|
445
|
+
lastReason = "no-reader-supplier";
|
|
446
|
+
continue;
|
|
447
|
+
}
|
|
448
|
+
const wasWarm = await readerCacheWasWarm(capability, url, deps, noCache);
|
|
449
|
+
try {
|
|
450
|
+
const result = await executeReaderOperation(capability.fetch, { url }, { noCache, ...(deps.retryPolicy !== undefined ? { retryPolicy: deps.retryPolicy } : {}) }, {
|
|
451
|
+
cache: deps.cache,
|
|
452
|
+
sleep: deps.sleep,
|
|
453
|
+
random: deps.random,
|
|
454
|
+
...(deps.consume !== undefined ? { consume: deps.consume } : {}),
|
|
455
|
+
...(deps.now !== undefined ? { now: deps.now } : {}),
|
|
456
|
+
});
|
|
457
|
+
if (wasWarm)
|
|
458
|
+
cacheHits.count += 1;
|
|
459
|
+
return { url, result, warm: wasWarm };
|
|
460
|
+
}
|
|
461
|
+
catch (error) {
|
|
462
|
+
// Reason CODE only, redacted — no error prose crossing the
|
|
463
|
+
// interface (house rule, D5). Advances to the next supplier;
|
|
464
|
+
// this code surfaces only if every later supplier also fails.
|
|
465
|
+
const code = typeof error === "object" && error !== null && "code" in error
|
|
466
|
+
? String(error.code)
|
|
467
|
+
: "UNKNOWN_ERROR";
|
|
468
|
+
lastReason =
|
|
469
|
+
error instanceof UnsupportedCapabilityError
|
|
470
|
+
? "no-reader-supplier"
|
|
471
|
+
: `reader-failed:${code}`;
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
return { url, reason: lastReason, warm: false };
|
|
475
|
+
}
|
|
476
|
+
/**
|
|
477
|
+
* Read-only warm probe on the reader partition key the executor will
|
|
478
|
+
* use — the operation's own cacheIdentity + decoder, never a
|
|
479
|
+
* fabricated key. Never seeds an entry; shared execution remains the
|
|
480
|
+
* sole read/write authority.
|
|
481
|
+
*/
|
|
482
|
+
async function readerCacheWasWarm(capability, url, deps, noCache) {
|
|
483
|
+
if (noCache)
|
|
484
|
+
return false;
|
|
485
|
+
try {
|
|
486
|
+
const identity = capability.fetch.cacheIdentity({ url });
|
|
487
|
+
const key = buildProviderCacheKey({
|
|
488
|
+
provider: identity.provider,
|
|
489
|
+
capability: `${identity.capability}-${identity.operation}`,
|
|
490
|
+
credentialFingerprint: identity.credentialFingerprint,
|
|
491
|
+
request: identity.request,
|
|
492
|
+
});
|
|
493
|
+
const raw = await deps.cache.get(key);
|
|
494
|
+
if (raw === null)
|
|
495
|
+
return false;
|
|
496
|
+
return capability.fetch.decodeCached(raw) !== null;
|
|
497
|
+
}
|
|
498
|
+
catch {
|
|
499
|
+
return false;
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
/**
|
|
503
|
+
* Bounded-concurrency map over the selected sources (D3 #5). A read
|
|
504
|
+
* slot is reused as soon as its previous read settles (start-aligned
|
|
505
|
+
* workers over a shared index — no per-chunk scheduling, no timers).
|
|
506
|
+
* `suppliers` is the run's ordered reader supplier list (review fix
|
|
507
|
+
* #2); serveRead falls through it per source.
|
|
508
|
+
*/
|
|
509
|
+
async function readPool(rows, suppliers, deps, noCache, cacheHits) {
|
|
510
|
+
const limit = Math.max(1, deps.readConcurrency ?? DEFAULT_READ_CONCURRENCY);
|
|
511
|
+
const outcomes = [];
|
|
512
|
+
let next = 0;
|
|
513
|
+
const worker = async () => {
|
|
514
|
+
for (;;) {
|
|
515
|
+
const index = next;
|
|
516
|
+
next += 1;
|
|
517
|
+
const row = rows[index];
|
|
518
|
+
if (row === undefined)
|
|
519
|
+
return;
|
|
520
|
+
outcomes.push(await serveRead(row.url, suppliers, deps, noCache, cacheHits));
|
|
521
|
+
}
|
|
522
|
+
};
|
|
523
|
+
await Promise.all(Array.from({ length: Math.min(limit, rows.length) }, worker));
|
|
524
|
+
return outcomes;
|
|
525
|
+
}
|
|
526
|
+
/**
|
|
527
|
+
* The run's reader supplier LIST (review fix #2): every descriptor in
|
|
528
|
+
* registry order whose injected `readerCapabilityFor` resolves a
|
|
529
|
+
* capability — the same first-configured-capable order handleRead's
|
|
530
|
+
* provider selection walks. serveRead falls through the list per
|
|
531
|
+
* source; an empty list keeps the legacy all-unread
|
|
532
|
+
* `no-reader-supplier` behavior.
|
|
533
|
+
*/
|
|
534
|
+
function selectReaderSuppliers(deps) {
|
|
535
|
+
return deps.descriptors.filter((descriptor) => deps.readerCapabilityFor(descriptor) !== undefined);
|
|
536
|
+
}
|
|
537
|
+
// ---------------------------------------------------------------------------
|
|
538
|
+
// Escaped-pipe join (the handleSearch context-mode precedent verbatim:
|
|
539
|
+
// trim trailing backslashes, then escape pipes, join on "|")
|
|
540
|
+
// ---------------------------------------------------------------------------
|
|
541
|
+
function joinSubQueries(subQueries) {
|
|
542
|
+
return subQueries.map((s) => s.replace(/\\+$/, "").replace(/\|/g, "\\|")).join("|");
|
|
543
|
+
}
|
|
544
|
+
// ---------------------------------------------------------------------------
|
|
545
|
+
// Handler
|
|
546
|
+
// ---------------------------------------------------------------------------
|
|
547
|
+
export async function investigate(question, options = {}, deps, context) {
|
|
548
|
+
if (typeof question !== "string" || question.trim().length === 0) {
|
|
549
|
+
throw new ValidationError("investigate requires a question.");
|
|
550
|
+
}
|
|
551
|
+
const sourcesCap = validateOptions(options);
|
|
552
|
+
const noCache = options.noCache === true;
|
|
553
|
+
// 1. Plan (T2) — injected loadContextText; no filesystem here.
|
|
554
|
+
// investigate-verify lane (D3): in verify mode the planner is NOT
|
|
555
|
+
// invoked — the claims ARE the grid (one verbatim sub-query per
|
|
556
|
+
// sentence-claim; no contextFile can be present, the pair is
|
|
557
|
+
// rejected at the index.ts parse seam). Question mode is the
|
|
558
|
+
// untouched else-branch (byte-identity pin).
|
|
559
|
+
let subQueries;
|
|
560
|
+
let verifyClaims;
|
|
561
|
+
if (options.verify === true) {
|
|
562
|
+
verifyClaims = splitClaims(question);
|
|
563
|
+
if (verifyClaims.length === 0) {
|
|
564
|
+
throw new ValidationError("investigate --verify requires at least one valid claim sentence.", "Split the statement into sentences terminated by '.', '!' or '?'.");
|
|
565
|
+
}
|
|
566
|
+
if (verifyClaims.length > MAX_VERIFY_CLAIMS) {
|
|
567
|
+
throw new ValidationError(`investigate --verify exceeds the ${MAX_VERIFY_CLAIMS}-claim cap (${verifyClaims.length} claims).`, "Split fewer claims, or investigate the statement in parts.");
|
|
568
|
+
}
|
|
569
|
+
subQueries = verifyClaims;
|
|
570
|
+
}
|
|
571
|
+
else {
|
|
572
|
+
const plan = await planSubQueries({
|
|
573
|
+
query: question,
|
|
574
|
+
...(options.contextFile !== undefined ? { contextFile: options.contextFile } : {}),
|
|
575
|
+
}, { loadContextText: deps.loadContextText });
|
|
576
|
+
subQueries = [...plan.subQueries];
|
|
577
|
+
// The planner's explicit-tier notice ("--context ignored") only
|
|
578
|
+
// means something when a context file was actually in play; without
|
|
579
|
+
// --context it would be a misleading stderr line on every pipe
|
|
580
|
+
// question.
|
|
581
|
+
if (plan.notice !== undefined && options.contextFile !== undefined) {
|
|
582
|
+
context?.notice(plan.notice);
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
const N = subQueries.length;
|
|
586
|
+
// 2. resolveFanoutPlan over the resolved provider pin (AC-1 tiers,
|
|
587
|
+
// the same inputs handleSearch passes).
|
|
588
|
+
const fanoutPlan = resolveFanoutPlan({
|
|
589
|
+
explicitProviderRaw: options.provider,
|
|
590
|
+
env: deps.env,
|
|
591
|
+
configFanout: deps.configFanout,
|
|
592
|
+
...(deps.routing !== undefined ? { routing: deps.routing } : {}),
|
|
593
|
+
descriptors: deps.descriptors,
|
|
594
|
+
});
|
|
595
|
+
const M = fanoutPlan.arms.length;
|
|
596
|
+
// Cost notice (AC-3; PR #264 F3 wording): the arithmetic stated
|
|
597
|
+
// literally, N/M/K spelled out, BEFORE any billable work runs. K
|
|
598
|
+
// counts SOURCES — the per-read supplier fallthrough can bill more
|
|
599
|
+
// than one reader attempt per source, so the notice names sources
|
|
600
|
+
// and discloses the attempt semantics instead of understating.
|
|
601
|
+
// Verify mode (PRD-3) names CLAIMS — the grid unit is the claim.
|
|
602
|
+
context?.notice(options.verify === true
|
|
603
|
+
? `investigate: ${N} claims × ${M} arms = ${N * M} billable searches + up to ${sourcesCap} sources (per-source supplier attempts apply)`
|
|
604
|
+
: `investigate: ${N} sub-queries × ${M} arms = ${N * M} billable searches + up to ${sourcesCap} sources (per-source supplier attempts apply)`);
|
|
605
|
+
if (fanoutPlan.suppress)
|
|
606
|
+
context?.notice(fanoutPlan.suppress);
|
|
607
|
+
const searchDepsBase = {
|
|
608
|
+
cache: deps.cache,
|
|
609
|
+
sleep: deps.sleep,
|
|
610
|
+
random: deps.random,
|
|
611
|
+
...(deps.retryPolicy !== undefined ? { retryPolicy: deps.retryPolicy } : {}),
|
|
612
|
+
...(deps.consume !== undefined ? { consume: deps.consume } : {}),
|
|
613
|
+
...(deps.now !== undefined ? { now: deps.now } : {}),
|
|
614
|
+
};
|
|
615
|
+
// Search-stage warm-serve count. MUST run BEFORE the grid executes:
|
|
616
|
+
// the grid seeds exactly these keys on a cold run, so a post-grid
|
|
617
|
+
// probe would count its own writes (the recorded trap this ordering
|
|
618
|
+
// exists to avoid). Read and search partitions are disjoint
|
|
619
|
+
// (`reader-reader-fetch` vs `search`), so later reads cannot pollute
|
|
620
|
+
// these probes either.
|
|
621
|
+
const searchHits = await countSearchCacheHits(fanoutPlan, subQueries, deps, noCache);
|
|
622
|
+
// 3. Grid execution through the exported seams only. Both paths emit
|
|
623
|
+
// FormattedResult[] (rank-merged rows); the fan-out path carries
|
|
624
|
+
// mergedFrom provenance, the single path carries rows verbatim.
|
|
625
|
+
let merged;
|
|
626
|
+
let armsUsed;
|
|
627
|
+
let singleArmProviderId;
|
|
628
|
+
if (fanoutPlan.mode === "fanout") {
|
|
629
|
+
const fanoutResult = await executeFanoutPlan(fanoutPlan, {
|
|
630
|
+
descriptors: deps.descriptors,
|
|
631
|
+
env: deps.env,
|
|
632
|
+
query: joinSubQueries(subQueries),
|
|
633
|
+
searchOptions: {
|
|
634
|
+
// The (arm × sub-query) merge grid: merge=true makes every
|
|
635
|
+
// arm run every sub-query — the search seam's own grammar.
|
|
636
|
+
merge: N > 1,
|
|
637
|
+
...(noCache ? { noCache: true } : {}),
|
|
638
|
+
},
|
|
639
|
+
fusionMode: deps.fusionMode ?? "rrf",
|
|
640
|
+
dependencies: searchDepsBase,
|
|
641
|
+
}, context);
|
|
642
|
+
if (fanoutResult.kind !== "data" || !Array.isArray(fanoutResult.data)) {
|
|
643
|
+
throw new Error("investigate: fan-out returned a non-grid result");
|
|
644
|
+
}
|
|
645
|
+
merged = fanoutResult.data;
|
|
646
|
+
armsUsed = M;
|
|
647
|
+
}
|
|
648
|
+
else {
|
|
649
|
+
singleArmProviderId = fanoutPlan.arms[0];
|
|
650
|
+
// Single mode: the exported search() with the escaped-pipe join —
|
|
651
|
+
// the exact context-mode join precedent (index.ts handleSearch).
|
|
652
|
+
// One arm executes every sub-query (search --merge semantics);
|
|
653
|
+
// count stays undefined (search default 10 per AC-3, applied after
|
|
654
|
+
// normalization by shared execution).
|
|
655
|
+
const singleResult = await search(joinSubQueries(subQueries), { merge: N > 1, ...(noCache ? { noCache: true } : {}) }, {
|
|
656
|
+
capability: singleArmCapability(fanoutPlan, deps),
|
|
657
|
+
...searchDepsBase,
|
|
658
|
+
fusionMode: deps.fusionMode ?? "rrf",
|
|
659
|
+
}, context);
|
|
660
|
+
if (singleResult.kind !== "data" || !Array.isArray(singleResult.data)) {
|
|
661
|
+
throw new Error("investigate: single-arm search returned a non-grid result");
|
|
662
|
+
}
|
|
663
|
+
merged = singleResult.data;
|
|
664
|
+
armsUsed = 1;
|
|
665
|
+
}
|
|
666
|
+
// 4. Top --sources distinct sources (post-cluster representatives —
|
|
667
|
+
// mergeResults already collapsed near-dups; a pair IS one row).
|
|
668
|
+
const sourcesConsidered = merged.length;
|
|
669
|
+
const selected = merged.slice(0, sourcesCap);
|
|
670
|
+
// 5. Reads — bounded-concurrency pool over the reader capability
|
|
671
|
+
// seam; per-source terminal failures continue the pool; suppliers
|
|
672
|
+
// fall through in registry order (review fix #2).
|
|
673
|
+
const suppliers = selectReaderSuppliers(deps);
|
|
674
|
+
const readerHits = { count: 0 };
|
|
675
|
+
const outcomes = await readPool(selected, suppliers, deps, noCache, readerHits);
|
|
676
|
+
const byUrl = new Map(outcomes.map((o) => [o.url, o]));
|
|
677
|
+
// 6 + 7. Extraction (T3) + pack assembly (T1 types). Terms = union of
|
|
678
|
+
// the question + sub-queries key-term derivations (deriveTemplateTopic
|
|
679
|
+
// per member — the T2-exposed term shape), case-folded and
|
|
680
|
+
// stopword-filtered by normalizeTerms inside extractPassages.
|
|
681
|
+
const terms = [
|
|
682
|
+
...new Set([question, ...subQueries].flatMap((q) => deriveTemplateTopic(q).split(" "))),
|
|
683
|
+
].filter((t) => t.length > 0);
|
|
684
|
+
const nowWall = deps.nowWall ?? (() => new Date());
|
|
685
|
+
const sources = [];
|
|
686
|
+
const unread = [];
|
|
687
|
+
for (const row of selected) {
|
|
688
|
+
const outcome = byUrl.get(row.url);
|
|
689
|
+
if (outcome === undefined || outcome.result === undefined) {
|
|
690
|
+
unread.push({ url: row.url, reason: outcome?.reason ?? "no-reader-supplier" });
|
|
691
|
+
continue;
|
|
692
|
+
}
|
|
693
|
+
const result = outcome.result;
|
|
694
|
+
// Provider provenance: the merged row's surfaced provider — first
|
|
695
|
+
// mergedFrom (fan-out) or the resolved arm (single mode). NOT the
|
|
696
|
+
// reader supplier: the row says who FOUND it, not who read it.
|
|
697
|
+
const surfacedProvider = row.mergedFrom?.[0] ?? singleArmProviderId ?? suppliers[0]?.id ?? "";
|
|
698
|
+
sources.push({
|
|
699
|
+
url: row.url,
|
|
700
|
+
finalUrl: result.finalUrl,
|
|
701
|
+
title: result.title,
|
|
702
|
+
fetchedAt: nowWall().toISOString(),
|
|
703
|
+
provider: surfacedProvider,
|
|
704
|
+
contentFormat: result.contentFormat,
|
|
705
|
+
contentSha256: createHash("sha256").update(result.content, "utf8").digest("hex"),
|
|
706
|
+
passages: extractPassages({ content: result.content, terms }),
|
|
707
|
+
});
|
|
708
|
+
}
|
|
709
|
+
const pack = {
|
|
710
|
+
schemaVersion: 1,
|
|
711
|
+
question,
|
|
712
|
+
subQueries,
|
|
713
|
+
sources,
|
|
714
|
+
coverage: {
|
|
715
|
+
subQueries: N,
|
|
716
|
+
armsUsed,
|
|
717
|
+
sourcesConsidered,
|
|
718
|
+
sourcesRead: sources.length,
|
|
719
|
+
cacheHits: searchHits + readerHits.count,
|
|
720
|
+
unread,
|
|
721
|
+
},
|
|
722
|
+
// investigate-verify lane (D3 step 3): the ONLY assembly
|
|
723
|
+
// difference — matchClaimsToEvidence over the read sources.
|
|
724
|
+
// Absent on question-mode packs by construction (the fork above).
|
|
725
|
+
...(verifyClaims !== undefined
|
|
726
|
+
? { verify: matchClaimsToEvidence({ statement: question, claims: verifyClaims, sources }) }
|
|
727
|
+
: {}),
|
|
728
|
+
};
|
|
729
|
+
// 8. `--synthesize` (T7): the pack is COMPLETE above — every search,
|
|
730
|
+
// every read, every passage — so the brief is attached here by
|
|
731
|
+
// construction and can only ever ADD a trailing key. A throwing
|
|
732
|
+
// dep propagates as this invocation's terminal error (house error
|
|
733
|
+
// contract) and the assembled pack is NOT emitted: a flag-bearing
|
|
734
|
+
// failure is loud, never a silent degradation to agent-synthesis.
|
|
735
|
+
let payload = pack;
|
|
736
|
+
if (options.synthesize === true) {
|
|
737
|
+
if (deps.synthesize === undefined) {
|
|
738
|
+
// Wiring bug, not a user error: the flag is documented and
|
|
739
|
+
// parsed, so a missing dep means the handler seam forgot to
|
|
740
|
+
// inject the transport. Fail loud — never silently skip.
|
|
741
|
+
throw new Error("investigate: --synthesize was requested but no synthesis dep is wired");
|
|
742
|
+
}
|
|
743
|
+
const quotes = [];
|
|
744
|
+
for (const source of sources) {
|
|
745
|
+
for (const passage of source.passages) {
|
|
746
|
+
if (quotes.length >= SYNTHESIS_QUOTE_CAP)
|
|
747
|
+
break;
|
|
748
|
+
quotes.push(passage.quote);
|
|
749
|
+
}
|
|
750
|
+
if (quotes.length >= SYNTHESIS_QUOTE_CAP)
|
|
751
|
+
break;
|
|
752
|
+
}
|
|
753
|
+
const brief = await deps.synthesize({ question, subQueries, quotes });
|
|
754
|
+
payload = { ...pack, brief };
|
|
755
|
+
}
|
|
756
|
+
// 9. Output Budget (T5, D7): the pack is fully assembled first — the
|
|
757
|
+
// compaction artifact is the FULL untrimmed pack, so the budget
|
|
758
|
+
// rides AFTER assembly by construction.
|
|
759
|
+
const investigateArgs = {
|
|
760
|
+
...(options.provider !== undefined ? { provider: options.provider } : {}),
|
|
761
|
+
...(options.sources !== undefined ? { sources: options.sources } : {}),
|
|
762
|
+
...(noCache ? { "no-cache": true } : {}),
|
|
763
|
+
...(options.isolated ? { isolated: true } : {}),
|
|
764
|
+
};
|
|
765
|
+
return applyInvestigateOutputBudget({ kind: "data", data: payload }, options.maxChars, {
|
|
766
|
+
context: context ?? { stdinIsTTY: false, readStdin: async () => "", notice: () => { } },
|
|
767
|
+
deps,
|
|
768
|
+
args: investigateArgs,
|
|
769
|
+
providerRouting: {
|
|
770
|
+
mode: fanoutPlan.mode,
|
|
771
|
+
...(fanoutPlan.mode === "fanout"
|
|
772
|
+
? { arms: fanoutPlan.arms }
|
|
773
|
+
: { effective: singleArmProviderId ?? "" }),
|
|
774
|
+
...(options.provider !== undefined ? { requested: options.provider } : {}),
|
|
775
|
+
},
|
|
776
|
+
});
|
|
777
|
+
}
|
|
778
|
+
/**
|
|
779
|
+
* Resolve the single-arm search capability. The resolver's single mode
|
|
780
|
+
* always names the arm (`arms[0]`); a descriptor MUST exist for it —
|
|
781
|
+
* an unknown id means the pin never matched the registry, which is
|
|
782
|
+
* the capability seam's typed error to raise.
|
|
783
|
+
*/
|
|
784
|
+
function singleArmCapability(fanoutPlan, deps) {
|
|
785
|
+
const armId = fanoutPlan.arms[0];
|
|
786
|
+
const descriptor = deps.descriptors.find((d) => d.id === armId);
|
|
787
|
+
if (descriptor === undefined) {
|
|
788
|
+
throw new UnsupportedCapabilityError(String(armId ?? "(none)"), "search");
|
|
789
|
+
}
|
|
790
|
+
const adapter = descriptor.create({ env: deps.env });
|
|
791
|
+
const capability = adapter.search;
|
|
792
|
+
if (capability === undefined) {
|
|
793
|
+
throw new UnsupportedCapabilityError(descriptor.id, "search");
|
|
794
|
+
}
|
|
795
|
+
return capability;
|
|
796
|
+
}
|
|
797
|
+
/**
|
|
798
|
+
* Search-stage warm-serve count. Mirrors the read-stage boundary: a
|
|
799
|
+
* warm serve is a cache get() on the arm's exact partition key —
|
|
800
|
+
* resolved through the injected descriptors' own cacheIdentity, never
|
|
801
|
+
* a fabricated key — consulted once per (arm × sub-query) BEFORE the
|
|
802
|
+
* grid runs (see the ordering note at the call site). executeSearch
|
|
803
|
+
* treats any non-null raw value on this key as a hit (no decoder), so
|
|
804
|
+
* the probe matches: non-null = warm.
|
|
805
|
+
*/
|
|
806
|
+
async function countSearchCacheHits(fanoutPlan, subQueries, deps, noCache) {
|
|
807
|
+
if (noCache)
|
|
808
|
+
return 0;
|
|
809
|
+
const arms = fanoutPlan.mode === "fanout" ? fanoutPlan.arms : fanoutPlan.arms.slice(0, 1);
|
|
810
|
+
let hits = 0;
|
|
811
|
+
for (const armId of arms) {
|
|
812
|
+
const descriptor = deps.descriptors.find((d) => d.id === armId);
|
|
813
|
+
if (descriptor === undefined)
|
|
814
|
+
continue;
|
|
815
|
+
const capability = descriptor.create({ env: deps.env }).search;
|
|
816
|
+
if (capability === undefined)
|
|
817
|
+
continue;
|
|
818
|
+
for (const query of subQueries) {
|
|
819
|
+
try {
|
|
820
|
+
const identity = capability.cacheIdentity({ query });
|
|
821
|
+
const key = buildProviderCacheKey({
|
|
822
|
+
provider: identity.provider,
|
|
823
|
+
capability: identity.capability,
|
|
824
|
+
credentialFingerprint: identity.credentialFingerprint,
|
|
825
|
+
request: identity.request,
|
|
826
|
+
});
|
|
827
|
+
if ((await deps.cache.get(key)) !== null)
|
|
828
|
+
hits += 1;
|
|
829
|
+
}
|
|
830
|
+
catch {
|
|
831
|
+
// A capability whose cacheIdentity cannot be probed counts
|
|
832
|
+
// nothing — never guess a hit.
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
return hits;
|
|
837
|
+
}
|
|
838
|
+
//# sourceMappingURL=investigate.js.map
|