domainforge 0.13.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.
Files changed (481) hide show
  1. package/.cargo/config.toml +6 -0
  2. package/.claude/settings.local.json +18 -0
  3. package/.coderabbit.yml +43 -0
  4. package/.codex/skills/release-management/SKILL.md +151 -0
  5. package/.codex/skills/release-management/agents/openai.yaml +4 -0
  6. package/.github/actions/decrypt-secrets/action.yml +121 -0
  7. package/.github/agents/Coder.agent.md +97 -0
  8. package/.github/agents/DeepResearch.agent.md +61 -0
  9. package/.github/chatmodes/tdd.vibepro.chatmode.md +1183 -0
  10. package/.github/copilot-instructions.md +13 -0
  11. package/.github/dependabot.yml +68 -0
  12. package/.github/workflows/README.md +165 -0
  13. package/.github/workflows/ci.yml +335 -0
  14. package/.github/workflows/dependabot-automerge.yml +114 -0
  15. package/.github/workflows/dependency-review.yml +27 -0
  16. package/.github/workflows/deploy.yml +87 -0
  17. package/.github/workflows/prepare-release.yml +168 -0
  18. package/.github/workflows/release-crates.yml +42 -0
  19. package/.github/workflows/release-npm.yml +137 -0
  20. package/.github/workflows/release-please.yml +29 -0
  21. package/.github/workflows/release-pypi.yml +96 -0
  22. package/.gitkeep +1 -0
  23. package/.release-please-manifest.json +5 -0
  24. package/.sea-registry.toml +10 -0
  25. package/.serena/project.yml +133 -0
  26. package/.sops.yaml +10 -0
  27. package/AGENTS.md +216 -0
  28. package/CHANGELOG.md +400 -0
  29. package/CLAUDE.md +62 -0
  30. package/CONTRIBUTING.md +323 -0
  31. package/Cargo.lock +3612 -0
  32. package/Cargo.toml +12 -0
  33. package/LICENSE +201 -0
  34. package/README.md +660 -0
  35. package/README_PYTHON.md +256 -0
  36. package/README_TYPESCRIPT.md +305 -0
  37. package/README_WASM.md +329 -0
  38. package/RELEASE_NOTES.md +41 -0
  39. package/bun.lock +378 -0
  40. package/bunfig.toml +11 -0
  41. package/check_output.txt +83 -0
  42. package/clippy_output.txt +80 -0
  43. package/commitlint.config.cjs +8 -0
  44. package/deny.toml +42 -0
  45. package/devbox.json +14 -0
  46. package/devbox.lock +76 -0
  47. package/docs/RELEASE_PROCESS.md +360 -0
  48. package/docs/diagnostics.md +161 -0
  49. package/docs/doc_guidelines.md +53 -0
  50. package/docs/explanations/README.md +21 -0
  51. package/docs/explanations/architecture-overview.md +109 -0
  52. package/docs/explanations/cross-language-binding-strategy.md +68 -0
  53. package/docs/explanations/graph-store-design.md +47 -0
  54. package/docs/explanations/performance-benchmarks.md +63 -0
  55. package/docs/explanations/policy-evaluation-logic.md +106 -0
  56. package/docs/explanations/semantic-modeling-concepts.md +109 -0
  57. package/docs/explanations/three-valued-logic.md +66 -0
  58. package/docs/explanations/versioning-strategy.md +45 -0
  59. package/docs/governance.md +168 -0
  60. package/docs/how-tos/README.md +46 -0
  61. package/docs/how-tos/ci-cd-validation.md +93 -0
  62. package/docs/how-tos/create-custom-units.md +125 -0
  63. package/docs/how-tos/define-policies.md +119 -0
  64. package/docs/how-tos/export-to-calm.md +110 -0
  65. package/docs/how-tos/export-to-protobuf.md +312 -0
  66. package/docs/how-tos/extend-grammar.md +133 -0
  67. package/docs/how-tos/generate-rdf-turtle.md +106 -0
  68. package/docs/how-tos/import-from-calm.md +114 -0
  69. package/docs/how-tos/import-from-sbvr.md +249 -0
  70. package/docs/how-tos/install-cli.md +126 -0
  71. package/docs/how-tos/parse-sea-files.md +132 -0
  72. package/docs/how-tos/policy-evaluation-modes.md +30 -0
  73. package/docs/how-tos/run-cross-language-tests.md +115 -0
  74. package/docs/how-tos/troubleshoot-napi-builds.md +55 -0
  75. package/docs/how-tos/use-modules-imports.md +285 -0
  76. package/docs/index.md +13 -0
  77. package/docs/plans/canonical-normalizer.md +121 -0
  78. package/docs/plans/cd_improvement.md +112 -0
  79. package/docs/plans/cli-ast.md +29 -0
  80. package/docs/plans/expression-bindings-and-normalizer-integration.md +174 -0
  81. package/docs/plans/protobuf_advanced_features_plan.md +597 -0
  82. package/docs/plans/protobuf_plan.yml +525 -0
  83. package/docs/plans/refactor_dsl_architecture.md +131 -0
  84. package/docs/plans/release-plan.md +163 -0
  85. package/docs/plans/sea_fmt_implementation_plan.md +516 -0
  86. package/docs/playbooks/README.md +18 -0
  87. package/docs/playbooks/adding-new-primitive.md +68 -0
  88. package/docs/playbooks/debugging-parser-failures.md +42 -0
  89. package/docs/playbooks/local-release-preparation.md +139 -0
  90. package/docs/playbooks/migrating-schema-versions.md +43 -0
  91. package/docs/playbooks/onboarding-contributors.md +64 -0
  92. package/docs/playbooks/releasing-beta.md +86 -0
  93. package/docs/playbooks/secret-management.md +64 -0
  94. package/docs/reference/README.md +199 -0
  95. package/docs/reference/ast-json-api.md +427 -0
  96. package/docs/reference/calm-mapping.md +519 -0
  97. package/docs/reference/cli-commands.md +588 -0
  98. package/docs/reference/configuration.md +202 -0
  99. package/docs/reference/error-codes.md +664 -0
  100. package/docs/reference/generated-artifacts-policy.md +53 -0
  101. package/docs/reference/grammar-spec.md +255 -0
  102. package/docs/reference/primitives-api.md +317 -0
  103. package/docs/reference/protobuf-api.md +426 -0
  104. package/docs/reference/python-api.md +485 -0
  105. package/docs/reference/registry.md +50 -0
  106. package/docs/reference/sea-dsl-ai-cheatsheet.yaml +913 -0
  107. package/docs/reference/security-model.md +74 -0
  108. package/docs/reference/typescript-api.md +508 -0
  109. package/docs/reference/wasm-api.md +420 -0
  110. package/docs/semantic-pack-review.md +144 -0
  111. package/docs/semantic-pack-signing.md +234 -0
  112. package/docs/semantic-packs.md +284 -0
  113. package/docs/specs/ADR-001-sea-dsl-semantic-source-of-truth.md +33 -0
  114. package/docs/specs/ADR-002-projection-first-class-construct.md +50 -0
  115. package/docs/specs/ADR-003-protobuf-projection-target.md +51 -0
  116. package/docs/specs/ADR-004-projection-compatibility-semantics.md +57 -0
  117. package/docs/specs/ADR-005-multi-language-support-strategy.md +112 -0
  118. package/docs/specs/ADR-006-error-handling-strategy.md +115 -0
  119. package/docs/specs/ADR-007-policy-evaluation-engine.md +95 -0
  120. package/docs/specs/ADR-008-knowledge-graph-integration.md +90 -0
  121. package/docs/specs/ADR-009-module-resolution-strategy.md +115 -0
  122. package/docs/specs/ADR-010-unit-system.md +106 -0
  123. package/docs/specs/PRD-001-sea-projection-framework.md +155 -0
  124. package/docs/specs/PRD-002-sea-cli-tooling.md +169 -0
  125. package/docs/specs/PRD-003-dsl-core-capabilities.md +275 -0
  126. package/docs/specs/README.md +62 -0
  127. package/docs/specs/SDS-001-protobuf-projection-engine.md +451 -0
  128. package/docs/specs/SDS-002-sea-core-architecture.md +268 -0
  129. package/docs/specs/SDS-003-parser-semantic-graph.md +377 -0
  130. package/docs/specs/SDS-004-policy-engine-design.md +362 -0
  131. package/docs/specs/SDS-005-knowledge-graph-module.md +364 -0
  132. package/docs/specs/SDS-006-calm-integration.md +367 -0
  133. package/docs/specs/SDS-007-sbvr-import.md +347 -0
  134. package/docs/templates/template_explanation.md +14 -0
  135. package/docs/templates/template_howto.md +21 -0
  136. package/docs/templates/template_playbook.md +21 -0
  137. package/docs/templates/template_reference.md +17 -0
  138. package/docs/templates/template_tutorial.md +24 -0
  139. package/docs/tutorials/README.md +12 -0
  140. package/docs/tutorials/first-sea-model.md +85 -0
  141. package/docs/tutorials/getting-started.md +98 -0
  142. package/docs/tutorials/python-binding-quickstart.md +107 -0
  143. package/docs/tutorials/typescript-binding-quickstart.md +91 -0
  144. package/docs/tutorials/wasm-in-browser.md +75 -0
  145. package/domainforge-core/CHANGELOG.md +138 -0
  146. package/domainforge-core/Cargo.toml +101 -0
  147. package/domainforge-core/MIGRATING.md +32 -0
  148. package/domainforge-core/README.md +197 -0
  149. package/domainforge-core/benchmark_results.txt +51 -0
  150. package/domainforge-core/build.rs +6 -0
  151. package/domainforge-core/deny.toml +31 -0
  152. package/domainforge-core/docs/specs/projections/sbvr_kg_mapping.md +43 -0
  153. package/domainforge-core/examples/basic.sea +7 -0
  154. package/domainforge-core/examples/cli/import_export_workflow.sh +38 -0
  155. package/domainforge-core/examples/cli/validate_example.sh +30 -0
  156. package/domainforge-core/examples/evolution_semantics.sea +31 -0
  157. package/domainforge-core/examples/parser_demo.rs +203 -0
  158. package/domainforge-core/grammar/sea.pest +408 -0
  159. package/domainforge-core/schemas/calm-v1.schema.json +170 -0
  160. package/domainforge-core/schemas/shacl/sea_shapes.ttl +19 -0
  161. package/domainforge-core/src/authority/compiler.rs +309 -0
  162. package/domainforge-core/src/authority/environment.rs +203 -0
  163. package/domainforge-core/src/authority/error.rs +164 -0
  164. package/domainforge-core/src/authority/fact_resolver.rs +224 -0
  165. package/domainforge-core/src/authority/mod.rs +25 -0
  166. package/domainforge-core/src/authority/pack.rs +133 -0
  167. package/domainforge-core/src/authority/policy.rs +224 -0
  168. package/domainforge-core/src/authority/resolver.rs +446 -0
  169. package/domainforge-core/src/authority/trace.rs +217 -0
  170. package/domainforge-core/src/authority/transform.rs +168 -0
  171. package/domainforge-core/src/authority/types.rs +617 -0
  172. package/domainforge-core/src/bin/domainforge.rs +25 -0
  173. package/domainforge-core/src/calm/export.rs +538 -0
  174. package/domainforge-core/src/calm/import.rs +1220 -0
  175. package/domainforge-core/src/calm/mod.rs +9 -0
  176. package/domainforge-core/src/calm/models.rs +108 -0
  177. package/domainforge-core/src/calm/sbvr_import.rs +9 -0
  178. package/domainforge-core/src/cli/authority.rs +149 -0
  179. package/domainforge-core/src/cli/format.rs +85 -0
  180. package/domainforge-core/src/cli/import.rs +133 -0
  181. package/domainforge-core/src/cli/mod.rs +64 -0
  182. package/domainforge-core/src/cli/normalize.rs +180 -0
  183. package/domainforge-core/src/cli/pack.rs +904 -0
  184. package/domainforge-core/src/cli/parse.rs +112 -0
  185. package/domainforge-core/src/cli/project.rs +294 -0
  186. package/domainforge-core/src/cli/registry.rs +41 -0
  187. package/domainforge-core/src/cli/test.rs +12 -0
  188. package/domainforge-core/src/cli/validate.rs +195 -0
  189. package/domainforge-core/src/cli/validate_kg.rs +80 -0
  190. package/domainforge-core/src/concept_id.rs +89 -0
  191. package/domainforge-core/src/error/diagnostics.rs +426 -0
  192. package/domainforge-core/src/error/fuzzy.rs +253 -0
  193. package/domainforge-core/src/error/mod.rs +13 -0
  194. package/domainforge-core/src/formatter/comments.rs +223 -0
  195. package/domainforge-core/src/formatter/config.rs +114 -0
  196. package/domainforge-core/src/formatter/mod.rs +22 -0
  197. package/domainforge-core/src/formatter/printer.rs +906 -0
  198. package/domainforge-core/src/graph/mod.rs +858 -0
  199. package/domainforge-core/src/graph/to_ast.rs +66 -0
  200. package/domainforge-core/src/kg.rs +1476 -0
  201. package/domainforge-core/src/kg_import.rs +251 -0
  202. package/domainforge-core/src/lib.rs +203 -0
  203. package/domainforge-core/src/module/mod.rs +1 -0
  204. package/domainforge-core/src/module/resolver.rs +260 -0
  205. package/domainforge-core/src/parser/ast.rs +2919 -0
  206. package/domainforge-core/src/parser/ast_convert.rs +494 -0
  207. package/domainforge-core/src/parser/ast_schema.rs +491 -0
  208. package/domainforge-core/src/parser/error.rs +291 -0
  209. package/domainforge-core/src/parser/lint.rs +39 -0
  210. package/domainforge-core/src/parser/mod.rs +193 -0
  211. package/domainforge-core/src/parser/printer.rs +702 -0
  212. package/domainforge-core/src/parser/profiles.rs +71 -0
  213. package/domainforge-core/src/parser/string_utils.rs +138 -0
  214. package/domainforge-core/src/patterns.rs +68 -0
  215. package/domainforge-core/src/policy/core.rs +1148 -0
  216. package/domainforge-core/src/policy/expression.rs +399 -0
  217. package/domainforge-core/src/policy/mod.rs +18 -0
  218. package/domainforge-core/src/policy/normalize.rs +1028 -0
  219. package/domainforge-core/src/policy/quantifier.rs +940 -0
  220. package/domainforge-core/src/policy/three_valued.rs +140 -0
  221. package/domainforge-core/src/policy/three_valued_microbench.rs +104 -0
  222. package/domainforge-core/src/policy/type_inference.rs +67 -0
  223. package/domainforge-core/src/policy/violation.rs +36 -0
  224. package/domainforge-core/src/primitives/concept_change.rs +61 -0
  225. package/domainforge-core/src/primitives/entity.rs +224 -0
  226. package/domainforge-core/src/primitives/flow.rs +111 -0
  227. package/domainforge-core/src/primitives/instance.rs +93 -0
  228. package/domainforge-core/src/primitives/mapping_contract.rs +50 -0
  229. package/domainforge-core/src/primitives/metric.rs +79 -0
  230. package/domainforge-core/src/primitives/mod.rs +25 -0
  231. package/domainforge-core/src/primitives/projection_contract.rs +50 -0
  232. package/domainforge-core/src/primitives/quantity.rs +56 -0
  233. package/domainforge-core/src/primitives/relation.rs +68 -0
  234. package/domainforge-core/src/primitives/resource.rs +237 -0
  235. package/domainforge-core/src/primitives/resource_instance.rs +88 -0
  236. package/domainforge-core/src/primitives/role.rs +49 -0
  237. package/domainforge-core/src/projection/buf.rs +404 -0
  238. package/domainforge-core/src/projection/contracts.rs +22 -0
  239. package/domainforge-core/src/projection/engine.rs +19 -0
  240. package/domainforge-core/src/projection/mod.rs +16 -0
  241. package/domainforge-core/src/projection/protobuf.rs +3331 -0
  242. package/domainforge-core/src/projection/registry.rs +43 -0
  243. package/domainforge-core/src/python/authority.rs +253 -0
  244. package/domainforge-core/src/python/error.rs +227 -0
  245. package/domainforge-core/src/python/formatter.rs +86 -0
  246. package/domainforge-core/src/python/graph.rs +366 -0
  247. package/domainforge-core/src/python/mod.rs +9 -0
  248. package/domainforge-core/src/python/policy.rs +651 -0
  249. package/domainforge-core/src/python/primitives.rs +796 -0
  250. package/domainforge-core/src/python/registry.rs +98 -0
  251. package/domainforge-core/src/python/semantic_pack.rs +619 -0
  252. package/domainforge-core/src/python/units.rs +96 -0
  253. package/domainforge-core/src/registry/mod.rs +432 -0
  254. package/domainforge-core/src/registry/tests.rs +210 -0
  255. package/domainforge-core/src/sbvr.rs +744 -0
  256. package/domainforge-core/src/semantic_pack/builder.rs +470 -0
  257. package/domainforge-core/src/semantic_pack/canonical_json.rs +184 -0
  258. package/domainforge-core/src/semantic_pack/diagnostics.rs +214 -0
  259. package/domainforge-core/src/semantic_pack/diff.rs +216 -0
  260. package/domainforge-core/src/semantic_pack/mod.rs +31 -0
  261. package/domainforge-core/src/semantic_pack/pack_set.rs +240 -0
  262. package/domainforge-core/src/semantic_pack/resolver.rs +437 -0
  263. package/domainforge-core/src/semantic_pack/review.rs +125 -0
  264. package/domainforge-core/src/semantic_pack/schema.rs +342 -0
  265. package/domainforge-core/src/semantic_pack/signing.rs +105 -0
  266. package/domainforge-core/src/semantic_pack/validator.rs +368 -0
  267. package/domainforge-core/src/semantic_version.rs +140 -0
  268. package/domainforge-core/src/test_utils.rs +12 -0
  269. package/domainforge-core/src/typescript/authority.rs +184 -0
  270. package/domainforge-core/src/typescript/error.rs +146 -0
  271. package/domainforge-core/src/typescript/formatter.rs +76 -0
  272. package/domainforge-core/src/typescript/graph.rs +391 -0
  273. package/domainforge-core/src/typescript/mod.rs +9 -0
  274. package/domainforge-core/src/typescript/policy.rs +564 -0
  275. package/domainforge-core/src/typescript/primitives.rs +784 -0
  276. package/domainforge-core/src/typescript/registry.rs +88 -0
  277. package/domainforge-core/src/typescript/semantic_pack.rs +470 -0
  278. package/domainforge-core/src/typescript/units.rs +76 -0
  279. package/domainforge-core/src/units/mod.rs +462 -0
  280. package/domainforge-core/src/uuid_module.rs +42 -0
  281. package/domainforge-core/src/validation_error.rs +818 -0
  282. package/domainforge-core/src/validation_result.rs +30 -0
  283. package/domainforge-core/src/wasm/authority.rs +192 -0
  284. package/domainforge-core/src/wasm/error.rs +145 -0
  285. package/domainforge-core/src/wasm/formatter.rs +69 -0
  286. package/domainforge-core/src/wasm/graph.rs +471 -0
  287. package/domainforge-core/src/wasm/mod.rs +16 -0
  288. package/domainforge-core/src/wasm/policy.rs +607 -0
  289. package/domainforge-core/src/wasm/primitives.rs +295 -0
  290. package/domainforge-core/src/wasm/semantic_pack.rs +471 -0
  291. package/domainforge-core/src/wasm/units.rs +62 -0
  292. package/domainforge-core/std/aws.sea +6 -0
  293. package/domainforge-core/std/core.sea +6 -0
  294. package/domainforge-core/std/http.sea +27 -0
  295. package/domainforge-core/tests/aggregation_enhanced_tests.rs +162 -0
  296. package/domainforge-core/tests/aggregation_eval_tests.rs +248 -0
  297. package/domainforge-core/tests/aggregation_integration_tests.rs +379 -0
  298. package/domainforge-core/tests/aggregation_parser_tests.rs +92 -0
  299. package/domainforge-core/tests/aggregation_tests.rs +102 -0
  300. package/domainforge-core/tests/authority_conformance_tests.rs +1173 -0
  301. package/domainforge-core/tests/calm_round_trip_tests.rs +283 -0
  302. package/domainforge-core/tests/calm_schema_validation_tests.rs +137 -0
  303. package/domainforge-core/tests/cast_operator_tests.rs +85 -0
  304. package/domainforge-core/tests/cli_binary_check.rs +37 -0
  305. package/domainforge-core/tests/cli_import_tests.rs +291 -0
  306. package/domainforge-core/tests/cli_path_traversal_tests.rs +124 -0
  307. package/domainforge-core/tests/cli_tests.rs +63 -0
  308. package/domainforge-core/tests/diagnostics_tests.rs +203 -0
  309. package/domainforge-core/tests/dimension_unit_tests.rs +80 -0
  310. package/domainforge-core/tests/entity_tests.rs +69 -0
  311. package/domainforge-core/tests/evolution_semantics_tests.rs +157 -0
  312. package/domainforge-core/tests/flow_tests.rs +78 -0
  313. package/domainforge-core/tests/flow_unit_validation_tests.rs +31 -0
  314. package/domainforge-core/tests/graph_integration_tests.rs +218 -0
  315. package/domainforge-core/tests/graph_tests.rs +626 -0
  316. package/domainforge-core/tests/import_parsing_tests.rs +23 -0
  317. package/domainforge-core/tests/instance_integration_tests.rs +98 -0
  318. package/domainforge-core/tests/instance_parsing_tests.rs +58 -0
  319. package/domainforge-core/tests/instance_tests.rs +61 -0
  320. package/domainforge-core/tests/kg_uri_encoding_tests.rs +53 -0
  321. package/domainforge-core/tests/lint_tests.rs +19 -0
  322. package/domainforge-core/tests/metric_tests.rs +143 -0
  323. package/domainforge-core/tests/module_resolution_tests.rs +100 -0
  324. package/domainforge-core/tests/namespace_registry_tests.rs +247 -0
  325. package/domainforge-core/tests/null_handling_tests.rs +26 -0
  326. package/domainforge-core/tests/parser_ast_v3.rs +53 -0
  327. package/domainforge-core/tests/parser_dimension_registry_tests.rs +20 -0
  328. package/domainforge-core/tests/parser_integration_tests.rs +294 -0
  329. package/domainforge-core/tests/parser_metadata_tests.rs +97 -0
  330. package/domainforge-core/tests/parser_resource_domain_only_graph_test.rs +21 -0
  331. package/domainforge-core/tests/parser_resource_limits_tests.rs +122 -0
  332. package/domainforge-core/tests/parser_tests.rs +512 -0
  333. package/domainforge-core/tests/pattern_semantics_tests.rs +87 -0
  334. package/domainforge-core/tests/phase_14_determinism_tests.rs +166 -0
  335. package/domainforge-core/tests/phase_15_validation_error_tests.rs +136 -0
  336. package/domainforge-core/tests/phase_16_unicode_tests.rs +248 -0
  337. package/domainforge-core/tests/phase_17_export_tests.rs +285 -0
  338. package/domainforge-core/tests/phase_17_round_trip_tests.rs +264 -0
  339. package/domainforge-core/tests/policy_tests.rs +635 -0
  340. package/domainforge-core/tests/primitives_integration_tests.rs +151 -0
  341. package/domainforge-core/tests/print_rdf_xml.rs +14 -0
  342. package/domainforge-core/tests/printer_tests.rs +204 -0
  343. package/domainforge-core/tests/profile_tests.rs +35 -0
  344. package/domainforge-core/tests/projection_contracts_tests.rs +154 -0
  345. package/domainforge-core/tests/protobuf_projection_tests.rs +199 -0
  346. package/domainforge-core/tests/quantity_tests.rs +41 -0
  347. package/domainforge-core/tests/rdf_xml_typed_literal_tests.rs +105 -0
  348. package/domainforge-core/tests/registry_schema_tests.rs +33 -0
  349. package/domainforge-core/tests/resource_tests.rs +50 -0
  350. package/domainforge-core/tests/resource_unit_tests.rs +24 -0
  351. package/domainforge-core/tests/roles_relations_tests.rs +61 -0
  352. package/domainforge-core/tests/round_trip_tests.rs +34 -0
  353. package/domainforge-core/tests/runtime_toggle_tests.rs +70 -0
  354. package/domainforge-core/tests/sbvr_fact_schema_tests.rs +60 -0
  355. package/domainforge-core/tests/sbvr_flow_facts_tests.rs +55 -0
  356. package/domainforge-core/tests/sbvr_parsing_tests.rs +53 -0
  357. package/domainforge-core/tests/semantic_pack_alias_resolution.rs +197 -0
  358. package/domainforge-core/tests/semantic_pack_build.rs +302 -0
  359. package/domainforge-core/tests/semantic_pack_consumer_smoke.rs +150 -0
  360. package/domainforge-core/tests/semantic_pack_pack_set.rs +160 -0
  361. package/domainforge-core/tests/semantic_pack_signing.rs +157 -0
  362. package/domainforge-core/tests/semantic_pack_three_valued.rs +250 -0
  363. package/domainforge-core/tests/semantic_pack_validate.rs +196 -0
  364. package/domainforge-core/tests/std_lib_tests.rs +37 -0
  365. package/domainforge-core/tests/temporal_evaluation_tests.rs +159 -0
  366. package/domainforge-core/tests/temporal_semantics_tests.rs +214 -0
  367. package/domainforge-core/tests/three_valued_quantifiers_tests.rs +164 -0
  368. package/domainforge-core/tests/turtle_entity_export_tests.rs +38 -0
  369. package/domainforge-core/tests/turtle_escaping_tests.rs +53 -0
  370. package/domainforge-core/tests/turtle_resource_export_tests.rs +34 -0
  371. package/domainforge-core/tests/type_inference_tests.rs +40 -0
  372. package/domainforge-core/tests/unicode_validation_tests.rs +169 -0
  373. package/domainforge-core/tests/unit_tests.rs +81 -0
  374. package/domainforge-core/tests/validate_tests.rs +38 -0
  375. package/domainforge-core/tests/validation_unit_mismatch_tests.rs +83 -0
  376. package/domainforge-core/tests/wasm_tests.rs +229 -0
  377. package/domainforge-python/CHANGELOG-python.md +12 -0
  378. package/domainforge-python/MIGRATING.md +24 -0
  379. package/domainforge-python/README.md +256 -0
  380. package/domainforge-python/domainforge/__init__.py +95 -0
  381. package/domainforge-python/domainforge/domainforge.pyi +519 -0
  382. package/domainforge-python/pyproject.toml +36 -0
  383. package/domainforge-typescript/CHANGELOG-typescript.md +12 -0
  384. package/domainforge-typescript/LICENSE +201 -0
  385. package/domainforge-typescript/MIGRATING.md +24 -0
  386. package/domainforge-typescript/README.md +305 -0
  387. package/domainforge-typescript/index.d.ts +452 -0
  388. package/domainforge-typescript/index.js +361 -0
  389. package/domainforge-typescript/package.json +60 -0
  390. package/example.js +61 -0
  391. package/examples/browser.html +366 -0
  392. package/examples/namespaces/finance/cashflow.sea +5 -0
  393. package/examples/namespaces/logistics/core.sea +7 -0
  394. package/examples/observability_metrics.sea +38 -0
  395. package/fixtures/semantic_packs/acme_procurement/domain/entities.sea +39 -0
  396. package/fixtures/semantic_packs/acme_procurement/domain/metrics.sea +11 -0
  397. package/fixtures/semantic_packs/acme_procurement/domain/relations.sea +7 -0
  398. package/fixtures/semantic_packs/acme_procurement/domain/resources.sea +9 -0
  399. package/fixtures/semantic_packs/acme_procurement/review/acme.procurement.semantic-review.jsonl +7 -0
  400. package/fixtures/semantic_packs/acme_procurement/tests/ambiguous_vendor_alias.sea +8 -0
  401. package/fixtures/semantic_packs/acme_procurement/tests/deprecated_vendor_alias.sea +8 -0
  402. package/fixtures/semantic_packs/acme_procurement/tests/invalid_relation.sea +3 -0
  403. package/fixtures/semantic_packs/acme_procurement/tests/proposed_concept.sea +8 -0
  404. package/fixtures/semantic_packs/acme_procurement/tests/rejected_concept.sea +8 -0
  405. package/fixtures/semantic_packs/acme_procurement/tests/unit_mismatch.sea +7 -0
  406. package/fixtures/semantic_packs/acme_procurement/tests/unknown_vendor_policy.sea +8 -0
  407. package/fixtures/semantic_packs/acme_procurement/tests/valid_purchase_policy.sea +8 -0
  408. package/index.d.ts +2 -0
  409. package/index.js +8 -0
  410. package/justfile +200 -0
  411. package/lefthook.yml +13 -0
  412. package/lib/validate_native_exports.d.ts +4 -0
  413. package/lib/validate_native_exports.js +12 -0
  414. package/package.json +22 -0
  415. package/pytest.ini +5 -0
  416. package/python/tests/test_registry.py +75 -0
  417. package/python/tests/test_units.py +18 -0
  418. package/release-please-config.json +49 -0
  419. package/requirements-dev.txt +3 -0
  420. package/requirements.txt +3 -0
  421. package/rust-toolchain.toml +3 -0
  422. package/schemas/ast-v1.schema.json +72 -0
  423. package/schemas/ast-v2.schema.json +1200 -0
  424. package/schemas/ast-v3.schema.json +1200 -0
  425. package/schemas/sea-registry.schema.json +45 -0
  426. package/scripts/build-python.sh +37 -0
  427. package/scripts/build-release.sh +279 -0
  428. package/scripts/build-typescript.sh +13 -0
  429. package/scripts/build-wasm.sh +113 -0
  430. package/scripts/bump-version.sh +245 -0
  431. package/scripts/check_unused_test_imports.py +85 -0
  432. package/scripts/ci_tasks.py +379 -0
  433. package/scripts/clear_debug_test.sh +10 -0
  434. package/scripts/create-github-release.sh +262 -0
  435. package/scripts/create-tag.sh +203 -0
  436. package/scripts/find_and_link_test_binary.sh +70 -0
  437. package/scripts/generate-changelog.sh +271 -0
  438. package/scripts/generate-release-notes.sh +205 -0
  439. package/scripts/lint_release_security.py +96 -0
  440. package/scripts/lint_release_workflows.py +82 -0
  441. package/scripts/lint_workflow_gates.py +113 -0
  442. package/scripts/optimized-wasm-build.sh +61 -0
  443. package/scripts/patch_napi_types.py +62 -0
  444. package/scripts/pre-release-check.sh +289 -0
  445. package/scripts/prepare_rust_debug.sh +52 -0
  446. package/scripts/release.sh +373 -0
  447. package/scripts/resolve_rust_binary.py +230 -0
  448. package/scripts/run_commitlint.sh +29 -0
  449. package/scripts/test-all.sh +77 -0
  450. package/scripts/update_launch_program.py +93 -0
  451. package/secrets/README.md +27 -0
  452. package/secrets/secrets.yaml +21 -0
  453. package/test_integration.py +67 -0
  454. package/tests/test_authority.py +328 -0
  455. package/tests/test_ci_tasks.py +143 -0
  456. package/tests/test_expression.py +256 -0
  457. package/tests/test_golden_payment_flow.py +42 -0
  458. package/tests/test_graph.py +127 -0
  459. package/tests/test_instance.py +136 -0
  460. package/tests/test_parser.py +82 -0
  461. package/tests/test_primitives.py +68 -0
  462. package/tests/test_role_relation_parity.py +56 -0
  463. package/tests/test_runtime_toggle.py +156 -0
  464. package/tests/test_semantic_pack.py +639 -0
  465. package/tests/test_three_valued_eval.py +159 -0
  466. package/tsconfig.json +30 -0
  467. package/typescript-tests/advanced.test.ts +165 -0
  468. package/typescript-tests/authority.test.ts +216 -0
  469. package/typescript-tests/expression.test.ts +228 -0
  470. package/typescript-tests/golden-payment-flow.test.ts +51 -0
  471. package/typescript-tests/graph.test.ts +142 -0
  472. package/typescript-tests/native-binding.test.ts +20 -0
  473. package/typescript-tests/primitives.test.ts +88 -0
  474. package/typescript-tests/registry.test.ts +122 -0
  475. package/typescript-tests/role_relation.test.ts +63 -0
  476. package/typescript-tests/runtime_toggle.test.ts +141 -0
  477. package/typescript-tests/semantic-pack.test.ts +556 -0
  478. package/typescript-tests/three_valued_eval.test.ts +135 -0
  479. package/typescript-tests/units.test.ts +36 -0
  480. package/vitest.config.ts +13 -0
  481. package/wasm_demo.html +225 -0
@@ -0,0 +1,1148 @@
1
+ use super::expression::{BinaryOp, Expression, UnaryOp};
2
+ use super::violation::{Severity, Violation};
3
+ use crate::graph::Graph;
4
+ use crate::policy::ThreeValuedBool;
5
+ use crate::units::get_default_registry;
6
+ use crate::{ConceptId, SemanticVersion};
7
+ use rust_decimal::prelude::{FromPrimitive, FromStr};
8
+ use rust_decimal::Decimal;
9
+ use serde::{Deserialize, Serialize};
10
+
11
+ #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
12
+ pub enum PolicyModality {
13
+ Obligation,
14
+ Prohibition,
15
+ Permission,
16
+ }
17
+
18
+ /// Backwards-compatible alias used by existing tests and documentation
19
+ #[allow(unused_imports)]
20
+ pub use PolicyModality as DeonticModality;
21
+
22
+ #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
23
+ pub enum PolicyKind {
24
+ Constraint,
25
+ Derivation,
26
+ Obligation,
27
+ }
28
+
29
+ #[derive(Debug, Clone, Serialize, Deserialize)]
30
+ pub struct Policy {
31
+ pub id: ConceptId,
32
+ pub name: String,
33
+ pub namespace: String,
34
+ pub version: SemanticVersion,
35
+ expression: Expression,
36
+ pub modality: PolicyModality,
37
+ pub kind: PolicyKind,
38
+ pub priority: i32,
39
+ pub rationale: Option<String>,
40
+ pub tags: Vec<String>,
41
+ #[serde(skip)]
42
+ cached_normalized_expr: std::sync::OnceLock<super::NormalizedExpression>,
43
+ }
44
+
45
+ #[derive(Debug, Clone, Serialize, Deserialize)]
46
+ pub struct EvaluationResult {
47
+ /// Backwards compatible boolean representing whether a policy was satisfied.
48
+ /// Defaults to `false` if the evaluator produced an unknown (NULL) result.
49
+ pub is_satisfied: bool,
50
+ /// Tri-state evaluation result: Some(true/false) or None when evaluation is unknown.
51
+ pub is_satisfied_tristate: Option<bool>,
52
+ pub violations: Vec<Violation>,
53
+ }
54
+
55
+ impl Policy {
56
+ /// Returns the normalized canonical form of this policy's expression.
57
+ ///
58
+ /// Useful for caching, equivalence checking, and deterministic display.
59
+ #[must_use]
60
+ pub fn normalized_expression(&self) -> &super::NormalizedExpression {
61
+ self.cached_normalized_expr
62
+ .get_or_init(|| self.expression.normalize())
63
+ }
64
+
65
+ /// Get the policy expression.
66
+ pub fn expression(&self) -> &Expression {
67
+ &self.expression
68
+ }
69
+
70
+ /// Set the policy expression and invalidate the normalized cache.
71
+ pub fn set_expression(&mut self, expr: Expression) {
72
+ self.expression = expr;
73
+ // Invalidate cache by creating a new empty lock
74
+ self.cached_normalized_expr = std::sync::OnceLock::new();
75
+ }
76
+
77
+ pub fn new(name: impl Into<String>, expression: Expression) -> Self {
78
+ let name = name.into();
79
+ Self {
80
+ id: ConceptId::from_concept("default", &name),
81
+ name,
82
+ namespace: "default".to_string(),
83
+ version: SemanticVersion::default(),
84
+ expression,
85
+ modality: PolicyModality::Obligation,
86
+ kind: PolicyKind::Constraint,
87
+ priority: 0,
88
+ rationale: None,
89
+ tags: Vec::new(),
90
+ cached_normalized_expr: std::sync::OnceLock::new(),
91
+ }
92
+ }
93
+
94
+ pub fn new_with_namespace(
95
+ name: impl Into<String>,
96
+ namespace: impl Into<String>,
97
+ expression: Expression,
98
+ ) -> Self {
99
+ let namespace = namespace.into();
100
+ let name = name.into();
101
+ let id = ConceptId::from_concept(&namespace, &name);
102
+
103
+ Self {
104
+ id,
105
+ name,
106
+ namespace,
107
+ version: SemanticVersion::default(),
108
+ expression,
109
+ modality: PolicyModality::Obligation,
110
+ kind: PolicyKind::Constraint,
111
+ priority: 0,
112
+ rationale: None,
113
+ tags: Vec::new(),
114
+ cached_normalized_expr: std::sync::OnceLock::new(),
115
+ }
116
+ }
117
+
118
+ pub fn with_modality(mut self, modality: PolicyModality) -> Self {
119
+ self.modality = modality;
120
+ self
121
+ }
122
+
123
+ pub fn with_version(mut self, version: SemanticVersion) -> Self {
124
+ self.version = version;
125
+ self
126
+ }
127
+
128
+ pub fn with_kind(mut self, kind: PolicyKind) -> Self {
129
+ self.kind = kind;
130
+ self
131
+ }
132
+
133
+ pub fn with_priority(mut self, priority: i32) -> Self {
134
+ self.priority = priority;
135
+ self
136
+ }
137
+
138
+ pub fn with_rationale(mut self, rationale: impl Into<String>) -> Self {
139
+ self.rationale = Some(rationale.into());
140
+ self
141
+ }
142
+
143
+ pub fn with_tags<I, S>(mut self, tags: I) -> Self
144
+ where
145
+ I: IntoIterator<Item = S>,
146
+ S: Into<String>,
147
+ {
148
+ self.tags = tags.into_iter().map(Into::into).collect();
149
+ self
150
+ }
151
+
152
+ pub fn with_metadata(
153
+ mut self,
154
+ kind: Option<PolicyKind>,
155
+ modality: Option<PolicyModality>,
156
+ priority: Option<i32>,
157
+ rationale: Option<String>,
158
+ tags: Vec<String>,
159
+ ) -> Self {
160
+ if let Some(kind) = kind {
161
+ self.kind = kind;
162
+ }
163
+ if let Some(modality) = modality {
164
+ self.modality = modality;
165
+ }
166
+ if let Some(priority) = priority {
167
+ self.priority = priority;
168
+ }
169
+ self.rationale = rationale;
170
+ if !tags.is_empty() {
171
+ self.tags = tags;
172
+ }
173
+ self
174
+ }
175
+
176
+ pub fn kind(&self) -> &PolicyKind {
177
+ &self.kind
178
+ }
179
+
180
+ pub fn evaluate(&self, graph: &Graph) -> Result<EvaluationResult, String> {
181
+ self.evaluate_with_mode(graph, graph.use_three_valued_logic())
182
+ }
183
+
184
+ pub fn evaluate_with_mode(
185
+ &self,
186
+ graph: &Graph,
187
+ use_three_valued_logic: bool,
188
+ ) -> Result<EvaluationResult, String> {
189
+ Self::validate_aggregation_usage(&self.expression, true)?;
190
+
191
+ let expanded = self.expression.expand(graph)?;
192
+
193
+ // Evaluate expression; runtime toggle chooses three-valued vs boolean path.
194
+ // We compute the tri-state result and derive a backward-compatible boolean (false when Null).
195
+ let is_satisfied_tristate: Option<bool> = if use_three_valued_logic {
196
+ match self.evaluate_expression_three_valued(&expanded, graph)? {
197
+ ThreeValuedBool::True => Some(true),
198
+ ThreeValuedBool::False => Some(false),
199
+ ThreeValuedBool::Null => None,
200
+ }
201
+ } else {
202
+ Some(self.evaluate_expression_boolean(&expanded, graph)?)
203
+ };
204
+
205
+ let is_satisfied = is_satisfied_tristate.unwrap_or(false);
206
+
207
+ let violations = if is_satisfied_tristate == Some(true) {
208
+ vec![]
209
+ } else if is_satisfied_tristate == Some(false) {
210
+ vec![Violation::new(
211
+ &self.name,
212
+ format!("Policy '{}' was violated", self.name),
213
+ self.modality.to_severity(),
214
+ )]
215
+ } else {
216
+ // Unknown (NULL) evaluation: severity follows the policy modality.
217
+ vec![Violation::new(
218
+ &self.name,
219
+ format!("Policy '{}' evaluation is UNKNOWN (NULL)", self.name),
220
+ self.modality.to_severity(),
221
+ )]
222
+ };
223
+
224
+ Ok(EvaluationResult {
225
+ is_satisfied,
226
+ is_satisfied_tristate,
227
+ violations,
228
+ })
229
+ }
230
+
231
+ fn validate_aggregation_usage(
232
+ expr: &Expression,
233
+ in_boolean_context: bool,
234
+ ) -> Result<(), String> {
235
+ match expr {
236
+ Expression::Aggregation { .. } | Expression::AggregationComprehension { .. } => {
237
+ if in_boolean_context {
238
+ Err("Aggregation in boolean context requires explicit comparison (e.g., COUNT(...) > 0)".to_string())
239
+ } else {
240
+ Ok(())
241
+ }
242
+ }
243
+ Expression::Binary { op, left, right } => {
244
+ let child_boolean = matches!(op, BinaryOp::And | BinaryOp::Or);
245
+ Self::validate_aggregation_usage(left, child_boolean)?;
246
+ Self::validate_aggregation_usage(right, child_boolean)?;
247
+ Ok(())
248
+ }
249
+ Expression::Unary { op, operand } => {
250
+ let child_boolean = matches!(op, UnaryOp::Not);
251
+ Self::validate_aggregation_usage(operand, child_boolean)
252
+ }
253
+ Expression::Quantifier {
254
+ collection,
255
+ condition,
256
+ ..
257
+ } => {
258
+ Self::validate_aggregation_usage(collection, false)?;
259
+ Self::validate_aggregation_usage(condition, true)
260
+ }
261
+ Expression::GroupBy {
262
+ collection,
263
+ filter,
264
+ key,
265
+ condition,
266
+ ..
267
+ } => {
268
+ Self::validate_aggregation_usage(collection, false)?;
269
+ if let Some(f) = filter {
270
+ Self::validate_aggregation_usage(f, true)?;
271
+ }
272
+ Self::validate_aggregation_usage(key, false)?;
273
+ Self::validate_aggregation_usage(condition, true)
274
+ }
275
+ _ => Ok(()),
276
+ }
277
+ }
278
+
279
+ fn evaluate_expression_boolean(
280
+ &self,
281
+ expr: &Expression,
282
+ graph: &Graph,
283
+ ) -> Result<bool, String> {
284
+ match expr {
285
+ Expression::Literal(v) => v
286
+ .as_bool()
287
+ .ok_or_else(|| format!("Expected boolean literal, got: {}", v)),
288
+ Expression::Variable(name) => {
289
+ Err(format!("Cannot evaluate unexpanded variable: {}", name))
290
+ }
291
+ Expression::Cast { .. } => {
292
+ let val = Self::get_runtime_value(expr, graph)?;
293
+ val.as_bool()
294
+ .ok_or_else(|| format!("Expected boolean from cast, got: {}", val))
295
+ }
296
+ Expression::Binary { op, left, right } => match op {
297
+ BinaryOp::And | BinaryOp::Or => {
298
+ let left_val = self.evaluate_expression_boolean(left, graph)?;
299
+ let right_val = self.evaluate_expression_boolean(right, graph)?;
300
+
301
+ Ok(match op {
302
+ BinaryOp::And => left_val && right_val,
303
+ BinaryOp::Or => left_val || right_val,
304
+ _ => unreachable!(),
305
+ })
306
+ }
307
+ BinaryOp::Equal | BinaryOp::NotEqual => {
308
+ self.compare_values(left, right, graph, |l, r| match op {
309
+ BinaryOp::Equal => l == r,
310
+ BinaryOp::NotEqual => l != r,
311
+ _ => unreachable!(),
312
+ })
313
+ }
314
+ BinaryOp::GreaterThan
315
+ | BinaryOp::LessThan
316
+ | BinaryOp::GreaterThanOrEqual
317
+ | BinaryOp::LessThanOrEqual => {
318
+ self.compare_numeric(left, right, graph, |l, r| match op {
319
+ BinaryOp::GreaterThan => l > r,
320
+ BinaryOp::LessThan => l < r,
321
+ BinaryOp::GreaterThanOrEqual => l >= r,
322
+ BinaryOp::LessThanOrEqual => l <= r,
323
+ _ => unreachable!(),
324
+ })
325
+ }
326
+ BinaryOp::Plus | BinaryOp::Minus | BinaryOp::Multiply | BinaryOp::Divide => {
327
+ Err("Arithmetic operations not supported in boolean context".to_string())
328
+ }
329
+ BinaryOp::Contains | BinaryOp::StartsWith | BinaryOp::EndsWith => self
330
+ .compare_strings(left, right, graph, |l, r| match op {
331
+ BinaryOp::Contains => l.contains(r),
332
+ BinaryOp::StartsWith => l.starts_with(r),
333
+ BinaryOp::EndsWith => l.ends_with(r),
334
+ _ => unreachable!(),
335
+ }),
336
+ BinaryOp::HasRole => self.evaluate_has_role(left, right, graph),
337
+ BinaryOp::Matches => self.evaluate_pattern_match(left, right, graph),
338
+ BinaryOp::Before | BinaryOp::After | BinaryOp::During => {
339
+ // Temporal operators - parse and compare ISO 8601 timestamps
340
+ let left_str = self.get_string_value(left, graph)?;
341
+ let right_str = self.get_string_value(right, graph)?;
342
+
343
+ // Parse timestamps using chrono
344
+ let left_dt = chrono::DateTime::parse_from_rfc3339(&left_str).map_err(|e| {
345
+ format!("Failed to parse left timestamp '{}': {}", left_str, e)
346
+ })?;
347
+ let right_dt =
348
+ chrono::DateTime::parse_from_rfc3339(&right_str).map_err(|e| {
349
+ format!("Failed to parse right timestamp '{}': {}", right_str, e)
350
+ })?;
351
+
352
+ let result = match op {
353
+ BinaryOp::Before => left_dt < right_dt,
354
+ BinaryOp::After => left_dt > right_dt,
355
+ BinaryOp::During => {
356
+ return Err("'during' operator requires interval semantics which are not yet implemented. Use 'before' and 'after' for timestamp comparisons.".to_string())
357
+ }
358
+ _ => unreachable!(),
359
+ };
360
+ Ok(result)
361
+ }
362
+ },
363
+ Expression::Unary { op, operand } => {
364
+ let val = self.evaluate_expression_boolean(operand, graph)?;
365
+ Ok(match op {
366
+ UnaryOp::Not => !val,
367
+ UnaryOp::Negate => {
368
+ return Err("Negate operator not supported in boolean context".to_string())
369
+ }
370
+ })
371
+ }
372
+ Expression::Quantifier { .. } => {
373
+ Err("Cannot evaluate non-expanded quantifier".to_string())
374
+ }
375
+ Expression::MemberAccess { object, member } => {
376
+ let value = Self::get_runtime_value(expr, graph)?;
377
+ match value {
378
+ serde_json::Value::Bool(v) => Ok(v),
379
+ serde_json::Value::Null => Ok(false),
380
+ _ => Err(format!(
381
+ "Expected boolean value for member '{}.{}', but found {:?}",
382
+ object, member, value
383
+ )),
384
+ }
385
+ }
386
+ Expression::Aggregation { .. } => {
387
+ Err("Cannot evaluate non-expanded aggregation".to_string())
388
+ }
389
+ Expression::AggregationComprehension { .. } => {
390
+ Err("Cannot evaluate non-expanded aggregation comprehension".to_string())
391
+ }
392
+ Expression::QuantityLiteral { .. } => {
393
+ Err("Cannot evaluate quantity literal in boolean context".to_string())
394
+ }
395
+ Expression::TimeLiteral(_) => {
396
+ Err("Cannot evaluate time literal in boolean context".to_string())
397
+ }
398
+ Expression::IntervalLiteral { .. } => {
399
+ Err("Cannot evaluate interval literal in boolean context".to_string())
400
+ }
401
+ Expression::GroupBy { .. } => Err("Cannot evaluate non-expanded group_by".to_string()),
402
+ }
403
+ }
404
+
405
+ fn evaluate_expression_three_valued(
406
+ &self,
407
+ expr: &Expression,
408
+ graph: &Graph,
409
+ ) -> Result<ThreeValuedBool, String> {
410
+ use ThreeValuedBool as T;
411
+
412
+ match expr {
413
+ Expression::Literal(v) => Ok(T::from_option_bool(v.as_bool())),
414
+ Expression::Variable(name) => Err(format!("Cannot evaluate unexpanded variable: {}", name)),
415
+ Expression::Cast { .. } => {
416
+ let val = Self::get_runtime_value(expr, graph)?;
417
+ Ok(T::from_option_bool(val.as_bool()))
418
+ }
419
+ Expression::Binary { op, left, right } => match op {
420
+ BinaryOp::And | BinaryOp::Or => {
421
+ let l = self.evaluate_expression_three_valued(left, graph)?;
422
+ let r = self.evaluate_expression_three_valued(right, graph)?;
423
+ Ok(match op {
424
+ BinaryOp::And => l.and(r),
425
+ BinaryOp::Or => l.or(r),
426
+ _ => unreachable!(),
427
+ })
428
+ }
429
+ BinaryOp::Equal | BinaryOp::NotEqual => {
430
+ let left_val = Self::get_runtime_value(left, graph);
431
+ let right_val = Self::get_runtime_value(right, graph);
432
+ match (left_val, right_val) {
433
+ (Ok(lv), Ok(rv)) => {
434
+ // If either operand is JSON Null, the comparison yields Null.
435
+ if lv.is_null() || rv.is_null() {
436
+ Ok(T::Null)
437
+ } else {
438
+ let numeric_eq = match (
439
+ Self::parse_numeric_with_unit_value(&lv),
440
+ Self::parse_numeric_with_unit_value(&rv),
441
+ ) {
442
+ (Ok(ln), Ok(rn)) => {
443
+ self.normalize_units_nullable(ln, rn)?
444
+ .map(|(l, r)| l == r)
445
+ }
446
+ _ => None,
447
+ };
448
+
449
+ let equality = if let Some(eq) = numeric_eq {
450
+ Some(eq)
451
+ } else if lv.is_number() || rv.is_number() {
452
+ None
453
+ } else {
454
+ Some(lv == rv)
455
+ };
456
+
457
+ let eq = match (op, equality) {
458
+ (_, None) => return Ok(T::Null),
459
+ (BinaryOp::Equal, Some(true)) => true,
460
+ (BinaryOp::Equal, Some(false)) => false,
461
+ (BinaryOp::NotEqual, Some(true)) => false,
462
+ (BinaryOp::NotEqual, Some(false)) => true,
463
+ _ => unreachable!(),
464
+ };
465
+
466
+ Ok(T::from_option_bool(Some(eq)))
467
+ }
468
+ }
469
+ _ => Ok(T::Null),
470
+ }
471
+ }
472
+ BinaryOp::GreaterThan
473
+ | BinaryOp::LessThan
474
+ | BinaryOp::GreaterThanOrEqual
475
+ | BinaryOp::LessThanOrEqual => {
476
+ let left_v = Self::get_runtime_value(left, graph);
477
+ let right_v = Self::get_runtime_value(right, graph);
478
+ match (left_v, right_v) {
479
+ (Ok(lv), Ok(rv)) => {
480
+ if lv.is_null() || rv.is_null() {
481
+ Ok(T::Null)
482
+ } else {
483
+ let numeric = self
484
+ .normalize_units_nullable(
485
+ Self::parse_numeric_with_unit_value(&lv)
486
+ .ok()
487
+ .flatten(),
488
+ Self::parse_numeric_with_unit_value(&rv)
489
+ .ok()
490
+ .flatten(),
491
+ )?
492
+ .map(|(l, r)| match op {
493
+ BinaryOp::GreaterThan => l > r,
494
+ BinaryOp::LessThan => l < r,
495
+ BinaryOp::GreaterThanOrEqual => l >= r,
496
+ BinaryOp::LessThanOrEqual => l <= r,
497
+ _ => unreachable!(),
498
+ });
499
+
500
+ Ok(T::from_option_bool(numeric))
501
+ }
502
+ }
503
+ _ => Ok(T::Null),
504
+ }
505
+ }
506
+ BinaryOp::Plus | BinaryOp::Minus | BinaryOp::Multiply | BinaryOp::Divide => {
507
+ Err("Arithmetic operations not supported in boolean context".to_string())
508
+ }
509
+ BinaryOp::Contains | BinaryOp::StartsWith | BinaryOp::EndsWith => {
510
+ let left_v = Self::get_runtime_value(left, graph);
511
+ let right_v = Self::get_runtime_value(right, graph);
512
+ match (left_v, right_v) {
513
+ (Ok(lv), Ok(rv)) => {
514
+ if lv.is_null() || rv.is_null() {
515
+ Ok(T::Null)
516
+ } else if let (Some(ls), Some(rs)) = (lv.as_str(), rv.as_str()) {
517
+ let ok = match op {
518
+ BinaryOp::Contains => ls.contains(rs),
519
+ BinaryOp::StartsWith => ls.starts_with(rs),
520
+ BinaryOp::EndsWith => ls.ends_with(rs),
521
+ _ => unreachable!(),
522
+ };
523
+ Ok(T::from_option_bool(Some(ok)))
524
+ } else {
525
+ Ok(T::Null)
526
+ }
527
+ }
528
+ _ => Ok(T::Null),
529
+ }
530
+ }
531
+ BinaryOp::HasRole => {
532
+ let role_check = self.evaluate_has_role(left, right, graph)?;
533
+ Ok(T::from_option_bool(Some(role_check)))
534
+ }
535
+ BinaryOp::Matches => {
536
+ let left_v = Self::get_runtime_value(left, graph);
537
+ let right_v = Self::get_runtime_value(right, graph);
538
+
539
+ match (left_v, right_v) {
540
+ (Ok(lv), Ok(rv)) => {
541
+ if lv.is_null() || rv.is_null() {
542
+ return Ok(T::Null);
543
+ }
544
+
545
+ if let (Some(candidate), Some(pattern_name)) = (lv.as_str(), rv.as_str()) {
546
+ let pattern = graph
547
+ .find_pattern(pattern_name, Some(&self.namespace))
548
+ .ok_or_else(|| {
549
+ format!(
550
+ "Pattern '{}' not found in namespace '{}'",
551
+ pattern_name, self.namespace
552
+ )
553
+ })?;
554
+ let is_match = pattern.is_match(candidate).map_err(|e| {
555
+ format!(
556
+ "Pattern '{}' failed to evaluate: {}",
557
+ pattern_name, e
558
+ )
559
+ })?;
560
+ Ok(T::from_option_bool(Some(is_match)))
561
+ } else {
562
+ Ok(T::Null)
563
+ }
564
+ }
565
+ _ => Ok(T::Null),
566
+ }
567
+ }
568
+ BinaryOp::Before | BinaryOp::After | BinaryOp::During => {
569
+ // Temporal operators
570
+ if matches!(op, BinaryOp::During) {
571
+ return Err("'during' operator requires interval semantics which are not yet implemented. Use 'before' and 'after' for timestamp comparisons.".to_string());
572
+ }
573
+
574
+ // Parse and compare ISO 8601 timestamps
575
+ let left_v = Self::get_runtime_value(left, graph);
576
+ let right_v = Self::get_runtime_value(right, graph);
577
+ match (left_v, right_v) {
578
+ (Ok(lv), Ok(rv)) => {
579
+ if lv.is_null() || rv.is_null() {
580
+ Ok(T::Null)
581
+ } else if let (Some(ls), Some(rs)) = (lv.as_str(), rv.as_str()) {
582
+ // Parse timestamps using chrono
583
+ let left_dt = match chrono::DateTime::parse_from_rfc3339(ls) {
584
+ Ok(dt) => dt,
585
+ Err(_) => return Ok(T::Null), // Invalid timestamp -> Null
586
+ };
587
+ let right_dt = match chrono::DateTime::parse_from_rfc3339(rs) {
588
+ Ok(dt) => dt,
589
+ Err(_) => return Ok(T::Null), // Invalid timestamp -> Null
590
+ };
591
+
592
+ let result = match op {
593
+ BinaryOp::Before => left_dt < right_dt,
594
+ BinaryOp::After => left_dt > right_dt,
595
+ _ => unreachable!(),
596
+ };
597
+ Ok(T::from_option_bool(Some(result)))
598
+ } else {
599
+ Ok(T::Null)
600
+ }
601
+ }
602
+ _ => Ok(T::Null),
603
+ }
604
+ }
605
+ },
606
+ Expression::Unary { op, operand } => {
607
+ let v = self.evaluate_expression_three_valued(operand, graph)?;
608
+ Ok(match op {
609
+ UnaryOp::Not => v.not(),
610
+ UnaryOp::Negate => return Err("Negate operator not supported in boolean context".to_string()),
611
+ })
612
+ }
613
+ Expression::Quantifier { quantifier, variable, collection, condition } => {
614
+ // Evaluate quantifiers with three-valued semantics
615
+ let items = Expression::get_collection(collection, graph)?;
616
+ use super::expression::Quantifier as Q;
617
+
618
+ let mut saw_true = 0usize;
619
+ let mut saw_false = 0usize;
620
+ let mut saw_null = 0usize;
621
+
622
+ for item in items {
623
+ let substituted = condition.substitute(variable, &item)?;
624
+ let val = self.evaluate_expression_three_valued(&substituted, graph)?;
625
+ match val {
626
+ T::True => saw_true += 1,
627
+ T::False => saw_false += 1,
628
+ T::Null => saw_null += 1,
629
+ }
630
+ }
631
+
632
+ match quantifier {
633
+ Q::ForAll => {
634
+ if saw_false > 0 { return Ok(T::False); }
635
+ if saw_null > 0 { return Ok(T::Null); }
636
+ Ok(T::True)
637
+ }
638
+ Q::Exists => {
639
+ if saw_true > 0 { return Ok(T::True); }
640
+ if saw_null > 0 { return Ok(T::Null); }
641
+ Ok(T::False)
642
+ }
643
+ Q::ExistsUnique => {
644
+ if saw_true > 1 { return Ok(T::False); }
645
+ if saw_true == 1 && saw_null == 0 { return Ok(T::True); }
646
+ if saw_true == 1 && saw_null > 0 { return Ok(T::Null); }
647
+ if saw_true == 0 && saw_null > 0 { return Ok(T::Null); }
648
+ Ok(T::False)
649
+ }
650
+ }
651
+ }
652
+ Expression::MemberAccess { object: _, member: _ } => {
653
+ // Resolve object/member to a runtime value and convert to bool
654
+ let value = Self::get_runtime_value(expr, graph)?;
655
+ Ok(T::from_option_bool(value.as_bool()))
656
+ }
657
+ Expression::Aggregation { .. } => Err("Aggregation in boolean context requires explicit comparison (e.g., COUNT(...) > 0)".to_string()),
658
+ Expression::AggregationComprehension { .. } => Err("Aggregation in boolean context requires explicit comparison (e.g., COUNT(...) > 0)".to_string()),
659
+ Expression::QuantityLiteral { .. } => Err("Cannot convert quantity to boolean; compare against a threshold instead".to_string()),
660
+ Expression::TimeLiteral(_) => Err("Cannot convert time to boolean; use temporal comparison operators".to_string()),
661
+ Expression::IntervalLiteral { .. } => Err("Cannot convert interval to boolean; use temporal comparison operators".to_string()),
662
+ Expression::GroupBy { .. } => Err("Cannot evaluate non-expanded group_by".to_string()),
663
+ }
664
+ }
665
+
666
+ fn compare_values<F>(
667
+ &self,
668
+ left: &Expression,
669
+ right: &Expression,
670
+ graph: &Graph,
671
+ op: F,
672
+ ) -> Result<bool, String>
673
+ where
674
+ F: Fn(&serde_json::Value, &serde_json::Value) -> bool,
675
+ {
676
+ let left_val = self
677
+ .get_literal_value(left)
678
+ .or_else(|_| Self::get_runtime_value(left, graph))?;
679
+ let right_val = self
680
+ .get_literal_value(right)
681
+ .or_else(|_| Self::get_runtime_value(right, graph))?;
682
+ Ok(op(&left_val, &right_val))
683
+ }
684
+
685
+ fn compare_numeric<F>(
686
+ &self,
687
+ left: &Expression,
688
+ right: &Expression,
689
+ graph: &Graph,
690
+ op: F,
691
+ ) -> Result<bool, String>
692
+ where
693
+ F: Fn(Decimal, Decimal) -> bool,
694
+ {
695
+ let left_val = self.resolve_numeric_with_unit(left, graph)?;
696
+ let right_val = self.resolve_numeric_with_unit(right, graph)?;
697
+ let (left_aligned, right_aligned) = self.normalize_units_strict(left_val, right_val)?;
698
+ Ok(op(left_aligned, right_aligned))
699
+ }
700
+
701
+ fn compare_strings<F>(
702
+ &self,
703
+ left: &Expression,
704
+ right: &Expression,
705
+ graph: &Graph,
706
+ op: F,
707
+ ) -> Result<bool, String>
708
+ where
709
+ F: Fn(&str, &str) -> bool,
710
+ {
711
+ let left_val = self.get_string_value(left, graph)?;
712
+ let right_val = self.get_string_value(right, graph)?;
713
+ Ok(op(&left_val, &right_val))
714
+ }
715
+
716
+ fn evaluate_pattern_match(
717
+ &self,
718
+ left: &Expression,
719
+ right: &Expression,
720
+ graph: &Graph,
721
+ ) -> Result<bool, String> {
722
+ let candidate = self.get_string_value(left, graph)?;
723
+ let pattern_name = self.get_string_value(right, graph)?;
724
+
725
+ let pattern = graph
726
+ .find_pattern(&pattern_name, Some(&self.namespace))
727
+ .ok_or_else(|| {
728
+ format!(
729
+ "Pattern '{}' not found in namespace '{}'",
730
+ pattern_name, self.namespace
731
+ )
732
+ })?;
733
+
734
+ pattern
735
+ .is_match(&candidate)
736
+ .map_err(|e| format!("Pattern '{}' failed to evaluate: {}", pattern_name, e))
737
+ }
738
+
739
+ fn evaluate_has_role(
740
+ &self,
741
+ left: &Expression,
742
+ right: &Expression,
743
+ graph: &Graph,
744
+ ) -> Result<bool, String> {
745
+ let target_role = self.get_string_value(right, graph)?;
746
+ let roles = self.collect_roles(left, graph)?;
747
+
748
+ Ok(roles
749
+ .iter()
750
+ .any(|role| role.eq_ignore_ascii_case(&target_role)))
751
+ }
752
+
753
+ fn collect_roles(&self, expr: &Expression, graph: &Graph) -> Result<Vec<String>, String> {
754
+ let value = Self::get_runtime_value(expr, graph)?;
755
+
756
+ if let Some(arr) = value.as_array() {
757
+ return Ok(arr
758
+ .iter()
759
+ .filter_map(|v| v.as_str().map(|s| s.to_string()))
760
+ .collect());
761
+ }
762
+
763
+ if let Some(obj) = value.as_object() {
764
+ if let Some(roles) = obj.get("roles").and_then(|r| r.as_array()) {
765
+ return Ok(roles
766
+ .iter()
767
+ .filter_map(|v| v.as_str().map(|s| s.to_string()))
768
+ .collect());
769
+ }
770
+
771
+ if let Some(name) = obj.get("name").and_then(|v| v.as_str()) {
772
+ if let Some(entity_id) = graph.find_entity_by_name(name) {
773
+ return Ok(graph.role_names_for_entity(&entity_id));
774
+ }
775
+ }
776
+ }
777
+
778
+ if let Some(name) = value.as_str() {
779
+ if let Some(entity_id) = graph.find_entity_by_name(name) {
780
+ return Ok(graph.role_names_for_entity(&entity_id));
781
+ }
782
+
783
+ if let Some(role_id) = graph.find_role_by_name(name) {
784
+ if let Some(role) = graph.get_role(&role_id) {
785
+ return Ok(vec![role.name().to_string()]);
786
+ }
787
+ }
788
+ }
789
+
790
+ Ok(Vec::new())
791
+ }
792
+
793
+ fn get_literal_value(&self, expr: &Expression) -> Result<serde_json::Value, String> {
794
+ match expr {
795
+ Expression::Literal(v) => Ok(v.clone()),
796
+ _ => Err("Expected literal value".to_string()),
797
+ }
798
+ }
799
+
800
+ fn parse_decimal_value(value: &serde_json::Value) -> Result<Decimal, String> {
801
+ if let Some(s) = value.as_str() {
802
+ Decimal::from_str(s).map_err(|e| e.to_string())
803
+ } else if let Some(f) = value.as_f64() {
804
+ Decimal::from_f64(f).ok_or_else(|| format!("Unable to represent {} as Decimal", f))
805
+ } else if let Some(i) = value.as_i64() {
806
+ Ok(Decimal::from(i))
807
+ } else if let Some(u) = value.as_u64() {
808
+ Decimal::from_u64(u).ok_or_else(|| format!("Unable to represent {} as Decimal", u))
809
+ } else {
810
+ Err(format!("Expected numeric value, got: {}", value))
811
+ }
812
+ }
813
+
814
+ fn parse_numeric_with_unit_value(
815
+ value: &serde_json::Value,
816
+ ) -> Result<Option<(Decimal, Option<String>)>, String> {
817
+ if let Some(obj) = value.as_object() {
818
+ match (obj.get("__quantity_value"), obj.get("__quantity_unit")) {
819
+ (Some(q_val), Some(q_unit)) => {
820
+ let unit_str = q_unit
821
+ .as_str()
822
+ .ok_or_else(|| {
823
+ format!("Expected __quantity_unit to be string, got: {}", q_unit)
824
+ })?
825
+ .to_string();
826
+ let value_dec = Self::parse_decimal_value(q_val)
827
+ .map_err(|e| format!("Invalid __quantity_value: {}", e))?;
828
+ return Ok(Some((value_dec, Some(unit_str))));
829
+ }
830
+ (Some(_), None) | (None, Some(_)) => {
831
+ return Err(
832
+ "Quantity object must include both __quantity_value and __quantity_unit"
833
+ .to_string(),
834
+ )
835
+ }
836
+ _ => {}
837
+ }
838
+ }
839
+
840
+ if value.is_number() || value.is_string() {
841
+ return Ok(Some((Self::parse_decimal_value(value)?, None)));
842
+ }
843
+
844
+ Ok(None)
845
+ }
846
+
847
+ fn resolve_numeric_with_unit(
848
+ &self,
849
+ expr: &Expression,
850
+ graph: &Graph,
851
+ ) -> Result<(Decimal, Option<String>), String> {
852
+ let value = self
853
+ .get_literal_value(expr)
854
+ .or_else(|_| Self::get_runtime_value(expr, graph))?;
855
+ Self::parse_numeric_with_unit_value(&value)?
856
+ .ok_or_else(|| format!("Expected numeric value, got: {}", value))
857
+ }
858
+
859
+ fn normalize_units_strict(
860
+ &self,
861
+ left: (Decimal, Option<String>),
862
+ right: (Decimal, Option<String>),
863
+ ) -> Result<(Decimal, Decimal), String> {
864
+ match (left.1, right.1) {
865
+ (Some(l_unit), Some(r_unit)) => {
866
+ let registry = get_default_registry();
867
+ let registry = registry
868
+ .read()
869
+ .map_err(|e| format!("Failed to lock unit registry: {}", e))?;
870
+
871
+ if l_unit == r_unit {
872
+ Ok((left.0, right.0))
873
+ } else {
874
+ let from = registry
875
+ .get_unit(&r_unit)
876
+ .map_err(|e| format!("Invalid unit '{}': {}", r_unit, e))?;
877
+ let to = registry
878
+ .get_unit(&l_unit)
879
+ .map_err(|e| format!("Invalid unit '{}': {}", l_unit, e))?;
880
+ let converted = registry
881
+ .convert(right.0, from, to)
882
+ .map_err(|e| format!("Unit conversion failed: {}", e))?;
883
+ Ok((left.0, converted))
884
+ }
885
+ }
886
+ (None, None) => Ok((left.0, right.0)),
887
+ (Some(l_unit), None) => Err(format!(
888
+ "Cannot compare quantity with unit '{}' to unitless value",
889
+ l_unit
890
+ )),
891
+ (None, Some(r_unit)) => Err(format!(
892
+ "Cannot compare unitless value to quantity with unit '{}'",
893
+ r_unit
894
+ )),
895
+ }
896
+ }
897
+
898
+ fn normalize_units_nullable(
899
+ &self,
900
+ left: Option<(Decimal, Option<String>)>,
901
+ right: Option<(Decimal, Option<String>)>,
902
+ ) -> Result<Option<(Decimal, Decimal)>, String> {
903
+ let (left, right) = match (left, right) {
904
+ (Some(l), Some(r)) => (l, r),
905
+ _ => return Ok(None),
906
+ };
907
+
908
+ match (left.1.clone(), right.1.clone()) {
909
+ (Some(l_unit), Some(r_unit)) => {
910
+ let registry = get_default_registry();
911
+ let registry = registry
912
+ .read()
913
+ .map_err(|e| format!("Failed to lock unit registry: {}", e))?;
914
+ if l_unit == r_unit {
915
+ Ok(Some((left.0, right.0)))
916
+ } else {
917
+ let from = registry.get_unit(&r_unit);
918
+ let to = registry.get_unit(&l_unit);
919
+ if let (Ok(from), Ok(to)) = (from, to) {
920
+ match registry.convert(right.0, from, to) {
921
+ Ok(converted) => Ok(Some((left.0, converted))),
922
+ Err(_) => Ok(None),
923
+ }
924
+ } else {
925
+ Ok(None)
926
+ }
927
+ }
928
+ }
929
+ (None, None) => Ok(Some((left.0, right.0))),
930
+ _ => Ok(None),
931
+ }
932
+ }
933
+
934
+ fn get_string_value(&self, expr: &Expression, graph: &Graph) -> Result<String, String> {
935
+ let v = self
936
+ .get_literal_value(expr)
937
+ .or_else(|_| Self::get_runtime_value(expr, graph))?;
938
+ v.as_str()
939
+ .map(|s| s.to_string())
940
+ .ok_or_else(|| "Expected string value".to_string())
941
+ }
942
+
943
+ fn get_runtime_value(expr: &Expression, graph: &Graph) -> Result<serde_json::Value, String> {
944
+ match expr {
945
+ Expression::Literal(v) => Ok(v.clone()),
946
+ Expression::MemberAccess { object, member } => {
947
+ // Try resolving as entity
948
+ if let Some(id) = graph.find_entity_by_name(object) {
949
+ if let Some(entity) = graph.get_entity(&id) {
950
+ // Special known members
951
+ if member == "id" {
952
+ return Ok(serde_json::json!(entity.id().to_string()));
953
+ } else if member == "name" {
954
+ return Ok(serde_json::json!(entity.name()));
955
+ } else if member == "namespace" {
956
+ return Ok(serde_json::json!(entity.namespace()));
957
+ }
958
+ if let Some(val) = entity.get_attribute(member) {
959
+ if val.is_null() {
960
+ log::debug!(
961
+ "Entity '{}' member '{}' present but NULL; returning Null",
962
+ object,
963
+ member
964
+ );
965
+ }
966
+ return Ok(val.clone());
967
+ }
968
+ log::debug!(
969
+ "Entity '{}' found but member '{}' missing; returning Null",
970
+ object,
971
+ member
972
+ );
973
+ return Ok(serde_json::Value::Null);
974
+ } else {
975
+ log::debug!(
976
+ "Entity lookup for '{}' returned id {} but entity missing; returning Null",
977
+ object, id
978
+ );
979
+ }
980
+ } else {
981
+ log::debug!(
982
+ "Entity '{}' not found while resolving member '{}'; continuing lookup",
983
+ object,
984
+ member
985
+ );
986
+ }
987
+
988
+ if let Some(id) = graph.find_resource_by_name(object) {
989
+ if let Some(resource) = graph.get_resource(&id) {
990
+ if member == "id" {
991
+ return Ok(serde_json::json!(resource.id().to_string()));
992
+ } else if member == "name" {
993
+ return Ok(serde_json::json!(resource.name()));
994
+ } else if member == "unit" {
995
+ return Ok(serde_json::json!(resource.unit()));
996
+ }
997
+ if let Some(val) = resource.get_attribute(member) {
998
+ if val.is_null() {
999
+ log::debug!(
1000
+ "Resource '{}' member '{}' present but NULL; returning Null",
1001
+ object,
1002
+ member
1003
+ );
1004
+ }
1005
+ return Ok(val.clone());
1006
+ }
1007
+ log::debug!(
1008
+ "Resource '{}' found but member '{}' missing; returning Null",
1009
+ object,
1010
+ member
1011
+ );
1012
+ return Ok(serde_json::Value::Null);
1013
+ } else {
1014
+ log::debug!(
1015
+ "Resource lookup for '{}' returned id {} but resource missing; returning Null",
1016
+ object, id
1017
+ );
1018
+ }
1019
+ } else {
1020
+ log::debug!(
1021
+ "Resource '{}' not found while resolving member '{}'; returning Null",
1022
+ object,
1023
+ member
1024
+ );
1025
+ }
1026
+
1027
+ // Not found: return Null to indicate unknown member
1028
+ log::debug!(
1029
+ "Member access '{}.{}' did not resolve to entity or resource; returning Null",
1030
+ object,
1031
+ member
1032
+ );
1033
+ Ok(serde_json::Value::Null)
1034
+ }
1035
+ Expression::Aggregation {
1036
+ function,
1037
+ collection,
1038
+ field,
1039
+ filter,
1040
+ } => {
1041
+ let v =
1042
+ Expression::evaluate_aggregation(function, collection, field, filter, graph)?;
1043
+ Ok(v)
1044
+ }
1045
+ Expression::AggregationComprehension {
1046
+ function,
1047
+ variable,
1048
+ collection,
1049
+ window,
1050
+ predicate,
1051
+ projection,
1052
+ target_unit,
1053
+ } => {
1054
+ let v = Expression::evaluate_aggregation_comprehension(
1055
+ function,
1056
+ variable,
1057
+ collection,
1058
+ window,
1059
+ predicate,
1060
+ projection,
1061
+ target_unit.as_deref(),
1062
+ graph,
1063
+ )?;
1064
+ Ok(v)
1065
+ }
1066
+ Expression::Cast {
1067
+ operand,
1068
+ target_type,
1069
+ } => {
1070
+ let val = Self::get_runtime_value(operand, graph)?;
1071
+ let (value_dec, source_unit) = Self::parse_numeric_with_unit_value(&val)
1072
+ .map_err(|e| format!("Invalid cast operand: {}", e))?
1073
+ .ok_or_else(|| {
1074
+ format!("Cannot cast non-numeric value {} to {}", val, target_type)
1075
+ })?;
1076
+
1077
+ let registry = get_default_registry();
1078
+ let registry = registry
1079
+ .read()
1080
+ .map_err(|e| format!("Failed to lock unit registry: {}", e))?;
1081
+ let target_unit = registry
1082
+ .get_unit(target_type)
1083
+ .map_err(|e| format!("Unknown target unit '{}': {}", target_type, e))?;
1084
+
1085
+ let converted_value = if let Some(from_unit_symbol) = source_unit {
1086
+ let from_unit = registry
1087
+ .get_unit(&from_unit_symbol)
1088
+ .map_err(|e| format!("Unknown unit '{}': {}", from_unit_symbol, e))?;
1089
+ if from_unit.dimension() != target_unit.dimension() {
1090
+ return Err(format!(
1091
+ "Cannot cast from '{}' ({:?}) to '{}' ({:?})",
1092
+ from_unit_symbol,
1093
+ from_unit.dimension(),
1094
+ target_type,
1095
+ target_unit.dimension()
1096
+ ));
1097
+ }
1098
+ registry
1099
+ .convert(value_dec, from_unit, target_unit)
1100
+ .map_err(|e| format!("Unit conversion failed: {}", e))?
1101
+ } else {
1102
+ value_dec
1103
+ };
1104
+
1105
+ Ok(serde_json::json!({
1106
+ "__quantity_value": converted_value.to_string(),
1107
+ "__quantity_unit": target_type
1108
+ }))
1109
+ }
1110
+ Expression::QuantityLiteral { value, unit } => Ok(
1111
+ serde_json::json!({"__quantity_value": value.to_string(), "__quantity_unit": unit}),
1112
+ ),
1113
+ Expression::TimeLiteral(timestamp) => Ok(serde_json::json!(timestamp)),
1114
+ Expression::IntervalLiteral { start, end } => {
1115
+ Ok(serde_json::json!({"__interval_start": start, "__interval_end": end}))
1116
+ }
1117
+ _ => Err(
1118
+ "Expected a runtime-resolvable expression (literal, member access, or aggregation)"
1119
+ .to_string(),
1120
+ ),
1121
+ }
1122
+ }
1123
+ }
1124
+
1125
+ impl PolicyModality {
1126
+ pub fn to_severity(&self) -> Severity {
1127
+ match self {
1128
+ Self::Obligation => Severity::Error,
1129
+ Self::Prohibition => Severity::Error,
1130
+ Self::Permission => Severity::Info,
1131
+ }
1132
+ }
1133
+ }
1134
+
1135
+ impl EvaluationResult {
1136
+ pub fn has_errors(&self) -> bool {
1137
+ self.violations
1138
+ .iter()
1139
+ .any(|v| v.severity == Severity::Error)
1140
+ }
1141
+
1142
+ pub fn error_count(&self) -> usize {
1143
+ self.violations
1144
+ .iter()
1145
+ .filter(|v| v.severity == Severity::Error)
1146
+ .count()
1147
+ }
1148
+ }