arango-query-core 0.2.0__tar.gz → 0.2.1__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 (41) hide show
  1. {arango_query_core-0.2.0 → arango_query_core-0.2.1}/CHANGELOG.md +21 -0
  2. {arango_query_core-0.2.0 → arango_query_core-0.2.1}/PKG-INFO +1 -1
  3. {arango_query_core-0.2.0 → arango_query_core-0.2.1}/arango_query_core/owl_rdflib.py +37 -7
  4. {arango_query_core-0.2.0 → arango_query_core-0.2.1}/arango_query_core/owl_turtle.py +41 -15
  5. {arango_query_core-0.2.0 → arango_query_core-0.2.1}/pyproject.toml +1 -1
  6. {arango_query_core-0.2.0 → arango_query_core-0.2.1}/tests/test_owl_rdflib.py +50 -0
  7. {arango_query_core-0.2.0 → arango_query_core-0.2.1}/tests/test_owl_turtle_roundtrip.py +88 -0
  8. arango_query_core-0.2.0/.claude/settings.json +0 -8
  9. {arango_query_core-0.2.0 → arango_query_core-0.2.1}/.github/workflows/ci.yml +0 -0
  10. {arango_query_core-0.2.0 → arango_query_core-0.2.1}/.github/workflows/publish.yml +0 -0
  11. {arango_query_core-0.2.0 → arango_query_core-0.2.1}/.gitignore +0 -0
  12. {arango_query_core-0.2.0 → arango_query_core-0.2.1}/LICENSE +0 -0
  13. {arango_query_core-0.2.0 → arango_query_core-0.2.1}/README.md +0 -0
  14. {arango_query_core-0.2.0 → arango_query_core-0.2.1}/arango_query_core/__init__.py +0 -0
  15. {arango_query_core-0.2.0 → arango_query_core-0.2.1}/arango_query_core/aql.py +0 -0
  16. {arango_query_core-0.2.0 → arango_query_core-0.2.1}/arango_query_core/errors.py +0 -0
  17. {arango_query_core-0.2.0 → arango_query_core-0.2.1}/arango_query_core/exec.py +0 -0
  18. {arango_query_core-0.2.0 → arango_query_core-0.2.1}/arango_query_core/extensions.py +0 -0
  19. {arango_query_core-0.2.0 → arango_query_core-0.2.1}/arango_query_core/mapping.py +0 -0
  20. {arango_query_core-0.2.0 → arango_query_core-0.2.1}/arango_query_core/nl/__init__.py +0 -0
  21. {arango_query_core-0.2.0 → arango_query_core-0.2.1}/arango_query_core/nl/engine.py +0 -0
  22. {arango_query_core-0.2.0 → arango_query_core-0.2.1}/arango_query_core/nl/fewshot.py +0 -0
  23. {arango_query_core-0.2.0 → arango_query_core-0.2.1}/arango_query_core/nl/grounding.py +0 -0
  24. {arango_query_core-0.2.0 → arango_query_core-0.2.1}/arango_query_core/nl/pathindex.py +0 -0
  25. {arango_query_core-0.2.0 → arango_query_core-0.2.1}/arango_query_core/nl/postconditions.py +0 -0
  26. {arango_query_core-0.2.0 → arango_query_core-0.2.1}/arango_query_core/nl/providers.py +0 -0
  27. {arango_query_core-0.2.0 → arango_query_core-0.2.1}/arango_query_core/nl/seams.py +0 -0
  28. {arango_query_core-0.2.0 → arango_query_core-0.2.1}/arango_query_core/nl/synthbank.py +0 -0
  29. {arango_query_core-0.2.0 → arango_query_core-0.2.1}/arango_query_core/py.typed +0 -0
  30. {arango_query_core-0.2.0 → arango_query_core-0.2.1}/docs/PRD.md +0 -0
  31. {arango_query_core-0.2.0 → arango_query_core-0.2.1}/tests/fixtures/mappings/pg.owl.ttl +0 -0
  32. {arango_query_core-0.2.0 → arango_query_core-0.2.1}/tests/test_mapping_helpers.py +0 -0
  33. {arango_query_core-0.2.0 → arango_query_core-0.2.1}/tests/test_mapping_resolver_normalization.py +0 -0
  34. {arango_query_core-0.2.0 → arango_query_core-0.2.1}/tests/test_mapping_vci_detection.py +0 -0
  35. {arango_query_core-0.2.0 → arango_query_core-0.2.1}/tests/test_nl_engine.py +0 -0
  36. {arango_query_core-0.2.0 → arango_query_core-0.2.1}/tests/test_nl_fewshot.py +0 -0
  37. {arango_query_core-0.2.0 → arango_query_core-0.2.1}/tests/test_nl_grounding.py +0 -0
  38. {arango_query_core-0.2.0 → arango_query_core-0.2.1}/tests/test_nl_pathindex.py +0 -0
  39. {arango_query_core-0.2.0 → arango_query_core-0.2.1}/tests/test_nl_postconditions.py +0 -0
  40. {arango_query_core-0.2.0 → arango_query_core-0.2.1}/tests/test_nl_providers.py +0 -0
  41. {arango_query_core-0.2.0 → arango_query_core-0.2.1}/tests/test_nl_synthbank.py +0 -0
@@ -6,6 +6,27 @@ exact versions (contextual-data-fabric CC-9) and re-run their golden /
6
6
  eval suites on every bump — pre-1.0, **minor** bumps may break the API
7
7
  and this file must say so explicitly.
8
8
 
9
+ ## [0.2.1] - 2026-10-16
10
+
11
+ ### Fixed
12
+
13
+ - **OWL import keeps property names.** `owl_turtle.turtle_to_mapping` cut
14
+ every datatype-property name at its last underscore — a rule meant for the
15
+ `:<REL>_<prop>` relationship properties `mapping_to_turtle` writes — so
16
+ `file_name` imported as `name` and `account_id` as `id`, including on a
17
+ round trip of this library's own export. Entity properties now keep their
18
+ full name; relationship properties lose exactly their `<REL>_` prefix.
19
+ - **Shared fields reach every class.** Both importers (`turtle_to_mapping`
20
+ and the rdflib-based `parse_owl_with_rdflib`) dropped a property whose
21
+ `rdfs:domain` is an `owl:unionOf` list — arangodb-schema-analyzer
22
+ 0.15.1's form for a field several classes share — and the rdflib one read
23
+ only the first of several domains. Each member / domain now gets the field,
24
+ and the anonymous union class is no longer imported as an entity.
25
+ - **Stored field names.** `phys:attributeName` (analyzer 0.15.1) now maps a
26
+ property to its stored field in the physical mapping
27
+ (`properties.<name>.field`), so a sanitized or renamed field (`file-name`,
28
+ `page_count`) resolves to the attribute that holds it.
29
+
9
30
  ## [0.2.0] - 2026-08-23
10
31
 
11
32
  ### BREAKING
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: arango-query-core
3
- Version: 0.2.0
3
+ Version: 0.2.1
4
4
  Summary: Shared conceptual-model and NL substrate for ArangoDB query transpilers (arango-cypher-py, arango-sparql-py)
5
5
  Project-URL: Repository, https://github.com/ArthurKeen/arango-query-core
6
6
  Project-URL: Changelog, https://github.com/ArthurKeen/arango-query-core/blob/main/CHANGELOG.md
@@ -42,23 +42,30 @@ def parse_owl_with_rdflib(turtle_text: str) -> MappingBundle:
42
42
  entities: list[dict[str, Any]] = []
43
43
  entity_props: dict[str, list[dict[str, str]]] = {}
44
44
 
45
- for cls in g.subjects(RDF.type, OWL.Class):
45
+ for cls in _named_classes(g, RDF, OWL):
46
46
  name = _local_name(cls)
47
47
  entities.append({"name": name, "labels": [name], "properties": []})
48
48
  entity_props[name] = []
49
49
 
50
50
  # ── Extract datatype properties ──────────────────────────────────
51
+ # A property's domain may be one class, several (each gets the field),
52
+ # or an owl:unionOf class expression — arangodb-schema-analyzer's form
53
+ # for a field several classes share. phys:attributeName names the
54
+ # stored field when it differs from the property name.
55
+ fields: dict[str, dict[str, str]] = {}
51
56
  for prop in g.subjects(RDF.type, OWL.DatatypeProperty):
52
57
  prop_name = _local_name(prop)
53
- domain = g.value(prop, RDFS.domain)
54
58
  range_uri = g.value(prop, RDFS.range)
55
59
  prop_entry: dict[str, str] = {"name": prop_name}
56
60
  if range_uri:
57
61
  prop_entry["type"] = _xsd_to_simple(str(range_uri))
58
- if domain:
59
- domain_name = _local_name(domain)
60
- if domain_name in entity_props:
61
- entity_props[domain_name].append(prop_entry)
62
+ attribute = g.value(prop, PHYS.attributeName)
63
+ for domain_node in g.objects(prop, RDFS.domain):
64
+ for domain_name in _domain_names(g, domain_node, OWL):
65
+ if domain_name in entity_props:
66
+ entity_props[domain_name].append(prop_entry)
67
+ if attribute:
68
+ fields.setdefault(domain_name, {})[prop_name] = str(attribute)
62
69
 
63
70
  for entity in entities:
64
71
  entity["properties"] = entity_props.get(entity["name"], [])
@@ -82,7 +89,7 @@ def parse_owl_with_rdflib(turtle_text: str) -> MappingBundle:
82
89
  physical_entities: dict[str, dict[str, Any]] = {}
83
90
  physical_rels: dict[str, dict[str, Any]] = {}
84
91
 
85
- for cls in g.subjects(RDF.type, OWL.Class):
92
+ for cls in _named_classes(g, RDF, OWL):
86
93
  name = _local_name(cls)
87
94
  coll = g.value(cls, PHYS.collectionName)
88
95
  style = g.value(cls, PHYS.style) or g.value(cls, PHYS.mappingStyle)
@@ -98,6 +105,8 @@ def parse_owl_with_rdflib(turtle_text: str) -> MappingBundle:
98
105
  pm_entry["typeField"] = str(type_field)
99
106
  if type_value:
100
107
  pm_entry["typeValue"] = str(type_value)
108
+ for prop_name, field in fields.get(name, {}).items():
109
+ pm_entry["properties"][prop_name] = {"field": field}
101
110
  physical_entities[name] = pm_entry
102
111
 
103
112
  for prop in g.subjects(RDF.type, OWL.ObjectProperty):
@@ -127,6 +136,27 @@ def parse_owl_with_rdflib(turtle_text: str) -> MappingBundle:
127
136
  )
128
137
 
129
138
 
139
+ def _named_classes(g: Any, rdf: Any, owl: Any) -> list[Any]:
140
+ """Every ``owl:Class`` with an IRI. An anonymous class expression (the
141
+ ``[ a owl:Class ; owl:unionOf ( … ) ]`` of a shared field's domain) is not
142
+ an entity."""
143
+ import rdflib
144
+
145
+ return [c for c in g.subjects(rdf.type, owl.Class) if isinstance(c, rdflib.URIRef)]
146
+
147
+
148
+ def _domain_names(g: Any, domain: Any, owl: Any) -> list[str]:
149
+ """Class names a ``rdfs:domain`` value covers: the class itself, or every
150
+ member of an ``owl:unionOf`` list."""
151
+ import rdflib
152
+ from rdflib.collection import Collection
153
+
154
+ if isinstance(domain, rdflib.BNode):
155
+ members = g.value(domain, owl.unionOf)
156
+ return [_local_name(m) for m in Collection(g, members)] if members is not None else []
157
+ return [_local_name(domain)]
158
+
159
+
130
160
  def _local_name(uri: Any) -> str:
131
161
  """Extract the local (fragment / last-path-segment) name from a URI."""
132
162
  s = str(uri)
@@ -111,10 +111,21 @@ def mapping_to_turtle(bundle: MappingBundle) -> str:
111
111
 
112
112
 
113
113
  def turtle_to_mapping(turtle: str) -> MappingBundle:
114
- """Parse an OWL/Turtle string into a MappingBundle."""
114
+ """Parse an OWL/Turtle string into a MappingBundle.
115
+
116
+ A line-oriented reader for the layout :func:`mapping_to_turtle` and
117
+ arangodb-schema-analyzer write (no rdflib needed; see
118
+ :mod:`.owl_rdflib` for arbitrary Turtle). A datatype property keeps its
119
+ full local name (``:file_name`` is ``file_name``); only a relationship
120
+ property written ``:<REL>_<prop>`` with ``rdfs:domain :<REL>`` loses the
121
+ ``<REL>_`` prefix. Its domain may be one class, several ``rdfs:domain``
122
+ lines, or an ``owl:unionOf`` list (the analyzer's form for a field
123
+ several classes share), and ``phys:attributeName`` names the stored
124
+ field when it differs from the property name.
125
+ """
115
126
  classes: dict[str, dict[str, str]] = {}
116
127
  obj_props: dict[str, dict[str, str]] = {}
117
- data_props: list[dict[str, str]] = []
128
+ data_props: list[dict[str, Any]] = []
118
129
  annotations: dict[str, dict[str, str]] = {}
119
130
 
120
131
  current_subject: str | None = None
@@ -143,14 +154,20 @@ def turtle_to_mapping(turtle: str) -> MappingBundle:
143
154
  if m_dataprop:
144
155
  current_subject = m_dataprop.group(1)
145
156
  current_kind = "dataprop"
146
- data_props.append({"name": current_subject})
157
+ data_props.append({"name": current_subject, "domains": []})
158
+ continue
159
+
160
+ m_union = re.match(r"rdfs:domain\s+\[.*owl:unionOf\s*\(([^)]*)\)", stripped)
161
+ if m_union:
162
+ if current_kind == "dataprop" and data_props:
163
+ data_props[-1]["domains"].extend(re.findall(r":(\w+)", m_union.group(1)))
147
164
  continue
148
165
 
149
166
  m_domain = re.match(r"rdfs:domain\s+:(\w+)", stripped)
150
167
  if m_domain:
151
168
  domain = m_domain.group(1)
152
169
  if current_kind == "dataprop" and data_props:
153
- data_props[-1]["domain"] = domain
170
+ data_props[-1]["domains"].append(domain)
154
171
  elif current_kind == "objprop" and current_subject:
155
172
  obj_props[current_subject]["domain"] = domain
156
173
  continue
@@ -164,7 +181,14 @@ def turtle_to_mapping(turtle: str) -> MappingBundle:
164
181
  obj_props[current_subject]["range"] = rng.lstrip(":")
165
182
  continue
166
183
 
167
- for annot in ("mappingStyle", "collectionName", "typeField", "typeValue", "edgeCollectionName"):
184
+ for annot in (
185
+ "mappingStyle",
186
+ "collectionName",
187
+ "typeField",
188
+ "typeValue",
189
+ "edgeCollectionName",
190
+ "attributeName",
191
+ ):
168
192
  m_a = re.match(rf':(\w+)\s+phys:{annot}\s+"([^"]+)"', stripped)
169
193
  if m_a:
170
194
  subject = m_a.group(1)
@@ -174,14 +198,8 @@ def turtle_to_mapping(turtle: str) -> MappingBundle:
174
198
  entities_cs: list[dict[str, Any]] = []
175
199
  entities_pm: dict[str, dict[str, Any]] = {}
176
200
  for name in classes:
177
- props = [
178
- {
179
- "name": dp["name"].split("_")[-1] if "_" in dp["name"] else dp["name"],
180
- "type": _from_xsd_type(dp.get("range", "xsd:string")),
181
- }
182
- for dp in data_props
183
- if dp.get("domain") == name
184
- ]
201
+ own = [dp for dp in data_props if name in dp["domains"]]
202
+ props = [{"name": dp["name"], "type": _from_xsd_type(dp.get("range", "xsd:string"))} for dp in own]
185
203
  entities_cs.append({"name": name, "labels": [name], "properties": props})
186
204
  ann = annotations.get(name, {})
187
205
  pm_entry: dict[str, Any] = {
@@ -191,6 +209,13 @@ def turtle_to_mapping(turtle: str) -> MappingBundle:
191
209
  if pm_entry["style"] == "LABEL":
192
210
  pm_entry["typeField"] = ann.get("typeField", "type")
193
211
  pm_entry["typeValue"] = ann.get("typeValue", name)
212
+ fields = {
213
+ dp["name"]: {"field": annotations[dp["name"]]["attributeName"]}
214
+ for dp in own
215
+ if annotations.get(dp["name"], {}).get("attributeName")
216
+ }
217
+ if fields:
218
+ pm_entry["properties"] = fields
194
219
  entities_pm[name] = pm_entry
195
220
 
196
221
  rels_cs: list[dict[str, Any]] = []
@@ -201,11 +226,12 @@ def turtle_to_mapping(turtle: str) -> MappingBundle:
201
226
  to_e = op.get("range", "Any")
202
227
  props = [
203
228
  {
204
- "name": dp["name"].replace(f"{rtype}_", ""),
229
+ # Exactly the `<REL>_` prefix mapping_to_turtle adds, not every occurrence.
230
+ "name": dp["name"].removeprefix(f"{rtype}_"),
205
231
  "type": _from_xsd_type(dp.get("range", "xsd:string")),
206
232
  }
207
233
  for dp in data_props
208
- if dp.get("domain") == rtype
234
+ if rtype in dp["domains"]
209
235
  ]
210
236
  rels_cs.append({"type": rtype, "fromEntity": from_e, "toEntity": to_e, "properties": props})
211
237
  ann = annotations.get(rtype, {})
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "arango-query-core"
3
- version = "0.2.0"
3
+ version = "0.2.1"
4
4
  description = "Shared conceptual-model and NL substrate for ArangoDB query transpilers (arango-cypher-py, arango-sparql-py)"
5
5
  readme = "README.md"
6
6
  license = { file = "LICENSE" }
@@ -189,3 +189,53 @@ class TestImportError:
189
189
  with patch("builtins.__import__", side_effect=mock_import):
190
190
  with pytest.raises(ImportError, match="rdflib is required"):
191
191
  parse_owl_with_rdflib(MINIMAL_OWL)
192
+
193
+
194
+ ANALYZER_OWL = """\
195
+ @prefix : <http://arangodb.com/schema/hybrid#> .
196
+ @prefix owl: <http://www.w3.org/2002/07/owl#> .
197
+ @prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
198
+ @prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
199
+ @prefix phys: <http://arangodb.com/schema/physical#> .
200
+
201
+ :Chunk a owl:Class ; phys:collectionName "chunks" .
202
+ :Document a owl:Class ; phys:collectionName "documents" .
203
+ :Note a owl:Class ; phys:collectionName "notes" .
204
+
205
+ :content a owl:DatatypeProperty ;
206
+ rdfs:domain [ a owl:Class ; owl:unionOf ( :Chunk :Document ) ] .
207
+ :file_name a owl:DatatypeProperty ;
208
+ rdfs:domain :Document ;
209
+ phys:attributeName "file-name" .
210
+ :tag a owl:DatatypeProperty ;
211
+ rdfs:domain :Document , :Note ;
212
+ rdfs:range xsd:string .
213
+ """
214
+
215
+
216
+ def _names(bundle, entity):
217
+ return sorted(
218
+ p["name"]
219
+ for e in bundle.conceptual_schema["entities"]
220
+ if e["name"] == entity
221
+ for p in e["properties"]
222
+ )
223
+
224
+
225
+ def test_a_union_domain_gives_the_field_to_every_member():
226
+ bundle = parse_owl_with_rdflib(ANALYZER_OWL)
227
+ # The anonymous union class is not an entity of its own.
228
+ assert sorted(e["name"] for e in bundle.conceptual_schema["entities"]) == ["Chunk", "Document", "Note"]
229
+ assert _names(bundle, "Chunk") == ["content"]
230
+ assert _names(bundle, "Document") == ["content", "file_name", "tag"]
231
+
232
+
233
+ def test_several_domains_each_get_the_field():
234
+ assert _names(parse_owl_with_rdflib(ANALYZER_OWL), "Note") == ["tag"]
235
+
236
+
237
+ def test_attribute_name_becomes_the_physical_field():
238
+ bundle = parse_owl_with_rdflib(ANALYZER_OWL)
239
+ assert bundle.physical_mapping["entities"]["Document"]["properties"] == {
240
+ "file_name": {"field": "file-name"}
241
+ }
@@ -164,3 +164,91 @@ def test_export_label_style() -> None:
164
164
  assert pm_p["style"] == "LABEL"
165
165
  assert pm_p["typeField"] == "type"
166
166
  assert pm_p["typeValue"] == "Person"
167
+
168
+
169
+ # ---------------------------------------------------------------------------
170
+ # Property names, shared fields and stored-field names (import fixes)
171
+ # ---------------------------------------------------------------------------
172
+
173
+ # The layout arangodb-schema-analyzer >= 0.15.1 writes: a field two classes
174
+ # share has an owl:unionOf domain; a renamed or sanitized field carries its
175
+ # stored name in phys:attributeName.
176
+ ANALYZER_OWL = """\
177
+ @prefix : <http://arangodb.com/schema/hybrid#> .
178
+ @prefix owl: <http://www.w3.org/2002/07/owl#> .
179
+ @prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
180
+ @prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
181
+ @prefix phys: <http://arangodb.com/schema/physical#> .
182
+
183
+ :Chunk a owl:Class ;
184
+ rdfs:label "Chunk" .
185
+ :Chunk phys:mappingStyle "COLLECTION" .
186
+ :Chunk phys:collectionName "chunks" .
187
+
188
+ :Document a owl:Class ;
189
+ rdfs:label "Document" .
190
+ :Document phys:mappingStyle "COLLECTION" .
191
+ :Document phys:collectionName "documents" .
192
+
193
+ :content a owl:DatatypeProperty ;
194
+ rdfs:label "content" ;
195
+ rdfs:domain [ a owl:Class ; owl:unionOf ( :Chunk :Document ) ] .
196
+
197
+ :file_name a owl:DatatypeProperty ;
198
+ rdfs:label "file-name" ;
199
+ rdfs:domain :Document .
200
+ :file_name phys:attributeName "file-name" .
201
+
202
+ :page_count a owl:DatatypeProperty ;
203
+ rdfs:label "page_count" ;
204
+ rdfs:domain :Document ;
205
+ rdfs:range xsd:integer .
206
+ """
207
+
208
+
209
+ def _props(bundle: MappingBundle, entity: str) -> list[str]:
210
+ return [
211
+ p["name"]
212
+ for e in bundle.conceptual_schema["entities"]
213
+ if e["name"] == entity
214
+ for p in e["properties"]
215
+ ]
216
+
217
+
218
+ def test_snake_case_entity_properties_keep_their_names(simple_bundle: MappingBundle) -> None:
219
+ """Regression: every name was cut at its last underscore (file_name -> name)."""
220
+ simple_bundle.conceptual_schema["entities"][0]["properties"].append(
221
+ {"name": "account_id", "type": "string"}
222
+ )
223
+ back = turtle_to_mapping(mapping_to_turtle(simple_bundle))
224
+ assert _props(back, "Person") == ["name", "age", "account_id"]
225
+
226
+
227
+ def test_relationship_properties_lose_only_their_prefix(simple_bundle: MappingBundle) -> None:
228
+ simple_bundle.conceptual_schema["relationships"][0]["properties"] = [
229
+ {"name": "role_name", "type": "string"}
230
+ ]
231
+ back = turtle_to_mapping(mapping_to_turtle(simple_bundle))
232
+ (rel,) = back.conceptual_schema["relationships"]
233
+ assert [p["name"] for p in rel["properties"]] == ["role_name"]
234
+
235
+
236
+ def test_a_field_with_a_union_domain_belongs_to_every_member() -> None:
237
+ bundle = turtle_to_mapping(ANALYZER_OWL)
238
+ assert _props(bundle, "Chunk") == ["content"]
239
+ assert _props(bundle, "Document") == ["content", "file_name", "page_count"]
240
+
241
+
242
+ def test_attribute_name_maps_the_property_to_its_stored_field() -> None:
243
+ bundle = turtle_to_mapping(ANALYZER_OWL)
244
+ assert bundle.physical_mapping["entities"]["Document"]["properties"] == {
245
+ "file_name": {"field": "file-name"}
246
+ }
247
+ assert "properties" not in bundle.physical_mapping["entities"]["Chunk"]
248
+
249
+
250
+ def test_a_property_declared_once_per_domain_reaches_each(simple_bundle: MappingBundle) -> None:
251
+ """mapping_to_turtle writes one block per entity for a shared name."""
252
+ simple_bundle.conceptual_schema["entities"][1]["properties"].append({"name": "name", "type": "string"})
253
+ back = turtle_to_mapping(mapping_to_turtle(simple_bundle))
254
+ assert "name" in _props(back, "Person") and "name" in _props(back, "Movie")
@@ -1,8 +0,0 @@
1
- {
2
- "permissions": {
3
- "allow": [
4
- "Bash(gh run *)",
5
- "Bash(git worktree *)"
6
- ]
7
- }
8
- }