@plasm_lang/vercel-agent 0.3.63
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 +235 -0
- package/agent/.plasm/archives/runs/runs/pr2d5c4a99707b4c19b650553d50229a1d600d28e3d98a9c58f18e5026cecc86ca.json +64 -0
- package/agent/.plasm/archives/runs/runs/pr2e0c0d8ad443c63c82da7435ee1a002b0e0fa718b640263c0a9d3e6e5944812f.json +64 -0
- package/agent/.plasm/archives/runs/runs/pr2faedb8354f40ee6d828e3af07b421fda9ccda973a4f7347fce3639f03a0a869.json +64 -0
- package/agent/.plasm/archives/runs/runs/pr586b47c55547b0702c572bce4255558b22dbe5e682d6359169577e0ea75fe98f.json +64 -0
- package/agent/.plasm/archives/runs/runs/pr76212356445e3b00fcf256835aaec18bac68576324b90d0be92d47f0b4a862a7.json +56 -0
- package/agent/.plasm/archives/runs/runs/pr9ec805d689e00db9270a9539858f2fb7216c24acbfea943d450e37b641149da1.json +64 -0
- package/agent/.plasm/archives/runs/runs/prc3c0c4ba2e28fc94ed6d37b6796e277a7997d9cb3184640d14c35c98bc6d136f.json +64 -0
- package/agent/.plasm/archives/runs/runs/prf04de32522f2fdcb17818907d91bccce7dcaecbd1259041cc448d447b6993244.json +64 -0
- package/agent/.plasm/archives/traces/traces/local/00000000000000000000000000000000/records.ndjson +1 -0
- package/agent/.plasm/archives/traces/traces/local/00000000000000000000000000000000/summary.json +13 -0
- package/agent/.plasm/discovery/manifest.json +126 -0
- package/agent/.plasm/sessions/TGlzdCBwcm9kdWN0cyBmcm9tIHRoZSBleGVjdXRlX3RpbnkgY2F0YWxvZw.json +44 -0
- package/agent/.plasm/sessions/TGlzdCBwcm9kdWN0cyBmcm9tIHRoZSBleGVjdXRlX3RpbnkgY2F0YWxvZw.teaching.tsv +23 -0
- package/agent/.plasm/sessions/bGlzdCBleGVjdXRlX3RpbnkgcHJvZHVjdHM.json +151 -0
- package/agent/.plasm/sessions/bGlzdCBleGVjdXRlX3RpbnkgcHJvZHVjdHM.teaching.tsv +131 -0
- package/agent/.plasm/sessions/bGlzdCBleGVjdXRlX3RpbnkgcHJvZHVjdHMgYXN5bmMgdHJhbnNwb3J0.json +44 -0
- package/agent/.plasm/sessions/bGlzdCBleGVjdXRlX3RpbnkgcHJvZHVjdHMgYXN5bmMgdHJhbnNwb3J0.teaching.tsv +23 -0
- package/agent/.plasm/stubs/.gitkeep +0 -0
- package/agent/.plasm/stubs/execute_tiny.ts +107 -0
- package/agent/agent.ts +52 -0
- package/agent/catalogs/README.md +15 -0
- package/agent/channels/.gitkeep +0 -0
- package/agent/channels/execute-tiny-webhook.ts +59 -0
- package/agent/channels/health.ts +16 -0
- package/agent/hooks/.gitkeep +0 -0
- package/agent/hooks/trace-log.ts +10 -0
- package/agent/instructions.md +25 -0
- package/agent/schedules/.gitkeep +0 -0
- package/agent/schedules/ping.ts +13 -0
- package/agent/skills/.gitkeep +0 -0
- package/agent/skills/plasm-authoring.md +8 -0
- package/agent/subagents/.gitkeep +0 -0
- package/agent/subagents/tiny/agent.ts +28 -0
- package/bin/plasm-agent.mjs +18 -0
- package/package.json +77 -0
- package/scripts/plasm-node.mjs +19 -0
- package/scripts/resolve-ts-extension.mjs +18 -0
- package/src/archive/adapters.ts +27 -0
- package/src/archive/index.ts +99 -0
- package/src/archive/local-run-archive.ts +90 -0
- package/src/archive/local-trace-archive.ts +91 -0
- package/src/archive/paths.ts +15 -0
- package/src/archive/postgres-kv-adapter.ts +72 -0
- package/src/archive/prod-archive-store.ts +143 -0
- package/src/archive/resolve-backend.ts +60 -0
- package/src/archive/run-id.ts +23 -0
- package/src/archive/types.ts +75 -0
- package/src/archive/vercel-blob-adapter.ts +21 -0
- package/src/archive/vercel-kv-adapter.ts +24 -0
- package/src/authoring/channel-dispatch.ts +44 -0
- package/src/authoring/context.ts +34 -0
- package/src/authoring/define-channel.ts +83 -0
- package/src/authoring/define-hook.ts +51 -0
- package/src/authoring/define-schedule.ts +64 -0
- package/src/authoring/define-skill.ts +38 -0
- package/src/authoring/hook-runner.ts +18 -0
- package/src/authoring/schedule-manager.ts +118 -0
- package/src/authoring/slot-loader.ts +253 -0
- package/src/authoring/subagent-loader.ts +121 -0
- package/src/catalog/loader.ts +71 -0
- package/src/cli/build.ts +54 -0
- package/src/cli/dev.ts +60 -0
- package/src/cli/info.ts +12 -0
- package/src/cli/init.ts +372 -0
- package/src/cli/link.ts +68 -0
- package/src/cli/project-root.ts +57 -0
- package/src/define-agent.ts +150 -0
- package/src/dev/client/ansi.ts +36 -0
- package/src/dev/client/http-session.ts +180 -0
- package/src/dev/client/repl.ts +92 -0
- package/src/dev/client/slash.ts +119 -0
- package/src/dev/dev-session.ts +153 -0
- package/src/dev/http.ts +29 -0
- package/src/dev/server.ts +147 -0
- package/src/dev/session-routes.ts +185 -0
- package/src/discovery/project-walker.ts +272 -0
- package/src/engine/connect-auth.ts +135 -0
- package/src/engine/create-host-transport.ts +7 -0
- package/src/engine/fixture-mock-transport.ts +54 -0
- package/src/engine/host-transport-bridge.ts +32 -0
- package/src/engine/host-transport.ts +84 -0
- package/src/engine/napi-binding.ts +265 -0
- package/src/evals/define-eval.ts +56 -0
- package/src/evals/run-eval.ts +136 -0
- package/src/gateway-model.ts +43 -0
- package/src/index.ts +296 -0
- package/src/instrumentation.ts +56 -0
- package/src/load-env.ts +63 -0
- package/src/operator/routes.ts +287 -0
- package/src/operator/types.ts +63 -0
- package/src/operator/ui-shell.ts +134 -0
- package/src/project-info.ts +229 -0
- package/src/runtime/agent-runtime.ts +469 -0
- package/src/runtime/compaction.ts +81 -0
- package/src/runtime/logical-session.ts +72 -0
- package/src/runtime/plasm-agent.ts +199 -0
- package/src/server/plasm-handler.ts +331 -0
- package/src/session-state.ts +135 -0
- package/src/state/define-state.ts +57 -0
- package/src/state/fs-state-adapter.ts +72 -0
- package/src/state/kv-state-adapter.ts +62 -0
- package/src/state/postgres-state-adapter.ts +116 -0
- package/src/stubs/capability-invoke-shape.ts +135 -0
- package/src/stubs/catalog-client.ts +170 -0
- package/src/stubs/catalog-hash.ts +11 -0
- package/src/stubs/catalog-introspection.ts +121 -0
- package/src/stubs/cgs-ts-types.ts +164 -0
- package/src/stubs/domain-parser.ts +203 -0
- package/src/stubs/generator.ts +390 -0
- package/src/stubs/input-type-to-ts.ts +233 -0
- package/src/stubs/plasm-value-emitter.ts +162 -0
- package/src/stubs/program-builder.ts +82 -0
- package/src/stubs/stub-symbols.ts +89 -0
- package/src/symbol-registry.ts +74 -0
- package/src/telemetry/plasm-spans.ts +83 -0
- package/src/tools/descriptions.ts +94 -0
- package/src/tools/format.ts +29 -0
- package/src/tools/harness-tools.ts +65 -0
- package/src/tools/plasm-tools.ts +104 -0
- package/src/workflow/world-bootstrap.ts +52 -0
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { mkdir, readFile, stat, writeFile } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
import { createEngine, type PlasmEngine } from "../engine/napi-binding.js";
|
|
6
|
+
import { parseCatalogIntrospection, type CatalogIntrospectionJson } from "./catalog-introspection.js";
|
|
7
|
+
import {
|
|
8
|
+
capabilityNeedsInput,
|
|
9
|
+
capabilityReturnsScalar,
|
|
10
|
+
capabilityReturnsVoid,
|
|
11
|
+
classifyInvokeShape,
|
|
12
|
+
} from "./capability-invoke-shape.js";
|
|
13
|
+
import {
|
|
14
|
+
capabilityInputTypeName,
|
|
15
|
+
renderBrandTypes,
|
|
16
|
+
renderCapabilityInputType,
|
|
17
|
+
renderEntityRowType,
|
|
18
|
+
} from "./input-type-to-ts.js";
|
|
19
|
+
import { renderProgramStatements } from "./plasm-value-emitter.js";
|
|
20
|
+
import { parseCgsDomain, toLegacyParsedDomain, type ParsedCgsDomain } from "./domain-parser.js";
|
|
21
|
+
import {
|
|
22
|
+
assignCapabilityBindings,
|
|
23
|
+
capabilityReturnTypeName,
|
|
24
|
+
stubEntityNames,
|
|
25
|
+
type CapabilityBinding,
|
|
26
|
+
} from "./stub-symbols.js";
|
|
27
|
+
|
|
28
|
+
/** @deprecated Use {@link ParsedCgsDomain} from domain-parser. */
|
|
29
|
+
export interface ParsedEntity {
|
|
30
|
+
name: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** @deprecated Use {@link ParsedCgsDomain} from domain-parser. */
|
|
34
|
+
export interface ParsedCapability {
|
|
35
|
+
name: string;
|
|
36
|
+
entity: string;
|
|
37
|
+
kind?: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** @deprecated Use {@link ParsedCgsDomain} from domain-parser. */
|
|
41
|
+
export interface ParsedDomain {
|
|
42
|
+
entryId: string;
|
|
43
|
+
authScheme?: string;
|
|
44
|
+
entities: ParsedEntity[];
|
|
45
|
+
capabilities: ParsedCapability[];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface StubGenerationResult {
|
|
49
|
+
entryId: string;
|
|
50
|
+
catalogCgsHash: string;
|
|
51
|
+
outPath: string;
|
|
52
|
+
generatedAt: string;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const PROVENANCE_RE =
|
|
56
|
+
/@generated catalog_cgs_hash=([a-f0-9]+) entry_id=([^\s]+)/;
|
|
57
|
+
|
|
58
|
+
export function computeCatalogCgsHash(domainYaml: string, mappingsYaml: string): string {
|
|
59
|
+
return createHash("sha256").update(domainYaml).update("\n").update(mappingsYaml).digest("hex");
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Legacy parser — delegates to {@link parseCgsDomain}. */
|
|
63
|
+
export function parseDomainYaml(raw: string, fallbackEntryId: string): ParsedDomain {
|
|
64
|
+
return toLegacyParsedDomain(parseCgsDomain(raw, fallbackEntryId));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function toExportName(entryId: string): string {
|
|
68
|
+
const safe = entryId.replace(/[^a-zA-Z0-9_]/g, "_");
|
|
69
|
+
return /^[a-zA-Z_]/.test(safe) ? safe : `_${safe}`;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function indent(text: string, spaces: number): string {
|
|
73
|
+
const pad = " ".repeat(spaces);
|
|
74
|
+
return text
|
|
75
|
+
.split("\n")
|
|
76
|
+
.map((line) => (line ? pad + line : line))
|
|
77
|
+
.join("\n");
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function entityByName(catalog: CatalogIntrospectionJson, name: string) {
|
|
81
|
+
return catalog.entities.find((e) => e.name === name);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function renderCapabilityFunction(
|
|
85
|
+
catalog: CatalogIntrospectionJson,
|
|
86
|
+
binding: CapabilityBinding | undefined,
|
|
87
|
+
): string {
|
|
88
|
+
const cap = catalog.capabilities.find((c) => c.name === binding?.capability);
|
|
89
|
+
if (!cap || !binding) {
|
|
90
|
+
const name = binding?.capability ?? "unknown";
|
|
91
|
+
return `export async function ${name}(): Promise<unknown> {
|
|
92
|
+
throw new Error(${JSON.stringify(`${name}: unresolved capability binding`)});
|
|
93
|
+
}`;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const entity = entityByName(catalog, cap.entity);
|
|
97
|
+
if (!entity) {
|
|
98
|
+
return `export async function ${cap.name}(): Promise<unknown> {
|
|
99
|
+
throw new Error(${JSON.stringify(`${cap.name}: unknown entity ${cap.entity}`)});
|
|
100
|
+
}`;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const shape = binding.invokeShape;
|
|
104
|
+
const returnType = capabilityReturnTypeName(cap.entity);
|
|
105
|
+
const scalar = capabilityReturnsScalar(cap);
|
|
106
|
+
const voidReturn = capabilityReturnsVoid(cap);
|
|
107
|
+
const needsInput = capabilityNeedsInput(cap, shape, entity.id_field);
|
|
108
|
+
const inputType = capabilityInputTypeName(cap.name);
|
|
109
|
+
const optionsArg = "options?: StubInvokeOptions";
|
|
110
|
+
const params = needsInput ? `input: ${inputType}, ${optionsArg}` : optionsArg;
|
|
111
|
+
|
|
112
|
+
const returnTypeTs = voidReturn ? "void" : scalar ? returnType : `${returnType}[]`;
|
|
113
|
+
const programStmt = renderProgramStatements(
|
|
114
|
+
binding,
|
|
115
|
+
cap,
|
|
116
|
+
catalog.values,
|
|
117
|
+
shape,
|
|
118
|
+
entity.id_field,
|
|
119
|
+
needsInput ? "input" : "input",
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
let programBody: string;
|
|
123
|
+
if (voidReturn) {
|
|
124
|
+
programBody = `${programStmt}
|
|
125
|
+
await executeRows(builder, program, options);`;
|
|
126
|
+
} else if (needsInput) {
|
|
127
|
+
programBody = `${programStmt}
|
|
128
|
+
const result = await executeRows<${returnType}>(builder, program, options);`;
|
|
129
|
+
} else {
|
|
130
|
+
programBody = `${programStmt}
|
|
131
|
+
const result = await executeRows<${returnType}>(builder, program, options);`;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
let returnStmt = "";
|
|
135
|
+
if (!voidReturn) {
|
|
136
|
+
returnStmt = scalar
|
|
137
|
+
? `const row = result.rows[0];
|
|
138
|
+
if (!row) throw new Error(${JSON.stringify(`${cap.name}: empty result`)});
|
|
139
|
+
return row;`
|
|
140
|
+
: "return result.rows;";
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const desc = cap.input_schema?.description ?? cap.name;
|
|
144
|
+
return `/** ${desc} */
|
|
145
|
+
export async function ${cap.name}(${params}): Promise<${returnTypeTs}> {
|
|
146
|
+
${programBody}
|
|
147
|
+
${returnStmt}
|
|
148
|
+
}`;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export function renderStubModule(
|
|
152
|
+
catalog: CatalogIntrospectionJson,
|
|
153
|
+
generatedAt: string,
|
|
154
|
+
bindings?: Map<string, CapabilityBinding>,
|
|
155
|
+
): string {
|
|
156
|
+
const exportName = toExportName(catalog.entry_id);
|
|
157
|
+
const clientImport = "@plasm_lang/vercel-agent";
|
|
158
|
+
|
|
159
|
+
const entityTypes = new Set<string>();
|
|
160
|
+
const inputTypes = new Set<string>();
|
|
161
|
+
|
|
162
|
+
for (const entity of catalog.entities) {
|
|
163
|
+
const caps = catalog.capabilities.filter((c) => c.entity === entity.name);
|
|
164
|
+
const fieldNames = new Set<string>();
|
|
165
|
+
for (const cap of caps) {
|
|
166
|
+
for (const field of cap.provides) {
|
|
167
|
+
fieldNames.add(field);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
if (fieldNames.size) {
|
|
171
|
+
entityTypes.add(renderEntityRowType(entity.name, [...fieldNames], catalog));
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
for (const cap of catalog.capabilities) {
|
|
176
|
+
const shape = bindings?.get(cap.name)?.invokeShape ?? classifyInvokeShape(cap);
|
|
177
|
+
const inputType = renderCapabilityInputType(cap, catalog, shape);
|
|
178
|
+
if (inputType) inputTypes.add(inputType);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const capabilityFns = catalog.capabilities.map((cap) =>
|
|
182
|
+
renderCapabilityFunction(catalog, bindings?.get(cap.name)),
|
|
183
|
+
);
|
|
184
|
+
|
|
185
|
+
const capsByEntity = new Map<string, typeof catalog.capabilities>();
|
|
186
|
+
for (const cap of catalog.capabilities) {
|
|
187
|
+
const list = capsByEntity.get(cap.entity) ?? [];
|
|
188
|
+
list.push(cap);
|
|
189
|
+
capsByEntity.set(cap.entity, list);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const namespaceBlocks: string[] = [];
|
|
193
|
+
for (const entity of catalog.entities) {
|
|
194
|
+
const caps = capsByEntity.get(entity.name) ?? [];
|
|
195
|
+
if (!caps.length) continue;
|
|
196
|
+
const entries = caps.map((c) => `${c.name}`).join(",\n");
|
|
197
|
+
namespaceBlocks.push(`${entity.name}: {\n${indent(entries, 4)},\n }`);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const stubEntities = stubEntityNames(catalog);
|
|
201
|
+
|
|
202
|
+
return `/** @generated catalog_cgs_hash=${catalog.catalog_cgs_hash} entry_id=${catalog.entry_id} generated_at=${generatedAt} */
|
|
203
|
+
import path from "node:path";
|
|
204
|
+
import { fileURLToPath } from "node:url";
|
|
205
|
+
|
|
206
|
+
import {
|
|
207
|
+
createCatalogClient,
|
|
208
|
+
buildDottedArgs,
|
|
209
|
+
executeRows,
|
|
210
|
+
plasmBoolean,
|
|
211
|
+
plasmLiteral,
|
|
212
|
+
plasmNumber,
|
|
213
|
+
type StubInvokeOptions,
|
|
214
|
+
} from ${JSON.stringify(clientImport)};
|
|
215
|
+
|
|
216
|
+
export const catalogCgsHash = ${JSON.stringify(catalog.catalog_cgs_hash)};
|
|
217
|
+
export const entryId = ${JSON.stringify(catalog.entry_id)};
|
|
218
|
+
|
|
219
|
+
const catalogRoot = path.resolve(
|
|
220
|
+
path.dirname(fileURLToPath(import.meta.url)),
|
|
221
|
+
"../../catalogs/${catalog.entry_id}",
|
|
222
|
+
);
|
|
223
|
+
|
|
224
|
+
const stubEntities = ${JSON.stringify(stubEntities)} as const;
|
|
225
|
+
|
|
226
|
+
const builder = createCatalogClient({
|
|
227
|
+
entryId,
|
|
228
|
+
cgsHash: catalogCgsHash,
|
|
229
|
+
catalogRoot,
|
|
230
|
+
stubEntities: [...stubEntities],
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
${renderBrandTypes(catalog)}
|
|
234
|
+
|
|
235
|
+
${[...entityTypes, ...inputTypes].join("\n\n")}
|
|
236
|
+
|
|
237
|
+
${capabilityFns.join("\n\n")}
|
|
238
|
+
|
|
239
|
+
export const ${exportName} = {
|
|
240
|
+
${indent(namespaceBlocks.join(",\n"), 2)},
|
|
241
|
+
builder,
|
|
242
|
+
catalogCgsHash,
|
|
243
|
+
entryId,
|
|
244
|
+
} as const;
|
|
245
|
+
|
|
246
|
+
export default ${exportName};
|
|
247
|
+
`;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
export async function generateStubForCatalog(
|
|
251
|
+
catalogDir: string,
|
|
252
|
+
outDir: string,
|
|
253
|
+
engine?: PlasmEngine,
|
|
254
|
+
): Promise<StubGenerationResult> {
|
|
255
|
+
const fallbackEntryId = path.basename(catalogDir);
|
|
256
|
+
const domainYaml = await readFile(path.join(catalogDir, "domain.yaml"), "utf8");
|
|
257
|
+
const domainMeta = parseCgsDomain(domainYaml, fallbackEntryId);
|
|
258
|
+
const generatedAt = new Date().toISOString();
|
|
259
|
+
|
|
260
|
+
const activeEngine = engine ?? createEngine();
|
|
261
|
+
await activeEngine.loadCatalog({
|
|
262
|
+
rootDir: catalogDir,
|
|
263
|
+
manifest: { entryId: domainMeta.entryId, label: fallbackEntryId },
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
const raw = await activeEngine.introspectCatalog(domainMeta.entryId);
|
|
267
|
+
const catalog = parseCatalogIntrospection(raw);
|
|
268
|
+
const bindings = assignCapabilityBindings(catalog);
|
|
269
|
+
const source = renderStubModule(catalog, generatedAt, bindings);
|
|
270
|
+
|
|
271
|
+
await mkdir(outDir, { recursive: true });
|
|
272
|
+
const outPath = path.join(outDir, `${catalog.entry_id}.ts`);
|
|
273
|
+
await writeFile(outPath, source, "utf8");
|
|
274
|
+
|
|
275
|
+
return {
|
|
276
|
+
entryId: catalog.entry_id,
|
|
277
|
+
catalogCgsHash: catalog.catalog_cgs_hash,
|
|
278
|
+
outPath,
|
|
279
|
+
generatedAt,
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
export async function generateAllStubs(
|
|
284
|
+
agentRoot: string,
|
|
285
|
+
options?: { engine?: PlasmEngine },
|
|
286
|
+
): Promise<StubGenerationResult[]> {
|
|
287
|
+
const catalogsDir = path.join(agentRoot, "catalogs");
|
|
288
|
+
const outDir = path.join(agentRoot, ".plasm", "stubs");
|
|
289
|
+
const { access, readdir, stat } = await import("node:fs/promises");
|
|
290
|
+
|
|
291
|
+
const engine = options?.engine ?? createEngine();
|
|
292
|
+
|
|
293
|
+
let entries;
|
|
294
|
+
try {
|
|
295
|
+
entries = await readdir(catalogsDir, { withFileTypes: true });
|
|
296
|
+
} catch {
|
|
297
|
+
return [];
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const results: StubGenerationResult[] = [];
|
|
301
|
+
for (const entry of entries) {
|
|
302
|
+
if (!entry.isDirectory() && !entry.isSymbolicLink()) continue;
|
|
303
|
+
const rootDir = path.join(catalogsDir, entry.name);
|
|
304
|
+
try {
|
|
305
|
+
const info = await stat(rootDir);
|
|
306
|
+
if (!info.isDirectory()) continue;
|
|
307
|
+
await access(path.join(rootDir, "domain.yaml"));
|
|
308
|
+
await access(path.join(rootDir, "mappings.yaml"));
|
|
309
|
+
} catch {
|
|
310
|
+
continue;
|
|
311
|
+
}
|
|
312
|
+
results.push(await generateStubForCatalog(rootDir, outDir, engine));
|
|
313
|
+
}
|
|
314
|
+
results.sort((a, b) => a.entryId.localeCompare(b.entryId));
|
|
315
|
+
return results;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/** Generate stub from an arbitrary catalog directory (fixtures / matrix smoke). */
|
|
319
|
+
export async function generateStubFromCatalogDir(
|
|
320
|
+
catalogDir: string,
|
|
321
|
+
outDir: string,
|
|
322
|
+
engine?: PlasmEngine,
|
|
323
|
+
): Promise<StubGenerationResult> {
|
|
324
|
+
return generateStubForCatalog(catalogDir, outDir, engine);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
export interface StubProvenance {
|
|
328
|
+
catalogCgsHash: string;
|
|
329
|
+
entryId: string;
|
|
330
|
+
generatedAt?: string;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
export async function readStubProvenance(stubPath: string): Promise<StubProvenance | null> {
|
|
334
|
+
try {
|
|
335
|
+
const raw = await readFile(stubPath, "utf8");
|
|
336
|
+
const match = raw.match(PROVENANCE_RE);
|
|
337
|
+
if (!match) return null;
|
|
338
|
+
const generatedMatch = raw.match(/generated_at=([^\s*]+)/);
|
|
339
|
+
return {
|
|
340
|
+
catalogCgsHash: match[1],
|
|
341
|
+
entryId: match[2],
|
|
342
|
+
generatedAt: generatedMatch?.[1],
|
|
343
|
+
};
|
|
344
|
+
} catch {
|
|
345
|
+
return null;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
export async function stubFreshness(
|
|
350
|
+
liveHash: string,
|
|
351
|
+
stubPath: string,
|
|
352
|
+
): Promise<{
|
|
353
|
+
stubPath: string;
|
|
354
|
+
liveCatalogCgsHash: string;
|
|
355
|
+
generatedCatalogCgsHash: string | null;
|
|
356
|
+
fresh: boolean;
|
|
357
|
+
lastBuiltAt: string | null;
|
|
358
|
+
validationErrors: string[];
|
|
359
|
+
}> {
|
|
360
|
+
const validationErrors: string[] = [];
|
|
361
|
+
let lastBuiltAt: string | null = null;
|
|
362
|
+
let generatedCatalogCgsHash: string | null = null;
|
|
363
|
+
|
|
364
|
+
try {
|
|
365
|
+
const info = await stat(stubPath);
|
|
366
|
+
lastBuiltAt = info.mtime.toISOString();
|
|
367
|
+
} catch {
|
|
368
|
+
validationErrors.push("stub file missing");
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
const provenance = await readStubProvenance(stubPath);
|
|
372
|
+
if (!provenance) {
|
|
373
|
+
validationErrors.push("stub provenance comment missing or invalid");
|
|
374
|
+
} else {
|
|
375
|
+
generatedCatalogCgsHash = provenance.catalogCgsHash;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
const fresh = generatedCatalogCgsHash === liveHash && validationErrors.length === 0;
|
|
379
|
+
return {
|
|
380
|
+
stubPath,
|
|
381
|
+
liveCatalogCgsHash: liveHash,
|
|
382
|
+
generatedCatalogCgsHash,
|
|
383
|
+
fresh,
|
|
384
|
+
lastBuiltAt,
|
|
385
|
+
validationErrors,
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
export type { ParsedCgsDomain } from "./domain-parser.js";
|
|
390
|
+
export { parseCgsDomain } from "./domain-parser.js";
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
CapabilityIntrospectionJson,
|
|
3
|
+
CatalogIntrospectionJson,
|
|
4
|
+
FieldTypeJson,
|
|
5
|
+
InputFieldSchemaJson,
|
|
6
|
+
InputTypeJson,
|
|
7
|
+
NamedValueSchemaJson,
|
|
8
|
+
} from "./catalog-introspection.js";
|
|
9
|
+
import type { CapabilityInvokeShape } from "./capability-invoke-shape.js";
|
|
10
|
+
import {
|
|
11
|
+
invokeBodyFields,
|
|
12
|
+
objectFieldsFromCap,
|
|
13
|
+
searchFieldName,
|
|
14
|
+
} from "./capability-invoke-shape.js";
|
|
15
|
+
|
|
16
|
+
export type BrandRegistry = Map<string, string>;
|
|
17
|
+
|
|
18
|
+
export function entityTypeName(entityName: string): string {
|
|
19
|
+
return entityName.replace(/[^a-zA-Z0-9_]/g, "_");
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function refBrandName(entityName: string): string {
|
|
23
|
+
return `Ref${entityTypeName(entityName)}`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function idBrandName(entityName: string): string {
|
|
27
|
+
return `${entityTypeName(entityName)}Id`;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function buildBrandRegistry(catalog: CatalogIntrospectionJson): BrandRegistry {
|
|
31
|
+
const out = new Map<string, string>();
|
|
32
|
+
for (const entity of catalog.entities) {
|
|
33
|
+
out.set(entity.name, refBrandName(entity.name));
|
|
34
|
+
}
|
|
35
|
+
return out;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function resolveFieldType(
|
|
39
|
+
field: InputFieldSchemaJson,
|
|
40
|
+
values: Record<string, NamedValueSchemaJson>,
|
|
41
|
+
): FieldTypeJson | InputTypeJson | undefined {
|
|
42
|
+
if (field.value_ref) {
|
|
43
|
+
return values[field.value_ref]?.field_type;
|
|
44
|
+
}
|
|
45
|
+
return field.input_type;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function fieldTypeToTs(
|
|
49
|
+
ft: FieldTypeJson | InputTypeJson | undefined,
|
|
50
|
+
values: Record<string, NamedValueSchemaJson>,
|
|
51
|
+
brands: BrandRegistry,
|
|
52
|
+
/** Input positions brand entity_ref; output uses wire string. */
|
|
53
|
+
inputContext: boolean,
|
|
54
|
+
allowedValues?: string[],
|
|
55
|
+
): string {
|
|
56
|
+
if (!ft) return "unknown";
|
|
57
|
+
|
|
58
|
+
if (typeof ft === "object" && "type" in ft) {
|
|
59
|
+
return inputTypeToTs(ft, values, brands, inputContext);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const fieldType = ft as FieldTypeJson;
|
|
63
|
+
if (typeof fieldType === "object" && "entity_ref" in fieldType) {
|
|
64
|
+
const target = fieldType.entity_ref.target;
|
|
65
|
+
if (inputContext && brands.has(target)) {
|
|
66
|
+
return brands.get(target)!;
|
|
67
|
+
}
|
|
68
|
+
return "string";
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
switch (fieldType) {
|
|
72
|
+
case "boolean":
|
|
73
|
+
return "boolean";
|
|
74
|
+
case "integer":
|
|
75
|
+
case "number":
|
|
76
|
+
return "number";
|
|
77
|
+
case "string":
|
|
78
|
+
case "uuid":
|
|
79
|
+
case "date":
|
|
80
|
+
return "string";
|
|
81
|
+
case "blob":
|
|
82
|
+
case "json":
|
|
83
|
+
return "unknown";
|
|
84
|
+
case "select": {
|
|
85
|
+
const allowed = allowedValues ?? [];
|
|
86
|
+
if (!allowed.length) return "string";
|
|
87
|
+
return allowed.map((v) => JSON.stringify(v)).join(" | ");
|
|
88
|
+
}
|
|
89
|
+
case "multi_select":
|
|
90
|
+
return "string[]";
|
|
91
|
+
case "array":
|
|
92
|
+
return "unknown[]";
|
|
93
|
+
default:
|
|
94
|
+
return "unknown";
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function inputTypeToTs(
|
|
99
|
+
input: InputTypeJson,
|
|
100
|
+
values: Record<string, NamedValueSchemaJson>,
|
|
101
|
+
brands: BrandRegistry,
|
|
102
|
+
inputContext = true,
|
|
103
|
+
): string {
|
|
104
|
+
switch (input.type) {
|
|
105
|
+
case "none":
|
|
106
|
+
return "void";
|
|
107
|
+
case "value": {
|
|
108
|
+
const allowed = input.allowed_values;
|
|
109
|
+
return fieldTypeToTs(input.field_type, values, brands, inputContext, allowed);
|
|
110
|
+
}
|
|
111
|
+
case "object": {
|
|
112
|
+
const lines = input.fields.map((f) => {
|
|
113
|
+
const ft = resolveFieldType(f, values);
|
|
114
|
+
const allowed =
|
|
115
|
+
typeof ft === "string" || (typeof ft === "object" && !("type" in ft))
|
|
116
|
+
? values[f.value_ref ?? ""]?.allowed_values
|
|
117
|
+
: undefined;
|
|
118
|
+
const ts = fieldTypeToTs(ft, values, brands, inputContext, allowed);
|
|
119
|
+
return `${f.name}${f.required ? "" : "?"}: ${ts};`;
|
|
120
|
+
});
|
|
121
|
+
return `{\n ${lines.join("\n ")}\n}`;
|
|
122
|
+
}
|
|
123
|
+
case "array":
|
|
124
|
+
return `${inputTypeToTs(input.element_type, values, brands, inputContext)}[]`;
|
|
125
|
+
case "union": {
|
|
126
|
+
const variants = input.variants.map((v) => {
|
|
127
|
+
const body = inputTypeToTs(
|
|
128
|
+
{ type: "object", fields: v.fields },
|
|
129
|
+
values,
|
|
130
|
+
brands,
|
|
131
|
+
inputContext,
|
|
132
|
+
);
|
|
133
|
+
const tag = v.constructor_symbol ?? v.name;
|
|
134
|
+
return `{ readonly kind: ${JSON.stringify(tag)}; ${body.slice(1, -1).trim()} }`;
|
|
135
|
+
});
|
|
136
|
+
return variants.join(" | ");
|
|
137
|
+
}
|
|
138
|
+
default:
|
|
139
|
+
return "unknown";
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function capabilityInputTypeName(capName: string): string {
|
|
144
|
+
const base = capName
|
|
145
|
+
.split(/[^a-zA-Z0-9]+/)
|
|
146
|
+
.filter(Boolean)
|
|
147
|
+
.map((part) => part[0]!.toUpperCase() + part.slice(1))
|
|
148
|
+
.join("");
|
|
149
|
+
return `${base}Input`;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export function renderBrandTypes(catalog: CatalogIntrospectionJson): string {
|
|
153
|
+
const lines = [
|
|
154
|
+
"type Brand<B extends string, T> = T & { readonly __brand: B };",
|
|
155
|
+
];
|
|
156
|
+
for (const entity of catalog.entities) {
|
|
157
|
+
const id = idBrandName(entity.name);
|
|
158
|
+
const ref = refBrandName(entity.name);
|
|
159
|
+
lines.push(`export type ${id} = Brand<${JSON.stringify(id)}, string>;`);
|
|
160
|
+
lines.push(`export type ${ref} = ${id};`);
|
|
161
|
+
}
|
|
162
|
+
return lines.join("\n");
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export function renderEntityRowType(
|
|
166
|
+
entityName: string,
|
|
167
|
+
fieldNames: string[],
|
|
168
|
+
catalog: CatalogIntrospectionJson,
|
|
169
|
+
): string {
|
|
170
|
+
const entity = catalog.entities.find((e) => e.name === entityName);
|
|
171
|
+
if (!entity) return `export type ${entityTypeName(entityName)} = Record<string, unknown>;`;
|
|
172
|
+
|
|
173
|
+
const values = catalog.values;
|
|
174
|
+
const lines: string[] = [];
|
|
175
|
+
for (const fieldName of fieldNames) {
|
|
176
|
+
const field = entity.fields.find((f) => f.name === fieldName);
|
|
177
|
+
if (!field) {
|
|
178
|
+
lines.push(`${fieldName}?: unknown;`);
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
const nv = values[field.value_ref];
|
|
182
|
+
const ts = fieldTypeToTs(nv?.field_type, values, buildBrandRegistry(catalog), false, nv?.allowed_values);
|
|
183
|
+
lines.push(`${fieldName}${field.required ? "" : "?"}: ${ts};`);
|
|
184
|
+
}
|
|
185
|
+
const typeName = entityTypeName(entityName);
|
|
186
|
+
return `export type ${typeName} = {\n ${lines.join("\n ")}\n};`;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export function renderCapabilityInputType(
|
|
190
|
+
cap: CapabilityIntrospectionJson,
|
|
191
|
+
catalog: CatalogIntrospectionJson,
|
|
192
|
+
shape: CapabilityInvokeShape,
|
|
193
|
+
): string | null {
|
|
194
|
+
const entity = catalog.entities.find((e) => e.name === cap.entity);
|
|
195
|
+
if (!entity) return null;
|
|
196
|
+
|
|
197
|
+
const brands = buildBrandRegistry(catalog);
|
|
198
|
+
const values = catalog.values;
|
|
199
|
+
const lines: string[] = [];
|
|
200
|
+
|
|
201
|
+
if (
|
|
202
|
+
shape === "GetById" ||
|
|
203
|
+
shape === "ScopedUpdate" ||
|
|
204
|
+
shape === "ScopedAction" ||
|
|
205
|
+
shape === "ScopedDelete"
|
|
206
|
+
) {
|
|
207
|
+
const ref = brands.get(cap.entity) ?? "string";
|
|
208
|
+
lines.push(`${entity.id_field}: ${ref};`);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const bodyFields = invokeBodyFields(cap, shape, entity.id_field);
|
|
212
|
+
for (const field of bodyFields) {
|
|
213
|
+
const ft = resolveFieldType(field, values);
|
|
214
|
+
const allowed = field.value_ref ? values[field.value_ref]?.allowed_values : undefined;
|
|
215
|
+
const ts = fieldTypeToTs(ft, values, brands, true, allowed);
|
|
216
|
+
lines.push(`${field.name}${field.required ? "" : "?"}: ${ts};`);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (shape === "SearchText" || shape === "SearchFiltered") {
|
|
220
|
+
const q = searchFieldName(cap);
|
|
221
|
+
if (q && !lines.some((l) => l.startsWith(`${q}:`))) {
|
|
222
|
+
const field = objectFieldsFromCap(cap).find((f) => f.name === q);
|
|
223
|
+
if (field) {
|
|
224
|
+
const ft = resolveFieldType(field, values);
|
|
225
|
+
const ts = fieldTypeToTs(ft, values, brands, true);
|
|
226
|
+
lines.unshift(`${q}${field.required ? "" : "?"}: ${ts};`);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (!lines.length) return null;
|
|
232
|
+
return `export type ${capabilityInputTypeName(cap.name)} = {\n ${lines.join("\n ")}\n};`;
|
|
233
|
+
}
|