@adhd/apigen-engine-naming 0.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.
package/README.md ADDED
@@ -0,0 +1,18 @@
1
+ # @adhd/apigen-engine-naming
2
+
3
+ Naming and identifier helpers for apigen — case conversion, file-name normalization,
4
+ namespace/project id derivation, export collision detection, and the §9.1 envelope-key
5
+ conventions. Pure TypeScript (**platform: shared**).
6
+
7
+ Part of [apigen](../README.md).
8
+
9
+ ## Public API
10
+
11
+ ```ts
12
+ import { toKebab, toCamel, toPascal, toSnake, normalizeFileName, project, checkCollisions, CollisionDetectedError, envelopeKey, envelopeCliFlag, envelopeEnvVar, envelopeMetaKey } from '@adhd/apigen-engine-naming';
13
+ ```
14
+
15
+ - **case helpers** — `toKebab` / `toCamel` / `toPascal` / `toSnake`, `normalizeFileName`.
16
+ - **`project(...)`** — derive the namespace/id for a source.
17
+ - **`checkCollisions(...)`** — detect duplicate export ids (throws `CollisionDetectedError`).
18
+ - **envelope keys** — map an envelope field to its CLI flag / env var / MCP `_meta` key.
package/index.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ export { toKebab, toCamel, toPascal, toSnake, normalizeFileName, project, httpVerb, checkCollisions, CollisionDetectedError, envelopeKey, envelopeCliFlag, envelopeEnvVar, envelopeMetaKey, sanitizeIdentifier, } from './lib/naming';
2
+ export type { TransportProjection, HttpVerb, ProjectionConfig, CollisionError, } from './lib/naming';
package/index.js ADDED
@@ -0,0 +1,3 @@
1
+ "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});require("ts-morph");require("node:fs");require("node:path");require("ts-json-schema-generator");const y=new Set(["string","number","boolean","integer"]);function C(e){if(typeof e!="object"||e===null)return!1;const t=e;if(t.$ref!==void 0||t.oneOf||t.anyOf||t.allOf)return!1;const{type:n}=t;return typeof n=="string"?y.has(n):Array.isArray(n)?n.every(r=>y.has(r)||r==="null"):Array.isArray(t.enum)&&t.enum.length>0?t.enum.every(r=>["string","number","boolean"].includes(typeof r)):!1}function P(e){if(!e||typeof e!="object")return!1;const t=e.properties;return t===void 0||typeof t!="object"||t===null?!1:Object.values(t).every(C)}const k={"decimal.js":"Decimal"};function x(e){return e.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")}Object.entries(k).map(([e,t])=>({pattern:new RegExp(`import\\(["'][^"']*${x(e)}[^"']*["']\\)\\.\\w+`,"g"),key:t}));function h(e){return e.words.join("-")}function _(e){return e.words.map((t,n)=>n===0?t:E(t)).join("")}function v(e){return e.words.map(E).join("")}function $(e){return e.words.join("_")}function w(e){return e.replace(/\.[^.]+$/,"").replace(/[._]+/g,"-")}function b(e,t={}){var l,u;const n=[e.namespace,...e.path],r=e.safe||P(e.input)?"GET":"POST",a=((u=(l=t.http)==null?void 0:l.verb)==null?void 0:u[e.id])??r,p="/"+n.map(s=>h(s)).join("/"),c=n.map(s=>$(s)).join("_"),i=n.map(s=>$(s)).slice(0,-1).join("."),g=v(n[n.length-2]??n[0]),f=v(n[n.length-1]),d=n.map(s=>h(s));return{http:{verb:a,route:p},mcp:{name:c},grpc:{package:i,service:g,method:f},cli:{path:d}}}function K(e,t,n){var a,p;const r=(p=(a=n.http)==null?void 0:a.verb)==null?void 0:p[e];return r==="GET"||r==="POST"?r:t["x-apigen-safe"]?"GET":"POST"}class j extends Error{constructor(t){const n=t.map(r=>` [${r.transport}] "${r.target}" ← ${r.ids[0]} vs ${r.ids[1]}`);super(`apigen-naming: ${t.length} projection collision(s) detected:
2
+ ${n.join(`
3
+ `)}`),this.name="CollisionDetectedError",this.collisions=t}}function M(e,t={}){const n=new Map,r=new Map,a=new Map,p=new Map,c=[];for(const o of e){const i=b(o,t),g=`${i.http.verb} ${i.http.route}`,f=n.get(g);f!==void 0&&f!==o.id?c.push({transport:"http",target:g,ids:[f,o.id]}):n.set(g,o.id);const d=r.get(i.mcp.name);d!==void 0&&d!==o.id?c.push({transport:"mcp",target:i.mcp.name,ids:[d,o.id]}):r.set(i.mcp.name,o.id);const l=`${i.grpc.package}.${i.grpc.service}/${i.grpc.method}`,u=a.get(l);u!==void 0&&u!==o.id?c.push({transport:"grpc",target:l,ids:[u,o.id]}):a.set(l,o.id);const s=i.cli.path.join(" "),m=p.get(s);m!==void 0&&m!==o.id?c.push({transport:"cli",target:s,ids:[m,o.id]}):p.set(s,o.id)}if(c.length>0)throw new j(c)}function S(e,t){return`x-${e}-${t}`}function O(e,t){return`--${e}-${t}`}function T(e,t){const n=r=>r.toUpperCase().replace(/-/g,"_");return e==="adhd"?`APIGEN_${n(t)}`:`APIGEN_${n(e)}_${n(t)}`}const A=S;function z(e){let t=e.replace(/[^a-zA-Z0-9_$]/g,"_");return(t===""||/^[0-9]/.test(t))&&(t=`_${t}`),t}function E(e){return e.length===0?e:e[0].toUpperCase()+e.slice(1)}exports.CollisionDetectedError=j;exports.checkCollisions=M;exports.envelopeCliFlag=O;exports.envelopeEnvVar=T;exports.envelopeKey=S;exports.envelopeMetaKey=A;exports.httpVerb=K;exports.normalizeFileName=w;exports.project=b;exports.sanitizeIdentifier=z;exports.toCamel=_;exports.toKebab=h;exports.toPascal=v;exports.toSnake=$;
package/index.mjs ADDED
@@ -0,0 +1,148 @@
1
+ import "ts-morph";
2
+ import "node:fs";
3
+ import "node:path";
4
+ import "ts-json-schema-generator";
5
+ const h = /* @__PURE__ */ new Set(["string", "number", "boolean", "integer"]);
6
+ function j(e) {
7
+ if (typeof e != "object" || e === null)
8
+ return !1;
9
+ const t = e;
10
+ if (t.$ref !== void 0 || t.oneOf || t.anyOf || t.allOf)
11
+ return !1;
12
+ const { type: n } = t;
13
+ return typeof n == "string" ? h.has(n) : Array.isArray(n) ? n.every((r) => h.has(r) || r === "null") : Array.isArray(t.enum) && t.enum.length > 0 ? t.enum.every((r) => ["string", "number", "boolean"].includes(typeof r)) : !1;
14
+ }
15
+ function S(e) {
16
+ if (!e || typeof e != "object")
17
+ return !1;
18
+ const t = e.properties;
19
+ return t === void 0 || typeof t != "object" || t === null ? !1 : Object.values(t).every(
20
+ j
21
+ );
22
+ }
23
+ const E = {
24
+ "decimal.js": "Decimal"
25
+ };
26
+ function x(e) {
27
+ return e.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
28
+ }
29
+ Object.entries(E).map(([e, t]) => ({
30
+ // Matches: import("...{modSpec}...").<anyMemberName> (single or double quotes)
31
+ // The module specifier appears inside an absolute path, so we allow any
32
+ // characters between the quote and the specifier, and between the specifier
33
+ // and the closing quote. The trailing member accessor (`.default`, `.Decimal`,
34
+ // …) is matched generically (`\w+`) rather than anchored to one spelling —
35
+ // see the doc comment above for why.
36
+ pattern: new RegExp(
37
+ `import\\(["'][^"']*${x(e)}[^"']*["']\\)\\.\\w+`,
38
+ "g"
39
+ ),
40
+ key: t
41
+ }));
42
+ function $(e) {
43
+ return e.words.join("-");
44
+ }
45
+ function M(e) {
46
+ return e.words.map((t, n) => n === 0 ? t : b(t)).join("");
47
+ }
48
+ function v(e) {
49
+ return e.words.map(b).join("");
50
+ }
51
+ function y(e) {
52
+ return e.words.join("_");
53
+ }
54
+ function T(e) {
55
+ return e.replace(/\.[^.]+$/, "").replace(/[._]+/g, "-");
56
+ }
57
+ function P(e, t = {}) {
58
+ var l, u;
59
+ const n = [e.namespace, ...e.path], r = e.safe || S(e.input) ? "GET" : "POST", p = ((u = (l = t.http) == null ? void 0 : l.verb) == null ? void 0 : u[e.id]) ?? r, a = "/" + n.map((s) => $(s)).join("/"), c = n.map((s) => y(s)).join("_"), i = n.map((s) => y(s)).slice(0, -1).join("."), g = v(n[n.length - 2] ?? n[0]), f = v(n[n.length - 1]), m = n.map((s) => $(s));
60
+ return {
61
+ http: { verb: p, route: a },
62
+ mcp: { name: c },
63
+ grpc: { package: i, service: g, method: f },
64
+ cli: { path: m }
65
+ };
66
+ }
67
+ function G(e, t, n) {
68
+ var p, a;
69
+ const r = (a = (p = n.http) == null ? void 0 : p.verb) == null ? void 0 : a[e];
70
+ return r === "GET" || r === "POST" ? r : t["x-apigen-safe"] ? "GET" : "POST";
71
+ }
72
+ class _ extends Error {
73
+ constructor(t) {
74
+ const n = t.map(
75
+ (r) => ` [${r.transport}] "${r.target}" ← ${r.ids[0]} vs ${r.ids[1]}`
76
+ );
77
+ super(
78
+ `apigen-naming: ${t.length} projection collision(s) detected:
79
+ ${n.join(`
80
+ `)}`
81
+ ), this.name = "CollisionDetectedError", this.collisions = t;
82
+ }
83
+ }
84
+ function K(e, t = {}) {
85
+ const n = /* @__PURE__ */ new Map(), r = /* @__PURE__ */ new Map(), p = /* @__PURE__ */ new Map(), a = /* @__PURE__ */ new Map(), c = [];
86
+ for (const o of e) {
87
+ const i = P(o, t), g = `${i.http.verb} ${i.http.route}`, f = n.get(g);
88
+ f !== void 0 && f !== o.id ? c.push({
89
+ transport: "http",
90
+ target: g,
91
+ ids: [f, o.id]
92
+ }) : n.set(g, o.id);
93
+ const m = r.get(i.mcp.name);
94
+ m !== void 0 && m !== o.id ? c.push({
95
+ transport: "mcp",
96
+ target: i.mcp.name,
97
+ ids: [m, o.id]
98
+ }) : r.set(i.mcp.name, o.id);
99
+ const l = `${i.grpc.package}.${i.grpc.service}/${i.grpc.method}`, u = p.get(l);
100
+ u !== void 0 && u !== o.id ? c.push({
101
+ transport: "grpc",
102
+ target: l,
103
+ ids: [u, o.id]
104
+ }) : p.set(l, o.id);
105
+ const s = i.cli.path.join(" "), d = a.get(s);
106
+ d !== void 0 && d !== o.id ? c.push({
107
+ transport: "cli",
108
+ target: s,
109
+ ids: [d, o.id]
110
+ }) : a.set(s, o.id);
111
+ }
112
+ if (c.length > 0)
113
+ throw new _(c);
114
+ }
115
+ function w(e, t) {
116
+ return `x-${e}-${t}`;
117
+ }
118
+ function z(e, t) {
119
+ return `--${e}-${t}`;
120
+ }
121
+ function N(e, t) {
122
+ const n = (r) => r.toUpperCase().replace(/-/g, "_");
123
+ return e === "adhd" ? `APIGEN_${n(t)}` : `APIGEN_${n(e)}_${n(t)}`;
124
+ }
125
+ const D = w;
126
+ function U(e) {
127
+ let t = e.replace(/[^a-zA-Z0-9_$]/g, "_");
128
+ return (t === "" || /^[0-9]/.test(t)) && (t = `_${t}`), t;
129
+ }
130
+ function b(e) {
131
+ return e.length === 0 ? e : e[0].toUpperCase() + e.slice(1);
132
+ }
133
+ export {
134
+ _ as CollisionDetectedError,
135
+ K as checkCollisions,
136
+ z as envelopeCliFlag,
137
+ N as envelopeEnvVar,
138
+ w as envelopeKey,
139
+ D as envelopeMetaKey,
140
+ G as httpVerb,
141
+ T as normalizeFileName,
142
+ P as project,
143
+ U as sanitizeIdentifier,
144
+ M as toCamel,
145
+ $ as toKebab,
146
+ v as toPascal,
147
+ y as toSnake
148
+ };
@@ -0,0 +1,239 @@
1
+ import { Segment, Operation } from '@adhd/apigen-core-client';
2
+
3
+ /**
4
+ * Returns a kebab-case rendering of a {@link Segment}'s tokenized words.
5
+ *
6
+ * Used by HTTP routes (`/transform/humanize/humanize-bytes`) and CLI commands
7
+ * (`transform humanize humanize-bytes`).
8
+ *
9
+ * @example toKebab({ raw: 'humanizeBytes', words: ['humanize', 'bytes'] })
10
+ * // → 'humanize-bytes'
11
+ */
12
+ export declare function toKebab(seg: Segment): string;
13
+ /**
14
+ * Returns a camelCase rendering of a {@link Segment}'s tokenized words.
15
+ *
16
+ * @example toCamel({ raw: 'humanizeBytes', words: ['humanize', 'bytes'] })
17
+ * // → 'humanizeBytes'
18
+ */
19
+ export declare function toCamel(seg: Segment): string;
20
+ /**
21
+ * Returns a PascalCase rendering of a {@link Segment}'s tokenized words.
22
+ *
23
+ * Used by gRPC service/method names (`HumanizeBytes`).
24
+ *
25
+ * @example toPascal({ raw: 'humanizeBytes', words: ['humanize', 'bytes'] })
26
+ * // → 'HumanizeBytes'
27
+ */
28
+ export declare function toPascal(seg: Segment): string;
29
+ /**
30
+ * Returns a snake_case rendering of a {@link Segment}'s tokenized words.
31
+ *
32
+ * @example toSnake({ raw: 'humanizeBytes', words: ['humanize', 'bytes'] })
33
+ * // → 'humanize_bytes'
34
+ */
35
+ export declare function toSnake(seg: Segment): string;
36
+ /**
37
+ * Normalises a raw file name segment to a kebab-case path component.
38
+ *
39
+ * Rules (SPEC §5):
40
+ * - Strip extension.
41
+ * - Dots and underscores → hyphens (`file.name.ts` → `file-name`).
42
+ *
43
+ * @example normalizeFileName('file.name.ts') // → 'file-name'
44
+ * @example normalizeFileName('my_util.js') // → 'my-util'
45
+ */
46
+ export declare function normalizeFileName(raw: string): string;
47
+ /**
48
+ * Result of a full per-transport projection for one {@link Operation}.
49
+ */
50
+ export interface TransportProjection {
51
+ /** HTTP: full route (e.g. `POST /transform/humanize/humanize-bytes`). */
52
+ http: {
53
+ verb: HttpVerb;
54
+ route: string;
55
+ };
56
+ /** MCP: flat tool name joined with `_` (e.g. `transform_humanize_humanize_bytes`). */
57
+ mcp: {
58
+ name: string;
59
+ };
60
+ /**
61
+ * gRPC: dotted package + Pascal service (file segment) + Pascal method (export segment).
62
+ * e.g. `{ package: 'transform.humanize', service: 'Humanize', method: 'HumanizeBytes' }`.
63
+ */
64
+ grpc: {
65
+ package: string;
66
+ service: string;
67
+ method: string;
68
+ };
69
+ /** CLI: ordered kebab command segments (e.g. `['transform', 'humanize', 'humanize-bytes']`). */
70
+ cli: {
71
+ path: string[];
72
+ };
73
+ }
74
+ export type HttpVerb = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
75
+ /**
76
+ * Per-operation override configuration for projection.
77
+ *
78
+ * Pass at generate/run time via `--opt http.verb.<id>=GET` or a project
79
+ * config file. **Never** modify source (Tenet 1).
80
+ */
81
+ export interface ProjectionConfig {
82
+ /**
83
+ * Override the HTTP verb for specific operations by their canonical `id`.
84
+ *
85
+ * Example: `{ 'transform/humanize/humanize-bytes': 'GET' }`.
86
+ */
87
+ http?: {
88
+ verb?: Record<string, HttpVerb>;
89
+ };
90
+ }
91
+ /**
92
+ * Projects one {@link Operation} to all transport targets per SPEC §5.
93
+ *
94
+ * Verb derivation: `safe → GET`, `!safe → POST` — then overridable via
95
+ * {@link ProjectionConfig}. **Never** derived from `kind`.
96
+ *
97
+ * FEAT-APIGEN-022: also hoists to `GET` when `op.input`'s domain params are
98
+ * ALL "properly typed primitives" (or zero params) per
99
+ * {@link isPrimitiveOnlyInputSchema}, even when `op.safe` is `false` — this
100
+ * is the single decision point every transport (HTTP directly here for gRPC/
101
+ * openapi/mcp paths that read `op` directly; HTTP express/fastify indirectly
102
+ * via the `x-apigen-safe` stamp {@link httpVerb} reads, computed identically
103
+ * in `compose-schemas.ts`) inherits from, so auto-hoist logic is never
104
+ * duplicated per plugin.
105
+ *
106
+ * @param op The canonical operation descriptor.
107
+ * @param config Optional projection overrides (e.g. http.verb per id).
108
+ */
109
+ export declare function project(op: Operation, config?: ProjectionConfig): TransportProjection;
110
+ /**
111
+ * Resolves the effective HTTP verb for an already-`ComposedSchemas`-shaped
112
+ * function schema (`{ input, output, 'x-apigen-safe'?, … }`).
113
+ *
114
+ * This is the SINGLE definition every HTTP-emitting plugin
115
+ * (`apigen-plugin-api-express`'s `run.ts`/`generate.ts`, `apigen-plugin-api-
116
+ * fastify`'s `run.ts`/`generate.ts`) must import instead of re-declaring —
117
+ * previously duplicated verbatim at all four call sites (BUG-APIGEN-025's
118
+ * root-cause finding). Precedence:
119
+ *
120
+ * 1. `config.http.verb[fnId]` — the manual `--opt http.verb.<id>=GET`
121
+ * override (Tenet 1: expressed at generate/run time, never in source).
122
+ * **Always wins**, even against auto-detected `x-apigen-safe: true`.
123
+ * 2. `schema['x-apigen-safe']` — stamped by `composeSchemas()`
124
+ * (`op.safe` OR the FEAT-APIGEN-022 primitive-param-shape auto-hoist;
125
+ * see `compose-schemas.ts` / `get-safety.ts`).
126
+ * 3. Falls back to `'POST'` when neither is present.
127
+ *
128
+ * @param fnId The `<pkgId>:<fnName>` key used in the override map.
129
+ * @param schema The composed schema entry (or any object carrying an
130
+ * `x-apigen-safe` boolean at its top level).
131
+ * @param config Projection overrides.
132
+ */
133
+ export declare function httpVerb(fnId: string, schema: Record<string, unknown>, config: ProjectionConfig): 'GET' | 'POST';
134
+ /**
135
+ * A collision detected between two operations that project to the same target.
136
+ */
137
+ export interface CollisionError {
138
+ /** The transport where the collision occurred. */
139
+ transport: 'http' | 'mcp' | 'grpc' | 'cli';
140
+ /** The projected target string that both operations map to. */
141
+ target: string;
142
+ /** The two distinct operation ids that collide. */
143
+ ids: [string, string];
144
+ }
145
+ /**
146
+ * Thrown when {@link checkCollisions} finds two operations that project to
147
+ * the same transport target. This is a **hard extract-time error** (SPEC §5) —
148
+ * never silent last-writer-wins.
149
+ */
150
+ export declare class CollisionDetectedError extends Error {
151
+ readonly collisions: CollisionError[];
152
+ constructor(collisions: CollisionError[]);
153
+ }
154
+ /**
155
+ * Runs the uniqueness invariant over a merged descriptor array (SPEC §5).
156
+ *
157
+ * Checks every transport target (HTTP route+verb, MCP name, gRPC
158
+ * package+service+method, CLI path) across all operations. Two distinct `id`s
159
+ * that project to the same target in **any** transport constitute a hard
160
+ * extract-time error — {@link CollisionDetectedError} is thrown.
161
+ *
162
+ * Call this **once** over the full merged descriptor after all operations have
163
+ * been built. Guards the default-object recursion + multi-file glob cases.
164
+ *
165
+ * @param operations The merged set of operations to check.
166
+ * @param config Optional projection overrides (passed through to `project`).
167
+ * @throws {CollisionDetectedError} if any two distinct ids project to the same target.
168
+ */
169
+ export declare function checkCollisions(operations: Operation[], config?: ProjectionConfig): void;
170
+ /**
171
+ * Computes the canonical **HTTP / gRPC metadata / MCP `_meta`** key for an
172
+ * envelope field, following the `x-<pluginId>-<field>` convention (SPEC §9.1).
173
+ *
174
+ * Builtin fields (pluginId = `'adhd'`) drop the plugin segment and use
175
+ * `x-adhd-<field>` (per SPEC §9.1, rule 3).
176
+ *
177
+ * The **same key** is used for HTTP headers, gRPC metadata (ASCII value), and
178
+ * MCP `_meta` entries — one mental model across all k/v carriers (rule 1).
179
+ *
180
+ * For **binary gRPC metadata** append `-bin` to the returned key and base64-
181
+ * encode the value; that detail lives in the gRPC transport adapter, not here.
182
+ *
183
+ * @example envelopeKey('auth', 'session') // → 'x-auth-session'
184
+ * @example envelopeKey('adhd', 'trace-id') // → 'x-adhd-trace-id'
185
+ */
186
+ export declare function envelopeKey(pluginId: string, field: string): string;
187
+ /**
188
+ * Computes the **CLI flag name** for an envelope field (SPEC §9.1, rule 2).
189
+ *
190
+ * Surface: `--<pluginId>-<field>` (e.g. `--auth-session`).
191
+ * Builtin fields (`pluginId = 'adhd'`) use `--adhd-<field>`.
192
+ *
193
+ * @example envelopeCliFlag('auth', 'session') // → '--auth-session'
194
+ * @example envelopeCliFlag('adhd', 'trace-id') // → '--adhd-trace-id'
195
+ */
196
+ export declare function envelopeCliFlag(pluginId: string, field: string): string;
197
+ /**
198
+ * Computes the **environment variable name** for an envelope field
199
+ * (SPEC §9.1, rule 2). CLI flag takes precedence over env when both present.
200
+ *
201
+ * Surface: `APIGEN_<PLUGINID>_<FIELD>` (upper-cased, hyphens → underscores).
202
+ * Builtin fields (pluginId = `'adhd'`) drop the plugin segment and use
203
+ * `APIGEN_<FIELD>` (SPEC §9.1, rule 3).
204
+ *
205
+ * @example envelopeEnvVar('auth', 'session') // → 'APIGEN_AUTH_SESSION'
206
+ * @example envelopeEnvVar('adhd', 'trace-id') // → 'APIGEN_TRACE_ID'
207
+ */
208
+ export declare function envelopeEnvVar(pluginId: string, field: string): string;
209
+ /**
210
+ * Computes the **MCP `_meta` key** for an envelope field — the same
211
+ * `x-<pluginId>-<field>` string used in HTTP/gRPC (SPEC §9.1, rule 1).
212
+ *
213
+ * This is an explicit alias of {@link envelopeKey} with a transport-specific
214
+ * name so call sites are self-documenting.
215
+ *
216
+ * @example envelopeMetaKey('auth', 'session') // → 'x-auth-session'
217
+ */
218
+ export declare const envelopeMetaKey: typeof envelopeKey;
219
+ /**
220
+ * Sanitizes an arbitrary id (e.g. a discovered package id, derived verbatim
221
+ * from a source directory name) into a valid TypeScript/JavaScript identifier
222
+ * for use in generated import-namespace / variable-name positions.
223
+ *
224
+ * Repo convention mandates hyphenated package names (`dispatch-cli`,
225
+ * `pkg-a`, …), so any generator that splices an id directly into an
226
+ * identifier position (`import * as ${id}_ns from …`) produces a hard parse
227
+ * error (`Expected "from" but found "-"`) the moment a hyphenated id is
228
+ * used. This is the single source of truth for that sanitisation — no
229
+ * transport-specific plugin may re-derive its own copy (BUG-APIGEN-032).
230
+ *
231
+ * Only the emitted *identifier* positions need sanitizing — the raw id
232
+ * should still be used verbatim anywhere it appears as a string literal
233
+ * (e.g. a schema-key namespace like `'dispatch-cli:validate'`).
234
+ *
235
+ * @example sanitizeIdentifier('dispatch-cli') // → 'dispatch_cli'
236
+ * @example sanitizeIdentifier('pkg-a') // → 'pkg_a'
237
+ * @example sanitizeIdentifier('123abc') // → '_123abc'
238
+ */
239
+ export declare function sanitizeIdentifier(id: string): string;
package/package.json ADDED
@@ -0,0 +1,13 @@
1
+ {
2
+ "name": "@adhd/apigen-engine-naming",
3
+ "version": "0.1.1",
4
+ "dependencies": {
5
+ "@adhd/apigen-core-client": "^0.1.0"
6
+ },
7
+ "main": "./index.js",
8
+ "module": "./index.mjs",
9
+ "typings": "./index.d.ts",
10
+ "publishConfig": {
11
+ "access": "public"
12
+ }
13
+ }