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:
Colby McHenry
2026-08-06 13:58:36 -05:00
parent dc4fd755ef
commit d49265043c
11 changed files with 992 additions and 0 deletions
@@ -0,0 +1,5 @@
{
"name": "factory-closure-ts",
"version": "0.0.0",
"private": true
}
@@ -0,0 +1,23 @@
import { createDashboardStore } from './stores/dashboard-store';
import { createAlertsStore } from './stores/alerts-store';
import { mountPanel } from './ui/panel';
import { parseFilterText } from './services/filter-parser';
import { refreshMetricCache } from './services/metric-service';
import type { StoreDeps } from './stores/types';
/** Wire a dashboard: build both stores, mount the panel, boot it. */
export async function startDashboard(deps: StoreDeps, baseUrl: string, dashboardId: string) {
const store = createDashboardStore(deps, baseUrl);
const alerts = createAlertsStore(deps, baseUrl);
const panel = mountPanel(store, dashboardId);
await panel.boot();
await alerts.refreshAlerts(dashboardId);
return { store, alerts, panel };
}
/** Apply the filter bar's text to the dashboard store. */
export function searchDashboard(store: ReturnType<typeof createDashboardStore>, text: string) {
return store.applyFilter(parseFilterText(text));
}
export { refreshMetricCache };
@@ -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;
}
@@ -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();
}
@@ -0,0 +1,140 @@
import type { FilterSpec, StoreDeps } from './types';
import { joinPath, toQueryString } from '../lib/http';
const ALERT_ENDPOINT = '/api/dashboard/alerts';
export interface Alert {
id: string;
widgetId: string;
severity: 'info' | 'warn' | 'critical';
message: string;
raisedAt: number;
acknowledgedAt: number | null;
}
/**
* The alerts store — the dashboard's second factory closure. Same shape as the
* metric store: every operation is a closure over private state.
*/
export function createAlertsStore(deps: StoreDeps, baseUrl: string) {
let alerts: Alert[] = [];
let filters: FilterSpec[] = [];
let mutedWidgets = new Set<string>();
let lastRefreshedAt = 0;
/** Pull the current alert set and merge acknowledgements the user made locally. */
async function refreshAlerts(dashboardId: string): Promise<Alert[]> {
const url = joinPath(baseUrl, ALERT_ENDPOINT) + toQueryString({ dashboard: dashboardId });
let payload: unknown;
try {
payload = await deps.fetchJson(url);
} catch (error) {
deps.log(`refreshAlerts failed: ${error instanceof Error ? error.message : String(error)}`);
return alerts;
}
if (!Array.isArray(payload)) {
deps.log('refreshAlerts got a non-array payload');
return alerts;
}
const acknowledged = new Map(
alerts.filter((a) => a.acknowledgedAt !== null).map((a) => [a.id, a.acknowledgedAt]),
);
const merged: Alert[] = [];
for (const raw of payload as Alert[]) {
if (typeof raw.id !== 'string' || raw.id.length === 0) continue;
merged.push({
...raw,
acknowledgedAt: acknowledged.get(raw.id) ?? raw.acknowledgedAt ?? null,
});
}
merged.sort((a, b) => b.raisedAt - a.raisedAt);
alerts = merged;
lastRefreshedAt = deps.now();
return alerts;
}
/** Filter the alert list the same way the metric store filters widgets. */
function applyAlertFilter(specs: readonly FilterSpec[]): Alert[] {
filters = specs.slice();
if (filters.length === 0) return alerts;
const fieldOf = (alert: Alert, field: string): string => {
switch (field) {
case 'severity': return alert.severity;
case 'widget': return alert.widgetId;
case 'message': return alert.message;
default: return '';
}
};
return alerts.filter((alert) => filters.every((spec) => {
const value = fieldOf(alert, spec.field);
switch (spec.op) {
case 'eq': return value.toLowerCase() === spec.value.toLowerCase();
case 'contains': return value.toLowerCase().includes(spec.value.toLowerCase());
case 'gt': return value > spec.value;
case 'lt': return value < spec.value;
default: return false;
}
}));
}
/** Mark an alert acknowledged locally; the next refresh preserves it. */
function acknowledge(alertId: string): boolean {
const target = alerts.find((a) => a.id === alertId);
if (!target || target.acknowledgedAt !== null) return false;
target.acknowledgedAt = deps.now();
deps.log(`acknowledged ${alertId}`);
return true;
}
/** Silence a widget's alerts without dropping them from the buffer. */
function muteWidget(widgetId: string): void {
mutedWidgets.add(widgetId);
deps.log(`muted ${widgetId} (${mutedWidgets.size} muted)`);
}
function unmuteWidget(widgetId: string): boolean {
return mutedWidgets.delete(widgetId);
}
/** The alerts the dashboard should actually show right now. */
function visibleAlerts(): Alert[] {
return applyAlertFilter(filters)
.filter((a) => !mutedWidgets.has(a.widgetId))
.filter((a) => a.acknowledgedAt === null);
}
/** Counts per severity, for the badge on the alerts tab. */
function countBySeverity(): Record<Alert['severity'], number> {
const counts: Record<Alert['severity'], number> = { info: 0, warn: 0, critical: 0 };
for (const alert of visibleAlerts()) counts[alert.severity] += 1;
return counts;
}
function reset(): void {
alerts = [];
filters = [];
mutedWidgets = new Set();
lastRefreshedAt = 0;
}
function snapshot() {
return { alerts: visibleAlerts(), counts: countBySeverity(), lastRefreshedAt };
}
return {
refreshAlerts,
applyAlertFilter,
acknowledge,
muteWidget,
unmuteWidget,
visibleAlerts,
countBySeverity,
reset,
snapshot,
};
}
export type AlertsStore = ReturnType<typeof createAlertsStore>;
@@ -0,0 +1,384 @@
import type { FilterSpec, MetricSample, StoreDeps, Widget } from './types';
import { defaultRequestOptions, joinPath, toQueryString } from '../lib/http';
const WIDGET_ENDPOINT = '/api/dashboard/widgets';
const METRIC_ENDPOINT = '/api/dashboard/metrics';
const SAMPLE_RETENTION_MS = 6 * 60 * 60 * 1000;
const MAX_SAMPLES_PER_WIDGET = 720;
const COLUMN_COUNT = 12;
/**
* The dashboard store: one factory closure holding every operation the
* dashboard performs. Callers get an object of closures; nothing inside is
* exported on its own.
*/
export function createDashboardStore(deps: StoreDeps, baseUrl: string) {
let widgets: Widget[] = [];
let samples: MetricSample[] = [];
let activeFilters: FilterSpec[] = [];
let lastSyncedAt = 0;
let loading = false;
let lastError: string | null = null;
const listeners = new Set<(snapshot: ReturnType<typeof snapshot>) => void>();
function snapshot() {
return {
widgets: widgets.filter((w) => !w.hidden),
sampleCount: samples.length,
filters: activeFilters.slice(),
lastSyncedAt,
loading,
lastError,
};
}
/**
* Fetch the widget set for the current user and merge it into local state,
* preserving any layout the user has moved since the last sync.
*/
async function loadWidgets(dashboardId: string, includeHidden = false): Promise<Widget[]> {
loading = true;
lastError = null;
const url = joinPath(baseUrl, WIDGET_ENDPOINT) + toQueryString({
dashboard: dashboardId,
hidden: includeHidden ? '1' : undefined,
});
let attempt = 0;
let payload: unknown = null;
while (attempt <= defaultRequestOptions.retries) {
try {
payload = await deps.fetchJson(url);
break;
} catch (error) {
attempt += 1;
if (attempt > defaultRequestOptions.retries) {
lastError = error instanceof Error ? error.message : String(error);
loading = false;
deps.log(`loadWidgets failed after ${attempt} attempts: ${lastError}`);
notify();
return widgets;
}
deps.log(`loadWidgets retry ${attempt} for ${dashboardId}`);
}
}
const incoming = Array.isArray(payload) ? (payload as Widget[]) : [];
const byId = new Map(widgets.map((w) => [w.id, w]));
const merged: Widget[] = [];
for (const next of incoming) {
const existing = byId.get(next.id);
if (!existing) {
merged.push({ ...next });
continue;
}
// Server owns identity and content; the client owns placement.
merged.push({
...next,
column: existing.column,
row: existing.row,
span: existing.span,
hidden: existing.hidden,
});
byId.delete(next.id);
}
for (const orphan of byId.values()) {
deps.log(`widget ${orphan.id} no longer exists on the server`);
}
widgets = merged;
lastSyncedAt = deps.now();
loading = false;
notify();
return widgets;
}
/**
* Pull fresh metric samples for every visible widget, append them to the
* rolling buffer, and drop anything past the retention window.
*/
async function refreshMetrics(windowMs = SAMPLE_RETENTION_MS): Promise<MetricSample[]> {
if (widgets.length === 0) {
deps.log('refreshMetrics called with no widgets loaded');
return samples;
}
loading = true;
const visible = widgets.filter((w) => !w.hidden);
const collected: MetricSample[] = [];
for (const widget of visible) {
const url = joinPath(baseUrl, METRIC_ENDPOINT) + toQueryString({
widget: widget.id,
since: deps.now() - windowMs,
});
let payload: unknown;
try {
payload = await deps.fetchJson(url);
} catch (error) {
lastError = error instanceof Error ? error.message : String(error);
deps.log(`refreshMetrics failed for ${widget.id}: ${lastError}`);
continue;
}
if (!Array.isArray(payload)) {
deps.log(`refreshMetrics got a non-array payload for ${widget.id}`);
continue;
}
for (const raw of payload as MetricSample[]) {
if (typeof raw.value !== 'number' || Number.isNaN(raw.value)) continue;
if (typeof raw.at !== 'number' || raw.at <= 0) continue;
collected.push({
widgetId: widget.id,
at: raw.at,
value: raw.value,
unit: raw.unit ?? 'count',
});
}
}
const cutoff = deps.now() - windowMs;
const kept = samples.filter((s) => s.at >= cutoff);
samples = kept.concat(collected);
pruneSamples(MAX_SAMPLES_PER_WIDGET);
lastSyncedAt = deps.now();
loading = false;
notify();
return samples;
}
/**
* Replace the active filter set and recompute which widgets stay visible.
* A widget survives when every filter matches one of its fields.
*/
function applyFilter(specs: readonly FilterSpec[]): Widget[] {
activeFilters = specs.slice();
if (activeFilters.length === 0) {
widgets = widgets.map((w) => ({ ...w, hidden: false }));
notify();
return widgets;
}
const matches = (widget: Widget, spec: FilterSpec): boolean => {
const field = spec.field === 'title'
? widget.title
: spec.field === 'kind'
? widget.kind
: spec.field === 'column'
? String(widget.column)
: '';
switch (spec.op) {
case 'eq':
return field.toLowerCase() === spec.value.toLowerCase();
case 'contains':
return field.toLowerCase().includes(spec.value.toLowerCase());
case 'gt':
return Number(field) > Number(spec.value);
case 'lt':
return Number(field) < Number(spec.value);
default:
return false;
}
};
let hiddenCount = 0;
widgets = widgets.map((widget) => {
const visible = activeFilters.every((spec) => matches(widget, spec));
if (!visible) hiddenCount += 1;
return { ...widget, hidden: !visible };
});
deps.log(`applyFilter hid ${hiddenCount} of ${widgets.length} widgets`);
notify();
return widgets;
}
/**
* Render the current sample buffer as CSV, one row per sample, ordered by
* widget then timestamp so a diff between two exports stays readable.
*/
function exportCsv(separator = ','): string {
const header = ['widget', 'title', 'at', 'value', 'unit'].join(separator);
if (samples.length === 0) return header;
const titles = new Map(widgets.map((w) => [w.id, w.title]));
const ordered = samples.slice().sort((a, b) => {
if (a.widgetId !== b.widgetId) return a.widgetId < b.widgetId ? -1 : 1;
return a.at - b.at;
});
const escape = (value: string): string => {
if (!value.includes(separator) && !value.includes('"') && !value.includes('\n')) return value;
return `"${value.replace(/"/g, '""')}"`;
};
const rows = ordered.map((sample) => [
escape(sample.widgetId),
escape(titles.get(sample.widgetId) ?? '(unknown)'),
String(sample.at),
String(sample.value),
escape(sample.unit),
].join(separator));
return [header, ...rows].join('\n');
}
/**
* Pack widgets back into a dense grid after a move or a hide, so the layout
* never leaves a hole a user has to scroll past.
*/
function reconcileLayout(columnCount = COLUMN_COUNT): Widget[] {
const visible = widgets.filter((w) => !w.hidden);
const hidden = widgets.filter((w) => w.hidden);
const ordered = visible.slice().sort((a, b) => {
if (a.row !== b.row) return a.row - b.row;
return a.column - b.column;
});
const rowWidth = new Map<number, number>();
const placed: Widget[] = [];
for (const widget of ordered) {
const span = Math.max(1, Math.min(widget.span, columnCount));
let row = 0;
let column = 0;
for (;;) {
const used = rowWidth.get(row) ?? 0;
if (used + span <= columnCount) {
column = used;
rowWidth.set(row, used + span);
break;
}
row += 1;
}
placed.push({ ...widget, row, column, span });
}
let trailing = placed.length > 0 ? Math.max(...placed.map((w) => w.row)) + 1 : 0;
for (const widget of hidden) {
placed.push({ ...widget, row: trailing, column: 0 });
trailing += 1;
}
widgets = placed;
notify();
return widgets;
}
/**
* Cap the rolling buffer per widget, keeping the newest samples. Called after
* every refresh so memory stays bounded on a long-lived dashboard.
*/
function pruneSamples(perWidget = MAX_SAMPLES_PER_WIDGET): number {
if (samples.length === 0) return 0;
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]);
}
let dropped = 0;
const kept: MetricSample[] = [];
for (const [, bucket] of grouped) {
bucket.sort((a, b) => a.at - b.at);
if (bucket.length > perWidget) {
dropped += bucket.length - perWidget;
kept.push(...bucket.slice(bucket.length - perWidget));
} else {
kept.push(...bucket);
}
}
kept.sort((a, b) => a.at - b.at);
samples = kept;
if (dropped > 0) deps.log(`pruneSamples dropped ${dropped} samples`);
return dropped;
}
/**
* Reduce the buffer to one aggregate per widget — the numbers the summary
* strip at the top of the dashboard renders.
*/
function summarize(): Array<{ widgetId: string; title: string; min: number; max: number; mean: number; count: 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; min: number; max: number; mean: number; count: number }> = [];
for (const [widgetId, bucket] of grouped) {
let min = Number.POSITIVE_INFINITY;
let max = Number.NEGATIVE_INFINITY;
let total = 0;
for (const sample of bucket) {
if (sample.value < min) min = sample.value;
if (sample.value > max) max = sample.value;
total += sample.value;
}
out.push({
widgetId,
title: titles.get(widgetId) ?? '(unknown)',
min: bucket.length > 0 ? min : 0,
max: bucket.length > 0 ? max : 0,
mean: bucket.length > 0 ? total / bucket.length : 0,
count: bucket.length,
});
}
out.sort((a, b) => b.count - a.count || (a.title < b.title ? -1 : 1));
return out;
}
/** Register a listener and get an unsubscribe back. */
function subscribe(listener: (snapshot: ReturnType<typeof snapshot>) => void): () => void {
listeners.add(listener);
listener(snapshot());
return () => {
listeners.delete(listener);
};
}
function notify(): void {
const current = snapshot();
for (const listener of listeners) {
try {
listener(current);
} catch (error) {
deps.log(`dashboard listener threw: ${error instanceof Error ? error.message : String(error)}`);
}
}
}
/** Drop every sample and widget — used when the user switches dashboards. */
function reset(): void {
widgets = [];
samples = [];
activeFilters = [];
lastSyncedAt = 0;
lastError = null;
loading = false;
notify();
}
return {
loadWidgets,
refreshMetrics,
applyFilter,
exportCsv,
reconcileLayout,
pruneSamples,
summarize,
subscribe,
reset,
snapshot,
};
}
export type DashboardStore = ReturnType<typeof createDashboardStore>;
/** One-line description of a store's state, for the debug panel. */
export function describeStore(store: DashboardStore): string {
const state = store.snapshot();
return `${state.widgets.length} widgets · ${state.sampleCount} samples · synced ${state.lastSyncedAt}`;
}
@@ -0,0 +1,28 @@
export interface Widget {
id: string;
kind: 'chart' | 'table' | 'stat';
title: string;
column: number;
row: number;
span: number;
hidden: boolean;
}
export interface MetricSample {
widgetId: string;
at: number;
value: number;
unit: string;
}
export interface FilterSpec {
field: string;
op: 'eq' | 'gt' | 'lt' | 'contains';
value: string;
}
export interface StoreDeps {
fetchJson: (url: string) => Promise<unknown>;
now: () => number;
log: (message: string) => void;
}
@@ -0,0 +1,43 @@
import type { DashboardStore } from '../stores/dashboard-store';
import type { FilterSpec } from '../stores/types';
import { medianOf } from '../lib/metrics';
/** The dashboard panel — the only consumer of the store's closures. */
export function mountPanel(store: DashboardStore, dashboardId: string) {
let disposed = false;
const unsubscribe = store.subscribe((state) => {
if (disposed) return;
render(state.widgets.length, state.sampleCount, state.loading);
});
async function boot(): Promise<void> {
await store.loadWidgets(dashboardId);
await store.refreshMetrics();
store.reconcileLayout();
}
function search(text: string): void {
const specs: FilterSpec[] = text.trim().length === 0
? []
: [{ field: 'title', op: 'contains', value: text.trim() }];
store.applyFilter(specs);
}
function download(): string {
return store.exportCsv();
}
function render(widgetCount: number, sampleCount: number, loading: boolean): void {
void widgetCount;
void sampleCount;
void loading;
}
function dispose(): void {
disposed = true;
unsubscribe();
}
return { boot, search, download, dispose, median: medianOf };
}
@@ -0,0 +1,144 @@
#!/usr/bin/env node
/**
* CG-27 measurement probe — what a factory-closure file actually delivers.
*
* `probe-allocation.mjs` measures how the envelope is split BETWEEN files. This
* one measures what comes back from WITHIN one file whose top-level symbol spans
* almost all of it: a `createFoo()` factory returning an object of closures
* (Svelte 5 rune stores, React hook modules, Zustand `create((set,get)=>({…}))`,
* IIFE module-pattern JS). The claim under test is a ranking one, not a byte one
* — CG-30 already bounds the bytes — so the number that matters is WHICH inner
* symbols reach the agent, not how many chars did.
*
* Prints, for the factory file: every line range the response delivered, and for
* each inner function whether its DEFINITION LINE is inside one of them.
*
* Usage (needs a current `npm run build`):
* node scripts/agent-eval/probe-factory-closure.mjs
* node scripts/agent-eval/probe-factory-closure.mjs --json
* node scripts/agent-eval/probe-factory-closure.mjs --query "..."
*/
import { cpSync, mkdtempSync, readFileSync, rmSync, existsSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { dirname, join, resolve } from 'node:path';
import { fileURLToPath, pathToFileURL } from 'node:url';
const HERE = dirname(fileURLToPath(import.meta.url));
const REPO_ROOT = resolve(HERE, '../..');
const FIXTURE = join(REPO_ROOT, '__tests__/fixtures/factory-closure-ts');
const TARGET = 'src/stores/dashboard-store.ts';
const argv = process.argv.slice(2);
const asJson = argv.includes('--json');
const queryAt = argv.indexOf('--query');
const QUERY = queryAt >= 0
? argv[queryAt + 1]
: 'how does the dashboard store refresh its metrics and apply a filter';
const say = (s = '') => { if (!asJson) console.log(s); };
const num = (n) => Math.round(n).toLocaleString('en-US');
const load = (rel) => import(pathToFileURL(resolve(REPO_ROOT, rel)).href);
if (!existsSync(join(REPO_ROOT, 'dist/index.js'))) {
console.error('dist/ not built — run `npm run build` first.');
process.exit(2);
}
const idxMod = await load('dist/index.js');
const toolsMod = await load('dist/mcp/tools.js');
const CodeGraph = idxMod.default?.default ?? idxMod.default ?? idxMod.CodeGraph;
const ToolHandler = toolsMod.ToolHandler ?? toolsMod.default?.ToolHandler;
const dir = mkdtempSync(join(tmpdir(), 'cg-factory-'));
cpSync(FIXTURE, dir, { recursive: true });
rmSync(join(dir, '.codegraph'), { recursive: true, force: true });
let out;
try {
let cg = CodeGraph.initSync(dir);
await cg.indexAll();
// Inner function definitions, straight from the index — the symbols the file's
// enclosing factory range would otherwise swallow.
const nodes = cg.getNodesInFile(TARGET);
const factory = nodes.find((n) => n.name === 'createDashboardStore');
const inner = nodes
.filter((n) => (n.kind === 'function' || n.kind === 'method')
&& n.name !== 'createDashboardStore'
&& factory && n.startLine > factory.startLine && n.endLine <= factory.endLine)
.sort((a, b) => a.startLine - b.startLine);
cg.close?.();
const sidecar = join(dir, 'diag.jsonl');
process.env.CODEGRAPH_EXPLORE_DEBUG = sidecar;
cg = CodeGraph.openSync(dir);
const res = await new ToolHandler(cg).execute('codegraph_explore', { query: QUERY });
const text = res.content?.[0]?.text ?? '';
cg.close?.();
delete process.env.CODEGRAPH_EXPLORE_DEBUG;
const report = JSON.parse(readFileSync(sidecar, 'utf8').trim().split('\n').pop());
// Which source lines of the target file the response actually carries. The
// response numbers every delivered line `<n>\t<text>`; match them back against
// the file so a line number that merely appears in prose can't count.
const source = readFileSync(join(dir, TARGET), 'utf8').split('\n');
const delivered = new Set();
for (const line of text.split('\n')) {
const m = /^(\d+)\t(.*)$/.exec(line);
if (!m) continue;
const n = Number(m[1]);
if (n >= 1 && n <= source.length && source[n - 1] === m[2]) delivered.add(n);
}
// Collapse to ranges for display.
const ranges = [];
for (const n of [...delivered].sort((a, b) => a - b)) {
const last = ranges[ranges.length - 1];
if (last && n === last.end + 1) last.end = n;
else ranges.push({ start: n, end: n });
}
const covered = (n) => delivered.has(n.startLine);
const rec = report.files.find((f) => f.path === TARGET) ?? null;
out = {
query: QUERY,
target: TARGET,
fileLines: source.length,
factory: factory ? { name: factory.name, start: factory.startLine, end: factory.endLine } : null,
file: rec && {
rank: rec.rank, render: rec.render, clipped: rec.clipped,
emittedChars: rec.emittedChars, finalChars: rec.finalChars,
allowance: rec.allowance, spendable: rec.spendable, skipped: rec.skipped,
},
deliveredRanges: ranges,
deliveredLines: delivered.size,
inner: inner.map((n) => ({ name: n.name, start: n.startLine, end: n.endLine, delivered: covered(n) })),
innerDelivered: inner.filter(covered).length,
innerTotal: inner.length,
envelope: report.envelope,
allFiles: report.files
.filter((f) => f.emittedChars > 0 || f.finalChars > 0)
.map((f) => ({ rank: f.rank, path: f.path, render: f.render, emitted: f.emittedChars, final: f.finalChars })),
};
} finally {
rmSync(dir, { recursive: true, force: true });
}
if (asJson) {
console.log(JSON.stringify(out, null, 2));
} else {
say(`query "${out.query}"`);
say(`target ${out.target}${out.fileLines} lines, factory ${out.factory?.name} spans ${out.factory?.start}${out.factory?.end}`);
say('');
say(' # render emitted final file');
for (const f of out.allFiles) {
say(` ${String(f.rank).padStart(2)} ${(f.render ?? '-').padEnd(10)} ${num(f.emitted).padStart(7)} ${num(f.final).padStart(7)} ${f.path}`);
}
say('');
say(`delivered lines of ${out.target}: ${out.deliveredLines}`);
say(` ranges: ${out.deliveredRanges.map((r) => `${r.start}-${r.end}`).join(', ') || '(none)'}`);
say('');
say(`inner symbols whose definition reached the agent: ${out.innerDelivered}/${out.innerTotal}`);
for (const n of out.inner) {
say(` ${n.delivered ? '✓' : '·'} ${n.name} (${n.start}${n.end})`);
}
}