@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
@@ -3,7 +3,17 @@ import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
3
3
  import { tmpdir } from 'node:os'
4
4
  import { join } from 'node:path'
5
5
 
6
- import { hashAnatomyFiles } from './baseline'
6
+ import type { SchemaIR } from '../../shared/types'
7
+
8
+ import { STUDIO_SCHEMA_PROJECTION_VERSION } from '../../shared/types'
9
+ import {
10
+ BASELINE_FORMAT_VERSION,
11
+ captureBaseline,
12
+ computeChanges,
13
+ hashAnatomyFiles,
14
+ loadBaseline,
15
+ } from './baseline'
16
+ import { writeJson, writeState } from './store'
7
17
 
8
18
  const roots: string[] = []
9
19
 
@@ -11,13 +21,13 @@ afterEach(() => {
11
21
  while (roots.length) rmSync(roots.pop()!, { recursive: true, force: true })
12
22
  })
13
23
 
14
- test('hashes current implementation and semantic layer files alongside legacy anatomy', () => {
24
+ test('hashes current Application and vertical authoring files', () => {
15
25
  const root = mkdtempSync(join(tmpdir(), 'studio-current-baseline-'))
16
26
  roots.push(root)
17
27
  for (const dir of [
18
28
  'schema',
19
29
  'actions/risk',
20
- 'handlers/risk',
30
+ 'providers/mail',
21
31
  'queries/risk',
22
32
  'ui/risk',
23
33
  'views/risk',
@@ -26,9 +36,10 @@ test('hashes current implementation and semantic layer files alongside legacy an
26
36
  }
27
37
  for (const file of [
28
38
  'schema/index.ts',
29
- 'implementation.ts',
39
+ 'application.ts',
40
+ 'runtime.ts',
30
41
  'actions/risk/index.ts',
31
- 'handlers/risk/index.ts',
42
+ 'providers/mail/index.ts',
32
43
  'queries/risk/index.ts',
33
44
  'ui/risk/screen.tsx',
34
45
  'views/risk/index.ts',
@@ -40,12 +51,217 @@ test('hashes current implementation and semantic layer files alongside legacy an
40
51
 
41
52
  expect(Object.keys(hashAnatomyFiles(root, 'schema')).sort()).toEqual([
42
53
  'actions/risk/index.ts',
43
- 'handlers/risk/index.ts',
44
- 'implementation.ts',
54
+ 'application.ts',
45
55
  'package.json',
56
+ 'providers/mail/index.ts',
46
57
  'queries/risk/index.ts',
58
+ 'runtime.ts',
47
59
  'schema/index.ts',
48
60
  'ui/risk/screen.tsx',
49
61
  'views/risk/index.ts',
50
62
  ])
51
63
  })
64
+
65
+ const schema = (domain: string): SchemaIR => ({
66
+ format: 'astrale.dsl',
67
+ version: 'v1',
68
+ domain,
69
+ classes: {},
70
+ importsByKey: {},
71
+ importedClassesByKey: {},
72
+ functions: {},
73
+ views: {},
74
+ policies: {},
75
+ dependencies: [],
76
+ core: {},
77
+ })
78
+
79
+ const canonicalRoot = {
80
+ format: 'astrale.dsl',
81
+ version: 'v1',
82
+ origin: 'notes.example.dev',
83
+ } as const
84
+
85
+ test('persists a versioned canonical baseline with revision and raw root', () => {
86
+ const root = mkdtempSync(join(tmpdir(), 'studio-versioned-baseline-'))
87
+ roots.push(root)
88
+ const revision = `sha256:${'b'.repeat(64)}` as const
89
+
90
+ captureBaseline(
91
+ root,
92
+ { ir: schema('notes.example.dev'), root: canonicalRoot, revision },
93
+ { source: 'hash' },
94
+ )
95
+
96
+ expect(loadBaseline(root)).toMatchObject({
97
+ formatVersion: BASELINE_FORMAT_VERSION,
98
+ revision,
99
+ root: canonicalRoot,
100
+ files: { source: 'hash' },
101
+ })
102
+ })
103
+
104
+ test('invalidates an unversioned baseline instead of diffing it through a new projection', () => {
105
+ const root = mkdtempSync(join(tmpdir(), 'studio-legacy-baseline-'))
106
+ roots.push(root)
107
+ writeJson(root, '.cache/baseline/meta.json', { capturedAt: '2026-01-01T00:00:00.000Z' })
108
+ writeJson(root, '.cache/baseline/ir.json', schema('notes.example.dev'))
109
+ writeJson(root, '.cache/baseline/files.json', {})
110
+
111
+ expect(loadBaseline(root)).toBeNull()
112
+ })
113
+
114
+ const corruptBaselineCases = [
115
+ {
116
+ name: 'missing meta',
117
+ mutate: (root: string) => rmSync(join(root, '.domain-studio/.cache/baseline/meta.json')),
118
+ },
119
+ {
120
+ name: 'syntactically invalid meta',
121
+ mutate: (root: string) => writeState(root, '.cache/baseline/meta.json', '{'),
122
+ },
123
+ {
124
+ name: 'structurally invalid meta',
125
+ mutate: (root: string) =>
126
+ writeJson(root, '.cache/baseline/meta.json', {
127
+ formatVersion: BASELINE_FORMAT_VERSION,
128
+ projectionVersion: STUDIO_SCHEMA_PROJECTION_VERSION,
129
+ }),
130
+ },
131
+ {
132
+ name: 'future baseline format',
133
+ mutate: (root: string) =>
134
+ writeJson(root, '.cache/baseline/meta.json', {
135
+ formatVersion: BASELINE_FORMAT_VERSION + 1,
136
+ projectionVersion: STUDIO_SCHEMA_PROJECTION_VERSION,
137
+ revision: `sha256:${'d'.repeat(64)}`,
138
+ }),
139
+ },
140
+ {
141
+ name: 'future projection format',
142
+ mutate: (root: string) =>
143
+ writeJson(root, '.cache/baseline/meta.json', {
144
+ formatVersion: BASELINE_FORMAT_VERSION,
145
+ projectionVersion: STUDIO_SCHEMA_PROJECTION_VERSION + 1,
146
+ revision: `sha256:${'d'.repeat(64)}`,
147
+ }),
148
+ },
149
+ {
150
+ name: 'missing IR',
151
+ mutate: (root: string) => rmSync(join(root, '.domain-studio/.cache/baseline/ir.json')),
152
+ },
153
+ {
154
+ name: 'syntactically invalid IR',
155
+ mutate: (root: string) => writeState(root, '.cache/baseline/ir.json', '{'),
156
+ },
157
+ {
158
+ name: 'structurally invalid IR',
159
+ mutate: (root: string) =>
160
+ writeJson(root, '.cache/baseline/ir.json', {
161
+ ...schema('notes.example.dev'),
162
+ classes: { Broken: null },
163
+ }),
164
+ },
165
+ {
166
+ name: 'missing canonical root',
167
+ mutate: (root: string) => rmSync(join(root, '.domain-studio/.cache/baseline/schema-root.json')),
168
+ },
169
+ {
170
+ name: 'syntactically invalid canonical root',
171
+ mutate: (root: string) => writeState(root, '.cache/baseline/schema-root.json', '{'),
172
+ },
173
+ {
174
+ name: 'structurally invalid canonical root',
175
+ mutate: (root: string) =>
176
+ writeJson(root, '.cache/baseline/schema-root.json', { canonical: true }),
177
+ },
178
+ {
179
+ name: 'missing file hashes',
180
+ mutate: (root: string) => rmSync(join(root, '.domain-studio/.cache/baseline/files.json')),
181
+ },
182
+ {
183
+ name: 'syntactically invalid file hashes',
184
+ mutate: (root: string) => writeState(root, '.cache/baseline/files.json', '{'),
185
+ },
186
+ {
187
+ name: 'structurally invalid file hashes',
188
+ mutate: (root: string) => writeJson(root, '.cache/baseline/files.json', { source: 42 }),
189
+ },
190
+ ] as const
191
+
192
+ for (const scenario of corruptBaselineCases) {
193
+ test(`invalidates a baseline with ${scenario.name}`, () => {
194
+ const root = mkdtempSync(join(tmpdir(), 'studio-corrupt-baseline-'))
195
+ roots.push(root)
196
+ const revision = `sha256:${'d'.repeat(64)}` as const
197
+ captureBaseline(root, { ir: schema('notes.example.dev'), root: canonicalRoot, revision }, {})
198
+
199
+ scenario.mutate(root)
200
+
201
+ expect(loadBaseline(root)).toBeNull()
202
+ })
203
+ }
204
+
205
+ test('keeps a valid current-format legacy baseline without a canonical root or revision', () => {
206
+ const root = mkdtempSync(join(tmpdir(), 'studio-legacy-v2-baseline-'))
207
+ roots.push(root)
208
+ const legacyIr = { ...schema('notes.example.dev'), version: 'legacy' }
209
+
210
+ captureBaseline(root, { ir: legacyIr, root: null, revision: null }, { source: 'hash' })
211
+
212
+ expect(loadBaseline(root)).toMatchObject({
213
+ formatVersion: BASELINE_FORMAT_VERSION,
214
+ ir: legacyIr,
215
+ root: null,
216
+ revision: null,
217
+ files: { source: 'hash' },
218
+ })
219
+ })
220
+
221
+ test('uses equal admitted revisions to suppress projection-only schema churn', () => {
222
+ const root = mkdtempSync(join(tmpdir(), 'studio-revision-baseline-'))
223
+ roots.push(root)
224
+ const revision = `sha256:${'c'.repeat(64)}` as const
225
+ captureBaseline(
226
+ root,
227
+ {
228
+ ir: schema('old-projection.example.dev'),
229
+ root: { ...canonicalRoot, origin: 'old-projection.example.dev' },
230
+ revision,
231
+ },
232
+ {},
233
+ )
234
+
235
+ const changes = computeChanges(
236
+ root,
237
+ schema('new-projection.example.dev'),
238
+ {},
239
+ {
240
+ currentRevision: revision,
241
+ git: { hasGit: false },
242
+ },
243
+ )
244
+ expect(changes.schemaChanges).toEqual([])
245
+ expect(changes.structuralStatus).toBe('none')
246
+ expect(changes.baselineRevision).toBe(revision)
247
+ expect(changes.currentRevision).toBe(revision)
248
+ })
249
+
250
+ test('uses caller-provided Git enrichment without importing workspace effects', () => {
251
+ const root = mkdtempSync(join(tmpdir(), 'studio-enriched-baseline-'))
252
+ roots.push(root)
253
+ captureBaseline(root, { ir: schema('notes.example.dev'), root: null, revision: null }, {})
254
+
255
+ const changes = computeChanges(
256
+ root,
257
+ schema('notes.example.dev'),
258
+ {},
259
+ {
260
+ git: { hasGit: true, diffText: 'diff --git a/schema/index.ts b/schema/index.ts' },
261
+ },
262
+ )
263
+
264
+ expect(changes.source).toBe('git')
265
+ expect(changes.hasGit).toBe(true)
266
+ expect(changes.schemaDiffText).toBe('diff --git a/schema/index.ts b/schema/index.ts')
267
+ })
@@ -1,26 +1,38 @@
1
1
  /**
2
2
  * baseline.ts — the PRIMARY change tracker. On first launch we capture a
3
- * snapshot (the schema IR + a content hash of the "anatomy + schema fileset")
3
+ * versioned snapshot (canonical revision/root, render IR, and a content hash of
4
+ * the "anatomy + schema fileset")
4
5
  * under `.domain-studio/.cache/baseline/`. Subsequent runs diff the live state
5
6
  * against that baseline to compute a ChangeSet. Git (git.ts) only enriches when
6
7
  * a real repo is present; the fixtures are not repos, so baseline stands alone.
7
8
  *
8
9
  * Layout under `.cache/baseline/`:
9
- * ir.json — the captured SchemaIR (or null)
10
- * files.json — { [relpathFromRoot]: sha256 }
11
- * meta.json — { capturedAt }
10
+ * ir.json — the captured render SchemaIR (or null)
11
+ * schema-root.json — the admitted portable V1 root (or null)
12
+ * files.json — { [relpathFromRoot]: sha256 }
13
+ * meta.json — format/projection versions, revision, capturedAt
12
14
  */
13
15
  import { createHash } from 'node:crypto'
14
16
  import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs'
15
17
  import { join, relative, resolve } from 'node:path'
16
18
 
17
- import type { ChangeSet, FileChange, SchemaChange, SchemaIR } from '../../shared/types'
19
+ import type {
20
+ ChangeSet,
21
+ FileChange,
22
+ SchemaChange,
23
+ SchemaIR,
24
+ SchemaRevision,
25
+ } from '../../shared/types'
18
26
 
19
- import { classify, diffSchemas } from '../introspect/diff'
20
- import { detectGit, gitDiff } from './git'
21
- import { readJson, writeJson } from './store'
27
+ import { isSchemaRevision, STUDIO_SCHEMA_PROJECTION_VERSION } from '../../shared/types'
28
+ import { isCanonicalDomainSchemaV1 } from '../introspect/canonical-schema'
29
+ import { diffSchemas, structuralStatusOf } from '../introspect/diff'
30
+ import { decodeSchemaIR } from '../introspect/schema-ir-json'
31
+ import { asFiniteNumber, asJsonRecord, asStringRecord, parseJson } from '../json'
32
+ import { readState, writeJson } from './store'
22
33
 
23
34
  const BASE = '.cache/baseline'
35
+ export const BASELINE_FORMAT_VERSION = 2
24
36
 
25
37
  /** Directories to never descend into when hashing the fileset. */
26
38
  const SKIP_DIRS = new Set(['node_modules', '.dist', 'dist', '.domain-studio', '.git'])
@@ -33,12 +45,10 @@ const SKIP_DIRS = new Set(['node_modules', '.dist', 'dist', '.domain-studio', '.
33
45
  export const ANATOMY_GLOBS = {
34
46
  dirs: [
35
47
  'actions',
36
- 'capabilities',
37
48
  'core',
49
+ 'providers',
38
50
  'runtime',
39
51
  'views',
40
- 'functions',
41
- 'handlers',
42
52
  'integrations',
43
53
  'migrations',
44
54
  'mutations',
@@ -52,8 +62,8 @@ export const ANATOMY_GLOBS = {
52
62
  'client/src',
53
63
  ],
54
64
  files: [
55
- 'implementation.ts',
56
- 'domain.ts',
65
+ 'application.ts',
66
+ 'runtime.ts',
57
67
  'index.ts',
58
68
  'core.ts',
59
69
  'deps.ts',
@@ -140,30 +150,128 @@ export function hashAnatomyFiles(root: string, schemaDirName: string): Record<st
140
150
  }
141
151
 
142
152
  export interface Baseline {
153
+ formatVersion: typeof BASELINE_FORMAT_VERSION
154
+ projectionVersion: number
143
155
  ir: SchemaIR | null
156
+ /** Admitted canonical root only; null for legacy/preview snapshots. */
157
+ root: unknown | null
158
+ revision: SchemaRevision | null
144
159
  files: Record<string, string>
145
160
  capturedAt?: string
146
161
  }
147
162
 
163
+ export interface BaselineSchemaSnapshot {
164
+ ir: SchemaIR | null
165
+ /** Admitted canonical root only; preview roots must be passed as null. */
166
+ root: unknown | null
167
+ revision: SchemaRevision | null
168
+ }
169
+
170
+ interface BaselineMeta {
171
+ formatVersion: number
172
+ projectionVersion: number
173
+ revision: SchemaRevision | null
174
+ capturedAt?: string
175
+ }
176
+
177
+ function decodeBaselineMeta(value: unknown): BaselineMeta | undefined {
178
+ const record = asJsonRecord(value)
179
+ if (!record) return undefined
180
+ const formatVersion = asFiniteNumber(record.formatVersion)
181
+ const projectionVersion = asFiniteNumber(record.projectionVersion)
182
+ const revision =
183
+ record.revision === null
184
+ ? null
185
+ : isSchemaRevision(record.revision)
186
+ ? record.revision
187
+ : undefined
188
+ const capturedAt = record.capturedAt
189
+ if (
190
+ formatVersion === undefined ||
191
+ projectionVersion === undefined ||
192
+ revision === undefined ||
193
+ (capturedAt !== undefined && typeof capturedAt !== 'string')
194
+ ) {
195
+ return undefined
196
+ }
197
+ return {
198
+ formatVersion,
199
+ projectionVersion,
200
+ revision,
201
+ ...(capturedAt === undefined ? {} : { capturedAt }),
202
+ }
203
+ }
204
+
205
+ function decodeNullableSchemaIR(value: unknown): SchemaIR | null | undefined {
206
+ return value === null ? null : decodeSchemaIR(value)
207
+ }
208
+
209
+ function decodeNullableSchemaRoot(value: unknown): unknown | null | undefined {
210
+ return value === null ? null : isCanonicalDomainSchemaV1(value) ? value : undefined
211
+ }
212
+
213
+ function readBaselineJson<T>(
214
+ root: string,
215
+ subpath: string,
216
+ decode: (value: unknown) => T | undefined,
217
+ ): T | undefined {
218
+ const raw = readState(root, subpath)
219
+ if (raw === null) return undefined
220
+ const parsed = parseJson(raw)
221
+ return parsed === undefined ? undefined : decode(parsed)
222
+ }
223
+
148
224
  /** Persist a baseline snapshot under `.cache/baseline/`. All writes go through the store. */
149
225
  export function captureBaseline(
150
226
  root: string,
151
- ir: SchemaIR | null,
227
+ snapshot: BaselineSchemaSnapshot,
152
228
  fileHashes: Record<string, string>,
153
229
  ): void {
154
- writeJson(root, `${BASE}/ir.json`, ir)
230
+ writeJson(root, `${BASE}/ir.json`, snapshot.ir)
231
+ writeJson(root, `${BASE}/schema-root.json`, snapshot.root)
155
232
  writeJson(root, `${BASE}/files.json`, fileHashes)
156
- writeJson(root, `${BASE}/meta.json`, { capturedAt: new Date().toISOString() })
233
+ writeJson(root, `${BASE}/meta.json`, {
234
+ formatVersion: BASELINE_FORMAT_VERSION,
235
+ projectionVersion: STUDIO_SCHEMA_PROJECTION_VERSION,
236
+ revision: snapshot.revision,
237
+ capturedAt: new Date().toISOString(),
238
+ } satisfies BaselineMeta)
157
239
  }
158
240
 
159
- /** Load a previously-captured baseline, or null if none exists. Never throws. */
241
+ /**
242
+ * Load a current baseline. Older/unrecognised formats and render-projection
243
+ * versions are explicitly invalidated so adapter changes cannot masquerade as
244
+ * authored schema changes. The lifecycle will seed a replacement snapshot.
245
+ */
160
246
  export function loadBaseline(root: string): Baseline | null {
161
- const irPath = join(resolve(root), '.domain-studio', BASE, 'meta.json')
162
- if (!existsSync(irPath)) return null
163
- const ir = readJson<SchemaIR | null>(root, `${BASE}/ir.json`, null)
164
- const files = readJson<Record<string, string>>(root, `${BASE}/files.json`, {})
165
- const meta = readJson<{ capturedAt?: string }>(root, `${BASE}/meta.json`, {})
166
- return { ir, files, capturedAt: meta.capturedAt }
247
+ const meta = readBaselineJson(root, `${BASE}/meta.json`, decodeBaselineMeta)
248
+ if (
249
+ !meta ||
250
+ meta.formatVersion !== BASELINE_FORMAT_VERSION ||
251
+ meta.projectionVersion !== STUDIO_SCHEMA_PROJECTION_VERSION ||
252
+ !isSchemaRevisionOrNull(meta.revision)
253
+ ) {
254
+ return null
255
+ }
256
+ const ir = readBaselineJson(root, `${BASE}/ir.json`, decodeNullableSchemaIR)
257
+ const schemaRoot = readBaselineJson(root, `${BASE}/schema-root.json`, decodeNullableSchemaRoot)
258
+ const files = readBaselineJson(root, `${BASE}/files.json`, asStringRecord)
259
+ if (ir === undefined || schemaRoot === undefined || files === undefined) return null
260
+ if (meta.revision !== null && (schemaRoot === null || ir === null)) return null
261
+ if (meta.revision === null && schemaRoot !== null) return null
262
+ return {
263
+ formatVersion: BASELINE_FORMAT_VERSION,
264
+ projectionVersion: STUDIO_SCHEMA_PROJECTION_VERSION,
265
+ ir,
266
+ root: schemaRoot,
267
+ revision: meta.revision ?? null,
268
+ files,
269
+ capturedAt: meta.capturedAt,
270
+ }
271
+ }
272
+
273
+ function isSchemaRevisionOrNull(value: unknown): value is SchemaRevision | null {
274
+ return value === null || isSchemaRevision(value)
167
275
  }
168
276
 
169
277
  /** Compare two file-hash maps into added/modified/removed FileChange[]. */
@@ -185,9 +293,8 @@ function summarizeSchemaChanges(changes: SchemaChange[]): string {
185
293
  if (changes.length === 0) return 'No schema changes.'
186
294
  return changes
187
295
  .map((c) => {
188
- const flag = c.breaking ? 'breaking' : 'additive'
189
296
  const detail = c.detail ? `: ${c.detail}` : ''
190
- return `${c.kind} ${c.target}${detail} (${flag})`
297
+ return `${c.kind} ${c.target}${detail}`
191
298
  })
192
299
  .join('\n')
193
300
  }
@@ -202,9 +309,12 @@ export function computeChanges(
202
309
  root: string,
203
310
  currentIr: SchemaIR | null,
204
311
  currentFiles: Record<string, string>,
205
- opts: { schemaDirName: string },
312
+ opts: {
313
+ currentRevision?: SchemaRevision | null
314
+ git: { hasGit: boolean; diffText?: string }
315
+ },
206
316
  ): ChangeSet {
207
- const { hasGit } = detectGit(root)
317
+ const { hasGit } = opts.git
208
318
  const source: ChangeSet['source'] = hasGit ? 'git' : 'baseline'
209
319
  const baseline = loadBaseline(root)
210
320
 
@@ -215,17 +325,19 @@ export function computeChanges(
215
325
  hasBaseline: false,
216
326
  schemaChanges: [],
217
327
  fileChanges: [],
218
- classification: 'none',
328
+ structuralStatus: 'none',
329
+ ...(opts.currentRevision ? { currentRevision: opts.currentRevision } : {}),
219
330
  }
220
331
  }
221
332
 
222
- const schemaChanges = diffSchemas(baseline.ir ?? null, currentIr)
223
- const classification = classify(schemaChanges)
333
+ const schemaChanges =
334
+ baseline.revision && opts.currentRevision === baseline.revision
335
+ ? []
336
+ : diffSchemas(baseline.ir ?? null, currentIr)
337
+ const structuralStatus = structuralStatusOf(schemaChanges)
224
338
  const fileChanges = diffFiles(baseline.files ?? {}, currentFiles)
225
339
 
226
- const schemaDiffText = hasGit
227
- ? (gitDiff(root, opts.schemaDirName) ?? undefined)
228
- : summarizeSchemaChanges(schemaChanges)
340
+ const schemaDiffText = hasGit ? opts.git.diffText : summarizeSchemaChanges(schemaChanges)
229
341
 
230
342
  return {
231
343
  source,
@@ -234,7 +346,9 @@ export function computeChanges(
234
346
  schemaChanges,
235
347
  fileChanges,
236
348
  schemaDiffText,
237
- classification,
349
+ structuralStatus,
350
+ ...(baseline.revision ? { baselineRevision: baseline.revision } : {}),
351
+ ...(opts.currentRevision ? { currentRevision: opts.currentRevision } : {}),
238
352
  baselineCapturedAt: baseline.capturedAt,
239
353
  }
240
354
  }
@@ -3,7 +3,8 @@ import { mkdtempSync, rmSync } from 'node:fs'
3
3
  import { tmpdir } from 'node:os'
4
4
  import { join } from 'node:path'
5
5
 
6
- import { readComments, upsertComment } from './comments'
6
+ import { mergeReply, readComments, upsertComment } from './comments'
7
+ import { writeJson } from './store'
7
8
 
8
9
  const roots: string[] = []
9
10
 
@@ -27,3 +28,67 @@ test('missing comment stores do not share entries across domains', () => {
27
28
  ])
28
29
  expect(readComments(rootB).comments).toEqual([])
29
30
  })
31
+
32
+ test('normalizes malformed nested comment data and ignores future fields', () => {
33
+ const root = mkdtempSync(join(tmpdir(), 'studio-comments-corrupt-'))
34
+ roots.push(root)
35
+ writeJson(root, 'comments.json', {
36
+ schemaVersion: 'render-fingerprint',
37
+ futureStoreField: { version: 2 },
38
+ comments: [
39
+ {
40
+ id: 'kept',
41
+ anchors: ['Thing'],
42
+ anchorRefs: [
43
+ { ref: 'class.Thing', kind: 'schema', futureAnchorField: true },
44
+ { ref: 42, kind: 'schema' },
45
+ ],
46
+ status: 'open',
47
+ thread: [
48
+ {
49
+ id: 'entry',
50
+ role: 'author',
51
+ type: 'text',
52
+ text: 'A valid question',
53
+ futureEntryField: true,
54
+ },
55
+ { id: 'broken', role: 'nobody', type: 'text', text: 'ignored' },
56
+ ],
57
+ createdAt: '2026-08-20T00:00:00.000Z',
58
+ futureCommentField: true,
59
+ },
60
+ { id: 42, thread: [] },
61
+ ],
62
+ })
63
+
64
+ expect(readComments(root)).toEqual({
65
+ schemaVersion: 'render-fingerprint',
66
+ comments: [
67
+ {
68
+ id: 'kept',
69
+ anchors: ['Thing'],
70
+ anchorRefs: [{ ref: 'class.Thing', kind: 'schema' }],
71
+ status: 'open',
72
+ thread: [
73
+ {
74
+ id: 'entry',
75
+ role: 'author',
76
+ type: 'text',
77
+ text: 'A valid question',
78
+ },
79
+ ],
80
+ createdAt: '2026-08-20T00:00:00.000Z',
81
+ kind: 'question',
82
+ },
83
+ ],
84
+ })
85
+ })
86
+
87
+ test('rejects a syntactically valid machine-state block with the wrong root shape', () => {
88
+ const root = mkdtempSync(join(tmpdir(), 'studio-comments-paste-'))
89
+ roots.push(root)
90
+
91
+ expect(() => mergeReply(root, 'render-fingerprint', '```json\n[]\n```')).toThrow(
92
+ 'invalid machine-state json block',
93
+ )
94
+ })