@modelprofile.com/flexharness 13.0.0 → 13.1.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.
@@ -7,14 +7,11 @@ import type {
7
7
  TFlexTypedRequestSchema,
8
8
  } from './interfaces.js';
9
9
  import { normalizeOmissionPaths } from './omissions.js';
10
+ import { definitionsPrefix, isRecord, referencedType, referencesIn } from './references.js';
10
11
 
11
12
  const componentsPrefix = '#/components/schemas/';
12
- const definitionsPrefix = '#/$defs/';
13
13
  const maxSummaryCharacters = 120;
14
14
 
15
- const isRecord = (value: unknown): value is Record<string, unknown> =>
16
- typeof value === 'object' && value !== null && !Array.isArray(value);
17
-
18
15
  /** A schema whose references name `#/$defs/<type>` instead of the OpenAPI components. */
19
16
  const withDefinitionRefs = (value: unknown): unknown => {
20
17
  if (Array.isArray(value)) return value.map(withDefinitionRefs);
@@ -27,12 +24,6 @@ const withDefinitionRefs = (value: unknown): unknown => {
27
24
  ]));
28
25
  };
29
26
 
30
- /** The type a `#/$defs/<type>` reference names. */
31
- const referencedType = (schema: TFlexTypedRequestSchema): string | undefined =>
32
- typeof schema.$ref === 'string' && schema.$ref.startsWith(definitionsPrefix)
33
- ? schema.$ref.slice(definitionsPrefix.length)
34
- : undefined;
35
-
36
27
  /**
37
28
  * A request schema without the omitted properties: an object loses them, and a reference to a
38
29
  * named type that holds one becomes the type's own schema without them.
@@ -167,18 +158,6 @@ const requiresNothing = (
167
158
  || schema.allOf.every((member) => !isRecord(member) || requiresNothing(member, types));
168
159
  };
169
160
 
170
- /** The named types a schema refers to. */
171
- const referencesIn = (value: unknown, found: Set<string>): Set<string> => {
172
- if (Array.isArray(value)) {
173
- for (const entry of value) referencesIn(entry, found);
174
- } else if (isRecord(value)) {
175
- const target = referencedType(value);
176
- if (target !== undefined) found.add(target);
177
- for (const child of Object.values(value)) referencesIn(child, found);
178
- }
179
- return found;
180
- };
181
-
182
161
  /** The first sentence of a contract's JSDoc, its tags left out, within the summary's length. */
183
162
  const summaryOf = (documentation: string | undefined, method: string): string => {
184
163
  const prose = (documentation ?? '').split(/\n(?=@)/)[0].replace(/\s+/g, ' ').trim();
@@ -125,7 +125,10 @@ export interface IFlexTypedRequestCatalogOptions<TScope = unknown> {
125
125
  method: IFlexTypedRequestMethod,
126
126
  context: IFlexCatalogRunContext<TScope>,
127
127
  ): Record<string, unknown> | PromiseLike<Record<string, unknown>>;
128
- /** The methods the model may use. Default: every method that is not deprecated. */
128
+ /**
129
+ * The methods the model may use. Default: every method that is not deprecated. The catalog keeps
130
+ * only the named types these methods reach.
131
+ */
129
132
  include?(method: IFlexTypedRequestMethod): boolean;
130
133
  /**
131
134
  * The methods that only read: a call has no effect but its result. Such a method is declared
@@ -41,7 +41,7 @@ The surface is plain JSON, so a build step can write it next to the server and t
41
41
  - `request` fills the omitted properties. A call takes from what it returns only the omitted properties its method's request has, so one answer serves every method (`{ identity, csrfToken }`), and a method whose request has none is routed without asking it. A value the model passes for any omitted property never reaches the router.
42
42
  - Method-specific fills copy only the selected leaves, retaining public siblings. Array wildcards fill existing elements at corresponding indexes without adding or removing elements. An absent object parent can be created for a supplied leaf; an explicit null or scalar parent is refused. Input objects are not mutated. A surface with additional paths requires `request`; a serialized global-only surface without `omittedPaths` remains valid.
43
43
  - A refusal rejects with `FlexTypedRequestError`: the typed method, the error's `text` and `data`, as a client of the API would receive them. A handler's own failure arrives as the router's internal error. A host's `externalErrorProjector` decides what of it the model sees.
44
- - `include` picks the methods the model may use, by default every method that is not deprecated; `aliases` gives methods other names by typed method; `vocabulary`, `onCall` and `onProposals` are the catalog's own (`onCall` receives the typed method first).
44
+ - `include` picks the methods the model may use, by default every method that is not deprecated, and the catalog's types are the surface's types those methods reach, through their schemas' `$ref`s and the names their declarations' code uses: a type only methods left out use is neither declared nor described, unless it holds a lower identifier of a kept type's name, which keeps that type's identifier as the whole surface declares it (`a_b` stays for `a-b`, declared `a_b_2`; `FlexToolCatalog.identifiersOf()`). `aliases` gives methods other names by typed method; `vocabulary`, `onCall` and `onProposals` are the catalog's own (`onCall` receives the typed method first).
45
45
  - `readOnly` marks the methods that only read, including their policy hook. A marked method is declared `readOnly` and its `onCall` still decides every validated call, so reads run concurrently, may start while the model still streams, and a turn answers identical ones once after each call's hook allows it; every other method's calls, gated by `onCall` or not, run in the order the model made them. A policy that proposes changes must return `run` for the reads it allows, rather than relying on reads skipping the hook. Mark nothing that asks for permission.
46
46
  - `params` makes the request a call routes of the model's validated parameters, such as the parameters with the short names the model was shown for records turned back into their IDs; by default the parameters themselves. It runs when the call runs, after `onCall` decided it where the method has one, so `onCall` and a proposal see the model's parameters. The omitted properties are dropped from what it returns as from the model's, and only `request` fills them. A throw refuses the call with its error, and so does a result that is no object.
47
47
  - The same validation, policy, stripping and filling apply to `execute` and every call inside `run_code`. Private inputs are rejected before `onCall`; a proposed change does not run `params`, `request` or the router. Omissions cover inputs only: use `response` to remove secrets from answers before they reach the model.
@@ -0,0 +1,68 @@
1
+ import * as plugins from './plugins.js';
2
+ import type { IFlexCatalogType } from '@modelprofile.com/flexharness';
3
+ import type { IFlexTypedRequestMethod, TFlexTypedRequestSchema } from './interfaces.js';
4
+
5
+ export const definitionsPrefix = '#/$defs/';
6
+
7
+ export const isRecord = (value: unknown): value is Record<string, unknown> =>
8
+ typeof value === 'object' && value !== null && !Array.isArray(value);
9
+
10
+ /** The type a `#/$defs/<type>` reference names. */
11
+ export const referencedType = (schema: TFlexTypedRequestSchema): string | undefined =>
12
+ typeof schema.$ref === 'string' && schema.$ref.startsWith(definitionsPrefix)
13
+ ? schema.$ref.slice(definitionsPrefix.length)
14
+ : undefined;
15
+
16
+ /** The named types a schema refers to. */
17
+ export const referencesIn = (value: unknown, found: Set<string>): Set<string> => {
18
+ if (Array.isArray(value)) {
19
+ for (const entry of value) referencesIn(entry, found);
20
+ } else if (isRecord(value)) {
21
+ const target = referencedType(value);
22
+ if (target !== undefined) found.add(target);
23
+ for (const child of Object.values(value)) referencesIn(child, found);
24
+ }
25
+ return found;
26
+ };
27
+
28
+ /**
29
+ * The types of `types` that `methods` reach, in their order: the ones the methods' schemas refer
30
+ * to and their declarations' code names, and the ones those types reach the same way. A type only
31
+ * other methods use is left out. A declaration names a type by the identifier the catalog of all of
32
+ * `types` declares it under (`FlexToolCatalog.identifiersOf()`); as that depends on the other
33
+ * types, a reached type keeps the ones that took the lower identifiers of its name along, so it is
34
+ * declared under the same identifier.
35
+ */
36
+ export const typesReachedBy = (
37
+ methods: readonly IFlexTypedRequestMethod[],
38
+ types: readonly IFlexCatalogType[],
39
+ ): IFlexCatalogType[] => {
40
+ const identifiers = plugins.FlexToolCatalog.identifiersOf(types.map((type) => type.name));
41
+ const byName = new Map(types.map((type) => [type.name, type]));
42
+ const byIdentifier = new Map(types.map((type) => [identifiers.get(type.name)!, type]));
43
+ const pending: IFlexCatalogType[] = [];
44
+ const reach = (schemas: unknown, declaration: string | undefined) => {
45
+ for (const name of referencesIn(schemas, new Set())) {
46
+ const type = byName.get(name);
47
+ if (type !== undefined) pending.push(type);
48
+ }
49
+ for (const identifier of declaration === undefined ? [] : plugins.FlexToolCatalog.identifiersIn(declaration)) {
50
+ const type = byIdentifier.get(identifier);
51
+ if (type !== undefined) pending.push(type);
52
+ }
53
+ };
54
+ for (const method of methods) reach([method.inputSchema, method.outputSchema], method.declaration);
55
+ const reached = new Set<IFlexCatalogType>();
56
+ while (pending.length) {
57
+ const type = pending.pop()!;
58
+ if (reached.has(type)) continue;
59
+ reached.add(type);
60
+ reach(type.schema, type.declaration);
61
+ const identifier = identifiers.get(type.name)!;
62
+ const base = plugins.FlexToolCatalog.identifierOf(type.name);
63
+ for (let suffix = 1, lower = base; lower !== identifier; lower = `${base}_${++suffix}`) {
64
+ pending.push(byIdentifier.get(lower)!);
65
+ }
66
+ }
67
+ return types.filter((type) => reached.has(type));
68
+ };