@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 +18 -0
- package/index.d.ts +2 -0
- package/index.js +3 -0
- package/index.mjs +148 -0
- package/lib/naming.d.ts +239 -0
- package/package.json +13 -0
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
|
+
};
|
package/lib/naming.d.ts
ADDED
|
@@ -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
|
+
}
|