@astrale-os/sdk 0.5.0-beta.66 → 0.5.0-beta.67

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 (29) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/application/mutation/authoring/builder.d.ts +6 -21
  3. package/dist/application/mutation/authoring/builder.js +4 -65
  4. package/dist/application/mutation/authoring/transition.d.ts +17 -0
  5. package/dist/application/mutation/authoring/transition.js +47 -0
  6. package/dist/application/mutation/define.d.ts +3 -1
  7. package/dist/application/mutation/execution/execute.js +1 -1
  8. package/dist/application/mutation/failure.d.ts +10 -2
  9. package/dist/application/mutation/failure.js +24 -1
  10. package/dist/application/state/authority.d.ts +3 -0
  11. package/dist/application/state/authority.js +7 -0
  12. package/dist/application/state/define.js +4 -1
  13. package/dist/platform/schema/index.d.ts +2 -0
  14. package/dist/platform/schema/index.js +1 -0
  15. package/dist/platform/schema/state-property/authoring.d.ts +15 -0
  16. package/dist/platform/schema/state-property/authoring.js +49 -0
  17. package/dist/platform/schema/state-property/edge.d.ts +16 -0
  18. package/dist/platform/schema/state-property/edge.js +21 -0
  19. package/dist/platform/schema/state-property/index.d.ts +4 -0
  20. package/dist/platform/schema/state-property/index.js +3 -0
  21. package/dist/platform/schema/state-property/model.d.ts +49 -0
  22. package/dist/platform/schema/state-property/model.js +1 -0
  23. package/dist/platform/schema/state-property/registry.d.ts +9 -0
  24. package/dist/platform/schema/state-property/registry.js +74 -0
  25. package/dist/tooling/linter/implementations/source/mutations.js +19 -30
  26. package/dist/tooling/linter/implementations/source/schema.js +89 -6
  27. package/dist/tooling/linter/implementations/source/state-machine.d.ts +4 -0
  28. package/dist/tooling/linter/implementations/source/state-machine.js +62 -0
  29. package/package.json +2 -2
package/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.5.0-beta.67](https://github.com/astrale-os/sdk/compare/sdk-v0.5.0-beta.66...sdk-v0.5.0-beta.67) (2026-08-28)
4
+
5
+
6
+ ### Features
7
+
8
+ * make StateProperty transitions intrinsic ([#306](https://github.com/astrale-os/sdk/issues/306)) ([a2b2575](https://github.com/astrale-os/sdk/commit/a2b2575cf19c585c26539839c4777c0c2d413275))
9
+
10
+
11
+ ### Documentation
12
+
13
+ * **state:** ratify intrinsic property transitions ([#303](https://github.com/astrale-os/sdk/issues/303)) ([d484e58](https://github.com/astrale-os/sdk/commit/d484e58bc47f80c2571886b5c85d91d91da52b25))
14
+
3
15
  ## [0.5.0-beta.66](https://github.com/astrale-os/sdk/compare/sdk-v0.5.0-beta.65...sdk-v0.5.0-beta.66) (2026-08-27)
4
16
 
5
17
 
@@ -1,11 +1,12 @@
1
1
  import type { EdgeSlug } from '@astrale-os/kernel-core/graph/edge';
2
2
  import type { MutationBuilder } from '@astrale-os/kernel-core/graph/mutate';
3
3
  import type { QueryEdgeAST, QueryNodeAST } from '@astrale-os/kernel-core/graph/query';
4
- import type { ResolvedClass } from '../../../platform/schema/index.js';
5
- import type { StateMachine, StateOf, TransitionOf } from '../../state/machine.js';
4
+ import type { Domain, ResolvedClass } from '../../../platform/schema/index.js';
5
+ import type { EventOf, StateOf } from '../../state/machine.js';
6
6
  import type { CreatedNode, ExistingEndpoint, MutationEndpoint } from './endpoint.js';
7
7
  import type { QueryPrecondition } from './precondition.js';
8
- import type { RichPropertyConditions, RichPropertyDelta, RichPropertyInput, RichPropertyName } from './properties.js';
8
+ import type { RichPropertyConditions, RichPropertyDelta, RichPropertyInput } from './properties.js';
9
+ import { type StateMachineOf, type StatePropertyName, type StateTransitionInput } from './transition.js';
9
10
  export interface RichMutationBuilder {
10
11
  readonly expect: {
11
12
  node<Class extends ResolvedClass<'node'>>(input: {
@@ -29,14 +30,7 @@ export interface RichMutationBuilder {
29
30
  readonly class: Class;
30
31
  readonly props: RichPropertyDelta<Class>;
31
32
  }): this;
32
- transition<Machine extends StateMachine, Class extends ResolvedClass<'node'>, Property extends StateMachinePropertyName<Class, Machine>>(input: {
33
- readonly machine: Machine;
34
- readonly node: ExistingEndpoint;
35
- readonly class: Class;
36
- readonly property: Property;
37
- readonly decision: AllowedTransitionDecision<Machine>;
38
- readonly props?: RichPropertyConditions<Class>;
39
- }): this;
33
+ transition<Class extends ResolvedClass<'node'>, Property extends StatePropertyName<Class>, Machine extends StateMachineOf<Class, Property>, From extends StateOf<Machine>, Event extends EventOf<Machine, From>>(input: StateTransitionInput<Class, Property, Machine, From, Event>): this;
40
34
  deleteNode<Class extends ResolvedClass<'node'>>(input: {
41
35
  readonly node: ExistingEndpoint;
42
36
  readonly class: Class;
@@ -59,13 +53,4 @@ export interface EdgeSelector<Class extends ResolvedClass<'edge'>> {
59
53
  readonly target: ExistingEndpoint;
60
54
  readonly class: Class;
61
55
  }
62
- export declare function richMutationBuilder(core: MutationBuilder): RichMutationBuilder;
63
- type StateMachinePropertyName<Class extends ResolvedClass<'node'>, Machine extends StateMachine> = {
64
- [Name in RichPropertyName<Class>]: string extends StateOf<Machine> ? never : IsExact<Exclude<RichPropertyInput<Class>[Name], undefined>, StateOf<Machine>> extends true ? Name : never;
65
- }[RichPropertyName<Class>];
66
- type AllowedTransitionDecision<Machine extends StateMachine> = Readonly<{
67
- kind: 'allowed';
68
- transition: TransitionOf<Machine>;
69
- }>;
70
- type IsExact<Left, Right> = [Left] extends [Right] ? ([Right] extends [Left] ? true : false) : false;
71
- export {};
56
+ export declare function richMutationBuilder(core: MutationBuilder, domain: Domain): RichMutationBuilder;
@@ -2,8 +2,9 @@ import { LocalBinding } from '@astrale-os/kernel-core/graph/graph';
2
2
  import { ClassKey } from '@astrale-os/kernel-dsl/v1/addressing';
3
3
  import { endpoint, existingEndpoint } from './endpoint.js';
4
4
  import { precondition } from './precondition.js';
5
- import { conditions, delta, properties, resolvedProperty } from './properties.js';
6
- export function richMutationBuilder(core) {
5
+ import { conditions, delta, properties } from './properties.js';
6
+ import { authorStateTransition, } from './transition.js';
7
+ export function richMutationBuilder(core, domain) {
7
8
  const expectation = Object.freeze({
8
9
  node(input) {
9
10
  core.expect.node({
@@ -47,66 +48,7 @@ export function richMutationBuilder(core) {
47
48
  return rich;
48
49
  },
49
50
  transition(input) {
50
- const submitted = input.decision;
51
- const kind = submitted.kind;
52
- const rawTransition = submitted.transition;
53
- if (kind !== 'allowed' ||
54
- rawTransition === undefined ||
55
- rawTransition === null ||
56
- typeof rawTransition !== 'object') {
57
- throw new TypeError('StateMachine transition decision must be allowed.');
58
- }
59
- const fromState = rawTransition.from;
60
- const event = rawTransition.event;
61
- const toState = rawTransition.to;
62
- if (typeof fromState !== 'string' ||
63
- typeof event !== 'string' ||
64
- typeof toState !== 'string') {
65
- throw new TypeError('StateMachine transition decision must be allowed.');
66
- }
67
- const transition = Object.freeze({
68
- from: fromState,
69
- event: event,
70
- to: toState,
71
- });
72
- let decision;
73
- try {
74
- decision = input.machine.decide(transition.from, transition.event);
75
- }
76
- catch {
77
- invalidTransition(transition);
78
- }
79
- if (decision.kind !== 'allowed' || decision.transition.to !== transition.to) {
80
- invalidTransition(transition);
81
- }
82
- const selected = resolvedProperty(input.class, input.property);
83
- for (const state of [transition.from, transition.to]) {
84
- if (selected.validate(state).length > 0) {
85
- throw new TypeError(`StateMachine-backed property ${String(input.property)} does not admit state ${state}.`);
86
- }
87
- }
88
- const from = { [input.property]: transition.from };
89
- const to = { [input.property]: transition.to };
90
- const suppliedEquals = input.props?.equals;
91
- if ((suppliedEquals !== undefined &&
92
- Object.hasOwn(suppliedEquals, input.property) &&
93
- suppliedEquals[input.property] !== transition.from) ||
94
- input.props?.absent?.includes(input.property) === true) {
95
- throw new TypeError(`StateMachine transition conditions conflict with property ${String(input.property)}.`);
96
- }
97
- core.expect.node({
98
- node: existingEndpoint(input.node),
99
- class: classKey(input.class),
100
- props: conditions(input.class, {
101
- ...input.props,
102
- equals: { ...suppliedEquals, ...from },
103
- }),
104
- });
105
- core.updateNode({
106
- node: existingEndpoint(input.node),
107
- class: classKey(input.class),
108
- props: delta(input.class, { set: to }),
109
- });
51
+ authorStateTransition(core, domain, input);
110
52
  return rich;
111
53
  },
112
54
  deleteNode(input) {
@@ -158,6 +100,3 @@ export function richMutationBuilder(core) {
158
100
  function classKey(selected) {
159
101
  return ClassKey(selected.key);
160
102
  }
161
- function invalidTransition(transition) {
162
- throw new TypeError(`StateMachine transition decision is not legal: ${transition.from} + ${transition.event} -> ${transition.to}.`);
163
- }
@@ -0,0 +1,17 @@
1
+ import type { MutationBuilder } from '@astrale-os/kernel-core/graph/mutate';
2
+ import type { Domain, ResolvedClass } from '../../../platform/schema/index.js';
3
+ import type { StatePropertiesOfClass } from '../../../platform/schema/state-property/index.js';
4
+ import type { EventOf, StateMachine, StateOf } from '../../state/machine.js';
5
+ import type { ExistingEndpoint } from './endpoint.js';
6
+ import type { RichPropertyConditions, RichPropertyName } from './properties.js';
7
+ export type StatePropertyName<Class extends ResolvedClass<'node'>> = Extract<keyof StatePropertiesOfClass<Class>, RichPropertyName<Class>>;
8
+ export type StateMachineOf<Class extends ResolvedClass<'node'>, Property extends StatePropertyName<Class>> = Extract<StatePropertiesOfClass<Class>[Property], StateMachine>;
9
+ export interface StateTransitionInput<Class extends ResolvedClass<'node'>, Property extends StatePropertyName<Class>, Machine extends StateMachineOf<Class, Property>, From extends StateOf<Machine>, Event extends EventOf<Machine, From>> {
10
+ readonly node: ExistingEndpoint;
11
+ readonly class: Class;
12
+ readonly property: Property;
13
+ readonly from: From;
14
+ readonly event: Event;
15
+ readonly props?: RichPropertyConditions<Class>;
16
+ }
17
+ export declare function authorStateTransition<Class extends ResolvedClass<'node'>, Property extends StatePropertyName<Class>, Machine extends StateMachineOf<Class, Property>, From extends StateOf<Machine>, Event extends EventOf<Machine, From>>(core: MutationBuilder, domain: Domain, input: StateTransitionInput<Class, Property, Machine, From, Event>): void;
@@ -0,0 +1,47 @@
1
+ import { ClassKey } from '@astrale-os/kernel-dsl/v1/addressing';
2
+ import { stateMachineFor } from '../../../platform/schema/state-property/registry.js';
3
+ import { stateTransitionFailure } from '../failure.js';
4
+ import { existingEndpoint } from './endpoint.js';
5
+ import { conditions, delta, resolvedProperty } from './properties.js';
6
+ export function authorStateTransition(core, domain, input) {
7
+ const node = existingEndpoint(input.node);
8
+ const selectedClass = input.class;
9
+ const property = input.property;
10
+ const fromState = input.from;
11
+ const event = input.event;
12
+ const suppliedProps = input.props;
13
+ const equals = suppliedProps?.equals;
14
+ const absent = suppliedProps?.absent;
15
+ const suppliedEquals = equals === undefined ? undefined : { ...equals };
16
+ const suppliedAbsent = absent === undefined ? undefined : [...absent];
17
+ const selected = resolvedProperty(selectedClass, property);
18
+ const selectedClassKey = ClassKey(selectedClass.key);
19
+ const machine = stateMachineFor(domain, selected);
20
+ if (machine === undefined) {
21
+ throw new TypeError(`State transition requires StateProperty ${selectedClassKey}.${String(property)} in the selected Domain.`);
22
+ }
23
+ const decision = machine.decide(fromState, event);
24
+ if (decision.kind === 'rejected')
25
+ throw stateTransitionFailure(decision.from, decision.event);
26
+ const from = { [property]: fromState };
27
+ const to = { [property]: decision.transition.to };
28
+ if ((suppliedEquals !== undefined &&
29
+ Object.hasOwn(suppliedEquals, property) &&
30
+ suppliedEquals[property] !== fromState) ||
31
+ suppliedAbsent?.includes(property) === true) {
32
+ throw new TypeError(`StateMachine transition conditions conflict with property ${String(property)}.`);
33
+ }
34
+ core.expect.node({
35
+ node,
36
+ class: selectedClassKey,
37
+ props: conditions(selectedClass, {
38
+ ...(suppliedAbsent === undefined ? {} : { absent: suppliedAbsent }),
39
+ equals: { ...suppliedEquals, ...from },
40
+ }),
41
+ });
42
+ core.updateNode({
43
+ node,
44
+ class: selectedClassKey,
45
+ props: delta(selectedClass, { set: to }),
46
+ });
47
+ }
@@ -1,9 +1,11 @@
1
1
  import type { MutationResult } from '@astrale-os/kernel-core/graph/mutate';
2
2
  import type { Domain, schema } from '../../platform/schema/index.js';
3
+ import type { StatefulDomainOf } from '../../platform/schema/state-property/index.js';
3
4
  import type { Mutation, MutationDefinition } from './mutation.js';
4
5
  type ResolvedDomainOf<Schema extends schema.DomainSchema> = Extract<schema.DomainOf<Schema>, Domain>;
6
+ type AuthoringDomainOf<Schema extends schema.DomainSchema> = StatefulDomainOf<Schema>;
5
7
  interface MutationProjector<Schema extends schema.DomainSchema> {
6
- <Input, Output = MutationResult>(project: (domain: ResolvedDomainOf<Schema>) => MutationDefinition<Input, Output>): Mutation<ResolvedDomainOf<Schema>, Input, Output>;
8
+ <Input, Output = MutationResult>(project: (domain: AuthoringDomainOf<Schema>) => MutationDefinition<Input, Output>): Mutation<ResolvedDomainOf<Schema>, Input, Output>;
7
9
  }
8
10
  /** Define one inert Mutation recipe projected from its exact loaded Domain. */
9
11
  export declare function defineMutation<Schema extends schema.DomainSchema>(): MutationProjector<Schema>;
@@ -9,7 +9,7 @@ export async function executeMutation(client, domain, definition, input) {
9
9
  }
10
10
  const operation = realizeMutation(definition, domain);
11
11
  try {
12
- const ast = MutationAST.build((builder) => operation.build(input, richMutationBuilder(builder)));
12
+ const ast = MutationAST.build((builder) => operation.build(input, richMutationBuilder(builder, domain)));
13
13
  return projectMutation(operation, await client.mutate(ast, { expected: domain.closure }), input);
14
14
  }
15
15
  catch (cause) {
@@ -1,5 +1,13 @@
1
- export interface MutationFailure extends Error {
2
- readonly code: string;
1
+ export interface KernelMutationFailure extends Error {
2
+ readonly code: `MUTATION_${string}`;
3
3
  readonly outcomeKnown: boolean;
4
4
  }
5
+ export interface StateTransitionFailure extends Error {
6
+ readonly code: 'STATE_TRANSITION_ILLEGAL';
7
+ readonly outcomeKnown: true;
8
+ readonly from: string;
9
+ readonly event: string;
10
+ }
11
+ export type MutationFailure = KernelMutationFailure | StateTransitionFailure;
12
+ export declare function stateTransitionFailure(from: string, event: string): StateTransitionFailure;
5
13
  export declare function mutationFailure(input: unknown): MutationFailure | undefined;
@@ -1,4 +1,15 @@
1
1
  const UNKNOWN_OUTCOME_CODES = new Set(['MUTATION_OUTCOME_UNKNOWN']);
2
+ const localStateTransitionFailures = new WeakSet();
3
+ export function stateTransitionFailure(from, event) {
4
+ const failure = Object.assign(new Error(`State transition is illegal: ${from} + ${event}.`), {
5
+ code: 'STATE_TRANSITION_ILLEGAL',
6
+ outcomeKnown: true,
7
+ from,
8
+ event,
9
+ });
10
+ localStateTransitionFailures.add(failure);
11
+ return failure;
12
+ }
2
13
  export function mutationFailure(input) {
3
14
  if (!(input instanceof Error))
4
15
  return undefined;
@@ -8,6 +19,15 @@ export function mutationFailure(input) {
8
19
  const explicit = input.outcomeKnown;
9
20
  if (explicit !== undefined && typeof explicit !== 'boolean')
10
21
  return undefined;
22
+ if (code === 'STATE_TRANSITION_ILLEGAL') {
23
+ if (!localStateTransitionFailures.has(input))
24
+ return undefined;
25
+ const from = Reflect.get(input, 'from');
26
+ const event = Reflect.get(input, 'event');
27
+ if (typeof from !== 'string' || typeof event !== 'string' || explicit === false)
28
+ return undefined;
29
+ return Object.assign(input, { code, outcomeKnown: true, from, event });
30
+ }
11
31
  return Object.assign(input, {
12
32
  code,
13
33
  outcomeKnown: explicit !== false && !UNKNOWN_OUTCOME_CODES.has(code),
@@ -15,9 +35,12 @@ export function mutationFailure(input) {
15
35
  }
16
36
  function mutationCode(input) {
17
37
  const value = input;
38
+ if (value.code === 'STATE_TRANSITION_ILLEGAL')
39
+ return value.code;
18
40
  for (const candidate of [value.code, nestedCode(value.reason), nestedCode(value.data)]) {
19
- if (typeof candidate === 'string' && candidate.startsWith('MUTATION_'))
41
+ if (typeof candidate === 'string' && candidate.startsWith('MUTATION_')) {
20
42
  return candidate;
43
+ }
21
44
  }
22
45
  return undefined;
23
46
  }
@@ -0,0 +1,3 @@
1
+ import type { StateMachine } from './machine.js';
2
+ export declare function rememberStateMachine(machine: StateMachine): void;
3
+ export declare function isStateMachine(input: unknown): input is StateMachine;
@@ -0,0 +1,7 @@
1
+ const machines = new WeakSet();
2
+ export function rememberStateMachine(machine) {
3
+ machines.add(machine);
4
+ }
5
+ export function isStateMachine(input) {
6
+ return typeof input === 'object' && input !== null && machines.has(input);
7
+ }
@@ -1,4 +1,5 @@
1
1
  import { z } from 'zod';
2
+ import { rememberStateMachine } from './authority.js';
2
3
  import { relation } from './relation.js';
3
4
  export function stateMachine(input) {
4
5
  const admitted = relation(input);
@@ -10,7 +11,7 @@ export function stateMachine(input) {
10
11
  const eventSchema = z.enum(events);
11
12
  const stateSet = new Set(states);
12
13
  const eventSet = new Set(events);
13
- return Object.freeze({
14
+ const machine = Object.freeze({
14
15
  initial: admitted.initial,
15
16
  stateSchema,
16
17
  eventSchema,
@@ -40,6 +41,8 @@ export function stateMachine(input) {
40
41
  });
41
42
  },
42
43
  });
44
+ rememberStateMachine(machine);
45
+ return machine;
43
46
  }
44
47
  function admitState(state, states) {
45
48
  if (!states.has(state))
@@ -1,4 +1,6 @@
1
1
  import { KernelSchema as CoreKernelSchema } from '@astrale-os/kernel-core/schema';
2
+ export { defineSchema, edgeClass, nodeClass, stateProperty } from './state-property/index.js';
3
+ export type { StateClass, StateProperty, StateSchema } from './state-property/index.js';
2
4
  export * from '@astrale-os/kernel-dsl/v1';
3
5
  export { patterns } from '@astrale-os/kernel-dsl/v1/addressing';
4
6
  export type { CallablePolicyBuilder, CallablePolicyContext, CallableProjection, AuthoredMethodProjection, AuthoredPropertyProjection, ClassIdentityProjection, ClassOwnerProjection, CoreEdge, CoreEndpoint, CoreNode, DefinedSchema, DirectedEdgeClass, ClassReferenceProjection, ClassProjection, FunctionProjection, EdgeClass, Function, Method, MethodProjection, Lazy, NodeClass, Policy, Property, PropertyProjection, ResolvedDomainOfInput, View, } from '@astrale-os/kernel-dsl/v1/builder';
@@ -1,6 +1,7 @@
1
1
  import { KernelSchema as CoreKernelSchema } from '@astrale-os/kernel-core/schema';
2
2
  /** Canonical Schema authoring, compilation, loading, and resolved Domain surface. */
3
3
  import { schema as language } from '@astrale-os/kernel-dsl/v1';
4
+ export { defineSchema, edgeClass, nodeClass, stateProperty } from './state-property/index.js';
4
5
  export * from '@astrale-os/kernel-dsl/v1';
5
6
  export { patterns } from '@astrale-os/kernel-dsl/v1/addressing';
6
7
  /** Kernel-owned Schema values used by authored product Schemas and graph operations. */
@@ -0,0 +1,15 @@
1
+ import type { DefinedSchema, NodeClassInput, PropertyOptions, ResolvedDomainOfInput, SchemaInput as DomainSchemaInput } from '@astrale-os/kernel-dsl/v1/builder';
2
+ import { nodeClass as languageNodeClass } from '@astrale-os/kernel-dsl/v1';
3
+ import type { StateMachine } from '../../../application/state/machine.js';
4
+ import type { StateClass, StateClassesOfInput, StateDependenciesOfInput, StatePropertiesOfConfig, StateProperty, StateSchema } from './model.js';
5
+ interface StatePropertyOptions {
6
+ readonly visibility?: PropertyOptions['visibility'];
7
+ readonly description?: string;
8
+ }
9
+ type RequiredStatePropertyOptions<Options extends StatePropertyOptions> = Readonly<Options & {
10
+ readonly required: true;
11
+ }>;
12
+ export declare function stateProperty<const Machine extends StateMachine, const Options extends StatePropertyOptions = Record<never, never>>(machine: Machine, options?: Options & Record<Exclude<keyof Options, keyof StatePropertyOptions>, never>): StateProperty<Machine, RequiredStatePropertyOptions<Options>>;
13
+ export declare function nodeClass<const Config extends NodeClassInput = Record<never, never>>(config?: Config & Record<Exclude<keyof Config, keyof NodeClassInput>, never>): ReturnType<typeof languageNodeClass<Config>> & StateClass<StatePropertiesOfConfig<Config>>;
14
+ export declare function defineSchema<const Origin extends string, const Input extends DomainSchemaInput>(origin: Origin, input: Input): DefinedSchema<Origin, ResolvedDomainOfInput<Origin, Input>> & StateSchema<StateClassesOfInput<Input>, StateDependenciesOfInput<Input>>;
15
+ export {};
@@ -0,0 +1,49 @@
1
+ import { defineSchema as defineLanguageSchema, nodeClass as languageNodeClass, property, schema, } from '@astrale-os/kernel-dsl/v1';
2
+ import { isStateMachine } from '../../../application/state/authority.js';
3
+ import { rememberStateClass, rememberStateProperty, rememberStateSchema } from './registry.js';
4
+ export function stateProperty(machine, options) {
5
+ if (!isStateMachine(machine))
6
+ throw new TypeError('stateProperty requires an SDK StateMachine.');
7
+ if (options !== undefined &&
8
+ (typeof options !== 'object' ||
9
+ options === null ||
10
+ Array.isArray(options) ||
11
+ Reflect.ownKeys(options).some((key) => key !== 'visibility' && key !== 'description'))) {
12
+ throw new TypeError('stateProperty options are visibility and description only.');
13
+ }
14
+ const value = property(machine.stateSchema, {
15
+ ...options,
16
+ required: true,
17
+ });
18
+ rememberStateProperty(value, machine);
19
+ return value;
20
+ }
21
+ export function nodeClass(config) {
22
+ const captured = captureNodeClassInput(config);
23
+ const value = languageNodeClass(captured);
24
+ rememberStateClass(value, captured?.properties);
25
+ return value;
26
+ }
27
+ export function defineSchema(origin, input) {
28
+ const captured = captureSchemaInput(input);
29
+ const source = defineLanguageSchema(origin, captured);
30
+ rememberStateSchema(source, captured, schema.resolve(source));
31
+ return source;
32
+ }
33
+ function captureNodeClassInput(config) {
34
+ if (config === undefined)
35
+ return undefined;
36
+ const captured = { ...config };
37
+ return {
38
+ ...captured,
39
+ ...(captured.properties === undefined ? {} : { properties: { ...captured.properties } }),
40
+ };
41
+ }
42
+ function captureSchemaInput(input) {
43
+ const captured = { ...input };
44
+ return {
45
+ ...captured,
46
+ ...(captured.dependencies === undefined ? {} : { dependencies: { ...captured.dependencies } }),
47
+ ...(captured.classes === undefined ? {} : { classes: { ...captured.classes } }),
48
+ };
49
+ }
@@ -0,0 +1,16 @@
1
+ import type { DirectedEdgeInput, UndirectedEdgeInput } from '@astrale-os/kernel-dsl/v1/builder';
2
+ import { edgeClass as languageEdgeClass } from '@astrale-os/kernel-dsl/v1';
3
+ import type { StateProperty } from './model.js';
4
+ interface EdgeClassAuthoring {
5
+ directed<const Input extends Omit<DirectedEdgeInput, 'orientation'>>(input: Parameters<typeof languageEdgeClass.directed<Input>>[0] & WithoutStateProperties<Input>): ReturnType<typeof languageEdgeClass.directed<Input>>;
6
+ undirected<const Input extends Omit<UndirectedEdgeInput, 'orientation'>>(input: Parameters<typeof languageEdgeClass.undirected<Input>>[0] & WithoutStateProperties<Input>): ReturnType<typeof languageEdgeClass.undirected<Input>>;
7
+ }
8
+ type WithoutStateProperties<Input> = Input extends {
9
+ readonly properties?: infer Properties extends Readonly<Record<string, unknown>>;
10
+ } ? {
11
+ readonly properties?: {
12
+ readonly [Name in keyof Properties]: Properties[Name] extends StateProperty ? never : Properties[Name];
13
+ };
14
+ } : unknown;
15
+ export declare const edgeClass: Readonly<EdgeClassAuthoring>;
16
+ export {};
@@ -0,0 +1,21 @@
1
+ import { edgeClass as languageEdgeClass } from '@astrale-os/kernel-dsl/v1';
2
+ import { isStateProperty } from './registry.js';
3
+ export const edgeClass = Object.freeze({
4
+ directed(input) {
5
+ return languageEdgeClass.directed(captureEdgeInput(input));
6
+ },
7
+ undirected(input) {
8
+ return languageEdgeClass.undirected(captureEdgeInput(input));
9
+ },
10
+ });
11
+ function captureEdgeInput(input) {
12
+ const captured = { ...input };
13
+ const properties = captured.properties === undefined ? undefined : { ...captured.properties };
14
+ if (Object.values(properties ?? {}).some(isStateProperty)) {
15
+ throw new TypeError('StateProperty is supported only on Node Classes.');
16
+ }
17
+ return {
18
+ ...captured,
19
+ ...(properties === undefined ? {} : { properties }),
20
+ };
21
+ }
@@ -0,0 +1,4 @@
1
+ export { defineSchema, nodeClass, stateProperty } from './authoring.js';
2
+ export { edgeClass } from './edge.js';
3
+ export type { StateClass, StatePropertiesOfClass, StateProperty, StateSchema, StatefulDomainOf, } from './model.js';
4
+ export { stateMachineFor } from './registry.js';
@@ -0,0 +1,3 @@
1
+ export { defineSchema, nodeClass, stateProperty } from './authoring.js';
2
+ export { edgeClass } from './edge.js';
3
+ export { stateMachineFor } from './registry.js';
@@ -0,0 +1,49 @@
1
+ import type { Domain, ResolvedClass } from '@astrale-os/kernel-dsl/v1';
2
+ import type { NodeClassInput, Property, PropertyOptions, SchemaInput as DomainSchemaInput } from '@astrale-os/kernel-dsl/v1/builder';
3
+ import type * as languageSchema from '@astrale-os/kernel-dsl/v1/schema';
4
+ import type { StateMachine } from '../../../application/state/machine.js';
5
+ declare const STATE_PROPERTY_MACHINE: unique symbol;
6
+ declare const STATE_CLASS_PROPERTIES: unique symbol;
7
+ declare const STATE_SCHEMA_CLASSES: unique symbol;
8
+ export interface StateProperty<Machine extends StateMachine = StateMachine, Options extends PropertyOptions = PropertyOptions> extends Property<Machine['stateSchema'], Options> {
9
+ readonly [STATE_PROPERTY_MACHINE]: Machine;
10
+ }
11
+ export interface StateClass<Properties extends Readonly<Record<string, StateMachine>>> {
12
+ readonly [STATE_CLASS_PROPERTIES]: Properties;
13
+ }
14
+ export interface StateSchema<Classes extends Readonly<Record<string, Readonly<Record<string, StateMachine>>>>, Dependencies extends Readonly<Record<string, languageSchema.DomainSchema>> = Readonly<Record<never, never>>> {
15
+ readonly [STATE_SCHEMA_CLASSES]: {
16
+ readonly classes: Classes;
17
+ readonly dependencies: Dependencies;
18
+ };
19
+ }
20
+ export type StatePropertiesOfConfig<Config extends NodeClassInput> = Merge<ParentStateProperties<Config>, OwnStateProperties<Config>>;
21
+ export type StateClassesOfInput<Input extends DomainSchemaInput> = Input extends {
22
+ readonly classes?: infer Classes extends Readonly<Record<string, unknown>>;
23
+ } ? {
24
+ readonly [Name in keyof Classes]: Classes[Name] extends StateClass<infer Properties> ? Properties : Readonly<Record<never, never>>;
25
+ } : Readonly<Record<never, never>>;
26
+ export type StateDependenciesOfInput<Input extends DomainSchemaInput> = Input extends {
27
+ readonly dependencies?: infer Dependencies extends Readonly<Record<string, languageSchema.DomainSchema>>;
28
+ } ? Dependencies : Readonly<Record<never, never>>;
29
+ export type StatefulDomainOf<Schema extends languageSchema.DomainSchema> = Schema extends StateSchema<infer Classes, infer Dependencies> ? Extract<languageSchema.DomainOf<Schema>, Domain> extends infer DomainValue extends Domain ? DomainValue & {
30
+ readonly classes: {
31
+ readonly [Name in keyof DomainValue['classes']]: DomainValue['classes'][Name] & StateClass<Name extends keyof Classes ? Extract<Classes[Name], Readonly<Record<string, StateMachine>>> : Readonly<Record<never, never>>>;
32
+ };
33
+ readonly dependencies: {
34
+ readonly [Name in keyof DomainValue['dependencies']]: Name extends keyof Dependencies ? Dependencies[Name] extends languageSchema.DomainSchema ? StatefulDomainOf<Dependencies[Name]> : DomainValue['dependencies'][Name] : DomainValue['dependencies'][Name];
35
+ };
36
+ } : never : Extract<languageSchema.DomainOf<Schema>, Domain>;
37
+ export type StatePropertiesOfClass<Class extends ResolvedClass<'node'>> = Class extends StateClass<infer Properties> ? Properties : Readonly<Record<never, never>>;
38
+ type OwnStateProperties<Config extends NodeClassInput> = Config extends {
39
+ readonly properties?: infer Properties extends Readonly<Record<string, unknown>>;
40
+ } ? {
41
+ readonly [Name in keyof Properties as Properties[Name] extends StateProperty ? Name : never]: Properties[Name] extends StateProperty<infer Machine> ? Machine : never;
42
+ } : Readonly<Record<never, never>>;
43
+ type ParentStateProperties<Config extends NodeClassInput> = Config extends {
44
+ readonly extends?: readonly (infer Parent)[];
45
+ } ? UnionToIntersection<StatePropertiesOfReference<Parent>> : Readonly<Record<never, never>>;
46
+ type StatePropertiesOfReference<Value, Depth extends readonly unknown[] = []> = Depth['length'] extends 32 ? Readonly<Record<never, never>> : Value extends () => infer Resolved ? StatePropertiesOfReference<Resolved, readonly [...Depth, unknown]> : Value extends StateClass<infer Properties> ? Properties : Readonly<Record<never, never>>;
47
+ type UnionToIntersection<Value> = (Value extends unknown ? (input: Value) => void : never) extends (input: infer Intersection) => void ? Intersection : never;
48
+ type Merge<Parent, Own> = Readonly<Omit<Parent, keyof Own> & Own>;
49
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,9 @@
1
+ import type { Domain, ResolvedProperty } from '@astrale-os/kernel-dsl/v1';
2
+ import type { Property, SchemaInput as DomainSchemaInput } from '@astrale-os/kernel-dsl/v1/builder';
3
+ import type * as languageSchema from '@astrale-os/kernel-dsl/v1/schema';
4
+ import type { StateMachine } from '../../../application/state/machine.js';
5
+ export declare function rememberStateProperty(property: Property, machine: StateMachine): void;
6
+ export declare function isStateProperty(value: unknown): boolean;
7
+ export declare function rememberStateClass(selected: object, properties?: Readonly<Record<string, unknown>>): void;
8
+ export declare function rememberStateSchema(source: languageSchema.DomainSchema, input: DomainSchemaInput, domain: Domain): void;
9
+ export declare function stateMachineFor(domain: Domain, selected: ResolvedProperty<unknown>): StateMachine | undefined;
@@ -0,0 +1,74 @@
1
+ const propertyMachines = new WeakMap();
2
+ const classProperties = new WeakMap();
3
+ const schemaProperties = new WeakMap();
4
+ export function rememberStateProperty(property, machine) {
5
+ propertyMachines.set(property, machine);
6
+ }
7
+ export function isStateProperty(value) {
8
+ return typeof value === 'object' && value !== null && propertyMachines.has(value);
9
+ }
10
+ export function rememberStateClass(selected, properties) {
11
+ const machines = new Map();
12
+ for (const [name, property] of Object.entries(properties ?? {})) {
13
+ if (typeof property !== 'object' || property === null)
14
+ continue;
15
+ const machine = propertyMachines.get(property);
16
+ if (machine !== undefined)
17
+ machines.set(name, machine);
18
+ }
19
+ classProperties.set(selected, machines);
20
+ }
21
+ export function rememberStateSchema(source, input, domain) {
22
+ const byKey = new Map();
23
+ for (const dependency of Object.values(input.dependencies ?? {})) {
24
+ const associated = schemaProperties.get(dependency);
25
+ if (associated === undefined)
26
+ continue;
27
+ for (const [key, machine] of associated.byKey)
28
+ bindKey(byKey, key, machine);
29
+ }
30
+ for (const [className, authored] of Object.entries(input.classes ?? {})) {
31
+ const machines = classProperties.get(authored);
32
+ if (machines === undefined || machines.size === 0)
33
+ continue;
34
+ const resolved = Reflect.get(domain.classes, className);
35
+ if (resolved === undefined)
36
+ continue;
37
+ for (const [propertyName, machine] of machines) {
38
+ const property = Reflect.get(resolved.self.properties, propertyName);
39
+ if (property !== undefined)
40
+ bindKey(byKey, property.key, machine);
41
+ }
42
+ }
43
+ const byProperty = new WeakMap();
44
+ bindDomainProperties(domain, byKey, byProperty, new Set());
45
+ schemaProperties.set(source, { byKey, byProperty });
46
+ }
47
+ export function stateMachineFor(domain, selected) {
48
+ const source = Reflect.get(domain, 'source');
49
+ if (typeof source !== 'object' || source === null)
50
+ return undefined;
51
+ return schemaProperties.get(source)?.byProperty.get(selected);
52
+ }
53
+ function bindKey(byKey, key, machine) {
54
+ const existing = byKey.get(key);
55
+ if (existing !== undefined && existing !== machine) {
56
+ throw new TypeError(`StateProperty ${key} has more than one StateMachine authority.`);
57
+ }
58
+ byKey.set(key, machine);
59
+ }
60
+ function bindDomainProperties(domain, byKey, byProperty, seen) {
61
+ if (seen.has(domain))
62
+ return;
63
+ seen.add(domain);
64
+ for (const definition of domain.definitions('class')) {
65
+ for (const property of definition.self.properties) {
66
+ const machine = byKey.get(property.key);
67
+ if (machine !== undefined)
68
+ byProperty.set(property, machine);
69
+ }
70
+ }
71
+ for (const dependency of Object.values(domain.dependencies)) {
72
+ bindDomainProperties(dependency, byKey, byProperty, seen);
73
+ }
74
+ }
@@ -1,7 +1,7 @@
1
1
  import ts from 'typescript';
2
2
  import { importedSymbol, propertyChain, staticText, unwrap, visit, } from '../../adapters/typescript/index.js';
3
3
  import { ambiguity, callback, DSL_MODULES, definitionOriginAmbiguity, definitionObjects, domainProjectorEvidence, forbiddenIoImport, hasForbiddenGlobalCall, production, violation, } from './shared.js';
4
- import { stateMachineIdentity, stateSchemaIdentity } from './state-machine.js';
4
+ import { stateMachineIdentity, statePropertyIdentity } from './state-machine.js';
5
5
  const FORBIDDEN_LAYERS = new Set(['integrations', 'actions', 'providers', 'queries', 'workflows']);
6
6
  const FORBIDDEN_CALLS = new Set(['invoke', 'query', 'retry', 'run', 'submit']);
7
7
  export const mutationRules = [
@@ -150,19 +150,27 @@ export const mutationRules = [
150
150
  evidence.push(ambiguity(file, node, 'StateMachine transition input is not one static object.'));
151
151
  return;
152
152
  }
153
- if (assignedValue(input, 'to') !== undefined ||
154
- assignedValue(input, 'event') !== undefined ||
155
- assignedValue(input, 'transition') !== undefined ||
156
- assignedValue(input, 'witness') !== undefined) {
157
- evidence.push(violation(file, input, 'StateMachine transition accepts one allowed decision, not a target, event, transition, or witness field.'));
153
+ const removed = [
154
+ 'machine',
155
+ 'lifecycle',
156
+ 'decision',
157
+ 'to',
158
+ 'transition',
159
+ 'witness',
160
+ 'step',
161
+ ].filter((name) => assignedValue(input, name) !== undefined);
162
+ if (removed.length > 0) {
163
+ evidence.push(violation(file, input, `StateMachine transition derives authority and target from StateProperty; remove ${removed.join(', ')}.`));
158
164
  }
159
165
  const spread = hasSpread(input);
160
166
  if (spread) {
161
167
  evidence.push(ambiguity(file, input, 'StateMachine transition input contains a spread that may override or add governed fields.'));
162
168
  }
163
- if (assignedValue(input, 'decision') === undefined) {
164
- if (!spread) {
165
- evidence.push(violation(file, input, 'StateMachine transition has no allowed decision.'));
169
+ if (!spread) {
170
+ for (const required of ['from', 'event']) {
171
+ if (assignedValue(input, required) !== undefined)
172
+ continue;
173
+ evidence.push(violation(file, input, `StateMachine transition has no ${required} intent.`));
166
174
  }
167
175
  }
168
176
  const selectedClass = propertiesForClass(input, machineProperties);
@@ -177,25 +185,6 @@ export const mutationRules = [
177
185
  (selectedClass.kind === 'resolved' && !selectedClass.properties.has(property))) {
178
186
  evidence.push(violation(file, assignedValue(input, 'property') ?? input, `StateMachine transition property ${property} is not a machine-backed Property of the selected Class.`));
179
187
  }
180
- const legacyLifecycle = assignedValue(input, 'lifecycle');
181
- if (legacyLifecycle !== undefined) {
182
- evidence.push(violation(file, legacyLifecycle, 'StateMachine transition uses removed lifecycle field; use machine.'));
183
- }
184
- const machineInput = assignedValue(input, 'machine');
185
- const machine = machineInput === undefined
186
- ? { kind: 'absent' }
187
- : stateMachineIdentity(project, file, machineInput);
188
- if (machine.kind !== 'resolved') {
189
- if (legacyLifecycle === undefined) {
190
- evidence.push(ambiguity(file, machineInput ?? input, 'StateMachine transition machine has no resolvable canonical States source.'));
191
- }
192
- }
193
- else if (property !== undefined &&
194
- selectedClass.kind === 'resolved' &&
195
- selectedClass.properties.get(property) !== undefined &&
196
- selectedClass.properties.get(property) !== machine.identity) {
197
- evidence.push(violation(file, machineInput ?? input, `StateMachine transition uses a different authority than property ${property}.`));
198
- }
199
188
  });
200
189
  }
201
190
  return evidence;
@@ -587,7 +576,7 @@ function machinePropertiesByClass(project) {
587
576
  const ambiguousByClass = new Map();
588
577
  const ownerSources = new Map();
589
578
  const record = (file, owner, name, value) => {
590
- const machine = stateSchemaIdentity(project, file, value);
579
+ const machine = statePropertyIdentity(project, file, value);
591
580
  if (machine.kind === 'resolved') {
592
581
  const identities = all.get(name) ?? new Set();
593
582
  identities.add(machine.identity);
@@ -635,7 +624,7 @@ function machinePropertiesByClass(project) {
635
624
  }
636
625
  }
637
626
  else if (owner !== undefined) {
638
- const machine = stateSchemaIdentity(project, file, node.expression);
627
+ const machine = statePropertyIdentity(project, file, node.expression);
639
628
  if (machine.kind !== 'absent') {
640
629
  const names = ambiguousByClass.get(owner) ?? new Set();
641
630
  names.add('<spread>');
@@ -1,7 +1,7 @@
1
1
  import ts from 'typescript';
2
2
  import { importedSymbol, propertyChain, staticText, unwrap, visit, } from '../../adapters/typescript/index.js';
3
- import { ambiguity, forbiddenIoImport, production, violation } from './shared.js';
4
- import { stateMachineConstructorOrigin, stateMachineOrigin, stateSchemaIdentity, } from './state-machine.js';
3
+ import { ambiguity, DSL_MODULES, forbiddenIoImport, production, violation } from './shared.js';
4
+ import { stateMachineConstructorOrigin, stateMachineOrigin, statePropertyIdentity, statePropertyOrigin, stateSchemaIdentity, } from './state-machine.js';
5
5
  const FORBIDDEN_LAYERS = new Set([
6
6
  'integrations',
7
7
  'actions',
@@ -32,10 +32,23 @@ export const schemaRules = [
32
32
  const ownedVocabularies = file.submodule === undefined ? undefined : machineVocabularies.get(file.submodule);
33
33
  const reportedEnums = new Set();
34
34
  visit(file.source, (node) => {
35
- if (ts.isPropertyAssignment(node) && isPropertiesMember(node)) {
36
- const codec = stateSchemaIdentity(project, file, node.initializer);
37
- if (codec.kind === 'ambiguous') {
38
- evidence.push(ambiguity(file, node.initializer, 'Schema machine-backed Property codec has no exact stateSchema source.'));
35
+ if ((ts.isPropertyAssignment(node) || ts.isShorthandPropertyAssignment(node)) &&
36
+ isPropertiesMember(node)) {
37
+ const value = ts.isPropertyAssignment(node) ? node.initializer : node.name;
38
+ const result = persistedStatePropertyEvidence(project, file, value, node);
39
+ if (result !== undefined)
40
+ evidence.push(result);
41
+ }
42
+ if (ts.isSpreadAssignment(node) && isPropertiesObject(node.parent)) {
43
+ const spread = staticSchemaObject(file, node.expression);
44
+ for (const entry of spread?.properties ?? []) {
45
+ if (!ts.isPropertyAssignment(entry) && !ts.isShorthandPropertyAssignment(entry)) {
46
+ continue;
47
+ }
48
+ const value = ts.isPropertyAssignment(entry) ? entry.initializer : entry.name;
49
+ const result = persistedStatePropertyEvidence(project, file, value, node);
50
+ if (result !== undefined)
51
+ evidence.push(result);
39
52
  }
40
53
  }
41
54
  if (ts.isCallExpression(node)) {
@@ -277,9 +290,79 @@ function propertyName(name) {
277
290
  ? (staticText(name.expression) ?? '<dynamic>')
278
291
  : '<dynamic>';
279
292
  }
293
+ function persistedStatePropertyEvidence(project, file, value, placement) {
294
+ const associationOrigin = statePropertyOrigin(file, value);
295
+ const association = statePropertyIdentity(project, file, value);
296
+ if (associationOrigin === 'resolved' && isEdgePropertyPlacement(file, placement)) {
297
+ return violation(file, value, 'Schema StateProperty is supported only on Node Classes.');
298
+ }
299
+ if (associationOrigin === 'resolved' && association.kind === 'absent') {
300
+ return violation(file, value, 'Schema stateProperty does not reference an exported StateMachine authority.');
301
+ }
302
+ if (associationOrigin === 'ambiguous' || association.kind === 'ambiguous') {
303
+ return ambiguity(file, value, 'Schema StateProperty has no exact StateMachine source.');
304
+ }
305
+ if (associationOrigin !== 'absent')
306
+ return undefined;
307
+ const codec = stateSchemaIdentity(project, file, value);
308
+ if (codec.kind === 'resolved') {
309
+ return violation(file, value, 'Schema persists a StateMachine codec without retaining its authority; use stateProperty(machine).');
310
+ }
311
+ return codec.kind === 'ambiguous'
312
+ ? ambiguity(file, value, 'Schema machine-backed Property has no exact stateProperty source.')
313
+ : undefined;
314
+ }
315
+ function isEdgePropertyPlacement(file, placement) {
316
+ let current = placement;
317
+ while (current !== undefined && !ts.isSourceFile(current)) {
318
+ if (ts.isCallExpression(current)) {
319
+ const expression = unwrap(current.expression);
320
+ if (ts.isPropertyAccessExpression(expression) || ts.isElementAccessExpression(expression)) {
321
+ const member = ts.isPropertyAccessExpression(expression)
322
+ ? expression.name.text
323
+ : staticText(expression.argumentExpression);
324
+ const owner = importedSymbol(file, expression.expression);
325
+ if ((member === 'directed' || member === 'undirected') &&
326
+ owner?.name === 'edgeClass' &&
327
+ DSL_MODULES.has(owner.module)) {
328
+ return true;
329
+ }
330
+ }
331
+ }
332
+ current = current.parent;
333
+ }
334
+ return false;
335
+ }
336
+ function staticSchemaObject(file, expression, seen = new Set()) {
337
+ const value = unwrap(expression);
338
+ if (ts.isObjectLiteralExpression(value))
339
+ return value;
340
+ if (!ts.isIdentifier(value) || seen.has(value.text))
341
+ return undefined;
342
+ for (const statement of file.source.statements) {
343
+ if (!ts.isVariableStatement(statement))
344
+ continue;
345
+ if ((statement.declarationList.flags & ts.NodeFlags.Const) === 0)
346
+ continue;
347
+ for (const declaration of statement.declarationList.declarations) {
348
+ if (ts.isIdentifier(declaration.name) &&
349
+ declaration.name.text === value.text &&
350
+ declaration.initializer !== undefined) {
351
+ return staticSchemaObject(file, declaration.initializer, new Set([...seen, value.text]));
352
+ }
353
+ }
354
+ }
355
+ return undefined;
356
+ }
280
357
  function isPropertiesMember(node) {
281
358
  const owner = node.parent.parent;
282
359
  return (ts.isPropertyAssignment(owner) &&
283
360
  owner.name !== undefined &&
284
361
  propertyName(owner.name) === 'properties');
285
362
  }
363
+ function isPropertiesObject(node) {
364
+ const owner = node.parent;
365
+ return (ts.isPropertyAssignment(owner) &&
366
+ owner.name !== undefined &&
367
+ propertyName(owner.name) === 'properties');
368
+ }
@@ -16,5 +16,9 @@ export declare function stateMachineOrigin(project: SourceProject, file: SourceF
16
16
  export declare function stateMachineIdentity(project: SourceProject, file: SourceFile, expression: ts.Expression, seen?: ReadonlySet<string>): StateMachineResolution;
17
17
  /** Resolve a Schema codec expression to the exact machine whose stateSchema it aliases. */
18
18
  export declare function stateSchemaIdentity(project: SourceProject, file: SourceFile, expression: ts.Expression, seen?: ReadonlySet<string>): StateMachineResolution;
19
+ /** Resolve a persisted Schema stateProperty to its exact StateMachine authority. */
20
+ export declare function statePropertyIdentity(project: SourceProject, file: SourceFile, expression: ts.Expression, seen?: ReadonlySet<string>): StateMachineResolution;
21
+ /** Classify whether an expression is produced by the canonical SDK stateProperty helper. */
22
+ export declare function statePropertyOrigin(file: SourceFile, expression: ts.Expression, seen?: ReadonlySet<string>): StateMachineOrigin;
19
23
  /** Classify whether a stateMachine call is the value of an exported top-level binding. */
20
24
  export declare function machineExportStatus(file: SourceFile, call: ts.CallExpression): MachineExportStatus;
@@ -7,6 +7,7 @@ const SCHEMA_MODULES = new Set([
7
7
  '@astrale-os/kernel-dsl',
8
8
  '@astrale-os/kernel-dsl/v1',
9
9
  ]);
10
+ const STATE_PROPERTY_MODULES = new Set(['@astrale-os/sdk/schema']);
10
11
  /** Resolve a direct or static local alias of the SDK stateMachine constructor. */
11
12
  export function stateMachineConstructorOrigin(file, expression, seen = new Set()) {
12
13
  const value = unwrap(expression);
@@ -107,6 +108,67 @@ export function stateSchemaIdentity(project, file, expression, seen = new Set())
107
108
  });
108
109
  return candidate ? { kind: 'ambiguous' } : { kind: 'absent' };
109
110
  }
111
+ /** Resolve a persisted Schema stateProperty to its exact StateMachine authority. */
112
+ export function statePropertyIdentity(project, file, expression, seen = new Set()) {
113
+ const value = unwrap(expression);
114
+ if (ts.isCallExpression(value)) {
115
+ const symbol = resolveImportedSymbol(file, value.expression);
116
+ if (symbol.kind === 'resolved' &&
117
+ symbol.name === 'stateProperty' &&
118
+ STATE_PROPERTY_MODULES.has(symbol.module)) {
119
+ const machine = value.arguments[0];
120
+ return machine === undefined
121
+ ? { kind: 'absent' }
122
+ : stateMachineIdentity(project, file, machine);
123
+ }
124
+ }
125
+ if (ts.isIdentifier(value)) {
126
+ const declaration = localVariable(file, value.text);
127
+ if (declaration === undefined)
128
+ return { kind: 'absent' };
129
+ if (seen.has(value.text))
130
+ return { kind: 'ambiguous' };
131
+ const next = new Set(seen);
132
+ next.add(value.text);
133
+ const resolution = statePropertyIdentity(project, file, declaration.initializer, next);
134
+ return declaration.constant || resolution.kind === 'absent' ? resolution : { kind: 'ambiguous' };
135
+ }
136
+ let candidate = false;
137
+ visit(value, (node) => {
138
+ if (!ts.isCallExpression(node))
139
+ return;
140
+ const symbol = resolveImportedSymbol(file, node.expression);
141
+ if (symbol.kind !== 'local' &&
142
+ symbol.name === 'stateProperty' &&
143
+ (symbol.kind === 'ambiguous' || STATE_PROPERTY_MODULES.has(symbol.module))) {
144
+ candidate = true;
145
+ }
146
+ });
147
+ return candidate ? { kind: 'ambiguous' } : { kind: 'absent' };
148
+ }
149
+ /** Classify whether an expression is produced by the canonical SDK stateProperty helper. */
150
+ export function statePropertyOrigin(file, expression, seen = new Set()) {
151
+ const value = unwrap(expression);
152
+ if (ts.isCallExpression(value)) {
153
+ const symbol = resolveImportedSymbol(file, value.expression);
154
+ if (symbol.kind === 'resolved') {
155
+ return symbol.name === 'stateProperty' && STATE_PROPERTY_MODULES.has(symbol.module)
156
+ ? 'resolved'
157
+ : 'absent';
158
+ }
159
+ if (symbol.kind === 'ambiguous' && symbol.name === 'stateProperty')
160
+ return 'ambiguous';
161
+ }
162
+ if (!ts.isIdentifier(value) || seen.has(value.text))
163
+ return 'absent';
164
+ const declaration = localVariable(file, value.text);
165
+ if (declaration === undefined)
166
+ return 'absent';
167
+ const next = new Set(seen);
168
+ next.add(value.text);
169
+ const origin = statePropertyOrigin(file, declaration.initializer, next);
170
+ return declaration.constant || origin === 'absent' ? origin : 'ambiguous';
171
+ }
110
172
  function staticMemberReceiver(expression, member) {
111
173
  const value = unwrap(expression);
112
174
  if (ts.isPropertyAccessExpression(value) && value.name.text === member) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@astrale-os/sdk",
3
- "version": "0.5.0-beta.66",
3
+ "version": "0.5.0-beta.67",
4
4
  "description": "Schema-first SDK for defining, composing, and deploying Astrale domains",
5
5
  "keywords": [
6
6
  "astrale",
@@ -287,7 +287,7 @@
287
287
  "build": "node scripts/clean-dist.mjs && tsgo && node scripts/verify-no-source-maps.mjs dist && pnpm run verify:package",
288
288
  "e2e": "node __e2e__/flagship/runner.mjs",
289
289
  "typecheck": "tsgo --noEmit && pnpm run typecheck:specs && pnpm run typecheck:types && pnpm run typecheck:tests && pnpm run typecheck:e2e && pnpm run typecheck:knowledge",
290
- "typecheck:specs": "tsgo --noEmit -p src/application/query/.spec/tsconfig.json",
290
+ "typecheck:specs": "tsgo --noEmit -p src/application/query/.spec/tsconfig.json && tsgo --noEmit -p src/platform/schema/.spec/tsconfig.json && tsgo --noEmit -p src/application/mutation/.spec/tsconfig.json",
291
291
  "typecheck:types": "tsgo --noEmit -p __tests__/tsconfig.types.json",
292
292
  "typecheck:tests": "tsgo --noEmit -p tsconfig.test.json",
293
293
  "typecheck:e2e": "tsgo --noEmit -p __e2e__/flagship/fixtures/tsconfig.v1.json && tsgo --noEmit -p __e2e__/flagship/fixtures/tsconfig.json",