test(explore): add the factory-closure fixture and its selection probe (CG-27)
A file whose top-level symbol spans almost all of it — createFoo() returning
an object of closures — is how Svelte 5 rune stores, React custom-hook modules,
IIFE module-pattern JS and Zustand's create((set,get)=>({…})) are all written.
probe-factory-closure.mjs measures what such a file DELIVERS from within: which
inner symbols' definitions reach the agent, not how many bytes did.
This commit is contained in:
@@ -0,0 +1,25 @@
|
||||
/** Minimal fetch helpers the dashboard store depends on. */
|
||||
|
||||
export interface RequestOptions {
|
||||
retries: number;
|
||||
timeoutMs: number;
|
||||
}
|
||||
|
||||
export const defaultRequestOptions: RequestOptions = { retries: 2, timeoutMs: 5_000 };
|
||||
|
||||
/** Build a query string from a plain record, skipping empty values. */
|
||||
export function toQueryString(params: Record<string, string | number | undefined>): string {
|
||||
const parts: string[] = [];
|
||||
for (const [key, value] of Object.entries(params)) {
|
||||
if (value === undefined || value === '') continue;
|
||||
parts.push(`${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`);
|
||||
}
|
||||
return parts.length > 0 ? `?${parts.join('&')}` : '';
|
||||
}
|
||||
|
||||
/** Join a base path and a resource path without doubling the separator. */
|
||||
export function joinPath(base: string, resource: string): string {
|
||||
if (base.endsWith('/') && resource.startsWith('/')) return base + resource.slice(1);
|
||||
if (!base.endsWith('/') && !resource.startsWith('/')) return `${base}/${resource}`;
|
||||
return base + resource;
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import type { MetricSample } from '../stores/types';
|
||||
|
||||
/** Statistics helpers shared by the store and the panel. */
|
||||
|
||||
export function meanOf(samples: readonly MetricSample[]): number {
|
||||
if (samples.length === 0) return 0;
|
||||
let total = 0;
|
||||
for (const sample of samples) total += sample.value;
|
||||
return total / samples.length;
|
||||
}
|
||||
|
||||
export function medianOf(samples: readonly MetricSample[]): number {
|
||||
if (samples.length === 0) return 0;
|
||||
const values = samples.map((s) => s.value).sort((a, b) => a - b);
|
||||
const mid = Math.floor(values.length / 2);
|
||||
return values.length % 2 === 0 ? (values[mid - 1]! + values[mid]!) / 2 : values[mid]!;
|
||||
}
|
||||
|
||||
export function rateOfChange(samples: readonly MetricSample[]): number {
|
||||
if (samples.length < 2) return 0;
|
||||
const ordered = samples.slice().sort((a, b) => a.at - b.at);
|
||||
const first = ordered[0]!;
|
||||
const last = ordered[ordered.length - 1]!;
|
||||
const elapsed = last.at - first.at;
|
||||
return elapsed > 0 ? (last.value - first.value) / elapsed : 0;
|
||||
}
|
||||
|
||||
export function bucketByHour(samples: readonly MetricSample[]): Map<number, MetricSample[]> {
|
||||
const buckets = new Map<number, MetricSample[]>();
|
||||
for (const sample of samples) {
|
||||
const hour = Math.floor(sample.at / 3_600_000);
|
||||
const bucket = buckets.get(hour);
|
||||
if (bucket) bucket.push(sample);
|
||||
else buckets.set(hour, [sample]);
|
||||
}
|
||||
return buckets;
|
||||
}
|
||||
Reference in New Issue
Block a user