@ontrails/trails 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (121) hide show
  1. package/CHANGELOG.md +1906 -0
  2. package/README.md +48 -0
  3. package/bin/trails.ts +3 -0
  4. package/package.json +57 -0
  5. package/src/app.ts +167 -0
  6. package/src/clack.ts +111 -0
  7. package/src/cli.ts +308 -0
  8. package/src/completions.ts +431 -0
  9. package/src/lifecycle-source-io.ts +33 -0
  10. package/src/load-app-mirror.ts +202 -0
  11. package/src/local-state-io.ts +129 -0
  12. package/src/mcp-app.ts +42 -0
  13. package/src/mcp-options.ts +92 -0
  14. package/src/mcp.ts +8 -0
  15. package/src/project-writes.ts +377 -0
  16. package/src/regrade/audit.ts +571 -0
  17. package/src/regrade/config.ts +152 -0
  18. package/src/regrade/history.ts +636 -0
  19. package/src/regrade/lifecycle.ts +76 -0
  20. package/src/regrade/live-api-preserve.ts +123 -0
  21. package/src/regrade/plan-artifact.ts +515 -0
  22. package/src/regrade/plan-derivation.ts +301 -0
  23. package/src/regrade/prepared-run.ts +259 -0
  24. package/src/regrade/receipt-history.ts +446 -0
  25. package/src/regrade/source-transaction.ts +185 -0
  26. package/src/release/bindings.ts +58 -0
  27. package/src/release/changeset-packages.ts +99 -0
  28. package/src/release/check.ts +1191 -0
  29. package/src/release/cli-bundle.ts +575 -0
  30. package/src/release/config.ts +73 -0
  31. package/src/release/contract-facts.ts +425 -0
  32. package/src/release/homebrew.ts +221 -0
  33. package/src/release/index.ts +180 -0
  34. package/src/release/lock-roundtrip-smoke.ts +255 -0
  35. package/src/release/lock-roundtrip-workspace.ts +107 -0
  36. package/src/release/native-bun-publish.ts +964 -0
  37. package/src/release/native-bun-registry.ts +848 -0
  38. package/src/release/notes-cli.ts +171 -0
  39. package/src/release/notes.ts +390 -0
  40. package/src/release/pack-coherence.ts +455 -0
  41. package/src/release/package-route-facts.ts +146 -0
  42. package/src/release/packed-artifacts-smoke.ts +236 -0
  43. package/src/release/policy.ts +1780 -0
  44. package/src/release/semver.ts +104 -0
  45. package/src/release/smoke.ts +56 -0
  46. package/src/release/stable-version-release.ts +80 -0
  47. package/src/release/wayfinder-dogfood-smoke.ts +762 -0
  48. package/src/release/zero-line-transition.ts +68 -0
  49. package/src/retired-topo-command.ts +36 -0
  50. package/src/run-adapter-check.ts +76 -0
  51. package/src/run-argv.ts +133 -0
  52. package/src/run-collision.ts +126 -0
  53. package/src/run-completions-install.ts +179 -0
  54. package/src/run-example.ts +149 -0
  55. package/src/run-examples.ts +148 -0
  56. package/src/run-quiet.ts +75 -0
  57. package/src/run-regrade-progress.ts +47 -0
  58. package/src/run-release-check.ts +74 -0
  59. package/src/run-schema.ts +74 -0
  60. package/src/run-trace.ts +273 -0
  61. package/src/run-warden.ts +39 -0
  62. package/src/run-watch-project.ts +52 -0
  63. package/src/run-watch.ts +381 -0
  64. package/src/run-wayfind-outline.ts +170 -0
  65. package/src/scaffold-version-sync.ts +183 -0
  66. package/src/scaffold-versions.generated.ts +12 -0
  67. package/src/trails/adapter-check.ts +244 -0
  68. package/src/trails/add-surface.ts +816 -0
  69. package/src/trails/add-trail.ts +141 -0
  70. package/src/trails/add-verify.ts +252 -0
  71. package/src/trails/compile.ts +118 -0
  72. package/src/trails/completions-complete.ts +236 -0
  73. package/src/trails/completions.ts +47 -0
  74. package/src/trails/config-explain.ts +43 -0
  75. package/src/trails/create-adapter.ts +785 -0
  76. package/src/trails/create-scaffold.ts +1215 -0
  77. package/src/trails/create-versions.ts +62 -0
  78. package/src/trails/create.ts +652 -0
  79. package/src/trails/deprecate.ts +59 -0
  80. package/src/trails/dev-clean.ts +80 -0
  81. package/src/trails/dev-reset.ts +48 -0
  82. package/src/trails/dev-stats.ts +71 -0
  83. package/src/trails/dev-support.ts +360 -0
  84. package/src/trails/doctor.ts +77 -0
  85. package/src/trails/draft-promote.ts +949 -0
  86. package/src/trails/guide.ts +106 -0
  87. package/src/trails/load-app.ts +1145 -0
  88. package/src/trails/operator-context.ts +66 -0
  89. package/src/trails/project-context-output.ts +304 -0
  90. package/src/trails/project-context.ts +613 -0
  91. package/src/trails/project.ts +65 -0
  92. package/src/trails/regrade.ts +4951 -0
  93. package/src/trails/release-check.ts +113 -0
  94. package/src/trails/release-smoke.ts +49 -0
  95. package/src/trails/revise.ts +53 -0
  96. package/src/trails/root-dir.ts +21 -0
  97. package/src/trails/run-example.ts +592 -0
  98. package/src/trails/run-examples.ts +149 -0
  99. package/src/trails/run.ts +496 -0
  100. package/src/trails/scaffold-json.ts +60 -0
  101. package/src/trails/scaffold-topo-identity.ts +479 -0
  102. package/src/trails/survey.ts +990 -0
  103. package/src/trails/topo-activation.ts +14 -0
  104. package/src/trails/topo-constants.ts +2 -0
  105. package/src/trails/topo-history.ts +47 -0
  106. package/src/trails/topo-output-schemas.ts +259 -0
  107. package/src/trails/topo-pin.ts +38 -0
  108. package/src/trails/topo-read-support.ts +368 -0
  109. package/src/trails/topo-reports.ts +809 -0
  110. package/src/trails/topo-store-support.ts +323 -0
  111. package/src/trails/topo-support.ts +247 -0
  112. package/src/trails/topo-unpin.ts +61 -0
  113. package/src/trails/topo.ts +92 -0
  114. package/src/trails/validate.ts +348 -0
  115. package/src/trails/version-lifecycle-support.ts +936 -0
  116. package/src/trails/warden-guide.ts +134 -0
  117. package/src/trails/warden.ts +598 -0
  118. package/src/trails/wayfind-diff.ts +716 -0
  119. package/src/trails/wayfind-outline.ts +876 -0
  120. package/src/trails/wayfind.ts +1319 -0
  121. package/src/versions.ts +31 -0
@@ -0,0 +1,479 @@
1
+ import {
2
+ collectScopeFrameBindings,
3
+ extractStringLiteral,
4
+ getNodeArguments,
5
+ getNodeCallee,
6
+ getNodeDeclaration,
7
+ getNodeDeclarations,
8
+ getNodeExpression,
9
+ identifierName,
10
+ parseWithDiagnostics,
11
+ propertyKeyName,
12
+ } from '@ontrails/source';
13
+ import type { AstNode } from '@ontrails/source';
14
+
15
+ type ExportKey = 'app' | 'default' | 'graph';
16
+
17
+ type ExportCandidate = AstNode | 'unknown';
18
+
19
+ type DerivedIdentity =
20
+ | { readonly kind: 'known'; readonly id: string }
21
+ | { readonly kind: 'nullish' }
22
+ | { readonly kind: 'unknown' };
23
+
24
+ const exportKeys = ['default', 'graph', 'app'] as const;
25
+
26
+ const unwrapExpression = (node: AstNode): AstNode => {
27
+ let current = node;
28
+ while (
29
+ current.type === 'ParenthesizedExpression' ||
30
+ current.type === 'TSAsExpression' ||
31
+ current.type === 'TSSatisfiesExpression'
32
+ ) {
33
+ const expression = getNodeExpression(current);
34
+ if (expression === undefined) {
35
+ break;
36
+ }
37
+ current = expression;
38
+ }
39
+ return current;
40
+ };
41
+
42
+ const importSource = (node: AstNode): string | null =>
43
+ extractStringLiteral(node['source'] as AstNode | undefined);
44
+
45
+ const exportedName = (node: AstNode): string | null =>
46
+ identifierName(node['exported'] as AstNode | undefined) ??
47
+ extractStringLiteral(node['exported'] as AstNode | undefined);
48
+
49
+ const localName = (node: AstNode): string | null =>
50
+ identifierName(node['local'] as AstNode | undefined) ??
51
+ extractStringLiteral(node['local'] as AstNode | undefined);
52
+
53
+ const isExportKey = (value: string | null): value is ExportKey =>
54
+ value !== null && exportKeys.includes(value as ExportKey);
55
+
56
+ const setExportCandidate = (
57
+ candidates: Map<ExportKey, ExportCandidate>,
58
+ key: ExportKey,
59
+ candidate: ExportCandidate
60
+ ): void => {
61
+ candidates.set(key, candidates.has(key) ? 'unknown' : candidate);
62
+ };
63
+
64
+ interface StaticModuleFacts {
65
+ readonly bindings: ReadonlyMap<string, AstNode>;
66
+ readonly candidates: ReadonlyMap<ExportKey, ExportCandidate>;
67
+ readonly hasStarExport: boolean;
68
+ readonly moduleBindings: ReadonlySet<string>;
69
+ readonly namespaceTopoBindings: ReadonlySet<string>;
70
+ readonly topoBindings: ReadonlySet<string>;
71
+ }
72
+
73
+ const collectConstBindings = (
74
+ declaration: AstNode,
75
+ bindings: Map<string, AstNode>
76
+ ): void => {
77
+ if (declaration.type !== 'VariableDeclaration') {
78
+ return;
79
+ }
80
+ const isConst = declaration['kind'] === 'const';
81
+ for (const declarator of getNodeDeclarations(declaration)) {
82
+ const name = identifierName(declarator['id'] as AstNode | undefined);
83
+ const init = declarator['init'] as AstNode | undefined;
84
+ if (name !== null && init !== undefined && isConst) {
85
+ bindings.set(name, init);
86
+ }
87
+ }
88
+ };
89
+
90
+ const collectTopoImports = (
91
+ statement: AstNode,
92
+ moduleBindings: Set<string>,
93
+ namespaceTopoBindings: Set<string>,
94
+ topoBindings: Set<string>
95
+ ): void => {
96
+ if (
97
+ statement.type !== 'ImportDeclaration' ||
98
+ statement['importKind'] === 'type'
99
+ ) {
100
+ return;
101
+ }
102
+ const importsTrailsCore = importSource(statement) === '@ontrails/core';
103
+ const specifiers =
104
+ (statement['specifiers'] as readonly AstNode[] | undefined) ?? [];
105
+ for (const specifier of specifiers) {
106
+ const local = identifierName(specifier['local'] as AstNode | undefined);
107
+ if (specifier['importKind'] === 'type' || local === null) {
108
+ continue;
109
+ }
110
+ moduleBindings.add(local);
111
+ if (!importsTrailsCore) {
112
+ continue;
113
+ }
114
+ if (specifier.type === 'ImportNamespaceSpecifier') {
115
+ namespaceTopoBindings.add(local);
116
+ } else if (
117
+ specifier.type === 'ImportSpecifier' &&
118
+ identifierName(specifier['imported'] as AstNode | undefined) === 'topo'
119
+ ) {
120
+ topoBindings.add(local);
121
+ }
122
+ }
123
+ };
124
+
125
+ const collectNamedExportCandidates = (
126
+ statement: AstNode,
127
+ bindings: ReadonlyMap<string, AstNode>,
128
+ candidates: Map<ExportKey, ExportCandidate>
129
+ ): void => {
130
+ const declaration = getNodeDeclaration(statement);
131
+ if (declaration?.type === 'VariableDeclaration') {
132
+ for (const declarator of getNodeDeclarations(declaration)) {
133
+ const name = identifierName(declarator['id'] as AstNode | undefined);
134
+ if (isExportKey(name)) {
135
+ setExportCandidate(
136
+ candidates,
137
+ name,
138
+ (declarator['init'] as AstNode | undefined) ?? 'unknown'
139
+ );
140
+ }
141
+ }
142
+ } else if (declaration !== undefined && declaration !== null) {
143
+ const name = identifierName(declaration['id'] as AstNode | undefined);
144
+ if (isExportKey(name)) {
145
+ setExportCandidate(candidates, name, 'unknown');
146
+ }
147
+ }
148
+
149
+ const specifiers =
150
+ (statement['specifiers'] as readonly AstNode[] | undefined) ?? [];
151
+ for (const specifier of specifiers) {
152
+ const exported = exportedName(specifier);
153
+ if (!isExportKey(exported)) {
154
+ continue;
155
+ }
156
+ if (statement['source'] !== null && statement['source'] !== undefined) {
157
+ setExportCandidate(candidates, exported, 'unknown');
158
+ continue;
159
+ }
160
+ const local = localName(specifier);
161
+ setExportCandidate(
162
+ candidates,
163
+ exported,
164
+ local === null ? 'unknown' : (bindings.get(local) ?? 'unknown')
165
+ );
166
+ }
167
+ };
168
+
169
+ const collectStaticModuleFacts = (ast: AstNode): StaticModuleFacts => {
170
+ const bindings = new Map<string, AstNode>();
171
+ const candidates = new Map<ExportKey, ExportCandidate>();
172
+ const moduleBindings = new Set(collectScopeFrameBindings(ast));
173
+ const namespaceTopoBindings = new Set<string>();
174
+ const topoBindings = new Set<string>();
175
+ const body = (ast['body'] as readonly AstNode[] | undefined) ?? [];
176
+
177
+ for (const statement of body) {
178
+ collectTopoImports(
179
+ statement,
180
+ moduleBindings,
181
+ namespaceTopoBindings,
182
+ topoBindings
183
+ );
184
+ const declaration =
185
+ statement.type === 'ExportNamedDeclaration'
186
+ ? getNodeDeclaration(statement)
187
+ : statement;
188
+ if (declaration !== undefined && declaration !== null) {
189
+ collectConstBindings(declaration, bindings);
190
+ }
191
+ }
192
+
193
+ let hasStarExport = false;
194
+ for (const statement of body) {
195
+ if (statement.type === 'ExportDefaultDeclaration') {
196
+ const declaration = getNodeDeclaration(statement);
197
+ setExportCandidate(candidates, 'default', declaration ?? 'unknown');
198
+ } else if (statement.type === 'ExportAllDeclaration') {
199
+ const exported = identifierName(
200
+ statement['exported'] as AstNode | undefined
201
+ );
202
+ if (isExportKey(exported)) {
203
+ setExportCandidate(candidates, exported, 'unknown');
204
+ } else {
205
+ hasStarExport = true;
206
+ }
207
+ } else if (statement.type === 'ExportNamedDeclaration') {
208
+ collectNamedExportCandidates(statement, bindings, candidates);
209
+ }
210
+ }
211
+
212
+ return {
213
+ bindings,
214
+ candidates,
215
+ hasStarExport,
216
+ moduleBindings,
217
+ namespaceTopoBindings,
218
+ topoBindings,
219
+ };
220
+ };
221
+
222
+ const isTopoCall = (node: AstNode, facts: StaticModuleFacts): boolean => {
223
+ if (node.type !== 'CallExpression') {
224
+ return false;
225
+ }
226
+ const callee = getNodeCallee(node);
227
+ const bareName = identifierName(callee);
228
+ if (bareName !== null) {
229
+ return facts.topoBindings.has(bareName);
230
+ }
231
+ if (
232
+ callee?.type !== 'MemberExpression' &&
233
+ callee?.type !== 'StaticMemberExpression'
234
+ ) {
235
+ return false;
236
+ }
237
+ if (callee['computed'] === true) {
238
+ return false;
239
+ }
240
+ const receiver = identifierName(callee['object'] as AstNode | undefined);
241
+ const member = identifierName(callee['property'] as AstNode | undefined);
242
+ return (
243
+ receiver !== null &&
244
+ member === 'topo' &&
245
+ facts.namespaceTopoBindings.has(receiver)
246
+ );
247
+ };
248
+
249
+ const isSupportedStaticExpression = (
250
+ node: AstNode,
251
+ facts: StaticModuleFacts
252
+ ): boolean => {
253
+ const expression = unwrapExpression(node);
254
+ if (
255
+ identifierName(expression) !== null ||
256
+ extractStringLiteral(expression) !== null ||
257
+ expression.type === 'NullLiteral' ||
258
+ expression.type === 'BooleanLiteral' ||
259
+ expression.type === 'NumericLiteral' ||
260
+ expression.type === 'BigIntLiteral' ||
261
+ expression.type === 'FunctionExpression' ||
262
+ expression.type === 'ArrowFunctionExpression'
263
+ ) {
264
+ return true;
265
+ }
266
+ if (isTopoCall(expression, facts)) {
267
+ return getNodeArguments(expression).every((argument) =>
268
+ isSupportedStaticExpression(argument, facts)
269
+ );
270
+ }
271
+ if (expression.type === 'NewExpression') {
272
+ const constructorName = identifierName(getNodeCallee(expression));
273
+ const arguments_ = getNodeArguments(expression);
274
+ return (
275
+ (constructorName === 'Map' || constructorName === 'Set') &&
276
+ !facts.moduleBindings.has(constructorName) &&
277
+ arguments_.length === 0
278
+ );
279
+ }
280
+ if (expression.type === 'ArrayExpression') {
281
+ const elements =
282
+ (expression['elements'] as readonly (AstNode | null)[] | undefined) ?? [];
283
+ return elements.every(
284
+ (element) =>
285
+ element === null || isSupportedStaticExpression(element, facts)
286
+ );
287
+ }
288
+ if (expression.type !== 'ObjectExpression') {
289
+ return false;
290
+ }
291
+ const properties =
292
+ (expression['properties'] as readonly AstNode[] | undefined) ?? [];
293
+ return properties.every(
294
+ (property) =>
295
+ property.type === 'Property' &&
296
+ property['computed'] !== true &&
297
+ property['kind'] === 'init' &&
298
+ property['method'] !== true &&
299
+ isSupportedStaticExpression(property['value'] as AstNode, facts)
300
+ );
301
+ };
302
+
303
+ const isSupportedVariableDeclaration = (
304
+ declaration: AstNode,
305
+ facts: StaticModuleFacts
306
+ ): boolean =>
307
+ declaration.type === 'VariableDeclaration' &&
308
+ declaration['kind'] === 'const' &&
309
+ getNodeDeclarations(declaration).every((declarator) => {
310
+ const init = declarator['init'] as AstNode | undefined;
311
+ return (
312
+ identifierName(declarator['id'] as AstNode | undefined) !== null &&
313
+ (init === undefined || isSupportedStaticExpression(init, facts))
314
+ );
315
+ });
316
+
317
+ const isSupportedModuleStatement = (
318
+ statement: AstNode,
319
+ facts: StaticModuleFacts
320
+ ): boolean => {
321
+ if (
322
+ statement.type === 'ImportDeclaration' ||
323
+ statement.type === 'TSInterfaceDeclaration' ||
324
+ statement.type === 'TSTypeAliasDeclaration' ||
325
+ statement.type === 'FunctionDeclaration'
326
+ ) {
327
+ return true;
328
+ }
329
+ if (statement.type === 'VariableDeclaration') {
330
+ return isSupportedVariableDeclaration(statement, facts);
331
+ }
332
+ if (statement.type === 'ExportDefaultDeclaration') {
333
+ const declaration = getNodeDeclaration(statement);
334
+ return (
335
+ declaration !== undefined &&
336
+ declaration !== null &&
337
+ isSupportedStaticExpression(declaration, facts)
338
+ );
339
+ }
340
+ if (statement.type !== 'ExportNamedDeclaration') {
341
+ return false;
342
+ }
343
+ if (statement['source'] !== null && statement['source'] !== undefined) {
344
+ return false;
345
+ }
346
+ const declaration = getNodeDeclaration(statement);
347
+ return (
348
+ declaration === undefined ||
349
+ declaration === null ||
350
+ declaration.type === 'FunctionDeclaration' ||
351
+ declaration.type === 'TSInterfaceDeclaration' ||
352
+ declaration.type === 'TSTypeAliasDeclaration' ||
353
+ isSupportedVariableDeclaration(declaration, facts)
354
+ );
355
+ };
356
+
357
+ const hasSupportedStaticModuleShape = (
358
+ ast: AstNode,
359
+ facts: StaticModuleFacts
360
+ ): boolean => {
361
+ const body = (ast['body'] as readonly AstNode[] | undefined) ?? [];
362
+ return body.every((statement) =>
363
+ isSupportedModuleStatement(statement, facts)
364
+ );
365
+ };
366
+
367
+ const deriveCertainObjectName = (node: AstNode): string | undefined => {
368
+ if (node.type !== 'ObjectExpression') {
369
+ return undefined;
370
+ }
371
+ const properties =
372
+ (node['properties'] as readonly AstNode[] | undefined) ?? [];
373
+ const names: string[] = [];
374
+ for (const property of properties) {
375
+ if (property.type !== 'Property' || property['computed'] === true) {
376
+ return undefined;
377
+ }
378
+ if (propertyKeyName(property) !== 'name') {
379
+ continue;
380
+ }
381
+ const name = extractStringLiteral(property['value'] as AstNode | undefined);
382
+ if (name === null) {
383
+ return undefined;
384
+ }
385
+ names.push(name);
386
+ }
387
+ return names.length === 1 ? names[0] : undefined;
388
+ };
389
+
390
+ const deriveExpressionIdentity = (
391
+ candidate: AstNode,
392
+ facts: StaticModuleFacts,
393
+ seen: ReadonlySet<string> = new Set()
394
+ ): DerivedIdentity => {
395
+ const expression = unwrapExpression(candidate);
396
+ if (
397
+ expression.type === 'NullLiteral' ||
398
+ (expression.type === 'Literal' && expression['value'] === null)
399
+ ) {
400
+ return { kind: 'nullish' };
401
+ }
402
+
403
+ const bindingName = identifierName(expression);
404
+ if (bindingName !== null) {
405
+ if (seen.has(bindingName)) {
406
+ return { kind: 'unknown' };
407
+ }
408
+ const binding = facts.bindings.get(bindingName);
409
+ return binding === undefined
410
+ ? { kind: 'unknown' }
411
+ : deriveExpressionIdentity(
412
+ binding,
413
+ facts,
414
+ new Set([...seen, bindingName])
415
+ );
416
+ }
417
+
418
+ if (isTopoCall(expression, facts)) {
419
+ const [identity] = getNodeArguments(expression);
420
+ const id =
421
+ extractStringLiteral(identity) ??
422
+ (identity === undefined
423
+ ? undefined
424
+ : deriveCertainObjectName(unwrapExpression(identity)));
425
+ return id === undefined ? { kind: 'unknown' } : { id, kind: 'known' };
426
+ }
427
+
428
+ if (expression.type === 'ObjectExpression') {
429
+ const id = deriveCertainObjectName(expression);
430
+ return id === undefined ? { kind: 'unknown' } : { id, kind: 'known' };
431
+ }
432
+
433
+ return { kind: 'unknown' };
434
+ };
435
+
436
+ /**
437
+ * Derive the runtime-selected topo ID when source makes it statically certain.
438
+ *
439
+ * Unknown, dynamic, helper-built, and re-exported forms intentionally return
440
+ * undefined so runtime app loading remains their binding authority.
441
+ *
442
+ * @example
443
+ * ```ts
444
+ * deriveStaticSelectedTopoId('src/app.ts', "export const app = topo('demo')")
445
+ * // undefined: topo is not proven to be imported from @ontrails/core
446
+ * ```
447
+ */
448
+ export const deriveStaticSelectedTopoId = (
449
+ filePath: string,
450
+ sourceCode: string
451
+ ): string | undefined => {
452
+ const parsed = parseWithDiagnostics(filePath, sourceCode);
453
+ if (parsed.ast === null || parsed.diagnostics.length > 0) {
454
+ return undefined;
455
+ }
456
+
457
+ const facts = collectStaticModuleFacts(parsed.ast);
458
+ if (!hasSupportedStaticModuleShape(parsed.ast, facts)) {
459
+ return undefined;
460
+ }
461
+ for (const key of exportKeys) {
462
+ const candidate = facts.candidates.get(key);
463
+ if (candidate === undefined) {
464
+ if (key !== 'default' && facts.hasStarExport) {
465
+ return undefined;
466
+ }
467
+ continue;
468
+ }
469
+ if (candidate === 'unknown') {
470
+ return undefined;
471
+ }
472
+ const identity = deriveExpressionIdentity(candidate, facts);
473
+ if (identity.kind === 'nullish') {
474
+ continue;
475
+ }
476
+ return identity.kind === 'known' ? identity.id : undefined;
477
+ }
478
+ return undefined;
479
+ };