@astrale-os/sdk 0.5.0-beta.75 → 0.5.0-beta.76
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/CHANGELOG.md +7 -0
- package/dist/deployment/build/material.d.ts +3 -1
- package/dist/deployment/build/material.js +75 -16
- package/dist/platform/schema/state-property/material.d.ts +16 -0
- package/dist/platform/schema/state-property/material.js +118 -0
- package/dist/platform/schema/state-property/registry.d.ts +3 -0
- package/dist/platform/schema/state-property/registry.js +11 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.5.0-beta.76](https://github.com/astrale-os/sdk/compare/sdk-v0.5.0-beta.75...sdk-v0.5.0-beta.76) (2026-08-29)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Bug Fixes
|
|
7
|
+
|
|
8
|
+
* **build:** preserve state transition authority ([#328](https://github.com/astrale-os/sdk/issues/328)) ([5ae3301](https://github.com/astrale-os/sdk/commit/5ae3301c6b72cc1dc7a5bf901e143f3235cb53ab))
|
|
9
|
+
|
|
3
10
|
## [0.5.0-beta.75](https://github.com/astrale-os/sdk/compare/sdk-v0.5.0-beta.74...sdk-v0.5.0-beta.75) (2026-08-29)
|
|
4
11
|
|
|
5
12
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { DomainSchema } from '@astrale-os/kernel-dsl/v1/schema';
|
|
2
2
|
import type { Runtime } from '../../application/runtime/index.js';
|
|
3
|
+
import type { StatePropertyMaterial } from '../../platform/schema/state-property/material.js';
|
|
3
4
|
import type { Build } from './build.js';
|
|
4
5
|
import type { FrontendCompilation } from './frontend/index.js';
|
|
5
6
|
export interface BuildMaterial {
|
|
@@ -13,6 +14,7 @@ export interface BuildMaterial {
|
|
|
13
14
|
};
|
|
14
15
|
};
|
|
15
16
|
readonly callables: Build['callables'];
|
|
17
|
+
readonly stateProperties: readonly StatePropertyMaterial[];
|
|
16
18
|
readonly requirements: Build['requirements'];
|
|
17
19
|
readonly routes: Build['routes'];
|
|
18
20
|
readonly frontend?: FrontendCompilation;
|
|
@@ -30,7 +32,7 @@ export interface GeneratedBuildMaterial {
|
|
|
30
32
|
export declare function material(build: Build): BuildMaterial;
|
|
31
33
|
/** Produce opaque material for execution code from one admitted Build. */
|
|
32
34
|
export declare function generatedMaterial(build: Build): GeneratedBuildMaterial;
|
|
33
|
-
/** Re-admit
|
|
35
|
+
/** Re-admit detached provider-neutral Build material against the exact Runtime value. */
|
|
34
36
|
export declare function restoreBuild<RuntimeValue extends Runtime>(input: BuildMaterial, runtime: RuntimeValue): Build<DomainSchema, RuntimeValue>;
|
|
35
37
|
/**
|
|
36
38
|
* Bind material embedded by generated release code without repeating its external admission.
|
|
@@ -8,11 +8,18 @@ import { BUNDLE_MEDIA_TYPE } from '@astrale-os/kernel-protocol/publication/bundl
|
|
|
8
8
|
import { admitRequirements, validateRequirements, } from '@astrale-os/kernel-protocol/publication/requirements';
|
|
9
9
|
import * as routes from '@astrale-os/kernel-protocol/routes';
|
|
10
10
|
import { admitRuntimeRegistry, isRuntime } from '../../application/runtime/index.js';
|
|
11
|
+
import { restoreStatePropertyMaterial, statePropertyMaterial, } from '../../platform/schema/state-property/material.js';
|
|
11
12
|
import { admitBuild, isBuild } from './admission.js';
|
|
13
|
+
import { evidenceOf } from './admission.js';
|
|
14
|
+
const runtimeStateProperties = new WeakMap();
|
|
12
15
|
/** Detach the provider-neutral, runtime-free material embedded into generated execution code. */
|
|
13
16
|
export function material(build) {
|
|
14
17
|
if (!isBuild(build))
|
|
15
18
|
throw new TypeError('Build material requires an admitted Build.');
|
|
19
|
+
if (!isRuntime(build.runtime))
|
|
20
|
+
throw new TypeError('Build Runtime is not admitted.');
|
|
21
|
+
const stateProperties = statePropertyMaterial(evidenceOf(build).domain);
|
|
22
|
+
bindRuntimeStateProperties(build.runtime, build.schema.bundle.ref.digest, stateProperties);
|
|
16
23
|
return Object.freeze({
|
|
17
24
|
format: 'astrale.sdk.build-material',
|
|
18
25
|
version: 1,
|
|
@@ -24,6 +31,7 @@ export function material(build) {
|
|
|
24
31
|
}),
|
|
25
32
|
}),
|
|
26
33
|
callables: build.callables,
|
|
34
|
+
stateProperties,
|
|
27
35
|
requirements: build.requirements,
|
|
28
36
|
routes: build.routes,
|
|
29
37
|
...(build.frontend === undefined ? {} : { frontend: build.frontend }),
|
|
@@ -39,7 +47,7 @@ export function generatedMaterial(build) {
|
|
|
39
47
|
digest: digestOf(value),
|
|
40
48
|
});
|
|
41
49
|
}
|
|
42
|
-
/** Re-admit
|
|
50
|
+
/** Re-admit detached provider-neutral Build material against the exact Runtime value. */
|
|
43
51
|
export function restoreBuild(input, runtime) {
|
|
44
52
|
assertMaterialEnvelope(input, runtime);
|
|
45
53
|
const domain = loadCompiledSchema(input.schema.compiled);
|
|
@@ -53,6 +61,9 @@ export function restoreBuild(input, runtime) {
|
|
|
53
61
|
revision(decoded.root) !== input.schema.compiled.root.revision) {
|
|
54
62
|
invalid();
|
|
55
63
|
}
|
|
64
|
+
const stateProperties = snapshotStateProperties(input.stateProperties);
|
|
65
|
+
assertRuntimeStateProperties(runtime, input.schema.bundle.ref.digest, stateProperties);
|
|
66
|
+
restoreStatePropertyMaterial(domain, stateProperties);
|
|
56
67
|
const registry = bindRuntime(domain, runtime, input.callables);
|
|
57
68
|
const requirements = admitRequirements(input.requirements);
|
|
58
69
|
validateRequirements(decoded.root, decoded.closure, requirements);
|
|
@@ -86,38 +97,48 @@ export function restoreBuild(input, runtime) {
|
|
|
86
97
|
*/
|
|
87
98
|
export function restoreGeneratedBuild(input, runtime) {
|
|
88
99
|
const generated = acceptGeneratedMaterial(input, runtime);
|
|
89
|
-
const
|
|
90
|
-
const
|
|
91
|
-
const
|
|
100
|
+
const material = generated.material;
|
|
101
|
+
const domain = loadGeneratedSchema(material.schema.compiled);
|
|
102
|
+
const stateProperties = snapshotStateProperties(material.stateProperties);
|
|
103
|
+
restoreStatePropertyMaterial(domain, stateProperties);
|
|
104
|
+
bindRuntimeStateProperties(runtime, material.schema.bundle.ref.digest, stateProperties);
|
|
105
|
+
const registry = bindRuntime(domain, runtime, material.callables);
|
|
106
|
+
const detached = Uint8Array.from(material.schema.bundle.bytes);
|
|
92
107
|
const build = Object.freeze({
|
|
93
108
|
kind: 'build',
|
|
94
109
|
schema: Object.freeze({
|
|
95
|
-
compiled:
|
|
110
|
+
compiled: material.schema.compiled,
|
|
96
111
|
bundle: Object.freeze({
|
|
97
|
-
ref:
|
|
112
|
+
ref: material.schema.bundle.ref,
|
|
98
113
|
bytes: () => new Uint8Array(detached),
|
|
99
114
|
}),
|
|
100
115
|
}),
|
|
101
116
|
runtime,
|
|
102
|
-
callables: Object.freeze([...
|
|
103
|
-
requirements:
|
|
104
|
-
routes:
|
|
105
|
-
...(
|
|
117
|
+
callables: Object.freeze([...material.callables]),
|
|
118
|
+
requirements: material.requirements,
|
|
119
|
+
routes: material.routes,
|
|
120
|
+
...(material.frontend === undefined ? {} : { frontend: material.frontend }),
|
|
106
121
|
});
|
|
107
122
|
return admitBuild(build, { domain, registry });
|
|
108
123
|
}
|
|
109
124
|
function acceptGeneratedMaterial(input, runtime) {
|
|
125
|
+
const format = input?.format;
|
|
126
|
+
const version = input?.version;
|
|
127
|
+
const material = input?.material;
|
|
128
|
+
const digest = input?.digest;
|
|
110
129
|
if (input === null ||
|
|
111
130
|
typeof input !== 'object' ||
|
|
112
131
|
Array.isArray(input) ||
|
|
113
|
-
|
|
114
|
-
|
|
132
|
+
format !== 'astrale.sdk.generated-build-material' ||
|
|
133
|
+
version !== 1 ||
|
|
115
134
|
Reflect.ownKeys(input).some((key) => key !== 'format' && key !== 'version' && key !== 'material' && key !== 'digest') ||
|
|
116
|
-
digestOf(
|
|
135
|
+
digestOf(material) !== digest) {
|
|
117
136
|
invalid();
|
|
118
137
|
}
|
|
119
|
-
assertMaterialEnvelope(
|
|
120
|
-
|
|
138
|
+
assertMaterialEnvelope(material, runtime);
|
|
139
|
+
const accepted = deepFreeze({ material });
|
|
140
|
+
deepFreeze(input);
|
|
141
|
+
return accepted;
|
|
121
142
|
}
|
|
122
143
|
function digestOf(input) {
|
|
123
144
|
try {
|
|
@@ -127,9 +148,46 @@ function digestOf(input) {
|
|
|
127
148
|
invalid();
|
|
128
149
|
}
|
|
129
150
|
}
|
|
151
|
+
function bindRuntimeStateProperties(runtime, bundleDigest, stateProperties) {
|
|
152
|
+
const digest = statePropertyDigest(stateProperties);
|
|
153
|
+
const bindings = runtimeStateProperties.get(runtime) ?? new Map();
|
|
154
|
+
const existing = bindings.get(bundleDigest);
|
|
155
|
+
if (existing !== undefined && existing !== digest) {
|
|
156
|
+
throw new TypeError('Runtime has conflicting StateProperty authority for Build material.');
|
|
157
|
+
}
|
|
158
|
+
bindings.set(bundleDigest, digest);
|
|
159
|
+
runtimeStateProperties.set(runtime, bindings);
|
|
160
|
+
}
|
|
161
|
+
function assertRuntimeStateProperties(runtime, bundleDigest, stateProperties) {
|
|
162
|
+
const expected = runtimeStateProperties.get(runtime)?.get(bundleDigest);
|
|
163
|
+
if (expected === undefined) {
|
|
164
|
+
if (stateProperties.length === 0)
|
|
165
|
+
return;
|
|
166
|
+
throw new TypeError('Build StateProperty authority is not bound to its exact Runtime.');
|
|
167
|
+
}
|
|
168
|
+
if (statePropertyDigest(stateProperties) !== expected) {
|
|
169
|
+
throw new TypeError('Build StateProperty authority differs from its exact Runtime.');
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
function statePropertyDigest(input) {
|
|
173
|
+
try {
|
|
174
|
+
return artifact.identify(new TextEncoder().encode(JSON.stringify(input)), 'application/vnd.astrale.sdk.state-properties+json').digest;
|
|
175
|
+
}
|
|
176
|
+
catch {
|
|
177
|
+
invalid();
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
function snapshotStateProperties(input) {
|
|
181
|
+
try {
|
|
182
|
+
return deepFreeze(JSON.parse(JSON.stringify(input)));
|
|
183
|
+
}
|
|
184
|
+
catch {
|
|
185
|
+
invalid();
|
|
186
|
+
}
|
|
187
|
+
}
|
|
130
188
|
function assertMaterialEnvelope(input, runtime) {
|
|
131
189
|
if (!isRuntime(runtime))
|
|
132
|
-
throw new TypeError('
|
|
190
|
+
throw new TypeError('Build Runtime is not admitted.');
|
|
133
191
|
if (input === null ||
|
|
134
192
|
typeof input !== 'object' ||
|
|
135
193
|
Array.isArray(input) ||
|
|
@@ -139,6 +197,7 @@ function assertMaterialEnvelope(input, runtime) {
|
|
|
139
197
|
key !== 'version' &&
|
|
140
198
|
key !== 'schema' &&
|
|
141
199
|
key !== 'callables' &&
|
|
200
|
+
key !== 'stateProperties' &&
|
|
142
201
|
key !== 'requirements' &&
|
|
143
202
|
key !== 'routes' &&
|
|
144
203
|
key !== 'frontend')) {
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { Domain } from '@astrale-os/kernel-dsl/v1';
|
|
2
|
+
import type { PropertyKey } from '@astrale-os/kernel-dsl/v1/addressing';
|
|
3
|
+
export interface StatePropertyMaterial {
|
|
4
|
+
readonly property: PropertyKey;
|
|
5
|
+
readonly initial: string;
|
|
6
|
+
readonly states: readonly string[];
|
|
7
|
+
readonly transitions: readonly Readonly<{
|
|
8
|
+
from: string;
|
|
9
|
+
event: string;
|
|
10
|
+
to: string;
|
|
11
|
+
}>[];
|
|
12
|
+
}
|
|
13
|
+
/** Capture only the portable StateProperty authority required by generated execution. */
|
|
14
|
+
export declare function statePropertyMaterial(domain: Domain): readonly StatePropertyMaterial[];
|
|
15
|
+
/** Restore portable StateProperty authority onto an exact compiled Domain. */
|
|
16
|
+
export declare function restoreStatePropertyMaterial(domain: Domain, input: unknown): void;
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { stateMachine } from '../../../application/state/define.js';
|
|
2
|
+
import { rememberResolvedStateProperties, stateMachineFor } from './registry.js';
|
|
3
|
+
/** Capture only the portable StateProperty authority required by generated execution. */
|
|
4
|
+
export function statePropertyMaterial(domain) {
|
|
5
|
+
const material = [];
|
|
6
|
+
for (const property of ownProperties(domain)) {
|
|
7
|
+
const machine = stateMachineFor(domain, property);
|
|
8
|
+
if (machine === undefined)
|
|
9
|
+
continue;
|
|
10
|
+
material.push(Object.freeze({
|
|
11
|
+
property: property.key,
|
|
12
|
+
initial: machine.initial,
|
|
13
|
+
states: Object.freeze([...machine.states]),
|
|
14
|
+
transitions: Object.freeze(machine.transitions.map((transition) => Object.freeze({ ...transition }))),
|
|
15
|
+
}));
|
|
16
|
+
}
|
|
17
|
+
return Object.freeze(material.sort((left, right) => compareText(left.property, right.property)));
|
|
18
|
+
}
|
|
19
|
+
/** Restore portable StateProperty authority onto an exact compiled Domain. */
|
|
20
|
+
export function restoreStatePropertyMaterial(domain, input) {
|
|
21
|
+
if (!Array.isArray(input))
|
|
22
|
+
invalid();
|
|
23
|
+
const byKey = new Map();
|
|
24
|
+
const properties = new Map(ownProperties(domain).map((property) => [property.key, property]));
|
|
25
|
+
for (const candidate of input) {
|
|
26
|
+
if (!isRecord(candidate) ||
|
|
27
|
+
!exactKeys(candidate, ['property', 'initial', 'states', 'transitions'])) {
|
|
28
|
+
invalid();
|
|
29
|
+
}
|
|
30
|
+
const propertyKey = candidate.property;
|
|
31
|
+
const initial = candidate.initial;
|
|
32
|
+
const states = candidate.states;
|
|
33
|
+
const transitions = candidate.transitions;
|
|
34
|
+
if (typeof propertyKey !== 'string' ||
|
|
35
|
+
typeof initial !== 'string' ||
|
|
36
|
+
!Array.isArray(states) ||
|
|
37
|
+
states.some((state) => typeof state !== 'string') ||
|
|
38
|
+
!Array.isArray(transitions)) {
|
|
39
|
+
invalid();
|
|
40
|
+
}
|
|
41
|
+
const property = properties.get(propertyKey);
|
|
42
|
+
if (property === undefined || byKey.has(property.key))
|
|
43
|
+
invalid();
|
|
44
|
+
assertStateProperty(property, states);
|
|
45
|
+
const relation = Object.create(null);
|
|
46
|
+
for (const state of states)
|
|
47
|
+
relation[state] = Object.create(null);
|
|
48
|
+
for (const transition of transitions) {
|
|
49
|
+
if (!isRecord(transition) || !exactKeys(transition, ['from', 'event', 'to']))
|
|
50
|
+
invalid();
|
|
51
|
+
const { from, event, to } = transition;
|
|
52
|
+
if (typeof from !== 'string' ||
|
|
53
|
+
typeof event !== 'string' ||
|
|
54
|
+
typeof to !== 'string' ||
|
|
55
|
+
relation[from] === undefined ||
|
|
56
|
+
Object.hasOwn(relation[from], event)) {
|
|
57
|
+
invalid();
|
|
58
|
+
}
|
|
59
|
+
relation[from][event] = to;
|
|
60
|
+
}
|
|
61
|
+
const machine = Reflect.apply(stateMachine, undefined, [
|
|
62
|
+
{ initial, transitions: relation },
|
|
63
|
+
]);
|
|
64
|
+
if (!sameStrings(machine.states, states))
|
|
65
|
+
invalid();
|
|
66
|
+
byKey.set(property.key, machine);
|
|
67
|
+
}
|
|
68
|
+
rememberResolvedStateProperties(domain, byKey);
|
|
69
|
+
}
|
|
70
|
+
function ownProperties(domain) {
|
|
71
|
+
const properties = [];
|
|
72
|
+
const seen = new Set();
|
|
73
|
+
const visit = (selected) => {
|
|
74
|
+
if (seen.has(selected))
|
|
75
|
+
return;
|
|
76
|
+
seen.add(selected);
|
|
77
|
+
for (const definition of selected.definitions('class')) {
|
|
78
|
+
properties.push(...definition.self.properties);
|
|
79
|
+
}
|
|
80
|
+
for (const dependency of Object.values(selected.dependencies))
|
|
81
|
+
visit(dependency);
|
|
82
|
+
};
|
|
83
|
+
visit(domain);
|
|
84
|
+
return properties;
|
|
85
|
+
}
|
|
86
|
+
function assertStateProperty(property, states) {
|
|
87
|
+
const schema = property.definition.schema;
|
|
88
|
+
if (property.owner.kind !== 'node' ||
|
|
89
|
+
property.definition.required !== true ||
|
|
90
|
+
!isRecord(schema) ||
|
|
91
|
+
schema.type !== 'string' ||
|
|
92
|
+
!Array.isArray(schema.enum) ||
|
|
93
|
+
schema.enum.some((state) => typeof state !== 'string') ||
|
|
94
|
+
!sameStringSet(schema.enum, states)) {
|
|
95
|
+
invalid();
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
function sameStrings(left, right) {
|
|
99
|
+
return left.length === right.length && left.every((value, index) => value === right[index]);
|
|
100
|
+
}
|
|
101
|
+
function sameStringSet(left, right) {
|
|
102
|
+
if (new Set(left).size !== left.length || new Set(right).size !== right.length)
|
|
103
|
+
return false;
|
|
104
|
+
return sameStrings([...left].sort(compareText), [...right].sort(compareText));
|
|
105
|
+
}
|
|
106
|
+
function isRecord(input) {
|
|
107
|
+
return input !== null && typeof input === 'object' && !Array.isArray(input);
|
|
108
|
+
}
|
|
109
|
+
function exactKeys(input, expected) {
|
|
110
|
+
const keys = Reflect.ownKeys(input);
|
|
111
|
+
return keys.length === expected.length && keys.every((key) => expected.includes(String(key)));
|
|
112
|
+
}
|
|
113
|
+
function compareText(left, right) {
|
|
114
|
+
return left < right ? -1 : left > right ? 1 : 0;
|
|
115
|
+
}
|
|
116
|
+
function invalid() {
|
|
117
|
+
throw new TypeError('StateProperty material is invalid.');
|
|
118
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Domain, ResolvedProperty } from '@astrale-os/kernel-dsl/v1';
|
|
2
|
+
import type { PropertyKey } from '@astrale-os/kernel-dsl/v1/addressing';
|
|
2
3
|
import type { Property, SchemaInput as DomainSchemaInput } from '@astrale-os/kernel-dsl/v1/builder';
|
|
3
4
|
import type * as languageSchema from '@astrale-os/kernel-dsl/v1/schema';
|
|
4
5
|
import type { StateMachine } from '../../../application/state/machine.js';
|
|
@@ -7,3 +8,5 @@ export declare function isStateProperty(value: unknown): boolean;
|
|
|
7
8
|
export declare function rememberStateClass(selected: object, properties?: Readonly<Record<string, unknown>>): void;
|
|
8
9
|
export declare function rememberStateSchema(source: languageSchema.DomainSchema, input: DomainSchemaInput, domain: Domain): void;
|
|
9
10
|
export declare function stateMachineFor(domain: Domain, selected: ResolvedProperty<unknown>): StateMachine | undefined;
|
|
11
|
+
/** Internal restoration seam for generated execution material. */
|
|
12
|
+
export declare function rememberResolvedStateProperties(domain: Domain, byKey: ReadonlyMap<PropertyKey, StateMachine>): void;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
const propertyMachines = new WeakMap();
|
|
2
2
|
const classProperties = new WeakMap();
|
|
3
3
|
const schemaProperties = new WeakMap();
|
|
4
|
+
const domainProperties = new WeakMap();
|
|
4
5
|
export function rememberStateProperty(property, machine) {
|
|
5
6
|
propertyMachines.set(property, machine);
|
|
6
7
|
}
|
|
@@ -43,13 +44,23 @@ export function rememberStateSchema(source, input, domain) {
|
|
|
43
44
|
const byProperty = new WeakMap();
|
|
44
45
|
bindDomainProperties(domain, byKey, byProperty, new Set());
|
|
45
46
|
schemaProperties.set(source, { byKey, byProperty });
|
|
47
|
+
domainProperties.set(domain, byProperty);
|
|
46
48
|
}
|
|
47
49
|
export function stateMachineFor(domain, selected) {
|
|
50
|
+
const retained = domainProperties.get(domain)?.get(selected);
|
|
51
|
+
if (retained !== undefined)
|
|
52
|
+
return retained;
|
|
48
53
|
const source = Reflect.get(domain, 'source');
|
|
49
54
|
if (typeof source !== 'object' || source === null)
|
|
50
55
|
return undefined;
|
|
51
56
|
return schemaProperties.get(source)?.byProperty.get(selected);
|
|
52
57
|
}
|
|
58
|
+
/** Internal restoration seam for generated execution material. */
|
|
59
|
+
export function rememberResolvedStateProperties(domain, byKey) {
|
|
60
|
+
const byProperty = new WeakMap();
|
|
61
|
+
bindDomainProperties(domain, byKey, byProperty, new Set());
|
|
62
|
+
domainProperties.set(domain, byProperty);
|
|
63
|
+
}
|
|
53
64
|
function bindKey(byKey, key, machine) {
|
|
54
65
|
const existing = byKey.get(key);
|
|
55
66
|
if (existing !== undefined && existing !== machine) {
|