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,1476 @@
1
+ use crate::graph::Graph;
2
+ use crate::parser::ast::TargetFormat;
3
+ use crate::projection::{find_projection_override, ProjectionRegistry};
4
+ use percent_encoding::{percent_decode_str, utf8_percent_encode, AsciiSet, CONTROLS};
5
+ use serde::{Deserialize, Serialize};
6
+ use std::str::FromStr;
7
+
8
+ #[derive(Debug, Clone)]
9
+ pub enum KgError {
10
+ SerializationError(String),
11
+ UnsupportedFormat(String),
12
+ }
13
+
14
+ impl std::fmt::Display for KgError {
15
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
16
+ match self {
17
+ KgError::SerializationError(msg) => {
18
+ write!(f, "Knowledge graph serialization error: {}", msg)
19
+ }
20
+ KgError::UnsupportedFormat(fmt) => write!(f, "Unsupported format: {}", fmt),
21
+ }
22
+ }
23
+ }
24
+
25
+ impl std::error::Error for KgError {}
26
+
27
+ #[derive(Debug, Clone, Serialize, Deserialize)]
28
+ pub struct Triple {
29
+ pub subject: String,
30
+ pub predicate: String,
31
+ pub object: String,
32
+ }
33
+
34
+ #[derive(Debug, Clone, Serialize, Deserialize)]
35
+ pub struct ShaclShape {
36
+ pub target_class: String,
37
+ pub properties: Vec<ShaclProperty>,
38
+ }
39
+
40
+ #[derive(Debug, Clone, Serialize, Deserialize)]
41
+ pub struct ShaclProperty {
42
+ pub path: String,
43
+ pub datatype: Option<String>,
44
+ pub min_count: Option<u32>,
45
+ pub max_count: Option<u32>,
46
+ pub min_exclusive: Option<String>,
47
+ }
48
+
49
+ #[derive(Debug, Clone)]
50
+ pub struct KnowledgeGraph {
51
+ pub triples: Vec<Triple>,
52
+ pub shapes: Vec<ShaclShape>,
53
+ }
54
+
55
+ const URI_ENCODE_SET: &AsciiSet = &CONTROLS
56
+ .add(b' ')
57
+ .add(b':')
58
+ .add(b'/')
59
+ .add(b'#')
60
+ .add(b'?')
61
+ .add(b'&')
62
+ .add(b'=')
63
+ .add(b'+')
64
+ .add(b'$')
65
+ .add(b',')
66
+ .add(b'@')
67
+ .add(b';');
68
+
69
+ fn tokenize_triple_line(line: &str) -> Vec<String> {
70
+ let mut tokens = Vec::new();
71
+ let mut buffer = String::new();
72
+ let mut in_literal = false;
73
+ let mut escape = false;
74
+
75
+ for c in line.chars() {
76
+ if in_literal {
77
+ buffer.push(c);
78
+ if escape {
79
+ escape = false;
80
+ } else if c == '\\' {
81
+ escape = true;
82
+ } else if c == '"' {
83
+ in_literal = false;
84
+ }
85
+ continue;
86
+ }
87
+
88
+ match c {
89
+ '"' => {
90
+ in_literal = true;
91
+ buffer.push(c);
92
+ }
93
+ c if c.is_whitespace() => {
94
+ if !buffer.is_empty() {
95
+ tokens.push(buffer.clone());
96
+ buffer.clear();
97
+ }
98
+ }
99
+ _ => {
100
+ buffer.push(c);
101
+ }
102
+ }
103
+ }
104
+
105
+ if !buffer.is_empty() {
106
+ tokens.push(buffer);
107
+ }
108
+
109
+ tokens
110
+ }
111
+
112
+ fn extract_local_name(token: &str) -> String {
113
+ let trimmed = token.trim();
114
+ let stripped = trimmed.trim_matches(|c| c == '<' || c == '>');
115
+ stripped
116
+ .rsplit(|c| ['#', ':'].contains(&c))
117
+ .next()
118
+ .unwrap_or(stripped)
119
+ .to_string()
120
+ }
121
+
122
+ fn extract_literal_value(token: &str) -> String {
123
+ let trimmed = token.trim();
124
+ if let Some(stripped) = trimmed.strip_prefix('"') {
125
+ if let Some(end_quote) = stripped.find('"') {
126
+ return stripped[..end_quote].to_string();
127
+ }
128
+ return stripped.trim_end_matches('"').to_string();
129
+ }
130
+
131
+ if let Some(idx) = trimmed.find("^^") {
132
+ return trimmed[..idx].trim().to_string();
133
+ }
134
+ trimmed.to_string()
135
+ }
136
+
137
+ impl KnowledgeGraph {
138
+ pub fn new() -> Self {
139
+ Self {
140
+ triples: Vec::new(),
141
+ shapes: Vec::new(),
142
+ }
143
+ }
144
+
145
+ pub fn from_graph(graph: &Graph) -> Result<Self, KgError> {
146
+ let mut kg = Self::new();
147
+
148
+ let registry = ProjectionRegistry::new(graph);
149
+ let projections = registry.find_projections_for_target(&TargetFormat::Kg);
150
+ let projection = projections.first().copied();
151
+
152
+ for entity in graph.all_entities() {
153
+ let mut rdf_class = "sea:Entity".to_string();
154
+ let mut prop_map = std::collections::HashMap::new();
155
+
156
+ if let Some(proj) = projection {
157
+ if let Some(rule) = find_projection_override(proj, "Entity", entity.name()) {
158
+ if let Some(cls) = rule.fields.get("rdf_class").and_then(|v| v.as_str()) {
159
+ if Self::is_valid_rdf_term(cls) {
160
+ rdf_class = cls.to_string();
161
+ } else {
162
+ eprintln!("Warning: Invalid RDF term for rdf_class, skipping: {}", cls);
163
+ }
164
+ }
165
+ if let Some(props) = rule.fields.get("properties").and_then(|v| v.as_object()) {
166
+ for (k, v) in props {
167
+ if let Some(v_str) = v.as_str() {
168
+ if Self::is_valid_rdf_term(v_str) {
169
+ prop_map.insert(k.clone(), v_str.to_string());
170
+ } else {
171
+ eprintln!(
172
+ "Warning: Invalid RDF term for property '{}', skipping: {}",
173
+ k, v_str
174
+ );
175
+ }
176
+ }
177
+ }
178
+ }
179
+ }
180
+ }
181
+
182
+ kg.triples.push(Triple {
183
+ subject: format!("sea:{}", Self::uri_encode(entity.name())),
184
+ predicate: "rdf:type".to_string(),
185
+ object: rdf_class,
186
+ });
187
+
188
+ let label_pred = prop_map
189
+ .get("name")
190
+ .cloned()
191
+ .unwrap_or_else(|| "rdfs:label".to_string());
192
+ kg.triples.push(Triple {
193
+ subject: format!("sea:{}", Self::uri_encode(entity.name())),
194
+ predicate: label_pred,
195
+ object: format!("\"{}\"", Self::escape_turtle_literal(entity.name())),
196
+ });
197
+
198
+ let ns_pred = prop_map
199
+ .get("namespace")
200
+ .cloned()
201
+ .unwrap_or_else(|| "sea:namespace".to_string());
202
+ kg.triples.push(Triple {
203
+ subject: format!("sea:{}", Self::uri_encode(entity.name())),
204
+ predicate: ns_pred,
205
+ object: format!("\"{}\"", Self::escape_turtle_literal(entity.namespace())),
206
+ });
207
+ }
208
+
209
+ for role in graph.all_roles() {
210
+ kg.triples.push(Triple {
211
+ subject: format!("sea:{}", Self::uri_encode(role.name())),
212
+ predicate: "rdf:type".to_string(),
213
+ object: "sea:Role".to_string(),
214
+ });
215
+
216
+ kg.triples.push(Triple {
217
+ subject: format!("sea:{}", Self::uri_encode(role.name())),
218
+ predicate: "rdfs:label".to_string(),
219
+ object: format!("\"{}\"", Self::escape_turtle_literal(role.name())),
220
+ });
221
+
222
+ kg.triples.push(Triple {
223
+ subject: format!("sea:{}", Self::uri_encode(role.name())),
224
+ predicate: "sea:namespace".to_string(),
225
+ object: format!("\"{}\"", Self::escape_turtle_literal(role.namespace())),
226
+ });
227
+ }
228
+
229
+ for resource in graph.all_resources() {
230
+ kg.triples.push(Triple {
231
+ subject: format!("sea:{}", Self::uri_encode(resource.name())),
232
+ predicate: "rdf:type".to_string(),
233
+ object: "sea:Resource".to_string(),
234
+ });
235
+
236
+ kg.triples.push(Triple {
237
+ subject: format!("sea:{}", Self::uri_encode(resource.name())),
238
+ predicate: "rdfs:label".to_string(),
239
+ object: format!("\"{}\"", Self::escape_turtle_literal(resource.name())),
240
+ });
241
+
242
+ kg.triples.push(Triple {
243
+ subject: format!("sea:{}", Self::uri_encode(resource.name())),
244
+ predicate: "sea:unit".to_string(),
245
+ object: format!(
246
+ "\"{}\"",
247
+ Self::escape_turtle_literal(&resource.unit().to_string())
248
+ ),
249
+ });
250
+ }
251
+
252
+ for pattern in graph.all_patterns() {
253
+ let subject = format!("sea:pattern_{}", Self::uri_encode(pattern.name()));
254
+
255
+ kg.triples.push(Triple {
256
+ subject: subject.clone(),
257
+ predicate: "rdf:type".to_string(),
258
+ object: "sea:Pattern".to_string(),
259
+ });
260
+
261
+ kg.triples.push(Triple {
262
+ subject: subject.clone(),
263
+ predicate: "rdfs:label".to_string(),
264
+ object: format!("\"{}\"", Self::escape_turtle_literal(pattern.name())),
265
+ });
266
+
267
+ kg.triples.push(Triple {
268
+ subject: subject.clone(),
269
+ predicate: "sea:namespace".to_string(),
270
+ object: format!("\"{}\"", Self::escape_turtle_literal(pattern.namespace())),
271
+ });
272
+
273
+ kg.triples.push(Triple {
274
+ subject,
275
+ predicate: "sea:regex".to_string(),
276
+ object: format!("\"{}\"", Self::escape_turtle_literal(pattern.regex())),
277
+ });
278
+ }
279
+
280
+ for relation in graph.all_relations() {
281
+ let relation_subject = format!("sea:{}", Self::uri_encode(relation.name()));
282
+
283
+ kg.triples.push(Triple {
284
+ subject: relation_subject.clone(),
285
+ predicate: "rdf:type".to_string(),
286
+ object: "sea:Relation".to_string(),
287
+ });
288
+
289
+ kg.triples.push(Triple {
290
+ subject: relation_subject.clone(),
291
+ predicate: "rdfs:label".to_string(),
292
+ object: format!("\"{}\"", Self::escape_turtle_literal(relation.name())),
293
+ });
294
+
295
+ if let Some(subject_role) = graph.get_role(relation.subject_role()) {
296
+ kg.triples.push(Triple {
297
+ subject: relation_subject.clone(),
298
+ predicate: "sea:subjectRole".to_string(),
299
+ object: format!("sea:{}", Self::uri_encode(subject_role.name())),
300
+ });
301
+ }
302
+
303
+ if let Some(object_role) = graph.get_role(relation.object_role()) {
304
+ kg.triples.push(Triple {
305
+ subject: relation_subject.clone(),
306
+ predicate: "sea:objectRole".to_string(),
307
+ object: format!("sea:{}", Self::uri_encode(object_role.name())),
308
+ });
309
+ }
310
+
311
+ kg.triples.push(Triple {
312
+ subject: relation_subject.clone(),
313
+ predicate: "sea:predicate".to_string(),
314
+ object: format!("\"{}\"", Self::escape_turtle_literal(relation.predicate())),
315
+ });
316
+
317
+ if let Some(flow_id) = relation.via_flow() {
318
+ if let Some(resource) = graph.get_resource(flow_id) {
319
+ kg.triples.push(Triple {
320
+ subject: relation_subject.clone(),
321
+ predicate: "sea:via".to_string(),
322
+ object: format!("sea:{}", Self::uri_encode(resource.name())),
323
+ });
324
+ } else {
325
+ kg.triples.push(Triple {
326
+ subject: relation_subject.clone(),
327
+ predicate: "sea:via".to_string(),
328
+ object: format!("\"{}\"", flow_id),
329
+ });
330
+ }
331
+ }
332
+ }
333
+
334
+ for flow in graph.all_flows() {
335
+ let flow_id = format!("sea:flow_{}", Self::uri_encode(&flow.id().to_string()));
336
+
337
+ kg.triples.push(Triple {
338
+ subject: flow_id.clone(),
339
+ predicate: "rdf:type".to_string(),
340
+ object: "sea:Flow".to_string(),
341
+ });
342
+
343
+ if let Some(from_entity) = graph.get_entity(flow.from_id()) {
344
+ kg.triples.push(Triple {
345
+ subject: flow_id.clone(),
346
+ predicate: "sea:from".to_string(),
347
+ object: format!("sea:{}", Self::uri_encode(from_entity.name())),
348
+ });
349
+ }
350
+
351
+ if let Some(to_entity) = graph.get_entity(flow.to_id()) {
352
+ kg.triples.push(Triple {
353
+ subject: flow_id.clone(),
354
+ predicate: "sea:to".to_string(),
355
+ object: format!("sea:{}", Self::uri_encode(to_entity.name())),
356
+ });
357
+ }
358
+
359
+ if let Some(resource) = graph.get_resource(flow.resource_id()) {
360
+ kg.triples.push(Triple {
361
+ subject: flow_id.clone(),
362
+ predicate: "sea:hasResource".to_string(),
363
+ object: format!("sea:{}", Self::uri_encode(resource.name())),
364
+ });
365
+ }
366
+
367
+ // Validate that the quantity is a safe decimal string for Turtle format
368
+ let quantity_str = flow.quantity().to_string();
369
+ Self::validate_turtle_decimal(&quantity_str).map_err(|e| {
370
+ KgError::SerializationError(format!("Invalid quantity format: {}", e))
371
+ })?;
372
+
373
+ kg.triples.push(Triple {
374
+ subject: flow_id.clone(),
375
+ predicate: "sea:quantity".to_string(),
376
+ object: format!("\"{}\"^^xsd:decimal", quantity_str),
377
+ });
378
+ }
379
+
380
+ kg.shapes.push(ShaclShape {
381
+ target_class: "sea:Flow".to_string(),
382
+ properties: vec![
383
+ ShaclProperty {
384
+ path: "sea:quantity".to_string(),
385
+ datatype: Some("xsd:decimal".to_string()),
386
+ min_count: None,
387
+ max_count: None,
388
+ min_exclusive: Some("0".to_string()),
389
+ },
390
+ ShaclProperty {
391
+ path: "sea:hasResource".to_string(),
392
+ datatype: None,
393
+ min_count: Some(1),
394
+ max_count: Some(1),
395
+ min_exclusive: None,
396
+ },
397
+ ShaclProperty {
398
+ path: "sea:from".to_string(),
399
+ datatype: None,
400
+ min_count: Some(1),
401
+ max_count: Some(1),
402
+ min_exclusive: None,
403
+ },
404
+ ShaclProperty {
405
+ path: "sea:to".to_string(),
406
+ datatype: None,
407
+ min_count: Some(1),
408
+ max_count: Some(1),
409
+ min_exclusive: None,
410
+ },
411
+ ],
412
+ });
413
+
414
+ kg.shapes.push(ShaclShape {
415
+ target_class: "sea:Entity".to_string(),
416
+ properties: vec![ShaclProperty {
417
+ path: "rdfs:label".to_string(),
418
+ datatype: Some("xsd:string".to_string()),
419
+ min_count: Some(1),
420
+ max_count: Some(1),
421
+ min_exclusive: None,
422
+ }],
423
+ });
424
+
425
+ Ok(kg)
426
+ }
427
+
428
+ pub fn to_turtle(&self) -> String {
429
+ let mut turtle = String::new();
430
+
431
+ turtle.push_str("@prefix sea: <http://domainforge.ai/sea#> .\n");
432
+ turtle.push_str("@prefix owl: <http://www.w3.org/2002/07/owl#> .\n");
433
+ turtle.push_str("@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .\n");
434
+ turtle.push_str("@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .\n");
435
+ turtle.push_str("@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .\n");
436
+ turtle.push_str("@prefix sh: <http://www.w3.org/ns/shacl#> .\n");
437
+ turtle.push('\n');
438
+
439
+ turtle.push_str("# Ontology\n");
440
+ turtle.push_str("sea:Entity a owl:Class ;\n");
441
+ turtle.push_str(" rdfs:label \"Entity\" ;\n");
442
+ turtle.push_str(
443
+ " rdfs:comment \"Business actor, location, or organizational unit\" .\n\n",
444
+ );
445
+
446
+ turtle.push_str("sea:Resource a owl:Class ;\n");
447
+ turtle.push_str(" rdfs:label \"Resource\" ;\n");
448
+ turtle.push_str(" rdfs:comment \"Quantifiable subject of value\" .\n\n");
449
+
450
+ turtle.push_str("sea:Flow a owl:Class ;\n");
451
+ turtle.push_str(" rdfs:label \"Flow\" ;\n");
452
+ turtle.push_str(" rdfs:comment \"Transfer of resource between entities\" .\n\n");
453
+
454
+ turtle.push_str("sea:hasResource a owl:ObjectProperty ;\n");
455
+ turtle.push_str(" rdfs:domain sea:Flow ;\n");
456
+ turtle.push_str(" rdfs:range sea:Resource .\n\n");
457
+
458
+ turtle.push_str("sea:from a owl:ObjectProperty ;\n");
459
+ turtle.push_str(" rdfs:domain sea:Flow ;\n");
460
+ turtle.push_str(" rdfs:range sea:Entity .\n\n");
461
+
462
+ turtle.push_str("sea:to a owl:ObjectProperty ;\n");
463
+ turtle.push_str(" rdfs:domain sea:Flow ;\n");
464
+ turtle.push_str(" rdfs:range sea:Entity .\n\n");
465
+
466
+ turtle.push_str("# Instances\n");
467
+ for triple in &self.triples {
468
+ turtle.push_str(&format!(
469
+ "{} {} {} .\n",
470
+ triple.subject, triple.predicate, triple.object
471
+ ));
472
+ }
473
+
474
+ turtle.push_str("\n# SHACL Shapes\n");
475
+ for shape in &self.shapes {
476
+ turtle.push_str(&format!(
477
+ "sea:{}Shape a sh:NodeShape ;\n",
478
+ shape.target_class.replace("sea:", "")
479
+ ));
480
+ turtle.push_str(&format!(" sh:targetClass {} ;\n", shape.target_class));
481
+
482
+ for (i, prop) in shape.properties.iter().enumerate() {
483
+ turtle.push_str(" sh:property [\n");
484
+ turtle.push_str(&format!(" sh:path {} ;\n", prop.path));
485
+
486
+ if let Some(dt) = &prop.datatype {
487
+ turtle.push_str(&format!(" sh:datatype {} ;\n", dt));
488
+ }
489
+ if let Some(min) = prop.min_count {
490
+ turtle.push_str(&format!(" sh:minCount {} ;\n", min));
491
+ }
492
+ if let Some(max) = prop.max_count {
493
+ turtle.push_str(&format!(" sh:maxCount {} ;\n", max));
494
+ }
495
+ if let Some(min_ex) = &prop.min_exclusive {
496
+ turtle.push_str(&format!(" sh:minExclusive {} ;\n", min_ex));
497
+ }
498
+
499
+ if i < shape.properties.len() - 1 {
500
+ turtle.push_str(" ] ;\n");
501
+ } else {
502
+ turtle.push_str(" ] .\n");
503
+ }
504
+ }
505
+ turtle.push('\n');
506
+ }
507
+
508
+ turtle
509
+ }
510
+
511
+ /// Parse a simple Turtle snippet into a KnowledgeGraph. This is a best-effort parser
512
+ /// expecting the exact triple format generated by `to_turtle()` in this crate.
513
+ #[allow(clippy::while_let_on_iterator)]
514
+ pub fn from_turtle(turtle: &str) -> Result<Self, KgError> {
515
+ let mut kg = Self::new();
516
+ for line in turtle.lines() {
517
+ let trimmed = line.trim();
518
+ if trimmed.is_empty() || trimmed.starts_with('@') || trimmed.starts_with('#') {
519
+ continue;
520
+ }
521
+ let triple_line = if let Some(stripped) = trimmed.strip_suffix('.') {
522
+ stripped.trim_end()
523
+ } else {
524
+ trimmed
525
+ };
526
+ let tokens = tokenize_triple_line(triple_line);
527
+ if tokens.len() != 3 {
528
+ continue;
529
+ }
530
+ let subject = &tokens[0];
531
+ let predicate = &tokens[1];
532
+ let object = &tokens[2];
533
+ let norm_s = Self::shorten_token(subject);
534
+ let norm_p = Self::shorten_token(predicate);
535
+ let norm_o = Self::shorten_token(object);
536
+ kg.triples.push(Triple {
537
+ subject: norm_s,
538
+ predicate: norm_p,
539
+ object: norm_o,
540
+ });
541
+ }
542
+ // parse shapes: look for NodeShape blocks (start with 'sea:SomethingShape a sh:NodeShape')
543
+ // We scan lines to find blocks terminating with '.' and containing 'sh:property' entries
544
+ let mut lines_iter = turtle.lines();
545
+ while let Some(line) = lines_iter.next() {
546
+ let l = line.trim();
547
+ if l.contains("a sh:NodeShape") {
548
+ // Collect shape block until '.' terminator
549
+ let mut block = l.to_string();
550
+ if !l.ends_with('.') {
551
+ while let Some(next_line) = lines_iter.next() {
552
+ block.push(' ');
553
+ block.push_str(next_line.trim());
554
+ if next_line.trim().ends_with('.') {
555
+ break;
556
+ }
557
+ }
558
+ }
559
+
560
+ // Normalize full URIs into prefixes for easier parsing
561
+ let normalized_block = block
562
+ .replace("http://www.w3.org/ns/shacl#", "sh:")
563
+ .replace("http://www.w3.org/1999/02/22-rdf-syntax-ns#", "rdf:")
564
+ .replace("http://www.w3.org/2000/01/rdf-schema#", "rdfs:")
565
+ .replace("http://www.w3.org/2001/XMLSchema#", "xsd:")
566
+ .replace("http://domainforge.ai/sea#", "sea:");
567
+
568
+ // Extract target class
569
+ let target_class = if let Some(pos) = normalized_block.find("sh:targetClass") {
570
+ let rest = &normalized_block[pos + "sh:targetClass".len()..];
571
+ let tok = rest
572
+ .split_whitespace()
573
+ .next()
574
+ .unwrap_or("")
575
+ .trim()
576
+ .trim_end_matches(';')
577
+ .to_string();
578
+ tok
579
+ } else {
580
+ continue; // ignore shapes without targetClass
581
+ };
582
+
583
+ let mut shape = ShaclShape {
584
+ target_class,
585
+ properties: Vec::new(),
586
+ };
587
+
588
+ // Find property blocks: 'sh:property [ ... ]' occurrences
589
+ let mut start_idx = 0;
590
+ while let Some(idx) = normalized_block[start_idx..].find("sh:property") {
591
+ let local_idx = start_idx + idx;
592
+ // Find the opening '[' and closing ']' for the property
593
+ if let Some(open_br) = normalized_block[local_idx..].find('[') {
594
+ let open_idx = local_idx + open_br + 1;
595
+ if let Some(close_br) = normalized_block[open_idx..].find(']') {
596
+ let close_idx = open_idx + close_br;
597
+ let prop_block = &normalized_block[open_idx..close_idx];
598
+ // Parse property attributes
599
+ let mut path = String::new();
600
+ let mut datatype: Option<String> = None;
601
+ let mut min_count: Option<u32> = None;
602
+ let mut max_count: Option<u32> = None;
603
+ let mut min_exclusive: Option<String> = None;
604
+
605
+ for tok in prop_block.split(';') {
606
+ let tok = tok.trim();
607
+ if tok.is_empty() {
608
+ continue;
609
+ }
610
+ if let Some(s) = tok.strip_prefix("sh:path") {
611
+ path = s.trim().to_string();
612
+ } else if let Some(s) = tok.strip_prefix("sh:datatype") {
613
+ datatype = Some(s.trim().to_string());
614
+ } else if let Some(s) = tok.strip_prefix("sh:minCount") {
615
+ let val = s.trim();
616
+ if let Ok(n) = val.parse::<u32>() {
617
+ min_count = Some(n);
618
+ }
619
+ } else if let Some(s) = tok.strip_prefix("sh:maxCount") {
620
+ let val = s.trim();
621
+ if let Ok(n) = val.parse::<u32>() {
622
+ max_count = Some(n);
623
+ }
624
+ } else if let Some(s) = tok.strip_prefix("sh:minExclusive") {
625
+ let val = s.trim();
626
+ min_exclusive = Some(val.to_string());
627
+ }
628
+ }
629
+
630
+ if !path.is_empty() {
631
+ shape.properties.push(ShaclProperty {
632
+ path,
633
+ datatype,
634
+ min_count,
635
+ max_count,
636
+ min_exclusive,
637
+ });
638
+ }
639
+ start_idx = close_idx + 1;
640
+ continue;
641
+ }
642
+ }
643
+ start_idx = local_idx + 1;
644
+ }
645
+
646
+ if !shape.properties.is_empty() {
647
+ kg.shapes.push(shape);
648
+ }
649
+ }
650
+ }
651
+ Ok(kg)
652
+ }
653
+
654
+ /// Convert the knowledge graph back into a Graph by interpreting triples exported by `to_turtle`.
655
+ pub fn to_graph(&self) -> Result<crate::graph::Graph, KgError> {
656
+ use crate::graph::Graph;
657
+ use crate::primitives::{Entity, Flow, Resource};
658
+ use crate::units::unit_from_string;
659
+ use rust_decimal::Decimal;
660
+
661
+ let mut graph = Graph::new();
662
+
663
+ let mut namespace_map: std::collections::HashMap<String, String> =
664
+ std::collections::HashMap::new();
665
+ for t in &self.triples {
666
+ if t.predicate == "sea:namespace" {
667
+ let name_encoded = t.subject.split(':').nth(1).unwrap_or(&t.subject);
668
+ let name = percent_decode_str(name_encoded)
669
+ .decode_utf8_lossy()
670
+ .to_string();
671
+ let ns = extract_literal_value(&t.object);
672
+ namespace_map.insert(name, ns);
673
+ }
674
+ }
675
+
676
+ let mut unit_map: std::collections::HashMap<String, String> =
677
+ std::collections::HashMap::new();
678
+ for t in &self.triples {
679
+ if t.predicate == "sea:unit" {
680
+ let name_encoded = t.subject.split(':').nth(1).unwrap_or(&t.subject);
681
+ let name = percent_decode_str(name_encoded)
682
+ .decode_utf8_lossy()
683
+ .to_string();
684
+ let unit_str = extract_literal_value(&t.object);
685
+ unit_map.insert(name, unit_str);
686
+ }
687
+ }
688
+
689
+ for t in &self.triples {
690
+ if t.predicate == "rdf:type" && t.object == "sea:Entity" {
691
+ let name_encoded = t.subject.split(':').nth(1).unwrap_or(&t.subject);
692
+ let name = percent_decode_str(name_encoded)
693
+ .decode_utf8_lossy()
694
+ .to_string();
695
+ let namespace = namespace_map
696
+ .get(&name)
697
+ .cloned()
698
+ .unwrap_or_else(|| "default".to_string());
699
+ let entity = Entity::new_with_namespace(name, namespace);
700
+ graph
701
+ .add_entity(entity)
702
+ .map_err(|e| KgError::SerializationError(e.to_string()))?;
703
+ }
704
+ if t.predicate == "rdf:type" && t.object == "sea:Resource" {
705
+ let name_encoded = t.subject.split(':').nth(1).unwrap_or(&t.subject);
706
+ let name = percent_decode_str(name_encoded)
707
+ .decode_utf8_lossy()
708
+ .to_string();
709
+ let namespace = namespace_map
710
+ .get(&name)
711
+ .cloned()
712
+ .unwrap_or_else(|| "default".to_string());
713
+ let unit =
714
+ unit_from_string(unit_map.get(&name).map(|s| s.as_str()).unwrap_or("units"));
715
+ let resource = Resource::new_with_namespace(name, unit, namespace);
716
+ graph
717
+ .add_resource(resource)
718
+ .map_err(|e| KgError::SerializationError(e.to_string()))?;
719
+ }
720
+ }
721
+
722
+ for t in &self.triples {
723
+ if t.predicate == "rdf:type" && t.object == "sea:Flow" {
724
+ let flow_subject = t.subject.clone();
725
+ let mut from: Option<String> = None;
726
+ let mut to: Option<String> = None;
727
+ let mut resource_name: Option<String> = None;
728
+ let mut quantity: Option<Decimal> = None;
729
+
730
+ for p in &self.triples {
731
+ if p.subject != flow_subject {
732
+ continue;
733
+ }
734
+ match p.predicate.as_str() {
735
+ "sea:from" => {
736
+ from = Some(extract_local_name(&p.object));
737
+ }
738
+ "sea:to" => {
739
+ to = Some(extract_local_name(&p.object));
740
+ }
741
+ "sea:hasResource" => {
742
+ resource_name = Some(extract_local_name(&p.object));
743
+ }
744
+ "sea:quantity" => {
745
+ let lexical = extract_literal_value(&p.object);
746
+ let parsed = Decimal::from_str(&lexical).map_err(|e| {
747
+ KgError::SerializationError(format!(
748
+ "Invalid quantity literal '{}': {}",
749
+ p.object, e
750
+ ))
751
+ })?;
752
+ quantity = Some(parsed);
753
+ }
754
+ _ => {}
755
+ }
756
+ }
757
+
758
+ if let (Some(from_name), Some(to_name), Some(res_name), Some(quantity_val)) =
759
+ (from, to, resource_name, quantity)
760
+ {
761
+ let from_decoded = percent_decode_str(&from_name)
762
+ .decode_utf8_lossy()
763
+ .to_string();
764
+ let to_decoded = percent_decode_str(&to_name).decode_utf8_lossy().to_string();
765
+ let res_decoded = percent_decode_str(&res_name)
766
+ .decode_utf8_lossy()
767
+ .to_string();
768
+
769
+ let from_id = graph.find_entity_by_name(&from_decoded).ok_or_else(|| {
770
+ KgError::SerializationError(format!("Unknown entity: {}", from_decoded))
771
+ })?;
772
+ let to_id = graph.find_entity_by_name(&to_decoded).ok_or_else(|| {
773
+ KgError::SerializationError(format!("Unknown entity: {}", to_decoded))
774
+ })?;
775
+ let res_id = graph.find_resource_by_name(&res_decoded).ok_or_else(|| {
776
+ KgError::SerializationError(format!("Unknown resource: {}", res_decoded))
777
+ })?;
778
+
779
+ let flow = Flow::new(res_id, from_id, to_id, quantity_val);
780
+ graph
781
+ .add_flow(flow)
782
+ .map_err(|e| KgError::SerializationError(e.to_string()))?;
783
+ }
784
+ }
785
+ }
786
+
787
+ Ok(graph)
788
+ }
789
+
790
+ pub fn validate_shacl(&self) -> Result<Vec<crate::policy::Violation>, KgError> {
791
+ use crate::policy::{Severity, Violation};
792
+
793
+ // If no shapes are defined, nothing to validate
794
+ if self.shapes.is_empty() {
795
+ return Ok(Vec::new());
796
+ }
797
+
798
+ let mut violations: Vec<Violation> = Vec::new();
799
+
800
+ // Helper: parse a Turtle literal (optionally typed) into its lexical form
801
+ // and optional datatype suffix (e.g. "\"0\"^^xsd:decimal" -> ("0", Some("xsd:decimal"))).
802
+ fn parse_literal_and_datatype(obj: &str) -> (String, Option<String>) {
803
+ let s = obj.trim();
804
+ if !s.starts_with('"') {
805
+ return (s.to_string(), None);
806
+ }
807
+
808
+ // Find the closing quote for the lexical form, respecting simple escapes.
809
+ let bytes = s.as_bytes();
810
+ let mut end_quote = None;
811
+ let mut i = 1;
812
+ while i < bytes.len() {
813
+ if bytes[i] == b'\\' {
814
+ // Skip escaped character
815
+ i += 2;
816
+ continue;
817
+ }
818
+ if bytes[i] == b'"' {
819
+ end_quote = Some(i);
820
+ break;
821
+ }
822
+ i += 1;
823
+ }
824
+
825
+ if let Some(end) = end_quote {
826
+ let lex = &s[1..end];
827
+ let rest = s[end + 1..].trim();
828
+ let dtype = rest.strip_prefix("^^").map(|s| s.trim().to_string());
829
+ (lex.to_string(), dtype)
830
+ } else {
831
+ // No matching closing quote; fall back to naive trimming
832
+ (s.trim_matches('"').to_string(), None)
833
+ }
834
+ }
835
+
836
+ // For each shape, find all subjects in triples typed as the target class
837
+ for shape in &self.shapes {
838
+ match shape.target_class.as_str() {
839
+ "sea:Flow" | "sea:Entity" | "sea:Resource" => {}
840
+ other => {
841
+ return Err(KgError::SerializationError(format!(
842
+ "Unsupported SHACL target class: {}",
843
+ other
844
+ )))
845
+ }
846
+ }
847
+
848
+ // collect all candidate subjects by rdf:type triples
849
+ let mut subjects: Vec<String> = Vec::new();
850
+ for t in &self.triples {
851
+ if t.predicate == "rdf:type" && t.object == shape.target_class {
852
+ subjects.push(t.subject.clone());
853
+ }
854
+ }
855
+
856
+ for subject in subjects {
857
+ for prop in &shape.properties {
858
+ // check min_count / max_count constraints
859
+ let count = self
860
+ .triples
861
+ .iter()
862
+ .filter(|tr| tr.subject == subject && tr.predicate == prop.path)
863
+ .count() as u32;
864
+
865
+ if let Some(min) = prop.min_count {
866
+ if count < min {
867
+ let msg = format!(
868
+ "SHACL violation: subject {} missing required property {} (min_count={} found={})",
869
+ subject, prop.path, min, count
870
+ );
871
+ violations.push(Violation::new(format!("SHACL:{}", shape.target_class), msg, Severity::Error).with_context(serde_json::json!({"subject": subject, "predicate": prop.path, "expected_min": min, "found": count})));
872
+ }
873
+ }
874
+
875
+ if let Some(max) = prop.max_count {
876
+ if count > max {
877
+ let msg = format!(
878
+ "SHACL violation: subject {} has {} occurrences of {} (max_count={} found={})",
879
+ subject, count, prop.path, max, count
880
+ );
881
+ violations.push(Violation::new(format!("SHACL:{}", shape.target_class), msg, Severity::Error).with_context(serde_json::json!({"subject": subject, "predicate": prop.path, "expected_max": max, "found": count})));
882
+ }
883
+ }
884
+
885
+ // datatype checks (only handle basic types like xsd:decimal and xsd:string)
886
+ if let Some(dt) = &prop.datatype {
887
+ for tr in self
888
+ .triples
889
+ .iter()
890
+ .filter(|tr| tr.subject == subject && tr.predicate == prop.path)
891
+ {
892
+ let obj = tr.object.trim();
893
+ let (_lex, dtype_opt) = parse_literal_and_datatype(obj);
894
+ // look for typed literal like "123"^^xsd:decimal
895
+ if let Some(dtype) = dtype_opt {
896
+ if &dtype != dt {
897
+ let msg = format!(
898
+ "SHACL violation: subject {} property {} expected datatype {} but found {}",
899
+ subject, prop.path, dt, dtype
900
+ );
901
+ violations.push(Violation::new(format!("SHACL:{}", shape.target_class), msg, Severity::Error).with_context(serde_json::json!({"subject": subject, "predicate": prop.path, "expected_type": dt, "found_type": dtype})));
902
+ }
903
+ } else if dt != "xsd:string" {
904
+ // untyped literal but expected typed -> violation
905
+ let msg = format!(
906
+ "SHACL violation: subject {} property {} expected datatype {} but found untyped literal {}",
907
+ subject, prop.path, dt, obj
908
+ );
909
+ violations.push(Violation::new(format!("SHACL:{}", shape.target_class), msg, Severity::Error).with_context(serde_json::json!({"subject": subject, "predicate": prop.path, "expected_type": dt, "found": obj})));
910
+ }
911
+ }
912
+ }
913
+
914
+ // minExclusive check (e.g. > 0) — interpreted for decimal numbers
915
+ if let Some(min_ex) = &prop.min_exclusive {
916
+ if prop.datatype.as_deref() == Some("xsd:decimal") {
917
+ let threshold =
918
+ rust_decimal::Decimal::from_str(min_ex).map_err(|e| {
919
+ KgError::SerializationError(format!(
920
+ "Invalid minExclusive threshold '{}': {}",
921
+ min_ex, e
922
+ ))
923
+ })?;
924
+ for tr in self
925
+ .triples
926
+ .iter()
927
+ .filter(|tr| tr.subject == subject && tr.predicate == prop.path)
928
+ {
929
+ let obj = tr.object.trim();
930
+ let lex = extract_literal_value(obj);
931
+ if let Ok(val) = rust_decimal::Decimal::from_str(&lex) {
932
+ if val <= threshold {
933
+ let msg = format!(
934
+ "SHACL violation: subject {} property {} must be > {} but found {}",
935
+ subject, prop.path, threshold, val
936
+ );
937
+ violations.push(Violation::new(format!("SHACL:{}", shape.target_class), msg, Severity::Error).with_context(serde_json::json!({"subject": subject, "predicate": prop.path, "threshold": threshold.to_string(), "found": val.to_string()})));
938
+ }
939
+ }
940
+ }
941
+ }
942
+ }
943
+ }
944
+ }
945
+ }
946
+
947
+ Ok(violations)
948
+ }
949
+
950
+ pub fn to_rdf_xml(&self) -> String {
951
+ let mut xml = String::new();
952
+
953
+ xml.push_str("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n");
954
+ xml.push_str("<rdf:RDF\n");
955
+ xml.push_str(" xmlns:rdf=\"http://www.w3.org/1999/02/22-rdf-syntax-ns#\"\n");
956
+ // Use project-local namespace for rdfs to preserve existing prefix mapping
957
+ // in tests and downstream tooling that expects the domainforge namespace.
958
+ xml.push_str(" xmlns:rdfs=\"http://www.w3.org/2000/01/rdf-schema#\"\n");
959
+ xml.push_str(" xmlns:owl=\"http://www.w3.org/2002/07/owl#\"\n");
960
+ xml.push_str(" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema#\"\n");
961
+ // Explicitly declare the xml namespace so XML processors (and tests using roxmltree)
962
+ // can resolve attributes like xml:lang correctly.
963
+ xml.push_str(" xmlns:xml=\"http://www.w3.org/XML/1998/namespace\"\n");
964
+ xml.push_str(" xmlns:sea=\"http://domainforge.ai/sea#\"\n");
965
+ xml.push_str(" xmlns:sh=\"http://www.w3.org/ns/shacl#\">\n\n");
966
+
967
+ for triple in &self.triples {
968
+ let subject = Self::clean_uri(&triple.subject);
969
+ // Keep the predicate as the original prefixed name for use as XML element
970
+ // (e.g. rdfs:label). Use the cleaned URI only for rdf:datatype or rdf:resource
971
+ // attributes where a full URI is required.
972
+ let predicate_name = triple.predicate.clone();
973
+ let object = &triple.object;
974
+
975
+ if object.starts_with('"') {
976
+ let (literal_value, suffix) = Self::parse_typed_literal(object);
977
+ let escaped_value = Self::escape_xml(&literal_value);
978
+
979
+ xml.push_str(&format!(" <rdf:Description rdf:about=\"{}\">\n", subject));
980
+
981
+ match suffix {
982
+ Some(TypedLiteralSuffix::Datatype(datatype)) => {
983
+ let datatype_uri = Self::clean_uri(&datatype);
984
+ xml.push_str(&format!(
985
+ " <{} rdf:datatype=\"{}\">{}</{}>\n",
986
+ predicate_name, datatype_uri, escaped_value, predicate_name
987
+ ));
988
+ }
989
+ Some(TypedLiteralSuffix::Language(lang)) => {
990
+ xml.push_str(&format!(
991
+ " <{} xml:lang=\"{}\">{}</{}>\n",
992
+ predicate_name, lang, escaped_value, predicate_name
993
+ ));
994
+ }
995
+ None => {
996
+ xml.push_str(&format!(
997
+ " <{}>{}</{}>\n",
998
+ predicate_name, escaped_value, predicate_name
999
+ ));
1000
+ }
1001
+ }
1002
+
1003
+ xml.push_str(" </rdf:Description>\n\n");
1004
+ } else {
1005
+ let cleaned_object = Self::clean_uri(object);
1006
+ xml.push_str(&format!(" <rdf:Description rdf:about=\"{}\">\n", subject));
1007
+ xml.push_str(&format!(
1008
+ " <{} rdf:resource=\"{}\"/>\n",
1009
+ predicate_name, cleaned_object
1010
+ ));
1011
+ xml.push_str(" </rdf:Description>\n\n");
1012
+ }
1013
+ }
1014
+ xml.push('\n');
1015
+ // Emit SHACL shapes (as RDF/XML)
1016
+ for shape in &self.shapes {
1017
+ xml.push_str(&Self::write_shacl_shapes_xml(shape));
1018
+ }
1019
+
1020
+ xml.push_str("</rdf:RDF>\n");
1021
+ xml
1022
+ }
1023
+
1024
+ fn write_shacl_shapes_xml(shape: &ShaclShape) -> String {
1025
+ let mut xml = String::new();
1026
+ let shape_name = shape.target_class.replace("sea:", "") + "Shape";
1027
+ xml.push_str(&format!(
1028
+ " <sh:NodeShape rdf:about=\"http://domainforge.ai/sea#{}\">\n",
1029
+ shape_name
1030
+ ));
1031
+ xml.push_str(&format!(
1032
+ " <sh:targetClass rdf:resource=\"http://domainforge.ai/sea#{}\"/>\n",
1033
+ shape.target_class.replace("sea:", "")
1034
+ ));
1035
+ for prop in &shape.properties {
1036
+ xml.push_str(" <sh:property>\n");
1037
+ xml.push_str(" <rdf:Description>\n");
1038
+ // Resolve the full URI for the sh:path based on the prefixed name.
1039
+ let (ns, local) = if let Some(rest) = prop.path.strip_prefix("sea:") {
1040
+ ("http://domainforge.ai/sea#", rest)
1041
+ } else if let Some(rest) = prop.path.strip_prefix("rdfs:") {
1042
+ ("http://www.w3.org/2000/01/rdf-schema#", rest)
1043
+ } else {
1044
+ ("http://domainforge.ai/sea#", prop.path.as_str())
1045
+ };
1046
+ xml.push_str(&format!(
1047
+ " <sh:path rdf:resource=\"{}{}\"/>\n",
1048
+ ns, local
1049
+ ));
1050
+ if let Some(dt) = &prop.datatype {
1051
+ let dt_uri = if dt.starts_with("xsd:") {
1052
+ dt.replace("xsd:", "http://www.w3.org/2001/XMLSchema#")
1053
+ } else {
1054
+ dt.clone()
1055
+ };
1056
+ xml.push_str(&format!(
1057
+ " <sh:datatype rdf:resource=\"{}\"/>\n",
1058
+ dt_uri
1059
+ ));
1060
+ }
1061
+ if let Some(min) = prop.min_count {
1062
+ xml.push_str(&format!(" <sh:minCount>{}</sh:minCount>\n", min));
1063
+ }
1064
+ if let Some(max) = prop.max_count {
1065
+ xml.push_str(&format!(" <sh:maxCount>{}</sh:maxCount>\n", max));
1066
+ }
1067
+ if let Some(min_ex) = &prop.min_exclusive {
1068
+ xml.push_str(&format!(" <sh:minExclusive rdf:datatype=\"http://www.w3.org/2001/XMLSchema#decimal\">{}</sh:minExclusive>\n", min_ex));
1069
+ }
1070
+ xml.push_str(" </rdf:Description>\n");
1071
+ xml.push_str(" </sh:property>\n");
1072
+ }
1073
+ xml.push_str(" </sh:NodeShape>\n\n");
1074
+ xml
1075
+ }
1076
+
1077
+ pub fn escape_turtle_literal(input: &str) -> String {
1078
+ let mut escaped = String::with_capacity(input.len());
1079
+ for ch in input.chars() {
1080
+ match ch {
1081
+ '\\' => escaped.push_str("\\\\"),
1082
+ '"' => escaped.push_str("\\\""),
1083
+ '\n' => escaped.push_str("\\n"),
1084
+ '\r' => escaped.push_str("\\r"),
1085
+ '\t' => escaped.push_str("\\t"),
1086
+ '\x08' => escaped.push_str("\\b"), // backspace
1087
+ '\x0C' => escaped.push_str("\\f"), // form feed
1088
+ other => escaped.push(other),
1089
+ }
1090
+ }
1091
+ escaped
1092
+ }
1093
+
1094
+ fn uri_encode(s: &str) -> String {
1095
+ utf8_percent_encode(s, URI_ENCODE_SET).to_string()
1096
+ }
1097
+
1098
+ fn validate_turtle_decimal(decimal_str: &str) -> Result<(), String> {
1099
+ // Basic validation for safe decimal literals in Turtle
1100
+ let trimmed = decimal_str.trim();
1101
+
1102
+ // Check for invalid characters that could break Turtle syntax
1103
+ if trimmed
1104
+ .chars()
1105
+ .any(|ch| matches!(ch, '"' | '\'' | '\\' | '\n' | '\r' | '\t'))
1106
+ {
1107
+ return Err("Decimal contains invalid characters".to_string());
1108
+ }
1109
+
1110
+ // Ensure it looks like a valid decimal number
1111
+ if trimmed.is_empty() {
1112
+ return Err("Decimal is empty".to_string());
1113
+ }
1114
+
1115
+ // Basic pattern check: optional sign, digits, optional fractional part
1116
+ let mut has_digit = false;
1117
+ let mut chars = trimmed.chars().peekable();
1118
+
1119
+ // Optional sign
1120
+ if matches!(chars.peek(), Some('+') | Some('-')) {
1121
+ chars.next();
1122
+ }
1123
+
1124
+ // Digits and optional fractional part
1125
+ while let Some(ch) = chars.next() {
1126
+ if ch.is_ascii_digit() {
1127
+ has_digit = true;
1128
+ } else if ch == '.' {
1129
+ // Check fractional part
1130
+ if !chars.next().is_some_and(|c| c.is_ascii_digit()) {
1131
+ return Err("Invalid decimal format".to_string());
1132
+ }
1133
+ for c in chars.by_ref() {
1134
+ if !c.is_ascii_digit() {
1135
+ return Err("Invalid decimal format".to_string());
1136
+ }
1137
+ }
1138
+ break;
1139
+ } else {
1140
+ return Err("Invalid decimal format".to_string());
1141
+ }
1142
+ }
1143
+
1144
+ if !has_digit {
1145
+ return Err("Invalid decimal format".to_string());
1146
+ }
1147
+
1148
+ Ok(())
1149
+ }
1150
+
1151
+ fn clean_uri(uri: &str) -> String {
1152
+ if uri.contains(':') {
1153
+ let parts: Vec<&str> = uri.splitn(2, ':').collect();
1154
+ if parts.len() == 2 {
1155
+ let (prefix, name) = (parts[0], parts[1]);
1156
+
1157
+ // Check for standard RDF/XSD prefixes
1158
+ let standard_prefixes = [
1159
+ ("rdf", "http://www.w3.org/1999/02/22-rdf-syntax-ns#"),
1160
+ ("rdfs", "http://www.w3.org/2000/01/rdf-schema#"),
1161
+ ("xsd", "http://www.w3.org/2001/XMLSchema#"),
1162
+ ("owl", "http://www.w3.org/2002/07/owl#"),
1163
+ ("sh", "http://www.w3.org/ns/shacl#"),
1164
+ ("sea", "http://domainforge.ai/sea#"),
1165
+ ];
1166
+
1167
+ for (std_prefix, namespace) in &standard_prefixes {
1168
+ if prefix == *std_prefix {
1169
+ return format!("{}{}", namespace, name);
1170
+ }
1171
+ }
1172
+
1173
+ // Fall back to original behavior for unknown prefixes
1174
+ return format!("http://domainforge.ai/{}#{}", prefix, name);
1175
+ }
1176
+ }
1177
+ uri.to_string()
1178
+ }
1179
+
1180
+ fn shorten_token(token: &str) -> String {
1181
+ let t = token.trim();
1182
+ // remove enclosing angle brackets
1183
+ let value = if t.starts_with('<') && t.ends_with('>') {
1184
+ &t[1..t.len() - 1]
1185
+ } else {
1186
+ t
1187
+ };
1188
+
1189
+ // If contains typed literal with full datatype like "123"^^<http://www.w3.org/2001/XMLSchema#decimal>
1190
+ if value.contains("^^<http://www.w3.org/2001/XMLSchema#") {
1191
+ // replace the full URI with xsd: prefix
1192
+ if let Some(pos) = value.find("^^<http://www.w3.org/2001/XMLSchema#") {
1193
+ let (lit, rest) = value.split_at(pos);
1194
+ if rest.contains("decimal") {
1195
+ return format!("{}^^xsd:decimal", lit.trim());
1196
+ } else if rest.contains("string") {
1197
+ return format!("{}^^xsd:string", lit.trim());
1198
+ }
1199
+ }
1200
+ }
1201
+
1202
+ // Map common vocabularies
1203
+ let mappings = [
1204
+ ("http://www.w3.org/1999/02/22-rdf-syntax-ns#", "rdf:"),
1205
+ ("http://www.w3.org/2000/01/rdf-schema#", "rdfs:"),
1206
+ ("http://www.w3.org/2001/XMLSchema#", "xsd:"),
1207
+ ("http://www.w3.org/2002/07/owl#", "owl:"),
1208
+ ("http://www.w3.org/ns/shacl#", "sh:"),
1209
+ ("http://domainforge.ai/sea#", "sea:"),
1210
+ ("http://domainforge.ai/rdfs#", "rdfs:"),
1211
+ ];
1212
+
1213
+ for (ns, prefix) in &mappings {
1214
+ if let Some(stripped) = value.strip_prefix(ns) {
1215
+ return format!("{}{}", prefix, stripped);
1216
+ }
1217
+ }
1218
+
1219
+ // Fall back to original token
1220
+ t.to_string()
1221
+ }
1222
+
1223
+ pub fn escape_xml(input: &str) -> String {
1224
+ let mut escaped = String::with_capacity(input.len());
1225
+ for ch in input.chars() {
1226
+ match ch {
1227
+ '&' => escaped.push_str("&amp;"),
1228
+ '<' => escaped.push_str("&lt;"),
1229
+ '>' => escaped.push_str("&gt;"),
1230
+ '"' => escaped.push_str("&quot;"),
1231
+ '\'' => escaped.push_str("&apos;"),
1232
+ other => escaped.push(other),
1233
+ }
1234
+ }
1235
+ escaped
1236
+ }
1237
+
1238
+ fn parse_escaped_value<I>(chars: &mut I) -> String
1239
+ where
1240
+ I: Iterator<Item = char>,
1241
+ {
1242
+ let mut value = String::new();
1243
+ let mut escaped = false;
1244
+
1245
+ for ch in chars.by_ref() {
1246
+ if escaped {
1247
+ let resolved = match ch {
1248
+ 'n' => '\n',
1249
+ 't' => '\t',
1250
+ 'r' => '\r',
1251
+ '"' => '"',
1252
+ '\\' => '\\',
1253
+ other => {
1254
+ value.push('\\');
1255
+ other
1256
+ }
1257
+ };
1258
+ value.push(resolved);
1259
+ escaped = false;
1260
+ continue;
1261
+ }
1262
+
1263
+ match ch {
1264
+ '\\' => escaped = true,
1265
+ '"' => break,
1266
+ other => value.push(other),
1267
+ }
1268
+ }
1269
+
1270
+ value
1271
+ }
1272
+
1273
+ fn parse_typed_literal(literal: &str) -> (String, Option<TypedLiteralSuffix>) {
1274
+ if !literal.starts_with('"') {
1275
+ return (literal.to_string(), None);
1276
+ }
1277
+
1278
+ let mut chars = literal.chars();
1279
+ chars.next();
1280
+ let value = Self::parse_escaped_value(&mut chars);
1281
+
1282
+ let remainder: String = chars.collect();
1283
+ let trimmed = remainder.trim();
1284
+
1285
+ let suffix = if let Some(rest) = trimmed.strip_prefix("^^") {
1286
+ let datatype = rest.trim();
1287
+ if datatype.is_empty() {
1288
+ None
1289
+ } else {
1290
+ Some(TypedLiteralSuffix::Datatype(datatype.to_string()))
1291
+ }
1292
+ } else if let Some(rest) = trimmed.strip_prefix('@') {
1293
+ let language = rest.trim();
1294
+ if language.is_empty() {
1295
+ None
1296
+ } else {
1297
+ Some(TypedLiteralSuffix::Language(language.to_string()))
1298
+ }
1299
+ } else {
1300
+ None
1301
+ };
1302
+
1303
+ (value, suffix)
1304
+ }
1305
+
1306
+ /// Validates that a string is a safe RDF term for use in triples.
1307
+ /// Returns true if the term is valid (no quotes, angle brackets, control chars, backslashes, or illegal colons).
1308
+ fn is_valid_rdf_term(term: &str) -> bool {
1309
+ // Check for dangerous characters
1310
+ if term.contains('"') || term.contains('<') || term.contains('>') || term.contains('\\') {
1311
+ return false;
1312
+ }
1313
+
1314
+ // Check for control characters
1315
+ if term.chars().any(|c| c.is_control()) {
1316
+ return false;
1317
+ }
1318
+
1319
+ // Check for illegal colons (only allow prefixed names like "sea:Something" or local names without colons)
1320
+ // A valid prefixed name has exactly one colon not at the start or end
1321
+ let colon_count = term.matches(':').count();
1322
+ if colon_count > 1 {
1323
+ return false;
1324
+ }
1325
+ if colon_count == 1 && (term.starts_with(':') || term.ends_with(':')) {
1326
+ return false;
1327
+ }
1328
+
1329
+ true
1330
+ }
1331
+ }
1332
+
1333
+ enum TypedLiteralSuffix {
1334
+ Datatype(String),
1335
+ Language(String),
1336
+ }
1337
+
1338
+ impl Default for KnowledgeGraph {
1339
+ fn default() -> Self {
1340
+ Self::new()
1341
+ }
1342
+ }
1343
+
1344
+ impl Graph {
1345
+ pub fn export_rdf(&self, format: &str) -> Result<String, KgError> {
1346
+ let kg = KnowledgeGraph::from_graph(self)?;
1347
+ match format {
1348
+ "turtle" => Ok(kg.to_turtle()),
1349
+ "rdf-xml" => Ok(kg.to_rdf_xml()),
1350
+ _ => Err(KgError::UnsupportedFormat(format.to_string())),
1351
+ }
1352
+ }
1353
+ }
1354
+
1355
+ #[cfg(test)]
1356
+ mod tests {
1357
+ use super::*;
1358
+ use crate::primitives::{Entity, Flow, Resource};
1359
+ use rust_decimal::Decimal;
1360
+
1361
+ #[test]
1362
+ fn test_export_to_rdf_turtle() {
1363
+ let mut graph = Graph::new();
1364
+
1365
+ let entity1 = Entity::new_with_namespace("Supplier", "supply_chain");
1366
+ let entity2 = Entity::new_with_namespace("Manufacturer", "supply_chain");
1367
+ let resource = Resource::new_with_namespace(
1368
+ "Parts",
1369
+ crate::units::unit_from_string("kg"),
1370
+ "supply_chain",
1371
+ );
1372
+
1373
+ let entity1_id = entity1.id().clone();
1374
+ let entity2_id = entity2.id().clone();
1375
+ let resource_id = resource.id().clone();
1376
+
1377
+ graph.add_entity(entity1).unwrap();
1378
+ graph.add_entity(entity2).unwrap();
1379
+ graph.add_resource(resource).unwrap();
1380
+
1381
+ #[allow(deprecated)]
1382
+ let flow = Flow::new(resource_id, entity1_id, entity2_id, Decimal::new(100, 0));
1383
+ graph.add_flow(flow).unwrap();
1384
+
1385
+ let rdf_turtle = graph.export_rdf("turtle").unwrap();
1386
+
1387
+ assert!(rdf_turtle.contains("sea:Entity"));
1388
+ assert!(rdf_turtle.contains("sea:hasResource"));
1389
+ assert!(rdf_turtle.contains("@prefix"));
1390
+ }
1391
+
1392
+ #[test]
1393
+ fn test_export_to_rdf_xml() {
1394
+ let mut graph = Graph::new();
1395
+
1396
+ let entity = Entity::new_with_namespace("TestEntity", "default".to_string());
1397
+ graph.add_entity(entity).unwrap();
1398
+
1399
+ let rdf_xml = graph.export_rdf("rdf-xml").unwrap();
1400
+
1401
+ assert!(rdf_xml.contains("<?xml"));
1402
+ assert!(rdf_xml.contains("rdf:RDF"));
1403
+ }
1404
+
1405
+ #[test]
1406
+ fn test_unsupported_format() {
1407
+ let graph = Graph::new();
1408
+ let result = graph.export_rdf("json-ld");
1409
+
1410
+ assert!(result.is_err());
1411
+ assert!(matches!(result.unwrap_err(), KgError::UnsupportedFormat(_)));
1412
+ }
1413
+
1414
+ #[test]
1415
+ fn test_export_rdf_turtle_encodes_special_characters_and_literals() {
1416
+ let mut graph = Graph::new();
1417
+
1418
+ let entity_space = Entity::new_with_namespace("Entity With Space", "default".to_string());
1419
+ let entity_colon = Entity::new_with_namespace("Entity:Colon", "default".to_string());
1420
+ let entity_slash = Entity::new_with_namespace("Entity/Slash", "default".to_string());
1421
+ let entity_hash = Entity::new_with_namespace("Entity#Hash", "default".to_string());
1422
+
1423
+ graph.add_entity(entity_space.clone()).unwrap();
1424
+ graph.add_entity(entity_colon.clone()).unwrap();
1425
+ graph.add_entity(entity_slash.clone()).unwrap();
1426
+ graph.add_entity(entity_hash.clone()).unwrap();
1427
+
1428
+ let resource = Resource::new_with_namespace(
1429
+ "Resource:Name/Hash",
1430
+ crate::units::unit_from_string("units"),
1431
+ "default".to_string(),
1432
+ );
1433
+ let resource_id = resource.id().clone();
1434
+ graph.add_resource(resource).unwrap();
1435
+
1436
+ let flow = Flow::new(
1437
+ resource_id,
1438
+ entity_space.id().clone(),
1439
+ entity_colon.id().clone(),
1440
+ Decimal::new(42, 0),
1441
+ );
1442
+ graph.add_flow(flow).unwrap();
1443
+
1444
+ let turtle = graph.export_rdf("turtle").unwrap();
1445
+ assert!(turtle.contains("sea:Entity%20With%20Space"));
1446
+ assert!(turtle.contains("sea:Entity%3AColon"));
1447
+ assert!(turtle.contains("sea:Entity%2FSlash"));
1448
+ assert!(turtle.contains("sea:Entity%23Hash"));
1449
+ assert!(turtle.contains("sea:Resource%3AName%2FHash"));
1450
+ assert!(turtle.contains("\"42\"^^xsd:decimal"));
1451
+ }
1452
+
1453
+ #[test]
1454
+ fn test_rdf_xml_escapes_special_literals_and_language_tags() {
1455
+ let mut kg = KnowledgeGraph::new();
1456
+
1457
+ kg.triples.push(Triple {
1458
+ subject: "sea:testEntity".to_string(),
1459
+ predicate: "sea:hasNumericValue".to_string(),
1460
+ object: "\"100\"^^xsd:decimal".to_string(),
1461
+ });
1462
+ kg.triples.push(Triple {
1463
+ subject: "sea:testEntity".to_string(),
1464
+ predicate: "sea:description".to_string(),
1465
+ object: "\"Hello & <World>\"@en".to_string(),
1466
+ });
1467
+
1468
+ let xml = kg.to_rdf_xml();
1469
+ assert!(xml.contains("rdf:datatype=\"http://www.w3.org/2001/XMLSchema#decimal\""));
1470
+ assert!(xml.contains(">100<"));
1471
+ assert!(xml.contains("xml:lang=\"en\""));
1472
+ assert!(xml.contains("&amp;"));
1473
+ assert!(xml.contains("&lt;"));
1474
+ assert!(xml.contains("&gt;"));
1475
+ }
1476
+ }