@feniix/bridgekit 0.16.0 → 0.17.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
@@ -6,6 +6,29 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.17.0] - 2026-10-06
10
+
11
+ ### Changed
12
+
13
+ - `@types/node` moves from 25.x to 22.x to match the `>=22.19.0` engines floor,
14
+ which also matches pi (`@earendil-works/pi-*`); APIs added after Node 22 now
15
+ fail to typecheck. The Node floor itself is unchanged.
16
+ - Dependabot version updates are disabled (`.github/dependabot.yml` removed);
17
+ dependency bumps are now made manually. CI's `npm audit` gate is unchanged.
18
+
19
+ ## [0.16.1] - 2026-10-06
20
+
21
+ ### Fixed
22
+
23
+ - Validation errors report correct field names under TypeBox 1.3+, which the
24
+ `typebox ^1.1.31` range already resolves to on fresh installs. TypeBox 1.3
25
+ escapes JSON Pointer paths per RFC 6901, so a property named `a/b` surfaced as
26
+ `a~1b` (and `a~b` as `a~0b`); fields are now decoded back to the literal key.
27
+ - `additionalProperties` violations are reported once per key again. TypeBox 1.3
28
+ adds a per-key sub-schema error beside the summary, which duplicated fields and
29
+ let a losing union branch's closed-object errors slip past phantom suppression.
30
+ Both TypeBox 1.1 and 1.3 now produce identical validation errors.
31
+
9
32
  ## [0.16.0] - 2026-10-06
10
33
 
11
34
  ### Added
@@ -1,18 +1,30 @@
1
1
  import { Check, Errors, Pointer } from "typebox/value";
2
2
  import { assertPortableOutputSchema } from "./output-schema.js";
3
3
  const ROOT_FIELD = "(root)";
4
+ /**
5
+ * Decode one RFC 6901 JSON Pointer segment (`~1` → `/`, `~0` → `~`).
6
+ *
7
+ * TypeBox 1.3+ escapes property names in `instancePath` and `schemaPath`
8
+ * (`"a/b"` → `/a~1b`); TypeBox 1.1 did not. The package range spans both, so
9
+ * every lookup tries the raw segment first and the decoded form second.
10
+ */
11
+ function decodePointerSegment(segment) {
12
+ return segment.replaceAll("~1", "/").replaceAll("~0", "~");
13
+ }
4
14
  function fieldFromPath(instancePath) {
5
- return instancePath.split("/").filter(Boolean).at(-1) ?? ROOT_FIELD;
15
+ const last = instancePath.split("/").filter(Boolean).at(-1);
16
+ return last === undefined ? ROOT_FIELD : decodePointerSegment(last);
6
17
  }
7
18
  /**
8
19
  * Resolve the leaf property name from a TypeBox error by walking
9
20
  * `error.schemaPath` rather than `error.instancePath`.
10
21
  *
11
- * `instancePath` does not escape `/` inside property names (TypeBox does not
12
- * follow JSON Pointer RFC 6901's `~1` encoding), so a schema with BOTH a
13
- * slash-named property `"a/b"` AND a nested path `a.b` produces the same
22
+ * Before 1.3, TypeBox's `instancePath` did not escape `/` inside property
23
+ * names (no JSON Pointer RFC 6901 `~1` encoding), so a schema with BOTH a
24
+ * slash-named property `"a/b"` AND a nested path `a.b` produced the same
14
25
  * `instancePath: "/a/b"` for either failure — the two cases are
15
- * indistinguishable from the data path alone. `schemaPath` carries explicit
26
+ * indistinguishable from the data path alone. TypeBox 1.3+ escapes both paths
27
+ * (`/a~1b`, `#/properties/a~1b`); the walker accepts either form. `schemaPath` carries explicit
16
28
  * `/properties/` markers per nesting level, so the two cases become
17
29
  * structurally distinct:
18
30
  *
@@ -87,13 +99,17 @@ function walkSegments(node, segments, i, lastField) {
87
99
  return walkSegments(obj.items, segments, i + 1, lastField);
88
100
  }
89
101
  // `properties` command: subsequent segments form the property name (joined
90
- // by `/` to recover slash-named keys). Consume by greedy longest-prefix
91
- // match against the parent's actual `properties` keys, then descend.
102
+ // by `/` to recover unescaped slash-named keys from TypeBox < 1.3). Consume
103
+ // by greedy longest-prefix match against the parent's actual `properties`
104
+ // keys, then descend. The raw candidate wins; the RFC 6901-decoded form
105
+ // covers TypeBox 1.3+, which emits `a~1b` as a single segment.
92
106
  if (head === "properties" && obj.properties && typeof obj.properties === "object") {
93
107
  const props = obj.properties;
94
108
  for (let j = segments.length; j > i + 1; j--) {
95
- const candidate = segments.slice(i + 1, j).join("/");
96
- if (Object.hasOwn(props, candidate)) {
109
+ const raw = segments.slice(i + 1, j).join("/");
110
+ for (const candidate of new Set([raw, decodePointerSegment(raw)])) {
111
+ if (!Object.hasOwn(props, candidate))
112
+ continue;
97
113
  const child = walkSegments(props[candidate], segments, j, candidate);
98
114
  if (child !== undefined)
99
115
  return child;
@@ -151,12 +167,15 @@ function resolveSchemaAtPath(schema, instancePath) {
151
167
  if (!current || typeof current !== "object")
152
168
  return undefined;
153
169
  const obj = current;
154
- if (obj.properties && typeof obj.properties === "object" && segment in obj.properties) {
155
- const next = obj.properties[segment];
156
- if (next === undefined)
157
- return undefined;
158
- current = next;
159
- continue;
170
+ if (obj.properties && typeof obj.properties === "object") {
171
+ const key = segment in obj.properties ? segment : decodePointerSegment(segment);
172
+ if (key in obj.properties) {
173
+ const next = obj.properties[key];
174
+ if (next === undefined)
175
+ return undefined;
176
+ current = next;
177
+ continue;
178
+ }
160
179
  }
161
180
  if (obj.items !== undefined && /^\d+$/.test(segment)) {
162
181
  if (Array.isArray(obj.items)) {
@@ -357,7 +376,8 @@ function suppressSiblingErrorsUnderUnion(schema, value, errors) {
357
376
  const remainder = error.instancePath.slice(prefix.length);
358
377
  if (remainder.includes("/"))
359
378
  continue; // only direct discriminator props
360
- const losingValues = resolution.losingDiscriminators.get(remainder);
379
+ const losingValues = resolution.losingDiscriminators.get(remainder) ??
380
+ resolution.losingDiscriminators.get(decodePointerSegment(remainder));
361
381
  if (!losingValues)
362
382
  continue;
363
383
  if (error.keyword === "const" && losingValues.has(error.params.allowedValue))
@@ -370,6 +390,21 @@ function suppressSiblingErrorsUnderUnion(schema, value, errors) {
370
390
  return true;
371
391
  });
372
392
  }
393
+ /**
394
+ * TypeBox 1.3+ reports each additional property twice: once as the
395
+ * `additionalProperties` summary at the object (which `expandTypeBoxError`
396
+ * expands per key) and once per key as the failure of the `additionalProperties`
397
+ * sub-schema itself (`keyword: "boolean"` for `false`, or e.g. `"type"` for a
398
+ * schema value), with `schemaPath` ending at that keyword. TypeBox 1.1 emitted
399
+ * only the summary. Dropping the per-key duplicates keeps one error per key and
400
+ * lets union phantom suppression, which reasons about the summary, stay
401
+ * authoritative. A trailing segment after a `properties` command is a property
402
+ * literally named `additionalProperties`, not the keyword.
403
+ */
404
+ function isAdditionalPropertiesSubschemaError(error) {
405
+ const segments = error.schemaPath.split("/");
406
+ return segments.at(-1) === "additionalProperties" && segments.at(-2) !== "properties";
407
+ }
373
408
  function expandTypeBoxError(schema, error) {
374
409
  // Read offending property names from TypeBox's structured `params` rather
375
410
  // than parsing the message string. Two payoffs: locale independence (the
@@ -428,9 +463,7 @@ export function validatePortableToolArgs(tool, args) {
428
463
  // satisfy ONE branch, not all of them). Then dedupe the survivors by
429
464
  // (field, message) using JSON.stringify so the key can't collide regardless
430
465
  // of what characters field or message contain.
431
- const rawErrors = suppressSiblingErrorsUnderUnion(tool.parameters, args, [
432
- ...Errors(tool.parameters, args),
433
- ]);
466
+ const rawErrors = suppressSiblingErrorsUnderUnion(tool.parameters, args, [...Errors(tool.parameters, args)].filter((error) => !isAdditionalPropertiesSubschemaError(error)));
434
467
  const seen = new Set();
435
468
  const errors = rawErrors
436
469
  .flatMap((error) => expandTypeBoxError(tool.parameters, error))
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@feniix/bridgekit",
3
- "version": "0.16.0",
3
+ "version": "0.17.0",
4
4
  "description": "BridgeKit defines TypeBox-backed tools once and adapts them to pi, MCP, and other hosts.",
5
5
  "keywords": [
6
6
  "pi",
@@ -73,11 +73,11 @@
73
73
  "typebox": "^1.1.31"
74
74
  },
75
75
  "devDependencies": {
76
+ "@biomejs/biome": "2.5.15",
76
77
  "@modelcontextprotocol/client": "^2.3.1",
77
78
  "@modelcontextprotocol/sdk": "^1.30.1",
78
- "@biomejs/biome": "2.5.4",
79
79
  "@total-typescript/shoehorn": "^0.1.2",
80
- "@types/node": "^25.9.1",
80
+ "@types/node": "^22.20.5",
81
81
  "typescript": "^6.0.3"
82
82
  },
83
83
  "license": "MIT"