openmerit 0.1.12 → 0.3.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/CHANGELOG.md +14 -0
- package/README.md +11 -27
- package/dist/pi/scout-extension.mjs +23 -10
- package/dist/terminal/scout-update.json +1 -1
- package/dist/terminal/src/browser.mjs +2 -4
- package/dist/terminal/src/cli.mjs +6 -56
- package/dist/terminal/src/install.mjs +3 -2
- package/dist/terminal/src/scout-instructions.js +23 -10
- package/dist/terminal/src/scout-pi.js +1 -1
- package/dist/terminal/src/scout.js +181 -0
- package/dist/terminal/src/telemetry.mjs +8 -60
- package/docs/agent-evaluations.md +8 -148
- package/docs/commands.md +4 -86
- package/docs/getting-started.md +2 -16
- package/docs/index.md +4 -27
- package/docs/inference-terminal.md +13 -655
- package/docs/pi-extension.md +1 -1
- package/docs/roadmap.md +4 -17
- package/docs/scout.md +34 -251
- package/docs/security.md +8 -33
- package/docs/testing.md +7 -47
- package/docs/troubleshooting.md +3 -3
- package/package.json +13 -46
- package/dist/core/src/index.d.ts +0 -101
- package/dist/core/src/index.js +0 -1649
- package/dist/core/src/store.d.ts +0 -35
- package/dist/core/src/store.js +0 -102
- package/dist/eval/src/adapters.mjs +0 -112
- package/dist/eval/src/cli.mjs +0 -151
- package/dist/eval/src/config.mjs +0 -108
- package/dist/eval/src/process.mjs +0 -44
- package/dist/eval/src/report.mjs +0 -59
- package/dist/eval/src/runner.mjs +0 -123
- package/dist/eval/src/scout.mjs +0 -48
- package/dist/protocol/src/index.d.ts +0 -484
- package/dist/protocol/src/index.js +0 -47
- package/dist/protocol/src/schemas.d.ts +0 -576
- package/dist/protocol/src/schemas.js +0 -280
- package/dist/terminal/public/app.js +0 -298
- package/dist/terminal/public/brands/anthropic.png +0 -0
- package/dist/terminal/public/brands/baai.png +0 -0
- package/dist/terminal/public/brands/baseten.png +0 -0
- package/dist/terminal/public/brands/cerebras.png +0 -0
- package/dist/terminal/public/brands/cohere.png +0 -0
- package/dist/terminal/public/brands/deepseek.ico +0 -0
- package/dist/terminal/public/brands/google.png +0 -0
- package/dist/terminal/public/brands/groq.ico +0 -0
- package/dist/terminal/public/brands/huggingface.svg +0 -8
- package/dist/terminal/public/brands/lm-studio.png +0 -0
- package/dist/terminal/public/brands/meta.ico +0 -0
- package/dist/terminal/public/brands/mistral.png +0 -0
- package/dist/terminal/public/brands/nomic.png +0 -0
- package/dist/terminal/public/brands/ollama.png +0 -0
- package/dist/terminal/public/brands/openai.png +0 -0
- package/dist/terminal/public/brands/openrouter.png +0 -0
- package/dist/terminal/public/brands/qwen.png +0 -0
- package/dist/terminal/public/brands/vllm.ico +0 -0
- package/dist/terminal/public/brands/vllm.png +0 -0
- package/dist/terminal/public/favicon.svg +0 -1
- package/dist/terminal/public/flow.css +0 -1
- package/dist/terminal/public/flow.js +0 -770
- package/dist/terminal/public/index.html +0 -22
- package/dist/terminal/public/styles.css +0 -1046
- package/dist/terminal/src/activity-merge.mjs +0 -66
- package/dist/terminal/src/collect.mjs +0 -370
- package/dist/terminal/src/coverage.mjs +0 -33
- package/dist/terminal/src/demo-policy.mjs +0 -205
- package/dist/terminal/src/discovery.mjs +0 -99
- package/dist/terminal/src/enterprise-sample.mjs +0 -151
- package/dist/terminal/src/hardware.mjs +0 -57
- package/dist/terminal/src/history.mjs +0 -32
- package/dist/terminal/src/log-continuity.mjs +0 -26
- package/dist/terminal/src/project-activity.mjs +0 -157
- package/dist/terminal/src/repository-background.mjs +0 -37
- package/dist/terminal/src/repository-index.mjs +0 -267
- package/dist/terminal/src/repository-policy.mjs +0 -3
- package/dist/terminal/src/repository-syntax.mjs +0 -236
- package/dist/terminal/src/repository-worker.mjs +0 -15
- package/dist/terminal/src/sample.mjs +0 -171
- package/dist/terminal/src/scout-credentials.d.ts +0 -12
- package/dist/terminal/src/scout-credentials.ts +0 -44
- package/dist/terminal/src/scout-instructions.d.ts +0 -5
- package/dist/terminal/src/scout-instructions.ts +0 -106
- package/dist/terminal/src/scout-pi.d.ts +0 -44
- package/dist/terminal/src/scout-pi.ts +0 -76
- package/dist/terminal/src/server.mjs +0 -95
- package/dist/terminal/src/service-registry.mjs +0 -78
- package/dist/terminal/src/services.mjs +0 -44
- package/dist/terminal/src/topology.mjs +0 -30
- package/dist/terminal/src/updates.d.ts +0 -62
- package/dist/terminal/src/updates.ts +0 -313
- package/docs/adapter-guide.md +0 -191
- package/docs/architecture.md +0 -61
- package/docs/automation.md +0 -76
- package/docs/budgets.md +0 -39
- package/docs/demo-backfill.md +0 -31
- package/docs/demo-fieldkit.md +0 -49
- package/docs/demo-placement.md +0 -32
- package/docs/demo-spam.md +0 -17
- package/docs/demo-support.md +0 -44
- package/docs/demo.md +0 -74
- package/docs/first-trial.md +0 -62
- package/docs/interactive-demo.md +0 -118
- package/docs/lifecycle.md +0 -32
- package/docs/metrics-and-evidence.md +0 -48
- package/docs/operations.md +0 -40
- package/docs/pareto-spec.md +0 -76
- package/docs/ux-reference.md +0 -32
|
@@ -1,157 +0,0 @@
|
|
|
1
|
-
import { opendir, lstat, open, realpath } from 'node:fs/promises';
|
|
2
|
-
import { constants } from 'node:fs';
|
|
3
|
-
import { join, relative, sep } from 'node:path';
|
|
4
|
-
import { createHash } from 'node:crypto';
|
|
5
|
-
import { StringDecoder } from 'node:string_decoder';
|
|
6
|
-
import { checkpointForRead, checkpointMatches } from './log-continuity.mjs';
|
|
7
|
-
|
|
8
|
-
const FILE_BYTES = 512 * 1024, TOTAL_BYTES = 4 * 1024 * 1024;
|
|
9
|
-
const MAX_FILES = 128, MAX_ENTRIES = 2048, MAX_RECORDS = 10000;
|
|
10
|
-
const finite = value => typeof value === 'number' && Number.isFinite(value) && value >= 0;
|
|
11
|
-
const object = value => value && typeof value === 'object' && !Array.isArray(value);
|
|
12
|
-
const id = value => typeof value === 'string' && value.length > 0 && value.length <= 160;
|
|
13
|
-
const hash = value => createHash('sha256').update(value).digest('hex').slice(0, 24);
|
|
14
|
-
|
|
15
|
-
// Recognize recorded request objects, not arbitrary JSON or a model catalog.
|
|
16
|
-
// No source-code execution, prompt interpretation, or filename-based providers.
|
|
17
|
-
export function projectActivityRecord(value, sourceId, file, normalize) {
|
|
18
|
-
if (!object(value)) return null;
|
|
19
|
-
const body = object(value.response) ? value.response : value;
|
|
20
|
-
const usage = body.usage;
|
|
21
|
-
const responses = body.object === 'response';
|
|
22
|
-
const chat = body.object === 'chat.completion';
|
|
23
|
-
const structured = !responses && !chat && id(value.id) && typeof value.occurredAt === 'string';
|
|
24
|
-
if (!responses && !chat && !structured) return null;
|
|
25
|
-
const measured = object(usage) && [usage.input_tokens, usage.output_tokens, usage.prompt_tokens, usage.completion_tokens].some(finite);
|
|
26
|
-
const terminal = ['completed', 'failed', 'running'].includes(value.status);
|
|
27
|
-
if (!measured && !(structured && terminal && (finite(value.httpStatus) || value.event === 'inference.request'))) return null;
|
|
28
|
-
const requestId = structured ? value.requestId : body.id;
|
|
29
|
-
const recordId = structured ? value.id : body.id;
|
|
30
|
-
if (!id(recordId) || !id(body.model ?? value.model)) return null;
|
|
31
|
-
let occurredAt = value.occurredAt;
|
|
32
|
-
if (!occurredAt && (responses || chat)) {
|
|
33
|
-
const seconds = responses ? body.created_at : body.created;
|
|
34
|
-
if (finite(seconds) && seconds < 8640000000000) occurredAt = new Date(seconds * 1000).toISOString();
|
|
35
|
-
}
|
|
36
|
-
if (typeof occurredAt !== 'string' || !Number.isFinite(Date.parse(occurredAt))) return null;
|
|
37
|
-
// An API-compatible response shape doesn't prove which company served it.
|
|
38
|
-
// Connection, workload, and agent/task fields must be explicitly recorded.
|
|
39
|
-
const status = structured ? value.status : responses
|
|
40
|
-
? ({ completed: 'completed', failed: 'failed', cancelled: 'failed', incomplete: 'failed', in_progress: 'running', queued: 'running' })[body.status]
|
|
41
|
-
: body.choices?.length && body.choices.every(choice => typeof choice.finish_reason === 'string')
|
|
42
|
-
? body.choices.some(choice => ['error', 'content_filter'].includes(choice.finish_reason)) ? 'failed' : 'completed' : null;
|
|
43
|
-
if (!['completed', 'failed', 'running'].includes(status)) return null;
|
|
44
|
-
const record = normalize({
|
|
45
|
-
...value, id: recordId, requestId, occurredAt, status, model: body.model ?? value.model,
|
|
46
|
-
provider: value.provider ?? body.provider,
|
|
47
|
-
workload: value.workload || value.application || file,
|
|
48
|
-
inputTokens: structured && finite(value.inputTokens) ? value.inputTokens : usage?.input_tokens ?? usage?.prompt_tokens,
|
|
49
|
-
outputTokens: structured && finite(value.outputTokens) ? value.outputTokens : usage?.output_tokens ?? usage?.completion_tokens,
|
|
50
|
-
cacheReadTokens: structured && finite(value.cacheReadTokens) ? value.cacheReadTokens : usage?.input_tokens_details?.cached_tokens ?? usage?.prompt_tokens_details?.cached_tokens,
|
|
51
|
-
cost: finite(value.cost) ? value.cost : usage?.cost,
|
|
52
|
-
}, sourceId);
|
|
53
|
-
return record && { ...record, evidenceFormat: responses ? 'responses' : chat ? 'chat-completion' : 'structured-request' };
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
export function createProjectActivity(root, normalize, { exclude = [] } = {}) {
|
|
57
|
-
const readers = new Map();
|
|
58
|
-
return async function collect() {
|
|
59
|
-
const files = [], issues = []; let visited = 0, omitted = false, budget = TOTAL_BYTES;
|
|
60
|
-
const canonicalRoot = await realpath(root);
|
|
61
|
-
const excludedPaths = exclude.map(path => join(canonicalRoot, relative(root, path)));
|
|
62
|
-
const allowed = path => path.startsWith(canonicalRoot + sep);
|
|
63
|
-
async function scan(path, depth) {
|
|
64
|
-
let directory;
|
|
65
|
-
try {
|
|
66
|
-
const info = await lstat(path);
|
|
67
|
-
if (!info.isDirectory() || info.isSymbolicLink() || await realpath(path) !== path) return;
|
|
68
|
-
directory = await opendir(path);
|
|
69
|
-
for await (const entry of directory) {
|
|
70
|
-
if (++visited > MAX_ENTRIES) { omitted = true; break; }
|
|
71
|
-
if (entry.name.startsWith('.') || entry.isSymbolicLink()) continue;
|
|
72
|
-
const child = join(path, entry.name);
|
|
73
|
-
if (entry.isDirectory() && depth > 0) await scan(child, depth - 1);
|
|
74
|
-
else if (entry.isFile() && /\.jsonl(?:\.\d+)?$/.test(entry.name) && !excludedPaths.includes(child)) {
|
|
75
|
-
const info = await lstat(child);
|
|
76
|
-
if (info.isFile() && !info.isSymbolicLink()) files.push({ path: child, info });
|
|
77
|
-
}
|
|
78
|
-
if (visited >= MAX_ENTRIES) { omitted = true; break; }
|
|
79
|
-
}
|
|
80
|
-
} catch (error) { if (error.code !== 'ENOENT') issues.push(relative(canonicalRoot, path)); }
|
|
81
|
-
}
|
|
82
|
-
// Fixed project-local log roots. Never recursively inspect the repository,
|
|
83
|
-
// home directory, dependency trees, credentials, or arbitrary databases.
|
|
84
|
-
for (const name of ['logs', 'runs', 'results']) {
|
|
85
|
-
if (visited >= MAX_ENTRIES) break;
|
|
86
|
-
await scan(join(canonicalRoot, name), 2);
|
|
87
|
-
}
|
|
88
|
-
files.sort((a, b) => b.info.mtimeMs - a.info.mtimeMs || a.path.localeCompare(b.path));
|
|
89
|
-
if (files.length > MAX_FILES) omitted = true;
|
|
90
|
-
const chosen = files.slice(0, MAX_FILES), active = new Set(chosen.map(file => file.path));
|
|
91
|
-
for (const path of readers.keys()) if (!active.has(path)) readers.delete(path);
|
|
92
|
-
const sources = [], records = [];
|
|
93
|
-
for (const file of chosen) {
|
|
94
|
-
const name = relative(canonicalRoot, file.path), sourceId = `project-log:${hash(name)}`;
|
|
95
|
-
const source = { id: sourceId, name: 'Project request log', kind: 'Project activity', detail: name,
|
|
96
|
-
access: 'Selected project', coverage: ['Recorded requests', 'Reported identities', 'Tokens', 'Reported or estimated cost'],
|
|
97
|
-
note: 'Supported request metadata read from an existing JSONL log. Names and measurements are copied from recorded fields. Prompts, outputs, headers, and credentials are discarded. No account API or inference call is made.' };
|
|
98
|
-
try {
|
|
99
|
-
if (!allowed(file.path) || await realpath(file.path) !== file.path) throw new Error('Outside project or symlink');
|
|
100
|
-
const handle = await open(file.path, constants.O_RDONLY | (constants.O_NOFOLLOW || 0) | (constants.O_NONBLOCK || 0));
|
|
101
|
-
try {
|
|
102
|
-
const info = await handle.stat();
|
|
103
|
-
if (!info.isFile()) throw new Error('Not a regular file');
|
|
104
|
-
let reader = readers.get(file.path);
|
|
105
|
-
const identity = `${info.dev}:${info.ino}`;
|
|
106
|
-
if (!reader || reader.identity !== identity || info.size < reader.offset || info.size === reader.offset && info.mtimeMs !== reader.mtime || !await checkpointMatches(handle, reader.checkpoint)) {
|
|
107
|
-
reader = { identity, offset: 0, mtime: 0, pending: '', decoder: new StringDecoder('utf8'), records: new Map(), invalid: 0, unsupported: 0, truncated: false, checkpoint: null };
|
|
108
|
-
}
|
|
109
|
-
const limit = Math.min(FILE_BYTES, budget);
|
|
110
|
-
if (limit === 0 && info.size > reader.offset) { omitted = true; sources.push({ ...source, state: 'partial', records: 0, note: source.note + ' Waiting for the next bounded read.' }); continue; }
|
|
111
|
-
let skip = false;
|
|
112
|
-
if (info.size - reader.offset > limit) { reader.offset = info.size - limit; reader.pending = ''; reader.decoder = new StringDecoder('utf8'); reader.records.clear(); reader.truncated = true; skip = true; }
|
|
113
|
-
const buffer = Buffer.alloc(Math.min(limit, info.size - reader.offset));
|
|
114
|
-
const { bytesRead } = await handle.read(buffer, 0, buffer.length, reader.offset);
|
|
115
|
-
reader.checkpoint = checkpointForRead(buffer.subarray(0, bytesRead), reader.offset, reader.checkpoint);
|
|
116
|
-
reader.offset += bytesRead; reader.mtime = info.mtimeMs; budget -= bytesRead;
|
|
117
|
-
const lines = (reader.pending + reader.decoder.write(buffer.subarray(0, bytesRead))).split('\n');
|
|
118
|
-
reader.pending = lines.pop() ?? ''; if (skip) lines.shift();
|
|
119
|
-
for (const line of lines) {
|
|
120
|
-
if (!line.trim()) continue;
|
|
121
|
-
try {
|
|
122
|
-
const record = projectActivityRecord(JSON.parse(line), sourceId, name, normalize);
|
|
123
|
-
if (!record) { reader.unsupported++; continue; }
|
|
124
|
-
reader.records.delete(record.id); reader.records.set(record.id, record);
|
|
125
|
-
} catch { reader.invalid++; }
|
|
126
|
-
}
|
|
127
|
-
if (reader.pending.length > 128 * 1024) { reader.pending = ''; reader.invalid++; }
|
|
128
|
-
while (reader.records.size > 1000) { reader.records.delete(reader.records.keys().next().value); reader.truncated = true; }
|
|
129
|
-
readers.set(file.path, reader);
|
|
130
|
-
const retained = [...reader.records.values()].filter(record => Date.parse(record.occurredAt) <= Date.now());
|
|
131
|
-
records.push(...retained);
|
|
132
|
-
// Even an unsupported log is visible as a coverage gap, never a
|
|
133
|
-
// silently successful connection with zero fabricated requests.
|
|
134
|
-
sources.push({ ...source, state: reader.invalid || reader.unsupported || reader.truncated ? 'partial' : 'connected', records: retained.length,
|
|
135
|
-
updatedAt: new Date().toISOString(), invalid: reader.invalid, unsupported: reader.unsupported, truncated: reader.truncated,
|
|
136
|
-
note: source.note + (reader.truncated ? ' The bounded reader omitted older records.' : '') + (reader.invalid || reader.unsupported ? ' Some lines were malformed or did not match a supported request format.' : '') });
|
|
137
|
-
} finally { await handle.close(); }
|
|
138
|
-
} catch { readers.delete(file.path); sources.push({ ...source, state: 'unavailable', records: 0 }); }
|
|
139
|
-
}
|
|
140
|
-
records.sort((a, b) => Date.parse(b.occurredAt) - Date.parse(a.occurredAt));
|
|
141
|
-
if (records.length > MAX_RECORDS) {
|
|
142
|
-
omitted = true;
|
|
143
|
-
const retained = new Set(records.slice(0, MAX_RECORDS));
|
|
144
|
-
for (const [path, reader] of readers) {
|
|
145
|
-
let removed = false;
|
|
146
|
-
for (const [id, record] of reader.records) if (!retained.has(record)) { reader.records.delete(id); removed = true; }
|
|
147
|
-
if (removed) {
|
|
148
|
-
reader.truncated = true;
|
|
149
|
-
const source = sources.find(source => source.detail === relative(canonicalRoot, path));
|
|
150
|
-
if (source) { source.state = 'partial'; source.records = reader.records.size; source.truncated = true; source.note += ' The shared 10,000-record cache limit omitted older records.'; }
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
if (omitted || issues.length) sources.push({ id: 'project-log-coverage', name: 'Project log coverage', kind: 'Project activity', state: 'partial', detail: 'logs/ · runs/ · results/', note: 'The bounded scan omitted files or records, or a log directory was unreadable. These sources do not establish complete project usage.' });
|
|
155
|
-
return { records: records.slice(0, MAX_RECORDS), sources };
|
|
156
|
-
};
|
|
157
|
-
}
|
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
import { randomUUID } from 'node:crypto';
|
|
2
|
-
import { Worker } from 'node:worker_threads';
|
|
3
|
-
import { emptyIndexSummary } from './repository-policy.mjs';
|
|
4
|
-
|
|
5
|
-
// The HTTP server never parses project code. A failed worker leaves the last
|
|
6
|
-
// usable index visible, explicitly marked incomplete, and retries next cycle.
|
|
7
|
-
export function createBackgroundIndex(root,{intervalMs=60_000,limits}={}) {
|
|
8
|
-
let worker=null,timer=null,closed=false,started=false,revision=0;
|
|
9
|
-
const session=randomUUID(),nextRevision=()=>`${session}:${++revision}`;
|
|
10
|
-
let current={references:[],issues:[],models:[],inventory:[],summary:emptyIndexSummary(),checkedAt:null,revision:`${session}:0`,progress:{phase:'queued',processed:0,total:null,discovered:0,parsed:0,reused:0,elapsedMs:0}};
|
|
11
|
-
const schedule=()=>{if(!closed){clearTimeout(timer);timer=setTimeout(run,intervalMs);timer.unref();}};
|
|
12
|
-
const failed=()=>{if(closed)return;current={...current,revision:nextRevision(),progress:{...current.progress,phase:'failed'}};schedule();};
|
|
13
|
-
function run() {
|
|
14
|
-
if(closed)return;
|
|
15
|
-
if(!worker) {
|
|
16
|
-
worker=new Worker(new URL('./repository-worker.mjs',import.meta.url),{workerData:{root,limits},resourceLimits:{maxOldGenerationSizeMb:256},execArgv:process.execArgv.filter(arg=>!arg.startsWith('--input-type'))});
|
|
17
|
-
worker.on('message',message=>{
|
|
18
|
-
if(closed)return;
|
|
19
|
-
if(message.type==='failed'){failed();return;}
|
|
20
|
-
if(message.type==='complete') {
|
|
21
|
-
current={...message.index,revision:nextRevision()};schedule();
|
|
22
|
-
}else {
|
|
23
|
-
current=message.index?{...message.index,revision:nextRevision()}:{...current,progress:message.progress};
|
|
24
|
-
}
|
|
25
|
-
});
|
|
26
|
-
worker.on('error',()=>{ /* Exit handles termination and the retry. */ });
|
|
27
|
-
worker.on('exit',()=>{worker=null;failed();});
|
|
28
|
-
worker.unref();
|
|
29
|
-
}
|
|
30
|
-
worker.postMessage('scan');
|
|
31
|
-
}
|
|
32
|
-
return {
|
|
33
|
-
start(){if(!started&&!closed){started=true;run();}},
|
|
34
|
-
get(){return current;},
|
|
35
|
-
async close(){closed=true;clearTimeout(timer);const active=worker;worker=null;if(active)await active.terminate();},
|
|
36
|
-
};
|
|
37
|
-
}
|
|
@@ -1,267 +0,0 @@
|
|
|
1
|
-
import { opendir, open, realpath } from 'node:fs/promises';
|
|
2
|
-
import { constants } from 'node:fs';
|
|
3
|
-
import { join, relative, dirname, extname, sep } from 'node:path';
|
|
4
|
-
import { createHash } from 'node:crypto';
|
|
5
|
-
import { isIP } from 'node:net';
|
|
6
|
-
import { parseCode, parseConfig } from './repository-syntax.mjs';
|
|
7
|
-
import { endpointService } from './service-registry.mjs';
|
|
8
|
-
import { setImmediate as yieldThread } from 'node:timers/promises';
|
|
9
|
-
import { INDEX_LIMITS, emptyIndexSummary } from './repository-policy.mjs';
|
|
10
|
-
export { INDEX_LIMITS } from './repository-policy.mjs';
|
|
11
|
-
|
|
12
|
-
const excluded = new Set(['node_modules','vendor','dist','build','out','coverage','target','venv','env','__pycache__','site-packages','logs','runs','results']);
|
|
13
|
-
const extensions = new Set(['.py','.js','.jsx','.ts','.tsx','.mjs','.cjs','.json','.yaml','.yml','.toml']);
|
|
14
|
-
const privateFile = /(?:^|[._-])(?:credentials?|secrets?|tokens?|passwords?|auth)(?:[._-]|$)|(?:^|\/)id_(?:rsa|ed25519)/i;
|
|
15
|
-
const digest = value=>createHash('sha256').update(value).digest('hex').slice(0,24);
|
|
16
|
-
const safeName = value=>typeof value==='string' && value.length<=160 && !/[\x00-\x1f\x7f]/.test(value) && !/(?:\bsk[-_]|Bearer\s|(?:api[_-]?key|token|secret|password)\s*[:=])/i.test(value);
|
|
17
|
-
const modelValue = value=>safeName(value) && /^[A-Za-z0-9][A-Za-z0-9._:/+@-]*$/.test(value) && !value.includes('://');
|
|
18
|
-
const contextFor = file=> /(?:^|\/)(?:benchmarks?|evals?|backtests?)(?:\/|[._-])|(?:benchmark|backtest)/i.test(file)?'benchmark':/(?:^|\/)(?:tests?|fixtures?)(?:\/|[._-])|[._](?:test|spec)\./i.test(file)?'test':/(?:^|\/)(?:examples?|samples?)(?:\/|[._-])|(?:^|\/)\.env\.(?:example|sample|template)/i.test(file)?'example':'application';
|
|
19
|
-
const envFile = file=>/(?:^|\/)\.env(?:\.[\w.-]+)?$/.test(file);
|
|
20
|
-
|
|
21
|
-
// Display identity only. No result from this module is used as a network target.
|
|
22
|
-
export function endpointIdentity(value) {
|
|
23
|
-
if(typeof value!=='string'||value.length>2048||/[\x00-\x20\x7f\\$`]/.test(value))return null;
|
|
24
|
-
try {
|
|
25
|
-
const url=new URL(value);
|
|
26
|
-
if(!['http:','https:'].includes(url.protocol)||url.username||url.password)return null;
|
|
27
|
-
const known=endpointService(value), host=url.hostname;
|
|
28
|
-
if(['huggingface.co','www.huggingface.co','hf.co'].includes(host))return {nonInference:true};
|
|
29
|
-
if(/(?:^|\.)(?:example\.(?:com|org|net)|invalid|test)$/.test(host)||['url.here','your-endpoint','your-host'].includes(host))return {placeholder:true};
|
|
30
|
-
if(/(?:sk-|token|secret|password)/i.test(host))return null;
|
|
31
|
-
const displayHost=known?.endpointHost??host.split('.').map(label=>label.length>30?'…':label).join('.');
|
|
32
|
-
// Account/project/model identifiers can be embedded in paths. Keep only
|
|
33
|
-
// structural API paths; custom resource suffixes are deliberately omitted.
|
|
34
|
-
const path=/^\/(?:v\d+(?:beta\d*)?|api(?:\/v\d+)?|api\/alpha\/decisions|v1\/systemone|dsr1\/v1)\/?$/.test(url.pathname)?url.pathname.replace(/\/$/,''):'';
|
|
35
|
-
const address=displayHost+(url.port?':'+url.port:'')+path;
|
|
36
|
-
const local=['localhost','127.0.0.1','[::1]'].includes(host);
|
|
37
|
-
const privateHost=local||host.endsWith('.local')||host.endsWith('.internal')||/^10\.|^192\.168\.|^172\.(?:1[6-9]|2\d|3[01])\.|^\[(?:fc|fd|fe80)/i.test(host);
|
|
38
|
-
return {address,host:displayHost+(url.port?':'+url.port:''),service:known?.service.name??address,serviceType:known?.service.type??'unknown',location:privateHost?'private':known?'cloud':'unknown',loopback:local,ip:Boolean(isIP(host.replace(/^\[|\]$/g,''))),pathRedacted:url.pathname!=='/'&&!path};
|
|
39
|
-
}catch{return null;}
|
|
40
|
-
}
|
|
41
|
-
export function indexDiagnostics(index) {
|
|
42
|
-
// Sharing is opt-in by copying this output. Deliberately exclude paths,
|
|
43
|
-
// names, URLs, values, environment variables and source contents.
|
|
44
|
-
const counts=index?.summary?.issueCounts??{};
|
|
45
|
-
return {schemaVersion:1,scope:'repository-index',enabled:Boolean(index),summary:index?.summary??null,issueCounts:counts,limits:INDEX_LIMITS};
|
|
46
|
-
}
|
|
47
|
-
// Cache only parser metadata, never source text, and never write into the project.
|
|
48
|
-
// Endpoint values are sanitized before entering this process-local cache.
|
|
49
|
-
function cacheEntry(entry) {
|
|
50
|
-
const result={...entry};
|
|
51
|
-
if (entry.endpoints) result.endpoints=entry.endpoints.map(cacheEntry);
|
|
52
|
-
if (typeof entry.value==='string' && (entry.kind==='endpoint' || /^https?:\/\//i.test(entry.value))) {
|
|
53
|
-
result.endpointValue=endpointIdentity(entry.value) || {invalid:true};
|
|
54
|
-
result.value=result.endpointValue.address || '';
|
|
55
|
-
}
|
|
56
|
-
return result;
|
|
57
|
-
}
|
|
58
|
-
function reporter(summary, issues, limit) {
|
|
59
|
-
const seen=new Set();
|
|
60
|
-
return (code,file,line=1,field)=>{
|
|
61
|
-
const key=JSON.stringify([code,file,line,field]);if(seen.has(key))return;seen.add(key);
|
|
62
|
-
summary.unresolved++;summary.issueCounts[code]=(summary.issueCounts[code]||0)+1;
|
|
63
|
-
if(issues.length<limit)issues.push({code,...(file?{file}:{}),line,...(field&&safeName(field)?{field}:{})});
|
|
64
|
-
};
|
|
65
|
-
}
|
|
66
|
-
export function createRepositoryScanner(root, options={}) {
|
|
67
|
-
const limits={...INDEX_LIMITS,...options};
|
|
68
|
-
const cache=new Map();let running=false,scanId=0;
|
|
69
|
-
return async function scanRepository({onProgress=()=>{},signal}={}) {
|
|
70
|
-
if(running)throw new Error('Repository scan already running');
|
|
71
|
-
running=true;
|
|
72
|
-
const started=performance.now(),summary=emptyIndexSummary(),scanIssues=[],parsed=[],files=[],seen=new Set();
|
|
73
|
-
const issue=reporter(summary,scanIssues,limits.issues);
|
|
74
|
-
const progress={phase:'discovering',scan:++scanId,discovered:0,processed:0,parsed:0,reused:0,batches:0,total:null,elapsedMs:0};
|
|
75
|
-
let environmentFiles=Infinity;
|
|
76
|
-
let lastPublished=0,batchFiles=0,batchBytes=0,batchStart=performance.now();
|
|
77
|
-
const check=()=>signal?.throwIfAborted();
|
|
78
|
-
const status=()=>({...progress,elapsedMs:Math.round(performance.now()-started)});
|
|
79
|
-
const publish=async (force=false,results=false)=>{
|
|
80
|
-
check();
|
|
81
|
-
if(!force&&performance.now()-lastPublished<limits.publishMs)return;
|
|
82
|
-
lastPublished=performance.now();
|
|
83
|
-
// On a first scan, usable declarations arrive in batches. Later scans
|
|
84
|
-
// retain the last complete index until replacement, avoiding flicker.
|
|
85
|
-
const index=results?projectIndex(parsed,summary,scanIssues,limits):null;
|
|
86
|
-
await onProgress({progress:status(),summary:{...summary},...(index?{index:{...index,progress:status()}}:{})});
|
|
87
|
-
};
|
|
88
|
-
const batch=async()=>{
|
|
89
|
-
if(batchFiles<limits.batchFiles&&batchBytes<limits.batchBytes&&performance.now()-batchStart<limits.batchMs)return;
|
|
90
|
-
progress.batches++;await publish(false,progress.phase==='indexing'&&progress.processed>=environmentFiles&&scanId===1);
|
|
91
|
-
await yieldThread();check();batchFiles=0;batchBytes=0;batchStart=performance.now();
|
|
92
|
-
};
|
|
93
|
-
try {
|
|
94
|
-
const canonical=await realpath(root),directories=[canonical];
|
|
95
|
-
await publish(true);
|
|
96
|
-
// Iterative traversal has no depth/entry ceiling and does not consume
|
|
97
|
-
// the JS call stack. Symlinks and excluded directories remain excluded.
|
|
98
|
-
while(directories.length) {
|
|
99
|
-
check();const dir=directories.pop();
|
|
100
|
-
try {
|
|
101
|
-
if(await realpath(dir)!==dir){summary.skipped.symlink++;continue;}
|
|
102
|
-
const directory=await opendir(dir);
|
|
103
|
-
for await(const entry of directory) {
|
|
104
|
-
check();summary.entriesVisited++;
|
|
105
|
-
const isEnv=/^\.env(?:\.[\w.-]+)?$/.test(entry.name);
|
|
106
|
-
if(entry.isSymbolicLink())summary.skipped.symlink++;
|
|
107
|
-
else if(entry.name.startsWith('.')&&!isEnv||excluded.has(entry.name)||privateFile.test(entry.name)||/(?:package-lock|pnpm-lock|yarn\.lock|\.min\.|\.d\.ts$)/.test(entry.name))summary.skipped.policy++;
|
|
108
|
-
else {
|
|
109
|
-
const path=join(dir,entry.name);
|
|
110
|
-
if(entry.isDirectory())directories.push(path);
|
|
111
|
-
else if(entry.isFile()) {
|
|
112
|
-
if(isEnv||extensions.has(extname(entry.name))){files.push(path);progress.discovered++;}
|
|
113
|
-
else summary.skipped.unsupported++;
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
// Walking a tree full of unsupported files is still cooperative.
|
|
117
|
-
batchFiles++;await batch();
|
|
118
|
-
}
|
|
119
|
-
}catch(error){check();summary.skipped.unreadable++;issue('unreadable-directory',relative(canonical,dir)||'.');}
|
|
120
|
-
}
|
|
121
|
-
const priority=path=>envFile(path)?0:/\.(?:json|ya?ml|toml)$/.test(path)?1:/(?:inference|llm|model|provider|client|agent|router)/i.test(path)?2:3;
|
|
122
|
-
files.sort((a,b)=>priority(a)-priority(b)||a.localeCompare(b));
|
|
123
|
-
environmentFiles=files.filter(envFile).length;
|
|
124
|
-
progress.phase='indexing';progress.total=files.length;await publish(true);
|
|
125
|
-
for(const path of files) {
|
|
126
|
-
check();const file=relative(canonical,path);let handle;
|
|
127
|
-
try {
|
|
128
|
-
if(await realpath(path)!==path||!path.startsWith(canonical+sep)){summary.skipped.symlink++;continue;}
|
|
129
|
-
handle=await open(path,constants.O_RDONLY|(constants.O_NOFOLLOW||0)|(constants.O_NONBLOCK||0));
|
|
130
|
-
const stat=await handle.stat({bigint:true});
|
|
131
|
-
if(!stat.isFile()){summary.skipped.policy++;continue;}
|
|
132
|
-
if(stat.size>BigInt(limits.fileBytes)){summary.skipped.oversized++;issue('oversized-file',file);continue;}
|
|
133
|
-
const fingerprint=[stat.dev,stat.ino,stat.size,stat.mtimeNs,stat.ctimeNs].join(':');
|
|
134
|
-
seen.add(file);
|
|
135
|
-
const cached=cache.get(file);
|
|
136
|
-
let item;
|
|
137
|
-
if(cached?.fingerprint===fingerprint){item=cached;progress.reused++;}
|
|
138
|
-
else {
|
|
139
|
-
const buffer=Buffer.alloc(Math.min(limits.fileBytes+1,Number(stat.size)+1));
|
|
140
|
-
const {bytesRead}=await handle.read(buffer,0,buffer.length,0);
|
|
141
|
-
summary.filesRead++;summary.bytesRead+=bytesRead;batchBytes+=bytesRead;
|
|
142
|
-
if(bytesRead>limits.fileBytes){summary.skipped.oversized++;issue('oversized-file',file);seen.delete(file);continue;}
|
|
143
|
-
const after=await handle.stat({bigint:true});
|
|
144
|
-
if([after.dev,after.ino,after.size,after.mtimeNs,after.ctimeNs].join(':')!==fingerprint){issue('changed-during-read',file);seen.delete(file);continue;}
|
|
145
|
-
const body=buffer.subarray(0,bytesRead).toString('utf8');
|
|
146
|
-
const hash=digest(body);
|
|
147
|
-
if(cached?.hash===hash){item={...cached,fingerprint};progress.reused++;}
|
|
148
|
-
else {
|
|
149
|
-
const code=/\.(?:py|[cm]?js|jsx|tsx?)$/.test(file);
|
|
150
|
-
const result=body.includes('\0')?{entries:[],issues:[{code:'binary-file'}]}:code?parseCode(body,file):parseConfig(body,file);
|
|
151
|
-
// Dotenv bindings are read each scan and discarded after projection;
|
|
152
|
-
// arbitrary unrelated setting values never enter the cache.
|
|
153
|
-
item={file,context:contextFor(file),fingerprint,hash,bytes:bytesRead,entries:result.entries.map(cacheEntry),issues:result.issues};
|
|
154
|
-
progress.parsed++;
|
|
155
|
-
}
|
|
156
|
-
if(!envFile(file))cache.set(file,item);
|
|
157
|
-
}
|
|
158
|
-
parsed.push(item);summary.filesIndexed++;summary.bytesIndexed+=item.bytes;
|
|
159
|
-
const format=envFile(file)?'dotenv':extname(file).slice(1);
|
|
160
|
-
summary.filesByFormat[format]=(summary.filesByFormat[format]||0)+1;
|
|
161
|
-
for(const note of item.issues)issue(note.code,file,note.line,note.field);
|
|
162
|
-
}catch(error){check();seen.delete(file);summary.skipped.unreadable++;issue('unreadable-file',file);}
|
|
163
|
-
finally {await handle?.close();progress.processed++;batchFiles++;await batch();}
|
|
164
|
-
}
|
|
165
|
-
for(const file of cache.keys())if(!seen.has(file))cache.delete(file);
|
|
166
|
-
progress.phase='resolving';await publish(true);
|
|
167
|
-
const index=projectIndex(parsed,summary,scanIssues,limits);
|
|
168
|
-
progress.phase='complete';progress.batches++;
|
|
169
|
-
return {...index,progress:status()};
|
|
170
|
-
}finally {running=false;}
|
|
171
|
-
};
|
|
172
|
-
}
|
|
173
|
-
export async function indexRepository(root,limits=INDEX_LIMITS) {
|
|
174
|
-
return createRepositoryScanner(root,limits)();
|
|
175
|
-
}
|
|
176
|
-
function projectIndex(parsed,scanSummary,scanIssues,limits) {
|
|
177
|
-
const summary={...scanSummary,unresolved:0,issueCounts:{}},issues=[],references=[];
|
|
178
|
-
const issue=reporter(summary,issues,limits.issues);
|
|
179
|
-
// Preserve total issue counts even when the detailed list is bounded.
|
|
180
|
-
for(const note of scanIssues)issue(note.code,note.file,note.line,note.field);
|
|
181
|
-
summary.unresolved=scanSummary.unresolved;summary.issueCounts={...scanSummary.issueCounts};
|
|
182
|
-
const envValues=[];
|
|
183
|
-
for(const item of parsed.filter(p=>envFile(p.file)&&p.context==='application'))for(const e of item.entries)if(e.environmentDeclaration&&(e.value||e.endpointValue))envValues.push({...e,file:item.file});
|
|
184
|
-
const environment=new Map();
|
|
185
|
-
for(const e of envValues){const key=JSON.stringify([dirname(e.file),e.field]);if(!environment.has(key))environment.set(key,[]);environment.get(key).push(e);}
|
|
186
|
-
const resolveEnv=(entry,file)=>{
|
|
187
|
-
if(entry.value!==undefined)return [entry];
|
|
188
|
-
if(!entry.env)return [];
|
|
189
|
-
let dir=dirname(file);
|
|
190
|
-
for(;;){
|
|
191
|
-
const matching=environment.get(JSON.stringify([dir,entry.env]))||[];
|
|
192
|
-
if(matching.length){
|
|
193
|
-
const unique=[...new Set(matching.map(e=>JSON.stringify([e.value,e.endpointValue])))];
|
|
194
|
-
if(unique.length!==1){issue('ambiguous-environment',file,entry.line,entry.field);return [];}
|
|
195
|
-
return [{...entry,value:matching[0].value,...(matching[0].endpointValue?{endpointValue:matching[0].endpointValue}:{}),basis:'environment-reference',via:{file:matching[0].file,line:matching[0].line}}];
|
|
196
|
-
}
|
|
197
|
-
if(dir==='.')break;dir=dirname(dir);
|
|
198
|
-
}
|
|
199
|
-
issue('unresolved-environment',file,entry.line,entry.field);return [];
|
|
200
|
-
};
|
|
201
|
-
const unique=new Map();
|
|
202
|
-
function add(entry,item,extra={}) {
|
|
203
|
-
const {kind,field,line,basis}=entry;
|
|
204
|
-
let context=item.context;
|
|
205
|
-
if(context==='application'&&entry.alias)context='alias';
|
|
206
|
-
if(context==='application'&&(entry.candidate||basis==='branch'))context='candidate';
|
|
207
|
-
if(context==='application'&&basis==='fallback')context='fallback';
|
|
208
|
-
let value=entry.value,endpoint;
|
|
209
|
-
if(kind==='binding')return null;
|
|
210
|
-
if(kind==='endpoint') {
|
|
211
|
-
endpoint=entry.endpointValue??endpointIdentity(value);
|
|
212
|
-
if(endpoint?.nonInference){issue('non-inference-endpoint',item.file,line,field);return null;}
|
|
213
|
-
if(endpoint?.placeholder){issue('placeholder-endpoint',item.file,line,field);return null;}
|
|
214
|
-
if(!endpoint||endpoint.invalid){issue('unsupported-endpoint',item.file,line,field);return null;}
|
|
215
|
-
value=endpoint.address;
|
|
216
|
-
}else if(!(kind==='task' ? safeName(value) && value.trim() && !value.includes('://') : modelValue(value))){
|
|
217
|
-
issue(value?.includes('*')?'model-pattern':'unsupported-value',item.file,line,field);return null;
|
|
218
|
-
}
|
|
219
|
-
if(context==='application'&&kind==='model'&&!extra.endpointIds?.length&&!entry.inferenceCall&&!entry.localLibrary)context='unlinked';
|
|
220
|
-
const id=digest([kind,item.file,line,value,context].join('|'));
|
|
221
|
-
const previous=unique.get(id);
|
|
222
|
-
const result={id,kind,value,file:item.file,line,field,basis,context,...(entry.via?{via:entry.via}:{}),...(endpoint?{endpoint}:{}),...(entry.localLibrary?{localLibrary:true}:{}),...extra};
|
|
223
|
-
if(previous){Object.assign(previous,extra);return previous;}
|
|
224
|
-
unique.set(id,result);return result;
|
|
225
|
-
}
|
|
226
|
-
for(const item of parsed){
|
|
227
|
-
const groups=new Map();
|
|
228
|
-
for(const e of item.entries){const group=groups.get(e.group)||[];group.push(e);groups.set(e.group,group);}
|
|
229
|
-
for(const entries of groups.values()) {
|
|
230
|
-
const endpoints=entries.filter(e=>e.kind==='endpoint').flatMap(e=>resolveEnv(e,item.file));
|
|
231
|
-
for(const entry of entries.filter(e=>e.kind!=='binding'))for(const e of resolveEnv(entry,item.file)) {
|
|
232
|
-
let endpointIds=[];
|
|
233
|
-
if(e.kind==='model'||e.kind==='task'){
|
|
234
|
-
const linked=(e.endpoints?.length?e.endpoints:endpoints).flatMap(v=>resolveEnv({...v,field:v.field||'base_url',kind:'endpoint',line:v.line||e.line},item.file));
|
|
235
|
-
endpointIds=[...new Set(linked.map(v=>add(v,item)).filter(Boolean).map(r=>r.id))];
|
|
236
|
-
}
|
|
237
|
-
const ref=add(e,item,endpointIds.length?{endpointIds}:{});
|
|
238
|
-
if(ref&&e.kind==='model'&&e.task&&safeName(e.task))ref.task=e.task;
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
const resolvedSites=new Set([...unique.values()].filter(r=>r.context==='application').map(r=>[r.kind,r.file,r.line,r.value].join('|')));
|
|
243
|
-
references.push(...[...unique.values()].filter(r=>r.context!=='unlinked'||!resolvedSites.has([r.kind,r.file,r.line,r.value].join('|'))));
|
|
244
|
-
summary.declarations=references.length;
|
|
245
|
-
const inventory=[],models=[], byService=new Map();
|
|
246
|
-
for(const ref of references.filter(r=>r.kind==='endpoint'&&r.context==='application')) {
|
|
247
|
-
const endpoint=ref.endpoint,name=endpoint.service;
|
|
248
|
-
const item=byService.get(name)||{name,kind:['router','gateway','runtime'].includes(endpoint.serviceType)?endpoint.serviceType:endpoint.serviceType==='unknown'?'unknown':'provider',serviceType:endpoint.serviceType,location:endpoint.location,state:'declared',sourceId:'repository',evidence:[]};
|
|
249
|
-
item.evidence.push({sourceId:'repository',field:ref.field,basis:'repository-declaration',file:ref.file,line:ref.line,referenceId:ref.id,endpointHost:endpoint.host});byService.set(name,item);
|
|
250
|
-
}
|
|
251
|
-
inventory.push(...byService.values());
|
|
252
|
-
const byId=new Map(references.map(r=>[r.id,r]));
|
|
253
|
-
const linkedModels=new Set(references.filter(r=>r.kind==='model'&&r.context==='application'&&r.endpointIds?.length).map(r=>`${r.file}:${r.value}`));
|
|
254
|
-
const byModel=new Map();
|
|
255
|
-
for(const ref of references.filter(r=>r.kind==='model'&&r.context==='application')) {
|
|
256
|
-
if(!ref.endpointIds?.length&&linkedModels.has(`${ref.file}:${ref.value}`))continue;
|
|
257
|
-
const endpoints=(ref.endpointIds||[]).map(id=>byId.get(id)).filter(r=>r?.context==='application').map(r=>r.endpoint);
|
|
258
|
-
// Multiple alternatives do not establish which service supplies this model.
|
|
259
|
-
const services=[...new Set(endpoints.map(e=>e.service))];
|
|
260
|
-
const provider=services.length===1?services[0]:'Unattributed';
|
|
261
|
-
const key=JSON.stringify([provider,ref.value]);
|
|
262
|
-
if(byModel.has(key))byModel.get(key).declarationIds.push(ref.id);
|
|
263
|
-
else byModel.set(key,{id:`declaration:${ref.id}`,name:ref.value,provider,location:'unknown',state:'declared',sourceId:'repository',declarationIds:[ref.id],memoryBytes:null});
|
|
264
|
-
}
|
|
265
|
-
models.push(...byModel.values());
|
|
266
|
-
return {references,issues,summary,inventory,models,checkedAt:new Date().toISOString()};
|
|
267
|
-
}
|
|
@@ -1,3 +0,0 @@
|
|
|
1
|
-
// Work budgets govern each batch, not the total repository coverage.
|
|
2
|
-
export const INDEX_LIMITS=Object.freeze({batchFiles:32,batchBytes:1024*1024,batchMs:40,publishMs:500,fileBytes:128*1024,issues:100});
|
|
3
|
-
export const emptyIndexSummary=()=>({filesRead:0,filesIndexed:0,bytesRead:0,bytesIndexed:0,entriesVisited:0,filesByFormat:{},skipped:{policy:0,unsupported:0,symlink:0,oversized:0,unreadable:0},truncated:false,declarations:0,unresolved:0,issueCounts:{}});
|