@adhd/apigen-core-client 0.1.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.
@@ -0,0 +1,65 @@
1
+ import { InternalExtractionSession } from '../extraction-session';
2
+ import { Project, SourceFile } from 'ts-morph';
3
+
4
+ /**
5
+ * Canonical JSON-Schema fragments for TS built-in scalar types.
6
+ * Keyed by the exact type-text string the extractor emits.
7
+ *
8
+ * Mappings follow §3 / §12–13 of the apigen-logical-types DESIGN:
9
+ * Date → format:date-time (RFC 3339 UTC; Date.prototype.toJSON already emits this)
10
+ * bigint → format:int64 (decimal string to avoid JS f64 precision loss)
11
+ * Uint8Array → format:byte (base64 standard + padding)
12
+ * Buffer → format:byte (Node.js Buffer; same wire as Uint8Array)
13
+ * URL → format:uri
14
+ * RegExp → format:regex
15
+ * Decimal → format:decimal (decimal.js Decimal; arbitrary-precision decimal string)
16
+ *
17
+ * Map / Set are handled in later states (lt-extract-nominal / lt-scalars).
18
+ *
19
+ * NOTE on Decimal: ts-morph emits different type-text strings for the same
20
+ * Decimal class depending on how the user imports it:
21
+ * - `import { Decimal } from 'decimal.js'` → `"Decimal"` (keyed directly below)
22
+ * - `import Decimal from 'decimal.js'` → qualified import path like
23
+ * `import("/path/to/decimal.js/decimal").default`
24
+ * - `import { Decimal as D2 } from 'decimal.js'` / `import D2 from 'decimal.js'`
25
+ * → `"D2"` (local alias)
26
+ * All forms are normalised to the "Decimal" key before this map is consulted:
27
+ * - qualified-import form: handled by `normalizeTypeText` regex
28
+ * - alias form: handled by the alias map from `extractScalarAliases`
29
+ * See `normalizeTypeText` and `extractScalarAliases`.
30
+ */
31
+ export declare const SCALAR_SCHEMAS: Readonly<Record<string, Record<string, unknown>>>;
32
+ /**
33
+ * Scan a source file's import declarations and return a map of
34
+ * `localName → canonicalScalarKey`
35
+ * for any import whose module specifier is in MODULE_SCALAR_MAP and whose
36
+ * local name differs from the canonical key.
37
+ *
38
+ * Examples:
39
+ * `import { Decimal as D2 } from 'decimal.js'` → { D2: 'Decimal' }
40
+ * `import MyDec from 'decimal.js'` → { MyDec: 'Decimal' }
41
+ * `import Decimal from 'decimal.js'` → {} (no alias needed)
42
+ * `import { Decimal } from 'decimal.js'` → {} (no alias needed)
43
+ *
44
+ * The returned map is consumed by normalizeTypeText and morphFallback so that
45
+ * aliased external scalar types are recognised at ANY nesting depth.
46
+ */
47
+ export declare function extractScalarAliases(sf: SourceFile): ReadonlyMap<string, string>;
48
+ /**
49
+ * Attempts ts-json-schema-generator first; falls back to morphFallback for
50
+ * inline/anonymous types.
51
+ *
52
+ * When `session` is supplied, results are memoized per
53
+ * `(sourceFile, tsconfig, typeText)` for the session's lifetime — the
54
+ * orchestrator's extract + generateSchemas double pass over the same file, and
55
+ * repeated parameter types across functions, all become Map hits. Cached
56
+ * fragments are shared by reference; callers treat schemas as immutable.
57
+ *
58
+ * BUG-APIGEN-CORE-003 (cache-stampede fix): `session.schemaCache` stores the
59
+ * in-flight/resolved `Promise<Schema>` itself — not just the resolved value
60
+ * — and stores it SYNCHRONOUSLY before anything is awaited, so concurrent
61
+ * identical requests (e.g. `morph-walk.ts`'s `Promise.all` over union
62
+ * variants) join ONE computation instead of each redundantly recomputing.
63
+ * See the in-function comments below for the mechanism.
64
+ */
65
+ export declare function buildSchema(_project: Project, sf: SourceFile, typeText: string, tsconfig?: string, session?: InternalExtractionSession, _ancestors?: ReadonlySet<string>): Promise<Record<string, unknown>>;
@@ -0,0 +1,74 @@
1
+ import { X_APIGEN_LOGICAL } from '@adhd/apigen-base-logical';
2
+
3
+ /**
4
+ * One variant of a discriminated union.
5
+ *
6
+ * - `className` the PascalCase class name matching its `$defs` key
7
+ * (produced by `buildNominalSchema`'s `defKey` output).
8
+ * - `discriminantValue` the literal string value of the discriminant property
9
+ * on this variant (e.g. `"dog"` for `kind:"dog"`).
10
+ */
11
+ export interface UnionVariant {
12
+ /** The class name — must match the `$defs` key produced by `nominal.ts`. */
13
+ className: string;
14
+ /**
15
+ * The literal discriminant value for this variant (the `const` value on the
16
+ * discriminant property in the variant's $def, e.g. `"dog"` for `kind:"dog"`).
17
+ */
18
+ discriminantValue: string;
19
+ }
20
+ /**
21
+ * Everything `buildUnionSchema` needs to emit the union fragment.
22
+ */
23
+ export interface UnionInfo {
24
+ /**
25
+ * Name of the shared const-tag property that discriminates the variants,
26
+ * e.g. `"kind"`.
27
+ */
28
+ discriminatorPropertyName: string;
29
+ /** Ordered list of union variants; must contain at least two entries. */
30
+ variants: UnionVariant[];
31
+ }
32
+ /**
33
+ * The JSON Schema fragment for a discriminated union per DESIGN §4.1:
34
+ *
35
+ * ```json
36
+ * {
37
+ * "oneOf": [ {"$ref":"#/$defs/Dog"}, {"$ref":"#/$defs/Cat"} ],
38
+ * "discriminator": {
39
+ * "propertyName": "kind",
40
+ * "mapping": { "dog": "#/$defs/Dog", "cat": "#/$defs/Cat" }
41
+ * },
42
+ * "x-apigen-logical": "union"
43
+ * }
44
+ * ```
45
+ */
46
+ export interface UnionSchema {
47
+ oneOf: Array<{
48
+ $ref: string;
49
+ }>;
50
+ discriminator: {
51
+ propertyName: string;
52
+ mapping: Record<string, string>;
53
+ };
54
+ [X_APIGEN_LOGICAL]: 'union';
55
+ }
56
+ /**
57
+ * Given a discriminated-union descriptor, emit the canonical OpenAPI-compatible
58
+ * `oneOf` + `discriminator` + `x-apigen-logical:"union"` schema fragment.
59
+ *
60
+ * Each variant is referenced via `$ref` (`#/$defs/<ClassName>`) — the caller is
61
+ * responsible for ensuring each variant's `$def` is registered in the descriptor
62
+ * (via `buildNominalSchema` from `nominal.ts`).
63
+ *
64
+ * Per DESIGN §4.1 `[inv:hints-advisory]`: `x-apigen-logical:"union"` is advisory.
65
+ * The structural contract (the `oneOf` + `discriminator`) is the authoritative
66
+ * wire representation and MUST be usable without the `x-apigen-*` key.
67
+ *
68
+ * @throws {Error} When `variants` is empty or contains a single entry (a
69
+ * one-variant "union" is not a union).
70
+ *
71
+ * @param info - Discriminated union descriptor.
72
+ * @returns The `oneOf` + `discriminator` schema fragment for inline use.
73
+ */
74
+ export declare function buildUnionSchema(info: UnionInfo): UnionSchema;
@@ -0,0 +1,81 @@
1
+ import { PluginLanguage } from './types';
2
+
3
+ /**
4
+ * Derive the canonical {@link PluginLanguage} tag for a source file from its
5
+ * extension.
6
+ *
7
+ * Returns `undefined` when the extension is not recognised — callers should
8
+ * treat an unknown extension as "no plugin will consume this file" rather than
9
+ * guessing.
10
+ *
11
+ * @example
12
+ * ```ts
13
+ * languageOfSource('src/api.ts') // → 'ts'
14
+ * languageOfSource('src/api.tsx') // → 'ts'
15
+ * languageOfSource('src/api.py') // → 'py'
16
+ * languageOfSource('src/api.go') // → 'go'
17
+ * languageOfSource('README.md') // → undefined
18
+ * ```
19
+ */
20
+ export declare function languageOfSource(file: string): PluginLanguage | undefined;
21
+ /**
22
+ * The minimal plugin shape that the routing helpers need to inspect.
23
+ *
24
+ * Both the v1 `OutputPlugin` and the v2 `Plugin` satisfy this interface
25
+ * because `language` is defined on both (as an optional field).
26
+ */
27
+ export interface LanguageAwarePlugin {
28
+ /** @see {@link PluginLanguage} */
29
+ language?: PluginLanguage;
30
+ }
31
+ /**
32
+ * Return the effective language for a plugin — the declared `language` if set,
33
+ * or `'ts'` as the documented default for back-compat.
34
+ *
35
+ * @param plugin - Any plugin that may declare `language`.
36
+ */
37
+ export declare function effectiveLanguage(plugin: LanguageAwarePlugin): PluginLanguage;
38
+ /**
39
+ * Returns `true` when the given plugin should consume `file`.
40
+ *
41
+ * A plugin consumes a file when:
42
+ * 1. The file's extension maps to a known {@link PluginLanguage}, AND
43
+ * 2. That language matches the plugin's effective language (declared or
44
+ * defaulting to `'ts'`).
45
+ *
46
+ * Files with unrecognised extensions are never routed to any plugin.
47
+ *
48
+ * @param plugin - The plugin to test.
49
+ * @param file - Absolute or relative path to the source file.
50
+ *
51
+ * @example
52
+ * ```ts
53
+ * pluginConsumesSource({ language: 'ts' }, 'src/api.ts') // → true
54
+ * pluginConsumesSource({ language: 'ts' }, 'src/api.py') // → false
55
+ * pluginConsumesSource({ language: 'py' }, 'src/api.py') // → true
56
+ * pluginConsumesSource({}, 'src/api.ts') // → true (default 'ts')
57
+ * ```
58
+ */
59
+ export declare function pluginConsumesSource(plugin: LanguageAwarePlugin, file: string): boolean;
60
+ /**
61
+ * Filter `files` to the subset whose language matches the given plugin.
62
+ *
63
+ * This is the primary entry-point for the `serve` command's dispatch loop:
64
+ * call once per plugin to obtain the slice of changed/watched files it should
65
+ * re-process.
66
+ *
67
+ * @param plugin - The plugin to route for.
68
+ * @param files - All candidate source files.
69
+ * @returns The subset of `files` the plugin should consume (may be empty).
70
+ *
71
+ * @example
72
+ * ```ts
73
+ * const all = ['src/api.ts', 'src/api.py', 'src/utils.mts', 'README.md']
74
+ * sourcesForPlugin({ language: 'ts' }, all)
75
+ * // → ['src/api.ts', 'src/utils.mts']
76
+ *
77
+ * sourcesForPlugin({ language: 'py' }, all)
78
+ * // → ['src/api.py']
79
+ * ```
80
+ */
81
+ export declare function sourcesForPlugin(plugin: LanguageAwarePlugin, files: readonly string[]): string[];
package/lib/types.d.ts ADDED
@@ -0,0 +1,87 @@
1
+ import { Operation } from './descriptor';
2
+ import { Logger } from 'pino';
3
+
4
+ export interface GeneratedSchemas {
5
+ metadata: {
6
+ namespace: string;
7
+ phase: string;
8
+ };
9
+ schemas: Record<string, {
10
+ input: Record<string, unknown>;
11
+ output: Record<string, unknown>;
12
+ hasCtx?: boolean;
13
+ safe?: boolean;
14
+ }>;
15
+ }
16
+ export type ComposedSchemas = Record<string, {
17
+ input: Record<string, unknown>;
18
+ output: Record<string, unknown>;
19
+ hasCtx?: boolean;
20
+ 'x-apigen-safe'?: boolean;
21
+ }>;
22
+ export type ExportMode = {
23
+ type: 'named';
24
+ } | {
25
+ type: 'default';
26
+ } | {
27
+ type: 'named-object';
28
+ name: string;
29
+ };
30
+ export interface PluginInput {
31
+ packages: Array<{
32
+ id: string;
33
+ schemas: ComposedSchemas;
34
+ importPath: string;
35
+ fns?: Record<string, (...args: unknown[]) => unknown>;
36
+ createClient?: (envelope: Record<string, unknown>) => Promise<unknown>;
37
+ }>;
38
+ outputDir: string;
39
+ options: Record<string, unknown>;
40
+ /**
41
+ * Shared structured logger (pino). Built once by the CLI and threaded through
42
+ * the pipeline + plugins. Always targets stderr or a file — never stdout —
43
+ * so the MCP stdio JSON-RPC channel stays clean. Plugins should fall back to
44
+ * a default stderr logger when this is absent.
45
+ */
46
+ logger?: Logger;
47
+ }
48
+ export interface PluginOutput {
49
+ files: Array<{
50
+ path: string;
51
+ content: string;
52
+ }>;
53
+ postCommands?: string[];
54
+ }
55
+ export interface RunInput extends PluginInput {
56
+ signal?: AbortSignal;
57
+ /**
58
+ * BUG-APIGEN-024: the full merged `Operation[]` descriptor (the same set
59
+ * `buildDescriptor()` produces), threaded through so a `--use` mount plugin
60
+ * (e.g. `apigen-plugin-openapi`) can build its real `Descriptor` instead of
61
+ * the empty-`operations` stub `collectMountRoutes()` used to synthesize.
62
+ * Absent for non-TS-extraction run paths (e.g. py-flask), where mount
63
+ * plugins have nothing extracted to describe.
64
+ */
65
+ operations?: Operation[];
66
+ }
67
+ /** Source-language tags understood by apigen's routing layer. */
68
+ export type PluginLanguage = 'ts' | 'py' | 'rust' | 'go' | 'java';
69
+ export interface OutputPlugin {
70
+ id: string;
71
+ description: string;
72
+ /**
73
+ * The source language this plugin consumes.
74
+ *
75
+ * Used by the `serve` command to route each source file to the plugin(s)
76
+ * whose `language` matches its extension (`.ts`/`.tsx`/`.mts`/`.cts` → `'ts'`,
77
+ * `.py` → `'py'`, etc.).
78
+ *
79
+ * Defaults to `'ts'` when omitted for backward-compatibility with plugins
80
+ * authored before this field was introduced. All first-party plugins
81
+ * explicitly declare `language: 'ts'`.
82
+ */
83
+ language?: PluginLanguage;
84
+ optionsSchema?: Record<string, unknown>;
85
+ generate(input: PluginInput): PluginOutput | Promise<PluginOutput>;
86
+ run?(input: RunInput): Promise<void>;
87
+ }
package/package.json ADDED
@@ -0,0 +1,17 @@
1
+ {
2
+ "name": "@adhd/apigen-core-client",
3
+ "version": "0.1.0",
4
+ "dependencies": {
5
+ "ts-morph": "^23.0.0",
6
+ "ts-json-schema-generator": "^2.3.0",
7
+ "pino": "10.3.1",
8
+ "typescript": "^6.0.3",
9
+ "@adhd/apigen-base-logical": "^0.0.1"
10
+ },
11
+ "main": "./index.js",
12
+ "module": "./index.mjs",
13
+ "typings": "./index.d.ts",
14
+ "publishConfig": {
15
+ "access": "public"
16
+ }
17
+ }