@hawkeyexl/manni 4.2.0 → 4.3.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 +2 -2
- package/dist/batch-A5HZKYIW.js +18 -0
- package/dist/batch-A5HZKYIW.js.map +1 -0
- package/dist/calibrate-S5K6QHUY.js +13 -0
- package/dist/calibrate-S5K6QHUY.js.map +1 -0
- package/dist/capture-OZEZDABQ.js +15 -0
- package/dist/capture-OZEZDABQ.js.map +1 -0
- package/dist/chunk-3R23C5DT.js +120 -0
- package/dist/chunk-3R23C5DT.js.map +1 -0
- package/dist/chunk-5HNHEUD5.js +174 -0
- package/dist/chunk-5HNHEUD5.js.map +1 -0
- package/dist/chunk-CFDUZRM2.js +1941 -0
- package/dist/chunk-CFDUZRM2.js.map +1 -0
- package/dist/chunk-G5CM2VGQ.js +297 -0
- package/dist/chunk-G5CM2VGQ.js.map +1 -0
- package/dist/chunk-HAPTXWU2.js +379 -0
- package/dist/chunk-HAPTXWU2.js.map +1 -0
- package/dist/chunk-IOYXPBCL.js +37 -0
- package/dist/chunk-IOYXPBCL.js.map +1 -0
- package/dist/chunk-KJR7OVDX.js +4447 -0
- package/dist/chunk-KJR7OVDX.js.map +1 -0
- package/dist/chunk-KTSJ2ZHA.js +40 -0
- package/dist/chunk-KTSJ2ZHA.js.map +1 -0
- package/dist/{chunk-BNHQWIAE.js → chunk-UBMMXS2Y.js} +2 -2
- package/dist/{chunk-UYDRDFYF.js → chunk-V4KUVE47.js} +1943 -909
- package/dist/chunk-V4KUVE47.js.map +1 -0
- package/dist/{chunk-TH565O2N.js → chunk-ZQBTK3DO.js} +898 -278
- package/dist/chunk-ZQBTK3DO.js.map +1 -0
- package/dist/cli.js +437 -47
- package/dist/cli.js.map +1 -1
- package/dist/docmeta.js +3 -2
- package/dist/docmeta.js.map +1 -1
- package/dist/index.d.ts +3873 -795
- package/dist/index.js +292 -4
- package/dist/index.js.map +1 -1
- package/dist/picker-6HRMETE7.js +12 -0
- package/dist/picker-6HRMETE7.js.map +1 -0
- package/package.json +3 -1
- package/plugin/tracevals/.claude-plugin/plugin.json +9 -0
- package/plugin/tracevals/hooks/hooks.json +17 -0
- package/dist/chunk-TH565O2N.js.map +0 -1
- package/dist/chunk-UYDRDFYF.js.map +0 -1
- /package/dist/{chunk-BNHQWIAE.js.map → chunk-UBMMXS2Y.js.map} +0 -0
package/README.md
CHANGED
|
@@ -22,7 +22,7 @@ Project](https://www.thegooddocsproject.dev/template), and the [Seven-Action
|
|
|
22
22
|
model](https://passo.uno/seven-action-model/). They also cover the front matter
|
|
23
23
|
contracts of site generators such as
|
|
24
24
|
[Docusaurus](https://docusaurus.io/docs/api/plugins/@docusaurus/plugin-content-docs#markdown-front-matter)
|
|
25
|
-
3.10, Hugo, Jekyll, and MkDocs Material. Twenty-
|
|
25
|
+
3.10, Hugo, Jekyll, and MkDocs Material. Twenty-six are manni's own metadata
|
|
26
26
|
vocabularies and their strict overlays. Run `manni meta schemas` for the full
|
|
27
27
|
list. It follows [clig.dev](https://clig.dev) conventions and returns a nonzero
|
|
28
28
|
exit code (plus optional GitHub annotations) when validation fails.
|
|
@@ -102,7 +102,7 @@ actually reads rather than hand-written:
|
|
|
102
102
|
```yaml
|
|
103
103
|
repos:
|
|
104
104
|
- repo: https://github.com/hawkeyexl/manni
|
|
105
|
-
rev: v4.
|
|
105
|
+
rev: v4.3.1
|
|
106
106
|
hooks:
|
|
107
107
|
- id: manni-meta
|
|
108
108
|
```
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
keepNewerThan,
|
|
4
|
+
parseNewerThan,
|
|
5
|
+
resolveBatchTraces,
|
|
6
|
+
runBatch
|
|
7
|
+
} from "./chunk-KJR7OVDX.js";
|
|
8
|
+
import "./chunk-CFDUZRM2.js";
|
|
9
|
+
import "./chunk-ZQBTK3DO.js";
|
|
10
|
+
import "./chunk-3R23C5DT.js";
|
|
11
|
+
import "./chunk-IOYXPBCL.js";
|
|
12
|
+
export {
|
|
13
|
+
keepNewerThan,
|
|
14
|
+
parseNewerThan,
|
|
15
|
+
resolveBatchTraces,
|
|
16
|
+
runBatch
|
|
17
|
+
};
|
|
18
|
+
//# sourceMappingURL=batch-A5HZKYIW.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
runCalibrate
|
|
4
|
+
} from "./chunk-HAPTXWU2.js";
|
|
5
|
+
import "./chunk-KJR7OVDX.js";
|
|
6
|
+
import "./chunk-CFDUZRM2.js";
|
|
7
|
+
import "./chunk-ZQBTK3DO.js";
|
|
8
|
+
import "./chunk-3R23C5DT.js";
|
|
9
|
+
import "./chunk-IOYXPBCL.js";
|
|
10
|
+
export {
|
|
11
|
+
runCalibrate
|
|
12
|
+
};
|
|
13
|
+
//# sourceMappingURL=calibrate-S5K6QHUY.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
renderCapture,
|
|
4
|
+
runCapture
|
|
5
|
+
} from "./chunk-G5CM2VGQ.js";
|
|
6
|
+
import "./chunk-5HNHEUD5.js";
|
|
7
|
+
import "./chunk-CFDUZRM2.js";
|
|
8
|
+
import "./chunk-ZQBTK3DO.js";
|
|
9
|
+
import "./chunk-3R23C5DT.js";
|
|
10
|
+
import "./chunk-IOYXPBCL.js";
|
|
11
|
+
export {
|
|
12
|
+
renderCapture,
|
|
13
|
+
runCapture
|
|
14
|
+
};
|
|
15
|
+
//# sourceMappingURL=capture-OZEZDABQ.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
ToolError
|
|
4
|
+
} from "./chunk-IOYXPBCL.js";
|
|
5
|
+
|
|
6
|
+
// src/tracevals/trace/discover.ts
|
|
7
|
+
import { open, readdir, stat } from "node:fs/promises";
|
|
8
|
+
import { homedir } from "node:os";
|
|
9
|
+
import { join, resolve } from "node:path";
|
|
10
|
+
function slugFor(cwd) {
|
|
11
|
+
return cwd.replace(/[^a-zA-Z0-9]/g, "-");
|
|
12
|
+
}
|
|
13
|
+
function configDir(env = process.env) {
|
|
14
|
+
const configured = env.CLAUDE_CONFIG_DIR;
|
|
15
|
+
if (configured) return resolve(configured);
|
|
16
|
+
return join(homedir(), ".claude");
|
|
17
|
+
}
|
|
18
|
+
async function discoverTraces(options = {}) {
|
|
19
|
+
const store = join(configDir(options.env), "projects");
|
|
20
|
+
let projectDirs;
|
|
21
|
+
if (options.allProjects) {
|
|
22
|
+
try {
|
|
23
|
+
const entries = await readdir(store, { withFileTypes: true });
|
|
24
|
+
projectDirs = entries.filter((e) => e.isDirectory()).map((e) => join(store, e.name));
|
|
25
|
+
} catch {
|
|
26
|
+
return [];
|
|
27
|
+
}
|
|
28
|
+
} else {
|
|
29
|
+
const project = options.project ?? process.cwd();
|
|
30
|
+
projectDirs = [join(store, slugFor(project))];
|
|
31
|
+
}
|
|
32
|
+
const listings = [];
|
|
33
|
+
for (const dir of projectDirs) {
|
|
34
|
+
let names;
|
|
35
|
+
try {
|
|
36
|
+
names = (await readdir(dir)).filter((n) => n.endsWith(".jsonl"));
|
|
37
|
+
} catch {
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
for (const name of names) {
|
|
41
|
+
const file = join(dir, name);
|
|
42
|
+
try {
|
|
43
|
+
const info = await stat(file);
|
|
44
|
+
listings.push({
|
|
45
|
+
file,
|
|
46
|
+
mtimeMs: info.mtimeMs,
|
|
47
|
+
sizeBytes: info.size,
|
|
48
|
+
...await summarizeTrace(file)
|
|
49
|
+
});
|
|
50
|
+
} catch {
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
listings.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
56
|
+
return options.limit !== void 0 ? listings.slice(0, options.limit) : listings;
|
|
57
|
+
}
|
|
58
|
+
var SUMMARY_WINDOW = 256 * 1024;
|
|
59
|
+
async function readHead(file, bytes) {
|
|
60
|
+
let handle;
|
|
61
|
+
try {
|
|
62
|
+
handle = await open(file, "r");
|
|
63
|
+
const buffer = Buffer.alloc(bytes);
|
|
64
|
+
const { bytesRead } = await handle.read(buffer, 0, bytes, 0);
|
|
65
|
+
return buffer.toString("utf-8", 0, bytesRead);
|
|
66
|
+
} catch {
|
|
67
|
+
return null;
|
|
68
|
+
} finally {
|
|
69
|
+
await handle?.close();
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
async function summarizeTrace(file) {
|
|
73
|
+
const content = await readHead(file, SUMMARY_WINDOW);
|
|
74
|
+
if (content === null) return {};
|
|
75
|
+
const summary = {};
|
|
76
|
+
for (const line of content.split("\n")) {
|
|
77
|
+
const trimmed = line.trim();
|
|
78
|
+
if (!trimmed) continue;
|
|
79
|
+
let parsed;
|
|
80
|
+
try {
|
|
81
|
+
parsed = JSON.parse(trimmed);
|
|
82
|
+
} catch {
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
if (typeof parsed !== "object" || parsed === null) continue;
|
|
86
|
+
const rec = parsed;
|
|
87
|
+
if (summary.sessionId === void 0) {
|
|
88
|
+
const id = rec.sessionId ?? rec.session_id;
|
|
89
|
+
if (typeof id === "string") summary.sessionId = id;
|
|
90
|
+
}
|
|
91
|
+
if (summary.project === void 0 && typeof rec.cwd === "string") {
|
|
92
|
+
summary.project = rec.cwd;
|
|
93
|
+
}
|
|
94
|
+
if (summary.firstPrompt === void 0 && rec.type === "user" && rec.isSidechain !== true) {
|
|
95
|
+
const message = rec.message;
|
|
96
|
+
const content2 = message?.content;
|
|
97
|
+
if (typeof content2 === "string" && !content2.startsWith("<command-")) {
|
|
98
|
+
summary.firstPrompt = content2.split("\n")[0]?.slice(0, 200);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
if (summary.sessionId && summary.project && summary.firstPrompt) break;
|
|
102
|
+
}
|
|
103
|
+
return summary;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// src/tracevals/types.ts
|
|
107
|
+
var TracevalsError = class extends ToolError {
|
|
108
|
+
constructor(message) {
|
|
109
|
+
super(message);
|
|
110
|
+
this.name = "TracevalsError";
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
export {
|
|
115
|
+
slugFor,
|
|
116
|
+
configDir,
|
|
117
|
+
discoverTraces,
|
|
118
|
+
TracevalsError
|
|
119
|
+
};
|
|
120
|
+
//# sourceMappingURL=chunk-3R23C5DT.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/tracevals/trace/discover.ts","../src/tracevals/types.ts"],"sourcesContent":["/**\n * Session-store discovery: find Claude Code trace files for a project (or all\n * projects) in Claude Code's own config directory.\n *\n * The location is detected, not configured: `CLAUDE_CONFIG_DIR` is Claude\n * Code's variable for that directory, so a session that has moved it is\n * followed without tracevals adding a switch of its own, and a test or CI job\n * points at a fixture tree with the same variable the agent itself reads.\n */\nimport { open, readdir, stat } from \"node:fs/promises\";\nimport { homedir } from \"node:os\";\nimport { join, resolve } from \"node:path\";\n\nexport interface TraceListing {\n /** Absolute path of the trace file. */\n file: string;\n mtimeMs: number;\n sizeBytes: number;\n sessionId?: string;\n /** The cwd recorded in the session, when readable. */\n project?: string;\n /** First non-sidechain user prompt, for identification in lists. */\n firstPrompt?: string;\n}\n\nexport interface DiscoverOptions {\n /** Project directory to scope to (its session-store slug is scanned). */\n project?: string;\n /** Scan every project directory in the store. */\n allProjects?: boolean;\n /** Maximum entries returned, applied after newest-first sorting. */\n limit?: number;\n env?: Record<string, string | undefined>;\n}\n\n/** Claude Code's project-slug rule: every non-alphanumeric character → `-`. */\nexport function slugFor(cwd: string): string {\n return cwd.replace(/[^a-zA-Z0-9]/g, \"-\");\n}\n\n/**\n * Claude Code's config directory: `CLAUDE_CONFIG_DIR` when it is set, and\n * `~/.claude` otherwise. Everything the agent keeps per user — `projects/`,\n * `skills/`, `agents/`, `commands/`, `plugins/` — hangs off it.\n */\nexport function configDir(\n env: Record<string, string | undefined> = process.env,\n): string {\n const configured = env.CLAUDE_CONFIG_DIR;\n if (configured) return resolve(configured);\n return join(homedir(), \".claude\");\n}\n\nexport async function discoverTraces(\n options: DiscoverOptions = {},\n): Promise<TraceListing[]> {\n const store = join(configDir(options.env), \"projects\");\n\n let projectDirs: string[];\n if (options.allProjects) {\n try {\n const entries = await readdir(store, { withFileTypes: true });\n projectDirs = entries\n .filter((e) => e.isDirectory())\n .map((e) => join(store, e.name));\n } catch {\n return [];\n }\n } else {\n const project = options.project ?? process.cwd();\n projectDirs = [join(store, slugFor(project))];\n }\n\n const listings: TraceListing[] = [];\n for (const dir of projectDirs) {\n let names: string[];\n try {\n names = (await readdir(dir)).filter((n) => n.endsWith(\".jsonl\"));\n } catch {\n continue;\n }\n for (const name of names) {\n const file = join(dir, name);\n try {\n const info = await stat(file);\n listings.push({\n file,\n mtimeMs: info.mtimeMs,\n sizeBytes: info.size,\n ...(await summarizeTrace(file)),\n });\n } catch {\n continue;\n }\n }\n }\n\n listings.sort((a, b) => b.mtimeMs - a.mtimeMs);\n return options.limit !== undefined\n ? listings.slice(0, options.limit)\n : listings;\n}\n\n/** Bytes read from the head of a session file when summarizing for a list. */\nconst SUMMARY_WINDOW = 256 * 1024;\n\n/** Read only the first `bytes` of a file — session files run to many MB. */\nasync function readHead(file: string, bytes: number): Promise<string | null> {\n let handle;\n try {\n handle = await open(file, \"r\");\n const buffer = Buffer.alloc(bytes);\n const { bytesRead } = await handle.read(buffer, 0, bytes, 0);\n return buffer.toString(\"utf-8\", 0, bytesRead);\n } catch {\n return null;\n } finally {\n await handle?.close();\n }\n}\n\nasync function summarizeTrace(\n file: string,\n): Promise<Pick<TraceListing, \"sessionId\" | \"project\" | \"firstPrompt\">> {\n const content = await readHead(file, SUMMARY_WINDOW);\n if (content === null) return {};\n\n const summary: Pick<TraceListing, \"sessionId\" | \"project\" | \"firstPrompt\"> =\n {};\n for (const line of content.split(\"\\n\")) {\n const trimmed = line.trim();\n if (!trimmed) continue;\n let parsed: unknown;\n try {\n parsed = JSON.parse(trimmed);\n } catch {\n continue;\n }\n // A bare `null`, number or string is valid JSON but no record. The same\n // guard `detect.ts` uses: skip the line rather than let a property read\n // throw and drop the whole file from the listing.\n if (typeof parsed !== \"object\" || parsed === null) continue;\n const rec = parsed as Record<string, unknown>;\n if (summary.sessionId === undefined) {\n const id = rec.sessionId ?? rec.session_id;\n if (typeof id === \"string\") summary.sessionId = id;\n }\n if (summary.project === undefined && typeof rec.cwd === \"string\") {\n summary.project = rec.cwd;\n }\n if (\n summary.firstPrompt === undefined &&\n rec.type === \"user\" &&\n rec.isSidechain !== true\n ) {\n const message = rec.message as Record<string, unknown> | undefined;\n const content = message?.content;\n if (typeof content === \"string\" && !content.startsWith(\"<command-\")) {\n summary.firstPrompt = content.split(\"\\n\")[0]?.slice(0, 200);\n }\n }\n if (summary.sessionId && summary.project && summary.firstPrompt) break;\n }\n return summary;\n}\n","/** Shared result model and errors for manni tracevals. */\nimport type { ConsensusResult } from \"@hawkeyexl/inference\";\nimport type {\n AvailabilityReport,\n CoverageEntry,\n} from \"./artifacts/types.js\";\nimport type { ArtifactType } from \"./artifacts/types.js\";\nimport type { ManifestReport } from \"./capture/types.js\";\nimport type { Finding } from \"./graders/types.js\";\nimport { ToolError } from \"../shared/errors.js\";\n\n/**\n * Operational error: bad usage, unreadable input, unknown format. Exits 2.\n * A `ToolError`, so the shared bin runner prints it as one line.\n */\nexport class TracevalsError extends ToolError {\n constructor(message: string) {\n super(message);\n this.name = \"TracevalsError\";\n }\n}\n\nexport type Outcome = \"pass\" | \"fail\" | \"error\" | \"needs-review\" | \"skipped\";\n\nexport interface EvalResult {\n evalName: string;\n /** Path of the artifact the eval came from. */\n artifact: string;\n artifactName: string;\n artifactType: ArtifactType;\n grader: string;\n implicit: boolean;\n /**\n * Where the eval is declared: the item of `metadata.evals` that names it, in\n * the artifact or in the manifest that supplied the block (proposal 0037).\n * `file` is relative to the working directory and forward-slashed when it is\n * inside it, absolute when it is not, and a URL for a hosted manifest.\n * `line` is 1-based, and absent when nothing records one, as for the\n * implicit eval, which no entry declares.\n */\n location: { file: string; line?: number };\n outcome: Outcome;\n /** Present for deterministic evals that produced findings. */\n findings?: Finding[];\n /** Present for judged evals. */\n consensus?: ConsensusResult;\n error?: string;\n skipReason?: string;\n /**\n * Judged evals only: uncached ensemble runs this eval spent. `0` when the\n * ensemble came from cache, or when the turn budget stopped it.\n */\n turns?: number;\n /**\n * Set when the model that judged this eval also produced what it graded.\n *\n * `session` — the judge model is the model that ran the session. This is the\n * sharpest form of the bias in either manni tool: the judge is not being\n * asked about a document it happened to draft, it is being asked whether its\n * own behavior followed the rules. `criterion` — `meta-provenance` records\n * this model proposing this assertion, so the judge wrote the question.\n *\n * Reported, never fatal: bias skews a verdict, it does not stop one forming,\n * and erroring would punish a single-model setup with no second provider to\n * reach for.\n */\n selfPreference?: { axis: \"session\" | \"criterion\"; model: string };\n /**\n * The eval's weight in the run's pass rate. Never changes its own outcome —\n * the binary verdict is what the reporters and exit code consume.\n */\n weight?: number;\n durationMs: number;\n}\n\nexport interface RunSummary {\n total: number;\n pass: number;\n fail: number;\n error: number;\n needsReview: number;\n skipped: number;\n /**\n * Weighted share of the graded set that passed, 1 when nothing was graded.\n *\n * The graded set is pass + fail + error; `needs-review` and `skipped` are in\n * neither half, so a session awaiting review neither helps nor hurts. Each\n * eval contributes its `weight` (1 unless it says otherwise), which is what\n * lets a secondary check report without dominating.\n *\n * Reported, never gated: manni tracevals has no suite targets to compare it\n * against, and inventing an exit-code rule around a number nobody configured\n * would be a gate no one asked for.\n */\n passRate: number;\n}\n\nexport interface RunReport {\n trace: {\n file: string;\n source: string;\n sessionId?: string;\n cwd: string;\n model?: string;\n turnCount: number;\n };\n /** Trace + resolution warnings, surfaced in every format. */\n warnings: string[];\n coverage: CoverageEntry[];\n /**\n * Offered versus used (ADR 01016). An observation about the session's\n * configuration; it never contributes to `summary` or `exitCode`.\n */\n availability: AvailabilityReport;\n /**\n * The session manifest this run consulted (ADR 01024), when one was found.\n * Absent means no manifest existed, which is the ordinary case — every hash\n * check then reports `skipped` and staleness stays the mtime heuristic.\n * Like `availability`, an observation: never in `summary`, never in\n * `exitCode`.\n */\n manifest?: ManifestReport;\n evalResults: EvalResult[];\n summary: RunSummary;\n /** 0 pass, 1 any fail/error (and needs-review when failOnReview). */\n exitCode: 0 | 1;\n /** Uncached ensemble runs this run spent. See `judge.maxTurns`. */\n turns: number;\n durationMs: number;\n}\n\n/* -------------------------------------------------------------------------- *\n * Batch runs (ADR 01018)\n *\n * Adherence is a rate across sessions, not a single verdict. `BatchReport`\n * sits alongside `RunReport` rather than replacing it: naming one trace still\n * yields exactly the `RunReport` above, byte for byte.\n * -------------------------------------------------------------------------- */\n\n/** Outcome tallies for one aggregate row. */\nexport interface AggregateCounts {\n pass: number;\n fail: number;\n error: number;\n needsReview: number;\n skipped: number;\n}\n\n/**\n * One artifact, or one eval within it, summed over every trace in the batch.\n *\n * Keyed by type and *name* rather than by path, so the same skill evaluated\n * across several projects is one rate — which is the fleet question this\n * report exists to answer. The paths it resolved from are kept in `artifacts`\n * so a reader can see when a row spans more than one.\n */\nexport interface AggregateRow {\n /** `<type>:<name>` for an artifact row, plus `::<evalName>` for an eval row. */\n key: string;\n artifactName: string;\n artifactType: ArtifactType;\n /** Distinct resolved paths this row was aggregated from, sorted. */\n artifacts: string[];\n /** Absent on artifact rows. */\n evalName?: string;\n /** The grader kinds seen for this row, sorted. Usually exactly one. */\n graders?: string[];\n /** Traces that produced at least one result for this row. */\n traces: number;\n /** Eval results contributing to this row. */\n total: number;\n counts: AggregateCounts;\n /** `total` minus `skipped` — the pass-rate denominator. */\n graded: number;\n /**\n * `pass / graded`, or `null` when nothing was graded. Skipped results are\n * excluded deliberately: a check that never armed is not evidence either\n * way, and folding it in would let an artifact nobody invoked report a\n * perfect score.\n */\n passRate: number | null;\n /** Traces where this row produced a `fail` or an `error`, in batch order. */\n failingTraces: string[];\n /** Traces where it produced a `needs-review`, in batch order. */\n reviewTraces: string[];\n /** Distinct skip reasons, sorted — an exhausted budget surfaces here. */\n skipReasons: string[];\n}\n\n/** One trace's place in the batch. */\nexport interface BatchTraceEntry {\n file: string;\n sessionId?: string;\n /**\n * Set when the trace could not be evaluated at all — unreadable, or not a\n * recognized format. One bad file in a corpus of fifty must not cost the\n * other forty-nine, so this is an entry rather than an aborted run.\n */\n error?: string;\n /** Absent when `error` is set. */\n summary?: RunSummary;\n warnings: string[];\n exitCode: 0 | 1;\n /** Judge turns spent on that trace. */\n turns: number;\n durationMs: number;\n}\n\nexport interface BatchSummary extends RunSummary {\n /** Traces selected, including any that errored. */\n traces: number;\n tracesPassed: number;\n /** Traces that produced a report with exit code 1. */\n tracesFailed: number;\n /** Traces that could not be evaluated at all. */\n tracesErrored: number;\n}\n\nexport interface BatchReport {\n traces: BatchTraceEntry[];\n /** Per-artifact rates, sorted by `key`. */\n artifacts: AggregateRow[];\n /** Per-eval rates, sorted by `key`. */\n evals: AggregateRow[];\n /** Eval counts summed over every trace, plus the per-trace tallies. */\n summary: BatchSummary;\n /** Batch-level warnings: plugin loading and trace selection. */\n warnings: string[];\n /** `1` when any trace failed or errored. */\n exitCode: 0 | 1;\n /** Judge turns spent across the batch. The budget spans it, not each trace. */\n turns: number;\n durationMs: number;\n}\n"],"mappings":"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","names":["content"]}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
PROJECT_RULES_FILENAMES,
|
|
4
|
+
extractEvals,
|
|
5
|
+
listInTree,
|
|
6
|
+
safeRead,
|
|
7
|
+
segments
|
|
8
|
+
} from "./chunk-CFDUZRM2.js";
|
|
9
|
+
import {
|
|
10
|
+
TracevalsError
|
|
11
|
+
} from "./chunk-3R23C5DT.js";
|
|
12
|
+
|
|
13
|
+
// src/tracevals/artifacts/discover.ts
|
|
14
|
+
import {
|
|
15
|
+
basename,
|
|
16
|
+
dirname,
|
|
17
|
+
isAbsolute,
|
|
18
|
+
join,
|
|
19
|
+
relative,
|
|
20
|
+
resolve,
|
|
21
|
+
sep
|
|
22
|
+
} from "node:path";
|
|
23
|
+
import { stat } from "node:fs/promises";
|
|
24
|
+
import picomatch from "picomatch";
|
|
25
|
+
function excluder(globs, cwd) {
|
|
26
|
+
if (globs === void 0 || globs.length === 0) return () => false;
|
|
27
|
+
const isMatch = picomatch([...globs], { dot: true });
|
|
28
|
+
return (path) => {
|
|
29
|
+
const posix = resolve(path).split(sep).join("/");
|
|
30
|
+
const rel = relative(cwd, resolve(path)).split(sep).join("/");
|
|
31
|
+
const inside = rel !== "" && !rel.startsWith("../") && !isAbsolute(rel);
|
|
32
|
+
return isMatch(posix) || inside && isMatch(rel);
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
var SKILL_DIRS = ["skills", join("src", "skills"), join(".claude", "skills")];
|
|
36
|
+
var RULES_NAMES = new Set(PROJECT_RULES_FILENAMES);
|
|
37
|
+
function classify(path) {
|
|
38
|
+
const name = basename(path);
|
|
39
|
+
if (RULES_NAMES.has(name)) return "project-rules";
|
|
40
|
+
if (name === "SKILL.md") return "skill";
|
|
41
|
+
if (!name.endsWith(".md")) return void 0;
|
|
42
|
+
if (segments(dirname(path)).includes("commands")) return "slash-command";
|
|
43
|
+
if (segments(dirname(path)).at(-1) === "agents") return "agent";
|
|
44
|
+
return void 0;
|
|
45
|
+
}
|
|
46
|
+
function relativeSegments(anchor, path) {
|
|
47
|
+
const rel = relative(resolve(anchor), resolve(path));
|
|
48
|
+
if (rel === "" || rel.startsWith("..") || isAbsolute(rel)) return null;
|
|
49
|
+
return segments(rel);
|
|
50
|
+
}
|
|
51
|
+
function nameFor(type, path) {
|
|
52
|
+
if (type === "skill") return segments(dirname(path)).at(-1) ?? basename(path);
|
|
53
|
+
if (type === "agent" || type === "slash-command") {
|
|
54
|
+
return basename(path).replace(/\.md$/, "");
|
|
55
|
+
}
|
|
56
|
+
return basename(path);
|
|
57
|
+
}
|
|
58
|
+
function isRecognizedSlashCommand(path, anchor) {
|
|
59
|
+
const parts = relativeSegments(anchor, path);
|
|
60
|
+
if (parts === null) return false;
|
|
61
|
+
return parts.some(
|
|
62
|
+
(part, at) => part === "commands" && at >= 1 && parts[at - 1] === ".claude" && at < parts.length - 1
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
function isRecognizedSkill(path, anchor) {
|
|
66
|
+
const parts = relativeSegments(anchor, path);
|
|
67
|
+
if (parts === null) return false;
|
|
68
|
+
return SKILL_DIRS.some((dir) => {
|
|
69
|
+
const want = segments(dir);
|
|
70
|
+
const at = parts.length - want.length - 2;
|
|
71
|
+
if (at < 0) return false;
|
|
72
|
+
return want.every((part, i) => parts[at + i] === part);
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
function isRecognizedAgent(path, anchor) {
|
|
76
|
+
const parts = relativeSegments(anchor, path);
|
|
77
|
+
if (parts === null) return false;
|
|
78
|
+
if (parts.length === 2 && parts[0] === "agents") return true;
|
|
79
|
+
return parts.length >= 3 && parts.at(-3) === ".claude" && parts.at(-2) === "agents";
|
|
80
|
+
}
|
|
81
|
+
async function readOne(path, type, metadataFor) {
|
|
82
|
+
const content = await safeRead(path);
|
|
83
|
+
if (content === null) return null;
|
|
84
|
+
const artifact = {
|
|
85
|
+
name: nameFor(type, path),
|
|
86
|
+
type,
|
|
87
|
+
path,
|
|
88
|
+
content,
|
|
89
|
+
origin: "project"
|
|
90
|
+
};
|
|
91
|
+
try {
|
|
92
|
+
const extracted = extractEvals(artifact, await metadataFor?.(artifact));
|
|
93
|
+
if (extracted.errors.length > 0) {
|
|
94
|
+
const first = extracted.errors[0];
|
|
95
|
+
return {
|
|
96
|
+
artifact,
|
|
97
|
+
status: "invalid",
|
|
98
|
+
existingNames: [],
|
|
99
|
+
skip: false,
|
|
100
|
+
error: `metadata.evals is invalid: ${first?.message ?? "schema error"}`
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
return {
|
|
104
|
+
artifact,
|
|
105
|
+
status: "ok",
|
|
106
|
+
existingNames: extracted.evals.map((e) => e.id),
|
|
107
|
+
skip: extracted.skip
|
|
108
|
+
};
|
|
109
|
+
} catch (err) {
|
|
110
|
+
if (err instanceof TracevalsError) throw err;
|
|
111
|
+
return {
|
|
112
|
+
artifact,
|
|
113
|
+
status: "unreadable",
|
|
114
|
+
existingNames: [],
|
|
115
|
+
skip: false,
|
|
116
|
+
error: err instanceof Error ? err.message : String(err)
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
async function discoverArtifacts(options) {
|
|
121
|
+
const root = resolve(options.root);
|
|
122
|
+
const warnings = [];
|
|
123
|
+
const cwd = options.cwd ?? process.cwd();
|
|
124
|
+
const targets = options.paths?.length ? options.paths.map((p) => isAbsolute(p) ? p : resolve(cwd, p)) : [root];
|
|
125
|
+
const files = /* @__PURE__ */ new Map();
|
|
126
|
+
for (const target of targets) {
|
|
127
|
+
let info;
|
|
128
|
+
try {
|
|
129
|
+
info = await stat(target);
|
|
130
|
+
} catch {
|
|
131
|
+
throw new TracevalsError(`no such file or directory: ${target}`);
|
|
132
|
+
}
|
|
133
|
+
if (info.isFile()) {
|
|
134
|
+
files.set(target, classify(target) ?? "project-rules");
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
const found = await listInTree(target, (path) => {
|
|
138
|
+
const type = classify(path);
|
|
139
|
+
if (type === void 0) return false;
|
|
140
|
+
if (type === "skill") return isRecognizedSkill(path, target);
|
|
141
|
+
if (type === "agent") return isRecognizedAgent(path, target);
|
|
142
|
+
if (type === "slash-command") {
|
|
143
|
+
return isRecognizedSlashCommand(path, target);
|
|
144
|
+
}
|
|
145
|
+
return true;
|
|
146
|
+
});
|
|
147
|
+
for (const path of found) {
|
|
148
|
+
const type = classify(path);
|
|
149
|
+
if (type !== void 0) files.set(path, type);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
const excluded = excluder(options.exclude, cwd);
|
|
153
|
+
for (const path of [...files.keys()]) {
|
|
154
|
+
if (excluded(path)) files.delete(path);
|
|
155
|
+
}
|
|
156
|
+
const artifacts = [];
|
|
157
|
+
for (const [path, type] of files) {
|
|
158
|
+
const discovered = await readOne(path, type, options.metadataFor);
|
|
159
|
+
if (discovered === null) {
|
|
160
|
+
warnings.push(`could not read ${path}`);
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
if (discovered.status !== "ok") {
|
|
164
|
+
warnings.push(`${path}: ${discovered.error ?? discovered.status}`);
|
|
165
|
+
}
|
|
166
|
+
artifacts.push(discovered);
|
|
167
|
+
}
|
|
168
|
+
return { artifacts, warnings };
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export {
|
|
172
|
+
discoverArtifacts
|
|
173
|
+
};
|
|
174
|
+
//# sourceMappingURL=chunk-5HNHEUD5.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/tracevals/artifacts/discover.ts"],"sourcesContent":["/**\n * Static artifact discovery: find every skill, agent definition, and\n * project-rules file in a project without a trace.\n *\n * This is the inverse of `resolveArtifacts`, which maps names a trace *used*\n * onto files. Authoring (`manni tracevals fill`) needs the whole population instead,\n * including artifacts no session has exercised yet.\n *\n * Scope is deliberately the project only — never the user's `~/.claude` or the\n * plugin store, which hold third-party files that are not this project's to\n * edit.\n */\nimport {\n basename,\n dirname,\n isAbsolute,\n join,\n relative,\n resolve,\n sep,\n} from \"node:path\";\nimport { stat } from \"node:fs/promises\";\nimport picomatch from \"picomatch\";\nimport type { ExtractedMetadata } from \"../../meta/index.js\";\nimport { extractEvals } from \"../evals/extract.js\";\nimport { TracevalsError } from \"../types.js\";\nimport { listInTree, safeRead, segments } from \"./fs.js\";\nimport { PROJECT_RULES_FILENAMES } from \"./resolve.js\";\nimport type { ArtifactType, ResolvedArtifact } from \"./types.js\";\n\nexport interface DiscoveredArtifact {\n artifact: ResolvedArtifact;\n /**\n * `ok` — frontmatter parsed; `unreadable` — the YAML itself is malformed;\n * `invalid` — a `metadata.evals` block that fails the published schema.\n * Only `ok` artifacts are safe to propose against.\n */\n status: \"ok\" | \"unreadable\" | \"invalid\";\n /** Criterion names already declared, for dedupe and cache keying. */\n existingNames: string[];\n /** The artifact opted out via `metadata.evals.skip`. */\n skip: boolean;\n error?: string;\n}\n\nexport interface DiscoveryResult {\n artifacts: DiscoveredArtifact[];\n warnings: string[];\n}\n\nexport interface DiscoverOptions {\n /** Project root to scan. */\n root: string;\n /**\n * Explicit files or directories. A file is taken as an artifact regardless\n * of naming convention; a directory is scanned. Defaults to scanning `root`.\n * Relative entries resolve against `cwd`, like any CLI path argument.\n */\n paths?: string[];\n /** Base for relative `paths`; defaults to the process working directory. */\n cwd?: string;\n /**\n * `--exclude <glob>`, repeatable and never split on commas. Applied to\n * everything the scan found — an explicitly named file as much as a walked\n * directory — so one flag means the same thing however the artifact was\n * reached.\n */\n exclude?: string[];\n /**\n * The artifact's front matter as the run reads it: its own, or its own with a\n * manifest-supplied `metadata` block merged in (`evals/external.ts`). Without\n * it a relocated artifact looks empty here, and `fill` would propose evals it\n * already declares. Absent when no collection declares such a manifest.\n */\n metadataFor?: (artifact: ResolvedArtifact) => Promise<ExtractedMetadata | undefined>;\n}\n\n/**\n * Whether `path` is excluded by any of `globs`.\n *\n * Matched against the path as the user would write it, relative to `cwd` with\n * posix separators, so an exclude written against `test/fixtures` reads the\n * same on Windows as on Linux. A target outside `cwd` has no such spelling,\n * so the absolute posix path is offered too rather than a `../../` one no\n * glob would be written against.\n */\nfunction excluder(\n globs: readonly string[] | undefined,\n cwd: string,\n): (path: string) => boolean {\n if (globs === undefined || globs.length === 0) return () => false;\n const isMatch = picomatch([...globs], { dot: true });\n return (path) => {\n const posix = resolve(path).split(sep).join(\"/\");\n const rel = relative(cwd, resolve(path)).split(sep).join(\"/\");\n const inside = rel !== \"\" && !rel.startsWith(\"../\") && !isAbsolute(rel);\n return isMatch(posix) || (inside && isMatch(rel));\n };\n}\n\nconst SKILL_DIRS = [\"skills\", join(\"src\", \"skills\"), join(\".claude\", \"skills\")];\nconst RULES_NAMES = new Set<string>(PROJECT_RULES_FILENAMES);\n\n/** Path segments, separator-normalized, for convention matching. */\n/** Which artifact type a path represents by convention, if any. */\nfunction classify(path: string): ArtifactType | undefined {\n const name = basename(path);\n if (RULES_NAMES.has(name)) return \"project-rules\";\n if (name === \"SKILL.md\") return \"skill\";\n if (!name.endsWith(\".md\")) return undefined;\n // Candidates only — `isRecognizedSlashCommand` and `isRecognizedAgent`\n // decide whether this particular directory is one the resolver would ever\n // look in. Commands nest (an organizing subdirectory does not rename the\n // command), so any `commands` ancestor is a candidate; agents do not.\n if (segments(dirname(path)).includes(\"commands\")) return \"slash-command\";\n if (segments(dirname(path)).at(-1) === \"agents\") return \"agent\";\n return undefined;\n}\n\n/**\n * `path` split into segments relative to `anchor`, or null when it escapes.\n * Slicing by prefix length silently mangles paths that are not underneath the\n * anchor, so relative() does the work.\n */\nfunction relativeSegments(anchor: string, path: string): string[] | null {\n const rel = relative(resolve(anchor), resolve(path));\n if (rel === \"\" || rel.startsWith(\"..\") || isAbsolute(rel)) return null;\n return segments(rel);\n}\n\n/**\n * Artifact name as the rest of the system refers to it: a skill by its\n * directory, an agent by its filename stem, project rules by filename.\n */\nfunction nameFor(type: ArtifactType, path: string): string {\n if (type === \"skill\") return segments(dirname(path)).at(-1) ?? basename(path);\n // A slash command is referenced by its filename stem with the leading slash\n // stripped, and an organizing subdirectory does not enter the name — so\n // `commands/release/tag.md` is `tag`, the same string the trace records.\n if (type === \"agent\" || type === \"slash-command\") {\n return basename(path).replace(/\\.md$/, \"\");\n }\n return basename(path);\n}\n\n/**\n * Slash commands live in `.claude/commands/` at any depth — the only place\n * `resolveArtifacts` looks for a project command, so discovery and resolution\n * agree on the same population.\n *\n * Deliberately narrow, for the reason `isRecognizedAgent` is: `fill` writes by\n * default, and matching any directory named `commands` would classify a docs\n * page about a CLI as an instruction artifact and edit it.\n */\nfunction isRecognizedSlashCommand(path: string, anchor: string): boolean {\n const parts = relativeSegments(anchor, path);\n if (parts === null) return false;\n // Any `.claude/commands/` pair, not the first segment that happens to be\n // named \"commands\": a project with a top-level `commands/` directory would\n // otherwise fail the `at >= 1` test and have its real command files rejected.\n return parts.some(\n (part, at) =>\n part === \"commands\" &&\n at >= 1 &&\n parts[at - 1] === \".claude\" &&\n at < parts.length - 1,\n );\n}\n\n/** A skill file must sit as `<skillsDir>/<skillName>/SKILL.md`. */\nfunction isRecognizedSkill(path: string, anchor: string): boolean {\n const parts = relativeSegments(anchor, path);\n if (parts === null) return false;\n return SKILL_DIRS.some((dir) => {\n const want = segments(dir);\n const at = parts.length - want.length - 2;\n if (at < 0) return false;\n return want.every((part, i) => parts[at + i] === part);\n });\n}\n\n/**\n * Agent definitions live in `.claude/agents/` at any depth, or in `agents/`\n * directly at the project root — the two places `resolveArtifacts` looks.\n *\n * This is deliberately narrow. Matching any directory named `agents` would\n * classify ordinary prose in `docs/agents/` as an agent definition, and `fill`\n * writes by default, so the mistake would edit unrelated files.\n */\nfunction isRecognizedAgent(path: string, anchor: string): boolean {\n const parts = relativeSegments(anchor, path);\n if (parts === null) return false;\n if (parts.length === 2 && parts[0] === \"agents\") return true;\n return (\n parts.length >= 3 &&\n parts.at(-3) === \".claude\" &&\n parts.at(-2) === \"agents\"\n );\n}\n\nasync function readOne(\n path: string,\n type: ArtifactType,\n metadataFor?: DiscoverOptions[\"metadataFor\"],\n): Promise<DiscoveredArtifact | null> {\n const content = await safeRead(path);\n if (content === null) return null;\n const artifact: ResolvedArtifact = {\n name: nameFor(type, path),\n type,\n path,\n content,\n origin: \"project\",\n };\n\n try {\n const extracted = extractEvals(artifact, await metadataFor?.(artifact));\n if (extracted.errors.length > 0) {\n const first = extracted.errors[0];\n return {\n artifact,\n status: \"invalid\",\n existingNames: [],\n skip: false,\n error: `metadata.evals is invalid: ${first?.message ?? \"schema error\"}`,\n };\n }\n return {\n artifact,\n status: \"ok\",\n existingNames: extracted.evals.map((e) => e.id),\n skip: extracted.skip,\n };\n } catch (err) {\n // A manifest that cannot be read, or an artifact two collections both keep\n // `metadata` for, is the run's problem rather than this artifact's.\n if (err instanceof TracevalsError) throw err;\n // docmeta throws on malformed YAML. A half-written artifact must not take\n // down a scan of the whole project.\n return {\n artifact,\n status: \"unreadable\",\n existingNames: [],\n skip: false,\n error: err instanceof Error ? err.message : String(err),\n };\n }\n}\n\nexport async function discoverArtifacts(\n options: DiscoverOptions,\n): Promise<DiscoveryResult> {\n const root = resolve(options.root);\n const warnings: string[] = [];\n const cwd = options.cwd ?? process.cwd();\n const targets = options.paths?.length\n ? options.paths.map((p) => (isAbsolute(p) ? p : resolve(cwd, p)))\n : [root];\n\n const files = new Map<string, ArtifactType>();\n\n for (const target of targets) {\n let info;\n try {\n info = await stat(target);\n } catch {\n throw new TracevalsError(`no such file or directory: ${target}`);\n }\n\n if (info.isFile()) {\n // An explicit file is taken at its word; convention only supplies the\n // type, and an unrecognized name is treated as project rules.\n files.set(target, classify(target) ?? \"project-rules\");\n continue;\n }\n\n // Convention is anchored at the directory being scanned, so\n // `fill packages/api` treats that package as its own project rather than\n // silently finding nothing.\n const found = await listInTree(target, (path) => {\n const type = classify(path);\n if (type === undefined) return false;\n if (type === \"skill\") return isRecognizedSkill(path, target);\n if (type === \"agent\") return isRecognizedAgent(path, target);\n if (type === \"slash-command\") {\n return isRecognizedSlashCommand(path, target);\n }\n return true;\n });\n for (const path of found) {\n const type = classify(path);\n if (type !== undefined) files.set(path, type);\n }\n }\n\n // After the walk rather than inside it: a `--exclude` is about the artifact\n // set, and applying it to directory names during the walk would make\n // `**/fixtures/**` and `**/fixtures` mean different things.\n const excluded = excluder(options.exclude, cwd);\n for (const path of [...files.keys()]) {\n if (excluded(path)) files.delete(path);\n }\n\n const artifacts: DiscoveredArtifact[] = [];\n for (const [path, type] of files) {\n const discovered = await readOne(path, type, options.metadataFor);\n if (discovered === null) {\n warnings.push(`could not read ${path}`);\n continue;\n }\n if (discovered.status !== \"ok\") {\n warnings.push(`${path}: ${discovered.error ?? discovered.status}`);\n }\n artifacts.push(discovered);\n }\n return { artifacts, warnings };\n}\n"],"mappings":"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","names":[]}
|