@angriff36/manifest 2.18.0
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/LICENSE +21 -0
- package/README.md +476 -0
- package/dist/manifest/agent-sdk/agent-runtime.d.ts +30 -0
- package/dist/manifest/agent-sdk/agent-runtime.d.ts.map +1 -0
- package/dist/manifest/agent-sdk/agent-runtime.js +232 -0
- package/dist/manifest/agent-sdk/agent-runtime.js.map +1 -0
- package/dist/manifest/agent-sdk/index.d.ts +17 -0
- package/dist/manifest/agent-sdk/index.d.ts.map +1 -0
- package/dist/manifest/agent-sdk/index.js +21 -0
- package/dist/manifest/agent-sdk/index.js.map +1 -0
- package/dist/manifest/agent-sdk/intent-mapper.d.ts +17 -0
- package/dist/manifest/agent-sdk/intent-mapper.d.ts.map +1 -0
- package/dist/manifest/agent-sdk/intent-mapper.js +115 -0
- package/dist/manifest/agent-sdk/intent-mapper.js.map +1 -0
- package/dist/manifest/agent-sdk/introspect.d.ts +42 -0
- package/dist/manifest/agent-sdk/introspect.d.ts.map +1 -0
- package/dist/manifest/agent-sdk/introspect.js +284 -0
- package/dist/manifest/agent-sdk/introspect.js.map +1 -0
- package/dist/manifest/agent-sdk/json-schema.d.ts +29 -0
- package/dist/manifest/agent-sdk/json-schema.d.ts.map +1 -0
- package/dist/manifest/agent-sdk/json-schema.js +132 -0
- package/dist/manifest/agent-sdk/json-schema.js.map +1 -0
- package/dist/manifest/agent-sdk/tool-definitions.d.ts +41 -0
- package/dist/manifest/agent-sdk/tool-definitions.d.ts.map +1 -0
- package/dist/manifest/agent-sdk/tool-definitions.js +288 -0
- package/dist/manifest/agent-sdk/tool-definitions.js.map +1 -0
- package/dist/manifest/agent-sdk/types.d.ts +293 -0
- package/dist/manifest/agent-sdk/types.d.ts.map +1 -0
- package/dist/manifest/agent-sdk/types.js +6 -0
- package/dist/manifest/agent-sdk/types.js.map +1 -0
- package/dist/manifest/api-diagnostics.d.ts +41 -0
- package/dist/manifest/api-diagnostics.d.ts.map +1 -0
- package/dist/manifest/api-diagnostics.js +105 -0
- package/dist/manifest/api-diagnostics.js.map +1 -0
- package/dist/manifest/approval/approval-store.d.ts +52 -0
- package/dist/manifest/approval/approval-store.d.ts.map +1 -0
- package/dist/manifest/approval/approval-store.js +25 -0
- package/dist/manifest/approval/approval-store.js.map +1 -0
- package/dist/manifest/approval/stores/memory.d.ts +33 -0
- package/dist/manifest/approval/stores/memory.d.ts.map +1 -0
- package/dist/manifest/approval/stores/memory.js +56 -0
- package/dist/manifest/approval/stores/memory.js.map +1 -0
- package/dist/manifest/approval/stores/postgres.d.ts +41 -0
- package/dist/manifest/approval/stores/postgres.d.ts.map +1 -0
- package/dist/manifest/approval/stores/postgres.js +124 -0
- package/dist/manifest/approval/stores/postgres.js.map +1 -0
- package/dist/manifest/audit/audit-sink.d.ts +53 -0
- package/dist/manifest/audit/audit-sink.d.ts.map +1 -0
- package/dist/manifest/audit/audit-sink.js +15 -0
- package/dist/manifest/audit/audit-sink.js.map +1 -0
- package/dist/manifest/audit/sinks/memory.d.ts +50 -0
- package/dist/manifest/audit/sinks/memory.d.ts.map +1 -0
- package/dist/manifest/audit/sinks/memory.js +66 -0
- package/dist/manifest/audit/sinks/memory.js.map +1 -0
- package/dist/manifest/audit/sinks/postgres.d.ts +31 -0
- package/dist/manifest/audit/sinks/postgres.d.ts.map +1 -0
- package/dist/manifest/audit/sinks/postgres.js +67 -0
- package/dist/manifest/audit/sinks/postgres.js.map +1 -0
- package/dist/manifest/binary-ir.d.ts +76 -0
- package/dist/manifest/binary-ir.d.ts.map +1 -0
- package/dist/manifest/binary-ir.js +124 -0
- package/dist/manifest/binary-ir.js.map +1 -0
- package/dist/manifest/breaking-change.d.ts +75 -0
- package/dist/manifest/breaking-change.d.ts.map +1 -0
- package/dist/manifest/breaking-change.js +704 -0
- package/dist/manifest/breaking-change.js.map +1 -0
- package/dist/manifest/compiler.d.ts +12 -0
- package/dist/manifest/compiler.d.ts.map +1 -0
- package/dist/manifest/compiler.js +23 -0
- package/dist/manifest/compiler.js.map +1 -0
- package/dist/manifest/config.d.ts +171 -0
- package/dist/manifest/config.d.ts.map +1 -0
- package/dist/manifest/config.js +65 -0
- package/dist/manifest/config.js.map +1 -0
- package/dist/manifest/constraint-analysis.d.ts +122 -0
- package/dist/manifest/constraint-analysis.d.ts.map +1 -0
- package/dist/manifest/constraint-analysis.js +340 -0
- package/dist/manifest/constraint-analysis.js.map +1 -0
- package/dist/manifest/date-time.d.ts +13 -0
- package/dist/manifest/date-time.d.ts.map +1 -0
- package/dist/manifest/date-time.js +60 -0
- package/dist/manifest/date-time.js.map +1 -0
- package/dist/manifest/debug/command-trace.d.ts +37 -0
- package/dist/manifest/debug/command-trace.d.ts.map +1 -0
- package/dist/manifest/debug/command-trace.js +51 -0
- package/dist/manifest/debug/command-trace.js.map +1 -0
- package/dist/manifest/debug/index.d.ts +3 -0
- package/dist/manifest/debug/index.d.ts.map +1 -0
- package/dist/manifest/debug/index.js +2 -0
- package/dist/manifest/debug/index.js.map +1 -0
- package/dist/manifest/domain-completeness.d.ts +13 -0
- package/dist/manifest/domain-completeness.d.ts.map +1 -0
- package/dist/manifest/domain-completeness.js +245 -0
- package/dist/manifest/domain-completeness.js.map +1 -0
- package/dist/manifest/entity-composition.d.ts +24 -0
- package/dist/manifest/entity-composition.d.ts.map +1 -0
- package/dist/manifest/entity-composition.js +157 -0
- package/dist/manifest/entity-composition.js.map +1 -0
- package/dist/manifest/examples.d.ts +6 -0
- package/dist/manifest/examples.d.ts.map +1 -0
- package/dist/manifest/examples.js +443 -0
- package/dist/manifest/examples.js.map +1 -0
- package/dist/manifest/federation/client.d.ts +52 -0
- package/dist/manifest/federation/client.d.ts.map +1 -0
- package/dist/manifest/federation/client.js +152 -0
- package/dist/manifest/federation/client.js.map +1 -0
- package/dist/manifest/federation/descriptor.d.ts +25 -0
- package/dist/manifest/federation/descriptor.d.ts.map +1 -0
- package/dist/manifest/federation/descriptor.js +97 -0
- package/dist/manifest/federation/descriptor.js.map +1 -0
- package/dist/manifest/federation/http-adapter.d.ts +26 -0
- package/dist/manifest/federation/http-adapter.d.ts.map +1 -0
- package/dist/manifest/federation/http-adapter.js +209 -0
- package/dist/manifest/federation/http-adapter.js.map +1 -0
- package/dist/manifest/federation/index.d.ts +51 -0
- package/dist/manifest/federation/index.d.ts.map +1 -0
- package/dist/manifest/federation/index.js +49 -0
- package/dist/manifest/federation/index.js.map +1 -0
- package/dist/manifest/federation/policy-bridge.d.ts +51 -0
- package/dist/manifest/federation/policy-bridge.d.ts.map +1 -0
- package/dist/manifest/federation/policy-bridge.js +122 -0
- package/dist/manifest/federation/policy-bridge.js.map +1 -0
- package/dist/manifest/federation/registry.d.ts +88 -0
- package/dist/manifest/federation/registry.d.ts.map +1 -0
- package/dist/manifest/federation/registry.js +165 -0
- package/dist/manifest/federation/registry.js.map +1 -0
- package/dist/manifest/federation/types.d.ts +209 -0
- package/dist/manifest/federation/types.d.ts.map +1 -0
- package/dist/manifest/federation/types.js +13 -0
- package/dist/manifest/federation/types.js.map +1 -0
- package/dist/manifest/generator.d.ts +44 -0
- package/dist/manifest/generator.d.ts.map +1 -0
- package/dist/manifest/generator.js +899 -0
- package/dist/manifest/generator.js.map +1 -0
- package/dist/manifest/ir-cache.d.ts +48 -0
- package/dist/manifest/ir-cache.d.ts.map +1 -0
- package/dist/manifest/ir-cache.js +91 -0
- package/dist/manifest/ir-cache.js.map +1 -0
- package/dist/manifest/ir-compiler.d.ts +135 -0
- package/dist/manifest/ir-compiler.d.ts.map +1 -0
- package/dist/manifest/ir-compiler.js +1477 -0
- package/dist/manifest/ir-compiler.js.map +1 -0
- package/dist/manifest/ir-diff.d.ts +266 -0
- package/dist/manifest/ir-diff.d.ts.map +1 -0
- package/dist/manifest/ir-diff.js +731 -0
- package/dist/manifest/ir-diff.js.map +1 -0
- package/dist/manifest/ir-version-store.d.ts +109 -0
- package/dist/manifest/ir-version-store.d.ts.map +1 -0
- package/dist/manifest/ir-version-store.js +162 -0
- package/dist/manifest/ir-version-store.js.map +1 -0
- package/dist/manifest/ir.d.ts +616 -0
- package/dist/manifest/ir.d.ts.map +1 -0
- package/dist/manifest/ir.js +2 -0
- package/dist/manifest/ir.js.map +1 -0
- package/dist/manifest/lexer.d.ts +37 -0
- package/dist/manifest/lexer.d.ts.map +1 -0
- package/dist/manifest/lexer.js +224 -0
- package/dist/manifest/lexer.js.map +1 -0
- package/dist/manifest/masking.d.ts +11 -0
- package/dist/manifest/masking.d.ts.map +1 -0
- package/dist/manifest/masking.js +34 -0
- package/dist/manifest/masking.js.map +1 -0
- package/dist/manifest/module-resolver.d.ts +42 -0
- package/dist/manifest/module-resolver.d.ts.map +1 -0
- package/dist/manifest/module-resolver.js +162 -0
- package/dist/manifest/module-resolver.js.map +1 -0
- package/dist/manifest/multi-compiler.d.ts +40 -0
- package/dist/manifest/multi-compiler.d.ts.map +1 -0
- package/dist/manifest/multi-compiler.js +324 -0
- package/dist/manifest/multi-compiler.js.map +1 -0
- package/dist/manifest/outbox/outbox-store.d.ts +56 -0
- package/dist/manifest/outbox/outbox-store.d.ts.map +1 -0
- package/dist/manifest/outbox/outbox-store.js +13 -0
- package/dist/manifest/outbox/outbox-store.js.map +1 -0
- package/dist/manifest/outbox/stores/dynamodb.d.ts +100 -0
- package/dist/manifest/outbox/stores/dynamodb.d.ts.map +1 -0
- package/dist/manifest/outbox/stores/dynamodb.js +239 -0
- package/dist/manifest/outbox/stores/dynamodb.js.map +1 -0
- package/dist/manifest/outbox/stores/memory.d.ts +54 -0
- package/dist/manifest/outbox/stores/memory.d.ts.map +1 -0
- package/dist/manifest/outbox/stores/memory.js +131 -0
- package/dist/manifest/outbox/stores/memory.js.map +1 -0
- package/dist/manifest/outbox/stores/mongodb.d.ts +58 -0
- package/dist/manifest/outbox/stores/mongodb.d.ts.map +1 -0
- package/dist/manifest/outbox/stores/mongodb.js +151 -0
- package/dist/manifest/outbox/stores/mongodb.js.map +1 -0
- package/dist/manifest/outbox/stores/postgres.d.ts +81 -0
- package/dist/manifest/outbox/stores/postgres.d.ts.map +1 -0
- package/dist/manifest/outbox/stores/postgres.js +182 -0
- package/dist/manifest/outbox/stores/postgres.js.map +1 -0
- package/dist/manifest/outbox/stores/redis.d.ts +95 -0
- package/dist/manifest/outbox/stores/redis.d.ts.map +1 -0
- package/dist/manifest/outbox/stores/redis.js +248 -0
- package/dist/manifest/outbox/stores/redis.js.map +1 -0
- package/dist/manifest/parser.d.ts +148 -0
- package/dist/manifest/parser.d.ts.map +1 -0
- package/dist/manifest/parser.js +2243 -0
- package/dist/manifest/parser.js.map +1 -0
- package/dist/manifest/plugin-api.d.ts +202 -0
- package/dist/manifest/plugin-api.d.ts.map +1 -0
- package/dist/manifest/plugin-api.js +101 -0
- package/dist/manifest/plugin-api.js.map +1 -0
- package/dist/manifest/plugin-loader.d.ts +101 -0
- package/dist/manifest/plugin-loader.d.ts.map +1 -0
- package/dist/manifest/plugin-loader.js +332 -0
- package/dist/manifest/plugin-loader.js.map +1 -0
- package/dist/manifest/profiling.d.ts +183 -0
- package/dist/manifest/profiling.d.ts.map +1 -0
- package/dist/manifest/profiling.js +186 -0
- package/dist/manifest/profiling.js.map +1 -0
- package/dist/manifest/projections/analytics/generator.d.ts +27 -0
- package/dist/manifest/projections/analytics/generator.d.ts.map +1 -0
- package/dist/manifest/projections/analytics/generator.js +686 -0
- package/dist/manifest/projections/analytics/generator.js.map +1 -0
- package/dist/manifest/projections/analytics/types.d.ts +46 -0
- package/dist/manifest/projections/analytics/types.d.ts.map +1 -0
- package/dist/manifest/projections/analytics/types.js +8 -0
- package/dist/manifest/projections/analytics/types.js.map +1 -0
- package/dist/manifest/projections/builtins.d.ts +29 -0
- package/dist/manifest/projections/builtins.d.ts.map +1 -0
- package/dist/manifest/projections/builtins.js +143 -0
- package/dist/manifest/projections/builtins.js.map +1 -0
- package/dist/manifest/projections/convex/expression.d.ts +52 -0
- package/dist/manifest/projections/convex/expression.d.ts.map +1 -0
- package/dist/manifest/projections/convex/expression.js +166 -0
- package/dist/manifest/projections/convex/expression.js.map +1 -0
- package/dist/manifest/projections/convex/functions.d.ts +22 -0
- package/dist/manifest/projections/convex/functions.d.ts.map +1 -0
- package/dist/manifest/projections/convex/functions.js +786 -0
- package/dist/manifest/projections/convex/functions.js.map +1 -0
- package/dist/manifest/projections/convex/generator.d.ts +79 -0
- package/dist/manifest/projections/convex/generator.d.ts.map +1 -0
- package/dist/manifest/projections/convex/generator.js +406 -0
- package/dist/manifest/projections/convex/generator.js.map +1 -0
- package/dist/manifest/projections/convex/index.d.ts +10 -0
- package/dist/manifest/projections/convex/index.d.ts.map +1 -0
- package/dist/manifest/projections/convex/index.js +10 -0
- package/dist/manifest/projections/convex/index.js.map +1 -0
- package/dist/manifest/projections/convex/options.d.ts +153 -0
- package/dist/manifest/projections/convex/options.d.ts.map +1 -0
- package/dist/manifest/projections/convex/options.js +60 -0
- package/dist/manifest/projections/convex/options.js.map +1 -0
- package/dist/manifest/projections/convex/orchestration.d.ts +23 -0
- package/dist/manifest/projections/convex/orchestration.d.ts.map +1 -0
- package/dist/manifest/projections/convex/orchestration.js +150 -0
- package/dist/manifest/projections/convex/orchestration.js.map +1 -0
- package/dist/manifest/projections/convex/type-mapping.d.ts +40 -0
- package/dist/manifest/projections/convex/type-mapping.d.ts.map +1 -0
- package/dist/manifest/projections/convex/type-mapping.js +71 -0
- package/dist/manifest/projections/convex/type-mapping.js.map +1 -0
- package/dist/manifest/projections/dart/generator.d.ts +33 -0
- package/dist/manifest/projections/dart/generator.d.ts.map +1 -0
- package/dist/manifest/projections/dart/generator.js +981 -0
- package/dist/manifest/projections/dart/generator.js.map +1 -0
- package/dist/manifest/projections/dart/types.d.ts +29 -0
- package/dist/manifest/projections/dart/types.d.ts.map +1 -0
- package/dist/manifest/projections/dart/types.js +5 -0
- package/dist/manifest/projections/dart/types.js.map +1 -0
- package/dist/manifest/projections/drizzle/generator.d.ts +27 -0
- package/dist/manifest/projections/drizzle/generator.d.ts.map +1 -0
- package/dist/manifest/projections/drizzle/generator.js +654 -0
- package/dist/manifest/projections/drizzle/generator.js.map +1 -0
- package/dist/manifest/projections/drizzle/index.d.ts +12 -0
- package/dist/manifest/projections/drizzle/index.d.ts.map +1 -0
- package/dist/manifest/projections/drizzle/index.js +12 -0
- package/dist/manifest/projections/drizzle/index.js.map +1 -0
- package/dist/manifest/projections/drizzle/options.d.ts +94 -0
- package/dist/manifest/projections/drizzle/options.d.ts.map +1 -0
- package/dist/manifest/projections/drizzle/options.js +31 -0
- package/dist/manifest/projections/drizzle/options.js.map +1 -0
- package/dist/manifest/projections/drizzle/type-mapping.d.ts +74 -0
- package/dist/manifest/projections/drizzle/type-mapping.d.ts.map +1 -0
- package/dist/manifest/projections/drizzle/type-mapping.js +101 -0
- package/dist/manifest/projections/drizzle/type-mapping.js.map +1 -0
- package/dist/manifest/projections/dynamodb/generator.d.ts +48 -0
- package/dist/manifest/projections/dynamodb/generator.d.ts.map +1 -0
- package/dist/manifest/projections/dynamodb/generator.js +341 -0
- package/dist/manifest/projections/dynamodb/generator.js.map +1 -0
- package/dist/manifest/projections/elasticsearch/generator.d.ts +44 -0
- package/dist/manifest/projections/elasticsearch/generator.d.ts.map +1 -0
- package/dist/manifest/projections/elasticsearch/generator.js +613 -0
- package/dist/manifest/projections/elasticsearch/generator.js.map +1 -0
- package/dist/manifest/projections/elasticsearch/options.d.ts +56 -0
- package/dist/manifest/projections/elasticsearch/options.d.ts.map +1 -0
- package/dist/manifest/projections/elasticsearch/options.js +37 -0
- package/dist/manifest/projections/elasticsearch/options.js.map +1 -0
- package/dist/manifest/projections/elasticsearch/type-mapping.d.ts +30 -0
- package/dist/manifest/projections/elasticsearch/type-mapping.d.ts.map +1 -0
- package/dist/manifest/projections/elasticsearch/type-mapping.js +34 -0
- package/dist/manifest/projections/elasticsearch/type-mapping.js.map +1 -0
- package/dist/manifest/projections/elasticsearch/types.d.ts +81 -0
- package/dist/manifest/projections/elasticsearch/types.d.ts.map +1 -0
- package/dist/manifest/projections/elasticsearch/types.js +10 -0
- package/dist/manifest/projections/elasticsearch/types.js.map +1 -0
- package/dist/manifest/projections/express/generator.d.ts +38 -0
- package/dist/manifest/projections/express/generator.d.ts.map +1 -0
- package/dist/manifest/projections/express/generator.js +620 -0
- package/dist/manifest/projections/express/generator.js.map +1 -0
- package/dist/manifest/projections/express/index.d.ts +8 -0
- package/dist/manifest/projections/express/index.d.ts.map +1 -0
- package/dist/manifest/projections/express/index.js +7 -0
- package/dist/manifest/projections/express/index.js.map +1 -0
- package/dist/manifest/projections/express/types.d.ts +92 -0
- package/dist/manifest/projections/express/types.d.ts.map +1 -0
- package/dist/manifest/projections/express/types.js +9 -0
- package/dist/manifest/projections/express/types.js.map +1 -0
- package/dist/manifest/projections/graphql/generator.d.ts +43 -0
- package/dist/manifest/projections/graphql/generator.d.ts.map +1 -0
- package/dist/manifest/projections/graphql/generator.js +662 -0
- package/dist/manifest/projections/graphql/generator.js.map +1 -0
- package/dist/manifest/projections/graphql/index.d.ts +11 -0
- package/dist/manifest/projections/graphql/index.d.ts.map +1 -0
- package/dist/manifest/projections/graphql/index.js +10 -0
- package/dist/manifest/projections/graphql/index.js.map +1 -0
- package/dist/manifest/projections/graphql/types.d.ts +69 -0
- package/dist/manifest/projections/graphql/types.d.ts.map +1 -0
- package/dist/manifest/projections/graphql/types.js +9 -0
- package/dist/manifest/projections/graphql/types.js.map +1 -0
- package/dist/manifest/projections/health/generator.d.ts +36 -0
- package/dist/manifest/projections/health/generator.d.ts.map +1 -0
- package/dist/manifest/projections/health/generator.js +355 -0
- package/dist/manifest/projections/health/generator.js.map +1 -0
- package/dist/manifest/projections/health/types.d.ts +67 -0
- package/dist/manifest/projections/health/types.d.ts.map +1 -0
- package/dist/manifest/projections/health/types.js +28 -0
- package/dist/manifest/projections/health/types.js.map +1 -0
- package/dist/manifest/projections/hono/generator.d.ts +36 -0
- package/dist/manifest/projections/hono/generator.d.ts.map +1 -0
- package/dist/manifest/projections/hono/generator.js +578 -0
- package/dist/manifest/projections/hono/generator.js.map +1 -0
- package/dist/manifest/projections/hono/types.d.ts +86 -0
- package/dist/manifest/projections/hono/types.d.ts.map +1 -0
- package/dist/manifest/projections/hono/types.js +10 -0
- package/dist/manifest/projections/hono/types.js.map +1 -0
- package/dist/manifest/projections/index.d.ts +58 -0
- package/dist/manifest/projections/index.d.ts.map +1 -0
- package/dist/manifest/projections/index.js +41 -0
- package/dist/manifest/projections/index.js.map +1 -0
- package/dist/manifest/projections/interface.d.ts +285 -0
- package/dist/manifest/projections/interface.d.ts.map +1 -0
- package/dist/manifest/projections/interface.js +8 -0
- package/dist/manifest/projections/interface.js.map +1 -0
- package/dist/manifest/projections/jsonschema/generator.d.ts +35 -0
- package/dist/manifest/projections/jsonschema/generator.d.ts.map +1 -0
- package/dist/manifest/projections/jsonschema/generator.js +347 -0
- package/dist/manifest/projections/jsonschema/generator.js.map +1 -0
- package/dist/manifest/projections/jsonschema/index.d.ts +3 -0
- package/dist/manifest/projections/jsonschema/index.d.ts.map +1 -0
- package/dist/manifest/projections/jsonschema/index.js +2 -0
- package/dist/manifest/projections/jsonschema/index.js.map +1 -0
- package/dist/manifest/projections/jsonschema/types.d.ts +31 -0
- package/dist/manifest/projections/jsonschema/types.d.ts.map +1 -0
- package/dist/manifest/projections/jsonschema/types.js +2 -0
- package/dist/manifest/projections/jsonschema/types.js.map +1 -0
- package/dist/manifest/projections/kysely/generator.d.ts +36 -0
- package/dist/manifest/projections/kysely/generator.d.ts.map +1 -0
- package/dist/manifest/projections/kysely/generator.js +325 -0
- package/dist/manifest/projections/kysely/generator.js.map +1 -0
- package/dist/manifest/projections/kysely/index.d.ts +4 -0
- package/dist/manifest/projections/kysely/index.d.ts.map +1 -0
- package/dist/manifest/projections/kysely/index.js +2 -0
- package/dist/manifest/projections/kysely/index.js.map +1 -0
- package/dist/manifest/projections/kysely/options.d.ts +61 -0
- package/dist/manifest/projections/kysely/options.d.ts.map +1 -0
- package/dist/manifest/projections/kysely/options.js +31 -0
- package/dist/manifest/projections/kysely/options.js.map +1 -0
- package/dist/manifest/projections/kysely/type-mapping.d.ts +61 -0
- package/dist/manifest/projections/kysely/type-mapping.d.ts.map +1 -0
- package/dist/manifest/projections/kysely/type-mapping.js +84 -0
- package/dist/manifest/projections/kysely/type-mapping.js.map +1 -0
- package/dist/manifest/projections/llm-context/generator.d.ts +24 -0
- package/dist/manifest/projections/llm-context/generator.d.ts.map +1 -0
- package/dist/manifest/projections/llm-context/generator.js +371 -0
- package/dist/manifest/projections/llm-context/generator.js.map +1 -0
- package/dist/manifest/projections/llm-context/types.d.ts +205 -0
- package/dist/manifest/projections/llm-context/types.d.ts.map +1 -0
- package/dist/manifest/projections/llm-context/types.js +9 -0
- package/dist/manifest/projections/llm-context/types.js.map +1 -0
- package/dist/manifest/projections/materialized-views/expression-to-sql.d.ts +29 -0
- package/dist/manifest/projections/materialized-views/expression-to-sql.d.ts.map +1 -0
- package/dist/manifest/projections/materialized-views/expression-to-sql.js +211 -0
- package/dist/manifest/projections/materialized-views/expression-to-sql.js.map +1 -0
- package/dist/manifest/projections/materialized-views/generator.d.ts +29 -0
- package/dist/manifest/projections/materialized-views/generator.d.ts.map +1 -0
- package/dist/manifest/projections/materialized-views/generator.js +263 -0
- package/dist/manifest/projections/materialized-views/generator.js.map +1 -0
- package/dist/manifest/projections/materialized-views/options.d.ts +59 -0
- package/dist/manifest/projections/materialized-views/options.d.ts.map +1 -0
- package/dist/manifest/projections/materialized-views/options.js +34 -0
- package/dist/manifest/projections/materialized-views/options.js.map +1 -0
- package/dist/manifest/projections/materialized-views/types.d.ts +100 -0
- package/dist/manifest/projections/materialized-views/types.d.ts.map +1 -0
- package/dist/manifest/projections/materialized-views/types.js +11 -0
- package/dist/manifest/projections/materialized-views/types.js.map +1 -0
- package/dist/manifest/projections/mermaid/generator.d.ts +46 -0
- package/dist/manifest/projections/mermaid/generator.d.ts.map +1 -0
- package/dist/manifest/projections/mermaid/generator.js +436 -0
- package/dist/manifest/projections/mermaid/generator.js.map +1 -0
- package/dist/manifest/projections/mongoose/options.d.ts +30 -0
- package/dist/manifest/projections/mongoose/options.d.ts.map +1 -0
- package/dist/manifest/projections/mongoose/options.js +18 -0
- package/dist/manifest/projections/mongoose/options.js.map +1 -0
- package/dist/manifest/projections/mongoose/type-mapping.d.ts +42 -0
- package/dist/manifest/projections/mongoose/type-mapping.d.ts.map +1 -0
- package/dist/manifest/projections/mongoose/type-mapping.js +64 -0
- package/dist/manifest/projections/mongoose/type-mapping.js.map +1 -0
- package/dist/manifest/projections/nextjs/defaults.d.ts +161 -0
- package/dist/manifest/projections/nextjs/defaults.d.ts.map +1 -0
- package/dist/manifest/projections/nextjs/defaults.js +157 -0
- package/dist/manifest/projections/nextjs/defaults.js.map +1 -0
- package/dist/manifest/projections/nextjs/generator.d.ts +78 -0
- package/dist/manifest/projections/nextjs/generator.d.ts.map +1 -0
- package/dist/manifest/projections/nextjs/generator.js +1339 -0
- package/dist/manifest/projections/nextjs/generator.js.map +1 -0
- package/dist/manifest/projections/nextjs/schedule-generator.d.ts +10 -0
- package/dist/manifest/projections/nextjs/schedule-generator.d.ts.map +1 -0
- package/dist/manifest/projections/nextjs/schedule-generator.js +54 -0
- package/dist/manifest/projections/nextjs/schedule-generator.js.map +1 -0
- package/dist/manifest/projections/openapi/generator.d.ts +37 -0
- package/dist/manifest/projections/openapi/generator.d.ts.map +1 -0
- package/dist/manifest/projections/openapi/generator.js +690 -0
- package/dist/manifest/projections/openapi/generator.js.map +1 -0
- package/dist/manifest/projections/openapi/index.d.ts +11 -0
- package/dist/manifest/projections/openapi/index.d.ts.map +1 -0
- package/dist/manifest/projections/openapi/index.js +10 -0
- package/dist/manifest/projections/openapi/index.js.map +1 -0
- package/dist/manifest/projections/openapi/types.d.ts +75 -0
- package/dist/manifest/projections/openapi/types.d.ts.map +1 -0
- package/dist/manifest/projections/openapi/types.js +9 -0
- package/dist/manifest/projections/openapi/types.js.map +1 -0
- package/dist/manifest/projections/prisma/generator.d.ts +32 -0
- package/dist/manifest/projections/prisma/generator.d.ts.map +1 -0
- package/dist/manifest/projections/prisma/generator.js +861 -0
- package/dist/manifest/projections/prisma/generator.js.map +1 -0
- package/dist/manifest/projections/prisma/index.d.ts +12 -0
- package/dist/manifest/projections/prisma/index.d.ts.map +1 -0
- package/dist/manifest/projections/prisma/index.js +12 -0
- package/dist/manifest/projections/prisma/index.js.map +1 -0
- package/dist/manifest/projections/prisma/options.d.ts +243 -0
- package/dist/manifest/projections/prisma/options.d.ts.map +1 -0
- package/dist/manifest/projections/prisma/options.js +59 -0
- package/dist/manifest/projections/prisma/options.js.map +1 -0
- package/dist/manifest/projections/prisma/type-mapping.d.ts +60 -0
- package/dist/manifest/projections/prisma/type-mapping.d.ts.map +1 -0
- package/dist/manifest/projections/prisma/type-mapping.js +95 -0
- package/dist/manifest/projections/prisma/type-mapping.js.map +1 -0
- package/dist/manifest/projections/prisma-store/generator.d.ts +12 -0
- package/dist/manifest/projections/prisma-store/generator.d.ts.map +1 -0
- package/dist/manifest/projections/prisma-store/generator.js +52 -0
- package/dist/manifest/projections/prisma-store/generator.js.map +1 -0
- package/dist/manifest/projections/prisma-store/metadata-builder.d.ts +11 -0
- package/dist/manifest/projections/prisma-store/metadata-builder.d.ts.map +1 -0
- package/dist/manifest/projections/prisma-store/metadata-builder.js +183 -0
- package/dist/manifest/projections/prisma-store/metadata-builder.js.map +1 -0
- package/dist/manifest/projections/prisma-store/options.d.ts +31 -0
- package/dist/manifest/projections/prisma-store/options.d.ts.map +1 -0
- package/dist/manifest/projections/prisma-store/options.js +21 -0
- package/dist/manifest/projections/prisma-store/options.js.map +1 -0
- package/dist/manifest/projections/prisma-store/persistence.d.ts +4 -0
- package/dist/manifest/projections/prisma-store/persistence.d.ts.map +1 -0
- package/dist/manifest/projections/prisma-store/persistence.js +24 -0
- package/dist/manifest/projections/prisma-store/persistence.js.map +1 -0
- package/dist/manifest/projections/pydantic/generator.d.ts +27 -0
- package/dist/manifest/projections/pydantic/generator.d.ts.map +1 -0
- package/dist/manifest/projections/pydantic/generator.js +798 -0
- package/dist/manifest/projections/pydantic/generator.js.map +1 -0
- package/dist/manifest/projections/pydantic/types.d.ts +32 -0
- package/dist/manifest/projections/pydantic/types.d.ts.map +1 -0
- package/dist/manifest/projections/pydantic/types.js +5 -0
- package/dist/manifest/projections/pydantic/types.js.map +1 -0
- package/dist/manifest/projections/react-query/generator.d.ts +87 -0
- package/dist/manifest/projections/react-query/generator.d.ts.map +1 -0
- package/dist/manifest/projections/react-query/generator.js +413 -0
- package/dist/manifest/projections/react-query/generator.js.map +1 -0
- package/dist/manifest/projections/registry.d.ts +59 -0
- package/dist/manifest/projections/registry.d.ts.map +1 -0
- package/dist/manifest/projections/registry.js +110 -0
- package/dist/manifest/projections/registry.js.map +1 -0
- package/dist/manifest/projections/remix/generator.d.ts +58 -0
- package/dist/manifest/projections/remix/generator.d.ts.map +1 -0
- package/dist/manifest/projections/remix/generator.js +788 -0
- package/dist/manifest/projections/remix/generator.js.map +1 -0
- package/dist/manifest/projections/routes/generator.d.ts +32 -0
- package/dist/manifest/projections/routes/generator.d.ts.map +1 -0
- package/dist/manifest/projections/routes/generator.js +401 -0
- package/dist/manifest/projections/routes/generator.js.map +1 -0
- package/dist/manifest/projections/routes/types.d.ts +104 -0
- package/dist/manifest/projections/routes/types.d.ts.map +1 -0
- package/dist/manifest/projections/routes/types.js +11 -0
- package/dist/manifest/projections/routes/types.js.map +1 -0
- package/dist/manifest/projections/shared/naming.d.ts +64 -0
- package/dist/manifest/projections/shared/naming.d.ts.map +1 -0
- package/dist/manifest/projections/shared/naming.js +138 -0
- package/dist/manifest/projections/shared/naming.js.map +1 -0
- package/dist/manifest/projections/storybook/generator.d.ts +38 -0
- package/dist/manifest/projections/storybook/generator.d.ts.map +1 -0
- package/dist/manifest/projections/storybook/generator.js +462 -0
- package/dist/manifest/projections/storybook/generator.js.map +1 -0
- package/dist/manifest/projections/sveltekit/generator.d.ts +64 -0
- package/dist/manifest/projections/sveltekit/generator.d.ts.map +1 -0
- package/dist/manifest/projections/sveltekit/generator.js +1043 -0
- package/dist/manifest/projections/sveltekit/generator.js.map +1 -0
- package/dist/manifest/projections/sveltekit/index.d.ts +11 -0
- package/dist/manifest/projections/sveltekit/index.d.ts.map +1 -0
- package/dist/manifest/projections/sveltekit/index.js +10 -0
- package/dist/manifest/projections/sveltekit/index.js.map +1 -0
- package/dist/manifest/projections/sveltekit/types.d.ts +122 -0
- package/dist/manifest/projections/sveltekit/types.d.ts.map +1 -0
- package/dist/manifest/projections/sveltekit/types.js +10 -0
- package/dist/manifest/projections/sveltekit/types.js.map +1 -0
- package/dist/manifest/projections/terraform/generator.d.ts +29 -0
- package/dist/manifest/projections/terraform/generator.d.ts.map +1 -0
- package/dist/manifest/projections/terraform/generator.js +722 -0
- package/dist/manifest/projections/terraform/generator.js.map +1 -0
- package/dist/manifest/projections/terraform/index.d.ts +7 -0
- package/dist/manifest/projections/terraform/index.d.ts.map +1 -0
- package/dist/manifest/projections/terraform/index.js +7 -0
- package/dist/manifest/projections/terraform/index.js.map +1 -0
- package/dist/manifest/projections/terraform/options.d.ts +92 -0
- package/dist/manifest/projections/terraform/options.d.ts.map +1 -0
- package/dist/manifest/projections/terraform/options.js +37 -0
- package/dist/manifest/projections/terraform/options.js.map +1 -0
- package/dist/manifest/projections/terraform/types.d.ts +55 -0
- package/dist/manifest/projections/terraform/types.d.ts.map +1 -0
- package/dist/manifest/projections/terraform/types.js +38 -0
- package/dist/manifest/projections/terraform/types.js.map +1 -0
- package/dist/manifest/projections/zod/generator.d.ts +27 -0
- package/dist/manifest/projections/zod/generator.d.ts.map +1 -0
- package/dist/manifest/projections/zod/generator.js +367 -0
- package/dist/manifest/projections/zod/generator.js.map +1 -0
- package/dist/manifest/projections/zod/index.d.ts +8 -0
- package/dist/manifest/projections/zod/index.d.ts.map +1 -0
- package/dist/manifest/projections/zod/index.js +7 -0
- package/dist/manifest/projections/zod/index.js.map +1 -0
- package/dist/manifest/projections/zod/types.d.ts +14 -0
- package/dist/manifest/projections/zod/types.d.ts.map +1 -0
- package/dist/manifest/projections/zod/types.js +5 -0
- package/dist/manifest/projections/zod/types.js.map +1 -0
- package/dist/manifest/reaction-completeness-checks.d.ts +11 -0
- package/dist/manifest/reaction-completeness-checks.d.ts.map +1 -0
- package/dist/manifest/reaction-completeness-checks.js +106 -0
- package/dist/manifest/reaction-completeness-checks.js.map +1 -0
- package/dist/manifest/reaction-completeness.d.ts +9 -0
- package/dist/manifest/reaction-completeness.d.ts.map +1 -0
- package/dist/manifest/reaction-completeness.js +35 -0
- package/dist/manifest/reaction-completeness.js.map +1 -0
- package/dist/manifest/registry/emit.d.ts +53 -0
- package/dist/manifest/registry/emit.d.ts.map +1 -0
- package/dist/manifest/registry/emit.js +96 -0
- package/dist/manifest/registry/emit.js.map +1 -0
- package/dist/manifest/runtime-command-extensions.d.ts +21 -0
- package/dist/manifest/runtime-command-extensions.d.ts.map +1 -0
- package/dist/manifest/runtime-command-extensions.js +136 -0
- package/dist/manifest/runtime-command-extensions.js.map +1 -0
- package/dist/manifest/runtime-engine.d.ts +1019 -0
- package/dist/manifest/runtime-engine.d.ts.map +1 -0
- package/dist/manifest/runtime-engine.js +3872 -0
- package/dist/manifest/runtime-engine.js.map +1 -0
- package/dist/manifest/runtime-profiling-bridge.d.ts +22 -0
- package/dist/manifest/runtime-profiling-bridge.d.ts.map +1 -0
- package/dist/manifest/runtime-profiling-bridge.js +69 -0
- package/dist/manifest/runtime-profiling-bridge.js.map +1 -0
- package/dist/manifest/runtime-rate-limit.d.ts +52 -0
- package/dist/manifest/runtime-rate-limit.d.ts.map +1 -0
- package/dist/manifest/runtime-rate-limit.js +70 -0
- package/dist/manifest/runtime-rate-limit.js.map +1 -0
- package/dist/manifest/runtime-retry.d.ts +68 -0
- package/dist/manifest/runtime-retry.d.ts.map +1 -0
- package/dist/manifest/runtime-retry.js +93 -0
- package/dist/manifest/runtime-retry.js.map +1 -0
- package/dist/manifest/runtime-schedule.d.ts +47 -0
- package/dist/manifest/runtime-schedule.d.ts.map +1 -0
- package/dist/manifest/runtime-schedule.js +95 -0
- package/dist/manifest/runtime-schedule.js.map +1 -0
- package/dist/manifest/schedule-utils.d.ts +15 -0
- package/dist/manifest/schedule-utils.d.ts.map +1 -0
- package/dist/manifest/schedule-utils.js +82 -0
- package/dist/manifest/schedule-utils.js.map +1 -0
- package/dist/manifest/standalone-generator.d.ts +32 -0
- package/dist/manifest/standalone-generator.d.ts.map +1 -0
- package/dist/manifest/standalone-generator.js +596 -0
- package/dist/manifest/standalone-generator.js.map +1 -0
- package/dist/manifest/stores/prisma-generic/coercion.d.ts +17 -0
- package/dist/manifest/stores/prisma-generic/coercion.d.ts.map +1 -0
- package/dist/manifest/stores/prisma-generic/coercion.js +83 -0
- package/dist/manifest/stores/prisma-generic/coercion.js.map +1 -0
- package/dist/manifest/stores/prisma-generic/index.d.ts +3 -0
- package/dist/manifest/stores/prisma-generic/index.d.ts.map +1 -0
- package/dist/manifest/stores/prisma-generic/index.js +2 -0
- package/dist/manifest/stores/prisma-generic/index.js.map +1 -0
- package/dist/manifest/stores/prisma-generic/store.d.ts +35 -0
- package/dist/manifest/stores/prisma-generic/store.d.ts.map +1 -0
- package/dist/manifest/stores/prisma-generic/store.js +216 -0
- package/dist/manifest/stores/prisma-generic/store.js.map +1 -0
- package/dist/manifest/stores/prisma-generic/types.d.ts +46 -0
- package/dist/manifest/stores/prisma-generic/types.d.ts.map +1 -0
- package/dist/manifest/stores/prisma-generic/types.js +6 -0
- package/dist/manifest/stores/prisma-generic/types.js.map +1 -0
- package/dist/manifest/stores.node.d.ts +248 -0
- package/dist/manifest/stores.node.d.ts.map +1 -0
- package/dist/manifest/stores.node.js +718 -0
- package/dist/manifest/stores.node.js.map +1 -0
- package/dist/manifest/test/postgres-live-env.d.ts +9 -0
- package/dist/manifest/test/postgres-live-env.d.ts.map +1 -0
- package/dist/manifest/test/postgres-live-env.js +14 -0
- package/dist/manifest/test/postgres-live-env.js.map +1 -0
- package/dist/manifest/types.d.ts +570 -0
- package/dist/manifest/types.d.ts.map +1 -0
- package/dist/manifest/types.js +2 -0
- package/dist/manifest/types.js.map +1 -0
- package/dist/manifest/version.d.ts +15 -0
- package/dist/manifest/version.d.ts.map +1 -0
- package/dist/manifest/version.js +15 -0
- package/dist/manifest/version.js.map +1 -0
- package/dist/manifest/wasm/index.d.ts +15 -0
- package/dist/manifest/wasm/index.d.ts.map +1 -0
- package/dist/manifest/wasm/index.js +15 -0
- package/dist/manifest/wasm/index.js.map +1 -0
- package/dist/manifest/wasm/wasm-evaluator.d.ts +93 -0
- package/dist/manifest/wasm/wasm-evaluator.d.ts.map +1 -0
- package/dist/manifest/wasm/wasm-evaluator.js +242 -0
- package/dist/manifest/wasm/wasm-evaluator.js.map +1 -0
- package/dist/manifest/wasm/wasm-loader.d.ts +56 -0
- package/dist/manifest/wasm/wasm-loader.d.ts.map +1 -0
- package/dist/manifest/wasm/wasm-loader.js +132 -0
- package/dist/manifest/wasm/wasm-loader.js.map +1 -0
- package/docs/spec/config/manifest.config.schema.json +737 -0
- package/docs/spec/config/prisma-projection.schema.json +173 -0
- package/docs/spec/ir/ir-v1.schema.json +2033 -0
- package/docs/spec/plugins/plugin.schema.json +104 -0
- package/docs/spec/registry/bypasses.schema.json +87 -0
- package/docs/spec/registry/commands.schema.json +61 -0
- package/docs/spec/registry/entities.schema.json +52 -0
- package/docs/spec/semantics.md +630 -0
- package/package.json +356 -0
- package/packages/cli/dist/audit/bypass-violations.d.ts +16 -0
- package/packages/cli/dist/audit/bypass-violations.d.ts.map +1 -0
- package/packages/cli/dist/audit/bypass-violations.js +98 -0
- package/packages/cli/dist/audit/bypass-violations.js.map +1 -0
- package/packages/cli/dist/audit/direct-writes.d.ts +15 -0
- package/packages/cli/dist/audit/direct-writes.d.ts.map +1 -0
- package/packages/cli/dist/audit/direct-writes.js +86 -0
- package/packages/cli/dist/audit/direct-writes.js.map +1 -0
- package/packages/cli/dist/audit/event-fabrication.d.ts +17 -0
- package/packages/cli/dist/audit/event-fabrication.d.ts.map +1 -0
- package/packages/cli/dist/audit/event-fabrication.js +101 -0
- package/packages/cli/dist/audit/event-fabrication.js.map +1 -0
- package/packages/cli/dist/audit/existing-command-available.d.ts +20 -0
- package/packages/cli/dist/audit/existing-command-available.d.ts.map +1 -0
- package/packages/cli/dist/audit/existing-command-available.js +158 -0
- package/packages/cli/dist/audit/existing-command-available.js.map +1 -0
- package/packages/cli/dist/audit/missing-tests.d.ts +17 -0
- package/packages/cli/dist/audit/missing-tests.d.ts.map +1 -0
- package/packages/cli/dist/audit/missing-tests.js +108 -0
- package/packages/cli/dist/audit/missing-tests.js.map +1 -0
- package/packages/cli/dist/audit/route-drift.d.ts +16 -0
- package/packages/cli/dist/audit/route-drift.d.ts.map +1 -0
- package/packages/cli/dist/audit/route-drift.js +86 -0
- package/packages/cli/dist/audit/route-drift.js.map +1 -0
- package/packages/cli/dist/audit/types.d.ts +63 -0
- package/packages/cli/dist/audit/types.d.ts.map +1 -0
- package/packages/cli/dist/audit/types.js +10 -0
- package/packages/cli/dist/audit/types.js.map +1 -0
- package/packages/cli/dist/audit/unregistered-command-call.d.ts +25 -0
- package/packages/cli/dist/audit/unregistered-command-call.d.ts.map +1 -0
- package/packages/cli/dist/audit/unregistered-command-call.js +196 -0
- package/packages/cli/dist/audit/unregistered-command-call.js.map +1 -0
- package/packages/cli/dist/audit/unregistered-entity-write.d.ts +16 -0
- package/packages/cli/dist/audit/unregistered-entity-write.d.ts.map +1 -0
- package/packages/cli/dist/audit/unregistered-entity-write.js +96 -0
- package/packages/cli/dist/audit/unregistered-entity-write.js.map +1 -0
- package/packages/cli/dist/audit/write-receiver.d.ts +23 -0
- package/packages/cli/dist/audit/write-receiver.d.ts.map +1 -0
- package/packages/cli/dist/audit/write-receiver.js +32 -0
- package/packages/cli/dist/audit/write-receiver.js.map +1 -0
- package/packages/cli/dist/checks/dispatcher-presence.d.ts +37 -0
- package/packages/cli/dist/checks/dispatcher-presence.d.ts.map +1 -0
- package/packages/cli/dist/checks/dispatcher-presence.js +57 -0
- package/packages/cli/dist/checks/dispatcher-presence.js.map +1 -0
- package/packages/cli/dist/checks/package-shape.d.ts +81 -0
- package/packages/cli/dist/checks/package-shape.d.ts.map +1 -0
- package/packages/cli/dist/checks/package-shape.js +359 -0
- package/packages/cli/dist/checks/package-shape.js.map +1 -0
- package/packages/cli/dist/checks/runtime-smoke.d.ts +42 -0
- package/packages/cli/dist/checks/runtime-smoke.d.ts.map +1 -0
- package/packages/cli/dist/checks/runtime-smoke.js +167 -0
- package/packages/cli/dist/checks/runtime-smoke.js.map +1 -0
- package/packages/cli/dist/commands/analyze.d.ts +97 -0
- package/packages/cli/dist/commands/analyze.d.ts.map +1 -0
- package/packages/cli/dist/commands/analyze.js +411 -0
- package/packages/cli/dist/commands/analyze.js.map +1 -0
- package/packages/cli/dist/commands/audit-bypasses.d.ts +31 -0
- package/packages/cli/dist/commands/audit-bypasses.d.ts.map +1 -0
- package/packages/cli/dist/commands/audit-bypasses.js +152 -0
- package/packages/cli/dist/commands/audit-bypasses.js.map +1 -0
- package/packages/cli/dist/commands/audit-constitution.d.ts +20 -0
- package/packages/cli/dist/commands/audit-constitution.d.ts.map +1 -0
- package/packages/cli/dist/commands/audit-constitution.js +18 -0
- package/packages/cli/dist/commands/audit-constitution.js.map +1 -0
- package/packages/cli/dist/commands/audit-governance.d.ts +37 -0
- package/packages/cli/dist/commands/audit-governance.d.ts.map +1 -0
- package/packages/cli/dist/commands/audit-governance.js +78 -0
- package/packages/cli/dist/commands/audit-governance.js.map +1 -0
- package/packages/cli/dist/commands/audit-routes.d.ts +135 -0
- package/packages/cli/dist/commands/audit-routes.d.ts.map +1 -0
- package/packages/cli/dist/commands/audit-routes.js +514 -0
- package/packages/cli/dist/commands/audit-routes.js.map +1 -0
- package/packages/cli/dist/commands/breaking-change.d.ts +15 -0
- package/packages/cli/dist/commands/breaking-change.d.ts.map +1 -0
- package/packages/cli/dist/commands/breaking-change.js +150 -0
- package/packages/cli/dist/commands/breaking-change.js.map +1 -0
- package/packages/cli/dist/commands/build.d.ts +26 -0
- package/packages/cli/dist/commands/build.d.ts.map +1 -0
- package/packages/cli/dist/commands/build.js +58 -0
- package/packages/cli/dist/commands/build.js.map +1 -0
- package/packages/cli/dist/commands/changelog.d.ts +26 -0
- package/packages/cli/dist/commands/changelog.d.ts.map +1 -0
- package/packages/cli/dist/commands/changelog.js +365 -0
- package/packages/cli/dist/commands/changelog.js.map +1 -0
- package/packages/cli/dist/commands/check.d.ts +21 -0
- package/packages/cli/dist/commands/check.d.ts.map +1 -0
- package/packages/cli/dist/commands/check.js +31 -0
- package/packages/cli/dist/commands/check.js.map +1 -0
- package/packages/cli/dist/commands/compile.d.ts +19 -0
- package/packages/cli/dist/commands/compile.d.ts.map +1 -0
- package/packages/cli/dist/commands/compile.js +325 -0
- package/packages/cli/dist/commands/compile.js.map +1 -0
- package/packages/cli/dist/commands/config.d.ts +23 -0
- package/packages/cli/dist/commands/config.d.ts.map +1 -0
- package/packages/cli/dist/commands/config.js +172 -0
- package/packages/cli/dist/commands/config.js.map +1 -0
- package/packages/cli/dist/commands/coverage.d.ts +55 -0
- package/packages/cli/dist/commands/coverage.d.ts.map +1 -0
- package/packages/cli/dist/commands/coverage.js +343 -0
- package/packages/cli/dist/commands/coverage.js.map +1 -0
- package/packages/cli/dist/commands/diagram.d.ts +22 -0
- package/packages/cli/dist/commands/diagram.d.ts.map +1 -0
- package/packages/cli/dist/commands/diagram.js +176 -0
- package/packages/cli/dist/commands/diagram.js.map +1 -0
- package/packages/cli/dist/commands/docs.d.ts +18 -0
- package/packages/cli/dist/commands/docs.d.ts.map +1 -0
- package/packages/cli/dist/commands/docs.js +722 -0
- package/packages/cli/dist/commands/docs.js.map +1 -0
- package/packages/cli/dist/commands/doctor-lib.d.ts +147 -0
- package/packages/cli/dist/commands/doctor-lib.d.ts.map +1 -0
- package/packages/cli/dist/commands/doctor-lib.js +491 -0
- package/packages/cli/dist/commands/doctor-lib.js.map +1 -0
- package/packages/cli/dist/commands/doctor.d.ts +31 -0
- package/packages/cli/dist/commands/doctor.d.ts.map +1 -0
- package/packages/cli/dist/commands/doctor.js +628 -0
- package/packages/cli/dist/commands/doctor.js.map +1 -0
- package/packages/cli/dist/commands/emit-registries.d.ts +33 -0
- package/packages/cli/dist/commands/emit-registries.d.ts.map +1 -0
- package/packages/cli/dist/commands/emit-registries.js +109 -0
- package/packages/cli/dist/commands/emit-registries.js.map +1 -0
- package/packages/cli/dist/commands/enforce-surface.d.ts +62 -0
- package/packages/cli/dist/commands/enforce-surface.d.ts.map +1 -0
- package/packages/cli/dist/commands/enforce-surface.js +172 -0
- package/packages/cli/dist/commands/enforce-surface.js.map +1 -0
- package/packages/cli/dist/commands/fmt.d.ts +17 -0
- package/packages/cli/dist/commands/fmt.d.ts.map +1 -0
- package/packages/cli/dist/commands/fmt.js +129 -0
- package/packages/cli/dist/commands/fmt.js.map +1 -0
- package/packages/cli/dist/commands/gen-tests.d.ts +41 -0
- package/packages/cli/dist/commands/gen-tests.d.ts.map +1 -0
- package/packages/cli/dist/commands/gen-tests.js +370 -0
- package/packages/cli/dist/commands/gen-tests.js.map +1 -0
- package/packages/cli/dist/commands/generate-from-prompt.d.ts +34 -0
- package/packages/cli/dist/commands/generate-from-prompt.d.ts.map +1 -0
- package/packages/cli/dist/commands/generate-from-prompt.js +990 -0
- package/packages/cli/dist/commands/generate-from-prompt.js.map +1 -0
- package/packages/cli/dist/commands/generate.d.ts +36 -0
- package/packages/cli/dist/commands/generate.d.ts.map +1 -0
- package/packages/cli/dist/commands/generate.js +371 -0
- package/packages/cli/dist/commands/generate.js.map +1 -0
- package/packages/cli/dist/commands/harness.d.ts +13 -0
- package/packages/cli/dist/commands/harness.d.ts.map +1 -0
- package/packages/cli/dist/commands/harness.js +276 -0
- package/packages/cli/dist/commands/harness.js.map +1 -0
- package/packages/cli/dist/commands/init-ci.d.ts +19 -0
- package/packages/cli/dist/commands/init-ci.d.ts.map +1 -0
- package/packages/cli/dist/commands/init-ci.js +148 -0
- package/packages/cli/dist/commands/init-ci.js.map +1 -0
- package/packages/cli/dist/commands/init.d.ts +33 -0
- package/packages/cli/dist/commands/init.d.ts.map +1 -0
- package/packages/cli/dist/commands/init.js +157 -0
- package/packages/cli/dist/commands/init.js.map +1 -0
- package/packages/cli/dist/commands/install-hooks.d.ts +29 -0
- package/packages/cli/dist/commands/install-hooks.d.ts.map +1 -0
- package/packages/cli/dist/commands/install-hooks.js +139 -0
- package/packages/cli/dist/commands/install-hooks.js.map +1 -0
- package/packages/cli/dist/commands/integration-check.d.ts +62 -0
- package/packages/cli/dist/commands/integration-check.d.ts.map +1 -0
- package/packages/cli/dist/commands/integration-check.js +298 -0
- package/packages/cli/dist/commands/integration-check.js.map +1 -0
- package/packages/cli/dist/commands/ir-diff.d.ts +8 -0
- package/packages/cli/dist/commands/ir-diff.d.ts.map +1 -0
- package/packages/cli/dist/commands/ir-diff.js +199 -0
- package/packages/cli/dist/commands/ir-diff.js.map +1 -0
- package/packages/cli/dist/commands/lint-routes.d.ts +56 -0
- package/packages/cli/dist/commands/lint-routes.d.ts.map +1 -0
- package/packages/cli/dist/commands/lint-routes.js +228 -0
- package/packages/cli/dist/commands/lint-routes.js.map +1 -0
- package/packages/cli/dist/commands/load-test.d.ts +61 -0
- package/packages/cli/dist/commands/load-test.d.ts.map +1 -0
- package/packages/cli/dist/commands/load-test.js +675 -0
- package/packages/cli/dist/commands/load-test.js.map +1 -0
- package/packages/cli/dist/commands/migrate.d.ts +21 -0
- package/packages/cli/dist/commands/migrate.d.ts.map +1 -0
- package/packages/cli/dist/commands/migrate.js +264 -0
- package/packages/cli/dist/commands/migrate.js.map +1 -0
- package/packages/cli/dist/commands/mock.d.ts +79 -0
- package/packages/cli/dist/commands/mock.d.ts.map +1 -0
- package/packages/cli/dist/commands/mock.js +324 -0
- package/packages/cli/dist/commands/mock.js.map +1 -0
- package/packages/cli/dist/commands/pack-unpack.d.ts +30 -0
- package/packages/cli/dist/commands/pack-unpack.d.ts.map +1 -0
- package/packages/cli/dist/commands/pack-unpack.js +108 -0
- package/packages/cli/dist/commands/pack-unpack.js.map +1 -0
- package/packages/cli/dist/commands/preflight.d.ts +31 -0
- package/packages/cli/dist/commands/preflight.d.ts.map +1 -0
- package/packages/cli/dist/commands/preflight.js +246 -0
- package/packages/cli/dist/commands/preflight.js.map +1 -0
- package/packages/cli/dist/commands/profile.d.ts +30 -0
- package/packages/cli/dist/commands/profile.d.ts.map +1 -0
- package/packages/cli/dist/commands/profile.js +201 -0
- package/packages/cli/dist/commands/profile.js.map +1 -0
- package/packages/cli/dist/commands/repl.d.ts +16 -0
- package/packages/cli/dist/commands/repl.d.ts.map +1 -0
- package/packages/cli/dist/commands/repl.js +772 -0
- package/packages/cli/dist/commands/repl.js.map +1 -0
- package/packages/cli/dist/commands/routes.d.ts +24 -0
- package/packages/cli/dist/commands/routes.d.ts.map +1 -0
- package/packages/cli/dist/commands/routes.js +144 -0
- package/packages/cli/dist/commands/routes.js.map +1 -0
- package/packages/cli/dist/commands/scan.d.ts +23 -0
- package/packages/cli/dist/commands/scan.d.ts.map +1 -0
- package/packages/cli/dist/commands/scan.js +722 -0
- package/packages/cli/dist/commands/scan.js.map +1 -0
- package/packages/cli/dist/commands/seed.d.ts +35 -0
- package/packages/cli/dist/commands/seed.d.ts.map +1 -0
- package/packages/cli/dist/commands/seed.js +503 -0
- package/packages/cli/dist/commands/seed.js.map +1 -0
- package/packages/cli/dist/commands/validate-ai-ajv.d.ts +4 -0
- package/packages/cli/dist/commands/validate-ai-ajv.d.ts.map +1 -0
- package/packages/cli/dist/commands/validate-ai-ajv.js +78 -0
- package/packages/cli/dist/commands/validate-ai-ajv.js.map +1 -0
- package/packages/cli/dist/commands/validate-ai-compiler.d.ts +5 -0
- package/packages/cli/dist/commands/validate-ai-compiler.d.ts.map +1 -0
- package/packages/cli/dist/commands/validate-ai-compiler.js +8 -0
- package/packages/cli/dist/commands/validate-ai-compiler.js.map +1 -0
- package/packages/cli/dist/commands/validate-ai-report.d.ts +5 -0
- package/packages/cli/dist/commands/validate-ai-report.d.ts.map +1 -0
- package/packages/cli/dist/commands/validate-ai-report.js +80 -0
- package/packages/cli/dist/commands/validate-ai-report.js.map +1 -0
- package/packages/cli/dist/commands/validate-ai-resolve-inputs.d.ts +6 -0
- package/packages/cli/dist/commands/validate-ai-resolve-inputs.d.ts.map +1 -0
- package/packages/cli/dist/commands/validate-ai-resolve-inputs.js +54 -0
- package/packages/cli/dist/commands/validate-ai-resolve-inputs.js.map +1 -0
- package/packages/cli/dist/commands/validate-ai-semantic-checks.d.ts +4 -0
- package/packages/cli/dist/commands/validate-ai-semantic-checks.d.ts.map +1 -0
- package/packages/cli/dist/commands/validate-ai-semantic-checks.js +218 -0
- package/packages/cli/dist/commands/validate-ai-semantic-checks.js.map +1 -0
- package/packages/cli/dist/commands/validate-ai-types.d.ts +41 -0
- package/packages/cli/dist/commands/validate-ai-types.d.ts.map +1 -0
- package/packages/cli/dist/commands/validate-ai-types.js +2 -0
- package/packages/cli/dist/commands/validate-ai-types.js.map +1 -0
- package/packages/cli/dist/commands/validate-ai-validate-file.d.ts +5 -0
- package/packages/cli/dist/commands/validate-ai-validate-file.d.ts.map +1 -0
- package/packages/cli/dist/commands/validate-ai-validate-file.js +126 -0
- package/packages/cli/dist/commands/validate-ai-validate-file.js.map +1 -0
- package/packages/cli/dist/commands/validate-ai.d.ts +15 -0
- package/packages/cli/dist/commands/validate-ai.d.ts.map +1 -0
- package/packages/cli/dist/commands/validate-ai.js +124 -0
- package/packages/cli/dist/commands/validate-ai.js.map +1 -0
- package/packages/cli/dist/commands/validate.d.ts +15 -0
- package/packages/cli/dist/commands/validate.d.ts.map +1 -0
- package/packages/cli/dist/commands/validate.js +205 -0
- package/packages/cli/dist/commands/validate.js.map +1 -0
- package/packages/cli/dist/commands/versions.d.ts +56 -0
- package/packages/cli/dist/commands/versions.d.ts.map +1 -0
- package/packages/cli/dist/commands/versions.js +427 -0
- package/packages/cli/dist/commands/versions.js.map +1 -0
- package/packages/cli/dist/commands/watch.d.ts +34 -0
- package/packages/cli/dist/commands/watch.d.ts.map +1 -0
- package/packages/cli/dist/commands/watch.js +253 -0
- package/packages/cli/dist/commands/watch.js.map +1 -0
- package/packages/cli/dist/index.d.ts +14 -0
- package/packages/cli/dist/index.d.ts.map +1 -0
- package/packages/cli/dist/index.js +1159 -0
- package/packages/cli/dist/index.js.map +1 -0
- package/packages/cli/dist/utils/config-validate.d.ts +48 -0
- package/packages/cli/dist/utils/config-validate.d.ts.map +1 -0
- package/packages/cli/dist/utils/config-validate.js +116 -0
- package/packages/cli/dist/utils/config-validate.js.map +1 -0
- package/packages/cli/dist/utils/config.d.ts +360 -0
- package/packages/cli/dist/utils/config.d.ts.map +1 -0
- package/packages/cli/dist/utils/config.js +567 -0
- package/packages/cli/dist/utils/config.js.map +1 -0
- package/packages/cli/dist/utils/schema.d.ts +8 -0
- package/packages/cli/dist/utils/schema.d.ts.map +1 -0
- package/packages/cli/dist/utils/schema.js +13 -0
- package/packages/cli/dist/utils/schema.js.map +1 -0
- package/packages/lsp-server/bin/manifest-lsp.js +3 -0
- package/packages/lsp-server/dist/compiler-bridge.d.ts +24 -0
- package/packages/lsp-server/dist/compiler-bridge.d.ts.map +1 -0
- package/packages/lsp-server/dist/compiler-bridge.js +32 -0
- package/packages/lsp-server/dist/compiler-bridge.js.map +1 -0
- package/packages/lsp-server/dist/document-store.d.ts +23 -0
- package/packages/lsp-server/dist/document-store.d.ts.map +1 -0
- package/packages/lsp-server/dist/document-store.js +40 -0
- package/packages/lsp-server/dist/document-store.js.map +1 -0
- package/packages/lsp-server/dist/features/completion.d.ts +23 -0
- package/packages/lsp-server/dist/features/completion.d.ts.map +1 -0
- package/packages/lsp-server/dist/features/completion.js +293 -0
- package/packages/lsp-server/dist/features/completion.js.map +1 -0
- package/packages/lsp-server/dist/features/definition.d.ts +9 -0
- package/packages/lsp-server/dist/features/definition.d.ts.map +1 -0
- package/packages/lsp-server/dist/features/definition.js +24 -0
- package/packages/lsp-server/dist/features/definition.js.map +1 -0
- package/packages/lsp-server/dist/features/diagnostics.d.ts +8 -0
- package/packages/lsp-server/dist/features/diagnostics.d.ts.map +1 -0
- package/packages/lsp-server/dist/features/diagnostics.js +40 -0
- package/packages/lsp-server/dist/features/diagnostics.js.map +1 -0
- package/packages/lsp-server/dist/features/document-symbols.d.ts +8 -0
- package/packages/lsp-server/dist/features/document-symbols.d.ts.map +1 -0
- package/packages/lsp-server/dist/features/document-symbols.js +164 -0
- package/packages/lsp-server/dist/features/document-symbols.js.map +1 -0
- package/packages/lsp-server/dist/features/hover.d.ts +9 -0
- package/packages/lsp-server/dist/features/hover.d.ts.map +1 -0
- package/packages/lsp-server/dist/features/hover.js +100 -0
- package/packages/lsp-server/dist/features/hover.js.map +1 -0
- package/packages/lsp-server/dist/index.d.ts +6 -0
- package/packages/lsp-server/dist/index.d.ts.map +1 -0
- package/packages/lsp-server/dist/index.js +11 -0
- package/packages/lsp-server/dist/index.js.map +1 -0
- package/packages/lsp-server/dist/position-utils.d.ts +25 -0
- package/packages/lsp-server/dist/position-utils.d.ts.map +1 -0
- package/packages/lsp-server/dist/position-utils.js +39 -0
- package/packages/lsp-server/dist/position-utils.js.map +1 -0
- package/packages/lsp-server/dist/server.d.ts +14 -0
- package/packages/lsp-server/dist/server.d.ts.map +1 -0
- package/packages/lsp-server/dist/server.js +96 -0
- package/packages/lsp-server/dist/server.js.map +1 -0
- package/packages/lsp-server/dist/symbols/builtin-docs.d.ts +34 -0
- package/packages/lsp-server/dist/symbols/builtin-docs.d.ts.map +1 -0
- package/packages/lsp-server/dist/symbols/builtin-docs.js +193 -0
- package/packages/lsp-server/dist/symbols/builtin-docs.js.map +1 -0
- package/packages/lsp-server/dist/symbols/symbol-index.d.ts +18 -0
- package/packages/lsp-server/dist/symbols/symbol-index.d.ts.map +1 -0
- package/packages/lsp-server/dist/symbols/symbol-index.js +107 -0
- package/packages/lsp-server/dist/symbols/symbol-index.js.map +1 -0
- package/packages/mcp-server/bin/manifest-mcp.js +27 -0
- package/packages/mcp-server/dist/index.d.ts +24 -0
- package/packages/mcp-server/dist/index.d.ts.map +1 -0
- package/packages/mcp-server/dist/index.js +36 -0
- package/packages/mcp-server/dist/index.js.map +1 -0
- package/packages/mcp-server/dist/resources/ir-cache.d.ts +9 -0
- package/packages/mcp-server/dist/resources/ir-cache.d.ts.map +1 -0
- package/packages/mcp-server/dist/resources/ir-cache.js +47 -0
- package/packages/mcp-server/dist/resources/ir-cache.js.map +1 -0
- package/packages/mcp-server/dist/resources/ir-schema.d.ts +8 -0
- package/packages/mcp-server/dist/resources/ir-schema.d.ts.map +1 -0
- package/packages/mcp-server/dist/resources/ir-schema.js +39 -0
- package/packages/mcp-server/dist/resources/ir-schema.js.map +1 -0
- package/packages/mcp-server/dist/resources/semantics.d.ts +8 -0
- package/packages/mcp-server/dist/resources/semantics.d.ts.map +1 -0
- package/packages/mcp-server/dist/resources/semantics.js +38 -0
- package/packages/mcp-server/dist/resources/semantics.js.map +1 -0
- package/packages/mcp-server/dist/server.d.ts +7 -0
- package/packages/mcp-server/dist/server.d.ts.map +1 -0
- package/packages/mcp-server/dist/server.js +22 -0
- package/packages/mcp-server/dist/server.js.map +1 -0
- package/packages/mcp-server/dist/state/session-store.d.ts +39 -0
- package/packages/mcp-server/dist/state/session-store.d.ts.map +1 -0
- package/packages/mcp-server/dist/state/session-store.js +58 -0
- package/packages/mcp-server/dist/state/session-store.js.map +1 -0
- package/packages/mcp-server/dist/tools/compile.d.ts +30 -0
- package/packages/mcp-server/dist/tools/compile.d.ts.map +1 -0
- package/packages/mcp-server/dist/tools/compile.js +54 -0
- package/packages/mcp-server/dist/tools/compile.js.map +1 -0
- package/packages/mcp-server/dist/tools/execute.d.ts +112 -0
- package/packages/mcp-server/dist/tools/execute.d.ts.map +1 -0
- package/packages/mcp-server/dist/tools/execute.js +103 -0
- package/packages/mcp-server/dist/tools/execute.js.map +1 -0
- package/packages/mcp-server/dist/tools/explain.d.ts +25 -0
- package/packages/mcp-server/dist/tools/explain.d.ts.map +1 -0
- package/packages/mcp-server/dist/tools/explain.js +254 -0
- package/packages/mcp-server/dist/tools/explain.js.map +1 -0
- package/packages/mcp-server/dist/tools/validate.d.ts +23 -0
- package/packages/mcp-server/dist/tools/validate.d.ts.map +1 -0
- package/packages/mcp-server/dist/tools/validate.js +38 -0
- package/packages/mcp-server/dist/tools/validate.js.map +1 -0
- package/src/manifest/audit/sinks/postgres.sql +51 -0
- package/src/manifest/outbox/stores/postgres.sql +61 -0
|
@@ -0,0 +1,3872 @@
|
|
|
1
|
+
import { dateOf, timeOf, datetimeOf, isValidDateString, isValidTimeString } from './date-time.js';
|
|
2
|
+
import { applyMaskStrategy } from './masking.js';
|
|
3
|
+
import { RateLimiter } from './runtime-rate-limit.js';
|
|
4
|
+
import { checkRateLimitGate, executeWithRetry, policyHasRateLimit, } from './runtime-command-extensions.js';
|
|
5
|
+
import { getSchedulesFromIR } from './runtime-schedule.js';
|
|
6
|
+
import { RuntimeProfilingBridge } from './runtime-profiling-bridge.js';
|
|
7
|
+
// Note: PostgresStore and SupabaseStore are in stores.node.ts for server-side use only.
|
|
8
|
+
// This file is browser-safe and only includes MemoryStore and LocalStorageStore.
|
|
9
|
+
/**
|
|
10
|
+
* Detect if running in production mode.
|
|
11
|
+
* Checks NODE_ENV environment variable (server-side) or global location (browser).
|
|
12
|
+
* In browsers, defaults to development since there's no standard production detection.
|
|
13
|
+
*/
|
|
14
|
+
function isProductionMode() {
|
|
15
|
+
// Server-side: check process.env.NODE_ENV
|
|
16
|
+
if (typeof process !== 'undefined' && process.env?.NODE_ENV === 'production') {
|
|
17
|
+
return true;
|
|
18
|
+
}
|
|
19
|
+
// Browser: no standard production detection, default to development
|
|
20
|
+
// for safety. Users can explicitly set requireValidProvenance in browser apps.
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Thrown when an adapter action (persist/publish/effect) is executed in deterministicMode.
|
|
25
|
+
* This is a programming error, not a domain failure.
|
|
26
|
+
* See docs/spec/adapters.md for the normative exception to default no-op behavior.
|
|
27
|
+
*/
|
|
28
|
+
export class ManifestEffectBoundaryError extends Error {
|
|
29
|
+
actionKind;
|
|
30
|
+
constructor(actionKind) {
|
|
31
|
+
super(`Action '${actionKind}' is not allowed in deterministicMode. ` +
|
|
32
|
+
`Adapter actions (persist/publish/effect) must be handled externally. ` +
|
|
33
|
+
`See docs/spec/adapters.md.`);
|
|
34
|
+
this.name = 'ManifestEffectBoundaryError';
|
|
35
|
+
this.actionKind = actionKind;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Thrown when reaction cascading exceeds the maximum depth (default: 10).
|
|
40
|
+
* Indicates a potential infinite loop in reaction chains.
|
|
41
|
+
* See docs/spec/semantics.md § "Reactions".
|
|
42
|
+
*/
|
|
43
|
+
export class ManifestReactionDepthError extends Error {
|
|
44
|
+
depth;
|
|
45
|
+
triggerEvent;
|
|
46
|
+
targetCommand;
|
|
47
|
+
constructor(depth, triggerEvent, targetCommand) {
|
|
48
|
+
super(`Reaction depth limit (${depth}) exceeded. ` +
|
|
49
|
+
`Event '${triggerEvent}' → command '${targetCommand}' would exceed max depth. ` +
|
|
50
|
+
`Check for circular reaction chains.`);
|
|
51
|
+
this.name = 'ManifestReactionDepthError';
|
|
52
|
+
this.depth = depth;
|
|
53
|
+
this.triggerEvent = triggerEvent;
|
|
54
|
+
this.targetCommand = targetCommand;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* In-memory JobQueue implementation for async commands.
|
|
59
|
+
* Suitable for testing and development. Production deployments should
|
|
60
|
+
* provide a durable implementation (e.g. database-backed).
|
|
61
|
+
*/
|
|
62
|
+
export class MemoryJobQueue {
|
|
63
|
+
jobs = [];
|
|
64
|
+
async enqueue(job) {
|
|
65
|
+
this.jobs.push({ ...job });
|
|
66
|
+
}
|
|
67
|
+
async drainPending() {
|
|
68
|
+
const pending = this.jobs.filter(j => j.status === 'pending');
|
|
69
|
+
for (const job of pending) {
|
|
70
|
+
job.status = 'running';
|
|
71
|
+
}
|
|
72
|
+
return pending;
|
|
73
|
+
}
|
|
74
|
+
async updateStatus(jobId, status, detail) {
|
|
75
|
+
const job = this.jobs.find(j => j.jobId === jobId);
|
|
76
|
+
if (job) {
|
|
77
|
+
job.status = status;
|
|
78
|
+
if (detail) {
|
|
79
|
+
job.result = detail.result;
|
|
80
|
+
job.error = detail.error;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
/** Test utility: get all jobs */
|
|
85
|
+
getAll() {
|
|
86
|
+
return [...this.jobs];
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Thrown when expression evaluation exceeds configured depth or step limits.
|
|
91
|
+
* This is a domain failure (caught and converted to CommandResult), not a programming error.
|
|
92
|
+
* See docs/spec/manifest-vnext.md § "Diagnostic Payload Bounding".
|
|
93
|
+
*/
|
|
94
|
+
export class EvaluationBudgetExceededError extends Error {
|
|
95
|
+
limitType;
|
|
96
|
+
limit;
|
|
97
|
+
constructor(limitType, limit) {
|
|
98
|
+
super(`Evaluation budget exceeded: ${limitType} limit ${limit} reached`);
|
|
99
|
+
this.name = 'EvaluationBudgetExceededError';
|
|
100
|
+
this.limitType = limitType;
|
|
101
|
+
this.limit = limit;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
class MemoryStore {
|
|
105
|
+
items = new Map();
|
|
106
|
+
generateId;
|
|
107
|
+
constructor(generateId) {
|
|
108
|
+
this.generateId = generateId || (() => crypto.randomUUID());
|
|
109
|
+
}
|
|
110
|
+
async getAll() {
|
|
111
|
+
return Array.from(this.items.values());
|
|
112
|
+
}
|
|
113
|
+
async getById(id) {
|
|
114
|
+
return this.items.get(id);
|
|
115
|
+
}
|
|
116
|
+
async create(data) {
|
|
117
|
+
const id = data.id || this.generateId();
|
|
118
|
+
const item = { ...data, id };
|
|
119
|
+
this.items.set(id, item);
|
|
120
|
+
return item;
|
|
121
|
+
}
|
|
122
|
+
async update(id, data) {
|
|
123
|
+
const existing = this.items.get(id);
|
|
124
|
+
if (!existing)
|
|
125
|
+
return undefined;
|
|
126
|
+
const updated = { ...existing, ...data, id };
|
|
127
|
+
this.items.set(id, updated);
|
|
128
|
+
return updated;
|
|
129
|
+
}
|
|
130
|
+
async delete(id) {
|
|
131
|
+
return this.items.delete(id);
|
|
132
|
+
}
|
|
133
|
+
async clear() {
|
|
134
|
+
this.items.clear();
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
class LocalStorageStore {
|
|
138
|
+
key;
|
|
139
|
+
constructor(key) {
|
|
140
|
+
this.key = key;
|
|
141
|
+
}
|
|
142
|
+
load() {
|
|
143
|
+
try {
|
|
144
|
+
const data = localStorage.getItem(this.key);
|
|
145
|
+
return data ? JSON.parse(data) : [];
|
|
146
|
+
}
|
|
147
|
+
catch {
|
|
148
|
+
return [];
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
save(items) {
|
|
152
|
+
localStorage.setItem(this.key, JSON.stringify(items));
|
|
153
|
+
}
|
|
154
|
+
async getAll() {
|
|
155
|
+
return this.load();
|
|
156
|
+
}
|
|
157
|
+
async getById(id) {
|
|
158
|
+
return this.load().find(item => item.id === id);
|
|
159
|
+
}
|
|
160
|
+
async create(data) {
|
|
161
|
+
const items = this.load();
|
|
162
|
+
const id = data.id || crypto.randomUUID();
|
|
163
|
+
const item = { ...data, id };
|
|
164
|
+
items.push(item);
|
|
165
|
+
this.save(items);
|
|
166
|
+
return item;
|
|
167
|
+
}
|
|
168
|
+
async update(id, data) {
|
|
169
|
+
const items = this.load();
|
|
170
|
+
const idx = items.findIndex(item => item.id === id);
|
|
171
|
+
if (idx === -1)
|
|
172
|
+
return undefined;
|
|
173
|
+
const updated = { ...items[idx], ...data, id };
|
|
174
|
+
items[idx] = updated;
|
|
175
|
+
this.save(items);
|
|
176
|
+
return updated;
|
|
177
|
+
}
|
|
178
|
+
async delete(id) {
|
|
179
|
+
const items = this.load();
|
|
180
|
+
const idx = items.findIndex(item => item.id === id);
|
|
181
|
+
if (idx === -1)
|
|
182
|
+
return false;
|
|
183
|
+
items.splice(idx, 1);
|
|
184
|
+
this.save(items);
|
|
185
|
+
return true;
|
|
186
|
+
}
|
|
187
|
+
async clear() {
|
|
188
|
+
localStorage.removeItem(this.key);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
export class RuntimeEngine {
|
|
192
|
+
ir;
|
|
193
|
+
context;
|
|
194
|
+
options;
|
|
195
|
+
stores = new Map();
|
|
196
|
+
eventListeners = [];
|
|
197
|
+
eventLog = [];
|
|
198
|
+
/** Current reaction nesting depth to prevent infinite loops */
|
|
199
|
+
reactionDepth = 0;
|
|
200
|
+
static MAX_REACTION_DEPTH = 10;
|
|
201
|
+
/** Index of relationships for efficient lookup during expression evaluation */
|
|
202
|
+
relationshipIndex = new Map();
|
|
203
|
+
/** Memoization cache for resolved relationships to avoid repeated store queries */
|
|
204
|
+
relationshipMemoCache = new Map();
|
|
205
|
+
/** Index of roles by name for O(1) permission checks */
|
|
206
|
+
roleIndex = new Map();
|
|
207
|
+
/** Track whether version has been incremented for the current command execution */
|
|
208
|
+
versionIncrementedForCommand = false;
|
|
209
|
+
/** Track instances that were just created (to prevent version increment on subsequent mutate actions) */
|
|
210
|
+
justCreatedInstanceIds = new Set();
|
|
211
|
+
/**
|
|
212
|
+
* Command-scoped write buffer. While set, mutate/compute actions apply their
|
|
213
|
+
* changes to an in-memory working copy (`instance`) and accumulate a single
|
|
214
|
+
* store-form `patch` instead of issuing one store read + write per action.
|
|
215
|
+
* The buffer is flushed in one `store.update` at the end of the action loop,
|
|
216
|
+
* then cleared — so a command that mutates N fields performs one read and one
|
|
217
|
+
* write rather than N. Scoped to the command's target instance only; nested
|
|
218
|
+
* (reaction/fan-out) commands save and restore the outer buffer.
|
|
219
|
+
*/
|
|
220
|
+
commandBuffer = null;
|
|
221
|
+
/** Last transition validation error (set by updateInstance, checked by _executeCommandInternal) */
|
|
222
|
+
lastTransitionError = null;
|
|
223
|
+
/** Last concurrency conflict (set by updateInstance, checked by _executeCommandInternal) */
|
|
224
|
+
lastConcurrencyConflict = null;
|
|
225
|
+
/** Per-engine sliding-window rate limiter (in-memory; durable store is a follow-up) */
|
|
226
|
+
rateLimiter = new RateLimiter();
|
|
227
|
+
profilingBridge;
|
|
228
|
+
actionTraceCounter = 0;
|
|
229
|
+
/**
|
|
230
|
+
* In-process approval request cache, keyed by
|
|
231
|
+
* `${entity}:${instanceId}:${approvalName}`. Always maintained as a mirror
|
|
232
|
+
* so the synchronous `getApprovalRequest`/`expireApprovals` accessors work.
|
|
233
|
+
* When `options.approvalStore` is set, that store is the source of truth and
|
|
234
|
+
* this Map is just a write-through mirror; otherwise this Map IS the store.
|
|
235
|
+
*/
|
|
236
|
+
approvalRequests = new Map();
|
|
237
|
+
/**
|
|
238
|
+
* Load an approval request, preferring the durable store when configured.
|
|
239
|
+
* Refreshes the in-process mirror so synchronous accessors stay coherent.
|
|
240
|
+
*/
|
|
241
|
+
async loadApprovalState(key) {
|
|
242
|
+
const store = this.options.approvalStore;
|
|
243
|
+
if (store) {
|
|
244
|
+
const loaded = await store.load(key);
|
|
245
|
+
if (loaded)
|
|
246
|
+
this.approvalRequests.set(key, loaded);
|
|
247
|
+
else
|
|
248
|
+
this.approvalRequests.delete(key);
|
|
249
|
+
return loaded;
|
|
250
|
+
}
|
|
251
|
+
return this.approvalRequests.get(key);
|
|
252
|
+
}
|
|
253
|
+
/**
|
|
254
|
+
* Persist an approval request to the durable store (when configured) and
|
|
255
|
+
* always mirror it in-process so a later synchronous read sees it.
|
|
256
|
+
*/
|
|
257
|
+
async saveApprovalState(key, state) {
|
|
258
|
+
this.approvalRequests.set(key, state);
|
|
259
|
+
const store = this.options.approvalStore;
|
|
260
|
+
if (store)
|
|
261
|
+
await store.save(key, state);
|
|
262
|
+
}
|
|
263
|
+
/** Per-entry-point evaluation budget for bounded complexity enforcement */
|
|
264
|
+
evalBudget = null;
|
|
265
|
+
/** Cache for computed property values, keyed by "entityName:instanceId:propertyName" */
|
|
266
|
+
computedPropertyCache = new Map();
|
|
267
|
+
/** Request-scoped cache for computed properties (cleared per command) */
|
|
268
|
+
computedPropertyRequestCache = new Map();
|
|
269
|
+
/**
|
|
270
|
+
* Initialize evaluation budget if not already active (re-entrant safe).
|
|
271
|
+
* Returns true if this call initialized the budget (caller must clear it in finally).
|
|
272
|
+
* Returns false if budget was already active (caller should NOT clear it).
|
|
273
|
+
*/
|
|
274
|
+
initEvalBudget() {
|
|
275
|
+
if (this.evalBudget)
|
|
276
|
+
return false; // Already active — re-entrant call
|
|
277
|
+
this.evalBudget = {
|
|
278
|
+
depth: 0,
|
|
279
|
+
steps: 0,
|
|
280
|
+
maxDepth: this.options.evaluationLimits?.maxExpressionDepth ?? 64,
|
|
281
|
+
maxSteps: this.options.evaluationLimits?.maxEvaluationSteps ?? 10_000,
|
|
282
|
+
};
|
|
283
|
+
return true;
|
|
284
|
+
}
|
|
285
|
+
/** Clear evaluation budget (only call if initEvalBudget returned true) */
|
|
286
|
+
clearEvalBudget() {
|
|
287
|
+
this.evalBudget = null;
|
|
288
|
+
}
|
|
289
|
+
// ── Field-level encryption helpers ──────────────────────────────────
|
|
290
|
+
/**
|
|
291
|
+
* Returns the set of property names marked `encrypted` for the given entity.
|
|
292
|
+
* Cached per entity name since IR is immutable at runtime.
|
|
293
|
+
*/
|
|
294
|
+
encryptedPropertyNamesCache = new Map();
|
|
295
|
+
encryptedPropertyNames(entityName) {
|
|
296
|
+
let cached = this.encryptedPropertyNamesCache.get(entityName);
|
|
297
|
+
if (cached)
|
|
298
|
+
return cached;
|
|
299
|
+
const entity = this.getEntity(entityName);
|
|
300
|
+
cached = new Set();
|
|
301
|
+
if (entity) {
|
|
302
|
+
for (const prop of entity.properties) {
|
|
303
|
+
if (prop.modifiers.includes('encrypted')) {
|
|
304
|
+
cached.add(prop.name);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
this.encryptedPropertyNamesCache.set(entityName, cached);
|
|
309
|
+
return cached;
|
|
310
|
+
}
|
|
311
|
+
/**
|
|
312
|
+
* Encrypt property values before a store write.
|
|
313
|
+
* Returns a shallow copy with encrypted fields replaced by envelope JSON.
|
|
314
|
+
* No-op when encryptionProvider is not configured or entity has no encrypted fields.
|
|
315
|
+
*/
|
|
316
|
+
async encryptProperties(entityName, data) {
|
|
317
|
+
const provider = this.options.encryptionProvider;
|
|
318
|
+
if (!provider)
|
|
319
|
+
return data;
|
|
320
|
+
const names = this.encryptedPropertyNames(entityName);
|
|
321
|
+
if (names.size === 0)
|
|
322
|
+
return data;
|
|
323
|
+
const out = { ...data };
|
|
324
|
+
for (const name of names) {
|
|
325
|
+
if (!(name in out) || out[name] == null)
|
|
326
|
+
continue;
|
|
327
|
+
const plaintext = String(out[name]);
|
|
328
|
+
const { ciphertext, keyId } = await provider.encrypt(plaintext);
|
|
329
|
+
out[name] = JSON.stringify({ v: 1, kid: keyId, ct: ciphertext });
|
|
330
|
+
}
|
|
331
|
+
return out;
|
|
332
|
+
}
|
|
333
|
+
/**
|
|
334
|
+
* Decrypt property values after a store read.
|
|
335
|
+
* Returns a shallow copy with encrypted envelope JSON replaced by plaintext.
|
|
336
|
+
* No-op when encryptionProvider is not configured or entity has no encrypted fields.
|
|
337
|
+
*/
|
|
338
|
+
async decryptProperties(entityName, instance) {
|
|
339
|
+
const provider = this.options.encryptionProvider;
|
|
340
|
+
if (!provider)
|
|
341
|
+
return instance;
|
|
342
|
+
const names = this.encryptedPropertyNames(entityName);
|
|
343
|
+
if (names.size === 0)
|
|
344
|
+
return instance;
|
|
345
|
+
const out = { ...instance };
|
|
346
|
+
for (const name of names) {
|
|
347
|
+
const raw = out[name];
|
|
348
|
+
if (typeof raw !== 'string')
|
|
349
|
+
continue;
|
|
350
|
+
try {
|
|
351
|
+
const envelope = JSON.parse(raw);
|
|
352
|
+
if (envelope && envelope.v === 1 && envelope.kid && envelope.ct) {
|
|
353
|
+
out[name] = await provider.decrypt(envelope.ct, envelope.kid);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
catch {
|
|
357
|
+
// Not an envelope — leave value as-is (e.g. plaintext from before encryption was enabled)
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
return out;
|
|
361
|
+
}
|
|
362
|
+
/**
|
|
363
|
+
* Resolve the active tenant value from runtime context using the IR tenant
|
|
364
|
+
* config's contextPath. Returns undefined when no tenant declaration exists
|
|
365
|
+
* in the IR or the context lacks the value.
|
|
366
|
+
*/
|
|
367
|
+
resolveTenantValue() {
|
|
368
|
+
const tenantConfig = this.ir.tenant;
|
|
369
|
+
if (!tenantConfig)
|
|
370
|
+
return undefined;
|
|
371
|
+
const parts = tenantConfig.contextPath.split('.');
|
|
372
|
+
let current = undefined;
|
|
373
|
+
for (let i = 0; i < parts.length; i++) {
|
|
374
|
+
const part = parts[i];
|
|
375
|
+
if (i === 0) {
|
|
376
|
+
if (part === 'context')
|
|
377
|
+
current = this.context;
|
|
378
|
+
else if (part === 'user')
|
|
379
|
+
current = this.context.user;
|
|
380
|
+
else
|
|
381
|
+
current = this.context[part];
|
|
382
|
+
}
|
|
383
|
+
else {
|
|
384
|
+
if (current && typeof current === 'object') {
|
|
385
|
+
current = current[part];
|
|
386
|
+
}
|
|
387
|
+
else {
|
|
388
|
+
return undefined;
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
return typeof current === 'string' ? current : undefined;
|
|
393
|
+
}
|
|
394
|
+
constructor(ir, context = {}, options = {}) {
|
|
395
|
+
this.ir = ir;
|
|
396
|
+
this.context = context;
|
|
397
|
+
this.options = options;
|
|
398
|
+
this.profilingBridge = new RuntimeProfilingBridge(options.profiling);
|
|
399
|
+
this.initializeStores();
|
|
400
|
+
this.buildRelationshipIndex();
|
|
401
|
+
this.buildRoleIndex();
|
|
402
|
+
}
|
|
403
|
+
initializeStores() {
|
|
404
|
+
for (const entity of this.ir.entities) {
|
|
405
|
+
// First check if a storeProvider is configured and use it
|
|
406
|
+
if (this.options.storeProvider) {
|
|
407
|
+
const customStore = this.options.storeProvider(entity.name);
|
|
408
|
+
if (customStore) {
|
|
409
|
+
this.stores.set(entity.name, customStore);
|
|
410
|
+
continue;
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
// Fall back to default store initialization
|
|
414
|
+
const storeConfig = this.ir.stores.find(s => s.entity === entity.name);
|
|
415
|
+
let store;
|
|
416
|
+
if (storeConfig) {
|
|
417
|
+
switch (storeConfig.target) {
|
|
418
|
+
case 'localStorage': {
|
|
419
|
+
const key = storeConfig.config.key?.kind === 'string'
|
|
420
|
+
? storeConfig.config.key.value
|
|
421
|
+
: `${entity.name.toLowerCase()}s`;
|
|
422
|
+
store = new LocalStorageStore(key);
|
|
423
|
+
break;
|
|
424
|
+
}
|
|
425
|
+
case 'memory':
|
|
426
|
+
store = new MemoryStore(this.options.generateId);
|
|
427
|
+
break;
|
|
428
|
+
case 'postgres':
|
|
429
|
+
throw new Error(`PostgreSQL storage for entity '${entity.name}' is not available in browser environments. ` +
|
|
430
|
+
`Use 'memory' or 'localStorage' for browser, or provide a custom store via the storeProvider option. ` +
|
|
431
|
+
`For server-side use, import PostgresStore from stores.node.ts.`);
|
|
432
|
+
case 'supabase':
|
|
433
|
+
throw new Error(`Supabase storage for entity '${entity.name}' is not available in browser environments. ` +
|
|
434
|
+
`Use 'memory' or 'localStorage' for browser, or provide a custom store via the storeProvider option. ` +
|
|
435
|
+
`For server-side use, import SupabaseStore from stores.node.ts.`);
|
|
436
|
+
case 'mongodb':
|
|
437
|
+
throw new Error(`MongoDB storage for entity '${entity.name}' is not available in browser environments. ` +
|
|
438
|
+
`Use 'memory' or 'localStorage' for browser, or provide a custom store via the storeProvider option. ` +
|
|
439
|
+
`For server-side use, import MongoDBStore from stores.node.ts.`);
|
|
440
|
+
case 'durable':
|
|
441
|
+
// `'durable'` is a backend-neutral semantic signal — it intentionally does NOT
|
|
442
|
+
// map to any built-in store. Consumers MUST supply a custom store adapter via
|
|
443
|
+
// the storeProvider option (e.g. a Prisma-backed adapter). This is the deliberate
|
|
444
|
+
// handoff point: core stays backend-neutral. (Matches v0.9.0 behavior.)
|
|
445
|
+
throw new Error(`Entity '${entity.name}' declares 'store ... in durable' but no storeProvider is bound. ` +
|
|
446
|
+
`'durable' is backend-neutral and requires a runtime store adapter supplied via the storeProvider option.`);
|
|
447
|
+
default:
|
|
448
|
+
// Custom store adapter scheme — requires a storeProvider that handles this target.
|
|
449
|
+
// Plugin-registered adapters (e.g. 'redis', 'dynamodb') are resolved through the
|
|
450
|
+
// CompositeStoreProvider built by the plugin loader.
|
|
451
|
+
throw new Error(`Entity '${entity.name}' declares store target '${storeConfig.target}' but no storeProvider ` +
|
|
452
|
+
`returned a store for it. Custom store targets require a matching StoreAdapterPlugin registered ` +
|
|
453
|
+
`via the plugin API, or a storeProvider that handles the '${storeConfig.target}' scheme.`);
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
else {
|
|
457
|
+
store = new MemoryStore(this.options.generateId);
|
|
458
|
+
}
|
|
459
|
+
this.stores.set(entity.name, store);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
/**
|
|
463
|
+
* Build an index of all relationships for efficient lookup during expression evaluation.
|
|
464
|
+
* Maps "EntityName.relationshipName" to relationship metadata.
|
|
465
|
+
*/
|
|
466
|
+
buildRelationshipIndex() {
|
|
467
|
+
for (const entity of this.ir.entities) {
|
|
468
|
+
for (const rel of entity.relationships) {
|
|
469
|
+
const key = `${entity.name}.${rel.name}`;
|
|
470
|
+
this.relationshipIndex.set(key, {
|
|
471
|
+
entityName: entity.name,
|
|
472
|
+
relationshipName: rel.name,
|
|
473
|
+
kind: rel.kind,
|
|
474
|
+
targetEntity: rel.target,
|
|
475
|
+
// Only extract the FK field name for single-column FKs.
|
|
476
|
+
// Composite FKs (fields.length > 1) are left undefined so the runtime
|
|
477
|
+
// falls back to the `${relName}Id` convention rather than silently
|
|
478
|
+
// resolving by fields[0] alone and potentially returning the wrong row.
|
|
479
|
+
// Composite FK resolution requires a full store adapter (e.g. Prisma).
|
|
480
|
+
foreignKey: rel.foreignKey && rel.foreignKey.fields.length === 1
|
|
481
|
+
? rel.foreignKey.fields[0]
|
|
482
|
+
: undefined,
|
|
483
|
+
});
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
buildRoleIndex() {
|
|
488
|
+
if (this.ir.roles) {
|
|
489
|
+
for (const role of this.ir.roles) {
|
|
490
|
+
this.roleIndex.set(role.name, role);
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
/**
|
|
495
|
+
* Check if a role has a specific permission.
|
|
496
|
+
* Uses precomputed effectivePermissions for O(1) lookup.
|
|
497
|
+
* Unknown role → false (no permissive default, per house style).
|
|
498
|
+
*/
|
|
499
|
+
roleHasPermission(roleName, action, target) {
|
|
500
|
+
const role = this.roleIndex.get(roleName);
|
|
501
|
+
if (!role)
|
|
502
|
+
return false;
|
|
503
|
+
return role.effectivePermissions.some(p => {
|
|
504
|
+
const actionMatch = p.action === 'all' || p.action === action;
|
|
505
|
+
const targetMatch = p.target === undefined || p.target === target;
|
|
506
|
+
return actionMatch && targetMatch;
|
|
507
|
+
});
|
|
508
|
+
}
|
|
509
|
+
/**
|
|
510
|
+
* Clear the relationship memoization cache.
|
|
511
|
+
* Called at the start of each command execution to ensure fresh data.
|
|
512
|
+
*/
|
|
513
|
+
clearMemoCache() {
|
|
514
|
+
this.relationshipMemoCache.clear();
|
|
515
|
+
this.computedPropertyRequestCache.clear();
|
|
516
|
+
}
|
|
517
|
+
/**
|
|
518
|
+
* Resolve a relationship for a given instance.
|
|
519
|
+
* Uses memoization cache to avoid repeated store queries within a single command execution.
|
|
520
|
+
* @param entityName - The source entity name
|
|
521
|
+
* @param instance - The source instance (must have an id)
|
|
522
|
+
* @param relationshipName - The relationship name to resolve
|
|
523
|
+
* @returns For hasMany: array of related instances; for hasOne/belongsTo/ref: single instance or null
|
|
524
|
+
*/
|
|
525
|
+
async resolveRelationship(entityName, instance, relationshipName) {
|
|
526
|
+
const key = `${entityName}.${relationshipName}`;
|
|
527
|
+
const rel = this.relationshipIndex.get(key);
|
|
528
|
+
if (!rel) {
|
|
529
|
+
return null;
|
|
530
|
+
}
|
|
531
|
+
const sourceId = instance.id;
|
|
532
|
+
if (!sourceId) {
|
|
533
|
+
return null;
|
|
534
|
+
}
|
|
535
|
+
// Build cache key including instance ID for accurate memoization
|
|
536
|
+
const cacheKey = `${entityName}.${sourceId}.${relationshipName}`;
|
|
537
|
+
// Check cache first
|
|
538
|
+
const cached = this.relationshipMemoCache.get(cacheKey);
|
|
539
|
+
if (cached) {
|
|
540
|
+
return cached.result;
|
|
541
|
+
}
|
|
542
|
+
let result = null;
|
|
543
|
+
switch (rel.kind) {
|
|
544
|
+
case 'belongsTo':
|
|
545
|
+
case 'ref': {
|
|
546
|
+
// For belongsTo/ref: the foreign key on the source instance contains the target ID
|
|
547
|
+
const fkProperty = rel.foreignKey || `${rel.relationshipName}Id`;
|
|
548
|
+
const targetId = instance[fkProperty];
|
|
549
|
+
if (!targetId) {
|
|
550
|
+
result = null;
|
|
551
|
+
}
|
|
552
|
+
else {
|
|
553
|
+
result = await this.getInstanceRaw(rel.targetEntity, targetId) ?? null;
|
|
554
|
+
}
|
|
555
|
+
break;
|
|
556
|
+
}
|
|
557
|
+
case 'hasOne': {
|
|
558
|
+
// For hasOne: find the target instance where its belongsTo foreign key equals source ID
|
|
559
|
+
// We need to find the inverse relationship on the target entity
|
|
560
|
+
const targetEntity = this.getEntity(rel.targetEntity);
|
|
561
|
+
if (!targetEntity) {
|
|
562
|
+
result = null;
|
|
563
|
+
break;
|
|
564
|
+
}
|
|
565
|
+
// Find the inverse belongsTo relationship
|
|
566
|
+
const inverseRel = targetEntity.relationships.find(r => (r.kind === 'belongsTo' || r.kind === 'ref') &&
|
|
567
|
+
r.target === entityName);
|
|
568
|
+
if (inverseRel) {
|
|
569
|
+
// Use the inverse relationship's foreign key
|
|
570
|
+
const fkProperty = inverseRel.foreignKey?.fields[0] ?? `${inverseRel.name}Id`;
|
|
571
|
+
const allTargets = await this.getAllInstancesRaw(rel.targetEntity);
|
|
572
|
+
result = allTargets.find(t => t[fkProperty] === sourceId) ?? null;
|
|
573
|
+
}
|
|
574
|
+
else {
|
|
575
|
+
// Fallback: assume the foreign key is named after the source entity
|
|
576
|
+
const assumedFk = `${entityName.toLowerCase()}Id`;
|
|
577
|
+
const allTargets = await this.getAllInstancesRaw(rel.targetEntity);
|
|
578
|
+
result = allTargets.find(t => t[assumedFk] === sourceId) ?? null;
|
|
579
|
+
}
|
|
580
|
+
break;
|
|
581
|
+
}
|
|
582
|
+
case 'hasMany': {
|
|
583
|
+
// For hasMany: find all target instances where their belongsTo foreign key equals source ID
|
|
584
|
+
const targetEntity = this.getEntity(rel.targetEntity);
|
|
585
|
+
if (!targetEntity) {
|
|
586
|
+
result = [];
|
|
587
|
+
break;
|
|
588
|
+
}
|
|
589
|
+
// Find the inverse belongsTo relationship
|
|
590
|
+
const inverseRel = targetEntity.relationships.find(r => (r.kind === 'belongsTo' || r.kind === 'ref') &&
|
|
591
|
+
r.target === entityName);
|
|
592
|
+
if (inverseRel) {
|
|
593
|
+
const fkProperty = inverseRel.foreignKey?.fields[0] ?? `${inverseRel.name}Id`;
|
|
594
|
+
const allTargets = await this.getAllInstancesRaw(rel.targetEntity);
|
|
595
|
+
result = allTargets.filter(t => t[fkProperty] === sourceId);
|
|
596
|
+
}
|
|
597
|
+
else {
|
|
598
|
+
// Fallback: assume the foreign key is named after the source entity
|
|
599
|
+
const assumedFk = `${entityName.toLowerCase()}Id`;
|
|
600
|
+
const allTargets = await this.getAllInstancesRaw(rel.targetEntity);
|
|
601
|
+
result = allTargets.filter(t => t[assumedFk] === sourceId);
|
|
602
|
+
}
|
|
603
|
+
break;
|
|
604
|
+
}
|
|
605
|
+
default:
|
|
606
|
+
result = null;
|
|
607
|
+
}
|
|
608
|
+
// Enrich resolved instances with _entity metadata for chained traversal
|
|
609
|
+
// (e.g., self.order.customer.name follows Order → Customer → name)
|
|
610
|
+
if (result !== null) {
|
|
611
|
+
if (Array.isArray(result)) {
|
|
612
|
+
result = result.map(r => ({ ...r, _entity: rel.targetEntity }));
|
|
613
|
+
}
|
|
614
|
+
else {
|
|
615
|
+
result = { ...result, _entity: rel.targetEntity };
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
// Cache the result
|
|
619
|
+
this.relationshipMemoCache.set(cacheKey, {
|
|
620
|
+
result,
|
|
621
|
+
timestamp: this.getNow(),
|
|
622
|
+
});
|
|
623
|
+
return result;
|
|
624
|
+
}
|
|
625
|
+
getNow() {
|
|
626
|
+
return this.options.now ? this.options.now() : Date.now();
|
|
627
|
+
}
|
|
628
|
+
/**
|
|
629
|
+
* Generate a unique identifier for runtime-internal records (audit
|
|
630
|
+
* records, outbox entry ids). Uses the caller-supplied generator from
|
|
631
|
+
* RuntimeOptions when present; otherwise falls back to crypto.randomUUID.
|
|
632
|
+
* Distinct from `getBuiltins().uuid` only by intent — keeping a named
|
|
633
|
+
* helper avoids leaking the fallback chain across call sites.
|
|
634
|
+
*/
|
|
635
|
+
nextRuntimeId() {
|
|
636
|
+
return this.options.generateId ? this.options.generateId() : crypto.randomUUID();
|
|
637
|
+
}
|
|
638
|
+
getBuiltins() {
|
|
639
|
+
// Custom builtins from plugins are spread first; core builtins override
|
|
640
|
+
// any name collision so reserved names cannot be replaced.
|
|
641
|
+
const custom = this.options.customBuiltins;
|
|
642
|
+
return {
|
|
643
|
+
...(custom ? Object.fromEntries(custom) : undefined),
|
|
644
|
+
// Core builtins
|
|
645
|
+
now: () => this.getNow(),
|
|
646
|
+
uuid: () => this.options.generateId ? this.options.generateId() : crypto.randomUUID(),
|
|
647
|
+
// String builtins
|
|
648
|
+
trim: (s) => typeof s === 'string' ? s.trim() : s,
|
|
649
|
+
split: (s, sep) => typeof s === 'string' ? s.split(sep) : s,
|
|
650
|
+
count: (v) => Array.isArray(v) ? v.length : v,
|
|
651
|
+
startsWith: (s, prefix) => typeof s === 'string' ? s.startsWith(prefix) : false,
|
|
652
|
+
endsWith: (s, suffix) => typeof s === 'string' ? s.endsWith(suffix) : false,
|
|
653
|
+
replace: (s, search, replacement) => typeof s === 'string' ? s.replace(new RegExp(search.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g'), () => replacement) : s,
|
|
654
|
+
toUpperCase: (s) => typeof s === 'string' ? s.toUpperCase() : s,
|
|
655
|
+
toLowerCase: (s) => typeof s === 'string' ? s.toLowerCase() : s,
|
|
656
|
+
length: (v) => {
|
|
657
|
+
if (typeof v === 'string')
|
|
658
|
+
return v.length;
|
|
659
|
+
if (Array.isArray(v))
|
|
660
|
+
return v.length;
|
|
661
|
+
return v;
|
|
662
|
+
},
|
|
663
|
+
substring: (s, start, end) => typeof s === 'string'
|
|
664
|
+
? (end !== undefined ? s.substring(start, end) : s.substring(start))
|
|
665
|
+
: s,
|
|
666
|
+
indexOf: (s, search) => typeof s === 'string' ? s.indexOf(search) : -1,
|
|
667
|
+
matches: (s, pattern) => {
|
|
668
|
+
if (typeof s !== 'string' || typeof pattern !== 'string')
|
|
669
|
+
return false;
|
|
670
|
+
try {
|
|
671
|
+
return new RegExp(pattern).test(s);
|
|
672
|
+
}
|
|
673
|
+
catch {
|
|
674
|
+
return false;
|
|
675
|
+
}
|
|
676
|
+
},
|
|
677
|
+
search: (text, query) => {
|
|
678
|
+
if (typeof text !== 'string' || typeof query !== 'string')
|
|
679
|
+
return false;
|
|
680
|
+
const tokenize = (s) => s.toLowerCase().split(/[^a-z0-9]+/i).filter(Boolean);
|
|
681
|
+
const haystack = new Set(tokenize(text));
|
|
682
|
+
const needles = tokenize(query);
|
|
683
|
+
if (needles.length === 0)
|
|
684
|
+
return false;
|
|
685
|
+
return needles.every(n => haystack.has(n));
|
|
686
|
+
},
|
|
687
|
+
// Math builtins
|
|
688
|
+
abs: (v) => typeof v === 'number' ? Math.abs(v) : v,
|
|
689
|
+
round: (v) => typeof v === 'number' ? Math.round(v) : v,
|
|
690
|
+
floor: (v) => typeof v === 'number' ? Math.floor(v) : v,
|
|
691
|
+
ceil: (v) => typeof v === 'number' ? Math.ceil(v) : v,
|
|
692
|
+
min: (...args) => {
|
|
693
|
+
const nums = args.filter((a) => typeof a === 'number');
|
|
694
|
+
return nums.length > 0 ? Math.min(...nums) : undefined;
|
|
695
|
+
},
|
|
696
|
+
max: (...args) => {
|
|
697
|
+
const nums = args.filter((a) => typeof a === 'number');
|
|
698
|
+
return nums.length > 0 ? Math.max(...nums) : undefined;
|
|
699
|
+
},
|
|
700
|
+
between: (value, low, high) => typeof value === 'number' && typeof low === 'number' && typeof high === 'number'
|
|
701
|
+
? value >= low && value <= high
|
|
702
|
+
: false,
|
|
703
|
+
// Array / aggregate builtins
|
|
704
|
+
sum: (arr, mapper) => {
|
|
705
|
+
if (Array.isArray(arr)) {
|
|
706
|
+
if (typeof mapper === 'function') {
|
|
707
|
+
return (async () => {
|
|
708
|
+
let total = 0;
|
|
709
|
+
for (const element of arr) {
|
|
710
|
+
const v = await Promise.resolve(mapper(element));
|
|
711
|
+
if (typeof v === 'number')
|
|
712
|
+
total += v;
|
|
713
|
+
}
|
|
714
|
+
return total;
|
|
715
|
+
})();
|
|
716
|
+
}
|
|
717
|
+
return arr.reduce((acc, v) => typeof v === 'number' ? acc + v : acc, 0);
|
|
718
|
+
}
|
|
719
|
+
return arr;
|
|
720
|
+
},
|
|
721
|
+
avg: (arr, mapper) => {
|
|
722
|
+
if (Array.isArray(arr) && arr.length > 0) {
|
|
723
|
+
if (typeof mapper === 'function') {
|
|
724
|
+
return (async () => {
|
|
725
|
+
let total = 0;
|
|
726
|
+
let count = 0;
|
|
727
|
+
for (const element of arr) {
|
|
728
|
+
const v = await Promise.resolve(mapper(element));
|
|
729
|
+
if (typeof v === 'number') {
|
|
730
|
+
total += v;
|
|
731
|
+
count++;
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
return count > 0 ? total / count : 0;
|
|
735
|
+
})();
|
|
736
|
+
}
|
|
737
|
+
const nums = arr.filter((v) => typeof v === 'number');
|
|
738
|
+
return nums.length > 0 ? nums.reduce((a, b) => a + b, 0) / nums.length : 0;
|
|
739
|
+
}
|
|
740
|
+
return 0;
|
|
741
|
+
},
|
|
742
|
+
min_of: (arr, mapper) => {
|
|
743
|
+
if (Array.isArray(arr) && arr.length > 0) {
|
|
744
|
+
if (typeof mapper === 'function') {
|
|
745
|
+
return (async () => {
|
|
746
|
+
let result;
|
|
747
|
+
for (const element of arr) {
|
|
748
|
+
const v = await Promise.resolve(mapper(element));
|
|
749
|
+
if (typeof v === 'number' && (result === undefined || v < result))
|
|
750
|
+
result = v;
|
|
751
|
+
}
|
|
752
|
+
return result;
|
|
753
|
+
})();
|
|
754
|
+
}
|
|
755
|
+
const nums = arr.filter((v) => typeof v === 'number');
|
|
756
|
+
return nums.length > 0 ? Math.min(...nums) : undefined;
|
|
757
|
+
}
|
|
758
|
+
return undefined;
|
|
759
|
+
},
|
|
760
|
+
max_of: (arr, mapper) => {
|
|
761
|
+
if (Array.isArray(arr) && arr.length > 0) {
|
|
762
|
+
if (typeof mapper === 'function') {
|
|
763
|
+
return (async () => {
|
|
764
|
+
let result;
|
|
765
|
+
for (const element of arr) {
|
|
766
|
+
const v = await Promise.resolve(mapper(element));
|
|
767
|
+
if (typeof v === 'number' && (result === undefined || v > result))
|
|
768
|
+
result = v;
|
|
769
|
+
}
|
|
770
|
+
return result;
|
|
771
|
+
})();
|
|
772
|
+
}
|
|
773
|
+
const nums = arr.filter((v) => typeof v === 'number');
|
|
774
|
+
return nums.length > 0 ? Math.max(...nums) : undefined;
|
|
775
|
+
}
|
|
776
|
+
return undefined;
|
|
777
|
+
},
|
|
778
|
+
count_of: (arr, predicate) => {
|
|
779
|
+
if (Array.isArray(arr)) {
|
|
780
|
+
if (typeof predicate === 'function') {
|
|
781
|
+
return (async () => {
|
|
782
|
+
let count = 0;
|
|
783
|
+
for (const element of arr) {
|
|
784
|
+
const v = await Promise.resolve(predicate(element));
|
|
785
|
+
if (v)
|
|
786
|
+
count++;
|
|
787
|
+
}
|
|
788
|
+
return count;
|
|
789
|
+
})();
|
|
790
|
+
}
|
|
791
|
+
return arr.length;
|
|
792
|
+
}
|
|
793
|
+
return 0;
|
|
794
|
+
},
|
|
795
|
+
filter: (arr, predicate) => {
|
|
796
|
+
if (Array.isArray(arr) && typeof predicate === 'function') {
|
|
797
|
+
return (async () => {
|
|
798
|
+
const result = [];
|
|
799
|
+
for (const element of arr) {
|
|
800
|
+
const v = await Promise.resolve(predicate(element));
|
|
801
|
+
if (v)
|
|
802
|
+
result.push(element);
|
|
803
|
+
}
|
|
804
|
+
return result;
|
|
805
|
+
})();
|
|
806
|
+
}
|
|
807
|
+
return Array.isArray(arr) ? arr : [];
|
|
808
|
+
},
|
|
809
|
+
map: (arr, mapper) => {
|
|
810
|
+
if (Array.isArray(arr) && typeof mapper === 'function') {
|
|
811
|
+
return (async () => {
|
|
812
|
+
const result = [];
|
|
813
|
+
for (const element of arr) {
|
|
814
|
+
result.push(await Promise.resolve(mapper(element)));
|
|
815
|
+
}
|
|
816
|
+
return result;
|
|
817
|
+
})();
|
|
818
|
+
}
|
|
819
|
+
return Array.isArray(arr) ? arr : [];
|
|
820
|
+
},
|
|
821
|
+
// Date builtins (UTC components; ts is milliseconds since epoch)
|
|
822
|
+
year: (ts) => typeof ts === 'number' ? new Date(ts).getUTCFullYear() : ts,
|
|
823
|
+
month: (ts) => typeof ts === 'number' ? new Date(ts).getUTCMonth() + 1 : ts,
|
|
824
|
+
day: (ts) => typeof ts === 'number' ? new Date(ts).getUTCDate() : ts,
|
|
825
|
+
hours: (ts) => typeof ts === 'number' ? new Date(ts).getUTCHours() : ts,
|
|
826
|
+
minutes: (ts) => typeof ts === 'number' ? new Date(ts).getUTCMinutes() : ts,
|
|
827
|
+
seconds: (ts) => typeof ts === 'number' ? new Date(ts).getUTCSeconds() : ts,
|
|
828
|
+
// Date/time primitive builtins (pure, UTC-only).
|
|
829
|
+
// Convention note: the legacy `year`..`seconds` builtins above pass non-number
|
|
830
|
+
// input through unchanged; these newer builtins return null on invalid input
|
|
831
|
+
// (non-number, NaN, or Infinity).
|
|
832
|
+
dateOf: (ts) => dateOf(ts),
|
|
833
|
+
timeOf: (ts) => timeOf(ts),
|
|
834
|
+
datetimeOf: (d, t) => datetimeOf(d, t),
|
|
835
|
+
addDuration: (ts, d) => typeof ts === 'number' && Number.isFinite(ts) && typeof d === 'number' && Number.isFinite(d) ? ts + d : null,
|
|
836
|
+
durationBetween: (a, b) => typeof a === 'number' && Number.isFinite(a) && typeof b === 'number' && Number.isFinite(b) ? b - a : null,
|
|
837
|
+
durationDays: (n) => typeof n === 'number' && Number.isFinite(n) ? n * 86400000 : null,
|
|
838
|
+
durationHours: (n) => typeof n === 'number' && Number.isFinite(n) ? n * 3600000 : null,
|
|
839
|
+
durationMinutes: (n) => typeof n === 'number' && Number.isFinite(n) ? n * 60000 : null,
|
|
840
|
+
durationSeconds: (n) => typeof n === 'number' && Number.isFinite(n) ? n * 1000 : null,
|
|
841
|
+
// Feature flag builtin
|
|
842
|
+
flag: (name) => {
|
|
843
|
+
if (typeof name !== 'string')
|
|
844
|
+
return false;
|
|
845
|
+
if (this.options.flagProvider) {
|
|
846
|
+
return this.options.flagProvider(name);
|
|
847
|
+
}
|
|
848
|
+
return false;
|
|
849
|
+
},
|
|
850
|
+
// Role hierarchy builtins
|
|
851
|
+
hasPermission: (action, target) => {
|
|
852
|
+
if (typeof action !== 'string')
|
|
853
|
+
return false;
|
|
854
|
+
const roleName = this.context.user?.role;
|
|
855
|
+
if (typeof roleName !== 'string')
|
|
856
|
+
return false;
|
|
857
|
+
return this.roleHasPermission(roleName, action, typeof target === 'string' ? target : undefined);
|
|
858
|
+
},
|
|
859
|
+
roleAllows: (roleName, action, target) => {
|
|
860
|
+
if (typeof roleName !== 'string' || typeof action !== 'string')
|
|
861
|
+
return false;
|
|
862
|
+
return this.roleHasPermission(roleName, action, typeof target === 'string' ? target : undefined);
|
|
863
|
+
},
|
|
864
|
+
};
|
|
865
|
+
}
|
|
866
|
+
getIR() {
|
|
867
|
+
return this.ir;
|
|
868
|
+
}
|
|
869
|
+
/**
|
|
870
|
+
* Get the provenance metadata from the IR
|
|
871
|
+
*/
|
|
872
|
+
getProvenance() {
|
|
873
|
+
return this.ir.provenance;
|
|
874
|
+
}
|
|
875
|
+
/**
|
|
876
|
+
* Log provenance information at startup
|
|
877
|
+
* This can be called by UI code to display provenance
|
|
878
|
+
*/
|
|
879
|
+
logProvenance() {
|
|
880
|
+
const prov = this.getProvenance();
|
|
881
|
+
if (!prov) {
|
|
882
|
+
console.warn('[Manifest Runtime] No provenance information found in IR.');
|
|
883
|
+
return;
|
|
884
|
+
}
|
|
885
|
+
// Provenance information is available via getProvenance() for programmatic access
|
|
886
|
+
}
|
|
887
|
+
/**
|
|
888
|
+
* Verify the IR integrity by checking that the computed hash matches the expected hash.
|
|
889
|
+
* Returns true if verification passes, false otherwise.
|
|
890
|
+
*
|
|
891
|
+
* @param expectedHash - Optional expected hash. If not provided, uses the IR's self-reported irHash
|
|
892
|
+
* @returns true if hash matches or if no hash is available to verify
|
|
893
|
+
*/
|
|
894
|
+
async verifyIRHash(expectedHash) {
|
|
895
|
+
const prov = this.ir.provenance;
|
|
896
|
+
if (!prov) {
|
|
897
|
+
console.warn('[Manifest Runtime] No provenance information found, cannot verify IR hash.');
|
|
898
|
+
return false;
|
|
899
|
+
}
|
|
900
|
+
const targetHash = expectedHash || prov.irHash;
|
|
901
|
+
if (!targetHash) {
|
|
902
|
+
console.warn('[Manifest Runtime] No IR hash available for verification.');
|
|
903
|
+
return false;
|
|
904
|
+
}
|
|
905
|
+
try {
|
|
906
|
+
// Compute hash of the current IR (excluding the irHash field itself)
|
|
907
|
+
const { irHash: _irHash, ...provenanceWithoutIrHash } = prov;
|
|
908
|
+
const canonical = {
|
|
909
|
+
...this.ir,
|
|
910
|
+
provenance: provenanceWithoutIrHash,
|
|
911
|
+
};
|
|
912
|
+
// Use deterministic JSON serialization with recursive key sorting (same as compiler).
|
|
913
|
+
// A replacer function sorts object keys at every nesting level to ensure
|
|
914
|
+
// the recomputed hash matches the compiler's hash for unmodified IR.
|
|
915
|
+
const json = JSON.stringify(canonical, (_key, value) => {
|
|
916
|
+
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
|
917
|
+
const sorted = {};
|
|
918
|
+
for (const k of Object.keys(value).sort()) {
|
|
919
|
+
sorted[k] = value[k];
|
|
920
|
+
}
|
|
921
|
+
return sorted;
|
|
922
|
+
}
|
|
923
|
+
return value;
|
|
924
|
+
});
|
|
925
|
+
const encoder = new TextEncoder();
|
|
926
|
+
const data = encoder.encode(json);
|
|
927
|
+
const hashBuffer = await crypto.subtle.digest('SHA-256', data);
|
|
928
|
+
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
|
929
|
+
const computedHash = hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
|
|
930
|
+
const isValid = computedHash === targetHash;
|
|
931
|
+
if (!isValid) {
|
|
932
|
+
console.error(`[Manifest Runtime] IR hash verification failed!\n` +
|
|
933
|
+
` Expected: ${targetHash}\n` +
|
|
934
|
+
` Computed: ${computedHash}\n` +
|
|
935
|
+
` The IR may have been tampered with or modified since compilation.`);
|
|
936
|
+
}
|
|
937
|
+
return isValid;
|
|
938
|
+
}
|
|
939
|
+
catch (error) {
|
|
940
|
+
console.error('[Manifest Runtime] Error during IR hash verification:', error);
|
|
941
|
+
return false;
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
/**
|
|
945
|
+
* Verify IR and throw if invalid. Use this when requireValidProvenance is true.
|
|
946
|
+
* @throws Error if IR hash verification fails
|
|
947
|
+
*/
|
|
948
|
+
async assertValidProvenance() {
|
|
949
|
+
if (this.options.requireValidProvenance) {
|
|
950
|
+
const isValid = await this.verifyIRHash(this.options.expectedIRHash);
|
|
951
|
+
if (!isValid) {
|
|
952
|
+
throw new Error('IR provenance verification failed. The IR may have been modified since compilation. ' +
|
|
953
|
+
'This runtime requires valid provenance for execution.');
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
getContext() {
|
|
958
|
+
return this.context;
|
|
959
|
+
}
|
|
960
|
+
setContext(ctx) {
|
|
961
|
+
this.context = { ...this.context, ...ctx };
|
|
962
|
+
}
|
|
963
|
+
replaceContext(ctx) {
|
|
964
|
+
this.context = { ...ctx };
|
|
965
|
+
}
|
|
966
|
+
getEntities() {
|
|
967
|
+
return this.ir.entities;
|
|
968
|
+
}
|
|
969
|
+
getEntity(name) {
|
|
970
|
+
return this.ir.entities.find(e => e.name === name);
|
|
971
|
+
}
|
|
972
|
+
getCommands() {
|
|
973
|
+
return this.ir.commands;
|
|
974
|
+
}
|
|
975
|
+
getCommand(name, entityName) {
|
|
976
|
+
if (entityName) {
|
|
977
|
+
const entity = this.getEntity(entityName);
|
|
978
|
+
if (!entity || !entity.commands.includes(name))
|
|
979
|
+
return undefined;
|
|
980
|
+
return this.ir.commands.find(c => c.name === name && c.entity === entityName);
|
|
981
|
+
}
|
|
982
|
+
return this.ir.commands.find(c => c.name === name);
|
|
983
|
+
}
|
|
984
|
+
getPolicies() {
|
|
985
|
+
return this.ir.policies;
|
|
986
|
+
}
|
|
987
|
+
/** Return all schedule declarations from the compiled IR. */
|
|
988
|
+
getSchedules() {
|
|
989
|
+
return Array.from(getSchedulesFromIR(this.ir).values());
|
|
990
|
+
}
|
|
991
|
+
/**
|
|
992
|
+
* Run a named schedule: evaluate bound params and dispatch the target command.
|
|
993
|
+
* Sets context.source to 'schedule' and context.scheduleName for the invocation.
|
|
994
|
+
*/
|
|
995
|
+
async runSchedule(scheduleName, options = {}) {
|
|
996
|
+
const schedule = getSchedulesFromIR(this.ir).get(scheduleName);
|
|
997
|
+
if (!schedule) {
|
|
998
|
+
return {
|
|
999
|
+
success: false,
|
|
1000
|
+
error: `Schedule '${scheduleName}' not found`,
|
|
1001
|
+
emittedEvents: [],
|
|
1002
|
+
};
|
|
1003
|
+
}
|
|
1004
|
+
const input = {};
|
|
1005
|
+
const now = this.getNow();
|
|
1006
|
+
const paramContext = {
|
|
1007
|
+
...this.buildEvalContext({}, undefined, schedule.entityName),
|
|
1008
|
+
now,
|
|
1009
|
+
};
|
|
1010
|
+
if (schedule.params) {
|
|
1011
|
+
for (const param of schedule.params) {
|
|
1012
|
+
input[param.name] = await this.evaluateExpression(param.expression, paramContext);
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
const prevSource = this.context.source;
|
|
1016
|
+
const prevScheduleName = this.context.scheduleName;
|
|
1017
|
+
this.context.source = 'schedule';
|
|
1018
|
+
this.context.scheduleName = scheduleName;
|
|
1019
|
+
try {
|
|
1020
|
+
return await this.runCommand(schedule.commandName, input, {
|
|
1021
|
+
...(schedule.entityName ? { entityName: schedule.entityName } : {}),
|
|
1022
|
+
...(options.correlationId !== undefined ? { correlationId: options.correlationId } : {}),
|
|
1023
|
+
...(options.causationId !== undefined ? { causationId: options.causationId } : {}),
|
|
1024
|
+
});
|
|
1025
|
+
}
|
|
1026
|
+
finally {
|
|
1027
|
+
this.context.source = prevSource;
|
|
1028
|
+
if (prevScheduleName !== undefined) {
|
|
1029
|
+
this.context.scheduleName = prevScheduleName;
|
|
1030
|
+
}
|
|
1031
|
+
else {
|
|
1032
|
+
delete this.context.scheduleName;
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
getStore(entityName) {
|
|
1037
|
+
return this.stores.get(entityName);
|
|
1038
|
+
}
|
|
1039
|
+
/**
|
|
1040
|
+
* Get collected command profiles when profiling is enabled.
|
|
1041
|
+
* Returns an empty array when profiling is not configured.
|
|
1042
|
+
*/
|
|
1043
|
+
getProfiles() {
|
|
1044
|
+
return [...this.profilingBridge.getProfiles()];
|
|
1045
|
+
}
|
|
1046
|
+
/**
|
|
1047
|
+
* Execute middleware registered for a given hook.
|
|
1048
|
+
* Returns a short-circuit result if any middleware short-circuits,
|
|
1049
|
+
* or undefined to continue normal execution.
|
|
1050
|
+
*/
|
|
1051
|
+
async runMiddleware(hook, command, evalContext, input, options, emittedEvents = []) {
|
|
1052
|
+
const middlewares = this.options.middleware;
|
|
1053
|
+
if (!middlewares || middlewares.length === 0)
|
|
1054
|
+
return undefined;
|
|
1055
|
+
for (const mw of middlewares) {
|
|
1056
|
+
if (!mw.hooks.includes(hook))
|
|
1057
|
+
continue;
|
|
1058
|
+
const ctx = {
|
|
1059
|
+
hook,
|
|
1060
|
+
command,
|
|
1061
|
+
evalContext,
|
|
1062
|
+
input,
|
|
1063
|
+
runtimeContext: this.context,
|
|
1064
|
+
entityName: options.entityName,
|
|
1065
|
+
instanceId: options.instanceId,
|
|
1066
|
+
emittedEvents,
|
|
1067
|
+
};
|
|
1068
|
+
const result = await mw.handler(ctx);
|
|
1069
|
+
if (result.contextPatch) {
|
|
1070
|
+
Object.assign(evalContext, result.contextPatch);
|
|
1071
|
+
}
|
|
1072
|
+
if (result.shortCircuit && result.result) {
|
|
1073
|
+
return result.result;
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
return undefined;
|
|
1077
|
+
}
|
|
1078
|
+
/**
|
|
1079
|
+
* Public read surface: tenant filter → decrypt → mask (read-projection only).
|
|
1080
|
+
* Execution paths (guards, actions, policies, computed properties, relationship
|
|
1081
|
+
* resolution) use getAllInstancesRaw and always see real values.
|
|
1082
|
+
*/
|
|
1083
|
+
async getAllInstances(entityName) {
|
|
1084
|
+
const all = await this.getAllInstancesRaw(entityName);
|
|
1085
|
+
const masked = [];
|
|
1086
|
+
for (const inst of all) {
|
|
1087
|
+
masked.push(await this.applyMasking(entityName, inst));
|
|
1088
|
+
}
|
|
1089
|
+
return masked;
|
|
1090
|
+
}
|
|
1091
|
+
/** Internal read path: tenant filter + decryption, NO masking. */
|
|
1092
|
+
async getAllInstancesRaw(entityName) {
|
|
1093
|
+
const store = this.stores.get(entityName);
|
|
1094
|
+
if (!store)
|
|
1095
|
+
return [];
|
|
1096
|
+
let all = await store.getAll();
|
|
1097
|
+
if (this.ir.tenant) {
|
|
1098
|
+
const tv = this.resolveTenantValue();
|
|
1099
|
+
if (tv) {
|
|
1100
|
+
const prop = this.ir.tenant.property;
|
|
1101
|
+
all = all.filter(inst => inst[prop] === tv);
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
// Decrypt encrypted fields after store read
|
|
1105
|
+
const decrypted = [];
|
|
1106
|
+
for (const inst of all) {
|
|
1107
|
+
decrypted.push(await this.decryptProperties(entityName, inst));
|
|
1108
|
+
}
|
|
1109
|
+
return decrypted;
|
|
1110
|
+
}
|
|
1111
|
+
/**
|
|
1112
|
+
* Public read surface: tenant filter → decrypt → mask (read-projection only).
|
|
1113
|
+
* Execution paths use getInstanceRaw and always see real values.
|
|
1114
|
+
*/
|
|
1115
|
+
async getInstance(entityName, id) {
|
|
1116
|
+
const inst = await this.getInstanceRaw(entityName, id);
|
|
1117
|
+
return inst ? await this.applyMasking(entityName, inst) : inst;
|
|
1118
|
+
}
|
|
1119
|
+
/** Internal read path: tenant filter + decryption, NO masking. */
|
|
1120
|
+
async getInstanceRaw(entityName, id) {
|
|
1121
|
+
// Command working copy: during command execution, reads of the target
|
|
1122
|
+
// instance return the in-memory copy carrying this command's mutations,
|
|
1123
|
+
// so guards/computes/refreshes see pending changes without a store read.
|
|
1124
|
+
const buf = this.commandBuffer;
|
|
1125
|
+
if (buf && buf.entityName === entityName && buf.id === id && buf.instance) {
|
|
1126
|
+
return buf.instance;
|
|
1127
|
+
}
|
|
1128
|
+
const store = this.stores.get(entityName);
|
|
1129
|
+
if (!store)
|
|
1130
|
+
return undefined;
|
|
1131
|
+
const inst = await store.getById(id);
|
|
1132
|
+
if (inst && this.ir.tenant) {
|
|
1133
|
+
const tv = this.resolveTenantValue();
|
|
1134
|
+
if (tv && inst[this.ir.tenant.property] !== tv) {
|
|
1135
|
+
return undefined;
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1138
|
+
// Decrypt encrypted fields after store read
|
|
1139
|
+
return inst ? await this.decryptProperties(entityName, inst) : undefined;
|
|
1140
|
+
}
|
|
1141
|
+
// ── Property masking (docs/spec/semantics.md, "Property Masking") ───
|
|
1142
|
+
/** Cache of properties carrying maskStrategy, per entity (IR is immutable at runtime). */
|
|
1143
|
+
maskedPropertiesCache = new Map();
|
|
1144
|
+
maskedProperties(entityName) {
|
|
1145
|
+
let cached = this.maskedPropertiesCache.get(entityName);
|
|
1146
|
+
if (cached)
|
|
1147
|
+
return cached;
|
|
1148
|
+
const entity = this.getEntity(entityName);
|
|
1149
|
+
cached = entity ? entity.properties.filter(p => p.maskStrategy !== undefined) : [];
|
|
1150
|
+
this.maskedPropertiesCache.set(entityName, cached);
|
|
1151
|
+
return cached;
|
|
1152
|
+
}
|
|
1153
|
+
/**
|
|
1154
|
+
* Apply read-time masking to an instance (after decryption and tenant filtering).
|
|
1155
|
+
* - `private` wins over `masked`: the property is excluded entirely.
|
|
1156
|
+
* - `null`/`undefined` pass through unmasked.
|
|
1157
|
+
* - `unmaskWhen` falsy or throwing ⇒ value stays masked (secure by default).
|
|
1158
|
+
* An evaluation error additionally surfaces a diagnostic; it never changes
|
|
1159
|
+
* the masked outcome (diagnostics explain, never compensate).
|
|
1160
|
+
*/
|
|
1161
|
+
async applyMasking(entityName, instance) {
|
|
1162
|
+
const maskedProps = this.maskedProperties(entityName);
|
|
1163
|
+
if (maskedProps.length === 0)
|
|
1164
|
+
return instance;
|
|
1165
|
+
const out = { ...instance };
|
|
1166
|
+
for (const prop of maskedProps) {
|
|
1167
|
+
if (prop.modifiers.includes('private')) {
|
|
1168
|
+
delete out[prop.name];
|
|
1169
|
+
continue;
|
|
1170
|
+
}
|
|
1171
|
+
const value = out[prop.name];
|
|
1172
|
+
if (value === null || value === undefined)
|
|
1173
|
+
continue;
|
|
1174
|
+
const strategy = prop.maskStrategy;
|
|
1175
|
+
if (strategy.unmaskWhen) {
|
|
1176
|
+
const ownsEvalBudget = this.initEvalBudget();
|
|
1177
|
+
try {
|
|
1178
|
+
// self.* binds the raw instance (real values); user.*/context.* from runtime context
|
|
1179
|
+
const evalContext = this.buildEvalContext({}, instance, entityName);
|
|
1180
|
+
const allowed = await this.evaluateExpression(strategy.unmaskWhen, evalContext);
|
|
1181
|
+
if (allowed)
|
|
1182
|
+
continue; // truthy ⇒ real value returned
|
|
1183
|
+
}
|
|
1184
|
+
catch (error) {
|
|
1185
|
+
// Secure by default: an error keeps the value masked. Surface a diagnostic
|
|
1186
|
+
// carrying the expression and resolved values; never alter the outcome.
|
|
1187
|
+
let resolved = [];
|
|
1188
|
+
try {
|
|
1189
|
+
const evalContext = this.buildEvalContext({}, instance, entityName);
|
|
1190
|
+
resolved = await this.resolveExpressionValues(strategy.unmaskWhen, evalContext);
|
|
1191
|
+
}
|
|
1192
|
+
catch {
|
|
1193
|
+
// resolution itself failed — report without resolved values
|
|
1194
|
+
}
|
|
1195
|
+
console.warn(`[Manifest Runtime] unmaskWhen evaluation error for '${entityName}.${prop.name}' (value stays masked):`, {
|
|
1196
|
+
expression: this.formatExpression(strategy.unmaskWhen),
|
|
1197
|
+
resolved,
|
|
1198
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1199
|
+
});
|
|
1200
|
+
}
|
|
1201
|
+
finally {
|
|
1202
|
+
if (ownsEvalBudget)
|
|
1203
|
+
this.clearEvalBudget();
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
out[prop.name] = applyMaskStrategy(strategy, value);
|
|
1207
|
+
}
|
|
1208
|
+
return out;
|
|
1209
|
+
}
|
|
1210
|
+
/**
|
|
1211
|
+
* Check entity constraints against instance data
|
|
1212
|
+
* Returns array of constraint failures (empty if all pass)
|
|
1213
|
+
* Useful for diagnostic purposes without mutating state
|
|
1214
|
+
*/
|
|
1215
|
+
async checkConstraints(entityName, data) {
|
|
1216
|
+
const entity = this.getEntity(entityName);
|
|
1217
|
+
if (!entity)
|
|
1218
|
+
return [];
|
|
1219
|
+
const ownsEvalBudget = this.initEvalBudget();
|
|
1220
|
+
try {
|
|
1221
|
+
const outcomes = await this.validateConstraints(entity, data);
|
|
1222
|
+
// Return only failed constraints for backwards compatibility with test patterns
|
|
1223
|
+
// (Callers can still see all outcomes by using validateConstraints directly)
|
|
1224
|
+
return outcomes.filter(o => !o.passed);
|
|
1225
|
+
}
|
|
1226
|
+
finally {
|
|
1227
|
+
if (ownsEvalBudget)
|
|
1228
|
+
this.clearEvalBudget();
|
|
1229
|
+
}
|
|
1230
|
+
}
|
|
1231
|
+
/**
|
|
1232
|
+
* Evaluate all entity constraints against instance data, returning every outcome
|
|
1233
|
+
* (both passed and failed). Useful for diagnostic UIs that show full constraint status.
|
|
1234
|
+
*/
|
|
1235
|
+
async evaluateAllConstraints(entityName, data) {
|
|
1236
|
+
const entity = this.getEntity(entityName);
|
|
1237
|
+
if (!entity)
|
|
1238
|
+
return [];
|
|
1239
|
+
const ownsEvalBudget = this.initEvalBudget();
|
|
1240
|
+
try {
|
|
1241
|
+
return await this.validateConstraints(entity, data);
|
|
1242
|
+
}
|
|
1243
|
+
finally {
|
|
1244
|
+
if (ownsEvalBudget)
|
|
1245
|
+
this.clearEvalBudget();
|
|
1246
|
+
}
|
|
1247
|
+
}
|
|
1248
|
+
async createInstance(entityName, data) {
|
|
1249
|
+
const ownsEvalBudget = this.initEvalBudget();
|
|
1250
|
+
try {
|
|
1251
|
+
return (await this.createInstanceWithOutcomes(entityName, data)).instance;
|
|
1252
|
+
}
|
|
1253
|
+
finally {
|
|
1254
|
+
if (ownsEvalBudget)
|
|
1255
|
+
this.clearEvalBudget();
|
|
1256
|
+
}
|
|
1257
|
+
}
|
|
1258
|
+
prepareCreateData(entity, data) {
|
|
1259
|
+
const defaults = {};
|
|
1260
|
+
for (const prop of entity.properties) {
|
|
1261
|
+
if (prop.defaultValue) {
|
|
1262
|
+
defaults[prop.name] = this.irValueToJs(prop.defaultValue);
|
|
1263
|
+
}
|
|
1264
|
+
else if (prop.autoNow) {
|
|
1265
|
+
// `= now()` / `= today()` default: stamp current time on create.
|
|
1266
|
+
defaults[prop.name] = this.getNow();
|
|
1267
|
+
}
|
|
1268
|
+
else {
|
|
1269
|
+
defaults[prop.name] = this.getDefaultForType(prop.type);
|
|
1270
|
+
}
|
|
1271
|
+
}
|
|
1272
|
+
const mergedData = { ...defaults, ...data };
|
|
1273
|
+
if (this.ir.tenant) {
|
|
1274
|
+
const tv = this.resolveTenantValue();
|
|
1275
|
+
if (tv) {
|
|
1276
|
+
mergedData[this.ir.tenant.property] = tv;
|
|
1277
|
+
}
|
|
1278
|
+
}
|
|
1279
|
+
// Handle version properties for optimistic concurrency control
|
|
1280
|
+
if (entity.versionProperty) {
|
|
1281
|
+
mergedData[entity.versionProperty] = 1;
|
|
1282
|
+
}
|
|
1283
|
+
if (entity.versionAtProperty) {
|
|
1284
|
+
mergedData[entity.versionAtProperty] = this.getNow();
|
|
1285
|
+
}
|
|
1286
|
+
if (entity.timestamps) {
|
|
1287
|
+
const now = this.getNow();
|
|
1288
|
+
mergedData.createdAt = now;
|
|
1289
|
+
mergedData.updatedAt = now;
|
|
1290
|
+
}
|
|
1291
|
+
return mergedData;
|
|
1292
|
+
}
|
|
1293
|
+
reportConstraintOutcomes(constraintOutcomes) {
|
|
1294
|
+
const blockingFailures = constraintOutcomes.filter(o => !o.passed && o.severity === 'block');
|
|
1295
|
+
if (blockingFailures.length > 0) {
|
|
1296
|
+
// Log blocking constraint failures for diagnostics
|
|
1297
|
+
console.warn('[Manifest Runtime] Blocking constraint validation failed:', blockingFailures);
|
|
1298
|
+
return false;
|
|
1299
|
+
}
|
|
1300
|
+
// Log non-blocking outcomes (warn/ok) for diagnostics
|
|
1301
|
+
const nonBlockingOutcomes = constraintOutcomes.filter(o => !o.passed && o.severity !== 'block');
|
|
1302
|
+
if (nonBlockingOutcomes.length > 0) {
|
|
1303
|
+
console.info('[Manifest Runtime] Non-blocking constraint outcomes:', nonBlockingOutcomes);
|
|
1304
|
+
}
|
|
1305
|
+
return true;
|
|
1306
|
+
}
|
|
1307
|
+
async createInstanceWithOutcomes(entityName, data) {
|
|
1308
|
+
const entity = this.getEntity(entityName);
|
|
1309
|
+
if (!entity)
|
|
1310
|
+
return {};
|
|
1311
|
+
const mergedData = this.prepareCreateData(entity, data);
|
|
1312
|
+
return this.persistPreparedCreate(entityName, entity, mergedData);
|
|
1313
|
+
}
|
|
1314
|
+
/** Date/time primitive write-time validation (docs/spec/semantics.md, Date/Time Types). */
|
|
1315
|
+
validateDateTimeTypes(entity, data) {
|
|
1316
|
+
const outcomes = [];
|
|
1317
|
+
for (const prop of entity.properties) {
|
|
1318
|
+
const t = prop.type?.name;
|
|
1319
|
+
if (t !== 'date' && t !== 'time' && t !== 'datetime' && t !== 'duration')
|
|
1320
|
+
continue;
|
|
1321
|
+
if (!(prop.name in data))
|
|
1322
|
+
continue;
|
|
1323
|
+
const value = data[prop.name];
|
|
1324
|
+
if (value === null || value === undefined)
|
|
1325
|
+
continue;
|
|
1326
|
+
let ok = true;
|
|
1327
|
+
let code = '';
|
|
1328
|
+
if (t === 'date') {
|
|
1329
|
+
ok = isValidDateString(value);
|
|
1330
|
+
code = 'E_TYPE_DATE';
|
|
1331
|
+
}
|
|
1332
|
+
else if (t === 'time') {
|
|
1333
|
+
ok = isValidTimeString(value);
|
|
1334
|
+
code = 'E_TYPE_TIME';
|
|
1335
|
+
}
|
|
1336
|
+
else if (t === 'datetime') {
|
|
1337
|
+
// Must be within the representable Date range (±8,640,000,000,000,000 ms).
|
|
1338
|
+
ok = typeof value === 'number' && Number.isFinite(value) && Math.abs(value) <= 8.64e15;
|
|
1339
|
+
code = 'E_TYPE_DATETIME';
|
|
1340
|
+
}
|
|
1341
|
+
else {
|
|
1342
|
+
ok = typeof value === 'number' && Number.isFinite(value);
|
|
1343
|
+
code = 'E_TYPE_DURATION';
|
|
1344
|
+
}
|
|
1345
|
+
if (!ok) {
|
|
1346
|
+
const shown = typeof value === 'number' ? String(value) : JSON.stringify(value) ?? String(value);
|
|
1347
|
+
const message = `Property "${prop.name}" expects ${t}; got ${shown}`;
|
|
1348
|
+
outcomes.push({
|
|
1349
|
+
code,
|
|
1350
|
+
constraintName: prop.name,
|
|
1351
|
+
severity: 'block',
|
|
1352
|
+
passed: false,
|
|
1353
|
+
formatted: message,
|
|
1354
|
+
message,
|
|
1355
|
+
details: { property: prop.name, expectedType: t, value },
|
|
1356
|
+
});
|
|
1357
|
+
}
|
|
1358
|
+
}
|
|
1359
|
+
return outcomes;
|
|
1360
|
+
}
|
|
1361
|
+
async persistPreparedCreate(entityName, entity, mergedData) {
|
|
1362
|
+
const constraintOutcomes = [
|
|
1363
|
+
...this.validateDateTimeTypes(entity, mergedData),
|
|
1364
|
+
...await this.validateConstraints(entity, mergedData),
|
|
1365
|
+
];
|
|
1366
|
+
if (!this.reportConstraintOutcomes(constraintOutcomes)) {
|
|
1367
|
+
return { constraintOutcomes };
|
|
1368
|
+
}
|
|
1369
|
+
const store = this.stores.get(entityName);
|
|
1370
|
+
if (!store)
|
|
1371
|
+
return { constraintOutcomes };
|
|
1372
|
+
// Encrypt fields before store write (no-op without encryptionProvider)
|
|
1373
|
+
const dataToStore = await this.encryptProperties(entityName, mergedData);
|
|
1374
|
+
const result = await store.create(dataToStore);
|
|
1375
|
+
// Track newly created instance to prevent version increment on subsequent mutate actions
|
|
1376
|
+
if (result && result.id) {
|
|
1377
|
+
this.justCreatedInstanceIds.add(result.id);
|
|
1378
|
+
}
|
|
1379
|
+
// Decrypt fields before returning to caller
|
|
1380
|
+
const decrypted = result ? await this.decryptProperties(entityName, result) : result;
|
|
1381
|
+
return { instance: decrypted, constraintOutcomes };
|
|
1382
|
+
}
|
|
1383
|
+
async updateInstance(entityName, id, data) {
|
|
1384
|
+
const entity = this.getEntity(entityName);
|
|
1385
|
+
const store = this.stores.get(entityName);
|
|
1386
|
+
if (!store || !entity)
|
|
1387
|
+
return undefined;
|
|
1388
|
+
// During command execution the target instance is buffered: load it from
|
|
1389
|
+
// the store at most once, then mutate the in-memory working copy.
|
|
1390
|
+
const buf = this.commandBuffer;
|
|
1391
|
+
const buffering = !!buf && buf.entityName === entityName && buf.id === id;
|
|
1392
|
+
let existing;
|
|
1393
|
+
if (buffering && buf.instance) {
|
|
1394
|
+
existing = buf.instance;
|
|
1395
|
+
}
|
|
1396
|
+
else {
|
|
1397
|
+
const rawExisting = await store.getById(id);
|
|
1398
|
+
if (!rawExisting)
|
|
1399
|
+
return undefined;
|
|
1400
|
+
// Decrypt existing instance so constraint/transition checks see plaintext
|
|
1401
|
+
existing = await this.decryptProperties(entityName, rawExisting);
|
|
1402
|
+
if (buffering)
|
|
1403
|
+
buf.instance = existing;
|
|
1404
|
+
}
|
|
1405
|
+
const ownsEvalBudget = this.initEvalBudget();
|
|
1406
|
+
try {
|
|
1407
|
+
// Optimistic concurrency control: check version if entity has versionProperty
|
|
1408
|
+
if (entity.versionProperty) {
|
|
1409
|
+
const existingVersion = existing[entity.versionProperty];
|
|
1410
|
+
const providedVersion = data[entity.versionProperty];
|
|
1411
|
+
if (existingVersion !== undefined && providedVersion !== undefined) {
|
|
1412
|
+
if (existingVersion !== providedVersion) {
|
|
1413
|
+
// Concurrency conflict - store structured details, emit event, and return undefined
|
|
1414
|
+
this.lastConcurrencyConflict = {
|
|
1415
|
+
entityType: entityName,
|
|
1416
|
+
entityId: id,
|
|
1417
|
+
expectedVersion: providedVersion,
|
|
1418
|
+
actualVersion: existingVersion,
|
|
1419
|
+
conflictCode: 'VERSION_MISMATCH',
|
|
1420
|
+
};
|
|
1421
|
+
await this.emitConcurrencyConflictEvent(entityName, id, providedVersion, existingVersion);
|
|
1422
|
+
return undefined;
|
|
1423
|
+
}
|
|
1424
|
+
}
|
|
1425
|
+
// Auto-increment version on successful update
|
|
1426
|
+
// Only increment once per command execution to handle commands with multiple mutate actions
|
|
1427
|
+
// If version is explicitly provided in data, use that (for optimistic concurrency checks)
|
|
1428
|
+
// Skip increment for instances that were just created in the same command (e.g., create command's mutate actions)
|
|
1429
|
+
const wasJustCreated = this.justCreatedInstanceIds.has(id);
|
|
1430
|
+
if (providedVersion === undefined && !this.versionIncrementedForCommand && !wasJustCreated) {
|
|
1431
|
+
data[entity.versionProperty] = (existingVersion || 0) + 1;
|
|
1432
|
+
this.versionIncrementedForCommand = true;
|
|
1433
|
+
}
|
|
1434
|
+
}
|
|
1435
|
+
// Update versionAt timestamp if present
|
|
1436
|
+
if (entity.versionAtProperty) {
|
|
1437
|
+
data[entity.versionAtProperty] = this.getNow();
|
|
1438
|
+
}
|
|
1439
|
+
if (entity.timestamps) {
|
|
1440
|
+
data.updatedAt = this.getNow();
|
|
1441
|
+
}
|
|
1442
|
+
const mergedData = { ...existing, ...data };
|
|
1443
|
+
// Validate state transitions if entity declares them
|
|
1444
|
+
if (entity.transitions && entity.transitions.length > 0) {
|
|
1445
|
+
for (const [prop, newValue] of Object.entries(data)) {
|
|
1446
|
+
const rules = entity.transitions.filter(t => t.property === prop);
|
|
1447
|
+
if (rules.length === 0)
|
|
1448
|
+
continue;
|
|
1449
|
+
const currentValue = existing[prop];
|
|
1450
|
+
if (currentValue === undefined)
|
|
1451
|
+
continue;
|
|
1452
|
+
const matchingRule = rules.find(t => t.from === String(currentValue));
|
|
1453
|
+
if (matchingRule && !matchingRule.to.includes(String(newValue))) {
|
|
1454
|
+
const allowed = matchingRule.to.map(v => `'${v}'`).join(', ');
|
|
1455
|
+
this.lastTransitionError = `Invalid state transition for '${prop}': '${currentValue}' -> '${newValue}' is not allowed. Allowed from '${currentValue}': [${allowed}]`;
|
|
1456
|
+
return undefined;
|
|
1457
|
+
}
|
|
1458
|
+
}
|
|
1459
|
+
}
|
|
1460
|
+
// Validate entity constraints.
|
|
1461
|
+
// Date/time type validation runs against the patch only, so previously
|
|
1462
|
+
// stored values are not re-validated on unrelated updates.
|
|
1463
|
+
const constraintOutcomes = [
|
|
1464
|
+
...this.validateDateTimeTypes(entity, data),
|
|
1465
|
+
...await this.validateConstraints(entity, mergedData),
|
|
1466
|
+
];
|
|
1467
|
+
// Only block on severity='block' constraints that failed
|
|
1468
|
+
const blockingFailures = constraintOutcomes.filter(o => !o.passed && o.severity === 'block');
|
|
1469
|
+
if (blockingFailures.length > 0) {
|
|
1470
|
+
// Log blocking constraint failures for diagnostics
|
|
1471
|
+
console.warn('[Manifest Runtime] Blocking constraint validation failed:', blockingFailures);
|
|
1472
|
+
return undefined;
|
|
1473
|
+
}
|
|
1474
|
+
// Log non-blocking outcomes (warn/ok) for diagnostics
|
|
1475
|
+
const nonBlockingOutcomes = constraintOutcomes.filter(o => !o.passed && o.severity !== 'block');
|
|
1476
|
+
if (nonBlockingOutcomes.length > 0) {
|
|
1477
|
+
console.info('[Manifest Runtime] Non-blocking constraint outcomes:', nonBlockingOutcomes);
|
|
1478
|
+
}
|
|
1479
|
+
// Mark cached computed properties as stale when their dependencies change
|
|
1480
|
+
this.markComputedPropertiesStale(entityName, id, Object.keys(data));
|
|
1481
|
+
// Encrypt fields before store write, decrypt result before returning
|
|
1482
|
+
const encryptedData = await this.encryptProperties(entityName, data);
|
|
1483
|
+
if (buffering) {
|
|
1484
|
+
// Batched path: advance the working copy and accumulate the patch. The
|
|
1485
|
+
// single store.update runs once when the command flushes the buffer, so
|
|
1486
|
+
// a command mutating N fields persists once rather than N times.
|
|
1487
|
+
buf.instance = mergedData;
|
|
1488
|
+
Object.assign(buf.patch, encryptedData);
|
|
1489
|
+
return mergedData;
|
|
1490
|
+
}
|
|
1491
|
+
const result = await store.update(id, encryptedData);
|
|
1492
|
+
return result ? await this.decryptProperties(entityName, result) : result;
|
|
1493
|
+
}
|
|
1494
|
+
finally {
|
|
1495
|
+
if (ownsEvalBudget)
|
|
1496
|
+
this.clearEvalBudget();
|
|
1497
|
+
}
|
|
1498
|
+
}
|
|
1499
|
+
/**
|
|
1500
|
+
* Mark cached computed properties as stale when their dependencies are mutated.
|
|
1501
|
+
* Scans the entity's computed properties for any that depend on the changed properties,
|
|
1502
|
+
* and sets their cache entries' stale flag to true. Handles transitive staleness.
|
|
1503
|
+
*/
|
|
1504
|
+
markComputedPropertiesStale(entityName, instanceId, changedProperties, visited = new Set()) {
|
|
1505
|
+
const entity = this.getEntity(entityName);
|
|
1506
|
+
if (!entity)
|
|
1507
|
+
return;
|
|
1508
|
+
for (const cp of entity.computedProperties) {
|
|
1509
|
+
if (visited.has(cp.name))
|
|
1510
|
+
continue;
|
|
1511
|
+
const dependsOnChanged = cp.dependencies.some(dep => changedProperties.includes(dep));
|
|
1512
|
+
if (!dependsOnChanged)
|
|
1513
|
+
continue;
|
|
1514
|
+
visited.add(cp.name);
|
|
1515
|
+
const cacheKey = `${entityName}:${instanceId}:${cp.name}`;
|
|
1516
|
+
// Mark in session/TTL cache
|
|
1517
|
+
const cached = this.computedPropertyCache.get(cacheKey);
|
|
1518
|
+
if (cached)
|
|
1519
|
+
cached.stale = true;
|
|
1520
|
+
// Mark in request cache
|
|
1521
|
+
const reqCached = this.computedPropertyRequestCache.get(cacheKey);
|
|
1522
|
+
if (reqCached)
|
|
1523
|
+
reqCached.stale = true;
|
|
1524
|
+
// Also mark any computed properties that depend on this computed property (transitive staleness)
|
|
1525
|
+
this.markComputedPropertiesStale(entityName, instanceId, [cp.name], visited);
|
|
1526
|
+
}
|
|
1527
|
+
}
|
|
1528
|
+
async deleteInstance(entityName, id) {
|
|
1529
|
+
const store = this.stores.get(entityName);
|
|
1530
|
+
return store ? await store.delete(id) : false;
|
|
1531
|
+
}
|
|
1532
|
+
async runCommand(commandName, input, options = {}) {
|
|
1533
|
+
// Per docs/spec/adapters.md § "Audit Sink": when an AuditSink is wired in,
|
|
1534
|
+
// runCommand emits exactly one AuditRecord per invocation regardless of
|
|
1535
|
+
// outcome. The recordId is generated once up front so callers can correlate
|
|
1536
|
+
// the same attempt across logs even if `result` is constructed late.
|
|
1537
|
+
const auditEnabled = !!this.options.auditSink;
|
|
1538
|
+
const auditRecordId = auditEnabled ? this.nextRuntimeId() : undefined;
|
|
1539
|
+
const auditOccurredAt = auditEnabled ? this.getNow() : 0;
|
|
1540
|
+
let result;
|
|
1541
|
+
let thrown;
|
|
1542
|
+
try {
|
|
1543
|
+
// Tenant context gate: fail closed before ANY work, including idempotency
|
|
1544
|
+
// cache reads/writes. Falsy values (undefined, '', null) all count as
|
|
1545
|
+
// missing — preventing accidental empty-string passes.
|
|
1546
|
+
// Gate activates when EITHER the explicit option is set OR the IR declares a tenant.
|
|
1547
|
+
const tenantRequired = this.options.requireTenantContext || !!this.ir.tenant;
|
|
1548
|
+
const tenantValue = this.ir.tenant ? this.resolveTenantValue() : this.context.tenantId;
|
|
1549
|
+
if (tenantRequired && !tenantValue) {
|
|
1550
|
+
result = {
|
|
1551
|
+
success: false,
|
|
1552
|
+
error: 'MISSING_TENANT_CONTEXT: tenant-scoped command invoked without context.tenantId',
|
|
1553
|
+
emittedEvents: [],
|
|
1554
|
+
};
|
|
1555
|
+
return result;
|
|
1556
|
+
}
|
|
1557
|
+
// Idempotency short-circuit (before ANY evaluation)
|
|
1558
|
+
if (this.options.idempotencyStore) {
|
|
1559
|
+
if (options.idempotencyKey === undefined) {
|
|
1560
|
+
result = {
|
|
1561
|
+
success: false,
|
|
1562
|
+
error: 'IdempotencyStore is configured but no idempotencyKey was provided',
|
|
1563
|
+
emittedEvents: [],
|
|
1564
|
+
};
|
|
1565
|
+
return result;
|
|
1566
|
+
}
|
|
1567
|
+
const cached = await this.options.idempotencyStore.get(options.idempotencyKey);
|
|
1568
|
+
if (cached !== undefined) {
|
|
1569
|
+
result = cached;
|
|
1570
|
+
return cached;
|
|
1571
|
+
}
|
|
1572
|
+
}
|
|
1573
|
+
// Async command branch: enqueue job instead of executing synchronously.
|
|
1574
|
+
// Re-entry from job worker (context.source === 'job') bypasses this branch
|
|
1575
|
+
// so the actual command body runs during drainJobs().
|
|
1576
|
+
const command = this.getCommand(commandName, options.entityName);
|
|
1577
|
+
if (command?.async && this.context.source !== 'job') {
|
|
1578
|
+
// Validate policies/constraints/guards synchronously (fail-fast)
|
|
1579
|
+
const validation = await this._validateAsyncCommand(commandName, input, options);
|
|
1580
|
+
if (!validation.success) {
|
|
1581
|
+
result = validation;
|
|
1582
|
+
return result;
|
|
1583
|
+
}
|
|
1584
|
+
if (!this.options.jobQueue) {
|
|
1585
|
+
result = {
|
|
1586
|
+
success: false,
|
|
1587
|
+
error: 'MISSING_JOB_QUEUE: async command invoked but no jobQueue is configured in RuntimeOptions',
|
|
1588
|
+
emittedEvents: [],
|
|
1589
|
+
};
|
|
1590
|
+
return result;
|
|
1591
|
+
}
|
|
1592
|
+
const jobId = this.nextRuntimeId();
|
|
1593
|
+
const enqueuedAt = this.getNow();
|
|
1594
|
+
await this.options.jobQueue.enqueue({
|
|
1595
|
+
jobId,
|
|
1596
|
+
commandName,
|
|
1597
|
+
entityName: options.entityName,
|
|
1598
|
+
instanceId: options.instanceId,
|
|
1599
|
+
input,
|
|
1600
|
+
correlationId: options.correlationId,
|
|
1601
|
+
causationId: options.causationId,
|
|
1602
|
+
enqueuedAt,
|
|
1603
|
+
status: 'pending',
|
|
1604
|
+
});
|
|
1605
|
+
result = {
|
|
1606
|
+
success: true,
|
|
1607
|
+
result: { jobId, status: 'pending', enqueuedAt },
|
|
1608
|
+
emittedEvents: [],
|
|
1609
|
+
};
|
|
1610
|
+
return result;
|
|
1611
|
+
}
|
|
1612
|
+
// Full command execution (with optional retry wrapper)
|
|
1613
|
+
if (this.profilingBridge.isEnabled()) {
|
|
1614
|
+
this.profilingBridge.beginCommand(commandName, options.entityName, options.instanceId, this.getNow());
|
|
1615
|
+
}
|
|
1616
|
+
const runOnce = () => this._executeCommandInternal(commandName, input, options);
|
|
1617
|
+
if (command?.retry) {
|
|
1618
|
+
result = await executeWithRetry(command.retry, runOnce, {
|
|
1619
|
+
sleep: this.options.sleep,
|
|
1620
|
+
retryJitter: this.options.retryJitter,
|
|
1621
|
+
});
|
|
1622
|
+
}
|
|
1623
|
+
else {
|
|
1624
|
+
result = await runOnce();
|
|
1625
|
+
}
|
|
1626
|
+
// Outbox enqueue: when an OutboxStore is wired in and the command
|
|
1627
|
+
// succeeded with one or more emitted events, durably persist the
|
|
1628
|
+
// semantic events. NOTE: the in-memory runtime has no shared
|
|
1629
|
+
// transaction boundary, so this enqueue is NOT transactional w.r.t.
|
|
1630
|
+
// mutation today — see docs/spec/adapters.md § "Outbox Store".
|
|
1631
|
+
if (this.options.outboxStore && result.success && result.emittedEvents.length > 0) {
|
|
1632
|
+
await this.enqueueOutbox(result.emittedEvents, commandName, options);
|
|
1633
|
+
}
|
|
1634
|
+
// Cache result (success OR failure)
|
|
1635
|
+
if (this.options.idempotencyStore && options.idempotencyKey !== undefined) {
|
|
1636
|
+
await this.options.idempotencyStore.set(options.idempotencyKey, result);
|
|
1637
|
+
}
|
|
1638
|
+
return result;
|
|
1639
|
+
}
|
|
1640
|
+
catch (e) {
|
|
1641
|
+
thrown = e;
|
|
1642
|
+
throw e;
|
|
1643
|
+
}
|
|
1644
|
+
finally {
|
|
1645
|
+
if (this.profilingBridge.isEnabled() && result !== undefined) {
|
|
1646
|
+
this.profilingBridge.complete(result.success, this.ir.entities.length, this.stores.size);
|
|
1647
|
+
}
|
|
1648
|
+
if (auditEnabled) {
|
|
1649
|
+
await this.emitAudit({
|
|
1650
|
+
sink: this.options.auditSink,
|
|
1651
|
+
recordId: auditRecordId,
|
|
1652
|
+
occurredAt: auditOccurredAt,
|
|
1653
|
+
commandName,
|
|
1654
|
+
entityName: options.entityName,
|
|
1655
|
+
result,
|
|
1656
|
+
thrown,
|
|
1657
|
+
});
|
|
1658
|
+
}
|
|
1659
|
+
}
|
|
1660
|
+
}
|
|
1661
|
+
// ─── Saga Orchestration ──────────────────────────────────────────────
|
|
1662
|
+
/**
|
|
1663
|
+
* Execute a saga: run steps in declaration order, compensating completed
|
|
1664
|
+
* steps in reverse order on failure (when onFailure === 'compensate').
|
|
1665
|
+
* Each step dispatches via `runCommand` — all policies, guards, and
|
|
1666
|
+
* constraints of the step's command still apply.
|
|
1667
|
+
*/
|
|
1668
|
+
async runSaga(sagaName, stepInputs = {}, options = {}) {
|
|
1669
|
+
const saga = (this.ir.sagas || []).find(s => s.name === sagaName);
|
|
1670
|
+
if (!saga) {
|
|
1671
|
+
return {
|
|
1672
|
+
saga: sagaName, success: false, status: 'aborted',
|
|
1673
|
+
steps: [], emittedEvents: [],
|
|
1674
|
+
error: `Unknown saga '${sagaName}'`,
|
|
1675
|
+
};
|
|
1676
|
+
}
|
|
1677
|
+
const correlationId = options.correlationId ?? this.nextRuntimeId();
|
|
1678
|
+
const emittedEvents = [];
|
|
1679
|
+
const stepResults = [];
|
|
1680
|
+
const completed = [];
|
|
1681
|
+
// 1. Emit SagaStarted (only if declared in saga's emits)
|
|
1682
|
+
this.emitSagaLifecycle(saga, 'SagaStarted', { sagaName: saga.name }, correlationId, emittedEvents);
|
|
1683
|
+
// 2. Execute steps in declaration order
|
|
1684
|
+
for (const step of saga.steps) {
|
|
1685
|
+
const cfg = stepInputs[step.name] ?? {};
|
|
1686
|
+
const res = await this.runCommand(step.command, cfg.input ?? {}, {
|
|
1687
|
+
entityName: step.commandEntity,
|
|
1688
|
+
instanceId: cfg.instanceId,
|
|
1689
|
+
correlationId,
|
|
1690
|
+
causationId: `${saga.name}:${step.name}`,
|
|
1691
|
+
});
|
|
1692
|
+
emittedEvents.push(...res.emittedEvents);
|
|
1693
|
+
if (!res.success) {
|
|
1694
|
+
// Step failed
|
|
1695
|
+
stepResults.push({
|
|
1696
|
+
step: step.name, command: `${step.commandEntity}.${step.command}`,
|
|
1697
|
+
status: 'failed', result: res, error: res.error,
|
|
1698
|
+
});
|
|
1699
|
+
if (saga.onFailure === 'compensate') {
|
|
1700
|
+
// Compensate completed steps in reverse order
|
|
1701
|
+
await this.compensateSagaSteps(saga, completed, stepInputs, correlationId, emittedEvents, stepResults);
|
|
1702
|
+
this.emitSagaLifecycle(saga, 'SagaFailed', { sagaName: saga.name, failedStep: step.name }, correlationId, emittedEvents);
|
|
1703
|
+
return {
|
|
1704
|
+
saga: saga.name, success: false, status: 'compensated',
|
|
1705
|
+
steps: stepResults, emittedEvents, failedStep: step.name, error: res.error,
|
|
1706
|
+
};
|
|
1707
|
+
}
|
|
1708
|
+
else {
|
|
1709
|
+
// Abort: no compensation
|
|
1710
|
+
this.emitSagaLifecycle(saga, 'SagaFailed', { sagaName: saga.name, failedStep: step.name }, correlationId, emittedEvents);
|
|
1711
|
+
return {
|
|
1712
|
+
saga: saga.name, success: false, status: 'aborted',
|
|
1713
|
+
steps: stepResults, emittedEvents, failedStep: step.name, error: res.error,
|
|
1714
|
+
};
|
|
1715
|
+
}
|
|
1716
|
+
}
|
|
1717
|
+
// Step succeeded
|
|
1718
|
+
stepResults.push({
|
|
1719
|
+
step: step.name, command: `${step.commandEntity}.${step.command}`,
|
|
1720
|
+
status: 'completed', result: res,
|
|
1721
|
+
});
|
|
1722
|
+
completed.push({ step, instanceId: cfg.instanceId });
|
|
1723
|
+
this.emitSagaLifecycle(saga, 'SagaStepCompleted', { sagaName: saga.name, step: step.name }, correlationId, emittedEvents);
|
|
1724
|
+
}
|
|
1725
|
+
// 3. All steps completed successfully
|
|
1726
|
+
this.emitSagaLifecycle(saga, 'SagaCompleted', { sagaName: saga.name }, correlationId, emittedEvents);
|
|
1727
|
+
return {
|
|
1728
|
+
saga: saga.name, success: true, status: 'completed',
|
|
1729
|
+
steps: stepResults, emittedEvents,
|
|
1730
|
+
};
|
|
1731
|
+
}
|
|
1732
|
+
/**
|
|
1733
|
+
* Compensate completed saga steps in reverse order (best-effort).
|
|
1734
|
+
* Compensation failures are recorded but do not throw — all remaining
|
|
1735
|
+
* compensations still execute.
|
|
1736
|
+
*/
|
|
1737
|
+
async compensateSagaSteps(saga, completed, stepInputs, correlationId, emittedEvents, stepResults) {
|
|
1738
|
+
// Iterate completed steps in reverse
|
|
1739
|
+
for (let i = completed.length - 1; i >= 0; i--) {
|
|
1740
|
+
const { step, instanceId } = completed[i];
|
|
1741
|
+
const matchingResult = stepResults.find(r => r.step === step.name);
|
|
1742
|
+
if (!step.compensate || !step.compensateEntity) {
|
|
1743
|
+
// No compensation declared — mark as skipped
|
|
1744
|
+
if (matchingResult)
|
|
1745
|
+
matchingResult.status = 'skipped';
|
|
1746
|
+
continue;
|
|
1747
|
+
}
|
|
1748
|
+
// Hand the original forward-step input to the compensation. A refund
|
|
1749
|
+
// needs the charge's amount, a release needs the reserve's quantity, etc.
|
|
1750
|
+
// Without this, the compensation command's guards see nothing and the
|
|
1751
|
+
// reversal silently no-ops. See spec/semantics.md § Saga compensation.
|
|
1752
|
+
const compensationInput = stepInputs[step.name]?.input ?? {};
|
|
1753
|
+
try {
|
|
1754
|
+
const compResult = await this.runCommand(step.compensate, compensationInput, {
|
|
1755
|
+
entityName: step.compensateEntity,
|
|
1756
|
+
instanceId,
|
|
1757
|
+
correlationId,
|
|
1758
|
+
causationId: `${saga.name}:${step.name}:compensate`,
|
|
1759
|
+
});
|
|
1760
|
+
emittedEvents.push(...compResult.emittedEvents);
|
|
1761
|
+
if (matchingResult) {
|
|
1762
|
+
matchingResult.compensation = compResult;
|
|
1763
|
+
// A compensation that fails its guard/policy/constraint returns
|
|
1764
|
+
// success:false (no throw). Such a step was NOT reversed — surface
|
|
1765
|
+
// it as compensation_failed rather than mislabeling it 'compensated'.
|
|
1766
|
+
if (compResult.success) {
|
|
1767
|
+
matchingResult.status = 'compensated';
|
|
1768
|
+
}
|
|
1769
|
+
else {
|
|
1770
|
+
matchingResult.status = 'compensation_failed';
|
|
1771
|
+
matchingResult.error = compResult.error;
|
|
1772
|
+
}
|
|
1773
|
+
}
|
|
1774
|
+
}
|
|
1775
|
+
catch (e) {
|
|
1776
|
+
// A thrown compensation is also a failed reversal. Record the error and
|
|
1777
|
+
// keep compensating the remaining steps (best-effort), but do not claim
|
|
1778
|
+
// this step was reversed.
|
|
1779
|
+
if (matchingResult) {
|
|
1780
|
+
matchingResult.status = 'compensation_failed';
|
|
1781
|
+
matchingResult.error = e instanceof Error ? e.message : String(e);
|
|
1782
|
+
}
|
|
1783
|
+
}
|
|
1784
|
+
}
|
|
1785
|
+
}
|
|
1786
|
+
/**
|
|
1787
|
+
* Emit a saga lifecycle event (SagaStarted, SagaCompleted, SagaFailed,
|
|
1788
|
+
* SagaStepCompleted) only if declared in the saga's `emits` array.
|
|
1789
|
+
*/
|
|
1790
|
+
emitSagaLifecycle(saga, eventName, payload, correlationId, sink) {
|
|
1791
|
+
if (!saga.emits.includes(eventName))
|
|
1792
|
+
return;
|
|
1793
|
+
const event = (this.ir.events || []).find(e => e.name === eventName);
|
|
1794
|
+
const prov = this.ir.provenance;
|
|
1795
|
+
const emitted = {
|
|
1796
|
+
name: eventName,
|
|
1797
|
+
channel: event?.channel || eventName,
|
|
1798
|
+
payload,
|
|
1799
|
+
timestamp: this.getNow(),
|
|
1800
|
+
...(prov ? {
|
|
1801
|
+
provenance: {
|
|
1802
|
+
contentHash: prov.contentHash,
|
|
1803
|
+
compilerVersion: prov.compilerVersion,
|
|
1804
|
+
schemaVersion: prov.schemaVersion,
|
|
1805
|
+
},
|
|
1806
|
+
} : {}),
|
|
1807
|
+
correlationId,
|
|
1808
|
+
causationId: `saga:${saga.name}`,
|
|
1809
|
+
};
|
|
1810
|
+
sink.push(emitted);
|
|
1811
|
+
this.eventLog.push(emitted);
|
|
1812
|
+
this.notifyListeners(emitted);
|
|
1813
|
+
}
|
|
1814
|
+
/**
|
|
1815
|
+
* Map a CommandResult and any thrown error into a CommandOutcome for the
|
|
1816
|
+
* AuditRecord. The mapping mirrors the exit paths inside runCommand and
|
|
1817
|
+
* _executeCommandInternal — keep them in lock-step when adding new
|
|
1818
|
+
* failure modes.
|
|
1819
|
+
*/
|
|
1820
|
+
classifyOutcome(result, thrown) {
|
|
1821
|
+
if (thrown !== undefined)
|
|
1822
|
+
return 'error';
|
|
1823
|
+
if (!result)
|
|
1824
|
+
return 'error';
|
|
1825
|
+
if (result.success)
|
|
1826
|
+
return 'success';
|
|
1827
|
+
if (result.policyDenial)
|
|
1828
|
+
return 'policy_denied';
|
|
1829
|
+
if (result.guardFailure)
|
|
1830
|
+
return 'guard_denied';
|
|
1831
|
+
if (result.rateLimitDenial)
|
|
1832
|
+
return 'rate_limit_denied';
|
|
1833
|
+
if (result.concurrencyConflict)
|
|
1834
|
+
return 'concurrency_conflict';
|
|
1835
|
+
if (typeof result.error === 'string' && result.error.startsWith('MISSING_TENANT_CONTEXT')) {
|
|
1836
|
+
return 'missing_tenant_context';
|
|
1837
|
+
}
|
|
1838
|
+
// A blocking constraint failure is distinguishable by the presence of a
|
|
1839
|
+
// blocking outcome on result.constraintOutcomes (non-blocking warn/ok
|
|
1840
|
+
// outcomes ride along with success too, so we must check severity).
|
|
1841
|
+
if (result.constraintOutcomes?.some(o => !o.passed && !o.overridden && o.severity === 'block')) {
|
|
1842
|
+
return 'constraint_failed';
|
|
1843
|
+
}
|
|
1844
|
+
return 'error';
|
|
1845
|
+
}
|
|
1846
|
+
/**
|
|
1847
|
+
* Build and emit a single AuditRecord through the configured sink.
|
|
1848
|
+
* Sink errors are caught and logged — audit emission MUST NOT alter
|
|
1849
|
+
* command-execution behavior. This is the documented fail-open policy
|
|
1850
|
+
* (see docs/spec/adapters.md § "Audit Sink").
|
|
1851
|
+
*/
|
|
1852
|
+
async emitAudit(args) {
|
|
1853
|
+
const { sink, recordId, occurredAt, commandName, entityName, result, thrown } = args;
|
|
1854
|
+
const outcome = this.classifyOutcome(result, thrown);
|
|
1855
|
+
const commandId = entityName ? `${entityName}.${commandName}` : commandName;
|
|
1856
|
+
// Diagnostics surface the structured failure for failed outcomes; for
|
|
1857
|
+
// success we carry along non-blocking constraint outcomes when present.
|
|
1858
|
+
let diagnostics = undefined;
|
|
1859
|
+
if (thrown !== undefined) {
|
|
1860
|
+
diagnostics = { error: thrown instanceof Error ? thrown.message : String(thrown) };
|
|
1861
|
+
}
|
|
1862
|
+
else if (result) {
|
|
1863
|
+
const parts = {};
|
|
1864
|
+
if (result.error !== undefined)
|
|
1865
|
+
parts.error = result.error;
|
|
1866
|
+
if (result.deniedBy !== undefined)
|
|
1867
|
+
parts.deniedBy = result.deniedBy;
|
|
1868
|
+
if (result.policyDenial)
|
|
1869
|
+
parts.policyDenial = result.policyDenial;
|
|
1870
|
+
if (result.guardFailure)
|
|
1871
|
+
parts.guardFailure = result.guardFailure;
|
|
1872
|
+
if (result.concurrencyConflict)
|
|
1873
|
+
parts.concurrencyConflict = result.concurrencyConflict;
|
|
1874
|
+
if (result.constraintOutcomes && result.constraintOutcomes.length > 0) {
|
|
1875
|
+
parts.constraintOutcomes = result.constraintOutcomes;
|
|
1876
|
+
}
|
|
1877
|
+
if (result.overrideRequests && result.overrideRequests.length > 0) {
|
|
1878
|
+
parts.overrideRequests = result.overrideRequests;
|
|
1879
|
+
}
|
|
1880
|
+
if (Object.keys(parts).length > 0)
|
|
1881
|
+
diagnostics = parts;
|
|
1882
|
+
}
|
|
1883
|
+
const emittedEventNames = result?.emittedEvents?.map(e => e.name);
|
|
1884
|
+
const record = {
|
|
1885
|
+
recordId,
|
|
1886
|
+
occurredAt,
|
|
1887
|
+
command: commandName,
|
|
1888
|
+
commandId,
|
|
1889
|
+
outcome,
|
|
1890
|
+
...(entityName !== undefined ? { entity: entityName } : {}),
|
|
1891
|
+
...(this.context.tenantId !== undefined ? { tenantId: this.context.tenantId } : {}),
|
|
1892
|
+
...(this.context.orgId !== undefined ? { orgId: this.context.orgId } : {}),
|
|
1893
|
+
...(this.context.actorId !== undefined ? { actorId: this.context.actorId } : {}),
|
|
1894
|
+
...(this.context.requestId !== undefined ? { requestId: this.context.requestId } : {}),
|
|
1895
|
+
...(this.context.source !== undefined ? { source: this.context.source } : {}),
|
|
1896
|
+
...(this.ir.provenance?.contentHash ? { irHash: this.ir.provenance.contentHash } : {}),
|
|
1897
|
+
...(diagnostics !== undefined ? { diagnostics } : {}),
|
|
1898
|
+
...(emittedEventNames && emittedEventNames.length > 0 ? { emittedEventNames } : {}),
|
|
1899
|
+
};
|
|
1900
|
+
try {
|
|
1901
|
+
await sink.emit(record);
|
|
1902
|
+
}
|
|
1903
|
+
catch (sinkError) {
|
|
1904
|
+
// Fail-open: audit sink errors MUST NOT alter command execution.
|
|
1905
|
+
// Surface the failure on stderr so operators can wire alerts off it.
|
|
1906
|
+
console.warn('[Manifest Runtime] AuditSink.emit failed; record dropped:', sinkError instanceof Error ? sinkError.message : sinkError);
|
|
1907
|
+
}
|
|
1908
|
+
}
|
|
1909
|
+
/**
|
|
1910
|
+
* Enqueue emitted events into the configured OutboxStore as a batch.
|
|
1911
|
+
* Wraps the call in try/catch and logs failures — outbox failures MUST
|
|
1912
|
+
* NOT alter the CommandResult shape callers already received.
|
|
1913
|
+
*
|
|
1914
|
+
* Non-transactional caveat: the in-memory runtime does not yet expose a
|
|
1915
|
+
* shared transaction boundary, so a successful command followed by an
|
|
1916
|
+
* outbox enqueue failure leaves state mutated without an outbox row.
|
|
1917
|
+
* Durable adapters MUST honor the `tx` parameter and enqueue inside the
|
|
1918
|
+
* same transaction that mutated state.
|
|
1919
|
+
*/
|
|
1920
|
+
async enqueueOutbox(events, _commandName, _runOptions) {
|
|
1921
|
+
const store = this.options.outboxStore;
|
|
1922
|
+
if (!store)
|
|
1923
|
+
return;
|
|
1924
|
+
const enqueuedAt = this.getNow();
|
|
1925
|
+
const entries = events.map(event => ({
|
|
1926
|
+
entryId: this.nextRuntimeId(),
|
|
1927
|
+
enqueuedAt,
|
|
1928
|
+
event,
|
|
1929
|
+
status: 'pending',
|
|
1930
|
+
attempts: 0,
|
|
1931
|
+
}));
|
|
1932
|
+
try {
|
|
1933
|
+
await store.enqueue(entries);
|
|
1934
|
+
}
|
|
1935
|
+
catch (storeError) {
|
|
1936
|
+
console.warn('[Manifest Runtime] OutboxStore.enqueue failed; events not durably persisted:', storeError instanceof Error ? storeError.message : storeError);
|
|
1937
|
+
}
|
|
1938
|
+
}
|
|
1939
|
+
/**
|
|
1940
|
+
* Validate an async command synchronously (policies, constraints, guards)
|
|
1941
|
+
* without executing actions. Used for fail-fast before enqueuing a job.
|
|
1942
|
+
*/
|
|
1943
|
+
async _validateAsyncCommand(commandName, input, options) {
|
|
1944
|
+
const command = this.getCommand(commandName, options.entityName);
|
|
1945
|
+
if (!command) {
|
|
1946
|
+
return {
|
|
1947
|
+
success: false,
|
|
1948
|
+
error: `Command '${commandName}' not found`,
|
|
1949
|
+
emittedEvents: [],
|
|
1950
|
+
};
|
|
1951
|
+
}
|
|
1952
|
+
const instance = options.instanceId && options.entityName
|
|
1953
|
+
? await this.getInstanceRaw(options.entityName, options.instanceId)
|
|
1954
|
+
: undefined;
|
|
1955
|
+
const evalContext = this.buildEvalContext(input, instance, options.entityName);
|
|
1956
|
+
// Check policies
|
|
1957
|
+
const policyResult = await this.checkPolicies(command, evalContext);
|
|
1958
|
+
if (!policyResult.allowed) {
|
|
1959
|
+
return {
|
|
1960
|
+
success: false,
|
|
1961
|
+
error: policyResult.denial?.message,
|
|
1962
|
+
deniedBy: policyResult.denial?.policyName,
|
|
1963
|
+
policyDenial: policyResult.denial,
|
|
1964
|
+
emittedEvents: [],
|
|
1965
|
+
};
|
|
1966
|
+
}
|
|
1967
|
+
// Evaluate constraints
|
|
1968
|
+
const commandContext = { commandName, entityName: options.entityName, instanceId: options.instanceId };
|
|
1969
|
+
const constraintResult = await this.evaluateCommandConstraints(command, evalContext, options.overrideRequests, commandContext);
|
|
1970
|
+
if (!constraintResult.allowed) {
|
|
1971
|
+
const blocking = constraintResult.outcomes.find(o => !o.passed && !o.overridden && o.severity === 'block');
|
|
1972
|
+
return {
|
|
1973
|
+
success: false,
|
|
1974
|
+
error: blocking?.message || `Command blocked by constraint '${blocking?.constraintName}'`,
|
|
1975
|
+
constraintOutcomes: constraintResult.outcomes,
|
|
1976
|
+
emittedEvents: [],
|
|
1977
|
+
};
|
|
1978
|
+
}
|
|
1979
|
+
// Evaluate guards
|
|
1980
|
+
for (let i = 0; i < command.guards.length; i += 1) {
|
|
1981
|
+
const guard = command.guards[i];
|
|
1982
|
+
const result = await this.evaluateExpression(guard, evalContext);
|
|
1983
|
+
if (!result) {
|
|
1984
|
+
return {
|
|
1985
|
+
success: false,
|
|
1986
|
+
error: `Guard condition failed for command '${commandName}'`,
|
|
1987
|
+
guardFailure: {
|
|
1988
|
+
index: i + 1,
|
|
1989
|
+
expression: guard,
|
|
1990
|
+
formatted: this.formatExpression(guard),
|
|
1991
|
+
resolved: await this.resolveExpressionValues(guard, evalContext),
|
|
1992
|
+
},
|
|
1993
|
+
constraintOutcomes: constraintResult.outcomes.length > 0 ? constraintResult.outcomes : undefined,
|
|
1994
|
+
emittedEvents: [],
|
|
1995
|
+
};
|
|
1996
|
+
}
|
|
1997
|
+
}
|
|
1998
|
+
// All validation passed
|
|
1999
|
+
return { success: true, emittedEvents: [] };
|
|
2000
|
+
}
|
|
2001
|
+
/**
|
|
2002
|
+
* Drain all pending jobs from the job queue and execute them.
|
|
2003
|
+
* Returns an array of CommandResults, one per drained job.
|
|
2004
|
+
* For deterministic testing: executes jobs synchronously in FIFO order.
|
|
2005
|
+
*
|
|
2006
|
+
* For each job:
|
|
2007
|
+
* - Sets context.source = 'job' to bypass the async enqueue branch
|
|
2008
|
+
* - Executes the full command body (actions + emits)
|
|
2009
|
+
* - Emits completion or failure event on the synthesized channel
|
|
2010
|
+
* - Updates job status in the queue
|
|
2011
|
+
*/
|
|
2012
|
+
async drainJobs() {
|
|
2013
|
+
if (!this.options.jobQueue) {
|
|
2014
|
+
return [];
|
|
2015
|
+
}
|
|
2016
|
+
const pending = await this.options.jobQueue.drainPending();
|
|
2017
|
+
const results = [];
|
|
2018
|
+
const originalSource = this.context.source;
|
|
2019
|
+
for (const job of pending) {
|
|
2020
|
+
// Set context.source = 'job' so the async branch is bypassed
|
|
2021
|
+
this.context.source = 'job';
|
|
2022
|
+
try {
|
|
2023
|
+
const result = await this._executeCommandInternal(job.commandName, job.input, {
|
|
2024
|
+
entityName: job.entityName,
|
|
2025
|
+
instanceId: job.instanceId,
|
|
2026
|
+
correlationId: job.correlationId,
|
|
2027
|
+
causationId: job.causationId,
|
|
2028
|
+
});
|
|
2029
|
+
if (result.success) {
|
|
2030
|
+
await this.options.jobQueue.updateStatus(job.jobId, 'completed', { result: result.result });
|
|
2031
|
+
// Emit synthesized completion event
|
|
2032
|
+
const command = this.getCommand(job.commandName, job.entityName);
|
|
2033
|
+
if (command?.completionEvent) {
|
|
2034
|
+
const completionEvent = {
|
|
2035
|
+
name: command.completionEvent,
|
|
2036
|
+
channel: `jobs.${job.commandName}`,
|
|
2037
|
+
payload: { jobId: job.jobId, result: result.result, completedAt: this.getNow() },
|
|
2038
|
+
timestamp: this.getNow(),
|
|
2039
|
+
correlationId: job.correlationId,
|
|
2040
|
+
causationId: job.causationId,
|
|
2041
|
+
};
|
|
2042
|
+
result.emittedEvents.push(completionEvent);
|
|
2043
|
+
}
|
|
2044
|
+
}
|
|
2045
|
+
else {
|
|
2046
|
+
await this.options.jobQueue.updateStatus(job.jobId, 'failed', { error: result.error });
|
|
2047
|
+
// Emit synthesized failure event
|
|
2048
|
+
const command = this.getCommand(job.commandName, job.entityName);
|
|
2049
|
+
if (command?.failureEvent) {
|
|
2050
|
+
const failureEvent = {
|
|
2051
|
+
name: command.failureEvent,
|
|
2052
|
+
channel: `jobs.${job.commandName}`,
|
|
2053
|
+
payload: { jobId: job.jobId, error: result.error || 'Unknown error', failedAt: this.getNow() },
|
|
2054
|
+
timestamp: this.getNow(),
|
|
2055
|
+
correlationId: job.correlationId,
|
|
2056
|
+
causationId: job.causationId,
|
|
2057
|
+
};
|
|
2058
|
+
result.emittedEvents.push(failureEvent);
|
|
2059
|
+
}
|
|
2060
|
+
}
|
|
2061
|
+
results.push(result);
|
|
2062
|
+
}
|
|
2063
|
+
catch (e) {
|
|
2064
|
+
await this.options.jobQueue.updateStatus(job.jobId, 'failed', {
|
|
2065
|
+
error: e instanceof Error ? e.message : String(e),
|
|
2066
|
+
});
|
|
2067
|
+
// Emit synthesized failure event
|
|
2068
|
+
const command = this.getCommand(job.commandName, job.entityName);
|
|
2069
|
+
if (command?.failureEvent) {
|
|
2070
|
+
const failureEvent = {
|
|
2071
|
+
name: command.failureEvent,
|
|
2072
|
+
channel: `jobs.${job.commandName}`,
|
|
2073
|
+
payload: { jobId: job.jobId, error: e instanceof Error ? e.message : String(e), failedAt: this.getNow() },
|
|
2074
|
+
timestamp: this.getNow(),
|
|
2075
|
+
correlationId: job.correlationId,
|
|
2076
|
+
causationId: job.causationId,
|
|
2077
|
+
};
|
|
2078
|
+
results.push({
|
|
2079
|
+
success: false,
|
|
2080
|
+
error: e instanceof Error ? e.message : String(e),
|
|
2081
|
+
emittedEvents: [failureEvent],
|
|
2082
|
+
});
|
|
2083
|
+
}
|
|
2084
|
+
else {
|
|
2085
|
+
results.push({
|
|
2086
|
+
success: false,
|
|
2087
|
+
error: e instanceof Error ? e.message : String(e),
|
|
2088
|
+
emittedEvents: [],
|
|
2089
|
+
});
|
|
2090
|
+
}
|
|
2091
|
+
}
|
|
2092
|
+
}
|
|
2093
|
+
// Restore original source
|
|
2094
|
+
this.context.source = originalSource;
|
|
2095
|
+
return results;
|
|
2096
|
+
}
|
|
2097
|
+
async _executeCommandInternal(commandName, input, options) {
|
|
2098
|
+
// Clear relationship memoization cache at the start of each command execution
|
|
2099
|
+
// to ensure fresh data after any mutations
|
|
2100
|
+
this.clearMemoCache();
|
|
2101
|
+
// Reset version increment flag at the start of each command execution
|
|
2102
|
+
this.versionIncrementedForCommand = false;
|
|
2103
|
+
// Clear just-created instance tracking
|
|
2104
|
+
this.justCreatedInstanceIds.clear();
|
|
2105
|
+
// Clear transition error tracking
|
|
2106
|
+
this.lastTransitionError = null;
|
|
2107
|
+
// Clear concurrency conflict tracking
|
|
2108
|
+
this.lastConcurrencyConflict = null;
|
|
2109
|
+
this.actionTraceCounter = 0;
|
|
2110
|
+
// Initialize evaluation budget for bounded complexity enforcement
|
|
2111
|
+
const ownsEvalBudget = this.initEvalBudget();
|
|
2112
|
+
try {
|
|
2113
|
+
const command = this.getCommand(commandName, options.entityName);
|
|
2114
|
+
if (!command) {
|
|
2115
|
+
return {
|
|
2116
|
+
success: false,
|
|
2117
|
+
error: `Command '${commandName}' not found`,
|
|
2118
|
+
...(options.correlationId !== undefined ? { correlationId: options.correlationId } : {}),
|
|
2119
|
+
...(options.causationId !== undefined ? { causationId: options.causationId } : {}),
|
|
2120
|
+
emittedEvents: [],
|
|
2121
|
+
};
|
|
2122
|
+
}
|
|
2123
|
+
const shouldAutoCreateInstance = commandName === 'create' && !!options.entityName && !options.instanceId;
|
|
2124
|
+
let autoCreateEntity;
|
|
2125
|
+
let autoCreatePreparedData;
|
|
2126
|
+
let autoCreateEvalInput;
|
|
2127
|
+
if (shouldAutoCreateInstance && options.entityName) {
|
|
2128
|
+
autoCreateEntity = this.getEntity(options.entityName);
|
|
2129
|
+
if (autoCreateEntity) {
|
|
2130
|
+
const bodyId = typeof input.id === 'string' && input.id !== '' ? input.id : this.nextRuntimeId();
|
|
2131
|
+
autoCreatePreparedData = this.prepareCreateData(autoCreateEntity, {
|
|
2132
|
+
...input,
|
|
2133
|
+
id: bodyId,
|
|
2134
|
+
});
|
|
2135
|
+
autoCreateEvalInput = { ...input, id: autoCreatePreparedData.id };
|
|
2136
|
+
}
|
|
2137
|
+
}
|
|
2138
|
+
const instance = options.instanceId && options.entityName
|
|
2139
|
+
? await this.getInstanceRaw(options.entityName, options.instanceId)
|
|
2140
|
+
: autoCreatePreparedData;
|
|
2141
|
+
const evalContext = this.buildEvalContext(autoCreateEvalInput ?? input, instance, options.entityName);
|
|
2142
|
+
if (command.rateLimit) {
|
|
2143
|
+
const tenantValue = this.ir.tenant ? this.resolveTenantValue() : this.context.tenantId;
|
|
2144
|
+
const rl = checkRateLimitGate(this.rateLimiter, command.rateLimit, evalContext, tenantValue, this.getNow());
|
|
2145
|
+
if (!rl.allowed) {
|
|
2146
|
+
return {
|
|
2147
|
+
success: false,
|
|
2148
|
+
error: `Rate limit exceeded for scope ${rl.denial.scopeKey}`,
|
|
2149
|
+
rateLimitDenial: rl.denial,
|
|
2150
|
+
...(options.correlationId !== undefined ? { correlationId: options.correlationId } : {}),
|
|
2151
|
+
...(options.causationId !== undefined ? { causationId: options.causationId } : {}),
|
|
2152
|
+
emittedEvents: [],
|
|
2153
|
+
};
|
|
2154
|
+
}
|
|
2155
|
+
}
|
|
2156
|
+
// Middleware: before-policy hook
|
|
2157
|
+
const beforePolicyResult = await this.runMiddleware('before-policy', command, evalContext, input, options);
|
|
2158
|
+
if (beforePolicyResult)
|
|
2159
|
+
return beforePolicyResult;
|
|
2160
|
+
const policyResult = await this.profilingBridge.trackPhase('policyEvaluation', () => this.checkPolicies(command, evalContext));
|
|
2161
|
+
if (!policyResult.allowed) {
|
|
2162
|
+
return {
|
|
2163
|
+
success: false,
|
|
2164
|
+
error: policyResult.denial?.message,
|
|
2165
|
+
deniedBy: policyResult.denial?.policyName,
|
|
2166
|
+
policyDenial: policyResult.denial,
|
|
2167
|
+
...(options.correlationId !== undefined ? { correlationId: options.correlationId } : {}),
|
|
2168
|
+
...(options.causationId !== undefined ? { causationId: options.causationId } : {}),
|
|
2169
|
+
emittedEvents: [],
|
|
2170
|
+
};
|
|
2171
|
+
}
|
|
2172
|
+
// vNext: Evaluate command constraints (after policies, before guards)
|
|
2173
|
+
// Pass command context so OverrideApplied events include commandName/entityName/instanceId per spec
|
|
2174
|
+
const commandContext = { commandName, entityName: options.entityName, instanceId: options.instanceId };
|
|
2175
|
+
const constraintResult = await this.profilingBridge.trackPhase('constraintValidation', () => this.evaluateCommandConstraints(command, evalContext, options.overrideRequests, commandContext));
|
|
2176
|
+
if (!constraintResult.allowed) {
|
|
2177
|
+
// Find the blocking constraint for the error message
|
|
2178
|
+
const blocking = constraintResult.outcomes.find(o => !o.passed && !o.overridden && o.severity === 'block');
|
|
2179
|
+
return {
|
|
2180
|
+
success: false,
|
|
2181
|
+
error: blocking?.message || `Command blocked by constraint '${blocking?.constraintName}'`,
|
|
2182
|
+
constraintOutcomes: constraintResult.outcomes,
|
|
2183
|
+
overrideRequests: options.overrideRequests,
|
|
2184
|
+
...(options.correlationId !== undefined ? { correlationId: options.correlationId } : {}),
|
|
2185
|
+
...(options.causationId !== undefined ? { causationId: options.causationId } : {}),
|
|
2186
|
+
emittedEvents: [],
|
|
2187
|
+
};
|
|
2188
|
+
}
|
|
2189
|
+
// Middleware: before-guard hook
|
|
2190
|
+
const beforeGuardResult = await this.runMiddleware('before-guard', command, evalContext, input, options);
|
|
2191
|
+
if (beforeGuardResult)
|
|
2192
|
+
return beforeGuardResult;
|
|
2193
|
+
this.profilingBridge.startPhase('guardEvaluation');
|
|
2194
|
+
for (let i = 0; i < command.guards.length; i += 1) {
|
|
2195
|
+
const guard = command.guards[i];
|
|
2196
|
+
const result = await this.evaluateExpression(guard, evalContext);
|
|
2197
|
+
if (!result) {
|
|
2198
|
+
this.profilingBridge.endPhase('guardEvaluation');
|
|
2199
|
+
return {
|
|
2200
|
+
success: false,
|
|
2201
|
+
error: `Guard condition failed for command '${commandName}'`,
|
|
2202
|
+
guardFailure: {
|
|
2203
|
+
index: i + 1,
|
|
2204
|
+
expression: guard,
|
|
2205
|
+
formatted: this.formatExpression(guard),
|
|
2206
|
+
resolved: await this.resolveExpressionValues(guard, evalContext),
|
|
2207
|
+
},
|
|
2208
|
+
// Include constraint outcomes even if guards fail
|
|
2209
|
+
constraintOutcomes: constraintResult.outcomes.length > 0 ? constraintResult.outcomes : undefined,
|
|
2210
|
+
...(options.correlationId !== undefined ? { correlationId: options.correlationId } : {}),
|
|
2211
|
+
...(options.causationId !== undefined ? { causationId: options.causationId } : {}),
|
|
2212
|
+
emittedEvents: [],
|
|
2213
|
+
};
|
|
2214
|
+
}
|
|
2215
|
+
}
|
|
2216
|
+
this.profilingBridge.endPhase('guardEvaluation');
|
|
2217
|
+
// ── Approval gate: block command if pending approval required ──
|
|
2218
|
+
this.profilingBridge.startPhase('approvalGate');
|
|
2219
|
+
if (options.entityName) {
|
|
2220
|
+
const approvalResult = await this.checkApprovalGate(commandName, options.entityName, options.instanceId, evalContext, options);
|
|
2221
|
+
if (approvalResult) {
|
|
2222
|
+
this.profilingBridge.endPhase('approvalGate');
|
|
2223
|
+
return approvalResult;
|
|
2224
|
+
}
|
|
2225
|
+
}
|
|
2226
|
+
this.profilingBridge.endPhase('approvalGate');
|
|
2227
|
+
let autoCreatedInstance;
|
|
2228
|
+
let createConstraintOutcomes;
|
|
2229
|
+
if (shouldAutoCreateInstance && options.entityName && autoCreateEntity && autoCreatePreparedData) {
|
|
2230
|
+
this.profilingBridge.startPhase('autoCreate');
|
|
2231
|
+
const createResult = await this.persistPreparedCreate(options.entityName, autoCreateEntity, autoCreatePreparedData);
|
|
2232
|
+
this.profilingBridge.endPhase('autoCreate');
|
|
2233
|
+
createConstraintOutcomes = createResult.constraintOutcomes;
|
|
2234
|
+
if (!createResult.instance) {
|
|
2235
|
+
const blocking = createConstraintOutcomes?.find(o => !o.passed && !o.overridden && o.severity === 'block');
|
|
2236
|
+
return {
|
|
2237
|
+
success: false,
|
|
2238
|
+
error: blocking?.message || `Command blocked by constraint '${blocking?.constraintName}'`,
|
|
2239
|
+
constraintOutcomes: createConstraintOutcomes,
|
|
2240
|
+
overrideRequests: options.overrideRequests,
|
|
2241
|
+
...(options.correlationId !== undefined ? { correlationId: options.correlationId } : {}),
|
|
2242
|
+
...(options.causationId !== undefined ? { causationId: options.causationId } : {}),
|
|
2243
|
+
emittedEvents: [],
|
|
2244
|
+
};
|
|
2245
|
+
}
|
|
2246
|
+
autoCreatedInstance = createResult.instance;
|
|
2247
|
+
options.instanceId = createResult.instance.id;
|
|
2248
|
+
const createdEvalContext = this.buildEvalContext(autoCreateEvalInput ?? input, createResult.instance, options.entityName);
|
|
2249
|
+
Object.assign(evalContext, createdEvalContext);
|
|
2250
|
+
}
|
|
2251
|
+
// Include any OverrideApplied events from constraint evaluation
|
|
2252
|
+
// Per spec: OverrideApplied events are included in CommandResult.emittedEvents
|
|
2253
|
+
// alongside command-declared events (override events come first)
|
|
2254
|
+
const emittedEvents = [...constraintResult.overrideEvents];
|
|
2255
|
+
let result;
|
|
2256
|
+
const emitCounter = { value: emittedEvents.length };
|
|
2257
|
+
const workflowMeta = {
|
|
2258
|
+
correlationId: options.correlationId,
|
|
2259
|
+
causationId: options.causationId,
|
|
2260
|
+
};
|
|
2261
|
+
// Pre-compute base subject for action-emitted events (entity + command + instanceId).
|
|
2262
|
+
// Full subject.id resolution (created-id / payload.id fallbacks) happens after the
|
|
2263
|
+
// action loop for command-declared events.
|
|
2264
|
+
const baseSubject = { command: commandName };
|
|
2265
|
+
if (options.entityName) {
|
|
2266
|
+
baseSubject.entity = options.entityName;
|
|
2267
|
+
}
|
|
2268
|
+
if (options.instanceId) {
|
|
2269
|
+
baseSubject.id = options.instanceId;
|
|
2270
|
+
}
|
|
2271
|
+
// Middleware: before-action hook (before each action in the loop)
|
|
2272
|
+
const beforeActionResult = await this.runMiddleware('before-action', command, evalContext, input, options);
|
|
2273
|
+
if (beforeActionResult)
|
|
2274
|
+
return beforeActionResult;
|
|
2275
|
+
// Open a command-scoped write buffer so mutate/compute actions batch into a
|
|
2276
|
+
// single store.update (flushed once below) instead of one read+write each.
|
|
2277
|
+
// Seeded with the already-loaded instance so no extra read is incurred.
|
|
2278
|
+
// Nested (reaction) commands save and restore the outer buffer.
|
|
2279
|
+
const prevBuffer = this.commandBuffer;
|
|
2280
|
+
if (options.entityName && options.instanceId) {
|
|
2281
|
+
this.commandBuffer = {
|
|
2282
|
+
entityName: options.entityName,
|
|
2283
|
+
id: options.instanceId,
|
|
2284
|
+
instance: (autoCreatedInstance ?? instance) ?? null,
|
|
2285
|
+
patch: {},
|
|
2286
|
+
};
|
|
2287
|
+
}
|
|
2288
|
+
try {
|
|
2289
|
+
this.profilingBridge.startPhase('actionExecution');
|
|
2290
|
+
for (const action of command.actions) {
|
|
2291
|
+
const actionResult = await this.executeAction(action, evalContext, options, emitCounter, workflowMeta, baseSubject);
|
|
2292
|
+
// Check for transition validation errors after mutate/compute actions.
|
|
2293
|
+
// Returning here skips the flush below: a failed command persists nothing.
|
|
2294
|
+
if (this.lastTransitionError) {
|
|
2295
|
+
return {
|
|
2296
|
+
success: false,
|
|
2297
|
+
error: this.lastTransitionError,
|
|
2298
|
+
...(workflowMeta.correlationId !== undefined ? { correlationId: workflowMeta.correlationId } : {}),
|
|
2299
|
+
...(workflowMeta.causationId !== undefined ? { causationId: workflowMeta.causationId } : {}),
|
|
2300
|
+
emittedEvents: [],
|
|
2301
|
+
};
|
|
2302
|
+
}
|
|
2303
|
+
// Check for concurrency conflict after mutate/compute actions
|
|
2304
|
+
// Per spec: "Commands receiving a ConcurrencyConflict MUST NOT apply mutations"
|
|
2305
|
+
if (this.lastConcurrencyConflict) {
|
|
2306
|
+
const conflict = this.lastConcurrencyConflict;
|
|
2307
|
+
this.lastConcurrencyConflict = null;
|
|
2308
|
+
return {
|
|
2309
|
+
success: false,
|
|
2310
|
+
error: `Concurrency conflict on ${conflict.entityType}#${conflict.entityId}: expected version ${conflict.expectedVersion}, actual ${conflict.actualVersion}`,
|
|
2311
|
+
concurrencyConflict: conflict,
|
|
2312
|
+
...(workflowMeta.correlationId !== undefined ? { correlationId: workflowMeta.correlationId } : {}),
|
|
2313
|
+
...(workflowMeta.causationId !== undefined ? { causationId: workflowMeta.causationId } : {}),
|
|
2314
|
+
emittedEvents: [],
|
|
2315
|
+
};
|
|
2316
|
+
}
|
|
2317
|
+
if ((action.kind === 'mutate' || action.kind === 'compute') && options.instanceId && options.entityName) {
|
|
2318
|
+
const currentInstance = await this.getInstanceRaw(options.entityName, options.instanceId);
|
|
2319
|
+
// Enrich re-fetched instance with _entity for relationship resolution
|
|
2320
|
+
const enriched = currentInstance ? { ...currentInstance, _entity: options.entityName } : currentInstance;
|
|
2321
|
+
// Refresh both self/this bindings and spread instance properties into evalContext
|
|
2322
|
+
evalContext.self = enriched;
|
|
2323
|
+
evalContext.this = enriched;
|
|
2324
|
+
Object.assign(evalContext, enriched);
|
|
2325
|
+
Object.assign(evalContext, input);
|
|
2326
|
+
}
|
|
2327
|
+
result = actionResult;
|
|
2328
|
+
}
|
|
2329
|
+
this.profilingBridge.endPhase('actionExecution');
|
|
2330
|
+
if (autoCreatedInstance && options.entityName) {
|
|
2331
|
+
const currentInstance = await this.getInstanceRaw(options.entityName, autoCreatedInstance.id);
|
|
2332
|
+
autoCreatedInstance = currentInstance ?? autoCreatedInstance;
|
|
2333
|
+
result = autoCreatedInstance;
|
|
2334
|
+
}
|
|
2335
|
+
// Flush the accumulated field changes in a single store write. Runs before
|
|
2336
|
+
// event emission and reaction dispatch so emitted events and any reactions
|
|
2337
|
+
// observe the final committed command state.
|
|
2338
|
+
const cb = this.commandBuffer;
|
|
2339
|
+
if (cb && Object.keys(cb.patch).length > 0) {
|
|
2340
|
+
const cbStore = this.stores.get(cb.entityName);
|
|
2341
|
+
if (cbStore)
|
|
2342
|
+
await cbStore.update(cb.id, cb.patch);
|
|
2343
|
+
}
|
|
2344
|
+
}
|
|
2345
|
+
finally {
|
|
2346
|
+
this.commandBuffer = prevBuffer;
|
|
2347
|
+
}
|
|
2348
|
+
this.profilingBridge.startPhase('eventEmission');
|
|
2349
|
+
// Finalize canonical subject metadata for command-declared events.
|
|
2350
|
+
// Resolution order for subject.id:
|
|
2351
|
+
// 1. instanceId passed to runCommand (already set on baseSubject)
|
|
2352
|
+
// 2. A single deterministically created record id (justCreatedInstanceIds)
|
|
2353
|
+
// 3. Top-level payload.id from the emitted event payload (checked per-event below)
|
|
2354
|
+
// 4. Unset
|
|
2355
|
+
const subject = { ...baseSubject };
|
|
2356
|
+
if (!subject.id && this.justCreatedInstanceIds.size === 1) {
|
|
2357
|
+
const [createdId] = this.justCreatedInstanceIds;
|
|
2358
|
+
subject.id = createdId;
|
|
2359
|
+
}
|
|
2360
|
+
for (const eventName of command.emits) {
|
|
2361
|
+
const event = this.ir.events.find(e => e.name === eventName);
|
|
2362
|
+
const prov = this.ir.provenance;
|
|
2363
|
+
const eventPayload = { ...input, result };
|
|
2364
|
+
// G7: populate explicitly-declared payload fields (`emit Event { field: expr }`).
|
|
2365
|
+
// Evaluated against the post-action evalContext (self = current instance,
|
|
2366
|
+
// command input, user, context) so reactions can read declared event fields
|
|
2367
|
+
// instead of finding them undefined.
|
|
2368
|
+
const payloadSpec = command.emitPayloads?.find(ep => ep.eventName === eventName);
|
|
2369
|
+
if (payloadSpec) {
|
|
2370
|
+
for (const field of payloadSpec.fields) {
|
|
2371
|
+
eventPayload[field.name] = await this.evaluateExpression(field.expression, evalContext);
|
|
2372
|
+
}
|
|
2373
|
+
}
|
|
2374
|
+
// Fallback: resolve subject.id from payload.id if not yet set
|
|
2375
|
+
const eventSubject = subject.id
|
|
2376
|
+
? { ...subject }
|
|
2377
|
+
: {
|
|
2378
|
+
...subject,
|
|
2379
|
+
...(typeof eventPayload.id === 'string' &&
|
|
2380
|
+
eventPayload.id !== ''
|
|
2381
|
+
? { id: eventPayload.id }
|
|
2382
|
+
: {}),
|
|
2383
|
+
};
|
|
2384
|
+
const emitted = {
|
|
2385
|
+
name: eventName,
|
|
2386
|
+
channel: event?.channel || eventName,
|
|
2387
|
+
payload: eventPayload,
|
|
2388
|
+
subject: eventSubject,
|
|
2389
|
+
timestamp: this.getNow(),
|
|
2390
|
+
...(prov ? {
|
|
2391
|
+
provenance: {
|
|
2392
|
+
contentHash: prov.contentHash,
|
|
2393
|
+
compilerVersion: prov.compilerVersion,
|
|
2394
|
+
schemaVersion: prov.schemaVersion,
|
|
2395
|
+
},
|
|
2396
|
+
} : {}),
|
|
2397
|
+
...(workflowMeta.correlationId !== undefined ? { correlationId: workflowMeta.correlationId } : {}),
|
|
2398
|
+
...(workflowMeta.causationId !== undefined ? { causationId: workflowMeta.causationId } : {}),
|
|
2399
|
+
emitIndex: emitCounter.value++,
|
|
2400
|
+
};
|
|
2401
|
+
emittedEvents.push(emitted);
|
|
2402
|
+
this.eventLog.push(emitted);
|
|
2403
|
+
this.notifyListeners(emitted);
|
|
2404
|
+
}
|
|
2405
|
+
// Execute matching reaction rules for emitted events (declaration order)
|
|
2406
|
+
const reactions = this.ir.reactions || [];
|
|
2407
|
+
if (reactions.length > 0 && emittedEvents.length > 0) {
|
|
2408
|
+
// Use index-based iteration since cascading reactions may append to emittedEvents
|
|
2409
|
+
const initialLength = emittedEvents.length;
|
|
2410
|
+
for (let ei = 0; ei < initialLength; ei++) {
|
|
2411
|
+
const emitted = emittedEvents[ei];
|
|
2412
|
+
const matchingReactions = reactions.filter(r => r.event === emitted.name);
|
|
2413
|
+
for (const reaction of matchingReactions) {
|
|
2414
|
+
if (this.reactionDepth >= RuntimeEngine.MAX_REACTION_DEPTH) {
|
|
2415
|
+
throw new ManifestReactionDepthError(this.reactionDepth, reaction.event, `${reaction.targetEntity}.${reaction.targetCommand}`);
|
|
2416
|
+
}
|
|
2417
|
+
// Evaluate resolve and params expressions against event context.
|
|
2418
|
+
// Available bindings:
|
|
2419
|
+
// payload — event payload fields merged with subject metadata
|
|
2420
|
+
// self — alias for payload (convenient for member access)
|
|
2421
|
+
const eventPayloadBase = typeof emitted.payload === 'object' && emitted.payload !== null ? emitted.payload : {};
|
|
2422
|
+
const enrichedPayload = {
|
|
2423
|
+
...eventPayloadBase,
|
|
2424
|
+
// Alias the event source id to top-level `id` so reaction expressions
|
|
2425
|
+
// like `self.id` / `payload.id` resolve (the Convex projection's
|
|
2426
|
+
// reaction payload does the same). Only when the payload has no id.
|
|
2427
|
+
...(eventPayloadBase.id === undefined && emitted.subject?.id !== undefined ? { id: emitted.subject.id } : {}),
|
|
2428
|
+
_subject: emitted.subject,
|
|
2429
|
+
_eventName: emitted.name,
|
|
2430
|
+
_channel: emitted.channel,
|
|
2431
|
+
};
|
|
2432
|
+
const reactionContext = {
|
|
2433
|
+
payload: enrichedPayload,
|
|
2434
|
+
self: enrichedPayload,
|
|
2435
|
+
};
|
|
2436
|
+
// Fan-out reaction: dispatch the command on EVERY target row where
|
|
2437
|
+
// row.<matchField> == matchSource (evaluated against the event payload),
|
|
2438
|
+
// instead of one resolved target. The collection match replaces resolve.
|
|
2439
|
+
if (reaction.fanOut) {
|
|
2440
|
+
const matchValue = await this.evaluateExpression(reaction.fanOut.matchSource, reactionContext);
|
|
2441
|
+
const fanInput = {};
|
|
2442
|
+
if (reaction.params) {
|
|
2443
|
+
for (const p of reaction.params) {
|
|
2444
|
+
fanInput[p.name] = await this.evaluateExpression(p.expression, reactionContext);
|
|
2445
|
+
}
|
|
2446
|
+
}
|
|
2447
|
+
const matchField = reaction.fanOut.matchField;
|
|
2448
|
+
const matches = (await this.getAllInstancesRaw(reaction.targetEntity))
|
|
2449
|
+
.filter(inst => inst[matchField] === matchValue);
|
|
2450
|
+
for (const m of matches) {
|
|
2451
|
+
if (this.reactionDepth >= RuntimeEngine.MAX_REACTION_DEPTH) {
|
|
2452
|
+
throw new ManifestReactionDepthError(this.reactionDepth, reaction.event, `${reaction.targetEntity}.${reaction.targetCommand}`);
|
|
2453
|
+
}
|
|
2454
|
+
this.reactionDepth++;
|
|
2455
|
+
try {
|
|
2456
|
+
const fanResult = await this.runCommand(reaction.targetCommand, fanInput, {
|
|
2457
|
+
entityName: reaction.targetEntity,
|
|
2458
|
+
instanceId: String(m.id ?? ''),
|
|
2459
|
+
correlationId: workflowMeta.correlationId,
|
|
2460
|
+
causationId: emitted.name,
|
|
2461
|
+
});
|
|
2462
|
+
if (fanResult.emittedEvents)
|
|
2463
|
+
emittedEvents.push(...fanResult.emittedEvents);
|
|
2464
|
+
}
|
|
2465
|
+
finally {
|
|
2466
|
+
this.reactionDepth--;
|
|
2467
|
+
}
|
|
2468
|
+
}
|
|
2469
|
+
continue;
|
|
2470
|
+
}
|
|
2471
|
+
// Single-target reaction: fanOut reactions have no resolve and continue above.
|
|
2472
|
+
if (!reaction.resolve)
|
|
2473
|
+
continue;
|
|
2474
|
+
const resolvedId = await this.evaluateExpression(reaction.resolve, reactionContext);
|
|
2475
|
+
// Evaluate param mappings
|
|
2476
|
+
const reactionInput = {};
|
|
2477
|
+
if (reaction.params) {
|
|
2478
|
+
for (const param of reaction.params) {
|
|
2479
|
+
reactionInput[param.name] = await this.evaluateExpression(param.expression, reactionContext);
|
|
2480
|
+
}
|
|
2481
|
+
}
|
|
2482
|
+
// A reaction whose target command is `create` must flow through the
|
|
2483
|
+
// auto-create path (runCommand only auto-creates when instanceId is
|
|
2484
|
+
// ABSENT). Forcing instanceId here made create-target reactions run
|
|
2485
|
+
// mutate actions against a non-existent instance and persist nothing.
|
|
2486
|
+
// For create targets the resolved value identifies the NEW instance's
|
|
2487
|
+
// id, so thread it through as input.id (unless params set one).
|
|
2488
|
+
const isCreateTarget = reaction.targetCommand === 'create';
|
|
2489
|
+
let reactionInstanceId = String(resolvedId);
|
|
2490
|
+
if (isCreateTarget) {
|
|
2491
|
+
reactionInstanceId = undefined;
|
|
2492
|
+
if (reactionInput.id === undefined && resolvedId !== undefined && resolvedId !== null) {
|
|
2493
|
+
reactionInput.id = String(resolvedId);
|
|
2494
|
+
}
|
|
2495
|
+
}
|
|
2496
|
+
// Dispatch the reaction command
|
|
2497
|
+
this.reactionDepth++;
|
|
2498
|
+
try {
|
|
2499
|
+
const reactionResult = await this.runCommand(reaction.targetCommand, reactionInput, {
|
|
2500
|
+
entityName: reaction.targetEntity,
|
|
2501
|
+
instanceId: reactionInstanceId,
|
|
2502
|
+
correlationId: workflowMeta.correlationId,
|
|
2503
|
+
causationId: emitted.name,
|
|
2504
|
+
});
|
|
2505
|
+
// Collect events from reaction-triggered commands
|
|
2506
|
+
if (reactionResult.emittedEvents) {
|
|
2507
|
+
emittedEvents.push(...reactionResult.emittedEvents);
|
|
2508
|
+
}
|
|
2509
|
+
}
|
|
2510
|
+
finally {
|
|
2511
|
+
this.reactionDepth--;
|
|
2512
|
+
}
|
|
2513
|
+
}
|
|
2514
|
+
}
|
|
2515
|
+
}
|
|
2516
|
+
this.profilingBridge.endPhase('eventEmission');
|
|
2517
|
+
const commandResult = {
|
|
2518
|
+
success: true,
|
|
2519
|
+
result,
|
|
2520
|
+
...(autoCreatedInstance ? { instance: autoCreatedInstance } : {}),
|
|
2521
|
+
// Include constraint outcomes in successful result
|
|
2522
|
+
constraintOutcomes: [...constraintResult.outcomes, ...(createConstraintOutcomes ?? [])].length > 0
|
|
2523
|
+
? [...constraintResult.outcomes, ...(createConstraintOutcomes ?? [])]
|
|
2524
|
+
: undefined,
|
|
2525
|
+
...(workflowMeta.correlationId !== undefined ? { correlationId: workflowMeta.correlationId } : {}),
|
|
2526
|
+
...(workflowMeta.causationId !== undefined ? { causationId: workflowMeta.causationId } : {}),
|
|
2527
|
+
emittedEvents,
|
|
2528
|
+
};
|
|
2529
|
+
// Middleware: after-emit hook
|
|
2530
|
+
const afterEmitResult = await this.runMiddleware('after-emit', command, evalContext, input, options, emittedEvents);
|
|
2531
|
+
if (afterEmitResult)
|
|
2532
|
+
return afterEmitResult;
|
|
2533
|
+
return commandResult;
|
|
2534
|
+
}
|
|
2535
|
+
catch (e) {
|
|
2536
|
+
if (e instanceof EvaluationBudgetExceededError) {
|
|
2537
|
+
return {
|
|
2538
|
+
success: false,
|
|
2539
|
+
error: e.message,
|
|
2540
|
+
...(options.correlationId !== undefined ? { correlationId: options.correlationId } : {}),
|
|
2541
|
+
...(options.causationId !== undefined ? { causationId: options.causationId } : {}),
|
|
2542
|
+
emittedEvents: [],
|
|
2543
|
+
};
|
|
2544
|
+
}
|
|
2545
|
+
throw e; // re-throw other errors (ManifestEffectBoundaryError, etc.)
|
|
2546
|
+
}
|
|
2547
|
+
finally {
|
|
2548
|
+
if (ownsEvalBudget)
|
|
2549
|
+
this.clearEvalBudget();
|
|
2550
|
+
}
|
|
2551
|
+
}
|
|
2552
|
+
buildEvalContext(input, instance, entityName) {
|
|
2553
|
+
// Enrich instance with _entity metadata so relationship resolution works
|
|
2554
|
+
// when the member expression handler reads _entity from self/this
|
|
2555
|
+
const enrichedInstance = (instance && entityName)
|
|
2556
|
+
? { ...instance, _entity: entityName }
|
|
2557
|
+
: instance;
|
|
2558
|
+
const baseContext = {
|
|
2559
|
+
...(enrichedInstance || {}),
|
|
2560
|
+
...input,
|
|
2561
|
+
self: enrichedInstance ?? null,
|
|
2562
|
+
this: enrichedInstance ?? null,
|
|
2563
|
+
user: this.context.user ?? null,
|
|
2564
|
+
context: this.context ?? {},
|
|
2565
|
+
};
|
|
2566
|
+
return baseContext;
|
|
2567
|
+
}
|
|
2568
|
+
async checkPolicies(command, evalContext) {
|
|
2569
|
+
// If command has explicit policies (expanded from entity defaults or declared),
|
|
2570
|
+
// evaluate only those policies by name
|
|
2571
|
+
let relevantPolicies;
|
|
2572
|
+
if (command.policies && command.policies.length > 0) {
|
|
2573
|
+
// Filter by policy names specified on the command
|
|
2574
|
+
const policyNames = new Set(command.policies);
|
|
2575
|
+
relevantPolicies = this.ir.policies.filter(p => policyNames.has(p.name));
|
|
2576
|
+
}
|
|
2577
|
+
else {
|
|
2578
|
+
// Fallback: filter by entity match and action type (legacy behavior)
|
|
2579
|
+
relevantPolicies = this.ir.policies.filter(p => {
|
|
2580
|
+
if (p.entity && command.entity && p.entity !== command.entity)
|
|
2581
|
+
return false;
|
|
2582
|
+
if (p.action !== 'all' && p.action !== 'execute')
|
|
2583
|
+
return false;
|
|
2584
|
+
return true;
|
|
2585
|
+
});
|
|
2586
|
+
}
|
|
2587
|
+
const tenantValue = this.ir.tenant ? this.resolveTenantValue() : this.context.tenantId;
|
|
2588
|
+
for (const policy of relevantPolicies) {
|
|
2589
|
+
if (policyHasRateLimit(policy) && policy.rateLimit) {
|
|
2590
|
+
const rl = checkRateLimitGate(this.rateLimiter, policy.rateLimit, evalContext, tenantValue, this.getNow(), `policy:${policy.name}`);
|
|
2591
|
+
if (!rl.allowed) {
|
|
2592
|
+
return {
|
|
2593
|
+
allowed: false,
|
|
2594
|
+
denial: {
|
|
2595
|
+
policyName: policy.name,
|
|
2596
|
+
expression: policy.expression,
|
|
2597
|
+
formatted: this.formatExpression(policy.expression),
|
|
2598
|
+
message: policy.message || `Rate limit exceeded for policy '${policy.name}'`,
|
|
2599
|
+
contextKeys: this.extractContextKeys(policy.expression),
|
|
2600
|
+
},
|
|
2601
|
+
};
|
|
2602
|
+
}
|
|
2603
|
+
}
|
|
2604
|
+
const result = await this.evaluateExpression(policy.expression, evalContext);
|
|
2605
|
+
if (!result) {
|
|
2606
|
+
// Extract context keys (not values for security)
|
|
2607
|
+
const contextKeys = this.extractContextKeys(policy.expression);
|
|
2608
|
+
// Resolve expression values for diagnostics
|
|
2609
|
+
const resolved = await this.resolveExpressionValues(policy.expression, evalContext);
|
|
2610
|
+
return {
|
|
2611
|
+
allowed: false,
|
|
2612
|
+
denial: {
|
|
2613
|
+
policyName: policy.name,
|
|
2614
|
+
expression: policy.expression,
|
|
2615
|
+
formatted: this.formatExpression(policy.expression),
|
|
2616
|
+
message: policy.message || `Denied by policy '${policy.name}'`,
|
|
2617
|
+
contextKeys,
|
|
2618
|
+
resolved,
|
|
2619
|
+
},
|
|
2620
|
+
};
|
|
2621
|
+
}
|
|
2622
|
+
}
|
|
2623
|
+
return { allowed: true };
|
|
2624
|
+
}
|
|
2625
|
+
/**
|
|
2626
|
+
* Validate entity constraints against instance data
|
|
2627
|
+
* Returns array of constraint failures (empty if all pass)
|
|
2628
|
+
*
|
|
2629
|
+
* Constraint semantics:
|
|
2630
|
+
* - Expression evaluates to TRUE → condition is met → constraint PASSES
|
|
2631
|
+
* - Expression evaluates to FALSE → condition is not met → constraint FAILS
|
|
2632
|
+
*
|
|
2633
|
+
* Severity affects what gets reported as failures:
|
|
2634
|
+
* - severity='block': Failed constraints are returned as failures (block execution)
|
|
2635
|
+
* - severity='warn': Failed constraints are NOT returned as failures (informational only)
|
|
2636
|
+
* - severity='ok': Failed constraints are NOT returned as failures (informational only)
|
|
2637
|
+
*
|
|
2638
|
+
* CONSTRAINT SEMANTICS (vNext hybrid support):
|
|
2639
|
+
* - Positive constraints (default): Expression describes what MUST be true for validity
|
|
2640
|
+
* - When FALSE → constraint FAILS (e.g., "amount >= 0" fails when amount = -1)
|
|
2641
|
+
* - When TRUE → constraint PASSES
|
|
2642
|
+
* - Negative constraints (detected by "severity" prefix): Expression describes BAD state
|
|
2643
|
+
* - When TRUE → constraint FIRES (e.g., "status == 'cancelled'" fires when cancelled)
|
|
2644
|
+
* - When FALSE → constraint PASSES (no bad state present)
|
|
2645
|
+
*/
|
|
2646
|
+
async validateConstraints(entity, instanceData) {
|
|
2647
|
+
const outcomes = [];
|
|
2648
|
+
// Enrich instance with _entity metadata so relationship resolution works
|
|
2649
|
+
// when the member expression handler reads _entity from self/this
|
|
2650
|
+
const enrichedData = { ...instanceData, _entity: entity.name };
|
|
2651
|
+
const evalContext = {
|
|
2652
|
+
...enrichedData,
|
|
2653
|
+
self: enrichedData,
|
|
2654
|
+
this: enrichedData,
|
|
2655
|
+
user: this.context.user ?? null,
|
|
2656
|
+
context: this.context ?? {},
|
|
2657
|
+
};
|
|
2658
|
+
// Use evaluateConstraint to build proper ConstraintOutcome objects
|
|
2659
|
+
for (const constraint of entity.constraints) {
|
|
2660
|
+
const outcome = await this.evaluateConstraint(constraint, evalContext);
|
|
2661
|
+
outcomes.push(outcome);
|
|
2662
|
+
}
|
|
2663
|
+
return outcomes;
|
|
2664
|
+
}
|
|
2665
|
+
extractContextKeys(expr) {
|
|
2666
|
+
const keys = new Set();
|
|
2667
|
+
const walk = (node) => {
|
|
2668
|
+
switch (node.kind) {
|
|
2669
|
+
case 'identifier':
|
|
2670
|
+
// Add built-in identifiers and any user-defined identifiers
|
|
2671
|
+
if (node.name === 'self' || node.name === 'this' || node.name === 'user' || node.name === 'context') {
|
|
2672
|
+
keys.add(node.name);
|
|
2673
|
+
}
|
|
2674
|
+
return;
|
|
2675
|
+
case 'member': {
|
|
2676
|
+
// Add the base identifier (e.g., 'user' from 'user.role')
|
|
2677
|
+
walk(node.object);
|
|
2678
|
+
// Also add the full path as a key
|
|
2679
|
+
const base = this.formatExpression(node.object);
|
|
2680
|
+
keys.add(`${base}.${node.property}`);
|
|
2681
|
+
return;
|
|
2682
|
+
}
|
|
2683
|
+
case 'binary':
|
|
2684
|
+
walk(node.left);
|
|
2685
|
+
walk(node.right);
|
|
2686
|
+
return;
|
|
2687
|
+
case 'unary':
|
|
2688
|
+
walk(node.operand);
|
|
2689
|
+
return;
|
|
2690
|
+
case 'call':
|
|
2691
|
+
node.args.forEach(walk);
|
|
2692
|
+
return;
|
|
2693
|
+
case 'conditional':
|
|
2694
|
+
walk(node.condition);
|
|
2695
|
+
walk(node.consequent);
|
|
2696
|
+
walk(node.alternate);
|
|
2697
|
+
return;
|
|
2698
|
+
case 'array':
|
|
2699
|
+
node.elements.forEach(walk);
|
|
2700
|
+
return;
|
|
2701
|
+
case 'object':
|
|
2702
|
+
node.properties.forEach(p => walk(p.value));
|
|
2703
|
+
return;
|
|
2704
|
+
case 'lambda':
|
|
2705
|
+
walk(node.body);
|
|
2706
|
+
return;
|
|
2707
|
+
default:
|
|
2708
|
+
return;
|
|
2709
|
+
}
|
|
2710
|
+
};
|
|
2711
|
+
walk(expr);
|
|
2712
|
+
return Array.from(keys).sort();
|
|
2713
|
+
}
|
|
2714
|
+
formatExpression(expr) {
|
|
2715
|
+
switch (expr.kind) {
|
|
2716
|
+
case 'literal':
|
|
2717
|
+
return this.formatValue(expr.value);
|
|
2718
|
+
case 'identifier':
|
|
2719
|
+
return expr.name;
|
|
2720
|
+
case 'member':
|
|
2721
|
+
return `${this.formatExpression(expr.object)}.${expr.property}`;
|
|
2722
|
+
case 'binary':
|
|
2723
|
+
return `${this.formatExpression(expr.left)} ${expr.operator} ${this.formatExpression(expr.right)}`;
|
|
2724
|
+
case 'unary':
|
|
2725
|
+
return expr.operator === 'not'
|
|
2726
|
+
? `not ${this.formatExpression(expr.operand)}`
|
|
2727
|
+
: `${expr.operator}${this.formatExpression(expr.operand)}`;
|
|
2728
|
+
case 'call':
|
|
2729
|
+
return `${this.formatExpression(expr.callee)}(${expr.args.map(arg => this.formatExpression(arg)).join(', ')})`;
|
|
2730
|
+
case 'conditional':
|
|
2731
|
+
return `${this.formatExpression(expr.condition)} ? ${this.formatExpression(expr.consequent)} : ${this.formatExpression(expr.alternate)}`;
|
|
2732
|
+
case 'array':
|
|
2733
|
+
return `[${expr.elements.map(el => this.formatExpression(el)).join(', ')}]`;
|
|
2734
|
+
case 'object':
|
|
2735
|
+
return `{ ${expr.properties.map(p => `${p.key}: ${this.formatExpression(p.value)}`).join(', ')} }`;
|
|
2736
|
+
case 'lambda':
|
|
2737
|
+
return `(${expr.params.join(', ')}) => ${this.formatExpression(expr.body)}`;
|
|
2738
|
+
default:
|
|
2739
|
+
return '<expr>';
|
|
2740
|
+
}
|
|
2741
|
+
}
|
|
2742
|
+
formatValue(value) {
|
|
2743
|
+
switch (value.kind) {
|
|
2744
|
+
case 'string':
|
|
2745
|
+
return JSON.stringify(value.value);
|
|
2746
|
+
case 'number':
|
|
2747
|
+
return String(value.value);
|
|
2748
|
+
case 'boolean':
|
|
2749
|
+
return String(value.value);
|
|
2750
|
+
case 'null':
|
|
2751
|
+
return 'null';
|
|
2752
|
+
case 'array':
|
|
2753
|
+
return `[${value.elements.map(el => this.formatValue(el)).join(', ')}]`;
|
|
2754
|
+
case 'object':
|
|
2755
|
+
return `{ ${Object.entries(value.properties).map(([k, v]) => `${k}: ${this.formatValue(v)}`).join(', ')} }`;
|
|
2756
|
+
default:
|
|
2757
|
+
return 'null';
|
|
2758
|
+
}
|
|
2759
|
+
}
|
|
2760
|
+
async resolveExpressionValues(expr, evalContext) {
|
|
2761
|
+
const entries = [];
|
|
2762
|
+
const seen = new Set();
|
|
2763
|
+
const addEntry = async (node) => {
|
|
2764
|
+
const formatted = this.formatExpression(node);
|
|
2765
|
+
if (seen.has(formatted))
|
|
2766
|
+
return;
|
|
2767
|
+
seen.add(formatted);
|
|
2768
|
+
let value;
|
|
2769
|
+
try {
|
|
2770
|
+
value = await this.evaluateExpression(node, evalContext);
|
|
2771
|
+
}
|
|
2772
|
+
catch {
|
|
2773
|
+
value = undefined;
|
|
2774
|
+
}
|
|
2775
|
+
entries.push({ expression: formatted, value });
|
|
2776
|
+
};
|
|
2777
|
+
const walk = async (node) => {
|
|
2778
|
+
switch (node.kind) {
|
|
2779
|
+
case 'literal':
|
|
2780
|
+
case 'identifier':
|
|
2781
|
+
case 'member':
|
|
2782
|
+
await addEntry(node);
|
|
2783
|
+
return;
|
|
2784
|
+
case 'binary':
|
|
2785
|
+
await walk(node.left);
|
|
2786
|
+
await walk(node.right);
|
|
2787
|
+
return;
|
|
2788
|
+
case 'unary':
|
|
2789
|
+
await walk(node.operand);
|
|
2790
|
+
return;
|
|
2791
|
+
case 'call':
|
|
2792
|
+
for (const arg of node.args) {
|
|
2793
|
+
await walk(arg);
|
|
2794
|
+
}
|
|
2795
|
+
return;
|
|
2796
|
+
case 'conditional':
|
|
2797
|
+
await walk(node.condition);
|
|
2798
|
+
await walk(node.consequent);
|
|
2799
|
+
await walk(node.alternate);
|
|
2800
|
+
return;
|
|
2801
|
+
case 'array':
|
|
2802
|
+
for (const el of node.elements) {
|
|
2803
|
+
await walk(el);
|
|
2804
|
+
}
|
|
2805
|
+
return;
|
|
2806
|
+
case 'object':
|
|
2807
|
+
for (const prop of node.properties) {
|
|
2808
|
+
await walk(prop.value);
|
|
2809
|
+
}
|
|
2810
|
+
return;
|
|
2811
|
+
case 'lambda':
|
|
2812
|
+
await walk(node.body);
|
|
2813
|
+
return;
|
|
2814
|
+
default:
|
|
2815
|
+
return;
|
|
2816
|
+
}
|
|
2817
|
+
};
|
|
2818
|
+
await walk(expr);
|
|
2819
|
+
return entries;
|
|
2820
|
+
}
|
|
2821
|
+
async notifyActionTrace(index, kind, target, options) {
|
|
2822
|
+
const hook = this.options.actionTraceHook;
|
|
2823
|
+
if (!hook)
|
|
2824
|
+
return;
|
|
2825
|
+
await hook({
|
|
2826
|
+
index,
|
|
2827
|
+
kind,
|
|
2828
|
+
target,
|
|
2829
|
+
entityName: options.entityName,
|
|
2830
|
+
instanceId: options.instanceId,
|
|
2831
|
+
});
|
|
2832
|
+
}
|
|
2833
|
+
async executeAction(action, evalContext, options, emitCounter, workflowMeta, subject) {
|
|
2834
|
+
// Effect boundary enforcement: in deterministic mode, adapter actions hard-error.
|
|
2835
|
+
// Sources, in precedence order: options.deterministicMode (explicit caller intent),
|
|
2836
|
+
// then context.deterministic (ambient context). See docs/spec/semantics.md
|
|
2837
|
+
// § "Runtime Context Schema".
|
|
2838
|
+
const deterministic = this.options.deterministicMode ?? this.context.deterministic ?? false;
|
|
2839
|
+
if (deterministic &&
|
|
2840
|
+
(action.kind === 'persist' || action.kind === 'publish' || action.kind === 'effect')) {
|
|
2841
|
+
throw new ManifestEffectBoundaryError(action.kind);
|
|
2842
|
+
}
|
|
2843
|
+
const value = await this.evaluateExpression(action.expression, evalContext);
|
|
2844
|
+
const traceIndex = ++this.actionTraceCounter;
|
|
2845
|
+
switch (action.kind) {
|
|
2846
|
+
case 'mutate':
|
|
2847
|
+
if (action.target && options.instanceId && options.entityName) {
|
|
2848
|
+
await this.updateInstance(options.entityName, options.instanceId, {
|
|
2849
|
+
[action.target]: value,
|
|
2850
|
+
});
|
|
2851
|
+
}
|
|
2852
|
+
await this.notifyActionTrace(traceIndex, action.kind, action.target, options);
|
|
2853
|
+
return value;
|
|
2854
|
+
case 'emit':
|
|
2855
|
+
case 'publish': {
|
|
2856
|
+
const prov = this.ir.provenance;
|
|
2857
|
+
const event = {
|
|
2858
|
+
name: 'action_event',
|
|
2859
|
+
channel: 'default',
|
|
2860
|
+
payload: value,
|
|
2861
|
+
...(subject ? { subject } : {}),
|
|
2862
|
+
timestamp: this.getNow(),
|
|
2863
|
+
...(prov ? {
|
|
2864
|
+
provenance: {
|
|
2865
|
+
contentHash: prov.contentHash,
|
|
2866
|
+
compilerVersion: prov.compilerVersion,
|
|
2867
|
+
schemaVersion: prov.schemaVersion,
|
|
2868
|
+
},
|
|
2869
|
+
} : {}),
|
|
2870
|
+
...(workflowMeta.correlationId !== undefined ? { correlationId: workflowMeta.correlationId } : {}),
|
|
2871
|
+
...(workflowMeta.causationId !== undefined ? { causationId: workflowMeta.causationId } : {}),
|
|
2872
|
+
emitIndex: emitCounter.value++,
|
|
2873
|
+
};
|
|
2874
|
+
this.eventLog.push(event);
|
|
2875
|
+
this.notifyListeners(event);
|
|
2876
|
+
await this.notifyActionTrace(traceIndex, action.kind, undefined, options);
|
|
2877
|
+
return value;
|
|
2878
|
+
}
|
|
2879
|
+
case 'persist':
|
|
2880
|
+
await this.notifyActionTrace(traceIndex, action.kind, undefined, options);
|
|
2881
|
+
return value;
|
|
2882
|
+
case 'compute':
|
|
2883
|
+
if (action.target && options.instanceId && options.entityName) {
|
|
2884
|
+
await this.updateInstance(options.entityName, options.instanceId, {
|
|
2885
|
+
[action.target]: value,
|
|
2886
|
+
});
|
|
2887
|
+
}
|
|
2888
|
+
await this.notifyActionTrace(traceIndex, action.kind, action.target, options);
|
|
2889
|
+
return value;
|
|
2890
|
+
case 'effect':
|
|
2891
|
+
default:
|
|
2892
|
+
await this.notifyActionTrace(traceIndex, action.kind, undefined, options);
|
|
2893
|
+
return value;
|
|
2894
|
+
}
|
|
2895
|
+
}
|
|
2896
|
+
async evaluateExpression(expr, context) {
|
|
2897
|
+
// Bounded complexity enforcement
|
|
2898
|
+
if (this.evalBudget) {
|
|
2899
|
+
this.evalBudget.steps++;
|
|
2900
|
+
if (this.evalBudget.steps > this.evalBudget.maxSteps) {
|
|
2901
|
+
throw new EvaluationBudgetExceededError('steps', this.evalBudget.maxSteps);
|
|
2902
|
+
}
|
|
2903
|
+
this.evalBudget.depth++;
|
|
2904
|
+
if (this.evalBudget.depth > this.evalBudget.maxDepth) {
|
|
2905
|
+
throw new EvaluationBudgetExceededError('depth', this.evalBudget.maxDepth);
|
|
2906
|
+
}
|
|
2907
|
+
}
|
|
2908
|
+
try {
|
|
2909
|
+
// WASM fast path: if a WASM evaluator is configured and ready, and the
|
|
2910
|
+
// expression is a pure computational expression (no relationship resolution
|
|
2911
|
+
// needed), use the WASM module for near-native execution speed.
|
|
2912
|
+
// Falls back transparently to TypeScript on any error.
|
|
2913
|
+
if (this.options.wasmEvaluator?.isReady() && this.isWasmCompatible(expr)) {
|
|
2914
|
+
try {
|
|
2915
|
+
const result = await this.options.wasmEvaluator.evaluate(expr, context);
|
|
2916
|
+
return result;
|
|
2917
|
+
}
|
|
2918
|
+
catch {
|
|
2919
|
+
// Fall through to TypeScript evaluation on WASM error
|
|
2920
|
+
}
|
|
2921
|
+
}
|
|
2922
|
+
switch (expr.kind) {
|
|
2923
|
+
case 'literal':
|
|
2924
|
+
return this.irValueToJs(expr.value);
|
|
2925
|
+
case 'identifier': {
|
|
2926
|
+
const name = expr.name;
|
|
2927
|
+
if (name in context)
|
|
2928
|
+
return context[name];
|
|
2929
|
+
if (name === 'true')
|
|
2930
|
+
return true;
|
|
2931
|
+
if (name === 'false')
|
|
2932
|
+
return false;
|
|
2933
|
+
if (name === 'null')
|
|
2934
|
+
return null;
|
|
2935
|
+
return undefined;
|
|
2936
|
+
}
|
|
2937
|
+
case 'member': {
|
|
2938
|
+
const obj = await this.evaluateExpression(expr.object, context);
|
|
2939
|
+
if (obj && typeof obj === 'object') {
|
|
2940
|
+
// Check if this is an entity instance that may have relationships
|
|
2941
|
+
// Works for direct self/this access AND chained traversal (self.order.customer)
|
|
2942
|
+
// because resolveRelationship enriches results with _entity metadata
|
|
2943
|
+
if ('id' in obj && typeof obj.id === 'string') {
|
|
2944
|
+
const entityName = obj._entity;
|
|
2945
|
+
if (entityName) {
|
|
2946
|
+
const relKey = `${entityName}.${expr.property}`;
|
|
2947
|
+
if (this.relationshipIndex.has(relKey)) {
|
|
2948
|
+
return await this.resolveRelationship(entityName, obj, expr.property);
|
|
2949
|
+
}
|
|
2950
|
+
}
|
|
2951
|
+
}
|
|
2952
|
+
// Use hasOwnProperty check to prevent prototype pollution
|
|
2953
|
+
return Object.prototype.hasOwnProperty.call(obj, expr.property)
|
|
2954
|
+
? obj[expr.property]
|
|
2955
|
+
: undefined;
|
|
2956
|
+
}
|
|
2957
|
+
return undefined;
|
|
2958
|
+
}
|
|
2959
|
+
case 'binary': {
|
|
2960
|
+
const left = await this.evaluateExpression(expr.left, context);
|
|
2961
|
+
const right = await this.evaluateExpression(expr.right, context);
|
|
2962
|
+
return this.evaluateBinaryOp(expr.operator, left, right);
|
|
2963
|
+
}
|
|
2964
|
+
case 'unary': {
|
|
2965
|
+
const operand = await this.evaluateExpression(expr.operand, context);
|
|
2966
|
+
return this.evaluateUnaryOp(expr.operator, operand);
|
|
2967
|
+
}
|
|
2968
|
+
case 'call': {
|
|
2969
|
+
// Check if callee is a built-in function identifier
|
|
2970
|
+
const calleeExpr = expr.callee;
|
|
2971
|
+
if (calleeExpr.kind === 'identifier') {
|
|
2972
|
+
const builtins = this.getBuiltins();
|
|
2973
|
+
if (calleeExpr.name in builtins) {
|
|
2974
|
+
const args = await Promise.all(expr.args.map(a => this.evaluateExpression(a, context)));
|
|
2975
|
+
return builtins[calleeExpr.name](...args);
|
|
2976
|
+
}
|
|
2977
|
+
}
|
|
2978
|
+
// Array method calls: arr.contains(x), arr.all(pred), arr.any(pred)
|
|
2979
|
+
if (calleeExpr.kind === 'member') {
|
|
2980
|
+
const property = calleeExpr.property;
|
|
2981
|
+
const arr = await this.evaluateExpression(calleeExpr.object, context);
|
|
2982
|
+
if (Array.isArray(arr)) {
|
|
2983
|
+
if (property === 'contains') {
|
|
2984
|
+
const needle = await this.evaluateExpression(expr.args[0], context);
|
|
2985
|
+
return arr.includes(needle);
|
|
2986
|
+
}
|
|
2987
|
+
if (property === 'all' || property === 'any') {
|
|
2988
|
+
const predicate = await this.evaluateExpression(expr.args[0], context);
|
|
2989
|
+
if (typeof predicate === 'function') {
|
|
2990
|
+
if (property === 'all') {
|
|
2991
|
+
for (const element of arr) {
|
|
2992
|
+
const result = await Promise.resolve(predicate(element));
|
|
2993
|
+
if (!result)
|
|
2994
|
+
return false;
|
|
2995
|
+
}
|
|
2996
|
+
return true;
|
|
2997
|
+
}
|
|
2998
|
+
for (const element of arr) {
|
|
2999
|
+
const result = await Promise.resolve(predicate(element));
|
|
3000
|
+
if (result)
|
|
3001
|
+
return true;
|
|
3002
|
+
}
|
|
3003
|
+
return false;
|
|
3004
|
+
}
|
|
3005
|
+
}
|
|
3006
|
+
}
|
|
3007
|
+
}
|
|
3008
|
+
// Default: evaluate callee and call as function
|
|
3009
|
+
const callee = await this.evaluateExpression(expr.callee, context);
|
|
3010
|
+
const args = await Promise.all(expr.args.map(a => this.evaluateExpression(a, context)));
|
|
3011
|
+
if (typeof callee === 'function') {
|
|
3012
|
+
return callee(...args);
|
|
3013
|
+
}
|
|
3014
|
+
return undefined;
|
|
3015
|
+
}
|
|
3016
|
+
case 'conditional': {
|
|
3017
|
+
const condition = await this.evaluateExpression(expr.condition, context);
|
|
3018
|
+
return condition
|
|
3019
|
+
? await this.evaluateExpression(expr.consequent, context)
|
|
3020
|
+
: await this.evaluateExpression(expr.alternate, context);
|
|
3021
|
+
}
|
|
3022
|
+
case 'array':
|
|
3023
|
+
return await Promise.all(expr.elements.map(e => this.evaluateExpression(e, context)));
|
|
3024
|
+
case 'object': {
|
|
3025
|
+
const result = {};
|
|
3026
|
+
for (const prop of expr.properties) {
|
|
3027
|
+
result[prop.key] = await this.evaluateExpression(prop.value, context);
|
|
3028
|
+
}
|
|
3029
|
+
return result;
|
|
3030
|
+
}
|
|
3031
|
+
case 'lambda': {
|
|
3032
|
+
return (...args) => {
|
|
3033
|
+
const localContext = { ...context };
|
|
3034
|
+
expr.params.forEach((p, i) => {
|
|
3035
|
+
localContext[p] = args[i];
|
|
3036
|
+
});
|
|
3037
|
+
return this.evaluateExpression(expr.body, localContext);
|
|
3038
|
+
};
|
|
3039
|
+
}
|
|
3040
|
+
case 'aggregate': {
|
|
3041
|
+
// count(Entity where field == value, ...) — count rows of `entity`
|
|
3042
|
+
// matching every ANDed equality predicate. Predicate values resolve in
|
|
3043
|
+
// the surrounding context (reaction params: the event payload). Count
|
|
3044
|
+
// is order-independent, so this is deterministic for a given store
|
|
3045
|
+
// snapshot regardless of row ordering.
|
|
3046
|
+
if (expr.op !== 'count')
|
|
3047
|
+
return undefined;
|
|
3048
|
+
// Resolve predicate values once (they do not depend on the counted row).
|
|
3049
|
+
const resolved = await Promise.all(expr.predicates.map(async (p) => ({ field: p.field, value: await this.evaluateExpression(p.value, context) })));
|
|
3050
|
+
const rows = await this.getAllInstancesRaw(expr.entity);
|
|
3051
|
+
let count = 0;
|
|
3052
|
+
for (const row of rows) {
|
|
3053
|
+
const r = row;
|
|
3054
|
+
let match = true;
|
|
3055
|
+
for (const pred of resolved) {
|
|
3056
|
+
if (r[pred.field] !== pred.value) {
|
|
3057
|
+
match = false;
|
|
3058
|
+
break;
|
|
3059
|
+
}
|
|
3060
|
+
}
|
|
3061
|
+
if (match)
|
|
3062
|
+
count++;
|
|
3063
|
+
}
|
|
3064
|
+
return count;
|
|
3065
|
+
}
|
|
3066
|
+
default:
|
|
3067
|
+
return undefined;
|
|
3068
|
+
}
|
|
3069
|
+
}
|
|
3070
|
+
finally {
|
|
3071
|
+
if (this.evalBudget) {
|
|
3072
|
+
this.evalBudget.depth--;
|
|
3073
|
+
}
|
|
3074
|
+
}
|
|
3075
|
+
}
|
|
3076
|
+
/**
|
|
3077
|
+
* Check whether an expression can be safely evaluated by the WASM module.
|
|
3078
|
+
* Pure computational expressions (no entity relationships, no async effects)
|
|
3079
|
+
* are compatible. The check is conservative — when in doubt, return false
|
|
3080
|
+
* to ensure the TypeScript evaluator is used.
|
|
3081
|
+
*/
|
|
3082
|
+
isWasmCompatible(expr) {
|
|
3083
|
+
// Walk the expression tree checking for features that need TypeScript runtime
|
|
3084
|
+
const walk = (node) => {
|
|
3085
|
+
switch (node.kind) {
|
|
3086
|
+
case 'literal':
|
|
3087
|
+
case 'identifier':
|
|
3088
|
+
return true;
|
|
3089
|
+
case 'member': {
|
|
3090
|
+
// Member access on identifiers (e.g., self.foo) needs runtime context.
|
|
3091
|
+
// Only allow member access on plain property reads of simple identifiers.
|
|
3092
|
+
if (node.object.kind === 'identifier') {
|
|
3093
|
+
return walk(node.object);
|
|
3094
|
+
}
|
|
3095
|
+
return false;
|
|
3096
|
+
}
|
|
3097
|
+
case 'binary':
|
|
3098
|
+
return walk(node.left) && walk(node.right);
|
|
3099
|
+
case 'unary':
|
|
3100
|
+
return walk(node.operand);
|
|
3101
|
+
case 'call': {
|
|
3102
|
+
// Only allow calls to builtins (identifier callees), not function values
|
|
3103
|
+
if (node.callee.kind === 'identifier') {
|
|
3104
|
+
return node.args.every(walk);
|
|
3105
|
+
}
|
|
3106
|
+
return false;
|
|
3107
|
+
}
|
|
3108
|
+
case 'conditional':
|
|
3109
|
+
return walk(node.condition) && walk(node.consequent) && walk(node.alternate);
|
|
3110
|
+
case 'array':
|
|
3111
|
+
return node.elements.every(walk);
|
|
3112
|
+
case 'object':
|
|
3113
|
+
return node.properties.every(p => walk(p.value));
|
|
3114
|
+
case 'lambda':
|
|
3115
|
+
// Lambdas are not yet supported in WASM core
|
|
3116
|
+
return false;
|
|
3117
|
+
default:
|
|
3118
|
+
return false;
|
|
3119
|
+
}
|
|
3120
|
+
};
|
|
3121
|
+
return walk(expr);
|
|
3122
|
+
}
|
|
3123
|
+
evaluateBinaryOp(op, left, right) {
|
|
3124
|
+
switch (op) {
|
|
3125
|
+
case '+':
|
|
3126
|
+
if (typeof left === 'string' || typeof right === 'string') {
|
|
3127
|
+
return String(left) + String(right);
|
|
3128
|
+
}
|
|
3129
|
+
return left + right;
|
|
3130
|
+
case '-': return left - right;
|
|
3131
|
+
case '*': return left * right;
|
|
3132
|
+
case '/': return left / right;
|
|
3133
|
+
case '%': return left % right;
|
|
3134
|
+
case '==':
|
|
3135
|
+
case 'is': return left == right; // Loose equality: undefined == null is true
|
|
3136
|
+
case '!=': return left != right; // Loose inequality: undefined != null is false
|
|
3137
|
+
case '<': return left < right;
|
|
3138
|
+
case '>': return left > right;
|
|
3139
|
+
case '<=': return left <= right;
|
|
3140
|
+
case '>=': return left >= right;
|
|
3141
|
+
case '&&':
|
|
3142
|
+
case 'and': return Boolean(left) && Boolean(right);
|
|
3143
|
+
case '||':
|
|
3144
|
+
case 'or': return Boolean(left) || Boolean(right);
|
|
3145
|
+
case 'in':
|
|
3146
|
+
if (Array.isArray(right))
|
|
3147
|
+
return right.includes(left);
|
|
3148
|
+
if (typeof right === 'string')
|
|
3149
|
+
return right.includes(String(left));
|
|
3150
|
+
return false;
|
|
3151
|
+
case 'contains':
|
|
3152
|
+
if (Array.isArray(left))
|
|
3153
|
+
return left.includes(right);
|
|
3154
|
+
if (typeof left === 'string')
|
|
3155
|
+
return left.includes(String(right));
|
|
3156
|
+
return false;
|
|
3157
|
+
default:
|
|
3158
|
+
return undefined;
|
|
3159
|
+
}
|
|
3160
|
+
}
|
|
3161
|
+
evaluateUnaryOp(op, operand) {
|
|
3162
|
+
switch (op) {
|
|
3163
|
+
case '!':
|
|
3164
|
+
case 'not': return !operand;
|
|
3165
|
+
case '-': return -operand;
|
|
3166
|
+
default: return operand;
|
|
3167
|
+
}
|
|
3168
|
+
}
|
|
3169
|
+
irValueToJs(value) {
|
|
3170
|
+
switch (value.kind) {
|
|
3171
|
+
case 'string': return value.value;
|
|
3172
|
+
case 'number': return value.value;
|
|
3173
|
+
case 'boolean': return value.value;
|
|
3174
|
+
case 'null': return null;
|
|
3175
|
+
case 'array': return value.elements.map(e => this.irValueToJs(e));
|
|
3176
|
+
case 'object': {
|
|
3177
|
+
const result = {};
|
|
3178
|
+
for (const [k, v] of Object.entries(value.properties)) {
|
|
3179
|
+
result[k] = this.irValueToJs(v);
|
|
3180
|
+
}
|
|
3181
|
+
return result;
|
|
3182
|
+
}
|
|
3183
|
+
}
|
|
3184
|
+
}
|
|
3185
|
+
getDefaultForType(type) {
|
|
3186
|
+
if (type.nullable)
|
|
3187
|
+
return null;
|
|
3188
|
+
switch (type.name) {
|
|
3189
|
+
case 'string': return '';
|
|
3190
|
+
case 'number': return 0;
|
|
3191
|
+
case 'boolean': return false;
|
|
3192
|
+
case 'list': return [];
|
|
3193
|
+
case 'array': return [];
|
|
3194
|
+
case 'map': return {};
|
|
3195
|
+
default: return null;
|
|
3196
|
+
}
|
|
3197
|
+
}
|
|
3198
|
+
async evaluateComputed(entityName, instanceId, propertyName) {
|
|
3199
|
+
const meta = await this.evaluateComputedWithMeta(entityName, instanceId, propertyName);
|
|
3200
|
+
return meta?.value;
|
|
3201
|
+
}
|
|
3202
|
+
/**
|
|
3203
|
+
* Evaluate a computed property and return metadata including cache status and staleness.
|
|
3204
|
+
* Returns { value, stale, cached } or undefined if the entity/property/instance doesn't exist.
|
|
3205
|
+
*/
|
|
3206
|
+
async evaluateComputedWithMeta(entityName, instanceId, propertyName) {
|
|
3207
|
+
const entity = this.getEntity(entityName);
|
|
3208
|
+
if (!entity)
|
|
3209
|
+
return undefined;
|
|
3210
|
+
const computed = entity.computedProperties.find(c => c.name === propertyName);
|
|
3211
|
+
if (!computed)
|
|
3212
|
+
return undefined;
|
|
3213
|
+
const instance = await this.getInstanceRaw(entityName, instanceId);
|
|
3214
|
+
if (!instance)
|
|
3215
|
+
return undefined;
|
|
3216
|
+
const cacheKey = `${entityName}:${instanceId}:${propertyName}`;
|
|
3217
|
+
// Check cache based on strategy
|
|
3218
|
+
if (computed.cache) {
|
|
3219
|
+
const cached = this.getCachedComputedValue(computed.cache, cacheKey);
|
|
3220
|
+
if (cached !== undefined) {
|
|
3221
|
+
return { value: cached.value, stale: cached.stale, cached: true };
|
|
3222
|
+
}
|
|
3223
|
+
}
|
|
3224
|
+
const ownsEvalBudget = this.initEvalBudget();
|
|
3225
|
+
try {
|
|
3226
|
+
const value = await this.evaluateComputedInternal(entity, instance, propertyName, new Set());
|
|
3227
|
+
// Store in cache if strategy is configured
|
|
3228
|
+
if (computed.cache) {
|
|
3229
|
+
this.setCachedComputedValue(computed.cache, cacheKey, value);
|
|
3230
|
+
}
|
|
3231
|
+
return { value, stale: false, cached: false };
|
|
3232
|
+
}
|
|
3233
|
+
finally {
|
|
3234
|
+
if (ownsEvalBudget)
|
|
3235
|
+
this.clearEvalBudget();
|
|
3236
|
+
}
|
|
3237
|
+
}
|
|
3238
|
+
/**
|
|
3239
|
+
* Look up a cached computed property value based on the configured cache strategy.
|
|
3240
|
+
* Returns the cache entry if valid, or undefined if cache miss or expired.
|
|
3241
|
+
*/
|
|
3242
|
+
getCachedComputedValue(cacheConfig, cacheKey) {
|
|
3243
|
+
switch (cacheConfig.strategy) {
|
|
3244
|
+
case 'request': {
|
|
3245
|
+
const entry = this.computedPropertyRequestCache.get(cacheKey);
|
|
3246
|
+
if (entry)
|
|
3247
|
+
return { value: entry.value, stale: entry.stale };
|
|
3248
|
+
return undefined;
|
|
3249
|
+
}
|
|
3250
|
+
case 'session': {
|
|
3251
|
+
const entry = this.computedPropertyCache.get(cacheKey);
|
|
3252
|
+
if (entry)
|
|
3253
|
+
return { value: entry.value, stale: entry.stale };
|
|
3254
|
+
return undefined;
|
|
3255
|
+
}
|
|
3256
|
+
case 'ttl': {
|
|
3257
|
+
const entry = this.computedPropertyCache.get(cacheKey);
|
|
3258
|
+
if (entry) {
|
|
3259
|
+
const now = this.getNow();
|
|
3260
|
+
const ttlMs = (cacheConfig.ttlSeconds ?? 0) * 1000;
|
|
3261
|
+
if (now - entry.computedAt < ttlMs) {
|
|
3262
|
+
return { value: entry.value, stale: entry.stale };
|
|
3263
|
+
}
|
|
3264
|
+
// TTL expired — remove stale entry
|
|
3265
|
+
this.computedPropertyCache.delete(cacheKey);
|
|
3266
|
+
}
|
|
3267
|
+
return undefined;
|
|
3268
|
+
}
|
|
3269
|
+
default:
|
|
3270
|
+
return undefined;
|
|
3271
|
+
}
|
|
3272
|
+
}
|
|
3273
|
+
/**
|
|
3274
|
+
* Store a computed property value in the appropriate cache based on strategy.
|
|
3275
|
+
*/
|
|
3276
|
+
setCachedComputedValue(cacheConfig, cacheKey, value) {
|
|
3277
|
+
const entry = { value, computedAt: this.getNow(), stale: false };
|
|
3278
|
+
switch (cacheConfig.strategy) {
|
|
3279
|
+
case 'request':
|
|
3280
|
+
this.computedPropertyRequestCache.set(cacheKey, entry);
|
|
3281
|
+
break;
|
|
3282
|
+
case 'session':
|
|
3283
|
+
case 'ttl':
|
|
3284
|
+
this.computedPropertyCache.set(cacheKey, entry);
|
|
3285
|
+
break;
|
|
3286
|
+
}
|
|
3287
|
+
}
|
|
3288
|
+
async evaluateComputedInternal(entity, instance, propertyName, visited) {
|
|
3289
|
+
if (visited.has(propertyName))
|
|
3290
|
+
return undefined;
|
|
3291
|
+
visited.add(propertyName);
|
|
3292
|
+
const computed = entity.computedProperties.find(c => c.name === propertyName);
|
|
3293
|
+
if (!computed)
|
|
3294
|
+
return undefined;
|
|
3295
|
+
const computedValues = {};
|
|
3296
|
+
if (computed.dependencies) {
|
|
3297
|
+
for (const dep of computed.dependencies) {
|
|
3298
|
+
const depComputed = entity.computedProperties.find(c => c.name === dep);
|
|
3299
|
+
if (depComputed && !visited.has(dep)) {
|
|
3300
|
+
computedValues[dep] = await this.evaluateComputedInternal(entity, instance, dep, new Set(visited));
|
|
3301
|
+
}
|
|
3302
|
+
}
|
|
3303
|
+
}
|
|
3304
|
+
// Enrich instance with _entity metadata so relationship resolution works
|
|
3305
|
+
// when the member expression handler reads _entity from self/this
|
|
3306
|
+
const enrichedInstance = { ...instance, _entity: entity.name };
|
|
3307
|
+
const context = {
|
|
3308
|
+
self: enrichedInstance,
|
|
3309
|
+
this: enrichedInstance,
|
|
3310
|
+
...enrichedInstance,
|
|
3311
|
+
...computedValues,
|
|
3312
|
+
user: this.context.user ?? null,
|
|
3313
|
+
context: this.context ?? {},
|
|
3314
|
+
};
|
|
3315
|
+
return await this.evaluateExpression(computed.expression, context);
|
|
3316
|
+
}
|
|
3317
|
+
/**
|
|
3318
|
+
* vNext: Interpolate template placeholders with values from context
|
|
3319
|
+
* Supports {placeholder} syntax where placeholders are resolved from:
|
|
3320
|
+
* 1. details mapping (if present)
|
|
3321
|
+
* 2. resolved expression values (by expression string)
|
|
3322
|
+
* 3. evaluation context (direct property access)
|
|
3323
|
+
*/
|
|
3324
|
+
interpolateTemplate(template, evalContext, details, resolved) {
|
|
3325
|
+
// Create a lookup map for resolved values by expression
|
|
3326
|
+
const resolvedMap = new Map();
|
|
3327
|
+
if (resolved) {
|
|
3328
|
+
for (const r of resolved) {
|
|
3329
|
+
// Use the expression string as the key
|
|
3330
|
+
resolvedMap.set(r.expression, r.value);
|
|
3331
|
+
}
|
|
3332
|
+
}
|
|
3333
|
+
return template.replace(/\{([^}]+)\}/g, (_match, placeholder) => {
|
|
3334
|
+
// First check details mapping
|
|
3335
|
+
if (details && placeholder in details) {
|
|
3336
|
+
return String(details[placeholder]);
|
|
3337
|
+
}
|
|
3338
|
+
// Then check resolved expressions
|
|
3339
|
+
if (resolvedMap.has(placeholder)) {
|
|
3340
|
+
const value = resolvedMap.get(placeholder);
|
|
3341
|
+
return value === undefined ? placeholder : String(value);
|
|
3342
|
+
}
|
|
3343
|
+
// Finally check evaluation context
|
|
3344
|
+
if (placeholder in evalContext) {
|
|
3345
|
+
const value = evalContext[placeholder];
|
|
3346
|
+
return value === undefined ? placeholder : String(value);
|
|
3347
|
+
}
|
|
3348
|
+
// Placeholder not found, return original
|
|
3349
|
+
return _match;
|
|
3350
|
+
});
|
|
3351
|
+
}
|
|
3352
|
+
/**
|
|
3353
|
+
* vNext: Evaluate a single constraint and return detailed outcome
|
|
3354
|
+
*/
|
|
3355
|
+
async evaluateConstraint(constraint, evalContext) {
|
|
3356
|
+
const result = await this.evaluateExpression(constraint.expression, evalContext);
|
|
3357
|
+
// Hybrid constraint semantics:
|
|
3358
|
+
// - Negative-type constraints (name starts with "severity"): fire when TRUE (bad state detected)
|
|
3359
|
+
// - Positive-type constraints: fail when FALSE (required condition not met)
|
|
3360
|
+
const isNegativeType = constraint.name.startsWith('severity');
|
|
3361
|
+
const passed = isNegativeType ? !result : !!result;
|
|
3362
|
+
// Build details mapping if specified
|
|
3363
|
+
let details = undefined;
|
|
3364
|
+
if (constraint.detailsMapping) {
|
|
3365
|
+
details = {};
|
|
3366
|
+
for (const [key, expr] of Object.entries(constraint.detailsMapping)) {
|
|
3367
|
+
details[key] = await this.evaluateExpression(expr, evalContext);
|
|
3368
|
+
}
|
|
3369
|
+
}
|
|
3370
|
+
// Resolve expression values for debugging
|
|
3371
|
+
const resolved = await this.resolveExpressionValues(constraint.expression, evalContext);
|
|
3372
|
+
// Build message with template interpolation if messageTemplate is used
|
|
3373
|
+
let message = constraint.message;
|
|
3374
|
+
if (constraint.messageTemplate && !message) {
|
|
3375
|
+
message = this.interpolateTemplate(constraint.messageTemplate, evalContext, details, resolved.map(r => ({ expression: r.expression, value: r.value })));
|
|
3376
|
+
}
|
|
3377
|
+
return {
|
|
3378
|
+
code: constraint.code,
|
|
3379
|
+
constraintName: constraint.name,
|
|
3380
|
+
severity: constraint.severity || 'block',
|
|
3381
|
+
formatted: this.formatExpression(constraint.expression),
|
|
3382
|
+
message,
|
|
3383
|
+
details,
|
|
3384
|
+
passed,
|
|
3385
|
+
resolved: resolved.map(r => ({ expression: r.expression, value: r.value })),
|
|
3386
|
+
};
|
|
3387
|
+
}
|
|
3388
|
+
/**
|
|
3389
|
+
* vNext: Evaluate command constraints with override support
|
|
3390
|
+
* Returns allowed flag, all constraint outcomes, and any OverrideApplied events.
|
|
3391
|
+
* Per spec (manifest-vnext.md § OverrideApplied Event Shape):
|
|
3392
|
+
* OverrideApplied events MUST be included in CommandResult.emittedEvents.
|
|
3393
|
+
*/
|
|
3394
|
+
async evaluateCommandConstraints(command, evalContext, overrideRequests, commandContext) {
|
|
3395
|
+
const outcomes = [];
|
|
3396
|
+
const overrideEvents = [];
|
|
3397
|
+
for (const constraint of command.constraints || []) {
|
|
3398
|
+
const outcome = await this.evaluateConstraint(constraint, evalContext);
|
|
3399
|
+
// Check for override if constraint failed and is overrideable
|
|
3400
|
+
if (!outcome.passed && constraint.overrideable) {
|
|
3401
|
+
// First check for explicit override request
|
|
3402
|
+
if (overrideRequests) {
|
|
3403
|
+
const overrideReq = overrideRequests.find(o => o.constraintCode === constraint.code);
|
|
3404
|
+
if (overrideReq) {
|
|
3405
|
+
const authorized = await this.validateOverrideAuthorization(constraint, overrideReq, evalContext);
|
|
3406
|
+
if (authorized) {
|
|
3407
|
+
outcome.overridden = true;
|
|
3408
|
+
outcome.overriddenBy = overrideReq.authorizedBy;
|
|
3409
|
+
const event = this.buildOverrideAppliedEvent(constraint, overrideReq, commandContext);
|
|
3410
|
+
overrideEvents.push(event);
|
|
3411
|
+
this.eventLog.push(event);
|
|
3412
|
+
this.notifyListeners(event);
|
|
3413
|
+
}
|
|
3414
|
+
}
|
|
3415
|
+
}
|
|
3416
|
+
// If still not overridden and has overridePolicyRef, automatically check policy
|
|
3417
|
+
if (!outcome.overridden && constraint.overridePolicyRef) {
|
|
3418
|
+
const policy = this.ir.policies.find(p => p.name === constraint.overridePolicyRef);
|
|
3419
|
+
if (policy && policy.action === 'override') {
|
|
3420
|
+
const policyResult = await this.evaluateExpression(policy.expression, evalContext);
|
|
3421
|
+
const authorized = Boolean(policyResult);
|
|
3422
|
+
if (authorized) {
|
|
3423
|
+
outcome.overridden = true;
|
|
3424
|
+
outcome.overriddenBy = 'policy:' + policy.name;
|
|
3425
|
+
}
|
|
3426
|
+
}
|
|
3427
|
+
}
|
|
3428
|
+
}
|
|
3429
|
+
outcomes.push(outcome);
|
|
3430
|
+
// Block execution if non-passing constraint is not overridden
|
|
3431
|
+
if (!outcome.passed && !outcome.overridden && outcome.severity === 'block') {
|
|
3432
|
+
return { allowed: false, outcomes, overrideEvents };
|
|
3433
|
+
}
|
|
3434
|
+
}
|
|
3435
|
+
return { allowed: true, outcomes, overrideEvents };
|
|
3436
|
+
}
|
|
3437
|
+
/**
|
|
3438
|
+
* vNext: Validate override authorization via policy or default admin check
|
|
3439
|
+
*/
|
|
3440
|
+
async validateOverrideAuthorization(constraint, overrideReq, evalContext) {
|
|
3441
|
+
// If constraint has overridePolicyRef, check that policy
|
|
3442
|
+
if (constraint.overridePolicyRef) {
|
|
3443
|
+
const policy = this.ir.policies.find(p => p.name === constraint.overridePolicyRef);
|
|
3444
|
+
if (policy) {
|
|
3445
|
+
const overrideContext = {
|
|
3446
|
+
...evalContext,
|
|
3447
|
+
_override: {
|
|
3448
|
+
constraintCode: constraint.code,
|
|
3449
|
+
constraintName: constraint.name,
|
|
3450
|
+
reason: overrideReq.reason,
|
|
3451
|
+
authorizedBy: overrideReq.authorizedBy,
|
|
3452
|
+
},
|
|
3453
|
+
};
|
|
3454
|
+
const result = await this.evaluateExpression(policy.expression, overrideContext);
|
|
3455
|
+
return Boolean(result);
|
|
3456
|
+
}
|
|
3457
|
+
}
|
|
3458
|
+
// Default: check if user has admin-like role
|
|
3459
|
+
const user = this.context.user;
|
|
3460
|
+
return user?.role === 'admin' || false;
|
|
3461
|
+
}
|
|
3462
|
+
/**
|
|
3463
|
+
* vNext: Build OverrideApplied event for auditing.
|
|
3464
|
+
* Per spec (manifest-vnext.md § OverrideApplied Event Shape):
|
|
3465
|
+
* payload MUST contain: constraintCode, reason, authorizedBy, timestamp, commandName,
|
|
3466
|
+
* and optionally entityName, instanceId.
|
|
3467
|
+
* The event is a runtime-synthesized event included in CommandResult.emittedEvents.
|
|
3468
|
+
*/
|
|
3469
|
+
buildOverrideAppliedEvent(constraint, overrideReq, commandContext) {
|
|
3470
|
+
const payload = {
|
|
3471
|
+
constraintCode: constraint.code,
|
|
3472
|
+
reason: overrideReq.reason,
|
|
3473
|
+
authorizedBy: overrideReq.authorizedBy,
|
|
3474
|
+
timestamp: this.getNow(),
|
|
3475
|
+
commandName: commandContext?.commandName || '',
|
|
3476
|
+
};
|
|
3477
|
+
if (commandContext?.entityName) {
|
|
3478
|
+
payload.entityName = commandContext.entityName;
|
|
3479
|
+
}
|
|
3480
|
+
if (commandContext?.instanceId) {
|
|
3481
|
+
payload.instanceId = commandContext.instanceId;
|
|
3482
|
+
}
|
|
3483
|
+
return {
|
|
3484
|
+
name: 'OverrideApplied',
|
|
3485
|
+
channel: 'system',
|
|
3486
|
+
payload,
|
|
3487
|
+
timestamp: this.getNow(),
|
|
3488
|
+
provenance: this.getProvenanceInfo(),
|
|
3489
|
+
};
|
|
3490
|
+
}
|
|
3491
|
+
/**
|
|
3492
|
+
* vNext: Emit ConcurrencyConflict event
|
|
3493
|
+
*/
|
|
3494
|
+
async emitConcurrencyConflictEvent(entityName, entityId, expectedVersion, actualVersion) {
|
|
3495
|
+
const event = {
|
|
3496
|
+
name: 'ConcurrencyConflict',
|
|
3497
|
+
channel: 'system',
|
|
3498
|
+
payload: {
|
|
3499
|
+
entityType: entityName,
|
|
3500
|
+
entityId,
|
|
3501
|
+
expectedVersion,
|
|
3502
|
+
actualVersion,
|
|
3503
|
+
conflictCode: 'VERSION_MISMATCH',
|
|
3504
|
+
timestamp: this.getNow(),
|
|
3505
|
+
},
|
|
3506
|
+
timestamp: this.getNow(),
|
|
3507
|
+
provenance: this.getProvenanceInfo(),
|
|
3508
|
+
};
|
|
3509
|
+
this.eventLog.push(event);
|
|
3510
|
+
this.notifyListeners(event);
|
|
3511
|
+
}
|
|
3512
|
+
/**
|
|
3513
|
+
* vNext: Get provenance info for events
|
|
3514
|
+
*/
|
|
3515
|
+
getProvenanceInfo() {
|
|
3516
|
+
const prov = this.ir.provenance;
|
|
3517
|
+
if (!prov)
|
|
3518
|
+
return undefined;
|
|
3519
|
+
return {
|
|
3520
|
+
contentHash: prov.contentHash,
|
|
3521
|
+
compilerVersion: prov.compilerVersion,
|
|
3522
|
+
schemaVersion: prov.schemaVersion,
|
|
3523
|
+
};
|
|
3524
|
+
}
|
|
3525
|
+
onEvent(listener) {
|
|
3526
|
+
this.eventListeners.push(listener);
|
|
3527
|
+
return () => {
|
|
3528
|
+
const idx = this.eventListeners.indexOf(listener);
|
|
3529
|
+
if (idx !== -1)
|
|
3530
|
+
this.eventListeners.splice(idx, 1);
|
|
3531
|
+
};
|
|
3532
|
+
}
|
|
3533
|
+
/**
|
|
3534
|
+
* Subscribe to events for a single entity (docs/spec/semantics.md,
|
|
3535
|
+
* "Realtime Entities"). Convenience over onEvent: the listener receives
|
|
3536
|
+
* only events whose `subject.entity === entityName`. Events WITHOUT a
|
|
3537
|
+
* subject entity are NOT delivered — use onEvent for the unfiltered
|
|
3538
|
+
* firehose. Returns an unsubscribe function. Exists regardless of any
|
|
3539
|
+
* entity's `realtime` flag (the flag is a projection hint only).
|
|
3540
|
+
*/
|
|
3541
|
+
subscribe(entityName, listener) {
|
|
3542
|
+
return this.onEvent((event) => {
|
|
3543
|
+
if (event.subject?.entity === entityName) {
|
|
3544
|
+
listener(event);
|
|
3545
|
+
}
|
|
3546
|
+
});
|
|
3547
|
+
}
|
|
3548
|
+
notifyListeners(event) {
|
|
3549
|
+
for (const listener of this.eventListeners) {
|
|
3550
|
+
try {
|
|
3551
|
+
listener(event);
|
|
3552
|
+
}
|
|
3553
|
+
catch {
|
|
3554
|
+
// Ignore errors in event listeners
|
|
3555
|
+
}
|
|
3556
|
+
}
|
|
3557
|
+
}
|
|
3558
|
+
getEventLog() {
|
|
3559
|
+
return [...this.eventLog];
|
|
3560
|
+
}
|
|
3561
|
+
clearEventLog() {
|
|
3562
|
+
this.eventLog = [];
|
|
3563
|
+
}
|
|
3564
|
+
async serialize() {
|
|
3565
|
+
const storeData = {};
|
|
3566
|
+
for (const [name, store] of this.stores) {
|
|
3567
|
+
storeData[name] = await store.getAll();
|
|
3568
|
+
}
|
|
3569
|
+
return {
|
|
3570
|
+
ir: this.ir,
|
|
3571
|
+
context: this.context,
|
|
3572
|
+
stores: storeData,
|
|
3573
|
+
};
|
|
3574
|
+
}
|
|
3575
|
+
async restore(data) {
|
|
3576
|
+
for (const [name, instances] of Object.entries(data.stores)) {
|
|
3577
|
+
const store = this.stores.get(name);
|
|
3578
|
+
if (store) {
|
|
3579
|
+
await store.clear();
|
|
3580
|
+
for (const instance of instances) {
|
|
3581
|
+
await store.create(instance);
|
|
3582
|
+
}
|
|
3583
|
+
}
|
|
3584
|
+
}
|
|
3585
|
+
}
|
|
3586
|
+
/**
|
|
3587
|
+
* Static factory method to create a RuntimeEngine with optional provenance verification.
|
|
3588
|
+
* This is useful when you want to verify IR integrity before execution.
|
|
3589
|
+
*
|
|
3590
|
+
* In production mode (NODE_ENV=production), provenance verification is enabled by default.
|
|
3591
|
+
* Set `requireValidProvenance: false` to explicitly disable.
|
|
3592
|
+
*
|
|
3593
|
+
* @param ir - The IR to execute
|
|
3594
|
+
* @param context - Runtime context (user, etc.)
|
|
3595
|
+
* @param options - Runtime options including requireValidProvenance
|
|
3596
|
+
* @returns A tuple of [runtime, verificationResult]
|
|
3597
|
+
*
|
|
3598
|
+
* @example
|
|
3599
|
+
* ```ts
|
|
3600
|
+
* // Production: verification enabled by default
|
|
3601
|
+
* const [runtime, result] = await RuntimeEngine.create(ir, context);
|
|
3602
|
+
* if (!result.valid) {
|
|
3603
|
+
* throw new Error(`Invalid IR: ${result.error}`);
|
|
3604
|
+
* }
|
|
3605
|
+
*
|
|
3606
|
+
* // Development: explicitly disable verification
|
|
3607
|
+
* const [runtime] = await RuntimeEngine.create(ir, context, { requireValidProvenance: false });
|
|
3608
|
+
* ```
|
|
3609
|
+
*/
|
|
3610
|
+
// ─── Approval Workflow Methods ─────────────────────────────────────
|
|
3611
|
+
/**
|
|
3612
|
+
* Build an approval-request key for the Map.
|
|
3613
|
+
*/
|
|
3614
|
+
approvalKey(entity, instanceId, approvalName) {
|
|
3615
|
+
return `${entity}:${instanceId}:${approvalName}`;
|
|
3616
|
+
}
|
|
3617
|
+
/**
|
|
3618
|
+
* Find approval declarations on an entity that gate a given command name.
|
|
3619
|
+
*/
|
|
3620
|
+
findApprovalsForCommand(entityName, commandName) {
|
|
3621
|
+
const entity = this.getEntity(entityName);
|
|
3622
|
+
if (!entity?.approvals)
|
|
3623
|
+
return [];
|
|
3624
|
+
return entity.approvals.filter(a => a.command === commandName);
|
|
3625
|
+
}
|
|
3626
|
+
/**
|
|
3627
|
+
* Check the approval gate for a command. Returns a CommandResult (blocked)
|
|
3628
|
+
* if the command requires approval that hasn't been granted yet, or
|
|
3629
|
+
* undefined if the command may proceed.
|
|
3630
|
+
*/
|
|
3631
|
+
async checkApprovalGate(commandName, entityName, instanceId, evalContext, options) {
|
|
3632
|
+
const approvals = this.findApprovalsForCommand(entityName, commandName);
|
|
3633
|
+
if (approvals.length === 0)
|
|
3634
|
+
return undefined;
|
|
3635
|
+
// Use the first matching approval (typically one-to-one command→approval)
|
|
3636
|
+
const approval = approvals[0];
|
|
3637
|
+
const resolvedInstanceId = instanceId ?? 'unknown';
|
|
3638
|
+
const key = this.approvalKey(entityName, resolvedInstanceId, approval.name);
|
|
3639
|
+
// Determine which stages are required (evaluate `when` conditions)
|
|
3640
|
+
const requiredStages = [];
|
|
3641
|
+
for (const stage of approval.stages) {
|
|
3642
|
+
if (stage.when) {
|
|
3643
|
+
const whenResult = await this.evaluateExpression(stage.when, evalContext);
|
|
3644
|
+
if (whenResult)
|
|
3645
|
+
requiredStages.push(stage.name);
|
|
3646
|
+
}
|
|
3647
|
+
else {
|
|
3648
|
+
requiredStages.push(stage.name);
|
|
3649
|
+
}
|
|
3650
|
+
}
|
|
3651
|
+
// If no stages are required (all `when` conditions false), proceed
|
|
3652
|
+
if (requiredStages.length === 0)
|
|
3653
|
+
return undefined;
|
|
3654
|
+
// Check existing approval request (durable store wins when configured)
|
|
3655
|
+
const existing = await this.loadApprovalState(key);
|
|
3656
|
+
if (existing && existing.status === 'granted') {
|
|
3657
|
+
// All stages were granted — consume the approval and proceed
|
|
3658
|
+
return undefined;
|
|
3659
|
+
}
|
|
3660
|
+
// Create or refresh a pending request
|
|
3661
|
+
let request = existing;
|
|
3662
|
+
if (!request || request.status === 'expired' || request.status === 'denied') {
|
|
3663
|
+
const now = this.getNow();
|
|
3664
|
+
request = {
|
|
3665
|
+
entity: entityName,
|
|
3666
|
+
instanceId: resolvedInstanceId,
|
|
3667
|
+
approvalName: approval.name,
|
|
3668
|
+
command: commandName,
|
|
3669
|
+
status: 'pending',
|
|
3670
|
+
requiredStages,
|
|
3671
|
+
grants: [],
|
|
3672
|
+
requestedAt: now,
|
|
3673
|
+
expiresAt: approval.timeout ? now + approval.timeout * 3600000 : undefined,
|
|
3674
|
+
};
|
|
3675
|
+
await this.saveApprovalState(key, request);
|
|
3676
|
+
}
|
|
3677
|
+
// Determine which stages are still pending
|
|
3678
|
+
const pendingStages = this.getPendingStages(request, approval);
|
|
3679
|
+
return {
|
|
3680
|
+
success: false,
|
|
3681
|
+
error: `Command '${commandName}' requires approval '${approval.name}'`,
|
|
3682
|
+
approvalRequired: {
|
|
3683
|
+
approvalName: approval.name,
|
|
3684
|
+
pendingStages,
|
|
3685
|
+
requestKey: key,
|
|
3686
|
+
},
|
|
3687
|
+
...(options.correlationId !== undefined ? { correlationId: options.correlationId } : {}),
|
|
3688
|
+
...(options.causationId !== undefined ? { causationId: options.causationId } : {}),
|
|
3689
|
+
emittedEvents: [],
|
|
3690
|
+
};
|
|
3691
|
+
}
|
|
3692
|
+
/**
|
|
3693
|
+
* Get the list of stages that still need approvals.
|
|
3694
|
+
*/
|
|
3695
|
+
getPendingStages(request, approval) {
|
|
3696
|
+
const pending = [];
|
|
3697
|
+
for (const stageName of request.requiredStages) {
|
|
3698
|
+
const stageSpec = approval.stages.find(s => s.name === stageName);
|
|
3699
|
+
if (!stageSpec)
|
|
3700
|
+
continue;
|
|
3701
|
+
const grantCount = request.grants.filter(g => g.stage === stageName).length;
|
|
3702
|
+
if (grantCount < stageSpec.required) {
|
|
3703
|
+
pending.push(stageName);
|
|
3704
|
+
}
|
|
3705
|
+
}
|
|
3706
|
+
return pending;
|
|
3707
|
+
}
|
|
3708
|
+
/**
|
|
3709
|
+
* Request approval for a command on an entity instance.
|
|
3710
|
+
* Creates or returns the existing approval request state.
|
|
3711
|
+
*/
|
|
3712
|
+
async requestApproval(entityName, instanceId, approvalName) {
|
|
3713
|
+
const key = this.approvalKey(entityName, instanceId, approvalName);
|
|
3714
|
+
const existing = await this.loadApprovalState(key);
|
|
3715
|
+
if (existing)
|
|
3716
|
+
return existing;
|
|
3717
|
+
const entity = this.getEntity(entityName);
|
|
3718
|
+
const approval = entity?.approvals?.find(a => a.name === approvalName);
|
|
3719
|
+
if (!approval) {
|
|
3720
|
+
throw new Error(`Approval '${approvalName}' not found on entity '${entityName}'`);
|
|
3721
|
+
}
|
|
3722
|
+
// Evaluate which stages are required using a minimal context
|
|
3723
|
+
const instance = await this.getInstanceRaw(entityName, instanceId);
|
|
3724
|
+
const evalContext = this.buildEvalContext({}, instance, entityName);
|
|
3725
|
+
const requiredStages = [];
|
|
3726
|
+
for (const stage of approval.stages) {
|
|
3727
|
+
if (stage.when) {
|
|
3728
|
+
const whenResult = await this.evaluateExpression(stage.when, evalContext);
|
|
3729
|
+
if (whenResult)
|
|
3730
|
+
requiredStages.push(stage.name);
|
|
3731
|
+
}
|
|
3732
|
+
else {
|
|
3733
|
+
requiredStages.push(stage.name);
|
|
3734
|
+
}
|
|
3735
|
+
}
|
|
3736
|
+
const now = this.getNow();
|
|
3737
|
+
const state = {
|
|
3738
|
+
entity: entityName,
|
|
3739
|
+
instanceId,
|
|
3740
|
+
approvalName,
|
|
3741
|
+
command: approval.command,
|
|
3742
|
+
status: 'pending',
|
|
3743
|
+
requiredStages,
|
|
3744
|
+
grants: [],
|
|
3745
|
+
requestedAt: now,
|
|
3746
|
+
expiresAt: approval.timeout ? now + approval.timeout * 3600000 : undefined,
|
|
3747
|
+
};
|
|
3748
|
+
await this.saveApprovalState(key, state);
|
|
3749
|
+
return state;
|
|
3750
|
+
}
|
|
3751
|
+
/**
|
|
3752
|
+
* Grant approval for a specific stage. Evaluates the stage policy to verify
|
|
3753
|
+
* the approver is authorized. When all required stages are satisfied, marks
|
|
3754
|
+
* the approval as 'granted'.
|
|
3755
|
+
*/
|
|
3756
|
+
async approveStage(entityName, instanceId, approvalName, stageName, approver) {
|
|
3757
|
+
const key = this.approvalKey(entityName, instanceId, approvalName);
|
|
3758
|
+
const request = await this.loadApprovalState(key);
|
|
3759
|
+
if (!request) {
|
|
3760
|
+
throw new Error(`No pending approval request for key '${key}'`);
|
|
3761
|
+
}
|
|
3762
|
+
if (request.status !== 'pending') {
|
|
3763
|
+
throw new Error(`Approval '${approvalName}' is not pending (status: ${request.status})`);
|
|
3764
|
+
}
|
|
3765
|
+
const entity = this.getEntity(entityName);
|
|
3766
|
+
const approval = entity?.approvals?.find(a => a.name === approvalName);
|
|
3767
|
+
if (!approval) {
|
|
3768
|
+
throw new Error(`Approval '${approvalName}' not found on entity '${entityName}'`);
|
|
3769
|
+
}
|
|
3770
|
+
const stageSpec = approval.stages.find(s => s.name === stageName);
|
|
3771
|
+
if (!stageSpec) {
|
|
3772
|
+
throw new Error(`Stage '${stageName}' not found in approval '${approvalName}'`);
|
|
3773
|
+
}
|
|
3774
|
+
// Verify this stage is required
|
|
3775
|
+
if (!request.requiredStages.includes(stageName)) {
|
|
3776
|
+
throw new Error(`Stage '${stageName}' is not required for this approval request`);
|
|
3777
|
+
}
|
|
3778
|
+
// Build the approver's user context for the stage policy. A bare string
|
|
3779
|
+
// is the legacy form (userId doubles as role); an object carries a real
|
|
3780
|
+
// role/roles/permissions so RBAC policies like `user.role == "manager"`
|
|
3781
|
+
// evaluate against the actual role rather than the user id.
|
|
3782
|
+
const approverId = typeof approver === 'string' ? approver : approver.id;
|
|
3783
|
+
const userContext = typeof approver === 'string'
|
|
3784
|
+
? { id: approver, role: approver }
|
|
3785
|
+
: { ...approver };
|
|
3786
|
+
const instance = await this.getInstanceRaw(entityName, instanceId);
|
|
3787
|
+
const evalContext = this.buildEvalContext({}, instance, entityName);
|
|
3788
|
+
Object.assign(evalContext, { user: userContext });
|
|
3789
|
+
const policyResult = await this.evaluateExpression(stageSpec.policy, evalContext);
|
|
3790
|
+
if (!policyResult) {
|
|
3791
|
+
throw new Error(`User '${approverId}' is not authorized to approve stage '${stageName}'`);
|
|
3792
|
+
}
|
|
3793
|
+
// Record the grant
|
|
3794
|
+
request.grants.push({
|
|
3795
|
+
stage: stageName,
|
|
3796
|
+
by: approverId,
|
|
3797
|
+
at: this.getNow(),
|
|
3798
|
+
});
|
|
3799
|
+
// Check if all required stages are now satisfied
|
|
3800
|
+
const pendingStages = this.getPendingStages(request, approval);
|
|
3801
|
+
if (pendingStages.length === 0) {
|
|
3802
|
+
request.status = 'granted';
|
|
3803
|
+
}
|
|
3804
|
+
await this.saveApprovalState(key, request);
|
|
3805
|
+
return request;
|
|
3806
|
+
}
|
|
3807
|
+
/**
|
|
3808
|
+
* Deny an approval request.
|
|
3809
|
+
*/
|
|
3810
|
+
async denyApproval(entityName, instanceId, approvalName, deniedBy, reason) {
|
|
3811
|
+
const key = this.approvalKey(entityName, instanceId, approvalName);
|
|
3812
|
+
const request = await this.loadApprovalState(key);
|
|
3813
|
+
if (!request) {
|
|
3814
|
+
throw new Error(`No pending approval request for key '${key}'`);
|
|
3815
|
+
}
|
|
3816
|
+
if (request.status !== 'pending') {
|
|
3817
|
+
throw new Error(`Approval '${approvalName}' is not pending (status: ${request.status})`);
|
|
3818
|
+
}
|
|
3819
|
+
request.status = 'denied';
|
|
3820
|
+
request.deniedBy = deniedBy;
|
|
3821
|
+
request.deniedReason = reason;
|
|
3822
|
+
await this.saveApprovalState(key, request);
|
|
3823
|
+
return request;
|
|
3824
|
+
}
|
|
3825
|
+
/**
|
|
3826
|
+
* Expire any pending approvals that have exceeded their timeout.
|
|
3827
|
+
* Approvals with `onTimeout: 'cancel'` are set to 'expired'.
|
|
3828
|
+
* Approvals with `onTimeout: 'escalate'` are flagged but kept pending (future).
|
|
3829
|
+
*
|
|
3830
|
+
* Operates on the in-process request set. In durable mode
|
|
3831
|
+
* (`options.approvalStore` configured), run set-based expiry across all
|
|
3832
|
+
* stored requests via `approvalStore.expire(now)` from a cron/worker; this
|
|
3833
|
+
* synchronous accessor only sees requests this engine has touched.
|
|
3834
|
+
*/
|
|
3835
|
+
expireApprovals(now) {
|
|
3836
|
+
const currentTime = now ?? this.getNow();
|
|
3837
|
+
const expired = [];
|
|
3838
|
+
for (const request of this.approvalRequests.values()) {
|
|
3839
|
+
if (request.status !== 'pending' || !request.expiresAt)
|
|
3840
|
+
continue;
|
|
3841
|
+
if (currentTime >= request.expiresAt) {
|
|
3842
|
+
request.status = 'expired';
|
|
3843
|
+
expired.push(request);
|
|
3844
|
+
}
|
|
3845
|
+
}
|
|
3846
|
+
return expired;
|
|
3847
|
+
}
|
|
3848
|
+
/**
|
|
3849
|
+
* Get the current approval request state for an entity instance.
|
|
3850
|
+
*/
|
|
3851
|
+
getApprovalRequest(entityName, instanceId, approvalName) {
|
|
3852
|
+
return this.approvalRequests.get(this.approvalKey(entityName, instanceId, approvalName));
|
|
3853
|
+
}
|
|
3854
|
+
static async create(ir, context = {}, options = {}) {
|
|
3855
|
+
const runtime = new RuntimeEngine(ir, context, options);
|
|
3856
|
+
let result = { valid: true };
|
|
3857
|
+
// Default to true in production mode, or if explicitly set
|
|
3858
|
+
const shouldVerify = options.requireValidProvenance ?? isProductionMode();
|
|
3859
|
+
if (shouldVerify) {
|
|
3860
|
+
const isValid = await runtime.verifyIRHash(options.expectedIRHash);
|
|
3861
|
+
result = {
|
|
3862
|
+
valid: isValid,
|
|
3863
|
+
expectedHash: options.expectedIRHash || ir.provenance?.irHash,
|
|
3864
|
+
};
|
|
3865
|
+
if (!isValid) {
|
|
3866
|
+
result.error = 'IR hash verification failed';
|
|
3867
|
+
}
|
|
3868
|
+
}
|
|
3869
|
+
return [runtime, result];
|
|
3870
|
+
}
|
|
3871
|
+
}
|
|
3872
|
+
//# sourceMappingURL=runtime-engine.js.map
|