@astrale-os/cli 1.0.0-beta.0 → 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.
Files changed (180) hide show
  1. package/README.md +7 -11
  2. package/dist/astrale.js +3769 -3549
  3. package/dist/public/connect-core.js +396 -392
  4. package/dist/public/keys/index.js +371 -378
  5. package/dist/public/paths/index.js +371 -378
  6. package/dist/types/lib/idp.d.ts +6 -0
  7. package/dist/types/lib/meta.d.ts +2 -2
  8. package/package.json +6 -4
  9. package/src/admin/binding.ts +168 -0
  10. package/src/admin/catalog/__tests__/client.test.ts +2 -2
  11. package/src/admin/catalog/client.ts +4 -9
  12. package/src/admin/instance/__tests__/client.test.ts +5 -3
  13. package/src/admin/instance/client.ts +9 -27
  14. package/src/commands/__tests__/auth-login.test.ts +54 -1
  15. package/src/commands/__tests__/call.test.ts +5 -1
  16. package/src/commands/__tests__/domain-install-operation.test.ts +23 -7
  17. package/src/commands/__tests__/domain-install-owned.test.ts +5 -1
  18. package/src/commands/__tests__/domain-list.test.ts +83 -4
  19. package/src/commands/__tests__/install-identity-override.test.ts +30 -19
  20. package/src/commands/__tests__/studio-descriptor.test.ts +22 -0
  21. package/src/commands/__tests__/update.test.ts +73 -0
  22. package/src/commands/domain/install.ts +11 -15
  23. package/src/commands/domain/list.ts +17 -12
  24. package/src/commands/studio.ts +20 -0
  25. package/src/commands/update.ts +39 -13
  26. package/src/connection/__tests__/errors.test.ts +16 -12
  27. package/src/connection/__tests__/exchange.test.ts +6 -5
  28. package/src/connection/exchange.ts +5 -4
  29. package/src/lib/__tests__/domain-publication.test.ts +144 -0
  30. package/src/lib/__tests__/idp-session.test.ts +22 -0
  31. package/src/lib/__tests__/idp.test.ts +2 -0
  32. package/src/lib/__tests__/instance.test.ts +6 -0
  33. package/src/lib/__tests__/studio-server-deps.test.ts +53 -16
  34. package/src/lib/__tests__/update.test.ts +12 -0
  35. package/src/lib/domain-publication.ts +102 -0
  36. package/src/lib/idp.ts +5 -1
  37. package/src/lib/instance.ts +6 -1
  38. package/src/lib/login-flow.ts +13 -3
  39. package/src/lib/meta.ts +2 -2
  40. package/src/lib/update.ts +4 -4
  41. package/src/program/__tests__/program.test.ts +1 -1
  42. package/studio/client/dist/assets/{elk-api-D2xgMJvi.js → elk-api-lwhFo7vB.js} +1 -1
  43. package/studio/client/dist/assets/index-B-c-smo9.js +8 -0
  44. package/studio/client/dist/assets/index-BckHuAWk.js +81 -0
  45. package/studio/client/dist/assets/index-C4aNQ6dz.css +1 -0
  46. package/studio/client/dist/index.html +2 -2
  47. package/studio/package.json +3 -1
  48. package/studio/server/agent/bridge/stdio.ts +8 -1
  49. package/studio/server/agent/conversation.test.ts +10 -5
  50. package/studio/server/agent/conversation.ts +56 -8
  51. package/studio/server/agent/harness/codex/loadout.ts +18 -21
  52. package/studio/server/agent/harness/gateway/config.ts +20 -18
  53. package/studio/server/agent/harness/selection.ts +9 -1
  54. package/studio/server/agent/prompts/system.ts +3 -2
  55. package/studio/server/agent/prompts/turn.ts +13 -4
  56. package/studio/server/agent/run/completion.ts +2 -2
  57. package/studio/server/agent/run/preparation.ts +6 -5
  58. package/studio/server/agent/run/transcript.test.ts +35 -1
  59. package/studio/server/agent/run/transcript.ts +169 -3
  60. package/studio/server/agent/run/usage.test.ts +24 -0
  61. package/studio/server/agent/run/usage.ts +26 -1
  62. package/studio/server/api/canvas.ts +44 -0
  63. package/studio/server/api/comments.ts +85 -0
  64. package/studio/server/api/context.ts +61 -0
  65. package/studio/server/api/deployment.ts +39 -0
  66. package/studio/server/api/documents.ts +55 -0
  67. package/studio/server/api/domain.ts +59 -0
  68. package/studio/server/api/http.ts +45 -0
  69. package/studio/server/api/project.ts +57 -0
  70. package/studio/server/api/schema.ts +12 -0
  71. package/studio/server/api/views.ts +57 -0
  72. package/studio/server/api/workspace.ts +60 -0
  73. package/studio/server/api.test.ts +147 -0
  74. package/studio/server/api.ts +20 -422
  75. package/studio/server/cache.test.ts +111 -0
  76. package/studio/server/cache.ts +163 -9
  77. package/studio/server/cli-consumers.test.ts +494 -0
  78. package/studio/server/cli.test.ts +88 -0
  79. package/studio/server/cli.ts +240 -0
  80. package/studio/server/client-package.ts +25 -1
  81. package/studio/server/domain.test.ts +30 -2
  82. package/studio/server/domain.ts +14 -2
  83. package/studio/server/environment/dotenv-preview.test.ts +39 -0
  84. package/studio/server/environment/dotenv-preview.ts +27 -0
  85. package/studio/server/environment/files.test.ts +53 -0
  86. package/studio/server/{state/env.ts → environment/files.ts} +7 -48
  87. package/studio/server/{state → handoff}/copy.ts +21 -6
  88. package/studio/server/handoff/service.test.ts +22 -0
  89. package/studio/server/{state/handoff.ts → handoff/service.ts} +21 -14
  90. package/studio/server/instances/active.ts +72 -0
  91. package/studio/server/instances/deploy-record.test.ts +31 -0
  92. package/studio/server/instances/deploy-record.ts +37 -0
  93. package/studio/server/instances/deploy.ts +56 -0
  94. package/studio/server/{state/instance.test.ts → instances/probe.test.ts} +25 -21
  95. package/studio/server/instances/probe.ts +67 -0
  96. package/studio/server/instances/status.test.ts +12 -0
  97. package/studio/server/instances/status.ts +49 -0
  98. package/studio/server/introspect/anatomy/client-tree.ts +57 -0
  99. package/studio/server/introspect/anatomy/env-fields.ts +66 -0
  100. package/studio/server/introspect/anatomy/schema-definition.ts +53 -0
  101. package/studio/server/introspect/anatomy/source.ts +181 -0
  102. package/studio/server/introspect/anatomy/views/legacy.ts +232 -0
  103. package/studio/server/introspect/anatomy/views/routes.ts +105 -0
  104. package/studio/server/introspect/anatomy/views.ts +58 -0
  105. package/studio/server/introspect/anatomy-extras.test.ts +91 -4
  106. package/studio/server/introspect/anatomy-extras.ts +7 -726
  107. package/studio/server/introspect/anatomy.ts +46 -19
  108. package/studio/server/introspect/bundle.ts +15 -6
  109. package/studio/server/introspect/canonical-schema.test.ts +79 -0
  110. package/studio/server/introspect/canonical-schema.ts +80 -18
  111. package/studio/server/introspect/config-preview.test.ts +32 -0
  112. package/studio/server/introspect/config-preview.ts +119 -0
  113. package/studio/server/introspect/diff.test.ts +7 -11
  114. package/studio/server/introspect/diff.ts +28 -37
  115. package/studio/server/introspect/extractor.ts +8 -6
  116. package/studio/server/introspect/hash.ts +5 -2
  117. package/studio/server/introspect/overlay-tsmorph.ts +7 -1305
  118. package/studio/server/introspect/revision.test.ts +54 -0
  119. package/studio/server/introspect/revision.ts +49 -0
  120. package/studio/server/introspect/runtime.test.ts +63 -5
  121. package/studio/server/introspect/runtime.ts +49 -3
  122. package/studio/server/introspect/schema-ir-json.ts +181 -0
  123. package/studio/server/introspect/source-overlay/annotations.ts +14 -0
  124. package/studio/server/introspect/source-overlay/handlers.ts +681 -0
  125. package/studio/server/introspect/source-overlay/kernel-calls.ts +49 -0
  126. package/studio/server/introspect/source-overlay/project.ts +248 -0
  127. package/studio/server/introspect/source-overlay/spans.ts +360 -0
  128. package/studio/server/json.ts +46 -0
  129. package/studio/server/lifecycle.ts +5 -1
  130. package/studio/server/sse.test.ts +26 -0
  131. package/studio/server/sse.ts +4 -1
  132. package/studio/server/state/baseline.test.ts +212 -1
  133. package/studio/server/state/baseline.ts +145 -29
  134. package/studio/server/state/comments.test.ts +66 -1
  135. package/studio/server/state/comments.ts +173 -10
  136. package/studio/server/state/context.ts +34 -2
  137. package/studio/server/state/documents.ts +42 -1
  138. package/studio/server/state/integrations.ts +27 -1
  139. package/studio/server/state/layout.test.ts +34 -0
  140. package/studio/server/state/layout.ts +44 -17
  141. package/studio/server/state/settings.ts +29 -22
  142. package/studio/server/state/store.test.ts +122 -0
  143. package/studio/server/state/store.ts +66 -17
  144. package/studio/server/state/visibility.ts +22 -7
  145. package/studio/server/views/model.test.ts +57 -0
  146. package/studio/server/views/model.ts +66 -0
  147. package/studio/server/views/runtime.ts +40 -0
  148. package/studio/server/views/selection-repository.test.ts +31 -0
  149. package/studio/server/views/selection-repository.ts +60 -0
  150. package/studio/server/views/session.test.ts +73 -0
  151. package/studio/server/views/session.ts +166 -0
  152. package/studio/server/{state/views.test.ts → views/target.test.ts} +8 -159
  153. package/studio/server/views/target.ts +248 -0
  154. package/studio/server/watch.ts +1 -1
  155. package/studio/server/workspace/catalog.test.ts +17 -0
  156. package/studio/server/workspace/catalog.ts +43 -0
  157. package/studio/server/{state → workspace}/create.test.ts +1 -1
  158. package/studio/server/{state → workspace}/create.ts +1 -1
  159. package/studio/server/{state → workspace}/git.ts +1 -1
  160. package/studio/server/workspace/updates.ts +83 -0
  161. package/studio/server/workspace-state.ts +1 -1
  162. package/studio/shared/contracts/README.md +16 -0
  163. package/studio/shared/contracts/agent.ts +270 -0
  164. package/studio/shared/contracts/runtime.ts +50 -0
  165. package/studio/shared/contracts/schema.ts +462 -0
  166. package/studio/shared/contracts/surface.test.ts +319 -0
  167. package/studio/shared/contracts/workspace.ts +274 -0
  168. package/studio/shared/layout.test.ts +116 -0
  169. package/studio/shared/schema/identity.test.ts +58 -0
  170. package/studio/shared/schema/identity.ts +82 -0
  171. package/studio/shared/types.ts +11 -1099
  172. package/studio/tsconfig.json +1 -1
  173. package/viewer/dist/main.js +35 -35
  174. package/studio/client/dist/assets/index-BMdnsIJA.css +0 -1
  175. package/studio/client/dist/assets/index-D-vRV8w7.js +0 -8
  176. package/studio/client/dist/assets/index-LGSWRrk8.js +0 -81
  177. package/studio/server/state/catalog.ts +0 -117
  178. package/studio/server/state/instance.ts +0 -280
  179. package/studio/server/state/updates.ts +0 -63
  180. package/studio/server/state/views.ts +0 -535
@@ -0,0 +1,319 @@
1
+ import { expect, test } from 'bun:test'
2
+ import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs'
3
+ import { dirname, join, relative, resolve } from 'node:path'
4
+ import ts from 'typescript'
5
+
6
+ const SHARED_ROOT = resolve(import.meta.dir, '..')
7
+ const STUDIO_ROOT = resolve(SHARED_ROOT, '..')
8
+
9
+ // Frozen from HEAD's former monolithic shared/types.ts before it was split.
10
+ // Keeping this manifest in the test makes the compatibility proof independent
11
+ // of Git and catches any historical symbol silently dropped by a future move.
12
+ const HISTORICAL_BARREL_EXPORTS = [
13
+ 'AGENT_ACCESS_LEVELS',
14
+ 'AGENT_EFFORT_LEVELS',
15
+ 'AgentAccess',
16
+ 'AgentEffort',
17
+ 'AgentEvent',
18
+ 'AgentEventKind',
19
+ 'AgentPromptSnapshot',
20
+ 'AgentRun',
21
+ 'AgentRunSnapshot',
22
+ 'AgentRunStatus',
23
+ 'AgentSessionInfo',
24
+ 'AgentSystemPromptInfo',
25
+ 'AnchorKind',
26
+ 'AnchorRef',
27
+ 'BundleError',
28
+ 'ChangeSet',
29
+ 'ClientFeature',
30
+ 'ClientTree',
31
+ 'Comment',
32
+ 'CommentStore',
33
+ 'ContextItem',
34
+ 'ContextStore',
35
+ 'ConversationInfo',
36
+ 'CopyPayload',
37
+ 'CrossDomainImport',
38
+ 'DeployRecord',
39
+ 'DeployResult',
40
+ 'DocMeta',
41
+ 'DomainAnatomy',
42
+ 'DomainCatalogEntry',
43
+ 'DomainOverview',
44
+ 'DomainSummary',
45
+ 'DomainUsage',
46
+ 'EnvField',
47
+ 'EnvFileModel',
48
+ 'EnvName',
49
+ 'EnvVarRow',
50
+ 'FileChange',
51
+ 'HandlerLink',
52
+ 'HarnessCapabilities',
53
+ 'HarnessGatewayAuth',
54
+ 'HarnessGatewayConfig',
55
+ 'HarnessGatewayState',
56
+ 'HarnessLoadout',
57
+ 'HarnessModelOption',
58
+ 'HarnessStatus',
59
+ 'InstanceInfo',
60
+ 'InstanceStatus',
61
+ 'InstancesState',
62
+ 'Integration',
63
+ 'IntegrationsState',
64
+ 'IrCallableAuth',
65
+ 'IrCallableOutput',
66
+ 'IrClass',
67
+ 'IrDefinitionKey',
68
+ 'IrDefinitionRef',
69
+ 'IrEndpoint',
70
+ 'IrFunction',
71
+ 'IrImportDescriptor',
72
+ 'IrInterface',
73
+ 'IrMethod',
74
+ 'IrSchemaKey',
75
+ 'IrSchemaRef',
76
+ 'IrView',
77
+ 'IrViewTarget',
78
+ 'JsonSchema',
79
+ 'LayoutState',
80
+ 'LoadoutSkill',
81
+ 'McpServerInfo',
82
+ 'MergeResult',
83
+ 'MethodInheritance',
84
+ 'NodePosition',
85
+ 'RememberedViewTarget',
86
+ 'SchemaAnnotation',
87
+ 'SchemaChange',
88
+ 'SchemaChangeKind',
89
+ 'SchemaIR',
90
+ 'SchemaOverlay',
91
+ 'SourceSpan',
92
+ 'StaleReport',
93
+ 'StudioCore',
94
+ 'StudioCoreEdge',
95
+ 'StudioCoreNode',
96
+ 'StudioEvent',
97
+ 'StudioSchemaBundle',
98
+ 'StudioSettings',
99
+ 'ThreadEntry',
100
+ 'ThreadEntryType',
101
+ 'ThreadRole',
102
+ 'TypeDescriptor',
103
+ 'ViewInfo',
104
+ 'ViewRuntime',
105
+ 'ViewSessionResult',
106
+ 'ViewTargetCandidate',
107
+ 'ViewTargetResult',
108
+ 'VisibilityState',
109
+ ] as const
110
+
111
+ // Deliberate canonical identity/revision helpers added alongside the split.
112
+ const INTENTIONAL_BARREL_ADDITIONS = [
113
+ 'IR_SCHEMA_REF_KINDS',
114
+ 'IrSchemaRefKind',
115
+ 'STUDIO_SCHEMA_PROJECTION_VERSION',
116
+ 'SchemaRevision',
117
+ 'definitionRefKey',
118
+ 'isIrDefinitionRef',
119
+ 'isIrInterfaceRef',
120
+ 'isIrSchemaRef',
121
+ 'isSchemaRevision',
122
+ 'parseDefinitionRefKey',
123
+ 'parseSchemaRefKey',
124
+ 'schemaRefKey',
125
+ ] as const
126
+
127
+ function productionFiles(dir: string): string[] {
128
+ return readdirSync(dir).flatMap((entry) => {
129
+ const path = join(dir, entry)
130
+ if (statSync(path).isDirectory()) return productionFiles(path)
131
+ return /\.(?:ts|tsx)$/.test(path) && !/\.test\.(?:ts|tsx)$/.test(path) ? [path] : []
132
+ })
133
+ }
134
+
135
+ function parseTypeScript(file: string): ts.SourceFile {
136
+ return ts.createSourceFile(
137
+ file,
138
+ readFileSync(file, 'utf8'),
139
+ ts.ScriptTarget.Latest,
140
+ false,
141
+ file.endsWith('.tsx') ? ts.ScriptKind.TSX : ts.ScriptKind.TS,
142
+ )
143
+ }
144
+
145
+ function hasExportModifier(node: ts.Node): boolean {
146
+ return Boolean(
147
+ ts.canHaveModifiers(node) &&
148
+ ts.getModifiers(node)?.some((modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword),
149
+ )
150
+ }
151
+
152
+ function bindingNames(name: ts.BindingName): string[] {
153
+ if (ts.isIdentifier(name)) return [name.text]
154
+ return name.elements.flatMap((element) =>
155
+ ts.isOmittedExpression(element) ? [] : bindingNames(element.name),
156
+ )
157
+ }
158
+
159
+ function resolveTypeScriptModule(file: string, specifier: string): string {
160
+ const target = resolve(dirname(file), specifier)
161
+ const candidate = [target, `${target}.ts`, `${target}.tsx`, join(target, 'index.ts')].find(
162
+ existsSync,
163
+ )
164
+ if (!candidate) throw new Error(`Cannot resolve ${specifier} from ${file}`)
165
+ return candidate
166
+ }
167
+
168
+ function namedExports(file: string, seen = new Set<string>()): string[] {
169
+ if (seen.has(file)) return []
170
+ seen.add(file)
171
+ const names: string[] = []
172
+
173
+ for (const statement of parseTypeScript(file).statements) {
174
+ if (ts.isExportDeclaration(statement)) {
175
+ if (statement.exportClause && ts.isNamedExports(statement.exportClause)) {
176
+ names.push(...statement.exportClause.elements.map((element) => element.name.text))
177
+ } else if (statement.exportClause && ts.isNamespaceExport(statement.exportClause)) {
178
+ names.push(statement.exportClause.name.text)
179
+ } else if (
180
+ statement.moduleSpecifier &&
181
+ ts.isStringLiteral(statement.moduleSpecifier) &&
182
+ statement.moduleSpecifier.text.startsWith('.')
183
+ ) {
184
+ names.push(
185
+ ...namedExports(resolveTypeScriptModule(file, statement.moduleSpecifier.text), seen),
186
+ )
187
+ }
188
+ continue
189
+ }
190
+ if (!hasExportModifier(statement)) continue
191
+ if (ts.isVariableStatement(statement)) {
192
+ names.push(
193
+ ...statement.declarationList.declarations.flatMap(({ name }) => bindingNames(name)),
194
+ )
195
+ } else if (
196
+ (ts.isClassDeclaration(statement) ||
197
+ ts.isEnumDeclaration(statement) ||
198
+ ts.isFunctionDeclaration(statement) ||
199
+ ts.isInterfaceDeclaration(statement) ||
200
+ ts.isTypeAliasDeclaration(statement)) &&
201
+ statement.name
202
+ ) {
203
+ names.push(statement.name.text)
204
+ }
205
+ }
206
+ return names
207
+ }
208
+
209
+ function moduleSpecifiers(file: string): string[] {
210
+ const found: string[] = []
211
+ const visit = (node: ts.Node): void => {
212
+ if (
213
+ (ts.isImportDeclaration(node) || ts.isExportDeclaration(node)) &&
214
+ node.moduleSpecifier &&
215
+ ts.isStringLiteralLike(node.moduleSpecifier)
216
+ ) {
217
+ found.push(node.moduleSpecifier.text)
218
+ } else if (
219
+ ts.isImportEqualsDeclaration(node) &&
220
+ ts.isExternalModuleReference(node.moduleReference) &&
221
+ node.moduleReference.expression &&
222
+ ts.isStringLiteralLike(node.moduleReference.expression)
223
+ ) {
224
+ found.push(node.moduleReference.expression.text)
225
+ } else if (
226
+ ts.isCallExpression(node) &&
227
+ node.arguments.length === 1 &&
228
+ ts.isStringLiteralLike(node.arguments[0]!) &&
229
+ (node.expression.kind === ts.SyntaxKind.ImportKeyword ||
230
+ (ts.isIdentifier(node.expression) && node.expression.text === 'require'))
231
+ ) {
232
+ found.push(node.arguments[0]!.text)
233
+ }
234
+ ts.forEachChild(node, visit)
235
+ }
236
+ visit(parseTypeScript(file))
237
+ return found
238
+ }
239
+
240
+ function declaredTypes(file: string, candidates: ReadonlySet<string>): string[] {
241
+ const text = readFileSync(file, 'utf8')
242
+ if (![...candidates].some((name) => text.includes(name))) return []
243
+ return parseTypeScript(file).statements.flatMap((statement) => {
244
+ if (
245
+ !ts.isInterfaceDeclaration(statement) &&
246
+ !ts.isTypeAliasDeclaration(statement) &&
247
+ !ts.isClassDeclaration(statement) &&
248
+ !ts.isEnumDeclaration(statement)
249
+ ) {
250
+ return []
251
+ }
252
+ const name = statement.name?.text
253
+ return name && candidates.has(name) ? [name] : []
254
+ })
255
+ }
256
+
257
+ test('types.ts remains a re-export-only compatibility facade', () => {
258
+ const source = parseTypeScript(join(SHARED_ROOT, 'types.ts'))
259
+ const exports = source.statements.flatMap((statement) => {
260
+ if (
261
+ !ts.isExportDeclaration(statement) ||
262
+ statement.exportClause ||
263
+ !statement.moduleSpecifier ||
264
+ !ts.isStringLiteral(statement.moduleSpecifier)
265
+ ) {
266
+ return []
267
+ }
268
+ return [statement.moduleSpecifier.text]
269
+ })
270
+ expect(exports).toEqual([
271
+ './schema/identity',
272
+ './contracts/schema',
273
+ './contracts/workspace',
274
+ './contracts/agent',
275
+ './contracts/runtime',
276
+ ])
277
+ expect(source.statements.every(ts.isExportDeclaration)).toBe(true)
278
+ })
279
+
280
+ test('types.ts exhaustively preserves its historical named export surface', () => {
281
+ const exports = namedExports(join(SHARED_ROOT, 'types.ts'))
282
+ const duplicates = exports.filter((name, index) => exports.indexOf(name) !== index)
283
+ const expected = [...HISTORICAL_BARREL_EXPORTS, ...INTENTIONAL_BARREL_ADDITIONS].sort()
284
+
285
+ expect(duplicates).toEqual([])
286
+ expect(exports.sort()).toEqual(expected)
287
+ })
288
+
289
+ test('persisted layout and visibility shapes each have one semantic owner', () => {
290
+ const files = productionFiles(STUDIO_ROOT)
291
+ const names = new Set(['LayoutState', 'VisibilityState'])
292
+ const owners = new Map([...names].map((name) => [name, [] as string[]]))
293
+ for (const file of files) {
294
+ for (const name of declaredTypes(file, names)) {
295
+ owners.get(name)!.push(relative(STUDIO_ROOT, file).replaceAll('\\', '/'))
296
+ }
297
+ }
298
+ expect(Object.fromEntries(owners)).toEqual({
299
+ LayoutState: ['shared/contracts/workspace.ts'],
300
+ VisibilityState: ['shared/contracts/workspace.ts'],
301
+ })
302
+ })
303
+
304
+ test('contract modules follow the documented dependency direction', () => {
305
+ const expectedImports = new Map([
306
+ ['schema/identity.ts', []],
307
+ ['contracts/schema.ts', ['../schema/identity']],
308
+ ['contracts/workspace.ts', ['./schema']],
309
+ ['contracts/agent.ts', ['./workspace']],
310
+ ['contracts/runtime.ts', ['./agent']],
311
+ ])
312
+
313
+ for (const [file, expected] of expectedImports) {
314
+ const imports = moduleSpecifiers(join(SHARED_ROOT, file)).filter((specifier) =>
315
+ specifier.startsWith('.'),
316
+ )
317
+ expect(imports).toEqual(expected)
318
+ }
319
+ })
@@ -0,0 +1,274 @@
1
+ /**
2
+ * Shared workspace and persisted-state contracts.
3
+ *
4
+ * This module owns the JSON shapes written below `.domain-studio/` and the
5
+ * workspace summaries derived around them. Server stores consume these types;
6
+ * they do not redeclare the persisted state.
7
+ */
8
+
9
+ import type { SchemaRevision } from './schema'
10
+
11
+ /** A kernel instance the CLI knows about (a local bookmark or an admin-managed one). */
12
+ export interface InstanceInfo {
13
+ name: string
14
+ url: string
15
+ active: boolean
16
+ kind: 'bookmark' | 'managed'
17
+ }
18
+
19
+ /** Global (NOT per-domain) — the CLI's instances + which is active. Source of truth: `astrale instance list`. */
20
+ export interface InstancesState {
21
+ active: string | null
22
+ instances: InstanceInfo[]
23
+ }
24
+
25
+ /** What the studio recorded the last time it ran a deploy for this domain. */
26
+ export interface DeployRecord {
27
+ at: string
28
+ /** UI/cache fingerprint recorded by Studio; not a canonical schema revision. */
29
+ renderFingerprint: string
30
+ ok: boolean
31
+ url?: string
32
+ }
33
+
34
+ /** Per-domain deploy/install state (the active instance is global — see InstancesState). */
35
+ export interface InstanceStatus {
36
+ /** the domain's configured deploy target (astrale.config.ts prod.instance) */
37
+ deployTarget: string | null
38
+ /** has a `prod` script → deployable via `pnpm prod` */
39
+ deployable: boolean
40
+ /** GROUND TRUTH — queried from `astrale introspect <origin> --bundle`, not local state.
41
+ * 'unknown' = the instance couldn't be reached/queried (no target, not authed, offline). */
42
+ install: 'installed' | 'not-installed' | 'unknown'
43
+ /** local schema vs the schema actually INSTALLED on the instance (ground truth) */
44
+ drift: 'in-sync' | 'drifted' | 'unknown'
45
+ /** Canonical revisions compared only after local and installed SDK admission. */
46
+ localRevision: SchemaRevision | null
47
+ installedRevision: SchemaRevision | null
48
+ /** the studio's own last deploy — supplementary (service URL / when), not the source of truth */
49
+ lastDeploy: DeployRecord | null
50
+ }
51
+
52
+ export interface DeployResult {
53
+ ok: boolean
54
+ url: string | null
55
+ output: string
56
+ }
57
+
58
+ export type ThreadRole = 'user' | 'author'
59
+ export type ThreadEntryType = 'text' | 'choice'
60
+
61
+ export interface ThreadEntry {
62
+ id: string
63
+ role: ThreadRole
64
+ type: ThreadEntryType
65
+ text: string
66
+ options?: string[]
67
+ answer?: string | null
68
+ }
69
+
70
+ export type AnchorKind = 'schema' | 'section' | 'file' | 'free'
71
+
72
+ export interface AnchorRef {
73
+ ref: string
74
+ kind: AnchorKind
75
+ file?: string
76
+ startLine?: number
77
+ endLine?: number
78
+ label?: string
79
+ /** Flow-canvas coordinates for section-level canvas comments. */
80
+ x?: number
81
+ y?: number
82
+ }
83
+
84
+ export interface Comment {
85
+ id: string
86
+ /** annotate-compatible: array of string excerpts */
87
+ anchors: string[]
88
+ /** studio-only, index-aligned to `anchors` */
89
+ anchorRefs: AnchorRef[]
90
+ status: 'open' | 'closed'
91
+ closeNote?: string
92
+ thread: ThreadEntry[]
93
+ createdAt: string
94
+ /** derived: a thread whose first entry is role:'author' is a question */
95
+ kind: 'comment' | 'question'
96
+ /** set when the anchor ref no longer resolves against the current schema */
97
+ orphaned?: boolean
98
+ }
99
+
100
+ export interface CommentStore {
101
+ /** Historical annotate wire key; contains Studio's render fingerprint. */
102
+ schemaVersion: string
103
+ comments: Comment[]
104
+ }
105
+
106
+ export interface MergeResult {
107
+ merged: number
108
+ closed: number
109
+ unknownIds: string[]
110
+ schemaMismatch: boolean
111
+ pastedSchemaVersion?: string
112
+ }
113
+
114
+ export type SchemaChangeKind =
115
+ | 'schema-metadata-changed'
116
+ | 'type-added'
117
+ | 'type-removed'
118
+ | 'type-changed'
119
+ | 'import-added'
120
+ | 'import-removed'
121
+ | 'import-changed'
122
+ | 'class-added'
123
+ | 'class-removed'
124
+ | 'class-contract-changed'
125
+ | 'interface-added'
126
+ | 'interface-removed'
127
+ | 'interface-contract-changed'
128
+ | 'edge-added'
129
+ | 'edge-removed'
130
+ | 'edge-contract-changed'
131
+ | 'definition-metadata-changed'
132
+ | 'prop-added'
133
+ | 'prop-removed'
134
+ | 'prop-type-changed'
135
+ | 'prop-schema-changed'
136
+ | 'prop-required-changed'
137
+ | 'method-added'
138
+ | 'method-removed'
139
+ | 'method-signature-changed'
140
+ | 'method-metadata-changed'
141
+ | 'function-added'
142
+ | 'function-removed'
143
+ | 'function-signature-changed'
144
+ | 'function-metadata-changed'
145
+ | 'view-added'
146
+ | 'view-removed'
147
+ | 'view-changed'
148
+ | 'view-metadata-changed'
149
+ | 'policy-added'
150
+ | 'policy-removed'
151
+ | 'policy-changed'
152
+ | 'dependency-added'
153
+ | 'dependency-removed'
154
+ | 'dependency-changed'
155
+ | 'core-changed'
156
+
157
+ export interface SchemaChange {
158
+ kind: SchemaChangeKind
159
+ target: string
160
+ detail?: string
161
+ }
162
+
163
+ export interface FileChange {
164
+ file: string
165
+ status: 'added' | 'modified' | 'removed'
166
+ }
167
+
168
+ export interface ChangeSet {
169
+ source: 'baseline' | 'git'
170
+ hasGit: boolean
171
+ hasBaseline: boolean
172
+ schemaChanges: SchemaChange[]
173
+ fileChanges: FileChange[]
174
+ schemaDiffText?: string
175
+ /** Describes Studio-observed structure only; never a Runtime migration verdict. */
176
+ structuralStatus: 'none' | 'changed'
177
+ baselineRevision?: SchemaRevision
178
+ currentRevision?: SchemaRevision
179
+ baselineCapturedAt?: string
180
+ }
181
+
182
+ export interface ContextItem {
183
+ id: string
184
+ bucket: 'user' | 'auto'
185
+ title: string
186
+ body: string
187
+ source?: string
188
+ updatedAt: string
189
+ /** auto items: included in the copy payload only when true */
190
+ includeInHandoff?: boolean
191
+ freshness?: string
192
+ }
193
+
194
+ export interface ContextStore {
195
+ user: ContextItem[]
196
+ auto: ContextItem[]
197
+ }
198
+
199
+ /** A document the user drops in as context for the AI agent. */
200
+ export interface DocMeta {
201
+ id: string
202
+ name: string
203
+ type: string
204
+ size: number
205
+ addedAt: string
206
+ updatedAt?: string
207
+ /** relative path under the dotted folder (server-side) */
208
+ stored: string
209
+ }
210
+
211
+ export interface Integration {
212
+ id: string
213
+ name: string
214
+ kind: string
215
+ status: 'planned' | 'active' | 'deprecated' | string
216
+ notes?: string
217
+ }
218
+
219
+ export interface IntegrationsState {
220
+ integrations: Integration[]
221
+ detectedSubfolders: string[]
222
+ }
223
+
224
+ /** A domain in the catalog — local to this workspace, a faked external service, or the kernel. */
225
+ export interface DomainCatalogEntry {
226
+ origin: string
227
+ name: string
228
+ kind: 'kernel' | 'local' | 'external'
229
+ description: string
230
+ /** raw lucide-style SVG icon (stroke="currentColor") */
231
+ icon: string
232
+ /** kernel is always present + required */
233
+ required?: boolean
234
+ }
235
+
236
+ export interface DomainSummary {
237
+ id: string
238
+ origin: string
239
+ path: string
240
+ schemaDir: string
241
+ depsInstalled: boolean
242
+ hasGit: boolean
243
+ configFile: string
244
+ }
245
+
246
+ export interface CopyPayload {
247
+ markdown: string
248
+ openComments: number
249
+ }
250
+
251
+ export interface NodePosition {
252
+ x: number
253
+ y: number
254
+ /** persisted size — only expanded module containers carry one. */
255
+ w?: number
256
+ h?: number
257
+ }
258
+
259
+ /** Persisted manual graph layout. This is the sole authority for its JSON shape. */
260
+ export interface LayoutState {
261
+ renderFingerprint?: string
262
+ positions: Record<string, NodePosition>
263
+ }
264
+
265
+ /** Persisted per-domain canvas visibility. This is the sole authority for its JSON shape. */
266
+ export interface VisibilityState {
267
+ /** refs hidden on the canvas: `class.X` | `edge.X` | `domain.<origin>` (interfaces use
268
+ * `materializedInterfaces`, not this set — see below) */
269
+ hidden: Record<string, true>
270
+ showInheritedEdges: boolean
271
+ /** Local interfaces MATERIALIZED as canvas NODES (default = badge). Keyed by BARE interface
272
+ * name (e.g. `Fulfillable`) — a set distinct from `hidden`, so no ref namespace collision. */
273
+ materializedInterfaces: Record<string, true>
274
+ }
@@ -0,0 +1,116 @@
1
+ import { expect, test } from 'bun:test'
2
+ import { readdirSync, readFileSync, statSync } from 'node:fs'
3
+ import { dirname, join, relative, resolve } from 'node:path'
4
+
5
+ const STUDIO_ROOT = resolve(import.meta.dir, '..')
6
+ const CLIENT_ROOT = join(STUDIO_ROOT, 'client', 'src')
7
+ const SERVER_ROOT = join(STUDIO_ROOT, 'server')
8
+ const SHARED_ROOT = join(STUDIO_ROOT, 'shared')
9
+ const STATE_ROOT = join(SERVER_ROOT, 'state')
10
+
11
+ function productionFiles(dir: string): string[] {
12
+ return readdirSync(dir)
13
+ .flatMap((entry) => {
14
+ const path = join(dir, entry)
15
+ if (statSync(path).isDirectory()) return productionFiles(path)
16
+ if (!/\.(?:ts|tsx)$/.test(path) || /\.test\.(?:ts|tsx)$/.test(path)) return []
17
+ return [path]
18
+ })
19
+ .sort()
20
+ }
21
+
22
+ const files = [
23
+ ...productionFiles(CLIENT_ROOT),
24
+ ...productionFiles(SERVER_ROOT),
25
+ ...productionFiles(SHARED_ROOT),
26
+ ]
27
+ const fileSet = new Set(files)
28
+
29
+ function resolveLocalImport(file: string, specifier: string): string | null {
30
+ const target = specifier.startsWith('@/')
31
+ ? join(CLIENT_ROOT, specifier.slice(2))
32
+ : specifier.startsWith('@shared/')
33
+ ? join(SHARED_ROOT, specifier.slice('@shared/'.length))
34
+ : specifier.startsWith('.')
35
+ ? resolve(dirname(file), specifier)
36
+ : null
37
+ if (!target) return null
38
+ for (const candidate of [
39
+ target,
40
+ `${target}.ts`,
41
+ `${target}.tsx`,
42
+ join(target, 'index.ts'),
43
+ join(target, 'index.tsx'),
44
+ ]) {
45
+ if (fileSet.has(candidate)) return candidate
46
+ }
47
+ return null
48
+ }
49
+
50
+ function imports(file: string): string[] {
51
+ const source = readFileSync(file, 'utf8')
52
+ const found: string[] = []
53
+ const pattern = /(?:from\s+|import\s*(?:\(\s*)?)['"]([^'"]+)['"]\s*\)?|import\s+['"]([^'"]+)['"]/g
54
+ for (const match of source.matchAll(pattern)) {
55
+ const target = resolveLocalImport(file, match[1] ?? match[2])
56
+ if (target) found.push(target)
57
+ }
58
+ return found
59
+ }
60
+
61
+ const studioPath = (file: string) => relative(STUDIO_ROOT, file).replaceAll('\\', '/')
62
+
63
+ test('Studio production modules form an acyclic dependency graph', () => {
64
+ const graph = new Map(files.map((file) => [file, imports(file)]))
65
+ const visiting = new Set<string>()
66
+ const visited = new Set<string>()
67
+ const stack: string[] = []
68
+ const cycles: string[][] = []
69
+
70
+ const visit = (file: string) => {
71
+ if (visiting.has(file)) {
72
+ const start = stack.indexOf(file)
73
+ cycles.push([...stack.slice(start), file].map(studioPath))
74
+ return
75
+ }
76
+ if (visited.has(file)) return
77
+ visiting.add(file)
78
+ stack.push(file)
79
+ for (const dependency of graph.get(file) ?? []) visit(dependency)
80
+ stack.pop()
81
+ visiting.delete(file)
82
+ visited.add(file)
83
+ }
84
+
85
+ for (const file of files) visit(file)
86
+ expect(cycles).toEqual([])
87
+ })
88
+
89
+ test('production boundaries keep client/shared out of server and shared out of client', () => {
90
+ const violations = files.flatMap((file) =>
91
+ imports(file)
92
+ .filter(
93
+ (dependency) =>
94
+ (dependency.startsWith(SERVER_ROOT) &&
95
+ (file.startsWith(CLIENT_ROOT) || file.startsWith(SHARED_ROOT))) ||
96
+ (file.startsWith(SHARED_ROOT) && dependency.startsWith(CLIENT_ROOT)),
97
+ )
98
+ .map((dependency) => `${studioPath(file)} -> ${studioPath(dependency)}`),
99
+ )
100
+ expect(violations).toEqual([])
101
+ })
102
+
103
+ test('state repositories do not depend on workflow modules', () => {
104
+ const workflowRoots = ['api', 'environment', 'handoff', 'instances', 'views', 'workspace'].map(
105
+ (directory) => join(SERVER_ROOT, directory),
106
+ )
107
+ const edges = files
108
+ .filter((file) => file.startsWith(STATE_ROOT))
109
+ .flatMap((file) =>
110
+ imports(file)
111
+ .filter((dependency) => workflowRoots.some((root) => dependency.startsWith(root)))
112
+ .map((dependency) => `${studioPath(file)} -> ${studioPath(dependency)}`),
113
+ )
114
+
115
+ expect(edges).toEqual([])
116
+ })