@happyvertical/smrt-core 0.46.0 → 0.47.1

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.
Files changed (90) hide show
  1. package/AGENTS.md +39 -0
  2. package/agents/generators.md +19 -5
  3. package/agents/system-diagnostics.md +43 -0
  4. package/dist/browser.js +2 -1
  5. package/dist/change-feed.d.ts +8 -0
  6. package/dist/change-feed.d.ts.map +1 -1
  7. package/dist/change-feed.js +1 -1
  8. package/dist/change-feed.js.map +1 -1
  9. package/dist/decorators/compatibility.d.ts +46 -0
  10. package/dist/decorators/compatibility.d.ts.map +1 -1
  11. package/dist/decorators/compatibility.js +48 -5
  12. package/dist/decorators/compatibility.js.map +1 -1
  13. package/dist/decorators/index.d.ts +119 -1
  14. package/dist/decorators/index.d.ts.map +1 -1
  15. package/dist/decorators/index.js +52 -8
  16. package/dist/decorators/index.js.map +1 -1
  17. package/dist/generators/custom-action.d.ts +379 -2
  18. package/dist/generators/custom-action.d.ts.map +1 -1
  19. package/dist/generators/custom-action.js +694 -17
  20. package/dist/generators/custom-action.js.map +1 -1
  21. package/dist/generators/index.d.ts +1 -1
  22. package/dist/generators/index.d.ts.map +1 -1
  23. package/dist/generators/index.js +2 -2
  24. package/dist/generators/preflight-route.d.ts +12 -10
  25. package/dist/generators/preflight-route.d.ts.map +1 -1
  26. package/dist/generators/preflight-route.js +46 -14
  27. package/dist/generators/preflight-route.js.map +1 -1
  28. package/dist/generators/rest.d.ts +44 -0
  29. package/dist/generators/rest.d.ts.map +1 -1
  30. package/dist/generators/rest.js +84 -9
  31. package/dist/generators/rest.js.map +1 -1
  32. package/dist/generators.js +2 -2
  33. package/dist/index.d.ts +2 -2
  34. package/dist/index.d.ts.map +1 -1
  35. package/dist/index.js +5 -4
  36. package/dist/knowledge.d.ts.map +1 -1
  37. package/dist/knowledge.js +59 -15
  38. package/dist/knowledge.js.map +1 -1
  39. package/dist/manifest/static-manifest.d.ts.map +1 -1
  40. package/dist/manifest/static-manifest.js +125 -53
  41. package/dist/manifest/static-manifest.js.map +1 -1
  42. package/dist/manifest/store.js +1 -1
  43. package/dist/manifest/store.js.map +1 -1
  44. package/dist/manifest.json +184 -53
  45. package/dist/postgres-permissions.d.ts +9 -1
  46. package/dist/postgres-permissions.d.ts.map +1 -1
  47. package/dist/postgres-permissions.js +127 -13
  48. package/dist/postgres-permissions.js.map +1 -1
  49. package/dist/registry/index.d.ts +1 -1
  50. package/dist/registry/index.d.ts.map +1 -1
  51. package/dist/registry/shared-state.d.ts +19 -0
  52. package/dist/registry/shared-state.d.ts.map +1 -1
  53. package/dist/registry/shared-state.js +16 -1
  54. package/dist/registry/shared-state.js.map +1 -1
  55. package/dist/registry.d.ts +136 -1
  56. package/dist/registry.d.ts.map +1 -1
  57. package/dist/registry.js +231 -1
  58. package/dist/registry.js.map +1 -1
  59. package/dist/scanner/manifest-generator.d.ts.map +1 -1
  60. package/dist/scanner/manifest-generator.js +18 -16
  61. package/dist/scanner/manifest-generator.js.map +1 -1
  62. package/dist/scanner/types.d.ts +59 -6
  63. package/dist/scanner/types.d.ts.map +1 -1
  64. package/dist/scanner/types.js.map +1 -1
  65. package/dist/smrt-knowledge.json +42 -35
  66. package/dist/system/diagnostics.d.ts +299 -0
  67. package/dist/system/diagnostics.d.ts.map +1 -0
  68. package/dist/system/diagnostics.js +530 -0
  69. package/dist/system/diagnostics.js.map +1 -0
  70. package/dist/system/index.d.ts +1 -0
  71. package/dist/system/index.d.ts.map +1 -1
  72. package/dist/system/index.js +2 -1
  73. package/dist/utils/scanner-module.d.ts +16 -0
  74. package/dist/utils/scanner-module.d.ts.map +1 -1
  75. package/dist/vite-plugin/api-client-entries.d.ts.map +1 -1
  76. package/dist/vite-plugin/api-client-entries.js +10 -8
  77. package/dist/vite-plugin/api-client-entries.js.map +1 -1
  78. package/dist/vite-plugin/index.d.ts +34 -1
  79. package/dist/vite-plugin/index.d.ts.map +1 -1
  80. package/dist/vite-plugin/index.js +82 -3
  81. package/dist/vite-plugin/index.js.map +1 -1
  82. package/dist/vite-plugin/sveltekit-generator.d.ts +28 -2
  83. package/dist/vite-plugin/sveltekit-generator.d.ts.map +1 -1
  84. package/dist/vite-plugin/sveltekit-generator.js +121 -51
  85. package/dist/vite-plugin/sveltekit-generator.js.map +1 -1
  86. package/dist/vite-plugin/web-collections.d.ts.map +1 -1
  87. package/dist/vite-plugin/web-collections.js +1 -1
  88. package/dist/vite-plugin/web-collections.js.map +1 -1
  89. package/dist/vite-plugin.js +2 -2
  90. package/package.json +16 -11
@@ -303,9 +303,9 @@ function customActionParameterInputName(_metadata, parameterName) {
303
303
  */
304
304
  function resolveCustomActionMetadata(options) {
305
305
  const defaultScope = options.defaultScope ?? (options.method?.isStatic ? "collection" : "item");
306
- const requestedScope = readConfiguredScope(options.apiConfig, options.actionName);
306
+ const requestedScope = readConfiguredScope(options);
307
307
  const scope = requestedScope === defaultScope ? requestedScope : defaultScope;
308
- const configured = readConfiguredToolMetadata(options.apiConfig, options.actionName);
308
+ const configured = readConfiguredToolMetadata(options);
309
309
  const effect = configured.effect ?? "destructive";
310
310
  return {
311
311
  scope,
@@ -317,6 +317,25 @@ function resolveCustomActionMetadata(options) {
317
317
  openWorld: configured.openWorld ?? true
318
318
  };
319
319
  }
320
+ /**
321
+ * The declared scope, when it contradicts the receiver the method actually
322
+ * has; `undefined` when there is no declaration or it agrees.
323
+ *
324
+ * A scope is a DECLARATION about a method, not a relocation of it: nothing in
325
+ * a config can move an instance method onto the class. Silently ignoring a
326
+ * contradiction leaves an author believing a route exists at a collection URL
327
+ * that was never written, so the generators report this at build time
328
+ * (#2686).
329
+ */
330
+ function resolveDeclaredScopeMismatch(options) {
331
+ const declared = resolveEffectiveActionMetadata({
332
+ actionName: options.actionName,
333
+ ...options.method ? { method: options.method } : {},
334
+ apiConfig: options.apiConfig
335
+ }).scope;
336
+ if (!declared || declared === options.effectiveScope) return void 0;
337
+ return declared;
338
+ }
320
339
  /** Build the custom-action portion of an MCP/WebMCP JSON Schema. */
321
340
  function buildCustomActionInputSchema(metadata) {
322
341
  const properties = {};
@@ -365,7 +384,7 @@ function buildCustomActionInvocationArgs(metadata, args) {
365
384
  if (!metadata.parameters) return [isRecord(options) && Object.keys(options).length > 0 ? options : directArgs];
366
385
  if (metadata.parameters.length === 0) return [];
367
386
  if (metadata.parameters.length === 1 && metadata.parameters[0]?.name === "options") return [options];
368
- return metadata.parameters.map((parameter) => args[customActionParameterInputName(metadata, parameter.name)]);
387
+ return metadata.parameters.map((parameter) => coerceCustomActionArgument(args[customActionParameterInputName(metadata, parameter.name)], parameter.type));
369
388
  }
370
389
  /** Stable MCP `_meta` member shared with the app discovery contract (#2181). */
371
390
  var SMRT_CUSTOM_ACTION_ERROR_METADATA_KEY = "io.happyvertical/smrt";
@@ -388,21 +407,24 @@ function normalizeCustomActionFailure(value) {
388
407
  ...typeof value.correlationId === "string" ? { correlationId: value.correlationId } : {}
389
408
  };
390
409
  }
391
- function readConfiguredScope(apiConfig, actionName) {
392
- if (!isRecord(apiConfig) || !isRecord(apiConfig.routes)) return void 0;
393
- const route = apiConfig.routes[actionName];
394
- return isRecord(route) && (route.scope === "item" || route.scope === "collection") ? route.scope : void 0;
410
+ function readConfiguredScope(options) {
411
+ return resolveEffectiveActionMetadata({
412
+ actionName: options.actionName,
413
+ ...options.method ? { method: options.method } : {},
414
+ apiConfig: options.apiConfig
415
+ }).scope;
395
416
  }
396
- function readConfiguredToolMetadata(apiConfig, actionName) {
397
- if (!isRecord(apiConfig) || !isRecord(apiConfig.routes)) return {};
398
- const route = apiConfig.routes[actionName];
399
- if (!isRecord(route)) return {};
400
- const effect = route.effect === "read" || route.effect === "write" || route.effect === "destructive" ? route.effect : void 0;
401
- if (effect === "read" && (route.method === "PUT" || route.method === "PATCH" || route.method === "DELETE")) throw new Error(`Custom action ${actionName} cannot declare a read effect for a ${route.method} route`);
417
+ function readConfiguredToolMetadata(options) {
418
+ const effective = resolveEffectiveActionMetadata({
419
+ actionName: options.actionName,
420
+ ...options.method ? { method: options.method } : {},
421
+ apiConfig: options.apiConfig
422
+ });
423
+ if (effective.effect === "read" && (effective.httpMethod === "PUT" || effective.httpMethod === "PATCH" || effective.httpMethod === "DELETE")) throw new Error(`Custom action ${options.actionName} cannot declare a read effect for a ${effective.httpMethod} route`);
402
424
  return {
403
- ...effect ? { effect } : {},
404
- ...typeof route.idempotent === "boolean" ? { idempotent: route.idempotent } : {},
405
- ...typeof route.openWorld === "boolean" ? { openWorld: route.openWorld } : {}
425
+ ...effective.effect ? { effect: effective.effect } : {},
426
+ ...effective.idempotent !== void 0 ? { idempotent: effective.idempotent } : {},
427
+ ...effective.openWorld !== void 0 ? { openWorld: effective.openWorld } : {}
406
428
  };
407
429
  }
408
430
  function redactValue(value, seen = /* @__PURE__ */ new WeakSet()) {
@@ -426,7 +448,662 @@ function isSensitiveKey(key) {
426
448
  function isRecord(value) {
427
449
  return value !== null && typeof value === "object" && !Array.isArray(value);
428
450
  }
451
+ /**
452
+ * Read and validate the `@method()` config the scanner put on a manifest
453
+ * method. Returns `undefined` for an undecorated method.
454
+ */
455
+ function readMethodDecoratorConfig(method) {
456
+ const raw = method?.decoratorConfig;
457
+ if (!isRecord(raw)) return void 0;
458
+ const scope = raw.scope === "item" || raw.scope === "collection" ? raw.scope : void 0;
459
+ const effect = raw.effect === "read" || raw.effect === "write" || raw.effect === "destructive" ? raw.effect : void 0;
460
+ return {
461
+ ...typeof raw.expose === "boolean" ? { expose: raw.expose } : {},
462
+ ...typeof raw.reason === "string" ? { reason: raw.reason } : {},
463
+ ...isApiHttpMethod(raw.httpMethod) ? { httpMethod: raw.httpMethod } : {},
464
+ ...typeof raw.path === "string" ? { path: raw.path } : {},
465
+ ...scope ? { scope } : {},
466
+ ...effect ? { effect } : {},
467
+ ...typeof raw.idempotent === "boolean" ? { idempotent: raw.idempotent } : {},
468
+ ...typeof raw.openWorld === "boolean" ? { openWorld: raw.openWorld } : {},
469
+ ...typeof raw.description === "string" ? { description: raw.description } : {}
470
+ };
471
+ }
472
+ function isApiHttpMethod(value) {
473
+ return value === "GET" || value === "POST" || value === "PUT" || value === "PATCH" || value === "DELETE";
474
+ }
475
+ /**
476
+ * JavaScript values a JSON request body or query string cannot carry, keyed by
477
+ * the type NAME the manifest records for them.
478
+ *
479
+ * Deliberately a name list rather than "anything not primitive": the manifest
480
+ * cannot tell an interface from a class, so an unrecognized capitalized name
481
+ * is assumed to be a plain data bag (see {@link isWireableTypeName}). These are
482
+ * the well-known exceptions where that assumption is wrong for every project.
483
+ * Model classes are excluded separately, by asking the manifest.
484
+ */
485
+ var NON_SERIALIZABLE_TYPE_NAMES = /* @__PURE__ */ new Set([
486
+ "Function",
487
+ "bigint",
488
+ "symbol",
489
+ "Buffer",
490
+ "ArrayBuffer",
491
+ "SharedArrayBuffer",
492
+ "DataView",
493
+ "Uint8Array",
494
+ "Int8Array",
495
+ "Uint16Array",
496
+ "Int16Array",
497
+ "Uint32Array",
498
+ "Int32Array",
499
+ "Float32Array",
500
+ "Float64Array",
501
+ "BigInt64Array",
502
+ "BigUint64Array",
503
+ "Blob",
504
+ "File",
505
+ "FormData",
506
+ "ReadableStream",
507
+ "WritableStream",
508
+ "TransformStream",
509
+ "Stream",
510
+ "Readable",
511
+ "Writable",
512
+ "Request",
513
+ "Response",
514
+ "Headers",
515
+ "URL",
516
+ "URLSearchParams",
517
+ "AbortSignal",
518
+ "AbortController",
519
+ "Map",
520
+ "Set",
521
+ "WeakMap",
522
+ "WeakSet",
523
+ "RegExp",
524
+ "Error",
525
+ "Symbol",
526
+ "Promise",
527
+ "SmrtDatabase",
528
+ "DatabaseInterface",
529
+ "SmrtCollection",
530
+ "SmrtObject"
531
+ ]);
532
+ /**
533
+ * Generic container names whose ARGUMENTS carry the payload. `Record` is
534
+ * included: its value type is checked, its key type is always a string-ish
535
+ * index and never a receiver.
536
+ */
537
+ var JSON_CONTAINER_TYPE_NAMES = /* @__PURE__ */ new Set([
538
+ "Array",
539
+ "ReadonlyArray",
540
+ "Record",
541
+ "Partial",
542
+ "Required",
543
+ "Readonly",
544
+ "Pick",
545
+ "Omit",
546
+ "NonNullable"
547
+ ]);
548
+ /** Primitive/JSON-native type names a wire request can always carry. */
549
+ var JSON_PRIMITIVE_TYPE_NAMES = /* @__PURE__ */ new Set([
550
+ "string",
551
+ "number",
552
+ "boolean",
553
+ "null",
554
+ "undefined",
555
+ "void",
556
+ "any",
557
+ "unknown",
558
+ "object",
559
+ "Date"
560
+ ]);
561
+ var WIREABLE = { wireable: true };
562
+ /**
563
+ * True when a manifest type NAME can be carried by a JSON request body.
564
+ *
565
+ * The default is ACCEPT: the manifest records types as strings and cannot tell
566
+ * `RunContentReviewOptions` (an interface — a plain bag) from `Content` (a
567
+ * model class). Rejecting every unrecognized capitalized name would withhold
568
+ * routes from the overwhelmingly common options-bag shape, so an unrecognized
569
+ * name is assumed to be a bag and rejection is driven by positive evidence:
570
+ * a known non-serializable runtime type, a manifest class, or a bare type
571
+ * parameter.
572
+ */
573
+ function classifyTypeName(typeName, options, depth) {
574
+ const type = typeName.trim();
575
+ if (type === "") return WIREABLE;
576
+ if (type === "Date" && depth > 0) return {
577
+ wireable: false,
578
+ reason: "`Date` is only hydrated as a top-level parameter, so a nested one arrives as a string"
579
+ };
580
+ const unionParts = splitTopLevel(type, "|");
581
+ if (unionParts.length > 1) {
582
+ const branches = unionParts.filter((branch) => branch !== "null" && branch !== "undefined");
583
+ if (branches.length === 0) return WIREABLE;
584
+ const verdicts = branches.map((branch) => classifyTypeName(branch, options, depth));
585
+ if (verdicts.some((verdict) => verdict.wireable)) return WIREABLE;
586
+ return {
587
+ wireable: false,
588
+ reason: `every branch of \`${type}\` is unreachable over HTTP (${verdicts[0]?.reason ?? "not JSON-shaped"})`
589
+ };
590
+ }
591
+ if (type.endsWith("[]")) return classifyTypeName(type.slice(0, -2), options, depth + 1);
592
+ if (/^'.*'$/su.test(type) || /^-?\d/u.test(type)) return WIREABLE;
593
+ const generic = /^([\w$.]+)\s*<(.*)>$/su.exec(type);
594
+ if (generic) {
595
+ const base = generic[1];
596
+ if (JSON_CONTAINER_TYPE_NAMES.has(base)) {
597
+ if (depth >= WIREABILITY_MAX_DEPTH) return WIREABLE;
598
+ const args = splitTopLevel(generic[2], ",");
599
+ for (const arg of args) {
600
+ const verdict = classifyTypeName(arg, options, depth + 1);
601
+ if (!verdict.wireable) return verdict;
602
+ }
603
+ return WIREABLE;
604
+ }
605
+ return classifyTypeName(base, options, depth + 1);
606
+ }
607
+ if (type === "this") return {
608
+ wireable: false,
609
+ reason: "`this` is an instance of the declaring model class, not JSON data"
610
+ };
611
+ if (NON_SERIALIZABLE_TYPE_NAMES.has(type)) return {
612
+ wireable: false,
613
+ reason: `\`${type}\` is a runtime value a JSON request cannot carry`
614
+ };
615
+ if (JSON_PRIMITIVE_TYPE_NAMES.has(type)) return WIREABLE;
616
+ if (/^T(?:[0-9]|[A-Z][A-Za-z0-9]*)?$/u.test(type) || /^[A-Z]$/u.test(type)) return {
617
+ wireable: false,
618
+ reason: `\`${type}\` is an unresolved type parameter`
619
+ };
620
+ const simpleName = type.includes(".") ? type.split(".").pop() : type;
621
+ if (options.isModelClassName?.(simpleName)) return {
622
+ wireable: false,
623
+ reason: `\`${simpleName}\` is a model class instance, not JSON data`
624
+ };
625
+ return WIREABLE;
626
+ }
627
+ /** Recursion bound for generic type arguments. */
628
+ var WIREABILITY_MAX_DEPTH = 6;
629
+ /**
630
+ * Class names a manifest knows about, cached per manifest object.
631
+ *
632
+ * The manifest is rebuilt, never mutated in place, so identity is a safe cache
633
+ * key; a `WeakMap` keeps a discarded manifest's set collectable.
634
+ */
635
+ var manifestClassNameCache = /* @__PURE__ */ new WeakMap();
636
+ /**
637
+ * Build the `isModelClassName` predicate {@link classifyMethodWireability}
638
+ * needs, from a manifest.
639
+ *
640
+ * Both the SIMPLE and QUALIFIED name of every manifest class are registered: a
641
+ * parameter is annotated with the simple name in source, but a qualified name
642
+ * can reach the predicate through a `TSQualifiedName` annotation.
643
+ *
644
+ * Returns `undefined` for a missing manifest, which widens the gate — see
645
+ * {@link WireabilityOptions.isModelClassName}.
646
+ */
647
+ function createManifestClassNamePredicate(manifest) {
648
+ if (!manifest) return void 0;
649
+ let names = manifestClassNameCache.get(manifest);
650
+ if (!names) {
651
+ const collected = /* @__PURE__ */ new Set();
652
+ for (const [key, object] of Object.entries(manifest.objects ?? {})) {
653
+ collected.add(key);
654
+ if (object?.className) collected.add(object.className);
655
+ if (object?.qualifiedName) collected.add(object.qualifiedName);
656
+ }
657
+ names = collected;
658
+ manifestClassNameCache.set(manifest, names);
659
+ }
660
+ return createClassNamePredicate(names);
661
+ }
662
+ /**
663
+ * The same predicate from a bare name list, for a caller whose class inventory
664
+ * is the live `ObjectRegistry` rather than a manifest — notably
665
+ * `@happyvertical/smrt-users`' CLI resource listing, which iterates
666
+ * `ObjectRegistry.getAllClasses()` and has no manifest to hand.
667
+ *
668
+ * Exported so that caller does not grow its own copy: without a predicate the
669
+ * gate half-applies (every rejection EXCEPT model instances), which is worse
670
+ * than either extreme because the consumer then disagrees with the emitters on
671
+ * exactly the largest group of withheld methods.
672
+ */
673
+ function createClassNamePredicate(names) {
674
+ const resolved = names instanceof Set ? names : new Set(names);
675
+ return (name) => resolved.has(name);
676
+ }
677
+ /**
678
+ * Split a type string on `delimiter`, but only where it appears OUTSIDE every
679
+ * bracket pair — so `Record<string, Asset | null>` splits into two parts on
680
+ * `,` and one part on `|`, never into truncated fragments like `Record<string`.
681
+ *
682
+ * A naive `split()` on either delimiter produces fragments that match no
683
+ * classification rule and are therefore accepted by the default-accept path,
684
+ * silently widening the gate. Both the union test and the type-argument scan
685
+ * read this one implementation.
686
+ */
687
+ function splitTopLevel(source, delimiter) {
688
+ const parts = [];
689
+ let depth = 0;
690
+ let start = 0;
691
+ for (let index = 0; index < source.length; index += 1) {
692
+ const char = source[index];
693
+ if (char === "<" || char === "(" || char === "[" || char === "{") depth += 1;
694
+ else if (char === ">" || char === ")" || char === "]" || char === "}") depth -= 1;
695
+ else if (char === delimiter && depth === 0) {
696
+ parts.push(source.slice(start, index));
697
+ start = index + 1;
698
+ }
699
+ }
700
+ parts.push(source.slice(start));
701
+ return parts.map((part) => part.trim()).filter(Boolean);
702
+ }
703
+ /**
704
+ * Whether one declared parameter can be built from a JSON request body or
705
+ * query string.
706
+ */
707
+ function classifyParameterWireability(parameter, options = {}) {
708
+ if (parameter.name.startsWith("...")) return {
709
+ wireable: false,
710
+ reason: `rest parameter \`${parameter.name}\` cannot be projected from a request body`
711
+ };
712
+ if (parameter.typeUnresolved) return {
713
+ wireable: false,
714
+ reason: `the declared type of \`${parameter.name}\` could not be resolved by the scanner`
715
+ };
716
+ const unionBranches = parameter.unionBranches;
717
+ if (unionBranches && unionBranches.length > 0) {
718
+ for (const branch of unionBranches) {
719
+ if ((branch.memberTypes ?? []).find((memberType) => !classifyTypeName(memberType, options, 1).wireable) !== void 0) continue;
720
+ if (classifyTypeName(branch.type, options, 0).wireable) return WIREABLE;
721
+ }
722
+ return {
723
+ wireable: false,
724
+ reason: `parameter \`${parameter.name}\`: no branch of \`${parameter.type ?? "any"}\` can be built from a JSON request`
725
+ };
726
+ }
727
+ for (const memberType of parameter.memberTypes ?? []) {
728
+ const verdict = classifyTypeName(memberType, options, 1);
729
+ if (!verdict.wireable) return {
730
+ wireable: false,
731
+ reason: `\`${parameter.name}\` contains a member where ${verdict.reason}`
732
+ };
733
+ }
734
+ const verdict = classifyTypeName(parameter.type ?? "any", options, 0);
735
+ if (verdict.wireable) return WIREABLE;
736
+ return {
737
+ wireable: false,
738
+ reason: `parameter \`${parameter.name}\`: ${verdict.reason}`
739
+ };
740
+ }
741
+ /**
742
+ * Whether every declared parameter of a method can be built from a JSON
743
+ * request body or query string.
744
+ *
745
+ * A method with NO manifest parameter metadata is wire-able: that is the
746
+ * legacy options-bag contract every transport already supports, and absent
747
+ * metadata is not evidence of a hostile signature.
748
+ */
749
+ function classifyMethodWireability(method, options = {}) {
750
+ for (const parameter of method.parameters ?? []) {
751
+ const verdict = classifyParameterWireability(parameter, options);
752
+ if (!verdict.wireable) return verdict;
753
+ }
754
+ return WIREABLE;
755
+ }
756
+ var EXPOSED = { exposed: true };
757
+ /**
758
+ * The single decision every generated-API consumer asks: is this method
759
+ * reachable as a custom REST action, and if not, why?
760
+ *
761
+ * ONE resolver, four consumers — both SvelteKit route emitters
762
+ * (`generateRoutesForObject`, `generateCollectionRoutesForObject`), the
763
+ * cli↔api coherence resolver (`resolveApiActionSet`), and the knowledge
764
+ * artifact's API projection. They previously each re-derived a subset: the
765
+ * emitters filtered on `shouldIncludeInApi` plus their own receiver skip,
766
+ * `resolveApiActionSet` mirrored both, and `knowledge.ts` mirrored the
767
+ * receiver half a third time. A gate added to only one of them would report a
768
+ * method as unavailable while still writing its route file, which is the exact
769
+ * incoherence this issue exists to close (#2686).
770
+ *
771
+ * Order matters, and is the tested precedence contract:
772
+ *
773
+ * 1. `api: false` — the class has no REST surface at all.
774
+ * 2. A CRUD verb — the generated operation already owns the name (#2646).
775
+ * 3. Non-public — never a surface.
776
+ * 4. A framework lifecycle method (`save`, `initialize`, `toJSON`, ...) — the
777
+ * mechanism behind generated CRUD, not a distinct operation, even when a
778
+ * subclass declares its own override. `CLIGenerator` and `MCPGenerator`
779
+ * already gate on this; REST did not, and this is where it joins them
780
+ * (#2638, #2657).
781
+ * 5. `api.exclude` — an explicit withdrawal.
782
+ * 6. `api.include` — an explicit allowlist boundary.
783
+ * 7. `@method({ expose: false })` — an explicit withdrawal that outranks every
784
+ * remaining rule, including a legacy `api.routes` entry for the same
785
+ * method. This is why the decorator is `@method()` and not `@action()`:
786
+ * declaring something an action in order to say it is not one contradicts
787
+ * itself.
788
+ * 8. Explicit legacy exposure — a name listed in `api.include` or carrying an
789
+ * `api.routes` entry is a DECLARATION that this method is a route, made
790
+ * before the heuristic existed. It bypasses the heuristic. This is the
791
+ * documented compatibility exception that makes "nothing breaks" true for
792
+ * the 42 existing route entries; without it, migrating a class to the new
793
+ * gate could silently drop a route its author had spelled out.
794
+ * 9. `@method({ expose: true })` — bypasses the heuristic, and NOTHING else.
795
+ * It cannot manufacture a receiver (step 10 still applies), reach a
796
+ * non-public method, or hydrate a parameter the transport cannot build.
797
+ * 10. Wire-ability — every parameter must be constructible from JSON.
798
+ * 11. Receiver — a collection class emits only collection-scoped routes, and a
799
+ * model class cannot host a collection-scoped instance method.
800
+ */
801
+ function resolveApiMethodExposure(options) {
802
+ const { actionName, method, apiConfig, isCollectionClass = false } = options;
803
+ if (apiConfig === false) return {
804
+ exposed: false,
805
+ code: "api-disabled",
806
+ reason: "api is disabled"
807
+ };
808
+ if (isCrudOperation(actionName)) return {
809
+ exposed: false,
810
+ code: "crud-reserved",
811
+ reason: `\`${actionName}\` is reserved by the generated CRUD operation of the same name`
812
+ };
813
+ if (method.isPublic === false) return {
814
+ exposed: false,
815
+ code: "not-public",
816
+ reason: "not a public method"
817
+ };
818
+ if (isFrameworkLifecycleMethod(actionName)) return {
819
+ exposed: false,
820
+ code: "lifecycle-method",
821
+ reason: `\`${actionName}\` is a framework lifecycle method, not a distinct operation`
822
+ };
823
+ const config = getIncludeExclude(apiConfig);
824
+ if (config.exclude?.includes(actionName)) return {
825
+ exposed: false,
826
+ code: "excluded",
827
+ reason: "listed in api.exclude"
828
+ };
829
+ const includedExplicitly = config.include?.includes(actionName) === true;
830
+ if (config.include !== void 0 && !includedExplicitly) return {
831
+ exposed: false,
832
+ code: "not-included",
833
+ reason: "not listed in api.include"
834
+ };
835
+ const declared = readMethodDecoratorConfig(method);
836
+ if (declared?.expose === false) return {
837
+ exposed: false,
838
+ code: "withheld",
839
+ reason: declared.reason ?? "withheld by @method({ expose: false })"
840
+ };
841
+ const hasLegacyRoute = readApiRouteConfig(apiConfig, actionName) !== void 0;
842
+ if (!(declared?.expose === true || includedExplicitly || hasLegacyRoute)) {
843
+ const wireability = classifyMethodWireability(method, { ...options.isModelClassName ? { isModelClassName: options.isModelClassName } : {} });
844
+ if (!wireability.wireable) return {
845
+ exposed: false,
846
+ code: "not-wireable",
847
+ reason: `not routed: ${wireability.reason}`
848
+ };
849
+ }
850
+ const receiver = resolveActionReceiver(actionName, method, apiConfig, isCollectionClass);
851
+ if (!receiver.hosted) return {
852
+ exposed: false,
853
+ code: "no-receiver",
854
+ reason: receiver.reason
855
+ };
856
+ return EXPOSED;
857
+ }
858
+ /**
859
+ * Whether the resolved scope has an executable receiver on this host, matching
860
+ * both route emitters' own skips exactly.
861
+ *
862
+ * UNREACHABLE BY CONSTRUCTION, and deliberately kept:
863
+ * {@link resolveCustomActionMetadata} already collapses a contradicting
864
+ * declared scope back to the receiver-derived one, so the resolved scope always
865
+ * equals `defaultScope` and neither branch below can fire. That was equally
866
+ * true of the two `console.warn` skips in `generateRoutesForObject` /
867
+ * `generateCollectionRoutesForObject` that this replaced — moving them here
868
+ * changed nothing about when they fire, and dropping them would remove the only
869
+ * structural guard should that collapse ever be relaxed.
870
+ *
871
+ * The signal a developer actually sees for a contradicting declaration is
872
+ * {@link resolveDeclaredScopeMismatch}, reported by the emitters at build time.
873
+ */
874
+ function resolveActionReceiver(actionName, method, apiConfig, isCollectionClass) {
875
+ const defaultScope = isCollectionClass ? "collection" : method.isStatic ? "collection" : "item";
876
+ let scope;
877
+ try {
878
+ scope = resolveCustomActionMetadata({
879
+ actionName,
880
+ method: {
881
+ ...method.isStatic !== void 0 ? { isStatic: method.isStatic } : {},
882
+ ...method.decoratorConfig ? { decoratorConfig: method.decoratorConfig } : {}
883
+ },
884
+ apiConfig,
885
+ defaultScope
886
+ }).scope;
887
+ } catch {
888
+ scope = defaultScope;
889
+ }
890
+ if (isCollectionClass) {
891
+ if (scope !== "collection") return {
892
+ hosted: false,
893
+ reason: "collection class methods only support collection-scoped API routes"
894
+ };
895
+ return { hosted: true };
896
+ }
897
+ if (scope === "collection" && method.isStatic !== true) return {
898
+ hosted: false,
899
+ reason: "collection API routes require a static method"
900
+ };
901
+ return { hosted: true };
902
+ }
903
+ function resolveEffectiveActionMetadata(options) {
904
+ const route = readApiRouteConfig(options.apiConfig, options.actionName);
905
+ const declared = readMethodDecoratorConfig(options.method);
906
+ const legacyDescription = readAiDescription(options.aiConfig, options.actionName);
907
+ const httpMethod = declared?.httpMethod ?? normalizeRouteHttpMethod(route?.method);
908
+ const path = declared?.path ?? (typeof route?.path === "string" ? route.path : void 0);
909
+ const scope = declared?.scope ?? normalizeRouteScope(route?.scope);
910
+ const effect = declared?.effect ?? normalizeRouteEffect(route?.effect);
911
+ const idempotent = declared?.idempotent ?? (typeof route?.idempotent === "boolean" ? route.idempotent : void 0);
912
+ const openWorld = declared?.openWorld ?? (typeof route?.openWorld === "boolean" ? route.openWorld : void 0);
913
+ const description = declared?.description ?? legacyDescription;
914
+ return {
915
+ ...httpMethod ? { httpMethod } : {},
916
+ ...path !== void 0 ? { path } : {},
917
+ ...scope ? { scope } : {},
918
+ ...effect ? { effect } : {},
919
+ ...idempotent !== void 0 ? { idempotent } : {},
920
+ ...openWorld !== void 0 ? { openWorld } : {},
921
+ ...description !== void 0 ? { description } : {}
922
+ };
923
+ }
924
+ function normalizeRouteHttpMethod(value) {
925
+ return isApiHttpMethod(value) ? value : void 0;
926
+ }
927
+ function normalizeRouteScope(value) {
928
+ return value === "item" || value === "collection" ? value : void 0;
929
+ }
930
+ function normalizeRouteEffect(value) {
931
+ return value === "read" || value === "write" || value === "destructive" ? value : void 0;
932
+ }
933
+ function readAiDescription(aiConfig, actionName) {
934
+ if (!isRecord(aiConfig) || !isRecord(aiConfig.descriptions)) return void 0;
935
+ const description = aiConfig.descriptions[actionName];
936
+ return typeof description === "string" ? description : void 0;
937
+ }
938
+ function readApiRouteConfig(apiConfig, actionName) {
939
+ if (!isRecord(apiConfig) || !isRecord(apiConfig.routes)) return void 0;
940
+ const route = apiConfig.routes[actionName];
941
+ return isRecord(route) ? route : void 0;
942
+ }
943
+ /**
944
+ * Narrow a scanned transport config's `include`/`exclude`.
945
+ *
946
+ * A non-array value is treated as unset rather than throwing later on
947
+ * `.includes()` — the same defensive stance every other reader of scanned
948
+ * decorator config takes, because this data came from an AST, not a compiler.
949
+ */
950
+ function getIncludeExclude(config) {
951
+ if (config === true || config === void 0 || !isRecord(config)) return {};
952
+ return {
953
+ ...Array.isArray(config.include) ? { include: config.include } : {},
954
+ ...Array.isArray(config.exclude) ? { exclude: config.exclude } : {}
955
+ };
956
+ }
957
+ /**
958
+ * Whether a method's `@method()` declaration is also a RUNTIME REST route
959
+ * declaration, the way an `api.routes[m]` entry is.
960
+ *
961
+ * The runtime `APIGenerator` transport is deliberately declaration-gated: it
962
+ * serves a custom collection action only where one was declared, because its URL
963
+ * shape supports a single segment and an undeclared public method has never had
964
+ * a route there. `dispatchCustomCollectionAction` and the `isRestActionRoutable`
965
+ * preflight prediction must agree on that gate exactly, so both read this (#2686).
966
+ *
967
+ * True for any option that migrates from `ApiCustomRouteConfig` — its complete
968
+ * field set is `scope`, `method`, `path`, `effect`, `idempotent`, `openWorld` —
969
+ * because a legacy `routes: { m: { effect: 'write' } }` entry with no path or
970
+ * verb already dispatches at `POST /<collection>/m`, and migrating it onto the
971
+ * method must not silently delete that endpoint. Also true for an explicit
972
+ * `expose: true`, which is a stronger statement that the method is an action
973
+ * than an empty route entry is.
974
+ *
975
+ * FALSE for a bare `@method()` and for a `description`-only one. Neither
976
+ * migrates from a route entry — `description` migrates from `ai.descriptions`,
977
+ * and a bare decorator is a review marker — so counting them would hand the
978
+ * runtime transport endpoints it never served.
979
+ */
980
+ function declaresRuntimeRestRoute(method) {
981
+ if (readMethodDecoratorConfig(method)?.expose === false) return false;
982
+ return declaresRuntimeRestRouteShape(method);
983
+ }
984
+ /**
985
+ * Whether the author WROTE a runtime REST route declaration on this method,
986
+ * ignoring whether they then withheld it.
987
+ *
988
+ * Deliberately distinct from {@link declaresRuntimeRestRoute}: the dispatcher
989
+ * must still SEE a withheld declaration in order to refuse it explicitly. This
990
+ * router resolves `POST /<collection>/<segment>` to `create` when nothing
991
+ * claims the segment, so dropping a withheld action from the candidate set
992
+ * would turn a request aimed at an explicitly withheld operation into a silent
993
+ * row insert. The candidate set reads this; the preflight PREDICTION reads
994
+ * {@link declaresRuntimeRestRoute}, which adds the `expose: false` veto —
995
+ * "there is a declaration here" and "it is reachable" are different questions.
996
+ */
997
+ function declaresRuntimeRestRouteShape(method) {
998
+ const declared = readMethodDecoratorConfig(method);
999
+ if (!declared) return false;
1000
+ return declared.httpMethod !== void 0 || declared.path !== void 0 || declared.scope !== void 0 || declared.effect !== void 0 || declared.idempotent !== void 0 || declared.openWorld !== void 0 || declared.expose !== void 0;
1001
+ }
1002
+ /**
1003
+ * Coerce one transport-supplied argument into the runtime value the declared
1004
+ * parameter type needs.
1005
+ *
1006
+ * Today that means exactly one conversion: a `Date` parameter, which the
1007
+ * wire-ability heuristic accepts as JSON-shaped. JSON has no date type, so a
1008
+ * caller can only send an ISO string (or an epoch number) and the receiving
1009
+ * method — which calls `getTime()`, or hands the value to a query builder that
1010
+ * expects a `Date` — would otherwise get a string. Accepting `Date` as
1011
+ * wire-able and NOT hydrating it here would generate a route that 500s, so the
1012
+ * two are one decision (#2686).
1013
+ *
1014
+ * Deliberately narrow:
1015
+ * - Only a TOP-LEVEL declared parameter is converted. A `Date` nested inside a
1016
+ * named options bag is invisible to the manifest (the bag is accepted
1017
+ * heuristically, its members unresolved), so it is not hydrated and the
1018
+ * method must accept the string itself.
1019
+ * - An already-`Date` value, and anything that is not a string or finite
1020
+ * number, passes through untouched, so a runtime caller invoking the same
1021
+ * helper is never degraded.
1022
+ * - An unparseable string passes through as-is rather than becoming an
1023
+ * `Invalid Date`, leaving the method's own validation in charge of the error
1024
+ * message.
1025
+ */
1026
+ function coerceCustomActionArgument(value, declaredType) {
1027
+ if (!declaredType || !declaredTypeAcceptsDate(declaredType)) return value;
1028
+ return toCustomActionDate(value);
1029
+ }
1030
+ /**
1031
+ * The `Date` half of {@link coerceCustomActionArgument}, exported on its own
1032
+ * because generated SvelteKit route code calls it directly: the generator
1033
+ * already knows at build time which parameters are `Date`-typed, so the
1034
+ * emitted handler names the conversion rather than re-deriving it from a type
1035
+ * string at runtime. Both paths share this one implementation so the two
1036
+ * transports cannot drift.
1037
+ */
1038
+ function toCustomActionDate(value) {
1039
+ if (value instanceof Date) return value;
1040
+ if (typeof value === "number" && Number.isFinite(value)) return new Date(value);
1041
+ if (typeof value !== "string" || value.trim() === "") return value;
1042
+ const parsed = new Date(value);
1043
+ return Number.isNaN(parsed.getTime()) ? value : parsed;
1044
+ }
1045
+ /**
1046
+ * Decode a `number`-typed action argument that arrived over a QUERY STRING.
1047
+ *
1048
+ * A GET handler builds its options from `URLSearchParams`, so every value is a
1049
+ * string: `limit: number` reached the method as `'2'` and any arithmetic on it
1050
+ * silently produced string concatenation or `NaN` (#2686). A JSON body needs no
1051
+ * such repair, which is why this is emitted only on GET routes.
1052
+ *
1053
+ * Leaves anything it cannot decode alone, so a malformed value reaches the
1054
+ * method's own validation rather than becoming a silent `NaN`.
1055
+ */
1056
+ function toCustomActionNumber(value) {
1057
+ if (typeof value === "number") return value;
1058
+ if (typeof value !== "string" || value.trim() === "") return value;
1059
+ const parsed = Number(value);
1060
+ return Number.isFinite(parsed) ? parsed : value;
1061
+ }
1062
+ /**
1063
+ * The `boolean` counterpart to {@link toCustomActionNumber}. A query string
1064
+ * carries `?active=false`, and the bare string `'false'` is TRUTHY — the most
1065
+ * dangerous of these coercions, since it inverts a guard rather than degrading
1066
+ * it. Only the four canonical spellings decode; anything else is left for the
1067
+ * method's own validation.
1068
+ */
1069
+ function toCustomActionBoolean(value) {
1070
+ if (typeof value === "boolean") return value;
1071
+ if (typeof value !== "string") return value;
1072
+ const normalized = value.trim().toLowerCase();
1073
+ if (normalized === "true" || normalized === "1") return true;
1074
+ if (normalized === "false" || normalized === "0") return false;
1075
+ return value;
1076
+ }
1077
+ /**
1078
+ * The query-string decoder a GET route should apply to one parameter, or
1079
+ * `undefined` when the value passes through untouched.
1080
+ *
1081
+ * Mirrors {@link declaredTypeAcceptsDate}: a nullish branch does not change the
1082
+ * representation, but a genuine alternative (`number | string`) means the
1083
+ * method already accepts what the query string sends, so nothing is decoded.
1084
+ */
1085
+ function queryStringDecoderFor(declaredType) {
1086
+ if (!declaredType) return void 0;
1087
+ if (declaredTypeAcceptsDate(declaredType)) return "toCustomActionDate";
1088
+ const branches = splitTopLevel(declaredType, "|").map((branch) => branch.trim()).filter((branch) => branch !== "null" && branch !== "undefined");
1089
+ if (branches.length === 0) return void 0;
1090
+ if (branches.every((branch) => branch === "number")) return "toCustomActionNumber";
1091
+ if (branches.every((branch) => branch === "boolean")) return "toCustomActionBoolean";
1092
+ }
1093
+ /**
1094
+ * True when the declared type is a `Date` and nothing else.
1095
+ *
1096
+ * `Date | null` and `Date | undefined` qualify — a nullish branch is not an
1097
+ * alternative representation. `Date | string` deliberately does NOT: that
1098
+ * signature already accepts the string a JSON caller sends, so the method's
1099
+ * own handling is authoritative and converting behind its back would change
1100
+ * which branch it takes.
1101
+ */
1102
+ function declaredTypeAcceptsDate(declaredType) {
1103
+ const branches = declaredType.split("|").map((branch) => branch.trim()).filter((branch) => branch !== "null" && branch !== "undefined");
1104
+ return branches.length > 0 && branches.every((branch) => branch === "Date");
1105
+ }
429
1106
  //#endregion
430
- export { CRUD_OPERATIONS, FRAMEWORK_LIFECYCLE_METHOD_NAMES, SMRT_CUSTOM_ACTION_ERROR_METADATA_KEY, buildCustomActionInputSchema, buildCustomActionInvocationArgs, customActionParameterInputName, isCrudOperation, isCrudToolAction, isFrameworkLifecycleMethod, normalizeCustomActionFailure, resolveCustomActionMetadata, resolveCustomActionNames };
1107
+ export { CRUD_OPERATIONS, FRAMEWORK_LIFECYCLE_METHOD_NAMES, SMRT_CUSTOM_ACTION_ERROR_METADATA_KEY, buildCustomActionInputSchema, buildCustomActionInvocationArgs, classifyMethodWireability, classifyParameterWireability, coerceCustomActionArgument, createClassNamePredicate, createManifestClassNamePredicate, customActionParameterInputName, declaredTypeAcceptsDate, declaresRuntimeRestRoute, declaresRuntimeRestRouteShape, isCrudOperation, isCrudToolAction, isFrameworkLifecycleMethod, normalizeCustomActionFailure, queryStringDecoderFor, readMethodDecoratorConfig, resolveApiMethodExposure, resolveCustomActionMetadata, resolveCustomActionNames, resolveDeclaredScopeMismatch, resolveEffectiveActionMetadata, toCustomActionBoolean, toCustomActionDate, toCustomActionNumber };
431
1108
 
432
1109
  //# sourceMappingURL=custom-action.js.map