@tangle-network/traces 0.15.5 → 0.16.0
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 +50 -1
- package/dist/{chunk-RERVIGLH.js → chunk-G5KKIQZ6.js} +1 -0
- package/dist/cli.js +356 -13
- package/dist/index.js +1 -1
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -23,6 +23,7 @@ Emitting the contract is the supported way to integrate a new system. The adapte
|
|
|
23
23
|
|
|
24
24
|
- [Install](#install)
|
|
25
25
|
- [Quick start](#quick-start)
|
|
26
|
+
- [Gate CI on a trace contract](#gate-ci-on-a-trace-contract)
|
|
26
27
|
- [Integrate your own system](#integrate-your-own-system)
|
|
27
28
|
- [What it finds](#what-it-finds)
|
|
28
29
|
- [Supported harnesses](#supported-harnesses)
|
|
@@ -64,6 +65,7 @@ Requires Node ≥ 22.
|
|
|
64
65
|
traces validate spans.otlp.jsonl # what can this trace answer?
|
|
65
66
|
traces analyze --otlp spans.otlp.jsonl # analyse it, no adapter involved
|
|
66
67
|
traces analyze --harness claude-code --last 1 # or read a coding agent's own log
|
|
68
|
+
traces check session.jsonl --contract contract.json # gate CI on the path the run took
|
|
67
69
|
traces improve --harness claude-code --last 5 --dir .traces/improvement
|
|
68
70
|
traces watch --all
|
|
69
71
|
traces stream --all --mode findings
|
|
@@ -84,6 +86,48 @@ See [Trace analysts](#trace-analysts).
|
|
|
84
86
|
It writes one typed result, one report, flattened evidence rows, and the canonical OTLP trace.
|
|
85
87
|
Each finding already contains the claim, evidence, recommended action, confidence, and validation plan.
|
|
86
88
|
|
|
89
|
+
## Gate CI on a trace contract
|
|
90
|
+
|
|
91
|
+
Two runs can give the same answer while one of them calls a forbidden tool.
|
|
92
|
+
An output check passes both; a trace contract fails the wrong one.
|
|
93
|
+
|
|
94
|
+
```json
|
|
95
|
+
{
|
|
96
|
+
"name": "refund-desk",
|
|
97
|
+
"run": { "requireCompleted": true, "maxDurationMs": 300000 },
|
|
98
|
+
"tools": { "required": ["Read"], "forbidden": ["Bash"], "allowed": ["Read", "Grep"], "maxCalls": 10, "enforced": true },
|
|
99
|
+
"retries": { "reads": ["Read", "Grep"] },
|
|
100
|
+
"llm": { "maxCalls": 10 }
|
|
101
|
+
}
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
claude -p "$TASK" --tools Read,Grep --strict-mcp-config --output-format stream-json --verbose > run.jsonl
|
|
106
|
+
traces check run.jsonl --contract refund-desk.json --junit contract.xml
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
`tools.enforced` fails when the harness offered the model a tool the contract does not allow, which a flag such as `--tools` does not prove.
|
|
110
|
+
Without `--strict-mcp-config`, the run above is also offered every MCP server the account or project configures.
|
|
111
|
+
`retries` fails when a tool repeats a call with the same arguments and nothing proves the repeat is safe.
|
|
112
|
+
|
|
113
|
+
The trace is `claude -p --output-format stream-json` output, a Claude Code or Codex session file, OTLP spans (a file or a directory), or trace evidence; `--format` names it when the file could be read as more than one.
|
|
114
|
+
Only stream-json output records the offered tools and the run's own result.
|
|
115
|
+
**A saved interactive session file carries no run status at all** (the `result` record above only exists in `-p` mode), so `run.requireCompleted`/`run.allowedStatuses` always fail on one — write a contract for a session file with no `run` key, like the `refund-desk` example above running against `run.jsonl`.
|
|
116
|
+
Every rule prints `pass`, `fail`, `error`, or `skipped` (a rule of an alternative path that did not decide the result).
|
|
117
|
+
|
|
118
|
+
| Exit | Meaning |
|
|
119
|
+
|---|---|
|
|
120
|
+
| 0 | Every rule passed. |
|
|
121
|
+
| 1 | A rule failed. |
|
|
122
|
+
| 2 | The contract is malformed or contradicts itself, or a rule could not be evaluated. |
|
|
123
|
+
| 3 | The trace could not be read. |
|
|
124
|
+
| 4 | The trace reference is ambiguous. |
|
|
125
|
+
|
|
126
|
+
`--junit` writes one test case per rule, and GitHub annotations print under `GITHUB_ACTIONS=true` or `--annotations`.
|
|
127
|
+
`traces check --contract <file> --explain` prints what each compiled rule checks and reads no trace.
|
|
128
|
+
For a trace that does not pass, the verdict, the contract, its plain-language statement, and the redacted spans the violations cite go to `--evidence` (default `.traces/check`).
|
|
129
|
+
The contract keys and rule semantics are in [agent-eval's trace-contract guide](https://github.com/tangle-network/agent-eval/blob/main/docs/trace-contracts.md).
|
|
130
|
+
|
|
87
131
|
## Integrate your own system
|
|
88
132
|
|
|
89
133
|
No adapter, no plugin. Emit the span contract and point `--otlp` at the file.
|
|
@@ -253,6 +297,7 @@ traces facts --harness codex --session <id> # the deterministic facts she
|
|
|
253
297
|
traces diff run-a.otlp.jsonl run-b.otlp.jsonl # where two runs of one task first diverge
|
|
254
298
|
traces mcp --otlp spans.otlp.jsonl # read-only trace tools for an MCP client
|
|
255
299
|
traces analyze --all --since 2026-06-18 --out report.md
|
|
300
|
+
traces check spans.otlp.jsonl --contract c.json --junit c.xml # exit 0 pass, 1 fail, 2 bad contract, 3 unreadable, 4 ambiguous
|
|
256
301
|
traces validate spans.otlp.jsonl # conformance; exit 1 only when it is not a trace
|
|
257
302
|
traces validate results/sessions --out conformance.md # a whole directory of exports
|
|
258
303
|
traces analyze --otlp spans.otlp.jsonl # analyse foreign OTLP, no adapter
|
|
@@ -303,7 +348,11 @@ See [Replay verification](./docs/replay-verify.md) for setup, semantics, and hon
|
|
|
303
348
|
| `--since <t>` | `upload`: window, `30m`/`2h`/`7d` or ISO (default 24h); `analyze`: ISO cutoff |
|
|
304
349
|
| `--out <path>` | Write the report to a file |
|
|
305
350
|
| `--dir <path>` | `improve`: write the full artifact pack to this directory; `ask`: write `answers.json` + `report.md` there |
|
|
306
|
-
| `--otlp <file\|dir>` | **READ** OTLP-JSONL from any system, skipping the adapters; a directory reads the OTLP files under it (only `otlp/` when the producer made one) and names the JSONL that is not OTLP. `validate`, `analyze`, `investigate`, `improve`, `ask`, `stream` |
|
|
351
|
+
| `--otlp <file\|dir>` | **READ** OTLP-JSONL from any system, skipping the adapters; a directory reads the OTLP files under it (only `otlp/` when the producer made one) and names the JSONL that is not OTLP. `validate`, `check`, `analyze`, `investigate`, `improve`, `ask`, `stream` |
|
|
352
|
+
| `--contract <file>` | `check`: the declarative trace contract (JSON) |
|
|
353
|
+
| `--junit <file>` | `check`: write JUnit XML, one test case per rule |
|
|
354
|
+
| `--evidence <dir>` | `check`: where evidence for a trace that did not pass goes (default `.traces/check`) |
|
|
355
|
+
| `--annotations` | `check`: print GitHub workflow annotations (on by default under `GITHUB_ACTIONS=true`) |
|
|
307
356
|
| `--otlp-out <path>` | **WRITE** the OTLP artifact here (also evidence provenance / dry-run upload preview) |
|
|
308
357
|
| `--format <kind>` | File `analyze`, `export`, or `stream`: `auto`, `policy-evidence`, `sandbox-events`, `openinference`, `intelligence-spans`, or `chat-trajectory` |
|
|
309
358
|
| `--source-bundle <dir>` | `analyze` / `investigate` / `improve` / `ask`: read a retained full bundle and explicitly grant source-field reads |
|
|
@@ -19483,6 +19483,7 @@ function formatVerdict(result) {
|
|
|
19483
19483
|
|
|
19484
19484
|
export {
|
|
19485
19485
|
SOURCE_ATTRIBUTE_PREFIX,
|
|
19486
|
+
toolArgumentsFromAttributes,
|
|
19486
19487
|
SYNTHESIZED_SPAN_ATTR,
|
|
19487
19488
|
SYNTHESIZED_SOURCE_ATTR,
|
|
19488
19489
|
INHERITED_SPAN_ATTR,
|
package/dist/cli.js
CHANGED
|
@@ -38,11 +38,13 @@ import {
|
|
|
38
38
|
mergeTracesConfig,
|
|
39
39
|
normalizeTraceQuestions,
|
|
40
40
|
openAgenticTraceStore,
|
|
41
|
+
parseIsoToEpochMs,
|
|
41
42
|
parseSession,
|
|
42
43
|
parseSince,
|
|
43
44
|
planUpload,
|
|
44
45
|
primeAnalyzer,
|
|
45
46
|
projectSessionBundle,
|
|
47
|
+
readJsonl,
|
|
46
48
|
readOtlpInput,
|
|
47
49
|
readSessionIndexFile,
|
|
48
50
|
redactSpans,
|
|
@@ -63,6 +65,7 @@ import {
|
|
|
63
65
|
serializeTraceStreamEvent,
|
|
64
66
|
sessionReportSource,
|
|
65
67
|
streamSessions,
|
|
68
|
+
toolArgumentsFromAttributes,
|
|
66
69
|
totalAgenticFailureMessage,
|
|
67
70
|
traceStreamEventsFromSpans,
|
|
68
71
|
tracesVersion,
|
|
@@ -80,13 +83,13 @@ import {
|
|
|
80
83
|
writeSessionIndexFile,
|
|
81
84
|
writeTraceEvidenceExportFile,
|
|
82
85
|
writeTraceQuestionsArtifacts
|
|
83
|
-
} from "./chunk-
|
|
86
|
+
} from "./chunk-G5KKIQZ6.js";
|
|
84
87
|
|
|
85
88
|
// src/cli.ts
|
|
86
89
|
import { readFileSync } from "fs";
|
|
87
|
-
import { mkdir, mkdtemp, readFile, stat, writeFile } from "fs/promises";
|
|
90
|
+
import { mkdir as mkdir2, mkdtemp, readFile as readFile2, stat, writeFile as writeFile2 } from "fs/promises";
|
|
88
91
|
import { tmpdir } from "os";
|
|
89
|
-
import { basename, join, resolve } from "path";
|
|
92
|
+
import { basename, join as join2, resolve } from "path";
|
|
90
93
|
import {
|
|
91
94
|
parseCorpusFlag,
|
|
92
95
|
renderVerifiedFindingsSection
|
|
@@ -152,6 +155,252 @@ async function serveTraceMcp(options) {
|
|
|
152
155
|
}
|
|
153
156
|
}
|
|
154
157
|
|
|
158
|
+
// src/cli.ts
|
|
159
|
+
import { explainTraceContract as explainTraceContract2 } from "@tangle-network/agent-eval";
|
|
160
|
+
|
|
161
|
+
// src/check.ts
|
|
162
|
+
import { mkdir, readFile, writeFile } from "fs/promises";
|
|
163
|
+
import { join } from "path";
|
|
164
|
+
import {
|
|
165
|
+
compileTraceContractSpec,
|
|
166
|
+
evaluateTraceContract,
|
|
167
|
+
explainTraceContract,
|
|
168
|
+
lintTraceContractSpec
|
|
169
|
+
} from "@tangle-network/agent-eval";
|
|
170
|
+
var CHECK_EXIT = Object.freeze({
|
|
171
|
+
pass: 0,
|
|
172
|
+
fail: 1,
|
|
173
|
+
/** The contract is malformed or contradicts itself, or a rule could not run. */
|
|
174
|
+
badContract: 2,
|
|
175
|
+
unreadable: 3,
|
|
176
|
+
ambiguous: 4
|
|
177
|
+
});
|
|
178
|
+
var AmbiguousTraceInputError = class extends Error {
|
|
179
|
+
name = "AmbiguousTraceInputError";
|
|
180
|
+
};
|
|
181
|
+
var ContractFileError = class extends Error {
|
|
182
|
+
name = "ContractFileError";
|
|
183
|
+
};
|
|
184
|
+
async function loadContract(path) {
|
|
185
|
+
let text;
|
|
186
|
+
try {
|
|
187
|
+
text = await readFile(path, "utf8");
|
|
188
|
+
} catch (error) {
|
|
189
|
+
throw new ContractFileError(`cannot read contract ${path}: ${errorText(error)}`);
|
|
190
|
+
}
|
|
191
|
+
let json;
|
|
192
|
+
try {
|
|
193
|
+
json = JSON.parse(text);
|
|
194
|
+
} catch (error) {
|
|
195
|
+
throw new ContractFileError(`contract ${path} is not JSON: ${errorText(error)}`);
|
|
196
|
+
}
|
|
197
|
+
try {
|
|
198
|
+
const findings = lintTraceContractSpec(json);
|
|
199
|
+
const errors = findings.filter((f) => f.level === "error");
|
|
200
|
+
if (errors.length > 0) {
|
|
201
|
+
throw new ContractFileError(
|
|
202
|
+
`contract ${path} contradicts itself:
|
|
203
|
+
${errors.map((f) => ` ${f.path}: ${f.message} [${f.code}]`).join("\n")}`
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
return {
|
|
207
|
+
contract: compileTraceContractSpec(json),
|
|
208
|
+
warnings: findings.map((f) => `${f.path}: ${f.message} [${f.code}]`)
|
|
209
|
+
};
|
|
210
|
+
} catch (error) {
|
|
211
|
+
if (error instanceof ContractFileError) throw error;
|
|
212
|
+
throw new ContractFileError(`contract ${path}: ${errorText(error)}`);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
var SNIFF_ROWS = 20;
|
|
216
|
+
async function sniffCheckInput(path) {
|
|
217
|
+
const rows = [];
|
|
218
|
+
for await (const row of readJsonl(path)) {
|
|
219
|
+
rows.push(row);
|
|
220
|
+
if (rows.length >= SNIFF_ROWS) break;
|
|
221
|
+
}
|
|
222
|
+
const objects = rows.filter(isObject);
|
|
223
|
+
if (objects.length === 0) throw new Error(`${path} holds no JSON rows`);
|
|
224
|
+
const kinds = [];
|
|
225
|
+
if (objects.every((r) => typeof r.trace_id === "string" && typeof r.span_id === "string")) kinds.push("otlp");
|
|
226
|
+
if (objects.some((r) => (typeof r.sessionId === "string" || typeof r.session_id === "string") && typeof r.type === "string") && !objects.some(hasSpanId)) {
|
|
227
|
+
kinds.push("claude-code");
|
|
228
|
+
}
|
|
229
|
+
if (objects.some((r) => CODEX_LINE_TYPES.has(String(r.type)) && isObject(r.payload))) kinds.push("codex");
|
|
230
|
+
if (objects.every(isEvidenceRow)) kinds.push("evidence");
|
|
231
|
+
if (kinds.length === 1) return kinds[0];
|
|
232
|
+
if (kinds.length === 0) {
|
|
233
|
+
throw new Error(`${path} is not a trace this command reads (OTLP spans, a Claude Code or Codex session, or trace evidence)`);
|
|
234
|
+
}
|
|
235
|
+
throw new AmbiguousTraceInputError(
|
|
236
|
+
`${path} reads as ${kinds.join(" and ")}; pass --format ${kinds.join(" or --format ")}`
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
var CODEX_LINE_TYPES = /* @__PURE__ */ new Set(["session_meta", "response_item", "event_msg", "turn_context"]);
|
|
240
|
+
function hasSpanId(row) {
|
|
241
|
+
return typeof row.span_id === "string";
|
|
242
|
+
}
|
|
243
|
+
function isEvidenceRow(row) {
|
|
244
|
+
if (hasSpanId(row)) return false;
|
|
245
|
+
if (row.kind === "traces.policy_evidence.session") return true;
|
|
246
|
+
if (Array.isArray(row.messages) || typeof row.role === "string" && "content" in row) return true;
|
|
247
|
+
return typeof row.trace_id === "string" && typeof row.name === "string" && isObject(row.attributes);
|
|
248
|
+
}
|
|
249
|
+
function contractSpansFromOtlp(spans) {
|
|
250
|
+
return spans.map((span) => {
|
|
251
|
+
const startedAt = epochMs(span.start_time);
|
|
252
|
+
const endedAt = epochMs(span.end_time);
|
|
253
|
+
const args = toolArgumentsFromAttributes(span.attributes);
|
|
254
|
+
const truncated = span.attributes["traces.input.truncated"] === true;
|
|
255
|
+
return {
|
|
256
|
+
spanId: span.span_id,
|
|
257
|
+
parentSpanId: span.parent_span_id,
|
|
258
|
+
name: span.name,
|
|
259
|
+
...startedAt === void 0 ? {} : { startedAt },
|
|
260
|
+
...endedAt === void 0 ? {} : { endedAt },
|
|
261
|
+
...span.status.code === "ERROR" ? { status: "error" } : span.status.code === "OK" ? { status: "ok" } : {},
|
|
262
|
+
// A truncated argument capture is not the call's arguments.
|
|
263
|
+
...args.argsCaptured && !truncated ? { args: args.args } : { argsCaptured: false },
|
|
264
|
+
attributes: span.attributes
|
|
265
|
+
};
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
function epochMs(value) {
|
|
269
|
+
try {
|
|
270
|
+
return parseIsoToEpochMs(value);
|
|
271
|
+
} catch {
|
|
272
|
+
return void 0;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
function groupByTrace(spans) {
|
|
276
|
+
const groups = /* @__PURE__ */ new Map();
|
|
277
|
+
for (const span of spans) {
|
|
278
|
+
const list = groups.get(span.trace_id) ?? [];
|
|
279
|
+
list.push(span);
|
|
280
|
+
groups.set(span.trace_id, list);
|
|
281
|
+
}
|
|
282
|
+
return [...groups].map(([traceId, list]) => ({ traceId, spans: list }));
|
|
283
|
+
}
|
|
284
|
+
async function runCheck(contract, spans, opts) {
|
|
285
|
+
const traces = [];
|
|
286
|
+
for (const group of groupByTrace(spans)) {
|
|
287
|
+
const verdict = evaluateTraceContract(contract, contractSpansFromOtlp(group.spans));
|
|
288
|
+
const check = { traceId: group.traceId, spanCount: group.spans.length, verdict };
|
|
289
|
+
if (verdict.status !== "pass") {
|
|
290
|
+
check.evidenceDir = await writeEvidence(opts.evidenceRoot, contract, group.traceId, group.spans, verdict);
|
|
291
|
+
}
|
|
292
|
+
traces.push(check);
|
|
293
|
+
}
|
|
294
|
+
const status = traces.some((t) => t.verdict.status === "error") ? "error" : traces.every((t) => t.verdict.status === "pass") ? "pass" : "fail";
|
|
295
|
+
const exitCode = status === "pass" ? CHECK_EXIT.pass : status === "fail" ? CHECK_EXIT.fail : CHECK_EXIT.badContract;
|
|
296
|
+
return { contract: contract.name, status, exitCode, traces };
|
|
297
|
+
}
|
|
298
|
+
async function writeEvidence(root, contract, traceId, spans, verdict) {
|
|
299
|
+
const dir = join(root, `${safeName(contract.name)}-${safeName(traceId).slice(0, 40)}`);
|
|
300
|
+
await mkdir(dir, { recursive: true });
|
|
301
|
+
const cited = new Set(verdict.violations.map((v) => v.spanId).filter((id) => id !== void 0));
|
|
302
|
+
const redacted = redactSpans(spans.filter((s) => cited.has(s.span_id)));
|
|
303
|
+
await writeFile(join(dir, "verdict.json"), `${JSON.stringify({ traceId, verdict }, null, 2)}
|
|
304
|
+
`);
|
|
305
|
+
await writeFile(join(dir, "contract.json"), `${JSON.stringify(contract, null, 2)}
|
|
306
|
+
`);
|
|
307
|
+
await writeFile(join(dir, "explain.txt"), `${explainTraceContract(contract)}
|
|
308
|
+
`);
|
|
309
|
+
await writeFile(join(dir, "cited-spans.jsonl"), redacted.spans.map((s) => JSON.stringify(s)).join("\n") + (redacted.spans.length ? "\n" : ""));
|
|
310
|
+
return dir;
|
|
311
|
+
}
|
|
312
|
+
function safeName(value) {
|
|
313
|
+
return value.replace(/[^A-Za-z0-9._-]+/g, "_");
|
|
314
|
+
}
|
|
315
|
+
function ruleLines(verdict) {
|
|
316
|
+
const branchPassed = /* @__PURE__ */ new Map();
|
|
317
|
+
for (const e of verdict.ruleExecutions) {
|
|
318
|
+
if (e.alternative === void 0) continue;
|
|
319
|
+
branchPassed.set(e.alternative, (branchPassed.get(e.alternative) ?? true) && e.status === "pass");
|
|
320
|
+
}
|
|
321
|
+
const anyBranchPassed = [...branchPassed.values()].some(Boolean);
|
|
322
|
+
return verdict.ruleExecutions.map((e) => {
|
|
323
|
+
const rule = e.alternative ? `${e.alternative}/${e.rule}` : e.rule;
|
|
324
|
+
const status = e.alternative && anyBranchPassed && !branchPassed.get(e.alternative) ? "skipped" : e.status;
|
|
325
|
+
return {
|
|
326
|
+
rule,
|
|
327
|
+
status,
|
|
328
|
+
...e.error === void 0 ? {} : { error: e.error },
|
|
329
|
+
details: verdict.violations.filter((x) => x.rule === rule).map((x) => x.detail)
|
|
330
|
+
};
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
function renderCheckText(report) {
|
|
334
|
+
const lines = [];
|
|
335
|
+
for (const t of report.traces) {
|
|
336
|
+
const v = t.verdict;
|
|
337
|
+
lines.push(`${v.status.toUpperCase()} contract ${report.contract} trace ${t.traceId} (${t.spanCount} spans, ${v.notes})`);
|
|
338
|
+
for (const r of ruleLines(v)) {
|
|
339
|
+
lines.push(` ${r.status.padEnd(7)} ${r.rule}${r.error ? ` ${r.error}` : ""}`);
|
|
340
|
+
if (r.status === "fail") for (const detail of r.details.slice(0, 5)) lines.push(` ${detail}`);
|
|
341
|
+
}
|
|
342
|
+
if (t.evidenceDir) lines.push(` evidence \u2192 ${t.evidenceDir}`);
|
|
343
|
+
}
|
|
344
|
+
return lines.join("\n");
|
|
345
|
+
}
|
|
346
|
+
function renderJUnit(report) {
|
|
347
|
+
const suites = report.traces.map((t) => {
|
|
348
|
+
const rules = ruleLines(t.verdict);
|
|
349
|
+
const count = (status) => rules.filter((r) => r.status === status).length;
|
|
350
|
+
const cases = rules.map((r) => {
|
|
351
|
+
const open = ` <testcase classname="${xml(report.contract)}" name="${xml(r.rule)}">`;
|
|
352
|
+
if (r.status === "pass") return `${open}</testcase>`;
|
|
353
|
+
if (r.status === "skipped") return `${open}
|
|
354
|
+
<skipped message="another alternative passed"/>
|
|
355
|
+
</testcase>`;
|
|
356
|
+
if (r.status === "error") {
|
|
357
|
+
return `${open}
|
|
358
|
+
<error message="${xml(r.error ?? "rule could not be evaluated")}"/>
|
|
359
|
+
</testcase>`;
|
|
360
|
+
}
|
|
361
|
+
return `${open}
|
|
362
|
+
<failure message="${xml(r.details[0] ?? "rule failed")}">${xml(r.details.join("\n"))}</failure>
|
|
363
|
+
</testcase>`;
|
|
364
|
+
});
|
|
365
|
+
return [
|
|
366
|
+
` <testsuite name="${xml(`${report.contract} @ ${t.traceId}`)}" tests="${rules.length}" failures="${count("fail")}" errors="${count("error")}" skipped="${count("skipped")}">`,
|
|
367
|
+
...cases,
|
|
368
|
+
" </testsuite>"
|
|
369
|
+
].join("\n");
|
|
370
|
+
});
|
|
371
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
372
|
+
<testsuites name="traces check">
|
|
373
|
+
${suites.join("\n")}
|
|
374
|
+
</testsuites>
|
|
375
|
+
`;
|
|
376
|
+
}
|
|
377
|
+
function renderGithubAnnotations(report) {
|
|
378
|
+
const lines = [];
|
|
379
|
+
for (const t of report.traces) {
|
|
380
|
+
for (const r of ruleLines(t.verdict)) {
|
|
381
|
+
if (r.status === "pass" || r.status === "skipped") continue;
|
|
382
|
+
const detail = r.status === "error" ? `could not be evaluated: ${r.error ?? "unknown"}` : r.details.join("\n");
|
|
383
|
+
lines.push(`::error title=${ghProperty(`${report.contract}: ${r.rule}`)}::${ghData(`trace ${t.traceId}: ${detail}`)}`);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
return lines.join("\n");
|
|
387
|
+
}
|
|
388
|
+
function xml(value) {
|
|
389
|
+
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/[\u0000-\u0008\u000B\u000C\u000E-\u001F]/g, "");
|
|
390
|
+
}
|
|
391
|
+
function ghData(value) {
|
|
392
|
+
return value.replace(/%/g, "%25").replace(/\r/g, "%0D").replace(/\n/g, "%0A");
|
|
393
|
+
}
|
|
394
|
+
function ghProperty(value) {
|
|
395
|
+
return ghData(value).replace(/:/g, "%3A").replace(/,/g, "%2C");
|
|
396
|
+
}
|
|
397
|
+
function isObject(value) {
|
|
398
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
399
|
+
}
|
|
400
|
+
function errorText(error) {
|
|
401
|
+
return error instanceof Error ? error.message : String(error);
|
|
402
|
+
}
|
|
403
|
+
|
|
155
404
|
// src/cli.ts
|
|
156
405
|
function parseArgs(argv) {
|
|
157
406
|
const a = {
|
|
@@ -182,7 +431,9 @@ function parseArgs(argv) {
|
|
|
182
431
|
concurrency: 4,
|
|
183
432
|
verifyFindings: false,
|
|
184
433
|
replayCorpora: [],
|
|
185
|
-
questions: []
|
|
434
|
+
questions: [],
|
|
435
|
+
annotations: false,
|
|
436
|
+
explain: false
|
|
186
437
|
};
|
|
187
438
|
for (let i = 1; i < argv.length; i++) {
|
|
188
439
|
const arg = argv[i];
|
|
@@ -339,6 +590,21 @@ function parseArgs(argv) {
|
|
|
339
590
|
case "--format":
|
|
340
591
|
a.format = next();
|
|
341
592
|
break;
|
|
593
|
+
case "--contract":
|
|
594
|
+
a.contract = next();
|
|
595
|
+
break;
|
|
596
|
+
case "--junit":
|
|
597
|
+
a.junit = next();
|
|
598
|
+
break;
|
|
599
|
+
case "--evidence":
|
|
600
|
+
a.evidence = next();
|
|
601
|
+
break;
|
|
602
|
+
case "--annotations":
|
|
603
|
+
a.annotations = true;
|
|
604
|
+
break;
|
|
605
|
+
case "--explain":
|
|
606
|
+
a.explain = true;
|
|
607
|
+
break;
|
|
342
608
|
case "--help":
|
|
343
609
|
case "-h":
|
|
344
610
|
a.help = true;
|
|
@@ -355,7 +621,17 @@ function parseArgs(argv) {
|
|
|
355
621
|
}
|
|
356
622
|
return a;
|
|
357
623
|
}
|
|
358
|
-
var OTLP_INPUT_COMMANDS = /* @__PURE__ */ new Set([
|
|
624
|
+
var OTLP_INPUT_COMMANDS = /* @__PURE__ */ new Set([
|
|
625
|
+
"analyze",
|
|
626
|
+
"check",
|
|
627
|
+
"facts",
|
|
628
|
+
"investigate",
|
|
629
|
+
"improve",
|
|
630
|
+
"ask",
|
|
631
|
+
"stream",
|
|
632
|
+
"validate",
|
|
633
|
+
"mcp"
|
|
634
|
+
]);
|
|
359
635
|
function validateOtlpSelection(args) {
|
|
360
636
|
if (args.otlp === void 0) return args;
|
|
361
637
|
if (!OTLP_INPUT_COMMANDS.has(args.command)) {
|
|
@@ -499,7 +775,7 @@ async function resolveSelectedSession(args) {
|
|
|
499
775
|
`no ${args.all ? "selected" : adapter.harness} session with ID "${args.session}"; run \`traces list\` to select an ID, or pass an explicit session path`
|
|
500
776
|
);
|
|
501
777
|
}
|
|
502
|
-
throw new
|
|
778
|
+
throw new AmbiguousTraceInputError(
|
|
503
779
|
`session ID "${args.session}" is ambiguous across ${matches.length} files; select one harness or pass its explicit session path`
|
|
504
780
|
);
|
|
505
781
|
}
|
|
@@ -582,7 +858,7 @@ async function collectSpans(args) {
|
|
|
582
858
|
for (const output of [args.out, args.dir, args.otlpOut]) {
|
|
583
859
|
if (output) await assertOutsideSourceBundle(args.sourceBundle, output);
|
|
584
860
|
}
|
|
585
|
-
return collectOtlpSpans(
|
|
861
|
+
return collectOtlpSpans(join2(args.sourceBundle, "derived", "trace.otlp.jsonl"));
|
|
586
862
|
}
|
|
587
863
|
if (args.otlp) return collectOtlpSpans(args.otlp);
|
|
588
864
|
if (args.input) {
|
|
@@ -634,6 +910,62 @@ async function cmdValidate(args) {
|
|
|
634
910
|
}
|
|
635
911
|
process.exitCode = validationExitCode(validation);
|
|
636
912
|
}
|
|
913
|
+
var EVIDENCE_FORMATS = /* @__PURE__ */ new Set(["policy-evidence", "sandbox-events", "openinference", "intelligence-spans", "chat-trajectory"]);
|
|
914
|
+
async function checkInputArgs(args) {
|
|
915
|
+
if (!args.input) return args;
|
|
916
|
+
if (args.otlp || args.session) throw new Error("name the trace once: a positional path, --otlp, or --session");
|
|
917
|
+
const info = await stat(args.input);
|
|
918
|
+
let kind;
|
|
919
|
+
if (args.format && EVIDENCE_FORMATS.has(args.format)) kind = "evidence";
|
|
920
|
+
else if (args.format === "otlp" || args.format === "claude-code" || args.format === "codex") kind = args.format;
|
|
921
|
+
else if (args.format) throw new Error(`unknown --format "${args.format}" (otlp, claude-code, codex, or an evidence format)`);
|
|
922
|
+
else kind = info.isDirectory() ? "otlp" : await sniffCheckInput(args.input);
|
|
923
|
+
if (kind === "otlp") return { ...args, otlp: args.input, input: void 0 };
|
|
924
|
+
if (kind === "evidence") return args;
|
|
925
|
+
return { ...args, harness: kind, harnessExplicit: true, session: args.input, input: void 0 };
|
|
926
|
+
}
|
|
927
|
+
async function cmdCheck(args) {
|
|
928
|
+
if (!args.contract) {
|
|
929
|
+
process.stderr.write("error: check needs --contract <contract.json>\n");
|
|
930
|
+
process.exitCode = CHECK_EXIT.badContract;
|
|
931
|
+
return;
|
|
932
|
+
}
|
|
933
|
+
let loaded;
|
|
934
|
+
try {
|
|
935
|
+
loaded = await loadContract(args.contract);
|
|
936
|
+
} catch (error) {
|
|
937
|
+
process.stderr.write(`error: ${error instanceof Error ? error.message : String(error)}
|
|
938
|
+
`);
|
|
939
|
+
process.exitCode = CHECK_EXIT.badContract;
|
|
940
|
+
return;
|
|
941
|
+
}
|
|
942
|
+
for (const warning of loaded.warnings) process.stderr.write(`warning: ${warning}
|
|
943
|
+
`);
|
|
944
|
+
if (args.explain) {
|
|
945
|
+
console.log(explainTraceContract2(loaded.contract));
|
|
946
|
+
return;
|
|
947
|
+
}
|
|
948
|
+
let spans;
|
|
949
|
+
try {
|
|
950
|
+
spans = (await collectSpans(await checkInputArgs(args))).spans;
|
|
951
|
+
if (spans.length === 0) throw new Error("the trace holds no spans");
|
|
952
|
+
} catch (error) {
|
|
953
|
+
process.stderr.write(`error: ${error instanceof Error ? error.message : String(error)}
|
|
954
|
+
`);
|
|
955
|
+
process.exitCode = error instanceof AmbiguousTraceInputError ? CHECK_EXIT.ambiguous : CHECK_EXIT.unreadable;
|
|
956
|
+
return;
|
|
957
|
+
}
|
|
958
|
+
const report = await runCheck(loaded.contract, spans, { evidenceRoot: args.evidence ?? join2(".traces", "check") });
|
|
959
|
+
console.log(renderCheckText(report));
|
|
960
|
+
if (args.junit) await writeFile2(args.junit, renderJUnit(report), "utf8");
|
|
961
|
+
if (args.out) await writeFile2(args.out, `${JSON.stringify(report, null, 2)}
|
|
962
|
+
`, "utf8");
|
|
963
|
+
if (args.annotations || process.env.GITHUB_ACTIONS === "true") {
|
|
964
|
+
const annotations = renderGithubAnnotations(report);
|
|
965
|
+
if (annotations) console.log(annotations);
|
|
966
|
+
}
|
|
967
|
+
process.exitCode = report.exitCode;
|
|
968
|
+
}
|
|
637
969
|
async function cmdConvert(args) {
|
|
638
970
|
const { spans, workflow } = await collectSpans(args);
|
|
639
971
|
if (spans.length === 0) throw new Error("no spans found for the given selection");
|
|
@@ -872,7 +1204,7 @@ async function cmdFacts(args) {
|
|
|
872
1204
|
const rendered = format === "json" ? `${JSON.stringify(report, null, 2)}
|
|
873
1205
|
` : renderSessionFacts(report);
|
|
874
1206
|
if (args.out) {
|
|
875
|
-
await
|
|
1207
|
+
await writeFile2(args.out, rendered, "utf8");
|
|
876
1208
|
console.log(`session facts \u2192 ${args.out} (${report.sessions.length} session(s), $0, no model call)`);
|
|
877
1209
|
return;
|
|
878
1210
|
}
|
|
@@ -968,7 +1300,7 @@ function parseAttributeValue(raw) {
|
|
|
968
1300
|
async function loadExportAttributes(args) {
|
|
969
1301
|
const attributes = {};
|
|
970
1302
|
if (args.metadata) {
|
|
971
|
-
const parsed = JSON.parse(await
|
|
1303
|
+
const parsed = JSON.parse(await readFile2(args.metadata, "utf8"));
|
|
972
1304
|
if (parsed == null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
973
1305
|
throw new Error("--metadata must point to a JSON object");
|
|
974
1306
|
}
|
|
@@ -1193,8 +1525,8 @@ async function cmdAsk(args) {
|
|
|
1193
1525
|
const collected = await collectSpans(args);
|
|
1194
1526
|
if (collected.spans.length === 0) throw new Error("no spans found for the given selection");
|
|
1195
1527
|
warnIncompleteWorkflow(collected.workflow);
|
|
1196
|
-
const directory = resolve(args.dir ?? await mkdtemp(
|
|
1197
|
-
await
|
|
1528
|
+
const directory = resolve(args.dir ?? await mkdtemp(join2(tmpdir(), "traces-ask-")));
|
|
1529
|
+
await mkdir2(directory, { recursive: true });
|
|
1198
1530
|
const controller = new AbortController();
|
|
1199
1531
|
const interrupt = () => controller.abort(new Error("ask interrupted"));
|
|
1200
1532
|
process.once("SIGINT", interrupt);
|
|
@@ -1208,7 +1540,7 @@ async function cmdAsk(args) {
|
|
|
1208
1540
|
concurrency: args.concurrency,
|
|
1209
1541
|
...args.budget !== void 0 ? { budgetUsd: args.budget } : {},
|
|
1210
1542
|
...args.sourceBundle ? { sourceBundle: { path: args.sourceBundle } } : {},
|
|
1211
|
-
otlpOutPath: args.otlpOut ??
|
|
1543
|
+
otlpOutPath: args.otlpOut ?? join2(directory, "traces.otlp.jsonl"),
|
|
1212
1544
|
signal: controller.signal,
|
|
1213
1545
|
log: analystLog
|
|
1214
1546
|
});
|
|
@@ -1622,6 +1954,14 @@ Two ways in:
|
|
|
1622
1954
|
|
|
1623
1955
|
Commands:
|
|
1624
1956
|
validate Report what a trace can and cannot answer (exit 1 on error findings)
|
|
1957
|
+
check Gate a run on a trace contract: check <trace> --contract <file.json>
|
|
1958
|
+
[--junit out.xml] [--out report.json] [--evidence dir] [--annotations].
|
|
1959
|
+
check --contract <file.json> --explain prints what the contract checks.
|
|
1960
|
+
<trace> is claude -p stream-json output, OTLP spans (file or dir),
|
|
1961
|
+
a Claude Code or Codex session file, or trace evidence; --format
|
|
1962
|
+
names it when the sniff cannot.
|
|
1963
|
+
Exit 0 pass, 1 fail, 2 bad contract or a rule could not run,
|
|
1964
|
+
3 unreadable trace, 4 ambiguous trace reference
|
|
1625
1965
|
list List discovered sessions
|
|
1626
1966
|
analyze Run analyst suite + loop/waste pipelines over OTLP (--otlp), sessions,
|
|
1627
1967
|
or an input file; names every analysis the spans could not support
|
|
@@ -1852,6 +2192,9 @@ async function main() {
|
|
|
1852
2192
|
case "validate":
|
|
1853
2193
|
await cmdValidate(args);
|
|
1854
2194
|
break;
|
|
2195
|
+
case "check":
|
|
2196
|
+
await cmdCheck(args);
|
|
2197
|
+
break;
|
|
1855
2198
|
case "investigate":
|
|
1856
2199
|
await cmdInvestigate(args);
|
|
1857
2200
|
break;
|
|
@@ -1904,5 +2247,5 @@ async function main() {
|
|
|
1904
2247
|
main().catch((err) => {
|
|
1905
2248
|
process.stderr.write(`error: ${err instanceof Error ? err.message : String(err)}
|
|
1906
2249
|
`);
|
|
1907
|
-
process.exitCode = 1;
|
|
2250
|
+
process.exitCode = process.argv[2] === "check" ? CHECK_EXIT.badContract : 1;
|
|
1908
2251
|
});
|
package/dist/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tangle-network/traces",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.16.0",
|
|
4
4
|
"description": "Point it at your coding-agent session traces (Claude Code, Codex, OpenCode, Gemini, Pi, …) and get failure-mode + efficiency findings. CLI + SDK over the @tangle-network/agent-eval analyst suite — observe live sessions, run your own analysts, redact, and upload to the Tangle Intelligence Platform.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -57,6 +57,7 @@
|
|
|
57
57
|
"test": "vitest run",
|
|
58
58
|
"test:watch": "vitest",
|
|
59
59
|
"typecheck": "tsc --noEmit",
|
|
60
|
+
"verify:contracts-corpus": "tsx tests/contracts-corpus/verify.mjs",
|
|
60
61
|
"prepublishOnly": "pnpm check:source && pnpm build && pnpm check:package"
|
|
61
62
|
},
|
|
62
63
|
"dependencies": {
|