@astrale-os/cli 0.8.1-alpha.7 → 1.0.0-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -3
- package/dist/astrale.js +86 -35
- package/package.json +5 -5
- package/src/commands/__tests__/domain-install-operation.test.ts +121 -0
- package/src/commands/__tests__/domain-install-owned.test.ts +66 -0
- package/src/commands/__tests__/install-direct.test.ts +3 -2
- package/src/commands/domain/install.ts +78 -15
- package/src/connection/__tests__/errors.test.ts +82 -6
- package/src/connection/command.ts +9 -1
- package/src/connection/errors.ts +19 -8
- package/src/connection/index.ts +1 -1
- package/src/connection/reasons.ts +26 -12
- package/src/program/__tests__/program.test.ts +1 -1
- package/studio/client/dist/assets/{elk-api-D0cBetPW.js → elk-api-D2xgMJvi.js} +1 -1
- package/studio/client/dist/assets/{index-BQnJ5sgd.css → index-BMdnsIJA.css} +1 -1
- package/studio/client/dist/assets/index-D-vRV8w7.js +8 -0
- package/studio/client/dist/assets/index-LGSWRrk8.js +81 -0
- package/studio/client/dist/index.html +2 -2
- package/studio/package.json +1 -1
- package/studio/server/agent/prompts/anchors.test.ts +74 -0
- package/studio/server/agent/prompts/anchors.ts +85 -15
- package/studio/server/agent/prompts/system.test.ts +12 -0
- package/studio/server/agent/prompts/system.ts +6 -6
- package/studio/server/api.ts +1 -5
- package/studio/server/cache.ts +5 -2
- package/studio/server/domain.test.ts +67 -0
- package/studio/server/domain.ts +29 -6
- package/studio/server/index.ts +1 -3
- package/studio/server/introspect/anatomy-extras.test.ts +104 -1
- package/studio/server/introspect/anatomy-extras.ts +340 -8
- package/studio/server/introspect/anatomy.test.ts +33 -0
- package/studio/server/introspect/anatomy.ts +22 -7
- package/studio/server/introspect/bundle.ts +7 -1
- package/studio/server/introspect/canonical-schema.test.ts +395 -0
- package/studio/server/introspect/canonical-schema.ts +751 -0
- package/studio/server/introspect/core-extractor.ts +30 -10
- package/studio/server/introspect/core.ts +4 -2
- package/studio/server/introspect/diff.test.ts +124 -0
- package/studio/server/introspect/diff.ts +252 -23
- package/studio/server/introspect/extractor.ts +46 -14
- package/studio/server/introspect/overlay-tsmorph.test.ts +164 -1
- package/studio/server/introspect/overlay-tsmorph.ts +381 -106
- package/studio/server/introspect/overlay.test.ts +72 -0
- package/studio/server/introspect/overlay.ts +16 -6
- package/studio/server/introspect/runtime.test.ts +217 -0
- package/studio/server/introspect/runtime.ts +18 -6
- package/studio/server/introspect/schema-refs.test.ts +100 -0
- package/studio/server/introspect/schema-refs.ts +23 -2
- package/studio/server/state/baseline.test.ts +51 -0
- package/studio/server/state/baseline.ts +31 -2
- package/studio/server/state/create.test.ts +37 -0
- package/studio/server/state/create.ts +21 -15
- package/studio/server/state/instance.test.ts +63 -0
- package/studio/server/state/instance.ts +65 -29
- package/studio/server/state/views.test.ts +209 -9
- package/studio/server/state/views.ts +176 -69
- package/studio/server/watch.test.ts +36 -0
- package/studio/server/watch.ts +24 -16
- package/studio/server/workspace-watch.ts +8 -3
- package/studio/shared/types.ts +164 -33
- package/studio/client/dist/assets/index-Dspir4w7.js +0 -81
- package/studio/client/dist/assets/index-bVD2KJgz.js +0 -8
- package/studio/server/view-dev-server.test.ts +0 -111
- package/studio/server/view-dev-server.ts +0 -372
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync, readFileSync } from 'node:fs'
|
|
1
|
+
import { existsSync, readFileSync, realpathSync } from 'node:fs'
|
|
2
2
|
import { dirname, isAbsolute, relative, resolve as resolvePath } from 'node:path'
|
|
3
3
|
/**
|
|
4
4
|
* ts-morph overlay for IR gaps: handler links, source spans, and annotations.
|
|
@@ -37,7 +37,14 @@ function tryAddFile(project: Project, file: string): SourceFile | undefined {
|
|
|
37
37
|
/** Path relative to `root`, POSIX-style ('schema/monitor.ts'), never absolute. */
|
|
38
38
|
function relToRoot(root: string, file: string): string {
|
|
39
39
|
const abs = isAbsolute(file) ? file : resolvePath(root, file)
|
|
40
|
-
|
|
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('/')
|
|
41
48
|
}
|
|
42
49
|
|
|
43
50
|
/** 1-based start line of a node. */
|
|
@@ -111,6 +118,9 @@ interface WiredMethod {
|
|
|
111
118
|
/** the object literal / call expression carrying the handler config */
|
|
112
119
|
config: Node
|
|
113
120
|
wiringLine: number
|
|
121
|
+
wiringFile: string
|
|
122
|
+
ownerKind?: 'class' | 'interface' | 'function'
|
|
123
|
+
style?: 'legacy-config' | 'direct-handler'
|
|
114
124
|
}
|
|
115
125
|
|
|
116
126
|
const SINGLE_METHOD_HELPERS = new Set(['method', 'remoteMethod'])
|
|
@@ -529,7 +539,19 @@ function resolveViaModuleSpecifier(id: Node): { file: string; line: number } | u
|
|
|
529
539
|
|
|
530
540
|
/** Resolve a relative module specifier to a concrete .ts file path on disk. */
|
|
531
541
|
function resolveModuleFile(from: SourceFile, spec: string): string | undefined {
|
|
532
|
-
if (!spec.startsWith('.'))
|
|
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
|
+
}
|
|
533
555
|
const baseDir = dirname(from.getFilePath())
|
|
534
556
|
const base = resolvePath(baseDir, spec)
|
|
535
557
|
const sourceBase = base.replace(/\.(?:[cm]?js|jsx)$/, '')
|
|
@@ -625,55 +647,98 @@ export function buildHandlerLinks(args: {
|
|
|
625
647
|
}): HandlerLink[] {
|
|
626
648
|
const { ir, domainRoot } = args
|
|
627
649
|
if (!domainRoot) return []
|
|
628
|
-
const wiringRel = 'runtime/index.ts'
|
|
629
|
-
const wiringAbs = resolvePath(domainRoot, wiringRel)
|
|
630
650
|
const project = newProject()
|
|
631
|
-
const
|
|
632
|
-
|
|
651
|
+
const legacyWiringRel = 'runtime/index.ts'
|
|
652
|
+
const legacySf = tryAddFile(project, resolvePath(domainRoot, legacyWiringRel))
|
|
633
653
|
|
|
634
654
|
const interfaceNames = new Set(ir ? Object.keys(ir.interfaces ?? {}) : [])
|
|
635
655
|
|
|
636
656
|
const wired: WiredMethod[] = []
|
|
637
657
|
|
|
638
|
-
|
|
639
|
-
const
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
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
|
|
649
679
|
}
|
|
650
|
-
continue
|
|
651
|
-
}
|
|
652
680
|
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
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
|
+
})
|
|
667
698
|
}
|
|
668
|
-
|
|
669
|
-
wired.push({
|
|
670
|
-
owner,
|
|
671
|
-
method: methodName,
|
|
672
|
-
config: valueNode,
|
|
673
|
-
wiringLine: prop.getStartLineNumber(),
|
|
674
|
-
})
|
|
699
|
+
continue
|
|
675
700
|
}
|
|
676
|
-
|
|
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)
|
|
677
742
|
}
|
|
678
743
|
}
|
|
679
744
|
|
|
@@ -683,7 +748,7 @@ export function buildHandlerLinks(args: {
|
|
|
683
748
|
// falling back to the group entry (which references a variable / todo()).
|
|
684
749
|
const byKey = new Map<string, WiredMethod>()
|
|
685
750
|
for (const w of wired) {
|
|
686
|
-
const key = `${w.owner}.${w.method}`
|
|
751
|
+
const key = `${w.ownerKind ?? 'legacy'}:${w.owner}.${w.method}`
|
|
687
752
|
const existing = byKey.get(key)
|
|
688
753
|
if (!existing) {
|
|
689
754
|
byKey.set(key, w)
|
|
@@ -697,7 +762,8 @@ export function buildHandlerLinks(args: {
|
|
|
697
762
|
|
|
698
763
|
const links: HandlerLink[] = []
|
|
699
764
|
for (const w of byKey.values()) {
|
|
700
|
-
const ownerKind: 'class' | 'interface'
|
|
765
|
+
const ownerKind: 'class' | 'interface' | 'function' =
|
|
766
|
+
w.ownerKind ?? (interfaceNames.has(w.owner) ? 'interface' : 'class')
|
|
701
767
|
const isStatic = irMethodStatic(ir, w.owner, w.method, ownerKind)
|
|
702
768
|
|
|
703
769
|
const link: HandlerLink = {
|
|
@@ -705,20 +771,24 @@ export function buildHandlerLinks(args: {
|
|
|
705
771
|
ownerKind,
|
|
706
772
|
method: w.method,
|
|
707
773
|
static: isStatic,
|
|
708
|
-
wiringFile:
|
|
774
|
+
wiringFile: w.wiringFile,
|
|
709
775
|
wiringLine: w.wiringLine,
|
|
710
776
|
implemented: true,
|
|
711
777
|
}
|
|
712
778
|
|
|
713
|
-
const resolution = resolveConfigObject(w.config)
|
|
714
|
-
link.implemented = resolution.implemented
|
|
715
|
-
if (resolution.auth) link.auth = resolution.auth
|
|
716
|
-
if (resolution.authorize) link.authorize = resolution.authorize
|
|
717
|
-
if (resolution.authorizeSnippet) link.authorizeSnippet = resolution.authorizeSnippet
|
|
718
|
-
|
|
719
779
|
let target: { file: string; line: number } | undefined
|
|
720
|
-
if (
|
|
721
|
-
|
|
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)
|
|
722
792
|
}
|
|
723
793
|
|
|
724
794
|
if (target) {
|
|
@@ -740,14 +810,159 @@ export function buildHandlerLinks(args: {
|
|
|
740
810
|
return links
|
|
741
811
|
}
|
|
742
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
|
+
|
|
743
957
|
/** Look up a method's `static` flag from the IR (class or interface bucket). */
|
|
744
958
|
function irMethodStatic(
|
|
745
959
|
ir: SchemaIR | null,
|
|
746
960
|
owner: string,
|
|
747
961
|
method: string,
|
|
748
|
-
ownerKind: 'class' | 'interface',
|
|
962
|
+
ownerKind: 'class' | 'interface' | 'function',
|
|
749
963
|
): boolean {
|
|
750
964
|
if (!ir) return false
|
|
965
|
+
if (ownerKind === 'function') return true
|
|
751
966
|
const bucket = ownerKind === 'interface' ? ir.interfaces?.[owner] : ir.classes?.[owner]
|
|
752
967
|
const m = bucket?.methods?.[method]
|
|
753
968
|
return m ? m.static === true : false
|
|
@@ -755,10 +970,29 @@ function irMethodStatic(
|
|
|
755
970
|
|
|
756
971
|
// ───────────────────────────── source spans ─────────────────────────────
|
|
757
972
|
|
|
758
|
-
const DECL_HELPERS: Record<string, 'node' | 'interface' | 'edge'> = {
|
|
973
|
+
const DECL_HELPERS: Record<string, 'node' | 'interface' | 'edge' | 'function'> = {
|
|
759
974
|
nodeClass: 'node',
|
|
760
975
|
nodeInterface: 'interface',
|
|
761
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
|
|
762
996
|
}
|
|
763
997
|
|
|
764
998
|
/**
|
|
@@ -772,46 +1006,71 @@ const DECL_HELPERS: Record<string, 'node' | 'interface' | 'edge'> = {
|
|
|
772
1006
|
*/
|
|
773
1007
|
interface MemberName {
|
|
774
1008
|
schemaName: string
|
|
775
|
-
section: 'interface' | 'class' // the `classes` map also holds edge classes
|
|
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
|
|
776
1030
|
}
|
|
777
1031
|
|
|
778
1032
|
/** Map each registered member VARIABLE (as referenced in `defineSchema`) to its
|
|
779
1033
|
* schema name + section, from the domain's own schema files — never node_modules
|
|
780
1034
|
* (a dependency's `defineSchema` is not this domain's). */
|
|
781
|
-
function buildMemberNameMap(files: SourceFile[]):
|
|
782
|
-
const map =
|
|
1035
|
+
function buildMemberNameMap(files: SourceFile[]): MemberNameMap {
|
|
1036
|
+
const map: MemberNameMap = {
|
|
1037
|
+
byValue: new Map<string, MemberName>(),
|
|
1038
|
+
byIdentifier: new Map<string, MemberName>(),
|
|
1039
|
+
}
|
|
783
1040
|
for (const sf of files) {
|
|
784
1041
|
if (sf.getFilePath().includes('/node_modules/')) continue
|
|
785
1042
|
for (const call of sf.getDescendantsOfKind(SyntaxKind.CallExpression)) {
|
|
786
1043
|
if (calleeName(call) !== 'defineSchema') continue
|
|
787
|
-
const
|
|
788
|
-
|
|
1044
|
+
const input = call.getArguments()[1]
|
|
1045
|
+
const cfg = input ? resolveObjectLiteral(input) : undefined
|
|
1046
|
+
if (!cfg) continue
|
|
789
1047
|
collectSchemaSection(map, cfg, 'interfaces', 'interface')
|
|
790
1048
|
collectSchemaSection(map, cfg, 'classes', 'class')
|
|
1049
|
+
collectSchemaSection(map, cfg, 'functions', 'function')
|
|
791
1050
|
}
|
|
792
1051
|
}
|
|
793
1052
|
return map
|
|
794
1053
|
}
|
|
795
1054
|
|
|
796
|
-
/** Record `variable → { schemaName, section }` for one `defineSchema` section
|
|
797
|
-
*
|
|
1055
|
+
/** Record `variable → { schemaName, section }` for one `defineSchema` section,
|
|
1056
|
+
* handling both `Key: alias` and shorthand `Key`. */
|
|
798
1057
|
function collectSchemaSection(
|
|
799
|
-
map:
|
|
1058
|
+
map: MemberNameMap,
|
|
800
1059
|
cfg: Node,
|
|
801
|
-
prop: 'interfaces' | 'classes',
|
|
802
|
-
section: 'interface' | 'class',
|
|
1060
|
+
prop: 'interfaces' | 'classes' | 'functions',
|
|
1061
|
+
section: 'interface' | 'class' | 'function',
|
|
803
1062
|
): void {
|
|
804
1063
|
const obj = getObjectProp(cfg, prop)
|
|
805
1064
|
if (!obj) return
|
|
806
1065
|
for (const p of obj.getProperties()) {
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
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)
|
|
815
1074
|
}
|
|
816
1075
|
}
|
|
817
1076
|
|
|
@@ -826,9 +1085,10 @@ function collectSchemaSection(
|
|
|
826
1085
|
function resolveMemberKind(
|
|
827
1086
|
ir: SchemaIR | null,
|
|
828
1087
|
name: string,
|
|
829
|
-
section: 'interface' | 'class' | undefined,
|
|
830
|
-
helperKind: 'node' | 'interface' | 'edge',
|
|
831
|
-
): 'class' | 'interface' | 'edge' {
|
|
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'
|
|
832
1092
|
if (section === 'interface') return 'interface'
|
|
833
1093
|
const isEdge = helperKind === 'edge' || ir?.classes?.[name]?.type === 'edge'
|
|
834
1094
|
if (section === 'class') return isEdge ? 'edge' : 'class'
|
|
@@ -870,19 +1130,22 @@ export function buildSourceSpans(args: {
|
|
|
870
1130
|
if (!v.isExported()) continue
|
|
871
1131
|
const init = v.getInitializer()
|
|
872
1132
|
if (!init || !Node.isCallExpression(init)) continue
|
|
873
|
-
const
|
|
874
|
-
if (!
|
|
875
|
-
const
|
|
876
|
-
const member =
|
|
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())
|
|
877
1139
|
const name = member?.schemaName ?? v.getName()
|
|
878
1140
|
const ns = resolveMemberKind(ir, name, member?.section, helperKind)
|
|
879
1141
|
|
|
880
1142
|
const stmt = v.getVariableStatement() ?? v
|
|
881
1143
|
spans[`${ns}.${name}`] = makeSpan(domainRoot, fileRel, stmt, v)
|
|
882
1144
|
|
|
883
|
-
// The single object-literal argument: nodeClass({
|
|
1145
|
+
// The single object-literal argument: nodeClass({ properties, methods })
|
|
1146
|
+
// and current edgeClass.directed({ source, target, properties }).
|
|
884
1147
|
const cfgArg = init.getArguments()[0]
|
|
885
|
-
if (cfgArg && Node.isObjectLiteralExpression(cfgArg)) {
|
|
1148
|
+
if (ns !== 'function' && cfgArg && Node.isObjectLiteralExpression(cfgArg)) {
|
|
886
1149
|
collectPropsAndMethods(spans, ns, name, cfgArg, domainRoot, fileRel)
|
|
887
1150
|
}
|
|
888
1151
|
|
|
@@ -918,7 +1181,7 @@ function collectPropsAndMethods(
|
|
|
918
1181
|
fileRel: string,
|
|
919
1182
|
): void {
|
|
920
1183
|
if (!Node.isObjectLiteralExpression(cfg)) return
|
|
921
|
-
const propsObj = getObjectProp(cfg, 'props')
|
|
1184
|
+
const propsObj = getObjectProp(cfg, 'properties') ?? getObjectProp(cfg, 'props')
|
|
922
1185
|
if (propsObj) {
|
|
923
1186
|
for (const p of propsObj.getProperties()) {
|
|
924
1187
|
const pName = propertyKey(p)
|
|
@@ -945,6 +1208,34 @@ function collectEdge(
|
|
|
945
1208
|
fileRel: string,
|
|
946
1209
|
): void {
|
|
947
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
|
+
|
|
948
1239
|
for (let i = 0; i < Math.min(2, argsList.length); i++) {
|
|
949
1240
|
const ep = argsList[i]
|
|
950
1241
|
if (!Node.isObjectLiteralExpression(ep)) continue
|
|
@@ -952,10 +1243,10 @@ function collectEdge(
|
|
|
952
1243
|
if (!role) continue
|
|
953
1244
|
spans[`edge.${name}.endpoint.${role}`] = makeSpan(domainRoot, fileRel, ep, ep)
|
|
954
1245
|
}
|
|
955
|
-
//
|
|
1246
|
+
// Legacy edge props live in the third (config) arg.
|
|
956
1247
|
const cfgArg = argsList[2]
|
|
957
1248
|
if (cfgArg && Node.isObjectLiteralExpression(cfgArg)) {
|
|
958
|
-
const propsObj = getObjectProp(cfgArg, 'props')
|
|
1249
|
+
const propsObj = getObjectProp(cfgArg, 'properties') ?? getObjectProp(cfgArg, 'props')
|
|
959
1250
|
if (propsObj) {
|
|
960
1251
|
for (const p of propsObj.getProperties()) {
|
|
961
1252
|
const pName = propertyKey(p)
|
|
@@ -977,8 +1268,7 @@ function getObjectProp(
|
|
|
977
1268
|
let value: Node | undefined
|
|
978
1269
|
if (Node.isPropertyAssignment(prop)) value = prop.getInitializer()
|
|
979
1270
|
else if (Node.isShorthandPropertyAssignment(prop)) value = prop.getNameNode()
|
|
980
|
-
|
|
981
|
-
return undefined
|
|
1271
|
+
return value ? resolveObjectLiteral(value) : undefined
|
|
982
1272
|
}
|
|
983
1273
|
|
|
984
1274
|
/** A string-literal property value, e.g. `as: 'page'` → 'page'. */
|
|
@@ -1010,23 +1300,8 @@ function propertyKey(prop: Node): string | undefined {
|
|
|
1010
1300
|
|
|
1011
1301
|
// ───────────────────────────── annotations ─────────────────────────────
|
|
1012
1302
|
|
|
1013
|
-
export function buildSchemaAnnotations(
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
for (const cls of Object.values(ir.classes ?? {})) {
|
|
1018
|
-
const ns = cls.type === 'edge' ? 'edge' : 'class'
|
|
1019
|
-
for (const [propName, schema] of Object.entries(cls.properties ?? {})) {
|
|
1020
|
-
if (Array.isArray(schema?.enum) && schema.enum.length > 0) {
|
|
1021
|
-
out.push({
|
|
1022
|
-
target: `${ns}.${cls.name}.property.${propName}`,
|
|
1023
|
-
severity: 'warn',
|
|
1024
|
-
code: 'ENUM_DROPPED_BY_UPDATE',
|
|
1025
|
-
message:
|
|
1026
|
-
'z.enum props are silently dropped by ::update — track as a plain string if it must be updated.',
|
|
1027
|
-
})
|
|
1028
|
-
}
|
|
1029
|
-
}
|
|
1030
|
-
}
|
|
1031
|
-
return out
|
|
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 []
|
|
1032
1307
|
}
|