@remnic/bench 9.3.727 → 9.3.729

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 CHANGED
@@ -211,7 +211,8 @@ A dedicated benchmark suite for [`@remnic/coding-graph`](https://www.npmjs.com/p
211
211
  |---|---|
212
212
  | `fullIndexMs` | Wall time to index the entire fixture in one batch. |
213
213
  | `fullIndexLocsPerSecond` | Sustained LOC/s during full index (higher is better). |
214
- | `incrementalUpdateP50Ms` / `incrementalUpdateP95Ms` | Single-file re-ingest latency (p50/p95 over ≥20 iterations). |
214
+ | `incrementalUpdateP50Ms` / `incrementalUpdateP95Ms` | Single-file re-ingest latency for UNCHANGED content (the common-case no-op path; p50/p95 over ≥20 iterations). |
215
+ | `incrementalModifiedUpdateP50Ms` / `incrementalModifiedUpdateP95Ms` | Single-file re-ingest latency for MODIFIED content — the change-heavy path (edge deletion/creation, symbol re-resolution). Complementary to `incrementalUpdate` (#1688). |
215
216
  | `tracePathP95Ms` | `trace_path` (BFS, depth ≤ 5) p95. |
216
217
  | `searchGraphP95Ms` | `search_graph` name-pattern p95. |
217
218
  | `deadCodeMs` | Dead-code query wall time. |
@@ -225,18 +226,21 @@ The harness ships a **deterministic** synthetic repo generator (`generateSynthet
225
226
 
226
227
  The measured numbers live in [`baselines/coding-graph-baseline.json`](./baselines/coding-graph-baseline.json) — bench-owned, separate from the structural ratchets in `scripts/ratchet-baseline.json`. The regression gate (`checkCodingGraphRegression`) compares a report against the baseline with a generous tolerance (default 30%). It hard-fails on gross regression — a real failing step, not a warning (rule 50). Tightening the baseline is a deliberate PR act (mirrors `check-ratchets --update`).
227
228
 
229
+ The gate carries a **machine-fingerprint guard** (`compareMachineFingerprints`): when the report's machine class differs from the baseline's (arch/platform/Node major/cpuModel/cores), the comparison is *skipped* (`passed: true`, `skipped: true`, `machineMismatch` populated) rather than failed, so a cross-machine CI run does not false-positive on legitimate hardware variance (#1688). Regenerate the baseline on the target machine class for a real comparison.
230
+
228
231
  ### Measured numbers (first baseline)
229
232
 
230
233
  > Numbers below are from the **baseline JSON file** — this section is checked against it so prose can't drift from measurement. Run `remnic bench coding-graph` to reproduce.
231
234
 
232
235
  | Metric | Value | Machine |
233
236
  |---|---|---|
234
- | Full index | ~17.6 ms, ~115k LOC/s | Apple M2 Max, Node v22 |
235
- | Incremental update p95 | ~0.26 ms | Apple M2 Max |
236
- | trace_path p95 | ~0.14 ms | Apple M2 Max |
237
- | search_graph p95 | ~0.29 ms | Apple M2 Max |
238
- | dead_code | ~0.67 ms | Apple M2 Max |
239
- | DB size | ~2 KB/KLOC | Apple M2 Max |
237
+ | Full index | ~15 ms, ~131k LOC/s | Apple M2 Max, Node v22 |
238
+ | Incremental update p95 (idempotent) | ~0.24 ms | Apple M2 Max |
239
+ | Incremental modified update p95 (change-heavy) | ~0.96 ms | Apple M2 Max |
240
+ | trace_path p95 | ~0.13 ms | Apple M2 Max |
241
+ | search_graph p95 | ~0.18 ms | Apple M2 Max |
242
+ | dead_code | ~0.53 ms | Apple M2 Max |
243
+ | DB size | ~270 KB/KLOC | Apple M2 Max |
240
244
 
241
245
  These are **working targets on a small fixture (20 files, 200 symbols)**, NOT parity claims against codebase-memory-mcp's published numbers (28M LOC in 3 min, <1ms Cypher). Scale targets at 1M+ LOC are tracked as stretch goals — the harness will measure them when Tier-L fixtures are wired (issue #1557 PR2).
242
246
 
@@ -245,9 +249,14 @@ These are **working targets on a small fixture (20 files, 200 symbols)**, NOT pa
245
249
  ```typescript
246
250
  import { runCodingGraphBenchmark, checkCodingGraphRegression } from "@remnic/bench";
247
251
 
252
+ // Use the SAME fixture as the bundled baseline (20×10, density 0.3, seed
253
+ // 42) so the regression gate compares like-for-like. A custom fixture
254
+ // (different fileCount/symbolsPerFile/callDensity/seed/language) needs its
255
+ // OWN baseline — build one with buildBaselineFromReport(report, note) and
256
+ // commit it, otherwise checkCodingGraphRegression reports a fixture mismatch.
248
257
  const report = await runCodingGraphBenchmark({
249
- fixture: { fileCount: 1000, symbolsPerFile: 10, callDensity: 0.2 },
250
- iterations: 25,
258
+ fixture: { seed: 42, fileCount: 20, symbolsPerFile: 10, callDensity: 0.3, language: "typescript" },
259
+ iterations: 20,
251
260
  });
252
261
 
253
262
  const baseline = require("@remnic/bench/baselines/coding-graph-baseline.json");
@@ -256,4 +265,7 @@ if (!gate.passed) {
256
265
  console.error(gate.summary);
257
266
  process.exit(1);
258
267
  }
268
+ // gate.skipped === true when the report's machine fingerprint differs from
269
+ // the baseline's (different CPU/arch/Node major) — the comparison is skipped
270
+ // rather than failed so cross-machine CI does not false-positive (#1688).
259
271
  ```
@@ -1,5 +1,5 @@
1
1
  {
2
- "schemaVersion": 1,
2
+ "schemaVersion": 2,
3
3
  "machine": {
4
4
  "arch": "arm64",
5
5
  "platform": "darwin",
@@ -16,15 +16,17 @@
16
16
  "language": "typescript"
17
17
  },
18
18
  "metrics": {
19
- "fullIndexMs": 14.864042000000154,
20
- "fullIndexLocsPerSecond": 136302,
21
- "incrementalUpdateP50Ms": 0.19358299999998962,
22
- "incrementalUpdateP95Ms": 0.26237500000002,
23
- "tracePathP95Ms": 0.15750000000002728,
24
- "searchGraphP95Ms": 0.19700000000011642,
25
- "deadCodeMs": 2.29424999999992,
26
- "dbBytesPerKloc": 280636
19
+ "fullIndexMs": 15.452540999999655,
20
+ "fullIndexLocsPerSecond": 131111,
21
+ "incrementalUpdateP50Ms": 0.173167000000376,
22
+ "incrementalUpdateP95Ms": 0.23908399999982066,
23
+ "incrementalModifiedUpdateP50Ms": 0.6047909999997501,
24
+ "incrementalModifiedUpdateP95Ms": 0.9565000000002328,
25
+ "tracePathP95Ms": 0.13425000000006548,
26
+ "searchGraphP95Ms": 0.1764170000001286,
27
+ "deadCodeMs": 0.5299169999998412,
28
+ "dbBytesPerKloc": 270468
27
29
  },
28
- "createdAt": "2026-07-06T10:50:56.821Z",
29
- "note": "Initial baseline — first harness run with WAL-aware DB sizing (issue #1557 PR1)."
30
+ "createdAt": "2026-07-07T12:35:48.738Z",
31
+ "note": "Regenerated baseline — adds incrementalModifiedUpdate metric (#1688 item 1) + machine-fingerprint guard (#1688 item 2). WAL-aware DB sizing retained (#1557 PR1)."
30
32
  }
package/dist/index.d.ts CHANGED
@@ -3979,7 +3979,7 @@ interface WallMetric {
3979
3979
  * The regression gate compares only keys present in BOTH the report and
3980
3980
  * the baseline — new metrics are additive, never blocking.
3981
3981
  */
3982
- type CodingGraphMetricKey = "fullIndexMs" | "fullIndexLocsPerSecond" | "incrementalUpdateP50Ms" | "incrementalUpdateP95Ms" | "tracePathP95Ms" | "searchGraphP95Ms" | "deadCodeMs" | "dbBytesPerKloc";
3982
+ type CodingGraphMetricKey = "fullIndexMs" | "fullIndexLocsPerSecond" | "incrementalUpdateP50Ms" | "incrementalUpdateP95Ms" | "incrementalModifiedUpdateP50Ms" | "incrementalModifiedUpdateP95Ms" | "tracePathP95Ms" | "searchGraphP95Ms" | "deadCodeMs" | "dbBytesPerKloc";
3983
3983
  interface CodingGraphBenchReport {
3984
3984
  /** Schema version — bump when the report shape changes. */
3985
3985
  readonly schemaVersion: number;
@@ -3996,8 +3996,14 @@ interface CodingGraphBenchReport {
3996
3996
  readonly fullIndexMs: WallMetric;
3997
3997
  /** LOC/s sustained during full index (higher is better). */
3998
3998
  readonly fullIndexLocsPerSecond: number;
3999
- /** Incremental single-file update latency distribution. */
3999
+ /** Incremental single-file update latency distribution (idempotent
4000
+ * re-ingest of UNCHANGED content — the common-case no-op path). */
4000
4001
  readonly incrementalUpdate: MicroMetric;
4002
+ /** Incremental MODIFIED-content re-ingest latency distribution. A
4003
+ * complementary metric to `incrementalUpdate`: measures the change-heavy
4004
+ * path (edge deletion/creation, symbol re-resolution) rather than the
4005
+ * cache-hit no-op (#1688 item 1). */
4006
+ readonly incrementalModifiedUpdate: MicroMetric;
4001
4007
  /** trace_path (depth ≤ 5) latency distribution. */
4002
4008
  readonly tracePath: MicroMetric;
4003
4009
  /** search_graph name-pattern latency distribution. */
@@ -4046,6 +4052,24 @@ interface RegressionGateResult {
4046
4052
  readonly passed: boolean;
4047
4053
  readonly regressions: readonly RegressionMetricDetail[];
4048
4054
  readonly summary: string;
4055
+ /**
4056
+ * True when the gate SKIPPED the metric comparison because of a
4057
+ * non-fatal precondition (machine-fingerprint mismatch). `passed` is
4058
+ * also true in that case — a hardware variance must not fail CI (#1688
4059
+ * item 2). Callers that want to treat a skip as informational can check
4060
+ * this flag.
4061
+ */
4062
+ readonly skipped?: boolean;
4063
+ /**
4064
+ * Present when `skipped` is true due to a machine-fingerprint mismatch
4065
+ * between the report and the baseline. Lists the fingerprint fields that
4066
+ * differ so a human can decide whether the comparison is still meaningful.
4067
+ */
4068
+ readonly machineMismatch?: {
4069
+ readonly report: MachineFingerprint;
4070
+ readonly baseline: MachineFingerprint;
4071
+ readonly differingFields: readonly string[];
4072
+ };
4049
4073
  }
4050
4074
  interface CodingGraphBenchConfig {
4051
4075
  /** Fixture parameters. Defaults to a small, fast smoke fixture. */
@@ -4073,7 +4097,7 @@ declare const MIN_ITERATIONS = 20;
4073
4097
  /** Default regression tolerance (issue #1557: generous, e.g. 30%). */
4074
4098
  declare const DEFAULT_TOLERANCE_PERCENT = 30;
4075
4099
  /** Report schema version — bump when the shape changes. */
4076
- declare const CODING_GRAPH_BENCH_SCHEMA_VERSION = 1;
4100
+ declare const CODING_GRAPH_BENCH_SCHEMA_VERSION = 2;
4077
4101
 
4078
4102
  /**
4079
4103
  * Deterministic synthetic repo generator (issue #1557 design (a)).
@@ -4169,6 +4193,8 @@ declare const METRIC_DIRECTION: {
4169
4193
  fullIndexLocsPerSecond: "higher-is-better";
4170
4194
  incrementalUpdateP95Ms: "lower-is-better";
4171
4195
  incrementalUpdateP50Ms: "lower-is-better";
4196
+ incrementalModifiedUpdateP95Ms: "lower-is-better";
4197
+ incrementalModifiedUpdateP50Ms: "lower-is-better";
4172
4198
  tracePathP95Ms: "lower-is-better";
4173
4199
  searchGraphP95Ms: "lower-is-better";
4174
4200
  deadCodeMs: "lower-is-better";
package/dist/index.js CHANGED
@@ -36233,7 +36233,7 @@ var DEFAULT_10K_FIXTURE = {
36233
36233
  };
36234
36234
  var MIN_ITERATIONS = 20;
36235
36235
  var DEFAULT_TOLERANCE_PERCENT = 30;
36236
- var CODING_GRAPH_BENCH_SCHEMA_VERSION = 1;
36236
+ var CODING_GRAPH_BENCH_SCHEMA_VERSION = 2;
36237
36237
 
36238
36238
  // src/coding-graph/harness.ts
36239
36239
  function captureMachineFingerprint() {
@@ -36378,6 +36378,40 @@ async function runCodingGraphBenchmark(config = {}) {
36378
36378
  }
36379
36379
  const kloc = Math.max(1, repo.approximateLoc / 1e3);
36380
36380
  const dbBytesPerKloc = dbBytes / kloc;
36381
+ const modifiedSamples = [];
36382
+ for (let i = 0; i < iterations; i++) {
36383
+ const fileIdx = i % storeFiles.length;
36384
+ const original = storeFiles[fileIdx];
36385
+ if (!original) continue;
36386
+ const ownSyms = new Set(original.symbols.map((sym) => sym.qualifiedName));
36387
+ const churnName = `mod.benchChurnSymbol${i}`;
36388
+ const representativeEdges = (original.edges ?? []).filter((e) => !ownSyms.has(e.dstQualifiedName)).slice(0, 2).map((e) => ({ ...e, srcQualifiedName: churnName }));
36389
+ const modified = {
36390
+ ...original,
36391
+ contentHash: `${original.contentHash}-mod-${i}`,
36392
+ symbols: [
36393
+ {
36394
+ qualifiedName: churnName,
36395
+ name: `benchChurnSymbol${i}`,
36396
+ kind: "function",
36397
+ span: { startByte: 0, endByte: 0 }
36398
+ }
36399
+ ],
36400
+ edges: representativeEdges
36401
+ };
36402
+ const modResult = await timeAsync(
36403
+ () => store.upsertFileBatch([modified])
36404
+ );
36405
+ if (!modResult.result.ok) {
36406
+ throw new Error(`modified update failed: ${modResult.result.code}`);
36407
+ }
36408
+ modifiedSamples.push(modResult.ms);
36409
+ const restoreResult = await store.upsertFileBatch(storeFiles);
36410
+ if (!restoreResult.ok) {
36411
+ throw new Error(`restore failed: ${restoreResult.code}`);
36412
+ }
36413
+ sampleRss();
36414
+ }
36381
36415
  sampleRss();
36382
36416
  const peakRssBytes = peakRss;
36383
36417
  return {
@@ -36394,6 +36428,9 @@ async function runCodingGraphBenchmark(config = {}) {
36394
36428
  fullIndexMs: { ms: fullIndex.ms },
36395
36429
  fullIndexLocsPerSecond: Math.round(locsPerSecond),
36396
36430
  incrementalUpdate: computeMicroMetric(incrementalSamples),
36431
+ incrementalModifiedUpdate: computeMicroMetric(
36432
+ modifiedSamples.length > 0 ? modifiedSamples : [0]
36433
+ ),
36397
36434
  tracePath: computeMicroMetric(traceSamples.length > 0 ? traceSamples : [0]),
36398
36435
  searchGraph: computeMicroMetric(searchSamples),
36399
36436
  deadCodeMs: { ms: deadCode.ms },
@@ -36417,6 +36454,8 @@ var METRIC_DIRECTION = {
36417
36454
  fullIndexLocsPerSecond: "higher-is-better",
36418
36455
  incrementalUpdateP95Ms: "lower-is-better",
36419
36456
  incrementalUpdateP50Ms: "lower-is-better",
36457
+ incrementalModifiedUpdateP95Ms: "lower-is-better",
36458
+ incrementalModifiedUpdateP50Ms: "lower-is-better",
36420
36459
  tracePathP95Ms: "lower-is-better",
36421
36460
  searchGraphP95Ms: "lower-is-better",
36422
36461
  deadCodeMs: "lower-is-better",
@@ -36428,16 +36467,43 @@ function extractMetrics(report) {
36428
36467
  fullIndexLocsPerSecond: report.fullIndexLocsPerSecond,
36429
36468
  incrementalUpdateP50Ms: report.incrementalUpdate.p50,
36430
36469
  incrementalUpdateP95Ms: report.incrementalUpdate.p95,
36470
+ incrementalModifiedUpdateP50Ms: report.incrementalModifiedUpdate.p50,
36471
+ incrementalModifiedUpdateP95Ms: report.incrementalModifiedUpdate.p95,
36431
36472
  tracePathP95Ms: report.tracePath.p95,
36432
36473
  searchGraphP95Ms: report.searchGraph.p95,
36433
36474
  deadCodeMs: report.deadCodeMs.ms,
36434
36475
  dbBytesPerKloc: report.dbBytesPerKloc
36435
36476
  };
36436
36477
  }
36478
+ var NODE_MAJOR_CACHE = /* @__PURE__ */ new WeakMap();
36479
+ function nodeMajor(fp) {
36480
+ const cached = NODE_MAJOR_CACHE.get(fp);
36481
+ if (cached !== void 0) return cached;
36482
+ const major = fp.nodeVersion.replace(/^v/, "").split(".")[0] ?? fp.nodeVersion;
36483
+ NODE_MAJOR_CACHE.set(fp, major);
36484
+ return major;
36485
+ }
36486
+ function compareMachineFingerprints(report, baseline) {
36487
+ const differing = [];
36488
+ if (report.arch !== baseline.arch) differing.push("arch");
36489
+ if (report.platform !== baseline.platform) differing.push("platform");
36490
+ if (nodeMajor(report) !== nodeMajor(baseline)) differing.push("nodeVersion(major)");
36491
+ if (report.cpuModel !== null && baseline.cpuModel !== null && report.cpuModel !== baseline.cpuModel) {
36492
+ differing.push("cpuModel");
36493
+ }
36494
+ if (report.cpuCores !== baseline.cpuCores) differing.push("cpuCores");
36495
+ return { differingFields: differing };
36496
+ }
36497
+ function invalidFingerprintFields(fp) {
36498
+ const bad = [];
36499
+ if (typeof fp.arch !== "string") bad.push("arch");
36500
+ if (typeof fp.platform !== "string") bad.push("platform");
36501
+ if (typeof fp.nodeVersion !== "string") bad.push("nodeVersion");
36502
+ if (typeof fp.cpuCores !== "number" || !Number.isFinite(fp.cpuCores)) bad.push("cpuCores");
36503
+ if (fp.cpuModel !== null && typeof fp.cpuModel !== "string") bad.push("cpuModel");
36504
+ return bad;
36505
+ }
36437
36506
  function checkCodingGraphRegression(report, baseline, tolerancePercent = DEFAULT_TOLERANCE_PERCENT) {
36438
- const measured = extractMetrics(report);
36439
- const baselineMetrics = baseline.metrics;
36440
- const regressions = [];
36441
36507
  const reportFixture = report.fixture.config;
36442
36508
  const baselineFixture = baseline.fixtureConfig;
36443
36509
  const mismatchedKeys = Object.keys(baselineFixture).filter((key) => reportFixture[key] !== baselineFixture[key]);
@@ -36451,6 +36517,73 @@ function checkCodingGraphRegression(report, baseline, tolerancePercent = DEFAULT
36451
36517
  summary: `Fixture mismatch (${diffs}). Metrics are not comparable across different fixtures.`
36452
36518
  };
36453
36519
  }
36520
+ if (report.schemaVersion !== baseline.schemaVersion) {
36521
+ return {
36522
+ passed: false,
36523
+ regressions: [],
36524
+ summary: `Schema-version mismatch: report=${report.schemaVersion} baseline=${baseline.schemaVersion}. Metrics are not comparable across schema versions \u2014 regenerate the baseline (#1688).`
36525
+ };
36526
+ }
36527
+ const metricChecks = [
36528
+ ["incrementalUpdate.p50", report.incrementalUpdate?.p50],
36529
+ ["incrementalUpdate.p95", report.incrementalUpdate?.p95],
36530
+ ["incrementalModifiedUpdate.p50", report.incrementalModifiedUpdate?.p50],
36531
+ ["incrementalModifiedUpdate.p95", report.incrementalModifiedUpdate?.p95],
36532
+ ["tracePath.p95", report.tracePath?.p95],
36533
+ ["searchGraph.p95", report.searchGraph?.p95],
36534
+ ["fullIndexMs.ms", report.fullIndexMs?.ms],
36535
+ ["fullIndexLocsPerSecond", report.fullIndexLocsPerSecond],
36536
+ ["deadCodeMs.ms", report.deadCodeMs?.ms],
36537
+ ["dbBytesPerKloc", report.dbBytesPerKloc]
36538
+ ];
36539
+ const missingFields = metricChecks.filter(([, v]) => typeof v !== "number" || !Number.isFinite(v)).map(([k]) => k);
36540
+ if (missingFields.length > 0) {
36541
+ return {
36542
+ passed: false,
36543
+ regressions: [],
36544
+ summary: "Report claims schemaVersion " + report.schemaVersion + " but is missing required metric field(s): " + missingFields.join(", ") + " \u2014 the report is incomplete or corrupt. Regenerate it (#1688)."
36545
+ };
36546
+ }
36547
+ const badBaselineMetrics = Object.keys(METRIC_DIRECTION).filter((key) => {
36548
+ const v = baseline.metrics[key];
36549
+ return v != null && (typeof v !== "number" || !Number.isFinite(v));
36550
+ });
36551
+ if (badBaselineMetrics.length > 0) {
36552
+ return {
36553
+ passed: false,
36554
+ regressions: [],
36555
+ summary: "Baseline has a non-numeric required metric field(s): " + badBaselineMetrics.join(", ") + " \u2014 the baseline is corrupt. Regenerate it (#1688)."
36556
+ };
36557
+ }
36558
+ const reportFpBad = report.machine == null ? ["<missing>"] : invalidFingerprintFields(report.machine);
36559
+ const baselineFpBad = baseline.machine == null ? ["<missing>"] : invalidFingerprintFields(baseline.machine);
36560
+ if (reportFpBad.length > 0 || baselineFpBad.length > 0) {
36561
+ const which = [];
36562
+ if (reportFpBad.length > 0) which.push("report (" + reportFpBad.join(", ") + ")");
36563
+ if (baselineFpBad.length > 0) which.push("baseline (" + baselineFpBad.join(", ") + ")");
36564
+ return {
36565
+ passed: false,
36566
+ regressions: [],
36567
+ summary: "Report or baseline machine fingerprint is missing or has an invalid field type(s): " + which.join("; ") + " \u2014 the artifact is incomplete or corrupt. Regenerate it (#1688)."
36568
+ };
36569
+ }
36570
+ const mismatch = compareMachineFingerprints(report.machine, baseline.machine);
36571
+ if (mismatch.differingFields.length > 0) {
36572
+ return {
36573
+ passed: true,
36574
+ skipped: true,
36575
+ regressions: [],
36576
+ summary: "Machine-fingerprint mismatch \u2014 comparison skipped to avoid a false-positive hardware-variance failure. Differing fields: " + mismatch.differingFields.join(", ") + ". Regenerate the baseline on this machine for a real comparison (#1688).",
36577
+ machineMismatch: {
36578
+ report: report.machine,
36579
+ baseline: baseline.machine,
36580
+ differingFields: mismatch.differingFields
36581
+ }
36582
+ };
36583
+ }
36584
+ const measured = extractMetrics(report);
36585
+ const baselineMetrics = baseline.metrics;
36586
+ const regressions = [];
36454
36587
  for (const key of Object.keys(METRIC_DIRECTION)) {
36455
36588
  const baseVal = baselineMetrics[key];
36456
36589
  const measVal = measured[key];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remnic/bench",
3
- "version": "9.3.727",
3
+ "version": "9.3.729",
4
4
  "description": "Retrieval latency ladder benchmarks + CI regression gates for @remnic/core",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -36,8 +36,8 @@
36
36
  "dependencies": {
37
37
  "hyparquet": "^1.25.7",
38
38
  "yaml": "^2.4.2",
39
- "@remnic/coding-graph": "^9.3.727",
40
- "@remnic/core": "^9.3.727"
39
+ "@remnic/coding-graph": "^9.3.729",
40
+ "@remnic/core": "^9.3.729"
41
41
  },
42
42
  "devDependencies": {
43
43
  "tsup": "^8.5.1",