opencode-agora 0.4.1 → 0.4.3
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 +54 -6
- package/dist/cli/app.d.ts.map +1 -1
- package/dist/cli/app.js +18 -0
- package/dist/cli/app.js.map +1 -1
- package/dist/cli/commands/browse.d.ts.map +1 -1
- package/dist/cli/commands/browse.js +1 -5
- package/dist/cli/commands/browse.js.map +1 -1
- package/dist/cli/commands/capabilities.d.ts +3 -0
- package/dist/cli/commands/capabilities.d.ts.map +1 -0
- package/dist/cli/commands/capabilities.js +146 -0
- package/dist/cli/commands/capabilities.js.map +1 -0
- package/dist/cli/commands/community.d.ts.map +1 -1
- package/dist/cli/commands/community.js.map +1 -1
- package/dist/cli/commands/curate.d.ts +9 -0
- package/dist/cli/commands/curate.d.ts.map +1 -0
- package/dist/cli/commands/curate.js +62 -0
- package/dist/cli/commands/curate.js.map +1 -0
- package/dist/cli/commands/doctor.d.ts +3 -0
- package/dist/cli/commands/doctor.d.ts.map +1 -0
- package/dist/cli/commands/doctor.js +77 -0
- package/dist/cli/commands/doctor.js.map +1 -0
- package/dist/cli/commands/export.d.ts.map +1 -1
- package/dist/cli/commands/export.js +5 -3
- package/dist/cli/commands/export.js.map +1 -1
- package/dist/cli/commands/freeze.d.ts +3 -0
- package/dist/cli/commands/freeze.d.ts.map +1 -0
- package/dist/cli/commands/freeze.js +62 -0
- package/dist/cli/commands/freeze.js.map +1 -0
- package/dist/cli/commands/init.d.ts.map +1 -1
- package/dist/cli/commands/init.js +5 -1
- package/dist/cli/commands/init.js.map +1 -1
- package/dist/cli/commands/installed.d.ts +3 -0
- package/dist/cli/commands/installed.d.ts.map +1 -0
- package/dist/cli/commands/installed.js +82 -0
- package/dist/cli/commands/installed.js.map +1 -0
- package/dist/cli/commands/marketplace.d.ts.map +1 -1
- package/dist/cli/commands/marketplace.js.map +1 -1
- package/dist/cli/commands/notify.d.ts.map +1 -1
- package/dist/cli/commands/notify.js.map +1 -1
- package/dist/cli/commands/operations.d.ts.map +1 -1
- package/dist/cli/commands/operations.js +103 -5
- package/dist/cli/commands/operations.js.map +1 -1
- package/dist/cli/commands/outdated.d.ts +3 -0
- package/dist/cli/commands/outdated.d.ts.map +1 -0
- package/dist/cli/commands/outdated.js +48 -0
- package/dist/cli/commands/outdated.js.map +1 -0
- package/dist/cli/commands/ping.js +2 -1
- package/dist/cli/commands/ping.js.map +1 -1
- package/dist/cli/commands/scan.d.ts +3 -0
- package/dist/cli/commands/scan.d.ts.map +1 -0
- package/dist/cli/commands/scan.js +35 -0
- package/dist/cli/commands/scan.js.map +1 -0
- package/dist/cli/commands/sync.d.ts +3 -0
- package/dist/cli/commands/sync.d.ts.map +1 -0
- package/dist/cli/commands/sync.js +172 -0
- package/dist/cli/commands/sync.js.map +1 -0
- package/dist/cli/commands/today.d.ts.map +1 -1
- package/dist/cli/commands/today.js +11 -3
- package/dist/cli/commands/today.js.map +1 -1
- package/dist/cli/commands/try.d.ts +3 -0
- package/dist/cli/commands/try.d.ts.map +1 -0
- package/dist/cli/commands/try.js +157 -0
- package/dist/cli/commands/try.js.map +1 -0
- package/dist/cli/commands/watch.d.ts.map +1 -1
- package/dist/cli/commands/watch.js.map +1 -1
- package/dist/cli/commands/welcome.d.ts.map +1 -1
- package/dist/cli/commands/welcome.js +6 -27
- package/dist/cli/commands/welcome.js.map +1 -1
- package/dist/cli/commands-meta.d.ts +1 -1
- package/dist/cli/commands-meta.d.ts.map +1 -1
- package/dist/cli/commands-meta.js +242 -12
- package/dist/cli/commands-meta.js.map +1 -1
- package/dist/cli/completions-gen.d.ts.map +1 -1
- package/dist/cli/completions-gen.js +130 -52
- package/dist/cli/completions-gen.js.map +1 -1
- package/dist/cli/flags.d.ts.map +1 -1
- package/dist/cli/flags.js +7 -0
- package/dist/cli/flags.js.map +1 -1
- package/dist/cli/format.js +1 -1
- package/dist/cli/format.js.map +1 -1
- package/dist/cli/helpers.d.ts.map +1 -1
- package/dist/cli/helpers.js.map +1 -1
- package/dist/cli/mcp-server.d.ts +12 -1
- package/dist/cli/mcp-server.d.ts.map +1 -1
- package/dist/cli/mcp-server.js +292 -2
- package/dist/cli/mcp-server.js.map +1 -1
- package/dist/cli/pages/community.d.ts.map +1 -1
- package/dist/cli/pages/community.js +5 -8
- package/dist/cli/pages/community.js.map +1 -1
- package/dist/cli/pages/home.d.ts.map +1 -1
- package/dist/cli/pages/home.js +22 -8
- package/dist/cli/pages/home.js.map +1 -1
- package/dist/cli/pages/marketplace.d.ts.map +1 -1
- package/dist/cli/pages/marketplace.js +68 -7
- package/dist/cli/pages/marketplace.js.map +1 -1
- package/dist/cli/pages/news.d.ts.map +1 -1
- package/dist/cli/pages/news.js +2 -2
- package/dist/cli/pages/news.js.map +1 -1
- package/dist/cli/pages/stack.d.ts +3 -0
- package/dist/cli/pages/stack.d.ts.map +1 -0
- package/dist/cli/pages/stack.js +373 -0
- package/dist/cli/pages/stack.js.map +1 -0
- package/dist/cli/pages/types.d.ts +1 -1
- package/dist/cli/pages/types.d.ts.map +1 -1
- package/dist/cli/shell.d.ts +1 -1
- package/dist/cli/shell.d.ts.map +1 -1
- package/dist/cli/shell.js +70 -4
- package/dist/cli/shell.js.map +1 -1
- package/dist/cli/tui.d.ts.map +1 -1
- package/dist/cli/tui.js +14 -4
- package/dist/cli/tui.js.map +1 -1
- package/dist/community/client.d.ts.map +1 -1
- package/dist/community/client.js +3 -1
- package/dist/community/client.js.map +1 -1
- package/dist/curator/index.d.ts +95 -0
- package/dist/curator/index.d.ts.map +1 -0
- package/dist/curator/index.js +446 -0
- package/dist/curator/index.js.map +1 -0
- package/dist/format.d.ts +0 -2
- package/dist/format.d.ts.map +1 -1
- package/dist/format.js +0 -2
- package/dist/format.js.map +1 -1
- package/dist/hubs/enrichment.d.ts.map +1 -1
- package/dist/hubs/enrichment.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/live.js +7 -1
- package/dist/live.js.map +1 -1
- package/dist/marketplace.d.ts +2 -1
- package/dist/marketplace.d.ts.map +1 -1
- package/dist/marketplace.js +86 -17
- package/dist/marketplace.js.map +1 -1
- package/dist/outdated.d.ts +24 -0
- package/dist/outdated.d.ts.map +1 -0
- package/dist/outdated.js +74 -0
- package/dist/outdated.js.map +1 -0
- package/dist/scan.d.ts +26 -0
- package/dist/scan.d.ts.map +1 -0
- package/dist/scan.js +247 -0
- package/dist/scan.js.map +1 -0
- package/dist/search/catalog-index.d.ts +107 -0
- package/dist/search/catalog-index.d.ts.map +1 -0
- package/dist/search/catalog-index.js +276 -0
- package/dist/search/catalog-index.js.map +1 -0
- package/dist/stack/adapters/claude-code.d.ts +3 -0
- package/dist/stack/adapters/claude-code.d.ts.map +1 -0
- package/dist/stack/adapters/claude-code.js +282 -0
- package/dist/stack/adapters/claude-code.js.map +1 -0
- package/dist/stack/adapters/cursor.d.ts +3 -0
- package/dist/stack/adapters/cursor.d.ts.map +1 -0
- package/dist/stack/adapters/cursor.js +232 -0
- package/dist/stack/adapters/cursor.js.map +1 -0
- package/dist/stack/adapters/opencode.d.ts +3 -0
- package/dist/stack/adapters/opencode.d.ts.map +1 -0
- package/dist/stack/adapters/opencode.js +177 -0
- package/dist/stack/adapters/opencode.js.map +1 -0
- package/dist/stack/adapters/windsurf.d.ts +3 -0
- package/dist/stack/adapters/windsurf.d.ts.map +1 -0
- package/dist/stack/adapters/windsurf.js +218 -0
- package/dist/stack/adapters/windsurf.js.map +1 -0
- package/dist/stack/capability-cache.d.ts +19 -0
- package/dist/stack/capability-cache.d.ts.map +1 -0
- package/dist/stack/capability-cache.js +41 -0
- package/dist/stack/capability-cache.js.map +1 -0
- package/dist/stack/doctor.d.ts +30 -0
- package/dist/stack/doctor.d.ts.map +1 -0
- package/dist/stack/doctor.js +227 -0
- package/dist/stack/doctor.js.map +1 -0
- package/dist/stack/manifest.d.ts +40 -0
- package/dist/stack/manifest.d.ts.map +1 -0
- package/dist/stack/manifest.js +342 -0
- package/dist/stack/manifest.js.map +1 -0
- package/dist/stack/mcp-probe.d.ts +23 -0
- package/dist/stack/mcp-probe.d.ts.map +1 -0
- package/dist/stack/mcp-probe.js +230 -0
- package/dist/stack/mcp-probe.js.map +1 -0
- package/dist/stack/path-resolve.d.ts +7 -0
- package/dist/stack/path-resolve.d.ts.map +1 -0
- package/dist/stack/path-resolve.js +37 -0
- package/dist/stack/path-resolve.js.map +1 -0
- package/dist/stack/registry.d.ts +11 -0
- package/dist/stack/registry.d.ts.map +1 -0
- package/dist/stack/registry.js +42 -0
- package/dist/stack/registry.js.map +1 -0
- package/dist/stack/sync.d.ts +20 -0
- package/dist/stack/sync.d.ts.map +1 -0
- package/dist/stack/sync.js +226 -0
- package/dist/stack/sync.js.map +1 -0
- package/dist/stack/types.d.ts +53 -0
- package/dist/stack/types.d.ts.map +1 -0
- package/dist/stack/types.js +2 -0
- package/dist/stack/types.js.map +1 -0
- package/dist/transcript.d.ts +14 -0
- package/dist/transcript.d.ts.map +1 -1
- package/dist/transcript.js +98 -1
- package/dist/transcript.js.map +1 -1
- package/dist/types.d.ts +4 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +5 -2
package/dist/scan.d.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { MarketplaceItem } from './marketplace.js';
|
|
2
|
+
import type { FetchLike } from './live.js';
|
|
3
|
+
export type CheckStatus = 'pass' | 'warn' | 'fail';
|
|
4
|
+
export interface ScanCheck {
|
|
5
|
+
name: string;
|
|
6
|
+
label: string;
|
|
7
|
+
status: CheckStatus;
|
|
8
|
+
message: string;
|
|
9
|
+
}
|
|
10
|
+
export interface ScanResult {
|
|
11
|
+
id: string;
|
|
12
|
+
itemKind: 'package' | 'workflow';
|
|
13
|
+
checks: ScanCheck[];
|
|
14
|
+
summary: {
|
|
15
|
+
pass: number;
|
|
16
|
+
warn: number;
|
|
17
|
+
fail: number;
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
export interface ScanOptions {
|
|
21
|
+
fetcher?: FetchLike;
|
|
22
|
+
now?: () => Date;
|
|
23
|
+
githubToken?: string;
|
|
24
|
+
}
|
|
25
|
+
export declare function scanItem(item: MarketplaceItem, opts?: ScanOptions): Promise<ScanResult>;
|
|
26
|
+
//# sourceMappingURL=scan.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scan.d.ts","sourceRoot":"","sources":["../src/scan.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAA0B,MAAM,kBAAkB,CAAC;AAEhF,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AAE3C,MAAM,MAAM,WAAW,GAAG,MAAM,GAAG,MAAM,GAAG,MAAM,CAAC;AAEnD,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,WAAW,CAAC;IACpB,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,UAAU;IACzB,EAAE,EAAE,MAAM,CAAC;IACX,QAAQ,EAAE,SAAS,GAAG,UAAU,CAAC;IACjC,MAAM,EAAE,SAAS,EAAE,CAAC;IACpB,OAAO,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC;CACvD;AAED,MAAM,WAAW,WAAW;IAC1B,OAAO,CAAC,EAAE,SAAS,CAAC;IACpB,GAAG,CAAC,EAAE,MAAM,IAAI,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAqND,wBAAsB,QAAQ,CAAC,IAAI,EAAE,eAAe,EAAE,IAAI,GAAE,WAAgB,GAAG,OAAO,CAAC,UAAU,CAAC,CAkCjG"}
|
package/dist/scan.js
ADDED
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
import { hasPermissions, getInstallKind } from './marketplace.js';
|
|
2
|
+
function tally(checks) {
|
|
3
|
+
let pass = 0, warn = 0, fail = 0;
|
|
4
|
+
for (const c of checks) {
|
|
5
|
+
if (c.status === 'pass')
|
|
6
|
+
pass++;
|
|
7
|
+
else if (c.status === 'warn')
|
|
8
|
+
warn++;
|
|
9
|
+
else
|
|
10
|
+
fail++;
|
|
11
|
+
}
|
|
12
|
+
return { pass, warn, fail };
|
|
13
|
+
}
|
|
14
|
+
function licenseCheck(status, message) {
|
|
15
|
+
return { name: 'license_present', label: 'License declared', status, message };
|
|
16
|
+
}
|
|
17
|
+
// Returns repo_reachable plus (when the repo is reachable) license_present,
|
|
18
|
+
// both derived from a single GitHub repos API call. A missing license is a
|
|
19
|
+
// warning, never a hard fail — many legitimate repos lack a detected license.
|
|
20
|
+
async function checkRepo(item, opts) {
|
|
21
|
+
const base = {
|
|
22
|
+
name: 'repo_reachable',
|
|
23
|
+
label: 'Repository reachable'
|
|
24
|
+
};
|
|
25
|
+
if (!item.repository) {
|
|
26
|
+
return [{ ...base, status: 'pass', message: 'no repository field, skipped' }];
|
|
27
|
+
}
|
|
28
|
+
let url;
|
|
29
|
+
try {
|
|
30
|
+
url = new URL(item.repository);
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return [{ ...base, status: 'pass', message: 'non-github repository, skipped' }];
|
|
34
|
+
}
|
|
35
|
+
if (url.hostname !== 'github.com') {
|
|
36
|
+
return [{ ...base, status: 'pass', message: 'non-github repository, skipped' }];
|
|
37
|
+
}
|
|
38
|
+
const path = url.pathname
|
|
39
|
+
.replace(/^\//, '')
|
|
40
|
+
.replace(/\.git$/, '')
|
|
41
|
+
.split('/')
|
|
42
|
+
.slice(0, 2)
|
|
43
|
+
.join('/');
|
|
44
|
+
const apiUrl = `https://api.github.com/repos/${path}`;
|
|
45
|
+
const headers = { Accept: 'application/vnd.github+json' };
|
|
46
|
+
if (opts.githubToken)
|
|
47
|
+
headers.Authorization = `Bearer ${opts.githubToken}`;
|
|
48
|
+
const fetcher = opts.fetcher ?? globalThis.fetch;
|
|
49
|
+
try {
|
|
50
|
+
const res = await fetcher(apiUrl, { headers, signal: AbortSignal.timeout(8000) });
|
|
51
|
+
if (res.status === 200) {
|
|
52
|
+
const reachable = { ...base, status: 'pass', message: `github.com/${path}` };
|
|
53
|
+
let license;
|
|
54
|
+
try {
|
|
55
|
+
const body = (await res.json());
|
|
56
|
+
const spdx = body.license?.spdx_id;
|
|
57
|
+
license =
|
|
58
|
+
spdx && spdx !== 'NOASSERTION'
|
|
59
|
+
? licenseCheck('pass', spdx)
|
|
60
|
+
: licenseCheck('warn', 'no license detected on the repository');
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
license = licenseCheck('warn', 'could not read license metadata');
|
|
64
|
+
}
|
|
65
|
+
return [reachable, license];
|
|
66
|
+
}
|
|
67
|
+
if (res.status === 404)
|
|
68
|
+
return [{ ...base, status: 'fail', message: 'repo not found' }];
|
|
69
|
+
return [{ ...base, status: 'warn', message: 'could not verify (rate limited or network)' }];
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
return [{ ...base, status: 'warn', message: 'could not verify (rate limited or network)' }];
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
async function checkNpmExists(item, opts) {
|
|
76
|
+
const base = {
|
|
77
|
+
name: 'npm_exists',
|
|
78
|
+
label: 'npm package exists'
|
|
79
|
+
};
|
|
80
|
+
if (!item.npmPackage)
|
|
81
|
+
return { ...base, status: 'pass', message: 'no npm package, skipped' };
|
|
82
|
+
const encoded = encodeURIComponent(item.npmPackage).replace('%40', '@').replace('%2F', '/');
|
|
83
|
+
const fetcher = opts.fetcher ?? globalThis.fetch;
|
|
84
|
+
try {
|
|
85
|
+
const res = await fetcher(`https://registry.npmjs.org/${encoded}/latest`, {
|
|
86
|
+
signal: AbortSignal.timeout(8000)
|
|
87
|
+
});
|
|
88
|
+
if (res.status === 200) {
|
|
89
|
+
const json = (await res.json());
|
|
90
|
+
return {
|
|
91
|
+
...base,
|
|
92
|
+
status: 'pass',
|
|
93
|
+
message: `${item.npmPackage}@${json.version ?? 'unknown'}`
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
if (res.status === 404)
|
|
97
|
+
return { ...base, status: 'fail', message: 'package not found on npm' };
|
|
98
|
+
return { ...base, status: 'warn', message: 'could not verify (network)' };
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
return { ...base, status: 'warn', message: 'could not verify (network)' };
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
async function scanPackage(item, opts) {
|
|
105
|
+
const checks = [];
|
|
106
|
+
// 1. permissions_declared
|
|
107
|
+
const hasPerm = hasPermissions(item.permissions);
|
|
108
|
+
if (hasPerm) {
|
|
109
|
+
const parts = [];
|
|
110
|
+
if (item.permissions?.fs?.length)
|
|
111
|
+
parts.push('fs');
|
|
112
|
+
if (item.permissions?.net?.length)
|
|
113
|
+
parts.push('net');
|
|
114
|
+
if (item.permissions?.exec?.length)
|
|
115
|
+
parts.push('exec');
|
|
116
|
+
checks.push({
|
|
117
|
+
name: 'permissions_declared',
|
|
118
|
+
label: 'Permissions declared',
|
|
119
|
+
status: 'pass',
|
|
120
|
+
message: parts.join('|')
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
else {
|
|
124
|
+
checks.push({
|
|
125
|
+
name: 'permissions_declared',
|
|
126
|
+
label: 'Permissions declared',
|
|
127
|
+
status: 'warn',
|
|
128
|
+
message: 'no permissions manifest declared'
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
// 2. permission_consistency
|
|
132
|
+
const kind = getInstallKind(item);
|
|
133
|
+
if (kind === 'git-clone' && !item.permissions?.exec?.length) {
|
|
134
|
+
checks.push({
|
|
135
|
+
name: 'permission_consistency',
|
|
136
|
+
label: 'Permission consistency',
|
|
137
|
+
status: 'warn',
|
|
138
|
+
message: 'git-clone install runs shell commands; declare exec'
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
else if (kind === 'mcp-config-patch' && item.npmPackage && !item.permissions?.exec?.length) {
|
|
142
|
+
checks.push({
|
|
143
|
+
name: 'permission_consistency',
|
|
144
|
+
label: 'Permission consistency',
|
|
145
|
+
status: 'warn',
|
|
146
|
+
message: 'npx invocation runs binaries; declare exec'
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
else {
|
|
150
|
+
checks.push({
|
|
151
|
+
name: 'permission_consistency',
|
|
152
|
+
label: 'Permission consistency',
|
|
153
|
+
status: 'pass',
|
|
154
|
+
message: 'declared permissions match install kind'
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
// 3. repo_reachable (+ license_present when reachable)
|
|
158
|
+
if (item.repository) {
|
|
159
|
+
checks.push(...(await checkRepo(item, opts)));
|
|
160
|
+
}
|
|
161
|
+
// 4. npm_exists
|
|
162
|
+
if (item.npmPackage) {
|
|
163
|
+
checks.push(await checkNpmExists(item, opts));
|
|
164
|
+
}
|
|
165
|
+
// 5. recently_active
|
|
166
|
+
if (item.pushedAt) {
|
|
167
|
+
const now = opts.now ? opts.now() : new Date();
|
|
168
|
+
const days = Math.floor((now.getTime() - new Date(item.pushedAt).getTime()) / 86_400_000);
|
|
169
|
+
if (days <= 365) {
|
|
170
|
+
checks.push({
|
|
171
|
+
name: 'recently_active',
|
|
172
|
+
label: 'Recently active',
|
|
173
|
+
status: 'pass',
|
|
174
|
+
message: `pushed ${days}d ago`
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
else {
|
|
178
|
+
checks.push({
|
|
179
|
+
name: 'recently_active',
|
|
180
|
+
label: 'Recently active',
|
|
181
|
+
status: 'warn',
|
|
182
|
+
message: `last push ${days}d ago — may be unmaintained`
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
// 6. flag_count_low
|
|
187
|
+
const flags = item.flagCount ?? 0;
|
|
188
|
+
if (flags < 3) {
|
|
189
|
+
checks.push({
|
|
190
|
+
name: 'flag_count_low',
|
|
191
|
+
label: 'Flag count low',
|
|
192
|
+
status: 'pass',
|
|
193
|
+
message: `${flags} flags`
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
else if (flags < 10) {
|
|
197
|
+
checks.push({
|
|
198
|
+
name: 'flag_count_low',
|
|
199
|
+
label: 'Flag count low',
|
|
200
|
+
status: 'warn',
|
|
201
|
+
message: `${flags} flags — under review threshold`
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
else {
|
|
205
|
+
checks.push({
|
|
206
|
+
name: 'flag_count_low',
|
|
207
|
+
label: 'Flag count low',
|
|
208
|
+
status: 'fail',
|
|
209
|
+
message: `${flags} flags — would auto-hide`
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
return checks;
|
|
213
|
+
}
|
|
214
|
+
export async function scanItem(item, opts = {}) {
|
|
215
|
+
let checks;
|
|
216
|
+
if (item.kind === 'workflow') {
|
|
217
|
+
const n = item.flagCount ?? 0;
|
|
218
|
+
checks = [
|
|
219
|
+
{
|
|
220
|
+
name: 'workflow_kind',
|
|
221
|
+
label: 'Workflow kind',
|
|
222
|
+
status: 'pass',
|
|
223
|
+
message: 'Workflow items are inert prompts — no install side effects to scan.'
|
|
224
|
+
},
|
|
225
|
+
{
|
|
226
|
+
name: 'flag_count_low',
|
|
227
|
+
label: 'Flag count low',
|
|
228
|
+
status: n < 3 ? 'pass' : n < 10 ? 'warn' : 'fail',
|
|
229
|
+
message: n < 3
|
|
230
|
+
? `${n} flags`
|
|
231
|
+
: n < 10
|
|
232
|
+
? `${n} flags — under review threshold`
|
|
233
|
+
: `${n} flags — would auto-hide`
|
|
234
|
+
}
|
|
235
|
+
];
|
|
236
|
+
}
|
|
237
|
+
else {
|
|
238
|
+
checks = await scanPackage(item, opts);
|
|
239
|
+
}
|
|
240
|
+
return {
|
|
241
|
+
id: item.id,
|
|
242
|
+
itemKind: item.kind,
|
|
243
|
+
checks,
|
|
244
|
+
summary: tally(checks)
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
//# sourceMappingURL=scan.js.map
|
package/dist/scan.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scan.js","sourceRoot":"","sources":["../src/scan.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Offline BM25 inverted-index for the Agora marketplace catalog.
|
|
3
|
+
*
|
|
4
|
+
* ## Why BM25?
|
|
5
|
+
* BM25 (Best Match 25) is a probabilistic term-frequency / inverse-document-frequency
|
|
6
|
+
* ranking function that handles two key issues with naive TF-IDF:
|
|
7
|
+
* 1. Term-frequency saturation — a token appearing 100× in a doc is not 100× as
|
|
8
|
+
* relevant as one appearing once; BM25 saturates via (tf / (tf + k1*(1-b+b*dl/avgdl))).
|
|
9
|
+
* 2. Document-length normalization — long documents naturally accumulate more term
|
|
10
|
+
* hits; the `b` parameter (0–1) penalizes them proportionally.
|
|
11
|
+
*
|
|
12
|
+
* ## Field weighting via token repetition
|
|
13
|
+
* Rather than maintaining separate per-field inverted lists (which complicates the
|
|
14
|
+
* index structure), we inflate the term-frequency bag at index time:
|
|
15
|
+
* - name token contributes ×3 to TF (most signal: users search by name)
|
|
16
|
+
* - tags token contributes ×2 (curated semantic labels)
|
|
17
|
+
* - id token contributes ×2 (IDs are precise matches)
|
|
18
|
+
* - description/author/category ×1 (background context)
|
|
19
|
+
*
|
|
20
|
+
* This is equivalent to weighted field merging and requires zero extra complexity in
|
|
21
|
+
* the scoring loop.
|
|
22
|
+
*
|
|
23
|
+
* ## Fully offline, zero dependencies
|
|
24
|
+
* Matches Wave 1 "no external accounts / no network" constraint. The index is
|
|
25
|
+
* rebuilt in-memory from the item list; it never persists to disk and needs no
|
|
26
|
+
* embedding model or API key.
|
|
27
|
+
*/
|
|
28
|
+
export interface IndexableItem {
|
|
29
|
+
id: string;
|
|
30
|
+
name: string;
|
|
31
|
+
description: string;
|
|
32
|
+
author: string;
|
|
33
|
+
category: string;
|
|
34
|
+
tags: string[];
|
|
35
|
+
}
|
|
36
|
+
/** Per-document posting entry stored in the inverted list. */
|
|
37
|
+
interface Posting {
|
|
38
|
+
id: string;
|
|
39
|
+
tf: number;
|
|
40
|
+
}
|
|
41
|
+
export interface CatalogIndex {
|
|
42
|
+
/** postings[term] = array of {id, tf} for every doc containing that term */
|
|
43
|
+
postings: Map<string, Posting[]>;
|
|
44
|
+
/** df[term] = number of distinct documents containing that term */
|
|
45
|
+
df: Map<string, number>;
|
|
46
|
+
/** docLen[id] = sum of all weighted TF values for that document */
|
|
47
|
+
docLen: Map<string, number>;
|
|
48
|
+
/** average document length across all indexed documents */
|
|
49
|
+
avgDocLen: number;
|
|
50
|
+
/** total number of indexed documents */
|
|
51
|
+
N: number;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Combined English stopwords + intent/filler words.
|
|
55
|
+
* Intent words are stripped so that queries like
|
|
56
|
+
* "find a tool that talks to postgres" reduce to their content terms.
|
|
57
|
+
*/
|
|
58
|
+
export declare const STOPWORDS: Set<string>;
|
|
59
|
+
/**
|
|
60
|
+
* Tokenize text for indexing or querying:
|
|
61
|
+
* - lowercase
|
|
62
|
+
* - split on non-alphanumeric runs
|
|
63
|
+
* - drop tokens shorter than 2 characters
|
|
64
|
+
* - drop STOPWORDS
|
|
65
|
+
*
|
|
66
|
+
* Deterministic: identical input always yields identical output.
|
|
67
|
+
*/
|
|
68
|
+
export declare function tokenize(text: string): string[];
|
|
69
|
+
/**
|
|
70
|
+
* Developer-term synonym map. Applied ONLY during query tokenization, not at
|
|
71
|
+
* index time, so documents are indexed verbatim (no spurious term inflation).
|
|
72
|
+
*
|
|
73
|
+
* When a query token matches a key, its synonyms are added as additional query
|
|
74
|
+
* terms (deduped). This allows short aliases ("db", "k8s") to match their full
|
|
75
|
+
* forms in document text.
|
|
76
|
+
*/
|
|
77
|
+
export declare const SYNONYMS: Record<string, string[]>;
|
|
78
|
+
/**
|
|
79
|
+
* Tokenize a query string and expand synonyms.
|
|
80
|
+
* Returns a deduplicated array of tokens (original + synonym expansions).
|
|
81
|
+
*/
|
|
82
|
+
export declare function tokenizeQuery(text: string): string[];
|
|
83
|
+
/**
|
|
84
|
+
* Build a BM25 inverted index from a list of indexable items.
|
|
85
|
+
*
|
|
86
|
+
* Field weighting is achieved by repeating tokens in the TF bag:
|
|
87
|
+
* name ×3, tags ×2, id ×2, description ×1, author ×1, category ×1
|
|
88
|
+
*/
|
|
89
|
+
export declare function buildIndex(items: IndexableItem[]): CatalogIndex;
|
|
90
|
+
/**
|
|
91
|
+
* Search the index with BM25 scoring.
|
|
92
|
+
*
|
|
93
|
+
* @param index - built by buildIndex()
|
|
94
|
+
* @param query - raw query string (will be tokenized + synonym-expanded)
|
|
95
|
+
* @param opts - BM25 hyperparameters (defaults: k1=1.5, b=0.75)
|
|
96
|
+
* @returns - scored results sorted by score descending; empty array if
|
|
97
|
+
* query is blank or no documents match
|
|
98
|
+
*/
|
|
99
|
+
export declare function searchIndex(index: CatalogIndex, query: string, opts?: {
|
|
100
|
+
k1?: number;
|
|
101
|
+
b?: number;
|
|
102
|
+
}): {
|
|
103
|
+
id: string;
|
|
104
|
+
score: number;
|
|
105
|
+
}[];
|
|
106
|
+
export {};
|
|
107
|
+
//# sourceMappingURL=catalog-index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"catalog-index.d.ts","sourceRoot":"","sources":["../../src/search/catalog-index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAEH,MAAM,WAAW,aAAa;IAC5B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,EAAE,CAAC;CAChB;AAED,8DAA8D;AAC9D,UAAU,OAAO;IACf,EAAE,EAAE,MAAM,CAAC;IACX,EAAE,EAAE,MAAM,CAAC;CACZ;AAED,MAAM,WAAW,YAAY;IAC3B,4EAA4E;IAC5E,QAAQ,EAAE,GAAG,CAAC,MAAM,EAAE,OAAO,EAAE,CAAC,CAAC;IACjC,mEAAmE;IACnE,EAAE,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACxB,mEAAmE;IACnE,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC5B,2DAA2D;IAC3D,SAAS,EAAE,MAAM,CAAC;IAClB,wCAAwC;IACxC,CAAC,EAAE,MAAM,CAAC;CACX;AAID;;;;GAIG;AACH,eAAO,MAAM,SAAS,EAAE,GAAG,CAAC,MAAM,CAiFhC,CAAC;AAIH;;;;;;;;GAQG;AACH,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,CAK/C;AAID;;;;;;;GAOG;AACH,eAAO,MAAM,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAsB7C,CAAC;AAEF;;;GAGG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,CAYpD;AAID;;;;;GAKG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE,aAAa,EAAE,GAAG,YAAY,CAqD/D;AAID;;;;;;;;GAQG;AACH,wBAAgB,WAAW,CACzB,KAAK,EAAE,YAAY,EACnB,KAAK,EAAE,MAAM,EACb,IAAI,CAAC,EAAE;IAAE,EAAE,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,CAAC,EAAE,MAAM,CAAA;CAAE,GACjC;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,EAAE,CAoCjC"}
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Offline BM25 inverted-index for the Agora marketplace catalog.
|
|
3
|
+
*
|
|
4
|
+
* ## Why BM25?
|
|
5
|
+
* BM25 (Best Match 25) is a probabilistic term-frequency / inverse-document-frequency
|
|
6
|
+
* ranking function that handles two key issues with naive TF-IDF:
|
|
7
|
+
* 1. Term-frequency saturation — a token appearing 100× in a doc is not 100× as
|
|
8
|
+
* relevant as one appearing once; BM25 saturates via (tf / (tf + k1*(1-b+b*dl/avgdl))).
|
|
9
|
+
* 2. Document-length normalization — long documents naturally accumulate more term
|
|
10
|
+
* hits; the `b` parameter (0–1) penalizes them proportionally.
|
|
11
|
+
*
|
|
12
|
+
* ## Field weighting via token repetition
|
|
13
|
+
* Rather than maintaining separate per-field inverted lists (which complicates the
|
|
14
|
+
* index structure), we inflate the term-frequency bag at index time:
|
|
15
|
+
* - name token contributes ×3 to TF (most signal: users search by name)
|
|
16
|
+
* - tags token contributes ×2 (curated semantic labels)
|
|
17
|
+
* - id token contributes ×2 (IDs are precise matches)
|
|
18
|
+
* - description/author/category ×1 (background context)
|
|
19
|
+
*
|
|
20
|
+
* This is equivalent to weighted field merging and requires zero extra complexity in
|
|
21
|
+
* the scoring loop.
|
|
22
|
+
*
|
|
23
|
+
* ## Fully offline, zero dependencies
|
|
24
|
+
* Matches Wave 1 "no external accounts / no network" constraint. The index is
|
|
25
|
+
* rebuilt in-memory from the item list; it never persists to disk and needs no
|
|
26
|
+
* embedding model or API key.
|
|
27
|
+
*/
|
|
28
|
+
// ── Stopwords ─────────────────────────────────────────────────────────────────
|
|
29
|
+
/**
|
|
30
|
+
* Combined English stopwords + intent/filler words.
|
|
31
|
+
* Intent words are stripped so that queries like
|
|
32
|
+
* "find a tool that talks to postgres" reduce to their content terms.
|
|
33
|
+
*/
|
|
34
|
+
export const STOPWORDS = new Set([
|
|
35
|
+
// English function words
|
|
36
|
+
'the',
|
|
37
|
+
'a',
|
|
38
|
+
'an',
|
|
39
|
+
'to',
|
|
40
|
+
'of',
|
|
41
|
+
'for',
|
|
42
|
+
'and',
|
|
43
|
+
'or',
|
|
44
|
+
'with',
|
|
45
|
+
'that',
|
|
46
|
+
'this',
|
|
47
|
+
'my',
|
|
48
|
+
'i',
|
|
49
|
+
'in',
|
|
50
|
+
'on',
|
|
51
|
+
'is',
|
|
52
|
+
'are',
|
|
53
|
+
'it',
|
|
54
|
+
'by',
|
|
55
|
+
'at',
|
|
56
|
+
'as',
|
|
57
|
+
'be',
|
|
58
|
+
'was',
|
|
59
|
+
'has',
|
|
60
|
+
'have',
|
|
61
|
+
'not',
|
|
62
|
+
'its',
|
|
63
|
+
'from',
|
|
64
|
+
'into',
|
|
65
|
+
'than',
|
|
66
|
+
'but',
|
|
67
|
+
'about',
|
|
68
|
+
// Intent / filler words common in natural-language queries
|
|
69
|
+
'find',
|
|
70
|
+
'search',
|
|
71
|
+
'show',
|
|
72
|
+
'get',
|
|
73
|
+
'need',
|
|
74
|
+
'want',
|
|
75
|
+
'looking',
|
|
76
|
+
'something',
|
|
77
|
+
'anything',
|
|
78
|
+
'tool',
|
|
79
|
+
'tools',
|
|
80
|
+
'thing',
|
|
81
|
+
'things',
|
|
82
|
+
'does',
|
|
83
|
+
'do',
|
|
84
|
+
'help',
|
|
85
|
+
'please',
|
|
86
|
+
'can',
|
|
87
|
+
'give',
|
|
88
|
+
'use',
|
|
89
|
+
'using',
|
|
90
|
+
'used',
|
|
91
|
+
'like',
|
|
92
|
+
'also',
|
|
93
|
+
'which',
|
|
94
|
+
'how',
|
|
95
|
+
'what',
|
|
96
|
+
'where',
|
|
97
|
+
'when',
|
|
98
|
+
'why',
|
|
99
|
+
'who',
|
|
100
|
+
'some',
|
|
101
|
+
'any',
|
|
102
|
+
'all',
|
|
103
|
+
'talks',
|
|
104
|
+
'talk',
|
|
105
|
+
'connect',
|
|
106
|
+
'connects',
|
|
107
|
+
'access',
|
|
108
|
+
'accesses',
|
|
109
|
+
'let',
|
|
110
|
+
'lets',
|
|
111
|
+
'work',
|
|
112
|
+
'works',
|
|
113
|
+
'support',
|
|
114
|
+
'supports'
|
|
115
|
+
]);
|
|
116
|
+
// ── Tokenizer ─────────────────────────────────────────────────────────────────
|
|
117
|
+
/**
|
|
118
|
+
* Tokenize text for indexing or querying:
|
|
119
|
+
* - lowercase
|
|
120
|
+
* - split on non-alphanumeric runs
|
|
121
|
+
* - drop tokens shorter than 2 characters
|
|
122
|
+
* - drop STOPWORDS
|
|
123
|
+
*
|
|
124
|
+
* Deterministic: identical input always yields identical output.
|
|
125
|
+
*/
|
|
126
|
+
export function tokenize(text) {
|
|
127
|
+
return text
|
|
128
|
+
.toLowerCase()
|
|
129
|
+
.split(/[^a-z0-9]+/)
|
|
130
|
+
.filter((t) => t.length >= 2 && !STOPWORDS.has(t));
|
|
131
|
+
}
|
|
132
|
+
// ── Synonyms (query-side expansion only) ─────────────────────────────────────
|
|
133
|
+
/**
|
|
134
|
+
* Developer-term synonym map. Applied ONLY during query tokenization, not at
|
|
135
|
+
* index time, so documents are indexed verbatim (no spurious term inflation).
|
|
136
|
+
*
|
|
137
|
+
* When a query token matches a key, its synonyms are added as additional query
|
|
138
|
+
* terms (deduped). This allows short aliases ("db", "k8s") to match their full
|
|
139
|
+
* forms in document text.
|
|
140
|
+
*/
|
|
141
|
+
export const SYNONYMS = {
|
|
142
|
+
db: ['database'],
|
|
143
|
+
k8s: ['kubernetes'],
|
|
144
|
+
k8: ['kubernetes'],
|
|
145
|
+
postgres: ['postgresql', 'database'],
|
|
146
|
+
pg: ['postgresql', 'database'],
|
|
147
|
+
js: ['javascript'],
|
|
148
|
+
ts: ['typescript'],
|
|
149
|
+
ai: ['llm'],
|
|
150
|
+
auth: ['authentication'],
|
|
151
|
+
vcs: ['git'],
|
|
152
|
+
ml: ['machine', 'learning'],
|
|
153
|
+
api: ['integration'],
|
|
154
|
+
cli: ['command'],
|
|
155
|
+
ui: ['interface'],
|
|
156
|
+
gh: ['github'],
|
|
157
|
+
gl: ['gitlab'],
|
|
158
|
+
aws: ['amazon'],
|
|
159
|
+
gcp: ['google'],
|
|
160
|
+
kv: ['cache'],
|
|
161
|
+
nosql: ['database'],
|
|
162
|
+
sql: ['database']
|
|
163
|
+
};
|
|
164
|
+
/**
|
|
165
|
+
* Tokenize a query string and expand synonyms.
|
|
166
|
+
* Returns a deduplicated array of tokens (original + synonym expansions).
|
|
167
|
+
*/
|
|
168
|
+
export function tokenizeQuery(text) {
|
|
169
|
+
const base = tokenize(text);
|
|
170
|
+
const expanded = new Set(base);
|
|
171
|
+
for (const token of base) {
|
|
172
|
+
const syns = SYNONYMS[token];
|
|
173
|
+
if (syns) {
|
|
174
|
+
for (const syn of syns) {
|
|
175
|
+
expanded.add(syn);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
return Array.from(expanded);
|
|
180
|
+
}
|
|
181
|
+
// ── Index builder ─────────────────────────────────────────────────────────────
|
|
182
|
+
/**
|
|
183
|
+
* Build a BM25 inverted index from a list of indexable items.
|
|
184
|
+
*
|
|
185
|
+
* Field weighting is achieved by repeating tokens in the TF bag:
|
|
186
|
+
* name ×3, tags ×2, id ×2, description ×1, author ×1, category ×1
|
|
187
|
+
*/
|
|
188
|
+
export function buildIndex(items) {
|
|
189
|
+
const postings = new Map();
|
|
190
|
+
const df = new Map();
|
|
191
|
+
const docLen = new Map();
|
|
192
|
+
let totalLen = 0;
|
|
193
|
+
for (const item of items) {
|
|
194
|
+
// Build weighted TF bag for this document
|
|
195
|
+
const tfBag = new Map();
|
|
196
|
+
function addTokens(text, weight) {
|
|
197
|
+
for (const token of tokenize(text)) {
|
|
198
|
+
tfBag.set(token, (tfBag.get(token) ?? 0) + weight);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
addTokens(item.name, 3);
|
|
202
|
+
for (const tag of item.tags) {
|
|
203
|
+
addTokens(tag, 2);
|
|
204
|
+
}
|
|
205
|
+
addTokens(item.id, 2);
|
|
206
|
+
addTokens(item.description, 1);
|
|
207
|
+
addTokens(item.author, 1);
|
|
208
|
+
addTokens(item.category, 1);
|
|
209
|
+
// Document length = sum of weighted TFs
|
|
210
|
+
let dl = 0;
|
|
211
|
+
for (const tf of tfBag.values()) {
|
|
212
|
+
dl += tf;
|
|
213
|
+
}
|
|
214
|
+
docLen.set(item.id, dl);
|
|
215
|
+
totalLen += dl;
|
|
216
|
+
// Update postings and DF
|
|
217
|
+
for (const [term, tf] of tfBag.entries()) {
|
|
218
|
+
// DF: count this document once per term
|
|
219
|
+
df.set(term, (df.get(term) ?? 0) + 1);
|
|
220
|
+
// Postings list
|
|
221
|
+
let list = postings.get(term);
|
|
222
|
+
if (!list) {
|
|
223
|
+
list = [];
|
|
224
|
+
postings.set(term, list);
|
|
225
|
+
}
|
|
226
|
+
list.push({ id: item.id, tf });
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
const N = items.length;
|
|
230
|
+
const avgDocLen = N > 0 ? totalLen / N : 1;
|
|
231
|
+
return { postings, df, docLen, avgDocLen, N };
|
|
232
|
+
}
|
|
233
|
+
// ── BM25 scorer ───────────────────────────────────────────────────────────────
|
|
234
|
+
/**
|
|
235
|
+
* Search the index with BM25 scoring.
|
|
236
|
+
*
|
|
237
|
+
* @param index - built by buildIndex()
|
|
238
|
+
* @param query - raw query string (will be tokenized + synonym-expanded)
|
|
239
|
+
* @param opts - BM25 hyperparameters (defaults: k1=1.5, b=0.75)
|
|
240
|
+
* @returns - scored results sorted by score descending; empty array if
|
|
241
|
+
* query is blank or no documents match
|
|
242
|
+
*/
|
|
243
|
+
export function searchIndex(index, query, opts) {
|
|
244
|
+
if (!query || !query.trim())
|
|
245
|
+
return [];
|
|
246
|
+
const k1 = opts?.k1 ?? 1.5;
|
|
247
|
+
const b = opts?.b ?? 0.75;
|
|
248
|
+
const { postings, df, docLen, avgDocLen, N } = index;
|
|
249
|
+
const queryTokens = tokenizeQuery(query);
|
|
250
|
+
if (queryTokens.length === 0)
|
|
251
|
+
return [];
|
|
252
|
+
// Accumulate scores per document
|
|
253
|
+
const scores = new Map();
|
|
254
|
+
for (const term of queryTokens) {
|
|
255
|
+
const list = postings.get(term);
|
|
256
|
+
if (!list)
|
|
257
|
+
continue;
|
|
258
|
+
const termDf = df.get(term) ?? 0;
|
|
259
|
+
// IDF with smoothing (Robertson-Sparck Jones):
|
|
260
|
+
// idf = log((N - df + 0.5) / (df + 0.5) + 1)
|
|
261
|
+
const idf = Math.log((N - termDf + 0.5) / (termDf + 0.5) + 1);
|
|
262
|
+
for (const { id, tf } of list) {
|
|
263
|
+
const dl = docLen.get(id) ?? avgDocLen;
|
|
264
|
+
const norm = tf / (tf + k1 * (1 - b + b * (dl / avgDocLen)));
|
|
265
|
+
const contribution = idf * norm;
|
|
266
|
+
scores.set(id, (scores.get(id) ?? 0) + contribution);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
if (scores.size === 0)
|
|
270
|
+
return [];
|
|
271
|
+
return Array.from(scores.entries())
|
|
272
|
+
.filter(([, score]) => score > 0)
|
|
273
|
+
.map(([id, score]) => ({ id, score }))
|
|
274
|
+
.sort((a, b) => b.score - a.score);
|
|
275
|
+
}
|
|
276
|
+
//# sourceMappingURL=catalog-index.js.map
|