coverage meter one-truth fix + chunk-budget setting UI: fetchGraphRead resolves through the shared core resolver (the stale 4096 fallback filled the ring at half the 8192 truth); Graph section gains the extraction chunk budget row (floor 500, empty = factory) and the master switch composes instead of replacing the section
This commit is contained in:
@@ -7,6 +7,7 @@ import { useSettingsSaved } from "@/components/settings-signal";
|
|||||||
import { formatTokens } from "@/components/usage-format";
|
import { formatTokens } from "@/components/usage-format";
|
||||||
import { Hint } from "@/components/ui/hint";
|
import { Hint } from "@/components/ui/hint";
|
||||||
import { timeAgo } from "@/components/sidebar";
|
import { timeAgo } from "@/components/sidebar";
|
||||||
|
import { resolveSettings } from "@/src/core";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The extraction-coverage surfaces (extraction-UX redesign, owner-locked
|
* The extraction-coverage surfaces (extraction-UX redesign, owner-locked
|
||||||
@@ -53,25 +54,26 @@ async function fetchCoverage(chatId: string): Promise<SessionCoverage | null> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** The ring's denominator + visibility: the graph section of the settings
|
/** The ring's denominator + visibility, resolved through the SAME core
|
||||||
* record (both fields optional — absent means enabled/4096, the factory
|
* resolver the server's effective settings use (fix, 2026-10-02): the
|
||||||
* resolution; see GraphSettings). Null on fetch failure → no ring. */
|
* meter denominates at exactly the number the extraction trigger waits
|
||||||
|
* for — the old hand-rolled `?? 4096` fallback drifted from the factory
|
||||||
|
* default (8192) and the ring filled at half the truth. Null on fetch
|
||||||
|
* failure → no ring. */
|
||||||
export interface GraphRead {
|
export interface GraphRead {
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
chunkTokens: number;
|
chunkTokens: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchGraphRead(): Promise<GraphRead | null> {
|
/** Exported for the render pin (the ringFill precedent): resolves the
|
||||||
|
* raw settings delta to the factory-true graph section. */
|
||||||
|
export async function fetchGraphRead(): Promise<GraphRead | null> {
|
||||||
try {
|
try {
|
||||||
const res = await fetch("/api/settings");
|
const res = await fetch("/api/settings");
|
||||||
if (!res.ok) return null;
|
if (!res.ok) return null;
|
||||||
const body = (await res.json()) as { settings?: { graph?: { enabled?: boolean; extractionChunkTokens?: number } } };
|
const body = (await res.json()) as { settings?: Parameters<typeof resolveSettings>[0] };
|
||||||
const graph = body.settings?.graph;
|
const graph = resolveSettings(body.settings ?? null).graph;
|
||||||
if (!graph) return null;
|
return { enabled: graph.enabled, chunkTokens: graph.extractionChunkTokens };
|
||||||
return {
|
|
||||||
enabled: graph.enabled !== false,
|
|
||||||
chunkTokens: graph.extractionChunkTokens ?? 4096,
|
|
||||||
};
|
|
||||||
} catch {
|
} catch {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,7 +31,12 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import type { AppearanceSettings, GraphSettings } from "@/src/core";
|
import {
|
||||||
|
EXTRACTION_CHUNK_TOKENS_FLOOR,
|
||||||
|
resolveSettings,
|
||||||
|
type AppearanceSettings,
|
||||||
|
type GraphSettings,
|
||||||
|
} from "@/src/core";
|
||||||
import { MenuShell } from "@/components/menu-shell";
|
import { MenuShell } from "@/components/menu-shell";
|
||||||
import { Hint } from "@/components/ui/hint";
|
import { Hint } from "@/components/ui/hint";
|
||||||
import { useAppearance } from "./appearance.tsx";
|
import { useAppearance } from "./appearance.tsx";
|
||||||
@@ -346,18 +351,61 @@ export function GraphSection({
|
|||||||
onSectionChange: (next: GraphSettings) => void;
|
onSectionChange: (next: GraphSettings) => void;
|
||||||
}) {
|
}) {
|
||||||
// Wave 7 collapse (owner ruling 2026-09-23): the section is the MASTER
|
// Wave 7 collapse (owner ruling 2026-09-23): the section is the MASTER
|
||||||
// SWITCH ONLY. The embedding/clustering/labeling rungs and the BYOK
|
// SWITCH — the embedding/clustering/labeling rungs are gone; clustering
|
||||||
// embedding-model picker are gone — clustering is the free birth-session
|
// is the free birth-session partition derived on read, labels ARE session
|
||||||
// partition derived on read, labels ARE session names, and the prober's
|
// names. The chunk budget (2026-10-02) joined it as the section's second
|
||||||
// graph tools read the ledgers directly. There is nothing left to
|
// and only other control: it denominates the coverage ring AND gates the
|
||||||
// configure but on/off.
|
// auto-extraction trigger (turn settle ≥ budget fires the run).
|
||||||
|
//
|
||||||
|
// Resolved through the SAME core resolver the server's effective
|
||||||
|
// settings use (one source of truth, the 4096-fallback fix 2026-10-02):
|
||||||
|
// the hint and placeholder report exactly the number the trigger waits
|
||||||
|
// for — never a second hand-rolled default.
|
||||||
|
const resolved = resolveSettings(graph === undefined ? null : { version: 1, graph }).graph;
|
||||||
return (
|
return (
|
||||||
<SettingsRow label="Graph" testid="graph-row-enabled">
|
<>
|
||||||
<Toggle
|
<SettingsRow label="Graph" testid="graph-row-enabled">
|
||||||
checked={graph?.enabled ?? true}
|
<Toggle
|
||||||
label="graph enabled"
|
checked={graph?.enabled ?? true}
|
||||||
onChange={(on) => onSectionChange({ enabled: on })}
|
label="graph enabled"
|
||||||
/>
|
onChange={(on) =>
|
||||||
</SettingsRow>
|
// Compose, never replace-whole (the section PUT is, but the
|
||||||
|
// row must not drop its sibling field — fixed with the budget
|
||||||
|
// row's arrival: the old { enabled: on } wiped a stored budget).
|
||||||
|
onSectionChange({ ...graph, enabled: on })
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</SettingsRow>
|
||||||
|
<SettingsRow
|
||||||
|
label="Extraction chunk budget"
|
||||||
|
hint={`${resolved.extractionChunkTokens} tok`}
|
||||||
|
testid="graph-row-chunk-tokens"
|
||||||
|
>
|
||||||
|
<CommitInput
|
||||||
|
type="number"
|
||||||
|
value={String(graph?.extractionChunkTokens ?? "")}
|
||||||
|
placeholder={String(resolved.extractionChunkTokens)}
|
||||||
|
ariaLabel="extraction chunk budget"
|
||||||
|
testid="input-chunk-tokens"
|
||||||
|
onCommit={(v) => {
|
||||||
|
// Empty = factory default (the fix-wave discipline): absence is
|
||||||
|
// the unset for numbers — Number("") is 0, so clamping would
|
||||||
|
// write the floor instead of unsetting.
|
||||||
|
if (v === "") {
|
||||||
|
onSectionChange({ ...graph, extractionChunkTokens: undefined });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const n = Number(v);
|
||||||
|
if (!Number.isFinite(n) || n <= 0) return; // bad input: no write
|
||||||
|
// Mirror the server sanitize exactly: floor only (cost
|
||||||
|
// protection), no ceiling — a big budget is the user's call.
|
||||||
|
onSectionChange({
|
||||||
|
...graph,
|
||||||
|
extractionChunkTokens: Math.max(EXTRACTION_CHUNK_TOKENS_FLOOR, Math.round(n)),
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</SettingsRow>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
CoverageMenuBody,
|
CoverageMenuBody,
|
||||||
CoverageRingDetail,
|
CoverageRingDetail,
|
||||||
CoverageRingGlyph,
|
CoverageRingGlyph,
|
||||||
|
fetchGraphRead,
|
||||||
ringFill,
|
ringFill,
|
||||||
type SessionCoverage,
|
type SessionCoverage,
|
||||||
} from "../components/extraction-coverage.tsx";
|
} from "../components/extraction-coverage.tsx";
|
||||||
@@ -34,6 +35,58 @@ function coverage(over: Partial<SessionCoverage> = {}): SessionCoverage {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
describe("fetchGraphRead — one truth with the extraction trigger (2026-10-02)", () => {
|
||||||
|
// The meter's denominator MUST be the number the server's
|
||||||
|
// effectiveSettings gate waits for. The old hand-rolled fallback
|
||||||
|
// read the raw delta and guessed `?? 4096` — a stale pre-setting-era
|
||||||
|
// constant — so an unset budget filled the ring at half the truth
|
||||||
|
// (owner repro: 6k shown full, extraction correctly silent at 8192).
|
||||||
|
// Now the raw delta resolves through the SAME core resolver.
|
||||||
|
const stub = (payload: unknown, ok = true) => {
|
||||||
|
const real = globalThis.fetch;
|
||||||
|
globalThis.fetch = (async () =>
|
||||||
|
new Response(JSON.stringify(payload), { status: ok ? 200 : 500 })) as typeof fetch;
|
||||||
|
return () => {
|
||||||
|
globalThis.fetch = real;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
it("an absent graph section denominates at the factory 8192 — never 4096, never a missing ring", async () => {
|
||||||
|
const restore = stub({ settings: { version: 1 } });
|
||||||
|
const read = await fetchGraphRead();
|
||||||
|
restore();
|
||||||
|
assert.deepEqual(read, { enabled: true, chunkTokens: 8192 });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("the owner repro shape — graph section present, budget never written — is 8192", async () => {
|
||||||
|
const restore = stub({ settings: { version: 1, graph: { enabled: true } } });
|
||||||
|
const read = await fetchGraphRead();
|
||||||
|
restore();
|
||||||
|
assert.equal(read?.chunkTokens, 8192, "the record lacks the key: factory, not the stale 4096");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("first boot (settings null) resolves factory — the ring shows, at the true budget", async () => {
|
||||||
|
const restore = stub({ settings: null });
|
||||||
|
const read = await fetchGraphRead();
|
||||||
|
restore();
|
||||||
|
assert.deepEqual(read, { enabled: true, chunkTokens: 8192 });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("a custom budget rides through verbatim", async () => {
|
||||||
|
const restore = stub({ settings: { version: 1, graph: { enabled: true, extractionChunkTokens: 2000 } } });
|
||||||
|
const read = await fetchGraphRead();
|
||||||
|
restore();
|
||||||
|
assert.equal(read?.chunkTokens, 2000);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("fetch failure → null → no ring", async () => {
|
||||||
|
const restore = stub({}, false);
|
||||||
|
const read = await fetchGraphRead();
|
||||||
|
restore();
|
||||||
|
assert.equal(read, null);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("ringFill (the donut's fold, pure)", () => {
|
describe("ringFill (the donut's fold, pure)", () => {
|
||||||
it("fills uncovered/chunk, clamped to [0,1]", () => {
|
it("fills uncovered/chunk, clamped to [0,1]", () => {
|
||||||
assert.equal(ringFill(0, 4096), 0);
|
assert.equal(ringFill(0, 4096), 0);
|
||||||
|
|||||||
@@ -166,12 +166,15 @@ describe("AgentSection (thin SSR)", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("GraphSection — the collapsed master switch (wave 7)", () => {
|
describe("GraphSection — master switch + chunk budget (wave 7 + 2026-10-02)", () => {
|
||||||
// Wave 7 (owner ruling 2026-09-23): the section is the master switch
|
// Wave 7 (owner ruling 2026-09-23): the section is the master switch —
|
||||||
// ONLY — the embedding/clustering/labeling rungs and the BYOK model
|
// the embedding/clustering/labeling rungs and the BYOK model picker are
|
||||||
// picker are gone (clustering is the free birth-session partition,
|
// gone (clustering is the free birth-session partition, labels ARE
|
||||||
// labels ARE session names). One row, one toggle.
|
// session names). The chunk budget (2026-10-02) joined as the second and
|
||||||
it("renders the single master-switch row", () => {
|
// only other control — it denominates the coverage ring and gates the
|
||||||
|
// auto-extraction trigger, resolved through the same core resolver the
|
||||||
|
// server uses (the 4096-fallback fix).
|
||||||
|
it("renders the master-switch row", () => {
|
||||||
const html = renderToStaticMarkup(<GraphSection graph={undefined} onSectionChange={() => {}} />);
|
const html = renderToStaticMarkup(<GraphSection graph={undefined} onSectionChange={() => {}} />);
|
||||||
assert.ok(html.includes('data-testid="graph-row-enabled"'));
|
assert.ok(html.includes('data-testid="graph-row-enabled"'));
|
||||||
assert.ok(!html.includes("graph-row-embedding"), "the dead rungs render nothing");
|
assert.ok(!html.includes("graph-row-embedding"), "the dead rungs render nothing");
|
||||||
@@ -185,6 +188,22 @@ describe("GraphSection — the collapsed master switch (wave 7)", () => {
|
|||||||
const html = renderToStaticMarkup(<GraphSection graph={{ enabled: false }} onSectionChange={() => {}} />);
|
const html = renderToStaticMarkup(<GraphSection graph={{ enabled: false }} onSectionChange={() => {}} />);
|
||||||
assert.ok(tagOf(html, "toggle-graph-enabled").includes('aria-checked="false"'));
|
assert.ok(tagOf(html, "toggle-graph-enabled").includes('aria-checked="false"'));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("the chunk-budget row: unset shows EMPTY with the factory placeholder + hint — one truth with the trigger", () => {
|
||||||
|
const html = renderToStaticMarkup(<GraphSection graph={undefined} onSectionChange={() => {}} />);
|
||||||
|
const input = tagOf(html, "input-chunk-tokens");
|
||||||
|
assert.ok(input.includes('placeholder="8192"'), "the placeholder IS the resolved factory default");
|
||||||
|
assert.ok(!/value="\d/.test(input), "unset renders EMPTY, not a materialized 8192");
|
||||||
|
assert.ok(html.includes(">8192 tok</"), "the hint reports the resolved truth");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("a stored budget shows its value and denominates the hint", () => {
|
||||||
|
const html = renderToStaticMarkup(
|
||||||
|
<GraphSection graph={{ enabled: true, extractionChunkTokens: 6000 }} onSectionChange={() => {}} />,
|
||||||
|
);
|
||||||
|
assert.ok(tagOf(html, "input-chunk-tokens").includes('value="6000"'));
|
||||||
|
assert.ok(html.includes(">6000 tok</"));
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("KeysSection (thin SSR)", () => {
|
describe("KeysSection (thin SSR)", () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user