Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 27 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,30 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [0.3.0] - 2026-07-21

### Added

- **Canonical synset keys.** `Synset.key` and `Pointer.target_key` pair an 8-digit offset with a normalized part of speech (e.g. `"00001740n"`), with `normalize_pos` and `make_synset_key` in `glazing.wordnet.types`. Offsets are byte offsets into a per-POS data file and 333 of them name more than one synset in WordNet 3.1, so an offset alone cannot identify a synset. Satellites normalize to `"a"`, matching the POS WordNet records in every pointer to an adjective, so a synset record and any pointer targeting it now produce the same key
- **Converted-data versioning.** The `.initialized` marker records a `DATA_SCHEMA_VERSION`; `glazing init` re-converts when it predates the current version, and importing `glazing` warns when converted data is stale. Previously the marker was empty, so upgraded users silently kept outdated files

### Fixed

- **WordNet adjective satellites** are no longer dropped during conversion; all 10,717 `ss_type="s"` synsets, about 59% of WordNet's adjectives, were discarded while the pointers referencing them were still emitted, leaving ~10,720 dangling references for any consumer following adjective relations (#11)
- **WordNet satellite sense enrichment** now populates `sense_number` and `tag_count` for satellites. Enrichment built a head-less sense key (`lemma%5:LL:II::`), but real satellite keys carry a `head_word:head_id` suffix that the data file does not record; lookups are now keyed on the synset offset from `index.sense`, which is unambiguous where the head-less prefix is not (2,966 of 20,384 satellite keys share one)
- **WordNet synsets sharing an offset across parts of speech** no longer overwrite each other in `WordNetLoader` or raise `ValueError` in `WordNetSearch`, and pointers resolve to the synset they name rather than to whichever synset happened to share the offset
- **WordNet lemmas beginning with a dot or apostrophe** (`.22_caliber`, `'hood`, `'tween`, `'s_Gravenhage`) no longer fail lemma validation, which discarded their entire synsets (7 in total)
- **`glazing search relations --dataset wordnet`** no longer fails with `Unknown format code 'd' for object of type 'str'`; synset IDs were built with `f"{synset.offset:08d}"` against a string offset

### Changed

- **BREAKING: `WordNetLoader.synsets`, `lemma_index`, and the four relation indices are keyed by `SynsetKey`** (e.g. `"00001740n"`) rather than by bare offset. `get_synset` still accepts a bare offset and returns `None` when one is ambiguous; pass `pos` or a canonical key to disambiguate. `WordNetSearch.by_offset` accepts either form
- **Adjective satellites are indexed under both `"s"` and `"a"`**, so a query for adjectives returns heads and satellites together while `ss_type` on the record still reports `"s"`

### Upgrading

Converted data from earlier versions is missing all 10,717 satellites and cannot be repaired in place. Run `glazing init` to re-convert from the already-downloaded source files; no re-download is needed.

## [0.2.3] - 2026-06-25

### Added
Expand Down Expand Up @@ -224,7 +248,9 @@ Initial release of `glazing`, a package containing unified data models and inter
- `tqdm >= 4.60.0` (progress bars)
- `rich >= 13.0.0` (CLI formatting)

[Unreleased]: https://github.com/factslab/glazing/compare/v0.2.2...HEAD
[Unreleased]: https://github.com/factslab/glazing/compare/v0.3.0...HEAD
[0.3.0]: https://github.com/factslab/glazing/releases/tag/v0.3.0
[0.2.3]: https://github.com/factslab/glazing/releases/tag/v0.2.3
[0.2.2]: https://github.com/factslab/glazing/releases/tag/v0.2.2
[0.2.1]: https://github.com/factslab/glazing/releases/tag/v0.2.1
[0.2.0]: https://github.com/factslab/glazing/releases/tag/v0.2.0
Expand Down
2 changes: 1 addition & 1 deletion docs/api/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ except ValidationError as e:

## Version Compatibility

This documentation covers glazing version 0.2.3. Check your installed version:
This documentation covers glazing version 0.3.0. Check your installed version:

```python
import glazing
Expand Down
8 changes: 4 additions & 4 deletions docs/citation.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,22 +12,22 @@ If you use Glazing in your research, please cite our work.
title = {Glazing: Unified Data Models and Interfaces for Syntactic and Semantic Frame Ontologies},
year = {2025},
url = {https://github.com/factslab/glazing},
version = {0.2.3},
version = {0.3.0},
doi = {10.5281/zenodo.17467082}
}
```

### APA

White, A. S. (2025). *Glazing: Unified Data Models and Interfaces for Syntactic and Semantic Frame Ontologies* (Version 0.2.3) [Computer software]. https://github.com/factslab/glazing
White, A. S. (2025). *Glazing: Unified Data Models and Interfaces for Syntactic and Semantic Frame Ontologies* (Version 0.3.0) [Computer software]. https://github.com/factslab/glazing

### Chicago

White, Aaron Steven. 2025. *Glazing: Unified Data Models and Interfaces for Syntactic and Semantic Frame Ontologies*. Version 0.2.3. https://github.com/factslab/glazing.
White, Aaron Steven. 2025. *Glazing: Unified Data Models and Interfaces for Syntactic and Semantic Frame Ontologies*. Version 0.3.0. https://github.com/factslab/glazing.

### MLA

White, Aaron Steven. *Glazing: Unified Data Models and Interfaces for Syntactic and Semantic Frame Ontologies*. Version 0.2.3, 2025, https://github.com/factslab/glazing.
White, Aaron Steven. *Glazing: Unified Data Models and Interfaces for Syntactic and Semantic Frame Ontologies*. Version 0.3.0, 2025, https://github.com/factslab/glazing.

## Citing Datasets

Expand Down
2 changes: 1 addition & 1 deletion docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ If you use Glazing in your research, please cite:
title = {Glazing: Unified Data Models and Interfaces for Syntactic and Semantic Frame Ontologies},
year = {2025},
url = {https://github.com/factslab/glazing},
version = {0.2.3},
version = {0.3.0},
doi = {10.5281/zenodo.17467082}
}
```
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"

[project]
name = "glazing"
version = "0.2.3"
version = "0.3.0"
description = "Unified data models and interfaces for syntactic and semantic frame ontologies"
readme = "README.md"
requires-python = ">=3.13"
Expand Down
12 changes: 10 additions & 2 deletions src/glazing/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
import warnings

from glazing.__version__ import __version__, __version_info__
from glazing.initialize import check_initialization, get_default_data_dir
from glazing.initialize import check_initialization, get_default_data_dir, is_data_current


def _check_initialization() -> None:
Expand All @@ -47,15 +47,23 @@ def _check_initialization() -> None:
):
return

data_dir = get_default_data_dir()
if not check_initialization():
data_dir = get_default_data_dir()
warnings.warn(
f"\nGlazing datasets not initialized.\n"
f"Run 'glazing init' to download and convert all datasets.\n"
f"Data will be stored in: {data_dir}",
UserWarning,
stacklevel=2,
)
elif not is_data_current():
warnings.warn(
f"\nGlazing datasets were converted by an older version and are out of date.\n"
f"Run 'glazing init' to re-convert them from the downloaded source files.\n"
f"Data location: {data_dir}",
UserWarning,
stacklevel=2,
)


# Check initialization on import (can be disabled via env var)
Expand Down
2 changes: 1 addition & 1 deletion src/glazing/__version__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
"""Version information for the glazing package."""

__version__ = "0.2.3"
__version__ = "0.3.0"
__version_info__ = tuple(int(i) for i in __version__.split("."))
2 changes: 1 addition & 1 deletion src/glazing/cli/search.py
Original file line number Diff line number Diff line change
Expand Up @@ -763,7 +763,7 @@ def search_relations(
table.add_column("Definition", style="white", no_wrap=False)

for synset in synsets[:20]:
synset_id = f"{synset.offset:08d}{synset.ss_type}"
synset_id = synset.key
words = ", ".join(w.lemma for w in synset.words[:3])
if len(synset.words) > 3:
words += f" (+{len(synset.words) - 3})"
Expand Down
71 changes: 66 additions & 5 deletions src/glazing/initialize.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

from __future__ import annotations

import json
import os
import sys
from pathlib import Path
Expand All @@ -24,6 +25,58 @@
from glazing.verbnet.converter import VerbNetConverter
from glazing.wordnet.converter import WordNetConverter

# Version of the converted-data layout. Bump whenever a converter change makes
# previously converted files stale, so that `glazing init` re-converts rather
# than leaving users on silently outdated data.
#
# 2: WordNet adjective satellites are retained (previously all 10,717 were
# dropped) and satellite senses are enriched.
DATA_SCHEMA_VERSION = 2


def _read_schema_version(marker_file: Path) -> int | None:
"""Read the data schema version recorded in an initialization marker.

Parameters
----------
marker_file : Path
Path to the ``.initialized`` marker.

Returns
-------
int | None
Recorded schema version, or None if absent or unreadable. Markers
written before versioning was introduced are empty and read as None.
"""
try:
payload = json.loads(marker_file.read_text(encoding="utf-8"))
except (OSError, json.JSONDecodeError):
return None
version = payload.get("schema_version") if isinstance(payload, dict) else None
return version if isinstance(version, int) else None


def is_data_current(data_dir: Path | None = None) -> bool:
"""Check whether converted data matches the current schema version.

Parameters
----------
data_dir : Path | None
Data directory to check. If None, uses default.

Returns
-------
bool
True if the marker records the current schema version.
"""
if data_dir is None:
data_dir = get_default_data_dir()

marker_file = Path(data_dir) / ".initialized"
if not marker_file.exists():
return False
return _read_schema_version(marker_file) == DATA_SCHEMA_VERSION


def get_default_data_dir() -> Path:
"""Get the default data directory for glazing.
Expand Down Expand Up @@ -260,12 +313,17 @@ def initialize_datasets(
data_dir = Path(data_dir)
data_dir.mkdir(parents=True, exist_ok=True)

# Check if already initialized (unless force is True)
# Check if already initialized at the current schema version. Data
# converted by an older glazing is stale even though the marker exists,
# so re-convert rather than leaving the user on outdated files.
marker_file = data_dir / ".initialized"
if marker_file.exists() and not force:
if _read_schema_version(marker_file) == DATA_SCHEMA_VERSION:
if verbose:
click.echo("Datasets already initialized. Use --force to re-download.")
return True
if verbose:
click.echo("Datasets already initialized. Use --force to re-download.")
return True
click.echo("Converted data predates the current schema; re-converting.")

if verbose:
click.echo(f"Initializing glazing datasets in {data_dir}")
Expand All @@ -276,9 +334,12 @@ def initialize_datasets(
results = [_process_dataset(name, data_dir, verbose) for name in datasets]
success = all(results)

# Create marker file
# Create marker file recording the schema the data was converted against
if success:
marker_file.touch()
marker_file.write_text(
json.dumps({"schema_version": DATA_SCHEMA_VERSION}),
encoding="utf-8",
)
if verbose:
click.echo("\n" + "=" * 60)
click.echo("✅ All datasets successfully initialized!")
Expand Down
24 changes: 13 additions & 11 deletions src/glazing/search.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

from dataclasses import dataclass
from pathlib import Path
from typing import cast

from glazing.framenet.loader import FrameNetLoader
from glazing.framenet.models import Frame
Expand Down Expand Up @@ -36,6 +37,7 @@
from glazing.wordnet.models import Synset
from glazing.wordnet.search import WordNetSearch
from glazing.wordnet.symbol_parser import filter_by_relation_type
from glazing.wordnet.types import WordNetPOS, normalize_pos


@dataclass
Expand Down Expand Up @@ -395,20 +397,20 @@ def _convert_pos_for_wordnet(self, pos: str | None) -> str | None:
return None

pos_lower = pos.lower()
pos_map = {
"v": "v",
spelled_out: dict[str, WordNetPOS] = {
"verb": "v",
"n": "n",
"noun": "n",
"a": "a",
"adj": "a",
"adjective": "a",
"s": "a",
"r": "r",
"adv": "r",
"adverb": "r",
}
return pos_map.get(pos_lower)
if pos_lower in spelled_out:
return spelled_out[pos_lower]
if pos_lower in ("n", "v", "a", "r", "s"):
# normalize_pos folds satellites ("s") into "a".
return normalize_pos(cast(WordNetPOS, pos_lower))
return None

def _search_propbank_by_lemma(self, lemma: str) -> tuple[list[Frameset], list[Roleset]]:
"""Search PropBank by lemma.
Expand Down Expand Up @@ -681,7 +683,7 @@ def search(self, query: str) -> list[SearchResult]:
if self.wordnet:
synsets = self.wordnet.by_lemma(query)
for synset in synsets:
synset_id = f"{synset.offset:08d}{synset.ss_type}"
synset_id = synset.key
results.append(
SearchResult(
dataset="wordnet",
Expand Down Expand Up @@ -1165,9 +1167,9 @@ def load_wordnet_from_jsonl(self, synsets_path: str, _index_path: str, _pos: str
# Merge synsets with existing ones
existing_synsets = self.wordnet.get_all_synsets()
# Create a dict to merge by offset to avoid duplicates
synset_dict = {f"{s.offset:08d}{s.ss_type}": s for s in existing_synsets}
synset_dict = {s.key: s for s in existing_synsets}
for synset in synsets:
synset_id = f"{synset.offset:08d}{synset.ss_type}"
synset_id = synset.key
synset_dict[synset_id] = synset
# Recreate WordNetSearch with merged synsets
self.wordnet = WordNetSearch(list(synset_dict.values()))
Expand Down Expand Up @@ -1230,7 +1232,7 @@ def search_with_fuzzy( # noqa: C901, PLR0912
for word in synset.words:
similarity = levenshtein_ratio(query_normalized, word.lemma.lower())
if similarity >= fuzzy_threshold:
synset_id = f"{synset.offset:08d}{synset.ss_type}"
synset_id = synset.key
results.append(
SearchResult(
dataset="wordnet",
Expand Down
7 changes: 4 additions & 3 deletions src/glazing/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -161,9 +161,10 @@
VERBNET_KEY_PATTERN = r"^[a-z_-]+#\d+$" # e.g., "give#2"

# Name validation patterns
LEMMA_PATTERN = (
r"^[a-zA-Z0-9][a-zA-Z0-9_\'\-\.\/]*$" # Word lemmas (incl. proper nouns, abbreviations)
)
# Word lemmas (incl. proper nouns and abbreviations). A single leading dot or
# apostrophe is allowed because WordNet lemmatizes calibre terms as
# ".22_caliber" and elided forms as "'hood", "'tween", "'s_Gravenhage".
LEMMA_PATTERN = r"^['.]?[a-zA-Z0-9][a-zA-Z0-9_\'\-\.\/]*$"

# Color validation for FrameNet
HEX_COLOR_PATTERN = r"^#?[0-9A-Fa-f]{6}$" # 6-digit hex color with optional # prefix
Expand Down
Loading
Loading