@aventara/client 0.0.0-stage → 0.1.0-pilot.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.
Files changed (84) hide show
  1. package/LICENSE +91 -0
  2. package/LICENSE-ADDITIONAL-PERMISSION.md +9 -0
  3. package/README.md +268 -2
  4. package/dist/avclient.bin.d.ts +2 -0
  5. package/dist/avclient.bin.js +15 -0
  6. package/dist/cli/command.parser.d.ts +30 -0
  7. package/dist/cli/command.parser.js +132 -0
  8. package/dist/cli/generate.command.d.ts +24 -0
  9. package/dist/cli/generate.command.js +41 -0
  10. package/dist/cli/generation-failure.renderer.d.ts +6 -0
  11. package/dist/cli/generation-failure.renderer.js +54 -0
  12. package/dist/cli/generation-success.renderer.d.ts +32 -0
  13. package/dist/cli/generation-success.renderer.js +47 -0
  14. package/dist/cli/terminal.prompter.d.ts +13 -0
  15. package/dist/cli/terminal.prompter.js +53 -0
  16. package/dist/cli/warning.renderer.d.ts +10 -0
  17. package/dist/cli/warning.renderer.js +14 -0
  18. package/dist/cli.d.ts +29 -0
  19. package/dist/cli.js +71 -0
  20. package/dist/config/client-config.interface.d.ts +62 -0
  21. package/dist/config/client-config.interface.js +14 -0
  22. package/dist/config/config.loader.d.ts +33 -0
  23. package/dist/config/config.loader.js +80 -0
  24. package/dist/config/config.resolver.d.ts +50 -0
  25. package/dist/config/config.resolver.js +126 -0
  26. package/dist/config/env.cascade.d.ts +84 -0
  27. package/dist/config/env.cascade.js +126 -0
  28. package/dist/contract/contract.acceptance.d.ts +77 -0
  29. package/dist/contract/contract.acceptance.js +124 -0
  30. package/dist/contract/contract.fetcher.d.ts +64 -0
  31. package/dist/contract/contract.fetcher.js +85 -0
  32. package/dist/contract/contract.loader.d.ts +32 -0
  33. package/dist/contract/contract.loader.js +32 -0
  34. package/dist/emit/banner.emitter.d.ts +31 -0
  35. package/dist/emit/banner.emitter.js +42 -0
  36. package/dist/emit/client-surface.emitter.d.ts +32 -0
  37. package/dist/emit/client-surface.emitter.js +236 -0
  38. package/dist/emit/client-tree.emitter.d.ts +37 -0
  39. package/dist/emit/client-tree.emitter.js +103 -0
  40. package/dist/emit/contract-carrier.emitter.d.ts +13 -0
  41. package/dist/emit/contract-carrier.emitter.js +60 -0
  42. package/dist/emit/derivation.emitter.d.ts +45 -0
  43. package/dist/emit/derivation.emitter.js +233 -0
  44. package/dist/emit/descriptor.emitter.d.ts +4 -0
  45. package/dist/emit/descriptor.emitter.js +97 -0
  46. package/dist/emit/emitted-tree.interface.d.ts +61 -0
  47. package/dist/emit/emitted-tree.interface.js +18 -0
  48. package/dist/emit/enum.emitter.d.ts +24 -0
  49. package/dist/emit/enum.emitter.js +42 -0
  50. package/dist/emit/name.deriver.d.ts +153 -0
  51. package/dist/emit/name.deriver.js +411 -0
  52. package/dist/emit/named-type.emitter.d.ts +32 -0
  53. package/dist/emit/named-type.emitter.js +50 -0
  54. package/dist/emit/runtime.emitter.d.ts +87 -0
  55. package/dist/emit/runtime.emitter.js +707 -0
  56. package/dist/emit/scalar.codec.d.ts +63 -0
  57. package/dist/emit/scalar.codec.js +498 -0
  58. package/dist/emit/transaction.emitter.d.ts +17 -0
  59. package/dist/emit/transaction.emitter.js +438 -0
  60. package/dist/generate.d.ts +123 -0
  61. package/dist/generate.js +98 -0
  62. package/dist/index.d.ts +8 -0
  63. package/dist/index.js +8 -0
  64. package/dist/init/client-config.template.d.ts +6 -0
  65. package/dist/init/client-config.template.js +22 -0
  66. package/dist/init/client-init.errors.d.ts +9 -0
  67. package/dist/init/client-init.errors.js +9 -0
  68. package/dist/init/client-init.orchestrator.d.ts +3 -0
  69. package/dist/init/client-init.orchestrator.js +82 -0
  70. package/dist/init/client-init.planner.d.ts +26 -0
  71. package/dist/init/client-init.planner.js +88 -0
  72. package/dist/init/client-init.questions.d.ts +52 -0
  73. package/dist/init/client-init.questions.js +124 -0
  74. package/dist/init/client-project.inspector.d.ts +15 -0
  75. package/dist/init/client-project.inspector.js +32 -0
  76. package/dist/init/command.runner.d.ts +8 -0
  77. package/dist/init/command.runner.js +17 -0
  78. package/dist/node-version.guard.d.ts +8 -0
  79. package/dist/node-version.guard.js +59 -0
  80. package/dist/output/output.validator.d.ts +76 -0
  81. package/dist/output/output.validator.js +254 -0
  82. package/dist/output/output.writer.d.ts +162 -0
  83. package/dist/output/output.writer.js +499 -0
  84. package/package.json +47 -3
@@ -0,0 +1,438 @@
1
+ /**
2
+ * The transaction builder and runner (§14; Phase 12-rest S6, Q13, Q14, P3):
3
+ * `runtime/fingerprint.ts` and `runtime/transaction.ts`, emitted iff the
4
+ * ClientContract advertises `interactive` transactions (P3) — `client.ts` wires
5
+ * them to `avClient.tx` and `avClient.transaction`.
6
+ *
7
+ * Both are fixed by protocol version, not by the Contract, and mirror core's own
8
+ * seams: the plan is assembled as `transactions/transaction-plan-assembler.ts`
9
+ * assembles it — handles as pure data, a `$ref` bound lazily to its source
10
+ * handle's position in the list it runs with, the two refusals core answers in
11
+ * process (Q13) — and each node's fingerprint is core's
12
+ * `computeOperationFingerprint` over the node's WIRE arguments (§14.4), computed
13
+ * here by an emitted JCS and SHA-256 (Q14: `crypto.subtle` is absent from a
14
+ * browser page served over plain http from a non-localhost host).
15
+ */
16
+ export function emitTransactionModules() {
17
+ return [
18
+ { path: "runtime/fingerprint.ts", source: FINGERPRINT_MODULE },
19
+ { path: "runtime/transaction.ts", source: TRANSACTION_MODULE },
20
+ ];
21
+ }
22
+ const FINGERPRINT_MODULE = `/**
23
+ * The v1 operation fingerprint (§14.4): RFC 8785 canonical JSON of
24
+ * { resource, family, variant, args } over the WIRE-form arguments, its UTF-8
25
+ * bytes hashed with SHA-256, the first 16 bytes in base64url, prefixed "fp1:".
26
+ * Dependency-free (Q14), and pinned byte for byte to core's
27
+ * computeOperationFingerprint.
28
+ */
29
+
30
+ /** One plan node, its arguments already in wire form. */
31
+ export interface FingerprintNode {
32
+ readonly resource: string;
33
+ readonly family: string;
34
+ readonly variant: string;
35
+ readonly args: unknown;
36
+ }
37
+
38
+ /**
39
+ * The node's fingerprint, or null when its arguments are not canonical JSON — a
40
+ * lone surrogate, a non-finite number — exactly when core cannot compute one
41
+ * either; the server then answers the operation itself.
42
+ */
43
+ export function fingerprintOf(node: FingerprintNode): string | null {
44
+ let canonical: string;
45
+ try {
46
+ canonical = canonicalJson({
47
+ resource: node.resource,
48
+ family: node.family,
49
+ variant: node.variant,
50
+ args: node.args,
51
+ });
52
+ } catch {
53
+ return null;
54
+ }
55
+ return "fp1:" + base64Url(sha256(utf8(canonical)).subarray(0, 16));
56
+ }
57
+
58
+ /** RFC 8785: JavaScript's own number and string spelling, keys in UTF-16 code-unit order. */
59
+ export function canonicalJson(value: unknown): string {
60
+ if (value === null) {
61
+ return "null";
62
+ }
63
+ if (typeof value === "boolean") {
64
+ return value ? "true" : "false";
65
+ }
66
+ if (typeof value === "number") {
67
+ if (!Number.isFinite(value)) {
68
+ throw new TypeError("A non-finite number is not canonical JSON.");
69
+ }
70
+ return JSON.stringify(value);
71
+ }
72
+ if (typeof value === "string") {
73
+ assertWellFormed(value);
74
+ return JSON.stringify(value);
75
+ }
76
+ if (Array.isArray(value)) {
77
+ return "[" + value.map((entry: unknown) => canonicalJson(entry)).join(",") + "]";
78
+ }
79
+ if (typeof value !== "object") {
80
+ throw new TypeError("A " + typeof value + " is not canonical JSON.");
81
+ }
82
+ const prototype: unknown = Object.getPrototypeOf(value);
83
+ if (prototype !== Object.prototype && prototype !== null) {
84
+ throw new TypeError("A non-plain object is not canonical JSON.");
85
+ }
86
+ const record = value as Readonly<Record<string, unknown>>;
87
+ const keys = Object.keys(record).sort((left, right) => (left < right ? -1 : left > right ? 1 : 0));
88
+ return (
89
+ "{" +
90
+ keys
91
+ .map((key) => {
92
+ assertWellFormed(key);
93
+ return JSON.stringify(key) + ":" + canonicalJson(record[key]);
94
+ })
95
+ .join(",") +
96
+ "}"
97
+ );
98
+ }
99
+
100
+ function assertWellFormed(text: string): void {
101
+ for (let index = 0; index < text.length; index += 1) {
102
+ const unit = text.charCodeAt(index);
103
+ if (unit >= 0xd800 && unit <= 0xdbff) {
104
+ const next = text.charCodeAt(index + 1);
105
+ if (!(next >= 0xdc00 && next <= 0xdfff)) {
106
+ throw new TypeError("A lone surrogate is not canonical JSON.");
107
+ }
108
+ index += 1;
109
+ } else if (unit >= 0xdc00 && unit <= 0xdfff) {
110
+ throw new TypeError("A lone surrogate is not canonical JSON.");
111
+ }
112
+ }
113
+ }
114
+
115
+ /** UTF-8 of well-formed text. */
116
+ function utf8(text: string): Uint8Array {
117
+ const bytes: number[] = [];
118
+ for (const character of text) {
119
+ const point = character.codePointAt(0) ?? 0;
120
+ if (point < 0x80) {
121
+ bytes.push(point);
122
+ } else if (point < 0x800) {
123
+ bytes.push(0xc0 | (point >> 6), 0x80 | (point & 63));
124
+ } else if (point < 0x10000) {
125
+ bytes.push(0xe0 | (point >> 12), 0x80 | ((point >> 6) & 63), 0x80 | (point & 63));
126
+ } else {
127
+ bytes.push(0xf0 | (point >> 18), 0x80 | ((point >> 12) & 63), 0x80 | ((point >> 6) & 63), 0x80 | (point & 63));
128
+ }
129
+ }
130
+ return new Uint8Array(bytes);
131
+ }
132
+
133
+ const ROUND = new Uint32Array([
134
+ 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
135
+ 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
136
+ 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
137
+ 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
138
+ 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
139
+ 0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
140
+ 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
141
+ 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2,
142
+ ]);
143
+
144
+ function rotate(word: number, bits: number): number {
145
+ return (word >>> bits) | (word << (32 - bits));
146
+ }
147
+
148
+ /** FIPS 180-4 SHA-256. */
149
+ export function sha256(message: Uint8Array): Uint8Array {
150
+ const blocks = Math.ceil((message.length + 9) / 64);
151
+ const padded = new Uint8Array(blocks * 64);
152
+ padded.set(message);
153
+ padded[message.length] = 0x80;
154
+ const bits = message.length * 8;
155
+ const view = new DataView(padded.buffer);
156
+ view.setUint32(padded.length - 8, Math.floor(bits / 0x100000000));
157
+ view.setUint32(padded.length - 4, bits >>> 0);
158
+ const state = new Uint32Array([
159
+ 0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19,
160
+ ]);
161
+ const schedule = new Uint32Array(64);
162
+ for (let block = 0; block < blocks; block += 1) {
163
+ for (let index = 0; index < 64; index += 1) {
164
+ if (index < 16) {
165
+ schedule[index] = view.getUint32(block * 64 + index * 4);
166
+ } else {
167
+ const early = schedule[index - 15] ?? 0;
168
+ const late = schedule[index - 2] ?? 0;
169
+ const sigma0 = rotate(early, 7) ^ rotate(early, 18) ^ (early >>> 3);
170
+ const sigma1 = rotate(late, 17) ^ rotate(late, 19) ^ (late >>> 10);
171
+ schedule[index] = ((schedule[index - 16] ?? 0) + sigma0 + (schedule[index - 7] ?? 0) + sigma1) >>> 0;
172
+ }
173
+ }
174
+ let [a, b, c, d, e, f, g, h] = state as unknown as [number, number, number, number, number, number, number, number];
175
+ for (let index = 0; index < 64; index += 1) {
176
+ const sum1 = rotate(e, 6) ^ rotate(e, 11) ^ rotate(e, 25);
177
+ const choose = (e & f) ^ (~e & g);
178
+ const first = (h + sum1 + choose + (ROUND[index] ?? 0) + (schedule[index] ?? 0)) >>> 0;
179
+ const sum0 = rotate(a, 2) ^ rotate(a, 13) ^ rotate(a, 22);
180
+ const majority = (a & b) ^ (a & c) ^ (b & c);
181
+ const second = (sum0 + majority) >>> 0;
182
+ h = g;
183
+ g = f;
184
+ f = e;
185
+ e = (d + first) >>> 0;
186
+ d = c;
187
+ c = b;
188
+ b = a;
189
+ a = (first + second) >>> 0;
190
+ }
191
+ const words = [a, b, c, d, e, f, g, h];
192
+ for (let index = 0; index < 8; index += 1) {
193
+ state[index] = ((state[index] ?? 0) + (words[index] ?? 0)) >>> 0;
194
+ }
195
+ }
196
+ const digest = new Uint8Array(32);
197
+ const output = new DataView(digest.buffer);
198
+ for (let index = 0; index < 8; index += 1) {
199
+ output.setUint32(index * 4, state[index] ?? 0);
200
+ }
201
+ return digest;
202
+ }
203
+
204
+ const BASE64URL_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
205
+
206
+ /** RFC 4648 §5 base64url, unpadded. */
207
+ function base64Url(bytes: Uint8Array): string {
208
+ let text = "";
209
+ for (let index = 0; index < bytes.length; index += 3) {
210
+ const remaining = bytes.length - index;
211
+ const group = ((bytes[index] ?? 0) << 16) | ((bytes[index + 1] ?? 0) << 8) | (bytes[index + 2] ?? 0);
212
+ text += BASE64URL_ALPHABET.charAt((group >> 18) & 63) + BASE64URL_ALPHABET.charAt((group >> 12) & 63);
213
+ if (remaining > 1) {
214
+ text += BASE64URL_ALPHABET.charAt((group >> 6) & 63);
215
+ }
216
+ if (remaining > 2) {
217
+ text += BASE64URL_ALPHABET.charAt(group & 63);
218
+ }
219
+ }
220
+ return text;
221
+ }
222
+ `;
223
+ const TRANSACTION_MODULE = `import { encodeArguments, serializeWireBody } from "./codec.js";
224
+ import { type Cause, type FrameworkError, frameworkErrorOf } from "./errors.js";
225
+ import { fingerprintOf } from "./fingerprint.js";
226
+ import { type CallOptions, executePlan, type TransportConnection } from "./transport.js";
227
+
228
+ /**
229
+ * The deferred transaction builder and runner (§14), as core assembles a plan
230
+ * in process (transaction-plan-assembler.ts): \`avClient.tx\` defers an operation
231
+ * into a handle — pure data, no request — and \`avClient.transaction\` binds a
232
+ * list of handles into the one §14.2 plan, sends it once, and resolves one
233
+ * result per handle in list order. A handle has no connection, so any client of
234
+ * this generated tree may run it (Q13, a plan ruling).
235
+ */
236
+
237
+ /** What a handle defers. */
238
+ interface DeferredRecord {
239
+ readonly resource: string;
240
+ readonly family: string;
241
+ readonly variant: string;
242
+ readonly args: unknown;
243
+ }
244
+
245
+ /** What a \`$ref\` placeholder stands for until the plan is assembled. */
246
+ interface Placeholder {
247
+ readonly source: object;
248
+ readonly field: string;
249
+ }
250
+
251
+ type MutableRecord = Record<string, unknown>;
252
+
253
+ /** One §14.2 wire node, its arguments in wire form. */
254
+ interface PlanNode {
255
+ readonly resource: string;
256
+ readonly family: string;
257
+ readonly variant: string;
258
+ readonly args: unknown;
259
+ readonly fingerprint: string | null;
260
+ }
261
+
262
+ /** One DA-3 container: a mutation-data record and its path inside the arguments. */
263
+ interface Container {
264
+ readonly path: readonly (string | number)[];
265
+ readonly record: Readonly<Record<string, unknown>>;
266
+ }
267
+
268
+ const HANDLES = new WeakMap<object, DeferredRecord>();
269
+ const PLACEHOLDERS = new WeakMap<object, Placeholder>();
270
+
271
+ function isPlainRecord(value: unknown): value is Readonly<Record<string, unknown>> {
272
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
273
+ return false;
274
+ }
275
+ const prototype: unknown = Object.getPrototypeOf(value);
276
+ return prototype === Object.prototype || prototype === null;
277
+ }
278
+
279
+ /** The argument keys holding DA-3 reference positions: the top-level mutation data. */
280
+ function referenceKeys(family: string): readonly string[] {
281
+ return family === "create" || family === "update" ? ["data"] : family === "upsert" ? ["create", "update"] : [];
282
+ }
283
+
284
+ function containers(family: string, args: unknown): readonly Container[] {
285
+ if (!isPlainRecord(args)) {
286
+ return [];
287
+ }
288
+ return referenceKeys(family).flatMap((key): Container[] => {
289
+ const value = args[key];
290
+ if (Array.isArray(value)) {
291
+ return value.flatMap((entry: unknown, index) => (isPlainRecord(entry) ? [{ path: [key, index], record: entry }] : []));
292
+ }
293
+ return isPlainRecord(value) ? [{ path: [key], record: value }] : [];
294
+ });
295
+ }
296
+
297
+ /** \`args\` with each DA-3 container replaced by \`rebuild\`, cloning only the containing path. */
298
+ function replaceContainers(family: string, args: unknown, rebuild: (container: Container) => MutableRecord): unknown {
299
+ if (!isPlainRecord(args)) {
300
+ return args;
301
+ }
302
+ const result: MutableRecord = { ...args };
303
+ for (const key of referenceKeys(family)) {
304
+ const value = args[key];
305
+ if (Array.isArray(value)) {
306
+ result[key] = value.map((entry: unknown, index) => (isPlainRecord(entry) ? rebuild({ path: [key, index], record: entry }) : entry));
307
+ } else if (isPlainRecord(value)) {
308
+ result[key] = rebuild({ path: [key], record: value });
309
+ }
310
+ }
311
+ return result;
312
+ }
313
+
314
+ /**
315
+ * Defers one operation into a handle (§14.1): nothing is sent. Its DA-3
316
+ * containers are copied now, so a later change to the caller's data cannot move
317
+ * a reference — and a handle can reference only handles built before it.
318
+ */
319
+ export function deferOperation(resource: string, family: string, variant: string, args: unknown): object {
320
+ const handle: object = Object.freeze({
321
+ $ref: (field: string): object => {
322
+ const placeholder = Object.freeze({ $ref: Object.freeze({ path: Object.freeze([field]) }) });
323
+ PLACEHOLDERS.set(placeholder, { source: handle, field });
324
+ return placeholder;
325
+ },
326
+ });
327
+ HANDLES.set(handle, {
328
+ resource,
329
+ family,
330
+ variant,
331
+ args: replaceContainers(family, args, ({ record }) => ({ ...record })),
332
+ });
333
+ return handle;
334
+ }
335
+
336
+ /** What core answers in process for a plan entry it cannot run (A2004 / V1001). */
337
+ function entryRefusal(operation: number, message: string): FrameworkError {
338
+ const cause: Cause = {
339
+ message: "Transaction plan failed framework validation.",
340
+ issues: [{ path: ["operation"], code: "V1001", message }],
341
+ operation,
342
+ };
343
+ return frameworkErrorOf("A2004", cause);
344
+ }
345
+
346
+ /**
347
+ * Binds \`steps\` into the plan, sends it once, and resolves one decoded result per
348
+ * step (§14.1). Refused before anything is sent, with what core answers in
349
+ * process (Q13): a list that is not one, an entry that is not a handle of this
350
+ * generated client, one handle twice (ValidationError A2004 / V1001 at
351
+ * ["operation"]); a reference to a handle not in the list (ValidationError A2007 /
352
+ * V1010 at its \`$ref\`).
353
+ */
354
+ export async function runTransaction(
355
+ connection: TransportConnection,
356
+ steps: unknown,
357
+ options: CallOptions = {},
358
+ ): Promise<unknown[]> {
359
+ if (!Array.isArray(steps)) {
360
+ throw frameworkErrorOf("A2004", {
361
+ message: "Transaction plan failed framework validation.",
362
+ issues: [{ path: ["operations"], code: "V1001", message: "Transaction operations must be a list of deferred operations." }],
363
+ });
364
+ }
365
+ const records: DeferredRecord[] = [];
366
+ const positions = new Map<object, number>();
367
+ for (const [index, entry] of steps.entries()) {
368
+ const record = typeof entry === "object" && entry !== null ? HANDLES.get(entry) : undefined;
369
+ if (record === undefined) {
370
+ throw entryRefusal(index, "Operation must be a deferred operation built by this client's tx.");
371
+ }
372
+ if (positions.has(entry)) {
373
+ throw entryRefusal(index, "Deferred operation appears more than once in this transaction.");
374
+ }
375
+ positions.set(entry, index);
376
+ records.push(record);
377
+ }
378
+ for (const [index, record] of records.entries()) {
379
+ for (const { path, record: container } of containers(record.family, record.args)) {
380
+ for (const [field, value] of Object.entries(container)) {
381
+ const placeholder = typeof value === "object" && value !== null ? PLACEHOLDERS.get(value) : undefined;
382
+ if (placeholder !== undefined && !positions.has(placeholder.source)) {
383
+ throw frameworkErrorOf("A2007", {
384
+ message: "Operation arguments failed framework validation.",
385
+ issues: [
386
+ {
387
+ path: ["operations", index, "args", ...path, field, "$ref"],
388
+ code: "V1010",
389
+ message: "Transaction reference names an operation that is not in this transaction.",
390
+ },
391
+ ],
392
+ operation: index,
393
+ });
394
+ }
395
+ }
396
+ }
397
+ }
398
+
399
+ // Bound lazily, as core binds them: a node's references need their source's
400
+ // fingerprint, so each node is built once, on first need, wherever its source
401
+ // stands in the list — the server, not this client, refuses a later one.
402
+ const nodes = new Map<number, PlanNode>();
403
+ const nodeAt = (index: number): PlanNode => {
404
+ const built = nodes.get(index);
405
+ if (built !== undefined) {
406
+ return built;
407
+ }
408
+ const record = records[index] as DeferredRecord;
409
+ const bound = replaceContainers(record.family, record.args, ({ record: container }) => {
410
+ const copy: MutableRecord = { ...container };
411
+ for (const [field, value] of Object.entries(container)) {
412
+ const placeholder = typeof value === "object" && value !== null ? PLACEHOLDERS.get(value) : undefined;
413
+ if (placeholder !== undefined) {
414
+ const operation = positions.get(placeholder.source) as number;
415
+ copy[field] = { $ref: { operation, fingerprint: nodeAt(operation).fingerprint, path: [placeholder.field] } };
416
+ }
417
+ }
418
+ return copy;
419
+ });
420
+ const args: unknown = JSON.parse(serializeWireBody(encodeArguments(bound)));
421
+ const node: PlanNode = {
422
+ resource: record.resource,
423
+ family: record.family,
424
+ variant: record.variant,
425
+ args,
426
+ fingerprint: fingerprintOf({ resource: record.resource, family: record.family, variant: record.variant, args }),
427
+ };
428
+ nodes.set(index, node);
429
+ return node;
430
+ };
431
+ return executePlan(
432
+ connection,
433
+ { operations: records.map((_, index) => nodeAt(index)) },
434
+ records.map((record) => record.resource),
435
+ options,
436
+ );
437
+ }
438
+ `;
@@ -0,0 +1,123 @@
1
+ import type { AbsolutePath } from "./config/client-config.interface.js";
2
+ import { type ProcessEnvInput } from "./config/env.cascade.js";
3
+ import type { ContractFetch } from "./contract/contract.fetcher.js";
4
+ import { type EmittedFilePath } from "./emit/emitted-tree.interface.js";
5
+ import type { OutputCheck, TypeScriptResolver } from "./output/output.validator.js";
6
+ /**
7
+ * §15.3's generation pipeline, as one call (S7b):
8
+ *
9
+ * load env + config S2 — `.env` cascade, then `framework.client.ts`
10
+ * → GET <entrypoint>/_contract S3 — conditional on the output's carrier (Q6):
11
+ * `304` and identical bytes → "up to date", nothing written
12
+ * → validate protocol support, ClientContract structure, advertised hash S3
13
+ * → emit S4–S6 — enums, metadata, codecs, errors, transport
14
+ * → emit into a temporary directory, validate, atomically replace S7
15
+ *
16
+ * Only what exists above Phase 12-partial's boundary is emitted: no Resource
17
+ * method, no projection, no transaction builder — those read capabilities and
18
+ * operation descriptors (plan §1).
19
+ *
20
+ * The cascade is resolved BEFORE the config is evaluated, as §15.2 orders it, so
21
+ * an unreadable or malformed `.env` is reported even when the config would also
22
+ * fail. The cascade is a returned record, never written into `process.env` (S2):
23
+ * the config reaches it through `env("NAME")`.
24
+ *
25
+ * Output goes to `generateAt`: `AvClient.ts` and `generated/`, nothing else
26
+ * there touched (architect, 2026-10-04). Content in those two the generator did
27
+ * not produce is never overwritten unasked: the run returns
28
+ * `ForeignOutputContent` instead, and the caller decides.
29
+ *
30
+ * Every failure is thrown, every diagnostic that is not a failure is returned:
31
+ * this function prints nothing and asks nothing. `cli.ts` owns the terminal. A
32
+ * refusal raised after a warning (`OutputWriteError`) carries the warnings
33
+ * raised before it, and so does `ForeignOutputContent`; a defect is rethrown as
34
+ * itself, its warnings kept beside it (`warningsRaisedBeforeDefect`). No run's
35
+ * warnings are lost because it stopped.
36
+ */
37
+ export interface ClientGenerationInput {
38
+ /** The project directory: where `framework.client.ts` and the `.env` files are. */
39
+ readonly directory: string;
40
+ /** A snapshot of the process environment. Never mutated. */
41
+ readonly processEnv: ProcessEnvInput;
42
+ /**
43
+ * Overwrite or remove content in `AvClient.ts` and `generated/` that the
44
+ * generator did not produce, without asking (the CLI's `--yes`). Otherwise
45
+ * finding any ends the run with `ForeignOutputContent` before anything is
46
+ * written.
47
+ */
48
+ readonly overrideForeign?: boolean;
49
+ /** Injected for tests; the platform `fetch` otherwise. */
50
+ readonly fetch?: ContractFetch;
51
+ /** The `typescript` optional peer; the installed one by default (Q6). */
52
+ readonly resolveTypeScript?: TypeScriptResolver;
53
+ }
54
+ export interface ClientGenerated {
55
+ readonly kind: "generated";
56
+ /** The shared directory the client was generated into. */
57
+ readonly generateAt: AbsolutePath;
58
+ /** The files written, in the tree's order. */
59
+ readonly files: readonly EmittedFilePath[];
60
+ /** How deeply the output was validated before it replaced the previous one. */
61
+ readonly checked: OutputCheck;
62
+ /**
63
+ * The deployment answered `304` — the ClientContract is the one the previous
64
+ * output was generated against — and the output was still replaced, because
65
+ * this generator or this entrypoint emits other bytes (Q6).
66
+ */
67
+ readonly contractUnchanged: boolean;
68
+ /**
69
+ * Everything the run has to say without failing, in a fixed order: the
70
+ * emission's rename notices, then the validator's degraded-check warning, then
71
+ * the writer's crash-recovery and cleanup notices.
72
+ */
73
+ readonly warnings: readonly string[];
74
+ }
75
+ /**
76
+ * The run found content in `AvClient.ts` or `generated/` the generator did not
77
+ * produce, and stopped before writing anything (architect, 2026-10-04). Whoever
78
+ * owns the terminal asks; `proceed` writes the emission already made — no second
79
+ * fetch — overwriting or removing exactly `foreign`, through the same
80
+ * temp → validate → replace path.
81
+ */
82
+ export interface ForeignOutputContent {
83
+ readonly kind: "foreign-content";
84
+ readonly generateAt: AbsolutePath;
85
+ /**
86
+ * Relative to `generateAt`, POSIX-separated, in code-unit order — including
87
+ * what a killed run left in the `generated/` it moved aside, named where its
88
+ * recovery puts it back.
89
+ */
90
+ readonly foreign: readonly string[];
91
+ /**
92
+ * What the run said before it stopped here — the emission's rename notices —
93
+ * for whoever stops it for good to report. `proceed` returns them again, first
94
+ * among its own.
95
+ */
96
+ readonly warnings: readonly string[];
97
+ proceed(): Promise<ClientGenerated>;
98
+ }
99
+ /**
100
+ * The deployment serves the ClientContract the output was generated against, and
101
+ * this generator, with this entrypoint, emits exactly the bytes already there
102
+ * (Phase 12-rest Q6): nothing was written.
103
+ */
104
+ export interface ClientUpToDate {
105
+ readonly kind: "up-to-date";
106
+ readonly generateAt: AbsolutePath;
107
+ /** The files already there, in the tree's order. */
108
+ readonly files: readonly EmittedFilePath[];
109
+ /** The emission's rename notices: still true of the output. */
110
+ readonly warnings: readonly string[];
111
+ }
112
+ export type ClientGenerationOutcome = ClientGenerated | ClientUpToDate | ForeignOutputContent;
113
+ /**
114
+ * Generates the client the project's config describes — or, when content the
115
+ * generator did not produce stands in its way and `overrideForeign` is not set,
116
+ * says what it is and writes nothing.
117
+ *
118
+ * @throws a refusal (`EnvFileError`, `ClientConfigError`, `ContractTransportError`,
119
+ * `ContractProtocolError`, `GeneratedNameError`, `OutputWriteError`) whose
120
+ * message is the whole diagnosis; the previous output is intact. Anything else
121
+ * is a defect.
122
+ */
123
+ export declare function generateClient(input: ClientGenerationInput): Promise<ClientGenerationOutcome>;
@@ -0,0 +1,98 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { loadClientConfigFile } from "./config/config.loader.js";
4
+ import { resolveClientConfig } from "./config/config.resolver.js";
5
+ import { resolveEnvCascade, } from "./config/env.cascade.js";
6
+ import { loadClientContractSince } from "./contract/contract.loader.js";
7
+ import { emitClientTree } from "./emit/client-tree.emitter.js";
8
+ import { parseContractCarrier } from "./emit/contract-carrier.emitter.js";
9
+ import { GENERATED_DIRECTORY, } from "./emit/emitted-tree.interface.js";
10
+ import { findForeignOutputContent, OutputWriteError, ownedOutputMatches, precedeDefect, writeClientOutput, } from "./output/output.writer.js";
11
+ /**
12
+ * The ClientContract the output in `generateAt` holds, when its owned entries
13
+ * are intact — nothing in them the generator did not produce — and its carrier
14
+ * parses back and re-verifies (Q4, Q6); otherwise undefined, and the GET is
15
+ * unconditional. A failure to inspect is no carrier: the run's own inspection,
16
+ * later, reports it.
17
+ */
18
+ async function storedContract(generateAt) {
19
+ try {
20
+ if ((await findForeignOutputContent(generateAt)).length > 0) {
21
+ return undefined;
22
+ }
23
+ return await parseContractCarrier(await readFile(path.join(generateAt, GENERATED_DIRECTORY, "contract.ts"), "utf8"));
24
+ }
25
+ catch {
26
+ return undefined;
27
+ }
28
+ }
29
+ /**
30
+ * Generates the client the project's config describes — or, when content the
31
+ * generator did not produce stands in its way and `overrideForeign` is not set,
32
+ * says what it is and writes nothing.
33
+ *
34
+ * @throws a refusal (`EnvFileError`, `ClientConfigError`, `ContractTransportError`,
35
+ * `ContractProtocolError`, `GeneratedNameError`, `OutputWriteError`) whose
36
+ * message is the whole diagnosis; the previous output is intact. Anything else
37
+ * is a defect.
38
+ */
39
+ export async function generateClient(input) {
40
+ const cascade = resolveEnvCascade({
41
+ directory: input.directory,
42
+ processEnv: input.processEnv,
43
+ });
44
+ const loaded = await loadClientConfigFile(input.directory);
45
+ const config = resolveClientConfig({
46
+ config: loaded.config,
47
+ cascade,
48
+ configDirectory: path.dirname(loaded.file),
49
+ });
50
+ const { contract, notModified } = await loadClientContractSince(input.fetch === undefined
51
+ ? { entrypoint: config.entrypoint }
52
+ : { entrypoint: config.entrypoint, fetch: input.fetch }, await storedContract(config.generateAt));
53
+ const emission = emitClientTree(contract, config.entrypoint);
54
+ if (notModified &&
55
+ (await ownedOutputMatches(config.generateAt, emission.tree))) {
56
+ return {
57
+ kind: "up-to-date",
58
+ generateAt: config.generateAt,
59
+ files: emission.tree.map((file) => file.path),
60
+ warnings: emission.warnings,
61
+ };
62
+ }
63
+ const write = async (overrideForeign) => {
64
+ const written = await writeClientOutput(input.resolveTypeScript === undefined
65
+ ? { emission, generateAt: config.generateAt, overrideForeign }
66
+ : {
67
+ emission,
68
+ generateAt: config.generateAt,
69
+ overrideForeign,
70
+ resolveTypeScript: input.resolveTypeScript,
71
+ });
72
+ return {
73
+ kind: "generated",
74
+ generateAt: written.generateAt,
75
+ files: emission.tree.map((file) => file.path),
76
+ checked: written.checked,
77
+ contractUnchanged: notModified,
78
+ warnings: written.warnings,
79
+ };
80
+ };
81
+ // A structural refusal or a defect here comes after the emission's renames
82
+ // were raised; it carries them, as the writer's own do.
83
+ const foreign = await findForeignOutputContent(config.generateAt).catch((error) => {
84
+ throw error instanceof OutputWriteError
85
+ ? error.precededBy(emission.warnings)
86
+ : precedeDefect(error, emission.warnings);
87
+ });
88
+ if (foreign.length === 0 || input.overrideForeign === true) {
89
+ return write(foreign);
90
+ }
91
+ return {
92
+ kind: "foreign-content",
93
+ generateAt: config.generateAt,
94
+ foreign,
95
+ warnings: emission.warnings,
96
+ proceed: () => write(foreign),
97
+ };
98
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * This package's version, read from its own manifest — the `package.json` every
3
+ * tarball carries beside `dist/` — so a release's `changeset version` is the one
4
+ * statement of it.
5
+ */
6
+ export declare const AVENTARA_CLIENT_GENERATOR_VERSION: string;
7
+ export type { ClientConfigInput, ConfigValue, EnvReference, } from "./config/client-config.interface.js";
8
+ export { defineClientConfig, env } from "./config/config.resolver.js";
package/dist/index.js ADDED
@@ -0,0 +1,8 @@
1
+ import { readFileSync } from "node:fs";
2
+ /**
3
+ * This package's version, read from its own manifest — the `package.json` every
4
+ * tarball carries beside `dist/` — so a release's `changeset version` is the one
5
+ * statement of it.
6
+ */
7
+ export const AVENTARA_CLIENT_GENERATOR_VERSION = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")).version;
8
+ export { defineClientConfig, env } from "./config/config.resolver.js";
@@ -0,0 +1,6 @@
1
+ /** R4, §15.2 — the `framework.client.ts` `avclient init` writes: developer-owned, read by `avclient generate`. */
2
+ export declare function clientConfigSource(input: {
3
+ readonly entrypoint: string;
4
+ readonly envVar: string | undefined;
5
+ readonly generateAt: string;
6
+ }): string;