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,62 @@
|
||||
import type { FilterSpec } from '../stores/types';
|
||||
|
||||
/** Parse the dashboard's filter bar text into filter specs. */
|
||||
|
||||
const OPERATORS: Record<string, FilterSpec['op']> = {
|
||||
':': 'eq',
|
||||
'~': 'contains',
|
||||
'>': 'gt',
|
||||
'<': 'lt',
|
||||
};
|
||||
|
||||
/** `title~sales kind:chart column>3` → three specs. */
|
||||
export function parseFilterText(text: string): FilterSpec[] {
|
||||
const specs: FilterSpec[] = [];
|
||||
for (const token of tokenize(text)) {
|
||||
const spec = parseToken(token);
|
||||
if (spec) specs.push(spec);
|
||||
}
|
||||
return specs;
|
||||
}
|
||||
|
||||
/** Split on whitespace, honouring double-quoted values. */
|
||||
export function tokenize(text: string): string[] {
|
||||
const tokens: string[] = [];
|
||||
let current = '';
|
||||
let quoted = false;
|
||||
for (const ch of text) {
|
||||
if (ch === '"') { quoted = !quoted; continue; }
|
||||
if (!quoted && /\s/.test(ch)) {
|
||||
if (current.length > 0) { tokens.push(current); current = ''; }
|
||||
continue;
|
||||
}
|
||||
current += ch;
|
||||
}
|
||||
if (current.length > 0) tokens.push(current);
|
||||
return tokens;
|
||||
}
|
||||
|
||||
/** One `field<op>value` token, or null when it does not parse. */
|
||||
export function parseToken(token: string): FilterSpec | null {
|
||||
for (const [symbol, op] of Object.entries(OPERATORS)) {
|
||||
const at = token.indexOf(symbol);
|
||||
if (at <= 0) continue;
|
||||
const field = token.slice(0, at).trim();
|
||||
const value = token.slice(at + symbol.length).trim();
|
||||
if (field.length === 0 || value.length === 0) return null;
|
||||
return { field, op, value };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Render specs back to filter-bar text — the round trip the URL uses. */
|
||||
export function formatFilterText(specs: readonly FilterSpec[]): string {
|
||||
const symbolFor = (op: FilterSpec['op']): string =>
|
||||
Object.entries(OPERATORS).find(([, candidate]) => candidate === op)?.[0] ?? ':';
|
||||
return specs
|
||||
.map((spec) => {
|
||||
const value = /\s/.test(spec.value) ? `"${spec.value}"` : spec.value;
|
||||
return `${spec.field}${symbolFor(spec.op)}${value}`;
|
||||
})
|
||||
.join(' ');
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
import type { FilterSpec, MetricSample, Widget } from '../stores/types';
|
||||
import { bucketByHour, meanOf, rateOfChange } from '../lib/metrics';
|
||||
|
||||
/**
|
||||
* Stateless metric helpers — the server-shaped half of the same domain. These
|
||||
* are ordinary top-level functions, not closures, so they are the control the
|
||||
* factory-closure file is measured against.
|
||||
*/
|
||||
|
||||
const STALE_AFTER_MS = 15 * 60 * 1000;
|
||||
|
||||
/** Refresh a cached metric map in place, returning the widgets that changed. */
|
||||
export function refreshMetricCache(
|
||||
cache: Map<string, MetricSample[]>,
|
||||
incoming: readonly MetricSample[],
|
||||
now: number,
|
||||
): string[] {
|
||||
const touched = new Set<string>();
|
||||
for (const sample of incoming) {
|
||||
if (typeof sample.value !== 'number' || Number.isNaN(sample.value)) continue;
|
||||
const bucket = cache.get(sample.widgetId);
|
||||
if (bucket) bucket.push(sample);
|
||||
else cache.set(sample.widgetId, [sample]);
|
||||
touched.add(sample.widgetId);
|
||||
}
|
||||
for (const [widgetId, bucket] of cache) {
|
||||
const fresh = bucket.filter((s) => now - s.at <= STALE_AFTER_MS);
|
||||
if (fresh.length !== bucket.length) {
|
||||
cache.set(widgetId, fresh);
|
||||
touched.add(widgetId);
|
||||
}
|
||||
}
|
||||
return [...touched].sort();
|
||||
}
|
||||
|
||||
/** Apply a filter spec set to raw samples rather than to widgets. */
|
||||
export function filterMetrics(
|
||||
samples: readonly MetricSample[],
|
||||
specs: readonly FilterSpec[],
|
||||
): MetricSample[] {
|
||||
if (specs.length === 0) return samples.slice();
|
||||
return samples.filter((sample) => specs.every((spec) => {
|
||||
const field = spec.field === 'unit'
|
||||
? sample.unit
|
||||
: spec.field === 'widget'
|
||||
? sample.widgetId
|
||||
: String(sample.value);
|
||||
switch (spec.op) {
|
||||
case 'eq': return field === spec.value;
|
||||
case 'contains': return field.includes(spec.value);
|
||||
case 'gt': return Number(field) > Number(spec.value);
|
||||
case 'lt': return Number(field) < Number(spec.value);
|
||||
default: return false;
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
/** Per-widget rollup used by the server-rendered summary card. */
|
||||
export function rollupByWidget(
|
||||
samples: readonly MetricSample[],
|
||||
widgets: readonly Widget[],
|
||||
): Array<{ widgetId: string; title: string; mean: number; slope: number; hours: number }> {
|
||||
const titles = new Map(widgets.map((w) => [w.id, w.title]));
|
||||
const grouped = new Map<string, MetricSample[]>();
|
||||
for (const sample of samples) {
|
||||
const bucket = grouped.get(sample.widgetId);
|
||||
if (bucket) bucket.push(sample);
|
||||
else grouped.set(sample.widgetId, [sample]);
|
||||
}
|
||||
|
||||
const out: Array<{ widgetId: string; title: string; mean: number; slope: number; hours: number }> = [];
|
||||
for (const [widgetId, bucket] of grouped) {
|
||||
out.push({
|
||||
widgetId,
|
||||
title: titles.get(widgetId) ?? '(unknown)',
|
||||
mean: meanOf(bucket),
|
||||
slope: rateOfChange(bucket),
|
||||
hours: bucketByHour(bucket).size,
|
||||
});
|
||||
}
|
||||
out.sort((a, b) => b.mean - a.mean);
|
||||
return out;
|
||||
}
|
||||
|
||||
/** Which widgets have not reported inside the staleness window. */
|
||||
export function staleWidgets(
|
||||
samples: readonly MetricSample[],
|
||||
widgets: readonly Widget[],
|
||||
now: number,
|
||||
): string[] {
|
||||
const newest = new Map<string, number>();
|
||||
for (const sample of samples) {
|
||||
const seen = newest.get(sample.widgetId) ?? 0;
|
||||
if (sample.at > seen) newest.set(sample.widgetId, sample.at);
|
||||
}
|
||||
return widgets
|
||||
.filter((w) => !w.hidden)
|
||||
.filter((w) => now - (newest.get(w.id) ?? 0) > STALE_AFTER_MS)
|
||||
.map((w) => w.id)
|
||||
.sort();
|
||||
}
|
||||
Reference in New Issue
Block a user