mirror of
https://github.com/DeusData/codebase-memory-mcp.git
synced 2026-11-08 08:53:43 +08:00
feat(ui): observed vs possible — runtime markers in flows and traces
Trace hops and flow steps that actually fired render '▶ observed ×N' with the run label and date; a fully-observed trace says so in one line, and the footer states the honesty rule: unmarked hops are possible, not dead — runs only cover what they exercised. The wiki gains the observed entry (first-class, with its caps), and F5's gap narrows to what is still missing (automatic capture). Live-smoke fix on the C side: resolve_store hands handlers a query-only connection, so ingest writes silently no-op'd against real project stores — ingest_traces now takes the dedicated read-write connection (open_store_for_write, né open_adr_store_for_write), whose open also creates the observed tables in pre-existing databases. Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
This commit is contained in:
@@ -289,6 +289,7 @@ export function App() {
|
||||
navigate({ tab: "flows", flow: id !== null ? String(id) : null })
|
||||
}
|
||||
onOpenSymbol={openSymbol}
|
||||
onOpenWiki={setWikiSlug}
|
||||
/>
|
||||
) : activeTab === "changes" && selectedProject ? (
|
||||
<ChangesTab project={selectedProject} onOpenSymbol={openSymbol} />
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
/* @vitest-environment jsdom */
|
||||
import "@testing-library/jest-dom/vitest";
|
||||
import { fireEvent, render, screen } from "@testing-library/react";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { ObservedChip } from "./FlowsTab";
|
||||
|
||||
/* The chip is exercised in isolation — FlowsTab itself fetches on mount.
|
||||
* The all-observed line logic lives in lib/observed.ts and is tested there. */
|
||||
describe("ObservedChip", () => {
|
||||
const observed = {
|
||||
count: 3,
|
||||
label: "pytest 2026-08-27",
|
||||
last_seen: "2026-08-27T14:12:00Z",
|
||||
};
|
||||
|
||||
it("renders glyph, word and count, with the run in the title", () => {
|
||||
render(<ObservedChip observed={observed} />);
|
||||
const chip = screen.getByTitle("ran in pytest 2026-08-27 · last 2026-08-27");
|
||||
expect(chip).toHaveTextContent("▶ observed ×3");
|
||||
/* No onOpenWiki → plain text, no nested button (flow-step rows are buttons). */
|
||||
expect(screen.queryByRole("button")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("opens the wiki entry from the marker word when onOpenWiki is given", () => {
|
||||
const onOpenWiki = vi.fn();
|
||||
render(<ObservedChip observed={observed} onOpenWiki={onOpenWiki} />);
|
||||
fireEvent.click(screen.getByRole("button", { name: "observed" }));
|
||||
expect(onOpenWiki).toHaveBeenCalledWith("observed");
|
||||
});
|
||||
});
|
||||
@@ -10,10 +10,13 @@ import {
|
||||
searchGraph,
|
||||
type FlowDetail,
|
||||
type FlowsPayload,
|
||||
type ObservedCall,
|
||||
type TracePayload,
|
||||
} from "../lib/atlas";
|
||||
import { groupFlowsByEntry } from "../lib/flowgroup";
|
||||
import { allHopsObserved, newestObserved, observedTitle } from "../lib/observed";
|
||||
import { formatGuard } from "../lib/why";
|
||||
import { MetricChip } from "./MetricChip";
|
||||
import { AddToPromptButton } from "./PromptBasket";
|
||||
|
||||
interface FlowsTabProps {
|
||||
@@ -21,6 +24,7 @@ interface FlowsTabProps {
|
||||
flowId: number | null;
|
||||
onOpenFlow: (id: number | null) => void;
|
||||
onOpenSymbol: (ref: { id?: number; qn?: string }) => void;
|
||||
onOpenWiki: (slug: string) => void;
|
||||
}
|
||||
|
||||
function flowToMermaid(detail: FlowDetail): string {
|
||||
@@ -35,6 +39,35 @@ function flowToMermaid(detail: FlowDetail): string {
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
/* "▶ observed ×N" — runtime-evidence marker beside the guard chips: this
|
||||
* exact hop fired in a recorded run. Glyph + word, never color-only. With
|
||||
* onOpenWiki the word opens the wiki entry; omit it where the chip sits
|
||||
* inside a row button (flow steps) — nested buttons are invalid HTML. */
|
||||
export function ObservedChip({
|
||||
observed,
|
||||
onOpenWiki,
|
||||
}: {
|
||||
observed: ObservedCall;
|
||||
onOpenWiki?: (slug: string) => void;
|
||||
}) {
|
||||
return (
|
||||
<span
|
||||
className="text-[10px] px-1 rounded-full border border-good/50 text-good/80 font-mono shrink-0"
|
||||
title={observedTitle(observed)}
|
||||
>
|
||||
▶{" "}
|
||||
{onOpenWiki ? (
|
||||
<MetricChip slug="observed" onOpen={onOpenWiki}>
|
||||
observed
|
||||
</MetricChip>
|
||||
) : (
|
||||
"observed"
|
||||
)}{" "}
|
||||
×{observed.count}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
/* One endpoint picker: type ≥2 chars, pick from symbol matches. */
|
||||
function EndpointPicker({
|
||||
project,
|
||||
@@ -118,7 +151,13 @@ function EndpointPicker({
|
||||
);
|
||||
}
|
||||
|
||||
export function FlowsTab({ project, flowId, onOpenFlow, onOpenSymbol }: FlowsTabProps) {
|
||||
export function FlowsTab({
|
||||
project,
|
||||
flowId,
|
||||
onOpenFlow,
|
||||
onOpenSymbol,
|
||||
onOpenWiki,
|
||||
}: FlowsTabProps) {
|
||||
const [payload, setPayload] = useState<FlowsPayload | null>(null);
|
||||
const [detail, setDetail] = useState<FlowDetail | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
@@ -186,6 +225,13 @@ export function FlowsTab({ project, flowId, onOpenFlow, onOpenSymbol }: FlowsTab
|
||||
};
|
||||
}, [payload]);
|
||||
|
||||
/* Newest observation each — names the runtime-freshness footers. */
|
||||
const traceNewest = useMemo(() => newestObserved(trace?.path ?? []), [trace]);
|
||||
const detailNewest = useMemo(
|
||||
() => (detail ? newestObserved(detail.steps) : null),
|
||||
[detail],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!copied) return;
|
||||
const timer = setTimeout(() => setCopied(false), 1500);
|
||||
@@ -254,6 +300,11 @@ export function FlowsTab({ project, flowId, onOpenFlow, onOpenSymbol }: FlowsTab
|
||||
reachable in {trace.hops} hop{trace.hops === 1 ? "" : "s"} via{" "}
|
||||
{trace.mode === "data" ? "data flow" : "calls"}
|
||||
</p>
|
||||
{allHopsObserved(trace.path ?? []) && (
|
||||
<p className="text-[11px] text-good/70 mb-1">
|
||||
This whole path was observed in recorded runs.
|
||||
</p>
|
||||
)}
|
||||
<div className="flex items-center gap-1 flex-wrap">
|
||||
{(trace.path ?? []).map((step, index) => (
|
||||
<span key={index} className="flex items-center gap-1 flex-wrap">
|
||||
@@ -280,9 +331,19 @@ export function FlowsTab({ project, flowId, onOpenFlow, onOpenSymbol }: FlowsTab
|
||||
{formatGuard(guard)}
|
||||
</span>
|
||||
))}
|
||||
{step.observed && (
|
||||
<ObservedChip observed={step.observed} onOpenWiki={onOpenWiki} />
|
||||
)}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
{traceNewest && (
|
||||
<p className="text-[11px] text-foreground/40 mt-1.5">
|
||||
Runtime markers from recorded runs ({traceNewest.label}).
|
||||
Unmarked hops are possible, not dead — runs only cover
|
||||
what they exercised.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-[12px] text-foreground/45">
|
||||
@@ -452,12 +513,20 @@ export function FlowsTab({ project, flowId, onOpenFlow, onOpenSymbol }: FlowsTab
|
||||
{formatGuard(guard)}
|
||||
</span>
|
||||
))}
|
||||
{step.observed && <ObservedChip observed={step.observed} />}
|
||||
<span className="text-[12px] text-foreground/35 truncate ml-auto shrink-0 max-w-[40%]">
|
||||
{step.file_path}
|
||||
</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
{detailNewest && (
|
||||
<p className="text-[11px] text-foreground/40 mt-3">
|
||||
Runtime markers from recorded runs ({detailNewest.label}).
|
||||
Unmarked hops are possible, not dead — runs only cover what
|
||||
they exercised.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
)}
|
||||
|
||||
@@ -138,6 +138,15 @@ export interface FlowsPayload {
|
||||
flows: FlowSummary[];
|
||||
}
|
||||
|
||||
/* Runtime evidence: this exact caller→callee pair fired in a recorded run.
|
||||
* Absent means "never recorded" — NOT dead; a run only covers what it
|
||||
* exercised. label/last_seen name the newest run, count spans all runs. */
|
||||
export interface ObservedCall {
|
||||
count: number;
|
||||
label: string;
|
||||
last_seen: string;
|
||||
}
|
||||
|
||||
export interface FlowStep {
|
||||
id: number;
|
||||
name: string;
|
||||
@@ -147,6 +156,7 @@ export interface FlowStep {
|
||||
guards?: { kind: string; cond?: string; negated?: boolean }[];
|
||||
/* Resolver confidence of the edge from the parent step (0..1). */
|
||||
confidence?: number;
|
||||
observed?: ObservedCall;
|
||||
}
|
||||
|
||||
export interface FlowDetail {
|
||||
@@ -283,6 +293,7 @@ export interface TracePayload {
|
||||
name: string;
|
||||
file_path?: string;
|
||||
guards?: { kind: string; cond?: string; negated?: boolean }[];
|
||||
observed?: ObservedCall;
|
||||
}[];
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { allHopsObserved, newestObserved, observedTitle } from "./observed";
|
||||
|
||||
const obs = (label: string, last_seen: string, count = 1) => ({
|
||||
count,
|
||||
label,
|
||||
last_seen,
|
||||
});
|
||||
|
||||
describe("allHopsObserved", () => {
|
||||
it("is false with no hops to judge — empty path or origin alone", () => {
|
||||
expect(allHopsObserved([])).toBe(false);
|
||||
expect(allHopsObserved([{}])).toBe(false);
|
||||
expect(allHopsObserved([{ observed: obs("pytest", "2026-08-27") }])).toBe(false);
|
||||
});
|
||||
|
||||
it("is true when every non-root hop is observed — the origin never counts", () => {
|
||||
expect(
|
||||
allHopsObserved([
|
||||
{},
|
||||
{ observed: obs("pytest 2026-08-27", "2026-08-27T10:00:00Z") },
|
||||
{ observed: obs("pytest 2026-08-27", "2026-08-27T10:00:00Z") },
|
||||
]),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("is false as soon as one non-root hop was never recorded", () => {
|
||||
expect(
|
||||
allHopsObserved([
|
||||
{},
|
||||
{ observed: obs("pytest 2026-08-27", "2026-08-27T10:00:00Z") },
|
||||
{},
|
||||
]),
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("newestObserved", () => {
|
||||
it("is null when nothing was observed", () => {
|
||||
expect(newestObserved([])).toBeNull();
|
||||
expect(newestObserved([{}, {}])).toBeNull();
|
||||
});
|
||||
|
||||
it("names the most recently seen run across steps", () => {
|
||||
const newest = newestObserved([
|
||||
{ observed: obs("pytest 2026-08-25", "2026-08-25T09:00:00Z") },
|
||||
{},
|
||||
{ observed: obs("pytest 2026-08-27", "2026-08-27T14:12:00Z") },
|
||||
{ observed: obs("pytest 2026-08-26", "2026-08-26T09:00:00Z") },
|
||||
]);
|
||||
expect(newest?.label).toBe("pytest 2026-08-27");
|
||||
});
|
||||
});
|
||||
|
||||
describe("observedTitle", () => {
|
||||
it("carries the run label and the ISO date of the newest sighting", () => {
|
||||
expect(observedTitle(obs("pytest 2026-08-27", "2026-08-27T14:12:00Z", 4))).toBe(
|
||||
"ran in pytest 2026-08-27 · last 2026-08-27",
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,31 @@
|
||||
/* Observed executions — runtime evidence on trace hops and flow steps from
|
||||
* ingested traces. Presence means the exact caller→callee pair fired in a
|
||||
* recorded run; absence means "never recorded", which must never be read
|
||||
* as dead or unused — a run only covers what it exercised. */
|
||||
import type { ObservedCall } from "./atlas";
|
||||
|
||||
/* True when every hop beyond the first is observed (the first entry is the
|
||||
* origin, not a call). False when there is no hop to judge. */
|
||||
export function allHopsObserved(steps: { observed?: ObservedCall }[]): boolean {
|
||||
if (steps.length < 2) return false;
|
||||
return steps.slice(1).every((step) => step.observed !== undefined);
|
||||
}
|
||||
|
||||
/* The most recently seen observation across steps — names the freshness
|
||||
* footer. Null when nothing was observed. */
|
||||
export function newestObserved(
|
||||
steps: { observed?: ObservedCall }[],
|
||||
): ObservedCall | null {
|
||||
let newest: ObservedCall | null = null;
|
||||
for (const step of steps) {
|
||||
if (step.observed && (!newest || step.observed.last_seen > newest.last_seen)) {
|
||||
newest = step.observed;
|
||||
}
|
||||
}
|
||||
return newest;
|
||||
}
|
||||
|
||||
/* Title text for the marker chip: "ran in pytest 2026-08-27 · last 2026-08-27". */
|
||||
export function observedTitle(observed: ObservedCall): string {
|
||||
return `ran in ${observed.label} · last ${observed.last_seen.slice(0, 10)}`;
|
||||
}
|
||||
@@ -59,7 +59,8 @@ export const QUESTION_FAMILIES: QuestionFamily[] = [
|
||||
question: "What code caused this behavior?",
|
||||
status: "partial",
|
||||
tab: "flows",
|
||||
gap: "Flows are static journeys; observed executions (traces) are not surfaced in the UI yet",
|
||||
hint: "Flows tab — ▶ observed markers on trace hops and flow steps show what actually ran",
|
||||
gap: "Flows and traces now mark observed vs possible hops from ingested runtime traces; automatic trace capture and failure reproduction are not built",
|
||||
},
|
||||
{
|
||||
id: "F6",
|
||||
|
||||
@@ -173,6 +173,25 @@ export const WIKI_ENTRIES: WikiEntry[] = [
|
||||
"Reflection, code generation, and runtime dispatch are not modelled. Coverage is best-effort.",
|
||||
appearsIn: ["Flows", "Trace", "Symbol connections"],
|
||||
},
|
||||
{
|
||||
id: "M-OBSERVED",
|
||||
slug: "observed",
|
||||
term: "observed",
|
||||
tier: "first-class",
|
||||
sentence:
|
||||
"A call that actually fired in a recorded run — observed, not merely possible from static analysis.",
|
||||
why: "Static edges say what could happen; observation says what did. An observed marker on a speculative edge is runtime evidence the resolver got it right.",
|
||||
computedParts: [
|
||||
"ingest_traces call paths, stored by qualified name per run label",
|
||||
],
|
||||
caps: [
|
||||
"capped: 200k observed pairs per project — whole runs age out together, oldest first",
|
||||
],
|
||||
notCovered:
|
||||
"Absence of observation never means dead or unused — a run covers only what it exercised. Counts survive reindexing (name-keyed), but renamed symbols start fresh.",
|
||||
appearsIn: ["Flows", "Trace"],
|
||||
seeAlso: ["confidence", "dead-candidate"],
|
||||
},
|
||||
{
|
||||
id: "M-DEAD",
|
||||
slug: "dead-candidate",
|
||||
|
||||
@@ -1 +1 @@
|
||||
{"root":["./src/app.tsx","./src/main.tsx","./src/vite-env.d.ts","./src/api/rpc.ts","./src/components/changestab.tsx","./src/components/commandk.tsx","./src/components/controltab.tsx","./src/components/dashboardtab.tsx","./src/components/displaysettingsmenu.tsx","./src/components/edgelines.tsx","./src/components/errorboundary.tsx","./src/components/filterpanel.tsx","./src/components/flowstab.tsx","./src/components/graphloader.tsx","./src/components/graphscene.test.ts","./src/components/graphscene.tsx","./src/components/graphtab.deadcode.test.tsx","./src/components/graphtab.filters.test.tsx","./src/components/graphtab.regions.test.tsx","./src/components/graphtab.test.ts","./src/components/graphtab.tsx","./src/components/halolayer.tsx","./src/components/metricchip.tsx","./src/components/minimap.tsx","./src/components/missedcallout.tsx","./src/components/modulestab.tsx","./src/components/nodecloud.tsx","./src/components/nodedetailpanel.showmore.test.tsx","./src/components/nodedetailpanel.test.tsx","./src/components/nodedetailpanel.tsx","./src/components/nodelabels.tsx","./src/components/nodetooltip.tsx","./src/components/overviewtab.tsx","./src/components/projectcard.tsx","./src/components/projectswitcher.tsx","./src/components/promptbasket.tsx","./src/components/regionpanel.tsx","./src/components/regiontooltip.tsx","./src/components/resizehandle.tsx","./src/components/sidebar.tsx","./src/components/statstab.test.tsx","./src/components/statstab.tsx","./src/components/symboltab.tsx","./src/components/tabbar.tsx","./src/components/triggertree.tsx","./src/components/wikihovercard.test.tsx","./src/components/wikihovercard.tsx","./src/components/wikipanel.test.tsx","./src/components/wikipanel.tsx","./src/components/wikitier.ts","./src/components/ui/badge.tsx","./src/components/ui/button.tsx","./src/components/ui/card.tsx","./src/components/ui/checkbox.tsx","./src/components/ui/input.tsx","./src/components/ui/scroll-area.tsx","./src/components/ui/separator.tsx","./src/hooks/usegraphdata.test.ts","./src/hooks/usegraphdata.ts","./src/hooks/useprojects.ts","./src/lib/atlas.test.ts","./src/lib/atlas.ts","./src/lib/colors.ts","./src/lib/complexity.test.ts","./src/lib/complexity.ts","./src/lib/composer.ts","./src/lib/density.test.ts","./src/lib/density.ts","./src/lib/findings.test.ts","./src/lib/findings.ts","./src/lib/firstread.ts","./src/lib/flowgroup.test.ts","./src/lib/flowgroup.ts","./src/lib/i18n.test.ts","./src/lib/i18n.ts","./src/lib/impact.test.ts","./src/lib/impact.ts","./src/lib/questions.test.ts","./src/lib/questions.ts","./src/lib/rationale.test.ts","./src/lib/rationale.ts","./src/lib/regions.test.ts","./src/lib/regions.ts","./src/lib/treemap.ts","./src/lib/types.ts","./src/lib/utils.ts","./src/lib/why.test.ts","./src/lib/why.ts","./src/lib/whyfetch.ts","./src/test/setup.ts","./src/wiki/entries.test.ts","./src/wiki/entries.ts"],"version":"5.9.3"}
|
||||
{"root":["./src/app.tsx","./src/main.tsx","./src/vite-env.d.ts","./src/api/rpc.ts","./src/components/changestab.tsx","./src/components/commandk.tsx","./src/components/controltab.tsx","./src/components/dashboardtab.tsx","./src/components/displaysettingsmenu.tsx","./src/components/edgelines.tsx","./src/components/errorboundary.tsx","./src/components/filterpanel.tsx","./src/components/flowstab.observed.test.tsx","./src/components/flowstab.tsx","./src/components/graphloader.tsx","./src/components/graphscene.test.ts","./src/components/graphscene.tsx","./src/components/graphtab.deadcode.test.tsx","./src/components/graphtab.filters.test.tsx","./src/components/graphtab.regions.test.tsx","./src/components/graphtab.test.ts","./src/components/graphtab.tsx","./src/components/halolayer.tsx","./src/components/metricchip.tsx","./src/components/minimap.tsx","./src/components/missedcallout.tsx","./src/components/modulestab.tsx","./src/components/nodecloud.tsx","./src/components/nodedetailpanel.showmore.test.tsx","./src/components/nodedetailpanel.test.tsx","./src/components/nodedetailpanel.tsx","./src/components/nodelabels.tsx","./src/components/nodetooltip.tsx","./src/components/overviewtab.tsx","./src/components/projectcard.tsx","./src/components/projectswitcher.tsx","./src/components/promptbasket.tsx","./src/components/regionpanel.tsx","./src/components/regiontooltip.tsx","./src/components/resizehandle.tsx","./src/components/sidebar.tsx","./src/components/statstab.test.tsx","./src/components/statstab.tsx","./src/components/symboltab.tsx","./src/components/tabbar.tsx","./src/components/triggertree.tsx","./src/components/wikihovercard.test.tsx","./src/components/wikihovercard.tsx","./src/components/wikipanel.test.tsx","./src/components/wikipanel.tsx","./src/components/wikitier.ts","./src/components/ui/badge.tsx","./src/components/ui/button.tsx","./src/components/ui/card.tsx","./src/components/ui/checkbox.tsx","./src/components/ui/input.tsx","./src/components/ui/scroll-area.tsx","./src/components/ui/separator.tsx","./src/hooks/usegraphdata.test.ts","./src/hooks/usegraphdata.ts","./src/hooks/useprojects.ts","./src/lib/atlas.test.ts","./src/lib/atlas.ts","./src/lib/colors.ts","./src/lib/complexity.test.ts","./src/lib/complexity.ts","./src/lib/composer.ts","./src/lib/density.test.ts","./src/lib/density.ts","./src/lib/findings.test.ts","./src/lib/findings.ts","./src/lib/firstread.ts","./src/lib/flowgroup.test.ts","./src/lib/flowgroup.ts","./src/lib/i18n.test.ts","./src/lib/i18n.ts","./src/lib/impact.test.ts","./src/lib/impact.ts","./src/lib/observed.test.ts","./src/lib/observed.ts","./src/lib/questions.test.ts","./src/lib/questions.ts","./src/lib/rationale.test.ts","./src/lib/rationale.ts","./src/lib/regions.test.ts","./src/lib/regions.ts","./src/lib/treemap.ts","./src/lib/types.ts","./src/lib/utils.ts","./src/lib/why.test.ts","./src/lib/why.ts","./src/lib/whyfetch.ts","./src/test/setup.ts","./src/wiki/entries.test.ts","./src/wiki/entries.ts"],"version":"5.9.3"}
|
||||
+18
-3
@@ -11147,7 +11147,7 @@ static char *adr_read_legacy_file(const char *root_path) {
|
||||
/* resolve_store opens file-backed projects query-only. A mutation must release
|
||||
* that reader before opening a dedicated writer because atomic publication uses
|
||||
* a self-contained DELETE-mode database that switches back to WAL on write. */
|
||||
static cbm_store_t *open_adr_store_for_write(cbm_mcp_server_t *srv, cbm_store_t *resolved,
|
||||
static cbm_store_t *open_store_for_write(cbm_mcp_server_t *srv, cbm_store_t *resolved,
|
||||
cbm_store_t **owned_rw) {
|
||||
if (!srv || !resolved || !owned_rw) {
|
||||
return NULL;
|
||||
@@ -11248,7 +11248,7 @@ static char *handle_manage_adr(cbm_mcp_server_t *srv, const char *args) {
|
||||
cbm_store_t *store = resolved;
|
||||
cbm_store_t *owned_rw = NULL;
|
||||
if (write_request) {
|
||||
store = open_adr_store_for_write(srv, resolved, &owned_rw);
|
||||
store = open_store_for_write(srv, resolved, &owned_rw);
|
||||
if (!store) {
|
||||
if (mutation_held) {
|
||||
mcp_project_mutation_end(srv, project);
|
||||
@@ -11285,7 +11285,7 @@ static char *handle_manage_adr(cbm_mcp_server_t *srv, const char *args) {
|
||||
resolved = resolve_store_internal(srv, project, true, false, NULL);
|
||||
}
|
||||
if (resolved) {
|
||||
store = open_adr_store_for_write(srv, resolved, &owned_rw);
|
||||
store = open_store_for_write(srv, resolved, &owned_rw);
|
||||
if (store) {
|
||||
have_adr = (cbm_store_adr_get(store, project, &adr) == CBM_STORE_OK);
|
||||
if (!have_adr &&
|
||||
@@ -11384,12 +11384,25 @@ static char *handle_ingest_traces(cbm_mcp_server_t *srv, const char *args) {
|
||||
if (!label || !label[0])
|
||||
label = "unlabeled";
|
||||
|
||||
/* resolve_store opens file-backed projects query-only (and skips schema
|
||||
* DDL); observation writes need the dedicated read-write connection —
|
||||
* whose open also creates the observed_* tables in pre-existing DBs. */
|
||||
cbm_store_t *owned_rw = NULL;
|
||||
store = open_store_for_write(srv, store, &owned_rw);
|
||||
if (!store) {
|
||||
yyjson_doc_free(adoc);
|
||||
free(project);
|
||||
return cbm_mcp_text_result("could not open the project store for writing", true);
|
||||
}
|
||||
|
||||
struct sqlite3 *db = cbm_store_get_db(store);
|
||||
sqlite3_stmt *exists = NULL;
|
||||
if (!db || sqlite3_prepare_v2(db,
|
||||
"SELECT 1 FROM nodes WHERE project=?1 AND qualified_name=?2 "
|
||||
"LIMIT 1",
|
||||
-1, &exists, NULL) != SQLITE_OK) {
|
||||
if (owned_rw)
|
||||
cbm_store_close(owned_rw);
|
||||
yyjson_doc_free(adoc);
|
||||
free(project);
|
||||
return cbm_mcp_text_result("store is not queryable", true);
|
||||
@@ -11518,6 +11531,8 @@ static char *handle_ingest_traces(cbm_mcp_server_t *srv, const char *args) {
|
||||
char *json = yy_doc_to_str(doc);
|
||||
yyjson_mut_doc_free(doc);
|
||||
yyjson_doc_free(adoc);
|
||||
if (owned_rw)
|
||||
cbm_store_close(owned_rw);
|
||||
free(project);
|
||||
|
||||
char *result = cbm_mcp_text_result(json, false);
|
||||
|
||||
Reference in New Issue
Block a user