deontic 0.1.0__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.
- deontic-0.1.0/LICENSE +21 -0
- deontic-0.1.0/PKG-INFO +86 -0
- deontic-0.1.0/README.md +65 -0
- deontic-0.1.0/pyproject.toml +45 -0
- deontic-0.1.0/setup.cfg +4 -0
- deontic-0.1.0/src/deontic/__init__.py +32 -0
- deontic-0.1.0/src/deontic/dictionary.py +203 -0
- deontic-0.1.0/src/deontic/errors.py +96 -0
- deontic-0.1.0/src/deontic/hooks.py +100 -0
- deontic-0.1.0/src/deontic/lexer.py +194 -0
- deontic-0.1.0/src/deontic/parser.py +744 -0
- deontic-0.1.0/src/deontic/resolve.py +261 -0
- deontic-0.1.0/src/deontic.egg-info/PKG-INFO +86 -0
- deontic-0.1.0/src/deontic.egg-info/SOURCES.txt +18 -0
- deontic-0.1.0/src/deontic.egg-info/dependency_links.txt +1 -0
- deontic-0.1.0/src/deontic.egg-info/requires.txt +4 -0
- deontic-0.1.0/src/deontic.egg-info/top_level.txt +1 -0
- deontic-0.1.0/tests/test_conformance.py +498 -0
- deontic-0.1.0/tests/test_packaging.py +31 -0
- deontic-0.1.0/tests/test_parser.py +94 -0
deontic-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Mikko Ahonen
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
deontic-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: deontic
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A constraint language for obligation, permission and prohibition
|
|
5
|
+
Author: Mikko Ahonen
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/mikko-ahonen/deontic
|
|
8
|
+
Project-URL: Source, https://github.com/mikko-ahonen/deontic
|
|
9
|
+
Keywords: deontic,constraints,policy,obligation,permission
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
13
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
14
|
+
Requires-Python: >=3.12
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
License-File: LICENSE
|
|
17
|
+
Provides-Extra: dev
|
|
18
|
+
Requires-Dist: pytest; extra == "dev"
|
|
19
|
+
Requires-Dist: jsonschema; extra == "dev"
|
|
20
|
+
Dynamic: license-file
|
|
21
|
+
|
|
22
|
+
# deontic
|
|
23
|
+
|
|
24
|
+
A constraint language for **obligation**, **permission** and **prohibition** —
|
|
25
|
+
the things a system must do, may do, and must not do.
|
|
26
|
+
|
|
27
|
+
> **Status: alpha.** The grammar is implemented and reproduces the whole
|
|
28
|
+
> conformance corpus: `deontic.parse` gives the AST, `deontic.load` reads a
|
|
29
|
+
> dictionary and `deontic.resolve` checks a sentence against it. There is no
|
|
30
|
+
> evaluator in this package, by design; the corpus's `evaluate/` cases are
|
|
31
|
+
> for implementations that add one.
|
|
32
|
+
|
|
33
|
+
## Why a language rather than a library
|
|
34
|
+
|
|
35
|
+
Deontic statements are usually buried in imperative checks, where the rule and
|
|
36
|
+
the enforcement of the rule are the same code. Writing them down as data makes
|
|
37
|
+
them reviewable by the people who own the policy rather than only by the people
|
|
38
|
+
who own the codebase, and lets more than one evaluator agree on what a rule
|
|
39
|
+
means.
|
|
40
|
+
|
|
41
|
+
## Design
|
|
42
|
+
|
|
43
|
+
[docs/design.md](docs/design.md) states what the language is meant to be:
|
|
44
|
+
the closed-class vocabulary, the shapes, filters and paths, attestation,
|
|
45
|
+
the storage form, the interpretation rules, the outcome model, and how it
|
|
46
|
+
relates to Attempto Controlled English and to deontic logic.
|
|
47
|
+
|
|
48
|
+
## Conformance
|
|
49
|
+
|
|
50
|
+
`conformance/` is the normative test data: source text plus the result every
|
|
51
|
+
implementation must agree on, kept as data rather than Python tests so that an
|
|
52
|
+
implementation in another language can consume it too. An implementation is
|
|
53
|
+
conformant when it reproduces the corpus. It has three parts — `parse/`
|
|
54
|
+
(source to AST, no lexicon needed), `reject/` (source to error) and
|
|
55
|
+
`evaluate/` (sentences, a world and a date to outcomes) — over one invented
|
|
56
|
+
fixture lexicon, and its README records the language decisions the cases fix.
|
|
57
|
+
|
|
58
|
+
## Use
|
|
59
|
+
|
|
60
|
+
```python
|
|
61
|
+
import deontic
|
|
62
|
+
|
|
63
|
+
ast = deontic.parse("Every $exhibit$ tagged fragile must @be located in@ a $gallery$ tagged storage.")
|
|
64
|
+
# {'shape': 'obligation', 'subject': {...}, 'predicate': {...}}
|
|
65
|
+
|
|
66
|
+
lexicon = deontic.load("conformance/lexicon.json")
|
|
67
|
+
deontic.resolve(ast, lexicon) # raises a ResolutionError with the corpus's error code otherwise
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Sentences are authored as English and stored with markers on the typed
|
|
71
|
+
spans (`$Term$`, `@verb@`, `#90 days#`, `<parameter>`); editors write the
|
|
72
|
+
markers, people do not. A verb's meaning is a `pattern` in the dictionary
|
|
73
|
+
or, failing that, code under the contract in [docs/hooks.md](docs/hooks.md).
|
|
74
|
+
|
|
75
|
+
## Install
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
pip install deontic
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Stdlib only, no runtime dependencies — a grammar meant to be implemented by
|
|
82
|
+
other people should not put a parser generator in everyone's dependency tree.
|
|
83
|
+
|
|
84
|
+
## License
|
|
85
|
+
|
|
86
|
+
MIT.
|
deontic-0.1.0/README.md
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# deontic
|
|
2
|
+
|
|
3
|
+
A constraint language for **obligation**, **permission** and **prohibition** —
|
|
4
|
+
the things a system must do, may do, and must not do.
|
|
5
|
+
|
|
6
|
+
> **Status: alpha.** The grammar is implemented and reproduces the whole
|
|
7
|
+
> conformance corpus: `deontic.parse` gives the AST, `deontic.load` reads a
|
|
8
|
+
> dictionary and `deontic.resolve` checks a sentence against it. There is no
|
|
9
|
+
> evaluator in this package, by design; the corpus's `evaluate/` cases are
|
|
10
|
+
> for implementations that add one.
|
|
11
|
+
|
|
12
|
+
## Why a language rather than a library
|
|
13
|
+
|
|
14
|
+
Deontic statements are usually buried in imperative checks, where the rule and
|
|
15
|
+
the enforcement of the rule are the same code. Writing them down as data makes
|
|
16
|
+
them reviewable by the people who own the policy rather than only by the people
|
|
17
|
+
who own the codebase, and lets more than one evaluator agree on what a rule
|
|
18
|
+
means.
|
|
19
|
+
|
|
20
|
+
## Design
|
|
21
|
+
|
|
22
|
+
[docs/design.md](docs/design.md) states what the language is meant to be:
|
|
23
|
+
the closed-class vocabulary, the shapes, filters and paths, attestation,
|
|
24
|
+
the storage form, the interpretation rules, the outcome model, and how it
|
|
25
|
+
relates to Attempto Controlled English and to deontic logic.
|
|
26
|
+
|
|
27
|
+
## Conformance
|
|
28
|
+
|
|
29
|
+
`conformance/` is the normative test data: source text plus the result every
|
|
30
|
+
implementation must agree on, kept as data rather than Python tests so that an
|
|
31
|
+
implementation in another language can consume it too. An implementation is
|
|
32
|
+
conformant when it reproduces the corpus. It has three parts — `parse/`
|
|
33
|
+
(source to AST, no lexicon needed), `reject/` (source to error) and
|
|
34
|
+
`evaluate/` (sentences, a world and a date to outcomes) — over one invented
|
|
35
|
+
fixture lexicon, and its README records the language decisions the cases fix.
|
|
36
|
+
|
|
37
|
+
## Use
|
|
38
|
+
|
|
39
|
+
```python
|
|
40
|
+
import deontic
|
|
41
|
+
|
|
42
|
+
ast = deontic.parse("Every $exhibit$ tagged fragile must @be located in@ a $gallery$ tagged storage.")
|
|
43
|
+
# {'shape': 'obligation', 'subject': {...}, 'predicate': {...}}
|
|
44
|
+
|
|
45
|
+
lexicon = deontic.load("conformance/lexicon.json")
|
|
46
|
+
deontic.resolve(ast, lexicon) # raises a ResolutionError with the corpus's error code otherwise
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Sentences are authored as English and stored with markers on the typed
|
|
50
|
+
spans (`$Term$`, `@verb@`, `#90 days#`, `<parameter>`); editors write the
|
|
51
|
+
markers, people do not. A verb's meaning is a `pattern` in the dictionary
|
|
52
|
+
or, failing that, code under the contract in [docs/hooks.md](docs/hooks.md).
|
|
53
|
+
|
|
54
|
+
## Install
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
pip install deontic
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Stdlib only, no runtime dependencies — a grammar meant to be implemented by
|
|
61
|
+
other people should not put a parser generator in everyone's dependency tree.
|
|
62
|
+
|
|
63
|
+
## License
|
|
64
|
+
|
|
65
|
+
MIT.
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# The deontic constraint language — public grammar, AST and conformance corpus.
|
|
2
|
+
#
|
|
3
|
+
# This repository is maintained by hand rather than generated from a project
|
|
4
|
+
# template, because it is public and the templates are not.
|
|
5
|
+
#
|
|
6
|
+
# Published to pypi.org from GitHub Actions via trusted publishing (OIDC), so
|
|
7
|
+
# no API token exists at rest anywhere.
|
|
8
|
+
[build-system]
|
|
9
|
+
requires = ["setuptools>=69"]
|
|
10
|
+
build-backend = "setuptools.build_meta"
|
|
11
|
+
|
|
12
|
+
[project]
|
|
13
|
+
name = "deontic"
|
|
14
|
+
version = "0.1.0"
|
|
15
|
+
description = "A constraint language for obligation, permission and prohibition"
|
|
16
|
+
readme = "README.md"
|
|
17
|
+
requires-python = ">=3.12"
|
|
18
|
+
license = "MIT"
|
|
19
|
+
authors = [{ name = "Mikko Ahonen" }]
|
|
20
|
+
keywords = ["deontic", "constraints", "policy", "obligation", "permission"]
|
|
21
|
+
classifiers = [
|
|
22
|
+
"Development Status :: 3 - Alpha",
|
|
23
|
+
"Intended Audience :: Developers",
|
|
24
|
+
"Programming Language :: Python :: 3.12",
|
|
25
|
+
"Topic :: Software Development :: Libraries",
|
|
26
|
+
]
|
|
27
|
+
# Stdlib only. A grammar other people are meant to implement should not drag a
|
|
28
|
+
# parser generator into every consumer's dependency tree.
|
|
29
|
+
dependencies = []
|
|
30
|
+
|
|
31
|
+
[project.optional-dependencies]
|
|
32
|
+
dev = ["pytest", "jsonschema"]
|
|
33
|
+
|
|
34
|
+
[project.urls]
|
|
35
|
+
Homepage = "https://github.com/mikko-ahonen/deontic"
|
|
36
|
+
Source = "https://github.com/mikko-ahonen/deontic"
|
|
37
|
+
|
|
38
|
+
[tool.setuptools]
|
|
39
|
+
package-dir = { "" = "src" }
|
|
40
|
+
|
|
41
|
+
[tool.setuptools.packages.find]
|
|
42
|
+
where = ["src"]
|
|
43
|
+
|
|
44
|
+
[tool.pytest.ini_options]
|
|
45
|
+
testpaths = ["tests"]
|
deontic-0.1.0/setup.cfg
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""The deontic constraint language.
|
|
2
|
+
|
|
3
|
+
`parse` turns one sentence into the AST the conformance corpus fixes
|
|
4
|
+
(conformance/ast.schema.json); `load` reads a dictionary; `resolve` checks a
|
|
5
|
+
parsed sentence against it. `deontic.hooks` is the contract between a
|
|
6
|
+
lexicon's code half and any implementation. No evaluator lives here.
|
|
7
|
+
|
|
8
|
+
The version is read from installed metadata rather than written as a literal,
|
|
9
|
+
so the value in a published wheel and the value this module reports cannot
|
|
10
|
+
disagree.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from importlib.metadata import PackageNotFoundError, version as _version
|
|
14
|
+
|
|
15
|
+
try:
|
|
16
|
+
__version__ = _version("deontic")
|
|
17
|
+
except PackageNotFoundError: # running from a source tree, not installed
|
|
18
|
+
__version__ = "0.0.0+unknown"
|
|
19
|
+
|
|
20
|
+
from .dictionary import Dictionary, DictionaryError, load # noqa: E402
|
|
21
|
+
from .errors import DeonticError, LexicalError, ResolutionError # noqa: E402
|
|
22
|
+
from .parser import parse # noqa: E402
|
|
23
|
+
from .resolve import Scope, resolve # noqa: E402
|
|
24
|
+
from .hooks import ( # noqa: E402
|
|
25
|
+
ENTRY_POINT_GROUP, Attestation, Context, Entity, Hook, Lexicon, Relation, Witness, World,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
__all__ = [
|
|
29
|
+
"__version__", "parse", "resolve", "load", "Dictionary", "DictionaryError", "Scope",
|
|
30
|
+
"DeonticError", "LexicalError", "ResolutionError", "ENTRY_POINT_GROUP", "Attestation", "Context", "Entity", "Hook",
|
|
31
|
+
"Lexicon", "Relation", "Witness", "World",
|
|
32
|
+
]
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
"""Loading a dictionary (conformance/dictionary.schema.json) into a form the
|
|
2
|
+
resolver can query: every declared name with its role, field kinds, tag
|
|
3
|
+
sets, relations by possessive word, verbs with their patterns.
|
|
4
|
+
|
|
5
|
+
Loading checks what the JSON Schema cannot: that references point at
|
|
6
|
+
declared types, that a pattern names real fields of the right kind, and
|
|
7
|
+
that no name is declared in two roles.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import json
|
|
13
|
+
from dataclasses import dataclass, field
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Any
|
|
16
|
+
|
|
17
|
+
SCALARS = ("text", "number", "date", "boolean")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class DictionaryError(ValueError):
|
|
21
|
+
pass
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def field_kind(f: Any) -> str:
|
|
25
|
+
return f if isinstance(f, str) else f["kind"]
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass
|
|
29
|
+
class Dictionary:
|
|
30
|
+
data: dict
|
|
31
|
+
types: dict[str, dict] = field(default_factory=dict)
|
|
32
|
+
roles: dict[str, str] = field(default_factory=dict) # name (lower) → type | plural | event | cadence | metric | attester | attester_plural | term | reference
|
|
33
|
+
canonical: dict[str, str] = field(default_factory=dict) # name (lower) → declared spelling
|
|
34
|
+
plural_of: dict[str, str] = field(default_factory=dict) # plural (lower) → singular type
|
|
35
|
+
verbs: dict[str, dict] = field(default_factory=dict) # canonical verb → declaration
|
|
36
|
+
verb_of: dict[str, str] = field(default_factory=dict) # any spelling → canonical
|
|
37
|
+
relations: dict[str, dict] = field(default_factory=dict)
|
|
38
|
+
|
|
39
|
+
@property
|
|
40
|
+
def name(self) -> str:
|
|
41
|
+
return self.data["name"]
|
|
42
|
+
|
|
43
|
+
@property
|
|
44
|
+
def strictness(self) -> str:
|
|
45
|
+
return self.data.get("strictness", "permissive")
|
|
46
|
+
|
|
47
|
+
# --- queries ---------------------------------------------------------------------
|
|
48
|
+
|
|
49
|
+
def role(self, name: str) -> str | None:
|
|
50
|
+
return self.roles.get(name.lower())
|
|
51
|
+
|
|
52
|
+
def type_of(self, name: str) -> str | None:
|
|
53
|
+
"""The type a marked name denotes: itself, or the singular of a plural."""
|
|
54
|
+
r = self.role(name)
|
|
55
|
+
if r == "type":
|
|
56
|
+
return self.canonical[name.lower()]
|
|
57
|
+
if r == "plural":
|
|
58
|
+
return self.plural_of[name.lower()]
|
|
59
|
+
return None
|
|
60
|
+
|
|
61
|
+
def fields(self, type_name: str) -> dict:
|
|
62
|
+
return self.types[type_name].get("fields", {})
|
|
63
|
+
|
|
64
|
+
def tags(self, type_name: str) -> set[str]:
|
|
65
|
+
return {t if isinstance(t, str) else t["name"] for t in self.types[type_name].get("tags", [])}
|
|
66
|
+
|
|
67
|
+
def step(self, type_name: str, word: str) -> tuple[str | None, str | None]:
|
|
68
|
+
"""Follow one possessive step. Returns (target type, error code or None)."""
|
|
69
|
+
f = self.fields(type_name).get(word)
|
|
70
|
+
if f is not None:
|
|
71
|
+
if field_kind(f) == "reference":
|
|
72
|
+
return f["to"], None
|
|
73
|
+
return None, "not_a_reference"
|
|
74
|
+
for r in self.relations.values():
|
|
75
|
+
if type_name in r["from"] and r.get("as", r["to"]) == word:
|
|
76
|
+
return r["to"], None
|
|
77
|
+
return None, "unknown_field"
|
|
78
|
+
|
|
79
|
+
def verb(self, spelling: str) -> dict | None:
|
|
80
|
+
c = self.verb_of.get(spelling.lower())
|
|
81
|
+
return self.verbs[c] if c else None
|
|
82
|
+
|
|
83
|
+
def verb_dated(self, decl: dict) -> bool:
|
|
84
|
+
if "pattern" in decl:
|
|
85
|
+
return "at" in decl["pattern"]
|
|
86
|
+
return bool(decl.get("dated", False))
|
|
87
|
+
|
|
88
|
+
def suggest(self, type_name: str, word: str) -> str | None:
|
|
89
|
+
import difflib
|
|
90
|
+
names = list(self.fields(type_name))
|
|
91
|
+
m = difflib.get_close_matches(word, names, n=1, cutoff=0.75)
|
|
92
|
+
return m[0] if m else None
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _declare(d: Dictionary, name: str, role: str, canonical: str | None = None):
|
|
96
|
+
key = name.lower()
|
|
97
|
+
if key in d.roles and d.roles[key] != role:
|
|
98
|
+
raise DictionaryError(f"{name!r} declared as both {d.roles[key]} and {role}")
|
|
99
|
+
d.roles[key] = role
|
|
100
|
+
d.canonical[key] = canonical or name
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def load(source: dict | str | Path) -> Dictionary:
|
|
104
|
+
data = source if isinstance(source, dict) else json.loads(Path(source).read_text())
|
|
105
|
+
d = Dictionary(data=data)
|
|
106
|
+
d.types = data.get("types", {})
|
|
107
|
+
for name, t in d.types.items():
|
|
108
|
+
_declare(d, name, "type")
|
|
109
|
+
plural = t.get("plural", name + "s")
|
|
110
|
+
_declare(d, plural, "plural")
|
|
111
|
+
d.plural_of[plural.lower()] = name
|
|
112
|
+
for fname, f in t.get("fields", {}).items():
|
|
113
|
+
k = field_kind(f)
|
|
114
|
+
if k == "reference":
|
|
115
|
+
if f["to"] not in d.types:
|
|
116
|
+
raise DictionaryError(f"{name}.{fname} refers to undeclared type {f['to']!r}")
|
|
117
|
+
elif k == "list":
|
|
118
|
+
if f["of"] not in SCALARS:
|
|
119
|
+
raise DictionaryError(f"{name}.{fname}: list of {f['of']!r}")
|
|
120
|
+
elif k not in SCALARS:
|
|
121
|
+
raise DictionaryError(f"{name}.{fname}: unknown kind {k!r}")
|
|
122
|
+
for name, r in data.get("relations", {}).items():
|
|
123
|
+
for s in r["from"]:
|
|
124
|
+
if s not in d.types:
|
|
125
|
+
raise DictionaryError(f"relation {name}: undeclared subject type {s!r}")
|
|
126
|
+
if r["to"] not in d.types:
|
|
127
|
+
raise DictionaryError(f"relation {name}: undeclared object type {r['to']!r}")
|
|
128
|
+
d.relations[name] = r
|
|
129
|
+
for key, role in (("events", "event"), ("cadences", "cadence"), ("metrics", "metric"), ("attesters", "attester")):
|
|
130
|
+
for name, decl in data.get(key, {}).items():
|
|
131
|
+
_declare(d, name, role)
|
|
132
|
+
if role == "attester" and "plural" in decl:
|
|
133
|
+
_declare(d, decl["plural"], "attester_plural", canonical=name)
|
|
134
|
+
if role == "event":
|
|
135
|
+
if decl["type"] not in d.types or field_kind(d.fields(decl["type"]).get(decl["field"], "text")) != "date":
|
|
136
|
+
raise DictionaryError(f"event {name}: not a date field of a declared type")
|
|
137
|
+
if role == "metric" and decl["of"] not in d.types:
|
|
138
|
+
raise DictionaryError(f"metric {name}: undeclared type {decl['of']!r}")
|
|
139
|
+
for name, v in data.get("verbs", {}).items():
|
|
140
|
+
d.verbs[name] = v
|
|
141
|
+
d.verb_of[name.lower()] = name
|
|
142
|
+
for syn in v.get("synonyms", []):
|
|
143
|
+
d.verb_of[syn.lower()] = name
|
|
144
|
+
_check_pattern(d, name, v)
|
|
145
|
+
for sentence in data.get("definitions", []):
|
|
146
|
+
from .lexer import fragments
|
|
147
|
+
first = next(f for f in fragments(sentence) if f.kind == "term")
|
|
148
|
+
_declare(d, first.text, "reference" if " refers to " in sentence else "term")
|
|
149
|
+
return d
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def _check_pattern(d: Dictionary, name: str, v: dict):
|
|
153
|
+
p = v.get("pattern")
|
|
154
|
+
if p is None:
|
|
155
|
+
if "dated" not in v:
|
|
156
|
+
raise DictionaryError(f"verb {name}: a hook verb must declare dated")
|
|
157
|
+
return
|
|
158
|
+
if "dated" in v:
|
|
159
|
+
raise DictionaryError(f"verb {name}: a verb is data or code, never both")
|
|
160
|
+
subj, obj = v.get("subject"), v.get("object", "entity")
|
|
161
|
+
|
|
162
|
+
def ref(type_name, fname, to):
|
|
163
|
+
f = d.fields(type_name).get(fname)
|
|
164
|
+
if f is None or field_kind(f) != "reference" or (to and f["to"] != to):
|
|
165
|
+
raise DictionaryError(f"verb {name}: {type_name}.{fname} is not a reference to {to}")
|
|
166
|
+
|
|
167
|
+
def date(type_name, fname):
|
|
168
|
+
f = d.fields(type_name).get(fname)
|
|
169
|
+
if f is None or field_kind(f) != "date":
|
|
170
|
+
raise DictionaryError(f"verb {name}: {type_name}.{fname} is not a date")
|
|
171
|
+
|
|
172
|
+
if p["kind"] == "field":
|
|
173
|
+
if subj is None:
|
|
174
|
+
raise DictionaryError(f"verb {name}: a field pattern needs a subject type")
|
|
175
|
+
if "object" in p:
|
|
176
|
+
ref(subj, p["object"], obj)
|
|
177
|
+
if "at" in p:
|
|
178
|
+
date(subj, p["at"])
|
|
179
|
+
elif p["kind"] == "record":
|
|
180
|
+
if p["via"] not in d.types:
|
|
181
|
+
raise DictionaryError(f"verb {name}: via type {p['via']!r} undeclared")
|
|
182
|
+
ref(p["via"], p["subject"], subj)
|
|
183
|
+
if p.get("object") == "self":
|
|
184
|
+
if obj != p["via"]:
|
|
185
|
+
raise DictionaryError(f"verb {name}: object self means the object type is {p['via']}")
|
|
186
|
+
elif "object" in p:
|
|
187
|
+
ref(p["via"], p["object"], obj if obj not in ("entity", "none") else None)
|
|
188
|
+
if "at" in p:
|
|
189
|
+
date(p["via"], p["at"])
|
|
190
|
+
elif p["kind"] == "relation":
|
|
191
|
+
r = d.relations.get(p["name"])
|
|
192
|
+
if r is None:
|
|
193
|
+
raise DictionaryError(f"verb {name}: relation {p['name']!r} undeclared")
|
|
194
|
+
if subj and subj not in r["from"]:
|
|
195
|
+
raise DictionaryError(f"verb {name}: subject {subj} is not a subject of {p['name']}")
|
|
196
|
+
if obj not in ("entity", "none") and obj != r["to"]:
|
|
197
|
+
raise DictionaryError(f"verb {name}: object {obj} is not the object of {p['name']}")
|
|
198
|
+
elif p["kind"] == "reference":
|
|
199
|
+
if obj != "entity":
|
|
200
|
+
raise DictionaryError(f"verb {name}: a reference pattern takes any entity")
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
__all__ = ["Dictionary", "DictionaryError", "load", "field_kind"]
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"""Errors, named by the codes the conformance corpus uses."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class DeonticError(Exception):
|
|
7
|
+
code = "error"
|
|
8
|
+
|
|
9
|
+
def __init__(self, message: str = "", **details):
|
|
10
|
+
super().__init__(message or self.code)
|
|
11
|
+
self.message = message or self.code
|
|
12
|
+
self.details = details
|
|
13
|
+
|
|
14
|
+
def as_dict(self) -> dict:
|
|
15
|
+
return {"code": self.code, **{k: v for k, v in self.details.items() if v is not None}}
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class LexicalError(DeonticError):
|
|
19
|
+
"""A sentence that cannot be read at all, in either strictness setting."""
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class UnmatchedMarker(LexicalError):
|
|
23
|
+
code = "unmatched_marker"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class EmptyMarker(LexicalError):
|
|
27
|
+
code = "empty_marker"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class SigilInsideMarker(LexicalError):
|
|
31
|
+
code = "sigil_inside_marker"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class UnterminatedString(LexicalError):
|
|
35
|
+
code = "unterminated_string"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class UnbalancedParenthesis(LexicalError):
|
|
39
|
+
code = "unbalanced_parenthesis"
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class MissingPeriod(LexicalError):
|
|
43
|
+
code = "missing_period"
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class NoShape(DeonticError):
|
|
47
|
+
"""Strict only: marked text matching no shape."""
|
|
48
|
+
code = "no_shape"
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class UnresolvedAnaphora(DeonticError):
|
|
52
|
+
code = "unresolved_anaphora"
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class ResolutionError(DeonticError):
|
|
56
|
+
"""An error found against a dictionary."""
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class UnknownTerm(ResolutionError):
|
|
60
|
+
code = "unknown_term"
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class UnknownField(ResolutionError):
|
|
64
|
+
code = "unknown_field"
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class NotAReference(ResolutionError):
|
|
68
|
+
code = "not_a_reference"
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class UnknownTag(ResolutionError):
|
|
72
|
+
code = "unknown_tag"
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class TypeMismatch(ResolutionError):
|
|
76
|
+
code = "type_mismatch"
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class UnknownVerb(ResolutionError):
|
|
80
|
+
code = "unknown_verb"
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class ObjectKindMismatch(ResolutionError):
|
|
84
|
+
code = "object_kind_mismatch"
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class UnknownRubric(ResolutionError):
|
|
88
|
+
code = "unknown_rubric"
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class ConfidenceRequiresSystem(ResolutionError):
|
|
92
|
+
code = "confidence_requires_system"
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
class UndatedVerb(ResolutionError):
|
|
96
|
+
code = "undated_verb"
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"""The Python form of a lexicon's code half (docs/hooks.md §4).
|
|
2
|
+
|
|
3
|
+
A verb the dictionary declares without a `pattern` is a hook verb. A lexicon
|
|
4
|
+
may ship a callable for it; an implementation that can run the callable
|
|
5
|
+
evaluates the verb, one that cannot skips constraints using it. This module
|
|
6
|
+
is the whole contract: the witness record, the read-only world and context
|
|
7
|
+
a hook receives, and the entry-point group lexicons register under.
|
|
8
|
+
|
|
9
|
+
Stdlib only, and no evaluator lives here.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from dataclasses import dataclass, field
|
|
15
|
+
from datetime import date
|
|
16
|
+
from typing import Any, Callable, Iterable, Mapping, Protocol, Sequence
|
|
17
|
+
|
|
18
|
+
ENTRY_POINT_GROUP = "deontic.lexicons"
|
|
19
|
+
"""Python lexicons register here, named after their dictionary. The entry
|
|
20
|
+
point resolves to an object with a `hooks` mapping from verb name to Hook."""
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass(frozen=True)
|
|
24
|
+
class Entity:
|
|
25
|
+
id: str
|
|
26
|
+
type: str
|
|
27
|
+
tags: frozenset[str] = frozenset()
|
|
28
|
+
fields: Mapping[str, Any] = field(default_factory=dict)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass(frozen=True)
|
|
32
|
+
class Relation:
|
|
33
|
+
name: str
|
|
34
|
+
subject: str
|
|
35
|
+
object: str
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass(frozen=True)
|
|
39
|
+
class Attestation:
|
|
40
|
+
subject: str
|
|
41
|
+
attester: str
|
|
42
|
+
claim: str
|
|
43
|
+
date: date
|
|
44
|
+
confidence: float | None = None
|
|
45
|
+
system: str | None = None
|
|
46
|
+
attester_id: str | None = None
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@dataclass(frozen=True)
|
|
50
|
+
class Witness:
|
|
51
|
+
"""One piece of evidence that a verb holds of a subject.
|
|
52
|
+
|
|
53
|
+
`object` is the entity the verb relates the subject to, if the verb takes
|
|
54
|
+
one; `at` is when, if the verb is dated; `via` is the record that carries
|
|
55
|
+
the evidence when it is neither the subject nor the object.
|
|
56
|
+
"""
|
|
57
|
+
object: str | None = None
|
|
58
|
+
at: date | None = None
|
|
59
|
+
via: str | None = None
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class World(Protocol):
|
|
63
|
+
"""Read access to the closed world, and nothing outside it."""
|
|
64
|
+
|
|
65
|
+
def entity(self, id: str) -> Entity | None: ...
|
|
66
|
+
def entities(self, type: str) -> Iterable[Entity]: ...
|
|
67
|
+
def relations(self, name: str) -> Iterable[Relation]: ...
|
|
68
|
+
def attestations(self, subject: str) -> Iterable[Attestation]: ...
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@dataclass(frozen=True)
|
|
72
|
+
class Context:
|
|
73
|
+
as_of: date
|
|
74
|
+
parameters: Mapping[str, Any]
|
|
75
|
+
verb: Mapping[str, Any]
|
|
76
|
+
"""The verb's declaration from the dictionary, as loaded."""
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
Hook = Callable[[Entity, World, Context], Iterable[Witness]]
|
|
80
|
+
"""A hook is pure: the same subject, world and context give the same
|
|
81
|
+
witnesses. It sees no sentence, filter or time expression."""
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class Lexicon(Protocol):
|
|
85
|
+
"""What an entry point in ENTRY_POINT_GROUP resolves to."""
|
|
86
|
+
|
|
87
|
+
hooks: Mapping[str, Hook]
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def dated(hook: Hook) -> bool:
|
|
91
|
+
"""Whether a hook's witnesses carry a date, per its `dated` attribute; a
|
|
92
|
+
hook without one is undated, so a time expression on its verb is an
|
|
93
|
+
`undated_verb` error rather than a silent pass."""
|
|
94
|
+
return bool(getattr(hook, "dated", False))
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
__all__ = [
|
|
98
|
+
"ENTRY_POINT_GROUP", "Entity", "Relation", "Attestation", "Witness",
|
|
99
|
+
"World", "Context", "Hook", "Lexicon", "dated",
|
|
100
|
+
]
|