@astrale-os/cli 1.0.0-beta.0 → 1.0.0-beta.10

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 (322) hide show
  1. package/README.md +23 -12
  2. package/dist/astrale.js +51494 -56307
  3. package/dist/public/connect-core.js +2028 -26846
  4. package/dist/public/keys/index.js +1755 -40694
  5. package/dist/public/paths/index.js +859 -39518
  6. package/dist/types/admin/binding.d.ts +19 -0
  7. package/dist/types/admin/instance/client.d.ts +22 -0
  8. package/dist/types/admin/instance/index.d.ts +2 -0
  9. package/dist/types/admin/instance/model.d.ts +5 -12
  10. package/dist/types/connection/auth.d.ts +3 -0
  11. package/dist/types/connection/call.d.ts +3 -0
  12. package/dist/types/connection/command.d.ts +27 -0
  13. package/dist/types/connection/credential.d.ts +14 -0
  14. package/dist/types/connection/errors.d.ts +1 -0
  15. package/dist/types/connection/exchange.d.ts +9 -0
  16. package/dist/types/connection/failure/classify.d.ts +2 -0
  17. package/dist/types/connection/failure/index.d.ts +5 -0
  18. package/dist/types/connection/failure/model.d.ts +25 -0
  19. package/dist/types/connection/failure/render.d.ts +3 -0
  20. package/dist/types/connection/index.d.ts +8 -0
  21. package/dist/types/connection/reasons.d.ts +37 -0
  22. package/dist/types/connection/self.d.ts +37 -0
  23. package/dist/types/connection/session.d.ts +31 -0
  24. package/dist/types/connection/target.d.ts +38 -0
  25. package/dist/types/errors.d.ts +1 -1
  26. package/dist/types/lib/admin-instance.d.ts +17 -0
  27. package/dist/types/lib/admin-target.d.ts +6 -2
  28. package/dist/types/lib/failure-debug.d.ts +1 -0
  29. package/dist/types/lib/idp.d.ts +7 -0
  30. package/dist/types/lib/instance-target.d.ts +1 -1
  31. package/dist/types/lib/instance.d.ts +10 -2
  32. package/dist/types/lib/log.d.ts +5 -3
  33. package/dist/types/lib/meta.d.ts +2 -2
  34. package/dist/types/lib/proc.d.ts +43 -0
  35. package/dist/types/lib/self.d.ts +9 -0
  36. package/dist/types/lib/update.d.ts +131 -0
  37. package/package.json +8 -10
  38. package/src/__tests__/fixtures/publication.ts +20 -0
  39. package/src/admin/__tests__/binding.test.ts +31 -0
  40. package/src/admin/__tests__/fixture.ts +101 -0
  41. package/src/admin/binding.ts +98 -0
  42. package/src/admin/catalog/.spec/api.d.ts +2 -2
  43. package/src/admin/catalog/__tests__/client.test.ts +20 -49
  44. package/src/admin/catalog/client.ts +36 -32
  45. package/src/admin/catalog/model.ts +3 -4
  46. package/src/admin/graph/.spec/api.d.ts +4 -10
  47. package/src/admin/graph/nodes.ts +1 -1
  48. package/src/admin/instance/.spec/api.d.ts +2 -2
  49. package/src/admin/instance/__tests__/client.test.ts +40 -163
  50. package/src/admin/instance/client.ts +35 -137
  51. package/src/admin/instance/index.ts +1 -2
  52. package/src/admin/instance/model.ts +11 -20
  53. package/src/commands/__tests__/admin-instance.test.ts +11 -8
  54. package/src/commands/__tests__/auth-login.test.ts +54 -1
  55. package/src/commands/__tests__/call-describe.test.ts +46 -33
  56. package/src/commands/__tests__/call.test.ts +5 -1
  57. package/src/commands/__tests__/domain-install-operation.test.ts +12 -7
  58. package/src/commands/__tests__/domain-install-owned.test.ts +6 -2
  59. package/src/commands/__tests__/domain-list.test.ts +63 -4
  60. package/src/commands/__tests__/install-identity-override.test.ts +19 -19
  61. package/src/commands/__tests__/instance-list-rows.test.ts +1 -1
  62. package/src/commands/__tests__/instance-status.test.ts +139 -0
  63. package/src/commands/__tests__/read-commands.test.ts +35 -10
  64. package/src/commands/__tests__/studio-descriptor.test.ts +22 -0
  65. package/src/commands/__tests__/update.test.ts +95 -0
  66. package/src/commands/call-describe.ts +60 -68
  67. package/src/commands/call.ts +43 -56
  68. package/src/commands/domain/install.ts +13 -17
  69. package/src/commands/domain/list.ts +18 -13
  70. package/src/commands/domain/publish.ts +14 -11
  71. package/src/commands/domain/uninstall.ts +2 -2
  72. package/src/commands/get.ts +14 -15
  73. package/src/commands/identity/__tests__/register.test.ts +3 -3
  74. package/src/commands/identity/register.ts +5 -4
  75. package/src/commands/instance/create.ts +8 -16
  76. package/src/commands/instance/delete.ts +12 -9
  77. package/src/commands/instance/list.ts +2 -2
  78. package/src/commands/instance/status.ts +66 -20
  79. package/src/commands/instance/use.ts +16 -12
  80. package/src/commands/introspect.ts +9 -16
  81. package/src/commands/logs.ts +24 -25
  82. package/src/commands/mutate.ts +6 -5
  83. package/src/commands/query.ts +27 -30
  84. package/src/commands/studio.ts +20 -0
  85. package/src/commands/token.ts +8 -7
  86. package/src/commands/update.ts +68 -22
  87. package/src/commands/view.ts +7 -6
  88. package/src/connection/.spec/api.d.ts +7 -8
  89. package/src/connection/.spec/architecture.md +5 -4
  90. package/src/connection/.spec/flows/session.ts +2 -2
  91. package/src/connection/.spec/laws/connection.ts +6 -2
  92. package/src/connection/.spec/layout.ts +1 -0
  93. package/src/connection/__tests__/credential.test.ts +20 -1
  94. package/src/connection/__tests__/errors.test.ts +43 -32
  95. package/src/connection/__tests__/exchange.test.ts +36 -6
  96. package/src/connection/__tests__/failure-fixtures.ts +39 -0
  97. package/src/connection/__tests__/failure-safety.test.ts +75 -0
  98. package/src/connection/__tests__/self.test.ts +31 -11
  99. package/src/connection/__tests__/session.test.ts +3 -3
  100. package/src/connection/__tests__/target.test.ts +1 -0
  101. package/src/connection/auth.ts +13 -0
  102. package/src/connection/call.ts +1 -1
  103. package/src/connection/credential.ts +2 -2
  104. package/src/connection/errors.ts +1 -355
  105. package/src/connection/exchange.ts +52 -27
  106. package/src/connection/failure/classify.ts +110 -0
  107. package/src/connection/failure/index.ts +18 -0
  108. package/src/connection/failure/model.ts +28 -0
  109. package/src/connection/failure/render.ts +116 -0
  110. package/src/connection/reasons.ts +16 -0
  111. package/src/connection/self.ts +39 -21
  112. package/src/connection/session.ts +19 -8
  113. package/src/errors.ts +3 -2
  114. package/src/graph/.spec/api.d.ts +12 -5
  115. package/src/graph/.spec/layout.ts +9 -1
  116. package/src/graph/__tests__/mutation.test.ts +2 -2
  117. package/src/graph/__tests__/query.test.ts +25 -6
  118. package/src/graph/class.ts +30 -0
  119. package/src/graph/index.ts +1 -0
  120. package/src/graph/query.ts +21 -32
  121. package/src/identity/.spec/api.d.ts +2 -2
  122. package/src/identity/__tests__/registration.test.ts +1 -1
  123. package/src/identity/registration.ts +3 -3
  124. package/src/keys/__tests__/keys.test.ts +4 -0
  125. package/src/keys/credential.ts +1 -0
  126. package/src/lib/__tests__/admin-target.test.ts +12 -0
  127. package/src/lib/__tests__/command-dx.test.ts +1 -1
  128. package/src/lib/__tests__/domain-publication.test.ts +138 -0
  129. package/src/lib/__tests__/idp-session.test.ts +22 -0
  130. package/src/lib/__tests__/idp.test.ts +36 -4
  131. package/src/lib/__tests__/instance-candidates.test.ts +1 -3
  132. package/src/lib/__tests__/instance-target.test.ts +7 -39
  133. package/src/lib/__tests__/instance.test.ts +6 -0
  134. package/src/lib/__tests__/log-errors.test.ts +64 -0
  135. package/src/lib/__tests__/studio-server-deps.test.ts +53 -16
  136. package/src/lib/__tests__/update.test.ts +113 -8
  137. package/src/lib/__tests__/view-open-intent.test.ts +0 -2
  138. package/src/lib/__tests__/view-server.test.ts +1 -1
  139. package/src/lib/__tests__/view-session.test.ts +1 -1
  140. package/src/lib/admin-instance.ts +14 -8
  141. package/src/lib/admin-target.ts +21 -2
  142. package/src/lib/command-dx.ts +9 -9
  143. package/src/lib/domain-publication.ts +104 -0
  144. package/src/lib/failure-debug.ts +26 -0
  145. package/src/lib/idp.ts +39 -4
  146. package/src/lib/instance-candidates.ts +2 -2
  147. package/src/lib/instance-target.ts +3 -14
  148. package/src/lib/instance.ts +22 -12
  149. package/src/lib/log.ts +16 -9
  150. package/src/lib/login-flow.ts +13 -3
  151. package/src/lib/meta.ts +2 -2
  152. package/src/lib/provision-instance.ts +34 -57
  153. package/src/lib/update.ts +124 -30
  154. package/src/lib/validation.ts +2 -2
  155. package/src/lib/view/resolve.ts +1 -1
  156. package/src/program/__tests__/program.test.ts +20 -4
  157. package/src/program/build.ts +2 -2
  158. package/src/setup/__tests__/instance-step.test.ts +27 -10
  159. package/src/setup/steps/instance.ts +22 -15
  160. package/studio/client/dist/assets/{elk-api-D2xgMJvi.js → elk-api-ByzoDZWH.js} +1 -1
  161. package/studio/client/dist/assets/index-B0RCgUOT.js +8 -0
  162. package/studio/client/dist/assets/index-BiGR8Oo9.css +1 -0
  163. package/studio/client/dist/assets/index-N8J8EKlB.js +81 -0
  164. package/studio/client/dist/index.html +2 -2
  165. package/studio/package.json +5 -10
  166. package/studio/server/agent/ask.test.ts +1 -1
  167. package/studio/server/agent/bridge/grant.test.ts +1 -1
  168. package/studio/server/agent/bridge/routes.test.ts +1 -1
  169. package/studio/server/agent/bridge/stdio.ts +8 -1
  170. package/studio/server/agent/conversation.test.ts +10 -5
  171. package/studio/server/agent/conversation.ts +56 -8
  172. package/studio/server/agent/harness/claude/adapter.test.ts +15 -5
  173. package/studio/server/agent/harness/codex/loadout.ts +18 -21
  174. package/studio/server/agent/harness/gateway/config.ts +20 -18
  175. package/studio/server/agent/harness/selection.ts +9 -1
  176. package/studio/server/agent/prompts/anchors.test.ts +32 -36
  177. package/studio/server/agent/prompts/anchors.ts +20 -49
  178. package/studio/server/agent/prompts/system.test.ts +3 -2
  179. package/studio/server/agent/prompts/system.ts +6 -5
  180. package/studio/server/agent/prompts/turn.ts +13 -4
  181. package/studio/server/agent/routes.test.ts +1 -1
  182. package/studio/server/agent/run/completion.ts +2 -2
  183. package/studio/server/agent/run/coordinator.test.ts +1 -1
  184. package/studio/server/agent/run/preparation.ts +6 -5
  185. package/studio/server/agent/run/transcript.test.ts +35 -1
  186. package/studio/server/agent/run/transcript.ts +169 -3
  187. package/studio/server/agent/run/usage.test.ts +24 -0
  188. package/studio/server/agent/run/usage.ts +26 -1
  189. package/studio/server/api/canvas.ts +43 -0
  190. package/studio/server/api/comments.ts +85 -0
  191. package/studio/server/api/context.ts +61 -0
  192. package/studio/server/api/deployment.ts +39 -0
  193. package/studio/server/api/documents.ts +55 -0
  194. package/studio/server/api/domain.ts +59 -0
  195. package/studio/server/api/http.ts +45 -0
  196. package/studio/server/api/project.ts +57 -0
  197. package/studio/server/api/schema.ts +12 -0
  198. package/studio/server/api/views.ts +57 -0
  199. package/studio/server/api/workspace.ts +60 -0
  200. package/studio/server/api-agent-loadout.test.ts +1 -1
  201. package/studio/server/api.test.ts +147 -0
  202. package/studio/server/api.ts +20 -422
  203. package/studio/server/cache.test.ts +122 -0
  204. package/studio/server/cache.ts +162 -11
  205. package/studio/server/cli-consumers.test.ts +494 -0
  206. package/studio/server/cli.test.ts +88 -0
  207. package/studio/server/cli.ts +240 -0
  208. package/studio/server/client-package.ts +25 -1
  209. package/studio/server/domain.test.ts +50 -40
  210. package/studio/server/domain.ts +83 -52
  211. package/studio/server/environment/dotenv-preview.test.ts +39 -0
  212. package/studio/server/environment/dotenv-preview.ts +27 -0
  213. package/studio/server/environment/files.test.ts +53 -0
  214. package/studio/server/{state/env.ts → environment/files.ts} +7 -48
  215. package/studio/server/{state → handoff}/copy.ts +21 -6
  216. package/studio/server/handoff/service.test.ts +22 -0
  217. package/studio/server/{state/handoff.ts → handoff/service.ts} +21 -14
  218. package/studio/server/index.ts +1 -1
  219. package/studio/server/instances/active.ts +72 -0
  220. package/studio/server/instances/deploy-record.test.ts +31 -0
  221. package/studio/server/instances/deploy-record.ts +37 -0
  222. package/studio/server/instances/deploy.ts +56 -0
  223. package/studio/server/{state/instance.test.ts → instances/probe.test.ts} +25 -21
  224. package/studio/server/instances/probe.ts +67 -0
  225. package/studio/server/instances/status.test.ts +12 -0
  226. package/studio/server/instances/status.ts +49 -0
  227. package/studio/server/introspect/anatomy/client-tree.ts +57 -0
  228. package/studio/server/introspect/anatomy/env-fields.ts +66 -0
  229. package/studio/server/introspect/anatomy/schema-definition.ts +53 -0
  230. package/studio/server/introspect/anatomy/source.ts +181 -0
  231. package/studio/server/introspect/anatomy/views/routes.ts +114 -0
  232. package/studio/server/introspect/anatomy/views.ts +53 -0
  233. package/studio/server/introspect/anatomy-extras.test.ts +106 -35
  234. package/studio/server/introspect/anatomy-extras.ts +7 -726
  235. package/studio/server/introspect/anatomy.test.ts +22 -14
  236. package/studio/server/introspect/anatomy.ts +52 -28
  237. package/studio/server/introspect/bundle.ts +15 -9
  238. package/studio/server/introspect/canonical-schema.test.ts +93 -309
  239. package/studio/server/introspect/canonical-schema.ts +257 -425
  240. package/studio/server/introspect/config-preview.test.ts +32 -0
  241. package/studio/server/introspect/config-preview.ts +119 -0
  242. package/studio/server/introspect/core-extractor.ts +18 -127
  243. package/studio/server/introspect/core.ts +1 -2
  244. package/studio/server/introspect/diff.test.ts +18 -18
  245. package/studio/server/introspect/diff.ts +44 -90
  246. package/studio/server/introspect/extractor.ts +23 -116
  247. package/studio/server/introspect/hash.ts +5 -2
  248. package/studio/server/introspect/overlay-tsmorph.test.ts +119 -240
  249. package/studio/server/introspect/overlay-tsmorph.ts +7 -1305
  250. package/studio/server/introspect/overlay.test.ts +26 -54
  251. package/studio/server/introspect/overlay.ts +10 -37
  252. package/studio/server/introspect/revision.test.ts +54 -0
  253. package/studio/server/introspect/revision.ts +49 -0
  254. package/studio/server/introspect/runtime.test.ts +83 -143
  255. package/studio/server/introspect/runtime.ts +40 -11
  256. package/studio/server/introspect/schema-ir-json.test.ts +70 -0
  257. package/studio/server/introspect/schema-ir-json.ts +151 -0
  258. package/studio/server/introspect/schema-refs.test.ts +43 -88
  259. package/studio/server/introspect/schema-refs.ts +14 -49
  260. package/studio/server/introspect/source-overlay/annotations.ts +14 -0
  261. package/studio/server/introspect/source-overlay/handlers.ts +161 -0
  262. package/studio/server/introspect/source-overlay/kernel-calls.ts +46 -0
  263. package/studio/server/introspect/source-overlay/project.ts +248 -0
  264. package/studio/server/introspect/source-overlay/spans.ts +330 -0
  265. package/studio/server/json.ts +46 -0
  266. package/studio/server/lifecycle.ts +5 -1
  267. package/studio/server/sse.test.ts +26 -0
  268. package/studio/server/sse.ts +4 -1
  269. package/studio/server/state/baseline.test.ts +223 -7
  270. package/studio/server/state/baseline.ts +148 -34
  271. package/studio/server/state/comments.test.ts +66 -1
  272. package/studio/server/state/comments.ts +173 -10
  273. package/studio/server/state/context.ts +34 -2
  274. package/studio/server/state/documents.ts +42 -1
  275. package/studio/server/state/integrations.ts +27 -1
  276. package/studio/server/state/layout.test.ts +34 -0
  277. package/studio/server/state/layout.ts +44 -17
  278. package/studio/server/state/settings.ts +29 -22
  279. package/studio/server/state/store.test.ts +122 -0
  280. package/studio/server/state/store.ts +66 -17
  281. package/studio/server/state/visibility.ts +22 -11
  282. package/studio/server/views/model.test.ts +57 -0
  283. package/studio/server/views/model.ts +66 -0
  284. package/studio/server/views/runtime.ts +40 -0
  285. package/studio/server/views/selection-repository.test.ts +31 -0
  286. package/studio/server/views/selection-repository.ts +60 -0
  287. package/studio/server/views/session.test.ts +73 -0
  288. package/studio/server/views/session.ts +166 -0
  289. package/studio/server/views/target.test.ts +78 -0
  290. package/studio/server/views/target.ts +222 -0
  291. package/studio/server/watch.test.ts +11 -11
  292. package/studio/server/watch.ts +9 -5
  293. package/studio/server/workspace/catalog.test.ts +17 -0
  294. package/studio/server/workspace/catalog.ts +43 -0
  295. package/studio/server/{state → workspace}/create.test.ts +1 -1
  296. package/studio/server/{state → workspace}/create.ts +1 -1
  297. package/studio/server/{state → workspace}/git.ts +1 -1
  298. package/studio/server/workspace/updates.ts +83 -0
  299. package/studio/server/workspace-state.ts +1 -1
  300. package/studio/server/workspace-watch.ts +2 -2
  301. package/studio/shared/contracts/README.md +16 -0
  302. package/studio/shared/contracts/agent.ts +270 -0
  303. package/studio/shared/contracts/runtime.ts +50 -0
  304. package/studio/shared/contracts/schema.ts +393 -0
  305. package/studio/shared/contracts/surface.test.ts +317 -0
  306. package/studio/shared/contracts/workspace.ts +265 -0
  307. package/studio/shared/layout.test.ts +116 -0
  308. package/studio/shared/schema/identity.test.ts +34 -0
  309. package/studio/shared/schema/identity.ts +68 -0
  310. package/studio/shared/types.ts +11 -1099
  311. package/studio/tsconfig.json +1 -1
  312. package/viewer/dist/main.js +40 -61
  313. package/.check-workspace.cjs +0 -40
  314. package/src/commands/__tests__/instance-create-hosts.test.ts +0 -29
  315. package/studio/client/dist/assets/index-BMdnsIJA.css +0 -1
  316. package/studio/client/dist/assets/index-D-vRV8w7.js +0 -8
  317. package/studio/client/dist/assets/index-LGSWRrk8.js +0 -81
  318. package/studio/server/state/catalog.ts +0 -117
  319. package/studio/server/state/instance.ts +0 -280
  320. package/studio/server/state/updates.ts +0 -63
  321. package/studio/server/state/views.test.ts +0 -286
  322. package/studio/server/state/views.ts +0 -535
@@ -4,8 +4,8 @@
4
4
  <meta charset="UTF-8" />
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
6
  <title>Domain Studio</title>
7
- <script type="module" crossorigin src="/assets/index-LGSWRrk8.js"></script>
8
- <link rel="stylesheet" crossorigin href="/assets/index-BMdnsIJA.css">
7
+ <script type="module" crossorigin src="/assets/index-N8J8EKlB.js"></script>
8
+ <link rel="stylesheet" crossorigin href="/assets/index-BiGR8Oo9.css">
9
9
  </head>
10
10
  <body>
11
11
  <div id="root"></div>
@@ -16,10 +16,13 @@
16
16
  "build": "vite build",
17
17
  "start": "bun server/index.ts",
18
18
  "dev:client": "vite",
19
+ "test": "bun test client/src server shared",
20
+ "test:e2e": "pnpm build && playwright test",
19
21
  "typecheck": "tsgo --noEmit"
20
22
  },
21
23
  "dependencies": {
22
- "@astrale-os/shell": "file:../vendor/astrale-os-shell-0.3.8-beta.2.tgz",
24
+ "@astrale-os/sdk": "0.5.0-beta.32",
25
+ "@astrale-os/shell": "0.4.2-beta.1",
23
26
  "@dagrejs/dagre": "^3.0.0",
24
27
  "@radix-ui/react-collapsible": "^1.1.0",
25
28
  "@radix-ui/react-dialog": "^1.1.6",
@@ -29,7 +32,6 @@
29
32
  "@radix-ui/react-scroll-area": "^1.2.3",
30
33
  "@radix-ui/react-separator": "^1.1.2",
31
34
  "@radix-ui/react-slot": "^1.1.2",
32
- "@radix-ui/react-tabs": "^1.1.3",
33
35
  "@radix-ui/react-tooltip": "^1.1.8",
34
36
  "@tanstack/react-query": "^5.66.0",
35
37
  "@xyflow/react": "^12.4.4",
@@ -49,6 +51,7 @@
49
51
  "zustand": "^5.0.3"
50
52
  },
51
53
  "devDependencies": {
54
+ "@playwright/test": "^1.62.1",
52
55
  "@tailwindcss/vite": "^4.0.6",
53
56
  "@types/node": "^20.19.43",
54
57
  "@types/react": "^19.2.0",
@@ -58,13 +61,5 @@
58
61
  "tailwindcss": "^4.0.6",
59
62
  "typescript": "^5.7.3",
60
63
  "vite": "^7.0.0"
61
- },
62
- "overrides": {
63
- "@astrale-os/kernel-client": "file:../vendor/kernel/astrale-os-kernel-client-0.6.0-beta.3.tgz",
64
- "@astrale-os/kernel-core": "file:../vendor/kernel/astrale-os-kernel-core-0.9.0-beta.2.tgz",
65
- "@astrale-os/kernel-dsl": "file:../vendor/kernel/astrale-os-kernel-dsl-0.2.0-beta.2.tgz",
66
- "@astrale-os/kernel-protocol": "file:../vendor/kernel/astrale-os-kernel-protocol-0.5.0-beta.2.tgz",
67
- "@astrale-os/kernel-server": "file:../vendor/kernel/astrale-os-kernel-server-0.5.0-beta.3.tgz",
68
- "@astrale-os/sdk": "file:../vendor/astrale-os-sdk-0.5.0-beta.3.tgz"
69
64
  }
70
65
  }
@@ -35,7 +35,7 @@ test('Ask uses the active harness model and matching per-harness conversation',
35
35
  roots.push(root)
36
36
  mkdirSync(join(root, 'schema'))
37
37
  writeFileSync(join(root, 'astrale.config.ts'), 'export default {}\n')
38
- writeFileSync(join(root, 'domain.ts'), 'export default {}\n')
38
+ writeFileSync(join(root, 'application.ts'), 'export default {}\n')
39
39
  writeFileSync(join(root, 'schema/index.ts'), 'export const Test = {}\n')
40
40
  const handle = registerDomain(root)!
41
41
  domainIds.push(handle.id)
@@ -18,7 +18,7 @@ function domain(root: string, id: string): DomainHandle {
18
18
  id,
19
19
  root,
20
20
  configFile: join(root, 'astrale.config.ts'),
21
- domainFile: join(root, 'domain.ts'),
21
+ applicationFile: join(root, 'application.ts'),
22
22
  schemaDirName: 'schema',
23
23
  schemaDir: join(root, 'schema'),
24
24
  schemaIndex: join(root, 'schema/index.ts'),
@@ -20,7 +20,7 @@ function domain(root: string, id: string): DomainHandle {
20
20
  id,
21
21
  root,
22
22
  configFile: join(root, 'astrale.config.ts'),
23
- domainFile: join(root, 'domain.ts'),
23
+ applicationFile: join(root, 'application.ts'),
24
24
  schemaDirName: 'schema',
25
25
  schemaDir: join(root, 'schema'),
26
26
  schemaIndex: join(root, 'schema/index.ts'),
@@ -10,6 +10,7 @@
10
10
  */
11
11
  import { readFileSync } from 'node:fs'
12
12
 
13
+ import { asJsonRecord, asString, parseJson } from '../../json'
13
14
  import { forwardBridgeTool } from './client'
14
15
 
15
16
  function configFromArgv(): { base?: string; token?: string } {
@@ -17,7 +18,13 @@ function configFromArgv(): { base?: string; token?: string } {
17
18
  const path = i >= 0 ? process.argv[i + 1] : undefined
18
19
  if (!path) return {}
19
20
  try {
20
- return JSON.parse(readFileSync(path, 'utf8')) as { base?: string; token?: string }
21
+ const record = asJsonRecord(parseJson(readFileSync(path, 'utf8')))
22
+ const base = asString(record?.base)
23
+ const token = asString(record?.token)
24
+ return {
25
+ ...(base === undefined ? {} : { base }),
26
+ ...(token === undefined ? {} : { token }),
27
+ }
21
28
  } catch {
22
29
  return {}
23
30
  }
@@ -3,6 +3,7 @@ import { mkdtempSync, rmSync } from 'node:fs'
3
3
  import { tmpdir } from 'node:os'
4
4
  import { join } from 'node:path'
5
5
 
6
+ import { asJsonRecord } from '../json'
6
7
  import { readJson, writeJson } from '../state/store'
7
8
  import {
8
9
  clearConversation,
@@ -60,10 +61,14 @@ describe('per-harness conversations', () => {
60
61
  expect(readConversation(dir, 'claude').turns).toBe(4)
61
62
 
62
63
  setConversationSession(dir, 'codex', 'codex-thread')
63
- const stored = readJson<any>(dir, '.cache/agent/session.json', {})
64
- expect(stored.version).toBe(1)
65
- expect(stored.conversations.claude.sessionId).toBe('legacy-session')
66
- expect(stored.conversations.codex.sessionId).toBe('codex-thread')
64
+ const stored = readJson(dir, '.cache/agent/session.json', asJsonRecord, {})
65
+ expect(stored).toMatchObject({
66
+ version: 1,
67
+ conversations: {
68
+ claude: { sessionId: 'legacy-session' },
69
+ codex: { sessionId: 'codex-thread' },
70
+ },
71
+ })
67
72
  })
68
73
 
69
74
  test('fails closed without rewriting a future conversation-store version', () => {
@@ -79,6 +84,6 @@ describe('per-harness conversations', () => {
79
84
  expect(() => setConversationSession(dir, 'claude', 'new-session')).toThrow(
80
85
  'unsupported agent conversation store version: 2',
81
86
  )
82
- expect(readJson<any>(dir, '.cache/agent/session.json', {})).toEqual(future)
87
+ expect(readJson(dir, '.cache/agent/session.json', asJsonRecord, {})).toEqual(future)
83
88
  })
84
89
  })
@@ -1,5 +1,6 @@
1
1
  import type { AgentSessionInfo, ConversationInfo } from '../../shared/types'
2
2
 
3
+ import { asFiniteNumber, asJsonRecord, asString } from '../json'
3
4
  import { readJson, removeState, writeJson } from '../state/store'
4
5
 
5
6
  const SESSION_FILE = '.cache/agent/session.json'
@@ -19,22 +20,69 @@ interface LegacySessionState extends HarnessConversation {
19
20
  harness?: string
20
21
  }
21
22
 
23
+ type DecodedConversationWire =
24
+ | {
25
+ kind: 'versioned'
26
+ version: number | undefined
27
+ conversations: Record<string, HarnessConversation>
28
+ }
29
+ | { kind: 'legacy'; state: LegacySessionState }
30
+
31
+ function decodeHarnessConversation(value: unknown): HarnessConversation | undefined {
32
+ const record = asJsonRecord(value)
33
+ if (!record) return undefined
34
+ const sessionId = asString(record.sessionId)
35
+ const turnsValue = asFiniteNumber(record.turns)
36
+ const turns =
37
+ turnsValue !== undefined && turnsValue >= 0 && Number.isInteger(turnsValue)
38
+ ? turnsValue
39
+ : undefined
40
+ const updatedAt = asString(record.updatedAt)
41
+ return {
42
+ ...(sessionId === undefined ? {} : { sessionId }),
43
+ ...(turns === undefined ? {} : { turns }),
44
+ ...(updatedAt === undefined ? {} : { updatedAt }),
45
+ }
46
+ }
47
+
48
+ function decodeConversationWire(value: unknown): DecodedConversationWire | undefined {
49
+ const record = asJsonRecord(value)
50
+ if (!record) return undefined
51
+ if ('conversations' in record) {
52
+ const conversations: Record<string, HarnessConversation> = {}
53
+ for (const [harness, candidate] of Object.entries(asJsonRecord(record.conversations) ?? {})) {
54
+ const conversation = decodeHarnessConversation(candidate)
55
+ if (conversation) conversations[harness] = conversation
56
+ }
57
+ return {
58
+ kind: 'versioned',
59
+ version: asFiniteNumber(record.version),
60
+ conversations,
61
+ }
62
+ }
63
+ const state = decodeHarnessConversation(record)
64
+ if (!state) return undefined
65
+ const harness = asString(record.harness)
66
+ return { kind: 'legacy', state: { ...state, ...(harness === undefined ? {} : { harness }) } }
67
+ }
68
+
22
69
  function readStore(root: string): ConversationStore {
23
- const raw = readJson<ConversationStore | LegacySessionState | null>(root, SESSION_FILE, null)
70
+ const raw = readJson(root, SESSION_FILE, decodeConversationWire, null)
24
71
  if (!raw) return { version: 1, conversations: {} }
25
- if ('conversations' in raw) {
72
+ if (raw.kind === 'versioned') {
26
73
  if (raw.version !== 1)
27
74
  throw new Error(`unsupported agent conversation store version: ${String(raw.version)}`)
28
- if (raw.conversations) return { version: 1, conversations: { ...raw.conversations } }
75
+ return { version: 1, conversations: { ...raw.conversations } }
29
76
  }
30
- if ('harness' in raw && raw.harness && raw.sessionId) {
77
+ const legacy = raw.state
78
+ if (legacy.harness && legacy.sessionId) {
31
79
  return {
32
80
  version: 1,
33
81
  conversations: {
34
- [raw.harness]: {
35
- sessionId: raw.sessionId,
36
- turns: raw.turns ?? 0,
37
- updatedAt: raw.updatedAt,
82
+ [legacy.harness]: {
83
+ sessionId: legacy.sessionId,
84
+ turns: legacy.turns ?? 0,
85
+ updatedAt: legacy.updatedAt,
38
86
  },
39
87
  },
40
88
  }
@@ -323,10 +323,20 @@ test('Claude Ask cancellation kills the entire stubborn subprocess group', async
323
323
  onDelta: () => {},
324
324
  })
325
325
 
326
- await waitFor(() => existsSync(pidLog))
327
- const pids = JSON.parse(readFileSync(pidLog, 'utf8')) as { parent: number; child: number }
328
- expect(processExists(pids.parent)).toBe(true)
329
- expect(processExists(pids.child)).toBe(true)
326
+ let pids: { parent: number; child: number } | undefined
327
+ await waitFor(() => {
328
+ if (!existsSync(pidLog)) return false
329
+ try {
330
+ pids = JSON.parse(readFileSync(pidLog, 'utf8')) as { parent: number; child: number }
331
+ return Number.isInteger(pids.parent) && Number.isInteger(pids.child)
332
+ } catch {
333
+ return false
334
+ }
335
+ })
336
+ if (!pids) throw new Error('fake Claude did not publish complete process evidence')
337
+ const processIds = pids
338
+ expect(processExists(processIds.parent)).toBe(true)
339
+ expect(processExists(processIds.child)).toBe(true)
330
340
  controller.abort()
331
341
 
332
342
  expect(await resultPromise).toEqual({
@@ -334,5 +344,5 @@ test('Claude Ask cancellation kills the entire stubborn subprocess group', async
334
344
  isError: true,
335
345
  errorMessage: 'canceled',
336
346
  })
337
- await waitFor(() => !processExists(pids.parent) && !processExists(pids.child))
347
+ await waitFor(() => !processExists(processIds.parent) && !processExists(processIds.child))
338
348
  })
@@ -1,18 +1,11 @@
1
1
  import type { HarnessLoadout } from '../../../../shared/types'
2
2
  import type { HarnessLoadoutOptions } from '../adapter'
3
3
 
4
+ import { asJsonArray, asJsonRecord, asString, parseJson } from '../../../json'
4
5
  import { captureCommand } from '../process'
5
6
  import { probeCodexModels } from './models'
6
7
  import { scanCodexSkills, type CodexPlugin } from './skills'
7
8
 
8
- function parseJson<T>(text: string, fallback: T): T {
9
- try {
10
- return JSON.parse(text) as T
11
- } catch {
12
- return fallback
13
- }
14
- }
15
-
16
9
  /** Inspect Codex configuration, plugins, skills, MCP servers, and models. */
17
10
  export async function loadCodexConfiguration(
18
11
  bin: string,
@@ -24,15 +17,15 @@ export async function loadCodexConfiguration(
24
17
  captureCommand(bin, ['plugin', 'list', '--json'], root),
25
18
  probeCodexModels(bin, root, options?.model, options?.env),
26
19
  ])
27
- const pluginWire = parseJson<any>(pluginResult.stdout, {})
28
- const plugins: CodexPlugin[] = (Array.isArray(pluginWire?.installed) ? pluginWire.installed : [])
29
- .map((plugin: any) => ({
30
- name: String(plugin?.name ?? plugin?.pluginId?.split('@')[0] ?? ''),
31
- path: String(plugin?.source?.path ?? ''),
32
- enabled: plugin?.enabled === true,
33
- }))
34
- .filter((plugin: CodexPlugin) => !!plugin.name && !!plugin.path)
35
- const mcpWire = parseJson<any[]>(mcpResult.stdout, [])
20
+ const pluginWire = asJsonRecord(parseJson(pluginResult.stdout))
21
+ const plugins: CodexPlugin[] = (asJsonArray(pluginWire?.installed) ?? []).flatMap((value) => {
22
+ const plugin = asJsonRecord(value)
23
+ const pluginId = asString(plugin?.pluginId)
24
+ const name = asString(plugin?.name) ?? pluginId?.split('@')[0] ?? ''
25
+ const path = asString(asJsonRecord(plugin?.source)?.path) ?? ''
26
+ return name && path ? [{ name, path, enabled: plugin?.enabled === true }] : []
27
+ })
28
+ const mcpWire = asJsonArray(parseJson(mcpResult.stdout)) ?? []
36
29
  return {
37
30
  ok: mcpResult.code === 0 && pluginResult.code === 0 && modelResult.ok,
38
31
  detail:
@@ -50,10 +43,14 @@ export async function loadCodexConfiguration(
50
43
  models: modelResult.models,
51
44
  cwd: root,
52
45
  tools: [],
53
- mcpServers: mcpWire.map((server) => ({
54
- name: String(server?.name ?? ''),
55
- status: server?.enabled === false ? 'disabled' : String(server?.auth_status ?? 'configured'),
56
- })),
46
+ mcpServers: mcpWire.map((value) => {
47
+ const server = asJsonRecord(value)
48
+ return {
49
+ name: asString(server?.name) ?? '',
50
+ status:
51
+ server?.enabled === false ? 'disabled' : (asString(server?.auth_status) ?? 'configured'),
52
+ }
53
+ }),
57
54
  skills: scanCodexSkills(root, plugins),
58
55
  agents: [],
59
56
  builtinCommandCount: 0,
@@ -22,6 +22,7 @@ import type {
22
22
  HarnessGatewayState,
23
23
  } from '../../../../shared/types'
24
24
 
25
+ import { asJsonRecord, asString, parseJson } from '../../../json'
25
26
  import { readJson, removeState, statePath, writeJson } from '../../../state/store'
26
27
  import { acquireGatewayToken } from './token'
27
28
 
@@ -32,29 +33,35 @@ const GLOBAL_FILE = join(homedir(), '.domain-studio', 'harness-gateway.json')
32
33
 
33
34
  /** Coerce a wire/disk auth block into a well-formed discriminated union. Default
34
35
  * is `mint` (no secret on disk). A legacy `{ apiKey }` shape maps to `token` mode. */
35
- function normalizeAuth(input: any): HarnessGatewayAuth {
36
- if (input?.mode === 'token' || (input?.token != null && input?.mode == null))
37
- return { mode: 'token', token: typeof input.token === 'string' ? input.token.trim() : '' }
38
- if (input?.mode === 'host') return { mode: 'host' }
39
- const instance = typeof input?.instance === 'string' ? input.instance.trim() : ''
36
+ function normalizeAuth(input: unknown): HarnessGatewayAuth {
37
+ const record = asJsonRecord(input)
38
+ if (record?.mode === 'token' || (record?.token != null && record?.mode == null))
39
+ return { mode: 'token', token: asString(record.token)?.trim() ?? '' }
40
+ if (record?.mode === 'host') return { mode: 'host' }
41
+ const instance = asString(record?.instance)?.trim() ?? ''
40
42
  return { mode: 'mint', ...(instance ? { instance } : {}) }
41
43
  }
42
44
 
43
45
  /** Coerce a wire/disk value into a well-formed config (trim, drop blanks). */
44
- function normalize(input: any): HarnessGatewayConfig {
45
- const model = typeof input?.model === 'string' ? input.model.trim() : ''
46
+ function normalize(input: unknown): HarnessGatewayConfig {
47
+ const record = asJsonRecord(input) ?? {}
48
+ const model = asString(record.model)?.trim() ?? ''
46
49
  // back-compat: a pre-union config stored only `apiKey` → treat as a static token
47
50
  const authInput =
48
- input?.auth ??
49
- (typeof input?.apiKey === 'string' ? { mode: 'token', token: input.apiKey } : undefined)
51
+ record.auth ??
52
+ (typeof record.apiKey === 'string' ? { mode: 'token', token: record.apiKey } : undefined)
50
53
  return {
51
- enabled: input?.enabled === true,
52
- baseUrl: typeof input?.baseUrl === 'string' ? input.baseUrl.trim() : '',
54
+ enabled: record.enabled === true,
55
+ baseUrl: asString(record.baseUrl)?.trim() ?? '',
53
56
  ...(model ? { model } : {}),
54
57
  auth: normalizeAuth(authInput),
55
58
  }
56
59
  }
57
60
 
61
+ function decodeConfig(value: unknown): HarnessGatewayConfig | undefined {
62
+ return asJsonRecord(value) ? normalize(value) : undefined
63
+ }
64
+
58
65
  function validateEnabledConfig(config: HarnessGatewayConfig): void {
59
66
  if (!config.enabled) return
60
67
  if (!config.baseUrl) throw new Error('gateway base URL is required while enabled')
@@ -71,17 +78,12 @@ function validateEnabledConfig(config: HarnessGatewayConfig): void {
71
78
  }
72
79
 
73
80
  function readLocal(root: string): HarnessGatewayConfig | null {
74
- const raw = readJson<HarnessGatewayConfig | null>(root, LOCAL_FILE, null)
75
- return raw ? normalize(raw) : null
81
+ return readJson(root, LOCAL_FILE, decodeConfig, null)
76
82
  }
77
83
 
78
84
  function readGlobal(): HarnessGatewayConfig | null {
79
85
  if (!existsSync(GLOBAL_FILE)) return null
80
- try {
81
- return normalize(JSON.parse(readFileSync(GLOBAL_FILE, 'utf8')))
82
- } catch {
83
- return null
84
- }
86
+ return decodeConfig(parseJson(readFileSync(GLOBAL_FILE, 'utf8'))) ?? null
85
87
  }
86
88
 
87
89
  /** Write (or, on null, delete) the global default. Not under any domain root, so
@@ -2,6 +2,7 @@ import type { AgentEffort, StudioSettings } from '../../../shared/types'
2
2
  import type { AgentHarness } from './adapter'
3
3
 
4
4
  import { effectiveAgentEffort } from '../../../shared/agent-effort'
5
+ import { asJsonRecord, asString } from '../../json'
5
6
  import { readSettings } from '../../state/settings'
6
7
  import { readJson, writeJson } from '../../state/store'
7
8
  import { resolveHarnessEnv } from './gateway/config'
@@ -27,11 +28,18 @@ export type HarnessConfigurationResult =
27
28
  | { ok: true; configuration: HarnessConfiguration }
28
29
  | { ok: false; error: string }
29
30
 
31
+ function decodeStoredSelection(value: unknown): { id?: string } | undefined {
32
+ const record = asJsonRecord(value)
33
+ if (!record) return undefined
34
+ const id = asString(record.id)
35
+ return id === undefined ? {} : { id }
36
+ }
37
+
30
38
  export function getHarnessSelection(root: string): HarnessSelection {
31
39
  const environment = process.env.DOMAIN_STUDIO_HARNESS?.trim().toLowerCase()
32
40
  if (environment && hasHarness(environment))
33
41
  return { id: environment, locked: true, source: 'environment' }
34
- const stored = readJson<{ id?: string }>(root, SELECTION_FILE, {})
42
+ const stored = readJson(root, SELECTION_FILE, decodeStoredSelection, {})
35
43
  const id = stored.id?.toLowerCase()
36
44
  if (id && hasHarness(id)) return { id, locked: false, source: 'domain' }
37
45
  return { id: 'claude', locked: false, source: 'default' }
@@ -4,30 +4,35 @@ import type { SchemaIR } from '../../../shared/types'
4
4
 
5
5
  import { describeAnchor } from './anchors'
6
6
 
7
- const importedKey = 'directory.example.dev:interface.Named' as const
8
-
7
+ const importedKey = 'directory.example.dev:class.Named' as const
9
8
  const ir: SchemaIR = {
10
9
  format: 'astrale.dsl',
11
10
  version: 'v1',
12
11
  domain: 'example.dev',
13
- types: {},
14
- interfaces: {},
15
- classes: {},
16
- imports: {},
12
+ classes: {
13
+ Item: {
14
+ type: 'node',
15
+ name: 'Item',
16
+ origin: 'example.dev',
17
+ ref: { origin: 'example.dev', kind: 'class', name: 'Item' },
18
+ properties: { count: { type: 'integer' } },
19
+ required: ['count'],
20
+ methods: {},
21
+ },
22
+ },
17
23
  importsByKey: {
18
24
  [importedKey]: {
19
25
  origin: 'directory.example.dev',
20
- definition: 'interface',
21
26
  key: importedKey,
22
- ref: { origin: 'directory.example.dev', kind: 'interface', name: 'Named' },
27
+ ref: { origin: 'directory.example.dev', kind: 'class', name: 'Named' },
23
28
  },
24
29
  },
25
- importedInterfacesByKey: {
30
+ importedClassesByKey: {
26
31
  [importedKey]: {
27
- type: 'interface',
32
+ type: 'node',
28
33
  name: 'Named',
29
34
  origin: 'directory.example.dev',
30
- ref: { origin: 'directory.example.dev', kind: 'interface', name: 'Named' },
35
+ ref: { origin: 'directory.example.dev', kind: 'class', name: 'Named' },
31
36
  properties: { label: { type: 'string' } },
32
37
  required: [],
33
38
  methods: {},
@@ -36,39 +41,30 @@ const ir: SchemaIR = {
36
41
  functions: {
37
42
  inspect: {
38
43
  name: 'inspect',
39
- input: {
40
- type: 'object',
41
- properties: { cursor: { type: 'string' } },
42
- required: [],
43
- },
44
- params: { cursor: { type: 'string' } },
45
- requiredParams: [],
44
+ input: { type: 'object', properties: { cursor: { type: 'string' } }, required: [] },
46
45
  output: { mode: 'stream', item: { type: 'integer' } },
47
- returns: { type: 'integer' },
48
- static: true,
49
- inheritance: 'default',
50
46
  auth: 'authenticated',
51
47
  },
52
48
  },
49
+ views: {},
50
+ policies: {},
51
+ dependencies: [],
52
+ core: {},
53
53
  }
54
54
 
55
- describe('canonical anchor descriptions', () => {
56
- test('describes standalone Functions with exact input, output mode, and auth', () => {
57
- expect(describeAnchor('function.inspect', ir, undefined)).toContain(
58
- 'inspect(cursor:string?)→stream<int> [static,authenticated]',
55
+ describe('anchor descriptions', () => {
56
+ test('describes local and exact imported Class properties', () => {
57
+ expect(describeAnchor('class.Item.property.count', ir, undefined)).toContain(
58
+ '**Item.count** : int',
59
+ )
60
+ expect(describeAnchor(`class.${importedKey}.property.label`, ir, undefined)).toContain(
61
+ '**Named.label** : string?',
59
62
  )
60
63
  })
61
64
 
62
- test('resolves an imported interface member by its qualified identity', () => {
63
- expect(
64
- describeAnchor(
65
- 'interface.directory.example.dev:interface.Named.property.label',
66
- ir,
67
- undefined,
68
- ),
69
- ).toBe('**Named.label** : string?')
70
- expect(
71
- describeAnchor('interface.directory.example.dev:interface.Named', ir, undefined),
72
- ).toContain('from directory.example.dev')
65
+ test('describes standalone Functions with output mode and authentication', () => {
66
+ expect(describeAnchor('function.inspect', ir, undefined)).toContain(
67
+ 'stream<int> [authenticated]',
68
+ )
73
69
  })
74
70
  })
@@ -5,9 +5,9 @@
5
5
  */
6
6
  import type {
7
7
  Comment,
8
- IrDefinitionKey,
8
+ IrClass,
9
+ IrClassKey,
9
10
  IrFunction,
10
- IrInterface,
11
11
  IrMethod,
12
12
  JsonSchema,
13
13
  SchemaIR,
@@ -33,32 +33,30 @@ function propType(s: JsonSchema | undefined, optionalOverride?: boolean): string
33
33
  }
34
34
 
35
35
  function methodSig(name: string, m: IrMethod | IrFunction): string {
36
- const params = Object.entries(m.params ?? {})
37
- .map(
38
- ([p, s]) =>
39
- `${p}:${propType(s, m.requiredParams ? !m.requiredParams.includes(p) : undefined)}`,
40
- )
36
+ const required = new Set(m.input.required ?? [])
37
+ const params = Object.entries(m.input.properties ?? {})
38
+ .map(([p, s]) => `${p}:${propType(s, !required.has(p))}`)
41
39
  .join(', ')
42
40
  const output =
43
- m.output?.mode === 'stream'
41
+ m.output.mode === 'stream'
44
42
  ? `stream<${propType(m.output.item)}>`
45
- : m.output?.mode === 'binary'
43
+ : m.output.mode === 'binary'
46
44
  ? 'binary'
47
- : propType(m.output?.mode === 'value' ? m.output.schema : m.returns)
45
+ : propType(m.output.schema)
48
46
  const tags = [
49
- m.static ? 'static' : '',
50
- m.inheritance === 'abstract' ? 'abstract' : '',
47
+ 'static' in m && m.static ? 'static' : '',
48
+ 'inheritance' in m && m.inheritance === 'abstract' ? 'abstract' : '',
51
49
  m.auth ?? '',
52
50
  ].filter(Boolean)
53
51
  return `${name}(${params})→${output}${tags.length ? ` [${tags.join(',')}]` : ''}`
54
52
  }
55
53
 
56
- function interfaceByToken(ir: SchemaIR, token: string): IrInterface | undefined {
57
- const exact = /^(.+):interface\.([A-Za-z_$][\w$]*)$/.exec(token)
58
- if (!exact) return ir.interfaces?.[token]
54
+ function classByToken(ir: SchemaIR, token: string): IrClass | undefined {
55
+ const exact = /^(.+):class\.([A-Za-z_$][\w$]*)$/.exec(token)
56
+ if (!exact) return ir.classes[token]
59
57
  const [, origin, name] = exact
60
- if (origin === ir.domain) return ir.interfaces?.[name]
61
- return ir.importedInterfacesByKey?.[`${origin}:interface.${name}` as IrDefinitionKey]
58
+ if (origin === ir.domain) return ir.classes[name]
59
+ return ir.importedClassesByKey[`${origin}:class.${name}` as IrClassKey]
62
60
  }
63
61
 
64
62
  /**
@@ -77,20 +75,20 @@ export function describeAnchor(
77
75
  const doc = span?.doc ? ` — ${span.doc.replace(/\s+/g, ' ').trim().slice(0, 160)}` : ''
78
76
 
79
77
  // class.X.property.y / class.X.method.m
80
- const member = ref.match(/^class\.([^.]+)\.(property|method)\.(.+)$/)
78
+ const member = ref.match(/^class\.(.+)\.(property|method)\.([^.]+)$/)
81
79
  if (member && ir) {
82
80
  const [, cls, kind, name] = member
83
- const c = ir.classes?.[cls]
81
+ const c = classByToken(ir, cls)
84
82
  if (c && kind === 'property' && c.properties?.[name])
85
- return `**${cls}.${name}** : ${propType(c.properties[name], c.required ? !c.required.includes(name) : undefined)}${loc ? ` (${loc})` : ''}${doc}`
83
+ return `**${c.name}.${name}** : ${propType(c.properties[name], c.required ? !c.required.includes(name) : undefined)}${loc ? ` (${loc})` : ''}${doc}`
86
84
  if (c && kind === 'method' && c.methods?.[name])
87
- return `**${cls}.${name}** — ${methodSig(name, c.methods[name])}${loc ? ` (${loc})` : ''}${doc}`
85
+ return `**${c.name}.${name}** — ${methodSig(name, c.methods[name])}${loc ? ` (${loc})` : ''}${doc}`
88
86
  }
89
87
 
90
88
  // class.X / edge.X (edges live in ir.classes too)
91
89
  const cm = ref.match(/^(?:class|edge)\.(.+)$/)
92
90
  if (cm && ir) {
93
- const c = ir.classes?.[cm[1]]
91
+ const c = classByToken(ir, cm[1])
94
92
  if (c) {
95
93
  const L = [`**${c.name}** (${c.type})${loc ? ` (${loc})` : ''}${doc}`]
96
94
  const props = Object.entries(c.properties ?? {})
@@ -121,33 +119,6 @@ export function describeAnchor(
121
119
  }
122
120
  }
123
121
 
124
- // interface.X.property.y / interface.<origin>:interface.X.method.m
125
- const interfaceMember = ref.match(/^interface\.(.+)\.(property|method)\.([^.]+)$/)
126
- if (interfaceMember && ir) {
127
- const [, token, kind, name] = interfaceMember
128
- const iface = interfaceByToken(ir, token)
129
- if (iface && kind === 'property' && iface.properties?.[name]) {
130
- return `**${iface.name}.${name}** : ${propType(iface.properties[name], iface.required ? !iface.required.includes(name) : undefined)}${loc ? ` (${loc})` : ''}${doc}`
131
- }
132
- if (iface && kind === 'method' && iface.methods?.[name]) {
133
- return `**${iface.name}.${name}** — ${methodSig(name, iface.methods[name])}${loc ? ` (${loc})` : ''}${doc}`
134
- }
135
- }
136
-
137
- // interface.X or exact interface.<origin>:interface.X
138
- const im = ref.match(/^interface\.(.+)$/)
139
- if (im && ir) {
140
- const i = interfaceByToken(ir, im[1])
141
- if (i) {
142
- const props = Object.entries(i.properties ?? {}).map(
143
- ([name, schema]) =>
144
- `${name}:${propType(schema, i.required ? !i.required.includes(name) : undefined)}`,
145
- )
146
- const ms = Object.entries(i.methods ?? {}).map(([n, m]) => methodSig(n, m))
147
- return `**${i.name}** (interface)${i.origin && i.origin !== ir.domain ? ` from ${i.origin}` : ''}${loc ? ` (${loc})` : ''}${doc}${props.length ? `\n props: ${props.join(' · ')}` : ''}${ms.length ? `\n methods: ${ms.join(' · ')}` : ''}`
148
- }
149
- }
150
-
151
122
  // Standalone canonical Function.
152
123
  const fm = ref.match(/^function\.([A-Za-z_$][\w$]*)$/)
153
124
  if (fm && ir) {