Skip to content

gate-54 relation-dialect: property_ids never includes items.properties.* — a correct nested relation is reported as misplaced #231

Description

@rubenvdlinde

Rule-6 check (c) reports a correctly shaped relation property as "placed off a property" whenever it is nested inside an array-of-objects.

Measured

Full-tree run against ConductionNL/larpingapp@development:

lib/Settings/larpingapp_register.json: x-relation-filter is placed off a property
(inside items / an x-* block / non-property node) — it rides only on the relation
property itself (ADR-062 rule 6)

The flagged node is character.skillOverrides.items.properties.skill:

{
  "type": "string",
  "format": "uuid",
  "$ref": "skill",
  "x-relation-filter": { "setting": "@object.setting" },
  "description": "The assigned skill whose requirements are waived",
  "title": "Skill"
}

That is the canonical dialect exactly — type + format: uuid + $ref + the filter riding on the same property. It is byte-for-byte the same shape as character.skills twenty lines above, which the gate accepts. The only difference is nesting depth.

Cause — call path

scripts/lib/check_relation_dialect.py:

  1. property_ids is populated in a single, non-recursive loop (~line 372-388):
property_ids = set()
for sname, schema in schemas.items():
    props = schema.get("properties")
    if not isinstance(props, dict):
        continue
    ...
    for pname, prop in props.items():
        ...
        property_ids.add(id(prop))

It only ever walks schema.properties.*. Nothing descends into properties.<p>.items.properties.*.

  1. _raw_walk() (~line 470) recurses into everything:
def _raw_walk(node, path, property_ids, findings):
    if isinstance(node, dict):
        ...
        if "x-relation-filter" in node:
            if id(node) not in property_ids:
                findings.append((path, "... placed off a property (inside items / an x-* block / non-property node) ..."))

So any nested relation property is structurally unrepresentable to check (c): _raw_walk finds it, property_ids can never contain it, and the finding is unconditional.

Why this one bites

The finding is unfixable in the app. The three ways to clear it are all wrong:

  • move the filter off the property — that is the actual rule-6 violation;
  • flatten skillOverrides from an array-of-objects to something else — a schema redesign to satisfy a linter;
  • delete the filter — loses the setting-scoped picker.

So it sits as permanent red. I have left it unfixed in ConductionNL/larpingapp#289 and documented it there rather than mangling correct schema.

Suggested fix

Collect property_ids recursively through items (and items.properties), mirroring what _raw_walk already traverses. Roughly: after adding id(prop), if prop.get("items") is a dict with a properties dict, add each of those too — and recurse, since arrays of objects can nest.

The other checks in the helper ((b) relation-shape, (f) $ref resolution) have the same blind spot for nested properties, so a nested relation missing its $ref is currently not reported — the gate is simultaneously false-positive on shape and false-negative on the thing that matters. Worth fixing both with one recursive collector.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions