nomark-engine 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.
- nomark_engine/__init__.py +51 -0
- nomark_engine/classifier.py +134 -0
- nomark_engine/decay.py +46 -0
- nomark_engine/ledger.py +91 -0
- nomark_engine/resolver.py +304 -0
- nomark_engine/schema.py +189 -0
- nomark_engine/utility.py +122 -0
- nomark_engine-0.1.0.dist-info/METADATA +67 -0
- nomark_engine-0.1.0.dist-info/RECORD +10 -0
- nomark_engine-0.1.0.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""NOMARK Engine — open-core agent outcome quality resolver."""
|
|
2
|
+
|
|
3
|
+
__version__ = "0.1.0"
|
|
4
|
+
|
|
5
|
+
from .schema import (
|
|
6
|
+
Context, Outcome, RequestType, PatternType, RubricStage, SignalType, Scope,
|
|
7
|
+
ContextCounts, OutcomeCounts,
|
|
8
|
+
SigPref, SigMap, SigAsn, SigMeta, SigRub,
|
|
9
|
+
LedgerEntryMeta, LedgerEntryPref, LedgerEntryMap, LedgerEntryAsn, LedgerEntryRub,
|
|
10
|
+
LedgerEntry,
|
|
11
|
+
parse_ledger_entry,
|
|
12
|
+
)
|
|
13
|
+
from .decay import compute_decay, effective_weight
|
|
14
|
+
from .ledger import (
|
|
15
|
+
parse_ledger, write_ledger, parse_ledger_line, format_ledger_line,
|
|
16
|
+
count_by_type, check_capacity, estimate_tokens,
|
|
17
|
+
ENTRY_CAPS, TOTAL_CAP,
|
|
18
|
+
)
|
|
19
|
+
from .utility import utility_score, is_protected, prune_to_capacity
|
|
20
|
+
from .classifier import classify, ClassificationResult, InputTier
|
|
21
|
+
from .resolver import (
|
|
22
|
+
scope_specificity, scope_matches, resolver_score,
|
|
23
|
+
resolve_dimension, match_meaning_maps, find_defaults,
|
|
24
|
+
create_resolver, Resolver, ResolverConfig, ResolverResult,
|
|
25
|
+
DimensionResult, MeaningMapMatch, DefaultMatch, ScoredPref, ScoringFactors, ResolverMeta,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
__all__ = [
|
|
29
|
+
# Schema types
|
|
30
|
+
"Context", "Outcome", "RequestType", "PatternType", "RubricStage", "SignalType", "Scope",
|
|
31
|
+
"ContextCounts", "OutcomeCounts",
|
|
32
|
+
"SigPref", "SigMap", "SigAsn", "SigMeta", "SigRub",
|
|
33
|
+
"LedgerEntryMeta", "LedgerEntryPref", "LedgerEntryMap", "LedgerEntryAsn", "LedgerEntryRub",
|
|
34
|
+
"LedgerEntry",
|
|
35
|
+
"parse_ledger_entry",
|
|
36
|
+
# Decay
|
|
37
|
+
"compute_decay", "effective_weight",
|
|
38
|
+
# Ledger
|
|
39
|
+
"parse_ledger", "write_ledger", "parse_ledger_line", "format_ledger_line",
|
|
40
|
+
"count_by_type", "check_capacity", "estimate_tokens",
|
|
41
|
+
"ENTRY_CAPS", "TOTAL_CAP",
|
|
42
|
+
# Utility
|
|
43
|
+
"utility_score", "is_protected", "prune_to_capacity",
|
|
44
|
+
# Classifier
|
|
45
|
+
"classify", "ClassificationResult", "InputTier",
|
|
46
|
+
# Resolver
|
|
47
|
+
"scope_specificity", "scope_matches", "resolver_score",
|
|
48
|
+
"resolve_dimension", "match_meaning_maps", "find_defaults",
|
|
49
|
+
"create_resolver", "Resolver", "ResolverConfig", "ResolverResult",
|
|
50
|
+
"DimensionResult", "MeaningMapMatch", "DefaultMatch", "ScoredPref", "ScoringFactors", "ResolverMeta",
|
|
51
|
+
]
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
"""Input classification (MEE Spec Section 1.1).
|
|
2
|
+
|
|
3
|
+
Ports packages/engine/src/classifier.ts.
|
|
4
|
+
|
|
5
|
+
Tier 0: Pass-through — already resolved. Confirmations, selections, JSON, exit codes.
|
|
6
|
+
Tier 1: Routing — match to established pattern. Skill invocations, continuations, corrections.
|
|
7
|
+
Tier 2: Extraction — full intent reconstruction through resolver + gate.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import json
|
|
13
|
+
import re
|
|
14
|
+
from dataclasses import dataclass
|
|
15
|
+
from typing import Literal, Protocol
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
InputTier = Literal[0, 1, 2]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass(frozen=True)
|
|
22
|
+
class ClassificationResult:
|
|
23
|
+
tier: InputTier
|
|
24
|
+
reason: str
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class ClassifierRule(Protocol):
|
|
28
|
+
tier: InputTier
|
|
29
|
+
reason: str
|
|
30
|
+
|
|
31
|
+
def test(self, input: str) -> bool: ...
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass(frozen=True)
|
|
35
|
+
class _Rule:
|
|
36
|
+
tier: InputTier
|
|
37
|
+
reason: str
|
|
38
|
+
_test: object # callable
|
|
39
|
+
|
|
40
|
+
def test(self, input: str) -> bool:
|
|
41
|
+
return self._test(input) # type: ignore[operator]
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _is_confirmation(s: str) -> bool:
|
|
45
|
+
return bool(re.match(
|
|
46
|
+
r"^(y|yes|no|n|ok|done|skip|cancel|approve|reject|confirm|confirmed)$",
|
|
47
|
+
s.strip(), re.IGNORECASE,
|
|
48
|
+
))
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _is_numeric(s: str) -> bool:
|
|
52
|
+
return bool(re.match(r"^[0-9]+$", s.strip()))
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _is_json(s: str) -> bool:
|
|
56
|
+
t = s.strip()
|
|
57
|
+
if not (t.startswith("{") or t.startswith("[")):
|
|
58
|
+
return False
|
|
59
|
+
try:
|
|
60
|
+
json.loads(s)
|
|
61
|
+
return True
|
|
62
|
+
except (json.JSONDecodeError, ValueError):
|
|
63
|
+
return False
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _is_exit(s: str) -> bool:
|
|
67
|
+
return bool(re.match(r"^(exit|quit|bye|stop)\s*$", s.strip(), re.IGNORECASE))
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _is_hash(s: str) -> bool:
|
|
71
|
+
return bool(re.match(r"^[a-f0-9]{6,40}$", s.strip(), re.IGNORECASE))
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _is_skill(s: str) -> bool:
|
|
75
|
+
return bool(re.match(r"^/\w", s.strip()))
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _is_continuation(s: str) -> bool:
|
|
79
|
+
return bool(re.match(
|
|
80
|
+
r"^(continue|go ahead|proceed|next|keep going|resume)\s*$",
|
|
81
|
+
s.strip(), re.IGNORECASE,
|
|
82
|
+
))
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _is_correction(s: str) -> bool:
|
|
86
|
+
t = s.strip()
|
|
87
|
+
return bool(
|
|
88
|
+
re.match(r"^(no[,.]?\s+(not that|wrong|different|the other|I meant))", t, re.IGNORECASE)
|
|
89
|
+
or re.match(r"^(actually|wait|hold on|scratch that|never\s?mind)", t, re.IGNORECASE)
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _is_letter_selection(s: str) -> bool:
|
|
94
|
+
t = s.strip()
|
|
95
|
+
return 0 < len(t) <= 3 and bool(re.match(r"^[a-z]$", t, re.IGNORECASE))
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
_TIER_0_RULES: list[_Rule] = [
|
|
99
|
+
_Rule(0, "confirmation", _is_confirmation),
|
|
100
|
+
_Rule(0, "numeric_selection", _is_numeric),
|
|
101
|
+
_Rule(0, "json_data", _is_json),
|
|
102
|
+
_Rule(0, "exit_signal", _is_exit),
|
|
103
|
+
_Rule(0, "hash_or_id", _is_hash),
|
|
104
|
+
]
|
|
105
|
+
|
|
106
|
+
_TIER_1_RULES: list[_Rule] = [
|
|
107
|
+
_Rule(1, "skill_invocation", _is_skill),
|
|
108
|
+
_Rule(1, "continuation", _is_continuation),
|
|
109
|
+
_Rule(1, "correction", _is_correction),
|
|
110
|
+
_Rule(1, "letter_selection", _is_letter_selection),
|
|
111
|
+
]
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def classify(input: str, custom_rules: list[_Rule] | None = None) -> ClassificationResult:
|
|
115
|
+
"""Classify input into Tier 0 (pass-through), Tier 1 (routing), or Tier 2 (extraction)."""
|
|
116
|
+
trimmed = input.strip()
|
|
117
|
+
|
|
118
|
+
if not trimmed:
|
|
119
|
+
return ClassificationResult(tier=0, reason="empty_input")
|
|
120
|
+
|
|
121
|
+
if custom_rules:
|
|
122
|
+
for rule in custom_rules:
|
|
123
|
+
if rule.test(trimmed):
|
|
124
|
+
return ClassificationResult(tier=rule.tier, reason=rule.reason)
|
|
125
|
+
|
|
126
|
+
for rule in _TIER_0_RULES:
|
|
127
|
+
if rule.test(trimmed):
|
|
128
|
+
return ClassificationResult(tier=rule.tier, reason=rule.reason)
|
|
129
|
+
|
|
130
|
+
for rule in _TIER_1_RULES:
|
|
131
|
+
if rule.test(trimmed):
|
|
132
|
+
return ClassificationResult(tier=rule.tier, reason=rule.reason)
|
|
133
|
+
|
|
134
|
+
return ClassificationResult(tier=2, reason="substantive_input")
|
nomark_engine/decay.py
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""Continuous decay computation (MEE Spec Section 8).
|
|
2
|
+
|
|
3
|
+
Ports packages/engine/src/decay.ts.
|
|
4
|
+
|
|
5
|
+
Base: max(0.1, 0.98^(days/30))
|
|
6
|
+
Contradiction acceleration: decay * 0.85 when recent_contradictions >= 2
|
|
7
|
+
Reinforcement recovery: decay * 1.1 (capped 1.0) when reinforced within 7 days
|
|
8
|
+
Floor: 0.1 — never total erasure
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from datetime import datetime, timezone
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def compute_decay(
|
|
17
|
+
last_date: str,
|
|
18
|
+
_contradictions: int,
|
|
19
|
+
recent_contradictions: int,
|
|
20
|
+
recent_reinforcement: bool,
|
|
21
|
+
now: datetime | None = None,
|
|
22
|
+
) -> float:
|
|
23
|
+
if now is None:
|
|
24
|
+
now = datetime.now(timezone.utc)
|
|
25
|
+
|
|
26
|
+
last = datetime.fromisoformat(last_date)
|
|
27
|
+
if last.tzinfo is None:
|
|
28
|
+
last = last.replace(tzinfo=timezone.utc)
|
|
29
|
+
if now.tzinfo is None:
|
|
30
|
+
now = now.replace(tzinfo=timezone.utc)
|
|
31
|
+
|
|
32
|
+
days_since_last = max(0.0, (now - last).total_seconds() / 86400)
|
|
33
|
+
|
|
34
|
+
decay = max(0.1, 0.98 ** (days_since_last / 30))
|
|
35
|
+
|
|
36
|
+
if recent_contradictions >= 2:
|
|
37
|
+
decay = max(0.1, decay * 0.85)
|
|
38
|
+
|
|
39
|
+
if recent_reinforcement:
|
|
40
|
+
decay = min(1.0, decay * 1.1)
|
|
41
|
+
|
|
42
|
+
return round(decay * 1000) / 1000
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def effective_weight(w: float, decay: float) -> float:
|
|
46
|
+
return round(w * decay * 1000) / 1000
|
nomark_engine/ledger.py
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"""Ledger JSONL parser/writer with capacity constraints.
|
|
2
|
+
|
|
3
|
+
Ports packages/engine/src/ledger.ts.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
import re
|
|
10
|
+
|
|
11
|
+
from .schema import LedgerEntry, SignalType, parse_ledger_entry
|
|
12
|
+
|
|
13
|
+
ENTRY_CAPS: dict[SignalType, int] = {
|
|
14
|
+
"meta": 1,
|
|
15
|
+
"pref": 20,
|
|
16
|
+
"map": 10,
|
|
17
|
+
"asn": 5,
|
|
18
|
+
"rub": 4,
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
TOTAL_CAP = 40
|
|
22
|
+
|
|
23
|
+
_SIGNAL_PREFIX_RE = re.compile(r"^\[sig:(\w+)\]\s+(.+)$")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def parse_ledger_line(line: str) -> LedgerEntry | None:
|
|
27
|
+
"""Parse a single ledger line: `[sig:type] {json}`. Returns None for empty or unparseable lines."""
|
|
28
|
+
trimmed = line.strip()
|
|
29
|
+
if not trimmed:
|
|
30
|
+
return None
|
|
31
|
+
|
|
32
|
+
match = _SIGNAL_PREFIX_RE.match(trimmed)
|
|
33
|
+
if not match:
|
|
34
|
+
return None
|
|
35
|
+
|
|
36
|
+
signal_type = match.group(1)
|
|
37
|
+
try:
|
|
38
|
+
data = json.loads(match.group(2))
|
|
39
|
+
except (json.JSONDecodeError, ValueError):
|
|
40
|
+
return None
|
|
41
|
+
|
|
42
|
+
return parse_ledger_entry(signal_type, data)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def format_ledger_line(entry: LedgerEntry) -> str:
|
|
46
|
+
"""Format a ledger entry back to `[sig:type] {json}` string."""
|
|
47
|
+
return f"[sig:{entry.type}] {json.dumps(entry.data.model_dump(exclude_none=True))}"
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def parse_ledger(content: str) -> list[LedgerEntry]:
|
|
51
|
+
"""Parse a full ledger JSONL string into typed entries."""
|
|
52
|
+
results: list[LedgerEntry] = []
|
|
53
|
+
for line in content.split("\n"):
|
|
54
|
+
entry = parse_ledger_line(line)
|
|
55
|
+
if entry is not None:
|
|
56
|
+
results.append(entry)
|
|
57
|
+
return results
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def write_ledger(entries: list[LedgerEntry]) -> str:
|
|
61
|
+
"""Serialize ledger entries to JSONL string with typed prefixes."""
|
|
62
|
+
return "\n".join(format_ledger_line(e) for e in entries) + "\n"
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def count_by_type(entries: list[LedgerEntry]) -> dict[SignalType, int]:
|
|
66
|
+
"""Count entries by type."""
|
|
67
|
+
counts: dict[SignalType, int] = {"meta": 0, "pref": 0, "map": 0, "asn": 0, "rub": 0}
|
|
68
|
+
for entry in entries:
|
|
69
|
+
counts[entry.type] += 1
|
|
70
|
+
return counts
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def check_capacity(entries: list[LedgerEntry]) -> list[str]:
|
|
74
|
+
"""Check if ledger exceeds capacity constraints. Returns violations or empty list."""
|
|
75
|
+
violations: list[str] = []
|
|
76
|
+
counts = count_by_type(entries)
|
|
77
|
+
|
|
78
|
+
if len(entries) > TOTAL_CAP:
|
|
79
|
+
violations.append(f"total {len(entries)} exceeds cap {TOTAL_CAP}")
|
|
80
|
+
|
|
81
|
+
for signal_type, cap in ENTRY_CAPS.items():
|
|
82
|
+
count = counts.get(signal_type, 0)
|
|
83
|
+
if count > cap:
|
|
84
|
+
violations.append(f"{signal_type} count {count} exceeds cap {cap}")
|
|
85
|
+
|
|
86
|
+
return violations
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def estimate_tokens(entries: list[LedgerEntry]) -> int:
|
|
90
|
+
"""Estimate token count for ledger entries (~75 tokens per entry)."""
|
|
91
|
+
return len(entries) * 75
|
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
"""MEE Resolver — intent resolution from preference ledger.
|
|
2
|
+
|
|
3
|
+
Ports packages/engine/src/resolver.ts.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
import re
|
|
10
|
+
from dataclasses import dataclass, field
|
|
11
|
+
from datetime import datetime, timezone
|
|
12
|
+
|
|
13
|
+
from .schema import LedgerEntry, SigPref, SigMap, SigAsn, LedgerEntryPref, LedgerEntryMap, LedgerEntryAsn
|
|
14
|
+
from .decay import effective_weight
|
|
15
|
+
from .ledger import parse_ledger
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def scope_specificity(scope: str) -> float:
|
|
19
|
+
if scope == "*":
|
|
20
|
+
return 0.3
|
|
21
|
+
if "+" in scope:
|
|
22
|
+
return 1.0
|
|
23
|
+
return 0.7
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def scope_matches(scope: str, context: str | None = None, topic: str | None = None) -> bool:
|
|
27
|
+
if scope == "*":
|
|
28
|
+
return True
|
|
29
|
+
|
|
30
|
+
for part in scope.split("+"):
|
|
31
|
+
kv = part.split(":", 1)
|
|
32
|
+
if len(kv) != 2:
|
|
33
|
+
continue
|
|
34
|
+
key, value = kv
|
|
35
|
+
if key == "context" and context and value != context:
|
|
36
|
+
return False
|
|
37
|
+
if key == "topic" and topic and value != topic:
|
|
38
|
+
return False
|
|
39
|
+
|
|
40
|
+
return True
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@dataclass(frozen=True)
|
|
44
|
+
class ScoringFactors:
|
|
45
|
+
specificity: float
|
|
46
|
+
evidence: float
|
|
47
|
+
recency: float
|
|
48
|
+
stability: float
|
|
49
|
+
portability: float
|
|
50
|
+
contradiction_penalty: float
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@dataclass(frozen=True)
|
|
54
|
+
class ScoredPref:
|
|
55
|
+
pref: SigPref
|
|
56
|
+
score: float
|
|
57
|
+
effective_w: float
|
|
58
|
+
factors: ScoringFactors
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def resolver_score(pref: SigPref, now: datetime | None = None) -> ScoredPref:
|
|
62
|
+
"""Score a preference entry using the five-factor weighted formula."""
|
|
63
|
+
if now is None:
|
|
64
|
+
now = datetime.now(timezone.utc)
|
|
65
|
+
|
|
66
|
+
last = datetime.fromisoformat(pref.last)
|
|
67
|
+
if last.tzinfo is None:
|
|
68
|
+
last = last.replace(tzinfo=timezone.utc)
|
|
69
|
+
if now.tzinfo is None:
|
|
70
|
+
now = now.replace(tzinfo=timezone.utc)
|
|
71
|
+
|
|
72
|
+
days_since_last = max(0.0, (now - last).total_seconds() / 86400)
|
|
73
|
+
|
|
74
|
+
specificity = scope_specificity(pref.scope)
|
|
75
|
+
evidence = min(1.0, pref.n / 20)
|
|
76
|
+
recency = max(0.0, 1.0 - days_since_last / 180)
|
|
77
|
+
stability = (1.0 - (pref.ctd / pref.n)) if pref.n > 0 else 0.5
|
|
78
|
+
|
|
79
|
+
portability = 0.0
|
|
80
|
+
if pref.src:
|
|
81
|
+
src_dict = pref.src.model_dump(exclude_none=True)
|
|
82
|
+
portability = sum(1 for v in src_dict.values() if isinstance(v, (int, float)) and v > 0) / 3
|
|
83
|
+
|
|
84
|
+
contradiction_penalty = pref.ctd * 0.15
|
|
85
|
+
|
|
86
|
+
score = (
|
|
87
|
+
(specificity * 0.30)
|
|
88
|
+
+ (evidence * 0.25)
|
|
89
|
+
+ (recency * 0.20)
|
|
90
|
+
+ (stability * 0.15)
|
|
91
|
+
+ (portability * 0.10)
|
|
92
|
+
- contradiction_penalty
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
return ScoredPref(
|
|
96
|
+
pref=pref,
|
|
97
|
+
score=round(score * 1000) / 1000,
|
|
98
|
+
effective_w=effective_weight(pref.w, pref.decay),
|
|
99
|
+
factors=ScoringFactors(
|
|
100
|
+
specificity=specificity,
|
|
101
|
+
evidence=evidence,
|
|
102
|
+
recency=recency,
|
|
103
|
+
stability=stability,
|
|
104
|
+
portability=portability,
|
|
105
|
+
contradiction_penalty=contradiction_penalty,
|
|
106
|
+
),
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
@dataclass(frozen=True)
|
|
111
|
+
class DimensionResult:
|
|
112
|
+
dimension: str
|
|
113
|
+
winner: ScoredPref | None
|
|
114
|
+
runner_up: ScoredPref | None
|
|
115
|
+
unstable: bool
|
|
116
|
+
action: str # 'use_winner' | 'ask'
|
|
117
|
+
candidates: int
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
@dataclass(frozen=True)
|
|
121
|
+
class MeaningMapMatch:
|
|
122
|
+
trigger: str
|
|
123
|
+
intent: list[str]
|
|
124
|
+
confidence: float
|
|
125
|
+
scope: str
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
@dataclass(frozen=True)
|
|
129
|
+
class DefaultMatch:
|
|
130
|
+
field: str
|
|
131
|
+
default: str
|
|
132
|
+
accuracy: float
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
@dataclass(frozen=True)
|
|
136
|
+
class ResolverMeta:
|
|
137
|
+
entry_count: int
|
|
138
|
+
estimated_tokens: int
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
@dataclass(frozen=True)
|
|
142
|
+
class ResolverResult:
|
|
143
|
+
dimensions: dict[str, DimensionResult]
|
|
144
|
+
meaning_maps: list[MeaningMapMatch]
|
|
145
|
+
defaults: list[DefaultMatch]
|
|
146
|
+
meta: ResolverMeta
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def resolve_dimension(
|
|
150
|
+
entries: list[LedgerEntry],
|
|
151
|
+
dimension: str,
|
|
152
|
+
context: str | None = None,
|
|
153
|
+
topic: str | None = None,
|
|
154
|
+
now: datetime | None = None,
|
|
155
|
+
) -> DimensionResult:
|
|
156
|
+
if now is None:
|
|
157
|
+
now = datetime.now(timezone.utc)
|
|
158
|
+
|
|
159
|
+
candidates = [
|
|
160
|
+
resolver_score(e.data, now)
|
|
161
|
+
for e in entries
|
|
162
|
+
if e.type == "pref" and e.data.dim == dimension and scope_matches(e.data.scope, context, topic)
|
|
163
|
+
]
|
|
164
|
+
candidates.sort(key=lambda c: c.score, reverse=True)
|
|
165
|
+
|
|
166
|
+
if not candidates:
|
|
167
|
+
return DimensionResult(
|
|
168
|
+
dimension=dimension, winner=None, runner_up=None,
|
|
169
|
+
unstable=False, action="ask", candidates=0,
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
winner = candidates[0]
|
|
173
|
+
runner_up = candidates[1] if len(candidates) > 1 else None
|
|
174
|
+
unstable = winner.score < 0.4
|
|
175
|
+
|
|
176
|
+
return DimensionResult(
|
|
177
|
+
dimension=dimension, winner=winner, runner_up=runner_up,
|
|
178
|
+
unstable=unstable, action="ask" if unstable else "use_winner",
|
|
179
|
+
candidates=len(candidates),
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def match_meaning_maps(
|
|
184
|
+
entries: list[LedgerEntry],
|
|
185
|
+
input_text: str,
|
|
186
|
+
context: str | None = None,
|
|
187
|
+
topic: str | None = None,
|
|
188
|
+
) -> list[MeaningMapMatch]:
|
|
189
|
+
normalized = input_text.lower().strip()
|
|
190
|
+
|
|
191
|
+
results: list[MeaningMapMatch] = []
|
|
192
|
+
for e in entries:
|
|
193
|
+
if e.type != "map":
|
|
194
|
+
continue
|
|
195
|
+
data: SigMap = e.data
|
|
196
|
+
entry_scope = data.scope or "*"
|
|
197
|
+
if normalized.find(data.trigger.lower()) == -1:
|
|
198
|
+
continue
|
|
199
|
+
if not scope_matches(entry_scope, context, topic):
|
|
200
|
+
continue
|
|
201
|
+
results.append(MeaningMapMatch(
|
|
202
|
+
trigger=data.trigger,
|
|
203
|
+
intent=data.intent,
|
|
204
|
+
confidence=data.conf,
|
|
205
|
+
scope=entry_scope,
|
|
206
|
+
))
|
|
207
|
+
|
|
208
|
+
return results
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def find_defaults(entries: list[LedgerEntry]) -> list[DefaultMatch]:
|
|
212
|
+
return [
|
|
213
|
+
DefaultMatch(field=e.data.field, default=e.data.default, accuracy=e.data.accuracy)
|
|
214
|
+
for e in entries
|
|
215
|
+
if e.type == "asn"
|
|
216
|
+
]
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
_SIGNAL_PREFIX_RE = re.compile(r"^\[sig:(\w+)\]\s+(.+)$")
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def _parse_ledger_content(content: str) -> list[LedgerEntry]:
|
|
223
|
+
return parse_ledger(content)
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
@dataclass
|
|
227
|
+
class ResolverConfig:
|
|
228
|
+
ledger_path: str | None = None
|
|
229
|
+
ledger_content: str | None = None
|
|
230
|
+
entries: list[LedgerEntry] | None = None
|
|
231
|
+
context: str | None = None
|
|
232
|
+
topic: str | None = None
|
|
233
|
+
now: datetime | None = None
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def _load_entries(config: ResolverConfig) -> list[LedgerEntry]:
|
|
237
|
+
if config.entries is not None:
|
|
238
|
+
return config.entries
|
|
239
|
+
if config.ledger_content is not None:
|
|
240
|
+
return _parse_ledger_content(config.ledger_content)
|
|
241
|
+
return []
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
class Resolver:
|
|
245
|
+
"""Resolver instance created from configuration."""
|
|
246
|
+
|
|
247
|
+
def __init__(self, config: ResolverConfig) -> None:
|
|
248
|
+
self._entries = _load_entries(config)
|
|
249
|
+
self._now = config.now or datetime.now(timezone.utc)
|
|
250
|
+
self._context = config.context
|
|
251
|
+
self._topic = config.topic
|
|
252
|
+
|
|
253
|
+
def resolve(self, dimension: str) -> DimensionResult:
|
|
254
|
+
return resolve_dimension(self._entries, dimension, self._context, self._topic, self._now)
|
|
255
|
+
|
|
256
|
+
def resolve_all(self) -> ResolverResult:
|
|
257
|
+
dimensions: set[str] = set()
|
|
258
|
+
for entry in self._entries:
|
|
259
|
+
if entry.type == "pref":
|
|
260
|
+
dimensions.add(entry.data.dim)
|
|
261
|
+
|
|
262
|
+
results: dict[str, DimensionResult] = {}
|
|
263
|
+
for dim in dimensions:
|
|
264
|
+
results[dim] = resolve_dimension(self._entries, dim, self._context, self._topic, self._now)
|
|
265
|
+
|
|
266
|
+
return ResolverResult(
|
|
267
|
+
dimensions=results,
|
|
268
|
+
meaning_maps=[],
|
|
269
|
+
defaults=find_defaults(self._entries),
|
|
270
|
+
meta=ResolverMeta(
|
|
271
|
+
entry_count=len(self._entries),
|
|
272
|
+
estimated_tokens=len(self._entries) * 75,
|
|
273
|
+
),
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
def resolve_input(self, input_text: str) -> ResolverResult:
|
|
277
|
+
dimensions: set[str] = set()
|
|
278
|
+
for entry in self._entries:
|
|
279
|
+
if entry.type == "pref":
|
|
280
|
+
dimensions.add(entry.data.dim)
|
|
281
|
+
|
|
282
|
+
dim_results: dict[str, DimensionResult] = {}
|
|
283
|
+
for dim in dimensions:
|
|
284
|
+
dim_results[dim] = resolve_dimension(self._entries, dim, self._context, self._topic, self._now)
|
|
285
|
+
|
|
286
|
+
return ResolverResult(
|
|
287
|
+
dimensions=dim_results,
|
|
288
|
+
meaning_maps=match_meaning_maps(self._entries, input_text, self._context, self._topic),
|
|
289
|
+
defaults=find_defaults(self._entries),
|
|
290
|
+
meta=ResolverMeta(
|
|
291
|
+
entry_count=len(self._entries),
|
|
292
|
+
estimated_tokens=len(self._entries) * 75,
|
|
293
|
+
),
|
|
294
|
+
)
|
|
295
|
+
|
|
296
|
+
def get_entries(self) -> list[LedgerEntry]:
|
|
297
|
+
return list(self._entries)
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def create_resolver(config: ResolverConfig | None = None, **kwargs) -> Resolver:
|
|
301
|
+
"""Create a resolver instance from configuration."""
|
|
302
|
+
if config is None:
|
|
303
|
+
config = ResolverConfig(**kwargs)
|
|
304
|
+
return Resolver(config)
|
nomark_engine/schema.py
ADDED
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
"""Schema types and validation for NOMARK ledger entries.
|
|
2
|
+
|
|
3
|
+
Ports packages/engine/src/schema.ts — Pydantic v2 models instead of Zod.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import re
|
|
9
|
+
from typing import Literal, Union
|
|
10
|
+
|
|
11
|
+
from pydantic import BaseModel, Field, field_validator
|
|
12
|
+
|
|
13
|
+
# --- String literal unions (match TS types) ---
|
|
14
|
+
|
|
15
|
+
Context = Literal["chat", "cowork", "code"]
|
|
16
|
+
Outcome = Literal["accepted", "edited", "corrected", "rejected", "abandoned", "unknown"]
|
|
17
|
+
RequestType = Literal["question", "task", "brainstorm", "decision", "critique", "creative", "continuation", "reaction"]
|
|
18
|
+
PatternType = Literal["rewrite_request", "scope_change", "quality_complaint", "format_request", "style_override", "abort"]
|
|
19
|
+
RubricStage = Literal["ephemeral", "pending", "proven", "trusted"]
|
|
20
|
+
SignalType = Literal["meta", "pref", "map", "asn", "rub"]
|
|
21
|
+
|
|
22
|
+
# Scope is a free-form string: '*' | 'context:{ctx}' | 'topic:{topic}' | compound
|
|
23
|
+
Scope = str
|
|
24
|
+
|
|
25
|
+
_ISO_DATE_RE = re.compile(r"^\d{4}-\d{2}-\d{2}$")
|
|
26
|
+
_ISO_DATETIME_RE = re.compile(r"^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _validate_iso_date(v: str) -> str:
|
|
30
|
+
if not _ISO_DATE_RE.match(v):
|
|
31
|
+
raise ValueError(f"Expected ISO date (YYYY-MM-DD), got {v!r}")
|
|
32
|
+
return v
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
# --- Context counts ---
|
|
36
|
+
|
|
37
|
+
class ContextCounts(BaseModel):
|
|
38
|
+
chat: int | None = None
|
|
39
|
+
code: int | None = None
|
|
40
|
+
cowork: int | None = None
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
# --- Signal data models ---
|
|
44
|
+
|
|
45
|
+
class SigPref(BaseModel):
|
|
46
|
+
dim: str
|
|
47
|
+
target: str
|
|
48
|
+
w: float = Field(ge=0, le=1)
|
|
49
|
+
n: int = Field(ge=0)
|
|
50
|
+
src: ContextCounts
|
|
51
|
+
ctd: int = Field(ge=0)
|
|
52
|
+
scope: str
|
|
53
|
+
decay: float = Field(ge=0, le=1)
|
|
54
|
+
last: str
|
|
55
|
+
staged: bool | None = None
|
|
56
|
+
note: str | None = None
|
|
57
|
+
|
|
58
|
+
@field_validator("last")
|
|
59
|
+
@classmethod
|
|
60
|
+
def _check_last(cls, v: str) -> str:
|
|
61
|
+
return _validate_iso_date(v)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class SigMap(BaseModel):
|
|
65
|
+
trigger: str
|
|
66
|
+
pattern_type: PatternType
|
|
67
|
+
intent: list[str]
|
|
68
|
+
neg: list[str] | None = None
|
|
69
|
+
conf: float = Field(ge=0, le=1)
|
|
70
|
+
n: int = Field(ge=0)
|
|
71
|
+
scope: str | None = None
|
|
72
|
+
last: str
|
|
73
|
+
|
|
74
|
+
@field_validator("last")
|
|
75
|
+
@classmethod
|
|
76
|
+
def _check_last(cls, v: str) -> str:
|
|
77
|
+
return _validate_iso_date(v)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class SigAsn(BaseModel):
|
|
81
|
+
field: str
|
|
82
|
+
default: str
|
|
83
|
+
accuracy: float = Field(ge=0, le=1)
|
|
84
|
+
total: int = Field(ge=0)
|
|
85
|
+
correct: int = Field(ge=0)
|
|
86
|
+
last: str
|
|
87
|
+
|
|
88
|
+
@field_validator("last")
|
|
89
|
+
@classmethod
|
|
90
|
+
def _check_last(cls, v: str) -> str:
|
|
91
|
+
return _validate_iso_date(v)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class OutcomeCounts(BaseModel):
|
|
95
|
+
accepted: int | None = None
|
|
96
|
+
edited: int | None = None
|
|
97
|
+
corrected: int | None = None
|
|
98
|
+
rejected: int | None = None
|
|
99
|
+
abandoned: int | None = None
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
class SigMeta(BaseModel):
|
|
103
|
+
profile: dict[str, object]
|
|
104
|
+
signals: int = Field(ge=0)
|
|
105
|
+
by_ctx: ContextCounts
|
|
106
|
+
by_out: OutcomeCounts
|
|
107
|
+
avg_conf: float = Field(ge=0, le=1)
|
|
108
|
+
avg_q: float = Field(ge=0)
|
|
109
|
+
updated: str
|
|
110
|
+
|
|
111
|
+
@field_validator("updated")
|
|
112
|
+
@classmethod
|
|
113
|
+
def _check_updated(cls, v: str) -> str:
|
|
114
|
+
return _validate_iso_date(v)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
class SigRub(BaseModel):
|
|
118
|
+
id: str
|
|
119
|
+
fmt: str
|
|
120
|
+
stage: RubricStage
|
|
121
|
+
uses: int = Field(ge=0)
|
|
122
|
+
accepts: int = Field(ge=0)
|
|
123
|
+
avg_ed: float = Field(ge=0)
|
|
124
|
+
dims: dict[str, float]
|
|
125
|
+
min: float
|
|
126
|
+
ref: str | None = None
|
|
127
|
+
last: str
|
|
128
|
+
|
|
129
|
+
@field_validator("last")
|
|
130
|
+
@classmethod
|
|
131
|
+
def _check_last(cls, v: str) -> str:
|
|
132
|
+
return _validate_iso_date(v)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
# --- Ledger entry (discriminated union) ---
|
|
136
|
+
|
|
137
|
+
class LedgerEntryMeta(BaseModel):
|
|
138
|
+
type: Literal["meta"] = "meta"
|
|
139
|
+
data: SigMeta
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
class LedgerEntryPref(BaseModel):
|
|
143
|
+
type: Literal["pref"] = "pref"
|
|
144
|
+
data: SigPref
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
class LedgerEntryMap(BaseModel):
|
|
148
|
+
type: Literal["map"] = "map"
|
|
149
|
+
data: SigMap
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
class LedgerEntryAsn(BaseModel):
|
|
153
|
+
type: Literal["asn"] = "asn"
|
|
154
|
+
data: SigAsn
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
class LedgerEntryRub(BaseModel):
|
|
158
|
+
type: Literal["rub"] = "rub"
|
|
159
|
+
data: SigRub
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
LedgerEntry = Union[LedgerEntryMeta, LedgerEntryPref, LedgerEntryMap, LedgerEntryAsn, LedgerEntryRub]
|
|
163
|
+
|
|
164
|
+
_SIGNAL_TYPE_MAP: dict[str, type[BaseModel]] = {
|
|
165
|
+
"meta": LedgerEntryMeta,
|
|
166
|
+
"pref": LedgerEntryPref,
|
|
167
|
+
"map": LedgerEntryMap,
|
|
168
|
+
"asn": LedgerEntryAsn,
|
|
169
|
+
"rub": LedgerEntryRub,
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
_SIGNAL_DATA_MAP: dict[str, type[BaseModel]] = {
|
|
173
|
+
"meta": SigMeta,
|
|
174
|
+
"pref": SigPref,
|
|
175
|
+
"map": SigMap,
|
|
176
|
+
"asn": SigAsn,
|
|
177
|
+
"rub": SigRub,
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def parse_ledger_entry(signal_type: str, data: dict) -> LedgerEntry | None:
|
|
182
|
+
"""Parse a signal type + data dict into a typed LedgerEntry. Returns None on validation failure."""
|
|
183
|
+
entry_cls = _SIGNAL_TYPE_MAP.get(signal_type)
|
|
184
|
+
if entry_cls is None:
|
|
185
|
+
return None
|
|
186
|
+
try:
|
|
187
|
+
return entry_cls.model_validate({"type": signal_type, "data": data}) # type: ignore[return-value]
|
|
188
|
+
except Exception:
|
|
189
|
+
return None
|
nomark_engine/utility.py
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
"""Utility scoring and capacity-bounded pruning (MEE Spec Section 7.3).
|
|
2
|
+
|
|
3
|
+
Ports packages/engine/src/utility.ts.
|
|
4
|
+
|
|
5
|
+
U = (F x 0.25) + (I x 0.25) + (R x 0.20) + (P x 0.15) + (T x 0.15)
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from datetime import datetime, timezone
|
|
11
|
+
|
|
12
|
+
from .schema import LedgerEntry
|
|
13
|
+
from .ledger import ENTRY_CAPS, TOTAL_CAP
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def utility_score(
|
|
17
|
+
entry_data: dict,
|
|
18
|
+
now: datetime | None = None,
|
|
19
|
+
) -> float:
|
|
20
|
+
"""Compute utility score for a ledger entry data dict."""
|
|
21
|
+
if now is None:
|
|
22
|
+
now = datetime.now(timezone.utc)
|
|
23
|
+
|
|
24
|
+
last_str = entry_data.get("last", "")
|
|
25
|
+
last = datetime.fromisoformat(last_str) if last_str else now
|
|
26
|
+
if last.tzinfo is None:
|
|
27
|
+
last = last.replace(tzinfo=timezone.utc)
|
|
28
|
+
if now.tzinfo is None:
|
|
29
|
+
now = now.replace(tzinfo=timezone.utc)
|
|
30
|
+
|
|
31
|
+
days_since_last = max(0.0, (now - last).total_seconds() / 86400)
|
|
32
|
+
|
|
33
|
+
frequency = min(1.0, (entry_data.get("_uses_30d", 0)) / 10)
|
|
34
|
+
impact = entry_data.get("_impact", 0.5)
|
|
35
|
+
recency = max(0.0, 1.0 - days_since_last / 180)
|
|
36
|
+
|
|
37
|
+
n = entry_data.get("n", entry_data.get("total", 0))
|
|
38
|
+
ctd = entry_data.get("ctd", 0)
|
|
39
|
+
|
|
40
|
+
portability = 0.0
|
|
41
|
+
src = entry_data.get("src")
|
|
42
|
+
if src and isinstance(src, dict):
|
|
43
|
+
portability = sum(1 for v in src.values() if isinstance(v, (int, float)) and v > 0) / 3
|
|
44
|
+
|
|
45
|
+
stability = (1.0 - (ctd / n)) if n > 0 else 0.5
|
|
46
|
+
|
|
47
|
+
return (frequency * 0.25) + (impact * 0.25) + (recency * 0.20) + (portability * 0.15) + (stability * 0.15)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def is_protected(entry: LedgerEntry) -> bool:
|
|
51
|
+
"""Check if an entry is protected from pruning."""
|
|
52
|
+
if entry.type == "meta":
|
|
53
|
+
return True
|
|
54
|
+
if entry.type == "rub" and entry.data.stage in ("proven", "trusted"):
|
|
55
|
+
return True
|
|
56
|
+
if entry.type == "pref" and entry.data.n >= 15 and entry.data.ctd == 0:
|
|
57
|
+
return True
|
|
58
|
+
return False
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _entry_data_dict(entry: LedgerEntry) -> dict:
|
|
62
|
+
"""Get entry data as a plain dict for utility scoring."""
|
|
63
|
+
return entry.data.model_dump(exclude_none=True)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def prune_to_capacity(
|
|
67
|
+
entries: list[LedgerEntry],
|
|
68
|
+
now: datetime | None = None,
|
|
69
|
+
) -> tuple[list[LedgerEntry], list[LedgerEntry]]:
|
|
70
|
+
"""Prune entries to fit within capacity constraints.
|
|
71
|
+
|
|
72
|
+
Returns (kept, evicted). Removes lowest-utility entries first, never removes protected entries.
|
|
73
|
+
"""
|
|
74
|
+
if now is None:
|
|
75
|
+
now = datetime.now(timezone.utc)
|
|
76
|
+
|
|
77
|
+
kept = list(entries)
|
|
78
|
+
evicted: list[LedgerEntry] = []
|
|
79
|
+
|
|
80
|
+
# Group by type
|
|
81
|
+
by_type: dict[str, list[tuple[int, LedgerEntry, float]]] = {}
|
|
82
|
+
for i, entry in enumerate(kept):
|
|
83
|
+
t = entry.type
|
|
84
|
+
if t not in by_type:
|
|
85
|
+
by_type[t] = []
|
|
86
|
+
by_type[t].append((i, entry, utility_score(_entry_data_dict(entry), now)))
|
|
87
|
+
|
|
88
|
+
# Enforce per-type caps
|
|
89
|
+
to_remove: set[int] = set()
|
|
90
|
+
for signal_type, items in by_type.items():
|
|
91
|
+
cap = ENTRY_CAPS.get(signal_type, 0) # type: ignore[arg-type]
|
|
92
|
+
if len(items) <= cap:
|
|
93
|
+
continue
|
|
94
|
+
items.sort(key=lambda x: x[2]) # sort by utility ascending
|
|
95
|
+
removed = 0
|
|
96
|
+
for idx, entry, _ in items:
|
|
97
|
+
if len(items) - removed <= cap:
|
|
98
|
+
break
|
|
99
|
+
if not is_protected(entry):
|
|
100
|
+
to_remove.add(idx)
|
|
101
|
+
removed += 1
|
|
102
|
+
|
|
103
|
+
# Remove in reverse order
|
|
104
|
+
for idx in sorted(to_remove, reverse=True):
|
|
105
|
+
evicted.append(kept.pop(idx))
|
|
106
|
+
|
|
107
|
+
# Enforce total cap
|
|
108
|
+
while len(kept) > TOTAL_CAP:
|
|
109
|
+
lowest_idx = -1
|
|
110
|
+
lowest_utility = float("inf")
|
|
111
|
+
for i, entry in enumerate(kept):
|
|
112
|
+
if is_protected(entry):
|
|
113
|
+
continue
|
|
114
|
+
u = utility_score(_entry_data_dict(entry), now)
|
|
115
|
+
if u < lowest_utility:
|
|
116
|
+
lowest_utility = u
|
|
117
|
+
lowest_idx = i
|
|
118
|
+
if lowest_idx == -1:
|
|
119
|
+
break
|
|
120
|
+
evicted.append(kept.pop(lowest_idx))
|
|
121
|
+
|
|
122
|
+
return kept, evicted
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: nomark-engine
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: NOMARK Engine — open-core agent outcome quality resolver
|
|
5
|
+
Project-URL: Homepage, https://github.com/nomark-dev/nomark
|
|
6
|
+
Project-URL: Repository, https://github.com/nomark-dev/nomark
|
|
7
|
+
Author: Reece Frazier
|
|
8
|
+
License-Expression: Apache-2.0
|
|
9
|
+
Keywords: agent,ai,intent,nomark,preferences,quality,trust
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
19
|
+
Requires-Python: >=3.10
|
|
20
|
+
Requires-Dist: pydantic<3,>=2.0
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
|
|
23
|
+
# nomark-engine
|
|
24
|
+
|
|
25
|
+
Open-core agent outcome quality resolver. Understands what a human means from incomplete input by learning preferences across sessions and platforms.
|
|
26
|
+
|
|
27
|
+
## Install
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
pip install nomark-engine
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Quick start
|
|
34
|
+
|
|
35
|
+
```python
|
|
36
|
+
from nomark_engine import create_resolver, parse_ledger, ResolverConfig
|
|
37
|
+
|
|
38
|
+
# Parse a NOMARK ledger
|
|
39
|
+
entries = parse_ledger(open("nomark-ledger.jsonl").read())
|
|
40
|
+
|
|
41
|
+
# Create resolver
|
|
42
|
+
resolver = create_resolver(ResolverConfig(entries=entries))
|
|
43
|
+
|
|
44
|
+
# Resolve all preference dimensions
|
|
45
|
+
result = resolver.resolve_all()
|
|
46
|
+
for dim, res in result.dimensions.items():
|
|
47
|
+
if res.winner:
|
|
48
|
+
print(f"{dim}: {res.winner.pref.target} (score: {res.winner.score})")
|
|
49
|
+
|
|
50
|
+
# Resolve intent from natural language
|
|
51
|
+
result = resolver.resolve_input("make it shorter")
|
|
52
|
+
for match in result.meaning_maps:
|
|
53
|
+
print(f"Matched: {match.trigger} -> {match.intent}")
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Modules
|
|
57
|
+
|
|
58
|
+
- **Schema** — Pydantic v2 models for all signal types (pref, map, asn, meta, rub)
|
|
59
|
+
- **Classifier** — Input tier classification (pass-through, routing, extraction)
|
|
60
|
+
- **Resolver** — MEE weighted scoring with scope matching and instability detection
|
|
61
|
+
- **Ledger** — JSONL parser/writer with capacity constraints
|
|
62
|
+
- **Decay** — Time-based decay with contradiction acceleration
|
|
63
|
+
- **Utility** — Multi-factor utility scoring and capacity-bounded pruning
|
|
64
|
+
|
|
65
|
+
## License
|
|
66
|
+
|
|
67
|
+
Apache 2.0
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
nomark_engine/__init__.py,sha256=DMUE6ad1p6V71SbQKtoQfj7eyKHnaWrrDtvBgBwOyMo,2110
|
|
2
|
+
nomark_engine/classifier.py,sha256=02X0FVNaseR4cP4NFanPc2l_U5M6_OaPKR0KdP-JPU4,3614
|
|
3
|
+
nomark_engine/decay.py,sha256=1vAdS5emQvnqyu3ArOqsc3MGsP1vLYj9uYRU2XcQKhM,1230
|
|
4
|
+
nomark_engine/ledger.py,sha256=Q4fAzONp3R6VQx7sCNZs65bOVTm0V4kco5HK45FzuPw,2627
|
|
5
|
+
nomark_engine/resolver.py,sha256=o1v2MaBn5Yh-X0bIFQOloToiOU8uF1mj8yEevGv7pyA,8527
|
|
6
|
+
nomark_engine/schema.py,sha256=7s4BuA_8OQjdKLvqn9ucm4xy2kdYA2gsYfQDh0l3-N8,4740
|
|
7
|
+
nomark_engine/utility.py,sha256=4rzQmiaYNgYSl_uDpIRbb1ClLXErmjgLtn4moysl67o,3865
|
|
8
|
+
nomark_engine-0.1.0.dist-info/METADATA,sha256=S7Xf8LLQwTqrANQAApRzW1jT_bu_VucrcZevzLKrIxc,2284
|
|
9
|
+
nomark_engine-0.1.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
10
|
+
nomark_engine-0.1.0.dist-info/RECORD,,
|