@pyreon/mcp 0.13.1 → 0.14.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/lib/index.js CHANGED
@@ -3,7 +3,9 @@ import Ajv from "ajv";
3
3
  import _addFormats from "ajv-formats";
4
4
  import { ZodOptional, z } from "zod";
5
5
  import process$1 from "node:process";
6
- import { detectReactPatterns, diagnoseError, generateContext, migrateReactCode } from "@pyreon/compiler";
6
+ import { auditTestEnvironment, detectPyreonPatterns, detectReactPatterns, diagnoseError, formatTestAudit, generateContext, migrateReactCode } from "@pyreon/compiler";
7
+ import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
8
+ import { dirname, join, resolve } from "node:path";
7
9
 
8
10
  //#region ../../../node_modules/.bun/zod@4.3.6/node_modules/zod/v3/helpers/util.js
9
11
  var util;
@@ -426,7 +428,8 @@ const handleResult = (ctx, result) => {
426
428
  success: false,
427
429
  get error() {
428
430
  if (this._error) return this._error;
429
- this._error = new ZodError$1(ctx.common.issues);
431
+ const error = new ZodError$1(ctx.common.issues);
432
+ this._error = error;
430
433
  return this._error;
431
434
  }
432
435
  };
@@ -2114,9 +2117,10 @@ var ZodObject$1 = class ZodObject$1 extends ZodType$1 {
2114
2117
  _getCached() {
2115
2118
  if (this._cached !== null) return this._cached;
2116
2119
  const shape = this._def.shape();
2120
+ const keys = util.objectKeys(shape);
2117
2121
  this._cached = {
2118
2122
  shape,
2119
- keys: util.objectKeys(shape)
2123
+ keys
2120
2124
  };
2121
2125
  return this._cached;
2122
2126
  }
@@ -4050,38 +4054,30 @@ const _encode = (_Err) => (schema, value, _ctx) => {
4050
4054
  const ctx = _ctx ? Object.assign(_ctx, { direction: "backward" }) : { direction: "backward" };
4051
4055
  return _parse(_Err)(schema, value, ctx);
4052
4056
  };
4053
- const encode$1 = /* @__PURE__ */ _encode($ZodRealError);
4054
4057
  const _decode = (_Err) => (schema, value, _ctx) => {
4055
4058
  return _parse(_Err)(schema, value, _ctx);
4056
4059
  };
4057
- const decode$1 = /* @__PURE__ */ _decode($ZodRealError);
4058
4060
  const _encodeAsync = (_Err) => async (schema, value, _ctx) => {
4059
4061
  const ctx = _ctx ? Object.assign(_ctx, { direction: "backward" }) : { direction: "backward" };
4060
4062
  return _parseAsync(_Err)(schema, value, ctx);
4061
4063
  };
4062
- const encodeAsync$1 = /* @__PURE__ */ _encodeAsync($ZodRealError);
4063
4064
  const _decodeAsync = (_Err) => async (schema, value, _ctx) => {
4064
4065
  return _parseAsync(_Err)(schema, value, _ctx);
4065
4066
  };
4066
- const decodeAsync$1 = /* @__PURE__ */ _decodeAsync($ZodRealError);
4067
4067
  const _safeEncode = (_Err) => (schema, value, _ctx) => {
4068
4068
  const ctx = _ctx ? Object.assign(_ctx, { direction: "backward" }) : { direction: "backward" };
4069
4069
  return _safeParse(_Err)(schema, value, ctx);
4070
4070
  };
4071
- const safeEncode$1 = /* @__PURE__ */ _safeEncode($ZodRealError);
4072
4071
  const _safeDecode = (_Err) => (schema, value, _ctx) => {
4073
4072
  return _safeParse(_Err)(schema, value, _ctx);
4074
4073
  };
4075
- const safeDecode$1 = /* @__PURE__ */ _safeDecode($ZodRealError);
4076
4074
  const _safeEncodeAsync = (_Err) => async (schema, value, _ctx) => {
4077
4075
  const ctx = _ctx ? Object.assign(_ctx, { direction: "backward" }) : { direction: "backward" };
4078
4076
  return _safeParseAsync(_Err)(schema, value, ctx);
4079
4077
  };
4080
- const safeEncodeAsync$1 = /* @__PURE__ */ _safeEncodeAsync($ZodRealError);
4081
4078
  const _safeDecodeAsync = (_Err) => async (schema, value, _ctx) => {
4082
4079
  return _safeParseAsync(_Err)(schema, value, _ctx);
4083
4080
  };
4084
- const safeDecodeAsync$1 = /* @__PURE__ */ _safeDecodeAsync($ZodRealError);
4085
4081
 
4086
4082
  //#endregion
4087
4083
  //#region ../../../node_modules/.bun/zod@4.3.6/node_modules/zod/v4/core/regexes.js
@@ -6326,32 +6322,6 @@ function _check(fn, params) {
6326
6322
  ch._zod.check = fn;
6327
6323
  return ch;
6328
6324
  }
6329
- /* @__NO_SIDE_EFFECTS__ */
6330
- function describe$2(description) {
6331
- const ch = new $ZodCheck({ check: "describe" });
6332
- ch._zod.onattach = [(inst) => {
6333
- const existing = globalRegistry.get(inst) ?? {};
6334
- globalRegistry.add(inst, {
6335
- ...existing,
6336
- description
6337
- });
6338
- }];
6339
- ch._zod.check = () => {};
6340
- return ch;
6341
- }
6342
- /* @__NO_SIDE_EFFECTS__ */
6343
- function meta$2(metadata) {
6344
- const ch = new $ZodCheck({ check: "meta" });
6345
- ch._zod.onattach = [(inst) => {
6346
- const existing = globalRegistry.get(inst) ?? {};
6347
- globalRegistry.add(inst, {
6348
- ...existing,
6349
- ...metadata
6350
- });
6351
- }];
6352
- ch._zod.check = () => {};
6353
- return ch;
6354
- }
6355
6325
 
6356
6326
  //#endregion
6357
6327
  //#region ../../../node_modules/.bun/zod@4.3.6/node_modules/zod/v4/core/to-json-schema.js
@@ -7139,8 +7109,6 @@ function object$1(shape, params) {
7139
7109
  ...normalizeParams(params)
7140
7110
  });
7141
7111
  }
7142
- const describe$1 = describe$2;
7143
- const meta$1 = meta$2;
7144
7112
 
7145
7113
  //#endregion
7146
7114
  //#region ../../../node_modules/.bun/@modelcontextprotocol+sdk@1.29.0/node_modules/@modelcontextprotocol/sdk/dist/esm/server/zod-compat.js
@@ -7954,8 +7922,6 @@ function refine(fn, _params = {}) {
7954
7922
  function superRefine(fn) {
7955
7923
  return _superRefine(fn);
7956
7924
  }
7957
- const describe = describe$2;
7958
- const meta = meta$2;
7959
7925
  function preprocess(fn, schema) {
7960
7926
  return pipe(transform(fn), schema);
7961
7927
  }
@@ -9239,7 +9205,7 @@ function isTerminal(status) {
9239
9205
  }
9240
9206
 
9241
9207
  //#endregion
9242
- //#region ../../../node_modules/.bun/zod-to-json-schema@3.25.1+3c5d820c62823f0b/node_modules/zod-to-json-schema/dist/esm/Options.js
9208
+ //#region ../../../node_modules/.bun/zod-to-json-schema@3.25.2+3c5d820c62823f0b/node_modules/zod-to-json-schema/dist/esm/Options.js
9243
9209
  const ignoreOverride = Symbol("Let zodToJsonSchema decide on which parser to use");
9244
9210
  const defaultOptions = {
9245
9211
  name: void 0,
@@ -9274,7 +9240,7 @@ const getDefaultOptions = (options) => typeof options === "string" ? {
9274
9240
  };
9275
9241
 
9276
9242
  //#endregion
9277
- //#region ../../../node_modules/.bun/zod-to-json-schema@3.25.1+3c5d820c62823f0b/node_modules/zod-to-json-schema/dist/esm/Refs.js
9243
+ //#region ../../../node_modules/.bun/zod-to-json-schema@3.25.2+3c5d820c62823f0b/node_modules/zod-to-json-schema/dist/esm/Refs.js
9278
9244
  const getRefs = (options) => {
9279
9245
  const _options = getDefaultOptions(options);
9280
9246
  const currentPath = _options.name !== void 0 ? [
@@ -9300,7 +9266,7 @@ const getRefs = (options) => {
9300
9266
  };
9301
9267
 
9302
9268
  //#endregion
9303
- //#region ../../../node_modules/.bun/zod-to-json-schema@3.25.1+3c5d820c62823f0b/node_modules/zod-to-json-schema/dist/esm/errorMessages.js
9269
+ //#region ../../../node_modules/.bun/zod-to-json-schema@3.25.2+3c5d820c62823f0b/node_modules/zod-to-json-schema/dist/esm/errorMessages.js
9304
9270
  function addErrorMessage(res, key, errorMessage, refs) {
9305
9271
  if (!refs?.errorMessages) return;
9306
9272
  if (errorMessage) res.errorMessage = {
@@ -9314,7 +9280,7 @@ function setResponseValueAndErrors(res, key, value, errorMessage, refs) {
9314
9280
  }
9315
9281
 
9316
9282
  //#endregion
9317
- //#region ../../../node_modules/.bun/zod-to-json-schema@3.25.1+3c5d820c62823f0b/node_modules/zod-to-json-schema/dist/esm/getRelativePath.js
9283
+ //#region ../../../node_modules/.bun/zod-to-json-schema@3.25.2+3c5d820c62823f0b/node_modules/zod-to-json-schema/dist/esm/getRelativePath.js
9318
9284
  const getRelativePath = (pathA, pathB) => {
9319
9285
  let i = 0;
9320
9286
  for (; i < pathA.length && i < pathB.length; i++) if (pathA[i] !== pathB[i]) break;
@@ -9322,7 +9288,7 @@ const getRelativePath = (pathA, pathB) => {
9322
9288
  };
9323
9289
 
9324
9290
  //#endregion
9325
- //#region ../../../node_modules/.bun/zod-to-json-schema@3.25.1+3c5d820c62823f0b/node_modules/zod-to-json-schema/dist/esm/parsers/any.js
9291
+ //#region ../../../node_modules/.bun/zod-to-json-schema@3.25.2+3c5d820c62823f0b/node_modules/zod-to-json-schema/dist/esm/parsers/any.js
9326
9292
  function parseAnyDef(refs) {
9327
9293
  if (refs.target !== "openAi") return {};
9328
9294
  const anyDefinitionPath = [
@@ -9335,7 +9301,7 @@ function parseAnyDef(refs) {
9335
9301
  }
9336
9302
 
9337
9303
  //#endregion
9338
- //#region ../../../node_modules/.bun/zod-to-json-schema@3.25.1+3c5d820c62823f0b/node_modules/zod-to-json-schema/dist/esm/parsers/array.js
9304
+ //#region ../../../node_modules/.bun/zod-to-json-schema@3.25.2+3c5d820c62823f0b/node_modules/zod-to-json-schema/dist/esm/parsers/array.js
9339
9305
  function parseArrayDef(def, refs) {
9340
9306
  const res = { type: "array" };
9341
9307
  if (def.type?._def && def.type?._def?.typeName !== ZodFirstPartyTypeKind.ZodAny) res.items = parseDef(def.type._def, {
@@ -9352,7 +9318,7 @@ function parseArrayDef(def, refs) {
9352
9318
  }
9353
9319
 
9354
9320
  //#endregion
9355
- //#region ../../../node_modules/.bun/zod-to-json-schema@3.25.1+3c5d820c62823f0b/node_modules/zod-to-json-schema/dist/esm/parsers/bigint.js
9321
+ //#region ../../../node_modules/.bun/zod-to-json-schema@3.25.2+3c5d820c62823f0b/node_modules/zod-to-json-schema/dist/esm/parsers/bigint.js
9356
9322
  function parseBigintDef(def, refs) {
9357
9323
  const res = {
9358
9324
  type: "integer",
@@ -9384,25 +9350,25 @@ function parseBigintDef(def, refs) {
9384
9350
  }
9385
9351
 
9386
9352
  //#endregion
9387
- //#region ../../../node_modules/.bun/zod-to-json-schema@3.25.1+3c5d820c62823f0b/node_modules/zod-to-json-schema/dist/esm/parsers/boolean.js
9353
+ //#region ../../../node_modules/.bun/zod-to-json-schema@3.25.2+3c5d820c62823f0b/node_modules/zod-to-json-schema/dist/esm/parsers/boolean.js
9388
9354
  function parseBooleanDef() {
9389
9355
  return { type: "boolean" };
9390
9356
  }
9391
9357
 
9392
9358
  //#endregion
9393
- //#region ../../../node_modules/.bun/zod-to-json-schema@3.25.1+3c5d820c62823f0b/node_modules/zod-to-json-schema/dist/esm/parsers/branded.js
9359
+ //#region ../../../node_modules/.bun/zod-to-json-schema@3.25.2+3c5d820c62823f0b/node_modules/zod-to-json-schema/dist/esm/parsers/branded.js
9394
9360
  function parseBrandedDef(_def, refs) {
9395
9361
  return parseDef(_def.type._def, refs);
9396
9362
  }
9397
9363
 
9398
9364
  //#endregion
9399
- //#region ../../../node_modules/.bun/zod-to-json-schema@3.25.1+3c5d820c62823f0b/node_modules/zod-to-json-schema/dist/esm/parsers/catch.js
9365
+ //#region ../../../node_modules/.bun/zod-to-json-schema@3.25.2+3c5d820c62823f0b/node_modules/zod-to-json-schema/dist/esm/parsers/catch.js
9400
9366
  const parseCatchDef = (def, refs) => {
9401
9367
  return parseDef(def.innerType._def, refs);
9402
9368
  };
9403
9369
 
9404
9370
  //#endregion
9405
- //#region ../../../node_modules/.bun/zod-to-json-schema@3.25.1+3c5d820c62823f0b/node_modules/zod-to-json-schema/dist/esm/parsers/date.js
9371
+ //#region ../../../node_modules/.bun/zod-to-json-schema@3.25.2+3c5d820c62823f0b/node_modules/zod-to-json-schema/dist/esm/parsers/date.js
9406
9372
  function parseDateDef(def, refs, overrideDateStrategy) {
9407
9373
  const strategy = overrideDateStrategy ?? refs.dateStrategy;
9408
9374
  if (Array.isArray(strategy)) return { anyOf: strategy.map((item, i) => parseDateDef(def, refs, item)) };
@@ -9437,7 +9403,7 @@ const integerDateParser = (def, refs) => {
9437
9403
  };
9438
9404
 
9439
9405
  //#endregion
9440
- //#region ../../../node_modules/.bun/zod-to-json-schema@3.25.1+3c5d820c62823f0b/node_modules/zod-to-json-schema/dist/esm/parsers/default.js
9406
+ //#region ../../../node_modules/.bun/zod-to-json-schema@3.25.2+3c5d820c62823f0b/node_modules/zod-to-json-schema/dist/esm/parsers/default.js
9441
9407
  function parseDefaultDef(_def, refs) {
9442
9408
  return {
9443
9409
  ...parseDef(_def.innerType._def, refs),
@@ -9446,13 +9412,13 @@ function parseDefaultDef(_def, refs) {
9446
9412
  }
9447
9413
 
9448
9414
  //#endregion
9449
- //#region ../../../node_modules/.bun/zod-to-json-schema@3.25.1+3c5d820c62823f0b/node_modules/zod-to-json-schema/dist/esm/parsers/effects.js
9415
+ //#region ../../../node_modules/.bun/zod-to-json-schema@3.25.2+3c5d820c62823f0b/node_modules/zod-to-json-schema/dist/esm/parsers/effects.js
9450
9416
  function parseEffectsDef(_def, refs) {
9451
9417
  return refs.effectStrategy === "input" ? parseDef(_def.schema._def, refs) : parseAnyDef(refs);
9452
9418
  }
9453
9419
 
9454
9420
  //#endregion
9455
- //#region ../../../node_modules/.bun/zod-to-json-schema@3.25.1+3c5d820c62823f0b/node_modules/zod-to-json-schema/dist/esm/parsers/enum.js
9421
+ //#region ../../../node_modules/.bun/zod-to-json-schema@3.25.2+3c5d820c62823f0b/node_modules/zod-to-json-schema/dist/esm/parsers/enum.js
9456
9422
  function parseEnumDef(def) {
9457
9423
  return {
9458
9424
  type: "string",
@@ -9461,7 +9427,7 @@ function parseEnumDef(def) {
9461
9427
  }
9462
9428
 
9463
9429
  //#endregion
9464
- //#region ../../../node_modules/.bun/zod-to-json-schema@3.25.1+3c5d820c62823f0b/node_modules/zod-to-json-schema/dist/esm/parsers/intersection.js
9430
+ //#region ../../../node_modules/.bun/zod-to-json-schema@3.25.2+3c5d820c62823f0b/node_modules/zod-to-json-schema/dist/esm/parsers/intersection.js
9465
9431
  const isJsonSchema7AllOfType = (type) => {
9466
9432
  if ("type" in type && type.type === "string") return false;
9467
9433
  return "allOf" in type;
@@ -9504,7 +9470,7 @@ function parseIntersectionDef(def, refs) {
9504
9470
  }
9505
9471
 
9506
9472
  //#endregion
9507
- //#region ../../../node_modules/.bun/zod-to-json-schema@3.25.1+3c5d820c62823f0b/node_modules/zod-to-json-schema/dist/esm/parsers/literal.js
9473
+ //#region ../../../node_modules/.bun/zod-to-json-schema@3.25.2+3c5d820c62823f0b/node_modules/zod-to-json-schema/dist/esm/parsers/literal.js
9508
9474
  function parseLiteralDef(def, refs) {
9509
9475
  const parsedType = typeof def.value;
9510
9476
  if (parsedType !== "bigint" && parsedType !== "number" && parsedType !== "boolean" && parsedType !== "string") return { type: Array.isArray(def.value) ? "array" : "object" };
@@ -9519,7 +9485,7 @@ function parseLiteralDef(def, refs) {
9519
9485
  }
9520
9486
 
9521
9487
  //#endregion
9522
- //#region ../../../node_modules/.bun/zod-to-json-schema@3.25.1+3c5d820c62823f0b/node_modules/zod-to-json-schema/dist/esm/parsers/string.js
9488
+ //#region ../../../node_modules/.bun/zod-to-json-schema@3.25.2+3c5d820c62823f0b/node_modules/zod-to-json-schema/dist/esm/parsers/string.js
9523
9489
  let emojiRegex = void 0;
9524
9490
  /**
9525
9491
  * Generated from the regular expressions found here as of 2024-05-22:
@@ -9765,7 +9731,7 @@ function stringifyRegExpWithFlags(regex, refs) {
9765
9731
  }
9766
9732
 
9767
9733
  //#endregion
9768
- //#region ../../../node_modules/.bun/zod-to-json-schema@3.25.1+3c5d820c62823f0b/node_modules/zod-to-json-schema/dist/esm/parsers/record.js
9734
+ //#region ../../../node_modules/.bun/zod-to-json-schema@3.25.2+3c5d820c62823f0b/node_modules/zod-to-json-schema/dist/esm/parsers/record.js
9769
9735
  function parseRecordDef(def, refs) {
9770
9736
  if (refs.target === "openAi") console.warn("Warning: OpenAI may not support records in schemas! Try an array of key-value pairs instead.");
9771
9737
  if (refs.target === "openApi3" && def.keyType?._def.typeName === ZodFirstPartyTypeKind.ZodEnum) return {
@@ -9813,7 +9779,7 @@ function parseRecordDef(def, refs) {
9813
9779
  }
9814
9780
 
9815
9781
  //#endregion
9816
- //#region ../../../node_modules/.bun/zod-to-json-schema@3.25.1+3c5d820c62823f0b/node_modules/zod-to-json-schema/dist/esm/parsers/map.js
9782
+ //#region ../../../node_modules/.bun/zod-to-json-schema@3.25.2+3c5d820c62823f0b/node_modules/zod-to-json-schema/dist/esm/parsers/map.js
9817
9783
  function parseMapDef(def, refs) {
9818
9784
  if (refs.mapStrategy === "record") return parseRecordDef(def, refs);
9819
9785
  return {
@@ -9845,7 +9811,7 @@ function parseMapDef(def, refs) {
9845
9811
  }
9846
9812
 
9847
9813
  //#endregion
9848
- //#region ../../../node_modules/.bun/zod-to-json-schema@3.25.1+3c5d820c62823f0b/node_modules/zod-to-json-schema/dist/esm/parsers/nativeEnum.js
9814
+ //#region ../../../node_modules/.bun/zod-to-json-schema@3.25.2+3c5d820c62823f0b/node_modules/zod-to-json-schema/dist/esm/parsers/nativeEnum.js
9849
9815
  function parseNativeEnumDef(def) {
9850
9816
  const object = def.values;
9851
9817
  const actualValues = Object.keys(def.values).filter((key) => {
@@ -9859,7 +9825,7 @@ function parseNativeEnumDef(def) {
9859
9825
  }
9860
9826
 
9861
9827
  //#endregion
9862
- //#region ../../../node_modules/.bun/zod-to-json-schema@3.25.1+3c5d820c62823f0b/node_modules/zod-to-json-schema/dist/esm/parsers/never.js
9828
+ //#region ../../../node_modules/.bun/zod-to-json-schema@3.25.2+3c5d820c62823f0b/node_modules/zod-to-json-schema/dist/esm/parsers/never.js
9863
9829
  function parseNeverDef(refs) {
9864
9830
  return refs.target === "openAi" ? void 0 : { not: parseAnyDef({
9865
9831
  ...refs,
@@ -9868,7 +9834,7 @@ function parseNeverDef(refs) {
9868
9834
  }
9869
9835
 
9870
9836
  //#endregion
9871
- //#region ../../../node_modules/.bun/zod-to-json-schema@3.25.1+3c5d820c62823f0b/node_modules/zod-to-json-schema/dist/esm/parsers/null.js
9837
+ //#region ../../../node_modules/.bun/zod-to-json-schema@3.25.2+3c5d820c62823f0b/node_modules/zod-to-json-schema/dist/esm/parsers/null.js
9872
9838
  function parseNullDef(refs) {
9873
9839
  return refs.target === "openApi3" ? {
9874
9840
  enum: ["null"],
@@ -9877,7 +9843,7 @@ function parseNullDef(refs) {
9877
9843
  }
9878
9844
 
9879
9845
  //#endregion
9880
- //#region ../../../node_modules/.bun/zod-to-json-schema@3.25.1+3c5d820c62823f0b/node_modules/zod-to-json-schema/dist/esm/parsers/union.js
9846
+ //#region ../../../node_modules/.bun/zod-to-json-schema@3.25.2+3c5d820c62823f0b/node_modules/zod-to-json-schema/dist/esm/parsers/union.js
9881
9847
  const primitiveMappings = {
9882
9848
  ZodString: "string",
9883
9849
  ZodNumber: "number",
@@ -9934,7 +9900,7 @@ const asAnyOf = (def, refs) => {
9934
9900
  };
9935
9901
 
9936
9902
  //#endregion
9937
- //#region ../../../node_modules/.bun/zod-to-json-schema@3.25.1+3c5d820c62823f0b/node_modules/zod-to-json-schema/dist/esm/parsers/nullable.js
9903
+ //#region ../../../node_modules/.bun/zod-to-json-schema@3.25.2+3c5d820c62823f0b/node_modules/zod-to-json-schema/dist/esm/parsers/nullable.js
9938
9904
  function parseNullableDef(def, refs) {
9939
9905
  if ([
9940
9906
  "ZodString",
@@ -9975,7 +9941,7 @@ function parseNullableDef(def, refs) {
9975
9941
  }
9976
9942
 
9977
9943
  //#endregion
9978
- //#region ../../../node_modules/.bun/zod-to-json-schema@3.25.1+3c5d820c62823f0b/node_modules/zod-to-json-schema/dist/esm/parsers/number.js
9944
+ //#region ../../../node_modules/.bun/zod-to-json-schema@3.25.2+3c5d820c62823f0b/node_modules/zod-to-json-schema/dist/esm/parsers/number.js
9979
9945
  function parseNumberDef(def, refs) {
9980
9946
  const res = { type: "number" };
9981
9947
  if (!def.checks) return res;
@@ -10008,7 +9974,7 @@ function parseNumberDef(def, refs) {
10008
9974
  }
10009
9975
 
10010
9976
  //#endregion
10011
- //#region ../../../node_modules/.bun/zod-to-json-schema@3.25.1+3c5d820c62823f0b/node_modules/zod-to-json-schema/dist/esm/parsers/object.js
9977
+ //#region ../../../node_modules/.bun/zod-to-json-schema@3.25.2+3c5d820c62823f0b/node_modules/zod-to-json-schema/dist/esm/parsers/object.js
10012
9978
  function parseObjectDef(def, refs) {
10013
9979
  const forceOptionalIntoNullable = refs.target === "openAi";
10014
9980
  const result = {
@@ -10068,7 +10034,7 @@ function safeIsOptional(schema) {
10068
10034
  }
10069
10035
 
10070
10036
  //#endregion
10071
- //#region ../../../node_modules/.bun/zod-to-json-schema@3.25.1+3c5d820c62823f0b/node_modules/zod-to-json-schema/dist/esm/parsers/optional.js
10037
+ //#region ../../../node_modules/.bun/zod-to-json-schema@3.25.2+3c5d820c62823f0b/node_modules/zod-to-json-schema/dist/esm/parsers/optional.js
10072
10038
  const parseOptionalDef = (def, refs) => {
10073
10039
  if (refs.currentPath.toString() === refs.propertyPath?.toString()) return parseDef(def.innerType._def, refs);
10074
10040
  const innerSchema = parseDef(def.innerType._def, {
@@ -10083,7 +10049,7 @@ const parseOptionalDef = (def, refs) => {
10083
10049
  };
10084
10050
 
10085
10051
  //#endregion
10086
- //#region ../../../node_modules/.bun/zod-to-json-schema@3.25.1+3c5d820c62823f0b/node_modules/zod-to-json-schema/dist/esm/parsers/pipeline.js
10052
+ //#region ../../../node_modules/.bun/zod-to-json-schema@3.25.2+3c5d820c62823f0b/node_modules/zod-to-json-schema/dist/esm/parsers/pipeline.js
10087
10053
  const parsePipelineDef = (def, refs) => {
10088
10054
  if (refs.pipeStrategy === "input") return parseDef(def.in._def, refs);
10089
10055
  else if (refs.pipeStrategy === "output") return parseDef(def.out._def, refs);
@@ -10106,13 +10072,13 @@ const parsePipelineDef = (def, refs) => {
10106
10072
  };
10107
10073
 
10108
10074
  //#endregion
10109
- //#region ../../../node_modules/.bun/zod-to-json-schema@3.25.1+3c5d820c62823f0b/node_modules/zod-to-json-schema/dist/esm/parsers/promise.js
10075
+ //#region ../../../node_modules/.bun/zod-to-json-schema@3.25.2+3c5d820c62823f0b/node_modules/zod-to-json-schema/dist/esm/parsers/promise.js
10110
10076
  function parsePromiseDef(def, refs) {
10111
10077
  return parseDef(def.type._def, refs);
10112
10078
  }
10113
10079
 
10114
10080
  //#endregion
10115
- //#region ../../../node_modules/.bun/zod-to-json-schema@3.25.1+3c5d820c62823f0b/node_modules/zod-to-json-schema/dist/esm/parsers/set.js
10081
+ //#region ../../../node_modules/.bun/zod-to-json-schema@3.25.2+3c5d820c62823f0b/node_modules/zod-to-json-schema/dist/esm/parsers/set.js
10116
10082
  function parseSetDef(def, refs) {
10117
10083
  const schema = {
10118
10084
  type: "array",
@@ -10128,7 +10094,7 @@ function parseSetDef(def, refs) {
10128
10094
  }
10129
10095
 
10130
10096
  //#endregion
10131
- //#region ../../../node_modules/.bun/zod-to-json-schema@3.25.1+3c5d820c62823f0b/node_modules/zod-to-json-schema/dist/esm/parsers/tuple.js
10097
+ //#region ../../../node_modules/.bun/zod-to-json-schema@3.25.2+3c5d820c62823f0b/node_modules/zod-to-json-schema/dist/esm/parsers/tuple.js
10132
10098
  function parseTupleDef(def, refs) {
10133
10099
  if (def.rest) return {
10134
10100
  type: "array",
@@ -10162,25 +10128,25 @@ function parseTupleDef(def, refs) {
10162
10128
  }
10163
10129
 
10164
10130
  //#endregion
10165
- //#region ../../../node_modules/.bun/zod-to-json-schema@3.25.1+3c5d820c62823f0b/node_modules/zod-to-json-schema/dist/esm/parsers/undefined.js
10131
+ //#region ../../../node_modules/.bun/zod-to-json-schema@3.25.2+3c5d820c62823f0b/node_modules/zod-to-json-schema/dist/esm/parsers/undefined.js
10166
10132
  function parseUndefinedDef(refs) {
10167
10133
  return { not: parseAnyDef(refs) };
10168
10134
  }
10169
10135
 
10170
10136
  //#endregion
10171
- //#region ../../../node_modules/.bun/zod-to-json-schema@3.25.1+3c5d820c62823f0b/node_modules/zod-to-json-schema/dist/esm/parsers/unknown.js
10137
+ //#region ../../../node_modules/.bun/zod-to-json-schema@3.25.2+3c5d820c62823f0b/node_modules/zod-to-json-schema/dist/esm/parsers/unknown.js
10172
10138
  function parseUnknownDef(refs) {
10173
10139
  return parseAnyDef(refs);
10174
10140
  }
10175
10141
 
10176
10142
  //#endregion
10177
- //#region ../../../node_modules/.bun/zod-to-json-schema@3.25.1+3c5d820c62823f0b/node_modules/zod-to-json-schema/dist/esm/parsers/readonly.js
10143
+ //#region ../../../node_modules/.bun/zod-to-json-schema@3.25.2+3c5d820c62823f0b/node_modules/zod-to-json-schema/dist/esm/parsers/readonly.js
10178
10144
  const parseReadonlyDef = (def, refs) => {
10179
10145
  return parseDef(def.innerType._def, refs);
10180
10146
  };
10181
10147
 
10182
10148
  //#endregion
10183
- //#region ../../../node_modules/.bun/zod-to-json-schema@3.25.1+3c5d820c62823f0b/node_modules/zod-to-json-schema/dist/esm/selectParser.js
10149
+ //#region ../../../node_modules/.bun/zod-to-json-schema@3.25.2+3c5d820c62823f0b/node_modules/zod-to-json-schema/dist/esm/selectParser.js
10184
10150
  const selectParser = (def, typeName, refs) => {
10185
10151
  switch (typeName) {
10186
10152
  case ZodFirstPartyTypeKind.ZodString: return parseStringDef(def, refs);
@@ -10224,7 +10190,7 @@ const selectParser = (def, typeName, refs) => {
10224
10190
  };
10225
10191
 
10226
10192
  //#endregion
10227
- //#region ../../../node_modules/.bun/zod-to-json-schema@3.25.1+3c5d820c62823f0b/node_modules/zod-to-json-schema/dist/esm/parseDef.js
10193
+ //#region ../../../node_modules/.bun/zod-to-json-schema@3.25.2+3c5d820c62823f0b/node_modules/zod-to-json-schema/dist/esm/parseDef.js
10228
10194
  function parseDef(def, refs, forceResolution = false) {
10229
10195
  const seenItem = refs.seen.get(def);
10230
10196
  if (refs.override) {
@@ -10274,7 +10240,7 @@ const addMeta = (def, refs, jsonSchema) => {
10274
10240
  };
10275
10241
 
10276
10242
  //#endregion
10277
- //#region ../../../node_modules/.bun/zod-to-json-schema@3.25.1+3c5d820c62823f0b/node_modules/zod-to-json-schema/dist/esm/zodToJsonSchema.js
10243
+ //#region ../../../node_modules/.bun/zod-to-json-schema@3.25.2+3c5d820c62823f0b/node_modules/zod-to-json-schema/dist/esm/zodToJsonSchema.js
10278
10244
  const zodToJsonSchema = (schema, options) => {
10279
10245
  const refs = getRefs(options);
10280
10246
  let definitions = typeof options === "object" && options.definitions ? Object.entries(options.definitions).reduce((acc, [name, schema]) => ({
@@ -12750,7 +12716,129 @@ var StdioServerTransport = class {
12750
12716
 
12751
12717
  //#endregion
12752
12718
  //#region package.json
12753
- var version = "0.13.1";
12719
+ var version = "0.14.0";
12720
+
12721
+ //#endregion
12722
+ //#region src/anti-patterns.ts
12723
+ const CATEGORY_MAP = {
12724
+ "Reactivity Mistakes": "reactivity",
12725
+ "JSX Mistakes": "jsx",
12726
+ "Context & Provider Mistakes": "context",
12727
+ "Architecture Mistakes": "architecture",
12728
+ "Testing Mistakes": "testing",
12729
+ "Lifecycle & Cleanup Mistakes": "lifecycle",
12730
+ "Documentation Mistakes": "documentation"
12731
+ };
12732
+ const ANTI_PATTERN_CATEGORIES = [
12733
+ "reactivity",
12734
+ "jsx",
12735
+ "context",
12736
+ "architecture",
12737
+ "testing",
12738
+ "lifecycle",
12739
+ "documentation"
12740
+ ];
12741
+ function normaliseCategory(heading) {
12742
+ return CATEGORY_MAP[heading.trim()] ?? null;
12743
+ }
12744
+ function splitSections(doc) {
12745
+ const lines = doc.split("\n");
12746
+ const sections = [];
12747
+ let currentHeading = null;
12748
+ let currentBody = [];
12749
+ for (const line of lines) {
12750
+ const headingMatch = /^## (.+)$/.exec(line);
12751
+ if (headingMatch) {
12752
+ if (currentHeading !== null) sections.push({
12753
+ heading: currentHeading,
12754
+ body: currentBody.join("\n")
12755
+ });
12756
+ currentHeading = headingMatch[1];
12757
+ currentBody = [];
12758
+ } else if (currentHeading !== null) currentBody.push(line);
12759
+ }
12760
+ if (currentHeading !== null) sections.push({
12761
+ heading: currentHeading,
12762
+ body: currentBody.join("\n")
12763
+ });
12764
+ return sections;
12765
+ }
12766
+ function splitBullets(sectionBody) {
12767
+ const lines = sectionBody.split("\n");
12768
+ const bullets = [];
12769
+ let current = [];
12770
+ for (const line of lines) if (/^- \*\*/.test(line)) {
12771
+ if (current.length > 0) bullets.push(current.join("\n").trim());
12772
+ current = [line];
12773
+ } else if (current.length > 0) current.push(line);
12774
+ if (current.length > 0) bullets.push(current.join("\n").trim());
12775
+ return bullets.filter((b) => b.length > 0);
12776
+ }
12777
+ function parseBullet(bullet) {
12778
+ const nameMatch = /^- \*\*([^*]+)\*\*/.exec(bullet);
12779
+ if (!nameMatch) return null;
12780
+ const name = nameMatch[1].trim();
12781
+ const afterName = bullet.slice(nameMatch[0].length);
12782
+ const detectorMatch = /`?\[detector:\s*([a-z0-9\-\/ ]+)\]`?/i.exec(afterName);
12783
+ const detectorCodes = [];
12784
+ if (detectorMatch) for (const code of detectorMatch[1].split("/")) {
12785
+ const c = code.trim();
12786
+ if (c) detectorCodes.push(c);
12787
+ }
12788
+ let description = afterName;
12789
+ if (detectorMatch) description = description.replace(detectorMatch[0], "");
12790
+ description = description.replace(/^[\s:]+/, "").trim();
12791
+ return {
12792
+ name,
12793
+ description,
12794
+ detectorCodes
12795
+ };
12796
+ }
12797
+ function parseAntiPatterns(doc) {
12798
+ const sections = splitSections(doc);
12799
+ const entries = [];
12800
+ for (const { heading, body } of sections) {
12801
+ const category = normaliseCategory(heading);
12802
+ if (!category) continue;
12803
+ for (const bullet of splitBullets(body)) {
12804
+ const parsed = parseBullet(bullet);
12805
+ if (!parsed) continue;
12806
+ entries.push({
12807
+ name: parsed.name,
12808
+ category,
12809
+ categoryHeading: heading,
12810
+ description: parsed.description,
12811
+ detectorCodes: parsed.detectorCodes
12812
+ });
12813
+ }
12814
+ }
12815
+ return entries;
12816
+ }
12817
+ /** Format a list of entries into a single Markdown block suitable for MCP. */
12818
+ function formatAntiPatterns(entries, filterCategory) {
12819
+ if (entries.length === 0) return filterCategory === "all" ? "No anti-patterns found. Check that `.claude/rules/anti-patterns.md` is reachable." : `No anti-patterns found in category '${filterCategory}'. Valid categories: ${ANTI_PATTERN_CATEGORIES.join(", ")}, all.`;
12820
+ const byCategory = /* @__PURE__ */ new Map();
12821
+ for (const entry of entries) {
12822
+ if (!byCategory.has(entry.category)) byCategory.set(entry.category, []);
12823
+ byCategory.get(entry.category).push(entry);
12824
+ }
12825
+ const parts = [];
12826
+ const header = filterCategory === "all" ? `# Pyreon Anti-Patterns (${entries.length} total, ${byCategory.size} categor${byCategory.size === 1 ? "y" : "ies"})` : `# Pyreon Anti-Patterns — ${filterCategory} (${entries.length})`;
12827
+ parts.push(header);
12828
+ parts.push("");
12829
+ parts.push("Each entry is a known mistake documented at `.claude/rules/anti-patterns.md`. Entries tagged `[detector: <code>]` are caught statically by the MCP `validate` tool — the rest require a human / AI review. Read them BEFORE writing new code, not during code review.");
12830
+ parts.push("");
12831
+ for (const [, catEntries] of byCategory) {
12832
+ parts.push(`## ${catEntries[0].categoryHeading} (${catEntries.length})`);
12833
+ parts.push("");
12834
+ for (const entry of catEntries) {
12835
+ const tag = entry.detectorCodes.length > 0 ? ` \`[detector: ${entry.detectorCodes.join(" / ")}]\`` : "";
12836
+ parts.push(`- **${entry.name}**${tag}: ${entry.description}`);
12837
+ }
12838
+ parts.push("");
12839
+ }
12840
+ return parts.join("\n").trimEnd();
12841
+ }
12754
12842
 
12755
12843
  //#endregion
12756
12844
  //#region src/api-reference.ts
@@ -13330,17 +13418,47 @@ useHead({ title: "My Page", meta: [{ name: "description", content: "..." }] })
13330
13418
  useHead(() => ({
13331
13419
  title: \`\${username()} — Profile\`,
13332
13420
  meta: [{ property: "og:title", content: username() }]
13333
- }))`
13421
+ }))`,
13422
+ notes: "Register head tags from any component in the tree. Pass a static `UseHeadInput` object for one-shot registration, or a `() => UseHeadInput` thunk for reactive re-registration when signal reads inside the thunk change. Calling `useHead()` outside a `HeadProvider` ancestor (CSR) or `renderWithHead()` invocation (SSR) is a silent no-op — it does not throw. See also: HeadProvider, renderWithHead.",
13423
+ mistakes: `- Using \`\${...}\` in a \`titleTemplate\` string — the placeholder is \`%s\` (or pass a function form \`(title) => …\`)
13424
+ - Calling \`useHead()\` outside any \`HeadProvider\` / \`renderWithHead()\` boundary — silent no-op, the entries simply go nowhere
13425
+ - Wrapping the input in \`computed()\` instead of a thunk — pass a plain \`() => ({...})\` arrow; \`useHead\` registers its own effect
13426
+ - Expecting \`<\/script>\` inside an inline script body to render verbatim — the SSR escaper rewrites it as \`<\/script>\` to prevent breaking out of the inline tag`
13334
13427
  },
13335
13428
  "head/HeadProvider": {
13336
- signature: "<HeadProvider>{children}</HeadProvider>",
13337
- example: `// Client-side setup:
13429
+ signature: "(props: HeadProviderProps) => VNodeChild",
13430
+ example: `<HeadProvider>{children}</HeadProvider>
13431
+
13432
+ // Client-side setup:
13338
13433
  mount(
13339
13434
  <HeadProvider>
13340
13435
  <App />
13341
13436
  </HeadProvider>,
13342
13437
  document.getElementById("app")!
13343
- )`
13438
+ )`,
13439
+ notes: "Client-side context provider that collects every `useHead()` call from descendants and syncs the resolved tags into the live `document.head` element. Mount once near the application root. Auto-creates a `HeadContextValue` when no `context` prop is passed; nested providers each own an independent context. See also: useHead, renderWithHead, createHeadContext.",
13440
+ mistakes: `- Mounting two \`HeadProvider\` instances at sibling roots — each owns an independent context, so a \`useHead()\` deeper in tree A is invisible to tree B
13441
+ - Forgetting to mount \`HeadProvider\` and expecting \`useHead()\` to still update \`document.head\` — silent no-op outside a provider`
13442
+ },
13443
+ "head/renderWithHead": {
13444
+ signature: "renderWithHead(app: VNode): Promise<{ html: string; head: string; htmlAttrs: string; bodyAttrs: string }>",
13445
+ example: `import { renderWithHead } from '@pyreon/head'
13446
+
13447
+ const { html, head, htmlAttrs, bodyAttrs } = await renderWithHead(<App />)
13448
+ const doc = \`<!doctype html><html\${htmlAttrs}><head>\${head}</head><body\${bodyAttrs}>\${html}</body></html>\``,
13449
+ notes: "SSR companion to `HeadProvider`. Renders the app to HTML via `renderToString` while collecting every `useHead()` call from the tree, then serializes the resolved tags into a single `head` string plus separate `htmlAttrs` / `bodyAttrs` strings. Async components that call `useHead()` in their body work — the renderer awaits suspended subtrees before serialization. See also: useHead, HeadProvider.",
13450
+ mistakes: `- Awaiting \`renderWithHead\` and then NOT splicing \`head\` into the \`<head>\` element — every \`useHead()\` call quietly disappears
13451
+ - Forgetting to interpolate \`htmlAttrs\` / \`bodyAttrs\` (the leading space is included in each string) — \`htmlAttrs.lang\` and \`bodyAttrs.class\` set via \`useHead\` won\'t reach the DOM`
13452
+ },
13453
+ "head/createHeadContext": {
13454
+ signature: "() => HeadContextValue",
13455
+ example: `import { createHeadContext, HeadContext } from '@pyreon/head'
13456
+
13457
+ const ctx = createHeadContext()
13458
+ provide(HeadContext, ctx)
13459
+ // ... render tree that calls useHead() ...
13460
+ const { tags, htmlAttrs, bodyAttrs } = ctx.resolve()`,
13461
+ notes: "Manual factory for a `HeadContextValue` — only needed when wiring up a custom SSR pipeline that bypasses `renderWithHead`, or when running multiple isolated head contexts in the same process. The value exposes `add` / `remove` / `resolve` / `resolveTitleTemplate` / `resolveHtmlAttrs` / `resolveBodyAttrs` for full programmatic control. See also: HeadProvider, renderWithHead."
13344
13462
  },
13345
13463
  "server/createHandler": {
13346
13464
  signature: "createHandler(options: HandlerOptions): (req: Request) => Promise<Response>",
@@ -13351,7 +13469,12 @@ export default createHandler({
13351
13469
  routes,
13352
13470
  clientEntry: "/src/entry-client.ts",
13353
13471
  mode: "stream", // or "string"
13354
- })`
13472
+ })`,
13473
+ notes: "Build a production SSR handler from your `App`, `routes`, and optional template / client entry / middleware. The template is precompiled once at handler-creation (split into 4 parts to skip three string scans per request); a missing `<!--pyreon-app-->` placeholder throws at creation time, not per request. Middleware runs before render with `ctx.locals` for cross-middleware data passing — return a `Response` to short-circuit the chain. `mode: \"stream\"` uses `renderToStream` so Suspense boundaries flush out-of-order; `mode: \"string\"` uses `renderToString` (default). See also: prerender, island, useRequestLocals.",
13474
+ mistakes: `- Omitting \`<!--pyreon-app-->\` from the custom template — throws at handler-creation, not per request
13475
+ - Returning a \`Response\` from middleware and expecting downstream middleware to still run — the chain short-circuits on the first \`Response\`
13476
+ - Reading \`ctx.locals\` from inside the component without \`useRequestLocals()\` — the component tree only sees locals when bridged through that hook
13477
+ - Forgetting to escape user data inserted into a custom template — \`createHandler\` only escapes its own loader-data injection (\`<\/script>\` → \`<\/script>\`); your template content is your responsibility`
13355
13478
  },
13356
13479
  "server/island": {
13357
13480
  signature: "island(loader: () => Promise<ComponentFn>, options: { name: string; hydrate?: HydrationStrategy }): ComponentFn",
@@ -13360,7 +13483,12 @@ export default createHandler({
13360
13483
  { name: "SearchBar", hydrate: "visible" }
13361
13484
  )
13362
13485
 
13363
- // Hydration strategies: "load" | "idle" | "visible" | "media" | "never"`
13486
+ // Hydration strategies: "load" | "idle" | "visible" | "media" | "never"`,
13487
+ notes: "Wrap a lazily-loaded component in a `<pyreon-island>` boundary with a hydration strategy. The rest of the page stays HTML-only; only the island fetches its JS bundle and hydrates. Strategies: `\"load\"` (immediate), `\"idle\"` (`requestIdleCallback`), `\"visible\"` (IntersectionObserver), `\"media(query)\"` (matchMedia), `\"never\"` (HTML-only, no JS). Props passed to islands are JSON-serialized — non-JSON values (functions, symbols, undefined, children) are stripped. See also: createHandler, hydrateIslands.",
13488
+ mistakes: `- Passing function props (event handlers, callbacks) — silently stripped during JSON serialization, the island sees \`undefined\`
13489
+ - Passing children to an island — stripped; islands cannot render arbitrary descendant trees from props
13490
+ - Forgetting to call \`hydrateIslands({ Name: () => import("./Path") })\` on the client — islands render as HTML and never hydrate
13491
+ - Using a duplicate \`name\` across two islands — the client-side registry collapses them, only one loader will fire`
13364
13492
  },
13365
13493
  "server/prerender": {
13366
13494
  signature: "prerender(options: PrerenderOptions): Promise<PrerenderResult>",
@@ -13368,7 +13496,11 @@ export default createHandler({
13368
13496
  handler,
13369
13497
  paths: ["/", "/about", "/blog/1", "/blog/2"],
13370
13498
  outDir: "./dist",
13371
- })`
13499
+ })`,
13500
+ notes: "Static-site generator built on `createHandler`. Walks the `paths` array (or async generator), invokes the handler for each path, and writes the rendered HTML to `outDir/<path>.html`. The `onPage(path, html)` callback fires per page so callers can post-process or stream output. Validates `outDir` against path traversal (`../` segments are rejected). Errors per-page are collected in the result, not thrown. See also: createHandler.",
13501
+ mistakes: `- Passing a relative \`outDir\` and being surprised when it resolves against \`process.cwd()\` — pass an absolute path for predictability
13502
+ - Expecting per-page errors to throw — they\'re collected in \`result.errors\`; check the array after \`await\`
13503
+ - Generating thousands of paths without batching — the function processes the array sequentially; if you need parallelism, batch the \`paths\` array yourself`
13372
13504
  },
13373
13505
  "runtime-dom/mount": {
13374
13506
  signature: "mount(root: VNodeChild, container: Element): () => void",
@@ -14554,7 +14686,7 @@ lint({
14554
14686
  "pyreon/no-window-in-ssr": { exemptPaths: ["src/foundation/"] },
14555
14687
  },
14556
14688
  })`,
14557
- notes: "Programmatic API. 59 rules across 12 categories. Auto-loads .pyreonlintrc.json. Presets: recommended, strict, app, lib. Per-rule options via tuple form in config (`[\"error\", { exemptPaths: [...] }]`) or `ruleOptionsOverrides`. Wrong-typed options surface on `result.configDiagnostics`. Uses oxc-parser with AST caching."
14689
+ notes: "59 rules across 12 categories. Auto-loads `.pyreonlintrc.json`. Presets: `recommended`, `strict`, `app`, `lib`. Per-rule options via tuple form in config (`[\"error\", { exemptPaths: [...] }]`) or `ruleOptionsOverrides`. Wrong-typed options surface on `result.configDiagnostics`. Uses `oxc-parser` with AST caching. See also: lintFile, getPreset, AstCache."
14558
14690
  },
14559
14691
  "lint/lintFile": {
14560
14692
  signature: "lintFile(filePath: string, sourceText: string, rules: Rule[], config: LintConfig, cache?: AstCache, configDiagnosticsSink?: ConfigDiagnostic[]): LintFileResult",
@@ -14564,17 +14696,17 @@ const cache = new AstCache()
14564
14696
  const config = getPreset("recommended")
14565
14697
  const configSink: ConfigDiagnostic[] = []
14566
14698
  const result = lintFile("app.tsx", source, allRules, config, cache, configSink)`,
14567
- notes: "Low-level single-file API. Optional AstCache for repeat runs (FNV-1a hash keyed). Optional `configDiagnosticsSink` collects malformed-option diagnostics; without it they print to stderr."
14699
+ notes: "Low-level single-file API. Optional `AstCache` for repeat runs (FNV-1a hash keyed). Optional `configDiagnosticsSink` collects malformed-option diagnostics; without it they print to stderr. See also: lint, AstCache."
14568
14700
  },
14569
14701
  "lint/cli": {
14570
- signature: "pyreon-lint [--preset name] [--fix] [--format text|json|compact] [--quiet] [--watch] [--list] [--config path] [--ignore path] [--rule id=severity] [--rule-options id='{json}'] [path...]",
14702
+ signature: `pyreon-lint [--preset name] [--fix] [--format text|json|compact] [--quiet] [--watch] [--list] [--config path] [--ignore path] [--rule id=severity] [--rule-options id='{json}'] [path...]`,
14571
14703
  example: `pyreon-lint --preset strict --quiet # CI mode
14572
14704
  pyreon-lint --fix # auto-fix
14573
14705
  pyreon-lint --watch src/ # watch mode
14574
14706
  pyreon-lint --list # list all 59 rules
14575
14707
  pyreon-lint --format json # machine-readable
14576
14708
  pyreon-lint --rule-options 'pyreon/no-window-in-ssr={"exemptPaths":["src/foundation/"]}' src/`,
14577
- notes: "CLI entry. Config: .pyreonlintrc.json (reference schema/pyreonlintrc.schema.json for IDE autocomplete), package.json 'pyreonlint' field. Ignore: .pyreonlintignore + .gitignore. Watch: fs.watch recursive with 100ms debounce. `--rule-options id='{json}'` passes per-rule options on a single run."
14709
+ notes: `CLI entry. Config: \`.pyreonlintrc.json\` (reference \`schema/pyreonlintrc.schema.json\` for IDE autocomplete) or \`package.json\`'s \`'pyreonlint'\` field. Ignore: \`.pyreonlintignore\` + \`.gitignore\`. Watch: \`fs.watch\` recursive with 100ms debounce. \`--rule-options id='{json}'\` passes per-rule options on a single run. See also: lint.`
14578
14710
  },
14579
14711
  "lint/no-process-dev-gate": {
14580
14712
  signature: "rule: pyreon/no-process-dev-gate (architecture, error, auto-fixable)",
@@ -14586,7 +14718,7 @@ if (__DEV__) console.warn('hello')
14586
14718
  // @ts-ignore — provided by Vite/Rolldown at build time
14587
14719
  const __DEV__ = import.meta.env?.DEV === true
14588
14720
  if (__DEV__) console.warn('hello')`,
14589
- notes: "The `typeof process !== 'undefined' && process.env.NODE_ENV !== 'production'` pattern works in vitest (Node, `process` is defined) but is silently dead code in real Vite browser bundles because Vite does NOT polyfill `process` for the client. Every `console.warn` gated on the broken constant never fires for real users in dev mode — unit tests pass while users get nothing. Use `import.meta.env.DEV` instead — Vite/Rolldown literal-replace it at build time, prod tree-shakes the warning to zero bytes, and vitest sets it to `true` automatically. Server-only packages (`zero`, `core/server`, `core/runtime-server`, `vite-plugin`, `cli`, `lint`, `mcp`, `storybook`, `typescript`) and test files are exempt. Reference implementation: `packages/fundamentals/flow/src/layout.ts:warnIgnoredOptions`. The rule has an auto-fix that replaces the broken expression with `import.meta.env?.DEV === true`.",
14721
+ notes: `The \`typeof process !== 'undefined' && process.env.NODE_ENV !== 'production'\` pattern works in vitest (Node, \`process\` is defined) but is silently dead code in real Vite browser bundles because Vite does NOT polyfill \`process\` for the client. Every \`console.warn\` gated on the broken constant never fires for real users in dev mode — unit tests pass while users get nothing. Use \`import.meta.env.DEV\` instead — Vite/Rolldown literal-replace it at build time, prod tree-shakes the warning to zero bytes, and vitest sets it to \`true\` automatically. Server-only packages (\`zero\`, \`core/server\`, \`core/runtime-server\`, \`vite-plugin\`, \`cli\`, \`lint\`, \`mcp\`, \`storybook\`, \`typescript\`) and test files are exempt. Reference implementation: \`packages/fundamentals/flow/src/layout.ts:warnIgnoredOptions\`. The rule has an auto-fix that replaces the broken expression with \`import.meta.env?.DEV === true\`. See also: require-browser-smoke-test.`,
14590
14722
  mistakes: `- Copying the \`typeof process !== 'undefined' && process.env.NODE_ENV !== 'production'\` pattern from existing codebases — it works in Node but is dead in browser bundles
14591
14723
  - Trying to test with \`delete globalThis.process\` — vitest's own \`import.meta.env\` depends on \`process\`, so deleting it breaks the FIXED gate too (not because the gate is wrong, but because vitest can't resolve it)
14592
14724
  - Adding \`process: { env: { ... } }\` polyfills to vite.config.ts as a workaround — fix the source instead
@@ -14606,7 +14738,7 @@ if (__DEV__) console.warn('hello')`,
14606
14738
  ]
14607
14739
  }
14608
14740
  }`,
14609
- notes: "Locks in the durability of the T1.1 browser smoke harness (PRs #224, #227, #229, #231). Every browser-categorized package MUST ship at least one `*.browser.test.{ts,tsx}` file under `src/`. Without this rule, new browser packages can quietly ship without smoke coverage and we drift back to the world before T1.1 — happy-dom silently masks environment-divergence bugs (PR #197 mock-vnode metadata drop, PR #200 `typeof process` dead code, multi-word event delegation bug). Default browser-package list mirrors `.claude/rules/test-environment-parity.md`. The rule fires once per package on its `src/index.ts`, walks the package directory looking for `*.browser.test.*`, and reports if none are found. Off in `app` preset because apps don't ship as packages with smoke obligations.",
14741
+ notes: `Locks in the durability of the T1.1 browser smoke harness (PRs #224, #227, #229, #231). Every browser-categorized package MUST ship at least one \`*.browser.test.{ts,tsx}\` file under \`src/\`. Without this rule, new browser packages can quietly ship without smoke coverage and we drift back to the world before T1.1 — happy-dom silently masks environment-divergence bugs (PR #197 mock-vnode metadata drop, PR #200 \`typeof process\` dead code, multi-word event delegation bug). Default browser-package list mirrors \`.claude/rules/test-environment-parity.md\`. The rule fires once per package on its \`src/index.ts\`, walks the package directory looking for \`*.browser.test.*\`, and reports if none are found. Off in \`app\` preset because apps don't ship as packages with smoke obligations. See also: no-process-dev-gate.`,
14610
14742
  mistakes: `- Adding a new browser-running package without a browser test — the rule will fail your PR
14611
14743
  - Hardcoding the browser-package list in the rule — the list lives in \`.claude/rules/browser-packages.json\` (single source of truth), not in the rule source
14612
14744
  - Disabling the rule globally — use \`exemptPaths\` to exempt specific packages still under construction
@@ -14618,11 +14750,84 @@ if (__DEV__) console.warn('hello')`,
14618
14750
  // "which Pyreon packages are missing browser smoke coverage?"
14619
14751
  // Tool walks packages/, matches against .claude/rules/browser-packages.json,
14620
14752
  // returns a coverage report.`,
14621
- notes: "Companion to the `pyreon/require-browser-smoke-test` lint rule. Reports which browser-categorized Pyreon packages have at least one `*.browser.test.{ts,tsx}` file under `src/`. Uses the same `.claude/rules/browser-packages.json` single source of truth as the rule + the CI script. Lets an AI agent check coverage before writing a new browser package (so it adds a smoke test in the same PR) instead of discovering the failure when CI runs. Falls back with a clear message if the JSON isn't present (e.g. consumer apps that don't ship the Pyreon monorepo layout).",
14753
+ notes: `Companion to the \`pyreon/require-browser-smoke-test\` lint rule. Reports which browser-categorized Pyreon packages have at least one \`*.browser.test.{ts,tsx}\` file under \`src/\`. Uses the same \`.claude/rules/browser-packages.json\` single source of truth as the rule + the CI script. Lets an AI agent check coverage before writing a new browser package (so it adds a smoke test in the same PR) instead of discovering the failure when CI runs. Falls back with a clear message if the JSON isn't present (e.g. consumer apps that don't ship the Pyreon monorepo layout). See also: audit_test_environment.`,
14622
14754
  mistakes: `- Using the tool's output as a substitute for running the CI script — this tool only checks file existence, not the self-expiring-exemption check that \`bun run lint:browser-smoke\` performs`
14623
14755
  },
14756
+ "mcp/get_api": {
14757
+ signature: "tool: get_api({ package: string; symbol: string }) → APIEntry",
14758
+ example: `// Agent-side
14759
+ get_api({ package: 'flow', symbol: 'createFlow' })
14760
+ get_api({ package: '@pyreon/router', symbol: 'useTypedSearchParams' })`,
14761
+ notes: `Look up any Pyreon API by \`package\` (e.g. \`"flow"\` or \`"@pyreon/flow"\`) and \`symbol\` (e.g. \`"createFlow"\`). Returns the canonical signature, example, foot-gun catalogue, and cross-references — drawn from \`api-reference.ts\`, which is regenerated from each package\'s \`manifest.ts\`. The single agent-facing entry point for "what does this API do and how do I avoid the common mistakes." See also: validate, get_pattern.`
14762
+ },
14763
+ "mcp/validate": {
14764
+ signature: "tool: validate({ code: string; filename?: string }) → Diagnostics[]",
14765
+ example: `validate({ code: \`
14766
+ function MyComp(props) {
14767
+ const { value } = props // → props-destructured
14768
+ return <For each={items}>{...}</For> // → for-missing-by
14769
+ }
14770
+ \` })`,
14771
+ notes: "Two AST-based detectors run in parallel: `detectReactPatterns` flags \"coming from React\" mistakes (`useState`, `useEffect`, `className`, `onChange` on inputs, React-package imports), and `detectPyreonPatterns` flags \"using Pyreon wrong\" mistakes (`<For>` missing `by`, props destructured at component signature, `typeof process` dev gates, raw `addEventListener`, `Date.now() + Math.random()` IDs). Diagnostics are merged + sorted by line / column for top-down reading. See also: get_anti_patterns, migrate_react."
14772
+ },
14773
+ "mcp/migrate_react": {
14774
+ signature: "tool: migrate_react({ code: string; filename?: string }) → MigrationResult",
14775
+ example: `migrate_react({ code: \`
14776
+ import { useState, useEffect } from 'react'
14777
+ function Counter() {
14778
+ const [count, setCount] = useState(0)
14779
+ useEffect(() => { console.log(count) }, [count])
14780
+ return <button onClick={() => setCount(count + 1)}>{count}</button>
14781
+ }
14782
+ \` })`,
14783
+ notes: "Convert React code to idiomatic Pyreon. Handles `useState` → `signal()`, `useEffect` → `effect()`, `className` → `class`, `onChange` → `onInput`, `useMemo` → `computed()`, React imports → Pyreon imports. Reports per-edit fixable diagnostics so callers can apply or review. See also: validate."
14784
+ },
14785
+ "mcp/diagnose": {
14786
+ signature: "tool: diagnose({ error: string }) → DiagnoseResult",
14787
+ example: `diagnose({ error: 'Cannot redefine property X on object [object Object]' })
14788
+ // → cause: configurable: false on a getter; fix: set configurable: true`,
14789
+ notes: "Parse a Pyreon runtime / build error message into structured fix information: probable cause, recommended fix, related docs, and the `.claude/rules/anti-patterns.md` entry (if any) the error matches. Useful when an agent sees a stack trace and wants to skip the \"search the codebase for similar errors\" step. See also: validate, get_anti_patterns."
14790
+ },
14791
+ "mcp/get_routes": {
14792
+ signature: "tool: get_routes() → Route[]",
14793
+ example: `get_routes()
14794
+ // → [{ path: '/', name: 'home', hasLoader: true, params: [] }, ...]`,
14795
+ notes: "List every route in the current project — path, loader presence, guards, params, and named-route name. Walks the project source from `process.cwd()` down. Cached per server instance with auto-invalidation on `cwd` change. See also: get_components."
14796
+ },
14797
+ "mcp/get_components": {
14798
+ signature: "tool: get_components() → ComponentInfo[]",
14799
+ example: `get_components()
14800
+ // → [{ name: 'Button', file: 'src/Button.tsx', props: ['onClick', 'children'], signals: ['count'] }, ...]`,
14801
+ notes: "List every component in the current project with its props and signal usage. Same scanner as `get_routes`. Useful for an agent before generating new code that needs to reference existing components. See also: get_routes."
14802
+ },
14803
+ "mcp/get_pattern": {
14804
+ signature: "tool: get_pattern({ name?: string }) → PatternBody | string[]",
14805
+ example: `get_pattern({ name: 'controllable-state' })
14806
+ // → full canonical pattern body
14807
+ get_pattern({})
14808
+ // → [{ name: 'controllable-state', summary: '...' }, ...]`,
14809
+ notes: "Fetch a canonical \"how do I do X\" pattern body from `docs/patterns/`. Eight foundational patterns ship: `dev-warnings`, `controllable-state`, `ssr-safe-hooks`, `signal-writes`, `keyed-lists`, `reactive-context`, `event-listeners`, `form-fields`. Omit `name` to list available patterns. Drop a new `docs/patterns/<slug>.md` file to add one — picked up on next call. See also: get_anti_patterns."
14810
+ },
14811
+ "mcp/get_anti_patterns": {
14812
+ signature: `tool: get_anti_patterns({ category?: 'reactivity' | 'jsx' | 'context' | 'architecture' | 'testing' | 'lifecycle' | 'documentation' | 'all' }) → AntiPattern[]`,
14813
+ example: `get_anti_patterns({ category: 'reactivity' })
14814
+ // → ['Bare signal in JSX text', 'Stale closures', 'Destructuring props', ...]`,
14815
+ notes: "Browse the anti-patterns catalog parsed from `.claude/rules/anti-patterns.md`. Each entry surfaces its `[detector: <code>]` tag inline so an agent can pair the catalog entry with the live static detector exposed by `validate`. Optional `category` filter; default returns all categories. See also: validate, get_pattern."
14816
+ },
14817
+ "mcp/get_changelog": {
14818
+ signature: "tool: get_changelog({ package?: string; limit?: number; includeDependencyUpdates?: boolean; since?: string }) → ChangelogEntry[]",
14819
+ example: `get_changelog({ package: 'flow', limit: 5 })
14820
+ get_changelog({ package: '@pyreon/router', since: '0.12.0' })`,
14821
+ notes: "Recent release notes for any `@pyreon/*` package without scraping `git log`. Parses `packages/**/CHANGELOG.md` into version entries (`{ version, changes[], dependencyUpdates[], empty }`) and returns the N most recent substantive versions (default 5). Filters out ceremonial version bumps (pure dependency-update releases with no user-facing body) by default — opt back in with `includeDependencyUpdates: true`. `since: \"0.12.0\"` returns the delta from a known floor — useful when an agent knows the version it was trained against. See also: get_api."
14822
+ },
14823
+ "mcp/audit_test_environment": {
14824
+ signature: `tool: audit_test_environment({ minRisk?: 'high' | 'medium' | 'low'; limit?: number }) → AuditReport`,
14825
+ example: `audit_test_environment({ minRisk: 'medium', limit: 10 })
14826
+ // → grouped report with HIGH / MEDIUM / LOW sections`,
14827
+ notes: `Scan every \`*.test.{ts,tsx}\` under \`packages/\` for the mock-vnode anti-pattern that caused PR #197\'s silent metadata drop. Files are classified HIGH / MEDIUM / LOW based on the balance of mock-vnode literals + helpers + helper-call sites vs real \`h()\` calls + \`@pyreon/core\` import. Three context-aware skips (helper-def vs binding discrimination, type-guard call-arg skip, template-string fixture mask) keep the false-positive rate low. Run before merging a new test file or after a framework change. See also: get_browser_smoke_status.`
14828
+ },
14624
14829
  "ui-core/PyreonUI": {
14625
- signature: "PyreonUI(props: { theme?: Theme; mode?: 'light' | 'dark' | 'system'; inversed?: boolean; children: VNodeChild }): VNodeChild",
14830
+ signature: `(props: { theme?: Theme; mode?: 'light' | 'dark' | 'system'; inversed?: boolean; children: VNodeChild }) => VNodeChild`,
14626
14831
  example: `import { PyreonUI } from "@pyreon/ui-core"
14627
14832
  import { enrichTheme } from "@pyreon/unistyle"
14628
14833
 
@@ -14634,18 +14839,22 @@ const theme = enrichTheme({ colors: { primary: "#3b82f6" } })
14634
14839
 
14635
14840
  // mode="system" auto-detects OS dark mode via prefers-color-scheme
14636
14841
  // inversed flips the resolved mode (light↔dark)`,
14637
- notes: "Unified provider replacing 3 separate providers (theme, mode, config). Calls init() internally. mode='system' uses matchMedia('(prefers-color-scheme: dark)') and reactively updates.",
14638
- mistakes: `- Using ThemeProvider + ModeProvider + ConfigProvider separately Use PyreonUI instead
14639
- - Forgetting enrichTheme() raw theme objects miss default breakpoints/spacing`
14842
+ notes: `Unified provider replacing the previous theme / mode / config split (3 nested providers became 1). Accepts an enriched \`theme\` object (merge with defaults via \`enrichTheme()\`), a \`mode\` of \`'light' | 'dark' | 'system'\`, and an optional \`inversed\` flip. When \`mode='system'\`, the provider subscribes to \`matchMedia('(prefers-color-scheme: dark)')\` and re-resolves the mode reactively. Calls \`init()\` internally so consumers don\'t need to wire it up themselves. Whole-theme swaps (user-preference themes) propagate through the styler resolver and re-resolve CSS without remounting the VNode. See also: useMode, enrichTheme, init.`,
14843
+ mistakes: `- Using \`ThemeProvider\` + \`ModeProvider\` + \`ConfigProvider\` separately \`PyreonUI\` is the single replacement covering all three
14844
+ - Forgetting \`enrichTheme()\` raw theme objects miss default breakpoints / spacing / unit utilities
14845
+ - Destructuring \`props\` inside the provider — components run once; destructuring captures values at setup. Read \`props.mode\` lazily inside reactive scopes
14846
+ - Re-augmenting the \`ThemeDefault\` / \`StylesDefault\` interfaces in your app — \`@pyreon/ui-theme\` already augments them; double-augmentation throws TS2320`
14640
14847
  },
14641
14848
  "ui-core/useMode": {
14642
- signature: "useMode(): Signal<'light' | 'dark'>",
14849
+ signature: `useMode(): Signal<'light' | 'dark'>`,
14643
14850
  example: `import { useMode } from "@pyreon/ui-core"
14644
14851
 
14645
14852
  const mode = useMode()
14646
14853
  // mode() returns "light" or "dark" (resolved, reactive)
14647
14854
  // Reflects OS preference when PyreonUI mode="system"`,
14648
- notes: "Returns the resolved mode as a reactive signal. When mode='system', reflects the OS preference. When inversed is true, the mode is flipped."
14855
+ notes: `Returns the currently resolved mode as a reactive signal — \`'light'\` or \`'dark'\`. When the nearest \`PyreonUI\` ancestor uses \`mode='system'\`, the signal reflects the OS preference and updates when the user changes their system setting. When \`inversed\` is true on any ancestor, the mode is flipped before resolution. Component-scoped subscription — readers re-run only when the resolved mode actually changes. See also: PyreonUI.`,
14856
+ mistakes: `- Reading \`useMode()\` without calling it — the value is a \`Signal\`; use \`mode()\` to read
14857
+ - Using \`useMode()\` outside any \`PyreonUI\` ancestor — falls back to a default but loses the reactive system / inversed handling`
14649
14858
  },
14650
14859
  "unistyle/enrichTheme": {
14651
14860
  signature: "enrichTheme(theme: PartialTheme): Theme",
@@ -14657,15 +14866,97 @@ const theme = enrichTheme({
14657
14866
  })
14658
14867
 
14659
14868
  // Merges user overrides with default breakpoints, spacing, and units`,
14660
- notes: "Merges a partial theme with the full default theme (breakpoints, spacing, unit utilities). Always use when passing a theme to PyreonUI."
14869
+ notes: "Merge a partial theme with the full default theme (breakpoints, spacing, unit utilities, fallback colors). Always call this before passing a user theme to `PyreonUI` — raw theme objects miss the default breakpoints and spacing scale that the rest of the UI system reads from. Idempotent: enriching an already-enriched theme is a no-op. See also: breakpoints, createMediaQueries.",
14870
+ mistakes: `- Passing the raw partial theme to \`<PyreonUI theme={...}>\` without enriching — \`theme.breakpoints\` is undefined and every responsive prop falls back to the desktop value
14871
+ - Mutating the theme after passing it to \`PyreonUI\` — the styler resolver caches off the theme identity; clone + re-enrich for whole-theme swaps`
14872
+ },
14873
+ "unistyle/breakpoints": {
14874
+ signature: "breakpoints(): Breakpoints",
14875
+ example: `import { breakpoints } from '@pyreon/unistyle'
14876
+
14877
+ const bp = breakpoints()
14878
+ // { xs: 0, sm: 640, md: 768, lg: 1024, xl: 1280, xxl: 1536 }`,
14879
+ notes: "Return the default breakpoint set keyed by name (`xs`, `sm`, `md`, `lg`, `xl`, `xxl`) with min-width values in pixels. The same map is folded into `enrichTheme()` output, so most consumers read `theme.breakpoints` rather than calling this directly. Use it when you need the defaults outside a theme context (e.g. building a custom theme programmatically). See also: enrichTheme, createMediaQueries."
14880
+ },
14881
+ "unistyle/createMediaQueries": {
14882
+ signature: "createMediaQueries(breakpoints: Breakpoints): Record<string, string>",
14883
+ example: `import { createMediaQueries, breakpoints } from '@pyreon/unistyle'
14884
+
14885
+ const queries = createMediaQueries(breakpoints())
14886
+ // { xs: '@media (min-width: 0)', sm: '@media (min-width: 640px)', md: '@media (min-width: 768px)', ... }`,
14887
+ notes: "Build a record of media-query strings keyed by breakpoint name. Each value is a `min-width` query — `xs` is `(min-width: 0)`, `sm` becomes `(min-width: 640px)`, and so on. Used internally by `makeItResponsive()`; expose to consumers when they need to compose custom CSS-in-JS rules outside the responsive-prop pipeline. See also: breakpoints, makeItResponsive."
14888
+ },
14889
+ "unistyle/makeItResponsive": {
14890
+ signature: "makeItResponsive<T>(options: { value: T | T[] | Record<string, T>; property: string; theme: Theme }): string",
14891
+ example: `import { makeItResponsive } from '@pyreon/unistyle'
14892
+
14893
+ makeItResponsive({ value: 16, property: 'padding', theme })
14894
+ // → 'padding: 16px;'
14895
+
14896
+ makeItResponsive({ value: [8, 12, 16], property: 'padding', theme })
14897
+ // → 'padding: 8px; @media (min-width: 640px) { padding: 12px } @media (min-width: 768px) { padding: 16px }'
14898
+
14899
+ makeItResponsive({ value: { xs: 8, md: 16, xl: 24 }, property: 'padding', theme })
14900
+ // → '@media (min-width: 0) { padding: 8px } @media (min-width: 768px) { padding: 16px } @media (min-width: 1280px) { padding: 24px }'`,
14901
+ notes: "Resolve a responsive prop value to CSS for the current screen. Accepts three input shapes: single value (applies at all breakpoints), mobile-first array `[xs, sm, md, lg]` (each entry maps to the next breakpoint), or breakpoint object `{ xs: ..., md: ..., xl: ... }` (named keys map directly). The output is a CSS string with media queries already embedded; insert into a styled component template literal. See also: createMediaQueries, styles.",
14902
+ mistakes: `- Passing CSS-spec property names (\`borderTopWidth\`) — unistyle uses property-first naming (\`borderWidthTop\`); the responsive transformer expects the unistyle convention
14903
+ - Forgetting to pass an enriched theme — without \`theme.breakpoints\`, the array form falls back to the first value at every breakpoint`
14904
+ },
14905
+ "unistyle/styles": {
14906
+ signature: "styles(theme: Theme): string",
14907
+ example: `import { styles, enrichTheme } from '@pyreon/unistyle'
14908
+
14909
+ const theme = enrichTheme({ colors: { primary: '#3b82f6' } })
14910
+ const css = styles(theme)
14911
+ // → ':root { --color-primary: #3b82f6; --spacing-xs: 4px; ... }'`,
14912
+ notes: `Generate the CSS string for a complete theme — colors, spacing, fonts, breakpoints, the works. Used to produce the cascade of CSS variables / global declarations that backs every styled component. Most consumers don\'t call this directly; the \`PyreonUI\` provider invokes it internally on theme mount. See also: enrichTheme, extendCss.`
14913
+ },
14914
+ "unistyle/alignContent": {
14915
+ signature: `alignContent(options: { alignX?: AlignXKey; alignY?: AlignYKey; direction?: 'row' | 'column' | 'inline' | 'rows' }): string`,
14916
+ example: `import { alignContent } from '@pyreon/unistyle'
14917
+
14918
+ alignContent({ alignX: 'center', alignY: 'start', direction: 'row' })
14919
+ // → 'justify-content: center; align-items: flex-start;'
14920
+
14921
+ alignContent({ alignX: 'spaceBetween', direction: 'inline' })
14922
+ // → 'justify-content: space-between;'`,
14923
+ notes: `Resolve \`alignX\` / \`alignY\` / \`direction\` shorthand to the matching flex / grid CSS (\`justify-content\`, \`align-items\`). The Element / Row / Column primitives use this internally — it\'s exposed for custom layout components that want the same alignment semantics. \`direction: "inline"\` maps to \`row\`; \`direction: "rows"\` maps to \`column\`. See also: makeItResponsive.`
14924
+ },
14925
+ "unistyle/extendCss": {
14926
+ signature: "extendCss(base: ExtendCss, override?: ExtendCss): ExtendCss",
14927
+ example: `import { extendCss } from '@pyreon/unistyle'
14928
+
14929
+ const base = { color: 'red', hover: { color: 'darkred' } }
14930
+ const extended = extendCss(base, { hover: { background: 'pink' } })
14931
+ // → { color: 'red', hover: { color: 'darkred', background: 'pink' } }`,
14932
+ notes: "Extend a CSS definition (theme block, style descriptor) with overrides — deep-merges nested objects without losing the base. Used by rocketstyle dimension chains to layer dimension-specific CSS over a baseline. The base is not mutated; the result is a new object. See also: styles."
14933
+ },
14934
+ "unistyle/stripUnit": {
14935
+ signature: "stripUnit(value: string | number): number",
14936
+ example: `import { stripUnit } from '@pyreon/unistyle'
14937
+
14938
+ stripUnit('16px') // → 16
14939
+ stripUnit('1.5rem') // → 1.5
14940
+ stripUnit(16) // → 16`,
14941
+ notes: "Strip the unit suffix from a CSS value and return the numeric part (`\"16px\"` → `16`, `\"1.5rem\"` → `1.5`). Returns the input unchanged when already a number. Useful for arithmetic on theme values declared as strings (`\"16px\"`) without manually parsing. See also: value, values."
14942
+ },
14943
+ "unistyle/value": {
14944
+ signature: "value(input: PropertyValue, fallback?: PropertyValue): UnitValue",
14945
+ example: `import { value } from '@pyreon/unistyle'
14946
+
14947
+ value(16) // → { value: 16, unit: 'px' }
14948
+ value('1.5rem') // → { value: 1.5, unit: 'rem' }
14949
+ value('50%') // → { value: 50, unit: '%' }
14950
+ value('garbage', 0) // → { value: 0, unit: 'px' }`,
14951
+ notes: "Parse and validate a single property value into a `UnitValue` shape (`{ value, unit }`). Accepts numbers (treated as pixels), strings with units (`\"16px\"`, `\"1rem\"`, `\"50%\"`), or objects already in `UnitValue` form. Optional `fallback` is returned when the input is invalid. The companion `values()` does the same over an array. See also: stripUnit, values."
14661
14952
  },
14662
14953
  "rx/rx": {
14663
- signature: "Readonly<{ filter, map, sortBy, groupBy, keyBy, uniqBy, take, skip, last, chunk, flatten, find, mapValues, count, sum, min, max, average, distinct, scan, combine, debounce, throttle, search, pipe }>",
14954
+ signature: "Readonly<{ filter, map, sortBy, groupBy, keyBy, uniqBy, take, skip, last, chunk, flatten, find, mapValues, first, compact, reverse, partition, takeWhile, dropWhile, unique, sample, count, sum, min, max, average, reduce, every, some, distinct, scan, combine, zip, merge, debounce, throttle, search, pipe }>",
14664
14955
  example: `const active = rx.filter(users, u => u.active) // Computed<User[]>
14665
14956
  const sorted = rx.sortBy(active, 'name') // Computed<User[]>
14666
14957
  const total = rx.sum(users, u => u.age) // Computed<number>
14667
14958
  const grouped = rx.groupBy(users, u => u.department) // Computed<Map<string, User[]>>`,
14668
- notes: "Namespaced object exposing all 24 reactive transform functions plus `pipe`. Use `rx.filter(...)` for dot-notation style, or destructure individual functions for tree-shaking. Every function is overloaded: `Signal<T[]>` input produces `Computed<T[]>` that auto-tracks, plain `T[]` input produces a static result. See also: pipe, filter.",
14959
+ notes: "Namespaced object exposing all 37 reactive transform functions plus `pipe`. Use `rx.filter(...)` for dot-notation style, or destructure individual functions for tree-shaking. Every function is overloaded: `Signal<T[]>` input produces `Computed<T[]>` that auto-tracks, plain `T[]` input produces a static result. See also: pipe, filter.",
14669
14960
  mistakes: `- Expecting \`rx.filter(signal, pred)\` to return a plain array — signal inputs always produce \`Computed\` outputs. Call the result to read: \`active()\`
14670
14961
  - Passing a signal accessor (\`() => items()\`) instead of the signal itself — pass \`items\` not \`() => items()\`; the function checks for \`.subscribe\` to detect signals`
14671
14962
  },
@@ -14759,39 +15050,22 @@ setUrlRouter(router)
14759
15050
  } from '@pyreon/document-primitives'
14760
15051
  import { download } from '@pyreon/document'
14761
15052
 
14762
- interface Resume { name: string; headline: string }
14763
-
14764
- function ResumeTemplate(props: { resume: () => Resume }) {
14765
- return (
14766
- // title and author accept reactive accessors — extractDocNode
14767
- // resolves them at extraction time, so each export click reads
14768
- // the LIVE value from the underlying signal
14769
- <DocDocument
14770
- title={() => \`\${props.resume().name} — Resume\`}
14771
- author={() => props.resume().name}
14772
- >
14773
- <DocPage>
14774
- <DocHeading level="h1">{() => props.resume().name}</DocHeading>
14775
- <DocText>{() => props.resume().headline}</DocText>
14776
- </DocPage>
14777
- </DocDocument>
14778
- )
14779
- }
14780
-
14781
- // One-step extraction. The two-step createDocumentExport(...).getDocNode()
14782
- // form is still exported for callers that want to pass the helper
14783
- // object around, but extractDocNode is the recommended form.
14784
- const tree = extractDocNode(() => <ResumeTemplate resume={store.resume} />)
14785
- await download(tree, 'resume.pdf')
14786
- await download(tree, 'resume.docx')
14787
- await download(tree, 'resume.html')
14788
- await download(tree, 'resume.md')`,
14789
- notes: "18 primitives: DocDocument, DocPage, DocSection, DocRow, DocColumn, DocHeading, DocText, DocLink, DocImage, DocTable, DocList, DocListItem, DocCode, DocDivider, DocSpacer, DocButton, DocQuote, DocPageBreak. Same component tree renders in browser AND exports — primitives carry _documentType statics that extractDocumentTree (from @pyreon/connector-document) walks to produce a DocNode for @pyreon/document's render() to consume. DocDocument's title/author/subject accept either a string OR a `() => string` accessor; function values are stored in _documentProps and resolved at extraction time so reactive metadata works without `const initial = get()` workarounds. PR #197 also fixed a latent bug in extractDocumentTree: it now CALLS rocketstyle component functions to read post-attrs _documentProps, where before it only looked at the JSX vnode's props directly — every primitive's metadata was silently dropped during export until that fix landed.",
14790
- mistakes: `- Calling props.title() at the top of a template body to "fix" reactivity — components run ONCE at mount, so this captures the initial value forever. Pass the accessor through to DocDocument as-is: <DocDocument title={() => get().name}>
14791
- - DocRow direction: layout props (direction, gap) go in .attrs() not .theme(). Element accepts 'inline' | 'rows' | 'reverseInline' | 'reverseRows' — 'row' is NOT valid
14792
- - For text children reactivity, pass a signal accessor and read inside body: <DocText>{() => store.field()}</DocText>
14793
- - Don't declare runtime-filled fields (tag, _documentProps) in the rocketstyle .attrs<P>() generic — they leak as required JSX props
14794
- - Using createDocumentExport(...).getDocNode() in new code — prefer extractDocNode(fn) which is one call instead of two. createDocumentExport is kept for backward compat`
15053
+ const tree = extractDocNode(() => (
15054
+ <DocDocument title="Quarterly Report" author="Aisha">
15055
+ <DocPage>
15056
+ <DocHeading level="h1">Q4 Results</DocHeading>
15057
+ <DocText>Revenue grew 23% YoY.</DocText>
15058
+ </DocPage>
15059
+ </DocDocument>
15060
+ ))
15061
+ await download(tree, 'report.pdf')
15062
+ await download(tree, 'report.docx')`,
15063
+ notes: `18 primitives: \`DocDocument\`, \`DocPage\`, \`DocSection\`, \`DocRow\`, \`DocColumn\`, \`DocHeading\`, \`DocText\`, \`DocLink\`, \`DocImage\`, \`DocTable\`, \`DocList\`, \`DocListItem\`, \`DocCode\`, \`DocDivider\`, \`DocSpacer\`, \`DocButton\`, \`DocQuote\`, \`DocPageBreak\`. Same component tree renders in browser AND exports — primitives carry \`_documentType\` statics that \`extractDocumentTree\` (from \`@pyreon/connector-document\`) walks to produce a \`DocNode\` for \`@pyreon/document\`\'s \`render()\` to consume. \`DocDocument\`\'s \`title\` / \`author\` / \`subject\` accept either a string OR a \`() => string\` accessor; function values are stored in \`_documentProps\` and resolved at extraction time so reactive metadata works without \`const initial = get()\` workarounds. PR #197 also fixed a latent bug in \`extractDocumentTree\`: it now CALLS rocketstyle component functions to read post-attrs \`_documentProps\`, where before it only looked at the JSX vnode\'s props directly — every primitive\'s metadata was silently dropped during export until that fix landed. See also: createDocumentExport.`,
15064
+ mistakes: `- Calling \`props.title()\` at the top of a template body to "fix" reactivity — components run ONCE at mount, so this captures the initial value forever. Pass the accessor through to DocDocument as-is: \`<DocDocument title={() => get().name}>\`
15065
+ - DocRow direction: layout props (direction, gap) go in \`.attrs()\` not \`.theme()\`. Element accepts \`'inline'\` | \`'rows'\` | \`'reverseInline'\` | \`'reverseRows'\` — \`'row'\` is NOT valid
15066
+ - For text children reactivity, pass a signal accessor and read inside body: \`<DocText>{() => store.field()}</DocText>\`
15067
+ - Don't declare runtime-filled fields (\`tag\`, \`_documentProps\`) in the rocketstyle \`.attrs<P>()\` generic — they leak as required JSX props
15068
+ - Using \`createDocumentExport(...).getDocNode()\` in new code — prefer \`extractDocNode(fn)\` which is one call instead of two. \`createDocumentExport\` is kept for backward compat`
14795
15069
  },
14796
15070
  "document-primitives/createDocumentExport": {
14797
15071
  signature: "createDocumentExport(templateFn: () => VNode): { getDocNode(): DocNode }",
@@ -14801,10 +15075,651 @@ import { createDocumentExport } from '@pyreon/document-primitives'
14801
15075
 
14802
15076
  const helper = createDocumentExport(() => <Resume name="Aisha" />)
14803
15077
  const tree = helper.getDocNode()`,
14804
- notes: "Wrapper around extractDocNode. The wrapper-object form is kept for callers that want to pass the helper around (e.g. to wrapper components that take a DocumentExport instance). New code should use extractDocNode(templateFn) which is one call instead of two."
15078
+ notes: "Wrapper around `extractDocNode`. The wrapper-object form is kept for callers that want to pass the helper around (e.g. to wrapper components that take a `DocumentExport` instance). New code should use `extractDocNode(templateFn)` which is one call instead of two. See also: extractDocNode."
15079
+ },
15080
+ "document-primitives/DocDocument": {
15081
+ signature: "(props: { title?: string | (() => string); author?: string | (() => string); subject?: string | (() => string); children: VNodeChild }) => VNodeChild",
15082
+ example: `<DocDocument title="Quarterly Report" author="Aisha" subject="Q4 2025">
15083
+ <DocPage>...</DocPage>
15084
+ </DocDocument>
15085
+
15086
+ // Reactive metadata via accessor
15087
+ <DocDocument title={() => \`\${user().name} — Resume\`}>
15088
+ <DocPage>...</DocPage>
15089
+ </DocDocument>`,
15090
+ notes: "Root container for a document tree — produces a `_documentType: \"document\"` node. Accepts optional metadata: `title`, `author`, `subject`. Each accepts either a plain string OR a `() => string` accessor; function values are stored in `_documentProps` and resolved at extraction time so each export call reads the LIVE value from any underlying signal. See also: DocPage, extractDocNode."
15091
+ },
15092
+ "document-primitives/DocPage": {
15093
+ signature: `(props: { size?: string; orientation?: 'portrait' | 'landscape'; children: VNodeChild }) => VNodeChild`,
15094
+ example: `<DocDocument>
15095
+ <DocPage size="A4" orientation="portrait">
15096
+ <DocHeading level="h1">Page 1</DocHeading>
15097
+ </DocPage>
15098
+ <DocPage size="A4" orientation="landscape">
15099
+ <DocHeading level="h1">Page 2 — landscape</DocHeading>
15100
+ </DocPage>
15101
+ </DocDocument>`,
15102
+ notes: "A page boundary inside a `DocDocument`. Paginated outputs (PDF, DOCX) treat each `DocPage` as a separate page; flow outputs (HTML, Markdown) render the contents inline with no page boundary. `size` and `orientation` configure paginated formats — common values: `\"A4\"`, `\"Letter\"`, `\"Legal\"`. See also: DocDocument, DocPageBreak."
15103
+ },
15104
+ "document-primitives/DocSection": {
15105
+ signature: `(props: { direction?: 'column' | 'row'; children: VNodeChild }) => VNodeChild`,
15106
+ example: `<DocPage>
15107
+ <DocSection direction="column">
15108
+ <DocHeading level="h2">Introduction</DocHeading>
15109
+ <DocText>Background paragraph.</DocText>
15110
+ </DocSection>
15111
+ </DocPage>`,
15112
+ notes: "Semantic grouping inside a page. Default `direction` is `\"column\"` (children stack vertically); `\"row\"` arranges them horizontally. Use to group related content for visual rhythm and for export targets that emit semantic section markers (HTML `<section>`, DOCX section breaks). See also: DocRow, DocColumn."
15113
+ },
15114
+ "document-primitives/DocRow": {
15115
+ signature: "(props: { children: VNodeChild }) => VNodeChild",
15116
+ example: `<DocRow>
15117
+ <DocText>Name:</DocText>
15118
+ <DocText>Aisha Patel</DocText>
15119
+ </DocRow>`,
15120
+ notes: "Horizontal layout container — children flow inline with a fixed 8px gap. Use for side-by-side content (label + value pairs, columns of metadata, button rows). Layout-only — no user-configurable props on this primitive; for columns with custom widths use `DocColumn` inside. See also: DocColumn, DocSection."
15121
+ },
15122
+ "document-primitives/DocColumn": {
15123
+ signature: "(props: { width?: number | string; children: VNodeChild }) => VNodeChild",
15124
+ example: `<DocRow>
15125
+ <DocColumn width="30%">
15126
+ <DocText>Label</DocText>
15127
+ </DocColumn>
15128
+ <DocColumn width="70%">
15129
+ <DocText>Value</DocText>
15130
+ </DocColumn>
15131
+ </DocRow>`,
15132
+ notes: `A column inside a row layout. Optional \`width\` controls the column\'s share of the row — accepts a number (interpreted as pixels) or a string (\`"50%"\`, \`"1fr"\`). When omitted, columns share available width equally. Most common shape is \`<DocRow><DocColumn width="30%" /> <DocColumn width="70%" /></DocRow>\`. See also: DocRow, DocSection.`
15133
+ },
15134
+ "document-primitives/DocHeading": {
15135
+ signature: `(props: { level?: 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6'; children: VNodeChild }) => VNodeChild`,
15136
+ example: `<DocHeading level="h1">Quarterly Report</DocHeading>
15137
+ <DocHeading level="h2">Q4 Results</DocHeading>
15138
+ <DocHeading level="h3">Revenue Breakdown</DocHeading>`,
15139
+ notes: "Heading text — `level` (`\"h1\"` through `\"h6\"`) controls both visual size and the semantic level emitted to outputs (HTML `<h1>...<h6>`, DOCX heading styles, Markdown `#`...`######`). Default `level` is `\"h1\"`. Used for document structure that downstream tooling can build a TOC from. See also: DocText, DocSection."
15140
+ },
15141
+ "document-primitives/DocText": {
15142
+ signature: "(props: { children: VNodeChild }) => VNodeChild",
15143
+ example: `<DocText>Static paragraph content.</DocText>
15144
+
15145
+ // Reactive children
15146
+ <DocText>{() => \`Hello, \${user().name}\`}</DocText>`,
15147
+ notes: "Paragraph / inline text. The most common primitive — wraps any text content for the document. Children may be string literals OR signal accessors (`{() => store.field()}`) for reactive content. Visual styling (font weight, variant) is controlled via rocketstyle dimension props on the wrapping component definition. See also: DocHeading, DocLink."
15148
+ },
15149
+ "document-primitives/DocLink": {
15150
+ signature: "(props: { href?: string; children: VNodeChild }) => VNodeChild",
15151
+ example: `<DocText>
15152
+ Read more on
15153
+ <DocLink href="https://pyreon.dev">our blog</DocLink>
15154
+ for the latest releases.
15155
+ </DocText>`,
15156
+ notes: "Hyperlink within text. `href` is the URL — defaults to `\"#\"`. Outputs that support hyperlinks (HTML, PDF, DOCX, email) render this as a clickable link; flat outputs (plain text, certain Slack variants) render the link target inline as `text (href)`. See also: DocText."
15157
+ },
15158
+ "document-primitives/DocImage": {
15159
+ signature: "(props: { src: string; alt?: string; width?: number; height?: number; caption?: string }) => VNodeChild",
15160
+ example: `<DocImage
15161
+ src="/charts/q4-revenue.png"
15162
+ alt="Revenue grew 23% in Q4"
15163
+ width={600}
15164
+ height={400}
15165
+ caption="Figure 1: Quarterly revenue, 2024-2025"
15166
+ />`,
15167
+ notes: "An image embedded in the document. `src` is the image URL or data URI. `alt` is the accessible description (also used as fallback text in non-visual outputs). `width` / `height` constrain dimensions in pixels. Optional `caption` renders a caption beneath the image. See also: DocCode."
15168
+ },
15169
+ "document-primitives/DocTable": {
15170
+ signature: "(props: { columns: TableColumn[]; rows: TableRow[]; headerStyle?: object; striped?: boolean; bordered?: boolean; caption?: string }) => VNodeChild",
15171
+ example: `<DocTable
15172
+ caption="Q4 results by region"
15173
+ bordered
15174
+ striped
15175
+ columns={[
15176
+ { key: 'region', label: 'Region', align: 'left' },
15177
+ { key: 'revenue', label: 'Revenue', align: 'right' },
15178
+ { key: 'growth', label: 'YoY Growth', align: 'right' },
15179
+ ]}
15180
+ rows={[
15181
+ { region: 'NA', revenue: '$12.4M', growth: '+23%' },
15182
+ { region: 'EU', revenue: '$8.7M', growth: '+18%' },
15183
+ { region: 'APAC', revenue: '$5.1M', growth: '+41%' },
15184
+ ]}
15185
+ />`,
15186
+ notes: "Tabular data. `columns` defines the header cells (label, key, optional alignment). `rows` is an array of data rows keyed by column key. `striped` adds alternating row backgrounds; `bordered` adds cell borders; `caption` renders an accessible table caption. Both `rows` and `columns` are filtered before reaching the DOM via `.attrs(..., { filter: [...] })` because `HTMLTableElement.rows` / `.cells` are read-only DOM properties — assignment would crash. See also: DocList, DocSection."
15187
+ },
15188
+ "document-primitives/DocList": {
15189
+ signature: "(props: { ordered?: boolean; children: VNodeChild }) => VNodeChild",
15190
+ example: `<DocList>
15191
+ <DocListItem>First bullet</DocListItem>
15192
+ <DocListItem>Second bullet</DocListItem>
15193
+ </DocList>
15194
+
15195
+ <DocList ordered>
15196
+ <DocListItem>First step</DocListItem>
15197
+ <DocListItem>Second step</DocListItem>
15198
+ </DocList>`,
15199
+ notes: "Bulleted (default) or numbered (`ordered`) list. Children are typically `DocListItem` instances. Outputs map this to the right native list type — HTML `<ul>` / `<ol>`, Markdown `-` / `1.`, DOCX list styles. See also: DocListItem."
15200
+ },
15201
+ "document-primitives/DocListItem": {
15202
+ signature: "(props: { children: VNodeChild }) => VNodeChild",
15203
+ example: `<DocList>
15204
+ <DocListItem>Top-level item</DocListItem>
15205
+ <DocListItem>
15206
+ Item with nested list
15207
+ <DocList>
15208
+ <DocListItem>Nested A</DocListItem>
15209
+ <DocListItem>Nested B</DocListItem>
15210
+ </DocList>
15211
+ </DocListItem>
15212
+ </DocList>`,
15213
+ notes: `Single item inside a \`DocList\`. Children may be plain text, \`DocText\`, nested \`DocList\` for sublists, or any other inline primitive. Visual marker (bullet vs number) is decided by the parent list\'s \`ordered\` prop, not by the item. See also: DocList.`
15214
+ },
15215
+ "document-primitives/DocCode": {
15216
+ signature: "(props: { language?: string; children: VNodeChild }) => VNodeChild",
15217
+ example: `<DocCode language="typescript">{
15218
+ \`const flow = createFlow({
15219
+ nodes: [{ id: '1', position: { x: 0, y: 0 } }],
15220
+ edges: [],
15221
+ })\`
15222
+ }</DocCode>`,
15223
+ notes: "Monospace code block. Optional `language` hint enables syntax highlighting in outputs that support it (HTML via Prism / Shiki, Markdown fenced code blocks with language tag). Whitespace is preserved verbatim — pass code as a single string child to keep newlines. See also: DocText."
15224
+ },
15225
+ "document-primitives/DocDivider": {
15226
+ signature: "(props: { color?: string; thickness?: number }) => VNodeChild",
15227
+ example: `<DocText>Above the divider.</DocText>
15228
+ <DocDivider color="#e5e7eb" thickness={1} />
15229
+ <DocText>Below the divider.</DocText>`,
15230
+ notes: "Horizontal rule — visual section separator. `color` controls the line color (any CSS color string); `thickness` controls the line thickness in pixels. Outputs map this to native dividers — HTML `<hr>`, Markdown `---`, DOCX horizontal rule. See also: DocSpacer."
15231
+ },
15232
+ "document-primitives/DocSpacer": {
15233
+ signature: "(props: { height?: number }) => VNodeChild",
15234
+ example: `<DocSection>
15235
+ <DocHeading level="h2">Section A</DocHeading>
15236
+ <DocText>Content...</DocText>
15237
+ <DocSpacer height={32} />
15238
+ <DocHeading level="h2">Section B</DocHeading>
15239
+ <DocText>More content...</DocText>
15240
+ </DocSection>`,
15241
+ notes: "Vertical whitespace — adds a blank vertical gap. `height` is in pixels (default 16). Use to space out content beyond what `DocSection` / `DocPage` margins provide. In flow outputs this becomes a styled blank block; in plain-text outputs, a sequence of newlines. See also: DocDivider."
15242
+ },
15243
+ "document-primitives/DocButton": {
15244
+ signature: "(props: { href?: string; children: VNodeChild }) => VNodeChild",
15245
+ example: `<DocButton href="https://pyreon.dev/signup">
15246
+ Get started
15247
+ </DocButton>`,
15248
+ notes: "Call-to-action button. Renders as a styled clickable element in HTML / email outputs (mail-safe button table layout for email), and as a labeled link in PDF / DOCX. `href` is the action URL — defaults to `\"#\"`. Visual style (variant) is controlled via rocketstyle dimensions on the component definition. See also: DocLink."
15249
+ },
15250
+ "document-primitives/DocQuote": {
15251
+ signature: "(props: { borderColor?: string; children: VNodeChild }) => VNodeChild",
15252
+ example: `<DocQuote borderColor="#3b82f6">
15253
+ <DocText>"The best way to predict the future is to build it."</DocText>
15254
+ <DocText>— Aisha Patel, Q4 keynote</DocText>
15255
+ </DocQuote>`,
15256
+ notes: "Block quote — sets off a quoted passage with an indented left border. `borderColor` controls the indicator stripe (any CSS color). Outputs map this to native quote styling — HTML `<blockquote>`, Markdown `> ...`, DOCX quote style. See also: DocText."
15257
+ },
15258
+ "document-primitives/DocPageBreak": {
15259
+ signature: "() => VNodeChild",
15260
+ example: `<DocPage>
15261
+ <DocHeading level="h1">Section 1</DocHeading>
15262
+ <DocText>...long content...</DocText>
15263
+ <DocPageBreak />
15264
+ <DocHeading level="h1">Section 2 — new page</DocHeading>
15265
+ </DocPage>`,
15266
+ notes: "Explicit page boundary inside a `DocPage`. Forces the renderer to start a new page at this point in paginated outputs (PDF, DOCX). In flow outputs (HTML, Markdown), it renders as visible whitespace or is omitted entirely. Use for explicit pagination control beyond what `DocPage` boundaries already provide. See also: DocPage."
14805
15267
  }
14806
15268
  };
14807
15269
 
15270
+ //#endregion
15271
+ //#region src/changelog.ts
15272
+ /**
15273
+ * Changelog parser + registry for the `get_changelog` MCP tool (T2.5.8).
15274
+ *
15275
+ * AI agents often ask "what changed in @pyreon/X recently?" before
15276
+ * writing code against the package — a stale mental model is a
15277
+ * frequent bug source. `get_changelog` surfaces recent release notes
15278
+ * without the agent having to scrape `git log` or read raw markdown.
15279
+ *
15280
+ * The parser reads `CHANGELOG.md` files populated by changesets. Each
15281
+ * changeset-managed package has a predictable shape:
15282
+ *
15283
+ * # @pyreon/<name>
15284
+ *
15285
+ * ## <version>
15286
+ *
15287
+ * ### Minor Changes | Patch Changes | Major Changes
15288
+ *
15289
+ * - [#PR] [`sha`] Thanks [@author]! - <body>
15290
+ *
15291
+ * <continuation>
15292
+ *
15293
+ * - Updated dependencies [[`sha`]]:
15294
+ * - @pyreon/core@0.13.0
15295
+ *
15296
+ * The parser extracts each `## <version>` section, collecting the
15297
+ * user-facing body plus the dependency bumps separately. Consumers
15298
+ * typically want the N most recent non-empty versions (the tool's
15299
+ * default is 5).
15300
+ */
15301
+ function findMonorepoRoot(startDir) {
15302
+ let dir = resolve(startDir);
15303
+ for (let i = 0; i < 30; i++) {
15304
+ if (existsSync(join(dir, "packages")) && statSync(join(dir, "packages")).isDirectory()) return dir;
15305
+ const parent = dirname(dir);
15306
+ if (parent === dir) return null;
15307
+ dir = parent;
15308
+ }
15309
+ return null;
15310
+ }
15311
+ function walkPackages(dir, out, depth = 0) {
15312
+ if (depth > 4) return;
15313
+ let entries;
15314
+ try {
15315
+ entries = readdirSync(dir);
15316
+ } catch {
15317
+ return;
15318
+ }
15319
+ for (const name of entries) {
15320
+ if (name.startsWith(".") || name === "node_modules") continue;
15321
+ const full = join(dir, name);
15322
+ let isDir = false;
15323
+ try {
15324
+ isDir = statSync(full).isDirectory();
15325
+ } catch {
15326
+ continue;
15327
+ }
15328
+ if (!isDir) continue;
15329
+ const pkgJsonPath = join(full, "package.json");
15330
+ const changelogPath = join(full, "CHANGELOG.md");
15331
+ if (existsSync(pkgJsonPath) && existsSync(changelogPath)) {
15332
+ try {
15333
+ const pkg = JSON.parse(readFileSync(pkgJsonPath, "utf8"));
15334
+ if (typeof pkg.name === "string") out.push({
15335
+ name: pkg.name,
15336
+ dir: full,
15337
+ changelogPath
15338
+ });
15339
+ } catch {}
15340
+ continue;
15341
+ }
15342
+ if (existsSync(pkgJsonPath)) continue;
15343
+ walkPackages(full, out, depth + 1);
15344
+ }
15345
+ }
15346
+ /**
15347
+ * Parse a CHANGELOG.md body into version entries. The parser assumes
15348
+ * the changesets format but tolerates:
15349
+ * - empty `## <version>` sections with no body (ceremonial bumps)
15350
+ * - mixed `### Patch Changes` / `### Minor Changes` / `### Major Changes` under one version
15351
+ * - bullets with multi-line continuations (indented with 2+ spaces)
15352
+ * - `- Updated dependencies …` bullets, split off separately
15353
+ */
15354
+ function parseChangelog(body) {
15355
+ const lines = body.split("\n");
15356
+ const entries = [];
15357
+ let currentVersion = null;
15358
+ let currentBullets = [];
15359
+ let currentDepUpdates = [];
15360
+ let currentBuf = [];
15361
+ let bufKind = null;
15362
+ const flushBullet = () => {
15363
+ if (currentBuf.length > 0 && bufKind !== null) {
15364
+ const text = currentBuf.join("\n").trim();
15365
+ if (text.length > 0) if (bufKind === "dep") currentDepUpdates.push(text);
15366
+ else currentBullets.push(text);
15367
+ }
15368
+ currentBuf = [];
15369
+ bufKind = null;
15370
+ };
15371
+ const flushVersion = () => {
15372
+ flushBullet();
15373
+ if (currentVersion !== null) entries.push({
15374
+ version: currentVersion,
15375
+ changes: currentBullets,
15376
+ dependencyUpdates: currentDepUpdates,
15377
+ empty: currentBullets.length === 0 && currentDepUpdates.length === 0
15378
+ });
15379
+ currentVersion = null;
15380
+ currentBullets = [];
15381
+ currentDepUpdates = [];
15382
+ };
15383
+ for (const line of lines) {
15384
+ const versionMatch = /^## (.+)$/.exec(line);
15385
+ if (versionMatch) {
15386
+ flushVersion();
15387
+ currentVersion = versionMatch[1].trim();
15388
+ continue;
15389
+ }
15390
+ if (currentVersion === null) continue;
15391
+ if (/^### /.test(line)) {
15392
+ flushBullet();
15393
+ continue;
15394
+ }
15395
+ if (/^- /.test(line)) {
15396
+ flushBullet();
15397
+ currentBuf = [line.replace(/^- /, "")];
15398
+ bufKind = /^- Updated dependencies/.test(line) ? "dep" : "change";
15399
+ continue;
15400
+ }
15401
+ if (bufKind !== null && line.length > 0 && /^\s/.test(line)) {
15402
+ currentBuf.push(line.replace(/^ {2,4}/, ""));
15403
+ continue;
15404
+ }
15405
+ if (bufKind !== null && line === "") {
15406
+ currentBuf.push("");
15407
+ continue;
15408
+ }
15409
+ flushBullet();
15410
+ }
15411
+ flushVersion();
15412
+ return entries;
15413
+ }
15414
+ function loadChangelogRegistry(startDir = process.cwd()) {
15415
+ const root = findMonorepoRoot(startDir);
15416
+ if (!root) return {
15417
+ root: null,
15418
+ byName: /* @__PURE__ */ new Map()
15419
+ };
15420
+ const found = [];
15421
+ walkPackages(join(root, "packages"), found);
15422
+ const byName = /* @__PURE__ */ new Map();
15423
+ for (const { name, dir, changelogPath } of found) {
15424
+ let source;
15425
+ try {
15426
+ source = readFileSync(changelogPath, "utf8");
15427
+ } catch {
15428
+ continue;
15429
+ }
15430
+ byName.set(name, {
15431
+ packageName: name,
15432
+ path: changelogPath,
15433
+ dir,
15434
+ entries: parseChangelog(source)
15435
+ });
15436
+ }
15437
+ return {
15438
+ root,
15439
+ byName
15440
+ };
15441
+ }
15442
+ function findChangelog(registry, packageName) {
15443
+ if (registry.byName.has(packageName)) return registry.byName.get(packageName);
15444
+ if (!packageName.startsWith("@")) {
15445
+ const qualified = `@pyreon/${packageName}`;
15446
+ if (registry.byName.has(qualified)) return registry.byName.get(qualified);
15447
+ }
15448
+ return null;
15449
+ }
15450
+ function suggestChangelogs(registry, needle) {
15451
+ const lower = needle.toLowerCase();
15452
+ const matches = [];
15453
+ for (const name of registry.byName.keys()) if (name.toLowerCase().includes(lower)) matches.push(name);
15454
+ return matches.slice(0, 5);
15455
+ }
15456
+ /**
15457
+ * Compare two semver-ish version strings. Returns negative if `a < b`,
15458
+ * positive if `a > b`, zero if equal. Pre-release suffixes (`-alpha.3`)
15459
+ * are compared lexicographically AFTER the numeric portion.
15460
+ *
15461
+ * Scoped to what changesets actually produce (`0.13.0`, `1.0.0-alpha.3`,
15462
+ * `2.5.1`). Not a general-purpose semver implementation — we intentionally
15463
+ * do not depend on an external semver package for a 30-line need.
15464
+ */
15465
+ function compareVersions(a, b) {
15466
+ const parse = (v) => {
15467
+ const [core, ...preParts] = v.split("-");
15468
+ const pre = preParts.join("-");
15469
+ return {
15470
+ parts: core.split(".").map((n) => {
15471
+ const num = Number.parseInt(n, 10);
15472
+ return Number.isNaN(num) ? 0 : num;
15473
+ }),
15474
+ pre
15475
+ };
15476
+ };
15477
+ const pa = parse(a);
15478
+ const pb = parse(b);
15479
+ const maxLen = Math.max(pa.parts.length, pb.parts.length);
15480
+ for (let i = 0; i < maxLen; i++) {
15481
+ const ai = pa.parts[i] ?? 0;
15482
+ const bi = pb.parts[i] ?? 0;
15483
+ if (ai !== bi) return ai - bi;
15484
+ }
15485
+ if (pa.pre === "" && pb.pre !== "") return 1;
15486
+ if (pa.pre !== "" && pb.pre === "") return -1;
15487
+ if (pa.pre < pb.pre) return -1;
15488
+ if (pa.pre > pb.pre) return 1;
15489
+ return 0;
15490
+ }
15491
+ /**
15492
+ * Filter entries to those strictly NEWER than `sinceVersion`. Ceremonial
15493
+ * bumps are preserved — the caller decides whether to also filter them
15494
+ * via the `empty` flag. Returns all entries in file order (newest first)
15495
+ * that satisfy `compareVersions(entry.version, sinceVersion) > 0`.
15496
+ */
15497
+ function filterSince(entries, sinceVersion) {
15498
+ return entries.filter((e) => compareVersions(e.version, sinceVersion) > 0);
15499
+ }
15500
+ /**
15501
+ * Format a package's changelog for the MCP response. Filters empty
15502
+ * versions and slices to the `limit` most recent.
15503
+ */
15504
+ function formatChangelog(changelog, { limit = 5, includeDependencyUpdates = false, since } = {}) {
15505
+ const nonEmpty = changelog.entries.filter((e) => !e.empty);
15506
+ const afterSince = since ? filterSince(nonEmpty, since) : nonEmpty;
15507
+ const sliced = afterSince.slice(0, limit);
15508
+ if (sliced.length === 0) {
15509
+ if (since) return `# ${changelog.packageName} — no changes since v${since}\n\nThe package has ${nonEmpty.length} substantive version entr${nonEmpty.length === 1 ? "y" : "ies"} in total, but none are newer than v${since}. The known latest substantive version is v${nonEmpty[0]?.version ?? "(none)"}. Drop the \`since\` filter, or pass a lower floor, to see earlier entries.`;
15510
+ const ceremonial = changelog.entries.length;
15511
+ const versions = changelog.entries.slice(0, 3).map((e) => e.version).join(", ");
15512
+ return `# ${changelog.packageName} — no substantive changes\n\nCHANGELOG.md has ${ceremonial} version entr${ceremonial === 1 ? "y" : "ies"} (${versions}${ceremonial > 3 ? ", …" : ""}) but every one is a ceremonial version bump with no user-facing body. This usually means the package only received dependency updates during the captured history. Check \`get_changelog({ includeDependencyUpdates: true })\` or the git log if you need the bumps themselves.`;
15513
+ }
15514
+ const parts = [];
15515
+ const sinceSuffix = since ? ` since v${since}` : "";
15516
+ parts.push(`# ${changelog.packageName} — changelog${sinceSuffix} (${sliced.length}/${afterSince.length} shown)`);
15517
+ parts.push("");
15518
+ if (afterSince.length > limit) {
15519
+ parts.push(`Showing the ${limit} most recent substantive versions${sinceSuffix}. ${afterSince.length - limit} older versions omitted. Pass \`limit\` to expand.`);
15520
+ parts.push("");
15521
+ }
15522
+ for (const entry of sliced) {
15523
+ parts.push(`## ${entry.version}`);
15524
+ parts.push("");
15525
+ for (const change of entry.changes) {
15526
+ parts.push(`- ${change}`);
15527
+ parts.push("");
15528
+ }
15529
+ if (includeDependencyUpdates && entry.dependencyUpdates.length > 0) {
15530
+ parts.push("### Updated dependencies");
15531
+ for (const dep of entry.dependencyUpdates) parts.push(`- ${dep}`);
15532
+ parts.push("");
15533
+ }
15534
+ }
15535
+ return parts.join("\n").trimEnd();
15536
+ }
15537
+ /**
15538
+ * Format the registry as an index when `get_changelog` is called
15539
+ * with no package name. Lists every package with its latest
15540
+ * substantive version (for orientation).
15541
+ */
15542
+ function formatChangelogIndex(registry) {
15543
+ if (!registry.root || registry.byName.size === 0) return "No changelogs found. This tool reads CHANGELOG.md files from the Pyreon monorepo. If you are running the MCP in a consumer project without the Pyreon packages directory, run the MCP from the Pyreon repo root to browse releases.";
15544
+ const names = [...registry.byName.keys()].sort();
15545
+ const parts = [`# Pyreon Changelogs (${names.length} packages)`, ""];
15546
+ parts.push(`Call \`get_changelog({ package: "<name>" })\` for per-package release notes. Pass \`limit\` to control how many versions (default 5). The short form \`foo\` maps to \`@pyreon/foo\`.`);
15547
+ parts.push("");
15548
+ for (const name of names) {
15549
+ const latest = registry.byName.get(name).entries.find((e) => !e.empty);
15550
+ const summary = latest ? `latest substantive: v${latest.version}` : "ceremonial bumps only";
15551
+ parts.push(`- **${name}** — ${summary}`);
15552
+ }
15553
+ return parts.join("\n");
15554
+ }
15555
+
15556
+ //#endregion
15557
+ //#region src/patterns.ts
15558
+ /**
15559
+ * Pattern registry for the `get_pattern` MCP tool (T2.5.3).
15560
+ *
15561
+ * Each pattern answers a "how do I do X the right way" question with a
15562
+ * code example and rationale. The content is the body of the
15563
+ * corresponding `docs/patterns/<name>.md` file, discovered at runtime
15564
+ * by walking up from `process.cwd()` to the nearest repo that contains
15565
+ * `docs/patterns/`.
15566
+ *
15567
+ * Why a filesystem lookup instead of bundled content: the patterns
15568
+ * belong in the VitePress site (they're first-class docs), and having
15569
+ * the MCP fetch them live means the AI sees the same text the human
15570
+ * would. Bundling copies would drift.
15571
+ *
15572
+ * Fallback: if no `docs/patterns/` exists in the walk (e.g. the MCP is
15573
+ * running in a consumer repo), the tool reports the miss and lists
15574
+ * what patterns WOULD be available if running against the Pyreon
15575
+ * monorepo. The list itself is seeded from the directory walk, so
15576
+ * adding a new pattern file makes it discoverable without code changes.
15577
+ */
15578
+ const PATTERN_PATH_CANDIDATES = [[
15579
+ "docs",
15580
+ "docs",
15581
+ "patterns"
15582
+ ], ["docs", "patterns"]];
15583
+ function findPatternsDir(startDir) {
15584
+ let dir = resolve(startDir);
15585
+ for (let i = 0; i < 30; i++) {
15586
+ for (const segments of PATTERN_PATH_CANDIDATES) {
15587
+ const candidate = join(dir, ...segments);
15588
+ if (existsSync(candidate) && statSync(candidate).isDirectory()) return candidate;
15589
+ }
15590
+ const parent = dirname(dir);
15591
+ if (parent === dir) return null;
15592
+ dir = parent;
15593
+ }
15594
+ return null;
15595
+ }
15596
+ /**
15597
+ * Minimal frontmatter parser. Supports:
15598
+ * title: string (unquoted or quoted)
15599
+ * summary: string
15600
+ * seeAlso: [one, two, three] OR seeAlso:\n - one\n - two
15601
+ *
15602
+ * Anything else is ignored. Full YAML would be overkill here.
15603
+ */
15604
+ function parseFrontmatter(source) {
15605
+ const match = /^---\n([\s\S]*?)\n---\n?([\s\S]*)$/.exec(source);
15606
+ if (!match) return {
15607
+ meta: {},
15608
+ body: source
15609
+ };
15610
+ const rawMeta = match[1];
15611
+ const body = match[2].trim();
15612
+ const meta = {};
15613
+ const lines = rawMeta.split("\n");
15614
+ let seeAlsoActive = false;
15615
+ const seeAlsoItems = [];
15616
+ for (const line of lines) {
15617
+ if (seeAlsoActive) {
15618
+ const bullet = /^\s*-\s*(.+?)\s*$/.exec(line);
15619
+ if (bullet) {
15620
+ seeAlsoItems.push(bullet[1]);
15621
+ continue;
15622
+ }
15623
+ seeAlsoActive = false;
15624
+ }
15625
+ const kv = /^([a-zA-Z]+):\s*(.*)$/.exec(line);
15626
+ if (!kv) continue;
15627
+ const key = kv[1];
15628
+ const value = kv[2].trim();
15629
+ if (key === "title") meta.title = value.replace(/^["']|["']$/g, "");
15630
+ else if (key === "summary") meta.summary = value.replace(/^["']|["']$/g, "");
15631
+ else if (key === "seeAlso") {
15632
+ if (value.startsWith("[") && value.endsWith("]")) meta.seeAlso = value.slice(1, -1).split(",").map((s) => s.trim().replace(/^["']|["']$/g, "")).filter(Boolean);
15633
+ else if (value === "") seeAlsoActive = true;
15634
+ }
15635
+ }
15636
+ if (seeAlsoActive && seeAlsoItems.length > 0) meta.seeAlso = seeAlsoItems;
15637
+ return {
15638
+ meta,
15639
+ body
15640
+ };
15641
+ }
15642
+ function extractFirstHeading(body) {
15643
+ for (const line of body.split("\n")) {
15644
+ const h = /^#\s+(.+)$/.exec(line);
15645
+ if (h) return h[1];
15646
+ }
15647
+ return null;
15648
+ }
15649
+ function loadPatternRegistry(startDir = process.cwd()) {
15650
+ const root = findPatternsDir(startDir);
15651
+ if (!root) return {
15652
+ root: null,
15653
+ patterns: []
15654
+ };
15655
+ const patterns = [];
15656
+ const entries = readdirSync(root).sort();
15657
+ for (const entry of entries) {
15658
+ if (!entry.endsWith(".md")) continue;
15659
+ if (entry.startsWith(".") || entry === "README.md" || entry === "index.md") continue;
15660
+ const filePath = join(root, entry);
15661
+ let source;
15662
+ try {
15663
+ source = readFileSync(filePath, "utf8");
15664
+ } catch {
15665
+ continue;
15666
+ }
15667
+ const { meta, body } = parseFrontmatter(source);
15668
+ const name = entry.replace(/\.md$/, "");
15669
+ const title = meta.title ?? extractFirstHeading(body) ?? name;
15670
+ patterns.push({
15671
+ name,
15672
+ path: filePath,
15673
+ body: source,
15674
+ title,
15675
+ summary: meta.summary ?? null,
15676
+ seeAlso: meta.seeAlso ?? []
15677
+ });
15678
+ }
15679
+ return {
15680
+ root,
15681
+ patterns
15682
+ };
15683
+ }
15684
+ /**
15685
+ * Format the registry as a short index listing for the "no arg" case.
15686
+ * Each entry: ` - slug — title (summary)`.
15687
+ */
15688
+ function formatPatternIndex(registry) {
15689
+ if (!registry.root || registry.patterns.length === 0) return "No patterns found. Patterns live at `docs/docs/patterns/<name>.md` (the VitePress content directory) in the Pyreon monorepo. If you are running the MCP in a consumer project, patterns are not available locally — run the MCP in the Pyreon repo to browse them.";
15690
+ const parts = [`# Pyreon Patterns (${registry.patterns.length})`, ""];
15691
+ parts.push("Call `get_pattern({ name: \"<slug>\" })` for the full body. Each pattern shows the canonical \"do it this way\" with code + rationale, plus the anti-pattern to avoid.");
15692
+ parts.push("");
15693
+ for (const p of registry.patterns) {
15694
+ const summary = p.summary ? ` — ${p.summary}` : "";
15695
+ parts.push(`- **${p.name}** — ${p.title}${summary}`);
15696
+ }
15697
+ return parts.join("\n");
15698
+ }
15699
+ /**
15700
+ * Format a single pattern's full body for the MCP response. Prepends
15701
+ * a breadcrumb and appends a cross-reference footer if `seeAlso` was
15702
+ * populated.
15703
+ */
15704
+ function formatPatternBody(pattern) {
15705
+ const parts = [pattern.body.trimEnd()];
15706
+ if (pattern.seeAlso.length > 0) {
15707
+ parts.push("");
15708
+ parts.push(`---\n\n**See also:** ${pattern.seeAlso.map((s) => `\`get_pattern({ name: "${s}" })\``).join(", ")}`);
15709
+ }
15710
+ return parts.join("\n");
15711
+ }
15712
+ function findPattern(registry, name) {
15713
+ for (const p of registry.patterns) if (p.name === name) return p;
15714
+ return null;
15715
+ }
15716
+ function suggestPatterns(registry, name) {
15717
+ const needle = name.toLowerCase();
15718
+ const matches = [];
15719
+ for (const p of registry.patterns) if (p.name.toLowerCase().includes(needle) || p.title.toLowerCase().includes(needle)) matches.push(p.name);
15720
+ return matches.slice(0, 5);
15721
+ }
15722
+
14808
15723
  //#endregion
14809
15724
  //#region src/index.ts
14810
15725
  /**
@@ -14821,204 +15736,292 @@ const tree = helper.getDocNode()`,
14821
15736
  * get_routes — List all routes in the current project
14822
15737
  * get_components — List all components with their props and signals
14823
15738
  * get_browser_smoke_status — Report which browser-categorized packages have smoke coverage
15739
+ * get_pattern — Fetch a "how do I do X" pattern body from docs/patterns/
15740
+ * get_anti_patterns — Browse the anti-patterns catalog, optionally filtered by category
15741
+ * get_changelog — Recent release notes for a @pyreon/* package, parsed from CHANGELOG.md
15742
+ * audit_test_environment — Scan test files for mock-vnode patterns (PR #197 bug class)
14824
15743
  *
14825
15744
  * Usage:
14826
15745
  * bunx @pyreon/mcp # stdio transport (for IDE integration)
14827
15746
  */
14828
- const server = new McpServer({
14829
- name: "pyreon",
14830
- version
14831
- });
14832
- let cachedContext = null;
14833
- let contextCwd = process.cwd();
14834
- function getContext() {
14835
- if (!cachedContext || contextCwd !== process.cwd()) {
14836
- contextCwd = process.cwd();
14837
- cachedContext = generateContext(contextCwd);
14838
- }
14839
- return cachedContext;
14840
- }
14841
15747
  function textResult(text) {
14842
15748
  return { content: [{
14843
15749
  type: "text",
14844
15750
  text
14845
15751
  }] };
14846
15752
  }
14847
- server.tool("get_api", {
14848
- package: z.string(),
14849
- symbol: z.string()
14850
- }, async ({ package: pkg, symbol }) => {
14851
- const entry = API_REFERENCE[`${pkg}/${symbol}`];
14852
- if (!entry) {
14853
- const suggestions = Object.keys(API_REFERENCE).filter((k) => k.toLowerCase().includes(symbol.toLowerCase())).slice(0, 5);
14854
- return textResult(`Symbol '${symbol}' not found in @pyreon/${pkg}.\n\n${suggestions.length > 0 ? `Did you mean one of these?\n${suggestions.map((s) => ` - ${s}`).join("\n")}` : "No similar symbols found."}`);
14855
- }
14856
- return textResult(`## @pyreon/${pkg} — ${symbol}\n\n**Signature:**\n\`\`\`typescript\n${entry.signature}\n\`\`\`\n\n**Usage:**\n\`\`\`typescript\n${entry.example}\n\`\`\`\n\n${entry.notes ? `**Notes:** ${entry.notes}\n\n` : ""}${entry.mistakes ? `**Common mistakes:**\n${entry.mistakes}\n` : ""}`);
14857
- });
14858
- server.tool("validate", {
14859
- code: z.string(),
14860
- filename: z.string().optional()
14861
- }, async ({ code, filename }) => {
14862
- const diagnostics = detectReactPatterns(code, filename ?? "snippet.tsx");
14863
- if (diagnostics.length === 0) return textResult("✓ No issues found. The code follows Pyreon patterns correctly.");
14864
- const issueText = diagnostics.map((d, i) => `${i + 1}. **${d.code}** (line ${d.line})\n ${d.message}\n Current: \`${d.current}\`\n Fix: \`${d.suggested}\`\n Auto-fixable: ${d.fixable ? "yes" : "no"}`).join("\n\n");
14865
- return textResult(`Found ${diagnostics.length} issue${diagnostics.length === 1 ? "" : "s"}:\n\n${issueText}`);
14866
- });
14867
- server.tool("migrate_react", {
14868
- code: z.string(),
14869
- filename: z.string().optional()
14870
- }, async ({ code, filename }) => {
14871
- const result = migrateReactCode(code, filename ?? "component.tsx");
14872
- const changeList = result.changes.map((c) => `- Line ${c.line}: ${c.description}`).join("\n");
14873
- const remainingIssues = result.diagnostics.filter((d) => !d.fixable);
14874
- const manualText = remainingIssues.length > 0 ? `\n\n**Remaining issues (manual fix needed):**\n${remainingIssues.map((d) => `- Line ${d.line}: ${d.message}\n Suggested: \`${d.suggested}\``).join("\n")}` : "";
14875
- return textResult(`## Migrated Code\n\n\`\`\`tsx\n${result.code}\n\`\`\`\n\n**Changes applied (${result.changes.length}):**\n${changeList || "No changes needed."}${manualText}`);
14876
- });
14877
- server.tool("diagnose", { error: z.string() }, async ({ error }) => {
14878
- const diagnosis = diagnoseError(error);
14879
- if (!diagnosis) return textResult(`Could not identify a Pyreon-specific pattern in this error.\n\nError: ${error}\n\nSuggestions:\n- Check for typos in variable/function names\n- Verify all imports are correct\n- Run \`bun run typecheck\` for full TypeScript diagnostics\n- Run \`pyreon doctor\` for project-wide health check`);
14880
- let text = `**Cause:** ${diagnosis.cause}\n\n**Fix:** ${diagnosis.fix}`;
14881
- if (diagnosis.fixCode) text += `\n\n**Code:**\n\`\`\`typescript\n${diagnosis.fixCode}\n\`\`\``;
14882
- if (diagnosis.related) text += `\n\n**Related:** ${diagnosis.related}`;
14883
- return textResult(text);
14884
- });
14885
- server.tool("get_routes", {}, async () => {
14886
- const ctx = getContext();
14887
- if (ctx.routes.length === 0) return textResult("No routes detected. Routes are defined via createRouter() or a routes array.");
14888
- const routeTable = ctx.routes.map((r) => {
14889
- const flags = [
14890
- r.hasLoader ? "loader" : "",
14891
- r.hasGuard ? "guard" : "",
14892
- r.params.length > 0 ? `params: ${r.params.join(", ")}` : "",
14893
- r.name ? `name: "${r.name}"` : ""
14894
- ].filter(Boolean).join(", ");
14895
- return ` ${r.path}${flags ? ` (${flags})` : ""}`;
14896
- }).join("\n");
14897
- return textResult(`**Routes (${ctx.routes.length}):**\n\n${routeTable}`);
14898
- });
14899
- server.tool("get_components", {}, async () => {
14900
- const ctx = getContext();
14901
- if (ctx.components.length === 0) return textResult("No components detected.");
14902
- const compList = ctx.components.map((c) => {
14903
- const details = [c.props.length > 0 ? `props: { ${c.props.join(", ")} }` : "", c.hasSignals ? `signals: [${c.signalNames.join(", ")}]` : ""].filter(Boolean).join(", ");
14904
- return ` ${c.name} ${c.file}${details ? `\n ${details}` : ""}`;
14905
- }).join("\n");
14906
- return textResult(`**Components (${ctx.components.length}):**\n\n${compList}`);
14907
- });
14908
- server.tool("get_browser_smoke_status", {}, async () => {
14909
- const fs = await import("node:fs");
14910
- const path = await import("node:path");
14911
- const cwd = process.cwd();
14912
- let browserPackages = [];
14913
- {
14914
- let dir = cwd;
14915
- for (let i = 0; i < 30; i++) {
14916
- const candidate = path.join(dir, ".claude", "rules", "browser-packages.json");
14917
- if (fs.existsSync(candidate)) {
14918
- try {
14919
- const parsed = JSON.parse(fs.readFileSync(candidate, "utf8"));
14920
- if (Array.isArray(parsed.packages)) browserPackages = parsed.packages.filter((p) => typeof p === "string");
14921
- } catch {}
14922
- break;
15753
+ function createServer() {
15754
+ const server = new McpServer({
15755
+ name: "pyreon",
15756
+ version
15757
+ });
15758
+ let cachedContext = null;
15759
+ let contextCwd = process.cwd();
15760
+ function getContext() {
15761
+ if (!cachedContext || contextCwd !== process.cwd()) {
15762
+ contextCwd = process.cwd();
15763
+ cachedContext = generateContext(contextCwd);
15764
+ }
15765
+ return cachedContext;
15766
+ }
15767
+ server.tool("get_api", {
15768
+ package: z.string(),
15769
+ symbol: z.string()
15770
+ }, async ({ package: pkg, symbol }) => {
15771
+ const entry = API_REFERENCE[`${pkg}/${symbol}`];
15772
+ if (!entry) {
15773
+ const suggestions = Object.keys(API_REFERENCE).filter((k) => k.toLowerCase().includes(symbol.toLowerCase())).slice(0, 5);
15774
+ return textResult(`Symbol '${symbol}' not found in @pyreon/${pkg}.\n\n${suggestions.length > 0 ? `Did you mean one of these?\n${suggestions.map((s) => ` - ${s}`).join("\n")}` : "No similar symbols found."}`);
15775
+ }
15776
+ return textResult(`## @pyreon/${pkg} ${symbol}\n\n**Signature:**\n\`\`\`typescript\n${entry.signature}\n\`\`\`\n\n**Usage:**\n\`\`\`typescript\n${entry.example}\n\`\`\`\n\n${entry.notes ? `**Notes:** ${entry.notes}\n\n` : ""}${entry.mistakes ? `**Common mistakes:**\n${entry.mistakes}\n` : ""}`);
15777
+ });
15778
+ server.tool("validate", {
15779
+ code: z.string(),
15780
+ filename: z.string().optional()
15781
+ }, async ({ code, filename }) => {
15782
+ const fname = filename ?? "snippet.tsx";
15783
+ const reactDiags = detectReactPatterns(code, fname);
15784
+ const pyreonDiags = detectPyreonPatterns(code, fname);
15785
+ if (reactDiags.length === 0 && pyreonDiags.length === 0) return textResult("✓ No issues found. The code follows Pyreon patterns correctly.");
15786
+ const merged = [...reactDiags, ...pyreonDiags];
15787
+ merged.sort((a, b) => a.line - b.line || a.column - b.column);
15788
+ const issueText = merged.map((d, i) => `${i + 1}. **${d.code}** (line ${d.line})\n ${d.message}\n Current: \`${d.current}\`\n Fix: \`${d.suggested}\`\n Auto-fixable: ${d.fixable ? "yes" : "no"}`).join("\n\n");
15789
+ return textResult(`Found ${merged.length} issue${merged.length === 1 ? "" : "s"}:\n\n${issueText}`);
15790
+ });
15791
+ server.tool("migrate_react", {
15792
+ code: z.string(),
15793
+ filename: z.string().optional()
15794
+ }, async ({ code, filename }) => {
15795
+ const result = migrateReactCode(code, filename ?? "component.tsx");
15796
+ const changeList = result.changes.map((c) => `- Line ${c.line}: ${c.description}`).join("\n");
15797
+ const remainingIssues = result.diagnostics.filter((d) => !d.fixable);
15798
+ const manualText = remainingIssues.length > 0 ? `\n\n**Remaining issues (manual fix needed):**\n${remainingIssues.map((d) => `- Line ${d.line}: ${d.message}\n Suggested: \`${d.suggested}\``).join("\n")}` : "";
15799
+ return textResult(`## Migrated Code\n\n\`\`\`tsx\n${result.code}\n\`\`\`\n\n**Changes applied (${result.changes.length}):**\n${changeList || "No changes needed."}${manualText}`);
15800
+ });
15801
+ server.tool("diagnose", { error: z.string() }, async ({ error }) => {
15802
+ const diagnosis = diagnoseError(error);
15803
+ if (!diagnosis) return textResult(`Could not identify a Pyreon-specific pattern in this error.\n\nError: ${error}\n\nSuggestions:\n- Check for typos in variable/function names\n- Verify all imports are correct\n- Run \`bun run typecheck\` for full TypeScript diagnostics\n- Run \`pyreon doctor\` for project-wide health check`);
15804
+ let text = `**Cause:** ${diagnosis.cause}\n\n**Fix:** ${diagnosis.fix}`;
15805
+ if (diagnosis.fixCode) text += `\n\n**Code:**\n\`\`\`typescript\n${diagnosis.fixCode}\n\`\`\``;
15806
+ if (diagnosis.related) text += `\n\n**Related:** ${diagnosis.related}`;
15807
+ return textResult(text);
15808
+ });
15809
+ server.tool("get_routes", {}, async () => {
15810
+ const ctx = getContext();
15811
+ if (ctx.routes.length === 0) return textResult("No routes detected. Routes are defined via createRouter() or a routes array.");
15812
+ const routeTable = ctx.routes.map((r) => {
15813
+ const flags = [
15814
+ r.hasLoader ? "loader" : "",
15815
+ r.hasGuard ? "guard" : "",
15816
+ r.params.length > 0 ? `params: ${r.params.join(", ")}` : "",
15817
+ r.name ? `name: "${r.name}"` : ""
15818
+ ].filter(Boolean).join(", ");
15819
+ return ` ${r.path}${flags ? ` (${flags})` : ""}`;
15820
+ }).join("\n");
15821
+ return textResult(`**Routes (${ctx.routes.length}):**\n\n${routeTable}`);
15822
+ });
15823
+ server.tool("get_components", {}, async () => {
15824
+ const ctx = getContext();
15825
+ if (ctx.components.length === 0) return textResult("No components detected.");
15826
+ const compList = ctx.components.map((c) => {
15827
+ const details = [c.props.length > 0 ? `props: { ${c.props.join(", ")} }` : "", c.hasSignals ? `signals: [${c.signalNames.join(", ")}]` : ""].filter(Boolean).join(", ");
15828
+ return ` ${c.name} — ${c.file}${details ? `\n ${details}` : ""}`;
15829
+ }).join("\n");
15830
+ return textResult(`**Components (${ctx.components.length}):**\n\n${compList}`);
15831
+ });
15832
+ server.tool("get_browser_smoke_status", {}, async () => {
15833
+ const fs = await import("node:fs");
15834
+ const path = await import("node:path");
15835
+ const cwd = process.cwd();
15836
+ let browserPackages = [];
15837
+ {
15838
+ let dir = cwd;
15839
+ for (let i = 0; i < 30; i++) {
15840
+ const candidate = path.join(dir, ".claude", "rules", "browser-packages.json");
15841
+ if (fs.existsSync(candidate)) {
15842
+ try {
15843
+ const parsed = JSON.parse(fs.readFileSync(candidate, "utf8"));
15844
+ if (Array.isArray(parsed.packages)) browserPackages = parsed.packages.filter((p) => typeof p === "string");
15845
+ } catch {}
15846
+ break;
15847
+ }
15848
+ const parent = path.dirname(dir);
15849
+ if (parent === dir) break;
15850
+ dir = parent;
14923
15851
  }
14924
- const parent = path.dirname(dir);
14925
- if (parent === dir) break;
14926
- dir = parent;
14927
- }
14928
- }
14929
- if (browserPackages.length === 0) return textResult("No `.claude/rules/browser-packages.json` found in the current project. This tool reports browser-smoke coverage for Pyreon monorepos that ship the single-source-of-truth list. Consumer apps can still opt in via the lint rule's `additionalPackages` option.");
14930
- function hasBrowserTest(dir) {
14931
- let entries;
14932
- try {
14933
- entries = fs.readdirSync(dir);
14934
- } catch {
14935
- return false;
14936
15852
  }
14937
- for (const name of entries) {
14938
- if (name.startsWith(".") || name === "node_modules" || name === "lib" || name === "dist") continue;
14939
- const full = path.join(dir, name);
14940
- let isDir = false;
15853
+ if (browserPackages.length === 0) return textResult("No `.claude/rules/browser-packages.json` found in the current project. This tool reports browser-smoke coverage for Pyreon monorepos that ship the single-source-of-truth list. Consumer apps can still opt in via the lint rule's `additionalPackages` option.");
15854
+ function hasBrowserTest(dir) {
15855
+ let entries;
14941
15856
  try {
14942
- isDir = fs.statSync(full).isDirectory();
15857
+ entries = fs.readdirSync(dir);
14943
15858
  } catch {
14944
- continue;
15859
+ return false;
14945
15860
  }
14946
- if (isDir) {
14947
- if (hasBrowserTest(full)) return true;
14948
- continue;
15861
+ for (const name of entries) {
15862
+ if (name.startsWith(".") || name === "node_modules" || name === "lib" || name === "dist") continue;
15863
+ const full = path.join(dir, name);
15864
+ let isDir = false;
15865
+ try {
15866
+ isDir = fs.statSync(full).isDirectory();
15867
+ } catch {
15868
+ continue;
15869
+ }
15870
+ if (isDir) {
15871
+ if (hasBrowserTest(full)) return true;
15872
+ continue;
15873
+ }
15874
+ if (/\.browser\.test\.(?:ts|tsx)$/.test(name)) return true;
14949
15875
  }
14950
- if (/\.browser\.test\.(?:ts|tsx)$/.test(name)) return true;
14951
- }
14952
- return false;
14953
- }
14954
- const pkgDirs = /* @__PURE__ */ new Map();
14955
- function walkPkgs(dir, depth = 0) {
14956
- if (depth > 4) return;
14957
- let entries;
14958
- try {
14959
- entries = fs.readdirSync(dir);
14960
- } catch {
14961
- return;
15876
+ return false;
14962
15877
  }
14963
- for (const name of entries) {
14964
- if (name.startsWith(".") || name === "node_modules") continue;
14965
- const full = path.join(dir, name);
14966
- let isDir = false;
15878
+ const pkgDirs = /* @__PURE__ */ new Map();
15879
+ function walkPkgs(dir, depth = 0) {
15880
+ if (depth > 4) return;
15881
+ let entries;
14967
15882
  try {
14968
- isDir = fs.statSync(full).isDirectory();
15883
+ entries = fs.readdirSync(dir);
14969
15884
  } catch {
14970
- continue;
15885
+ return;
15886
+ }
15887
+ for (const name of entries) {
15888
+ if (name.startsWith(".") || name === "node_modules") continue;
15889
+ const full = path.join(dir, name);
15890
+ let isDir = false;
15891
+ try {
15892
+ isDir = fs.statSync(full).isDirectory();
15893
+ } catch {
15894
+ continue;
15895
+ }
15896
+ if (!isDir) continue;
15897
+ const pkgJsonPath = path.join(full, "package.json");
15898
+ if (fs.existsSync(pkgJsonPath)) try {
15899
+ const parsed = JSON.parse(fs.readFileSync(pkgJsonPath, "utf8"));
15900
+ if (typeof parsed.name === "string") pkgDirs.set(parsed.name, full);
15901
+ } catch {}
15902
+ else walkPkgs(full, depth + 1);
14971
15903
  }
14972
- if (!isDir) continue;
14973
- const pkgJsonPath = path.join(full, "package.json");
14974
- if (fs.existsSync(pkgJsonPath)) try {
14975
- const parsed = JSON.parse(fs.readFileSync(pkgJsonPath, "utf8"));
14976
- if (typeof parsed.name === "string") pkgDirs.set(parsed.name, full);
14977
- } catch {}
14978
- else walkPkgs(full, depth + 1);
14979
15904
  }
14980
- }
14981
- walkPkgs(path.join(cwd, "packages"));
14982
- const covered = [];
14983
- const missing = [];
14984
- const unknown = [];
14985
- for (const name of browserPackages) {
14986
- const dir = pkgDirs.get(name);
14987
- if (!dir) {
14988
- unknown.push(name);
14989
- continue;
15905
+ walkPkgs(path.join(cwd, "packages"));
15906
+ const covered = [];
15907
+ const missing = [];
15908
+ const unknown = [];
15909
+ for (const name of browserPackages) {
15910
+ const dir = pkgDirs.get(name);
15911
+ if (!dir) {
15912
+ unknown.push(name);
15913
+ continue;
15914
+ }
15915
+ if (hasBrowserTest(dir)) covered.push(name);
15916
+ else missing.push(name);
14990
15917
  }
14991
- if (hasBrowserTest(dir)) covered.push(name);
14992
- else missing.push(name);
14993
- }
14994
- const parts = [];
14995
- parts.push(`**Browser smoke coverage** (${covered.length} / ${browserPackages.length}):`);
14996
- parts.push("");
14997
- if (covered.length > 0) {
14998
- parts.push(`✓ Covered (${covered.length}):`);
14999
- for (const n of covered) parts.push(` - ${n}`);
15918
+ const parts = [];
15919
+ parts.push(`**Browser smoke coverage** (${covered.length} / ${browserPackages.length}):`);
15000
15920
  parts.push("");
15921
+ if (covered.length > 0) {
15922
+ parts.push(`✓ Covered (${covered.length}):`);
15923
+ for (const n of covered) parts.push(` - ${n}`);
15924
+ parts.push("");
15925
+ }
15926
+ if (missing.length > 0) {
15927
+ parts.push(`✗ Missing \`*.browser.test.*\` (${missing.length}):`);
15928
+ for (const n of missing) parts.push(` - ${n}`);
15929
+ parts.push("");
15930
+ parts.push("Add a `*.browser.test.{ts,tsx}` file under `src/` in each missing package. See `.claude/rules/test-environment-parity.md` for the setup recipe.");
15931
+ }
15932
+ if (unknown.length > 0) {
15933
+ parts.push(`? Listed in browser-packages.json but not found in this repo (${unknown.length}):`);
15934
+ for (const n of unknown) parts.push(` - ${n}`);
15935
+ }
15936
+ return textResult(parts.join("\n"));
15937
+ });
15938
+ server.tool("get_pattern", { name: z.string().optional().describe("Pattern slug (e.g. \"dev-warnings\", \"controllable-state\"). Omit to list available patterns.") }, async ({ name }) => {
15939
+ const registry = loadPatternRegistry();
15940
+ if (!name) return textResult(formatPatternIndex(registry));
15941
+ const pattern = findPattern(registry, name);
15942
+ if (pattern) return textResult(formatPatternBody(pattern));
15943
+ const suggestions = suggestPatterns(registry, name);
15944
+ return textResult(`Pattern "${name}" not found.\n\n${suggestions.length > 0 ? `Did you mean one of these?\n${suggestions.map((s) => ` - ${s}`).join("\n")}\n\nOr call get_pattern() with no arg to see the full list.` : "Call get_pattern() with no arg to see available patterns."}`);
15945
+ });
15946
+ server.tool("get_anti_patterns", { category: z.enum([
15947
+ "reactivity",
15948
+ "jsx",
15949
+ "context",
15950
+ "architecture",
15951
+ "testing",
15952
+ "lifecycle",
15953
+ "documentation",
15954
+ "all"
15955
+ ]).optional().describe(`Category filter: ${ANTI_PATTERN_CATEGORIES.join(", ")}, or "all" (default).`) }, async ({ category = "all" }) => {
15956
+ const cat = category;
15957
+ const doc = loadAntiPatternsDoc();
15958
+ if (!doc) return textResult("Could not locate `.claude/rules/anti-patterns.md`. This tool reads the file from the Pyreon monorepo — running in a consumer project without the rules directory surfaces this miss. File issues against pyreon/pyreon if the file exists but is not being found.");
15959
+ const all = parseAntiPatterns(doc);
15960
+ return textResult(formatAntiPatterns(cat === "all" ? all : all.filter((e) => e.category === cat), cat));
15961
+ });
15962
+ server.tool("get_changelog", {
15963
+ package: z.string().optional().describe("Package name, e.g. \"query\" or \"@pyreon/query\". Omit to list every package with a CHANGELOG."),
15964
+ limit: z.number().int().positive().optional().describe("Maximum number of substantive versions to return. Default 5."),
15965
+ includeDependencyUpdates: z.boolean().optional().describe("Include `Updated dependencies` bullets. Default false (usually noise)."),
15966
+ since: z.string().optional().describe("Only include versions strictly newer than this one (e.g. \"0.12.0\"). Useful when an agent knows the version it was trained against and wants just the delta.")
15967
+ }, async ({ package: pkg, limit, includeDependencyUpdates, since }) => {
15968
+ const registry = loadChangelogRegistry();
15969
+ if (!pkg) return textResult(formatChangelogIndex(registry));
15970
+ const changelog = findChangelog(registry, pkg);
15971
+ if (changelog) return textResult(formatChangelog(changelog, {
15972
+ limit,
15973
+ includeDependencyUpdates,
15974
+ since
15975
+ }));
15976
+ const suggestions = suggestChangelogs(registry, pkg);
15977
+ return textResult(`Changelog for "${pkg}" not found.\n\n${suggestions.length > 0 ? `Did you mean one of these?\n${suggestions.map((s) => ` - ${s}`).join("\n")}\n\nOr call get_changelog() with no arg for the full list.` : "Call get_changelog() with no arg for the list of packages that ship a CHANGELOG."}`);
15978
+ });
15979
+ server.tool("audit_test_environment", {
15980
+ minRisk: z.enum([
15981
+ "high",
15982
+ "medium",
15983
+ "low"
15984
+ ]).optional().describe("Minimum risk level to surface. Default \"medium\" — HIGH + MEDIUM files. Use \"high\" for only the riskiest, \"low\" to see everything."),
15985
+ limit: z.number().int().positive().optional().describe("Maximum entries to show per risk group. Default 20.")
15986
+ }, async ({ minRisk, limit }) => {
15987
+ return textResult(formatTestAudit(auditTestEnvironment(process.cwd()), {
15988
+ minRisk,
15989
+ limit
15990
+ }));
15991
+ });
15992
+ return server;
15993
+ }
15994
+ /**
15995
+ * Locate `.claude/rules/anti-patterns.md` by walking up from cwd.
15996
+ * Returns the file contents or null if not found within 30 levels.
15997
+ * Separate from the patterns loader because the doc path is fixed
15998
+ * (`.claude/rules/`) — no glob needed.
15999
+ */
16000
+ function loadAntiPatternsDoc(startDir = process.cwd()) {
16001
+ let dir = resolve(startDir);
16002
+ for (let i = 0; i < 30; i++) {
16003
+ const candidate = join(dir, ".claude", "rules", "anti-patterns.md");
16004
+ if (existsSync(candidate)) try {
16005
+ return readFileSync(candidate, "utf8");
16006
+ } catch {
16007
+ return null;
16008
+ }
16009
+ const parent = dirname(dir);
16010
+ if (parent === dir) return null;
16011
+ dir = parent;
15001
16012
  }
15002
- if (missing.length > 0) {
15003
- parts.push(`✗ Missing \`*.browser.test.*\` (${missing.length}):`);
15004
- for (const n of missing) parts.push(` - ${n}`);
15005
- parts.push("");
15006
- parts.push("Add a `*.browser.test.{ts,tsx}` file under `src/` in each missing package. See `.claude/rules/test-environment-parity.md` for the setup recipe.");
15007
- }
15008
- if (unknown.length > 0) {
15009
- parts.push(`? Listed in browser-packages.json but not found in this repo (${unknown.length}):`);
15010
- for (const n of unknown) parts.push(` - ${n}`);
15011
- }
15012
- return textResult(parts.join("\n"));
15013
- });
16013
+ return null;
16014
+ }
15014
16015
  async function main() {
16016
+ const server = createServer();
15015
16017
  const transport = new StdioServerTransport();
15016
16018
  await server.connect(transport);
15017
16019
  }
15018
- main().catch((err) => {
16020
+ if (import.meta.main) main().catch((err) => {
15019
16021
  console.error("MCP server error:", err);
15020
16022
  process.exit(1);
15021
16023
  });
15022
16024
 
15023
16025
  //#endregion
16026
+ export { createServer };
15024
16027
  //# sourceMappingURL=index.js.map