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 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.
@@ -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"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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
+ ]