@aventara/client 0.1.0-pilot.1 → 0.1.0-pilot.3

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 (79) hide show
  1. package/README.md +44 -9
  2. package/dist/avclient.bin.js +0 -10
  3. package/dist/cli/command.parser.d.ts +15 -10
  4. package/dist/cli/command.parser.js +13 -19
  5. package/dist/cli/generate.command.js +0 -6
  6. package/dist/cli/generation-failure.renderer.js +0 -14
  7. package/dist/cli/generation-success.renderer.d.ts +4 -1
  8. package/dist/cli/generation-success.renderer.js +0 -13
  9. package/dist/cli/terminal.prompter.d.ts +1 -2
  10. package/dist/cli/warning.renderer.d.ts +2 -2
  11. package/dist/cli/warning.renderer.js +0 -8
  12. package/dist/cli.d.ts +8 -16
  13. package/dist/cli.js +5 -25
  14. package/dist/config/client-config.interface.d.ts +18 -16
  15. package/dist/config/client-config.interface.js +0 -13
  16. package/dist/config/config.loader.d.ts +34 -22
  17. package/dist/config/config.loader.js +49 -52
  18. package/dist/config/config.resolver.d.ts +13 -19
  19. package/dist/config/config.resolver.js +9 -48
  20. package/dist/config/env.cascade.d.ts +12 -14
  21. package/dist/config/env.cascade.js +0 -19
  22. package/dist/config/module-style.resolver.d.ts +52 -0
  23. package/dist/config/module-style.resolver.js +75 -0
  24. package/dist/config/tsconfig.locator.d.ts +45 -0
  25. package/dist/config/tsconfig.locator.js +52 -0
  26. package/dist/contract/contract.acceptance.d.ts +12 -26
  27. package/dist/contract/contract.acceptance.js +0 -54
  28. package/dist/contract/contract.fetcher.d.ts +12 -17
  29. package/dist/contract/contract.fetcher.js +0 -24
  30. package/dist/contract/contract.loader.d.ts +4 -5
  31. package/dist/contract/contract.loader.js +0 -10
  32. package/dist/emit/banner.emitter.d.ts +11 -12
  33. package/dist/emit/banner.emitter.js +0 -26
  34. package/dist/emit/client-surface.emitter.d.ts +17 -21
  35. package/dist/emit/client-surface.emitter.js +29 -55
  36. package/dist/emit/client-tree.emitter.d.ts +11 -20
  37. package/dist/emit/client-tree.emitter.js +12 -54
  38. package/dist/emit/contract-carrier.emitter.d.ts +5 -6
  39. package/dist/emit/contract-carrier.emitter.js +0 -28
  40. package/dist/emit/derivation.emitter.d.ts +7 -7
  41. package/dist/emit/derivation.emitter.js +2 -161
  42. package/dist/emit/descriptor.emitter.js +2 -28
  43. package/dist/emit/emitted-tree.interface.d.ts +40 -17
  44. package/dist/emit/emitted-tree.interface.js +6 -16
  45. package/dist/emit/enum.emitter.d.ts +4 -4
  46. package/dist/emit/enum.emitter.js +0 -24
  47. package/dist/emit/module-specifier.scanner.d.ts +25 -0
  48. package/dist/emit/module-specifier.scanner.js +160 -0
  49. package/dist/emit/module-style.interface.d.ts +58 -0
  50. package/dist/emit/module-style.interface.js +8 -0
  51. package/dist/emit/name.deriver.d.ts +33 -61
  52. package/dist/emit/name.deriver.js +0 -134
  53. package/dist/emit/named-type.emitter.d.ts +14 -21
  54. package/dist/emit/named-type.emitter.js +3 -30
  55. package/dist/emit/runtime.emitter.d.ts +23 -50
  56. package/dist/emit/runtime.emitter.js +68 -159
  57. package/dist/emit/scalar.codec.d.ts +20 -33
  58. package/dist/emit/scalar.codec.js +13 -69
  59. package/dist/emit/transaction.emitter.d.ts +6 -14
  60. package/dist/emit/transaction.emitter.js +24 -33
  61. package/dist/generate.d.ts +20 -34
  62. package/dist/generate.js +14 -22
  63. package/dist/index.js +0 -5
  64. package/dist/init/client-config.template.d.ts +6 -4
  65. package/dist/init/client-config.template.js +10 -13
  66. package/dist/init/client-init.errors.js +0 -3
  67. package/dist/init/client-init.orchestrator.js +8 -9
  68. package/dist/init/client-init.planner.d.ts +1 -9
  69. package/dist/init/client-init.planner.js +16 -24
  70. package/dist/init/client-init.questions.d.ts +8 -12
  71. package/dist/init/client-init.questions.js +0 -11
  72. package/dist/init/client-project.inspector.d.ts +6 -0
  73. package/dist/init/client-project.inspector.js +2 -2
  74. package/dist/node-version.guard.js +0 -12
  75. package/dist/output/output.validator.d.ts +49 -27
  76. package/dist/output/output.validator.js +113 -74
  77. package/dist/output/output.writer.d.ts +59 -52
  78. package/dist/output/output.writer.js +72 -134
  79. package/package.json +6 -4
@@ -3,29 +3,30 @@ import type { AbsolutePath } from "../config/client-config.interface.js";
3
3
  import { type ClientEmission, type EmittedTree } from "../emit/emitted-tree.interface.js";
4
4
  import { type OutputCheck, type TypeScriptResolver } from "./output.validator.js";
5
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
6
  * 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`).
7
+ * may hold their own files. The generator owns exactly these entries in it — the
8
+ * entry file `AvClient.ts` and `generated/` — and reads, refuses on, moves or
9
+ * deletes nothing else there, apart from the transient entries it names itself
10
+ * (`.aventara-next-*`, `.aventara-ready-*`, `.generated.aventara-previous`) and an
11
+ * unreleased build's entry files, `AvClient.mjs` and `AvClient.d.mts`, which it
12
+ * removes only while they carry the ownership line.
18
13
  *
19
14
  * - `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
15
+ * and this one does not is gone — an unreleased build's `.mjs` modules included.
16
+ * It is replaced only when every file in it carries the generated banner's
17
+ * ownership line (an empty directory owns nothing and is fine); otherwise the
18
+ * run is refused, naming the foreign files.
19
+ * - Each entry file is replaced only when it is absent or carries the ownership
24
20
  * line; otherwise the run is refused, naming it.
21
+ * - `AvClient.mjs` and `AvClient.d.mts` — an unreleased build's entry — are
22
+ * removed when they carry the ownership line, so no stale JavaScript is left
23
+ * beside `AvClient.ts`. Without the line each is the developer's own file: never
24
+ * read beyond its head, never named, never removed. (pilot.0's and pilot.1's
25
+ * `AvClient.ts` is the entry file itself, replaced in place.)
25
26
  *
26
27
  * # The order, and its one window
27
28
  *
28
- * The whole tree — `AvClient.ts` and `generated/` together — is written into a
29
+ * The whole tree — the entry files and `generated/` together — is written into a
29
30
  * staging directory inside `generateAt` (same filesystem, so every move is one
30
31
  * atomic `rename`) and validated there as one program. Then:
31
32
  *
@@ -33,19 +34,22 @@ import { type OutputCheck, type TypeScriptResolver } from "./output.validator.js
33
34
  * to be validated;
34
35
  * 2. the previous `generated/` → `.generated.aventara-previous`;
35
36
  * 3. the staged `generated/` → `generated/`;
36
- * 4. the staged `AvClient.ts` → `AvClient.ts`, one rename over the old file;
37
+ * 4. the previous entry files, and an owned earlier entry, move aside into the
38
+ * ready directory; then each staged entry file moves in;
37
39
  * 5. the previous `generated/` and the staging directory are removed.
38
40
  *
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:
41
+ * A failed step 3 renames the previous `generated/` straight back; a failed step 4
42
+ * moves what it moved back — the new entry files out, the previous ones in — then
43
+ * the new `generated/` out and the previous back, so a failure at any step leaves
44
+ * the previous output. Node has no atomic exchange of two entries, so two windows
45
+ * remain for a process KILLED mid-replace, and the next run's recovery closes each
46
+ * before it does anything else:
44
47
  *
45
48
  * - 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
+ * - between 3 and the end of 4, the new `generated/` sits beside the old entry
50
+ * files, or beside only some new ones: the validated entry files still staged in
51
+ * `.aventara-ready-*` are moved in, completing the new output rather than
52
+ * leaving a mismatched one.
49
53
  *
50
54
  * Staging directories that never reached step 1 were not validated and are
51
55
  * removed.
@@ -54,16 +58,16 @@ import { type OutputCheck, type TypeScriptResolver } from "./output.validator.js
54
58
  *
55
59
  * A refusal, a rejected tree or a filesystem error (an `Error` with a `code`) is
56
60
  * 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`.
61
+ * act on it. Any other error is a defect and propagates with its stack — the very
62
+ * error thrown, not a wrapper — and what the run said before it is kept beside it,
63
+ * read by `warningsRaisedBeforeDefect`.
60
64
  *
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
65
+ * A refusal carries every warning the run raised before it stopped, in the order a
66
+ * success would have returned them: a crash recovered in `prepare` changed the
63
67
  * disk, and the person has to hear that even when the run then fails.
64
68
  *
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
69
+ * A failed run removes the directories it created to reach `generateAt` — and only
70
+ * those, and only while empty — so a first run that fails leaves no empty
67
71
  * `generateAt` behind.
68
72
  */
69
73
  /** The four stages a write passes through, in order. */
@@ -105,22 +109,25 @@ export type OutputFileSystem = Pick<typeof fsPromises, "lstat" | "mkdir" | "open
105
109
  export interface ClientOutputWriteInput {
106
110
  /** What `emitClientTree` returned: the tree, and the warnings it raised. */
107
111
  readonly emission: ClientEmission;
108
- /** The resolved `generateAt` (§15.2, architect 2026-10-04). Created when missing. */
112
+ /** The resolved `generateAt`. Created when missing. */
109
113
  readonly generateAt: AbsolutePath;
110
114
  /**
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
+ * Content in the entry files or `generated/` the generator did not produce, which
116
+ * the person running it confirmed may be overwritten or removed — the paths
117
+ * `findForeignOutputContent` listed. Anything foreign that is not listed here is
118
+ * refused.
115
119
  */
116
120
  readonly overrideForeign?: readonly string[];
117
- /** The `typescript` optional peer; the installed one by default (Q6). */
121
+ /**
122
+ * The `typescript` optional peer; by default the project's own, resolved from
123
+ * `generateAt`.
124
+ */
118
125
  readonly resolveTypeScript?: TypeScriptResolver;
119
126
  readonly fileSystem?: OutputFileSystem;
120
127
  }
121
128
  export interface ClientOutputWritten {
122
129
  readonly generateAt: AbsolutePath;
123
- /** How deeply the tree was validated before it replaced the output (Q6). */
130
+ /** How deeply the tree was validated before it replaced the output. */
124
131
  readonly checked: OutputCheck;
125
132
  /**
126
133
  * Everything the run has to say without failing, in order: the emission's own
@@ -131,32 +138,32 @@ export interface ClientOutputWritten {
131
138
  readonly warnings: readonly string[];
132
139
  }
133
140
  /**
134
- * Writes `emission.tree` as `<generateAt>/AvClient.ts` and `<generateAt>/generated/`.
141
+ * Writes `emission.tree` as `<generateAt>/AvClient.ts` and
142
+ * `<generateAt>/generated/`.
135
143
  *
136
144
  * @throws OutputWriteError when the write stops; the previous output is intact.
137
145
  */
138
146
  export declare function writeClientOutput(input: ClientOutputWriteInput): Promise<ClientOutputWritten>;
139
147
  /**
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.
148
+ * Lists what in the entry files and `<generateAt>/generated/` the generator did
149
+ * not produce — a file without the ownership line, a symbolic link, anything that
150
+ * is not a regular file — so the person running it can be asked before a
151
+ * generation overwrites or removes it. Reads only; looks at nothing else in
152
+ * `generateAt`. Empty when `generateAt` does not exist yet.
146
153
  *
147
154
  * It sees what the write will see once it has recovered a killed run, without
148
155
  * recovering it: a `generated/` the killed run moved aside with nothing in its
149
156
  * 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.
157
+ * (`generated/…`). So the question asked, and `--yes`, cover them too — and a run
158
+ * that stops here has still touched nothing.
152
159
  *
153
160
  * @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.
161
+ * not a directory or an entry file is not a file: no confirmation repairs that.
155
162
  */
156
163
  export declare function findForeignOutputContent(generateAt: AbsolutePath, fileSystem?: OutputFileSystem): Promise<readonly string[]>;
157
164
  /**
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").
165
+ * Whether the entry files and `generated/` in `generateAt` hold exactly `tree` —
166
+ * every file byte for byte, no other file, no owned entry of an earlier build, and
167
+ * nothing a killed run moved aside — so that writing `tree` would change nothing.
161
168
  */
162
169
  export declare function ownedOutputMatches(generateAt: AbsolutePath, tree: EmittedTree): Promise<boolean>;
@@ -2,24 +2,17 @@ import { randomBytes } from "node:crypto";
2
2
  import * as fsPromises from "node:fs/promises";
3
3
  import path from "node:path";
4
4
  import { carriesGeneratedOwnership, GENERATED_OWNERSHIP_HEAD_BYTES, } from "../emit/banner.emitter.js";
5
- import { CLIENT_ENTRY_FILE, GENERATED_DIRECTORY, } from "../emit/emitted-tree.interface.js";
6
- import { resolveInstalledTypeScript, validateOutputTree, } from "./output.validator.js";
7
- /** A write that stopped; the previous output is intact unless it says otherwise. */
5
+ import { CLIENT_ENTRY_FILES, GENERATED_DIRECTORY, LEGACY_CLIENT_ENTRY_FILES, } from "../emit/emitted-tree.interface.js";
6
+ import { projectTypeScriptResolver, validateOutputTree, } from "./output.validator.js";
8
7
  export class OutputWriteError extends Error {
9
8
  stage;
10
9
  name = "OutputWriteError";
11
- /**
12
- * What the run said before it stopped, in the order a success returns its
13
- * warnings: the emission's, the validator's, the writer's. Printed before the
14
- * refusal's sentence.
15
- */
16
10
  warnings;
17
11
  constructor(stage, message, options) {
18
12
  super(message, options);
19
13
  this.stage = stage;
20
14
  this.warnings = options?.warnings ?? [];
21
15
  }
22
- /** The same refusal, with `earlier` said before the warnings it already carries. */
23
16
  precededBy(earlier) {
24
17
  return new OutputWriteError(this.stage, this.message, {
25
18
  cause: this.cause,
@@ -27,18 +20,7 @@ export class OutputWriteError extends Error {
27
20
  });
28
21
  }
29
22
  }
30
- /**
31
- * What a run said before a defect stopped it, kept beside the defect rather than
32
- * on a wrapper: a defect propagates as the very error thrown, with its own stack,
33
- * so a caller matching it by identity or type still can. Keyed weakly, so it
34
- * lives exactly as long as the error does. A thrown primitive cannot be a key;
35
- * what was said before one is not kept.
36
- */
37
23
  const WARNINGS_BEFORE_DEFECT = new WeakMap();
38
- /**
39
- * Records `earlier` as said before `defect`, ahead of anything already recorded,
40
- * and returns `defect` unchanged so the caller rethrows the same error.
41
- */
42
24
  export function precedeDefect(defect, earlier) {
43
25
  if (earlier.length > 0 && typeof defect === "object" && defect !== null) {
44
26
  WARNINGS_BEFORE_DEFECT.set(defect, [
@@ -48,10 +30,6 @@ export function precedeDefect(defect, earlier) {
48
30
  }
49
31
  return defect;
50
32
  }
51
- /**
52
- * What the run said before `defect` stopped it, in the order a success returns
53
- * its warnings; empty when nothing was, or for an error no run threw.
54
- */
55
33
  export function warningsRaisedBeforeDefect(defect) {
56
34
  return typeof defect === "object" && defect !== null
57
35
  ? (WARNINGS_BEFORE_DEFECT.get(defect) ?? [])
@@ -60,23 +38,20 @@ export function warningsRaisedBeforeDefect(defect) {
60
38
  const NEXT_PREFIX = ".aventara-next-";
61
39
  const READY_PREFIX = ".aventara-ready-";
62
40
  const PREVIOUS_GENERATED = `.${GENERATED_DIRECTORY}.aventara-previous`;
63
- /**
64
- * Writes `emission.tree` as `<generateAt>/AvClient.ts` and `<generateAt>/generated/`.
65
- *
66
- * @throws OutputWriteError when the write stops; the previous output is intact.
67
- */
68
- export async function writeClientOutput(input) {
69
- const fs = input.fileSystem ?? fsPromises;
70
- const generateAt = path.resolve(input.generateAt);
71
- const paths = {
41
+ const PREVIOUS_ENTRIES = ".previous-entries";
42
+ function outputPathsOf(generateAt) {
43
+ return {
72
44
  generateAt,
73
45
  generated: path.join(generateAt, GENERATED_DIRECTORY),
74
- entry: path.join(generateAt, CLIENT_ENTRY_FILE),
75
46
  previous: path.join(generateAt, PREVIOUS_GENERATED),
76
47
  };
48
+ }
49
+ export async function writeClientOutput(input) {
50
+ const fs = input.fileSystem ?? fsPromises;
51
+ const generateAt = path.resolve(input.generateAt);
52
+ const paths = outputPathsOf(generateAt);
77
53
  const writerWarnings = [];
78
54
  let validationWarnings = [];
79
- /** The outermost directory this run created on the way to `generateAt`, if any. */
80
55
  let created;
81
56
  try {
82
57
  return await writeStagedOutput();
@@ -95,7 +70,6 @@ export async function writeClientOutput(input) {
95
70
  : precedeDefect(error, said);
96
71
  }
97
72
  async function writeStagedOutput() {
98
- // prepare — recover a killed run, then decide whether the two entries are ours.
99
73
  const state = await stage("prepare", paths, async () => {
100
74
  created = await ensureDirectory(fs, generateAt);
101
75
  writerWarnings.push(...(await recoverInterruptedRun(fs, paths)));
@@ -107,14 +81,11 @@ export async function writeClientOutput(input) {
107
81
  }
108
82
  return inspection;
109
83
  });
110
- // temp — the whole tree, AvClient.ts and generated/ together, staged in generateAt.
111
84
  const suffix = randomBytes(6).toString("hex");
112
85
  let staging = path.join(generateAt, `${NEXT_PREFIX}${suffix}`);
113
86
  let stagingExists = false;
114
87
  try {
115
88
  await stage("temp", paths, async () => {
116
- // `mkdir`, not `mkdtemp`: generated/ is moved out of this directory and
117
- // takes the mode any directory the user creates takes (`mkdtemp` makes 0700).
118
89
  await fs.mkdir(staging);
119
90
  stagingExists = true;
120
91
  for (const file of input.emission.tree) {
@@ -123,8 +94,7 @@ export async function writeClientOutput(input) {
123
94
  await fs.writeFile(target, file.bytes);
124
95
  }
125
96
  });
126
- // validate — Q6, over AvClient.ts and generated/ as one program.
127
- const validation = await stage("validate", paths, () => validateOutputTree(staging, input.emission.tree, input.resolveTypeScript ?? resolveInstalledTypeScript));
97
+ const validation = await stage("validate", paths, () => validateOutputTree(staging, input.emission, input.resolveTypeScript ?? projectTypeScriptResolver(generateAt)));
128
98
  if (validation.accepted) {
129
99
  validationWarnings = validation.warnings;
130
100
  }
@@ -133,7 +103,6 @@ export async function writeClientOutput(input) {
133
103
  "This is a defect in @aventara/client; please report it with these findings:\n" +
134
104
  validation.findings.map((finding) => ` ${finding}`).join("\n"));
135
105
  }
136
- // replace — the order and its rollbacks are the module doc's.
137
106
  await stage("replace", paths, async () => {
138
107
  const ready = path.join(generateAt, `${READY_PREFIX}${suffix}`);
139
108
  await fs.rename(staging, ready);
@@ -151,11 +120,28 @@ export async function writeClientOutput(input) {
151
120
  }
152
121
  throw error;
153
122
  }
123
+ const aside = path.join(staging, PREVIOUS_ENTRIES);
124
+ const movedOut = [];
125
+ const movedIn = [];
154
126
  try {
155
- await fs.rename(path.join(staging, CLIENT_ENTRY_FILE), paths.entry);
127
+ await fs.mkdir(aside);
128
+ for (const name of [...state.entries, ...state.legacy]) {
129
+ await fs.rename(path.join(generateAt, name), path.join(aside, name));
130
+ movedOut.push(name);
131
+ }
132
+ for (const name of CLIENT_ENTRY_FILES) {
133
+ await fs.rename(path.join(staging, name), path.join(generateAt, name));
134
+ movedIn.push(name);
135
+ }
156
136
  }
157
137
  catch (error) {
158
138
  await rollBack(paths, error, async () => {
139
+ for (const name of movedIn.reverse()) {
140
+ await fs.rename(path.join(generateAt, name), path.join(staging, name));
141
+ }
142
+ for (const name of movedOut.reverse()) {
143
+ await fs.rename(path.join(aside, name), path.join(generateAt, name));
144
+ }
159
145
  await fs.rename(paths.generated, path.join(staging, GENERATED_DIRECTORY));
160
146
  if (movedAside) {
161
147
  await fs.rename(paths.previous, paths.generated);
@@ -186,23 +172,14 @@ export async function writeClientOutput(input) {
186
172
  finally {
187
173
  if (stagingExists) {
188
174
  await fs.rm(staging, { recursive: true, force: true }).catch(() => {
189
- // The next run removes it by name; the error being thrown matters more.
190
175
  });
191
176
  }
192
177
  }
193
178
  }
194
179
  }
195
- /** The sentence's ending when the previous output was not changed. */
196
180
  function LEFT_AS_THEY_WERE(paths) {
197
- return `${CLIENT_ENTRY_FILE} and ${GENERATED_DIRECTORY}/ in ${paths.generateAt} were left exactly as they were.`;
181
+ return `${CLIENT_ENTRY_FILES.join(", ")} and ${GENERATED_DIRECTORY}/ in ${paths.generateAt} were left exactly as they were.`;
198
182
  }
199
- /**
200
- * Creates `generateAt` when missing; refuses when something other than a
201
- * directory is there.
202
- *
203
- * @returns the outermost directory it created — `generateAt` or an ancestor —
204
- * or `undefined` when `generateAt` already existed.
205
- */
206
183
  async function ensureDirectory(fs, generateAt) {
207
184
  const stats = await lstatOrUndefined(fs, generateAt);
208
185
  if (stats === undefined) {
@@ -213,12 +190,6 @@ async function ensureDirectory(fs, generateAt) {
213
190
  }
214
191
  return undefined;
215
192
  }
216
- /**
217
- * Removes, after a failed run, the directories it created: `generateAt` and each
218
- * parent up to `outermost`, innermost first. `rmdir` removes only an empty
219
- * directory, so nothing anyone else put there meanwhile is lost; the first one
220
- * that cannot be removed ends the walk, and is named in the warning returned.
221
- */
222
193
  async function removeCreatedDirectories(fs, generateAt, outermost) {
223
194
  for (let directory = generateAt;; directory = path.dirname(directory)) {
224
195
  try {
@@ -235,29 +206,23 @@ async function removeCreatedDirectories(fs, generateAt, outermost) {
235
206
  }
236
207
  }
237
208
  }
238
- /**
239
- * Leftovers of a run that was killed, recognised only by the names this writer
240
- * gives them:
241
- *
242
- * - a staging directory that never became ready was not validated: removed;
243
- * - a ready one whose `generated/` was already moved in but whose `AvClient.ts`
244
- * was not: its `AvClient.ts` is moved in, completing the new pair;
245
- * - any other ready one: removed;
246
- * - a previous `generated/` moved aside: put back when nothing replaced it,
247
- * removed when something did.
248
- */
249
209
  async function recoverInterruptedRun(fs, paths) {
250
210
  const warnings = [];
251
211
  for (const entry of (await fs.readdir(paths.generateAt)).sort()) {
252
212
  const leftover = path.join(paths.generateAt, entry);
253
213
  if (entry.startsWith(READY_PREFIX)) {
254
- const stagedEntry = path.join(leftover, CLIENT_ENTRY_FILE);
255
214
  const generatedMovedIn = (await lstatOrUndefined(fs, path.join(leftover, GENERATED_DIRECTORY))) === undefined;
256
- if (generatedMovedIn &&
257
- (await lstatOrUndefined(fs, stagedEntry)) !== undefined) {
258
- await fs.rename(stagedEntry, paths.entry);
259
- warnings.push(`an interrupted run had moved its generated/ in but not its AvClient.ts; ` +
260
- `the validated AvClient.ts was moved in at ${paths.entry} before generating.`);
215
+ const completed = [];
216
+ for (const name of generatedMovedIn ? CLIENT_ENTRY_FILES : []) {
217
+ const staged = path.join(leftover, name);
218
+ if ((await lstatOrUndefined(fs, staged)) !== undefined) {
219
+ await fs.rename(staged, path.join(paths.generateAt, name));
220
+ completed.push(name);
221
+ }
222
+ }
223
+ if (completed.length > 0) {
224
+ warnings.push(`an interrupted run had moved its generated/ in but not all of its entry files; ` +
225
+ `the validated ${completed.join(", ")} ${completed.length === 1 ? "was" : "were"} moved in at ${paths.generateAt} before generating.`);
261
226
  }
262
227
  await fs.rm(leftover, { recursive: true, force: true });
263
228
  }
@@ -277,35 +242,12 @@ async function recoverInterruptedRun(fs, paths) {
277
242
  }
278
243
  return warnings;
279
244
  }
280
- /**
281
- * Where the `generated/` a run works with comes from once a killed run is
282
- * recovered: the previous one moved aside when nothing replaced it — recovery
283
- * puts it back — and `generated/` otherwise. One answer for the recovery and for
284
- * the read-only look ahead of it, so the two cannot disagree.
285
- */
286
245
  async function generatedAfterRecovery(fs, paths) {
287
246
  return (await lstatOrUndefined(fs, paths.generated)) === undefined &&
288
247
  (await lstatOrUndefined(fs, paths.previous)) !== undefined
289
248
  ? paths.previous
290
249
  : paths.generated;
291
250
  }
292
- /**
293
- * Lists what in `<generateAt>/AvClient.ts` and `<generateAt>/generated/` the
294
- * generator did not produce — a file without the ownership line, a symbolic link,
295
- * anything that is not a regular file — so the person running it can be asked
296
- * before a generation overwrites or removes it (architect, 2026-10-04). Reads
297
- * only; looks at nothing else in `generateAt`. Empty when `generateAt` does not
298
- * exist yet.
299
- *
300
- * It sees what the write will see once it has recovered a killed run, without
301
- * recovering it: a `generated/` the killed run moved aside with nothing in its
302
- * place is read where it lies and its files named where recovery puts them back
303
- * (`generated/…`). So the question asked, and `--yes`, cover them too — and a
304
- * run that stops here has still touched nothing.
305
- *
306
- * @throws OutputWriteError when `generateAt` is not a directory, `generated` is
307
- * not a directory or `AvClient.ts` is not a file: no confirmation repairs that.
308
- */
309
251
  export async function findForeignOutputContent(generateAt, fileSystem = fsPromises) {
310
252
  const root = path.resolve(generateAt);
311
253
  const stats = await lstatOrUndefined(fileSystem, root);
@@ -315,21 +257,10 @@ export async function findForeignOutputContent(generateAt, fileSystem = fsPromis
315
257
  if (!stats.isDirectory()) {
316
258
  throw notADirectory(root);
317
259
  }
318
- const paths = {
319
- generateAt: root,
320
- generated: path.join(root, GENERATED_DIRECTORY),
321
- entry: path.join(root, CLIENT_ENTRY_FILE),
322
- previous: path.join(root, PREVIOUS_GENERATED),
323
- };
260
+ const paths = outputPathsOf(root);
324
261
  const inspection = await inspectOwnedEntries(fileSystem, paths, await generatedAfterRecovery(fileSystem, paths));
325
262
  return inspection.foreign;
326
263
  }
327
- /**
328
- * What stands at the two owned entries. `generatedSource` is the directory read
329
- * as `generated/` — `generated/` itself, or the previous one a killed run moved
330
- * aside, which recovery will put back there — and its files are named as
331
- * `generated/…` either way.
332
- */
333
264
  async function inspectOwnedEntries(fs, paths, generatedSource = paths.generated) {
334
265
  const foreign = [];
335
266
  const relative = (file) => path.relative(paths.generateAt, file).split(path.sep).join("/");
@@ -353,23 +284,37 @@ async function inspectOwnedEntries(fs, paths, generatedSource = paths.generated)
353
284
  }
354
285
  }
355
286
  }
356
- const entryStats = await lstatOrUndefined(fs, paths.entry);
357
- if (entryStats !== undefined) {
358
- if (!entryStats.isFile()) {
359
- throw new OutputWriteError("prepare", `refusing to replace ${paths.entry}: it is not a file, and the generator owns ${CLIENT_ENTRY_FILE} ` +
287
+ const entries = [];
288
+ for (const name of CLIENT_ENTRY_FILES) {
289
+ const file = path.join(paths.generateAt, name);
290
+ const stats = await lstatOrUndefined(fs, file);
291
+ if (stats === undefined) {
292
+ continue;
293
+ }
294
+ if (!stats.isFile()) {
295
+ throw new OutputWriteError("prepare", `refusing to replace ${file}: it is not a file, and the generator owns ${name} ` +
360
296
  "as one; move it, or choose another `generateAt`. Nothing was written.");
361
297
  }
362
- if (!(await carriesOwnershipLine(fs, paths.entry))) {
363
- foreign.push(relative(paths.entry));
298
+ entries.push(name);
299
+ if (!(await carriesOwnershipLine(fs, file))) {
300
+ foreign.push(relative(file));
301
+ }
302
+ }
303
+ const legacy = [];
304
+ for (const name of LEGACY_CLIENT_ENTRY_FILES) {
305
+ const file = path.join(paths.generateAt, name);
306
+ if ((await lstatOrUndefined(fs, file))?.isFile() === true &&
307
+ (await carriesOwnershipLine(fs, file))) {
308
+ legacy.push(name);
364
309
  }
365
310
  }
366
311
  return {
367
312
  generated: generatedStats === undefined ? "absent" : "present",
368
- entry: entryStats === undefined ? "absent" : "present",
313
+ entries,
314
+ legacy,
369
315
  foreign: foreign.sort(),
370
316
  };
371
317
  }
372
- /** Foreign content nobody confirmed may be overwritten. */
373
318
  function foreignContentRefusal(paths, foreign) {
374
319
  const them = foreign.length === 1 ? "it" : "them";
375
320
  return new OutputWriteError("prepare", `refusing to generate into ${paths.generateAt}: ${foreign.join(", ")} ${foreign.length === 1 ? "was" : "were"} ` +
@@ -391,10 +336,6 @@ async function carriesOwnershipLine(fs, file) {
391
336
  await handle.close();
392
337
  }
393
338
  }
394
- /**
395
- * Undoes a replace that stopped part-way. When the undo itself fails, the
396
- * sentence says where the previous `generated/` is: the next run puts it back.
397
- */
398
339
  async function rollBack(paths, cause, undo) {
399
340
  try {
400
341
  await undo();
@@ -405,11 +346,6 @@ async function rollBack(paths, cause, undo) {
405
346
  `${GENERATED_DIRECTORY}/ is intact at ${paths.previous}; the next run puts it back.`, { cause });
406
347
  }
407
348
  }
408
- /**
409
- * Runs one stage, turning a filesystem error into that stage's sentence. An
410
- * `OutputWriteError` passes through; anything without a `code` is a defect and
411
- * keeps its stack.
412
- */
413
349
  async function stage(name, paths, run) {
414
350
  try {
415
351
  return await run();
@@ -447,16 +383,18 @@ function describe(error) {
447
383
  ? `${error.code}: ${error.message}`
448
384
  : String(error);
449
385
  }
450
- /**
451
- * Whether `AvClient.ts` and `generated/` in `generateAt` hold exactly `tree` —
452
- * every file byte for byte, no other file, and nothing a killed run moved aside —
453
- * so that writing `tree` would change nothing (Phase 12-rest Q6: "up to date").
454
- */
455
386
  export async function ownedOutputMatches(generateAt, tree) {
456
387
  const root = path.resolve(generateAt);
457
388
  if ((await lstatOrUndefined(fsPromises, path.join(root, PREVIOUS_GENERATED))) !== undefined) {
458
389
  return false;
459
390
  }
391
+ for (const name of LEGACY_CLIENT_ENTRY_FILES) {
392
+ const file = path.join(root, name);
393
+ if ((await lstatOrUndefined(fsPromises, file))?.isFile() === true &&
394
+ (await carriesOwnershipLine(fsPromises, file))) {
395
+ return false;
396
+ }
397
+ }
460
398
  let present;
461
399
  try {
462
400
  present = (await fsPromises.readdir(path.join(root, GENERATED_DIRECTORY), {
@@ -476,7 +414,7 @@ export async function ownedOutputMatches(generateAt, tree) {
476
414
  throw error;
477
415
  }
478
416
  const expected = new Set(tree.map((file) => file.path));
479
- if (present.length + 1 !== expected.size ||
417
+ if (present.length + CLIENT_ENTRY_FILES.length !== expected.size ||
480
418
  present.some((file) => !expected.has(file))) {
481
419
  return false;
482
420
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aventara/client",
3
- "version": "0.1.0-pilot.1",
3
+ "version": "0.1.0-pilot.3",
4
4
  "license": "SEE LICENSE IN LICENSE",
5
5
  "description": "Development-time generator for Aventara typed remote clients.",
6
6
  "type": "module",
@@ -23,7 +23,9 @@
23
23
  "LICENSE-ADDITIONAL-PERMISSION.md"
24
24
  ],
25
25
  "dependencies": {
26
- "@aventara/core": "0.1.0-pilot.1"
26
+ "@aventara/core": "0.1.0-pilot.3",
27
+ "c12": "3.3.4",
28
+ "get-tsconfig": "4.10.0"
27
29
  },
28
30
  "peerDependencies": {
29
31
  "typescript": ">=5.5.0"
@@ -37,12 +39,12 @@
37
39
  "access": "public"
38
40
  },
39
41
  "devDependencies": {
40
- "@aventara/testing": "0.1.0-pilot.1",
42
+ "@aventara/testing": "0.1.0-pilot.3",
41
43
  "@types/node": "24.10.1",
42
44
  "typescript": "^5.9.2"
43
45
  },
44
46
  "scripts": {
45
- "build": "tsc -p tsconfig.json",
47
+ "build": "tsc -p tsconfig.json --removeComments --declaration false && tsc -p tsconfig.json --emitDeclarationOnly && node ../../scripts/build/declaration-comments.sanitizer.ts dist",
46
48
  "typecheck": "tsc -p tsconfig.typecheck.json --noEmit",
47
49
  "test": "vitest run --typecheck --config ../../vitest.config.mts --root .",
48
50
  "lint": "biome lint src"