Skip to content

Commit fa07163

Browse files
authored
Merge pull request #223 from OpenSemanticLab/ci/run-ci-on-stage
ci: run Main on pushes to stage and Integration on PRs into stage
2 parents c43060c + 1b55f02 commit fa07163

11 files changed

Lines changed: 155 additions & 14 deletions

File tree

‎.claude-plugin/plugin.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"name": "osl-tasks",
33
"displayName": "OSL task management",
44
"description": "Task and project management for OpenSemanticLab, driven by the osw CLI and MCP server.",
5-
"version": "2.9.0",
5+
"version": "2.9.1",
66
"author": {
77
"name": "OpenSemanticLab"
88
},

‎.github/workflows/integration.yml‎

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,17 @@
22
# INTEGR_TEST_* secrets; where they are unavailable (e.g. forks), the
33
# credential fixtures make the tests skip instead of fail.
44
#
5-
# Runs on every pull request into main, so breaking changes surface before the
6-
# release job (gated separately by required review on the pypi environment)
7-
# ever starts. No push trigger: a push to main is already covered by the PR
8-
# that preceded it, so a second run would add nothing.
5+
# Runs on every pull request into main or stage, so breaking changes surface
6+
# before the release job (gated separately by required review on the pypi
7+
# environment) ever starts. stage collects reviewed pull requests before they
8+
# go to main together. No push trigger: a push to main is already covered by
9+
# the PR that preceded it, so a second run would add nothing.
910

1011
name: Integration
1112

1213
on:
1314
pull_request:
14-
branches: [main]
15+
branches: [main, stage]
1516
workflow_dispatch:
1617

1718
concurrency:

‎.github/workflows/main.yml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ name: Main
22

33
on:
44
push:
5-
branches: [main]
5+
branches: [main, stage]
66
pull_request:
77
types: [opened, synchronize, reopened, ready_for_review]
88
workflow_dispatch:

‎CHANGELOG.md‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,15 @@
1010
1111
<!-- version list -->
1212

13+
## v2.9.1 (2026-11-06)
14+
15+
### Bug Fixes
16+
17+
- **core**: Resolve bare $refs before dropping schema definitions
18+
([#164](https://github.com/OpenSemanticLab/osw-python/pull/164),
19+
[`d86ad31`](https://github.com/OpenSemanticLab/osw-python/commit/d86ad31b9531b027117707bfa159a36f8b5d0694))
20+
21+
1322
## v2.9.0 (2026-11-05)
1423

1524
### Features

‎CITATION.cff‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ message: "If you use this software, please cite it as below."
33
type: software
44
title: "OpenSemanticWorld Python Package (osw-python)"
55
# Managed by python-semantic-release (version_variables). Do not edit by hand.
6-
version: 2.9.0
6+
version: 2.9.1
77
abstract: "Python toolset for data processing, queries, wikicode generation and page manipulation within OpenSemanticLab: work with OpenSemanticLab instances as typed pydantic entities, query with semantic search, generate models from wiki schemas, and write changes back."
88
repository-code: "https://github.com/OpenSemanticLab/osw-python"
99
url: "https://opensemanticlab.github.io/osw-python/"

‎docs/dev.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,8 +66,8 @@ excluded by default:
6666

6767
Integration tests run against a live wiki and need credentials; tests
6868
whose credentials are missing skip instead of fail. CI runs them on every
69-
pull request into `main` (workflow `integration.yml`), so they gate the
70-
release rather than run after it.
69+
pull request into `main` or `stage` (workflow `integration.yml`), so they
70+
gate the release rather than run after it.
7171

7272
=== "make"
7373

‎pyproject.toml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ license-files = ["LICENSE.txt"]
88
requires-python = ">=3.10,<3.14"
99
# Managed by python-semantic-release (see [tool.semantic_release]). Do not
1010
# edit by hand; it is bumped automatically on merge to main.
11-
version = "2.9.0"
11+
version = "2.9.1"
1212
classifiers = [
1313
"Development Status :: 3 - Alpha",
1414
"Intended Audience :: Developers",

‎src/osw/core.py‎

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -485,7 +485,29 @@ def register_schema(self, schema_registration: SchemaRegistration):
485485
else:
486486
result_array.append(subschema)
487487
match.full_path.update_or_create(schema, result_array)
488-
if "definitions" in schema:
488+
489+
all_refs_resolved = True
490+
bare_ref_jsonpath_expr = parse("$..dollarref")
491+
# pydantic only wraps a $ref in allOf when the field carries extra
492+
# metadata (e.g. Field(description=...)). A plain nested-model
493+
# field produces a bare {"$ref": ...} that is not reachable via
494+
# the "$..allOf" jsonpath above, so replace those local
495+
# definitions (#/definitions/...) with embedded definitions too,
496+
# to prevent a dangling ref once "definitions" is removed below
497+
for match in bare_ref_jsonpath_expr.find(schema):
498+
value = match.value
499+
if value.startswith("#"):
500+
definition_jsonpath_expr = parse(
501+
value.replace("#", "$").replace("/", ".")
502+
)
503+
def_matches = definition_jsonpath_expr.find(schema)
504+
for def_match in def_matches:
505+
match.context.full_path.update_or_create(
506+
schema, def_match.value
507+
)
508+
if not def_matches:
509+
all_refs_resolved = False
510+
if "definitions" in schema and all_refs_resolved:
489511
del schema["definitions"]
490512

491513
if "allOf" not in schema:

‎src/osw/skills/osl-tasks/SKILL.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
name: osl-tasks
33
description: Use when importing local todo or note files into an OpenSemanticLab (OSL) wiki as Task entities, or when listing, filtering and updating tasks that already live in OSL. Requires the `osw` CLI or the osw MCP server.
44
metadata:
5-
version: "2.9.0"
5+
version: "2.9.1"
66
---
77

88
# OSL task management
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
"""Unit tests for register_schema()'s $ref post-processing (issue #101).
2+
3+
Regression guard: pydantic v1 only wraps a nested-model $ref in "allOf" when
4+
the field carries extra metadata (e.g. Field(description=...)). A plain
5+
nested-model field produces a bare {"$ref": ...} that the old code never
6+
rewrote before deleting "definitions", leaving a dangling ref behind.
7+
8+
These run fully offline: WtPage.init and WtPage.edit are stubbed so no
9+
network is required, following the pattern in test_store_entity_failure.py.
10+
"""
11+
12+
import uuid
13+
14+
import pytest
15+
from pydantic.v1 import Field
16+
17+
import osw.model.entity as model
18+
from osw.core import OSW
19+
from osw.wtsite import WtPage
20+
21+
22+
class Inner(model.OswBaseModel):
23+
a: str = "x"
24+
25+
26+
class OuterBare(model.OswBaseModel):
27+
"""Nested field without Field(...) metadata: pydantic emits a bare $ref."""
28+
29+
inner: Inner
30+
31+
32+
class OuterAllOf(model.OswBaseModel):
33+
"""Nested field with Field(description=...): pydantic wraps the $ref in allOf."""
34+
35+
inner: Inner = Field(description="the inner thing")
36+
37+
38+
class _FakeMwSite:
39+
host = "example.org"
40+
41+
42+
class _FakeSite:
43+
mw_site = _FakeMwSite()
44+
45+
46+
@pytest.fixture
47+
def offline_osw(monkeypatch):
48+
# no network when a WtPage is constructed with do_init=True; mimic the
49+
# do_init=False branch of WtPage.__init__ which sets .exists
50+
monkeypatch.setattr(WtPage, "init", lambda self: setattr(self, "exists", False))
51+
captured_schemas = []
52+
53+
def fake_edit(self, *args, **kwargs):
54+
captured_schemas.append(self._slots.get("jsonschema"))
55+
56+
monkeypatch.setattr(WtPage, "edit", fake_edit)
57+
return OSW.construct(site=_FakeSite()), captured_schemas
58+
59+
60+
def _register_and_get_schema(offline_osw, model_cls, name):
61+
osw, captured_schemas = offline_osw
62+
osw.register_schema(
63+
OSW.SchemaRegistration(
64+
model_cls=model_cls,
65+
schema_uuid=str(uuid.uuid4()),
66+
schema_name=name,
67+
)
68+
)
69+
return captured_schemas[-1]
70+
71+
72+
def _has_dangling_ref(node):
73+
"""Recursively look for a "dollarref" (register_schema's stand-in for
74+
"$ref") anywhere in the schema. Since register_schema only ever emits
75+
local (#/definitions/...) refs, any leftover one is dangling once
76+
"definitions" is removed.
77+
"""
78+
if isinstance(node, dict):
79+
if "dollarref" in node:
80+
return True
81+
return any(_has_dangling_ref(v) for v in node.values())
82+
if isinstance(node, list):
83+
return any(_has_dangling_ref(v) for v in node)
84+
return False
85+
86+
87+
def test_bare_nested_ref_is_resolved_like_allof(offline_osw):
88+
"""A nested field with no Field(...) metadata must not leave a dangling
89+
ref behind, and must be embedded the same way an allOf-wrapped ref is.
90+
"""
91+
schema = _register_and_get_schema(offline_osw, OuterBare, "OuterBare")
92+
93+
assert not _has_dangling_ref(schema)
94+
assert "definitions" not in schema
95+
assert schema["properties"]["inner"]["title"] == "Inner"
96+
assert schema["properties"]["inner"]["properties"]["a"]["type"] == "string"
97+
98+
99+
def test_allof_wrapped_ref_is_still_resolved(offline_osw):
100+
"""Existing behaviour for allOf-wrapped refs must be unchanged."""
101+
schema = _register_and_get_schema(offline_osw, OuterAllOf, "OuterAllOf")
102+
103+
assert not _has_dangling_ref(schema)
104+
assert "definitions" not in schema
105+
assert schema["properties"]["inner"]["description"] == "the inner thing"
106+
assert schema["properties"]["inner"]["allOf"][0]["title"] == "Inner"
107+
assert (
108+
schema["properties"]["inner"]["allOf"][0]["properties"]["a"]["type"] == "string"
109+
)

0 commit comments

Comments
 (0)