@tangle-network/traces 0.23.1 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +61 -1078
- package/dist/analyst-model-call-JVUOYZ5V.js +20 -0
- package/dist/ask-2KF4KSPB.js +39 -0
- package/dist/check-YF6ZHPGG.js +322 -0
- package/dist/chunk-2XXABIRL.js +25 -0
- package/dist/chunk-4VKPN432.js +138 -0
- package/dist/chunk-6GOG5KIK.js +53 -0
- package/dist/chunk-7USMM4MA.js +5695 -0
- package/dist/chunk-CG5QHQO4.js +492 -0
- package/dist/chunk-CQ6UYZG6.js +1217 -0
- package/dist/chunk-DU534HAX.js +263 -0
- package/dist/chunk-FC4V25CG.js +503 -0
- package/dist/chunk-GLUDGUGJ.js +232 -0
- package/dist/chunk-GNRXKQEQ.js +120 -0
- package/dist/chunk-K2KH2HHD.js +228 -0
- package/dist/chunk-M3W6TZHQ.js +381 -0
- package/dist/chunk-O52XK7OB.js +19 -0
- package/dist/chunk-PYH3PL3N.js +62 -0
- package/dist/chunk-QLVP37YB.js +782 -0
- package/dist/chunk-QUHUMZ4W.js +759 -0
- package/dist/chunk-REO2JQJY.js +535 -0
- package/dist/chunk-RZL5GAWH.js +60 -0
- package/dist/chunk-TOCNGML5.js +273 -0
- package/dist/chunk-UQSAGXPH.js +208 -0
- package/dist/chunk-VFOLC4OX.js +407 -0
- package/dist/chunk-WF7PTDCZ.js +3921 -0
- package/dist/chunk-YAX72TST.js +259 -0
- package/dist/cli.js +2086 -2050
- package/dist/diff-J4RAIOXO.js +16 -0
- package/dist/improvement-LCCVGGUS.js +25 -0
- package/dist/index.d.ts +1235 -4357
- package/dist/index.js +53 -1204
- package/dist/live-XXL7VZWB.js +14 -0
- package/dist/mcp-EYIDKBEK.js +85 -0
- package/dist/report-E3G2T5TJ.js +338 -0
- package/dist/run-watch-RAOOAXOF.js +649 -0
- package/dist/supervisor-report-RMPLLBGZ.js +29 -0
- package/dist/supervisor-run-context-WO6VOTGV.js +30 -0
- package/dist/upload-3R7GRSKS.js +20 -0
- package/dist/verify-safe-C5ZXZOZ2.js +8 -0
- package/docs/analyze.md +76 -0
- package/docs/ask.md +88 -0
- package/docs/bundle.md +40 -0
- package/docs/check.md +77 -0
- package/docs/convert.md +44 -0
- package/docs/develop.md +32 -0
- package/docs/diff.md +38 -0
- package/docs/facts.md +114 -0
- package/docs/mcp.md +36 -0
- package/docs/migrate-1.0.md +33 -0
- package/docs/parsers.md +66 -0
- package/docs/release-1.0.0.md +147 -0
- package/docs/release-1.0.1.md +4 -0
- package/docs/sdk.md +77 -0
- package/docs/select.md +70 -0
- package/docs/skills.md +46 -0
- package/docs/upload.md +47 -0
- package/docs/watch-jsonl.v1.example.jsonl +5 -0
- package/docs/watch.md +66 -0
- package/examples/README.md +9 -0
- package/examples/custom-analyst.ts +65 -74
- package/install.sh +22 -2
- package/package.json +10 -9
- package/skills/inspect-agent-traces/SKILL.md +47 -110
- package/dist/chunk-7DVZCCZI.js +0 -20983
- package/docs/replay-verify.md +0 -123
- package/docs/stream-findings.svg +0 -39
- package/docs/trace-analysts.md +0 -643
- package/examples/custom-backend.ts +0 -21
- package/examples/external-engines.ts +0 -62
- package/examples/improvement-config.mjs +0 -63
- package/examples/observe-and-alert.ts +0 -19
- package/examples/register-harness.ts +0 -26
- package/skills/build-trace-analyst/SKILL.md +0 -76
- package/skills/build-trace-analyst/agents/openai.yaml +0 -4
|
@@ -0,0 +1,407 @@
|
|
|
1
|
+
import {
|
|
2
|
+
detectEvidenceFormat,
|
|
3
|
+
exportTraceEvidenceFile
|
|
4
|
+
} from "./chunk-CG5QHQO4.js";
|
|
5
|
+
import {
|
|
6
|
+
isCodexExecRecord,
|
|
7
|
+
listAdapters,
|
|
8
|
+
parseSession,
|
|
9
|
+
readsStoreRoots,
|
|
10
|
+
resolveAdapter
|
|
11
|
+
} from "./chunk-7USMM4MA.js";
|
|
12
|
+
import {
|
|
13
|
+
isOtlpJsonFile,
|
|
14
|
+
isStandardOtlpRow,
|
|
15
|
+
readJsonl,
|
|
16
|
+
readOtlpInput
|
|
17
|
+
} from "./chunk-CQ6UYZG6.js";
|
|
18
|
+
|
|
19
|
+
// src/refs.ts
|
|
20
|
+
import { readFile, stat } from "fs/promises";
|
|
21
|
+
import { homedir } from "os";
|
|
22
|
+
import { join, resolve, sep } from "path";
|
|
23
|
+
import { CODEX_HOME_RULE, STORES, codexHomes, isCodexHome } from "@tangle-network/harness-sessions";
|
|
24
|
+
import { ATTR } from "@tangle-network/agent-trace-contract";
|
|
25
|
+
var MIN_REF_CHARS = 8;
|
|
26
|
+
var RefError = class extends Error {
|
|
27
|
+
code;
|
|
28
|
+
candidates;
|
|
29
|
+
constructor(message, code, candidates = []) {
|
|
30
|
+
super(message);
|
|
31
|
+
this.name = "RefError";
|
|
32
|
+
this.code = code;
|
|
33
|
+
this.candidates = candidates;
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
function isObject(value) {
|
|
37
|
+
return value != null && typeof value === "object" && !Array.isArray(value);
|
|
38
|
+
}
|
|
39
|
+
function errorText(error) {
|
|
40
|
+
return error instanceof Error ? error.message : String(error);
|
|
41
|
+
}
|
|
42
|
+
async function statOf(path) {
|
|
43
|
+
try {
|
|
44
|
+
return await stat(path);
|
|
45
|
+
} catch (error) {
|
|
46
|
+
const code = error.code;
|
|
47
|
+
if (code === "ENOENT" || code === "ENOTDIR") return null;
|
|
48
|
+
throw error;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
async function isDirectory(path) {
|
|
52
|
+
return (await statOf(path))?.isDirectory() ?? false;
|
|
53
|
+
}
|
|
54
|
+
async function isFile(path) {
|
|
55
|
+
return (await statOf(path))?.isFile() ?? false;
|
|
56
|
+
}
|
|
57
|
+
function ownHome() {
|
|
58
|
+
return process.env.HOME || homedir();
|
|
59
|
+
}
|
|
60
|
+
var HEAD_ROWS = 20;
|
|
61
|
+
var MAX_WHOLE_JSON_BYTES = 256 * 1024 * 1024;
|
|
62
|
+
var CODEX_ROLLOUT_TYPES = /* @__PURE__ */ new Set(["session_meta", "response_item", "event_msg", "turn_context"]);
|
|
63
|
+
var STORE_PATHS = [
|
|
64
|
+
{ harness: "claude-code", root: STORES["claude-code.projects-jsonl"].root },
|
|
65
|
+
{ harness: "pi", root: STORES["pi.session-jsonl"].root },
|
|
66
|
+
{ harness: "kimi", root: STORES["kimi.session-dir"].root },
|
|
67
|
+
{ harness: "factory", root: STORES["factory.session-jsonl"].root }
|
|
68
|
+
];
|
|
69
|
+
function harnessOfStorePath(path) {
|
|
70
|
+
const absolute = resolve(path);
|
|
71
|
+
for (const { harness, root } of STORE_PATHS) {
|
|
72
|
+
if (absolute.includes(`${sep}${root.split("/").join(sep)}${sep}`)) return harness;
|
|
73
|
+
}
|
|
74
|
+
const segments = absolute.split(sep);
|
|
75
|
+
for (let i = 1; i < segments.length - 1; i += 1) {
|
|
76
|
+
if (!CODEX_HOME_RULE.sessionDirs.includes(segments[i + 1])) continue;
|
|
77
|
+
if (isCodexHome(segments.slice(0, i + 1).join(sep))) return "codex";
|
|
78
|
+
}
|
|
79
|
+
return void 0;
|
|
80
|
+
}
|
|
81
|
+
async function headRows(path) {
|
|
82
|
+
const rows = [];
|
|
83
|
+
let unparsed = 0;
|
|
84
|
+
for await (const row of readJsonl(path, { mode: "recover", onCorruption: () => {
|
|
85
|
+
unparsed += 1;
|
|
86
|
+
} })) {
|
|
87
|
+
rows.push(row);
|
|
88
|
+
if (rows.length >= HEAD_ROWS) return rows;
|
|
89
|
+
if (unparsed > HEAD_ROWS && rows.length === 0) break;
|
|
90
|
+
}
|
|
91
|
+
if (rows.length > 0 && unparsed === 0) {
|
|
92
|
+
const only = rows.length === 1 ? rows[0] : void 0;
|
|
93
|
+
if (Array.isArray(only)) return only;
|
|
94
|
+
if (isObject(only) && Array.isArray(only.events)) return only.events;
|
|
95
|
+
return rows;
|
|
96
|
+
}
|
|
97
|
+
const info = await stat(path);
|
|
98
|
+
if (info.size > MAX_WHOLE_JSON_BYTES) return rows;
|
|
99
|
+
let parsed;
|
|
100
|
+
try {
|
|
101
|
+
parsed = JSON.parse(await readFile(path, "utf8"));
|
|
102
|
+
} catch {
|
|
103
|
+
return rows;
|
|
104
|
+
}
|
|
105
|
+
if (Array.isArray(parsed)) return parsed;
|
|
106
|
+
if (isObject(parsed) && Array.isArray(parsed.events)) return parsed.events;
|
|
107
|
+
return [parsed];
|
|
108
|
+
}
|
|
109
|
+
function isClaudeRecord(row) {
|
|
110
|
+
return isObject(row) && (typeof row.sessionId === "string" || typeof row.session_id === "string") && typeof row.type === "string" && ("message" in row || "uuid" in row || "subtype" in row);
|
|
111
|
+
}
|
|
112
|
+
async function sniffTraceFile(path) {
|
|
113
|
+
const stored = harnessOfStorePath(path);
|
|
114
|
+
if (stored) return { kind: "session", harness: stored };
|
|
115
|
+
if (await isOtlpJsonFile(path)) return { kind: "spans", reader: "otlp" };
|
|
116
|
+
const rows = await headRows(path);
|
|
117
|
+
if (rows.some(isCodexExecRecord)) return { kind: "session", harness: "codex-exec" };
|
|
118
|
+
if (rows.length > 0 && rows.every((row) => isObject(row) && typeof row.trace_id === "string" && typeof row.span_id === "string")) {
|
|
119
|
+
return { kind: "spans", reader: "otlp" };
|
|
120
|
+
}
|
|
121
|
+
if (rows.length > 0 && rows.every(isStandardOtlpRow)) return { kind: "spans", reader: "otlp" };
|
|
122
|
+
if (!rows.some((row) => isObject(row) && typeof row.span_id === "string")) {
|
|
123
|
+
if (rows.some(isClaudeRecord)) return { kind: "session", harness: "claude-code" };
|
|
124
|
+
if (rows.some((row) => isObject(row) && CODEX_ROLLOUT_TYPES.has(String(row.type)) && isObject(row.payload))) {
|
|
125
|
+
return { kind: "session", harness: "codex" };
|
|
126
|
+
}
|
|
127
|
+
const first = rows[0];
|
|
128
|
+
if (isObject(first) && first.type === "session" && typeof first.id === "string") return { kind: "session", harness: "pi" };
|
|
129
|
+
if (rows.some((row) => isObject(row) && row.type === "session_start")) return { kind: "session", harness: "factory" };
|
|
130
|
+
}
|
|
131
|
+
const evidence = detectEvidenceFormat(rows);
|
|
132
|
+
if (evidence === "sandbox-events") {
|
|
133
|
+
for await (const row of readJsonl(path, { mode: "recover", onCorruption: () => {
|
|
134
|
+
} })) {
|
|
135
|
+
if (isCodexExecRecord(row)) return { kind: "session", harness: "codex-exec" };
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
if (evidence) return { kind: "spans", reader: evidence };
|
|
139
|
+
return { kind: "spans", reader: "otlp" };
|
|
140
|
+
}
|
|
141
|
+
function adapterFor(harness) {
|
|
142
|
+
const adapter = resolveAdapter(harness);
|
|
143
|
+
if (!adapter) {
|
|
144
|
+
throw new RefError(`unknown harness "${harness}"; known: ${listAdapters().map((a) => a.harness).join(", ")}`, "usage");
|
|
145
|
+
}
|
|
146
|
+
return adapter;
|
|
147
|
+
}
|
|
148
|
+
async function fileRef(path, ref, selector, options) {
|
|
149
|
+
if (options.format) return { kind: "spans", ref, path, reader: options.format, ...selector ? { selector } : {} };
|
|
150
|
+
const sniffed = options.harness ? { kind: "session", harness: options.harness } : await sniffTraceFile(path);
|
|
151
|
+
if (sniffed.kind === "spans") return { kind: "spans", ref, path, reader: sniffed.reader, ...selector ? { selector } : {} };
|
|
152
|
+
if (selector) throw new RefError(`${ref}: a #selector picks a run from a span file; ${path} is a ${sniffed.harness} session`, "usage");
|
|
153
|
+
const adapter = adapterFor(sniffed.harness);
|
|
154
|
+
const info = await stat(path);
|
|
155
|
+
return {
|
|
156
|
+
kind: "session",
|
|
157
|
+
ref,
|
|
158
|
+
adapter,
|
|
159
|
+
session: { harness: adapter.harness, sessionId: ref, path, cwd: null, mtimeMs: info.mtimeMs }
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
var HOME_STORES = [
|
|
163
|
+
STORES["claude-code.projects-jsonl"].root,
|
|
164
|
+
STORES["opencode.sqlite"].root,
|
|
165
|
+
STORES["pi.session-jsonl"].root,
|
|
166
|
+
STORES["kimi.session-dir"].root,
|
|
167
|
+
STORES["factory.session-jsonl"].root
|
|
168
|
+
];
|
|
169
|
+
async function isStoreRoot(dir) {
|
|
170
|
+
for (const root of HOME_STORES) if (await isDirectory(join(dir, root))) return true;
|
|
171
|
+
for (const home of await codexHomes(dir)) {
|
|
172
|
+
for (const sessions of CODEX_HOME_RULE.sessionDirs) if (await isDirectory(join(home, sessions))) return true;
|
|
173
|
+
}
|
|
174
|
+
return false;
|
|
175
|
+
}
|
|
176
|
+
var BUNDLE_SPAN_FILES = ["spans.otlp.jsonl", join("derived", "trace.otlp.jsonl")];
|
|
177
|
+
async function directoryRef(path, ref) {
|
|
178
|
+
if (await isFile(join(path, "manifest.json"))) {
|
|
179
|
+
for (const file of BUNDLE_SPAN_FILES) {
|
|
180
|
+
if (await isFile(join(path, file))) return { kind: "spans", ref, path: join(path, file), reader: "otlp" };
|
|
181
|
+
}
|
|
182
|
+
throw new RefError(`${ref} has a manifest.json but no spans.otlp.jsonl; it is not a traces bundle`, "unreadable");
|
|
183
|
+
}
|
|
184
|
+
const { findSupervisorRunDirs, isRuntimeSupervisorRunDir } = await import("@tangle-network/agent-eval/supervisor-run");
|
|
185
|
+
if (await isRuntimeSupervisorRunDir(path)) return { kind: "run-dir", ref, path };
|
|
186
|
+
if (await isStoreRoot(path)) return { kind: "store-root", ref, path: resolve(path) };
|
|
187
|
+
if ((await findSupervisorRunDirs(path)).length > 0) return { kind: "run-dir", ref, path };
|
|
188
|
+
return { kind: "spans", ref, path, reader: "otlp" };
|
|
189
|
+
}
|
|
190
|
+
function looksLikePath(ref) {
|
|
191
|
+
return ref.includes("/") || ref.startsWith(".") || ref.startsWith("~") || /\.(?:jsonl|ndjson|json|db)$/iu.test(ref);
|
|
192
|
+
}
|
|
193
|
+
function candidate({ adapter, session }) {
|
|
194
|
+
return { harness: adapter.harness, sessionId: session.sessionId, path: session.path };
|
|
195
|
+
}
|
|
196
|
+
var COMPLETE_ID_CHARS = 20;
|
|
197
|
+
async function lookupSession(query, options, storeRoot) {
|
|
198
|
+
const short = query.length < MIN_REF_CHARS;
|
|
199
|
+
const home = storeRoot ?? ownHome();
|
|
200
|
+
const selected = options.harness ? [adapterFor(options.harness)] : listAdapters();
|
|
201
|
+
const adapters = storeRoot === void 0 ? selected : selected.filter(readsStoreRoots);
|
|
202
|
+
const failures = /* @__PURE__ */ new Map();
|
|
203
|
+
const listed = /* @__PURE__ */ new Map();
|
|
204
|
+
const sessionsOf = (adapter, exact) => {
|
|
205
|
+
if (readsStoreRoots(adapter) && exact) return adapter.locateIn(home, { nativeSessionId: query });
|
|
206
|
+
let all = listed.get(adapter);
|
|
207
|
+
if (!all) {
|
|
208
|
+
all = readsStoreRoots(adapter) ? adapter.locateIn(home) : adapter.locate();
|
|
209
|
+
listed.set(adapter, all);
|
|
210
|
+
}
|
|
211
|
+
return all;
|
|
212
|
+
};
|
|
213
|
+
const search = async (exact) => {
|
|
214
|
+
const found2 = /* @__PURE__ */ new Map();
|
|
215
|
+
for (const adapter of adapters) {
|
|
216
|
+
let sessions;
|
|
217
|
+
try {
|
|
218
|
+
sessions = await sessionsOf(adapter, exact);
|
|
219
|
+
} catch (error) {
|
|
220
|
+
failures.set(adapter.harness, errorText(error));
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
for (const session of sessions) {
|
|
224
|
+
const id = session.sessionId;
|
|
225
|
+
if (id === query || !exact && (id.startsWith(query) || id.endsWith(query))) {
|
|
226
|
+
found2.set(`${adapter.harness}\0${session.path}\0${id}`, { adapter, session });
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
return [...found2.values()].sort((a, b) => b.session.mtimeMs - a.session.mtimeMs);
|
|
231
|
+
};
|
|
232
|
+
let found = query.length >= COMPLETE_ID_CHARS || short ? await search(true) : [];
|
|
233
|
+
if (found.length === 0 && short) {
|
|
234
|
+
throw new RefError(
|
|
235
|
+
`"${query}" is not a file, a directory or a session id, and a session id prefix or suffix needs at least ${MIN_REF_CHARS} characters`,
|
|
236
|
+
"usage"
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
if (found.length === 0) {
|
|
240
|
+
const matched = await search(false);
|
|
241
|
+
const exact = matched.filter(({ session }) => session.sessionId === query);
|
|
242
|
+
found = exact.length > 0 ? exact : matched;
|
|
243
|
+
}
|
|
244
|
+
if (found.length === 1) return { kind: "session", ref: query, adapter: found[0].adapter, session: found[0].session };
|
|
245
|
+
const where = storeRoot === void 0 ? "" : ` under ${storeRoot}`;
|
|
246
|
+
if (found.length === 0) {
|
|
247
|
+
const unread = failures.size > 0 ? `; could not read ${[...failures].map(([harness, reason]) => `${harness}: ${reason}`).join("; ")}` : "";
|
|
248
|
+
throw new RefError(
|
|
249
|
+
`no session id is, starts with or ends with "${query}"${where} (${adapters.map((a) => a.harness).join(", ")})${unread}; \`traces list\` shows ids`,
|
|
250
|
+
"unreadable"
|
|
251
|
+
);
|
|
252
|
+
}
|
|
253
|
+
const shown = found.slice(0, 5).map((item) => `${item.adapter.harness} ${item.session.sessionId}`);
|
|
254
|
+
const more = found.length > shown.length ? ` and ${found.length - shown.length} more` : "";
|
|
255
|
+
throw new RefError(
|
|
256
|
+
`"${query}" matches ${found.length} sessions${where} (${shown.join(", ")}${more}); pass more of the id`,
|
|
257
|
+
"ambiguous",
|
|
258
|
+
found.map(candidate)
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
function shortestRefs(ids) {
|
|
262
|
+
const unique = [...new Set(ids)];
|
|
263
|
+
const reverse = (text) => [...text].reverse().join("");
|
|
264
|
+
const forward = [...unique].sort();
|
|
265
|
+
const backward = unique.map(reverse).sort();
|
|
266
|
+
const starting = (sorted, prefix) => {
|
|
267
|
+
let low = 0;
|
|
268
|
+
let high = sorted.length;
|
|
269
|
+
while (low < high) {
|
|
270
|
+
const mid = low + high >> 1;
|
|
271
|
+
if (sorted[mid] < prefix) low = mid + 1;
|
|
272
|
+
else high = mid;
|
|
273
|
+
}
|
|
274
|
+
const found = [];
|
|
275
|
+
for (let i = low; i < sorted.length && found.length < 2 && sorted[i].startsWith(prefix); i += 1) found.push(sorted[i]);
|
|
276
|
+
return found;
|
|
277
|
+
};
|
|
278
|
+
const alone = (id, form) => {
|
|
279
|
+
const matches = /* @__PURE__ */ new Set([...starting(forward, form), ...starting(backward, reverse(form)).map(reverse)]);
|
|
280
|
+
return matches.size === 1 && matches.has(id);
|
|
281
|
+
};
|
|
282
|
+
const shortest = /* @__PURE__ */ new Map();
|
|
283
|
+
for (const id of unique) {
|
|
284
|
+
let best = id;
|
|
285
|
+
for (let length = MIN_REF_CHARS; length < id.length; length += 1) {
|
|
286
|
+
const prefix = id.slice(0, length);
|
|
287
|
+
const suffix = id.slice(-length);
|
|
288
|
+
if (alone(id, prefix)) {
|
|
289
|
+
best = prefix;
|
|
290
|
+
break;
|
|
291
|
+
}
|
|
292
|
+
if (alone(id, suffix)) {
|
|
293
|
+
best = suffix;
|
|
294
|
+
break;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
shortest.set(id, best);
|
|
298
|
+
}
|
|
299
|
+
return shortest;
|
|
300
|
+
}
|
|
301
|
+
async function splitSelector(ref) {
|
|
302
|
+
const hash = ref.lastIndexOf("#");
|
|
303
|
+
if (hash <= 0 || hash === ref.length - 1 || await statOf(ref) !== null) return { path: ref };
|
|
304
|
+
const path = ref.slice(0, hash);
|
|
305
|
+
return await statOf(path) === null ? { path: ref } : { path, selector: ref.slice(hash + 1) };
|
|
306
|
+
}
|
|
307
|
+
async function pathRef(ref, options) {
|
|
308
|
+
const { path, selector } = await splitSelector(ref);
|
|
309
|
+
const info = await statOf(path);
|
|
310
|
+
if (info === null) {
|
|
311
|
+
if (looksLikePath(ref)) throw new RefError(`no such file or directory: ${ref}`, "unreadable");
|
|
312
|
+
return void 0;
|
|
313
|
+
}
|
|
314
|
+
if (info.isDirectory()) {
|
|
315
|
+
if (selector) throw new RefError(`${ref}: a #selector picks a run from a span file, not a directory`, "usage");
|
|
316
|
+
return directoryRef(path, ref);
|
|
317
|
+
}
|
|
318
|
+
if (!info.isFile()) throw new RefError(`${ref} is neither a file nor a directory`, "unreadable");
|
|
319
|
+
return fileRef(path, ref, selector, options);
|
|
320
|
+
}
|
|
321
|
+
async function resolveRefs(refs, options = {}) {
|
|
322
|
+
const byPath = await Promise.all(refs.map((ref) => pathRef(ref, options)));
|
|
323
|
+
const roots = byPath.filter((ref) => ref?.kind === "store-root");
|
|
324
|
+
if (roots.length > 1) {
|
|
325
|
+
throw new RefError(`name one store root per command, not ${roots.length}: ${roots.map((root) => root.ref).join(", ")}`, "usage");
|
|
326
|
+
}
|
|
327
|
+
const resolved = [];
|
|
328
|
+
for (const [index, ref] of refs.entries()) {
|
|
329
|
+
resolved.push(byPath[index] ?? await lookupSession(ref, options, roots[0]?.path));
|
|
330
|
+
}
|
|
331
|
+
return resolved;
|
|
332
|
+
}
|
|
333
|
+
function branchSpans(spans, branch) {
|
|
334
|
+
const byId = /* @__PURE__ */ new Map();
|
|
335
|
+
for (const span of spans) byId.set(span.span_id, span);
|
|
336
|
+
const children = /* @__PURE__ */ new Map();
|
|
337
|
+
for (const span of spans) {
|
|
338
|
+
if (!span.parent_span_id) continue;
|
|
339
|
+
const list = children.get(span.parent_span_id);
|
|
340
|
+
if (list) list.push(span);
|
|
341
|
+
else children.set(span.parent_span_id, [span]);
|
|
342
|
+
}
|
|
343
|
+
const matched = spans.filter(
|
|
344
|
+
(span) => span.attributes[ATTR.branchId] === branch || span.attributes[ATTR.branchArm] === branch
|
|
345
|
+
);
|
|
346
|
+
const matchedIds = new Set(matched.map((span) => span.span_id));
|
|
347
|
+
const hasMatchedAncestor = (span) => {
|
|
348
|
+
let current = span;
|
|
349
|
+
while (current.parent_span_id) {
|
|
350
|
+
const parent = byId.get(current.parent_span_id);
|
|
351
|
+
if (!parent) return false;
|
|
352
|
+
if (matchedIds.has(parent.span_id)) return true;
|
|
353
|
+
current = parent;
|
|
354
|
+
}
|
|
355
|
+
return false;
|
|
356
|
+
};
|
|
357
|
+
const roots = matched.filter((span) => !hasMatchedAncestor(span));
|
|
358
|
+
if (roots.length > 1) {
|
|
359
|
+
const named = roots.map((span) => `${span.span_id} (${ATTR.branchId}=${JSON.stringify(span.attributes[ATTR.branchId] ?? null)})`).join(", ");
|
|
360
|
+
throw new Error(
|
|
361
|
+
`"${branch}" matches ${roots.length} separate subtrees, not one arm: ${named} \u2014 sibling arms can share a branch id; select by span id, or narrow the input to one arm first`
|
|
362
|
+
);
|
|
363
|
+
}
|
|
364
|
+
const selected = /* @__PURE__ */ new Set();
|
|
365
|
+
const stack = [...matched];
|
|
366
|
+
while (stack.length > 0) {
|
|
367
|
+
const span = stack.pop();
|
|
368
|
+
if (selected.has(span)) continue;
|
|
369
|
+
selected.add(span);
|
|
370
|
+
stack.push(...children.get(span.span_id) ?? []);
|
|
371
|
+
}
|
|
372
|
+
return spans.filter((span) => selected.has(span));
|
|
373
|
+
}
|
|
374
|
+
function selectSpans(spans, selector, path) {
|
|
375
|
+
if (selector.startsWith("branch=")) {
|
|
376
|
+
const branch = selector.slice("branch=".length);
|
|
377
|
+
const picked2 = branchSpans(spans, branch);
|
|
378
|
+
if (picked2.length === 0) throw new RefError(`${path} holds no span with ${ATTR.branchId} or ${ATTR.branchArm} "${branch}"`, "unreadable");
|
|
379
|
+
return picked2;
|
|
380
|
+
}
|
|
381
|
+
const picked = spans.filter((span) => span.trace_id === selector);
|
|
382
|
+
if (picked.length === 0) throw new RefError(`${path} holds no run with trace id ${selector}`, "unreadable");
|
|
383
|
+
return picked;
|
|
384
|
+
}
|
|
385
|
+
async function readRefSpans(ref, options = {}) {
|
|
386
|
+
const resolved = (await resolveRefs([ref], options))[0];
|
|
387
|
+
switch (resolved.kind) {
|
|
388
|
+
case "session":
|
|
389
|
+
return parseSession(resolved.adapter, resolved.session);
|
|
390
|
+
case "run-dir":
|
|
391
|
+
return [...(await readOtlpInput(resolved.path)).spans];
|
|
392
|
+
case "store-root":
|
|
393
|
+
throw new RefError(`${ref} is a store root, which holds many sessions; name one session in it`, "usage");
|
|
394
|
+
case "spans": {
|
|
395
|
+
const spans = resolved.reader === "otlp" ? [...(await readOtlpInput(resolved.path)).spans] : (await exportTraceEvidenceFile(resolved.path, { format: resolved.reader })).spans;
|
|
396
|
+
return resolved.selector ? selectSpans(spans, resolved.selector, resolved.path) : spans;
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
export {
|
|
402
|
+
RefError,
|
|
403
|
+
shortestRefs,
|
|
404
|
+
resolveRefs,
|
|
405
|
+
selectSpans,
|
|
406
|
+
readRefSpans
|
|
407
|
+
};
|