@feniix/bridgekit 0.8.2 → 0.8.3

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
@@ -4,7 +4,21 @@ All notable changes to `@feniix/bridgekit` are documented here. The format
4
4
  follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the project
5
5
  adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
- ## [0.8.2] - Unreleased
7
+ ## [0.8.3] - Unreleased
8
+
9
+ ### Fixed
10
+
11
+ - `PortableValidationError.field` now preserves the full property name when
12
+ it contains `/`. TypeBox does not implement JSON Pointer RFC 6901's `~1`
13
+ escape, so a property named `a/b` previously produced `field: "b"` for
14
+ wrong-type / `const` / `enum` errors (the string-split fallback dropped
15
+ the prefix). The fix walks the actual schema with a greedy longest-prefix
16
+ match against `properties` keys and falls back to the prior behavior for
17
+ paths the schema doesn't model. The `required` and `additionalProperties`
18
+ paths were already correct via structured `params` and are unchanged.
19
+ Resolves [#36](https://github.com/feniix/bridgekit/issues/36).
20
+
21
+ ## [0.8.2] - 2026-05-26
8
22
 
9
23
  ### Fixed
10
24
 
@@ -3,6 +3,82 @@ const ROOT_FIELD = "(root)";
3
3
  function fieldFromPath(instancePath) {
4
4
  return instancePath.split("/").filter(Boolean).at(-1) ?? ROOT_FIELD;
5
5
  }
6
+ /**
7
+ * Resolve the leaf field name of an instancePath against the actual schema.
8
+ *
9
+ * TypeBox does not escape `/` inside property names when building
10
+ * `instancePath` (it does not follow JSON Pointer RFC 6901's `~1` encoding),
11
+ * so a property literally named `a/b` produces `instancePath: "/a/b"`. The
12
+ * string-split fallback (`fieldFromPath`) would then surface `"b"` and lose
13
+ * the prefix. This helper walks the schema greedily — at each object node it
14
+ * matches the longest prefix of remaining path segments that names a real
15
+ * property key, descending into the matched sub-schema — and returns the
16
+ * actual leaf key. Array nodes consume a single numeric segment via `items`.
17
+ *
18
+ * Returns `undefined` if the walk gets stuck (the schema does not model the
19
+ * data path — e.g. `additionalProperties` content). The caller then falls
20
+ * back to the string-split `fieldFromPath`, preserving prior behavior.
21
+ *
22
+ * The `required` and `additionalProperties` callers already read structured
23
+ * `params` and are unaffected; this helper covers the `const` / `enum` /
24
+ * default keyword branches.
25
+ */
26
+ function fieldFromSchemaWalk(schema, instancePath) {
27
+ if (instancePath === "")
28
+ return undefined;
29
+ const segments = instancePath.split("/").filter(Boolean);
30
+ if (segments.length === 0)
31
+ return undefined;
32
+ let current = schema;
33
+ let lastKey;
34
+ let i = 0;
35
+ while (i < segments.length) {
36
+ if (!current || typeof current !== "object")
37
+ return undefined;
38
+ const node = current;
39
+ // Array descent: numeric segment against an `items` schema. Tuple-form
40
+ // `items` (array) indexes positionally; schema-form descends uniformly.
41
+ if (node.items !== undefined && /^\d+$/.test(segments[i])) {
42
+ const idx = Number(segments[i]);
43
+ if (Array.isArray(node.items)) {
44
+ if (idx >= node.items.length)
45
+ return undefined;
46
+ current = node.items[idx];
47
+ }
48
+ else {
49
+ current = node.items;
50
+ }
51
+ lastKey = segments[i];
52
+ i++;
53
+ continue;
54
+ }
55
+ // Object descent: greedy longest-prefix match against `properties` keys.
56
+ // This is what lets `a/b` resolve to a single key when the schema
57
+ // declares it, even though the instancePath segments it as ["a", "b"].
58
+ if (node.properties && typeof node.properties === "object") {
59
+ const props = node.properties;
60
+ let matched = -1;
61
+ for (let j = segments.length; j > i; j--) {
62
+ const candidate = segments.slice(i, j).join("/");
63
+ if (Object.hasOwn(props, candidate)) {
64
+ matched = j;
65
+ lastKey = candidate;
66
+ current = props[candidate];
67
+ break;
68
+ }
69
+ }
70
+ if (matched === -1)
71
+ return undefined;
72
+ i = matched;
73
+ continue;
74
+ }
75
+ return undefined;
76
+ }
77
+ return lastKey;
78
+ }
79
+ function fieldFromError(schema, error) {
80
+ return fieldFromSchemaWalk(schema, error.instancePath) ?? fieldFromPath(error.instancePath);
81
+ }
6
82
  function readDiscriminatorValues(propSchema) {
7
83
  // Extract the allowed values from a discriminator-eligible prop schema.
8
84
  // Recognizes three shapes:
@@ -253,7 +329,7 @@ function suppressSiblingErrorsUnderUnion(schema, value, errors) {
253
329
  return true;
254
330
  });
255
331
  }
256
- function expandTypeBoxError(error) {
332
+ function expandTypeBoxError(schema, error) {
257
333
  // Read offending property names from TypeBox's structured `params` rather
258
334
  // than parsing the message string. Two payoffs: locale independence (the
259
335
  // helper works the same after `Locale.Set("de_DE")`), and faithful
@@ -284,7 +360,7 @@ function expandTypeBoxError(error) {
284
360
  if (error.keyword === "const") {
285
361
  return [
286
362
  {
287
- field: fieldFromPath(error.instancePath),
363
+ field: fieldFromError(schema, error),
288
364
  message: `must equal ${JSON.stringify(error.params.allowedValue)}`,
289
365
  },
290
366
  ];
@@ -293,12 +369,12 @@ function expandTypeBoxError(error) {
293
369
  const allowed = error.params.allowedValues ?? [];
294
370
  return [
295
371
  {
296
- field: fieldFromPath(error.instancePath),
372
+ field: fieldFromError(schema, error),
297
373
  message: allowed.length > 0 ? `must equal one of ${allowed.map((v) => JSON.stringify(v)).join(", ")}` : error.message,
298
374
  },
299
375
  ];
300
376
  }
301
- return [{ field: fieldFromPath(error.instancePath), message: error.message }];
377
+ return [{ field: fieldFromError(schema, error), message: error.message }];
302
378
  }
303
379
  export function validatePortableToolArgs(tool, args) {
304
380
  if (Check(tool.parameters, args)) {
@@ -316,7 +392,7 @@ export function validatePortableToolArgs(tool, args) {
316
392
  ]);
317
393
  const seen = new Set();
318
394
  const errors = rawErrors
319
- .flatMap((error) => expandTypeBoxError(error))
395
+ .flatMap((error) => expandTypeBoxError(tool.parameters, error))
320
396
  .filter(({ field, message }) => {
321
397
  const key = JSON.stringify([field, message]);
322
398
  if (seen.has(key))
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@feniix/bridgekit",
3
- "version": "0.8.2",
3
+ "version": "0.8.3",
4
4
  "description": "BridgeKit defines TypeBox-backed tools once and adapts them to pi, MCP, and other hosts.",
5
5
  "keywords": [
6
6
  "pi",