dsh-session-eval 0.3.0 → 0.4.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/CHANGELOG.md CHANGED
@@ -1,3 +1,19 @@
1
+ ## 0.4.1 — 2026-11-05
2
+
3
+ Peer-range compatibility release (no behavior change).
4
+
5
+ - `@deepseek-ai/dsh-*` peers widened to `^0.1.5-rc.3 || ^0.2.0 || 0.2.2-rc.1` and `@deepseek-ai/cordis` to `^4.0.2`, ahead of the DSH 0.2.2 release train.
6
+ - Verified against `@deepseek-ai/*@0.2.2-rc.1` in a scratch install: full suite 60/60 green. Notably `SESSION_FORMAT_VERSION` moved 3 → 4 upstream; a v3-header archive still round-trips through the 0.2.2 `buildArchiveFromLog` (historical formats are translated), so existing exported bundles keep grading identically.
7
+
8
+ ## 0.4.0 — 2026-11-05
9
+
10
+ Evidence-handoff closure (#8): `/eval --report <bundle-or-archive.zip>`.
11
+
12
+ - Grades straight from an exported dsh-session-export artifact — bundle ZIPs (nested archive.zip) and raw /archive ZIPs — no session id to copy, and it works after the session is gone from the store.
13
+ - The artifact's JSONL is the canonical event log, so `--report` matches `/eval --id` exactly; a round-trip test builds bundles with the real dsh-session-export writer and asserts identical metrics and cards.
14
+ - Zero runtime dependencies kept: a minimal ZIP reader (central directory + raw DEFLATE via node:zlib, CRC32-verified) mirrors the export writer; dsh-session-export is a devDependency only, for the interop test.
15
+ - README.md / README.zh.md document the recall → report → eval closure with a worked example.
16
+
1
17
  ## 0.3.0 — 2026-10-23
2
18
 
3
19
  Feature release: configurable grade bands (#1) and the history time window (#2).
package/README.md CHANGED
@@ -61,6 +61,19 @@ Notes:
61
61
 
62
62
  All three read through the same trusted `ctx.sessionQuery` seam, so any persistence backend (JSONL or SQLite) works without touching raw artifacts.
63
63
 
64
+ ### Evidence-handoff closure (#8)
65
+
66
+ `/eval --report` grades straight from an artifact `dsh-session-export` already wrote — no session id to copy, and it works after the session is gone from the store:
67
+
68
+ ```
69
+ recall "resume template font"
70
+ → session session-ca62e005-… · report /proj/dsh-transcripts/bundle-ca62e005-20261104-120000.zip
71
+
72
+ /eval --report /proj/dsh-transcripts/bundle-ca62e005-20261104-120000.zip
73
+ ```
74
+
75
+ Bundle ZIPs (the nested `archive.zip` raw JSONL) and raw `/archive` ZIPs are both accepted. The JSONL inside is the canonical event log — the same bytes `readSession` returns — so `--report` grades **identically** to `/eval --id` (verified by a round-trip test against the real `dsh-session-export` writer). `--id` and `--report` are mutually exclusive.
76
+
64
77
  ## Contributing
65
78
 
66
79
  - **Local dev**: `npm ci && npm run typecheck && npm test && npm run bundle` (Node 20 or 22).
@@ -85,6 +98,7 @@ Both are useful; they don't compete. Benchmarks test what you *predicted*; retro
85
98
  |---|---|
86
99
  | `/eval` | Grade card for the current session |
87
100
  | `/eval --id <sessionId>` | Grade a specific session |
101
+ | `/eval --report <bundle-or-archive.zip>` | Grade from an exported artifact (works after the session is gone from the store) |
88
102
  | `/eval --json` | Machine-readable metrics + card |
89
103
  | `/eval-diff <beforeId> <afterId>` | Regression comparison, noise-tolerant |
90
104
  | `/eval-diff … --json` | Machine-readable trend report |
package/README.zh.md CHANGED
@@ -61,6 +61,19 @@ Notes:
61
61
 
62
62
  三者都通过同一个可信的 `ctx.sessionQuery` 缝读取,JSONL / SQLite 等任何持久化后端均可使用。
63
63
 
64
+ ### 证据联动闭环(#8)
65
+
66
+ `/eval --report` 直接从 `dsh-session-export` 已写出的产物评分——不用抄会话 id,会话从存储中删除后依然可评:
67
+
68
+ ```
69
+ recall "resume template font"
70
+ → session session-ca62e005-… · report /proj/dsh-transcripts/bundle-ca62e005-20261104-120000.zip
71
+
72
+ /eval --report /proj/dsh-transcripts/bundle-ca62e005-20261104-120000.zip
73
+ ```
74
+
75
+ 接受 bundle ZIP(内嵌 `archive.zip` 原始 JSONL)与裸 `/archive` ZIP。里面的 JSONL 就是权威事件日志——与 `readSession` 返回的是同一份字节——所以 `--report` 与 `/eval --id` **评分完全一致**(用真实 `dsh-session-export` 写入器做过往返测试)。`--id` 与 `--report` 互斥。
76
+
64
77
  ## 贡献
65
78
 
66
79
  - **本地开发**:`npm ci && npm run typecheck && npm test && npm run bundle`(Node 20 或 22)。
package/lib/index.d.ts CHANGED
@@ -114,13 +114,18 @@ declare function gradeSession(m: SessionMetrics, bands?: Partial<GradeBands>): G
114
114
  /** Compare two sessions dimension by dimension. */
115
115
  declare function compareSessions(before: SessionMetrics, after: SessionMetrics): TrendReport;
116
116
  //#endregion
117
+ //#region src/artifact.d.ts
118
+ /** Filesystem seam (injectable for tests). */
119
+ type ReadFile = (path: string) => Buffer;
120
+ //#endregion
117
121
  //#region src/evalCommand.d.ts
118
- declare const EVAL_USAGE = "Usage: /eval [--id <sessionId>] [--json]";
122
+ declare const EVAL_USAGE = "Usage: /eval [--id <sessionId>] [--report <bundle-or-archive.zip>] [--json]";
119
123
  declare const EVAL_DIFF_USAGE = "Usage: /eval-diff <beforeSessionId> <afterSessionId> [--json]";
120
124
  declare function id8(sessionId: string): string;
121
125
  /** Args for /eval. */
122
126
  interface EvalArgs {
123
127
  readonly sessionId?: string;
128
+ readonly report?: string;
124
129
  readonly json?: boolean;
125
130
  }
126
131
  /** Args for /eval-diff. */
@@ -142,8 +147,8 @@ interface QuerySeam {
142
147
  events: readonly unknown[];
143
148
  }>;
144
149
  }
145
- /** Execute /eval against the session-query seam. */
146
- declare function executeEval(ctx: Context, invocation: CommandInvocation, seam?: QuerySeam, bands?: Partial<GradeBands>): Promise<CommandResult>;
150
+ /** Execute /eval against the session-query seam or an exported artifact. */
151
+ declare function executeEval(ctx: Context, invocation: CommandInvocation, seam?: QuerySeam, bands?: Partial<GradeBands>, readFile?: ReadFile): Promise<CommandResult>;
147
152
  /** Execute /eval-diff against the session-query seam. */
148
153
  declare function executeEvalDiff(ctx: Context, invocation: CommandInvocation, seam?: QuerySeam): Promise<CommandResult>;
149
154
  //#endregion
package/lib/index.js CHANGED
@@ -1,4 +1,6 @@
1
1
  import { SessionId } from "@deepseek-ai/dsh-session";
2
+ import { crc32, inflateRawSync } from "node:zlib";
3
+ import { readFileSync } from "node:fs";
2
4
  import { writeFile } from "node:fs/promises";
3
5
  import { resolve } from "node:path";
4
6
  //#region src/metrics.ts
@@ -85,6 +87,137 @@ function readBool(source, key) {
85
87
  return typeof value === "boolean" ? value : void 0;
86
88
  }
87
89
  //#endregion
90
+ //#region src/zip.ts
91
+ /**
92
+ * Minimal single-file ZIP reader (methods 0 and 8, no external dependency).
93
+ *
94
+ * The mirror of dsh-session-export's `util/zip.ts` writer: parses the
95
+ * End-of-Central-Directory record, walks the central directory, and inflates
96
+ * each entry with raw DEFLATE (`node:zlib.inflateRawSync`, ZIP method 8) or
97
+ * copies stored entries (method 0). Data descriptors are not needed because
98
+ * the writer emits none; CRC32 is verified per entry. Only the features the
99
+ * toolchain's own archives use are supported — a ZIP using extensions beyond
100
+ * that is rejected with a clear error, not misread.
101
+ *
102
+ * @module dsh-session-eval/zip
103
+ */
104
+ const EOCD_SIG = 101010256;
105
+ const CD_SIG = 33639248;
106
+ const LFH_SIG = 67324752;
107
+ /** Locate the EOCD record: normally the last 22 bytes; scan back over a comment if present. */
108
+ function findEocd(buf) {
109
+ const limit = Math.max(0, buf.length - 22 - 65536);
110
+ for (let i = buf.length - 22; i >= limit; i -= 1) if (buf.readUInt32LE(i) === EOCD_SIG) return i;
111
+ return -1;
112
+ }
113
+ /**
114
+ * Read every entry of a ZIP archive. Throws `Error` with a plain message on
115
+ * anything that is not a supported archive (truncated, unknown method, or a
116
+ * CRC mismatch) — callers surface the message to the user.
117
+ */
118
+ function readZipEntries(buf) {
119
+ const eocd = findEocd(buf);
120
+ if (eocd < 0) throw new Error("not a ZIP archive (no end-of-central-directory record)");
121
+ const entryCount = buf.readUInt16LE(eocd + 10);
122
+ const cdOffset = buf.readUInt32LE(eocd + 16);
123
+ const entries = [];
124
+ let p = cdOffset;
125
+ for (let i = 0; i < entryCount; i += 1) {
126
+ if (p + 46 > buf.length || buf.readUInt32LE(p) !== CD_SIG) throw new Error(`corrupt ZIP: central directory entry ${i} is unreadable`);
127
+ const method = buf.readUInt16LE(p + 10);
128
+ const crcExpected = buf.readUInt32LE(p + 16);
129
+ const compressedSize = buf.readUInt32LE(p + 20);
130
+ const rawSize = buf.readUInt32LE(p + 24);
131
+ const nameLength = buf.readUInt16LE(p + 28);
132
+ const extraLength = buf.readUInt16LE(p + 30);
133
+ const commentLength = buf.readUInt16LE(p + 32);
134
+ const localOffset = buf.readUInt32LE(p + 42);
135
+ const name = buf.toString("utf8", p + 46, p + 46 + nameLength);
136
+ if (buf.readUInt32LE(localOffset) !== LFH_SIG) throw new Error(`corrupt ZIP: entry "${name}" has no local file header`);
137
+ const lfhNameLength = buf.readUInt16LE(localOffset + 26);
138
+ const lfhExtraLength = buf.readUInt16LE(localOffset + 28);
139
+ const dataStart = localOffset + 30 + lfhNameLength + lfhExtraLength;
140
+ const compressed = buf.subarray(dataStart, dataStart + compressedSize);
141
+ let raw;
142
+ if (method === 0) raw = Buffer.from(compressed);
143
+ else if (method === 8) raw = inflateRawSync(compressed);
144
+ else throw new Error(`unsupported ZIP compression method ${method} for entry "${name}"`);
145
+ if (raw.length !== rawSize) throw new Error(`corrupt ZIP: entry "${name}" decompressed to the wrong size`);
146
+ if (crc32(raw) >>> 0 !== crcExpected) throw new Error(`corrupt ZIP: CRC mismatch for entry "${name}"`);
147
+ entries.push({
148
+ name,
149
+ data: raw
150
+ });
151
+ p += 46 + nameLength + extraLength + commentLength;
152
+ }
153
+ return entries;
154
+ }
155
+ //#endregion
156
+ //#region src/artifact.ts
157
+ /**
158
+ * Evidence-handoff loader: grade a session straight from an artifact
159
+ * dsh-session-export already wrote (#8 closure).
160
+ *
161
+ * A bundle ZIP (`bundle-<id8>-<timestamp>.zip`) carries the raw JSONL
162
+ * archive as a nested `archive.zip`, and a `/archive` ZIP carries
163
+ * `session.jsonl` at its root. Both are the canonical event log — the same
164
+ * bytes `readSession` would return — so grading from an artifact matches
165
+ * grading through the seam exactly, and works after the session is gone
166
+ * from the store.
167
+ *
168
+ * @module dsh-session-eval/artifact
169
+ */
170
+ function jsonlEvents(data) {
171
+ const out = [];
172
+ for (const line of data.toString("utf8").split("\n")) {
173
+ const trimmed = line.trim();
174
+ if (trimmed === "") continue;
175
+ out.push(JSON.parse(trimmed));
176
+ }
177
+ return out;
178
+ }
179
+ function byName(entries) {
180
+ return new Map(entries.map((entry) => [entry.name, entry.data]));
181
+ }
182
+ /** Parse one zip level; returns null when this level holds no session.jsonl. */
183
+ function loadFromZipLevel(buf, origin) {
184
+ const files = byName(readZipEntries(buf));
185
+ const jsonl = files.get("session.jsonl");
186
+ if (jsonl === void 0) return null;
187
+ const manifestRaw = files.get("manifest.json");
188
+ const manifest = manifestRaw === void 0 ? {} : JSON.parse(manifestRaw.toString("utf8"));
189
+ return {
190
+ sessionId: typeof manifest.sessionId === "string" ? manifest.sessionId : "unknown-session",
191
+ events: jsonlEvents(jsonl),
192
+ origin
193
+ };
194
+ }
195
+ /**
196
+ * Load a gradeable session from an exported artifact path — a bundle ZIP
197
+ * (nested archive) or a raw `/archive` ZIP. Returns a plain error string on
198
+ * anything unreadable or unrecognized; never throws.
199
+ */
200
+ function loadReportArtifact(path, readFile) {
201
+ let buf;
202
+ try {
203
+ buf = readFile(path);
204
+ } catch (error) {
205
+ return `Could not read ${path}: ${error instanceof Error ? error.message : String(error)}`;
206
+ }
207
+ try {
208
+ const nested = readZipEntries(buf).find((entry) => entry.name === "archive.zip");
209
+ if (nested !== void 0) {
210
+ const loaded = loadFromZipLevel(nested.data, `${path} (archive.zip)`);
211
+ if (loaded !== null) return loaded;
212
+ }
213
+ const direct = loadFromZipLevel(buf, path);
214
+ if (direct !== null) return direct;
215
+ return `${path} is a ZIP but carries no session.jsonl (neither at the root nor inside archive.zip) — is it a transcript report rather than a bundle or archive?`;
216
+ } catch (error) {
217
+ return `Could not load artifact ${path}: ${error instanceof Error ? error.message : String(error)}`;
218
+ }
219
+ }
220
+ //#endregion
88
221
  //#region src/grade.ts
89
222
  /** The documented defaults; unchanged when no config is given. */
90
223
  const DEFAULT_GRADE_BANDS = {
@@ -248,7 +381,7 @@ function compareSessions(before, after) {
248
381
  const VERSION = "0.3.0";
249
382
  //#endregion
250
383
  //#region src/evalCommand.ts
251
- const EVAL_USAGE = "Usage: /eval [--id <sessionId>] [--json]";
384
+ const EVAL_USAGE = "Usage: /eval [--id <sessionId>] [--report <bundle-or-archive.zip>] [--json]";
252
385
  const EVAL_DIFF_USAGE = "Usage: /eval-diff <beforeSessionId> <afterSessionId> [--json]";
253
386
  function id8(sessionId) {
254
387
  return sessionId.length > 8 ? `${sessionId.slice(0, 8)}…` : sessionId;
@@ -271,6 +404,15 @@ function parseEvalArgs(rawInput) {
271
404
  i += 2;
272
405
  continue;
273
406
  }
407
+ if (token === "--report") {
408
+ const value = tokens[i + 1];
409
+ if (value === void 0 || value.startsWith("--")) return `--report requires an artifact path value.\n${EVAL_USAGE}`;
410
+ if (args.report !== void 0) return `--report may be given only once.\n${EVAL_USAGE}`;
411
+ if (args.sessionId !== void 0) return `--id and --report are mutually exclusive.\n${EVAL_USAGE}`;
412
+ args.report = value;
413
+ i += 2;
414
+ continue;
415
+ }
274
416
  if (token === "--json") {
275
417
  if (args.json === true) return `--json may be given only once.\n${EVAL_USAGE}`;
276
418
  args.json = true;
@@ -327,13 +469,36 @@ async function readEvents(ctx, seam, sessionIdRaw) {
327
469
  return `Could not read session ${id8(sessionIdRaw)}: ${error instanceof Error ? error.message : String(error)}`;
328
470
  }
329
471
  }
330
- /** Execute /eval against the session-query seam. */
331
- async function executeEval(ctx, invocation, seam = ctx.sessionQuery, bands = {}) {
472
+ /** Execute /eval against the session-query seam or an exported artifact. */
473
+ async function executeEval(ctx, invocation, seam = ctx.sessionQuery, bands = {}, readFile = (path) => readFileSync(path)) {
332
474
  const parsed = parseEvalArgs(invocation.rawInput);
333
475
  if (typeof parsed === "string") return {
334
476
  kind: "error",
335
477
  text: parsed
336
478
  };
479
+ if (parsed.report !== void 0) {
480
+ const artifact = loadReportArtifact(parsed.report, readFile);
481
+ if (typeof artifact === "string") return {
482
+ kind: "error",
483
+ text: artifact
484
+ };
485
+ const metrics = computeMetrics(artifact.sessionId, adaptEvents(artifact.events));
486
+ const card = gradeSession(metrics, bands);
487
+ const note = `from artifact ${artifact.origin}`;
488
+ if (parsed.json === true) return {
489
+ kind: "success",
490
+ text: JSON.stringify({
491
+ generator: "dsh-session-eval v0.3.0",
492
+ source: note,
493
+ metrics,
494
+ card
495
+ }, null, 2)
496
+ };
497
+ return {
498
+ kind: "success",
499
+ text: `${renderCard(card, "dsh-session-eval v0.3.0")}\n(${note})`
500
+ };
501
+ }
337
502
  const sessionIdRaw = parsed.sessionId ?? String(invocation.agent.session.id);
338
503
  const events = await readEvents(ctx, seam, sessionIdRaw);
339
504
  if (typeof events === "string") return {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "dsh-session-eval",
3
3
  "description": "Retrospective agent-session evaluation for DeepSeek Harness: deterministic grade cards from persisted session logs, cross-session regression diffs, era comparison — no benchmark runs, no LLM judges.",
4
- "version": "0.3.0",
4
+ "version": "0.4.1",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },
@@ -38,21 +38,22 @@
38
38
  "regression"
39
39
  ],
40
40
  "peerDependencies": {
41
+ "@deepseek-ai/cordis": "^4.0.2",
42
+ "@deepseek-ai/dsh-commands": "^0.1.5-rc.3 || ^0.2.0 || 0.2.2-rc.1",
43
+ "@deepseek-ai/dsh-llm": "^0.1.5-rc.3 || ^0.2.0 || 0.2.2-rc.1",
44
+ "@deepseek-ai/dsh-session": "^0.1.5-rc.3 || ^0.2.0 || 0.2.2-rc.1",
45
+ "@deepseek-ai/dsh-session-query": "^0.1.5-rc.3 || ^0.2.0 || 0.2.2-rc.1",
46
+ "@deepseek-ai/dsh-tools": "^0.1.5-rc.3 || ^0.2.0 || 0.2.2-rc.1"
47
+ },
48
+ "devDependencies": {
41
49
  "@deepseek-ai/cordis": "4.0.2",
42
50
  "@deepseek-ai/dsh-commands": "^0.1.5-rc.3",
43
- "@deepseek-ai/dsh-llm": "^0.1.5-rc.3",
44
51
  "@deepseek-ai/dsh-session": "^0.1.5-rc.3",
45
- "@deepseek-ai/dsh-session-query": "^0.1.5-rc.3",
46
- "@deepseek-ai/dsh-tools": "^0.1.5-rc.3"
47
- },
48
- "devDependencies": {
49
52
  "@types/node": "^26.2.0",
53
+ "dsh-session-export": "^1.9.0",
50
54
  "tsdown": "^0.22.14",
51
55
  "typescript": "^7.0.2",
52
- "vitest": "^4.1.11",
53
- "@deepseek-ai/cordis": "4.0.2",
54
- "@deepseek-ai/dsh-commands": "^0.1.5-rc.3",
55
- "@deepseek-ai/dsh-session": "^0.1.5-rc.3"
56
+ "vitest": "^4.1.11"
56
57
  },
57
58
  "scripts": {
58
59
  "bundle": "tsdown",