@astrale-os/cli 1.0.0-beta.1 → 1.0.0-beta.2
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.
- package/README.md +7 -11
- package/dist/astrale.js +3737 -3523
- package/dist/public/connect-core.js +396 -392
- package/dist/public/keys/index.js +371 -378
- package/dist/public/paths/index.js +371 -378
- package/dist/types/lib/idp.d.ts +6 -0
- package/dist/types/lib/meta.d.ts +2 -2
- package/package.json +6 -4
- package/src/admin/binding.ts +168 -0
- package/src/admin/catalog/__tests__/client.test.ts +2 -2
- package/src/admin/catalog/client.ts +4 -9
- package/src/admin/instance/__tests__/client.test.ts +2 -2
- package/src/admin/instance/client.ts +6 -11
- package/src/commands/__tests__/auth-login.test.ts +54 -1
- package/src/commands/__tests__/call.test.ts +5 -1
- package/src/commands/__tests__/domain-install-operation.test.ts +23 -7
- package/src/commands/__tests__/domain-install-owned.test.ts +5 -1
- package/src/commands/__tests__/domain-list.test.ts +83 -4
- package/src/commands/__tests__/install-identity-override.test.ts +30 -19
- package/src/commands/__tests__/studio-descriptor.test.ts +22 -0
- package/src/commands/__tests__/update.test.ts +23 -16
- package/src/commands/domain/install.ts +11 -15
- package/src/commands/domain/list.ts +17 -12
- package/src/commands/studio.ts +20 -0
- package/src/commands/update.ts +9 -5
- package/src/connection/__tests__/errors.test.ts +16 -12
- package/src/connection/__tests__/exchange.test.ts +6 -5
- package/src/connection/exchange.ts +5 -4
- package/src/lib/__tests__/domain-publication.test.ts +144 -0
- package/src/lib/__tests__/idp-session.test.ts +22 -0
- package/src/lib/__tests__/idp.test.ts +2 -0
- package/src/lib/__tests__/instance.test.ts +6 -0
- package/src/lib/__tests__/studio-server-deps.test.ts +53 -16
- package/src/lib/__tests__/update.test.ts +12 -0
- package/src/lib/domain-publication.ts +102 -0
- package/src/lib/idp.ts +5 -1
- package/src/lib/instance.ts +6 -1
- package/src/lib/login-flow.ts +13 -3
- package/src/lib/meta.ts +2 -2
- package/src/lib/update.ts +4 -4
- package/src/program/__tests__/program.test.ts +1 -1
- package/studio/client/dist/assets/{elk-api-D2xgMJvi.js → elk-api-lwhFo7vB.js} +1 -1
- package/studio/client/dist/assets/index-B-c-smo9.js +8 -0
- package/studio/client/dist/assets/index-BckHuAWk.js +81 -0
- package/studio/client/dist/assets/index-C4aNQ6dz.css +1 -0
- package/studio/client/dist/index.html +2 -2
- package/studio/package.json +3 -1
- package/studio/server/agent/bridge/stdio.ts +8 -1
- package/studio/server/agent/conversation.test.ts +10 -5
- package/studio/server/agent/conversation.ts +56 -8
- package/studio/server/agent/harness/codex/loadout.ts +18 -21
- package/studio/server/agent/harness/gateway/config.ts +20 -18
- package/studio/server/agent/harness/selection.ts +9 -1
- package/studio/server/agent/prompts/system.ts +3 -2
- package/studio/server/agent/prompts/turn.ts +13 -4
- package/studio/server/agent/run/completion.ts +2 -2
- package/studio/server/agent/run/preparation.ts +6 -5
- package/studio/server/agent/run/transcript.test.ts +35 -1
- package/studio/server/agent/run/transcript.ts +169 -3
- package/studio/server/agent/run/usage.test.ts +24 -0
- package/studio/server/agent/run/usage.ts +26 -1
- package/studio/server/api/canvas.ts +44 -0
- package/studio/server/api/comments.ts +85 -0
- package/studio/server/api/context.ts +61 -0
- package/studio/server/api/deployment.ts +39 -0
- package/studio/server/api/documents.ts +55 -0
- package/studio/server/api/domain.ts +59 -0
- package/studio/server/api/http.ts +45 -0
- package/studio/server/api/project.ts +57 -0
- package/studio/server/api/schema.ts +12 -0
- package/studio/server/api/views.ts +57 -0
- package/studio/server/api/workspace.ts +60 -0
- package/studio/server/api.test.ts +147 -0
- package/studio/server/api.ts +20 -422
- package/studio/server/cache.test.ts +111 -0
- package/studio/server/cache.ts +163 -9
- package/studio/server/cli-consumers.test.ts +494 -0
- package/studio/server/cli.test.ts +88 -0
- package/studio/server/cli.ts +240 -0
- package/studio/server/client-package.ts +25 -1
- package/studio/server/domain.test.ts +30 -2
- package/studio/server/domain.ts +14 -2
- package/studio/server/environment/dotenv-preview.test.ts +39 -0
- package/studio/server/environment/dotenv-preview.ts +27 -0
- package/studio/server/environment/files.test.ts +53 -0
- package/studio/server/{state/env.ts → environment/files.ts} +7 -48
- package/studio/server/{state → handoff}/copy.ts +21 -6
- package/studio/server/handoff/service.test.ts +22 -0
- package/studio/server/{state/handoff.ts → handoff/service.ts} +21 -14
- package/studio/server/instances/active.ts +72 -0
- package/studio/server/instances/deploy-record.test.ts +31 -0
- package/studio/server/instances/deploy-record.ts +37 -0
- package/studio/server/instances/deploy.ts +56 -0
- package/studio/server/{state/instance.test.ts → instances/probe.test.ts} +25 -21
- package/studio/server/instances/probe.ts +67 -0
- package/studio/server/instances/status.test.ts +12 -0
- package/studio/server/instances/status.ts +49 -0
- package/studio/server/introspect/anatomy/client-tree.ts +57 -0
- package/studio/server/introspect/anatomy/env-fields.ts +66 -0
- package/studio/server/introspect/anatomy/schema-definition.ts +53 -0
- package/studio/server/introspect/anatomy/source.ts +181 -0
- package/studio/server/introspect/anatomy/views/legacy.ts +232 -0
- package/studio/server/introspect/anatomy/views/routes.ts +105 -0
- package/studio/server/introspect/anatomy/views.ts +58 -0
- package/studio/server/introspect/anatomy-extras.test.ts +91 -4
- package/studio/server/introspect/anatomy-extras.ts +7 -726
- package/studio/server/introspect/anatomy.ts +46 -19
- package/studio/server/introspect/bundle.ts +15 -6
- package/studio/server/introspect/canonical-schema.test.ts +79 -0
- package/studio/server/introspect/canonical-schema.ts +80 -18
- package/studio/server/introspect/config-preview.test.ts +32 -0
- package/studio/server/introspect/config-preview.ts +119 -0
- package/studio/server/introspect/diff.test.ts +7 -11
- package/studio/server/introspect/diff.ts +28 -37
- package/studio/server/introspect/extractor.ts +8 -6
- package/studio/server/introspect/hash.ts +5 -2
- package/studio/server/introspect/overlay-tsmorph.ts +7 -1305
- package/studio/server/introspect/revision.test.ts +54 -0
- package/studio/server/introspect/revision.ts +49 -0
- package/studio/server/introspect/runtime.test.ts +63 -5
- package/studio/server/introspect/runtime.ts +49 -3
- package/studio/server/introspect/schema-ir-json.ts +181 -0
- package/studio/server/introspect/source-overlay/annotations.ts +14 -0
- package/studio/server/introspect/source-overlay/handlers.ts +681 -0
- package/studio/server/introspect/source-overlay/kernel-calls.ts +49 -0
- package/studio/server/introspect/source-overlay/project.ts +248 -0
- package/studio/server/introspect/source-overlay/spans.ts +360 -0
- package/studio/server/json.ts +46 -0
- package/studio/server/lifecycle.ts +5 -1
- package/studio/server/sse.test.ts +26 -0
- package/studio/server/sse.ts +4 -1
- package/studio/server/state/baseline.test.ts +212 -1
- package/studio/server/state/baseline.ts +145 -29
- package/studio/server/state/comments.test.ts +66 -1
- package/studio/server/state/comments.ts +173 -10
- package/studio/server/state/context.ts +34 -2
- package/studio/server/state/documents.ts +42 -1
- package/studio/server/state/integrations.ts +27 -1
- package/studio/server/state/layout.test.ts +34 -0
- package/studio/server/state/layout.ts +44 -17
- package/studio/server/state/settings.ts +29 -22
- package/studio/server/state/store.test.ts +122 -0
- package/studio/server/state/store.ts +66 -17
- package/studio/server/state/visibility.ts +22 -7
- package/studio/server/views/model.test.ts +57 -0
- package/studio/server/views/model.ts +66 -0
- package/studio/server/views/runtime.ts +40 -0
- package/studio/server/views/selection-repository.test.ts +31 -0
- package/studio/server/views/selection-repository.ts +60 -0
- package/studio/server/views/session.test.ts +73 -0
- package/studio/server/views/session.ts +166 -0
- package/studio/server/{state/views.test.ts → views/target.test.ts} +8 -159
- package/studio/server/views/target.ts +248 -0
- package/studio/server/watch.ts +1 -1
- package/studio/server/workspace/catalog.test.ts +17 -0
- package/studio/server/workspace/catalog.ts +43 -0
- package/studio/server/{state → workspace}/create.test.ts +1 -1
- package/studio/server/{state → workspace}/create.ts +1 -1
- package/studio/server/{state → workspace}/git.ts +1 -1
- package/studio/server/workspace/updates.ts +83 -0
- package/studio/server/workspace-state.ts +1 -1
- package/studio/shared/contracts/README.md +16 -0
- package/studio/shared/contracts/agent.ts +270 -0
- package/studio/shared/contracts/runtime.ts +50 -0
- package/studio/shared/contracts/schema.ts +462 -0
- package/studio/shared/contracts/surface.test.ts +319 -0
- package/studio/shared/contracts/workspace.ts +274 -0
- package/studio/shared/layout.test.ts +116 -0
- package/studio/shared/schema/identity.test.ts +58 -0
- package/studio/shared/schema/identity.ts +82 -0
- package/studio/shared/types.ts +11 -1099
- package/studio/tsconfig.json +1 -1
- package/viewer/dist/main.js +35 -35
- package/studio/client/dist/assets/index-BMdnsIJA.css +0 -1
- package/studio/client/dist/assets/index-D-vRV8w7.js +0 -8
- package/studio/client/dist/assets/index-LGSWRrk8.js +0 -81
- package/studio/server/state/catalog.ts +0 -117
- package/studio/server/state/instance.ts +0 -280
- package/studio/server/state/updates.ts +0 -63
- package/studio/server/state/views.ts +0 -535
|
@@ -1,1307 +1,9 @@
|
|
|
1
|
-
import { existsSync, readFileSync, realpathSync } from 'node:fs'
|
|
2
|
-
import { dirname, isAbsolute, relative, resolve as resolvePath } from 'node:path'
|
|
3
1
|
/**
|
|
4
|
-
*
|
|
5
|
-
*
|
|
2
|
+
* Compatibility facade for the ts-morph source overlay.
|
|
3
|
+
*
|
|
4
|
+
* Keep this path stable for the overlay composer and focused tests. The
|
|
5
|
+
* implementation lives under source-overlay, split by responsibility.
|
|
6
6
|
*/
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
// ───────────────────────────── shared helpers ─────────────────────────────
|
|
12
|
-
|
|
13
|
-
/** A fresh, in-memory-ish ts-morph project: no tsconfig, tolerant of errors. */
|
|
14
|
-
function newProject(): Project {
|
|
15
|
-
return new Project({
|
|
16
|
-
useInMemoryFileSystem: false,
|
|
17
|
-
skipFileDependencyResolution: true,
|
|
18
|
-
skipLoadingLibFiles: true,
|
|
19
|
-
compilerOptions: {
|
|
20
|
-
allowJs: true,
|
|
21
|
-
// `allowImportingTsExtensions` keeps `.ts`-suffixed imports from blowing up.
|
|
22
|
-
allowImportingTsExtensions: true,
|
|
23
|
-
},
|
|
24
|
-
})
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/** Add a file to the project if it exists; returns undefined otherwise. */
|
|
28
|
-
function tryAddFile(project: Project, file: string): SourceFile | undefined {
|
|
29
|
-
try {
|
|
30
|
-
if (!existsSync(file)) return undefined
|
|
31
|
-
return project.getSourceFile(file) ?? project.addSourceFileAtPath(file)
|
|
32
|
-
} catch {
|
|
33
|
-
return undefined
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/** Path relative to `root`, POSIX-style ('schema/monitor.ts'), never absolute. */
|
|
38
|
-
function relToRoot(root: string, file: string): string {
|
|
39
|
-
const abs = isAbsolute(file) ? file : resolvePath(root, file)
|
|
40
|
-
const canonical = (value: string) => {
|
|
41
|
-
try {
|
|
42
|
-
return realpathSync(value)
|
|
43
|
-
} catch {
|
|
44
|
-
return value
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
return relative(canonical(root), canonical(abs)).split('\\').join('/')
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/** 1-based start line of a node. */
|
|
51
|
-
function startLine(node: Node): number {
|
|
52
|
-
return node.getStartLineNumber()
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* Harvest the leading JSDoc / line-comment block immediately above `node`,
|
|
57
|
-
* stripped of comment markers, collapsed to a single trimmed string.
|
|
58
|
-
*/
|
|
59
|
-
function leadingDoc(node: Node): string | undefined {
|
|
60
|
-
// Prefer real JSDoc nodes when present (ts-morph exposes them on many decls).
|
|
61
|
-
const anyNode = node as unknown as { getJsDocs?: () => Array<{ getText: () => string }> }
|
|
62
|
-
if (typeof anyNode.getJsDocs === 'function') {
|
|
63
|
-
const docs = anyNode.getJsDocs()
|
|
64
|
-
if (docs.length > 0) {
|
|
65
|
-
const text = docs.map((d) => d.getText()).join('\n')
|
|
66
|
-
const cleaned = cleanComment(text)
|
|
67
|
-
if (cleaned) return cleaned
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
// Fall back to raw leading comment ranges (covers `//` line comments too).
|
|
71
|
-
const ranges = node.getLeadingCommentRanges()
|
|
72
|
-
if (ranges.length === 0) return undefined
|
|
73
|
-
const raw = ranges.map((r) => r.getText()).join('\n')
|
|
74
|
-
const cleaned = cleanComment(raw)
|
|
75
|
-
return cleaned || undefined
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
/** Strip `/** */`, `//`, leading `*` gutters; collapse to a tidy single line. */
|
|
79
|
-
function cleanComment(raw: string): string {
|
|
80
|
-
const lines = raw
|
|
81
|
-
.replace(/\/\*\*?/g, '')
|
|
82
|
-
.replace(/\*\//g, '')
|
|
83
|
-
.split('\n')
|
|
84
|
-
.map((l) =>
|
|
85
|
-
l
|
|
86
|
-
.replace(/^\s*\*\s?/, '')
|
|
87
|
-
.replace(/^\s*\/\/\s?/, '')
|
|
88
|
-
.trim(),
|
|
89
|
-
)
|
|
90
|
-
.filter((l) => l.length > 0)
|
|
91
|
-
return lines.join(' ').replace(/\s+/g, ' ').trim()
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
/** The call's callee identifier name, e.g. `method` / `classMethods` / `todo`. */
|
|
95
|
-
function calleeName(call: CallExpression): string | undefined {
|
|
96
|
-
const expr = call.getExpression()
|
|
97
|
-
if (Node.isIdentifier(expr)) return expr.getText()
|
|
98
|
-
if (Node.isPropertyAccessExpression(expr)) return expr.getName()
|
|
99
|
-
return undefined
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
/** Unwrap a string literal argument to its value. */
|
|
103
|
-
function stringArg(call: CallExpression, index: number): string | undefined {
|
|
104
|
-
const arg = call.getArguments()[index]
|
|
105
|
-
if (!arg) return undefined
|
|
106
|
-
if (Node.isStringLiteral(arg) || Node.isNoSubstitutionTemplateLiteral(arg)) {
|
|
107
|
-
return arg.getLiteralText()
|
|
108
|
-
}
|
|
109
|
-
return undefined
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
// ───────────────────────────── handler links ─────────────────────────────
|
|
113
|
-
|
|
114
|
-
/** Recognized method-wiring calls after normalizing single-method and grouped helpers. */
|
|
115
|
-
interface WiredMethod {
|
|
116
|
-
owner: string
|
|
117
|
-
method: string
|
|
118
|
-
/** the object literal / call expression carrying the handler config */
|
|
119
|
-
config: Node
|
|
120
|
-
wiringLine: number
|
|
121
|
-
wiringFile: string
|
|
122
|
-
ownerKind?: 'class' | 'interface' | 'function'
|
|
123
|
-
style?: 'legacy-config' | 'direct-handler'
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
const SINGLE_METHOD_HELPERS = new Set(['method', 'remoteMethod'])
|
|
127
|
-
const GROUP_HELPERS = new Set([
|
|
128
|
-
'classMethods',
|
|
129
|
-
'interfaceMethods',
|
|
130
|
-
'remoteClassMethods',
|
|
131
|
-
'remoteInterfaceMethods',
|
|
132
|
-
])
|
|
133
|
-
|
|
134
|
-
type KernelToken = { token: string; label?: string }
|
|
135
|
-
|
|
136
|
-
/** Kernel-op idioms surfaced as `kernelCalls`; entries are longest-first. */
|
|
137
|
-
const KERNEL_TOKENS: KernelToken[] = [
|
|
138
|
-
{ token: 'graph.createEdge' },
|
|
139
|
-
{ token: 'graph.removeEdge' },
|
|
140
|
-
{ token: 'function.mutate' },
|
|
141
|
-
{ token: 'graph.children' },
|
|
142
|
-
{ token: 'function.get' },
|
|
143
|
-
{ token: 'graph.create' },
|
|
144
|
-
{ token: 'graph.update' },
|
|
145
|
-
{ token: 'graph.remove' },
|
|
146
|
-
{ token: 'graph.mutate' },
|
|
147
|
-
{ token: 'auth.revoke' },
|
|
148
|
-
{ token: 'graph.links' },
|
|
149
|
-
{ token: 'auth.grant' },
|
|
150
|
-
{ token: 'auth.check' },
|
|
151
|
-
{ token: 'graph.tree' },
|
|
152
|
-
{ token: 'graph.node' },
|
|
153
|
-
{ token: 'revokePerm', label: 'revokePerm (legacy)' },
|
|
154
|
-
{ token: 'checkPerm', label: 'checkPerm (legacy)' },
|
|
155
|
-
{ token: 'graph.get' },
|
|
156
|
-
{ token: 'grantPerm', label: 'grantPerm (legacy)' },
|
|
157
|
-
]
|
|
158
|
-
|
|
159
|
-
/**
|
|
160
|
-
* Resolve a method-config node (object literal `{ authorize, execute }` or a
|
|
161
|
-
* call like `todo('…')`) down to: the symbol that carries the real logic, plus
|
|
162
|
-
* whether it is a NotImplemented stub and the authorize flavour.
|
|
163
|
-
*/
|
|
164
|
-
interface ConfigResolution {
|
|
165
|
-
/** the node whose `execute` body we follow to a handler file */
|
|
166
|
-
executeNode?: Node
|
|
167
|
-
implemented: boolean
|
|
168
|
-
/** declared auth policy; defaults to 'required' when the prop is absent. */
|
|
169
|
-
auth?: 'public' | 'optional' | 'required'
|
|
170
|
-
/** authorize hook shape: 'absent' | 'noop' | 'custom'. */
|
|
171
|
-
authorize?: 'absent' | 'noop' | 'custom'
|
|
172
|
-
authorizeSnippet?: string
|
|
173
|
-
unlinkedReason?: 'no-config'
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
function resolveConfigObject(configNode: Node, seen = new Set<string>()): ConfigResolution {
|
|
177
|
-
const node = unwrapExpression(configNode)
|
|
178
|
-
const key = `${node.getSourceFile().getFilePath()}:${node.getStart()}`
|
|
179
|
-
if (seen.has(key)) return { implemented: false, unlinkedReason: 'no-config' }
|
|
180
|
-
seen.add(key)
|
|
181
|
-
|
|
182
|
-
if (Node.isCallExpression(node)) {
|
|
183
|
-
// Curried SDK helpers carry the handler config in their final argument:
|
|
184
|
-
// `remoteMethod()(schema, 'Issue', 'delete', { authorize, execute })`.
|
|
185
|
-
const lastArg = node.getArguments().at(-1)
|
|
186
|
-
if (lastArg) {
|
|
187
|
-
const candidate = unwrapExpression(lastArg)
|
|
188
|
-
if (Node.isObjectLiteralExpression(candidate) || Node.isIdentifier(candidate)) {
|
|
189
|
-
return resolveConfigObject(candidate, seen)
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
// Local factories such as `todo('Owner.name')` return the config object themselves.
|
|
194
|
-
const obj = followFactoryToObject(node)
|
|
195
|
-
if (obj) return classifyObjectLiteral(obj)
|
|
196
|
-
return { implemented: false, unlinkedReason: 'no-config' }
|
|
197
|
-
}
|
|
198
|
-
if (Node.isObjectLiteralExpression(node)) {
|
|
199
|
-
return classifyObjectLiteral(node)
|
|
200
|
-
}
|
|
201
|
-
// Handler maps normally reference a local or imported `remoteMethod` value.
|
|
202
|
-
if (Node.isIdentifier(node)) {
|
|
203
|
-
const value = valueOfIdentifier(node)
|
|
204
|
-
if (value) return resolveConfigObject(value, seen)
|
|
205
|
-
}
|
|
206
|
-
return { implemented: false, unlinkedReason: 'no-config' }
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
/** Remove syntax-only wrappers around a runtime value. */
|
|
210
|
-
function unwrapExpression(node: Node): Node {
|
|
211
|
-
let current = node
|
|
212
|
-
while (
|
|
213
|
-
Node.isParenthesizedExpression(current) ||
|
|
214
|
-
Node.isAsExpression(current) ||
|
|
215
|
-
Node.isSatisfiesExpression(current) ||
|
|
216
|
-
Node.isTypeAssertion(current)
|
|
217
|
-
) {
|
|
218
|
-
current = current.getExpression()
|
|
219
|
-
}
|
|
220
|
-
return current
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
/** Resolve a local or relatively imported identifier to its runtime value. */
|
|
224
|
-
function valueOfIdentifier(id: Node): Node | undefined {
|
|
225
|
-
if (!Node.isIdentifier(id)) return undefined
|
|
226
|
-
const decl = firstValueDeclaration(id)
|
|
227
|
-
if (decl && Node.isVariableDeclaration(decl)) return decl.getInitializer()
|
|
228
|
-
return resolveImportedValue(id)
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
/** Follow a named relative import to the exported value that defines it. */
|
|
232
|
-
function resolveImportedValue(id: Node): Node | undefined {
|
|
233
|
-
if (!Node.isIdentifier(id)) return undefined
|
|
234
|
-
const localName = id.getText()
|
|
235
|
-
const sourceFile = id.getSourceFile()
|
|
236
|
-
|
|
237
|
-
for (const imp of sourceFile.getImportDeclarations()) {
|
|
238
|
-
for (const named of imp.getNamedImports()) {
|
|
239
|
-
const local = named.getAliasNode()?.getText() ?? named.getName()
|
|
240
|
-
if (local !== localName) continue
|
|
241
|
-
const target = resolveModuleFile(sourceFile, imp.getModuleSpecifierValue())
|
|
242
|
-
if (!target) return undefined
|
|
243
|
-
return findExportedValue(id.getProject(), target, named.getName(), new Set<string>())
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
return undefined
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
/** Find an exported variable/function value, following local and barrel exports. */
|
|
250
|
-
function findExportedValue(
|
|
251
|
-
project: Project,
|
|
252
|
-
file: string,
|
|
253
|
-
exportName: string,
|
|
254
|
-
seen: Set<string>,
|
|
255
|
-
): Node | undefined {
|
|
256
|
-
const key = `${file}:${exportName}`
|
|
257
|
-
if (seen.has(key)) return undefined
|
|
258
|
-
seen.add(key)
|
|
259
|
-
const sf = tryAddFile(project, file)
|
|
260
|
-
if (!sf) return undefined
|
|
261
|
-
|
|
262
|
-
const localValue = (name: string, exportedOnly: boolean): Node | undefined => {
|
|
263
|
-
const variable = sf
|
|
264
|
-
.getVariableDeclarations()
|
|
265
|
-
.find(
|
|
266
|
-
(candidate) => candidate.getName() === name && (!exportedOnly || candidate.isExported()),
|
|
267
|
-
)
|
|
268
|
-
if (variable) return variable.getInitializer()
|
|
269
|
-
return sf
|
|
270
|
-
.getFunctions()
|
|
271
|
-
.find(
|
|
272
|
-
(candidate) => candidate.getName() === name && (!exportedOnly || candidate.isExported()),
|
|
273
|
-
)
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
const direct = localValue(exportName, true)
|
|
277
|
-
if (direct) return direct
|
|
278
|
-
|
|
279
|
-
for (const ex of sf.getExportDeclarations()) {
|
|
280
|
-
const modSpec = ex.getModuleSpecifierValue()
|
|
281
|
-
const target = modSpec ? resolveModuleFile(sf, modSpec) : undefined
|
|
282
|
-
const named = ex.getNamedExports()
|
|
283
|
-
if (named.length > 0) {
|
|
284
|
-
for (const specifier of named) {
|
|
285
|
-
const exposed = specifier.getAliasNode()?.getText() ?? specifier.getName()
|
|
286
|
-
if (exposed !== exportName) continue
|
|
287
|
-
const original = specifier.getName()
|
|
288
|
-
if (target) return findExportedValue(project, target, original, seen)
|
|
289
|
-
return localValue(original, false)
|
|
290
|
-
}
|
|
291
|
-
} else if (target) {
|
|
292
|
-
const found = findExportedValue(project, target, exportName, seen)
|
|
293
|
-
if (found) return found
|
|
294
|
-
}
|
|
295
|
-
}
|
|
296
|
-
return undefined
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
/** Given `todo('x')`, find the factory's `return { … }` object literal. */
|
|
300
|
-
function followFactoryToObject(call: CallExpression): Node | undefined {
|
|
301
|
-
const expr = call.getExpression()
|
|
302
|
-
if (!Node.isIdentifier(expr)) return undefined
|
|
303
|
-
const decl = firstValueDeclaration(expr)
|
|
304
|
-
if (!decl) return undefined
|
|
305
|
-
let body: Node | undefined
|
|
306
|
-
if (Node.isVariableDeclaration(decl)) {
|
|
307
|
-
body = decl.getInitializer()
|
|
308
|
-
} else if (Node.isFunctionDeclaration(decl)) {
|
|
309
|
-
body = decl
|
|
310
|
-
}
|
|
311
|
-
if (!body) return undefined
|
|
312
|
-
// Arrow returning an object literal directly: `(name) => ({ … })`.
|
|
313
|
-
if (Node.isArrowFunction(body)) {
|
|
314
|
-
const arrowBody = body.getBody()
|
|
315
|
-
if (Node.isParenthesizedExpression(arrowBody)) {
|
|
316
|
-
const inner = arrowBody.getExpression()
|
|
317
|
-
if (Node.isObjectLiteralExpression(inner)) return inner
|
|
318
|
-
}
|
|
319
|
-
if (Node.isObjectLiteralExpression(arrowBody)) return arrowBody
|
|
320
|
-
// Block body with a `return { … }`.
|
|
321
|
-
const ret = arrowBody.getFirstDescendantByKind?.(SyntaxKind.ReturnStatement)
|
|
322
|
-
const retExpr = ret?.getExpression()
|
|
323
|
-
if (retExpr && Node.isObjectLiteralExpression(retExpr)) return retExpr
|
|
324
|
-
}
|
|
325
|
-
if (Node.isFunctionDeclaration(body) || Node.isFunctionExpression(body)) {
|
|
326
|
-
const ret = body.getFirstDescendantByKind(SyntaxKind.ReturnStatement)
|
|
327
|
-
const retExpr = ret?.getExpression()
|
|
328
|
-
if (retExpr && Node.isObjectLiteralExpression(retExpr)) return retExpr
|
|
329
|
-
}
|
|
330
|
-
return undefined
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
/** Inspect `{ auth, authorize, execute }` for the auth policy, authorize shape,
|
|
334
|
-
* parsed permission checks, source snippets, and execute stub-ness. */
|
|
335
|
-
function classifyObjectLiteral(obj: Node): ConfigResolution {
|
|
336
|
-
if (!Node.isObjectLiteralExpression(obj)) {
|
|
337
|
-
return { implemented: false, unlinkedReason: 'no-config' }
|
|
338
|
-
}
|
|
339
|
-
const executeProp = getProp(obj, 'execute')
|
|
340
|
-
const authorizeProp = getProp(obj, 'authorize')
|
|
341
|
-
|
|
342
|
-
const auth = authPolicy(obj)
|
|
343
|
-
const authorize = authorizeProp ? authorizeFlavour(authorizeProp) : 'absent'
|
|
344
|
-
const authorizeSnippet = authorizeProp ? snippetOf(authorizeProp) : undefined
|
|
345
|
-
const common = { auth, authorize, authorizeSnippet } as const
|
|
346
|
-
|
|
347
|
-
if (!executeProp) {
|
|
348
|
-
return { implemented: false, ...common, unlinkedReason: 'no-config' }
|
|
349
|
-
}
|
|
350
|
-
const implemented = !isStubFunction(executeProp)
|
|
351
|
-
return { executeNode: executeProp, implemented, ...common }
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
/** The declared `auth` policy on a method config; defaults to 'required'. */
|
|
355
|
-
function authPolicy(obj: Node): 'public' | 'optional' | 'required' {
|
|
356
|
-
const v = stringLiteralOfProp(obj, 'auth')
|
|
357
|
-
return v === 'public' || v === 'optional' ? v : 'required'
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
const SNIPPET_CAP = 1200
|
|
361
|
-
|
|
362
|
-
/** Source text of a node, capped for a hover preview. */
|
|
363
|
-
function snippetOf(node: Node): string {
|
|
364
|
-
const t = node.getText()
|
|
365
|
-
return t.length > SNIPPET_CAP ? `${t.slice(0, SNIPPET_CAP)}\n/* … */` : t
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
/** Get the value node of an object-literal property (handles shorthand). */
|
|
369
|
-
function getProp(obj: Node, name: string): Node | undefined {
|
|
370
|
-
if (!Node.isObjectLiteralExpression(obj)) return undefined
|
|
371
|
-
const prop = obj.getProperty(name)
|
|
372
|
-
if (!prop) return undefined
|
|
373
|
-
if (Node.isPropertyAssignment(prop)) return prop.getInitializer()
|
|
374
|
-
if (Node.isShorthandPropertyAssignment(prop)) return prop.getNameNode()
|
|
375
|
-
if (Node.isMethodDeclaration(prop)) return prop
|
|
376
|
-
return prop
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
/** 'noop' when the authorize body just returns undefined/void, else 'custom'.
|
|
380
|
-
* Follows identifier references (e.g. `authorize: allow` → `const allow = …`). */
|
|
381
|
-
function authorizeFlavour(node: Node): 'noop' | 'custom' {
|
|
382
|
-
const fnBody = functionBodyOf(node)
|
|
383
|
-
if (fnBody === undefined) return 'custom' // opaque (unresolved identifier) → assume real
|
|
384
|
-
if (fnBody === null) return 'noop' // expression arrow returning `undefined`
|
|
385
|
-
const text = fnBody.getText().replace(/[{}]/g, '').trim()
|
|
386
|
-
// Empty block, or a single `return;` / `return undefined;`.
|
|
387
|
-
if (text === '' || /^return\s*(undefined)?\s*;?$/.test(text)) return 'noop'
|
|
388
|
-
return 'custom'
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
/**
|
|
392
|
-
* For a function-like node return its block body (Node), or `null` if it is an
|
|
393
|
-
* expression-bodied arrow whose expression is `undefined`/void, or `undefined`
|
|
394
|
-
* if it is not function-like.
|
|
395
|
-
*/
|
|
396
|
-
function functionBodyOf(node: Node): Node | null | undefined {
|
|
397
|
-
let fn: Node | undefined
|
|
398
|
-
if (Node.isArrowFunction(node) || Node.isFunctionExpression(node)) fn = node
|
|
399
|
-
else if (Node.isMethodDeclaration(node)) fn = node
|
|
400
|
-
else if (Node.isIdentifier(node)) {
|
|
401
|
-
const value = valueOfIdentifier(node)
|
|
402
|
-
if (value) return functionBodyOf(value)
|
|
403
|
-
return undefined
|
|
404
|
-
}
|
|
405
|
-
if (!fn) return undefined
|
|
406
|
-
const body = (fn as unknown as { getBody?: () => Node | undefined }).getBody?.()
|
|
407
|
-
if (!body) return undefined
|
|
408
|
-
if (Node.isBlock(body)) return body
|
|
409
|
-
// Expression body: `async () => undefined`.
|
|
410
|
-
if (body.getKind() === SyntaxKind.UndefinedKeyword || body.getText() === 'undefined') {
|
|
411
|
-
return null
|
|
412
|
-
}
|
|
413
|
-
return body
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
/** A handler body that is a NotImplemented stub: only throws / `todo()`. */
|
|
417
|
-
function isStubFunction(node: Node): boolean {
|
|
418
|
-
const body = functionBodyOf(node)
|
|
419
|
-
if (!body || body === null) return false
|
|
420
|
-
if (!Node.isBlock(body)) return false
|
|
421
|
-
const statements = body.getStatements()
|
|
422
|
-
if (statements.length === 0) return false
|
|
423
|
-
// Stub iff every statement is a throw, and there is at least one throw.
|
|
424
|
-
let throws = 0
|
|
425
|
-
for (const st of statements) {
|
|
426
|
-
if (Node.isThrowStatement(st)) {
|
|
427
|
-
throws++
|
|
428
|
-
continue
|
|
429
|
-
}
|
|
430
|
-
return false
|
|
431
|
-
}
|
|
432
|
-
return throws > 0
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
/** First value/declaration node a name resolves to (definition, not reference). */
|
|
436
|
-
function firstValueDeclaration(node: Node): Node | undefined {
|
|
437
|
-
const idNode = Node.isIdentifier(node)
|
|
438
|
-
? node
|
|
439
|
-
: node.getFirstDescendantByKind(SyntaxKind.Identifier)
|
|
440
|
-
if (!idNode || !Node.isIdentifier(idNode)) return undefined
|
|
441
|
-
const symbol = idNode.getSymbol()
|
|
442
|
-
if (!symbol) return undefined
|
|
443
|
-
const decls = symbol.getDeclarations()
|
|
444
|
-
return decls[0]
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
/**
|
|
448
|
-
* Follow an `execute` arrow → the delegated logic function it calls → that
|
|
449
|
-
* function's defining file:line, chasing through barrel re-exports.
|
|
450
|
-
*/
|
|
451
|
-
function resolveExecuteTarget(executeNode: Node): { file: string; line: number } | undefined {
|
|
452
|
-
// Find the call inside the execute body that targets an imported symbol.
|
|
453
|
-
const body = functionBodyOf(executeNode)
|
|
454
|
-
const searchRoot = body && Node.isBlock(body) ? body : executeNode
|
|
455
|
-
const calls = searchRoot.getDescendantsOfKind(SyntaxKind.CallExpression)
|
|
456
|
-
for (const call of calls) {
|
|
457
|
-
const expr = call.getExpression()
|
|
458
|
-
if (!Node.isIdentifier(expr)) continue
|
|
459
|
-
const resolved = resolveImportedFunction(expr)
|
|
460
|
-
if (resolved) return resolved
|
|
461
|
-
}
|
|
462
|
-
return undefined
|
|
463
|
-
}
|
|
464
|
-
|
|
465
|
-
/** Resolve an identifier (a delegated logic fn) to its defining file:line. */
|
|
466
|
-
function resolveImportedFunction(id: Node): { file: string; line: number } | undefined {
|
|
467
|
-
if (!Node.isIdentifier(id)) return undefined
|
|
468
|
-
const symbol = id.getSymbol()
|
|
469
|
-
if (!symbol) return undefined
|
|
470
|
-
// Walk through alias chains (import { x as y } / barrel re-export `export { x }`).
|
|
471
|
-
let current = symbol
|
|
472
|
-
const seen = new Set<string>()
|
|
473
|
-
for (let i = 0; i < 12; i++) {
|
|
474
|
-
const decls = current.getDeclarations()
|
|
475
|
-
for (const decl of decls) {
|
|
476
|
-
// A concrete function/variable declaration in a real file → done.
|
|
477
|
-
if (Node.isFunctionDeclaration(decl)) {
|
|
478
|
-
return { file: decl.getSourceFile().getFilePath(), line: decl.getStartLineNumber() }
|
|
479
|
-
}
|
|
480
|
-
if (Node.isVariableDeclaration(decl)) {
|
|
481
|
-
const init = decl.getInitializer()
|
|
482
|
-
if (init && (Node.isArrowFunction(init) || Node.isFunctionExpression(init))) {
|
|
483
|
-
return { file: decl.getSourceFile().getFilePath(), line: decl.getStartLineNumber() }
|
|
484
|
-
}
|
|
485
|
-
}
|
|
486
|
-
}
|
|
487
|
-
// Try to hop to the aliased symbol (import specifier / export specifier).
|
|
488
|
-
const aliased = trySymbolHop(current)
|
|
489
|
-
if (!aliased) break
|
|
490
|
-
const key = aliased
|
|
491
|
-
.getDeclarations()
|
|
492
|
-
.map((d) => d.getSourceFile().getFilePath() + ':' + d.getStartLineNumber())
|
|
493
|
-
.join(',')
|
|
494
|
-
if (seen.has(key)) break
|
|
495
|
-
seen.add(key)
|
|
496
|
-
current = aliased
|
|
497
|
-
}
|
|
498
|
-
// Last resort: resolve the module specifier of the import and grep the file.
|
|
499
|
-
return resolveViaModuleSpecifier(id)
|
|
500
|
-
}
|
|
501
|
-
|
|
502
|
-
/** Hop one alias link via ts-morph's getAliasedSymbol, if any. */
|
|
503
|
-
function trySymbolHop(symbol: import('ts-morph').Symbol): import('ts-morph').Symbol | undefined {
|
|
504
|
-
try {
|
|
505
|
-
const aliased = symbol.getAliasedSymbol?.()
|
|
506
|
-
if (aliased && aliased !== symbol) return aliased
|
|
507
|
-
} catch {
|
|
508
|
-
/* not an alias */
|
|
509
|
-
}
|
|
510
|
-
return undefined
|
|
511
|
-
}
|
|
512
|
-
|
|
513
|
-
/**
|
|
514
|
-
* Fallback resolution: find the import declaration that brought `id` in, load
|
|
515
|
-
* the target module (and barrel re-exports), and locate the exported function.
|
|
516
|
-
*/
|
|
517
|
-
function resolveViaModuleSpecifier(id: Node): { file: string; line: number } | undefined {
|
|
518
|
-
if (!Node.isIdentifier(id)) return undefined
|
|
519
|
-
const sourceFile = id.getSourceFile()
|
|
520
|
-
const name = id.getText()
|
|
521
|
-
for (const imp of sourceFile.getImportDeclarations()) {
|
|
522
|
-
// Match the local binding name, capturing the original export name.
|
|
523
|
-
let exportName: string | undefined
|
|
524
|
-
for (const named of imp.getNamedImports()) {
|
|
525
|
-
const local = named.getAliasNode()?.getText() ?? named.getName()
|
|
526
|
-
if (local === name) {
|
|
527
|
-
exportName = named.getName()
|
|
528
|
-
break
|
|
529
|
-
}
|
|
530
|
-
}
|
|
531
|
-
if (!exportName) continue
|
|
532
|
-
const modPath = resolveModuleFile(sourceFile, imp.getModuleSpecifierValue())
|
|
533
|
-
if (!modPath) continue
|
|
534
|
-
const found = findExportInFile(modPath, exportName, new Set())
|
|
535
|
-
if (found) return found
|
|
536
|
-
}
|
|
537
|
-
return undefined
|
|
538
|
-
}
|
|
539
|
-
|
|
540
|
-
/** Resolve a relative module specifier to a concrete .ts file path on disk. */
|
|
541
|
-
function resolveModuleFile(from: SourceFile, spec: string): string | undefined {
|
|
542
|
-
if (!spec.startsWith('.')) {
|
|
543
|
-
// Current SDK projects use package `imports` aliases such as
|
|
544
|
-
// `#actions/risk`. Resolve them from the authored file's directory through
|
|
545
|
-
// Bun so the overlay follows the same project-local map as runtime imports.
|
|
546
|
-
if (!spec.startsWith('#')) return undefined
|
|
547
|
-
try {
|
|
548
|
-
const resolved = Bun.resolveSync(spec, dirname(from.getFilePath()))
|
|
549
|
-
if (existsSync(resolved)) return resolved
|
|
550
|
-
} catch {
|
|
551
|
-
return undefined
|
|
552
|
-
}
|
|
553
|
-
return undefined
|
|
554
|
-
}
|
|
555
|
-
const baseDir = dirname(from.getFilePath())
|
|
556
|
-
const base = resolvePath(baseDir, spec)
|
|
557
|
-
const sourceBase = base.replace(/\.(?:[cm]?js|jsx)$/, '')
|
|
558
|
-
const candidates = [
|
|
559
|
-
base,
|
|
560
|
-
sourceBase.endsWith('.ts') ? sourceBase : `${sourceBase}.ts`,
|
|
561
|
-
sourceBase.endsWith('.tsx') ? sourceBase : `${sourceBase}.tsx`,
|
|
562
|
-
`${base}/index.ts`,
|
|
563
|
-
`${base}/index.tsx`,
|
|
564
|
-
]
|
|
565
|
-
for (const c of candidates) if (existsSync(c)) return c
|
|
566
|
-
return undefined
|
|
567
|
-
}
|
|
568
|
-
|
|
569
|
-
/**
|
|
570
|
-
* Find where `exportName` is *defined* within `file`, following barrel
|
|
571
|
-
* `export { x } from './y'` / `export * from './y'` re-exports.
|
|
572
|
-
*/
|
|
573
|
-
function findExportInFile(
|
|
574
|
-
file: string,
|
|
575
|
-
exportName: string,
|
|
576
|
-
seen: Set<string>,
|
|
577
|
-
): { file: string; line: number } | undefined {
|
|
578
|
-
if (seen.has(file)) return undefined
|
|
579
|
-
seen.add(file)
|
|
580
|
-
const project = newProject()
|
|
581
|
-
const sf = tryAddFile(project, file)
|
|
582
|
-
if (!sf) return undefined
|
|
583
|
-
|
|
584
|
-
// 1) A local function declaration with that name.
|
|
585
|
-
for (const fn of sf.getFunctions()) {
|
|
586
|
-
if (fn.getName() === exportName && fn.isExported()) {
|
|
587
|
-
return { file, line: fn.getStartLineNumber() }
|
|
588
|
-
}
|
|
589
|
-
}
|
|
590
|
-
// 2) A local exported variable bound to a function.
|
|
591
|
-
for (const v of sf.getVariableDeclarations()) {
|
|
592
|
-
if (v.getName() === exportName && v.isExported()) {
|
|
593
|
-
return { file, line: v.getStartLineNumber() }
|
|
594
|
-
}
|
|
595
|
-
}
|
|
596
|
-
// 3) Re-export: `export { a, b as exportName } from './mod'`.
|
|
597
|
-
for (const ex of sf.getExportDeclarations()) {
|
|
598
|
-
const modSpec = ex.getModuleSpecifierValue()
|
|
599
|
-
const target = modSpec ? resolveModuleFile(sf, modSpec) : undefined
|
|
600
|
-
const named = ex.getNamedExports()
|
|
601
|
-
if (named.length > 0) {
|
|
602
|
-
for (const ne of named) {
|
|
603
|
-
const exposed = ne.getAliasNode()?.getText() ?? ne.getName()
|
|
604
|
-
if (exposed !== exportName) continue
|
|
605
|
-
const original = ne.getName()
|
|
606
|
-
if (target) {
|
|
607
|
-
const found = findExportInFile(target, original, seen)
|
|
608
|
-
if (found) return found
|
|
609
|
-
}
|
|
610
|
-
}
|
|
611
|
-
} else if (target) {
|
|
612
|
-
// `export * from './mod'` — search the target for the same name.
|
|
613
|
-
const found = findExportInFile(target, exportName, seen)
|
|
614
|
-
if (found) return found
|
|
615
|
-
}
|
|
616
|
-
}
|
|
617
|
-
return undefined
|
|
618
|
-
}
|
|
619
|
-
|
|
620
|
-
/** Scan handler file text for kernel-op tokens. */
|
|
621
|
-
function scanKernelCalls(file: string): string[] {
|
|
622
|
-
let text: string
|
|
623
|
-
try {
|
|
624
|
-
text = readFileSync(file, 'utf8')
|
|
625
|
-
} catch {
|
|
626
|
-
return []
|
|
627
|
-
}
|
|
628
|
-
const found: string[] = []
|
|
629
|
-
let work = text
|
|
630
|
-
for (const entry of KERNEL_TOKENS) {
|
|
631
|
-
if (work.includes(entry.token)) {
|
|
632
|
-
found.push(entry.label ?? entry.token)
|
|
633
|
-
// Blank out matches so `::getLinks` doesn't also count as `::getLink`.
|
|
634
|
-
work = work.split(entry.token).join(' '.repeat(entry.token.length))
|
|
635
|
-
}
|
|
636
|
-
}
|
|
637
|
-
return found
|
|
638
|
-
}
|
|
639
|
-
|
|
640
|
-
/**
|
|
641
|
-
* Collect every (owner, method, config) wiring from runtime/index.ts, across
|
|
642
|
-
* both fixture styles, then build a HandlerLink per method.
|
|
643
|
-
*/
|
|
644
|
-
export function buildHandlerLinks(args: {
|
|
645
|
-
ir: SchemaIR | null
|
|
646
|
-
domainRoot: string
|
|
647
|
-
}): HandlerLink[] {
|
|
648
|
-
const { ir, domainRoot } = args
|
|
649
|
-
if (!domainRoot) return []
|
|
650
|
-
const project = newProject()
|
|
651
|
-
const legacyWiringRel = 'runtime/index.ts'
|
|
652
|
-
const legacySf = tryAddFile(project, resolvePath(domainRoot, legacyWiringRel))
|
|
653
|
-
|
|
654
|
-
const interfaceNames = new Set(ir ? Object.keys(ir.interfaces ?? {}) : [])
|
|
655
|
-
|
|
656
|
-
const wired: WiredMethod[] = []
|
|
657
|
-
|
|
658
|
-
if (legacySf) {
|
|
659
|
-
for (const call of legacySf.getDescendantsOfKind(SyntaxKind.CallExpression)) {
|
|
660
|
-
const name = calleeName(call)
|
|
661
|
-
if (!name) continue
|
|
662
|
-
|
|
663
|
-
// Style A: single-method helper — method(schema, 'Owner', 'name', { … }).
|
|
664
|
-
if (SINGLE_METHOD_HELPERS.has(name)) {
|
|
665
|
-
const owner = stringArg(call, 1)
|
|
666
|
-
const method = stringArg(call, 2)
|
|
667
|
-
const config = call.getArguments()[3]
|
|
668
|
-
if (owner && method && config) {
|
|
669
|
-
wired.push({
|
|
670
|
-
owner,
|
|
671
|
-
method,
|
|
672
|
-
config,
|
|
673
|
-
wiringLine: call.getStartLineNumber(),
|
|
674
|
-
wiringFile: legacyWiringRel,
|
|
675
|
-
style: 'legacy-config',
|
|
676
|
-
})
|
|
677
|
-
}
|
|
678
|
-
continue
|
|
679
|
-
}
|
|
680
|
-
|
|
681
|
-
// Style B: group helper — classMethods(schema, 'Owner', { name: cfg, … }).
|
|
682
|
-
if (GROUP_HELPERS.has(name)) {
|
|
683
|
-
const owner = stringArg(call, 1)
|
|
684
|
-
const mapArg = call.getArguments()[2]
|
|
685
|
-
if (!owner || !mapArg || !Node.isObjectLiteralExpression(mapArg)) continue
|
|
686
|
-
for (const prop of mapArg.getProperties()) {
|
|
687
|
-
const valueNode = objectPropertyValue(prop)
|
|
688
|
-
const methodName = propertyKey(prop)
|
|
689
|
-
if (!methodName || !valueNode) continue
|
|
690
|
-
wired.push({
|
|
691
|
-
owner,
|
|
692
|
-
method: methodName,
|
|
693
|
-
config: valueNode,
|
|
694
|
-
wiringLine: prop.getStartLineNumber(),
|
|
695
|
-
wiringFile: legacyWiringRel,
|
|
696
|
-
style: 'legacy-config',
|
|
697
|
-
})
|
|
698
|
-
}
|
|
699
|
-
continue
|
|
700
|
-
}
|
|
701
|
-
}
|
|
702
|
-
}
|
|
703
|
-
|
|
704
|
-
// Current SDK layout: `implementation.ts` owns a plain
|
|
705
|
-
// `handlers: { classes, interfaces, functions }` object passed to
|
|
706
|
-
// `defineDomain` / `defineDomain.public`. Start from that call so inline
|
|
707
|
-
// handler maps and arbitrarily named handler variables are both visible.
|
|
708
|
-
// Class/interface entries point directly at handler functions; there is no
|
|
709
|
-
// legacy `{ authorize, execute }` wrapper.
|
|
710
|
-
for (const wiringRel of ['implementation.ts', 'domain.ts']) {
|
|
711
|
-
const sf = tryAddFile(project, resolvePath(domainRoot, wiringRel))
|
|
712
|
-
if (!sf) continue
|
|
713
|
-
|
|
714
|
-
const handlerObjects: import('ts-morph').ObjectLiteralExpression[] = []
|
|
715
|
-
const seenHandlerObjects = new Set<string>()
|
|
716
|
-
const addHandlerObject = (candidate: Node | undefined) => {
|
|
717
|
-
const handlersObject = candidate ? resolveObjectLiteral(candidate) : undefined
|
|
718
|
-
if (!handlersObject) return
|
|
719
|
-
const key = `${handlersObject.getSourceFile().getFilePath()}:${handlersObject.getStart()}`
|
|
720
|
-
if (seenHandlerObjects.has(key)) return
|
|
721
|
-
seenHandlerObjects.add(key)
|
|
722
|
-
handlerObjects.push(handlersObject)
|
|
723
|
-
}
|
|
724
|
-
|
|
725
|
-
for (const call of sf.getDescendantsOfKind(SyntaxKind.CallExpression)) {
|
|
726
|
-
if (!isDefineDomainCall(call)) continue
|
|
727
|
-
const input = call.getArguments()[0]
|
|
728
|
-
const inputObject = input ? resolveObjectLiteral(input) : undefined
|
|
729
|
-
addHandlerObject(inputObject ? getProp(inputObject, 'handlers') : undefined)
|
|
730
|
-
}
|
|
731
|
-
|
|
732
|
-
// Tolerate pre-composition fixtures that expose a conventional `handlers`
|
|
733
|
-
// variable without a parseable defineDomain call.
|
|
734
|
-
const handlersDecl = sf.getVariableDeclaration('handlers')
|
|
735
|
-
addHandlerObject(handlersDecl?.getInitializer())
|
|
736
|
-
|
|
737
|
-
for (const handlersObject of handlerObjects) {
|
|
738
|
-
const handlersFile = relToRoot(domainRoot, handlersObject.getSourceFile().getFilePath())
|
|
739
|
-
collectDirectHandlerSection(wired, handlersObject, 'classes', 'class', handlersFile)
|
|
740
|
-
collectDirectHandlerSection(wired, handlersObject, 'interfaces', 'interface', handlersFile)
|
|
741
|
-
collectDirectFunctionHandlers(wired, handlersObject, ir?.domain ?? 'domain', handlersFile)
|
|
742
|
-
}
|
|
743
|
-
}
|
|
744
|
-
|
|
745
|
-
// Deduplicate (owner, method): the group helper restates entries the single
|
|
746
|
-
// helper already produced (e.g. my-domain wires each method, then groups them).
|
|
747
|
-
// Prefer the entry whose config resolves to a real object (the single helper),
|
|
748
|
-
// falling back to the group entry (which references a variable / todo()).
|
|
749
|
-
const byKey = new Map<string, WiredMethod>()
|
|
750
|
-
for (const w of wired) {
|
|
751
|
-
const key = `${w.ownerKind ?? 'legacy'}:${w.owner}.${w.method}`
|
|
752
|
-
const existing = byKey.get(key)
|
|
753
|
-
if (!existing) {
|
|
754
|
-
byKey.set(key, w)
|
|
755
|
-
continue
|
|
756
|
-
}
|
|
757
|
-
// Prefer the one whose config is an object literal (richer info).
|
|
758
|
-
const existingIsObj = Node.isObjectLiteralExpression(existing.config)
|
|
759
|
-
const candidateIsObj = Node.isObjectLiteralExpression(w.config)
|
|
760
|
-
if (candidateIsObj && !existingIsObj) byKey.set(key, w)
|
|
761
|
-
}
|
|
762
|
-
|
|
763
|
-
const links: HandlerLink[] = []
|
|
764
|
-
for (const w of byKey.values()) {
|
|
765
|
-
const ownerKind: 'class' | 'interface' | 'function' =
|
|
766
|
-
w.ownerKind ?? (interfaceNames.has(w.owner) ? 'interface' : 'class')
|
|
767
|
-
const isStatic = irMethodStatic(ir, w.owner, w.method, ownerKind)
|
|
768
|
-
|
|
769
|
-
const link: HandlerLink = {
|
|
770
|
-
owner: w.owner,
|
|
771
|
-
ownerKind,
|
|
772
|
-
method: w.method,
|
|
773
|
-
static: isStatic,
|
|
774
|
-
wiringFile: w.wiringFile,
|
|
775
|
-
wiringLine: w.wiringLine,
|
|
776
|
-
implemented: true,
|
|
777
|
-
}
|
|
778
|
-
|
|
779
|
-
let target: { file: string; line: number } | undefined
|
|
780
|
-
if (w.style === 'direct-handler') {
|
|
781
|
-
const direct = resolveDirectHandler(w.config)
|
|
782
|
-
link.implemented = direct.implemented
|
|
783
|
-
target = direct.target
|
|
784
|
-
applyCanonicalMethodAuth(link, ir, w.owner, w.method, ownerKind)
|
|
785
|
-
} else {
|
|
786
|
-
const resolution = resolveConfigObject(w.config)
|
|
787
|
-
link.implemented = resolution.implemented
|
|
788
|
-
if (resolution.auth) link.auth = resolution.auth
|
|
789
|
-
if (resolution.authorize) link.authorize = resolution.authorize
|
|
790
|
-
if (resolution.authorizeSnippet) link.authorizeSnippet = resolution.authorizeSnippet
|
|
791
|
-
if (resolution.executeNode) target = resolveExecuteTarget(resolution.executeNode)
|
|
792
|
-
}
|
|
793
|
-
|
|
794
|
-
if (target) {
|
|
795
|
-
link.handlerFile = relToRoot(domainRoot, target.file)
|
|
796
|
-
link.handlerLine = target.line
|
|
797
|
-
const kernelCalls = scanKernelCalls(target.file)
|
|
798
|
-
if (kernelCalls.length > 0) link.kernelCalls = kernelCalls
|
|
799
|
-
} else {
|
|
800
|
-
link.unlinked = true
|
|
801
|
-
}
|
|
802
|
-
|
|
803
|
-
links.push(link)
|
|
804
|
-
}
|
|
805
|
-
|
|
806
|
-
// Stable order: by owner, then method.
|
|
807
|
-
links.sort((a, b) =>
|
|
808
|
-
a.owner === b.owner ? a.method.localeCompare(b.method) : a.owner.localeCompare(b.owner),
|
|
809
|
-
)
|
|
810
|
-
return links
|
|
811
|
-
}
|
|
812
|
-
|
|
813
|
-
/** Whether a call is the current SDK composition entrypoint. */
|
|
814
|
-
function isDefineDomainCall(call: CallExpression): boolean {
|
|
815
|
-
const expression = unwrapExpression(call.getExpression())
|
|
816
|
-
if (Node.isIdentifier(expression)) return expression.getText() === 'defineDomain'
|
|
817
|
-
if (!Node.isPropertyAccessExpression(expression) || expression.getName() !== 'public') {
|
|
818
|
-
return false
|
|
819
|
-
}
|
|
820
|
-
const receiver = unwrapExpression(expression.getExpression())
|
|
821
|
-
return Node.isIdentifier(receiver) && receiver.getText() === 'defineDomain'
|
|
822
|
-
}
|
|
823
|
-
|
|
824
|
-
/** Resolve a value to an object literal, following local/imported identifiers. */
|
|
825
|
-
function resolveObjectLiteral(
|
|
826
|
-
node: Node,
|
|
827
|
-
seen = new Set<string>(),
|
|
828
|
-
): import('ts-morph').ObjectLiteralExpression | undefined {
|
|
829
|
-
const value = unwrapExpression(node)
|
|
830
|
-
const key = `${value.getSourceFile().getFilePath()}:${value.getStart()}`
|
|
831
|
-
if (seen.has(key)) return undefined
|
|
832
|
-
seen.add(key)
|
|
833
|
-
if (Node.isObjectLiteralExpression(value)) return value
|
|
834
|
-
if (Node.isIdentifier(value)) {
|
|
835
|
-
const resolved = valueOfIdentifier(value)
|
|
836
|
-
if (resolved) return resolveObjectLiteral(resolved, seen)
|
|
837
|
-
}
|
|
838
|
-
return undefined
|
|
839
|
-
}
|
|
840
|
-
|
|
841
|
-
/** Value carried by an object-literal property, including shorthand/method syntax. */
|
|
842
|
-
function objectPropertyValue(prop: Node): Node | undefined {
|
|
843
|
-
if (Node.isPropertyAssignment(prop)) return prop.getInitializer()
|
|
844
|
-
if (Node.isShorthandPropertyAssignment(prop)) return prop.getNameNode()
|
|
845
|
-
if (Node.isMethodDeclaration(prop)) return prop
|
|
846
|
-
return undefined
|
|
847
|
-
}
|
|
848
|
-
|
|
849
|
-
/** Collect current `handlers.classes/interfaces.Owner.method = fn` bindings. */
|
|
850
|
-
function collectDirectHandlerSection(
|
|
851
|
-
out: WiredMethod[],
|
|
852
|
-
handlers: import('ts-morph').ObjectLiteralExpression,
|
|
853
|
-
section: 'classes' | 'interfaces',
|
|
854
|
-
ownerKind: 'class' | 'interface',
|
|
855
|
-
wiringFile: string,
|
|
856
|
-
): void {
|
|
857
|
-
const sectionValue = getProp(handlers, section)
|
|
858
|
-
const sectionObject = sectionValue ? resolveObjectLiteral(sectionValue) : undefined
|
|
859
|
-
if (!sectionObject) return
|
|
860
|
-
for (const ownerProp of sectionObject.getProperties()) {
|
|
861
|
-
const owner = propertyKey(ownerProp)
|
|
862
|
-
const ownerValue = objectPropertyValue(ownerProp)
|
|
863
|
-
const ownerObject = ownerValue ? resolveObjectLiteral(ownerValue) : undefined
|
|
864
|
-
if (!owner || !ownerObject) continue
|
|
865
|
-
for (const methodProp of ownerObject.getProperties()) {
|
|
866
|
-
const method = propertyKey(methodProp)
|
|
867
|
-
const handler = objectPropertyValue(methodProp)
|
|
868
|
-
if (!method || !handler) continue
|
|
869
|
-
out.push({
|
|
870
|
-
owner,
|
|
871
|
-
ownerKind,
|
|
872
|
-
method,
|
|
873
|
-
config: handler,
|
|
874
|
-
wiringLine: methodProp.getStartLineNumber(),
|
|
875
|
-
wiringFile,
|
|
876
|
-
style: 'direct-handler',
|
|
877
|
-
})
|
|
878
|
-
}
|
|
879
|
-
}
|
|
880
|
-
}
|
|
881
|
-
|
|
882
|
-
/** Collect current standalone `handlers.functions.name = fn` bindings. */
|
|
883
|
-
function collectDirectFunctionHandlers(
|
|
884
|
-
out: WiredMethod[],
|
|
885
|
-
handlers: import('ts-morph').ObjectLiteralExpression,
|
|
886
|
-
owner: string,
|
|
887
|
-
wiringFile: string,
|
|
888
|
-
): void {
|
|
889
|
-
const functionsValue = getProp(handlers, 'functions')
|
|
890
|
-
const functions = functionsValue ? resolveObjectLiteral(functionsValue) : undefined
|
|
891
|
-
if (!functions) return
|
|
892
|
-
for (const functionProp of functions.getProperties()) {
|
|
893
|
-
const method = propertyKey(functionProp)
|
|
894
|
-
const handler = objectPropertyValue(functionProp)
|
|
895
|
-
if (!method || !handler) continue
|
|
896
|
-
out.push({
|
|
897
|
-
owner,
|
|
898
|
-
ownerKind: 'function',
|
|
899
|
-
method,
|
|
900
|
-
config: handler,
|
|
901
|
-
wiringLine: functionProp.getStartLineNumber(),
|
|
902
|
-
wiringFile,
|
|
903
|
-
style: 'direct-handler',
|
|
904
|
-
})
|
|
905
|
-
}
|
|
906
|
-
}
|
|
907
|
-
|
|
908
|
-
/** Follow a direct SDK handler to its defining source node. */
|
|
909
|
-
function resolveDirectHandler(config: Node): {
|
|
910
|
-
implemented: boolean
|
|
911
|
-
target?: { file: string; line: number }
|
|
912
|
-
} {
|
|
913
|
-
let value = unwrapExpression(config)
|
|
914
|
-
if (Node.isIdentifier(value)) value = valueOfIdentifier(value) ?? value
|
|
915
|
-
value = unwrapExpression(value)
|
|
916
|
-
const concrete =
|
|
917
|
-
Node.isArrowFunction(value) ||
|
|
918
|
-
Node.isFunctionExpression(value) ||
|
|
919
|
-
Node.isFunctionDeclaration(value) ||
|
|
920
|
-
Node.isMethodDeclaration(value) ||
|
|
921
|
-
Node.isCallExpression(value)
|
|
922
|
-
if (!concrete) return { implemented: false }
|
|
923
|
-
return {
|
|
924
|
-
implemented: Node.isCallExpression(value) ? true : !isStubFunction(value),
|
|
925
|
-
target: { file: value.getSourceFile().getFilePath(), line: value.getStartLineNumber() },
|
|
926
|
-
}
|
|
927
|
-
}
|
|
928
|
-
|
|
929
|
-
/** Map the canonical callable auth declaration onto the existing Studio badge model. */
|
|
930
|
-
function applyCanonicalMethodAuth(
|
|
931
|
-
link: HandlerLink,
|
|
932
|
-
ir: SchemaIR | null,
|
|
933
|
-
owner: string,
|
|
934
|
-
method: string,
|
|
935
|
-
ownerKind: 'class' | 'interface' | 'function',
|
|
936
|
-
): void {
|
|
937
|
-
const callable =
|
|
938
|
-
ownerKind === 'function'
|
|
939
|
-
? ir?.functions?.[method]
|
|
940
|
-
: ownerKind === 'interface'
|
|
941
|
-
? ir?.interfaces?.[owner]?.methods?.[method]
|
|
942
|
-
: ir?.classes?.[owner]?.methods?.[method]
|
|
943
|
-
const auth = callable?.auth
|
|
944
|
-
if (auth) link.callableAuth = auth
|
|
945
|
-
if (auth === 'anonymous') {
|
|
946
|
-
link.auth = 'public'
|
|
947
|
-
link.authorize = 'absent'
|
|
948
|
-
} else if (auth === 'authorized') {
|
|
949
|
-
link.auth = 'required'
|
|
950
|
-
link.authorize = 'custom'
|
|
951
|
-
} else if (auth === 'authenticated') {
|
|
952
|
-
link.auth = 'required'
|
|
953
|
-
link.authorize = 'absent'
|
|
954
|
-
}
|
|
955
|
-
}
|
|
956
|
-
|
|
957
|
-
/** Look up a method's `static` flag from the IR (class or interface bucket). */
|
|
958
|
-
function irMethodStatic(
|
|
959
|
-
ir: SchemaIR | null,
|
|
960
|
-
owner: string,
|
|
961
|
-
method: string,
|
|
962
|
-
ownerKind: 'class' | 'interface' | 'function',
|
|
963
|
-
): boolean {
|
|
964
|
-
if (!ir) return false
|
|
965
|
-
if (ownerKind === 'function') return true
|
|
966
|
-
const bucket = ownerKind === 'interface' ? ir.interfaces?.[owner] : ir.classes?.[owner]
|
|
967
|
-
const m = bucket?.methods?.[method]
|
|
968
|
-
return m ? m.static === true : false
|
|
969
|
-
}
|
|
970
|
-
|
|
971
|
-
// ───────────────────────────── source spans ─────────────────────────────
|
|
972
|
-
|
|
973
|
-
const DECL_HELPERS: Record<string, 'node' | 'interface' | 'edge' | 'function'> = {
|
|
974
|
-
nodeClass: 'node',
|
|
975
|
-
nodeInterface: 'interface',
|
|
976
|
-
edgeClass: 'edge',
|
|
977
|
-
fn: 'function',
|
|
978
|
-
}
|
|
979
|
-
|
|
980
|
-
/** Recognize both legacy `edgeClass(...)` and current
|
|
981
|
-
* `edgeClass.directed/undirected({...})` authoring forms. */
|
|
982
|
-
function declarationHelper(
|
|
983
|
-
call: CallExpression,
|
|
984
|
-
): 'node' | 'interface' | 'edge' | 'function' | undefined {
|
|
985
|
-
const direct = calleeName(call)
|
|
986
|
-
if (direct && direct in DECL_HELPERS) return DECL_HELPERS[direct]
|
|
987
|
-
const expression = call.getExpression()
|
|
988
|
-
if (
|
|
989
|
-
Node.isPropertyAccessExpression(expression) &&
|
|
990
|
-
(expression.getName() === 'directed' || expression.getName() === 'undirected') &&
|
|
991
|
-
expression.getExpression().getText() === 'edgeClass'
|
|
992
|
-
) {
|
|
993
|
-
return 'edge'
|
|
994
|
-
}
|
|
995
|
-
return undefined
|
|
996
|
-
}
|
|
997
|
-
|
|
998
|
-
/**
|
|
999
|
-
* A schema member's true NAME + section, resolved from the `defineSchema` map.
|
|
1000
|
-
* The declaration helpers (nodeClass/nodeInterface/edgeClass) carry no name — a
|
|
1001
|
-
* member is named by the KEY it is registered under, not by its variable — and a
|
|
1002
|
-
* domain may register a class and an interface under the SAME name (the
|
|
1003
|
-
* intentional same-name pattern: the `iUser` interface alongside the `User`
|
|
1004
|
-
* class). So the map is the sole authority for both the anchor name and the
|
|
1005
|
-
* interface-vs-class distinction; the variable identifier alone tells us neither.
|
|
1006
|
-
*/
|
|
1007
|
-
interface MemberName {
|
|
1008
|
-
schemaName: string
|
|
1009
|
-
section: 'interface' | 'class' | 'function' // the `classes` map also holds edge classes
|
|
1010
|
-
}
|
|
1011
|
-
|
|
1012
|
-
interface MemberNameMap {
|
|
1013
|
-
/** Exact declaration initializer, robust to imported/local aliases. */
|
|
1014
|
-
byValue: Map<string, MemberName>
|
|
1015
|
-
/** Legacy fallback when a symbol cannot be resolved. */
|
|
1016
|
-
byIdentifier: Map<string, MemberName>
|
|
1017
|
-
}
|
|
1018
|
-
|
|
1019
|
-
/** Stable source coordinate for the value behind a local/imported alias. */
|
|
1020
|
-
function memberValueKey(node: Node, seen = new Set<string>()): string | undefined {
|
|
1021
|
-
const value = unwrapExpression(node)
|
|
1022
|
-
const key = `${value.getSourceFile().getFilePath()}:${value.getStart()}`
|
|
1023
|
-
if (seen.has(key)) return undefined
|
|
1024
|
-
seen.add(key)
|
|
1025
|
-
if (Node.isIdentifier(value)) {
|
|
1026
|
-
const resolved = valueOfIdentifier(value)
|
|
1027
|
-
if (resolved) return memberValueKey(resolved, seen)
|
|
1028
|
-
}
|
|
1029
|
-
return key
|
|
1030
|
-
}
|
|
1031
|
-
|
|
1032
|
-
/** Map each registered member VARIABLE (as referenced in `defineSchema`) to its
|
|
1033
|
-
* schema name + section, from the domain's own schema files — never node_modules
|
|
1034
|
-
* (a dependency's `defineSchema` is not this domain's). */
|
|
1035
|
-
function buildMemberNameMap(files: SourceFile[]): MemberNameMap {
|
|
1036
|
-
const map: MemberNameMap = {
|
|
1037
|
-
byValue: new Map<string, MemberName>(),
|
|
1038
|
-
byIdentifier: new Map<string, MemberName>(),
|
|
1039
|
-
}
|
|
1040
|
-
for (const sf of files) {
|
|
1041
|
-
if (sf.getFilePath().includes('/node_modules/')) continue
|
|
1042
|
-
for (const call of sf.getDescendantsOfKind(SyntaxKind.CallExpression)) {
|
|
1043
|
-
if (calleeName(call) !== 'defineSchema') continue
|
|
1044
|
-
const input = call.getArguments()[1]
|
|
1045
|
-
const cfg = input ? resolveObjectLiteral(input) : undefined
|
|
1046
|
-
if (!cfg) continue
|
|
1047
|
-
collectSchemaSection(map, cfg, 'interfaces', 'interface')
|
|
1048
|
-
collectSchemaSection(map, cfg, 'classes', 'class')
|
|
1049
|
-
collectSchemaSection(map, cfg, 'functions', 'function')
|
|
1050
|
-
}
|
|
1051
|
-
}
|
|
1052
|
-
return map
|
|
1053
|
-
}
|
|
1054
|
-
|
|
1055
|
-
/** Record `variable → { schemaName, section }` for one `defineSchema` section,
|
|
1056
|
-
* handling both `Key: alias` and shorthand `Key`. */
|
|
1057
|
-
function collectSchemaSection(
|
|
1058
|
-
map: MemberNameMap,
|
|
1059
|
-
cfg: Node,
|
|
1060
|
-
prop: 'interfaces' | 'classes' | 'functions',
|
|
1061
|
-
section: 'interface' | 'class' | 'function',
|
|
1062
|
-
): void {
|
|
1063
|
-
const obj = getObjectProp(cfg, prop)
|
|
1064
|
-
if (!obj) return
|
|
1065
|
-
for (const p of obj.getProperties()) {
|
|
1066
|
-
const schemaName = propertyKey(p)
|
|
1067
|
-
const memberValue = objectPropertyValue(p)
|
|
1068
|
-
if (!schemaName || !memberValue) continue
|
|
1069
|
-
const member = { schemaName, section } satisfies MemberName
|
|
1070
|
-
const valueKey = memberValueKey(memberValue)
|
|
1071
|
-
if (valueKey) map.byValue.set(valueKey, member)
|
|
1072
|
-
const unwrapped = unwrapExpression(memberValue)
|
|
1073
|
-
if (Node.isIdentifier(unwrapped)) map.byIdentifier.set(unwrapped.getText(), member)
|
|
1074
|
-
}
|
|
1075
|
-
}
|
|
1076
|
-
|
|
1077
|
-
/**
|
|
1078
|
-
* The anchor namespace ('class' | 'interface' | 'edge') for a declared member.
|
|
1079
|
-
* The `defineSchema` SECTION is authoritative for interface-vs-class — a
|
|
1080
|
-
* `nodeClass` whose name collides with a same-named interface must still anchor
|
|
1081
|
-
* as a class. Within the class section, an `edgeClass` (or an IR edge type)
|
|
1082
|
-
* anchors as 'edge'. Falls back to the declaration helper when the member isn't
|
|
1083
|
-
* in a parseable `defineSchema` map (e.g. an imported kernel member).
|
|
1084
|
-
*/
|
|
1085
|
-
function resolveMemberKind(
|
|
1086
|
-
ir: SchemaIR | null,
|
|
1087
|
-
name: string,
|
|
1088
|
-
section: 'interface' | 'class' | 'function' | undefined,
|
|
1089
|
-
helperKind: 'node' | 'interface' | 'edge' | 'function',
|
|
1090
|
-
): 'class' | 'interface' | 'edge' | 'function' {
|
|
1091
|
-
if (section === 'function' || helperKind === 'function') return 'function'
|
|
1092
|
-
if (section === 'interface') return 'interface'
|
|
1093
|
-
const isEdge = helperKind === 'edge' || ir?.classes?.[name]?.type === 'edge'
|
|
1094
|
-
if (section === 'class') return isEdge ? 'edge' : 'class'
|
|
1095
|
-
if (helperKind === 'interface') return 'interface'
|
|
1096
|
-
return isEdge ? 'edge' : 'class'
|
|
1097
|
-
}
|
|
1098
|
-
|
|
1099
|
-
export function buildSourceSpans(args: {
|
|
1100
|
-
ir: SchemaIR | null
|
|
1101
|
-
schemaDir: string
|
|
1102
|
-
}): Record<string, SourceSpan> {
|
|
1103
|
-
const { ir, schemaDir } = args
|
|
1104
|
-
if (!schemaDir || !existsSync(schemaDir)) return {}
|
|
1105
|
-
|
|
1106
|
-
// The domain root is the parent of the schema dir (spans are relative to it).
|
|
1107
|
-
const domainRoot = dirname(schemaDir.replace(/\/$/, ''))
|
|
1108
|
-
|
|
1109
|
-
const project = newProject()
|
|
1110
|
-
let files: string[] = []
|
|
1111
|
-
try {
|
|
1112
|
-
const added = project.addSourceFilesAtPaths(`${schemaDir.replace(/\/$/, '')}/**/*.ts`)
|
|
1113
|
-
files = added.map((f) => f.getFilePath())
|
|
1114
|
-
} catch {
|
|
1115
|
-
return {}
|
|
1116
|
-
}
|
|
1117
|
-
|
|
1118
|
-
const spans: Record<string, SourceSpan> = {}
|
|
1119
|
-
|
|
1120
|
-
const sourceFiles = files.map((f) => project.getSourceFile(f)).filter((f): f is SourceFile => !!f)
|
|
1121
|
-
// The domain's `defineSchema` map is authoritative for each member's name +
|
|
1122
|
-
// interface-vs-class kind, so an aliased interface (`iUser`→`User`) and a class
|
|
1123
|
-
// sharing its name (`User`) each anchor correctly instead of colliding.
|
|
1124
|
-
const memberNames = buildMemberNameMap(sourceFiles)
|
|
1125
|
-
|
|
1126
|
-
for (const sf of sourceFiles) {
|
|
1127
|
-
const fileRel = relToRoot(domainRoot, sf.getFilePath())
|
|
1128
|
-
|
|
1129
|
-
for (const v of sf.getVariableDeclarations()) {
|
|
1130
|
-
if (!v.isExported()) continue
|
|
1131
|
-
const init = v.getInitializer()
|
|
1132
|
-
if (!init || !Node.isCallExpression(init)) continue
|
|
1133
|
-
const helperKind = declarationHelper(init)
|
|
1134
|
-
if (!helperKind) continue
|
|
1135
|
-
const valueKey = memberValueKey(init)
|
|
1136
|
-
const member =
|
|
1137
|
-
(valueKey ? memberNames.byValue.get(valueKey) : undefined) ??
|
|
1138
|
-
memberNames.byIdentifier.get(v.getName())
|
|
1139
|
-
const name = member?.schemaName ?? v.getName()
|
|
1140
|
-
const ns = resolveMemberKind(ir, name, member?.section, helperKind)
|
|
1141
|
-
|
|
1142
|
-
const stmt = v.getVariableStatement() ?? v
|
|
1143
|
-
spans[`${ns}.${name}`] = makeSpan(domainRoot, fileRel, stmt, v)
|
|
1144
|
-
|
|
1145
|
-
// The single object-literal argument: nodeClass({ properties, methods })
|
|
1146
|
-
// and current edgeClass.directed({ source, target, properties }).
|
|
1147
|
-
const cfgArg = init.getArguments()[0]
|
|
1148
|
-
if (ns !== 'function' && cfgArg && Node.isObjectLiteralExpression(cfgArg)) {
|
|
1149
|
-
collectPropsAndMethods(spans, ns, name, cfgArg, domainRoot, fileRel)
|
|
1150
|
-
}
|
|
1151
|
-
|
|
1152
|
-
// Edges: endpoints are the first two args; props live in the third.
|
|
1153
|
-
if (ns === 'edge') {
|
|
1154
|
-
collectEdge(spans, name, init, domainRoot, fileRel)
|
|
1155
|
-
}
|
|
1156
|
-
}
|
|
1157
|
-
}
|
|
1158
|
-
|
|
1159
|
-
return spans
|
|
1160
|
-
}
|
|
1161
|
-
|
|
1162
|
-
/** Build a SourceSpan, harvesting leading doc from the declaration `docNode`. */
|
|
1163
|
-
function makeSpan(domainRoot: string, fileRel: string, spanNode: Node, docNode: Node): SourceSpan {
|
|
1164
|
-
const span: SourceSpan = {
|
|
1165
|
-
file: fileRel,
|
|
1166
|
-
startLine: spanNode.getStartLineNumber(),
|
|
1167
|
-
endLine: spanNode.getEndLineNumber(),
|
|
1168
|
-
}
|
|
1169
|
-
const doc = leadingDoc(docNode) ?? leadingDoc(spanNode)
|
|
1170
|
-
if (doc) span.doc = doc
|
|
1171
|
-
return span
|
|
1172
|
-
}
|
|
1173
|
-
|
|
1174
|
-
/** Record `<ns>.<Name>.property.<p>` and `.method.<m>` from a config object. */
|
|
1175
|
-
function collectPropsAndMethods(
|
|
1176
|
-
spans: Record<string, SourceSpan>,
|
|
1177
|
-
ns: string,
|
|
1178
|
-
name: string,
|
|
1179
|
-
cfg: Node,
|
|
1180
|
-
domainRoot: string,
|
|
1181
|
-
fileRel: string,
|
|
1182
|
-
): void {
|
|
1183
|
-
if (!Node.isObjectLiteralExpression(cfg)) return
|
|
1184
|
-
const propsObj = getObjectProp(cfg, 'properties') ?? getObjectProp(cfg, 'props')
|
|
1185
|
-
if (propsObj) {
|
|
1186
|
-
for (const p of propsObj.getProperties()) {
|
|
1187
|
-
const pName = propertyKey(p)
|
|
1188
|
-
if (!pName) continue
|
|
1189
|
-
spans[`${ns}.${name}.property.${pName}`] = makeSpan(domainRoot, fileRel, p, p)
|
|
1190
|
-
}
|
|
1191
|
-
}
|
|
1192
|
-
const methodsObj = getObjectProp(cfg, 'methods')
|
|
1193
|
-
if (methodsObj) {
|
|
1194
|
-
for (const m of methodsObj.getProperties()) {
|
|
1195
|
-
const mName = propertyKey(m)
|
|
1196
|
-
if (!mName) continue
|
|
1197
|
-
spans[`${ns}.${name}.method.${mName}`] = makeSpan(domainRoot, fileRel, m, m)
|
|
1198
|
-
}
|
|
1199
|
-
}
|
|
1200
|
-
}
|
|
1201
|
-
|
|
1202
|
-
/** Record edge endpoint spans `edge.<Name>.endpoint.<role>` from the two args. */
|
|
1203
|
-
function collectEdge(
|
|
1204
|
-
spans: Record<string, SourceSpan>,
|
|
1205
|
-
name: string,
|
|
1206
|
-
init: CallExpression,
|
|
1207
|
-
domainRoot: string,
|
|
1208
|
-
fileRel: string,
|
|
1209
|
-
): void {
|
|
1210
|
-
const argsList = init.getArguments()
|
|
1211
|
-
const expression = init.getExpression()
|
|
1212
|
-
const currentObjectForm =
|
|
1213
|
-
Node.isPropertyAccessExpression(expression) &&
|
|
1214
|
-
(expression.getName() === 'directed' || expression.getName() === 'undirected') &&
|
|
1215
|
-
argsList[0] &&
|
|
1216
|
-
Node.isObjectLiteralExpression(argsList[0])
|
|
1217
|
-
|
|
1218
|
-
if (currentObjectForm) {
|
|
1219
|
-
const config = argsList[0]
|
|
1220
|
-
if (Node.isObjectLiteralExpression(config)) {
|
|
1221
|
-
for (const endpointName of ['source', 'target'] as const) {
|
|
1222
|
-
const ep = getObjectProp(config, endpointName)
|
|
1223
|
-
if (!ep) continue
|
|
1224
|
-
const role = stringLiteralOfProp(ep, 'as') ?? stringLiteralOfProp(ep, 'role')
|
|
1225
|
-
if (role) spans[`edge.${name}.endpoint.${role}`] = makeSpan(domainRoot, fileRel, ep, ep)
|
|
1226
|
-
}
|
|
1227
|
-
const properties = getObjectProp(config, 'properties') ?? getObjectProp(config, 'props')
|
|
1228
|
-
if (properties) {
|
|
1229
|
-
for (const p of properties.getProperties()) {
|
|
1230
|
-
const pName = propertyKey(p)
|
|
1231
|
-
if (!pName) continue
|
|
1232
|
-
spans[`edge.${name}.property.${pName}`] = makeSpan(domainRoot, fileRel, p, p)
|
|
1233
|
-
}
|
|
1234
|
-
}
|
|
1235
|
-
}
|
|
1236
|
-
return
|
|
1237
|
-
}
|
|
1238
|
-
|
|
1239
|
-
for (let i = 0; i < Math.min(2, argsList.length); i++) {
|
|
1240
|
-
const ep = argsList[i]
|
|
1241
|
-
if (!Node.isObjectLiteralExpression(ep)) continue
|
|
1242
|
-
const role = stringLiteralOfProp(ep, 'as')
|
|
1243
|
-
if (!role) continue
|
|
1244
|
-
spans[`edge.${name}.endpoint.${role}`] = makeSpan(domainRoot, fileRel, ep, ep)
|
|
1245
|
-
}
|
|
1246
|
-
// Legacy edge props live in the third (config) arg.
|
|
1247
|
-
const cfgArg = argsList[2]
|
|
1248
|
-
if (cfgArg && Node.isObjectLiteralExpression(cfgArg)) {
|
|
1249
|
-
const propsObj = getObjectProp(cfgArg, 'properties') ?? getObjectProp(cfgArg, 'props')
|
|
1250
|
-
if (propsObj) {
|
|
1251
|
-
for (const p of propsObj.getProperties()) {
|
|
1252
|
-
const pName = propertyKey(p)
|
|
1253
|
-
if (!pName) continue
|
|
1254
|
-
spans[`edge.${name}.property.${pName}`] = makeSpan(domainRoot, fileRel, p, p)
|
|
1255
|
-
}
|
|
1256
|
-
}
|
|
1257
|
-
}
|
|
1258
|
-
}
|
|
1259
|
-
|
|
1260
|
-
/** The object-literal value of a named property, if it is itself an object. */
|
|
1261
|
-
function getObjectProp(
|
|
1262
|
-
obj: Node,
|
|
1263
|
-
name: string,
|
|
1264
|
-
): import('ts-morph').ObjectLiteralExpression | undefined {
|
|
1265
|
-
if (!Node.isObjectLiteralExpression(obj)) return undefined
|
|
1266
|
-
const prop = obj.getProperty(name)
|
|
1267
|
-
if (!prop) return undefined
|
|
1268
|
-
let value: Node | undefined
|
|
1269
|
-
if (Node.isPropertyAssignment(prop)) value = prop.getInitializer()
|
|
1270
|
-
else if (Node.isShorthandPropertyAssignment(prop)) value = prop.getNameNode()
|
|
1271
|
-
return value ? resolveObjectLiteral(value) : undefined
|
|
1272
|
-
}
|
|
1273
|
-
|
|
1274
|
-
/** A string-literal property value, e.g. `as: 'page'` → 'page'. */
|
|
1275
|
-
function stringLiteralOfProp(obj: Node, name: string): string | undefined {
|
|
1276
|
-
if (!Node.isObjectLiteralExpression(obj)) return undefined
|
|
1277
|
-
const prop = obj.getProperty(name)
|
|
1278
|
-
if (!prop || !Node.isPropertyAssignment(prop)) return undefined
|
|
1279
|
-
const v = prop.getInitializer()
|
|
1280
|
-
if (v && (Node.isStringLiteral(v) || Node.isNoSubstitutionTemplateLiteral(v)))
|
|
1281
|
-
return v.getLiteralText()
|
|
1282
|
-
return undefined
|
|
1283
|
-
}
|
|
1284
|
-
|
|
1285
|
-
/** The key name of an object-literal property (assignment / shorthand / method). */
|
|
1286
|
-
function propertyKey(prop: Node): string | undefined {
|
|
1287
|
-
if (
|
|
1288
|
-
Node.isPropertyAssignment(prop) ||
|
|
1289
|
-
Node.isShorthandPropertyAssignment(prop) ||
|
|
1290
|
-
Node.isMethodDeclaration(prop)
|
|
1291
|
-
) {
|
|
1292
|
-
const nameNode = prop.getNameNode()
|
|
1293
|
-
if (Node.isStringLiteral(nameNode)) return nameNode.getLiteralText()
|
|
1294
|
-
if (Node.isComputedPropertyName(nameNode)) return undefined // skip `[expr]: …`
|
|
1295
|
-
return nameNode.getText()
|
|
1296
|
-
}
|
|
1297
|
-
// Spread (`...knobs`) — no single key.
|
|
1298
|
-
return undefined
|
|
1299
|
-
}
|
|
1300
|
-
|
|
1301
|
-
// ───────────────────────────── annotations ─────────────────────────────
|
|
1302
|
-
|
|
1303
|
-
export function buildSchemaAnnotations(_args: { ir: SchemaIR | null }): SchemaAnnotation[] {
|
|
1304
|
-
// The current Kernel admits updated values against their exact Value Schema;
|
|
1305
|
-
// enum properties therefore need no Studio-specific compatibility warning.
|
|
1306
|
-
return []
|
|
1307
|
-
}
|
|
7
|
+
export { buildSchemaAnnotations } from './source-overlay/annotations'
|
|
8
|
+
export { buildHandlerLinks } from './source-overlay/handlers'
|
|
9
|
+
export { buildSourceSpans } from './source-overlay/spans'
|