brepjs-verify 0.26.0 → 0.28.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/CHANGELOG.md CHANGED
@@ -1,5 +1,32 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.28.0](https://github.com/andymai/brepjs/compare/brepjs-verify-v0.27.0...brepjs-verify-v0.28.0) (2026-06-19)
4
+
5
+
6
+ ### Features
7
+
8
+ * **brepjs-verify:** fan out the eval across sharded CI runners ([#1503](https://github.com/andymai/brepjs/issues/1503)) ([8eb1f69](https://github.com/andymai/brepjs/commit/8eb1f690588e418e4b884e1294ba91b679667d61))
9
+
10
+
11
+ ### Bug Fixes
12
+
13
+ * **brepjs-verify:** chamfer-fragility + unwrap/TS2322 SKILL.md rules ([#1506](https://github.com/andymai/brepjs/issues/1506)) ([0f24004](https://github.com/andymai/brepjs/commit/0f24004983effbd85545372b167f472eed35ec96))
14
+
15
+ ## [0.27.0](https://github.com/andymai/brepjs/compare/brepjs-verify-v0.26.0...brepjs-verify-v0.27.0) (2026-06-19)
16
+
17
+
18
+ ### Features
19
+
20
+ * **brepjs-verify:** enrich experiment traces with request + code ([#1502](https://github.com/andymai/brepjs/issues/1502)) ([5b9141f](https://github.com/andymai/brepjs/commit/5b9141f8107794683d3ef7945466e326132d4a6d))
21
+ * **brepjs-verify:** Langfuse observability foundation ([#1498](https://github.com/andymai/brepjs/issues/1498)) ([78c81f3](https://github.com/andymai/brepjs/commit/78c81f3b3e4890cbd3abc2474fcc5b7e4926bd3e))
22
+ * **brepjs-verify:** looped skill eval + Langfuse v5 telemetry ([#1494](https://github.com/andymai/brepjs/issues/1494)) ([06a7e7c](https://github.com/andymai/brepjs/commit/06a7e7c6e882fe795122141dce1e4c04933f3fdc))
23
+
24
+
25
+ ### Bug Fixes
26
+
27
+ * **brepjs-verify:** SKILL.md import rule — eval-driven self-heal ([#1501](https://github.com/andymai/brepjs/issues/1501)) ([550d2ba](https://github.com/andymai/brepjs/commit/550d2ba08849a1692ff237d1a79a28afd88b9053))
28
+ * **brepjs-verify:** three deferred Langfuse-eval follow-ups ([#1500](https://github.com/andymai/brepjs/issues/1500)) ([90b63bd](https://github.com/andymai/brepjs/commit/90b63bd6a2f277db6ec634f2bbecadf1379f4e4d))
29
+
3
30
  ## [0.26.0](https://github.com/andymai/brepjs/compare/brepjs-verify-v0.25.1...brepjs-verify-v0.26.0) (2026-06-18)
4
31
 
5
32
 
package/README.md CHANGED
@@ -110,9 +110,13 @@ Few-shot examples live under `skill/examples/<name>.brep.ts`, each with a `<name
110
110
 
111
111
  `npm run eval` (`bench/run.ts`) replays every `skill/examples/*.brep.ts` with a sibling `*.expected.json` through the public `runPart` runtime, compares measured volume/area/validity/shape-type against the recorded baseline within each file's tolerance (default 0.5%), prints a PASS/FAIL scorecard, and exits non-zero on any regression. It is deterministic (no LLM or API key) so it runs in CI as the package's regression net. Refresh a baseline by re-recording the example's `*.expected.json` after an intentional geometry change.
112
112
 
113
+ ### Manual skill eval (`/eval-skill`)
114
+
115
+ The fast iteration loop — run on your Claude subscription, no API key, no billing. The `/eval-skill` slash command drives the same `bench/prompts.ts` corpus through the current Claude Code session: Claude authors each part from the **deployed `SKILL.md`**, runs `verify --check --snapshot`, judges the rendered snapshots against each prompt's rubric, emits the two-signal scorecard, and proposes SKILL.md fixes from the failures. This is the recommended way to answer "did my SKILL.md edit help?" — see `.claude/commands/eval-skill.md`.
116
+
113
117
  ### Live eval (`npm run eval:live`)
114
118
 
115
- The measurement flywheel: sends ~18 natural-language part prompts (`bench/prompts.ts`) to a real model (using the **deployed `SKILL.md` as the system prompt**, so it measures the actual skill), then verifies each generated part two ways:
119
+ The **automated / isolated** counterpart to `/eval-skill` opt-in and billed. An SDK harness sends the `bench/prompts.ts` prompts to a model with the **deployed `SKILL.md` as the system prompt** (so it measures the skill _in isolation_, not inside a Claude Code session), then verifies each generated part two ways:
116
120
 
117
121
  - **Auto (objective):** `runPart --check` → valid solid + any pinned dims within tolerance.
118
122
  - **Judge (intent):** a multimodal Claude call looks at the rendered iso/front/top/right snapshots and decides whether the part matches the request + rubric.
@@ -297,6 +297,70 @@ async function appendRunRecord(filePath, record) {
297
297
  await (0, node_fs_promises.appendFile)(filePath, `${JSON.stringify(record)}\n`);
298
298
  }
299
299
  //#endregion
300
+ //#region src/mcp/telemetry.ts
301
+ /**
302
+ * Optional Langfuse tracing for MCP `run_program` executions — a strictly best-effort side channel,
303
+ * exactly like the `BREPJS_RUN_RECORD_PATH` JSONL record. The @langfuse / OpenTelemetry packages are
304
+ * NOT runtime dependencies of the shipped MCP: they are *dynamically* imported, and only when the
305
+ * `LANGFUSE_*` keys are present. So the server never hard-requires telemetry — no keys (or the deps
306
+ * simply absent in a published install) → {@link traceRun} is a silent no-op. It never throws.
307
+ */
308
+ var tracerPromise;
309
+ async function initTracer() {
310
+ if (!process.env["LANGFUSE_PUBLIC_KEY"] || !process.env["LANGFUSE_SECRET_KEY"]) return null;
311
+ try {
312
+ const [{ NodeSDK }, otel, { startActiveObservation }, { LangfuseClient }] = await Promise.all([
313
+ import("@opentelemetry/sdk-node"),
314
+ import("@langfuse/otel"),
315
+ import("@langfuse/tracing"),
316
+ import("@langfuse/client")
317
+ ]);
318
+ const processor = new otel.LangfuseSpanProcessor();
319
+ new NodeSDK({ spanProcessors: [processor] }).start();
320
+ const client = new LangfuseClient();
321
+ return { trace: async (code, result) => {
322
+ const rec = buildRunRecord(code, result);
323
+ startActiveObservation("run_program", (obs) => {
324
+ obs.update({
325
+ input: { code },
326
+ output: {
327
+ outcome: rec.outcome,
328
+ ok: rec.ok,
329
+ measurements: rec.measurements
330
+ },
331
+ metadata: {
332
+ source: "mcp",
333
+ resultHash: rec.resultHash
334
+ }
335
+ });
336
+ try {
337
+ client.score.create({
338
+ traceId: obs.traceId,
339
+ name: "ok",
340
+ value: rec.ok ? 1 : 0,
341
+ dataType: "NUMERIC"
342
+ });
343
+ } catch {}
344
+ });
345
+ await Promise.allSettled([processor.forceFlush(), client.flush()]);
346
+ } };
347
+ } catch (e) {
348
+ console.warn(`langfuse: MCP tracing unavailable (${e.message.split("\n")[0]})`);
349
+ return null;
350
+ }
351
+ }
352
+ /**
353
+ * Trace one `run_program` execution to Langfuse (best-effort; no-op without the `LANGFUSE_*` keys or
354
+ * the optional telemetry deps). Never throws — callers fire-and-forget it as a side channel.
355
+ */
356
+ async function traceRun(code, result) {
357
+ try {
358
+ await (await (tracerPromise ??= initTracer()))?.trace(code, result);
359
+ } catch (e) {
360
+ console.warn(`langfuse: traceRun failed (${e.message.split("\n")[0]})`);
361
+ }
362
+ }
363
+ //#endregion
300
364
  //#region src/mcp/tools.ts
301
365
  /** JSON Schema for `run_program` input (the low-level Server API takes plain JSON Schema). */
302
366
  var RUN_PROGRAM_INPUT_SCHEMA = {
@@ -330,6 +394,7 @@ async function runProgramTool(args) {
330
394
  if (recordPath) appendRunRecord(recordPath, buildRunRecord(args.code, result)).catch((err) => {
331
395
  console.warn(`run-record append failed (BREPJS_RUN_RECORD_PATH=${recordPath}):`, err);
332
396
  });
397
+ traceRun(args.code, result);
333
398
  if (result.outcome === "completed") {
334
399
  const payload = {
335
400
  outcome: "completed",
@@ -297,6 +297,70 @@ async function appendRunRecord(filePath, record) {
297
297
  await appendFile(filePath, `${JSON.stringify(record)}\n`);
298
298
  }
299
299
  //#endregion
300
+ //#region src/mcp/telemetry.ts
301
+ /**
302
+ * Optional Langfuse tracing for MCP `run_program` executions — a strictly best-effort side channel,
303
+ * exactly like the `BREPJS_RUN_RECORD_PATH` JSONL record. The @langfuse / OpenTelemetry packages are
304
+ * NOT runtime dependencies of the shipped MCP: they are *dynamically* imported, and only when the
305
+ * `LANGFUSE_*` keys are present. So the server never hard-requires telemetry — no keys (or the deps
306
+ * simply absent in a published install) → {@link traceRun} is a silent no-op. It never throws.
307
+ */
308
+ var tracerPromise;
309
+ async function initTracer() {
310
+ if (!process.env["LANGFUSE_PUBLIC_KEY"] || !process.env["LANGFUSE_SECRET_KEY"]) return null;
311
+ try {
312
+ const [{ NodeSDK }, otel, { startActiveObservation }, { LangfuseClient }] = await Promise.all([
313
+ import("@opentelemetry/sdk-node"),
314
+ import("@langfuse/otel"),
315
+ import("@langfuse/tracing"),
316
+ import("@langfuse/client")
317
+ ]);
318
+ const processor = new otel.LangfuseSpanProcessor();
319
+ new NodeSDK({ spanProcessors: [processor] }).start();
320
+ const client = new LangfuseClient();
321
+ return { trace: async (code, result) => {
322
+ const rec = buildRunRecord(code, result);
323
+ startActiveObservation("run_program", (obs) => {
324
+ obs.update({
325
+ input: { code },
326
+ output: {
327
+ outcome: rec.outcome,
328
+ ok: rec.ok,
329
+ measurements: rec.measurements
330
+ },
331
+ metadata: {
332
+ source: "mcp",
333
+ resultHash: rec.resultHash
334
+ }
335
+ });
336
+ try {
337
+ client.score.create({
338
+ traceId: obs.traceId,
339
+ name: "ok",
340
+ value: rec.ok ? 1 : 0,
341
+ dataType: "NUMERIC"
342
+ });
343
+ } catch {}
344
+ });
345
+ await Promise.allSettled([processor.forceFlush(), client.flush()]);
346
+ } };
347
+ } catch (e) {
348
+ console.warn(`langfuse: MCP tracing unavailable (${e.message.split("\n")[0]})`);
349
+ return null;
350
+ }
351
+ }
352
+ /**
353
+ * Trace one `run_program` execution to Langfuse (best-effort; no-op without the `LANGFUSE_*` keys or
354
+ * the optional telemetry deps). Never throws — callers fire-and-forget it as a side channel.
355
+ */
356
+ async function traceRun(code, result) {
357
+ try {
358
+ await (await (tracerPromise ??= initTracer()))?.trace(code, result);
359
+ } catch (e) {
360
+ console.warn(`langfuse: traceRun failed (${e.message.split("\n")[0]})`);
361
+ }
362
+ }
363
+ //#endregion
300
364
  //#region src/mcp/tools.ts
301
365
  /** JSON Schema for `run_program` input (the low-level Server API takes plain JSON Schema). */
302
366
  var RUN_PROGRAM_INPUT_SCHEMA = {
@@ -330,6 +394,7 @@ async function runProgramTool(args) {
330
394
  if (recordPath) appendRunRecord(recordPath, buildRunRecord(args.code, result)).catch((err) => {
331
395
  console.warn(`run-record append failed (BREPJS_RUN_RECORD_PATH=${recordPath}):`, err);
332
396
  });
397
+ traceRun(args.code, result);
333
398
  if (result.outcome === "completed") {
334
399
  const payload = {
335
400
  outcome: "completed",
@@ -0,0 +1,6 @@
1
+ import { RunProgramResult } from '../sandbox/runProgram.js';
2
+ /**
3
+ * Trace one `run_program` execution to Langfuse (best-effort; no-op without the `LANGFUSE_*` keys or
4
+ * the optional telemetry deps). Never throws — callers fire-and-forget it as a side channel.
5
+ */
6
+ export declare function traceRun(code: string, result: RunProgramResult): Promise<void>;
@@ -69,6 +69,27 @@ export declare function killActiveSandboxes(signal?: NodeJS.Signals): void;
69
69
  export declare function installSandboxShutdownHandlers(): void;
70
70
  /** Execute `code` (an agent-authored `.brep.ts` module) in an isolated, resource-bounded child. */
71
71
  export declare function runProgram(code: string, opts?: RunProgramOptions): Promise<RunProgramResult>;
72
+ /** Outcome of a sandboxed `--check --step` run. `completed` carries the report; `stepPath` is set
73
+ * only when a valid solid produced a STEP. */
74
+ export type RunProgramWithStepResult = {
75
+ outcome: 'completed';
76
+ report: SerializedReport;
77
+ stepPath?: string;
78
+ } | {
79
+ outcome: 'timeout';
80
+ timeoutMs: number;
81
+ } | {
82
+ outcome: 'crashed';
83
+ exitCode: number | null;
84
+ detail: string;
85
+ };
86
+ /**
87
+ * Execute `code` with `verify --check --step` in ONE bounded child process: a single spawn yields
88
+ * both the `auto`-signal report (parsed from stdout) and the STEP the judge renders — one kernel
89
+ * boot, not the two a `runProgram` + `exportProgram` pair would cost. The caller owns `stepOutPath`;
90
+ * only the temp program dir is cleaned up.
91
+ */
92
+ export declare function runProgramWithStep(code: string, stepOutPath: string, opts?: RunProgramOptions): Promise<RunProgramWithStepResult>;
72
93
  /**
73
94
  * Execute `code` and export artifacts to `outDir`. Artifacts persist in `outDir` (the caller owns
74
95
  * it); only the temp program directory is cleaned up. Returns the written paths and any errors.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "brepjs-verify",
3
- "version": "0.26.0",
3
+ "version": "0.28.0",
4
4
  "description": "Agent skill + verify/preview tooling for authoring parametric brepjs CAD code",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -47,6 +47,9 @@
47
47
  "test": "vitest run",
48
48
  "eval": "tsx bench/run.ts",
49
49
  "eval:live": "tsx bench/live.ts",
50
+ "eval:push": "tsx bench/pushScorecard.ts",
51
+ "eval:combine": "tsx bench/combineShards.ts",
52
+ "eval:dataset:sync": "tsx bench/syncDataset.ts",
50
53
  "smoke": "node dist/cli/main.js verify tests/fixtures/validBox.brep.ts | node -e \"const r=JSON.parse(require('fs').readFileSync(0,'utf8'));if(r.ok!==true||!(r.measurements.volume>0))process.exit(1)\"",
51
54
  "smoke:standalone": "node scripts/smokeStandalone.mjs",
52
55
  "prepack": "npm run build"
@@ -63,6 +66,10 @@
63
66
  },
64
67
  "devDependencies": {
65
68
  "@anthropic-ai/sdk": "0.102.0",
69
+ "@langfuse/client": "5.5.3",
70
+ "@langfuse/otel": "5.5.3",
71
+ "@langfuse/tracing": "5.5.3",
72
+ "@opentelemetry/sdk-node": "0.219.0",
66
73
  "@react-three/drei": "^10.7.7",
67
74
  "@react-three/fiber": "^9.6.1",
68
75
  "@types/node": "25.9.2",
@@ -736,10 +736,10 @@ import {
736
736
  measureDistance
737
737
  } from 'brepjs';
738
738
 
739
- measureVolume(solid); // number
740
- measureArea(face); // number
741
- measureLength(edge); // number
742
- measureDistance(shape1, shape2); // number
739
+ measureVolume(solid); // Result<number> — unwrap()
740
+ measureArea(face); // Result<number> — unwrap()
741
+ measureLength(edge); // Result<number> — unwrap()
742
+ measureDistance(shape1, shape2); // Result<number> — unwrap()
743
743
  ```
744
744
 
745
745
  Functional measurement API: