@fougere/compiler 0.11.0-alpha.0 → 0.12.1-alpha.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.
@@ -1,158 +1,7 @@
1
1
  import { refusalsIn } from './refusals.js';
2
- import { readFileSync, existsSync, statSync } from 'node:fs';
2
+ import { loadTS, getTS, checkedSourceOf, sourceOf, findDefaultClass, projectOf } from './TypeProgram.js';
3
+ import { existsSync } from 'node:fs';
3
4
  import { join, dirname, resolve as resolvePath } from 'node:path';
4
- /** Lazy-loaded TypeScript module — avoids bundling the 9MB compiler. */
5
- let _ts;
6
- async function loadTS() {
7
- if (!_ts)
8
- _ts = (await import('@typescript/typescript6')).default;
9
- return _ts;
10
- }
11
- function getTS() {
12
- if (!_ts)
13
- throw new Error('TypeScript not loaded — call an async parse function first');
14
- return _ts;
15
- }
16
- /** One checked program per project/configuration during a scan. */
17
- const typeProjects = new Map();
18
- const compilerProjects = new Map();
19
- /** What survives a run. */
20
- const sourceFiles = new Map();
21
- const retained = new Map();
22
- export function resetTypePrograms() {
23
- typeProjects.clear();
24
- compilerProjects.clear();
25
- }
26
- function keptHost(key, options) {
27
- const cached = retained.get(key);
28
- if (cached)
29
- return cached.host;
30
- const typescript = getTS();
31
- const base = typescript.createCompilerHost(options);
32
- const host = {
33
- ...base,
34
- getSourceFile(fileName, languageVersion, onError, shouldCreate) {
35
- const path = resolvePath(fileName);
36
- const mtime = statSync(path, { throwIfNoEntry: false })?.mtimeMs ?? -1;
37
- const cached = sourceFiles.get(path);
38
- if (cached && cached.mtime === mtime)
39
- return cached.file;
40
- const file = base.getSourceFile(fileName, languageVersion, onError, shouldCreate);
41
- if (file && mtime >= 0)
42
- sourceFiles.set(path, { mtime, file });
43
- return file;
44
- },
45
- };
46
- retained.set(key, { host, program: undefined });
47
- return host;
48
- }
49
- function builtProgram(key, roots, options) {
50
- const typescript = getTS();
51
- const host = keptHost(key, options);
52
- const program = typescript.createProgram({
53
- rootNames: [...roots], options, host, oldProgram: retained.get(key)?.program,
54
- });
55
- retained.set(key, { host, program });
56
- return program;
57
- }
58
- function compilerProjectOf(filePath, projectRoot) {
59
- const typescript = getTS();
60
- const absolute = resolvePath(filePath);
61
- const configPath = typescript.findConfigFile(dirname(absolute), typescript.sys.fileExists);
62
- if (configPath) {
63
- const key = `${configPath}:${projectRoot ?? ''}`;
64
- const cached = compilerProjects.get(key);
65
- if (cached)
66
- return cached;
67
- const read = typescript.readConfigFile(configPath, typescript.sys.readFile);
68
- if (read.error)
69
- throw new Error(typescript.flattenDiagnosticMessageText(read.error.messageText, '\n'));
70
- const parsed = typescript.parseJsonConfigFileContent(read.config, typescript.sys, dirname(configPath));
71
- const configured = {
72
- key,
73
- // Compiler options belong to the project; its entire include glob does not belong
74
- // to this scan. Each declaration inspected below becomes a root and TypeScript
75
- // follows its imports. Seeding the monorepo here made a one-file scan compile it all.
76
- roots: [],
77
- // Handlers may be authored or emitted as JavaScript. They still need to belong to
78
- // the checked program so constructor parsing does not fail on the first cold scan.
79
- options: { ...parsed.options, allowJs: true, noEmit: true },
80
- };
81
- compilerProjects.set(key, configured);
82
- return configured;
83
- }
84
- const key = projectRoot ?? dirname(absolute);
85
- const cached = compilerProjects.get(key);
86
- if (cached)
87
- return cached;
88
- const configured = {
89
- key,
90
- roots: [],
91
- options: {
92
- target: typescript.ScriptTarget.ES2022,
93
- module: typescript.ModuleKind.Node16,
94
- moduleResolution: typescript.ModuleResolutionKind.Node16,
95
- strict: true,
96
- skipLibCheck: true,
97
- allowJs: true,
98
- noEmit: true,
99
- },
100
- };
101
- compilerProjects.set(key, configured);
102
- return configured;
103
- }
104
- /** Declare every file a run will read, so one program covers it. */
105
- export async function seedTypeProgram(filePaths, projectRoot) {
106
- const typescript = await loadTS();
107
- const grouped = new Map();
108
- for (const filePath of filePaths) {
109
- const absolute = resolvePath(filePath);
110
- const typescript = getTS();
111
- const configured = compilerProjectOf(absolute, projectRoot);
112
- const group = grouped.get(configured.key) ?? { options: configured.options, paths: [] };
113
- group.paths.push(absolute);
114
- grouped.set(configured.key, group);
115
- }
116
- for (const [key, { options, paths }] of grouped) {
117
- const roots = new Set(typeProjects.get(key)?.roots ?? []);
118
- for (const path of paths)
119
- roots.add(path);
120
- typeProjects.set(key, { roots, options, program: builtProgram(key, [...roots], options) });
121
- }
122
- }
123
- /** The program a file belongs to, built once and widened as more files are asked for. */
124
- function projectOf(filePath, projectRoot) {
125
- const absolute = resolvePath(filePath);
126
- const configured = compilerProjectOf(absolute, projectRoot);
127
- let project = typeProjects.get(configured.key);
128
- if (!project) {
129
- // `path.resolve` is variadic, so handing it directly to `map` also passed the
130
- // index and the whole roots array as path segments. A fixture without a warm scan
131
- // cache exposed that first-run-only failure.
132
- const roots = new Set(configured.roots.map((root) => resolvePath(root)));
133
- roots.add(absolute);
134
- const program = builtProgram(configured.key, [...roots], configured.options);
135
- project = { roots, options: configured.options, program };
136
- typeProjects.set(configured.key, project);
137
- }
138
- else if (!project.roots.has(absolute)) {
139
- project.roots.add(absolute);
140
- project.program = builtProgram(configured.key, [...project.roots], project.options);
141
- }
142
- return { program: project.program, absolute };
143
- }
144
- function checkedSourceOf(filePath, projectRoot) {
145
- const { program, absolute } = projectOf(filePath, projectRoot);
146
- const source = program.getSourceFile(absolute);
147
- if (!source)
148
- throw new Error(`TypeScript did not include '${absolute}' in its program.`);
149
- return { source, checker: program.getTypeChecker() };
150
- }
151
- /** A file, opened. Five places read and parsed one, each spelling the same two calls. */
152
- function sourceOf(filePath) {
153
- const ts = getTS();
154
- return ts.createSourceFile(filePath, readFileSync(filePath, 'utf-8'), ts.ScriptTarget.Latest, true);
155
- }
156
5
  /** One declared parameter — a constructor's and a method's are read the same way. */
157
6
  function parsedParam(param, source, checker) {
158
7
  const ts = getTS();
@@ -171,61 +20,66 @@ const ANNOUNCED = new Set(['Fact', 'Pipe']);
171
20
  function parseTypeNode(node, source, checker) {
172
21
  const ts = getTS();
173
22
  const raw = node.getText(source);
174
- if (checker) {
175
- // `Fact<T>` and `Pipe<T>` are deliberately transparent in TypeScript (`= T`), because
176
- // both receive the payload itself — one reads it, the other answers the value every
177
- // reader then gets. The checker erases the marker, while the binding plan still needs
178
- // it to tell either from an ordinary body.
179
- if (ts.isTypeReferenceNode(node) && ts.isIdentifier(node.typeName) && ANNOUNCED.has(node.typeName.text)) {
180
- return {
181
- raw,
182
- name: node.typeName.text,
183
- generics: node.typeArguments?.map((arg) => parseTypeNode(arg, source, checker)) ?? [],
184
- };
185
- }
186
- return parseCheckedType(checker.getTypeFromTypeNode(node), raw, checker);
23
+ if (checker)
24
+ return announced(node, source, checker) ?? parseCheckedType(checker.getTypeFromTypeNode(node), raw, checker);
25
+ if (ts.isUnionTypeNode(node))
26
+ return fromUnion(node, source, raw);
27
+ if (ts.isTypeReferenceNode(node))
28
+ return fromReference(node, source, raw);
29
+ if (ts.isArrayTypeNode(node)) {
30
+ const inner = parseTypeNode(node.elementType, source);
31
+ return { ...inner, array: true, arrayDepth: (inner.arrayDepth ?? 0) + 1, raw };
187
32
  }
188
- // Union types — extract nullable, strip Promise
189
- if (ts.isUnionTypeNode(node)) {
190
- const nonNull = node.types.filter((t) => !(ts.isLiteralTypeNode(t) && t.literal.kind === ts.SyntaxKind.NullKeyword)
191
- && !(t.kind === ts.SyntaxKind.UndefinedKeyword)
192
- && !(t.kind === ts.SyntaxKind.VoidKeyword)
193
- && !(t.kind === ts.SyntaxKind.NullKeyword));
194
- const nullable = node.types.some((t) => (ts.isLiteralTypeNode(t) && t.literal.kind === ts.SyntaxKind.NullKeyword)
195
- || t.kind === ts.SyntaxKind.NullKeyword);
196
- const undefinable = node.types.some((t) => t.kind === ts.SyntaxKind.UndefinedKeyword || t.kind === ts.SyntaxKind.VoidKeyword);
197
- if (nonNull.length === 1) {
198
- const inner = parseTypeNode(nonNull[0], source);
199
- return { ...inner, nullable: nullable || inner.nullable, undefined: undefinable || inner.undefined, raw };
200
- }
201
- return { raw, name: raw, nullable, undefined: undefinable };
33
+ return fromKeyword(node, raw) ?? { raw, name: raw };
34
+ }
35
+ /**
36
+ * `Fact<T>` and `Pipe<T>` are deliberately transparent in TypeScript (`= T`), because both
37
+ * receive the payload itself — one reads it, the other answers the value every reader then gets.
38
+ * The checker erases the marker, while the binding plan still needs it to tell either from an
39
+ * ordinary body.
40
+ */
41
+ function announced(node, source, checker) {
42
+ const ts = getTS();
43
+ if (!ts.isTypeReferenceNode(node) || !ts.isIdentifier(node.typeName) || !ANNOUNCED.has(node.typeName.text)) {
44
+ return undefined;
202
45
  }
203
- // Promise<T> — unwrap
204
- if (ts.isTypeReferenceNode(node)) {
205
- const typeName = node.typeName.getText(source);
206
- if (typeName === 'Promise' && node.typeArguments?.length === 1) {
207
- const inner = parseTypeNode(node.typeArguments[0], source);
208
- return { ...inner, promise: true, raw };
209
- }
210
- // Array<T>
211
- if (typeName === 'Array' && node.typeArguments?.length === 1) {
212
- const inner = parseTypeNode(node.typeArguments[0], source);
213
- return { ...inner, array: true, arrayDepth: (inner.arrayDepth ?? 0) + 1, raw };
214
- }
215
- // Generic type: Foo<Bar, Baz>
216
- if (node.typeArguments && node.typeArguments.length > 0) {
217
- const generics = node.typeArguments.map((arg) => parseTypeNode(arg, source));
218
- return { raw, name: typeName, generics };
219
- }
220
- // Simple type reference: Post, string, etc.
221
- return { raw, name: typeName };
46
+ return {
47
+ raw: node.getText(source),
48
+ name: node.typeName.text,
49
+ generics: node.typeArguments?.map((arg) => parseTypeNode(arg, source, checker)) ?? [],
50
+ };
51
+ }
52
+ /** `T | null`, `T | undefined`, `T | void` — the absence is an axis, the rest is the type. */
53
+ function fromUnion(node, source, raw) {
54
+ const ts = getTS();
55
+ const isNull = (one) => (ts.isLiteralTypeNode(one) && one.literal.kind === ts.SyntaxKind.NullKeyword)
56
+ || one.kind === ts.SyntaxKind.NullKeyword;
57
+ const isAbsent = (one) => one.kind === ts.SyntaxKind.UndefinedKeyword || one.kind === ts.SyntaxKind.VoidKeyword;
58
+ const nullable = node.types.some(isNull);
59
+ const undefinable = node.types.some(isAbsent);
60
+ const nonNull = node.types.filter((one) => !isNull(one) && !isAbsent(one));
61
+ if (nonNull.length !== 1)
62
+ return { raw, name: raw, nullable, undefined: undefinable };
63
+ const inner = parseTypeNode(nonNull[0], source);
64
+ return { ...inner, nullable: nullable || inner.nullable, undefined: undefinable || inner.undefined, raw };
65
+ }
66
+ /** `Promise<T>` and `Array<T>` are carriers; anything else keeps its name and its generics. */
67
+ function fromReference(node, source, raw) {
68
+ const typeName = node.typeName.getText(source);
69
+ if (typeName === 'Promise' && node.typeArguments?.length === 1) {
70
+ return { ...parseTypeNode(node.typeArguments[0], source), promise: true, raw };
222
71
  }
223
- // T[]
224
- if (ts.isArrayTypeNode(node)) {
225
- const inner = parseTypeNode(node.elementType, source);
72
+ if (typeName === 'Array' && node.typeArguments?.length === 1) {
73
+ const inner = parseTypeNode(node.typeArguments[0], source);
226
74
  return { ...inner, array: true, arrayDepth: (inner.arrayDepth ?? 0) + 1, raw };
227
75
  }
228
- // Keyword types: string, number, boolean, void
76
+ if (node.typeArguments && node.typeArguments.length > 0) {
77
+ return { raw, name: typeName, generics: node.typeArguments.map((arg) => parseTypeNode(arg, source)) };
78
+ }
79
+ return { raw, name: typeName };
80
+ }
81
+ function fromKeyword(node, raw) {
82
+ const ts = getTS();
229
83
  switch (node.kind) {
230
84
  case ts.SyntaxKind.StringKeyword: return { raw, name: 'string' };
231
85
  case ts.SyntaxKind.NumberKeyword: return { raw, name: 'number' };
@@ -235,13 +89,8 @@ function parseTypeNode(node, source, checker) {
235
89
  case ts.SyntaxKind.NullKeyword: return { raw, name: 'null', nullable: true };
236
90
  case ts.SyntaxKind.AnyKeyword: return { raw, name: 'any' };
237
91
  case ts.SyntaxKind.UnknownKeyword: return { raw, name: 'unknown' };
92
+ default: return undefined;
238
93
  }
239
- // Object literal type: { title: string; limit: number }
240
- if (ts.isTypeLiteralNode(node)) {
241
- return { raw, name: raw };
242
- }
243
- // Fallback
244
- return { raw, name: raw };
245
94
  }
246
95
  function meaningfulSymbolName(type, checker) {
247
96
  const typescript = getTS();
@@ -262,34 +111,10 @@ function parseCheckedType(type, raw, checker, depth = 0) {
262
111
  const typescript = getTS();
263
112
  if (depth > 12)
264
113
  return { raw, name: checker.typeToString(type) };
265
- if (type.isUnion()) {
266
- const nullable = type.types.some((member) => (member.flags & typescript.TypeFlags.Null) !== 0);
267
- const undefinable = type.types.some((member) => (member.flags & (typescript.TypeFlags.Undefined | typescript.TypeFlags.Void)) !== 0);
268
- const members = type.types.filter((member) => (member.flags & (typescript.TypeFlags.Null | typescript.TypeFlags.Undefined | typescript.TypeFlags.Void)) === 0);
269
- // The checker represents the `boolean` keyword itself as `false | true`. Preserve
270
- // the primitive vocabulary consumed by binding and presenter metadata.
271
- if (members.length > 0
272
- && members.every((member) => (member.flags & typescript.TypeFlags.BooleanLiteral) !== 0)) {
273
- return { raw, name: 'boolean', nullable, undefined: undefinable };
274
- }
275
- if (members.length === 1) {
276
- const inner = parseCheckedType(members[0], raw, checker, depth + 1);
277
- return {
278
- ...inner,
279
- raw,
280
- nullable: nullable || inner.nullable,
281
- undefined: undefinable || inner.undefined,
282
- };
283
- }
284
- return {
285
- raw,
286
- name: members.map((member) => meaningfulSymbolName(member, checker)).join(' | ') || raw,
287
- nullable,
288
- undefined: undefinable,
289
- };
290
- }
291
- // Present at runtime in the supported compiler versions, but intentionally omitted
292
- // from TypeScript's public TypeChecker declaration.
114
+ if (type.isUnion())
115
+ return fromCheckedUnion(type, raw, checker, depth);
116
+ // Present at runtime in the supported compiler versions, but intentionally omitted from
117
+ // TypeScript's public TypeChecker declaration.
293
118
  const promised = checker.getPromisedTypeOfPromise(type);
294
119
  if (promised)
295
120
  return { ...parseCheckedType(promised, raw, checker, depth + 1), raw, promise: true };
@@ -298,33 +123,149 @@ function parseCheckedType(type, raw, checker, depth = 0) {
298
123
  const inner = element ? parseCheckedType(element, raw, checker, depth + 1) : { raw, name: 'unknown' };
299
124
  return { ...inner, raw, array: true, arrayDepth: (inner.arrayDepth ?? 0) + 1 };
300
125
  }
301
- if ((type.flags & typescript.TypeFlags.StringLike) !== 0)
126
+ const primitive = fromCheckedFlags(type.flags, raw);
127
+ if (primitive)
128
+ return primitive;
129
+ // A declared alias such as `Emit<PostPublished>` may reduce to a function or object type, so it
130
+ // is no longer a TypeReference. The checker keeps its arguments beside `aliasSymbol`; losing
131
+ // them turns DI keys into bare `Emit`/`Facade`.
132
+ const args = type.aliasTypeArguments
133
+ ?? checker.getTypeArguments(type);
134
+ return {
135
+ raw,
136
+ name: meaningfulSymbolName(type, checker),
137
+ ...(args.length && { generics: args.map((arg) => parseCheckedType(arg, checker.typeToString(arg), checker, depth + 1)) }),
138
+ };
139
+ }
140
+ /**
141
+ * What an operation ANSWERS is read here, and the question asked of it is whether it is data.
142
+ *
143
+ * Data is what JSON keeps, so every node is asked one thing: can something in it be CALLED? A
144
+ * `Date` carries `toString`, a `Map` carries `clear`, an Effect carries `pipe`. Measured
145
+ * 2026-09-18 over the facade's own exit: `{ at: new Date(0) }` reaches a local caller as a `Date`
146
+ * and a remote one as a string, and an object with methods loses them on both sides, differently.
147
+ *
148
+ * It answers the PATH and the type sitting there — `at: Date` reads as the declaration it came
149
+ * from, where the first callable member (`toString`) names nothing an author wrote. `any` and
150
+ * `unknown` are left alone: nothing was declared to judge.
151
+ *
152
+ * Documented: [handlers](https://fougere.dev/docs/business/handlers).
153
+ */
154
+ function notData(type, path, checker, seen = new Set()) {
155
+ const typescript = getTS();
156
+ if (seen.has(type))
157
+ return undefined;
158
+ const judged = typescript.TypeFlags.Any | typescript.TypeFlags.Unknown | typescript.TypeFlags.Never
159
+ | typescript.TypeFlags.StringLike | typescript.TypeFlags.NumberLike | typescript.TypeFlags.BooleanLike
160
+ | typescript.TypeFlags.BigIntLike | typescript.TypeFlags.Null | typescript.TypeFlags.Undefined
161
+ | typescript.TypeFlags.Void;
162
+ if (type.flags & judged)
163
+ return undefined;
164
+ if (type.isUnion() || type.isIntersection())
165
+ return firstRefused(type.types, (member) => notData(member, path, checker, seen));
166
+ if (type.getCallSignatures().length > 0)
167
+ return `${path}: ${checker.typeToString(type)}`;
168
+ if (declaresShape(type))
169
+ return undefined;
170
+ seen.add(type);
171
+ const items = itemsOf(type, checker);
172
+ if (items)
173
+ return firstRefused(items, (item) => notData(item, `${path}[]`, checker, seen));
174
+ return firstRefused(checker.getPropertiesOfType(type), (property) => {
175
+ const held = checker.getTypeOfSymbol(property);
176
+ // A symbol key never survives JSON either, and it is how an iterable declares itself.
177
+ if (property.escapedName.toString().startsWith('__@') || held.getCallSignatures().length > 0) {
178
+ return `${path}: ${checker.typeToString(type)}`;
179
+ }
180
+ return notData(held, `${path}.${property.name}`, checker, seen);
181
+ });
182
+ }
183
+ /**
184
+ * A class the schema declares — `extends entity({…})`, or a projection of one.
185
+ *
186
+ * Read from the HERITAGE rather than from what `resolveSchema` resolved: a frond answering with
187
+ * an entity its neighbour declares (a `Pipe<PostPublished>` finishing a fact) has no schema in
188
+ * its own module exports, so the op carries no output and would be judged as if nothing
189
+ * converted it. Its fields do convert it, wherever the class was written.
190
+ */
191
+ function declaresShape(type) {
192
+ const typescript = getTS();
193
+ return (type.getSymbol()?.declarations ?? []).some((declaration) => typescript.isClassDeclaration(declaration)
194
+ && (declaration.heritageClauses ?? []).some((clause) => /entity\(|\.(pick|omit|partial|extend)\(/.test(clause.getText())));
195
+ }
196
+ /**
197
+ * What a type holds if it is read as a list — its elements, or nothing.
198
+ *
199
+ * By its numeric INDEX and not by `isArrayType`: `ListResult<T> extends Array<T>` is a subtype,
200
+ * so the array test says no and the walk reached `push` and `map` instead of the rows. What rides
201
+ * beside the rows (`total`, `hasMore`) is data and is judged with everything else.
202
+ */
203
+ function itemsOf(type, checker) {
204
+ const typescript = getTS();
205
+ if (checker.isArrayType(type) || checker.isTupleType(type)) {
206
+ return checker.getTypeArguments(type);
207
+ }
208
+ const numeric = checker.getIndexTypeOfType(type, typescript.IndexKind.Number);
209
+ return numeric ? [numeric] : undefined;
210
+ }
211
+ function firstRefused(members, read) {
212
+ for (const member of members) {
213
+ const refused = read(member);
214
+ if (refused)
215
+ return refused;
216
+ }
217
+ return undefined;
218
+ }
219
+ /** The type an operation hands back, past the promise every dispatch awaits. */
220
+ function answeredBy(signature, checker) {
221
+ if (!signature)
222
+ return undefined;
223
+ const returned = signature.getReturnType();
224
+ return checker.getAwaitedType(returned) ?? returned;
225
+ }
226
+ /** A union carries an absence on the side; what remains is the type. */
227
+ function fromCheckedUnion(type, raw, checker, depth) {
228
+ const typescript = getTS();
229
+ const nullable = type.types.some((member) => (member.flags & typescript.TypeFlags.Null) !== 0);
230
+ const undefinable = type.types.some((member) => (member.flags & (typescript.TypeFlags.Undefined | typescript.TypeFlags.Void)) !== 0);
231
+ const members = type.types.filter((member) => (member.flags & (typescript.TypeFlags.Null | typescript.TypeFlags.Undefined | typescript.TypeFlags.Void)) === 0);
232
+ // The checker represents the `boolean` keyword itself as `false | true`. Preserve the
233
+ // primitive vocabulary consumed by binding and presenter metadata.
234
+ const allBooleans = members.length > 0
235
+ && members.every((member) => (member.flags & typescript.TypeFlags.BooleanLiteral) !== 0);
236
+ if (allBooleans)
237
+ return { raw, name: 'boolean', nullable, undefined: undefinable };
238
+ if (members.length === 1) {
239
+ const inner = parseCheckedType(members[0], raw, checker, depth + 1);
240
+ return { ...inner, raw, nullable: nullable || inner.nullable, undefined: undefinable || inner.undefined };
241
+ }
242
+ return {
243
+ raw,
244
+ name: members.map((member) => meaningfulSymbolName(member, checker)).join(' | ') || raw,
245
+ nullable,
246
+ undefined: undefinable,
247
+ };
248
+ }
249
+ /** The keywords, read off the flags the checker already carries. */
250
+ function fromCheckedFlags(flags, raw) {
251
+ const typescript = getTS();
252
+ if ((flags & typescript.TypeFlags.StringLike) !== 0)
302
253
  return { raw, name: 'string' };
303
- if ((type.flags & typescript.TypeFlags.NumberLike) !== 0)
254
+ if ((flags & typescript.TypeFlags.NumberLike) !== 0)
304
255
  return { raw, name: 'number' };
305
- if ((type.flags & typescript.TypeFlags.BooleanLike) !== 0)
256
+ if ((flags & typescript.TypeFlags.BooleanLike) !== 0)
306
257
  return { raw, name: 'boolean' };
307
- if ((type.flags & typescript.TypeFlags.Void) !== 0)
258
+ if ((flags & typescript.TypeFlags.Void) !== 0)
308
259
  return { raw, name: 'void', undefined: true };
309
- if ((type.flags & typescript.TypeFlags.Undefined) !== 0)
260
+ if ((flags & typescript.TypeFlags.Undefined) !== 0)
310
261
  return { raw, name: 'undefined', undefined: true };
311
- if ((type.flags & typescript.TypeFlags.Null) !== 0)
262
+ if ((flags & typescript.TypeFlags.Null) !== 0)
312
263
  return { raw, name: 'null', nullable: true };
313
- if ((type.flags & typescript.TypeFlags.Any) !== 0)
264
+ if ((flags & typescript.TypeFlags.Any) !== 0)
314
265
  return { raw, name: 'any' };
315
- if ((type.flags & typescript.TypeFlags.Unknown) !== 0)
266
+ if ((flags & typescript.TypeFlags.Unknown) !== 0)
316
267
  return { raw, name: 'unknown' };
317
- const reference = type;
318
- // A declared alias such as `Emit<PostPublished>` may reduce to a function or object
319
- // type, so it is no longer a TypeReference. The checker keeps its arguments beside
320
- // `aliasSymbol`; losing them turns DI keys into bare `Emit`/`Facade`.
321
- const args = type.aliasTypeArguments
322
- ?? checker.getTypeArguments(reference);
323
- return {
324
- raw,
325
- name: meaningfulSymbolName(type, checker),
326
- ...(args.length && { generics: args.map((arg) => parseCheckedType(arg, checker.typeToString(arg), checker, depth + 1)) }),
327
- };
268
+ return undefined;
328
269
  }
329
270
  // ── Module resolution ────────────────────────
330
271
  const TS_KEYWORDS = new Set([
@@ -425,9 +366,18 @@ function extractClassMethods(cls, source, skip, checker) {
425
366
  if (skip.has(name))
426
367
  continue;
427
368
  const params = member.parameters.map((p) => parsedParam(p, source, checker));
428
- const returnType = member.type ? parseTypeNode(member.type, source, checker) : undefined;
369
+ // An op that annotates nothing still HAS a return type, and the checker holds it — so it
370
+ // carries an output on the card, a cardinality, and the codecs a caller reads it back
371
+ // through. Measured 2026-09-18: 18 of the 42 unannotated ops here answer a declared entity,
372
+ // and every one of them handed a caller the encoded row its type called a `Date`.
373
+ const answered = checker ? answeredBy(checker.getSignatureFromDeclaration(member), checker) : undefined;
374
+ const returnType = member.type ? parseTypeNode(member.type, source, checker)
375
+ : answered && checker ? parseCheckedType(answered, checker.typeToString(answered), checker)
376
+ : undefined;
377
+ const refused = answered && checker ? notData(answered, name, checker) : undefined;
429
378
  results.push({
430
379
  name, params, returnType,
380
+ ...(refused ? { notData: refused } : {}),
431
381
  description: docSentenceOf(member, source),
432
382
  });
433
383
  }
@@ -481,6 +431,7 @@ function inheritedFromBase(base, checker, skip) {
481
431
  if (!signature)
482
432
  continue;
483
433
  const returned = signature.getReturnType();
434
+ const refusedOutput = notData(checker.getAwaitedType(returned) ?? returned, property.name, checker);
484
435
  const sentence = typescript.displayPartsToString(property.getDocumentationComment(checker)).trim();
485
436
  results.push({
486
437
  name: property.name,
@@ -493,6 +444,7 @@ function inheritedFromBase(base, checker, skip) {
493
444
  };
494
445
  }),
495
446
  returnType: parseCheckedType(returned, checker.typeToString(returned), checker),
447
+ ...(refusedOutput ? { notData: refusedOutput } : {}),
496
448
  ...(sentence ? { description: sentence.split(/(?<=\.)\s/)[0] } : {}),
497
449
  });
498
450
  }
@@ -538,28 +490,6 @@ unresolved) {
538
490
  }
539
491
  return [];
540
492
  }
541
- // ── Class finding ────────────────────────────
542
- /** Find the default exported class in a source file. */
543
- function findDefaultClass(source) {
544
- const ts = getTS();
545
- for (const stmt of source.statements) {
546
- // export default class Foo { ... }
547
- if (ts.isClassDeclaration(stmt) && stmt.modifiers?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword) && stmt.modifiers?.some((m) => m.kind === ts.SyntaxKind.DefaultKeyword)) {
548
- return stmt;
549
- }
550
- }
551
- // export default Foo (separate statement) — find the class it points to
552
- for (const stmt of source.statements) {
553
- if (ts.isExportAssignment(stmt) && !stmt.isExportEquals && ts.isIdentifier(stmt.expression)) {
554
- const name = stmt.expression.text;
555
- for (const s of source.statements) {
556
- if (ts.isClassDeclaration(s) && s.name?.text === name)
557
- return s;
558
- }
559
- }
560
- }
561
- return undefined;
562
- }
563
493
  /**
564
494
  * Every method a handler declares, its inherited ones included — the raw material of a
565
495
  * binding plan. With a `projectRoot`, the heritage clause is followed too.