Skip to content

fix(gate-54): a relation nested in an array-of-objects is still a property - #241

Closed
rubenvdlinde wants to merge 1 commit into
mainfrom
fix/gate-54-nested-relation-properties
Closed

fix(gate-54): a relation nested in an array-of-objects is still a property#241
rubenvdlinde wants to merge 1 commit into
mainfrom
fix/gate-54-nested-relation-properties

Conversation

@rubenvdlinde

Copy link
Copy Markdown
Contributor

Closes #231.

gate-54 check (c) reported a correctly shaped relation as "placed off a property" whenever it sat inside an array of objects. The finding was unfixable in the app — every way to clear it damages correct schema — so it sat as permanent red on any repo with that (common) shape.

The finding

ConductionNL/larpingapp@development, character.skillOverrides.items.properties.skill:

{ "type": "string", "format": "uuid", "$ref": "skill",
  "x-relation-filter": { "setting": "@object.setting" }, "title": "Skill" }

Byte-for-byte the shape the gate accepts twenty lines above on character.skills. The only difference is nesting depth.

Cause

Check (c) works by identity. _raw_walk() flags every x-relation-filter whose containing dict is not in property_ids, and property_ids was built in a single non-recursive pass:

for sname, schema in schemas.items():
    props = schema.get("properties")
    ...
    for pname, prop in props.items():
        property_ids.add(id(prop))

Nothing descended into properties.<p>.items.properties.*, so a nested property could never be in the set and the finding was unconditional.

Fix

_collect_nested_property_ids() registers property dicts below a top-level property, through items.properties and inline properties, bounded at depth 8. A register file is JSON so it cannot contain a cycle; the realistic hazard is pathological depth, and past the bound the walk over-reports rather than under-reports — the safe direction.

New test suite

scripts/lib/test_check_relation_dialect.py. The helper had no suite, so tests/run-helper-suites.sh (which globs scripts/lib/test_*) will now execute one.

Every accepted case is paired with an arm that must still be reported, so the suite cannot pass against a checker that returns nothing — the failure mode a checker test most easily falls into:

# case expectation
1 top-level relation accepted (harness positive control)
2 relation in items.properties accepted — the regression
3 filter on a non-property items node still reported
4 filter in a schema-level x-* block still reported
5 relation two array levels deep accepted
6 pathological depth terminates, over-reports
test_check_relation_dialect: 6 passed, 0 failed

Can-fail proof. Removing the _collect_nested_property_ids() call:

test_check_relation_dialect: 4 passed, 2 failed
  FAIL relation inside items.properties is accepted (gate-54 nested FP)
       — got [... x-relation-filter is placed off a property (inside items / an x-* block / non-property node) ...]
  FAIL relation nested two array levels deep is accepted
       — got [... same text ...]

Restoring it returns 6 passed, 0 failed.

Measured on real trees, not just fixtures

larpingapp full-tree gate-54: 4 findings → 3, and its own lib/Settings/larpingapp_register.json goes clean. The three that remain are genuine rule-7 findings in register.d/ fragments, two of which are fixed in ConductionNL/larpingapp#289.

One thing worth knowing about rule 7 while you are here

Applying rule 7's remediation (add the canonical $ref) to character.ownerRef in larpingapp broke the character create dialog: character.ocName already declares $ref: "player", and with a second player-targeted relation on the same schema the dialog stopped rendering a player selector entirely. Reproduced on two independent CI runs (detail-forms-admin.spec.ts:570, 1 failed / 170 passed) and green on development. Not addressed here — flagging it because rule 7 currently reads as unconditionally safe and is not.

Also noted while measuring, filed separately: #230 (gate-58 matches comment lines), #232 (gate-38 scopes ISettings fragments), #233 (gate-60 reports 43 confident FAILs when node_modules is absent), #238 (gate-53 never joins src/registry.js).

…perty

Check (c) reported a CORRECTLY shaped relation as "placed off a property"
whenever it sat inside an array of objects. Observed on larpingapp:

  character.skillOverrides.items.properties.skill
  { "type": "string", "format": "uuid", "$ref": "skill",
    "x-relation-filter": { "setting": "@object.setting" } }

That is byte-for-byte the shape the gate accepts one level up on
character.skills. The only difference is nesting depth.

Cause: check (c) works by identity. _raw_walk() flags every x-relation-filter
whose containing dict is not in property_ids, and property_ids was built from
schema["properties"] in a single non-recursive pass. A nested property could
therefore never be in the set, so the finding was unconditional — and
unfixable in the app: moving the filter off the property IS the rule-6
violation, flattening the array is a schema redesign to satisfy a linter, and
deleting the filter loses the setting-scoped picker.

_collect_nested_property_ids() registers property dicts below a top-level
property, through items.properties and inline properties, bounded at depth 8
(a register file is JSON so it cannot cycle; the hazard is pathological depth,
and past the bound the walk over-reports rather than under-reports).

test_check_relation_dialect.py is new — the helper had no suite, so
run-helper-suites.sh will now execute one. Every accepted case is paired with
an arm that must still be REPORTED, so the suite cannot pass against a checker
that returns nothing:

  1 top-level relation accepted            (harness positive control)
  2 relation in items.properties accepted  (the regression)
  3 filter on a non-property items node    STILL REPORTED
  4 filter in a schema-level x-* block     STILL REPORTED
  5 relation two array levels deep accepted
  6 pathological depth terminates and over-reports

Can-fail proof: removing the _collect_nested_property_ids() call turns cases 2
and 5 red with the exact 'placed off a property' text; restoring it returns
the suite to 6 passed, 0 failed.

Measured on real trees — larpingapp full-tree gate-54 drops from 4 findings to
3, and its own lib/Settings/larpingapp_register.json goes clean; the remaining
findings are genuine ones in register.d/ fragments.

Closes #231
@rubenvdlinde

Copy link
Copy Markdown
Contributor Author

Superseded by #255.

This branch fixed only the false-positive half: it added nested property ids to property_ids but never iterated them, so checks (b) relation-shape, (d) filter tokens and (f) $ref resolution stayed blind to nested properties — the false negative #231 explicitly asks to close stayed open. Its test file also contained no runnable def test_.

#255 uses one recursive collector that feeds all the checks, keeps this branch's depth-cap fail-safe assertion (carried over as a test), and ships 24 mutation-checked tests.

@rubenvdlinde
rubenvdlinde deleted the fix/gate-54-nested-relation-properties branch August 8, 2026 13:49
@rubenvdlinde
rubenvdlinde restored the fix/gate-54-nested-relation-properties branch August 8, 2026 13:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

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

1 participant