@open-mercato/web-research 0.7.1-develop.7194.1.ab4fc81f82

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (121) hide show
  1. package/.turbo/turbo-build.log +2 -0
  2. package/AGENTS.md +67 -0
  3. package/build.mjs +10 -0
  4. package/dist/contract/adapter.js +13 -0
  5. package/dist/contract/adapter.js.map +7 -0
  6. package/dist/contract/errors.js +28 -0
  7. package/dist/contract/errors.js.map +7 -0
  8. package/dist/contract/http.js +7 -0
  9. package/dist/contract/http.js.map +7 -0
  10. package/dist/contract/index.js +9 -0
  11. package/dist/contract/index.js.map +7 -0
  12. package/dist/contract/outcomes.js +58 -0
  13. package/dist/contract/outcomes.js.map +7 -0
  14. package/dist/contract/policy.js +91 -0
  15. package/dist/contract/policy.js.map +7 -0
  16. package/dist/contract/results.js +13 -0
  17. package/dist/contract/results.js.map +7 -0
  18. package/dist/contract/steps.js +1 -0
  19. package/dist/contract/steps.js.map +7 -0
  20. package/dist/contract/version.js +11 -0
  21. package/dist/contract/version.js.map +7 -0
  22. package/dist/engine/cache.js +29 -0
  23. package/dist/engine/cache.js.map +7 -0
  24. package/dist/engine/deadline.js +54 -0
  25. package/dist/engine/deadline.js.map +7 -0
  26. package/dist/engine/engine.js +481 -0
  27. package/dist/engine/engine.js.map +7 -0
  28. package/dist/engine/index.js +20 -0
  29. package/dist/engine/index.js.map +7 -0
  30. package/dist/engine/runAdapter.js +57 -0
  31. package/dist/engine/runAdapter.js.map +7 -0
  32. package/dist/engine/types.js +1 -0
  33. package/dist/engine/types.js.map +7 -0
  34. package/dist/extract/classify.js +51 -0
  35. package/dist/extract/classify.js.map +7 -0
  36. package/dist/extract/content.js +104 -0
  37. package/dist/extract/content.js.map +7 -0
  38. package/dist/extract/entities.js +95 -0
  39. package/dist/extract/entities.js.map +7 -0
  40. package/dist/extract/index.js +24 -0
  41. package/dist/extract/index.js.map +7 -0
  42. package/dist/extract/text.js +114 -0
  43. package/dist/extract/text.js.map +7 -0
  44. package/dist/extract/tokenizer.js +141 -0
  45. package/dist/extract/tokenizer.js.map +7 -0
  46. package/dist/fusion/canonicalize.js +88 -0
  47. package/dist/fusion/canonicalize.js.map +7 -0
  48. package/dist/fusion/fuse.js +88 -0
  49. package/dist/fusion/fuse.js.map +7 -0
  50. package/dist/fusion/index.js +11 -0
  51. package/dist/fusion/index.js.map +7 -0
  52. package/dist/index.js +21 -0
  53. package/dist/index.js.map +7 -0
  54. package/dist/loader/describe.js +92 -0
  55. package/dist/loader/describe.js.map +7 -0
  56. package/dist/loader/index.js +31 -0
  57. package/dist/loader/index.js.map +7 -0
  58. package/dist/loader/manifest.js +32 -0
  59. package/dist/loader/manifest.js.map +7 -0
  60. package/dist/loader/registry.js +98 -0
  61. package/dist/loader/registry.js.map +7 -0
  62. package/dist/net/client.js +298 -0
  63. package/dist/net/client.js.map +7 -0
  64. package/dist/net/ssrf.js +165 -0
  65. package/dist/net/ssrf.js.map +7 -0
  66. package/dist/net/url.js +9 -0
  67. package/dist/net/url.js.map +7 -0
  68. package/dist/testing/contract.js +118 -0
  69. package/dist/testing/contract.js.map +7 -0
  70. package/dist/testing/fakeAdapter.js +44 -0
  71. package/dist/testing/fakeAdapter.js.map +7 -0
  72. package/dist/testing/index.js +15 -0
  73. package/dist/testing/index.js.map +7 -0
  74. package/jest.config.cjs +24 -0
  75. package/package.json +65 -0
  76. package/src/contract/adapter.ts +100 -0
  77. package/src/contract/errors.ts +48 -0
  78. package/src/contract/http.ts +46 -0
  79. package/src/contract/index.ts +8 -0
  80. package/src/contract/outcomes.ts +91 -0
  81. package/src/contract/policy.ts +157 -0
  82. package/src/contract/results.ts +79 -0
  83. package/src/contract/steps.ts +45 -0
  84. package/src/contract/version.ts +25 -0
  85. package/src/engine/__tests__/engine.test.ts +597 -0
  86. package/src/engine/cache.ts +46 -0
  87. package/src/engine/deadline.ts +65 -0
  88. package/src/engine/engine.ts +633 -0
  89. package/src/engine/index.ts +22 -0
  90. package/src/engine/runAdapter.ts +89 -0
  91. package/src/engine/types.ts +137 -0
  92. package/src/extract/__tests__/content.test.ts +81 -0
  93. package/src/extract/__tests__/text.test.ts +61 -0
  94. package/src/extract/__tests__/tokenizer.test.ts +85 -0
  95. package/src/extract/classify.ts +81 -0
  96. package/src/extract/content.ts +136 -0
  97. package/src/extract/entities.ts +98 -0
  98. package/src/extract/index.ts +11 -0
  99. package/src/extract/text.ts +124 -0
  100. package/src/extract/tokenizer.ts +197 -0
  101. package/src/fusion/__tests__/fusion.test.ts +198 -0
  102. package/src/fusion/canonicalize.ts +119 -0
  103. package/src/fusion/fuse.ts +145 -0
  104. package/src/fusion/index.ts +8 -0
  105. package/src/index.ts +15 -0
  106. package/src/loader/__tests__/describe.test.ts +119 -0
  107. package/src/loader/__tests__/registry.test.ts +134 -0
  108. package/src/loader/describe.ts +145 -0
  109. package/src/loader/index.ts +25 -0
  110. package/src/loader/manifest.ts +44 -0
  111. package/src/loader/registry.ts +156 -0
  112. package/src/net/__tests__/client.test.ts +273 -0
  113. package/src/net/__tests__/ssrf.test.ts +165 -0
  114. package/src/net/client.ts +434 -0
  115. package/src/net/ssrf.ts +235 -0
  116. package/src/net/url.ts +13 -0
  117. package/src/testing/contract.ts +172 -0
  118. package/src/testing/fakeAdapter.ts +69 -0
  119. package/src/testing/index.ts +9 -0
  120. package/tsconfig.json +10 -0
  121. package/watch.mjs +7 -0
@@ -0,0 +1,157 @@
1
+ import { z } from 'zod'
2
+
3
+ export type SettleMode = 'race' | 'quorum' | 'exhaustive'
4
+
5
+ export type AdapterPolicyEntry = {
6
+ readonly id: string
7
+ readonly enabled: boolean
8
+ readonly order: number
9
+ /** Relative pull in the fusion step; higher trusts this adapter's ranking more. */
10
+ readonly weight: number
11
+ /**
12
+ * Overrides `adapterTimeoutMs` for this adapter alone. Latency is not uniform
13
+ * across kinds: a SERP read finishes in about a second, while a model doing its
14
+ * own multi-step web search takes tens of seconds. One shared budget has to be
15
+ * either too tight for the model or too slack for everything else.
16
+ */
17
+ readonly timeoutMs?: number
18
+ /**
19
+ * Ceiling on how often this adapter may be called, per tenant, per hour.
20
+ *
21
+ * The engine does not enforce this — it is per-request and has no memory. The
22
+ * host counts calls and simply stops enabling the adapter once the ceiling is
23
+ * reached, which is why the knob lives on the policy rather than in the engine.
24
+ * Undefined means no ceiling.
25
+ */
26
+ readonly maxCallsPerHour?: number
27
+ }
28
+
29
+ export type ContentPolicy = {
30
+ readonly enabledByDefault: boolean
31
+ readonly maxPages: number
32
+ readonly maxBytesPerPage: number
33
+ }
34
+
35
+ export type SearchPolicy = {
36
+ readonly settleMode: SettleMode
37
+ readonly concurrency: number
38
+ readonly minResults: number
39
+ readonly minAdapters: number
40
+ readonly minConfidence: number
41
+ readonly softDeadlineMs: number
42
+ readonly hardDeadlineMs: number
43
+ readonly adapterTimeoutMs: number
44
+ readonly maxPerDomain: number
45
+ readonly cacheTtlMs: number
46
+ /**
47
+ * Adapter run when every wave is exhausted and the fused set is still short,
48
+ * even if it is disabled in the normal order. This is what makes "the engine
49
+ * always returns something" true rather than aspirational.
50
+ */
51
+ readonly lastResort: string | null
52
+ readonly escalateToBrowser: boolean
53
+ readonly content: ContentPolicy
54
+ readonly adapters: readonly AdapterPolicyEntry[]
55
+ }
56
+
57
+ /**
58
+ * Budget for `model-native`, the adapter enabled by default. It runs the model's
59
+ * own multi-step web search, which measures at roughly 30s end to end, so the
60
+ * generic adapter budget would time it out on every single run and the shipped
61
+ * configuration could never return anything.
62
+ */
63
+ export const MODEL_ADAPTER_TIMEOUT_MS = 45_000
64
+
65
+ export const DEFAULT_POLICY: SearchPolicy = {
66
+ settleMode: 'quorum',
67
+ concurrency: 2,
68
+ minResults: 5,
69
+ minAdapters: 1,
70
+ minConfidence: 0,
71
+ // The soft deadline only binds once results exist, so a fast adapter still
72
+ // settles the run in about a second; this ceiling is what a lone slow adapter
73
+ // is allowed to use, not what a normal search costs.
74
+ softDeadlineMs: 6_000,
75
+ hardDeadlineMs: 60_000,
76
+ adapterTimeoutMs: 8_000,
77
+ maxPerDomain: 3,
78
+ cacheTtlMs: 900_000,
79
+ lastResort: 'model-native',
80
+ escalateToBrowser: true,
81
+ content: { enabledByDefault: false, maxPages: 3, maxBytesPerPage: 128 * 1024 },
82
+ adapters: [],
83
+ }
84
+
85
+ export const adapterPolicyEntrySchema = z.object({
86
+ id: z.string().min(1),
87
+ enabled: z.boolean().optional(),
88
+ order: z.number().int().min(0).optional(),
89
+ weight: z.number().min(0).max(10).optional(),
90
+ timeoutMs: z.number().int().min(250).max(120_000).optional(),
91
+ maxCallsPerHour: z.number().int().min(1).max(1_000_000).optional(),
92
+ })
93
+
94
+ export const searchPolicySchema = z.object({
95
+ settleMode: z.enum(['race', 'quorum', 'exhaustive']).optional(),
96
+ concurrency: z.number().int().min(1).max(16).optional(),
97
+ minResults: z.number().int().min(1).max(50).optional(),
98
+ minAdapters: z.number().int().min(1).max(16).optional(),
99
+ minConfidence: z.number().min(0).max(1).optional(),
100
+ softDeadlineMs: z.number().int().min(250).max(120_000).optional(),
101
+ hardDeadlineMs: z.number().int().min(500).max(180_000).optional(),
102
+ adapterTimeoutMs: z.number().int().min(250).max(120_000).optional(),
103
+ maxPerDomain: z.number().int().min(1).max(50).optional(),
104
+ cacheTtlMs: z.number().int().min(0).max(86_400_000).optional(),
105
+ lastResort: z.string().min(1).nullable().optional(),
106
+ escalateToBrowser: z.boolean().optional(),
107
+ content: z
108
+ .object({
109
+ enabledByDefault: z.boolean().optional(),
110
+ maxPages: z.number().int().min(0).max(20).optional(),
111
+ maxBytesPerPage: z.number().int().min(1024).max(4 * 1024 * 1024).optional(),
112
+ })
113
+ .optional(),
114
+ adapters: z.array(adapterPolicyEntrySchema).optional(),
115
+ })
116
+
117
+ export type SearchPolicyInput = z.infer<typeof searchPolicySchema>
118
+
119
+ /**
120
+ * Merges a partial policy over the defaults. Deadlines are reconciled rather
121
+ * than trusted: a soft deadline past the hard one would make the scheduler wait
122
+ * for a barrier it can never reach.
123
+ */
124
+ export function resolvePolicy(input: SearchPolicyInput | null | undefined): SearchPolicy {
125
+ const patch = input ?? {}
126
+ const hardDeadlineMs = patch.hardDeadlineMs ?? DEFAULT_POLICY.hardDeadlineMs
127
+ const softDeadlineMs = Math.min(patch.softDeadlineMs ?? DEFAULT_POLICY.softDeadlineMs, hardDeadlineMs)
128
+ return {
129
+ settleMode: patch.settleMode ?? DEFAULT_POLICY.settleMode,
130
+ concurrency: patch.concurrency ?? DEFAULT_POLICY.concurrency,
131
+ minResults: patch.minResults ?? DEFAULT_POLICY.minResults,
132
+ minAdapters: patch.minAdapters ?? DEFAULT_POLICY.minAdapters,
133
+ minConfidence: patch.minConfidence ?? DEFAULT_POLICY.minConfidence,
134
+ softDeadlineMs,
135
+ hardDeadlineMs,
136
+ adapterTimeoutMs: Math.min(patch.adapterTimeoutMs ?? DEFAULT_POLICY.adapterTimeoutMs, hardDeadlineMs),
137
+ maxPerDomain: patch.maxPerDomain ?? DEFAULT_POLICY.maxPerDomain,
138
+ cacheTtlMs: patch.cacheTtlMs ?? DEFAULT_POLICY.cacheTtlMs,
139
+ lastResort: patch.lastResort === undefined ? DEFAULT_POLICY.lastResort : patch.lastResort,
140
+ escalateToBrowser: patch.escalateToBrowser ?? DEFAULT_POLICY.escalateToBrowser,
141
+ content: {
142
+ enabledByDefault: patch.content?.enabledByDefault ?? DEFAULT_POLICY.content.enabledByDefault,
143
+ maxPages: patch.content?.maxPages ?? DEFAULT_POLICY.content.maxPages,
144
+ maxBytesPerPage: patch.content?.maxBytesPerPage ?? DEFAULT_POLICY.content.maxBytesPerPage,
145
+ },
146
+ adapters: (patch.adapters ?? []).map((entry, index) => ({
147
+ id: entry.id,
148
+ enabled: entry.enabled ?? true,
149
+ order: entry.order ?? index,
150
+ weight: entry.weight ?? 1,
151
+ // Clamped to the hard deadline for the same reason the shared budget is:
152
+ // a per-adapter timeout past it can never be reached.
153
+ ...(entry.timeoutMs === undefined ? {} : { timeoutMs: Math.min(entry.timeoutMs, hardDeadlineMs) }),
154
+ ...(entry.maxCallsPerHour === undefined ? {} : { maxCallsPerHour: entry.maxCallsPerHour }),
155
+ })),
156
+ }
157
+ }
@@ -0,0 +1,79 @@
1
+ import { z } from 'zod'
2
+
3
+ export type Freshness = 'day' | 'week' | 'month' | 'year'
4
+
5
+ /** Normalized request handed to adapters. The engine resolves defaults first. */
6
+ export type SearchRequest = {
7
+ readonly query: string
8
+ readonly limit: number
9
+ readonly locale?: string
10
+ readonly freshness?: Freshness
11
+ readonly site?: string
12
+ }
13
+
14
+ /**
15
+ * What an adapter returns per hit. Everything but `url` is optional because
16
+ * sources differ wildly — an LLM citation list has no snippet, a SERP has no
17
+ * content, a scraping API has both.
18
+ */
19
+ export type RawResult = {
20
+ readonly url: string
21
+ readonly title?: string | null
22
+ readonly snippet?: string | null
23
+ readonly content?: string | null
24
+ readonly publishedAt?: string | null
25
+ /** Adapter-native relevance, used only to break ties within one adapter. */
26
+ readonly score?: number | null
27
+ }
28
+
29
+ export type ResultProvenance = {
30
+ readonly adapterId: string
31
+ readonly rank: number
32
+ }
33
+
34
+ /** A hit after canonicalization, dedup and cross-adapter fusion. */
35
+ export type SearchResult = {
36
+ readonly url: string
37
+ readonly title: string
38
+ readonly snippet: string | null
39
+ readonly content: string | null
40
+ readonly publishedAt: string | null
41
+ readonly confidence: number
42
+ readonly sources: readonly ResultProvenance[]
43
+ }
44
+
45
+ export type RenderMode = 'auto' | 'never' | 'always'
46
+
47
+ export type FetchRequest = {
48
+ readonly url: string
49
+ readonly maxBytes?: number
50
+ readonly render?: RenderMode
51
+ }
52
+
53
+ /**
54
+ * Why an HTTP read was judged insufficient. Drives browser escalation, and is
55
+ * reported back so the decision is auditable rather than invisible.
56
+ */
57
+ export type PageVerdict = 'ok' | 'js-shell' | 'blocked' | 'empty'
58
+
59
+ export type FetchedPage = {
60
+ readonly url: string
61
+ readonly title: string | null
62
+ readonly text: string
63
+ readonly contentType: string | null
64
+ readonly status: number
65
+ readonly truncated: boolean
66
+ readonly renderedWith: 'http' | 'browser'
67
+ readonly escalatedBecause?: PageVerdict
68
+ }
69
+
70
+ export const searchRequestSchema = z.object({
71
+ query: z.string().trim().min(1).max(500),
72
+ limit: z.number().int().min(1).max(50).optional(),
73
+ locale: z.string().min(2).max(16).optional(),
74
+ freshness: z.enum(['day', 'week', 'month', 'year']).optional(),
75
+ site: z.string().min(1).max(253).optional(),
76
+ includeContent: z.boolean().optional(),
77
+ })
78
+
79
+ export type SearchRequestInput = z.infer<typeof searchRequestSchema>
@@ -0,0 +1,45 @@
1
+ export type SearchStepPhase = 'plan' | 'adapter' | 'fetch' | 'fusion' | 'done'
2
+
3
+ export type SearchStepEvent =
4
+ | 'queued'
5
+ | 'started'
6
+ | 'ok'
7
+ | 'empty'
8
+ | 'blocked'
9
+ | 'timeout'
10
+ | 'unavailable'
11
+ | 'error'
12
+ | 'cancelled'
13
+ | 'escalated'
14
+ | 'cached'
15
+ | 'fused'
16
+ | 'completed'
17
+
18
+ export type SearchStepMetrics = {
19
+ readonly latencyMs?: number
20
+ readonly resultCount?: number
21
+ readonly bytes?: number
22
+ readonly httpStatus?: number
23
+ }
24
+
25
+ /**
26
+ * One narratable unit of engine work. Emitted live and persisted, so `detail`
27
+ * must be human-readable and free of secrets — it reaches an operator's screen.
28
+ */
29
+ export type SearchStep = {
30
+ readonly seq: number
31
+ readonly at: string
32
+ readonly phase: SearchStepPhase
33
+ readonly event: SearchStepEvent
34
+ readonly adapterId?: string
35
+ readonly detail?: string
36
+ readonly metrics?: SearchStepMetrics
37
+ }
38
+
39
+ export type StepReporter = (
40
+ event: SearchStepEvent,
41
+ detail?: string,
42
+ metrics?: SearchStepMetrics,
43
+ ) => void
44
+
45
+ export type StepSink = (step: SearchStep) => void
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Adapter contract version. Adapter packages declare the version they were built
3
+ * against; the loader refuses to instantiate a mismatched major rather than let a
4
+ * stale adapter fail in an unpredictable place at request time.
5
+ */
6
+ export const CONTRACT_VERSION = 1
7
+
8
+ /**
9
+ * Oldest adapter contract this engine still accepts.
10
+ *
11
+ * An exact-match check makes every bump a flag day: a purely additive change
12
+ * would disable every third-party adapter at once, with nothing wrong with any
13
+ * of them. A range lets an additive bump raise `CONTRACT_VERSION` while leaving
14
+ * this behind, and a genuinely breaking one raise both together.
15
+ */
16
+ export const MIN_SUPPORTED_CONTRACT_VERSION = 1
17
+
18
+ export function isSupportedContractVersion(version: unknown): version is number {
19
+ return (
20
+ typeof version === 'number' &&
21
+ Number.isInteger(version) &&
22
+ version >= MIN_SUPPORTED_CONTRACT_VERSION &&
23
+ version <= CONTRACT_VERSION
24
+ )
25
+ }