lorebase-core 0.2.3__tar.gz → 0.3.0__tar.gz

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 (185) hide show
  1. {lorebase_core-0.2.3 → lorebase_core-0.3.0}/PKG-INFO +1 -1
  2. {lorebase_core-0.2.3 → lorebase_core-0.3.0}/pyproject.toml +1 -1
  3. lorebase_core-0.3.0/src/lorebase_core/__init__.py +1 -0
  4. {lorebase_core-0.2.3 → lorebase_core-0.3.0}/src/lorebase_core/canonical/hashing.py +1 -0
  5. {lorebase_core-0.2.3 → lorebase_core-0.3.0}/src/lorebase_core/expr/visibility.py +23 -0
  6. lorebase_core-0.3.0/src/lorebase_core/infer/phrases.py +40 -0
  7. {lorebase_core-0.2.3 → lorebase_core-0.3.0}/src/lorebase_core/infer/suggestions.py +277 -15
  8. {lorebase_core-0.2.3 → lorebase_core-0.3.0}/src/lorebase_core/merge/__init__.py +2 -0
  9. {lorebase_core-0.2.3 → lorebase_core-0.3.0}/src/lorebase_core/merge/data_merge.py +90 -4
  10. {lorebase_core-0.2.3 → lorebase_core-0.3.0}/src/lorebase_core/merge/engine.py +71 -2
  11. {lorebase_core-0.2.3 → lorebase_core-0.3.0}/src/lorebase_core/merge/objects.py +2 -0
  12. {lorebase_core-0.2.3 → lorebase_core-0.3.0}/src/lorebase_core/merge/policies.py +27 -1
  13. {lorebase_core-0.2.3 → lorebase_core-0.3.0}/src/lorebase_core/merge/schema_merge.py +1 -0
  14. lorebase_core-0.3.0/src/lorebase_core/merge/text_merge.py +297 -0
  15. {lorebase_core-0.2.3 → lorebase_core-0.3.0}/src/lorebase_core/model/__init__.py +10 -0
  16. lorebase_core-0.3.0/src/lorebase_core/model/attribute.py +161 -0
  17. {lorebase_core-0.2.3 → lorebase_core-0.3.0}/src/lorebase_core/model/base.py +16 -0
  18. {lorebase_core-0.2.3 → lorebase_core-0.3.0}/src/lorebase_core/model/domain.py +18 -1
  19. {lorebase_core-0.2.3 → lorebase_core-0.3.0}/src/lorebase_core/model/ids.py +2 -0
  20. {lorebase_core-0.2.3 → lorebase_core-0.3.0}/src/lorebase_core/model/kind.py +32 -4
  21. lorebase_core-0.3.0/src/lorebase_core/model/lens.py +32 -0
  22. {lorebase_core-0.2.3 → lorebase_core-0.3.0}/src/lorebase_core/model/lineage.py +58 -2
  23. {lorebase_core-0.2.3 → lorebase_core-0.3.0}/src/lorebase_core/model/schema.py +13 -0
  24. {lorebase_core-0.2.3 → lorebase_core-0.3.0}/src/lorebase_core/model/source.py +58 -1
  25. {lorebase_core-0.2.3 → lorebase_core-0.3.0}/src/lorebase_core/model/yaml_schema.py +29 -0
  26. lorebase_core-0.3.0/src/lorebase_core/reconcile/__init__.py +122 -0
  27. lorebase_core-0.3.0/src/lorebase_core/reconcile/candidates.py +236 -0
  28. lorebase_core-0.3.0/src/lorebase_core/reconcile/claims.py +200 -0
  29. lorebase_core-0.3.0/src/lorebase_core/reconcile/detect.py +342 -0
  30. lorebase_core-0.3.0/src/lorebase_core/reconcile/normalize.py +301 -0
  31. lorebase_core-0.3.0/src/lorebase_core/reconcile/options.py +229 -0
  32. lorebase_core-0.3.0/src/lorebase_core/reconcile/units.yaml +43 -0
  33. {lorebase_core-0.2.3 → lorebase_core-0.3.0}/src/lorebase_core/validate/issues.py +4 -1
  34. {lorebase_core-0.2.3 → lorebase_core-0.3.0}/src/lorebase_core/validate/schema_rules.py +149 -1
  35. {lorebase_core-0.2.3 → lorebase_core-0.3.0}/tests/canonical/vectors/object_hashes.json +9 -0
  36. lorebase_core-0.3.0/tests/expr/test_lens.py +145 -0
  37. {lorebase_core-0.2.3 → lorebase_core-0.3.0}/tests/infer/test_suggestions.py +159 -0
  38. lorebase_core-0.3.0/tests/merge/test_preserved_base.py +94 -0
  39. {lorebase_core-0.2.3 → lorebase_core-0.3.0}/tests/merge/test_resolutions.py +44 -0
  40. lorebase_core-0.3.0/tests/merge/test_text_merge.py +556 -0
  41. lorebase_core-0.3.0/tests/model/test_resolution_fields.py +132 -0
  42. {lorebase_core-0.2.3 → lorebase_core-0.3.0}/tests/model/test_yaml_schema.py +29 -0
  43. lorebase_core-0.3.0/tests/reconcile/test_detect.py +311 -0
  44. lorebase_core-0.3.0/tests/reconcile/test_model.py +110 -0
  45. lorebase_core-0.3.0/tests/reconcile/test_reconcile.py +440 -0
  46. lorebase_core-0.3.0/tests/validate/__init__.py +0 -0
  47. {lorebase_core-0.2.3 → lorebase_core-0.3.0}/tests/validate/test_schema_rules.py +106 -0
  48. lorebase_core-0.2.3/src/lorebase_core/__init__.py +0 -1
  49. lorebase_core-0.2.3/src/lorebase_core/model/attribute.py +0 -72
  50. {lorebase_core-0.2.3 → lorebase_core-0.3.0}/.gitignore +0 -0
  51. {lorebase_core-0.2.3 → lorebase_core-0.3.0}/src/lorebase_core/canonical/__init__.py +0 -0
  52. {lorebase_core-0.2.3 → lorebase_core-0.3.0}/src/lorebase_core/canonical/jcs.py +0 -0
  53. {lorebase_core-0.2.3 → lorebase_core-0.3.0}/src/lorebase_core/expr/__init__.py +0 -0
  54. {lorebase_core-0.2.3 → lorebase_core-0.3.0}/src/lorebase_core/expr/ast.py +0 -0
  55. {lorebase_core-0.2.3 → lorebase_core-0.3.0}/src/lorebase_core/expr/checker.py +0 -0
  56. {lorebase_core-0.2.3 → lorebase_core-0.3.0}/src/lorebase_core/expr/compiler.py +0 -0
  57. {lorebase_core-0.2.3 → lorebase_core-0.3.0}/src/lorebase_core/expr/context.py +0 -0
  58. {lorebase_core-0.2.3 → lorebase_core-0.3.0}/src/lorebase_core/expr/derivations.py +0 -0
  59. {lorebase_core-0.2.3 → lorebase_core-0.3.0}/src/lorebase_core/expr/environment.py +0 -0
  60. {lorebase_core-0.2.3 → lorebase_core-0.3.0}/src/lorebase_core/expr/errors.py +0 -0
  61. {lorebase_core-0.2.3 → lorebase_core-0.3.0}/src/lorebase_core/expr/evaluator.py +0 -0
  62. {lorebase_core-0.2.3 → lorebase_core-0.3.0}/src/lorebase_core/expr/functions.py +0 -0
  63. {lorebase_core-0.2.3 → lorebase_core-0.3.0}/src/lorebase_core/expr/limits.py +0 -0
  64. {lorebase_core-0.2.3 → lorebase_core-0.3.0}/src/lorebase_core/expr/operations.py +0 -0
  65. {lorebase_core-0.2.3 → lorebase_core-0.3.0}/src/lorebase_core/expr/ops.py +0 -0
  66. {lorebase_core-0.2.3 → lorebase_core-0.3.0}/src/lorebase_core/expr/resolver.py +0 -0
  67. {lorebase_core-0.2.3 → lorebase_core-0.3.0}/src/lorebase_core/expr/rows.py +0 -0
  68. {lorebase_core-0.2.3 → lorebase_core-0.3.0}/src/lorebase_core/expr/sensitivity.py +0 -0
  69. {lorebase_core-0.2.3 → lorebase_core-0.3.0}/src/lorebase_core/expr/types.py +0 -0
  70. {lorebase_core-0.2.3 → lorebase_core-0.3.0}/src/lorebase_core/expr/validation.py +0 -0
  71. {lorebase_core-0.2.3 → lorebase_core-0.3.0}/src/lorebase_core/expr/values.py +0 -0
  72. {lorebase_core-0.2.3 → lorebase_core-0.3.0}/src/lorebase_core/infer/__init__.py +0 -0
  73. {lorebase_core-0.2.3 → lorebase_core-0.3.0}/src/lorebase_core/infer/columns.py +0 -0
  74. {lorebase_core-0.2.3 → lorebase_core-0.3.0}/src/lorebase_core/infer/engine.py +0 -0
  75. {lorebase_core-0.2.3 → lorebase_core-0.3.0}/src/lorebase_core/infer/keys.py +0 -0
  76. {lorebase_core-0.2.3 → lorebase_core-0.3.0}/src/lorebase_core/infer/names.py +0 -0
  77. {lorebase_core-0.2.3 → lorebase_core-0.3.0}/src/lorebase_core/infer/options.py +0 -0
  78. {lorebase_core-0.2.3 → lorebase_core-0.3.0}/src/lorebase_core/infer/profile.py +0 -0
  79. {lorebase_core-0.2.3 → lorebase_core-0.3.0}/src/lorebase_core/infer/proposal.py +0 -0
  80. {lorebase_core-0.2.3 → lorebase_core-0.3.0}/src/lorebase_core/infer/references.py +0 -0
  81. {lorebase_core-0.2.3 → lorebase_core-0.3.0}/src/lorebase_core/infer/tables.py +0 -0
  82. {lorebase_core-0.2.3 → lorebase_core-0.3.0}/src/lorebase_core/merge/conflicts.py +0 -0
  83. {lorebase_core-0.2.3 → lorebase_core-0.3.0}/src/lorebase_core/merge/consistency.py +0 -0
  84. {lorebase_core-0.2.3 → lorebase_core-0.3.0}/src/lorebase_core/merge/index.py +0 -0
  85. {lorebase_core-0.2.3 → lorebase_core-0.3.0}/src/lorebase_core/merge/resolve.py +0 -0
  86. {lorebase_core-0.2.3 → lorebase_core-0.3.0}/src/lorebase_core/merge/state.py +0 -0
  87. {lorebase_core-0.2.3 → lorebase_core-0.3.0}/src/lorebase_core/model/derivation.py +0 -0
  88. {lorebase_core-0.2.3 → lorebase_core-0.3.0}/src/lorebase_core/model/embedding.py +0 -0
  89. {lorebase_core-0.2.3 → lorebase_core-0.3.0}/src/lorebase_core/model/operation.py +0 -0
  90. {lorebase_core-0.2.3 → lorebase_core-0.3.0}/src/lorebase_core/model/paths.py +0 -0
  91. {lorebase_core-0.2.3 → lorebase_core-0.3.0}/src/lorebase_core/model/record.py +0 -0
  92. {lorebase_core-0.2.3 → lorebase_core-0.3.0}/src/lorebase_core/model/relation.py +0 -0
  93. {lorebase_core-0.2.3 → lorebase_core-0.3.0}/src/lorebase_core/model/trait.py +0 -0
  94. {lorebase_core-0.2.3 → lorebase_core-0.3.0}/src/lorebase_core/model/vault.py +0 -0
  95. {lorebase_core-0.2.3 → lorebase_core-0.3.0}/src/lorebase_core/query/__init__.py +0 -0
  96. {lorebase_core-0.2.3 → lorebase_core-0.3.0}/src/lorebase_core/query/dsl.py +0 -0
  97. {lorebase_core-0.2.3 → lorebase_core-0.3.0}/src/lorebase_core/query/resolve.py +0 -0
  98. {lorebase_core-0.2.3 → lorebase_core-0.3.0}/src/lorebase_core/revision/__init__.py +0 -0
  99. {lorebase_core-0.2.3 → lorebase_core-0.3.0}/src/lorebase_core/revision/ancestry.py +0 -0
  100. {lorebase_core-0.2.3 → lorebase_core-0.3.0}/src/lorebase_core/revision/changeset.py +0 -0
  101. {lorebase_core-0.2.3 → lorebase_core-0.3.0}/src/lorebase_core/revision/revision.py +0 -0
  102. {lorebase_core-0.2.3 → lorebase_core-0.3.0}/src/lorebase_core/tree/__init__.py +0 -0
  103. {lorebase_core-0.2.3 → lorebase_core-0.3.0}/src/lorebase_core/tree/diff.py +0 -0
  104. {lorebase_core-0.2.3 → lorebase_core-0.3.0}/src/lorebase_core/tree/hamt.py +0 -0
  105. {lorebase_core-0.2.3 → lorebase_core-0.3.0}/src/lorebase_core/tree/node.py +0 -0
  106. {lorebase_core-0.2.3 → lorebase_core-0.3.0}/src/lorebase_core/tree/params.py +0 -0
  107. {lorebase_core-0.2.3 → lorebase_core-0.3.0}/src/lorebase_core/tree/root.py +0 -0
  108. {lorebase_core-0.2.3 → lorebase_core-0.3.0}/src/lorebase_core/tree/store.py +0 -0
  109. {lorebase_core-0.2.3 → lorebase_core-0.3.0}/src/lorebase_core/types/__init__.py +0 -0
  110. {lorebase_core-0.2.3 → lorebase_core-0.3.0}/src/lorebase_core/types/casts.py +0 -0
  111. {lorebase_core-0.2.3 → lorebase_core-0.3.0}/src/lorebase_core/types/constraints.py +0 -0
  112. {lorebase_core-0.2.3 → lorebase_core-0.3.0}/src/lorebase_core/types/encoding.py +0 -0
  113. {lorebase_core-0.2.3 → lorebase_core-0.3.0}/src/lorebase_core/types/errors.py +0 -0
  114. {lorebase_core-0.2.3 → lorebase_core-0.3.0}/src/lorebase_core/types/matching.py +0 -0
  115. {lorebase_core-0.2.3 → lorebase_core-0.3.0}/src/lorebase_core/types/types.py +0 -0
  116. {lorebase_core-0.2.3 → lorebase_core-0.3.0}/src/lorebase_core/validate/__init__.py +0 -0
  117. {lorebase_core-0.2.3 → lorebase_core-0.3.0}/src/lorebase_core/validate/changes.py +0 -0
  118. {lorebase_core-0.2.3 → lorebase_core-0.3.0}/src/lorebase_core/validate/changeset.py +0 -0
  119. {lorebase_core-0.2.3 → lorebase_core-0.3.0}/src/lorebase_core/validate/data_rules.py +0 -0
  120. {lorebase_core-0.2.3 → lorebase_core-0.3.0}/tests/__init__.py +0 -0
  121. {lorebase_core-0.2.3 → lorebase_core-0.3.0}/tests/canonical/__init__.py +0 -0
  122. {lorebase_core-0.2.3 → lorebase_core-0.3.0}/tests/canonical/test_hashing.py +0 -0
  123. {lorebase_core-0.2.3 → lorebase_core-0.3.0}/tests/canonical/test_jcs.py +0 -0
  124. {lorebase_core-0.2.3 → lorebase_core-0.3.0}/tests/canonical/vectors/rfc8785/nested.expected.json +0 -0
  125. {lorebase_core-0.2.3 → lorebase_core-0.3.0}/tests/canonical/vectors/rfc8785/nested.input.json +0 -0
  126. {lorebase_core-0.2.3 → lorebase_core-0.3.0}/tests/canonical/vectors/rfc8785/numbers.json +0 -0
  127. {lorebase_core-0.2.3 → lorebase_core-0.3.0}/tests/canonical/vectors/rfc8785/sorting.expected.json +0 -0
  128. {lorebase_core-0.2.3 → lorebase_core-0.3.0}/tests/canonical/vectors/rfc8785/sorting.input.json +0 -0
  129. {lorebase_core-0.2.3 → lorebase_core-0.3.0}/tests/canonical/vectors/rfc8785/values.expected.json +0 -0
  130. {lorebase_core-0.2.3 → lorebase_core-0.3.0}/tests/canonical/vectors/rfc8785/values.input.json +0 -0
  131. {lorebase_core-0.2.3 → lorebase_core-0.3.0}/tests/expr/__init__.py +0 -0
  132. {lorebase_core-0.2.3 → lorebase_core-0.3.0}/tests/expr/support.py +0 -0
  133. {lorebase_core-0.2.3 → lorebase_core-0.3.0}/tests/expr/test_compile.py +0 -0
  134. {lorebase_core-0.2.3 → lorebase_core-0.3.0}/tests/expr/test_evaluate.py +0 -0
  135. {lorebase_core-0.2.3 → lorebase_core-0.3.0}/tests/expr/test_operations.py +0 -0
  136. {lorebase_core-0.2.3 → lorebase_core-0.3.0}/tests/expr/test_rows.py +0 -0
  137. {lorebase_core-0.2.3 → lorebase_core-0.3.0}/tests/expr/test_tag_origins.py +0 -0
  138. {lorebase_core-0.2.3 → lorebase_core-0.3.0}/tests/expr/test_validation.py +0 -0
  139. {lorebase_core-0.2.3 → lorebase_core-0.3.0}/tests/expr/test_visibility.py +0 -0
  140. {lorebase_core-0.2.3 → lorebase_core-0.3.0}/tests/infer/__init__.py +0 -0
  141. {lorebase_core-0.2.3 → lorebase_core-0.3.0}/tests/infer/support.py +0 -0
  142. {lorebase_core-0.2.3 → lorebase_core-0.3.0}/tests/infer/test_columns.py +0 -0
  143. {lorebase_core-0.2.3 → lorebase_core-0.3.0}/tests/infer/test_engine.py +0 -0
  144. {lorebase_core-0.2.3 → lorebase_core-0.3.0}/tests/infer/test_keys.py +0 -0
  145. {lorebase_core-0.2.3 → lorebase_core-0.3.0}/tests/infer/test_names.py +0 -0
  146. {lorebase_core-0.2.3 → lorebase_core-0.3.0}/tests/infer/test_references.py +0 -0
  147. {lorebase_core-0.2.3 → lorebase_core-0.3.0}/tests/infer/test_supply_gold.py +0 -0
  148. {lorebase_core-0.2.3 → lorebase_core-0.3.0}/tests/merge/__init__.py +0 -0
  149. {lorebase_core-0.2.3 → lorebase_core-0.3.0}/tests/merge/support.py +0 -0
  150. {lorebase_core-0.2.3 → lorebase_core-0.3.0}/tests/merge/test_conflict_kinds.py +0 -0
  151. {lorebase_core-0.2.3 → lorebase_core-0.3.0}/tests/merge/test_data_merge.py +0 -0
  152. {lorebase_core-0.2.3 → lorebase_core-0.3.0}/tests/merge/test_end_to_end.py +0 -0
  153. {lorebase_core-0.2.3 → lorebase_core-0.3.0}/tests/merge/test_index.py +0 -0
  154. {lorebase_core-0.2.3 → lorebase_core-0.3.0}/tests/merge/test_policies.py +0 -0
  155. {lorebase_core-0.2.3 → lorebase_core-0.3.0}/tests/merge/test_properties.py +0 -0
  156. {lorebase_core-0.2.3 → lorebase_core-0.3.0}/tests/merge/test_schema_merge.py +0 -0
  157. {lorebase_core-0.2.3 → lorebase_core-0.3.0}/tests/merge/test_virtual_base.py +0 -0
  158. {lorebase_core-0.2.3 → lorebase_core-0.3.0}/tests/model/__init__.py +0 -0
  159. {lorebase_core-0.2.3 → lorebase_core-0.3.0}/tests/model/supply.py +0 -0
  160. {lorebase_core-0.2.3 → lorebase_core-0.3.0}/tests/model/test_embedding.py +0 -0
  161. {lorebase_core-0.2.3 → lorebase_core-0.3.0}/tests/model/test_model.py +0 -0
  162. {lorebase_core-0.2.3 → lorebase_core-0.3.0}/tests/model/test_paths.py +0 -0
  163. {lorebase_core-0.2.3 → lorebase_core-0.3.0}/tests/model/test_supply_example.py +0 -0
  164. {lorebase_core-0.2.3 → lorebase_core-0.3.0}/tests/model/yaml_support.py +0 -0
  165. {lorebase_core-0.2.3 → lorebase_core-0.3.0}/tests/query/__init__.py +0 -0
  166. {lorebase_core-0.2.3 → lorebase_core-0.3.0}/tests/query/test_resolve.py +0 -0
  167. {lorebase_core-0.2.3/tests/revision → lorebase_core-0.3.0/tests/reconcile}/__init__.py +0 -0
  168. {lorebase_core-0.2.3/tests/tree → lorebase_core-0.3.0/tests/revision}/__init__.py +0 -0
  169. {lorebase_core-0.2.3 → lorebase_core-0.3.0}/tests/revision/test_ancestry.py +0 -0
  170. {lorebase_core-0.2.3 → lorebase_core-0.3.0}/tests/revision/test_revision.py +0 -0
  171. {lorebase_core-0.2.3/tests/types → lorebase_core-0.3.0/tests/tree}/__init__.py +0 -0
  172. {lorebase_core-0.2.3 → lorebase_core-0.3.0}/tests/tree/conftest.py +0 -0
  173. {lorebase_core-0.2.3 → lorebase_core-0.3.0}/tests/tree/support.py +0 -0
  174. {lorebase_core-0.2.3 → lorebase_core-0.3.0}/tests/tree/test_diff.py +0 -0
  175. {lorebase_core-0.2.3 → lorebase_core-0.3.0}/tests/tree/test_hamt.py +0 -0
  176. {lorebase_core-0.2.3 → lorebase_core-0.3.0}/tests/tree/test_node.py +0 -0
  177. {lorebase_core-0.2.3/tests/validate → lorebase_core-0.3.0/tests/types}/__init__.py +0 -0
  178. {lorebase_core-0.2.3 → lorebase_core-0.3.0}/tests/types/strategies.py +0 -0
  179. {lorebase_core-0.2.3 → lorebase_core-0.3.0}/tests/types/test_casts.py +0 -0
  180. {lorebase_core-0.2.3 → lorebase_core-0.3.0}/tests/types/test_constraints.py +0 -0
  181. {lorebase_core-0.2.3 → lorebase_core-0.3.0}/tests/types/test_encoding.py +0 -0
  182. {lorebase_core-0.2.3 → lorebase_core-0.3.0}/tests/validate/data.py +0 -0
  183. {lorebase_core-0.2.3 → lorebase_core-0.3.0}/tests/validate/test_changeset.py +0 -0
  184. {lorebase_core-0.2.3 → lorebase_core-0.3.0}/tests/validate/test_data_rules.py +0 -0
  185. {lorebase_core-0.2.3 → lorebase_core-0.3.0}/tests/validate/test_vault_rules.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: lorebase-core
3
- Version: 0.2.3
3
+ Version: 0.3.0
4
4
  Summary: Lorebase core: pure domain logic and versioning engine
5
5
  Requires-Python: >=3.12
6
6
  Requires-Dist: cel-python>=0.5
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "lorebase-core"
3
- version = "0.2.3"
3
+ version = "0.3.0"
4
4
  description = "Lorebase core: pure domain logic and versioning engine"
5
5
  requires-python = ">=3.12"
6
6
  dependencies = [
@@ -0,0 +1 @@
1
+ __version__ = "0.3.0"
@@ -45,6 +45,7 @@ class ObjectType(StrEnum):
45
45
  SCHEMA_OPERATION = "schema.operation"
46
46
  SCHEMA_DERIVATION = "schema.derivation"
47
47
  SCHEMA_SOURCE = "schema.source"
48
+ SCHEMA_LENS = "schema.lens"
48
49
  SCHEMA_DOMAIN = "schema.domain"
49
50
  RECORD = "record"
50
51
  LINK = "link"
@@ -43,6 +43,7 @@ from lorebase_core.model.derivation import (
43
43
  PathDerivation,
44
44
  )
45
45
  from lorebase_core.model.kind import Kind
46
+ from lorebase_core.model.lens import Lens
46
47
  from lorebase_core.model.operation import CreateLinkStep, DeleteLinkStep, Operation
47
48
  from lorebase_core.model.relation import Relation
48
49
  from lorebase_core.model.schema import Schema
@@ -126,10 +127,32 @@ def visible_schema(
126
127
  d: schema.derivations[d] for d in schema.derivations if d in derivations
127
128
  },
128
129
  "sources": {s: schema.sources[s] for s in schema.sources if s in sources},
130
+ "lenses": _lenses(schema, shown, traits, relations | paths),
129
131
  }
130
132
  )
131
133
 
132
134
 
135
+ def _lenses(
136
+ schema: Schema, kinds: Collection[str], traits: Collection[str], relations: Collection[str]
137
+ ) -> dict[str, Lens]:
138
+ """The lenses as the reader sees them: members left out of the projection are dropped; a
139
+ lens that named Kinds or Traits and keeps none answers like an unknown one (design 8.8)."""
140
+ kept: dict[str, Lens] = {}
141
+ for lens_id, lens in schema.lenses.items():
142
+ shown_kinds = tuple(k for k in lens.kinds if k in kinds)
143
+ shown_traits = tuple(t for t in lens.traits if t in traits)
144
+ if (lens.kinds or lens.traits) and not (shown_kinds or shown_traits):
145
+ continue
146
+ kept[lens_id] = lens.model_copy(
147
+ update={
148
+ "kinds": shown_kinds,
149
+ "traits": shown_traits,
150
+ "relations": tuple(r for r in lens.relations if r in relations),
151
+ }
152
+ )
153
+ return kept
154
+
155
+
133
156
  def _project(
134
157
  schema: Schema, shown: Collection[str], limits: ExprLimits
135
158
  ) -> tuple[set[str], set[str], set[str], set[str]]:
@@ -0,0 +1,40 @@
1
+ """The compared form of words, values and names (design 8.9).
2
+
3
+ Resolution, the value index and vocabulary suggestions compare text in one form: lower case
4
+ (final sigma folded), Latin combining marks dropped after NFD, blanks collapsed. The value
5
+ index computes the same form in SQL (`lb_value_norm`, revision 0045); `normalize` is its Python
6
+ twin, so both sides of a comparison agree.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import re
12
+ import unicodedata
13
+
14
+ _COMBINING = re.compile(f"[{chr(0x300)}-{chr(0x36F)}]")
15
+ BLANKS = "".join(
16
+ chr(c)
17
+ for c in (
18
+ *range(0x09, 0x0E), *range(0x1C, 0x21), 0x85, 0xA0, 0x1680, *range(0x2000, 0x200B),
19
+ 0x2028, 0x2029, 0x202F, 0x205F, 0x3000,
20
+ )
21
+ ) # fmt: skip
22
+ """The blanks `lb_value_norm` collapses: what `str.split()` takes for white space."""
23
+ _BLANKS = re.compile(f"[{re.escape(BLANKS)}]+")
24
+ _CAMEL = re.compile(r"(?<=[a-z0-9])(?=[A-Z])")
25
+
26
+
27
+ def normalize(value: str) -> str:
28
+ """The compared form of a value or a word, as `lb_value_norm` computes it: lower case
29
+ (final sigma folded), Latin combining marks dropped after NFD, blanks collapsed."""
30
+ lowered = value.lower().replace(chr(0x3C2), chr(0x3C3)) # final sigma
31
+ decomposed = unicodedata.normalize("NFD", lowered)
32
+ return _BLANKS.sub(" ", _COMBINING.sub("", decomposed)).strip(" ")
33
+
34
+
35
+ def name_words(api_name: str) -> str:
36
+ """An api name as words: `unit_price` and `UnitPrice` give `unit price`."""
37
+ return normalize(_CAMEL.sub(" ", api_name).replace("_", " ").replace(".", " "))
38
+
39
+
40
+ __all__ = ["BLANKS", "name_words", "normalize"]
@@ -20,6 +20,16 @@ This module holds the pure parts:
20
20
  activates them; Attributes added to an existing Kind are active.
21
21
 
22
22
  A Kind proposed by another suggestion is referred to as ``+<KindName>`` until it exists.
23
+
24
+ **Vocabulary suggestions** (design 8.10) change elements that exist instead of adding new ones:
25
+ an alias of a Kind (``kind_alias``), of an Attribute (``attribute_alias``) or of a stored value
26
+ (``value_alias``), or a new description of an Attribute (``attribute_description``). Scribe's
27
+ consolidation pass proposes them from the words agents asked `lorebase_resolve` for that matched
28
+ nothing. ``name`` is the alias as compared (``lorebase_core.infer.phrases.normalize``), ``kind``
29
+ the Kind id, ``target`` the Attribute id, and ``payload`` holds what else the change needs (the
30
+ stored ``value`` an alias stands for, the proposed ``description``). They are never proposed or
31
+ applied for Attributes whose values are sensitive, kept in the vault or not resolvable
32
+ (``vocabulary_open``).
23
33
  """
24
34
 
25
35
  from __future__ import annotations
@@ -37,8 +47,10 @@ from lorebase_core.expr.sensitivity import effective_tags
37
47
  from lorebase_core.infer.columns import analyze_column, shape_pattern
38
48
  from lorebase_core.infer.names import humanize, normalize, pascal, plural, singular, unique_name
39
49
  from lorebase_core.infer.options import InferenceOptions
50
+ from lorebase_core.infer.phrases import name_words as phrase_of_name
51
+ from lorebase_core.infer.phrases import normalize as phrase_form
40
52
  from lorebase_core.infer.profile import ColumnProfile
41
- from lorebase_core.model.attribute import Attribute, Sensitivity
53
+ from lorebase_core.model.attribute import MAX_ALIASES, Attribute, Sensitivity
42
54
  from lorebase_core.model.base import ElementStatus, snake_case
43
55
  from lorebase_core.model.ids import IdPrefix, new_id
44
56
  from lorebase_core.model.kind import Kind
@@ -55,6 +67,30 @@ class SuggestionType(StrEnum):
55
67
  KIND = "kind"
56
68
  ATTRIBUTE = "attribute"
57
69
  RELATION = "relation"
70
+ KIND_ALIAS = "kind_alias"
71
+ ATTRIBUTE_ALIAS = "attribute_alias"
72
+ VALUE_ALIAS = "value_alias"
73
+ ATTRIBUTE_DESCRIPTION = "attribute_description"
74
+
75
+
76
+ VOCABULARY = frozenset(
77
+ {
78
+ SuggestionType.KIND_ALIAS,
79
+ SuggestionType.ATTRIBUTE_ALIAS,
80
+ SuggestionType.VALUE_ALIAS,
81
+ SuggestionType.ATTRIBUTE_DESCRIPTION,
82
+ }
83
+ )
84
+ """Suggestion types that change existing elements (module doc)."""
85
+ _ORDER = (
86
+ SuggestionType.KIND,
87
+ SuggestionType.ATTRIBUTE,
88
+ SuggestionType.RELATION,
89
+ SuggestionType.KIND_ALIAS,
90
+ SuggestionType.ATTRIBUTE_ALIAS,
91
+ SuggestionType.VALUE_ALIAS,
92
+ SuggestionType.ATTRIBUTE_DESCRIPTION,
93
+ )
58
94
 
59
95
 
60
96
  def _type_inference() -> InferenceOptions:
@@ -86,7 +122,15 @@ class SuggestionOptions(BaseModel):
86
122
  max_tracked: int = Field(
87
123
  default=2000,
88
124
  ge=1,
89
- description="Suggestions kept per Domain; observations of new ones beyond are dropped.",
125
+ description="Suggestions from document extraction kept per Domain; observations of new "
126
+ "ones beyond are dropped.",
127
+ )
128
+ max_vocabulary_tracked: int = Field(
129
+ default=500,
130
+ ge=1,
131
+ description="Pending vocabulary suggestions (from resolution misses) kept per Domain, "
132
+ "counted apart from extraction's so that neither crowds out the other; new ones "
133
+ "beyond are dropped.",
90
134
  )
91
135
  name_similarity: float = Field(
92
136
  default=0.85,
@@ -191,6 +235,10 @@ class Observation:
191
235
  ``kind`` is the owner Kind of an Attribute or the from Kind of a Relation, ``target`` the
192
236
  to Kind of a Relation: a Kind id, or ``+<KindName>`` for a proposed Kind. ``value`` is a
193
237
  sample value (an Attribute value, the key or name of a thing of a new Kind).
238
+
239
+ For vocabulary types (module doc) ``label`` is the alias as compared, ``kind`` the Kind id,
240
+ ``target`` the Attribute id and ``payload`` what the change needs. ``weight`` is how many
241
+ sightings the observation stands for (the misses of a phrase).
194
242
  """
195
243
 
196
244
  type: SuggestionType
@@ -202,25 +250,45 @@ class Observation:
202
250
  target: str | None = None
203
251
  value: str | None = None
204
252
  evidence: Mapping[str, Any] | None = None
253
+ payload: Mapping[str, Any] | None = None
254
+ weight: int = 1
205
255
 
206
256
  @property
207
257
  def name(self) -> str:
258
+ if self.type in VOCABULARY:
259
+ return self.label
208
260
  return kind_name(self.label) if self.type is SuggestionType.KIND else field_name(self.label)
209
261
 
210
262
  @property
211
263
  def fingerprint(self) -> str:
212
- return fingerprint(self.type, self.name, self.kind, self.target)
264
+ value = (self.payload or {}).get("value")
265
+ return fingerprint(
266
+ self.type, self.name, self.kind, self.target, value if isinstance(value, str) else None
267
+ )
213
268
 
214
269
 
215
270
  def fingerprint(
216
- type_: SuggestionType, name: str, kind: str | None = None, target: str | None = None
271
+ type_: SuggestionType,
272
+ name: str,
273
+ kind: str | None = None,
274
+ target: str | None = None,
275
+ value: str | None = None,
217
276
  ) -> str:
218
- """Identity of a suggestion: observations with the same fingerprint aggregate."""
277
+ """Identity of a suggestion: observations with the same fingerprint aggregate. A
278
+ vocabulary suggestion is one per alias and element (per Attribute for a description)."""
219
279
  if type_ is SuggestionType.KIND:
220
280
  return f"kind:{_canonical(name)}"
221
281
  if type_ is SuggestionType.ATTRIBUTE:
222
282
  return f"attribute:{kind}:{_canonical(name)}"
223
- return f"relation:{kind}:{_canonical(name)}:{target}"
283
+ if type_ is SuggestionType.RELATION:
284
+ return f"relation:{kind}:{_canonical(name)}:{target}"
285
+ if type_ is SuggestionType.KIND_ALIAS:
286
+ return f"kind_alias:{kind}:{phrase_form(name)}"
287
+ if type_ is SuggestionType.ATTRIBUTE_ALIAS:
288
+ return f"attribute_alias:{kind}:{target}:{phrase_form(name)}"
289
+ if type_ is SuggestionType.VALUE_ALIAS:
290
+ return f"value_alias:{kind}:{target}:{value}:{phrase_form(name)}"
291
+ return f"attribute_description:{kind}:{target}"
224
292
 
225
293
 
226
294
  # Deduplication ----------------------------------------------------------------------------------
@@ -453,6 +521,7 @@ def existing_match(
453
521
  *,
454
522
  kind: str | None = None,
455
523
  target: str | None = None,
524
+ value: str | None = None,
456
525
  options: SuggestionOptions,
457
526
  tags: Mapping[str, frozenset[Sensitivity]] | None = None,
458
527
  ) -> ExistingMatch | None:
@@ -463,8 +532,15 @@ def existing_match(
463
532
  (a field naming another Kind refers to it). A Relation matches a Relation between its
464
533
  Kinds, in either direction, by name or by the name of its far Kind. ``tags`` are the
465
534
  sensitivity tags by Attribute id (``effective_tags`` of the schema by default).
535
+
536
+ An alias matches the element whose name or alias it is already: any Kind for a Kind
537
+ alias, any Attribute of the Kind for an Attribute alias, the Attribute holding it as a
538
+ stored value's alias (or that value itself) for a value alias (``value``: the stored
539
+ value it would stand for). A description never duplicates.
466
540
  """
467
541
  tags = effective_tags(schema) if tags is None else tags
542
+ if type_ in VOCABULARY:
543
+ return _vocabulary_match(schema, type_, name, kind, target, tags)
468
544
  if type_ is SuggestionType.KIND:
469
545
  return _best_kind(schema, name, options)
470
546
  owner = resolve_kind(schema, kind, options)
@@ -506,6 +582,104 @@ def sensitive_attributes(schema: Schema) -> dict[str, frozenset[Sensitivity]]:
506
582
  return {k: v for k, v in effective_tags(schema).items() if v}
507
583
 
508
584
 
585
+ # Vocabulary -------------------------------------------------------------------------------------
586
+
587
+
588
+ def kind_phrases(kind: Kind) -> frozenset[str]:
589
+ """The compared forms that name a Kind: api name as words, display name, aliases."""
590
+ return frozenset(
591
+ p
592
+ for p in (
593
+ phrase_of_name(kind.api_name),
594
+ phrase_form(kind.display_name),
595
+ *(phrase_form(a) for a in kind.aliases or ()),
596
+ )
597
+ if p
598
+ )
599
+
600
+
601
+ def attribute_phrases(attribute: Attribute) -> frozenset[str]:
602
+ """The compared forms that name an Attribute: api name as words, display name, aliases."""
603
+ return frozenset(
604
+ p
605
+ for p in (
606
+ phrase_of_name(attribute.api_name),
607
+ phrase_form(attribute.display_name),
608
+ *(phrase_form(a) for a in attribute.aliases or ()),
609
+ )
610
+ if p
611
+ )
612
+
613
+
614
+ def value_phrases(attribute: Attribute) -> frozenset[str]:
615
+ """The compared forms of the values the schema names for an Attribute: enum values,
616
+ values that carry aliases, and those aliases."""
617
+ found: set[str] = set()
618
+ spec = attribute.type.element if attribute.type.name is AttributeType.ARRAY else attribute.type
619
+ if spec is not None:
620
+ found.update(phrase_form(v) for v in spec.enum_values or ())
621
+ for stored, aliases in (attribute.value_aliases or {}).items():
622
+ found.add(phrase_form(stored))
623
+ found.update(phrase_form(a) for a in aliases)
624
+ return frozenset(p for p in found if p)
625
+
626
+
627
+ def vocabulary_open(
628
+ attribute: Attribute, tags: frozenset[Sensitivity] = frozenset(), *, values: bool = False
629
+ ) -> bool:
630
+ """Whether aliases or a description may be proposed for ``attribute``: it carries no
631
+ sensitivity tag (``tags``: its effective tags, through Derivations), is not kept in the
632
+ vault, not derived and not marked ``resolvable: false``; with ``values``, its values are
633
+ strings or enums (value aliases)."""
634
+ if tags or attribute.sensitivity or attribute.in_vault or attribute.is_derived:
635
+ return False
636
+ if attribute.resolvable is False:
637
+ return False
638
+ return attribute.value_resolvable or not values
639
+
640
+
641
+ def _vocabulary_match(
642
+ schema: Schema,
643
+ type_: SuggestionType,
644
+ name: str,
645
+ kind: str | None,
646
+ target: str | None,
647
+ tags: Mapping[str, frozenset[Sensitivity]],
648
+ ) -> ExistingMatch | None:
649
+ owner = schema.kinds.get(kind or "")
650
+ phrase = phrase_form(name)
651
+ if owner is None or type_ is SuggestionType.ATTRIBUTE_DESCRIPTION:
652
+ return None
653
+ if type_ is SuggestionType.KIND_ALIAS:
654
+ for other in schema.kinds.values():
655
+ if phrase in kind_phrases(other):
656
+ return ExistingMatch("kinds", other.id, other.api_name, 1.0)
657
+ return None
658
+ attribute = owner.attributes.get(target or "")
659
+ if attribute is None:
660
+ return None
661
+ if type_ is SuggestionType.ATTRIBUTE_ALIAS:
662
+ for sibling in owner.attributes.values():
663
+ if phrase in attribute_phrases(sibling):
664
+ return ExistingMatch(
665
+ "attributes",
666
+ sibling.id,
667
+ sibling.api_name,
668
+ 1.0,
669
+ tags.get(sibling.id, frozenset()),
670
+ )
671
+ return None
672
+ if phrase in value_phrases(attribute):
673
+ return ExistingMatch(
674
+ "attributes",
675
+ attribute.id,
676
+ attribute.api_name,
677
+ 1.0,
678
+ tags.get(attribute.id, frozenset()),
679
+ )
680
+ return None
681
+
682
+
509
683
  # Types ------------------------------------------------------------------------------------------
510
684
 
511
685
 
@@ -543,6 +717,7 @@ class Suggestion:
543
717
  target: str | None = None
544
718
  samples: tuple[str, ...] = ()
545
719
  labels: tuple[str, ...] = ()
720
+ payload: Mapping[str, Any] = field(default_factory=dict)
546
721
 
547
722
 
548
723
  class SuggestionError(ValueError):
@@ -632,6 +807,92 @@ def _relation_names(schema: Schema, extra: Iterable[Relation]) -> set[str]:
632
807
  return names
633
808
 
634
809
 
810
+ def _with_alias(
811
+ aliases: tuple[str, ...] | None, alias: str, suggestion: Suggestion
812
+ ) -> tuple[str, ...]:
813
+ kept = tuple(aliases or ())
814
+ if alias in kept:
815
+ return kept
816
+ if len(kept) >= MAX_ALIASES:
817
+ raise SuggestionError(suggestion.id, f"the element has {MAX_ALIASES} aliases already")
818
+ return (*kept, alias)
819
+
820
+
821
+ def _draft_phrases(draft: _Draft) -> dict[str, str]:
822
+ """Every name and alias of the draft schema (the schema with the changes applied so far),
823
+ compared form -> what it names."""
824
+ kinds = {k.api_name: k for k in draft.schema.kinds.values()}
825
+ kinds.update(draft.kinds)
826
+ taken: dict[str, str] = {}
827
+ for kind in kinds.values():
828
+ for phrase in kind_phrases(kind):
829
+ taken.setdefault(phrase, f"Kind {kind.api_name}")
830
+ for attribute in kind.attributes.values():
831
+ where = f"{kind.api_name}.{attribute.api_name}"
832
+ for phrase in attribute_phrases(attribute):
833
+ taken.setdefault(phrase, f"Attribute {where}")
834
+ for phrase in value_phrases(attribute):
835
+ taken.setdefault(phrase, f"a value of {where}")
836
+ return taken
837
+
838
+
839
+ def _vocabulary_change(
840
+ draft: _Draft, suggestion: Suggestion, tags: Mapping[str, frozenset[Sensitivity]]
841
+ ) -> None:
842
+ """Apply an alias or description suggestion to the draft copy of its Kind. An alias
843
+ that already names anything in the draft (a Kind, an Attribute, a value or one of their
844
+ aliases, the suggestions accepted with it included) is refused: it would be ambiguous."""
845
+ owner = draft.kind(suggestion.kind, suggestion)
846
+ alias = phrase_form(suggestion.name)
847
+ if suggestion.type is not SuggestionType.ATTRIBUTE_DESCRIPTION:
848
+ named = _draft_phrases(draft).get(alias)
849
+ if named is not None:
850
+ raise SuggestionError(suggestion.id, f"{alias!r} already names {named}")
851
+ if suggestion.type is SuggestionType.KIND_ALIAS:
852
+ if not alias:
853
+ raise SuggestionError(suggestion.id, "the alias is empty")
854
+ changed = owner.model_copy(
855
+ update={"aliases": _with_alias(owner.aliases, alias, suggestion)}
856
+ )
857
+ draft.kinds[owner.api_name] = changed
858
+ draft.touched_kinds.add(owner.api_name)
859
+ return
860
+ attribute = owner.attributes.get(suggestion.target or "")
861
+ if attribute is None:
862
+ raise SuggestionError(
863
+ suggestion.id, f"Kind {owner.api_name!r} has no Attribute {suggestion.target!r}"
864
+ )
865
+ values = suggestion.type is SuggestionType.VALUE_ALIAS
866
+ if not vocabulary_open(attribute, tags.get(attribute.id, frozenset()), values=values):
867
+ raise SuggestionError(
868
+ suggestion.id,
869
+ f"Attribute {attribute.api_name!r} is sensitive, kept in the vault, derived or not "
870
+ "resolvable: no alias or description is proposed for it",
871
+ )
872
+ update: dict[str, Any]
873
+ if suggestion.type is SuggestionType.ATTRIBUTE_DESCRIPTION:
874
+ description = suggestion.payload.get("description")
875
+ if not isinstance(description, str) or not description.strip():
876
+ raise SuggestionError(suggestion.id, "the suggestion holds no description")
877
+ update = {"description": description.strip()}
878
+ elif suggestion.type is SuggestionType.ATTRIBUTE_ALIAS:
879
+ if not alias:
880
+ raise SuggestionError(suggestion.id, "the alias is empty")
881
+ update = {"aliases": _with_alias(attribute.aliases, alias, suggestion)}
882
+ else:
883
+ value = suggestion.payload.get("value")
884
+ if not isinstance(value, str) or not value or not alias:
885
+ raise SuggestionError(suggestion.id, "the suggestion names no stored value")
886
+ current = dict(attribute.value_aliases or {})
887
+ current[value] = _with_alias(current.get(value), alias, suggestion)
888
+ update = {"value_aliases": current}
889
+ changed_attribute = attribute.model_copy(update=update)
890
+ draft.kinds[owner.api_name] = owner.model_copy(
891
+ update={"attributes": {**owner.attributes, attribute.id: changed_attribute}}
892
+ )
893
+ draft.touched_kinds.add(owner.api_name)
894
+
895
+
635
896
  def suggestion_patch(
636
897
  schema: Schema, suggestions: Sequence[Suggestion], options: SuggestionOptions
637
898
  ) -> dict[str, Any]:
@@ -639,16 +900,12 @@ def suggestion_patch(
639
900
  ``relations`` sections holding the new and the changed elements (whole, as the patch
640
901
  replaces elements by api name). Raises ``SuggestionError``."""
641
902
  draft = _Draft(schema, options)
642
- ordered = sorted(
643
- suggestions,
644
- key=lambda s: [
645
- SuggestionType.KIND,
646
- SuggestionType.ATTRIBUTE,
647
- SuggestionType.RELATION,
648
- ].index(s.type),
649
- )
903
+ ordered = sorted(suggestions, key=lambda s: _ORDER.index(s.type))
904
+ tags = effective_tags(schema)
650
905
  for suggestion in ordered:
651
- if suggestion.type is SuggestionType.KIND:
906
+ if suggestion.type in VOCABULARY:
907
+ _vocabulary_change(draft, suggestion, tags)
908
+ elif suggestion.type is SuggestionType.KIND:
652
909
  if schema.kind_by_api_name(suggestion.name) or suggestion.name in draft.kinds:
653
910
  raise SuggestionError(suggestion.id, f"Kind {suggestion.name!r} exists")
654
911
  kind = _new_kind(suggestion, options)
@@ -703,6 +960,7 @@ def suggestion_patch(
703
960
 
704
961
  __all__ = [
705
962
  "PROPOSED",
963
+ "VOCABULARY",
706
964
  "AttributeGuess",
707
965
  "AttributeSighting",
708
966
  "ExistingMatch",
@@ -711,6 +969,7 @@ __all__ = [
711
969
  "SuggestionError",
712
970
  "SuggestionOptions",
713
971
  "SuggestionType",
972
+ "attribute_phrases",
714
973
  "dangling_reference_names",
715
974
  "existing_match",
716
975
  "field_name",
@@ -720,6 +979,7 @@ __all__ = [
720
979
  "kind_is_independent",
721
980
  "kind_is_noise",
722
981
  "kind_name",
982
+ "kind_phrases",
723
983
  "merge_attributes",
724
984
  "name_similarity",
725
985
  "proposed_kind",
@@ -727,5 +987,7 @@ __all__ = [
727
987
  "sensitive_attributes",
728
988
  "suggestion_patch",
729
989
  "value_classes",
990
+ "value_phrases",
991
+ "vocabulary_open",
730
992
  "word_overlap",
731
993
  ]
@@ -46,6 +46,7 @@ from lorebase_core.merge.policies import (
46
46
  PolicyName,
47
47
  origin_labels,
48
48
  )
49
+ from lorebase_core.merge.text_merge import TextGranularity
49
50
 
50
51
  __all__ = [
51
52
  "CHOICES",
@@ -76,6 +77,7 @@ __all__ = [
76
77
  "ResolutionError",
77
78
  "SchemaLoader",
78
79
  "Side",
80
+ "TextGranularity",
79
81
  "TreeIndexView",
80
82
  "UnresolvedConflictsError",
81
83
  "apply_resolutions",