@astrale-os/cli 1.0.0-beta.2 → 1.0.0-beta.20

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 (282) hide show
  1. package/README.md +16 -1
  2. package/dist/astrale.js +51105 -58057
  3. package/dist/public/connect-core.js +2085 -30702
  4. package/dist/public/keys/index.js +884 -29476
  5. package/dist/public/paths/index.js +13101 -41452
  6. package/dist/types/admin/contract.d.ts +26 -0
  7. package/dist/types/admin/instance/client.d.ts +20 -0
  8. package/dist/types/admin/instance/index.d.ts +2 -0
  9. package/dist/types/admin/instance/model.d.ts +6 -10
  10. package/dist/types/connection/auth.d.ts +3 -0
  11. package/dist/types/connection/call.d.ts +3 -0
  12. package/dist/types/connection/command.d.ts +27 -0
  13. package/dist/types/connection/credential.d.ts +14 -0
  14. package/dist/types/connection/errors.d.ts +1 -0
  15. package/dist/types/connection/exchange.d.ts +9 -0
  16. package/dist/types/connection/failure/classify.d.ts +2 -0
  17. package/dist/types/connection/failure/index.d.ts +5 -0
  18. package/dist/types/connection/failure/model.d.ts +25 -0
  19. package/dist/types/connection/failure/render.d.ts +3 -0
  20. package/dist/types/connection/index.d.ts +8 -0
  21. package/dist/types/connection/reasons.d.ts +37 -0
  22. package/dist/types/connection/self.d.ts +37 -0
  23. package/dist/types/connection/session.d.ts +31 -0
  24. package/dist/types/connection/target.d.ts +38 -0
  25. package/dist/types/errors.d.ts +1 -1
  26. package/dist/types/lib/admin-instance.d.ts +17 -0
  27. package/dist/types/lib/admin-target.d.ts +11 -36
  28. package/dist/types/lib/config.d.ts +7 -59
  29. package/dist/types/lib/failure-debug.d.ts +1 -0
  30. package/dist/types/lib/idp.d.ts +25 -173
  31. package/dist/types/lib/instance-target.d.ts +1 -1
  32. package/dist/types/lib/instance.d.ts +30 -88
  33. package/dist/types/lib/log.d.ts +5 -3
  34. package/dist/types/lib/output.d.ts +1 -1
  35. package/dist/types/lib/proc.d.ts +43 -0
  36. package/dist/types/lib/self.d.ts +9 -0
  37. package/dist/types/lib/update.d.ts +109 -0
  38. package/dist/types/lib/validation.d.ts +4 -1
  39. package/package.json +15 -20
  40. package/src/__tests__/fixtures/publication.ts +20 -0
  41. package/src/admin/.spec/architecture.md +12 -5
  42. package/src/admin/__tests__/fixture.ts +25 -0
  43. package/src/admin/catalog/.spec/api.d.ts +1 -3
  44. package/src/admin/catalog/.spec/architecture.md +5 -4
  45. package/src/admin/catalog/__tests__/client.test.ts +143 -69
  46. package/src/admin/catalog/client.ts +40 -54
  47. package/src/admin/catalog/model.ts +3 -4
  48. package/src/admin/contract.ts +46 -0
  49. package/src/admin/graph/.spec/api.d.ts +4 -10
  50. package/src/admin/graph/nodes.ts +1 -1
  51. package/src/admin/instance/.spec/api.d.ts +5 -9
  52. package/src/admin/instance/.spec/architecture.md +7 -7
  53. package/src/admin/instance/__tests__/client.test.ts +157 -175
  54. package/src/admin/instance/client.ts +39 -128
  55. package/src/admin/instance/index.ts +1 -2
  56. package/src/admin/instance/model.ts +12 -18
  57. package/src/commands/__tests__/admin-instance.test.ts +11 -8
  58. package/src/commands/__tests__/call-describe.test.ts +46 -33
  59. package/src/commands/__tests__/call.test.ts +34 -0
  60. package/src/commands/__tests__/domain-install-operation.test.ts +7 -22
  61. package/src/commands/__tests__/domain-install-owned.test.ts +1 -1
  62. package/src/commands/__tests__/domain-list.test.ts +5 -25
  63. package/src/commands/__tests__/install-direct.test.ts +115 -8
  64. package/src/commands/__tests__/install-identity-override.test.ts +3 -14
  65. package/src/commands/__tests__/instance-list-rows.test.ts +1 -1
  66. package/src/commands/__tests__/instance-status.test.ts +139 -0
  67. package/src/commands/__tests__/introspect-parse.test.ts +80 -1
  68. package/src/commands/__tests__/logs.test.ts +250 -2
  69. package/src/commands/__tests__/read-commands.test.ts +62 -10
  70. package/src/commands/__tests__/token-ttl.test.ts +49 -4
  71. package/src/commands/__tests__/update.test.ts +25 -3
  72. package/src/commands/__tests__/view.test.ts +49 -0
  73. package/src/commands/call-describe.ts +60 -68
  74. package/src/commands/call.ts +71 -59
  75. package/src/commands/domain/install.ts +54 -35
  76. package/src/commands/domain/list.ts +1 -1
  77. package/src/commands/domain/publish.ts +14 -11
  78. package/src/commands/domain/uninstall.ts +2 -2
  79. package/src/commands/get.ts +14 -15
  80. package/src/commands/identity/__tests__/register.test.ts +3 -3
  81. package/src/commands/identity/register.ts +5 -4
  82. package/src/commands/instance/create.ts +8 -16
  83. package/src/commands/instance/delete.ts +12 -9
  84. package/src/commands/instance/list.ts +2 -2
  85. package/src/commands/instance/status.ts +66 -20
  86. package/src/commands/instance/use.ts +16 -12
  87. package/src/commands/introspect.ts +48 -35
  88. package/src/commands/logs.ts +123 -33
  89. package/src/commands/mutate.ts +6 -5
  90. package/src/commands/query.ts +35 -32
  91. package/src/commands/token.ts +42 -27
  92. package/src/commands/ui/__tests__/commands.test.ts +152 -0
  93. package/src/commands/ui/add.ts +51 -0
  94. package/src/commands/ui/doctor.ts +13 -0
  95. package/src/commands/ui/init.ts +38 -0
  96. package/src/commands/ui/list.ts +26 -0
  97. package/src/commands/ui/preset-apply.ts +19 -0
  98. package/src/commands/ui/preset-list.ts +12 -0
  99. package/src/commands/ui/shared.ts +25 -0
  100. package/src/commands/update.ts +31 -11
  101. package/src/commands/view.ts +82 -37
  102. package/src/connection/.spec/api.d.ts +7 -8
  103. package/src/connection/.spec/architecture.md +5 -4
  104. package/src/connection/.spec/flows/session.ts +2 -2
  105. package/src/connection/.spec/laws/connection.ts +6 -2
  106. package/src/connection/.spec/layout.ts +1 -0
  107. package/src/connection/__tests__/credential.test.ts +20 -1
  108. package/src/connection/__tests__/errors.test.ts +27 -20
  109. package/src/connection/__tests__/exchange.test.ts +30 -1
  110. package/src/connection/__tests__/failure-fixtures.ts +39 -0
  111. package/src/connection/__tests__/failure-safety.test.ts +75 -0
  112. package/src/connection/__tests__/self.test.ts +31 -11
  113. package/src/connection/__tests__/session.test.ts +3 -3
  114. package/src/connection/__tests__/target.test.ts +1 -0
  115. package/src/connection/auth.ts +13 -0
  116. package/src/connection/call.ts +1 -1
  117. package/src/connection/credential.ts +2 -2
  118. package/src/connection/errors.ts +1 -355
  119. package/src/connection/exchange.ts +47 -23
  120. package/src/connection/failure/classify.ts +110 -0
  121. package/src/connection/failure/index.ts +18 -0
  122. package/src/connection/failure/model.ts +28 -0
  123. package/src/connection/failure/render.ts +116 -0
  124. package/src/connection/reasons.ts +16 -0
  125. package/src/connection/self.ts +39 -21
  126. package/src/connection/session.ts +19 -8
  127. package/src/errors.ts +3 -2
  128. package/src/graph/.spec/api.d.ts +12 -5
  129. package/src/graph/.spec/layout.ts +9 -1
  130. package/src/graph/__tests__/mutation.test.ts +2 -2
  131. package/src/graph/__tests__/query.test.ts +25 -6
  132. package/src/graph/class.ts +30 -0
  133. package/src/graph/index.ts +1 -0
  134. package/src/graph/query.ts +21 -32
  135. package/src/identity/.spec/api.d.ts +2 -2
  136. package/src/identity/__tests__/registration.test.ts +1 -1
  137. package/src/identity/registration.ts +3 -3
  138. package/src/keys/__tests__/keys.test.ts +4 -0
  139. package/src/keys/credential.ts +1 -0
  140. package/src/lib/__tests__/admin-target.test.ts +12 -0
  141. package/src/lib/__tests__/binary.test.ts +16 -1
  142. package/src/lib/__tests__/command-dx.test.ts +1 -1
  143. package/src/lib/__tests__/domain-publication.test.ts +10 -16
  144. package/src/lib/__tests__/idp.test.ts +34 -4
  145. package/src/lib/__tests__/instance-candidates.test.ts +1 -3
  146. package/src/lib/__tests__/instance-target.test.ts +7 -39
  147. package/src/lib/__tests__/log-errors.test.ts +64 -0
  148. package/src/lib/__tests__/studio-server-deps.test.ts +1 -1
  149. package/src/lib/__tests__/update.test.ts +264 -11
  150. package/src/lib/__tests__/view-assets.test.ts +14 -0
  151. package/src/lib/__tests__/view-external-open-intent.test.ts +150 -0
  152. package/src/lib/__tests__/view-external-open-origins.test.ts +27 -0
  153. package/src/lib/__tests__/view-open-intent.test.ts +0 -2
  154. package/src/lib/__tests__/view-server.test.ts +26 -3
  155. package/src/lib/__tests__/view-session.test.ts +2 -1
  156. package/src/lib/admin-instance.ts +14 -8
  157. package/src/lib/admin-target.ts +21 -2
  158. package/src/lib/binary.ts +22 -5
  159. package/src/lib/command-dx.ts +9 -9
  160. package/src/lib/domain-publication.ts +9 -7
  161. package/src/lib/failure-debug.ts +26 -0
  162. package/src/lib/idp.ts +34 -3
  163. package/src/lib/instance-candidates.ts +2 -2
  164. package/src/lib/instance-target.ts +3 -14
  165. package/src/lib/instance.ts +16 -11
  166. package/src/lib/log.ts +16 -9
  167. package/src/lib/output.ts +1 -1
  168. package/src/lib/proc.ts +7 -2
  169. package/src/lib/provision-instance.ts +34 -57
  170. package/src/lib/update.ts +298 -45
  171. package/src/lib/validation.ts +2 -2
  172. package/src/lib/view/assets.ts +11 -2
  173. package/src/lib/view/external-open-intent.ts +41 -0
  174. package/src/lib/view/external-open-origins.ts +37 -0
  175. package/src/lib/view/host-capabilities.ts +17 -0
  176. package/src/lib/view/resolve.ts +1 -1
  177. package/src/lib/view/server.ts +18 -3
  178. package/src/lib/view/session.ts +2 -0
  179. package/src/program/.spec/api.d.ts +1 -0
  180. package/src/program/__tests__/program.test.ts +163 -6
  181. package/src/program/argv.ts +14 -0
  182. package/src/program/build.ts +26 -4
  183. package/src/program/command.ts +1 -0
  184. package/src/program/index.ts +1 -0
  185. package/src/program/registry.ts +2 -1
  186. package/src/setup/__tests__/instance-step.test.ts +27 -10
  187. package/src/setup/steps/instance.ts +22 -15
  188. package/src/telemetry/__tests__/recorder.test.ts +1 -0
  189. package/src/telemetry/recorder.ts +9 -1
  190. package/src/ui/.spec/api.d.ts +14 -0
  191. package/src/ui/.spec/architecture.md +10 -0
  192. package/src/ui/.spec/laws.ts +36 -0
  193. package/src/ui/.spec/layout.ts +16 -0
  194. package/src/ui/__tests__/ui.test.ts +981 -0
  195. package/src/ui/index.ts +13 -0
  196. package/src/ui/lock.ts +87 -0
  197. package/src/ui/model.ts +83 -0
  198. package/src/ui/operations.ts +710 -0
  199. package/src/ui/project.ts +219 -0
  200. package/src/ui/release.ts +321 -0
  201. package/src/ui/runner.ts +18 -0
  202. package/studio/client/dist/assets/elk-api-DjmKsHXc.js +1 -0
  203. package/studio/client/dist/assets/index-7YHdeCxp.js +81 -0
  204. package/studio/client/dist/assets/index-C0FAnwNw.css +2 -0
  205. package/studio/client/dist/assets/rolldown-runtime-Dd_uD5pT.js +1 -0
  206. package/studio/client/dist/assets/schema-studio-BilUNb6Z.css +1 -0
  207. package/studio/client/dist/assets/schema-studio-CABpTfvH.js +8 -0
  208. package/studio/client/dist/index.html +3 -2
  209. package/studio/package.json +7 -13
  210. package/studio/server/agent/ask.test.ts +1 -1
  211. package/studio/server/agent/bridge/grant.test.ts +1 -1
  212. package/studio/server/agent/bridge/routes.test.ts +1 -1
  213. package/studio/server/agent/harness/claude/adapter.test.ts +15 -5
  214. package/studio/server/agent/harness/gateway/token.test.ts +1 -1
  215. package/studio/server/agent/harness/gateway/token.ts +3 -3
  216. package/studio/server/agent/prompts/anchors.test.ts +32 -36
  217. package/studio/server/agent/prompts/anchors.ts +20 -49
  218. package/studio/server/agent/prompts/system.test.ts +3 -2
  219. package/studio/server/agent/prompts/system.ts +3 -3
  220. package/studio/server/agent/routes.test.ts +1 -1
  221. package/studio/server/agent/run/coordinator.test.ts +1 -1
  222. package/studio/server/api/canvas.ts +0 -1
  223. package/studio/server/api-agent-loadout.test.ts +1 -1
  224. package/studio/server/api.test.ts +1 -1
  225. package/studio/server/cache.test.ts +23 -12
  226. package/studio/server/cache.ts +12 -15
  227. package/studio/server/domain.test.ts +50 -68
  228. package/studio/server/domain.ts +82 -63
  229. package/studio/server/handoff/copy.ts +1 -1
  230. package/studio/server/index.ts +1 -1
  231. package/studio/server/instances/probe.test.ts +1 -1
  232. package/studio/server/introspect/anatomy/views/routes.ts +46 -37
  233. package/studio/server/introspect/anatomy/views.ts +9 -14
  234. package/studio/server/introspect/anatomy-extras.test.ts +26 -42
  235. package/studio/server/introspect/anatomy.test.ts +22 -14
  236. package/studio/server/introspect/anatomy.ts +6 -9
  237. package/studio/server/introspect/bundle.ts +0 -3
  238. package/studio/server/introspect/canonical-schema.test.ts +94 -389
  239. package/studio/server/introspect/canonical-schema.ts +227 -457
  240. package/studio/server/introspect/core-extractor.ts +18 -127
  241. package/studio/server/introspect/core.ts +1 -2
  242. package/studio/server/introspect/diff.test.ts +11 -7
  243. package/studio/server/introspect/diff.ts +18 -55
  244. package/studio/server/introspect/extractor.ts +22 -117
  245. package/studio/server/introspect/overlay-tsmorph.test.ts +119 -240
  246. package/studio/server/introspect/overlay.test.ts +26 -54
  247. package/studio/server/introspect/overlay.ts +10 -37
  248. package/studio/server/introspect/revision.test.ts +1 -1
  249. package/studio/server/introspect/revision.ts +1 -1
  250. package/studio/server/introspect/runtime.test.ts +65 -183
  251. package/studio/server/introspect/runtime.ts +8 -25
  252. package/studio/server/introspect/schema-ir-json.test.ts +70 -0
  253. package/studio/server/introspect/schema-ir-json.ts +38 -68
  254. package/studio/server/introspect/schema-refs.test.ts +43 -88
  255. package/studio/server/introspect/schema-refs.ts +14 -49
  256. package/studio/server/introspect/source-overlay/handlers.ts +116 -636
  257. package/studio/server/introspect/source-overlay/kernel-calls.ts +0 -3
  258. package/studio/server/introspect/source-overlay/spans.ts +15 -45
  259. package/studio/server/state/baseline.test.ts +14 -9
  260. package/studio/server/state/baseline.ts +3 -5
  261. package/studio/server/state/visibility.ts +1 -5
  262. package/studio/server/views/model.ts +3 -3
  263. package/studio/server/views/target.test.ts +17 -74
  264. package/studio/server/views/target.ts +24 -50
  265. package/studio/server/watch.test.ts +11 -11
  266. package/studio/server/watch.ts +8 -4
  267. package/studio/server/workspace-watch.ts +2 -2
  268. package/studio/shared/contracts/schema.ts +38 -107
  269. package/studio/shared/contracts/surface.test.ts +13 -15
  270. package/studio/shared/contracts/workspace.ts +2 -11
  271. package/studio/shared/schema/identity.test.ts +20 -44
  272. package/studio/shared/schema/identity.ts +9 -23
  273. package/viewer/dist/main.js +30 -51
  274. package/.check-workspace.cjs +0 -40
  275. package/src/admin/binding.ts +0 -168
  276. package/src/commands/__tests__/instance-create-hosts.test.ts +0 -29
  277. package/studio/client/dist/assets/elk-api-lwhFo7vB.js +0 -1
  278. package/studio/client/dist/assets/index-B-c-smo9.js +0 -8
  279. package/studio/client/dist/assets/index-BckHuAWk.js +0 -81
  280. package/studio/client/dist/assets/index-C4aNQ6dz.css +0 -1
  281. package/studio/client/dist/assets/index-ChOr3yP0.css +0 -1
  282. package/studio/server/introspect/anatomy/views/legacy.ts +0 -232
@@ -1,382 +1,121 @@
1
- /**
2
- * Handler wiring overlay for canonical implementation maps and legacy runtime helpers.
3
- */
4
- import { resolve as resolvePath } from 'node:path'
1
+ /** Source links for SDK V1 Action and Workflow declarations. */
2
+ import { readdirSync } from 'node:fs'
3
+ import { join } from 'node:path'
5
4
  import { Node, SyntaxKind, type CallExpression } from 'ts-morph'
6
5
 
7
6
  import type { HandlerLink, SchemaIR } from '../../../shared/types'
8
7
 
9
8
  import { scanKernelCalls } from './kernel-calls'
10
- import {
11
- calleeName,
12
- firstValueDeclaration,
13
- getProp,
14
- newProject,
15
- objectPropertyValue,
16
- propertyKey,
17
- relToRoot,
18
- resolveModuleFile,
19
- resolveObjectLiteral,
20
- stringLiteralOfProp,
21
- tryAddFile,
22
- unwrapExpression,
23
- valueOfIdentifier,
24
- } from './project'
9
+ import { newProject, relToRoot, unwrapExpression, valueOfIdentifier } from './project'
10
+
11
+ const IGNORED_DIRECTORIES = new Set([
12
+ '.git',
13
+ '.history',
14
+ '.astrale',
15
+ '.cache',
16
+ '__tests__',
17
+ 'dist',
18
+ 'node_modules',
19
+ ])
25
20
 
26
- /** Unwrap a string literal argument to its value. */
27
- function stringArg(call: CallExpression, index: number): string | undefined {
28
- const arg = call.getArguments()[index]
29
- if (!arg) return undefined
30
- if (Node.isStringLiteral(arg) || Node.isNoSubstitutionTemplateLiteral(arg)) {
31
- return arg.getLiteralText()
21
+ function authoredTypeScriptFiles(root: string): string[] {
22
+ const files: string[] = []
23
+ const visit = (directory: string) => {
24
+ let entries
25
+ try {
26
+ entries = readdirSync(directory, { withFileTypes: true })
27
+ } catch {
28
+ return
29
+ }
30
+ for (const entry of entries) {
31
+ if (entry.isSymbolicLink()) continue
32
+ const path = join(directory, entry.name)
33
+ if (entry.isDirectory()) {
34
+ if (!IGNORED_DIRECTORIES.has(entry.name)) visit(path)
35
+ } else if (entry.isFile() && path.endsWith('.ts') && !path.endsWith('.test.ts')) {
36
+ files.push(path)
37
+ }
38
+ }
32
39
  }
33
- return undefined
40
+ visit(root)
41
+ return files
34
42
  }
35
43
 
36
- // ───────────────────────────── handler links ─────────────────────────────
37
-
38
- /** Recognized method-wiring calls after normalizing single-method and grouped helpers. */
39
- interface WiredMethod {
44
+ interface CallableOwner {
40
45
  owner: string
46
+ ownerKind: 'class' | 'function'
41
47
  method: string
42
- /** the object literal / call expression carrying the handler config */
43
- config: Node
44
- wiringLine: number
45
- wiringFile: string
46
- ownerKind?: 'class' | 'interface' | 'function'
47
- style?: 'legacy-config' | 'direct-handler'
48
+ static: boolean
48
49
  }
49
50
 
50
- const SINGLE_METHOD_HELPERS = new Set(['method', 'remoteMethod'])
51
- const GROUP_HELPERS = new Set([
52
- 'classMethods',
53
- 'interfaceMethods',
54
- 'remoteClassMethods',
55
- 'remoteInterfaceMethods',
56
- ])
57
-
58
- /**
59
- * Resolve a method-config node (object literal `{ authorize, execute }` or a
60
- * call like `todo('…')`) down to: the symbol that carries the real logic, plus
61
- * whether it is a NotImplemented stub and the authorize flavour.
62
- */
63
- interface ConfigResolution {
64
- /** the node whose `execute` body we follow to a handler file */
65
- executeNode?: Node
66
- implemented: boolean
67
- /** declared auth policy; defaults to 'required' when the prop is absent. */
68
- auth?: 'public' | 'optional' | 'required'
69
- /** authorize hook shape: 'absent' | 'noop' | 'custom'. */
70
- authorize?: 'absent' | 'noop' | 'custom'
71
- authorizeSnippet?: string
72
- unlinkedReason?: 'no-config'
73
- }
74
-
75
- function resolveConfigObject(configNode: Node, seen = new Set<string>()): ConfigResolution {
76
- const node = unwrapExpression(configNode)
77
- const key = `${node.getSourceFile().getFilePath()}:${node.getStart()}`
78
- if (seen.has(key)) return { implemented: false, unlinkedReason: 'no-config' }
79
- seen.add(key)
80
-
81
- if (Node.isCallExpression(node)) {
82
- // Curried SDK helpers carry the handler config in their final argument:
83
- // `remoteMethod()(schema, 'Issue', 'delete', { authorize, execute })`.
84
- const lastArg = node.getArguments().at(-1)
85
- if (lastArg) {
86
- const candidate = unwrapExpression(lastArg)
87
- if (Node.isObjectLiteralExpression(candidate) || Node.isIdentifier(candidate)) {
88
- return resolveConfigObject(candidate, seen)
89
- }
51
+ function declaredCallable(ir: SchemaIR | null, key: string): CallableOwner | undefined {
52
+ if (!ir) return undefined
53
+ const standalone = ir.functions[key]
54
+ if (standalone) {
55
+ return {
56
+ owner: ir.domain,
57
+ ownerKind: 'function',
58
+ method: key,
59
+ static: true,
90
60
  }
91
-
92
- // Local factories such as `todo('Owner.name')` return the config object themselves.
93
- const obj = followFactoryToObject(node)
94
- if (obj) return classifyObjectLiteral(obj)
95
- return { implemented: false, unlinkedReason: 'no-config' }
96
61
  }
97
- if (Node.isObjectLiteralExpression(node)) {
98
- return classifyObjectLiteral(node)
99
- }
100
- // Handler maps normally reference a local or imported `remoteMethod` value.
101
- if (Node.isIdentifier(node)) {
102
- const value = valueOfIdentifier(node)
103
- if (value) return resolveConfigObject(value, seen)
104
- }
105
- return { implemented: false, unlinkedReason: 'no-config' }
62
+ const separator = key.lastIndexOf('.')
63
+ if (separator <= 0) return undefined
64
+ const owner = key.slice(0, separator)
65
+ const method = key.slice(separator + 1)
66
+ const definition = ir.classes[owner]?.methods[method]
67
+ return definition
68
+ ? {
69
+ owner,
70
+ ownerKind: 'class',
71
+ method,
72
+ static: definition.static,
73
+ }
74
+ : undefined
106
75
  }
107
76
 
108
- /** Given `todo('x')`, find the factory's `return { … }` object literal. */
109
- function followFactoryToObject(call: CallExpression): Node | undefined {
110
- const expr = call.getExpression()
111
- if (!Node.isIdentifier(expr)) return undefined
112
- const decl = firstValueDeclaration(expr)
113
- if (!decl) return undefined
114
- let body: Node | undefined
115
- if (Node.isVariableDeclaration(decl)) {
116
- body = decl.getInitializer()
117
- } else if (Node.isFunctionDeclaration(decl)) {
118
- body = decl
119
- }
120
- if (!body) return undefined
121
- // Arrow returning an object literal directly: `(name) => ({ … })`.
122
- if (Node.isArrowFunction(body)) {
123
- const arrowBody = body.getBody()
124
- if (Node.isParenthesizedExpression(arrowBody)) {
125
- const inner = arrowBody.getExpression()
126
- if (Node.isObjectLiteralExpression(inner)) return inner
127
- }
128
- if (Node.isObjectLiteralExpression(arrowBody)) return arrowBody
129
- // Block body with a `return { … }`.
130
- const ret = arrowBody.getFirstDescendantByKind?.(SyntaxKind.ReturnStatement)
131
- const retExpr = ret?.getExpression()
132
- if (retExpr && Node.isObjectLiteralExpression(retExpr)) return retExpr
133
- }
134
- if (Node.isFunctionDeclaration(body) || Node.isFunctionExpression(body)) {
135
- const ret = body.getFirstDescendantByKind(SyntaxKind.ReturnStatement)
136
- const retExpr = ret?.getExpression()
137
- if (retExpr && Node.isObjectLiteralExpression(retExpr)) return retExpr
138
- }
77
+ function declarationKind(call: CallExpression): 'action' | 'workflow' | undefined {
78
+ const expression = unwrapExpression(call.getExpression())
79
+ if (!Node.isCallExpression(expression)) return undefined
80
+ const factory = unwrapExpression(expression.getExpression())
81
+ if (!Node.isIdentifier(factory)) return undefined
82
+ if (factory.getText() === 'defineAction') return 'action'
83
+ if (factory.getText() === 'defineWorkflow') return 'workflow'
139
84
  return undefined
140
85
  }
141
86
 
142
- /** Inspect `{ auth, authorize, execute }` for the auth policy, authorize shape,
143
- * parsed permission checks, source snippets, and execute stub-ness. */
144
- function classifyObjectLiteral(obj: Node): ConfigResolution {
145
- if (!Node.isObjectLiteralExpression(obj)) {
146
- return { implemented: false, unlinkedReason: 'no-config' }
147
- }
148
- const executeProp = getProp(obj, 'execute')
149
- const authorizeProp = getProp(obj, 'authorize')
150
-
151
- const auth = authPolicy(obj)
152
- const authorize = authorizeProp ? authorizeFlavour(authorizeProp) : 'absent'
153
- const authorizeSnippet = authorizeProp ? snippetOf(authorizeProp) : undefined
154
- const common = { auth, authorize, authorizeSnippet } as const
155
-
156
- if (!executeProp) {
157
- return { implemented: false, ...common, unlinkedReason: 'no-config' }
158
- }
159
- const implemented = !isStubFunction(executeProp)
160
- return { executeNode: executeProp, implemented, ...common }
161
- }
162
-
163
- /** The declared `auth` policy on a method config; defaults to 'required'. */
164
- function authPolicy(obj: Node): 'public' | 'optional' | 'required' {
165
- const v = stringLiteralOfProp(obj, 'auth')
166
- return v === 'public' || v === 'optional' ? v : 'required'
87
+ function stringArgument(call: CallExpression, index: number): string | undefined {
88
+ const argument = call.getArguments()[index]
89
+ return argument &&
90
+ (Node.isStringLiteral(argument) || Node.isNoSubstitutionTemplateLiteral(argument))
91
+ ? argument.getLiteralText()
92
+ : undefined
167
93
  }
168
94
 
169
- const SNIPPET_CAP = 1200
170
-
171
- /** Source text of a node, capped for a hover preview. */
172
- function snippetOf(node: Node): string {
173
- const t = node.getText()
174
- return t.length > SNIPPET_CAP ? `${t.slice(0, SNIPPET_CAP)}\n/* … */` : t
95
+ function resolveHandler(node: Node): Node {
96
+ let current = unwrapExpression(node)
97
+ if (Node.isIdentifier(current)) current = unwrapExpression(valueOfIdentifier(current) ?? current)
98
+ return current
175
99
  }
176
100
 
177
- /** 'noop' when the authorize body just returns undefined/void, else 'custom'.
178
- * Follows identifier references (e.g. `authorize: allow` → `const allow = …`). */
179
- function authorizeFlavour(node: Node): 'noop' | 'custom' {
180
- const fnBody = functionBodyOf(node)
181
- if (fnBody === undefined) return 'custom' // opaque (unresolved identifier) → assume real
182
- if (fnBody === null) return 'noop' // expression arrow returning `undefined`
183
- const text = fnBody.getText().replace(/[{}]/g, '').trim()
184
- // Empty block, or a single `return;` / `return undefined;`.
185
- if (text === '' || /^return\s*(undefined)?\s*;?$/.test(text)) return 'noop'
186
- return 'custom'
187
- }
188
-
189
- /**
190
- * For a function-like node return its block body (Node), or `null` if it is an
191
- * expression-bodied arrow whose expression is `undefined`/void, or `undefined`
192
- * if it is not function-like.
193
- */
194
- function functionBodyOf(node: Node): Node | null | undefined {
195
- let fn: Node | undefined
196
- if (Node.isArrowFunction(node) || Node.isFunctionExpression(node)) fn = node
197
- else if (Node.isMethodDeclaration(node)) fn = node
198
- else if (Node.isIdentifier(node)) {
199
- const value = valueOfIdentifier(node)
200
- if (value) return functionBodyOf(value)
201
- return undefined
202
- }
203
- if (!fn) return undefined
204
- const body = (fn as unknown as { getBody?: () => Node | undefined }).getBody?.()
205
- if (!body) return undefined
206
- if (Node.isBlock(body)) return body
207
- // Expression body: `async () => undefined`.
208
- if (body.getKind() === SyntaxKind.UndefinedKeyword || body.getText() === 'undefined') {
209
- return null
101
+ function isStubHandler(node: Node): boolean {
102
+ if (
103
+ !Node.isArrowFunction(node) &&
104
+ !Node.isFunctionExpression(node) &&
105
+ !Node.isFunctionDeclaration(node) &&
106
+ !Node.isMethodDeclaration(node)
107
+ ) {
108
+ return false
210
109
  }
211
- return body
212
- }
213
-
214
- /** A handler body that is a NotImplemented stub: only throws / `todo()`. */
215
- function isStubFunction(node: Node): boolean {
216
- const body = functionBodyOf(node)
217
- if (!body || body === null) return false
110
+ const body = node.getBody()
218
111
  if (!Node.isBlock(body)) return false
219
112
  const statements = body.getStatements()
220
- if (statements.length === 0) return false
221
- // Stub iff every statement is a throw, and there is at least one throw.
222
- let throws = 0
223
- for (const st of statements) {
224
- if (Node.isThrowStatement(st)) {
225
- throws++
226
- continue
227
- }
228
- return false
229
- }
230
- return throws > 0
113
+ return statements.length > 0 && statements.every(Node.isThrowStatement)
231
114
  }
232
115
 
233
116
  /**
234
- * Follow an `execute` arrow the delegated logic function it calls → that
235
- * function's defining file:line, chasing through barrel re-exports.
236
- */
237
- function resolveExecuteTarget(executeNode: Node): { file: string; line: number } | undefined {
238
- // Find the call inside the execute body that targets an imported symbol.
239
- const body = functionBodyOf(executeNode)
240
- const searchRoot = body && Node.isBlock(body) ? body : executeNode
241
- const calls = searchRoot.getDescendantsOfKind(SyntaxKind.CallExpression)
242
- for (const call of calls) {
243
- const expr = call.getExpression()
244
- if (!Node.isIdentifier(expr)) continue
245
- const resolved = resolveImportedFunction(expr)
246
- if (resolved) return resolved
247
- }
248
- return undefined
249
- }
250
-
251
- /** Resolve an identifier (a delegated logic fn) to its defining file:line. */
252
- function resolveImportedFunction(id: Node): { file: string; line: number } | undefined {
253
- if (!Node.isIdentifier(id)) return undefined
254
- const symbol = id.getSymbol()
255
- if (!symbol) return undefined
256
- // Walk through alias chains (import { x as y } / barrel re-export `export { x }`).
257
- let current = symbol
258
- const seen = new Set<string>()
259
- for (let i = 0; i < 12; i++) {
260
- const decls = current.getDeclarations()
261
- for (const decl of decls) {
262
- // A concrete function/variable declaration in a real file → done.
263
- if (Node.isFunctionDeclaration(decl)) {
264
- return { file: decl.getSourceFile().getFilePath(), line: decl.getStartLineNumber() }
265
- }
266
- if (Node.isVariableDeclaration(decl)) {
267
- const init = decl.getInitializer()
268
- if (init && (Node.isArrowFunction(init) || Node.isFunctionExpression(init))) {
269
- return { file: decl.getSourceFile().getFilePath(), line: decl.getStartLineNumber() }
270
- }
271
- }
272
- }
273
- // Try to hop to the aliased symbol (import specifier / export specifier).
274
- const aliased = trySymbolHop(current)
275
- if (!aliased) break
276
- const key = aliased
277
- .getDeclarations()
278
- .map((d) => d.getSourceFile().getFilePath() + ':' + d.getStartLineNumber())
279
- .join(',')
280
- if (seen.has(key)) break
281
- seen.add(key)
282
- current = aliased
283
- }
284
- // Last resort: resolve the module specifier of the import and grep the file.
285
- return resolveViaModuleSpecifier(id)
286
- }
287
-
288
- /** Hop one alias link via ts-morph's getAliasedSymbol, if any. */
289
- function trySymbolHop(symbol: import('ts-morph').Symbol): import('ts-morph').Symbol | undefined {
290
- try {
291
- const aliased = symbol.getAliasedSymbol?.()
292
- if (aliased && aliased !== symbol) return aliased
293
- } catch {
294
- /* not an alias */
295
- }
296
- return undefined
297
- }
298
-
299
- /**
300
- * Fallback resolution: find the import declaration that brought `id` in, load
301
- * the target module (and barrel re-exports), and locate the exported function.
302
- */
303
- function resolveViaModuleSpecifier(id: Node): { file: string; line: number } | undefined {
304
- if (!Node.isIdentifier(id)) return undefined
305
- const sourceFile = id.getSourceFile()
306
- const name = id.getText()
307
- for (const imp of sourceFile.getImportDeclarations()) {
308
- // Match the local binding name, capturing the original export name.
309
- let exportName: string | undefined
310
- for (const named of imp.getNamedImports()) {
311
- const local = named.getAliasNode()?.getText() ?? named.getName()
312
- if (local === name) {
313
- exportName = named.getName()
314
- break
315
- }
316
- }
317
- if (!exportName) continue
318
- const modPath = resolveModuleFile(sourceFile, imp.getModuleSpecifierValue())
319
- if (!modPath) continue
320
- const found = findExportInFile(modPath, exportName, new Set())
321
- if (found) return found
322
- }
323
- return undefined
324
- }
325
-
326
- /**
327
- * Find where `exportName` is *defined* within `file`, following barrel
328
- * `export { x } from './y'` / `export * from './y'` re-exports.
329
- */
330
- function findExportInFile(
331
- file: string,
332
- exportName: string,
333
- seen: Set<string>,
334
- ): { file: string; line: number } | undefined {
335
- if (seen.has(file)) return undefined
336
- seen.add(file)
337
- const project = newProject()
338
- const sf = tryAddFile(project, file)
339
- if (!sf) return undefined
340
-
341
- // 1) A local function declaration with that name.
342
- for (const fn of sf.getFunctions()) {
343
- if (fn.getName() === exportName && fn.isExported()) {
344
- return { file, line: fn.getStartLineNumber() }
345
- }
346
- }
347
- // 2) A local exported variable bound to a function.
348
- for (const v of sf.getVariableDeclarations()) {
349
- if (v.getName() === exportName && v.isExported()) {
350
- return { file, line: v.getStartLineNumber() }
351
- }
352
- }
353
- // 3) Re-export: `export { a, b as exportName } from './mod'`.
354
- for (const ex of sf.getExportDeclarations()) {
355
- const modSpec = ex.getModuleSpecifierValue()
356
- const target = modSpec ? resolveModuleFile(sf, modSpec) : undefined
357
- const named = ex.getNamedExports()
358
- if (named.length > 0) {
359
- for (const ne of named) {
360
- const exposed = ne.getAliasNode()?.getText() ?? ne.getName()
361
- if (exposed !== exportName) continue
362
- const original = ne.getName()
363
- if (target) {
364
- const found = findExportInFile(target, original, seen)
365
- if (found) return found
366
- }
367
- }
368
- } else if (target) {
369
- // `export * from './mod'` — search the target for the same name.
370
- const found = findExportInFile(target, exportName, seen)
371
- if (found) return found
372
- }
373
- }
374
- return undefined
375
- }
376
-
377
- /**
378
- * Collect every (owner, method, config) wiring from runtime/index.ts, across
379
- * both fixture styles, then build a HandlerLink per method.
117
+ * Discover modular `defineAction()` and `defineWorkflow()` declarations. Runtime
118
+ * admission remains authoritative for exhaustiveness; this is a source overlay.
380
119
  */
381
120
  export function buildHandlerLinks(args: {
382
121
  ir: SchemaIR | null
@@ -385,297 +124,38 @@ export function buildHandlerLinks(args: {
385
124
  const { ir, domainRoot } = args
386
125
  if (!domainRoot) return []
387
126
  const project = newProject()
388
- const legacyWiringRel = 'runtime/index.ts'
389
- const legacySf = tryAddFile(project, resolvePath(domainRoot, legacyWiringRel))
390
-
391
- const interfaceNames = new Set(ir ? Object.keys(ir.interfaces ?? {}) : [])
392
-
393
- const wired: WiredMethod[] = []
394
-
395
- if (legacySf) {
396
- for (const call of legacySf.getDescendantsOfKind(SyntaxKind.CallExpression)) {
397
- const name = calleeName(call)
398
- if (!name) continue
399
-
400
- // Style A: single-method helper — method(schema, 'Owner', 'name', { … }).
401
- if (SINGLE_METHOD_HELPERS.has(name)) {
402
- const owner = stringArg(call, 1)
403
- const method = stringArg(call, 2)
404
- const config = call.getArguments()[3]
405
- if (owner && method && config) {
406
- wired.push({
407
- owner,
408
- method,
409
- config,
410
- wiringLine: call.getStartLineNumber(),
411
- wiringFile: legacyWiringRel,
412
- style: 'legacy-config',
413
- })
414
- }
415
- continue
416
- }
417
-
418
- // Style B: group helper — classMethods(schema, 'Owner', { name: cfg, … }).
419
- if (GROUP_HELPERS.has(name)) {
420
- const owner = stringArg(call, 1)
421
- const mapArg = call.getArguments()[2]
422
- if (!owner || !mapArg || !Node.isObjectLiteralExpression(mapArg)) continue
423
- for (const prop of mapArg.getProperties()) {
424
- const valueNode = objectPropertyValue(prop)
425
- const methodName = propertyKey(prop)
426
- if (!methodName || !valueNode) continue
427
- wired.push({
428
- owner,
429
- method: methodName,
430
- config: valueNode,
431
- wiringLine: prop.getStartLineNumber(),
432
- wiringFile: legacyWiringRel,
433
- style: 'legacy-config',
434
- })
435
- }
436
- continue
127
+ for (const file of authoredTypeScriptFiles(domainRoot)) project.addSourceFileAtPath(file)
128
+ const links = new Map<string, HandlerLink>()
129
+
130
+ for (const source of project.getSourceFiles()) {
131
+ for (const call of source.getDescendantsOfKind(SyntaxKind.CallExpression)) {
132
+ const kind = declarationKind(call)
133
+ if (!kind) continue
134
+ const key = stringArgument(call, 0)
135
+ const handlerArgument = call.getArguments()[1]
136
+ if (!key || !handlerArgument) continue
137
+ const callable = declaredCallable(ir, key)
138
+ if (!callable) continue
139
+ const handler = resolveHandler(handlerArgument)
140
+ const handlerFile = relToRoot(domainRoot, handler.getSourceFile().getFilePath())
141
+ const link: HandlerLink = {
142
+ ...callable,
143
+ kind,
144
+ wiringFile: handlerFile,
145
+ wiringLine: call.getStartLineNumber(),
146
+ handlerFile,
147
+ handlerLine: handler.getStartLineNumber(),
148
+ implemented: !isStubHandler(handler),
437
149
  }
438
- }
439
- }
440
-
441
- // Current SDK layout: `implementation.ts` owns a plain
442
- // `handlers: { classes, interfaces, functions }` object passed to
443
- // `defineDomain` / `defineDomain.public`. Start from that call so inline
444
- // handler maps and arbitrarily named handler variables are both visible.
445
- // Class/interface entries point directly at handler functions; there is no
446
- // legacy `{ authorize, execute }` wrapper.
447
- for (const wiringRel of ['implementation.ts', 'domain.ts']) {
448
- const sf = tryAddFile(project, resolvePath(domainRoot, wiringRel))
449
- if (!sf) continue
450
-
451
- const handlerObjects: import('ts-morph').ObjectLiteralExpression[] = []
452
- const seenHandlerObjects = new Set<string>()
453
- const addHandlerObject = (candidate: Node | undefined) => {
454
- const handlersObject = candidate ? resolveObjectLiteral(candidate) : undefined
455
- if (!handlersObject) return
456
- const key = `${handlersObject.getSourceFile().getFilePath()}:${handlersObject.getStart()}`
457
- if (seenHandlerObjects.has(key)) return
458
- seenHandlerObjects.add(key)
459
- handlerObjects.push(handlersObject)
460
- }
461
-
462
- for (const call of sf.getDescendantsOfKind(SyntaxKind.CallExpression)) {
463
- if (!isDefineDomainCall(call)) continue
464
- const input = call.getArguments()[0]
465
- const inputObject = input ? resolveObjectLiteral(input) : undefined
466
- addHandlerObject(inputObject ? getProp(inputObject, 'handlers') : undefined)
467
- }
468
-
469
- // Tolerate pre-composition fixtures that expose a conventional `handlers`
470
- // variable without a parseable defineDomain call.
471
- const handlersDecl = sf.getVariableDeclaration('handlers')
472
- addHandlerObject(handlersDecl?.getInitializer())
473
-
474
- for (const handlersObject of handlerObjects) {
475
- const handlersFile = relToRoot(domainRoot, handlersObject.getSourceFile().getFilePath())
476
- collectDirectHandlerSection(wired, handlersObject, 'classes', 'class', handlersFile)
477
- collectDirectHandlerSection(wired, handlersObject, 'interfaces', 'interface', handlersFile)
478
- collectDirectFunctionHandlers(wired, handlersObject, ir?.domain ?? 'domain', handlersFile)
479
- }
480
- }
481
-
482
- // Deduplicate (owner, method): the group helper restates entries the single
483
- // helper already produced (e.g. my-domain wires each method, then groups them).
484
- // Prefer the entry whose config resolves to a real object (the single helper),
485
- // falling back to the group entry (which references a variable / todo()).
486
- const byKey = new Map<string, WiredMethod>()
487
- for (const w of wired) {
488
- const key = `${w.ownerKind ?? 'legacy'}:${w.owner}.${w.method}`
489
- const existing = byKey.get(key)
490
- if (!existing) {
491
- byKey.set(key, w)
492
- continue
493
- }
494
- // Prefer the one whose config is an object literal (richer info).
495
- const existingIsObj = Node.isObjectLiteralExpression(existing.config)
496
- const candidateIsObj = Node.isObjectLiteralExpression(w.config)
497
- if (candidateIsObj && !existingIsObj) byKey.set(key, w)
498
- }
499
-
500
- const links: HandlerLink[] = []
501
- for (const w of byKey.values()) {
502
- const ownerKind: 'class' | 'interface' | 'function' =
503
- w.ownerKind ?? (interfaceNames.has(w.owner) ? 'interface' : 'class')
504
- const isStatic = irMethodStatic(ir, w.owner, w.method, ownerKind)
505
-
506
- const link: HandlerLink = {
507
- owner: w.owner,
508
- ownerKind,
509
- method: w.method,
510
- static: isStatic,
511
- wiringFile: w.wiringFile,
512
- wiringLine: w.wiringLine,
513
- implemented: true,
514
- }
515
-
516
- let target: { file: string; line: number } | undefined
517
- if (w.style === 'direct-handler') {
518
- const direct = resolveDirectHandler(w.config)
519
- link.implemented = direct.implemented
520
- target = direct.target
521
- applyCanonicalMethodAuth(link, ir, w.owner, w.method, ownerKind)
522
- } else {
523
- const resolution = resolveConfigObject(w.config)
524
- link.implemented = resolution.implemented
525
- if (resolution.auth) link.auth = resolution.auth
526
- if (resolution.authorize) link.authorize = resolution.authorize
527
- if (resolution.authorizeSnippet) link.authorizeSnippet = resolution.authorizeSnippet
528
- if (resolution.executeNode) target = resolveExecuteTarget(resolution.executeNode)
529
- }
530
-
531
- if (target) {
532
- link.handlerFile = relToRoot(domainRoot, target.file)
533
- link.handlerLine = target.line
534
- const kernelCalls = scanKernelCalls(target.file)
150
+ const kernelCalls = scanKernelCalls(handler.getSourceFile().getFilePath())
535
151
  if (kernelCalls.length > 0) link.kernelCalls = kernelCalls
536
- } else {
537
- link.unlinked = true
152
+ links.set(`${callable.ownerKind}:${callable.owner}.${callable.method}`, link)
538
153
  }
539
-
540
- links.push(link)
541
154
  }
542
155
 
543
- // Stable order: by owner, then method.
544
- links.sort((a, b) =>
545
- a.owner === b.owner ? a.method.localeCompare(b.method) : a.owner.localeCompare(b.owner),
156
+ return [...links.values()].sort((left, right) =>
157
+ left.owner === right.owner
158
+ ? left.method.localeCompare(right.method)
159
+ : left.owner.localeCompare(right.owner),
546
160
  )
547
- return links
548
- }
549
-
550
- /** Whether a call is the current SDK composition entrypoint. */
551
- function isDefineDomainCall(call: CallExpression): boolean {
552
- const expression = unwrapExpression(call.getExpression())
553
- if (Node.isIdentifier(expression)) return expression.getText() === 'defineDomain'
554
- if (!Node.isPropertyAccessExpression(expression) || expression.getName() !== 'public') {
555
- return false
556
- }
557
- const receiver = unwrapExpression(expression.getExpression())
558
- return Node.isIdentifier(receiver) && receiver.getText() === 'defineDomain'
559
- }
560
-
561
- /** Collect current `handlers.classes/interfaces.Owner.method = fn` bindings. */
562
- function collectDirectHandlerSection(
563
- out: WiredMethod[],
564
- handlers: import('ts-morph').ObjectLiteralExpression,
565
- section: 'classes' | 'interfaces',
566
- ownerKind: 'class' | 'interface',
567
- wiringFile: string,
568
- ): void {
569
- const sectionValue = getProp(handlers, section)
570
- const sectionObject = sectionValue ? resolveObjectLiteral(sectionValue) : undefined
571
- if (!sectionObject) return
572
- for (const ownerProp of sectionObject.getProperties()) {
573
- const owner = propertyKey(ownerProp)
574
- const ownerValue = objectPropertyValue(ownerProp)
575
- const ownerObject = ownerValue ? resolveObjectLiteral(ownerValue) : undefined
576
- if (!owner || !ownerObject) continue
577
- for (const methodProp of ownerObject.getProperties()) {
578
- const method = propertyKey(methodProp)
579
- const handler = objectPropertyValue(methodProp)
580
- if (!method || !handler) continue
581
- out.push({
582
- owner,
583
- ownerKind,
584
- method,
585
- config: handler,
586
- wiringLine: methodProp.getStartLineNumber(),
587
- wiringFile,
588
- style: 'direct-handler',
589
- })
590
- }
591
- }
592
- }
593
-
594
- /** Collect current standalone `handlers.functions.name = fn` bindings. */
595
- function collectDirectFunctionHandlers(
596
- out: WiredMethod[],
597
- handlers: import('ts-morph').ObjectLiteralExpression,
598
- owner: string,
599
- wiringFile: string,
600
- ): void {
601
- const functionsValue = getProp(handlers, 'functions')
602
- const functions = functionsValue ? resolveObjectLiteral(functionsValue) : undefined
603
- if (!functions) return
604
- for (const functionProp of functions.getProperties()) {
605
- const method = propertyKey(functionProp)
606
- const handler = objectPropertyValue(functionProp)
607
- if (!method || !handler) continue
608
- out.push({
609
- owner,
610
- ownerKind: 'function',
611
- method,
612
- config: handler,
613
- wiringLine: functionProp.getStartLineNumber(),
614
- wiringFile,
615
- style: 'direct-handler',
616
- })
617
- }
618
- }
619
-
620
- /** Follow a direct SDK handler to its defining source node. */
621
- function resolveDirectHandler(config: Node): {
622
- implemented: boolean
623
- target?: { file: string; line: number }
624
- } {
625
- let value = unwrapExpression(config)
626
- if (Node.isIdentifier(value)) value = valueOfIdentifier(value) ?? value
627
- value = unwrapExpression(value)
628
- const concrete =
629
- Node.isArrowFunction(value) ||
630
- Node.isFunctionExpression(value) ||
631
- Node.isFunctionDeclaration(value) ||
632
- Node.isMethodDeclaration(value) ||
633
- Node.isCallExpression(value)
634
- if (!concrete) return { implemented: false }
635
- return {
636
- implemented: Node.isCallExpression(value) ? true : !isStubFunction(value),
637
- target: { file: value.getSourceFile().getFilePath(), line: value.getStartLineNumber() },
638
- }
639
- }
640
-
641
- /** Map the canonical callable auth declaration onto the existing Studio badge model. */
642
- function applyCanonicalMethodAuth(
643
- link: HandlerLink,
644
- ir: SchemaIR | null,
645
- owner: string,
646
- method: string,
647
- ownerKind: 'class' | 'interface' | 'function',
648
- ): void {
649
- const callable =
650
- ownerKind === 'function'
651
- ? ir?.functions?.[method]
652
- : ownerKind === 'interface'
653
- ? ir?.interfaces?.[owner]?.methods?.[method]
654
- : ir?.classes?.[owner]?.methods?.[method]
655
- const auth = callable?.auth
656
- if (auth) link.callableAuth = auth
657
- if (auth === 'anonymous') {
658
- link.auth = 'public'
659
- link.authorize = 'absent'
660
- } else if (auth === 'authorized') {
661
- link.auth = 'required'
662
- link.authorize = 'custom'
663
- } else if (auth === 'authenticated') {
664
- link.auth = 'required'
665
- link.authorize = 'absent'
666
- }
667
- }
668
-
669
- /** Look up a method's `static` flag from the IR (class or interface bucket). */
670
- function irMethodStatic(
671
- ir: SchemaIR | null,
672
- owner: string,
673
- method: string,
674
- ownerKind: 'class' | 'interface' | 'function',
675
- ): boolean {
676
- if (!ir) return false
677
- if (ownerKind === 'function') return true
678
- const bucket = ownerKind === 'interface' ? ir.interfaces?.[owner] : ir.classes?.[owner]
679
- const m = bucket?.methods?.[method]
680
- return m ? m.static === true : false
681
161
  }