sm-resolver 0.1.0__py3-none-any.whl
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.
- sm_resolver/__init__.py +39 -0
- sm_resolver/corroborate.py +77 -0
- sm_resolver/diff.py +71 -0
- sm_resolver/models.py +38 -0
- sm_resolver/py.typed +0 -0
- sm_resolver/resolver.py +37 -0
- sm_resolver/view.py +28 -0
- sm_resolver-0.1.0.dist-info/METADATA +111 -0
- sm_resolver-0.1.0.dist-info/RECORD +11 -0
- sm_resolver-0.1.0.dist-info/WHEEL +4 -0
- sm_resolver-0.1.0.dist-info/licenses/LICENSE +21 -0
sm_resolver/__init__.py
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""sm-resolver — the source-agnostic corroboration kernel.
|
|
2
|
+
|
|
3
|
+
A tiny, dependency-free kernel for detecting when independent sources disagree
|
|
4
|
+
about the same subject. It is the machinery under cross-registry / cross-method
|
|
5
|
+
divergence detection, factored out so any layer can reuse it:
|
|
6
|
+
|
|
7
|
+
- ``View`` — the contract a claim implements (``comparable() → named fields``).
|
|
8
|
+
- ``Resolver[T]`` — a per-source adapter: canonical id → ``(Status, View)``.
|
|
9
|
+
- ``diff_views`` — the pure diff: per-source claims → ``Finding`` list
|
|
10
|
+
(``omission`` + one per disagreeing field). It never learns its layer.
|
|
11
|
+
- ``Corroborator`` — resolve every source, diff, emit (deduped).
|
|
12
|
+
|
|
13
|
+
A consumer supplies a view type (its fields are what gets compared) and thin
|
|
14
|
+
per-source resolvers; the kernel does the rest, identically across discovery,
|
|
15
|
+
identity, capability, or evidence layers. See ``sm-divergence`` for the reference
|
|
16
|
+
layers (registries, DID methods) built on this.
|
|
17
|
+
|
|
18
|
+
Zero runtime dependencies.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from .corroborate import Corroborator, OnFinding
|
|
22
|
+
from .diff import diff_views
|
|
23
|
+
from .models import OMISSION, Finding
|
|
24
|
+
from .resolver import Resolver, Status
|
|
25
|
+
from .view import View, ViewT
|
|
26
|
+
|
|
27
|
+
__version__ = "0.1.0"
|
|
28
|
+
|
|
29
|
+
__all__ = [
|
|
30
|
+
"OMISSION",
|
|
31
|
+
"Corroborator",
|
|
32
|
+
"Finding",
|
|
33
|
+
"OnFinding",
|
|
34
|
+
"Resolver",
|
|
35
|
+
"Status",
|
|
36
|
+
"View",
|
|
37
|
+
"ViewT",
|
|
38
|
+
"diff_views",
|
|
39
|
+
]
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"""Generic orchestration: resolve every watched id against every source, diff,
|
|
2
|
+
and hand new findings to a callback. Layer-agnostic — a thin adapter for any
|
|
3
|
+
source set (e.g. two NANDA Indexes, or a NEST + an Index) drives this directly
|
|
4
|
+
with its own ``Resolver`` instances; no discovery internals required.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from collections.abc import Callable, Iterable
|
|
10
|
+
from typing import Generic
|
|
11
|
+
|
|
12
|
+
from .diff import diff_views
|
|
13
|
+
from .models import Finding
|
|
14
|
+
from .resolver import Resolver
|
|
15
|
+
from .view import ViewT
|
|
16
|
+
|
|
17
|
+
OnFinding = Callable[[Finding], None]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class Corroborator(Generic[ViewT]):
|
|
21
|
+
"""Corroborate a set of sources for a watch set.
|
|
22
|
+
|
|
23
|
+
Give it two or more ``Resolver`` instances; each ``check`` asks all of them
|
|
24
|
+
the same by-id questions and reports any disagreement. Fewer than two
|
|
25
|
+
sources → nothing to corroborate → no-op. Sources are deduped by ``label``.
|
|
26
|
+
|
|
27
|
+
``on_finding`` fires once per distinct finding across the corroborator's
|
|
28
|
+
lifetime (deduped by fingerprint); ``check`` always returns ALL current
|
|
29
|
+
findings. Never raises.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
def __init__(self, resolvers: Iterable[Resolver[ViewT]], *, on_finding: OnFinding | None = None) -> None:
|
|
33
|
+
deduped: list[Resolver[ViewT]] = []
|
|
34
|
+
seen: set[str] = set()
|
|
35
|
+
for r in resolvers:
|
|
36
|
+
if r.label in seen:
|
|
37
|
+
continue
|
|
38
|
+
seen.add(r.label)
|
|
39
|
+
deduped.append(r)
|
|
40
|
+
self.resolvers = deduped
|
|
41
|
+
self.on_finding = on_finding
|
|
42
|
+
self._emitted: set[str] = set()
|
|
43
|
+
|
|
44
|
+
@property
|
|
45
|
+
def labels(self) -> list[str]:
|
|
46
|
+
return [r.label for r in self.resolvers]
|
|
47
|
+
|
|
48
|
+
async def check(self, watch_ids: Iterable[str]) -> list[Finding]:
|
|
49
|
+
ids = sorted(set(watch_ids))
|
|
50
|
+
if len(self.resolvers) < 2 or not ids:
|
|
51
|
+
return []
|
|
52
|
+
|
|
53
|
+
views: dict[str, dict[str, ViewT | None]] = {}
|
|
54
|
+
for resolver in self.resolvers:
|
|
55
|
+
per_source: dict[str, ViewT | None] = {}
|
|
56
|
+
for aid in ids:
|
|
57
|
+
try:
|
|
58
|
+
status, view = await resolver.resolve(aid)
|
|
59
|
+
except Exception:
|
|
60
|
+
status, view = "error", None
|
|
61
|
+
if status == "error":
|
|
62
|
+
continue # unreachable is not a claim
|
|
63
|
+
per_source[aid] = view
|
|
64
|
+
views[resolver.label] = per_source
|
|
65
|
+
|
|
66
|
+
findings = diff_views(views, ids)
|
|
67
|
+
if self.on_finding is not None:
|
|
68
|
+
for f in findings:
|
|
69
|
+
fp = f.fingerprint()
|
|
70
|
+
if fp in self._emitted:
|
|
71
|
+
continue
|
|
72
|
+
self._emitted.add(fp)
|
|
73
|
+
try:
|
|
74
|
+
self.on_finding(f)
|
|
75
|
+
except Exception:
|
|
76
|
+
pass
|
|
77
|
+
return findings
|
sm_resolver/diff.py
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"""The pure diff — no I/O, generic over the view type. Given each source's view
|
|
2
|
+
of each id, find the disagreements. This is the whole idea; everything else is
|
|
3
|
+
plumbing to feed it, and it never learns what layer it is serving.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from collections.abc import Iterable, Mapping
|
|
9
|
+
|
|
10
|
+
from .models import OMISSION, Finding
|
|
11
|
+
from .view import ViewT
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def diff_views(
|
|
15
|
+
views: Mapping[str, Mapping[str, ViewT | None]],
|
|
16
|
+
watch_ids: Iterable[str],
|
|
17
|
+
) -> list[Finding]:
|
|
18
|
+
"""Compare what every source claims about each watched id.
|
|
19
|
+
|
|
20
|
+
``views[source][agent_id]`` is:
|
|
21
|
+
- a view — the source served a claim;
|
|
22
|
+
- ``None`` — the source POSITIVELY reports the id absent (a 404);
|
|
23
|
+
- simply *missing from the inner mapping* — the source made no claim
|
|
24
|
+
(unreachable). A silent source is never present or absent: a timeout is
|
|
25
|
+
not evidence.
|
|
26
|
+
|
|
27
|
+
Findings, per id:
|
|
28
|
+
- ``omission`` — present on ≥1 source AND confirmed-absent on ≥1 other.
|
|
29
|
+
- one per view field (``endpoint``, ``did``, ``key``, …) whose non-``None``
|
|
30
|
+
values disagree across the sources that served it.
|
|
31
|
+
|
|
32
|
+
Deterministic and side-effect free; never raises.
|
|
33
|
+
"""
|
|
34
|
+
findings: list[Finding] = []
|
|
35
|
+
for aid in sorted(set(watch_ids)):
|
|
36
|
+
present: dict[str, Mapping[str, str | None]] = {}
|
|
37
|
+
confirmed_absent: list[str] = []
|
|
38
|
+
for source, per_source in views.items():
|
|
39
|
+
if aid not in per_source:
|
|
40
|
+
continue # no claim — unreachable, not an omission
|
|
41
|
+
claim = per_source[aid]
|
|
42
|
+
if claim is None:
|
|
43
|
+
confirmed_absent.append(source)
|
|
44
|
+
else:
|
|
45
|
+
present[source] = claim.comparable()
|
|
46
|
+
|
|
47
|
+
if present and confirmed_absent:
|
|
48
|
+
findings.append(
|
|
49
|
+
Finding(
|
|
50
|
+
OMISSION,
|
|
51
|
+
aid,
|
|
52
|
+
{"present_on": sorted(present), "missing_from": sorted(confirmed_absent)},
|
|
53
|
+
)
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
field_names: list[str] = []
|
|
57
|
+
for comparable in present.values():
|
|
58
|
+
for name in comparable:
|
|
59
|
+
if name not in field_names:
|
|
60
|
+
field_names.append(name)
|
|
61
|
+
|
|
62
|
+
for name in field_names:
|
|
63
|
+
values: dict[str, str] = {}
|
|
64
|
+
for source, comparable in present.items():
|
|
65
|
+
value = comparable.get(name)
|
|
66
|
+
if value is not None:
|
|
67
|
+
values[source] = value
|
|
68
|
+
if len(set(values.values())) > 1:
|
|
69
|
+
findings.append(Finding(name, aid, {"field": name, "values": dict(sorted(values.items()))}))
|
|
70
|
+
|
|
71
|
+
return findings
|
sm_resolver/models.py
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""The finding — what disagreement was found. Layer-agnostic."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from dataclasses import dataclass, field
|
|
7
|
+
|
|
8
|
+
# The universal divergence kind (present on one source, confirmed-absent on
|
|
9
|
+
# another). Field-level kinds are named after the view field that diverged
|
|
10
|
+
# (e.g. "endpoint", "did", "key") and are supplied by the view, not fixed here.
|
|
11
|
+
OMISSION = "omission"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass(frozen=True)
|
|
15
|
+
class Finding:
|
|
16
|
+
"""One divergence about one subject across the compared sources.
|
|
17
|
+
|
|
18
|
+
``kind`` is ``omission`` or the name of a view field that diverged.
|
|
19
|
+
``detail`` carries the contradicting per-source claims. For ``omission`` it
|
|
20
|
+
is ``{present_on, missing_from}``; for a field divergence it is
|
|
21
|
+
``{field, values}`` (``values`` keyed by source label) — one uniform shape
|
|
22
|
+
across every layer, so any consumer parses findings the same way.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
kind: str
|
|
26
|
+
agent_id: str
|
|
27
|
+
detail: dict[str, object] = field(default_factory=dict)
|
|
28
|
+
|
|
29
|
+
def fingerprint(self) -> str:
|
|
30
|
+
"""A stable key for deduping a persisting finding across repeated
|
|
31
|
+
checks — same disagreement, same fingerprint."""
|
|
32
|
+
return json.dumps(
|
|
33
|
+
{"kind": self.kind, "agent_id": self.agent_id, "detail": self.detail},
|
|
34
|
+
sort_keys=True,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
def to_dict(self) -> dict[str, object]:
|
|
38
|
+
return {"kind": self.kind, "agent_id": self.agent_id, "detail": self.detail}
|
sm_resolver/py.typed
ADDED
|
File without changes
|
sm_resolver/resolver.py
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""The Resolver protocol — one registry/source, one way of being asked.
|
|
2
|
+
|
|
3
|
+
Sources do not share a wire format: a NEST server answers ``GET /api/agents/{id}``;
|
|
4
|
+
a NANDA Index answers a two-hop resolve; a DID method resolves a document; a
|
|
5
|
+
DNS-AID zone answers a TXT lookup. A ``Resolver`` hides that difference: given a
|
|
6
|
+
canonical agent id it performs *that* source's query, applies *that* source's
|
|
7
|
+
native verification, and normalizes the answer to ``(Status, View)`` — so the
|
|
8
|
+
diff downstream is untouched by format heterogeneity. This is the seam a thin
|
|
9
|
+
per-source adapter (e.g. a NANDA Index adapter) implements.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from typing import Literal, Protocol, TypeVar
|
|
15
|
+
|
|
16
|
+
from .view import View
|
|
17
|
+
|
|
18
|
+
# One source's claim about one id: served / positively-absent / no-claim.
|
|
19
|
+
Status = Literal["present", "absent", "error"]
|
|
20
|
+
|
|
21
|
+
# Covariant: a Resolver only ever PRODUCES its view type, so Resolver[Sub] is
|
|
22
|
+
# usable where Resolver[Base] is expected.
|
|
23
|
+
T_co = TypeVar("T_co", bound=View, covariant=True)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class Resolver(Protocol[T_co]):
|
|
27
|
+
"""Resolves a canonical agent id to one source's normalized claim.
|
|
28
|
+
|
|
29
|
+
``label`` names the source in findings. ``resolve`` MUST NOT raise — an
|
|
30
|
+
unreachable or unparseable source is the ``"error"`` claim (no claim),
|
|
31
|
+
never a false ``"absent"``.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
@property
|
|
35
|
+
def label(self) -> str: ...
|
|
36
|
+
|
|
37
|
+
async def resolve(self, agent_id: str) -> tuple[Status, T_co | None]: ...
|
sm_resolver/view.py
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""The comparable-view contract — the one thing a layer must provide.
|
|
2
|
+
|
|
3
|
+
A view is any object that reduces itself to a set of named string fields via
|
|
4
|
+
``comparable()``. Those fields ARE what corroboration compares: each field whose
|
|
5
|
+
value differs across sources becomes a divergence finding named for that field;
|
|
6
|
+
a field value of ``None`` never participates (an unverifiable value is not a
|
|
7
|
+
disagreement). Omission (present vs confirmed-absent) is universal and needs no
|
|
8
|
+
field.
|
|
9
|
+
|
|
10
|
+
This is the whole contract an adapter author implements — return a view type
|
|
11
|
+
from a ``Resolver``, give it a ``comparable()``, and the generic diff does the
|
|
12
|
+
rest, identically across discovery, identity, capability, and evidence layers.
|
|
13
|
+
The discovery layer's ``RecordView`` is just the first implementation.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
from collections.abc import Mapping
|
|
19
|
+
from typing import Protocol, TypeVar
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class View(Protocol):
|
|
23
|
+
"""A claim reduced to named, comparable string fields."""
|
|
24
|
+
|
|
25
|
+
def comparable(self) -> Mapping[str, str | None]: ...
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
ViewT = TypeVar("ViewT", bound=View)
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: sm-resolver
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: The source-agnostic corroboration kernel — Resolver[T], the View contract, a pure diff, and the Corroborator. Zero runtime dependencies.
|
|
5
|
+
Project-URL: Homepage, https://github.com/Sharathvc23/sm-resolver
|
|
6
|
+
Project-URL: Spec, https://github.com/Sharathvc23/sm-resolver/blob/main/SPEC.md
|
|
7
|
+
Project-URL: Whitepaper, https://github.com/Sharathvc23/sm-resolver/blob/main/WHITEPAPER.md
|
|
8
|
+
Author: Sharath (Stellarminds)
|
|
9
|
+
License: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: accountability,ai-agents,corroboration,divergence,nanda,resolver
|
|
12
|
+
Requires-Python: >=3.11
|
|
13
|
+
Provides-Extra: dev
|
|
14
|
+
Requires-Dist: mypy>=1.10; extra == 'dev'
|
|
15
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
|
|
16
|
+
Requires-Dist: pytest>=8; extra == 'dev'
|
|
17
|
+
Requires-Dist: ruff>=0.6; extra == 'dev'
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
|
|
20
|
+
# sm-resolver — the corroboration kernel
|
|
21
|
+
|
|
22
|
+
**A tiny, dependency-free kernel for detecting when independent sources disagree
|
|
23
|
+
about the same subject.** Point it at two or more sources, ask them the same
|
|
24
|
+
question, and it reports any disagreement. It is the machinery under
|
|
25
|
+
cross-registry / cross-method divergence detection, factored out so any layer can
|
|
26
|
+
reuse it.
|
|
27
|
+
|
|
28
|
+
Four pieces, and only the resolvers know a wire format:
|
|
29
|
+
|
|
30
|
+
- **`View`** — the contract a claim implements: `comparable() → {field: value}`.
|
|
31
|
+
Those fields are what gets compared; a `None` value never participates.
|
|
32
|
+
- **`Resolver[T]`** — a per-source adapter: a canonical id → `(Status, View)`.
|
|
33
|
+
It hides one source's format (an HTTP GET, a DID resolve, a DNS lookup) and
|
|
34
|
+
MUST NOT raise — an unreachable source is `error` (no claim), never a false
|
|
35
|
+
`absent`.
|
|
36
|
+
- **`diff_views`** — the pure diff: per-source claims → `Finding` list. It emits
|
|
37
|
+
`omission` (present on one source, positively absent on another) and one
|
|
38
|
+
finding per view field whose values disagree. It never learns its layer.
|
|
39
|
+
- **`Corroborator`** — resolve every source, diff, and emit new findings once
|
|
40
|
+
(deduped). Fewer than two sources → no-op.
|
|
41
|
+
|
|
42
|
+
## Install
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
pip install sm-resolver # zero runtime dependencies
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Use
|
|
49
|
+
|
|
50
|
+
Supply a view (its fields are what you compare) and a thin resolver per source:
|
|
51
|
+
|
|
52
|
+
```python
|
|
53
|
+
import asyncio
|
|
54
|
+
from collections.abc import Mapping
|
|
55
|
+
from dataclasses import dataclass
|
|
56
|
+
from sm_resolver import Corroborator, Status
|
|
57
|
+
|
|
58
|
+
@dataclass(frozen=True)
|
|
59
|
+
class RecordView: # your layer's view
|
|
60
|
+
endpoint: str | None = None
|
|
61
|
+
def comparable(self) -> Mapping[str, str | None]:
|
|
62
|
+
return {"endpoint": self.endpoint}
|
|
63
|
+
|
|
64
|
+
class MyResolver: # your thin per-source adapter
|
|
65
|
+
def __init__(self, label): self.label = label
|
|
66
|
+
async def resolve(self, agent_id) -> tuple[Status, RecordView | None]:
|
|
67
|
+
... # query this source; normalize to RecordView
|
|
68
|
+
|
|
69
|
+
findings = asyncio.run(
|
|
70
|
+
Corroborator([MyResolver("a"), MyResolver("b")]).check(["agent-1"])
|
|
71
|
+
)
|
|
72
|
+
for f in findings:
|
|
73
|
+
print(f.kind, f.agent_id, f.detail)
|
|
74
|
+
# endpoint agent-1 {'field': 'endpoint', 'values': {'a': 'https://real', 'b': 'https://evil'}}
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Long-running? Pass `on_finding=` — it fires once per distinct finding across the
|
|
78
|
+
corroborator's lifetime.
|
|
79
|
+
|
|
80
|
+
## Who uses it
|
|
81
|
+
|
|
82
|
+
[`sm-divergence`](https://github.com/Sharathvc23/sm-divergence) builds the
|
|
83
|
+
reference layers on this kernel: a **discovery** layer (registries — NEST, the
|
|
84
|
+
NANDA Index) and an **identity** layer (DID methods — `did:key`, `did:web`,
|
|
85
|
+
Universal Resolver), each supplying its own view and thin resolvers. Capability
|
|
86
|
+
and evidence layers are the same shape.
|
|
87
|
+
|
|
88
|
+
## Design rules
|
|
89
|
+
|
|
90
|
+
- **A timeout is not a claim.** An unreachable or erroring source is excluded,
|
|
91
|
+
never counted as an omission.
|
|
92
|
+
- **Only present, comparable values disagree.** A `None` field contributes
|
|
93
|
+
nothing — an unverifiable value is not a disagreement.
|
|
94
|
+
- **The diff stays pure and generic** — deterministic, no I/O, never raises,
|
|
95
|
+
never learns its layer. All I/O lives in the resolvers.
|
|
96
|
+
|
|
97
|
+
## Develop
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
make ci-local # uv: sync → ruff → format → mypy --strict → pytest
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## License
|
|
104
|
+
|
|
105
|
+
[MIT](LICENSE)
|
|
106
|
+
|
|
107
|
+
---
|
|
108
|
+
|
|
109
|
+
*First published: 2026-07-04 | Last modified: 2026-07-04*
|
|
110
|
+
|
|
111
|
+
*Personal research contributions aligned with [Project NANDA](https://projectnanda.org) standards. [Stellarminds.ai](https://stellarminds.ai)*
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
sm_resolver/__init__.py,sha256=iOjfO6iAufXEbjrdabmKXEx3GNfIJw4X69NSSWbZoa8,1347
|
|
2
|
+
sm_resolver/corroborate.py,sha256=JuF6rhxYzM_Cwbze3N4YUp0SX17bWRlfJKuNAqAHlug,2739
|
|
3
|
+
sm_resolver/diff.py,sha256=sXS2rQCt3yqqHNbJFL4yRFI9Z_DmKVW_M61ALTqVWHI,2640
|
|
4
|
+
sm_resolver/models.py,sha256=E2SlnHIok2XFaKeNEswectY8l9royeRlEJK80MudcYA,1440
|
|
5
|
+
sm_resolver/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
6
|
+
sm_resolver/resolver.py,sha256=g825OkEQ8cTnpMPBFkcVYRQBnFTEfh-15CdckdCnvV4,1444
|
|
7
|
+
sm_resolver/view.py,sha256=oxIkWKM5Jk08ZO4TTdMSGZAcjbBesnuttRI5tFTI5mM,1082
|
|
8
|
+
sm_resolver-0.1.0.dist-info/METADATA,sha256=9xVcErd1POy9Y2R5R3vHv99sVYToQFLNdG3--B4htOc,4326
|
|
9
|
+
sm_resolver-0.1.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
|
|
10
|
+
sm_resolver-0.1.0.dist-info/licenses/LICENSE,sha256=mo4bEHboox6Cvk1DuRHhTstvfo1NRFPs5QqRfvrufBc,1072
|
|
11
|
+
sm_resolver-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 stellarminds.ai
|
|
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.
|