@astrale-os/cli 1.0.0-beta.27 → 1.0.0-beta.28
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 +26 -3
- package/dist/astrale.js +243 -76
- package/package.json +1 -1
- package/src/commands/__tests__/domain-install-operation.test.ts +23 -0
- package/src/commands/__tests__/install-identity-override.test.ts +3 -3
- package/src/commands/domain/install.ts +19 -11
- package/src/commands/get.ts +1 -1
- package/src/commands/identity/register.ts +3 -4
- package/src/commands/logs.ts +2 -5
- package/src/commands/session/analyze.ts +26 -4
- package/src/lib/__tests__/skills.test.ts +8 -2
- package/src/program/__tests__/program.test.ts +1 -1
- package/src/telemetry/__tests__/retention.test.ts +180 -0
- package/src/telemetry/__tests__/settings.test.ts +102 -0
- package/src/telemetry/__tests__/store-scan.test.ts +128 -0
- package/src/telemetry/__tests__/trigger.test.ts +33 -4
- package/src/telemetry/recorder.ts +6 -3
- package/src/telemetry/retention.ts +129 -0
- package/src/telemetry/session.ts +18 -12
- package/src/telemetry/settings.ts +64 -11
- package/src/telemetry/store.ts +87 -9
- package/src/telemetry/trigger.ts +16 -28
- package/studio/client/dist/assets/index-BFVFs_8x.js +81 -0
- package/studio/client/dist/assets/index-DuB9iUdu.css +2 -0
- package/studio/client/dist/assets/schema-studio--a0kX3QU.css +1 -0
- package/studio/client/dist/assets/schema-studio-DH6oEWME.js +8 -0
- package/studio/client/dist/index.html +3 -3
- package/studio/server/agent/prompts/anchors.test.ts +17 -4
- package/studio/server/agent/prompts/anchors.ts +3 -2
- package/studio/server/agent/prompts/system.test.ts +2 -3
- package/studio/server/agent/prompts/system.ts +3 -3
- package/studio/server/agent/run/preparation.ts +1 -1
- package/studio/server/api/context.ts +1 -1
- package/studio/server/api/deployment.ts +1 -1
- package/studio/server/api/views.ts +2 -2
- package/studio/server/api/workspace.ts +1 -1
- package/studio/server/cache.test.ts +3 -8
- package/studio/server/cache.ts +4 -45
- package/studio/server/handoff/copy.ts +1 -1
- package/studio/server/introspect/canonical-schema.test.ts +154 -128
- package/studio/server/introspect/canonical-schema.ts +183 -302
- package/studio/server/introspect/core.ts +14 -21
- package/studio/server/introspect/extractor.ts +11 -15
- package/studio/server/introspect/overlay-tsmorph.test.ts +1 -5
- package/studio/server/introspect/overlay-tsmorph.ts +0 -1
- package/studio/server/introspect/overlay.ts +3 -24
- package/studio/server/introspect/revision.test.ts +24 -28
- package/studio/server/introspect/revision.ts +10 -21
- package/studio/server/introspect/runtime.test.ts +14 -14
- package/studio/server/introspect/runtime.ts +1 -46
- package/studio/server/lifecycle.ts +4 -1
- package/studio/server/state/documents.test.ts +69 -0
- package/studio/server/state/documents.ts +57 -10
- package/studio/shared/contracts/schema.ts +10 -26
- package/studio/shared/contracts/surface.test.ts +2 -2
- package/studio/shared/contracts/workspace.ts +3 -0
- package/studio/shared/schema/identity.ts +0 -1
- package/studio/client/dist/assets/index-C0FAnwNw.css +0 -2
- package/studio/client/dist/assets/index-CpBed0V1.js +0 -81
- package/studio/client/dist/assets/schema-studio-BilUNb6Z.css +0 -1
- package/studio/client/dist/assets/schema-studio-DJm9guI8.js +0 -8
- package/studio/server/introspect/core-extractor.ts +0 -30
- package/studio/server/introspect/overlay.test.ts +0 -44
- package/studio/server/introspect/source-overlay/annotations.ts +0 -14
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
/** Bun subprocess that imports only the authored Schema entry and its installed SDK. */
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
projectCanonicalSchema,
|
|
6
|
-
} from './canonical-schema'
|
|
2
|
+
import type { SchemaSdk } from './canonical-schema'
|
|
3
|
+
|
|
4
|
+
import { extractCanonicalSchemaFromSdk, findCanonicalDomainSchemaExport } from './canonical-schema'
|
|
7
5
|
|
|
8
6
|
const schemaPath = process.argv[2]
|
|
9
7
|
const projectRoot = process.argv[3] ?? process.cwd()
|
|
@@ -15,19 +13,17 @@ async function main(): Promise<void> {
|
|
|
15
13
|
if (candidate === null) {
|
|
16
14
|
throw new Error('Schema entry exports no canonical V1 DomainSchema.')
|
|
17
15
|
}
|
|
18
|
-
const sdk = (await import(
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
const admission = admitCanonicalSchemaFromSdk(sdk, candidate)
|
|
23
|
-
const projected = projectCanonicalSchema(admission.root, admission.closure)
|
|
16
|
+
const sdk = (await import(
|
|
17
|
+
Bun.resolveSync('@astrale-os/sdk/schema', projectRoot)
|
|
18
|
+
)) as unknown as SchemaSdk
|
|
19
|
+
const extraction = extractCanonicalSchemaFromSdk(sdk, candidate)
|
|
24
20
|
process.stdout.write(
|
|
25
21
|
JSON.stringify({
|
|
26
22
|
ok: true,
|
|
27
|
-
ir:
|
|
28
|
-
root:
|
|
29
|
-
schemaMode:
|
|
30
|
-
revision:
|
|
23
|
+
ir: extraction.ir,
|
|
24
|
+
root: extraction.root,
|
|
25
|
+
schemaMode: extraction.status === 'admitted' ? 'canonical-admitted' : 'canonical-preview',
|
|
26
|
+
revision: extraction.revision,
|
|
31
27
|
}),
|
|
32
28
|
)
|
|
33
29
|
}
|
|
@@ -5,7 +5,7 @@ import { join } from 'node:path'
|
|
|
5
5
|
|
|
6
6
|
import type { SchemaIR } from '../../shared/types'
|
|
7
7
|
|
|
8
|
-
import { buildHandlerLinks,
|
|
8
|
+
import { buildHandlerLinks, buildSourceSpans } from './overlay-tsmorph'
|
|
9
9
|
|
|
10
10
|
const roots: string[] = []
|
|
11
11
|
afterEach(() => {
|
|
@@ -139,8 +139,4 @@ describe('source overlay', () => {
|
|
|
139
139
|
expect(spans['edge.assigned_to.property.note']?.file).toBe('schema/members.ts')
|
|
140
140
|
expect(spans['function.createIssue']?.file).toBe('schema/members.ts')
|
|
141
141
|
})
|
|
142
|
-
|
|
143
|
-
test('does not invent warnings for admitted enum values', () => {
|
|
144
|
-
expect(buildSchemaAnnotations({ ir })).toEqual([])
|
|
145
|
-
})
|
|
146
142
|
})
|
|
@@ -4,6 +4,5 @@
|
|
|
4
4
|
* Keep this path stable for the overlay composer and focused tests. The
|
|
5
5
|
* implementation lives under source-overlay, split by responsibility.
|
|
6
6
|
*/
|
|
7
|
-
export { buildSchemaAnnotations } from './source-overlay/annotations'
|
|
8
7
|
export { buildHandlerLinks } from './source-overlay/handlers'
|
|
9
8
|
export { buildSourceSpans } from './source-overlay/spans'
|
|
@@ -1,12 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
* carry. The import split and dependency list are computed here; handlerLinks,
|
|
4
|
-
* static parse). handlerLinks, sourceSpans (+JSDoc) and annotations are filled
|
|
5
|
-
* by a ts-morph pass over current Schema, Runtime, Action, and Workflow files.
|
|
2
|
+
* Source-only metadata that the admitted DSL Schema cannot carry.
|
|
6
3
|
*/
|
|
7
|
-
import type {
|
|
4
|
+
import type { SchemaIR, SchemaOverlay } from '../../shared/types'
|
|
8
5
|
|
|
9
|
-
import { buildHandlerLinks,
|
|
6
|
+
import { buildHandlerLinks, buildSourceSpans } from './overlay-tsmorph'
|
|
10
7
|
|
|
11
8
|
export interface OverlayArgs {
|
|
12
9
|
ir: SchemaIR | null
|
|
@@ -15,26 +12,8 @@ export interface OverlayArgs {
|
|
|
15
12
|
}
|
|
16
13
|
|
|
17
14
|
export function buildOverlay({ ir, domainRoot, schemaDir }: OverlayArgs): SchemaOverlay {
|
|
18
|
-
const origin = ir?.domain ?? ''
|
|
19
|
-
const mixins: CrossDomainImport[] = []
|
|
20
|
-
const crossDomainImports: CrossDomainImport[] = []
|
|
21
|
-
for (const descriptor of Object.values(ir?.importsByKey ?? {})) {
|
|
22
|
-
const entry: CrossDomainImport = {
|
|
23
|
-
name: descriptor.ref.name,
|
|
24
|
-
origin: descriptor.origin,
|
|
25
|
-
ref: descriptor.ref,
|
|
26
|
-
}
|
|
27
|
-
if (descriptor.origin === 'kernel.astrale.ai') mixins.push(entry)
|
|
28
|
-
else crossDomainImports.push(entry)
|
|
29
|
-
}
|
|
30
|
-
|
|
31
15
|
return {
|
|
32
|
-
origin,
|
|
33
|
-
requires: (ir?.dependencies ?? []).map(({ origin }) => origin),
|
|
34
|
-
crossDomainImports,
|
|
35
|
-
mixins,
|
|
36
16
|
handlerLinks: buildHandlerLinks({ ir, domainRoot }),
|
|
37
17
|
sourceSpans: buildSourceSpans({ ir, schemaDir }),
|
|
38
|
-
annotations: buildSchemaAnnotations({ ir }),
|
|
39
18
|
}
|
|
40
19
|
}
|
|
@@ -1,53 +1,49 @@
|
|
|
1
|
+
import * as sdk from '@astrale-os/sdk/schema'
|
|
2
|
+
import { defineSchema } from '@astrale-os/sdk/schema'
|
|
1
3
|
import { describe, expect, test } from 'bun:test'
|
|
2
4
|
|
|
3
5
|
import { admittedBundleRevisionFromSdk } from './revision'
|
|
4
6
|
|
|
5
|
-
const
|
|
7
|
+
const schema = defineSchema('notes.example.dev', {})
|
|
8
|
+
const wire = JSON.parse(JSON.stringify(sdk.bundle.create(schema)))
|
|
6
9
|
|
|
7
10
|
describe('installed Bundle revision', () => {
|
|
8
|
-
test('uses
|
|
11
|
+
test('uses Bundle admission and the resolved Domain identity', () => {
|
|
9
12
|
const calls: string[] = []
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
const sdk = {
|
|
13
|
+
const wrapped = {
|
|
14
|
+
...sdk,
|
|
13
15
|
bundle: {
|
|
16
|
+
...sdk.bundle,
|
|
14
17
|
accept(value: unknown) {
|
|
15
18
|
calls.push('bundle.accept')
|
|
16
|
-
|
|
17
|
-
return { root, closure: [] }
|
|
19
|
+
return sdk.bundle.accept(value)
|
|
18
20
|
},
|
|
19
21
|
},
|
|
20
22
|
schema: {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
return
|
|
23
|
+
...sdk.schema,
|
|
24
|
+
resolve(value: typeof schema) {
|
|
25
|
+
calls.push('schema.resolve')
|
|
26
|
+
return sdk.schema.resolve(value)
|
|
25
27
|
},
|
|
26
28
|
},
|
|
27
|
-
}
|
|
29
|
+
} as unknown as typeof sdk
|
|
28
30
|
|
|
29
|
-
expect(admittedBundleRevisionFromSdk(
|
|
30
|
-
expect(calls).toEqual(['bundle.accept', 'schema.
|
|
31
|
+
expect(admittedBundleRevisionFromSdk(wrapped, wire)).toBe(sdk.schema.revision(schema))
|
|
32
|
+
expect(calls).toEqual(['bundle.accept', 'schema.resolve'])
|
|
31
33
|
})
|
|
32
34
|
|
|
33
|
-
test('fails closed when
|
|
34
|
-
expect(
|
|
35
|
-
admittedBundleRevisionFromSdk(
|
|
36
|
-
{ bundle: { accept: () => ({ root: {} }) }, schema: { revision: () => 'sha-short' } },
|
|
37
|
-
{},
|
|
38
|
-
),
|
|
39
|
-
).toBeNull()
|
|
35
|
+
test('fails closed when Bundle admission or the resolved identity is invalid', () => {
|
|
36
|
+
expect(admittedBundleRevisionFromSdk({} as typeof sdk, {})).toBeNull()
|
|
40
37
|
expect(
|
|
41
38
|
admittedBundleRevisionFromSdk(
|
|
42
39
|
{
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
},
|
|
40
|
+
...sdk,
|
|
41
|
+
schema: {
|
|
42
|
+
...sdk.schema,
|
|
43
|
+
resolve: () => ({ source: schema, revision: 'sha-short' }),
|
|
47
44
|
},
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
{},
|
|
45
|
+
} as unknown as typeof sdk,
|
|
46
|
+
wire,
|
|
51
47
|
),
|
|
52
48
|
).toBeNull()
|
|
53
49
|
})
|
|
@@ -1,28 +1,23 @@
|
|
|
1
1
|
import type { SchemaRevision } from '../../shared/types'
|
|
2
|
+
import type { SchemaAdmissionSdk } from './canonical-schema'
|
|
2
3
|
|
|
3
4
|
import { isSchemaRevision } from '../../shared/types'
|
|
4
5
|
|
|
5
|
-
type AnyRecord = Record<string, unknown>
|
|
6
|
-
|
|
7
6
|
/**
|
|
8
7
|
* Cross the installed-bundle JSON boundary through the Domain's installed SDK, then
|
|
9
8
|
* ask the DSL owner for the root revision. Invalid/unavailable bundles return
|
|
10
9
|
* null so Studio reports unknown drift instead of inventing an identity.
|
|
11
10
|
*/
|
|
12
11
|
export function admittedBundleRevisionFromSdk(
|
|
13
|
-
|
|
12
|
+
sdk: SchemaAdmissionSdk,
|
|
14
13
|
input: unknown,
|
|
15
14
|
): SchemaRevision | null {
|
|
16
|
-
const bundleApi = asRecord(sdkModule.bundle)
|
|
17
|
-
const schemaApi = asRecord(sdkModule.schema)
|
|
18
|
-
if (typeof bundleApi?.accept !== 'function' || typeof schemaApi?.revision !== 'function') {
|
|
19
|
-
return null
|
|
20
|
-
}
|
|
21
15
|
try {
|
|
22
|
-
const accepted =
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
16
|
+
const accepted = sdk.bundle.accept(input)
|
|
17
|
+
const domain = sdk.schema.resolve(accepted.root)
|
|
18
|
+
return domain.source === accepted.root && isSchemaRevision(domain.revision)
|
|
19
|
+
? domain.revision
|
|
20
|
+
: null
|
|
26
21
|
} catch {
|
|
27
22
|
return null
|
|
28
23
|
}
|
|
@@ -33,17 +28,11 @@ export async function admittedBundleRevision(
|
|
|
33
28
|
input: unknown,
|
|
34
29
|
): Promise<SchemaRevision | null> {
|
|
35
30
|
try {
|
|
36
|
-
const
|
|
31
|
+
const sdk = (await import(
|
|
37
32
|
Bun.resolveSync('@astrale-os/sdk/schema', domainRoot)
|
|
38
|
-
)
|
|
39
|
-
return admittedBundleRevisionFromSdk(
|
|
33
|
+
)) as unknown as SchemaAdmissionSdk
|
|
34
|
+
return admittedBundleRevisionFromSdk(sdk, input)
|
|
40
35
|
} catch {
|
|
41
36
|
return null
|
|
42
37
|
}
|
|
43
38
|
}
|
|
44
|
-
|
|
45
|
-
function asRecord(value: unknown): AnyRecord | null {
|
|
46
|
-
return value !== null && typeof value === 'object' && !Array.isArray(value)
|
|
47
|
-
? (value as AnyRecord)
|
|
48
|
-
: null
|
|
49
|
-
}
|
|
@@ -6,9 +6,10 @@ import { join } from 'node:path'
|
|
|
6
6
|
import type { DomainHandle } from '../domain'
|
|
7
7
|
|
|
8
8
|
import { buildBundle } from './bundle'
|
|
9
|
+
import { buildCore } from './core'
|
|
9
10
|
import { renderFingerprintOf } from './hash'
|
|
10
11
|
import { admittedBundleRevision } from './revision'
|
|
11
|
-
import {
|
|
12
|
+
import { runtimeExtract } from './runtime'
|
|
12
13
|
|
|
13
14
|
const roots: string[] = []
|
|
14
15
|
afterEach(() => {
|
|
@@ -128,19 +129,18 @@ describe('SDK V1 schema extractor', () => {
|
|
|
128
129
|
|
|
129
130
|
test('extracts Core through the pure Schema entry without importing Application', async () => {
|
|
130
131
|
const handle = currentFixture()
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
},
|
|
132
|
+
const bundle = await buildBundle(handle)
|
|
133
|
+
expect(buildCore(handle, bundle)).toMatchObject({
|
|
134
|
+
domain: 'documents.runtime.test',
|
|
135
|
+
nodes: [
|
|
136
|
+
{
|
|
137
|
+
path: '/:documents.runtime.test:core.welcome',
|
|
138
|
+
className: 'Document',
|
|
139
|
+
data: {},
|
|
140
|
+
},
|
|
141
|
+
],
|
|
142
|
+
edges: [],
|
|
143
|
+
error: null,
|
|
144
144
|
})
|
|
145
145
|
})
|
|
146
146
|
})
|
|
@@ -5,12 +5,11 @@
|
|
|
5
5
|
* render IR plus the raw canonical root (when present), or an error render-state
|
|
6
6
|
* — never throws.
|
|
7
7
|
*/
|
|
8
|
-
import type { SchemaIR, SchemaRevision,
|
|
8
|
+
import type { SchemaIR, SchemaRevision, StudioSchemaBundle } from '../../shared/types'
|
|
9
9
|
|
|
10
10
|
import { isSchemaRevision } from '../../shared/types'
|
|
11
11
|
|
|
12
12
|
const EXTRACTOR = new URL('./extractor.ts', import.meta.url).pathname
|
|
13
|
-
const CORE_EXTRACTOR = new URL('./core-extractor.ts', import.meta.url).pathname
|
|
14
13
|
|
|
15
14
|
export interface RuntimeExtractResult {
|
|
16
15
|
ok: boolean
|
|
@@ -94,47 +93,3 @@ export async function runtimeExtract(
|
|
|
94
93
|
}
|
|
95
94
|
}
|
|
96
95
|
}
|
|
97
|
-
|
|
98
|
-
export interface CoreExtractResult {
|
|
99
|
-
ok: boolean
|
|
100
|
-
/** the resolved core graph, or null when the domain defines no core */
|
|
101
|
-
core: Pick<StudioCore, 'domain' | 'nodes' | 'edges'> | null
|
|
102
|
-
error?: { message: string }
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
/**
|
|
106
|
-
* Spawn the Core extractor over the pure Schema entry. Returns the projected
|
|
107
|
-
* graph or an error and never imports Application or Runtime modules.
|
|
108
|
-
*/
|
|
109
|
-
export async function coreExtract(
|
|
110
|
-
schemaIndexPath: string,
|
|
111
|
-
domainDir: string,
|
|
112
|
-
timeoutMs = 20000,
|
|
113
|
-
): Promise<CoreExtractResult> {
|
|
114
|
-
try {
|
|
115
|
-
const proc = Bun.spawn(['bun', 'run', CORE_EXTRACTOR, schemaIndexPath, domainDir], {
|
|
116
|
-
cwd: domainDir,
|
|
117
|
-
stdout: 'pipe',
|
|
118
|
-
stderr: 'pipe',
|
|
119
|
-
})
|
|
120
|
-
const timer = setTimeout(() => proc.kill(9), timeoutMs)
|
|
121
|
-
const out = await new Response(proc.stdout).text()
|
|
122
|
-
await proc.exited
|
|
123
|
-
clearTimeout(timer)
|
|
124
|
-
|
|
125
|
-
if (!out.trim()) {
|
|
126
|
-
const err = await new Response(proc.stderr).text()
|
|
127
|
-
return {
|
|
128
|
-
ok: false,
|
|
129
|
-
core: null,
|
|
130
|
-
error: { message: err.trim() || 'core extractor produced no output' },
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
const parsed = JSON.parse(out)
|
|
134
|
-
if (!parsed.ok)
|
|
135
|
-
return { ok: false, core: null, error: parsed.error ?? { message: 'core extraction failed' } }
|
|
136
|
-
return { ok: true, core: (parsed.core ?? null) as CoreExtractResult['core'] }
|
|
137
|
-
} catch (e: any) {
|
|
138
|
-
return { ok: false, core: null, error: { message: String(e?.message ?? e) } }
|
|
139
|
-
}
|
|
140
|
-
}
|
|
@@ -9,6 +9,7 @@ import type { DomainHandle } from './domain'
|
|
|
9
9
|
*/
|
|
10
10
|
import { getBundle } from './cache'
|
|
11
11
|
import { captureBaseline, hashAnatomyFiles, loadBaseline } from './state/baseline'
|
|
12
|
+
import { migrateDocuments } from './state/documents'
|
|
12
13
|
import { initDotDir } from './state/store'
|
|
13
14
|
import { watchDomain } from './watch'
|
|
14
15
|
|
|
@@ -22,6 +23,8 @@ export interface BootedDomain {
|
|
|
22
23
|
/** Initialize + start watching one domain. Returns its origin + a stop handle. */
|
|
23
24
|
export async function bootDomain(handle: DomainHandle): Promise<BootedDomain> {
|
|
24
25
|
initDotDir(handle.root)
|
|
26
|
+
// one-shot: uuid-named documents become readable file names under context/docs
|
|
27
|
+
migrateDocuments(handle.root)
|
|
25
28
|
const bundle = await getBundle(handle.id)
|
|
26
29
|
if (!loadBaseline(handle.root))
|
|
27
30
|
captureBaseline(
|
|
@@ -35,7 +38,7 @@ export async function bootDomain(handle: DomainHandle): Promise<BootedDomain> {
|
|
|
35
38
|
)
|
|
36
39
|
const stop = watchDomain(handle)
|
|
37
40
|
return {
|
|
38
|
-
origin: bundle?.
|
|
41
|
+
origin: bundle?.ir?.domain ?? handle.origin ?? handle.id,
|
|
39
42
|
depsInstalled: !!bundle?.depsInstalled,
|
|
40
43
|
stop,
|
|
41
44
|
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { afterEach, expect, test } from 'bun:test'
|
|
2
|
+
import { existsSync, mkdtempSync, readFileSync, rmSync } from 'node:fs'
|
|
3
|
+
import { tmpdir } from 'node:os'
|
|
4
|
+
import { join } from 'node:path'
|
|
5
|
+
|
|
6
|
+
import { addDocument, listDocuments, migrateDocuments, readDocument } from './documents'
|
|
7
|
+
import { statePath, writeJson, writeStateBuffer } from './store'
|
|
8
|
+
|
|
9
|
+
const roots: string[] = []
|
|
10
|
+
|
|
11
|
+
function root(): string {
|
|
12
|
+
const created = mkdtempSync(join(tmpdir(), 'studio-documents-'))
|
|
13
|
+
roots.push(created)
|
|
14
|
+
return created
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
afterEach(() => {
|
|
18
|
+
while (roots.length) rmSync(roots.pop()!, { recursive: true, force: true })
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
const bytes = (text: string) => new TextEncoder().encode(text)
|
|
22
|
+
|
|
23
|
+
test('a document is stored under a readable name, not its id', () => {
|
|
24
|
+
const domain = root()
|
|
25
|
+
const meta = addDocument(domain, 'Pricing Decisions.MD', 'text/markdown', bytes('# pricing'))
|
|
26
|
+
|
|
27
|
+
expect(meta.stored).toBe('context/docs/pricing-decisions.md')
|
|
28
|
+
expect(readFileSync(statePath(domain, meta.stored), 'utf8')).toBe('# pricing')
|
|
29
|
+
expect(readDocument(domain, meta.id)?.meta.name).toBe('Pricing Decisions.MD')
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
test('same-named documents never overwrite each other', () => {
|
|
33
|
+
const domain = root()
|
|
34
|
+
const first = addDocument(domain, 'notes.md', 'text/markdown', bytes('first'))
|
|
35
|
+
const second = addDocument(domain, 'notes.md', 'text/markdown', bytes('second'))
|
|
36
|
+
|
|
37
|
+
expect([first.stored, second.stored]).toEqual([
|
|
38
|
+
'context/docs/notes.md',
|
|
39
|
+
'context/docs/notes-2.md',
|
|
40
|
+
])
|
|
41
|
+
expect(readFileSync(statePath(domain, first.stored), 'utf8')).toBe('first')
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
test('uuid-named documents are migrated in place, once', () => {
|
|
45
|
+
const domain = root()
|
|
46
|
+
const legacy = 'context/documents/2b0d9d7e-1f2a-4a10-9f0c-1a2b3c4d5e6f.md'
|
|
47
|
+
writeStateBuffer(domain, legacy, bytes('legacy body'))
|
|
48
|
+
writeJson(domain, 'context/documents/index.json', [
|
|
49
|
+
{
|
|
50
|
+
id: '2b0d9d7e-1f2a-4a10-9f0c-1a2b3c4d5e6f',
|
|
51
|
+
name: 'Meeting notes.md',
|
|
52
|
+
type: 'text/markdown',
|
|
53
|
+
size: 11,
|
|
54
|
+
addedAt: new Date(0).toISOString(),
|
|
55
|
+
stored: legacy,
|
|
56
|
+
},
|
|
57
|
+
])
|
|
58
|
+
|
|
59
|
+
migrateDocuments(domain)
|
|
60
|
+
const [migrated] = listDocuments(domain)
|
|
61
|
+
|
|
62
|
+
expect(migrated?.stored).toBe('context/docs/meeting-notes.md')
|
|
63
|
+
expect(readFileSync(statePath(domain, migrated!.stored), 'utf8')).toBe('legacy body')
|
|
64
|
+
expect(existsSync(statePath(domain, legacy))).toBe(false)
|
|
65
|
+
|
|
66
|
+
// idempotent: a second boot leaves the already-migrated store alone
|
|
67
|
+
migrateDocuments(domain)
|
|
68
|
+
expect(listDocuments(domain)[0]?.stored).toBe('context/docs/meeting-notes.md')
|
|
69
|
+
})
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* documents.ts — context DOCUMENTS the user drops in for the AI agent. Files
|
|
3
|
-
*
|
|
4
|
-
*
|
|
2
|
+
* documents.ts — context DOCUMENTS the user drops in for the AI agent. Files live
|
|
3
|
+
* under `.domain-studio/context/docs/`, named after the document (not its id), so
|
|
4
|
+
* the folder is readable by a human and by the agent that is handed its path.
|
|
5
|
+
* They travel with the domain and are part of the agent handoff context.
|
|
5
6
|
*/
|
|
6
7
|
import { randomUUID } from 'node:crypto'
|
|
7
|
-
import { existsSync } from 'node:fs'
|
|
8
|
+
import { existsSync, readFileSync } from 'node:fs'
|
|
8
9
|
import { extname } from 'node:path'
|
|
9
10
|
|
|
10
11
|
import type { DocMeta } from '../../shared/types'
|
|
@@ -13,7 +14,54 @@ import { asFiniteNumber, asJsonRecord, asString } from '../json'
|
|
|
13
14
|
import { readJson, removeState, statePath, writeJson, writeStateBuffer } from './store'
|
|
14
15
|
|
|
15
16
|
const INDEX = 'context/documents/index.json'
|
|
16
|
-
const
|
|
17
|
+
const DIR = 'context/docs'
|
|
18
|
+
/** Where documents lived before they were named: `context/documents/<uuid>.<ext>`. */
|
|
19
|
+
const LEGACY_DIR = 'context/documents'
|
|
20
|
+
|
|
21
|
+
/** `Pricing decisions.md` → `pricing-decisions` — a file name you can read. */
|
|
22
|
+
function slugify(name: string): string {
|
|
23
|
+
const base = name.replace(/\.[^.]+$/, '')
|
|
24
|
+
const slug = base
|
|
25
|
+
.normalize('NFKD')
|
|
26
|
+
.replace(/[\u0300-\u036f]/g, '')
|
|
27
|
+
.toLowerCase()
|
|
28
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
29
|
+
.replace(/^-+|-+$/g, '')
|
|
30
|
+
.slice(0, 60)
|
|
31
|
+
return slug || 'document'
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** A stored path no other document (and no file on disk) already claims. */
|
|
35
|
+
function uniqueStoredPath(root: string, docs: DocMeta[], name: string): string {
|
|
36
|
+
const extension = extname(name).toLowerCase()
|
|
37
|
+
const slug = slugify(name)
|
|
38
|
+
const taken = new Set(docs.map((doc) => doc.stored))
|
|
39
|
+
for (let attempt = 0; ; attempt++) {
|
|
40
|
+
const candidate = `${DIR}/${slug}${attempt === 0 ? '' : `-${attempt + 1}`}${extension}`
|
|
41
|
+
if (!taken.has(candidate) && !existsSync(statePath(root, candidate))) return candidate
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Move documents written under the old uuid-named layout into `context/docs/`.
|
|
47
|
+
* Idempotent and only ever renames inside the studio's own state directory.
|
|
48
|
+
*/
|
|
49
|
+
export function migrateDocuments(root: string): void {
|
|
50
|
+
const docs = listDocuments(root)
|
|
51
|
+
if (!docs.some((doc) => doc.stored.startsWith(`${LEGACY_DIR}/`))) return
|
|
52
|
+
let moved = false
|
|
53
|
+
for (const doc of docs) {
|
|
54
|
+
if (!doc.stored.startsWith(`${LEGACY_DIR}/`)) continue
|
|
55
|
+
const from = statePath(root, doc.stored)
|
|
56
|
+
if (!existsSync(from)) continue
|
|
57
|
+
const next = uniqueStoredPath(root, docs, doc.name)
|
|
58
|
+
writeStateBuffer(root, next, readFileSync(from))
|
|
59
|
+
removeState(root, doc.stored)
|
|
60
|
+
doc.stored = next
|
|
61
|
+
moved = true
|
|
62
|
+
}
|
|
63
|
+
if (moved) writeJson(root, INDEX, docs)
|
|
64
|
+
}
|
|
17
65
|
|
|
18
66
|
function decodeDocument(value: unknown): DocMeta | undefined {
|
|
19
67
|
const record = asJsonRecord(value)
|
|
@@ -30,7 +78,7 @@ function decodeDocument(value: unknown): DocMeta | undefined {
|
|
|
30
78
|
size === undefined ||
|
|
31
79
|
size < 0 ||
|
|
32
80
|
!addedAt ||
|
|
33
|
-
!stored?.startsWith(
|
|
81
|
+
!(stored?.startsWith(`${DIR}/`) || stored?.startsWith(`${LEGACY_DIR}/`)) ||
|
|
34
82
|
stored.split('/').includes('..')
|
|
35
83
|
) {
|
|
36
84
|
return undefined
|
|
@@ -60,18 +108,17 @@ export function listDocuments(root: string): DocMeta[] {
|
|
|
60
108
|
}
|
|
61
109
|
|
|
62
110
|
export function addDocument(root: string, name: string, type: string, data: Uint8Array): DocMeta {
|
|
63
|
-
const
|
|
64
|
-
const stored =
|
|
111
|
+
const docs = listDocuments(root)
|
|
112
|
+
const stored = uniqueStoredPath(root, docs, name || 'untitled')
|
|
65
113
|
writeStateBuffer(root, stored, data)
|
|
66
114
|
const meta: DocMeta = {
|
|
67
|
-
id,
|
|
115
|
+
id: randomUUID(),
|
|
68
116
|
name: name || 'untitled',
|
|
69
117
|
type: type || 'application/octet-stream',
|
|
70
118
|
size: data.byteLength,
|
|
71
119
|
addedAt: new Date().toISOString(),
|
|
72
120
|
stored,
|
|
73
121
|
}
|
|
74
|
-
const docs = listDocuments(root)
|
|
75
122
|
docs.unshift(meta)
|
|
76
123
|
writeJson(root, INDEX, docs)
|
|
77
124
|
return meta
|
|
@@ -18,11 +18,17 @@ export interface JsonSchema {
|
|
|
18
18
|
maximum?: number
|
|
19
19
|
format?: string
|
|
20
20
|
description?: string
|
|
21
|
-
$
|
|
22
|
-
$dataRef?: unknown
|
|
21
|
+
$ref?: string
|
|
23
22
|
[k: string]: unknown
|
|
24
23
|
}
|
|
25
24
|
|
|
25
|
+
const NODE_PATH_SCHEMA_ID = 'https://schemas.astrale.ai/graph/1/node-path'
|
|
26
|
+
|
|
27
|
+
/** Structural form emitted by the V1 DSL for a graph Node path. */
|
|
28
|
+
export function isNodePathSchema(schema: JsonSchema): boolean {
|
|
29
|
+
return schema.$ref === NODE_PATH_SCHEMA_ID && Object.hasOwn(schema, 'x-astrale-path')
|
|
30
|
+
}
|
|
31
|
+
|
|
26
32
|
export type MethodInheritance = 'default' | 'abstract' | 'sealed'
|
|
27
33
|
|
|
28
34
|
/** Portable form of the DSL-owned canonical schema revision. */
|
|
@@ -37,7 +43,7 @@ export function isSchemaRevision(value: unknown): value is SchemaRevision {
|
|
|
37
43
|
* Version of Studio's lossy canonical-schema -> render-IR projection. Persisted
|
|
38
44
|
* baselines must match this value before their IR can be compared.
|
|
39
45
|
*/
|
|
40
|
-
export const STUDIO_SCHEMA_PROJECTION_VERSION =
|
|
46
|
+
export const STUDIO_SCHEMA_PROJECTION_VERSION = 3
|
|
41
47
|
|
|
42
48
|
export type IrCallableAuth = 'anonymous' | 'authenticated' | 'authorized'
|
|
43
49
|
|
|
@@ -151,32 +157,10 @@ export interface SourceSpan {
|
|
|
151
157
|
doc?: string
|
|
152
158
|
}
|
|
153
159
|
|
|
154
|
-
export interface SchemaAnnotation {
|
|
155
|
-
/** anchor ref key, e.g. 'class.Monitor.property.status' */
|
|
156
|
-
target: string
|
|
157
|
-
severity: 'warn' | 'info'
|
|
158
|
-
code: 'COMPILE_ERROR' | 'EDGE_PROP_TYPE_MISSING'
|
|
159
|
-
message: string
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
export interface CrossDomainImport {
|
|
163
|
-
name: string
|
|
164
|
-
origin: string
|
|
165
|
-
ref: IrClassRef
|
|
166
|
-
}
|
|
167
|
-
|
|
168
160
|
export interface SchemaOverlay {
|
|
169
|
-
origin: string
|
|
170
|
-
/** Exact declared Domain dependencies. */
|
|
171
|
-
requires: string[]
|
|
172
|
-
/** Non-Kernel imported Classes. */
|
|
173
|
-
crossDomainImports: CrossDomainImport[]
|
|
174
|
-
/** Kernel base Classes rendered as inherited platform facts. */
|
|
175
|
-
mixins: CrossDomainImport[]
|
|
176
161
|
handlerLinks: HandlerLink[]
|
|
177
162
|
/** Keyed by Class, Property, Method, Function, Policy, View, or Core anchor. */
|
|
178
163
|
sourceSpans: Record<string, SourceSpan>
|
|
179
|
-
annotations: SchemaAnnotation[]
|
|
180
164
|
}
|
|
181
165
|
|
|
182
166
|
export interface BundleError {
|
|
@@ -388,4 +372,4 @@ export type TypeDescriptor =
|
|
|
388
372
|
required: string[]
|
|
389
373
|
optional: boolean
|
|
390
374
|
}
|
|
391
|
-
| { kind: 'ref'; target:
|
|
375
|
+
| { kind: 'ref'; target: 'node'; optional: boolean }
|
|
@@ -33,9 +33,9 @@ const TARGET_BARREL_EXPORTS = [
|
|
|
33
33
|
'ContextStore',
|
|
34
34
|
'ConversationInfo',
|
|
35
35
|
'CopyPayload',
|
|
36
|
-
'CrossDomainImport',
|
|
37
36
|
'DeployRecord',
|
|
38
37
|
'DeployResult',
|
|
38
|
+
'DOCUMENTS_DIR',
|
|
39
39
|
'DocMeta',
|
|
40
40
|
'DomainAnatomy',
|
|
41
41
|
'DomainCatalogEntry',
|
|
@@ -82,7 +82,6 @@ const TARGET_BARREL_EXPORTS = [
|
|
|
82
82
|
'MethodInheritance',
|
|
83
83
|
'NodePosition',
|
|
84
84
|
'RememberedViewTarget',
|
|
85
|
-
'SchemaAnnotation',
|
|
86
85
|
'SchemaChange',
|
|
87
86
|
'SchemaChangeKind',
|
|
88
87
|
'SchemaIR',
|
|
@@ -115,6 +114,7 @@ const TARGET_BARREL_HELPERS = [
|
|
|
115
114
|
'classRefKey',
|
|
116
115
|
'isIrClassRef',
|
|
117
116
|
'isIrSchemaRef',
|
|
117
|
+
'isNodePathSchema',
|
|
118
118
|
'isSchemaRevision',
|
|
119
119
|
'parseClassRefKey',
|
|
120
120
|
'parseSchemaRefKey',
|
|
@@ -202,6 +202,9 @@ export interface DocMeta {
|
|
|
202
202
|
stored: string
|
|
203
203
|
}
|
|
204
204
|
|
|
205
|
+
/** Where context documents live inside a domain — shown in the UI, read by the agent. */
|
|
206
|
+
export const DOCUMENTS_DIR = '.domain-studio/context/docs'
|
|
207
|
+
|
|
205
208
|
export interface Integration {
|
|
206
209
|
id: string
|
|
207
210
|
name: string
|
|
@@ -4,7 +4,6 @@
|
|
|
4
4
|
* These helpers validate and encode only the structural wire coordinate. SDK
|
|
5
5
|
* admission remains the authority for whether a reference is semantically valid.
|
|
6
6
|
*/
|
|
7
|
-
|
|
8
7
|
export const IR_SCHEMA_REF_KINDS = ['class', 'function', 'policy', 'view', 'core'] as const
|
|
9
8
|
|
|
10
9
|
export type IrSchemaRefKind = (typeof IR_SCHEMA_REF_KINDS)[number]
|