@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.
- package/README.md +7 -11
- package/dist/astrale.js +3769 -3549
- package/dist/public/connect-core.js +396 -392
- package/dist/public/keys/index.js +371 -378
- package/dist/public/paths/index.js +371 -378
- package/dist/types/lib/idp.d.ts +6 -0
- package/dist/types/lib/meta.d.ts +2 -2
- package/package.json +6 -4
- package/src/admin/binding.ts +168 -0
- package/src/admin/catalog/__tests__/client.test.ts +2 -2
- package/src/admin/catalog/client.ts +4 -9
- package/src/admin/instance/__tests__/client.test.ts +5 -3
- package/src/admin/instance/client.ts +9 -27
- package/src/commands/__tests__/auth-login.test.ts +54 -1
- package/src/commands/__tests__/call.test.ts +5 -1
- package/src/commands/__tests__/domain-install-operation.test.ts +23 -7
- package/src/commands/__tests__/domain-install-owned.test.ts +5 -1
- package/src/commands/__tests__/domain-list.test.ts +83 -4
- package/src/commands/__tests__/install-identity-override.test.ts +30 -19
- package/src/commands/__tests__/studio-descriptor.test.ts +22 -0
- package/src/commands/__tests__/update.test.ts +73 -0
- package/src/commands/domain/install.ts +11 -15
- package/src/commands/domain/list.ts +17 -12
- package/src/commands/studio.ts +20 -0
- package/src/commands/update.ts +39 -13
- package/src/connection/__tests__/errors.test.ts +16 -12
- package/src/connection/__tests__/exchange.test.ts +6 -5
- package/src/connection/exchange.ts +5 -4
- package/src/lib/__tests__/domain-publication.test.ts +144 -0
- package/src/lib/__tests__/idp-session.test.ts +22 -0
- package/src/lib/__tests__/idp.test.ts +2 -0
- package/src/lib/__tests__/instance.test.ts +6 -0
- package/src/lib/__tests__/studio-server-deps.test.ts +53 -16
- package/src/lib/__tests__/update.test.ts +12 -0
- package/src/lib/domain-publication.ts +102 -0
- package/src/lib/idp.ts +5 -1
- package/src/lib/instance.ts +6 -1
- package/src/lib/login-flow.ts +13 -3
- package/src/lib/meta.ts +2 -2
- package/src/lib/update.ts +4 -4
- package/src/program/__tests__/program.test.ts +1 -1
- package/studio/client/dist/assets/{elk-api-D2xgMJvi.js → elk-api-lwhFo7vB.js} +1 -1
- package/studio/client/dist/assets/index-B-c-smo9.js +8 -0
- package/studio/client/dist/assets/index-BckHuAWk.js +81 -0
- package/studio/client/dist/assets/index-C4aNQ6dz.css +1 -0
- package/studio/client/dist/index.html +2 -2
- package/studio/package.json +3 -1
- package/studio/server/agent/bridge/stdio.ts +8 -1
- package/studio/server/agent/conversation.test.ts +10 -5
- package/studio/server/agent/conversation.ts +56 -8
- package/studio/server/agent/harness/codex/loadout.ts +18 -21
- package/studio/server/agent/harness/gateway/config.ts +20 -18
- package/studio/server/agent/harness/selection.ts +9 -1
- package/studio/server/agent/prompts/system.ts +3 -2
- package/studio/server/agent/prompts/turn.ts +13 -4
- package/studio/server/agent/run/completion.ts +2 -2
- package/studio/server/agent/run/preparation.ts +6 -5
- package/studio/server/agent/run/transcript.test.ts +35 -1
- package/studio/server/agent/run/transcript.ts +169 -3
- package/studio/server/agent/run/usage.test.ts +24 -0
- package/studio/server/agent/run/usage.ts +26 -1
- package/studio/server/api/canvas.ts +44 -0
- package/studio/server/api/comments.ts +85 -0
- package/studio/server/api/context.ts +61 -0
- package/studio/server/api/deployment.ts +39 -0
- package/studio/server/api/documents.ts +55 -0
- package/studio/server/api/domain.ts +59 -0
- package/studio/server/api/http.ts +45 -0
- package/studio/server/api/project.ts +57 -0
- package/studio/server/api/schema.ts +12 -0
- package/studio/server/api/views.ts +57 -0
- package/studio/server/api/workspace.ts +60 -0
- package/studio/server/api.test.ts +147 -0
- package/studio/server/api.ts +20 -422
- package/studio/server/cache.test.ts +111 -0
- package/studio/server/cache.ts +163 -9
- package/studio/server/cli-consumers.test.ts +494 -0
- package/studio/server/cli.test.ts +88 -0
- package/studio/server/cli.ts +240 -0
- package/studio/server/client-package.ts +25 -1
- package/studio/server/domain.test.ts +30 -2
- package/studio/server/domain.ts +14 -2
- package/studio/server/environment/dotenv-preview.test.ts +39 -0
- package/studio/server/environment/dotenv-preview.ts +27 -0
- package/studio/server/environment/files.test.ts +53 -0
- package/studio/server/{state/env.ts → environment/files.ts} +7 -48
- package/studio/server/{state → handoff}/copy.ts +21 -6
- package/studio/server/handoff/service.test.ts +22 -0
- package/studio/server/{state/handoff.ts → handoff/service.ts} +21 -14
- package/studio/server/instances/active.ts +72 -0
- package/studio/server/instances/deploy-record.test.ts +31 -0
- package/studio/server/instances/deploy-record.ts +37 -0
- package/studio/server/instances/deploy.ts +56 -0
- package/studio/server/{state/instance.test.ts → instances/probe.test.ts} +25 -21
- package/studio/server/instances/probe.ts +67 -0
- package/studio/server/instances/status.test.ts +12 -0
- package/studio/server/instances/status.ts +49 -0
- package/studio/server/introspect/anatomy/client-tree.ts +57 -0
- package/studio/server/introspect/anatomy/env-fields.ts +66 -0
- package/studio/server/introspect/anatomy/schema-definition.ts +53 -0
- package/studio/server/introspect/anatomy/source.ts +181 -0
- package/studio/server/introspect/anatomy/views/legacy.ts +232 -0
- package/studio/server/introspect/anatomy/views/routes.ts +105 -0
- package/studio/server/introspect/anatomy/views.ts +58 -0
- package/studio/server/introspect/anatomy-extras.test.ts +91 -4
- package/studio/server/introspect/anatomy-extras.ts +7 -726
- package/studio/server/introspect/anatomy.ts +46 -19
- package/studio/server/introspect/bundle.ts +15 -6
- package/studio/server/introspect/canonical-schema.test.ts +79 -0
- package/studio/server/introspect/canonical-schema.ts +80 -18
- package/studio/server/introspect/config-preview.test.ts +32 -0
- package/studio/server/introspect/config-preview.ts +119 -0
- package/studio/server/introspect/diff.test.ts +7 -11
- package/studio/server/introspect/diff.ts +28 -37
- package/studio/server/introspect/extractor.ts +8 -6
- package/studio/server/introspect/hash.ts +5 -2
- package/studio/server/introspect/overlay-tsmorph.ts +7 -1305
- package/studio/server/introspect/revision.test.ts +54 -0
- package/studio/server/introspect/revision.ts +49 -0
- package/studio/server/introspect/runtime.test.ts +63 -5
- package/studio/server/introspect/runtime.ts +49 -3
- package/studio/server/introspect/schema-ir-json.ts +181 -0
- package/studio/server/introspect/source-overlay/annotations.ts +14 -0
- package/studio/server/introspect/source-overlay/handlers.ts +681 -0
- package/studio/server/introspect/source-overlay/kernel-calls.ts +49 -0
- package/studio/server/introspect/source-overlay/project.ts +248 -0
- package/studio/server/introspect/source-overlay/spans.ts +360 -0
- package/studio/server/json.ts +46 -0
- package/studio/server/lifecycle.ts +5 -1
- package/studio/server/sse.test.ts +26 -0
- package/studio/server/sse.ts +4 -1
- package/studio/server/state/baseline.test.ts +212 -1
- package/studio/server/state/baseline.ts +145 -29
- package/studio/server/state/comments.test.ts +66 -1
- package/studio/server/state/comments.ts +173 -10
- package/studio/server/state/context.ts +34 -2
- package/studio/server/state/documents.ts +42 -1
- package/studio/server/state/integrations.ts +27 -1
- package/studio/server/state/layout.test.ts +34 -0
- package/studio/server/state/layout.ts +44 -17
- package/studio/server/state/settings.ts +29 -22
- package/studio/server/state/store.test.ts +122 -0
- package/studio/server/state/store.ts +66 -17
- package/studio/server/state/visibility.ts +22 -7
- package/studio/server/views/model.test.ts +57 -0
- package/studio/server/views/model.ts +66 -0
- package/studio/server/views/runtime.ts +40 -0
- package/studio/server/views/selection-repository.test.ts +31 -0
- package/studio/server/views/selection-repository.ts +60 -0
- package/studio/server/views/session.test.ts +73 -0
- package/studio/server/views/session.ts +166 -0
- package/studio/server/{state/views.test.ts → views/target.test.ts} +8 -159
- package/studio/server/views/target.ts +248 -0
- package/studio/server/watch.ts +1 -1
- package/studio/server/workspace/catalog.test.ts +17 -0
- package/studio/server/workspace/catalog.ts +43 -0
- package/studio/server/{state → workspace}/create.test.ts +1 -1
- package/studio/server/{state → workspace}/create.ts +1 -1
- package/studio/server/{state → workspace}/git.ts +1 -1
- package/studio/server/workspace/updates.ts +83 -0
- package/studio/server/workspace-state.ts +1 -1
- package/studio/shared/contracts/README.md +16 -0
- package/studio/shared/contracts/agent.ts +270 -0
- package/studio/shared/contracts/runtime.ts +50 -0
- package/studio/shared/contracts/schema.ts +462 -0
- package/studio/shared/contracts/surface.test.ts +319 -0
- package/studio/shared/contracts/workspace.ts +274 -0
- package/studio/shared/layout.test.ts +116 -0
- package/studio/shared/schema/identity.test.ts +58 -0
- package/studio/shared/schema/identity.ts +82 -0
- package/studio/shared/types.ts +11 -1099
- package/studio/tsconfig.json +1 -1
- package/viewer/dist/main.js +35 -35
- package/studio/client/dist/assets/index-BMdnsIJA.css +0 -1
- package/studio/client/dist/assets/index-D-vRV8w7.js +0 -8
- package/studio/client/dist/assets/index-LGSWRrk8.js +0 -81
- package/studio/server/state/catalog.ts +0 -117
- package/studio/server/state/instance.ts +0 -280
- package/studio/server/state/updates.ts +0 -63
- package/studio/server/state/views.ts +0 -535
|
@@ -10,24 +10,32 @@
|
|
|
10
10
|
import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs'
|
|
11
11
|
import { join, relative } from 'node:path'
|
|
12
12
|
|
|
13
|
-
import type { DomainAnatomy, DomainOverview } from '../../shared/types'
|
|
13
|
+
import type { DomainAnatomy, DomainOverview, SchemaIR } from '../../shared/types'
|
|
14
14
|
|
|
15
|
+
import { asJsonRecord, asString, parseJson } from '../json'
|
|
15
16
|
import { readSettings } from '../state/settings'
|
|
16
17
|
import { buildClientTree, buildEnvFields, buildViews, findSchemaDefinition } from './anatomy-extras'
|
|
18
|
+
import { readConfigPreview } from './config-preview'
|
|
17
19
|
|
|
18
20
|
export interface AnatomyArgs {
|
|
19
21
|
root: string
|
|
20
22
|
schemaDirName: string
|
|
21
23
|
clientDir?: string
|
|
24
|
+
canonicalViews?: NonNullable<SchemaIR['views']>
|
|
22
25
|
}
|
|
23
26
|
|
|
24
|
-
export function buildAnatomy({
|
|
27
|
+
export function buildAnatomy({
|
|
28
|
+
root,
|
|
29
|
+
schemaDirName,
|
|
30
|
+
clientDir,
|
|
31
|
+
canonicalViews,
|
|
32
|
+
}: AnatomyArgs): DomainAnatomy {
|
|
25
33
|
const schema = findSchemaDefinition(root, schemaDirName)
|
|
26
34
|
const authoredClientDir =
|
|
27
35
|
clientDir ?? (existsSync(join(root, 'ui')) ? join(root, 'ui') : undefined)
|
|
28
36
|
return {
|
|
29
37
|
overview: buildOverview(root, schemaDirName, authoredClientDir, schema?.origin),
|
|
30
|
-
views: buildViews(root, schemaDirName),
|
|
38
|
+
views: buildViews(root, schemaDirName, canonicalViews),
|
|
31
39
|
client: buildClientTree(root, clientDir ?? null),
|
|
32
40
|
env: buildEnvFields(root),
|
|
33
41
|
detectedIntegrations: detectIntegrations(root),
|
|
@@ -40,7 +48,7 @@ function buildOverview(
|
|
|
40
48
|
clientDir?: string,
|
|
41
49
|
schemaOrigin?: string,
|
|
42
50
|
): DomainOverview {
|
|
43
|
-
const pkg =
|
|
51
|
+
const pkg = readPackageJsonSafe(join(root, 'package.json'))
|
|
44
52
|
const astraleDeps: Record<string, string> = {}
|
|
45
53
|
for (const [k, v] of Object.entries({
|
|
46
54
|
...(pkg?.dependencies ?? {}),
|
|
@@ -49,16 +57,7 @@ function buildOverview(
|
|
|
49
57
|
if (k.startsWith('@astrale-os/')) astraleDeps[k] = String(v)
|
|
50
58
|
}
|
|
51
59
|
|
|
52
|
-
const config =
|
|
53
|
-
let adapter: DomainOverview['adapter'] = 'unknown'
|
|
54
|
-
if (/\bastrale\s*\(/.test(config)) adapter = 'astrale'
|
|
55
|
-
else if (/\bcloudflare\s*\(/.test(config)) adapter = 'cloudflare'
|
|
56
|
-
|
|
57
|
-
const instance = config.match(/instance\s*:\s*['"]([^'"]+)['"]/)?.[1]
|
|
58
|
-
const route = config.match(/route\s*:\s*['"]([^'"]+)['"]/)?.[1]
|
|
59
|
-
const devSecrets =
|
|
60
|
-
config.match(/dev\s*:\s*\{[^}]*secrets\s*:\s*['"]([^'"]+)['"]/)?.[1] ??
|
|
61
|
-
config.match(/secrets\s*:\s*['"]([^'"]+)['"]/)?.[1]
|
|
60
|
+
const config = readConfigPreview(root)
|
|
62
61
|
|
|
63
62
|
const compositionFile = existsSync(join(root, 'implementation.ts'))
|
|
64
63
|
? 'implementation.ts'
|
|
@@ -77,9 +76,9 @@ function buildOverview(
|
|
|
77
76
|
return {
|
|
78
77
|
origin,
|
|
79
78
|
compositionFile,
|
|
80
|
-
adapter,
|
|
81
|
-
prodTarget:
|
|
82
|
-
devSecrets,
|
|
79
|
+
adapter: config.adapter,
|
|
80
|
+
prodTarget: config.prodTarget,
|
|
81
|
+
devSecrets: config.devSecrets,
|
|
83
82
|
postInstall: undefined,
|
|
84
83
|
requires: [],
|
|
85
84
|
packageName: pkg?.name,
|
|
@@ -114,9 +113,37 @@ function readTextSafe(f: string): string {
|
|
|
114
113
|
}
|
|
115
114
|
}
|
|
116
115
|
|
|
117
|
-
|
|
116
|
+
interface PackageOverview {
|
|
117
|
+
name?: string
|
|
118
|
+
version?: string
|
|
119
|
+
dependencies?: Record<string, string>
|
|
120
|
+
devDependencies?: Record<string, string>
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function decodeDependencyMap(value: unknown): Record<string, string> | undefined {
|
|
124
|
+
const record = asJsonRecord(value)
|
|
125
|
+
if (!record) return undefined
|
|
126
|
+
return Object.fromEntries(
|
|
127
|
+
Object.entries(record).filter(
|
|
128
|
+
(entry): entry is [string, string] => typeof entry[1] === 'string',
|
|
129
|
+
),
|
|
130
|
+
)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function readPackageJsonSafe(f: string): PackageOverview | null {
|
|
118
134
|
try {
|
|
119
|
-
|
|
135
|
+
const record = asJsonRecord(parseJson(readFileSync(f, 'utf8')))
|
|
136
|
+
if (!record) return null
|
|
137
|
+
const name = asString(record.name)
|
|
138
|
+
const version = asString(record.version)
|
|
139
|
+
const dependencies = decodeDependencyMap(record.dependencies)
|
|
140
|
+
const devDependencies = decodeDependencyMap(record.devDependencies)
|
|
141
|
+
return {
|
|
142
|
+
...(name === undefined ? {} : { name }),
|
|
143
|
+
...(version === undefined ? {} : { version }),
|
|
144
|
+
...(dependencies === undefined ? {} : { dependencies }),
|
|
145
|
+
...(devDependencies === undefined ? {} : { devDependencies }),
|
|
146
|
+
}
|
|
120
147
|
} catch {
|
|
121
148
|
return null
|
|
122
149
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* bundle.ts — assembles the StudioSchemaBundle: runtime IR (PRIMARY) + ts-morph
|
|
3
|
-
* overlay +
|
|
3
|
+
* overlay + render fingerprint + deps-installed precondition. Never throws; a failed
|
|
4
4
|
* runtime import becomes a render-state error (with the overlay still computed
|
|
5
5
|
* statically, so anchors/handler-links survive a mid-edit compile break).
|
|
6
6
|
*/
|
|
@@ -8,7 +8,7 @@ import type { StudioSchemaBundle } from '../../shared/types'
|
|
|
8
8
|
|
|
9
9
|
import { type DomainHandle, depsInstalled } from '../domain'
|
|
10
10
|
import { readSettings } from '../state/settings'
|
|
11
|
-
import {
|
|
11
|
+
import { renderFingerprintOf } from './hash'
|
|
12
12
|
import { buildOverlay } from './overlay'
|
|
13
13
|
import { runtimeExtract } from './runtime'
|
|
14
14
|
|
|
@@ -16,6 +16,8 @@ export async function buildBundle(handle: DomainHandle): Promise<StudioSchemaBun
|
|
|
16
16
|
const installed = depsInstalled(handle.root)
|
|
17
17
|
let ir = null
|
|
18
18
|
let schemaRoot: unknown | undefined
|
|
19
|
+
let schemaMode: StudioSchemaBundle['schemaMode'] = 'unavailable'
|
|
20
|
+
let schemaRevision: StudioSchemaBundle['schemaRevision']
|
|
19
21
|
let importedInterfaces: StudioSchemaBundle['importedInterfaces']
|
|
20
22
|
let error: StudioSchemaBundle['error'] = null
|
|
21
23
|
let extractedBy: StudioSchemaBundle['extractedBy'] = 'runtime-bun'
|
|
@@ -29,6 +31,8 @@ export async function buildBundle(handle: DomainHandle): Promise<StudioSchemaBun
|
|
|
29
31
|
if (r.ok) {
|
|
30
32
|
ir = r.ir
|
|
31
33
|
if (r.root !== null) schemaRoot = r.root
|
|
34
|
+
schemaMode = r.schemaMode
|
|
35
|
+
if (r.revision !== null) schemaRevision = r.revision
|
|
32
36
|
importedInterfaces = r.importedInterfaces
|
|
33
37
|
} else {
|
|
34
38
|
error = { message: r.error?.message ?? 'schema failed to compile' }
|
|
@@ -44,13 +48,18 @@ export async function buildBundle(handle: DomainHandle): Promise<StudioSchemaBun
|
|
|
44
48
|
|
|
45
49
|
const overlay = buildOverlay({ ir, domainRoot: handle.root, schemaDir: handle.schemaDir })
|
|
46
50
|
if (ir) handle.origin = ir.domain
|
|
51
|
+
const renderFingerprint =
|
|
52
|
+
schemaRoot !== undefined
|
|
53
|
+
? renderFingerprintOf(schemaRoot)
|
|
54
|
+
: ir
|
|
55
|
+
? renderFingerprintOf(ir)
|
|
56
|
+
: 'sha-none'
|
|
47
57
|
|
|
48
58
|
return {
|
|
49
59
|
domainId: handle.id,
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
schemaRoot !== undefined ? schemaHashOf(schemaRoot) : ir ? schemaHashOf(ir) : 'sha-none',
|
|
60
|
+
renderFingerprint,
|
|
61
|
+
schemaMode,
|
|
62
|
+
...(schemaRevision === undefined ? {} : { schemaRevision }),
|
|
54
63
|
extractedBy,
|
|
55
64
|
depsInstalled: installed,
|
|
56
65
|
ir,
|
|
@@ -344,6 +344,85 @@ describe('canonical DomainSchema V1 projection', () => {
|
|
|
344
344
|
expect(projected.importedInterfaces.Named).toBeUndefined()
|
|
345
345
|
})
|
|
346
346
|
|
|
347
|
+
test('collects an exact external Definition referenced only by a top-level Policy', () => {
|
|
348
|
+
const policyOrigin = 'authorization.example.dev'
|
|
349
|
+
const subjectRef = { origin: policyOrigin, kind: 'class', name: 'Subject' } as const
|
|
350
|
+
const objectRef = { origin: policyOrigin, kind: 'class', name: 'Object' } as const
|
|
351
|
+
const policyOnlyRef = { origin: policyOrigin, kind: 'class', name: 'may_access' } as const
|
|
352
|
+
const policyDependency = {
|
|
353
|
+
format: 'astrale.dsl',
|
|
354
|
+
version: 'v1',
|
|
355
|
+
origin: policyOrigin,
|
|
356
|
+
dependencies: [],
|
|
357
|
+
types: {},
|
|
358
|
+
interfaces: {},
|
|
359
|
+
classes: {
|
|
360
|
+
Subject: {
|
|
361
|
+
family: 'node',
|
|
362
|
+
implements: [],
|
|
363
|
+
properties: emptyProperties,
|
|
364
|
+
propertyMetadata: {},
|
|
365
|
+
methods: {},
|
|
366
|
+
policies: {},
|
|
367
|
+
},
|
|
368
|
+
Object: {
|
|
369
|
+
family: 'node',
|
|
370
|
+
implements: [],
|
|
371
|
+
properties: emptyProperties,
|
|
372
|
+
propertyMetadata: {},
|
|
373
|
+
methods: {},
|
|
374
|
+
policies: {},
|
|
375
|
+
},
|
|
376
|
+
may_access: {
|
|
377
|
+
family: 'edge',
|
|
378
|
+
implements: [],
|
|
379
|
+
properties: emptyProperties,
|
|
380
|
+
propertyMetadata: {},
|
|
381
|
+
orientation: 'directed',
|
|
382
|
+
endpoints: {
|
|
383
|
+
source: { role: 'subject', accepts: [subjectRef], outgoing: '0..*' },
|
|
384
|
+
target: { role: 'object', accepts: [objectRef], incoming: '0..*' },
|
|
385
|
+
},
|
|
386
|
+
policies: {},
|
|
387
|
+
constraints: {},
|
|
388
|
+
},
|
|
389
|
+
},
|
|
390
|
+
functions: {},
|
|
391
|
+
policies: {},
|
|
392
|
+
views: {},
|
|
393
|
+
core: { nodes: {}, edges: [] },
|
|
394
|
+
} satisfies CanonicalDomainSchemaV1
|
|
395
|
+
const policyOnlyRoot = {
|
|
396
|
+
format: 'astrale.dsl',
|
|
397
|
+
version: 'v1',
|
|
398
|
+
origin: 'policy-consumer.example.dev',
|
|
399
|
+
dependencies: [{ origin: policyDependency.origin, revision: `sha256:${'2'.repeat(64)}` }],
|
|
400
|
+
types: {},
|
|
401
|
+
interfaces: {},
|
|
402
|
+
classes: {},
|
|
403
|
+
functions: {},
|
|
404
|
+
policies: {
|
|
405
|
+
canRead: {
|
|
406
|
+
match: {
|
|
407
|
+
source: { kind: 'subject' },
|
|
408
|
+
class: policyOnlyRef,
|
|
409
|
+
target: { kind: 'object' },
|
|
410
|
+
},
|
|
411
|
+
},
|
|
412
|
+
},
|
|
413
|
+
views: {},
|
|
414
|
+
core: { nodes: {}, edges: [] },
|
|
415
|
+
} satisfies CanonicalDomainSchemaV1
|
|
416
|
+
|
|
417
|
+
const projected = projectCanonicalSchema(policyOnlyRoot, [policyDependency])
|
|
418
|
+
expect(projected.ir.importsByKey?.['authorization.example.dev:class.may_access']).toEqual({
|
|
419
|
+
origin: policyDependency.origin,
|
|
420
|
+
definition: 'class',
|
|
421
|
+
ref: policyOnlyRef,
|
|
422
|
+
key: 'authorization.example.dev:class.may_access',
|
|
423
|
+
})
|
|
424
|
+
})
|
|
425
|
+
|
|
347
426
|
test('projects canonical core paths and the owning Domain endpoint', () => {
|
|
348
427
|
expect(projectCanonicalCore(root)).toEqual({
|
|
349
428
|
domain: root.origin,
|
|
@@ -9,13 +9,11 @@ import type {
|
|
|
9
9
|
IrCallableOutput,
|
|
10
10
|
IrClass,
|
|
11
11
|
IrDefinitionKey,
|
|
12
|
-
IrDefinitionRef,
|
|
13
12
|
IrEndpoint,
|
|
14
13
|
IrFunction,
|
|
15
14
|
IrImportDescriptor,
|
|
16
15
|
IrInterface,
|
|
17
16
|
IrMethod,
|
|
18
|
-
IrSchemaKey,
|
|
19
17
|
IrSchemaRef,
|
|
20
18
|
IrView,
|
|
21
19
|
JsonSchema,
|
|
@@ -23,6 +21,9 @@ import type {
|
|
|
23
21
|
StudioCore,
|
|
24
22
|
} from '../../shared/types'
|
|
25
23
|
|
|
24
|
+
import { definitionRefKey, isIrDefinitionRef, schemaRefKey } from '../../shared/schema/identity'
|
|
25
|
+
import { isSchemaRevision } from '../../shared/types'
|
|
26
|
+
|
|
26
27
|
type AnyRecord = Record<string, unknown>
|
|
27
28
|
|
|
28
29
|
export type CanonicalDomainSchemaV1 = AnyRecord & {
|
|
@@ -36,6 +37,20 @@ export interface CanonicalSchemaProjection {
|
|
|
36
37
|
importedInterfaces: Record<string, IrInterface>
|
|
37
38
|
}
|
|
38
39
|
|
|
40
|
+
export type CanonicalSchemaAdmission =
|
|
41
|
+
| {
|
|
42
|
+
status: 'admitted'
|
|
43
|
+
root: CanonicalDomainSchemaV1
|
|
44
|
+
closure: CanonicalDomainSchemaV1[]
|
|
45
|
+
revision: `sha256:${string}`
|
|
46
|
+
}
|
|
47
|
+
| {
|
|
48
|
+
status: 'preview'
|
|
49
|
+
root: CanonicalDomainSchemaV1
|
|
50
|
+
closure: CanonicalDomainSchemaV1[]
|
|
51
|
+
revision: null
|
|
52
|
+
}
|
|
53
|
+
|
|
39
54
|
export function isCanonicalDomainSchemaV1(value: unknown): value is CanonicalDomainSchemaV1 {
|
|
40
55
|
const candidate = asRecord(value)
|
|
41
56
|
return (
|
|
@@ -103,6 +118,65 @@ export function closureFromSdk(
|
|
|
103
118
|
return []
|
|
104
119
|
}
|
|
105
120
|
|
|
121
|
+
/**
|
|
122
|
+
* Re-admit a structurally V1-looking export through the Domain's own SDK cohort.
|
|
123
|
+
* Retained Builder context supplies the exact dependency closure. If that proof
|
|
124
|
+
* is unavailable or admission fails, Studio may still render the document as a
|
|
125
|
+
* structural preview, but it must not assign a canonical revision to it.
|
|
126
|
+
*/
|
|
127
|
+
export function admitCanonicalSchemaFromSdk(
|
|
128
|
+
sdkModule: Record<string, unknown>,
|
|
129
|
+
candidate: CanonicalDomainSchemaV1,
|
|
130
|
+
): CanonicalSchemaAdmission {
|
|
131
|
+
const retainedClosure = closureFromSdk(sdkModule, candidate)
|
|
132
|
+
const schemaApi = asRecord(sdkModule.schema)
|
|
133
|
+
const accept = schemaApi?.accept
|
|
134
|
+
const revision = schemaApi?.revision
|
|
135
|
+
if (!schemaApi || typeof accept !== 'function' || typeof revision !== 'function') {
|
|
136
|
+
return { status: 'preview', root: candidate, closure: retainedClosure, revision: null }
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
try {
|
|
140
|
+
const revisions = new Map<string, string>()
|
|
141
|
+
for (const dependency of retainedClosure) {
|
|
142
|
+
const value = Reflect.apply(revision, schemaApi, [dependency])
|
|
143
|
+
if (!isSchemaRevision(value)) throw new TypeError('SDK returned an invalid schema revision')
|
|
144
|
+
revisions.set(dependency.origin, value)
|
|
145
|
+
}
|
|
146
|
+
const lookup = {
|
|
147
|
+
get(origin: string, expected: string): CanonicalDomainSchemaV1 | undefined {
|
|
148
|
+
const dependency = retainedClosure.find((entry) => entry.origin === origin)
|
|
149
|
+
return dependency && revisions.get(origin) === expected ? dependency : undefined
|
|
150
|
+
},
|
|
151
|
+
}
|
|
152
|
+
const accepted = Reflect.apply(accept, schemaApi, [candidate, lookup])
|
|
153
|
+
if (!isCanonicalDomainSchemaV1(accepted) || accepted.origin !== candidate.origin) {
|
|
154
|
+
throw new TypeError('SDK admission returned a different schema root')
|
|
155
|
+
}
|
|
156
|
+
const acceptedRevision = Reflect.apply(revision, schemaApi, [accepted])
|
|
157
|
+
if (!isSchemaRevision(acceptedRevision)) {
|
|
158
|
+
throw new TypeError('SDK returned an invalid schema revision')
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Resolve the newly accepted value once more so its retained closure, rather
|
|
162
|
+
// than Studio's structural candidate, is what feeds exact-ref projection.
|
|
163
|
+
const resolve = schemaApi.resolve
|
|
164
|
+
let closure = retainedClosure
|
|
165
|
+
if (typeof resolve === 'function') {
|
|
166
|
+
const resolved = asRecord(Reflect.apply(resolve, schemaApi, [accepted]))
|
|
167
|
+
const proof = asRecord(resolved?.$)
|
|
168
|
+
if (proof?.schema !== accepted) throw new TypeError('SDK resolution did not retain the root')
|
|
169
|
+
const admitted = admitClosure(proof.closure, accepted)
|
|
170
|
+
if (!admitted) throw new TypeError('SDK resolution returned an invalid dependency closure')
|
|
171
|
+
closure = admitted
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return { status: 'admitted', root: accepted, closure, revision: acceptedRevision }
|
|
175
|
+
} catch {
|
|
176
|
+
return { status: 'preview', root: candidate, closure: retainedClosure, revision: null }
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
106
180
|
/** Add fields required by the current shared contract to a legacy serializer IR. */
|
|
107
181
|
export function normalizeLegacySchemaIR(value: unknown): SchemaIR {
|
|
108
182
|
const ir = asRecord(value)
|
|
@@ -174,8 +248,8 @@ export function projectCanonicalSchema(
|
|
|
174
248
|
|
|
175
249
|
for (let index = 0; index < pending.length; index++) {
|
|
176
250
|
const ref = pending[index]
|
|
177
|
-
if (!
|
|
178
|
-
const key =
|
|
251
|
+
if (!isIrDefinitionRef(ref) || ref.origin === origin) continue
|
|
252
|
+
const key = definitionRefKey(ref)
|
|
179
253
|
if (visited.has(key)) continue
|
|
180
254
|
visited.add(key)
|
|
181
255
|
|
|
@@ -556,7 +630,7 @@ function collectDefinitionRefs(root: CanonicalDomainSchemaV1): IrSchemaRef[] {
|
|
|
556
630
|
addRef(declaration?.source, refs)
|
|
557
631
|
addRef(declaration?.target, refs)
|
|
558
632
|
}
|
|
559
|
-
|
|
633
|
+
for (const [, policy] of entriesOf(root.policies)) collectPolicyRefs(policy, refs)
|
|
560
634
|
return uniqueRefs(refs)
|
|
561
635
|
}
|
|
562
636
|
|
|
@@ -636,25 +710,13 @@ function addRef(value: unknown, refs: IrSchemaRef[]): void {
|
|
|
636
710
|
function uniqueRefs(refs: readonly IrSchemaRef[]): IrSchemaRef[] {
|
|
637
711
|
const seen = new Set<string>()
|
|
638
712
|
return refs.filter((ref) => {
|
|
639
|
-
const key =
|
|
713
|
+
const key = schemaRefKey(ref)
|
|
640
714
|
if (seen.has(key)) return false
|
|
641
715
|
seen.add(key)
|
|
642
716
|
return true
|
|
643
717
|
})
|
|
644
718
|
}
|
|
645
719
|
|
|
646
|
-
function refKey(ref: IrSchemaRef): IrSchemaKey {
|
|
647
|
-
return `${ref.origin}:${ref.kind}.${ref.name}`
|
|
648
|
-
}
|
|
649
|
-
|
|
650
|
-
function isDefinitionRef(ref: IrSchemaRef): ref is IrDefinitionRef {
|
|
651
|
-
return ref.kind === 'class' || ref.kind === 'interface'
|
|
652
|
-
}
|
|
653
|
-
|
|
654
|
-
function definitionKey(ref: IrDefinitionRef): IrDefinitionKey {
|
|
655
|
-
return `${ref.origin}:${ref.kind}.${ref.name}`
|
|
656
|
-
}
|
|
657
|
-
|
|
658
720
|
function coreEndpoint(value: unknown, rootOrigin: string): string {
|
|
659
721
|
const endpoint = asRecord(value)
|
|
660
722
|
if (endpoint?.kind === 'domain') return `/:${rootOrigin}`
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { expect, test } from 'bun:test'
|
|
2
|
+
|
|
3
|
+
import { parseConfigPreview } from './config-preview'
|
|
4
|
+
|
|
5
|
+
test('config preview ignores examples in comments and preserves URL literals', () => {
|
|
6
|
+
const preview = parseConfigPreview(`
|
|
7
|
+
// cloudflare({ prod: { route: 'commented.invalid', secrets: '.env.fake' } })
|
|
8
|
+
/* astrale({ prod: { instance: 'commented-instance' } }) */
|
|
9
|
+
export default deploy(domain, cloudflare({
|
|
10
|
+
dev: { client: { dir: 'client' }, secrets: '.env.dev' },
|
|
11
|
+
prod: {
|
|
12
|
+
client: { dir: 'client' },
|
|
13
|
+
route: 'https://services.example.dev/path',
|
|
14
|
+
secrets: '.env.prod',
|
|
15
|
+
},
|
|
16
|
+
}))
|
|
17
|
+
`)
|
|
18
|
+
|
|
19
|
+
expect(preview).toEqual({
|
|
20
|
+
adapter: 'cloudflare',
|
|
21
|
+
prodTarget: 'route: https://services.example.dev/path',
|
|
22
|
+
devSecrets: '.env.dev',
|
|
23
|
+
configuredSecretFiles: ['.env.dev', '.env.prod'],
|
|
24
|
+
})
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
test('config preview reports dynamic configuration as unknown instead of inventing authority', () => {
|
|
28
|
+
expect(parseConfigPreview(`export default makeDeployment(domain)`)).toEqual({
|
|
29
|
+
adapter: 'unknown',
|
|
30
|
+
configuredSecretFiles: [],
|
|
31
|
+
})
|
|
32
|
+
})
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Static, non-authoritative preview of astrale.config.ts.
|
|
3
|
+
*
|
|
4
|
+
* Deployment configuration is owned by the SDK adapter and is executable
|
|
5
|
+
* TypeScript. Studio deliberately does not execute it during anatomy/env reads;
|
|
6
|
+
* these few fields are only display/editor hints and may be unknown when the
|
|
7
|
+
* configuration is computed dynamically.
|
|
8
|
+
*/
|
|
9
|
+
import { readFileSync } from 'node:fs'
|
|
10
|
+
import { join } from 'node:path'
|
|
11
|
+
|
|
12
|
+
export interface ConfigPreview {
|
|
13
|
+
adapter: 'astrale' | 'cloudflare' | 'unknown'
|
|
14
|
+
prodTarget?: string
|
|
15
|
+
devSecrets?: string
|
|
16
|
+
configuredSecretFiles: string[]
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function withoutComments(source: string): string {
|
|
20
|
+
let output = ''
|
|
21
|
+
let quote: "'" | '"' | '`' | null = null
|
|
22
|
+
let escaped = false
|
|
23
|
+
for (let index = 0; index < source.length; index++) {
|
|
24
|
+
const char = source[index]!
|
|
25
|
+
const next = source[index + 1]
|
|
26
|
+
if (quote) {
|
|
27
|
+
output += char
|
|
28
|
+
if (escaped) escaped = false
|
|
29
|
+
else if (char === '\\') escaped = true
|
|
30
|
+
else if (char === quote) quote = null
|
|
31
|
+
continue
|
|
32
|
+
}
|
|
33
|
+
if (char === "'" || char === '"' || char === '`') {
|
|
34
|
+
quote = char
|
|
35
|
+
output += char
|
|
36
|
+
continue
|
|
37
|
+
}
|
|
38
|
+
if (char === '/' && next === '/') {
|
|
39
|
+
while (index + 1 < source.length && source[index + 1] !== '\n') index++
|
|
40
|
+
continue
|
|
41
|
+
}
|
|
42
|
+
if (char === '/' && next === '*') {
|
|
43
|
+
index += 2
|
|
44
|
+
while (index < source.length && !(source[index] === '*' && source[index + 1] === '/')) {
|
|
45
|
+
if (source[index] === '\n') output += '\n'
|
|
46
|
+
index++
|
|
47
|
+
}
|
|
48
|
+
index++
|
|
49
|
+
continue
|
|
50
|
+
}
|
|
51
|
+
output += char
|
|
52
|
+
}
|
|
53
|
+
return output
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function stringField(source: string, name: string): string | undefined {
|
|
57
|
+
const match = new RegExp(`\\b${name}\\s*:\\s*(['"])(.*?)\\1`).exec(source)
|
|
58
|
+
return match?.[2]
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function objectBody(source: string, name: string): string | undefined {
|
|
62
|
+
const start = new RegExp(`\\b${name}\\s*:\\s*\\{`).exec(source)
|
|
63
|
+
if (!start) return undefined
|
|
64
|
+
const opening = source.indexOf('{', start.index)
|
|
65
|
+
let depth = 1
|
|
66
|
+
let quote: "'" | '"' | '`' | null = null
|
|
67
|
+
let escaped = false
|
|
68
|
+
for (let index = opening + 1; index < source.length; index++) {
|
|
69
|
+
const char = source[index]!
|
|
70
|
+
if (quote) {
|
|
71
|
+
if (escaped) escaped = false
|
|
72
|
+
else if (char === '\\') escaped = true
|
|
73
|
+
else if (char === quote) quote = null
|
|
74
|
+
continue
|
|
75
|
+
}
|
|
76
|
+
if (char === "'" || char === '"' || char === '`') {
|
|
77
|
+
quote = char
|
|
78
|
+
continue
|
|
79
|
+
}
|
|
80
|
+
if (char === '{') depth++
|
|
81
|
+
else if (char === '}' && --depth === 0) return source.slice(opening + 1, index)
|
|
82
|
+
}
|
|
83
|
+
return undefined
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function parseConfigPreview(source: string): ConfigPreview {
|
|
87
|
+
const active = withoutComments(source)
|
|
88
|
+
const adapter: ConfigPreview['adapter'] = /\bastrale\s*\(/.test(active)
|
|
89
|
+
? 'astrale'
|
|
90
|
+
: /\bcloudflare\s*\(/.test(active)
|
|
91
|
+
? 'cloudflare'
|
|
92
|
+
: 'unknown'
|
|
93
|
+
const prod = objectBody(active, 'prod') ?? active
|
|
94
|
+
const dev = objectBody(active, 'dev') ?? ''
|
|
95
|
+
const instance = stringField(prod, 'instance')
|
|
96
|
+
const route = stringField(prod, 'route')
|
|
97
|
+
const configuredSecretFiles = [...active.matchAll(/\bsecrets\s*:\s*(['"])(.*?)\1/g)].map(
|
|
98
|
+
(match) => match[2]!,
|
|
99
|
+
)
|
|
100
|
+
const devSecrets = stringField(dev, 'secrets')
|
|
101
|
+
return {
|
|
102
|
+
adapter,
|
|
103
|
+
...(instance
|
|
104
|
+
? { prodTarget: `instance: ${instance}` }
|
|
105
|
+
: route
|
|
106
|
+
? { prodTarget: `route: ${route}` }
|
|
107
|
+
: {}),
|
|
108
|
+
...(devSecrets ? { devSecrets } : {}),
|
|
109
|
+
configuredSecretFiles: [...new Set(configuredSecretFiles)],
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function readConfigPreview(root: string): ConfigPreview {
|
|
114
|
+
try {
|
|
115
|
+
return parseConfigPreview(readFileSync(join(root, 'astrale.config.ts'), 'utf8'))
|
|
116
|
+
} catch {
|
|
117
|
+
return { adapter: 'unknown', configuredSecretFiles: [] }
|
|
118
|
+
}
|
|
119
|
+
}
|
|
@@ -2,7 +2,7 @@ import { describe, expect, test } from 'bun:test'
|
|
|
2
2
|
|
|
3
3
|
import type { IrClass, IrFunction, SchemaIR } from '../../shared/types'
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import { diffSchemas, structuralStatusOf } from './diff'
|
|
6
6
|
|
|
7
7
|
const callable = (returnsType = 'string'): IrFunction => ({
|
|
8
8
|
name: 'inspect',
|
|
@@ -31,21 +31,19 @@ const withClass = (member: IrClass): SchemaIR => ({
|
|
|
31
31
|
})
|
|
32
32
|
|
|
33
33
|
describe('canonical standalone Function diffs', () => {
|
|
34
|
-
test('
|
|
34
|
+
test('reports additions and removals without claiming Runtime compatibility', () => {
|
|
35
35
|
const empty = schema({})
|
|
36
36
|
const populated = schema({ inspect: callable() })
|
|
37
|
-
expect(diffSchemas(empty, populated)).toEqual([
|
|
38
|
-
{ kind: 'function-added', target: 'inspect', breaking: false },
|
|
39
|
-
])
|
|
37
|
+
expect(diffSchemas(empty, populated)).toEqual([{ kind: 'function-added', target: 'inspect' }])
|
|
40
38
|
const removed = diffSchemas(populated, empty)
|
|
41
|
-
expect(removed).toEqual([{ kind: 'function-removed', target: 'inspect'
|
|
42
|
-
expect(
|
|
39
|
+
expect(removed).toEqual([{ kind: 'function-removed', target: 'inspect' }])
|
|
40
|
+
expect(structuralStatusOf(removed)).toBe('changed')
|
|
43
41
|
})
|
|
44
42
|
|
|
45
43
|
test('detects a standalone Function signature change', () => {
|
|
46
44
|
expect(
|
|
47
45
|
diffSchemas(schema({ inspect: callable() }), schema({ inspect: callable('number') })),
|
|
48
|
-
).toEqual([{ kind: 'function-signature-changed', target: 'inspect'
|
|
46
|
+
).toEqual([{ kind: 'function-signature-changed', target: 'inspect' }])
|
|
49
47
|
})
|
|
50
48
|
|
|
51
49
|
test('detects auth and output-mode changes in callable contracts', () => {
|
|
@@ -57,7 +55,7 @@ describe('canonical standalone Function diffs', () => {
|
|
|
57
55
|
}
|
|
58
56
|
|
|
59
57
|
expect(diffSchemas(schema({ inspect: before }), schema({ inspect: after }))).toEqual([
|
|
60
|
-
{ kind: 'function-signature-changed', target: 'inspect'
|
|
58
|
+
{ kind: 'function-signature-changed', target: 'inspect' },
|
|
61
59
|
])
|
|
62
60
|
})
|
|
63
61
|
|
|
@@ -75,13 +73,11 @@ describe('canonical standalone Function diffs', () => {
|
|
|
75
73
|
kind: 'prop-schema-changed',
|
|
76
74
|
target: 'Person.name',
|
|
77
75
|
detail: 'value constraints changed',
|
|
78
|
-
breaking: true,
|
|
79
76
|
},
|
|
80
77
|
{
|
|
81
78
|
kind: 'prop-required-changed',
|
|
82
79
|
target: 'Person.name',
|
|
83
80
|
detail: 'optional → required',
|
|
84
|
-
breaking: true,
|
|
85
81
|
},
|
|
86
82
|
])
|
|
87
83
|
})
|