@maintainabilityai/research-runner 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +82 -0
- package/bin/research-runner.js +2 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +209 -0
- package/dist/llm/anthropic-client.d.ts +39 -0
- package/dist/llm/anthropic-client.js +74 -0
- package/dist/llm/github-models-client.d.ts +46 -0
- package/dist/llm/github-models-client.js +78 -0
- package/dist/llm/llm-router.d.ts +46 -0
- package/dist/llm/llm-router.js +60 -0
- package/dist/mesh/get-mesh-sha.d.ts +1 -0
- package/dist/mesh/get-mesh-sha.js +27 -0
- package/dist/mesh/mesh-reader.d.ts +14 -0
- package/dist/mesh/mesh-reader.js +392 -0
- package/dist/mesh/prompt-loader.d.ts +22 -0
- package/dist/mesh/prompt-loader.js +119 -0
- package/dist/mesh/threat-model-reader.d.ts +33 -0
- package/dist/mesh/threat-model-reader.js +123 -0
- package/dist/runner/archeologist.d.ts +39 -0
- package/dist/runner/archeologist.js +620 -0
- package/dist/runner/audit-emitter.d.ts +62 -0
- package/dist/runner/audit-emitter.js +210 -0
- package/dist/runner/hatters-tag-builder.d.ts +52 -0
- package/dist/runner/hatters-tag-builder.js +40 -0
- package/dist/runner/nodes/analyze-architecture.d.ts +10 -0
- package/dist/runner/nodes/analyze-architecture.js +447 -0
- package/dist/runner/nodes/arxiv-search.d.ts +12 -0
- package/dist/runner/nodes/arxiv-search.js +52 -0
- package/dist/runner/nodes/clone-and-index.d.ts +32 -0
- package/dist/runner/nodes/clone-and-index.js +158 -0
- package/dist/runner/nodes/dedupe-and-rank.d.ts +27 -0
- package/dist/runner/nodes/dedupe-and-rank.js +98 -0
- package/dist/runner/nodes/deterministic-review.d.ts +55 -0
- package/dist/runner/nodes/deterministic-review.js +206 -0
- package/dist/runner/nodes/expert-review.d.ts +68 -0
- package/dist/runner/nodes/expert-review.js +197 -0
- package/dist/runner/nodes/gap-analysis.d.ts +48 -0
- package/dist/runner/nodes/gap-analysis.js +153 -0
- package/dist/runner/nodes/generate-prd-manifest.d.ts +53 -0
- package/dist/runner/nodes/generate-prd-manifest.js +209 -0
- package/dist/runner/nodes/hackernews-search.d.ts +12 -0
- package/dist/runner/nodes/hackernews-search.js +63 -0
- package/dist/runner/nodes/identify-gaps.d.ts +33 -0
- package/dist/runner/nodes/identify-gaps.js +185 -0
- package/dist/runner/nodes/plan-queries.d.ts +28 -0
- package/dist/runner/nodes/plan-queries.js +120 -0
- package/dist/runner/nodes/prd-validator.d.ts +51 -0
- package/dist/runner/nodes/prd-validator.js +203 -0
- package/dist/runner/nodes/synthesis-archaeology-validator.d.ts +22 -0
- package/dist/runner/nodes/synthesis-archaeology-validator.js +131 -0
- package/dist/runner/nodes/synthesis-validator.d.ts +51 -0
- package/dist/runner/nodes/synthesis-validator.js +185 -0
- package/dist/runner/nodes/synthesize-prd.d.ts +84 -0
- package/dist/runner/nodes/synthesize-prd.js +202 -0
- package/dist/runner/nodes/synthesize-report.d.ts +53 -0
- package/dist/runner/nodes/synthesize-report.js +188 -0
- package/dist/runner/nodes/tavily-search.d.ts +21 -0
- package/dist/runner/nodes/tavily-search.js +57 -0
- package/dist/runner/nodes/uspto-search.d.ts +13 -0
- package/dist/runner/nodes/uspto-search.js +62 -0
- package/dist/runner/nodes/verify-grounding.d.ts +54 -0
- package/dist/runner/nodes/verify-grounding.js +134 -0
- package/dist/runner/prd.d.ts +28 -0
- package/dist/runner/prd.js +494 -0
- package/dist/schemas/audit-event.d.ts +1151 -0
- package/dist/schemas/audit-event.js +141 -0
- package/dist/schemas/index.d.ts +17 -0
- package/dist/schemas/index.js +33 -0
- package/dist/schemas/mesh-context.d.ts +415 -0
- package/dist/schemas/mesh-context.js +95 -0
- package/dist/schemas/observed-architecture.d.ts +262 -0
- package/dist/schemas/observed-architecture.js +90 -0
- package/dist/schemas/prd-brief.d.ts +111 -0
- package/dist/schemas/prd-brief.js +37 -0
- package/dist/schemas/prd-doc.d.ts +249 -0
- package/dist/schemas/prd-doc.js +42 -0
- package/dist/schemas/prd-manifest.d.ts +171 -0
- package/dist/schemas/prd-manifest.js +73 -0
- package/dist/schemas/primitives.d.ts +47 -0
- package/dist/schemas/primitives.js +41 -0
- package/dist/schemas/query-plan.d.ts +33 -0
- package/dist/schemas/query-plan.js +25 -0
- package/dist/schemas/ranked-source.d.ts +82 -0
- package/dist/schemas/ranked-source.js +29 -0
- package/dist/schemas/research-brief.d.ts +114 -0
- package/dist/schemas/research-brief.js +49 -0
- package/dist/schemas/research-doc.d.ts +104 -0
- package/dist/schemas/research-doc.js +37 -0
- package/dist/search/arxiv-client.d.ts +41 -0
- package/dist/search/arxiv-client.js +88 -0
- package/dist/search/hackernews-client.d.ts +33 -0
- package/dist/search/hackernews-client.js +44 -0
- package/dist/search/provider-result.d.ts +25 -0
- package/dist/search/provider-result.js +2 -0
- package/dist/search/tavily-client.d.ts +38 -0
- package/dist/search/tavily-client.js +53 -0
- package/dist/search/uspto-client.d.ts +50 -0
- package/dist/search/uspto-client.js +112 -0
- package/dist/utils/run-id.d.ts +2 -0
- package/dist/utils/run-id.js +22 -0
- package/package.json +53 -0
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RankedSource — normalised search result shape after dedupe_and_rank.
|
|
3
|
+
*
|
|
4
|
+
* Each provider (Tavily / arXiv / USPTO / HackerNews) returns its own native
|
|
5
|
+
* payload; the dedupe node converts them all to this common shape with a
|
|
6
|
+
* salience score (0.0-1.0) used by synthesize to choose what to cite.
|
|
7
|
+
*/
|
|
8
|
+
import { z } from 'zod';
|
|
9
|
+
export declare const RankedSource: z.ZodObject<{
|
|
10
|
+
/** Stable id used by the synthesis prompt as `S[N]` citation. */
|
|
11
|
+
id: z.ZodString;
|
|
12
|
+
provider: z.ZodEnum<["tavily", "arxiv", "uspto", "hackernews"]>;
|
|
13
|
+
title: z.ZodString;
|
|
14
|
+
url: z.ZodString;
|
|
15
|
+
retrieved_at: z.ZodEffects<z.ZodString, string, string>;
|
|
16
|
+
/** 0.0 - 1.0, higher = more relevant. Computed by dedupe_and_rank. */
|
|
17
|
+
salience_score: z.ZodNumber;
|
|
18
|
+
/** ≤500-char excerpt the synthesis node may quote directly. */
|
|
19
|
+
excerpt: z.ZodString;
|
|
20
|
+
/** Optional: pub date if the source has one (papers, news, patents). */
|
|
21
|
+
published_at: z.ZodOptional<z.ZodEffects<z.ZodString, string, string>>;
|
|
22
|
+
/** Optional: authors (arxiv / news). */
|
|
23
|
+
authors: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
|
|
24
|
+
}, "strip", z.ZodTypeAny, {
|
|
25
|
+
id: string;
|
|
26
|
+
title: string;
|
|
27
|
+
provider: "tavily" | "arxiv" | "uspto" | "hackernews";
|
|
28
|
+
url: string;
|
|
29
|
+
retrieved_at: string;
|
|
30
|
+
salience_score: number;
|
|
31
|
+
excerpt: string;
|
|
32
|
+
published_at?: string | undefined;
|
|
33
|
+
authors?: string[] | undefined;
|
|
34
|
+
}, {
|
|
35
|
+
id: string;
|
|
36
|
+
title: string;
|
|
37
|
+
provider: "tavily" | "arxiv" | "uspto" | "hackernews";
|
|
38
|
+
url: string;
|
|
39
|
+
retrieved_at: string;
|
|
40
|
+
salience_score: number;
|
|
41
|
+
excerpt: string;
|
|
42
|
+
published_at?: string | undefined;
|
|
43
|
+
authors?: string[] | undefined;
|
|
44
|
+
}>;
|
|
45
|
+
export type RankedSource = z.infer<typeof RankedSource>;
|
|
46
|
+
export declare const RankedSourceList: z.ZodArray<z.ZodObject<{
|
|
47
|
+
/** Stable id used by the synthesis prompt as `S[N]` citation. */
|
|
48
|
+
id: z.ZodString;
|
|
49
|
+
provider: z.ZodEnum<["tavily", "arxiv", "uspto", "hackernews"]>;
|
|
50
|
+
title: z.ZodString;
|
|
51
|
+
url: z.ZodString;
|
|
52
|
+
retrieved_at: z.ZodEffects<z.ZodString, string, string>;
|
|
53
|
+
/** 0.0 - 1.0, higher = more relevant. Computed by dedupe_and_rank. */
|
|
54
|
+
salience_score: z.ZodNumber;
|
|
55
|
+
/** ≤500-char excerpt the synthesis node may quote directly. */
|
|
56
|
+
excerpt: z.ZodString;
|
|
57
|
+
/** Optional: pub date if the source has one (papers, news, patents). */
|
|
58
|
+
published_at: z.ZodOptional<z.ZodEffects<z.ZodString, string, string>>;
|
|
59
|
+
/** Optional: authors (arxiv / news). */
|
|
60
|
+
authors: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
|
|
61
|
+
}, "strip", z.ZodTypeAny, {
|
|
62
|
+
id: string;
|
|
63
|
+
title: string;
|
|
64
|
+
provider: "tavily" | "arxiv" | "uspto" | "hackernews";
|
|
65
|
+
url: string;
|
|
66
|
+
retrieved_at: string;
|
|
67
|
+
salience_score: number;
|
|
68
|
+
excerpt: string;
|
|
69
|
+
published_at?: string | undefined;
|
|
70
|
+
authors?: string[] | undefined;
|
|
71
|
+
}, {
|
|
72
|
+
id: string;
|
|
73
|
+
title: string;
|
|
74
|
+
provider: "tavily" | "arxiv" | "uspto" | "hackernews";
|
|
75
|
+
url: string;
|
|
76
|
+
retrieved_at: string;
|
|
77
|
+
salience_score: number;
|
|
78
|
+
excerpt: string;
|
|
79
|
+
published_at?: string | undefined;
|
|
80
|
+
authors?: string[] | undefined;
|
|
81
|
+
}>, "many">;
|
|
82
|
+
export type RankedSourceList = z.infer<typeof RankedSourceList>;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.RankedSourceList = exports.RankedSource = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* RankedSource — normalised search result shape after dedupe_and_rank.
|
|
6
|
+
*
|
|
7
|
+
* Each provider (Tavily / arXiv / USPTO / HackerNews) returns its own native
|
|
8
|
+
* payload; the dedupe node converts them all to this common shape with a
|
|
9
|
+
* salience score (0.0-1.0) used by synthesize to choose what to cite.
|
|
10
|
+
*/
|
|
11
|
+
const zod_1 = require("zod");
|
|
12
|
+
const primitives_1 = require("./primitives");
|
|
13
|
+
exports.RankedSource = zod_1.z.object({
|
|
14
|
+
/** Stable id used by the synthesis prompt as `S[N]` citation. */
|
|
15
|
+
id: zod_1.z.string().regex(/^S\d+$/),
|
|
16
|
+
provider: primitives_1.SearchProvider,
|
|
17
|
+
title: zod_1.z.string().min(1),
|
|
18
|
+
url: zod_1.z.string().url(),
|
|
19
|
+
retrieved_at: primitives_1.IsoTimestamp,
|
|
20
|
+
/** 0.0 - 1.0, higher = more relevant. Computed by dedupe_and_rank. */
|
|
21
|
+
salience_score: zod_1.z.number().min(0).max(1),
|
|
22
|
+
/** ≤500-char excerpt the synthesis node may quote directly. */
|
|
23
|
+
excerpt: zod_1.z.string().max(500),
|
|
24
|
+
/** Optional: pub date if the source has one (papers, news, patents). */
|
|
25
|
+
published_at: primitives_1.IsoTimestamp.optional(),
|
|
26
|
+
/** Optional: authors (arxiv / news). */
|
|
27
|
+
authors: zod_1.z.array(zod_1.z.string()).optional(),
|
|
28
|
+
});
|
|
29
|
+
exports.RankedSourceList = zod_1.z.array(exports.RankedSource);
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ResearchBrief — validated input to the Archeologist pipeline.
|
|
3
|
+
*
|
|
4
|
+
* Produced by `validate_brief` (the pure first node) from CLI args / issue
|
|
5
|
+
* body / workflow_dispatch inputs / Looking Glass form. Every downstream
|
|
6
|
+
* node receives this object.
|
|
7
|
+
*/
|
|
8
|
+
import { z } from 'zod';
|
|
9
|
+
export declare const ResearchBrief: z.ZodEffects<z.ZodObject<{
|
|
10
|
+
/** Plain-English research request (the topic). */
|
|
11
|
+
topic: z.ZodString;
|
|
12
|
+
scope: z.ZodObject<{
|
|
13
|
+
level: z.ZodEnum<["platform", "bar"]>;
|
|
14
|
+
/** Required: platform slug (e.g. `imdb-lite`) or BAR id (e.g. `APP-IMDB-002`). */
|
|
15
|
+
id: z.ZodString;
|
|
16
|
+
}, "strip", z.ZodTypeAny, {
|
|
17
|
+
level: "platform" | "bar";
|
|
18
|
+
id: string;
|
|
19
|
+
}, {
|
|
20
|
+
level: "platform" | "bar";
|
|
21
|
+
id: string;
|
|
22
|
+
}>;
|
|
23
|
+
/** `research` = market research; `archaeology` = codebase analysis path. */
|
|
24
|
+
path: z.ZodDefault<z.ZodEnum<["research", "archaeology"]>>;
|
|
25
|
+
/** Archaeology path only: `owner/repo` of the codebase to analyze. */
|
|
26
|
+
target_repo: z.ZodOptional<z.ZodString>;
|
|
27
|
+
/** Guardrail mode applied to LLM nodes. */
|
|
28
|
+
guardrails: z.ZodDefault<z.ZodEnum<["strict", "default", "lenient"]>>;
|
|
29
|
+
/** LLM provider for the synthesis + planning nodes. */
|
|
30
|
+
llm_provider: z.ZodDefault<z.ZodEnum<["anthropic", "openai", "azure-openai", "github-models"]>>;
|
|
31
|
+
/** Token budget cap (warn before exceeding). */
|
|
32
|
+
cost_cap_tokens: z.ZodDefault<z.ZodNumber>;
|
|
33
|
+
/** Caller-supplied trigger context — flows into the audit log envelope. */
|
|
34
|
+
trigger: z.ZodObject<{
|
|
35
|
+
kind: z.ZodEnum<["workflow_dispatch", "issue_label", "issue_comment", "project_card", "local_dev"]>;
|
|
36
|
+
/** Issue number when triggered by issue / comment events. */
|
|
37
|
+
issue_number: z.ZodOptional<z.ZodNumber>;
|
|
38
|
+
/** Actor login when known (GitHub Actions sets this). */
|
|
39
|
+
actor: z.ZodOptional<z.ZodString>;
|
|
40
|
+
}, "strip", z.ZodTypeAny, {
|
|
41
|
+
kind: "workflow_dispatch" | "issue_label" | "issue_comment" | "project_card" | "local_dev";
|
|
42
|
+
issue_number?: number | undefined;
|
|
43
|
+
actor?: string | undefined;
|
|
44
|
+
}, {
|
|
45
|
+
kind: "workflow_dispatch" | "issue_label" | "issue_comment" | "project_card" | "local_dev";
|
|
46
|
+
issue_number?: number | undefined;
|
|
47
|
+
actor?: string | undefined;
|
|
48
|
+
}>;
|
|
49
|
+
}, "strip", z.ZodTypeAny, {
|
|
50
|
+
path: "research" | "archaeology";
|
|
51
|
+
topic: string;
|
|
52
|
+
scope: {
|
|
53
|
+
level: "platform" | "bar";
|
|
54
|
+
id: string;
|
|
55
|
+
};
|
|
56
|
+
guardrails: "strict" | "default" | "lenient";
|
|
57
|
+
llm_provider: "anthropic" | "openai" | "azure-openai" | "github-models";
|
|
58
|
+
cost_cap_tokens: number;
|
|
59
|
+
trigger: {
|
|
60
|
+
kind: "workflow_dispatch" | "issue_label" | "issue_comment" | "project_card" | "local_dev";
|
|
61
|
+
issue_number?: number | undefined;
|
|
62
|
+
actor?: string | undefined;
|
|
63
|
+
};
|
|
64
|
+
target_repo?: string | undefined;
|
|
65
|
+
}, {
|
|
66
|
+
topic: string;
|
|
67
|
+
scope: {
|
|
68
|
+
level: "platform" | "bar";
|
|
69
|
+
id: string;
|
|
70
|
+
};
|
|
71
|
+
trigger: {
|
|
72
|
+
kind: "workflow_dispatch" | "issue_label" | "issue_comment" | "project_card" | "local_dev";
|
|
73
|
+
issue_number?: number | undefined;
|
|
74
|
+
actor?: string | undefined;
|
|
75
|
+
};
|
|
76
|
+
path?: "research" | "archaeology" | undefined;
|
|
77
|
+
target_repo?: string | undefined;
|
|
78
|
+
guardrails?: "strict" | "default" | "lenient" | undefined;
|
|
79
|
+
llm_provider?: "anthropic" | "openai" | "azure-openai" | "github-models" | undefined;
|
|
80
|
+
cost_cap_tokens?: number | undefined;
|
|
81
|
+
}>, {
|
|
82
|
+
path: "research" | "archaeology";
|
|
83
|
+
topic: string;
|
|
84
|
+
scope: {
|
|
85
|
+
level: "platform" | "bar";
|
|
86
|
+
id: string;
|
|
87
|
+
};
|
|
88
|
+
guardrails: "strict" | "default" | "lenient";
|
|
89
|
+
llm_provider: "anthropic" | "openai" | "azure-openai" | "github-models";
|
|
90
|
+
cost_cap_tokens: number;
|
|
91
|
+
trigger: {
|
|
92
|
+
kind: "workflow_dispatch" | "issue_label" | "issue_comment" | "project_card" | "local_dev";
|
|
93
|
+
issue_number?: number | undefined;
|
|
94
|
+
actor?: string | undefined;
|
|
95
|
+
};
|
|
96
|
+
target_repo?: string | undefined;
|
|
97
|
+
}, {
|
|
98
|
+
topic: string;
|
|
99
|
+
scope: {
|
|
100
|
+
level: "platform" | "bar";
|
|
101
|
+
id: string;
|
|
102
|
+
};
|
|
103
|
+
trigger: {
|
|
104
|
+
kind: "workflow_dispatch" | "issue_label" | "issue_comment" | "project_card" | "local_dev";
|
|
105
|
+
issue_number?: number | undefined;
|
|
106
|
+
actor?: string | undefined;
|
|
107
|
+
};
|
|
108
|
+
path?: "research" | "archaeology" | undefined;
|
|
109
|
+
target_repo?: string | undefined;
|
|
110
|
+
guardrails?: "strict" | "default" | "lenient" | undefined;
|
|
111
|
+
llm_provider?: "anthropic" | "openai" | "azure-openai" | "github-models" | undefined;
|
|
112
|
+
cost_cap_tokens?: number | undefined;
|
|
113
|
+
}>;
|
|
114
|
+
export type ResearchBrief = z.infer<typeof ResearchBrief>;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ResearchBrief = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* ResearchBrief — validated input to the Archeologist pipeline.
|
|
6
|
+
*
|
|
7
|
+
* Produced by `validate_brief` (the pure first node) from CLI args / issue
|
|
8
|
+
* body / workflow_dispatch inputs / Looking Glass form. Every downstream
|
|
9
|
+
* node receives this object.
|
|
10
|
+
*/
|
|
11
|
+
const zod_1 = require("zod");
|
|
12
|
+
const primitives_1 = require("./primitives");
|
|
13
|
+
exports.ResearchBrief = zod_1.z.object({
|
|
14
|
+
/** Plain-English research request (the topic). */
|
|
15
|
+
topic: zod_1.z.string().min(3).max(2000),
|
|
16
|
+
scope: zod_1.z.object({
|
|
17
|
+
level: primitives_1.ScopeLevel,
|
|
18
|
+
/** Required: platform slug (e.g. `imdb-lite`) or BAR id (e.g. `APP-IMDB-002`). */
|
|
19
|
+
id: zod_1.z.string().min(1),
|
|
20
|
+
}),
|
|
21
|
+
/** `research` = market research; `archaeology` = codebase analysis path. */
|
|
22
|
+
path: primitives_1.ResearchPath.default('research'),
|
|
23
|
+
/** Archaeology path only: `owner/repo` of the codebase to analyze. */
|
|
24
|
+
target_repo: zod_1.z.string().regex(/^[\w.-]+\/[\w.-]+$/).optional(),
|
|
25
|
+
/** Guardrail mode applied to LLM nodes. */
|
|
26
|
+
guardrails: primitives_1.GuardrailMode.default('default'),
|
|
27
|
+
/** LLM provider for the synthesis + planning nodes. */
|
|
28
|
+
llm_provider: primitives_1.LlmProvider.default('anthropic'),
|
|
29
|
+
/** Token budget cap (warn before exceeding). */
|
|
30
|
+
cost_cap_tokens: zod_1.z.number().int().positive().default(200_000),
|
|
31
|
+
/** Caller-supplied trigger context — flows into the audit log envelope. */
|
|
32
|
+
trigger: zod_1.z.object({
|
|
33
|
+
kind: zod_1.z.enum(['workflow_dispatch', 'issue_label', 'issue_comment', 'project_card', 'local_dev']),
|
|
34
|
+
/** Issue number when triggered by issue / comment events. */
|
|
35
|
+
issue_number: zod_1.z.number().int().optional(),
|
|
36
|
+
/** Actor login when known (GitHub Actions sets this). */
|
|
37
|
+
actor: zod_1.z.string().optional(),
|
|
38
|
+
}),
|
|
39
|
+
}).superRefine((brief, ctx) => {
|
|
40
|
+
// Archaeology runs require a target repo
|
|
41
|
+
if (brief.path === 'archaeology' && !brief.target_repo) {
|
|
42
|
+
ctx.addIssue({
|
|
43
|
+
code: zod_1.z.ZodIssueCode.custom,
|
|
44
|
+
message: 'archaeology path requires target_repo (owner/repo)',
|
|
45
|
+
path: ['target_repo'],
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
// scope.id required-ness is enforced at the field level (min(1)).
|
|
49
|
+
});
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ResearchDoc — the published research artifact (research path).
|
|
3
|
+
*
|
|
4
|
+
* The synthesis LLM produces structured markdown matching the 10 canonical
|
|
5
|
+
* sections defined in `.caterpillar/prompts/research/synthesis.md`. The
|
|
6
|
+
* runner validates section presence + citation rules with a separate
|
|
7
|
+
* structural validator; this schema only enforces the surrounding metadata.
|
|
8
|
+
*/
|
|
9
|
+
import { z } from 'zod';
|
|
10
|
+
/** A formal conclusion line as parsed from the body. Used by the structural validator. */
|
|
11
|
+
export declare const FormalConclusion: z.ZodObject<{
|
|
12
|
+
id: z.ZodString;
|
|
13
|
+
statement: z.ZodString;
|
|
14
|
+
confidence: z.ZodEnum<["HIGH", "MEDIUM", "LOW"]>;
|
|
15
|
+
/** Source premise IDs (S1, S2, …) cited by this conclusion. */
|
|
16
|
+
cited_sources: z.ZodArray<z.ZodString, "many">;
|
|
17
|
+
}, "strip", z.ZodTypeAny, {
|
|
18
|
+
id: string;
|
|
19
|
+
statement: string;
|
|
20
|
+
confidence: "HIGH" | "MEDIUM" | "LOW";
|
|
21
|
+
cited_sources: string[];
|
|
22
|
+
}, {
|
|
23
|
+
id: string;
|
|
24
|
+
statement: string;
|
|
25
|
+
confidence: "HIGH" | "MEDIUM" | "LOW";
|
|
26
|
+
cited_sources: string[];
|
|
27
|
+
}>;
|
|
28
|
+
export declare const ResearchDoc: z.ZodObject<{
|
|
29
|
+
run_id: z.ZodString;
|
|
30
|
+
topic: z.ZodString;
|
|
31
|
+
generated_at: z.ZodEffects<z.ZodString, string, string>;
|
|
32
|
+
/** Final published markdown — the body the auditor + reviewers read. */
|
|
33
|
+
body_md: z.ZodString;
|
|
34
|
+
/** Pre-extracted formal conclusions, used for downstream PRD grounding. */
|
|
35
|
+
conclusions: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
36
|
+
id: z.ZodString;
|
|
37
|
+
statement: z.ZodString;
|
|
38
|
+
confidence: z.ZodEnum<["HIGH", "MEDIUM", "LOW"]>;
|
|
39
|
+
/** Source premise IDs (S1, S2, …) cited by this conclusion. */
|
|
40
|
+
cited_sources: z.ZodArray<z.ZodString, "many">;
|
|
41
|
+
}, "strip", z.ZodTypeAny, {
|
|
42
|
+
id: string;
|
|
43
|
+
statement: string;
|
|
44
|
+
confidence: "HIGH" | "MEDIUM" | "LOW";
|
|
45
|
+
cited_sources: string[];
|
|
46
|
+
}, {
|
|
47
|
+
id: string;
|
|
48
|
+
statement: string;
|
|
49
|
+
confidence: "HIGH" | "MEDIUM" | "LOW";
|
|
50
|
+
cited_sources: string[];
|
|
51
|
+
}>, "many">>;
|
|
52
|
+
/** Citation counts the structural validator reports back. */
|
|
53
|
+
citation_stats: z.ZodObject<{
|
|
54
|
+
source_count: z.ZodNumber;
|
|
55
|
+
conclusion_count: z.ZodNumber;
|
|
56
|
+
recommendation_count: z.ZodNumber;
|
|
57
|
+
untraced_claims: z.ZodNumber;
|
|
58
|
+
}, "strip", z.ZodTypeAny, {
|
|
59
|
+
source_count: number;
|
|
60
|
+
conclusion_count: number;
|
|
61
|
+
recommendation_count: number;
|
|
62
|
+
untraced_claims: number;
|
|
63
|
+
}, {
|
|
64
|
+
source_count: number;
|
|
65
|
+
conclusion_count: number;
|
|
66
|
+
recommendation_count: number;
|
|
67
|
+
untraced_claims: number;
|
|
68
|
+
}>;
|
|
69
|
+
}, "strip", z.ZodTypeAny, {
|
|
70
|
+
topic: string;
|
|
71
|
+
run_id: string;
|
|
72
|
+
generated_at: string;
|
|
73
|
+
body_md: string;
|
|
74
|
+
citation_stats: {
|
|
75
|
+
source_count: number;
|
|
76
|
+
conclusion_count: number;
|
|
77
|
+
recommendation_count: number;
|
|
78
|
+
untraced_claims: number;
|
|
79
|
+
};
|
|
80
|
+
conclusions?: {
|
|
81
|
+
id: string;
|
|
82
|
+
statement: string;
|
|
83
|
+
confidence: "HIGH" | "MEDIUM" | "LOW";
|
|
84
|
+
cited_sources: string[];
|
|
85
|
+
}[] | undefined;
|
|
86
|
+
}, {
|
|
87
|
+
topic: string;
|
|
88
|
+
run_id: string;
|
|
89
|
+
generated_at: string;
|
|
90
|
+
body_md: string;
|
|
91
|
+
citation_stats: {
|
|
92
|
+
source_count: number;
|
|
93
|
+
conclusion_count: number;
|
|
94
|
+
recommendation_count: number;
|
|
95
|
+
untraced_claims: number;
|
|
96
|
+
};
|
|
97
|
+
conclusions?: {
|
|
98
|
+
id: string;
|
|
99
|
+
statement: string;
|
|
100
|
+
confidence: "HIGH" | "MEDIUM" | "LOW";
|
|
101
|
+
cited_sources: string[];
|
|
102
|
+
}[] | undefined;
|
|
103
|
+
}>;
|
|
104
|
+
export type ResearchDoc = z.infer<typeof ResearchDoc>;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ResearchDoc = exports.FormalConclusion = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* ResearchDoc — the published research artifact (research path).
|
|
6
|
+
*
|
|
7
|
+
* The synthesis LLM produces structured markdown matching the 10 canonical
|
|
8
|
+
* sections defined in `.caterpillar/prompts/research/synthesis.md`. The
|
|
9
|
+
* runner validates section presence + citation rules with a separate
|
|
10
|
+
* structural validator; this schema only enforces the surrounding metadata.
|
|
11
|
+
*/
|
|
12
|
+
const zod_1 = require("zod");
|
|
13
|
+
const primitives_1 = require("./primitives");
|
|
14
|
+
/** A formal conclusion line as parsed from the body. Used by the structural validator. */
|
|
15
|
+
exports.FormalConclusion = zod_1.z.object({
|
|
16
|
+
id: zod_1.z.string().regex(/^C\d+$/),
|
|
17
|
+
statement: zod_1.z.string(),
|
|
18
|
+
confidence: primitives_1.Confidence,
|
|
19
|
+
/** Source premise IDs (S1, S2, …) cited by this conclusion. */
|
|
20
|
+
cited_sources: zod_1.z.array(zod_1.z.string().regex(/^S\d+$/)).min(1),
|
|
21
|
+
});
|
|
22
|
+
exports.ResearchDoc = zod_1.z.object({
|
|
23
|
+
run_id: primitives_1.RunId,
|
|
24
|
+
topic: zod_1.z.string(),
|
|
25
|
+
generated_at: primitives_1.IsoTimestamp,
|
|
26
|
+
/** Final published markdown — the body the auditor + reviewers read. */
|
|
27
|
+
body_md: zod_1.z.string().min(1),
|
|
28
|
+
/** Pre-extracted formal conclusions, used for downstream PRD grounding. */
|
|
29
|
+
conclusions: zod_1.z.array(exports.FormalConclusion).optional(),
|
|
30
|
+
/** Citation counts the structural validator reports back. */
|
|
31
|
+
citation_stats: zod_1.z.object({
|
|
32
|
+
source_count: zod_1.z.number().int().nonnegative(),
|
|
33
|
+
conclusion_count: zod_1.z.number().int().nonnegative(),
|
|
34
|
+
recommendation_count: zod_1.z.number().int().nonnegative(),
|
|
35
|
+
untraced_claims: zod_1.z.number().int().nonnegative(),
|
|
36
|
+
}),
|
|
37
|
+
});
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* arxiv-client — query the arXiv Atom API.
|
|
3
|
+
*
|
|
4
|
+
* GET http://export.arxiv.org/api/query?search_query=<q>&max_results=N
|
|
5
|
+
*
|
|
6
|
+
* No auth required (arXiv rate-limits by IP — be polite, run queries
|
|
7
|
+
* sequentially or with small parallelism). Response is Atom XML.
|
|
8
|
+
*
|
|
9
|
+
* We parse with a tiny purpose-built reader instead of pulling in an XML
|
|
10
|
+
* dependency. The arXiv schema is stable and the fields we need (title,
|
|
11
|
+
* summary, id/url, published, authors) are well-known.
|
|
12
|
+
*/
|
|
13
|
+
export interface ArxivResult {
|
|
14
|
+
id: string;
|
|
15
|
+
title: string;
|
|
16
|
+
summary: string;
|
|
17
|
+
abstractUrl: string;
|
|
18
|
+
published: string;
|
|
19
|
+
authors: string[];
|
|
20
|
+
}
|
|
21
|
+
export interface ArxivSearchOpts {
|
|
22
|
+
query: string;
|
|
23
|
+
/** 1..30. arXiv accepts more but 5 is a sensible default for research. */
|
|
24
|
+
maxResults?: number;
|
|
25
|
+
fetchImpl?: typeof fetch;
|
|
26
|
+
timeoutMs?: number;
|
|
27
|
+
/** Override base URL for tests. */
|
|
28
|
+
endpoint?: string;
|
|
29
|
+
}
|
|
30
|
+
export interface ArxivSearchResult {
|
|
31
|
+
query: string;
|
|
32
|
+
results: ArxivResult[];
|
|
33
|
+
responseBytes: number;
|
|
34
|
+
httpStatus: number;
|
|
35
|
+
}
|
|
36
|
+
export declare function arxivSearch(opts: ArxivSearchOpts): Promise<ArxivSearchResult>;
|
|
37
|
+
/**
|
|
38
|
+
* Tiny Atom-XML reader for arXiv responses. Tolerant of CDATA + whitespace.
|
|
39
|
+
* Returns a normalised ArxivResult per `<entry>` element.
|
|
40
|
+
*/
|
|
41
|
+
export declare function parseArxivAtom(xml: string): ArxivResult[];
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* arxiv-client — query the arXiv Atom API.
|
|
4
|
+
*
|
|
5
|
+
* GET http://export.arxiv.org/api/query?search_query=<q>&max_results=N
|
|
6
|
+
*
|
|
7
|
+
* No auth required (arXiv rate-limits by IP — be polite, run queries
|
|
8
|
+
* sequentially or with small parallelism). Response is Atom XML.
|
|
9
|
+
*
|
|
10
|
+
* We parse with a tiny purpose-built reader instead of pulling in an XML
|
|
11
|
+
* dependency. The arXiv schema is stable and the fields we need (title,
|
|
12
|
+
* summary, id/url, published, authors) are well-known.
|
|
13
|
+
*/
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
exports.arxivSearch = arxivSearch;
|
|
16
|
+
exports.parseArxivAtom = parseArxivAtom;
|
|
17
|
+
const DEFAULT_ENDPOINT = 'http://export.arxiv.org/api/query';
|
|
18
|
+
async function arxivSearch(opts) {
|
|
19
|
+
const fetchImpl = opts.fetchImpl ?? globalThis.fetch;
|
|
20
|
+
const endpoint = opts.endpoint ?? DEFAULT_ENDPOINT;
|
|
21
|
+
const max = Math.min(Math.max(1, opts.maxResults ?? 5), 30);
|
|
22
|
+
// arXiv wants `search_query` like `all:"agentic planning"` for phrase searches;
|
|
23
|
+
// we URL-encode the query and prefix with `all:` so the search hits any field.
|
|
24
|
+
const searchQuery = `all:${opts.query}`;
|
|
25
|
+
const url = `${endpoint}?search_query=${encodeURIComponent(searchQuery)}&max_results=${max}&sortBy=relevance`;
|
|
26
|
+
const controller = new AbortController();
|
|
27
|
+
const timer = setTimeout(() => controller.abort(), opts.timeoutMs ?? 30_000);
|
|
28
|
+
let response;
|
|
29
|
+
try {
|
|
30
|
+
response = await fetchImpl(url, { method: 'GET', signal: controller.signal });
|
|
31
|
+
}
|
|
32
|
+
finally {
|
|
33
|
+
clearTimeout(timer);
|
|
34
|
+
}
|
|
35
|
+
const httpStatus = response.status;
|
|
36
|
+
const rawText = await response.text();
|
|
37
|
+
if (!response.ok) {
|
|
38
|
+
throw new Error(`arXiv returned ${httpStatus}: ${rawText.slice(0, 400)}`);
|
|
39
|
+
}
|
|
40
|
+
return {
|
|
41
|
+
query: opts.query,
|
|
42
|
+
results: parseArxivAtom(rawText),
|
|
43
|
+
responseBytes: Buffer.byteLength(rawText, 'utf8'),
|
|
44
|
+
httpStatus,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Tiny Atom-XML reader for arXiv responses. Tolerant of CDATA + whitespace.
|
|
49
|
+
* Returns a normalised ArxivResult per `<entry>` element.
|
|
50
|
+
*/
|
|
51
|
+
function parseArxivAtom(xml) {
|
|
52
|
+
const entries = [...xml.matchAll(/<entry\b[\s\S]*?<\/entry>/g)].map(m => m[0]);
|
|
53
|
+
const results = [];
|
|
54
|
+
for (const entry of entries) {
|
|
55
|
+
const id = extractFirst(entry, /<id>([\s\S]*?)<\/id>/);
|
|
56
|
+
const title = textOf(extractFirst(entry, /<title>([\s\S]*?)<\/title>/));
|
|
57
|
+
const summary = textOf(extractFirst(entry, /<summary>([\s\S]*?)<\/summary>/));
|
|
58
|
+
const published = extractFirst(entry, /<published>([\s\S]*?)<\/published>/);
|
|
59
|
+
const authors = [...entry.matchAll(/<author[^>]*>\s*<name>([\s\S]*?)<\/name>\s*<\/author>/g)]
|
|
60
|
+
.map(m => textOf(m[1]));
|
|
61
|
+
// arXiv ids look like "http://arxiv.org/abs/2401.12345v1" — strip version + scheme
|
|
62
|
+
const cleanId = id.replace(/^https?:\/\/arxiv\.org\/abs\//, '').replace(/v\d+$/, '');
|
|
63
|
+
const abstractUrl = `https://arxiv.org/abs/${cleanId}`;
|
|
64
|
+
if (cleanId && title) {
|
|
65
|
+
results.push({
|
|
66
|
+
id: cleanId,
|
|
67
|
+
title,
|
|
68
|
+
summary,
|
|
69
|
+
abstractUrl,
|
|
70
|
+
published: published || '',
|
|
71
|
+
authors,
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return results;
|
|
76
|
+
}
|
|
77
|
+
function extractFirst(haystack, re) {
|
|
78
|
+
const m = haystack.match(re);
|
|
79
|
+
return m ? m[1].trim() : '';
|
|
80
|
+
}
|
|
81
|
+
/** Collapse whitespace + strip CDATA wrappers. */
|
|
82
|
+
function textOf(raw) {
|
|
83
|
+
return raw
|
|
84
|
+
.replace(/^\s*<!\[CDATA\[/, '')
|
|
85
|
+
.replace(/\]\]>\s*$/, '')
|
|
86
|
+
.replace(/\s+/g, ' ')
|
|
87
|
+
.trim();
|
|
88
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* hackernews-client — query Algolia's HN Search API.
|
|
3
|
+
*
|
|
4
|
+
* GET https://hn.algolia.com/api/v1/search?query=<q>&tags=story&hitsPerPage=N
|
|
5
|
+
*
|
|
6
|
+
* No auth, no rate-limits worth worrying about for research-scale traffic.
|
|
7
|
+
* Returns JSON with a `hits` array; we keep stories only (drop comments).
|
|
8
|
+
*/
|
|
9
|
+
export interface HackerNewsResult {
|
|
10
|
+
objectId: string;
|
|
11
|
+
title: string;
|
|
12
|
+
url: string;
|
|
13
|
+
hnUrl: string;
|
|
14
|
+
author: string;
|
|
15
|
+
points: number;
|
|
16
|
+
numComments: number;
|
|
17
|
+
createdAt: string;
|
|
18
|
+
}
|
|
19
|
+
export interface HackerNewsSearchOpts {
|
|
20
|
+
query: string;
|
|
21
|
+
/** 1..20. */
|
|
22
|
+
hitsPerPage?: number;
|
|
23
|
+
fetchImpl?: typeof fetch;
|
|
24
|
+
timeoutMs?: number;
|
|
25
|
+
endpoint?: string;
|
|
26
|
+
}
|
|
27
|
+
export interface HackerNewsSearchResult {
|
|
28
|
+
query: string;
|
|
29
|
+
results: HackerNewsResult[];
|
|
30
|
+
responseBytes: number;
|
|
31
|
+
httpStatus: number;
|
|
32
|
+
}
|
|
33
|
+
export declare function hackerNewsSearch(opts: HackerNewsSearchOpts): Promise<HackerNewsSearchResult>;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* hackernews-client — query Algolia's HN Search API.
|
|
4
|
+
*
|
|
5
|
+
* GET https://hn.algolia.com/api/v1/search?query=<q>&tags=story&hitsPerPage=N
|
|
6
|
+
*
|
|
7
|
+
* No auth, no rate-limits worth worrying about for research-scale traffic.
|
|
8
|
+
* Returns JSON with a `hits` array; we keep stories only (drop comments).
|
|
9
|
+
*/
|
|
10
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
11
|
+
exports.hackerNewsSearch = hackerNewsSearch;
|
|
12
|
+
const DEFAULT_ENDPOINT = 'https://hn.algolia.com/api/v1/search';
|
|
13
|
+
async function hackerNewsSearch(opts) {
|
|
14
|
+
const fetchImpl = opts.fetchImpl ?? globalThis.fetch;
|
|
15
|
+
const endpoint = opts.endpoint ?? DEFAULT_ENDPOINT;
|
|
16
|
+
const hits = Math.min(Math.max(1, opts.hitsPerPage ?? 5), 20);
|
|
17
|
+
const url = `${endpoint}?query=${encodeURIComponent(opts.query)}&tags=story&hitsPerPage=${hits}`;
|
|
18
|
+
const controller = new AbortController();
|
|
19
|
+
const timer = setTimeout(() => controller.abort(), opts.timeoutMs ?? 30_000);
|
|
20
|
+
let response;
|
|
21
|
+
try {
|
|
22
|
+
response = await fetchImpl(url, { method: 'GET', signal: controller.signal });
|
|
23
|
+
}
|
|
24
|
+
finally {
|
|
25
|
+
clearTimeout(timer);
|
|
26
|
+
}
|
|
27
|
+
const httpStatus = response.status;
|
|
28
|
+
const rawText = await response.text();
|
|
29
|
+
if (!response.ok) {
|
|
30
|
+
throw new Error(`Hacker News (Algolia) returned ${httpStatus}: ${rawText.slice(0, 400)}`);
|
|
31
|
+
}
|
|
32
|
+
const data = JSON.parse(rawText);
|
|
33
|
+
const results = (data.hits ?? []).map(h => ({
|
|
34
|
+
objectId: h.objectID ?? '',
|
|
35
|
+
title: h.title ?? '',
|
|
36
|
+
url: h.url ?? '',
|
|
37
|
+
hnUrl: h.objectID ? `https://news.ycombinator.com/item?id=${h.objectID}` : '',
|
|
38
|
+
author: h.author ?? '',
|
|
39
|
+
points: h.points ?? 0,
|
|
40
|
+
numComments: h.num_comments ?? 0,
|
|
41
|
+
createdAt: h.created_at ?? '',
|
|
42
|
+
})).filter(r => r.objectId && r.title);
|
|
43
|
+
return { query: opts.query, results, responseBytes: Buffer.byteLength(rawText, 'utf8'), httpStatus };
|
|
44
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* provider-result — uniform shape every search provider's node emits.
|
|
3
|
+
*
|
|
4
|
+
* dedupe_and_rank consumes a flat array of these (across all providers)
|
|
5
|
+
* and produces RankedSource entries with S1..SN ids. Keeping the per-
|
|
6
|
+
* provider clients honest about a single shape avoids per-provider
|
|
7
|
+
* branches in the dedupe + ranking logic.
|
|
8
|
+
*/
|
|
9
|
+
import type { SearchProvider } from '../schemas';
|
|
10
|
+
export interface ProviderResult {
|
|
11
|
+
provider: SearchProvider;
|
|
12
|
+
/** The query string that surfaced this result — used by the recall boost. */
|
|
13
|
+
fromQuery: string;
|
|
14
|
+
title: string;
|
|
15
|
+
/** Canonical-ish URL. dedupe_and_rank further canonicalizes. */
|
|
16
|
+
url: string;
|
|
17
|
+
/** Excerpt / abstract / snippet — capped at ≈500 chars by dedupe. */
|
|
18
|
+
content: string;
|
|
19
|
+
/** Provider-supplied relevance score, normalised to 0..1. */
|
|
20
|
+
score: number;
|
|
21
|
+
/** ISO publication date when the provider returns one (papers, news, patents). */
|
|
22
|
+
publishedDate?: string;
|
|
23
|
+
/** Optional author list (arxiv papers, patent inventors). */
|
|
24
|
+
authors?: string[];
|
|
25
|
+
}
|