@aventara/client 0.0.0-stage → 0.1.0-pilot.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.
Files changed (84) hide show
  1. package/LICENSE +91 -0
  2. package/LICENSE-ADDITIONAL-PERMISSION.md +9 -0
  3. package/README.md +234 -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 +37 -0
  7. package/dist/cli/command.parser.js +177 -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 +52 -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 +75 -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 +41 -0
  23. package/dist/config/config.loader.js +95 -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 +11 -0
  65. package/dist/init/client-config.template.js +27 -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 +86 -0
  70. package/dist/init/client-init.planner.d.ts +27 -0
  71. package/dist/init/client-init.planner.js +99 -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 +75 -0
  81. package/dist/output/output.validator.js +262 -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,75 @@
1
+ import type * as ts from "typescript";
2
+ import type { EmittedTree } from "../emit/emitted-tree.interface.js";
3
+ /**
4
+ * §15.3's "type/sanity validation" step, between emit-to-temp and the replace:
5
+ * the gate that makes the atomicity worth having (plan §4 Q6). It judges a
6
+ * directory the writer has just filled with `tree`, and answers with a value — the
7
+ * writer decides what a rejection means for the previous output.
8
+ *
9
+ * # Three checks, the strongest available first
10
+ *
11
+ * - **Shape**, always: the directory holds exactly `tree` — no file missing, none
12
+ * extra, every one byte-for-byte what was emitted — and every file carries the
13
+ * ownership line. That last is not decoration: the NEXT run reads it to decide
14
+ * that this directory is a previous generation it may replace whole, so a file
15
+ * without it would make the next run refuse.
16
+ * - **Types**, when the `typescript` optional peer resolves (Q5, Q6): a real
17
+ * program over the tree under a consumer's strictest plausible settings. The
18
+ * tree is judged ALONE — the compiler host serves nothing outside the directory
19
+ * but TypeScript's own `lib` files, so an import the tree cannot satisfy itself
20
+ * fails here even when a `node_modules` beside the output could satisfy it
21
+ * (§15.5).
22
+ * - **Syntax**, when it does not — or when the `typescript` that resolves has no
23
+ * classic compiler API (TypeScript 7, plan B3; pilot.1): each file through
24
+ * Node's own TypeScript parser (`node:module`'s `stripTypeScriptTypes`), with a
25
+ * loud warning that the output was NOT type-checked. Degraded, never skipped
26
+ * (S7), and never a refusal: the developer's own `tsc` still checks the tree
27
+ * when their project compiles.
28
+ */
29
+ /** The `typescript` module, as the validator uses it. */
30
+ export type TypeScriptCompiler = typeof ts;
31
+ /**
32
+ * Finds the `typescript` optional peer: the module, or `undefined` when it is not
33
+ * installed. Anything else — an installation that resolves but fails to load — is
34
+ * not "absent" and is thrown.
35
+ */
36
+ export type TypeScriptResolver = () => Promise<TypeScriptCompiler | undefined>;
37
+ /**
38
+ * A resolver for `specifier`, looked up from this package the way Node would look
39
+ * it up: an optional peer is linked beside the package that declares it.
40
+ */
41
+ export declare function typeScriptResolverFor(specifier: string): TypeScriptResolver;
42
+ /** The `typescript` this package's optional peer names (Q5). */
43
+ export declare const resolveInstalledTypeScript: TypeScriptResolver;
44
+ /** How deeply the tree was judged. */
45
+ export type OutputCheck = "types" | "syntax" | "shape";
46
+ export interface OutputAccepted {
47
+ readonly accepted: true;
48
+ readonly checked: OutputCheck;
49
+ /** Said once to the person running the generator; empty when nothing degraded. */
50
+ readonly warnings: readonly string[];
51
+ }
52
+ export interface OutputRejected {
53
+ readonly accepted: false;
54
+ readonly checked: OutputCheck;
55
+ /** What is wrong, one line per finding, paths relative to the tree. */
56
+ readonly findings: readonly string[];
57
+ }
58
+ export type OutputValidation = OutputAccepted | OutputRejected;
59
+ /**
60
+ * The warning a run without `typescript` carries. Loud on purpose, in its words:
61
+ * the prefix is the CLI's one `warning:`, so the text carries none of its own.
62
+ */
63
+ export declare const TYPESCRIPT_UNRESOLVED_WARNING: string;
64
+ /** The warning a run with neither `typescript` nor Node's parser carries. */
65
+ export declare const SYNTAX_CHECK_UNAVAILABLE_WARNING: string;
66
+ /**
67
+ * The warning a run carries when the `typescript` that resolves is one the
68
+ * generator cannot type-check with (TypeScript 7): what was checked instead,
69
+ * and where the type check still happens.
70
+ */
71
+ export declare function typeScriptWithoutCompilerApiWarning(version: string, checked: "syntax" | "shape"): string;
72
+ /**
73
+ * Judges `directory`, which the caller has just filled with `tree`.
74
+ */
75
+ export declare function validateOutputTree(directory: string, tree: EmittedTree, resolveTypeScript?: TypeScriptResolver): Promise<OutputValidation>;
@@ -0,0 +1,262 @@
1
+ import { readdir, readFile } from "node:fs/promises";
2
+ import * as nodeModule from "node:module";
3
+ import { createRequire } from "node:module";
4
+ import path from "node:path";
5
+ import { pathToFileURL } from "node:url";
6
+ import { carriesGeneratedOwnership } from "../emit/banner.emitter.js";
7
+ /**
8
+ * A resolver for `specifier`, looked up from this package the way Node would look
9
+ * it up: an optional peer is linked beside the package that declares it.
10
+ */
11
+ export function typeScriptResolverFor(specifier) {
12
+ return async () => {
13
+ let resolved;
14
+ try {
15
+ resolved = createRequire(import.meta.url).resolve(specifier);
16
+ }
17
+ catch (error) {
18
+ if (isErrorWithCode(error, "MODULE_NOT_FOUND")) {
19
+ return undefined;
20
+ }
21
+ throw error;
22
+ }
23
+ const loaded = (await import(pathToFileURL(resolved).href));
24
+ return loaded.default ?? loaded;
25
+ };
26
+ }
27
+ /** The `typescript` this package's optional peer names (Q5). */
28
+ export const resolveInstalledTypeScript = typeScriptResolverFor("typescript");
29
+ /**
30
+ * Whether `compiler` has the classic compiler API {@link typeFindingsOf} uses.
31
+ * TypeScript 7 does not (plan B3: its package exports its version and nothing
32
+ * else); before pilot.1 the generator refused it, and its peer range stopped
33
+ * below it — which made `npm i` fail with ERESOLVE in any frontend whose
34
+ * `typescript` is the current `latest`.
35
+ */
36
+ function hasClassicCompilerApi(compiler) {
37
+ // One member, not `Partial<typeof ts>`: a mapped type over the whole
38
+ // compiler namespace costs thousands of instantiations to ask one question.
39
+ return (typeof compiler.createProgram ===
40
+ "function");
41
+ }
42
+ /** The version a resolved `typescript` names, whatever its API. */
43
+ function versionOf(compiler) {
44
+ const { version } = compiler;
45
+ return typeof version === "string" ? version : "(no version)";
46
+ }
47
+ /**
48
+ * The warning a run without `typescript` carries. Loud on purpose, in its words:
49
+ * the prefix is the CLI's one `warning:`, so the text carries none of its own.
50
+ */
51
+ export const TYPESCRIPT_UNRESOLVED_WARNING = "`typescript` could not be resolved, so the generated output was NOT type-checked — " +
52
+ "only its shape and syntax were. Install `typescript` (an optional peer of @aventara/client) " +
53
+ "in this project to restore the type check.";
54
+ /** The warning a run with neither `typescript` nor Node's parser carries. */
55
+ export const SYNTAX_CHECK_UNAVAILABLE_WARNING = "neither `typescript` nor Node's TypeScript parser is available, so the generated " +
56
+ "output was NOT type-checked or parsed — only its shape was. Install `typescript` (an " +
57
+ "optional peer of @aventara/client) in this project to restore the type check.";
58
+ /**
59
+ * The warning a run carries when the `typescript` that resolves is one the
60
+ * generator cannot type-check with (TypeScript 7): what was checked instead,
61
+ * and where the type check still happens.
62
+ */
63
+ export function typeScriptWithoutCompilerApiWarning(version, checked) {
64
+ return (`the installed \`typescript\` ${version} has no classic compiler API (\`createProgram\`), so the generated ` +
65
+ `output was NOT type-checked${checked === "shape" ? " or parsed — only its shape was" : " — only its shape and syntax were"}. ` +
66
+ "Your project's own `tsc` checks it when it compiles; a `typescript` 5.5 to 6 restores the generator's own type check.");
67
+ }
68
+ /**
69
+ * Judges `directory`, which the caller has just filled with `tree`.
70
+ */
71
+ export async function validateOutputTree(directory, tree, resolveTypeScript = resolveInstalledTypeScript) {
72
+ const shapeFindings = await shapeFindingsOf(directory, tree);
73
+ if (shapeFindings.length > 0) {
74
+ return { accepted: false, checked: "shape", findings: shapeFindings };
75
+ }
76
+ const compiler = await resolveTypeScript();
77
+ if (compiler !== undefined && hasClassicCompilerApi(compiler)) {
78
+ const findings = typeFindingsOf(compiler, directory, tree);
79
+ return findings.length > 0
80
+ ? { accepted: false, checked: "types", findings }
81
+ : { accepted: true, checked: "types", warnings: [] };
82
+ }
83
+ const strip = nodeTypeScriptParser();
84
+ if (strip === undefined) {
85
+ return {
86
+ accepted: true,
87
+ checked: "shape",
88
+ warnings: [
89
+ compiler === undefined
90
+ ? SYNTAX_CHECK_UNAVAILABLE_WARNING
91
+ : typeScriptWithoutCompilerApiWarning(versionOf(compiler), "shape"),
92
+ ],
93
+ };
94
+ }
95
+ const findings = syntaxFindingsOf(strip, tree);
96
+ return findings.length > 0
97
+ ? { accepted: false, checked: "syntax", findings }
98
+ : {
99
+ accepted: true,
100
+ checked: "syntax",
101
+ warnings: [
102
+ compiler === undefined
103
+ ? TYPESCRIPT_UNRESOLVED_WARNING
104
+ : typeScriptWithoutCompilerApiWarning(versionOf(compiler), "syntax"),
105
+ ],
106
+ };
107
+ }
108
+ /* ------------------------------------------------------------------ *
109
+ * Shape *
110
+ * ------------------------------------------------------------------ */
111
+ async function shapeFindingsOf(directory, tree) {
112
+ const findings = [];
113
+ const expected = new Map(tree.map((file) => [file.path, file.bytes]));
114
+ const entries = await readdir(directory, {
115
+ recursive: true,
116
+ withFileTypes: true,
117
+ });
118
+ const present = new Set();
119
+ for (const entry of entries) {
120
+ if (entry.isDirectory()) {
121
+ continue;
122
+ }
123
+ const relative = path
124
+ .relative(directory, path.join(entry.parentPath, entry.name))
125
+ .split(path.sep)
126
+ .join("/");
127
+ present.add(relative);
128
+ if (!entry.isFile() || !expected.has(relative)) {
129
+ findings.push(`${relative}: not part of the emitted tree`);
130
+ }
131
+ }
132
+ const decoder = new TextDecoder("utf-8", { fatal: true });
133
+ for (const [relative, bytes] of expected) {
134
+ if (!present.has(relative)) {
135
+ findings.push(`${relative}: missing`);
136
+ continue;
137
+ }
138
+ const written = await readFile(path.join(directory, relative));
139
+ if (!Buffer.from(bytes).equals(written)) {
140
+ findings.push(`${relative}: not the bytes that were emitted`);
141
+ continue;
142
+ }
143
+ let text;
144
+ try {
145
+ text = decoder.decode(written);
146
+ }
147
+ catch {
148
+ findings.push(`${relative}: not UTF-8`);
149
+ continue;
150
+ }
151
+ if (!carriesGeneratedOwnership(text)) {
152
+ findings.push(`${relative}: does not carry the generated banner`);
153
+ }
154
+ }
155
+ return findings.sort();
156
+ }
157
+ /* ------------------------------------------------------------------ *
158
+ * Types: a real program over the tree, and nothing outside it *
159
+ * ------------------------------------------------------------------ */
160
+ /**
161
+ * The emitted-tree fixture's settings (`__fixtures__/emitted-tree.fixture.ts`),
162
+ * restated here because production code does not import a test fixture: a
163
+ * consumer's strictest plausible configuration, with no DOM and no Node (§15.7:
164
+ * both are first-class targets, so the tree may assume neither).
165
+ */
166
+ function compilerOptionsFor(compiler) {
167
+ return {
168
+ target: compiler.ScriptTarget.ES2022,
169
+ module: compiler.ModuleKind.NodeNext,
170
+ moduleResolution: compiler.ModuleResolutionKind.NodeNext,
171
+ lib: ["lib.es2022.d.ts"],
172
+ types: [],
173
+ strict: true,
174
+ noUncheckedIndexedAccess: true,
175
+ exactOptionalPropertyTypes: true,
176
+ verbatimModuleSyntax: true,
177
+ isolatedModules: true,
178
+ erasableSyntaxOnly: true,
179
+ noUnusedLocals: true,
180
+ noUnusedParameters: true,
181
+ noImplicitReturns: true,
182
+ noImplicitOverride: true,
183
+ noFallthroughCasesInSwitch: true,
184
+ noPropertyAccessFromIndexSignature: true,
185
+ skipDefaultLibCheck: true,
186
+ noEmit: true,
187
+ };
188
+ }
189
+ /**
190
+ * The tree is served as an ES module package of its own: NodeNext reads the
191
+ * nearest `package.json` to decide a `.ts` file's module format, and the one a
192
+ * consumer's project happens to have must not decide this verdict.
193
+ */
194
+ const VIRTUAL_PACKAGE_JSON = '{ "type": "module" }';
195
+ function typeFindingsOf(compiler, directory, tree) {
196
+ const root = path.resolve(directory);
197
+ const options = compilerOptionsFor(compiler);
198
+ const libraryDirectory = path.dirname(compiler.getDefaultLibFilePath(options));
199
+ const virtualPackageJson = path.join(root, "package.json");
200
+ const servable = (file) => isInside(root, file) || isInside(libraryDirectory, file);
201
+ const host = compiler.createCompilerHost(options, true);
202
+ const baseFileExists = host.fileExists.bind(host);
203
+ const baseReadFile = host.readFile.bind(host);
204
+ const baseDirectoryExists = host.directoryExists?.bind(host);
205
+ host.fileExists = (file) => path.resolve(file) === virtualPackageJson ||
206
+ (servable(path.resolve(file)) && baseFileExists(file));
207
+ host.readFile = (file) => path.resolve(file) === virtualPackageJson
208
+ ? VIRTUAL_PACKAGE_JSON
209
+ : servable(path.resolve(file))
210
+ ? baseReadFile(file)
211
+ : undefined;
212
+ host.directoryExists = (candidate) => {
213
+ const resolved = path.resolve(candidate);
214
+ return ((servable(resolved) || isInside(resolved, root)) &&
215
+ (baseDirectoryExists?.(candidate) ?? true));
216
+ };
217
+ host.getCurrentDirectory = () => root;
218
+ const program = compiler.createProgram({
219
+ rootNames: tree.map((file) => path.join(root, file.path)),
220
+ options,
221
+ host,
222
+ });
223
+ const diagnostics = compiler.getPreEmitDiagnostics(program);
224
+ return diagnostics.map((diagnostic) => compiler
225
+ .formatDiagnostic(diagnostic, {
226
+ getCanonicalFileName: (file) => file,
227
+ getCurrentDirectory: () => root,
228
+ getNewLine: () => "\n",
229
+ })
230
+ .trimEnd());
231
+ }
232
+ /** Whether `file` is `directory` or lies beneath it. */
233
+ function isInside(directory, file) {
234
+ const relative = path.relative(directory, file);
235
+ return (relative === "" ||
236
+ (!relative.startsWith("..") && !path.isAbsolute(relative)));
237
+ }
238
+ /** Node's TypeScript parser, where this Node has one. */
239
+ function nodeTypeScriptParser() {
240
+ const candidate = nodeModule.stripTypeScriptTypes;
241
+ return typeof candidate === "function" ? candidate : undefined;
242
+ }
243
+ function syntaxFindingsOf(strip, tree) {
244
+ const decoder = new TextDecoder("utf-8");
245
+ const findings = [];
246
+ for (const file of tree) {
247
+ try {
248
+ strip(decoder.decode(file.bytes));
249
+ }
250
+ catch (error) {
251
+ if (isErrorWithCode(error, "ERR_INVALID_TYPESCRIPT_SYNTAX")) {
252
+ findings.push(`${file.path}: ${error.message}`);
253
+ continue;
254
+ }
255
+ throw error;
256
+ }
257
+ }
258
+ return findings;
259
+ }
260
+ function isErrorWithCode(error, code) {
261
+ return error instanceof Error && error.code === code;
262
+ }
@@ -0,0 +1,162 @@
1
+ import * as fsPromises from "node:fs/promises";
2
+ import type { AbsolutePath } from "../config/client-config.interface.js";
3
+ import { type ClientEmission, type EmittedTree } from "../emit/emitted-tree.interface.js";
4
+ import { type OutputCheck, type TypeScriptResolver } from "./output.validator.js";
5
+ /**
6
+ * §15.3's last two steps as one call — emit into a temporary directory, validate
7
+ * it, atomically replace the generated output — and the property that makes them
8
+ * one: **a failed generation leaves the previous valid output intact**, byte for
9
+ * byte.
10
+ *
11
+ * # What it owns (architect, 2026-10-04)
12
+ *
13
+ * The output is written into `generateAt`, a directory the developer SHARES: it
14
+ * may hold their own files. The generator owns exactly two entries in it —
15
+ * `AvClient.ts` and `generated/` — and reads, refuses on, moves or deletes
16
+ * nothing else there, apart from the transient entries it names itself
17
+ * (`.aventara-next-*`, `.aventara-ready-*`, `.generated.aventara-previous`).
18
+ *
19
+ * - `generated/` is replaced WHOLE, never merged: a file the previous run emitted
20
+ * and this one does not is gone. It is replaced only when every file in it
21
+ * carries the generated banner's ownership line (an empty directory owns
22
+ * nothing and is fine); otherwise the run is refused, naming the foreign files.
23
+ * - `AvClient.ts` is replaced only when it is absent or carries the ownership
24
+ * line; otherwise the run is refused, naming it.
25
+ *
26
+ * # The order, and its one window
27
+ *
28
+ * The whole tree — `AvClient.ts` and `generated/` together — is written into a
29
+ * staging directory inside `generateAt` (same filesystem, so every move is one
30
+ * atomic `rename`) and validated there as one program. Then:
31
+ *
32
+ * 1. the staging directory is renamed `.aventara-ready-*`: from here it is known
33
+ * to be validated;
34
+ * 2. the previous `generated/` → `.generated.aventara-previous`;
35
+ * 3. the staged `generated/` → `generated/`;
36
+ * 4. the staged `AvClient.ts` → `AvClient.ts`, one rename over the old file;
37
+ * 5. the previous `generated/` and the staging directory are removed.
38
+ *
39
+ * A failed step 3 renames the previous `generated/` straight back; a failed step
40
+ * 4 moves the new `generated/` out again and the previous back, so a failure at
41
+ * any step leaves the previous pair. Node has no atomic exchange of two entries,
42
+ * so two windows remain for a process KILLED mid-replace, and the next run's
43
+ * recovery closes each before it does anything else:
44
+ *
45
+ * - between 2 and 3, `generated/` does not exist: the previous one is put back;
46
+ * - between 3 and 4, the new `generated/` sits beside the old `AvClient.ts`: the
47
+ * validated `AvClient.ts` is still staged in `.aventara-ready-*`, so it is
48
+ * moved in, completing the new pair rather than leaving a mismatched one.
49
+ *
50
+ * Staging directories that never reached step 1 were not validated and are
51
+ * removed.
52
+ *
53
+ * # Failure is a sentence
54
+ *
55
+ * A refusal, a rejected tree or a filesystem error (an `Error` with a `code`) is
56
+ * an `OutputWriteError` naming its stage — the person running the generator can
57
+ * act on it (M3). Any other error is a defect and propagates with its stack —
58
+ * the very error thrown, not a wrapper — and what the run said before it is
59
+ * kept beside it, read by `warningsRaisedBeforeDefect`.
60
+ *
61
+ * A refusal carries every warning the run raised before it stopped, in the order
62
+ * a success would have returned them: a crash recovered in `prepare` changed the
63
+ * disk, and the person has to hear that even when the run then fails.
64
+ *
65
+ * A failed run removes the directories it created to reach `generateAt` — and
66
+ * only those, and only while empty — so a first run that fails leaves no empty
67
+ * `generateAt` behind.
68
+ */
69
+ /** The four stages a write passes through, in order. */
70
+ export type OutputWriteStage = "prepare" | "temp" | "validate" | "replace";
71
+ /** A write that stopped; the previous output is intact unless it says otherwise. */
72
+ export declare class OutputWriteError extends Error {
73
+ readonly stage: OutputWriteStage;
74
+ readonly name = "OutputWriteError";
75
+ /**
76
+ * What the run said before it stopped, in the order a success returns its
77
+ * warnings: the emission's, the validator's, the writer's. Printed before the
78
+ * refusal's sentence.
79
+ */
80
+ readonly warnings: readonly string[];
81
+ constructor(stage: OutputWriteStage, message: string, options?: {
82
+ readonly cause?: unknown;
83
+ readonly warnings?: readonly string[];
84
+ });
85
+ /** The same refusal, with `earlier` said before the warnings it already carries. */
86
+ precededBy(earlier: readonly string[]): OutputWriteError;
87
+ }
88
+ /**
89
+ * Records `earlier` as said before `defect`, ahead of anything already recorded,
90
+ * and returns `defect` unchanged so the caller rethrows the same error.
91
+ */
92
+ export declare function precedeDefect(defect: unknown, earlier: readonly string[]): unknown;
93
+ /**
94
+ * What the run said before `defect` stopped it, in the order a success returns
95
+ * its warnings; empty when nothing was, or for an error no run threw.
96
+ */
97
+ export declare function warningsRaisedBeforeDefect(defect: unknown): readonly string[];
98
+ /**
99
+ * The filesystem calls the writer makes, `node:fs/promises` by default. A seam
100
+ * because the filesystem is the writer's one external collaborator, and the only
101
+ * way to prove a failure at the replace stage leaves the previous output intact
102
+ * is to make a real rename fail.
103
+ */
104
+ export type OutputFileSystem = Pick<typeof fsPromises, "lstat" | "mkdir" | "open" | "readdir" | "rename" | "rm" | "rmdir" | "writeFile">;
105
+ export interface ClientOutputWriteInput {
106
+ /** What `emitClientTree` returned: the tree, and the warnings it raised. */
107
+ readonly emission: ClientEmission;
108
+ /** The resolved `generateAt` (§15.2, architect 2026-10-04). Created when missing. */
109
+ readonly generateAt: AbsolutePath;
110
+ /**
111
+ * Content in `AvClient.ts` or `generated/` the generator did not produce, which
112
+ * the person running it confirmed may be overwritten or removed (architect,
113
+ * 2026-10-04) — the paths `findForeignOutputContent` listed. Anything foreign
114
+ * that is not listed here is refused.
115
+ */
116
+ readonly overrideForeign?: readonly string[];
117
+ /** The `typescript` optional peer; the installed one by default (Q6). */
118
+ readonly resolveTypeScript?: TypeScriptResolver;
119
+ readonly fileSystem?: OutputFileSystem;
120
+ }
121
+ export interface ClientOutputWritten {
122
+ readonly generateAt: AbsolutePath;
123
+ /** How deeply the tree was validated before it replaced the output (Q6). */
124
+ readonly checked: OutputCheck;
125
+ /**
126
+ * Everything the run has to say without failing, in order: the emission's own
127
+ * warnings (the rename ladder's), then the validator's (a degraded check), then
128
+ * the writer's (a recovered crash, a leftover it could not remove). Returned,
129
+ * never printed: the CLI owns the terminal.
130
+ */
131
+ readonly warnings: readonly string[];
132
+ }
133
+ /**
134
+ * Writes `emission.tree` as `<generateAt>/AvClient.ts` and `<generateAt>/generated/`.
135
+ *
136
+ * @throws OutputWriteError when the write stops; the previous output is intact.
137
+ */
138
+ export declare function writeClientOutput(input: ClientOutputWriteInput): Promise<ClientOutputWritten>;
139
+ /**
140
+ * Lists what in `<generateAt>/AvClient.ts` and `<generateAt>/generated/` the
141
+ * generator did not produce — a file without the ownership line, a symbolic link,
142
+ * anything that is not a regular file — so the person running it can be asked
143
+ * before a generation overwrites or removes it (architect, 2026-10-04). Reads
144
+ * only; looks at nothing else in `generateAt`. Empty when `generateAt` does not
145
+ * exist yet.
146
+ *
147
+ * It sees what the write will see once it has recovered a killed run, without
148
+ * recovering it: a `generated/` the killed run moved aside with nothing in its
149
+ * place is read where it lies and its files named where recovery puts them back
150
+ * (`generated/…`). So the question asked, and `--yes`, cover them too — and a
151
+ * run that stops here has still touched nothing.
152
+ *
153
+ * @throws OutputWriteError when `generateAt` is not a directory, `generated` is
154
+ * not a directory or `AvClient.ts` is not a file: no confirmation repairs that.
155
+ */
156
+ export declare function findForeignOutputContent(generateAt: AbsolutePath, fileSystem?: OutputFileSystem): Promise<readonly string[]>;
157
+ /**
158
+ * Whether `AvClient.ts` and `generated/` in `generateAt` hold exactly `tree` —
159
+ * every file byte for byte, no other file, and nothing a killed run moved aside —
160
+ * so that writing `tree` would change nothing (Phase 12-rest Q6: "up to date").
161
+ */
162
+ export declare function ownedOutputMatches(generateAt: AbsolutePath, tree: EmittedTree): Promise<boolean>;