dsh-plugin-xujie-memory 0.5.0 → 0.6.0

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.
@@ -1,170 +1,170 @@
1
- """关系规格(RelationSpec)。
2
-
3
- 设计文档 7.1 的理由:哪些关系可自动接受、负值是否允许、谁能写,
4
- 不能由模型临时决定,也不应散落在代码里。每种关系用一份版本化规格声明,
5
- 内核只执行规格,不自行推断。
6
- """
7
-
8
- from __future__ import annotations
9
-
10
- import json
11
- from dataclasses import dataclass, field
12
- from pathlib import Path
13
- from typing import Any
14
-
15
- from .errors import SpecNotFound
16
-
17
- RISK_TIERS = ("R0", "R1", "R2", "R3")
18
- ADMISSION_MODES = ("shadow", "assisted", "auto")
19
- EFFECT_KINDS = ("set", "delta", "invalidate")
20
- MODALITIES = (
21
- "asserted",
22
- # 请求 / 祈使。「给我免三碗」不是在陈述事实,也不是假设——
23
- # 说话人是真的在要求这件事。缺了这一格,它会被判成 hypothetical
24
- # 然后 record_only,用户明确要求的事反而不记(实测踩到)。
25
- "request",
26
- "hypothetical",
27
- "conditional",
28
- "quoted",
29
- "denied",
30
- "planned",
31
- "joking",
32
- "ironic",
33
- "in_world",
34
- "unclear",
35
- )
36
-
37
-
38
- @dataclass(frozen=True)
39
- class RelationSpec:
40
- relation: str
41
- version: str
42
- risk_tier: str
43
- required_slots: tuple[str, ...]
44
- allowed_modalities: tuple[str, ...]
45
- writers: tuple[str, ...]
46
- effect_kind: str
47
- admission_mode: str = "shadow"
48
- value_type: str = "integer" # integer / number / string / enum
49
- # XM-12: unit 只用于展示,不参与 state_key,也不参与校验。
50
- # 编码器给出与规格不符的单位时不会报错——这是有意的取舍,见 ISSUES。
51
- # 真要校验,应判 disputed 而不是拒绝(拒绝会丢掉证据)。
52
- unit: str | None = None
53
- allow_negative: bool = False
54
- enum_values: tuple[str, ...] = ()
55
- confirmation: str = "none" # none / implicit / explicit / authority
56
- validity: str = "permanent" # permanent / ttl / reconfirm
57
- ttl_seconds: int | None = None
58
- conflict_policy: str = "dispute" # dispute / reject / last_write_wins
59
- conservation: str | None = None # None / "repayment":追踪累计偿付量
60
- feeds: tuple[str, ...] = ()
61
- derived_from: tuple[str, ...] = ()
62
- note: str = ""
63
-
64
- # ------------------------------------------------------------- 自检
65
- def validate(self) -> None:
66
- if self.risk_tier not in RISK_TIERS:
67
- raise ValueError(f"{self.relation}: bad risk_tier {self.risk_tier!r}")
68
- if self.admission_mode not in ADMISSION_MODES:
69
- raise ValueError(f"{self.relation}: bad admission_mode {self.admission_mode!r}")
70
- if self.effect_kind not in EFFECT_KINDS:
71
- raise ValueError(f"{self.relation}: bad effect_kind {self.effect_kind!r}")
72
- for m in self.allowed_modalities:
73
- if m not in MODALITIES:
74
- raise ValueError(f"{self.relation}: unknown modality {m!r}")
75
- if not self.required_slots:
76
- raise ValueError(f"{self.relation}: required_slots must not be empty")
77
- # R3 永远不允许自动提交(设计文档 7.3 / 15.5)
78
- if self.risk_tier == "R3" and self.admission_mode == "auto":
79
- raise ValueError(
80
- f"{self.relation}: R3 relation must not be 'auto' "
81
- "(permissions/deletion go through the control channel only)"
82
- )
83
- if self.value_type == "enum" and not self.enum_values:
84
- raise ValueError(f"{self.relation}: enum value_type requires enum_values")
85
- if self.conservation not in (None, "repayment"):
86
- raise ValueError(f"{self.relation}: unknown conservation {self.conservation!r}")
87
-
88
- @property
89
- def spec_version(self) -> str:
90
- return f"{self.relation}@{self.version}"
91
-
92
-
93
- @dataclass
94
- class SpecRegistry:
95
- _by_relation: dict[str, RelationSpec] = field(default_factory=dict)
96
-
97
- # ------------------------------------------------------------- 加载
98
- @classmethod
99
- def from_dir(cls, path: str | Path) -> "SpecRegistry":
100
- reg = cls()
101
- for f in sorted(Path(path).glob("*.json")):
102
- reg.add(RelationSpec(**json.loads(f.read_text(encoding="utf-8"))))
103
- return reg
104
-
105
- @classmethod
106
- def from_specs(cls, specs: list[RelationSpec]) -> "SpecRegistry":
107
- reg = cls()
108
- for s in specs:
109
- reg.add(s)
110
- return reg
111
-
112
- def add(self, spec: RelationSpec) -> None:
113
- spec.validate()
114
- if spec.relation in self._by_relation:
115
- raise ValueError(f"duplicate relation spec: {spec.relation}")
116
- self._by_relation[spec.relation] = spec
117
-
118
- # ------------------------------------------------------------- 查询
119
- def get(self, relation: str) -> RelationSpec:
120
- try:
121
- return self._by_relation[relation]
122
- except KeyError:
123
- raise SpecNotFound(f"no spec registered for relation {relation!r}") from None
124
-
125
- def all(self) -> list[RelationSpec]:
126
- return list(self._by_relation.values())
127
-
128
- # ------------------------------------------------------------- 依赖图
129
- def dependency_edges(self) -> list[tuple[str, str]]:
130
- """返回 (input_relation, derived_relation) 边列表,用于环检测。"""
131
- edges: list[tuple[str, str]] = []
132
- for spec in self._by_relation.values():
133
- for src in spec.derived_from:
134
- edges.append((src, spec.relation))
135
- return edges
136
-
137
- def assert_acyclic(self) -> None:
138
- """依赖图必须为 DAG(设计文档 9.5)。"""
139
- from .errors import DependencyCycle
140
-
141
- graph: dict[str, set[str]] = {}
142
- for src, dst in self.dependency_edges():
143
- graph.setdefault(src, set()).add(dst)
144
-
145
- WHITE, GREY, BLACK = 0, 1, 2
146
- color: dict[str, int] = {}
147
-
148
- def visit(node: str, stack: list[str]) -> None:
149
- color[node] = GREY
150
- stack.append(node)
151
- for nxt in graph.get(node, ()):
152
- c = color.get(nxt, WHITE)
153
- if c == GREY:
154
- cycle = " -> ".join(stack[stack.index(nxt):] + [nxt])
155
- raise DependencyCycle(f"dependency cycle: {cycle}")
156
- if c == WHITE:
157
- visit(nxt, stack)
158
- stack.pop()
159
- color[node] = BLACK
160
-
161
- for node in list(graph):
162
- if color.get(node, WHITE) == WHITE:
163
- visit(node, [])
164
-
165
-
166
- def slot_value(spec: RelationSpec, slots: dict[str, Any], name: str) -> Any:
167
- """取槽位;缺失时抛错,不做默认值填充(I5)。"""
168
- if name not in slots:
169
- raise KeyError(f"missing required slot {name!r} for relation {spec.relation!r}")
170
- return slots[name]
1
+ """关系规格(RelationSpec)。
2
+
3
+ 设计文档 7.1 的理由:哪些关系可自动接受、负值是否允许、谁能写,
4
+ 不能由模型临时决定,也不应散落在代码里。每种关系用一份版本化规格声明,
5
+ 内核只执行规格,不自行推断。
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import json
11
+ from dataclasses import dataclass, field
12
+ from pathlib import Path
13
+ from typing import Any
14
+
15
+ from .errors import SpecNotFound
16
+
17
+ RISK_TIERS = ("R0", "R1", "R2", "R3")
18
+ ADMISSION_MODES = ("shadow", "assisted", "auto")
19
+ EFFECT_KINDS = ("set", "delta", "invalidate")
20
+ MODALITIES = (
21
+ "asserted",
22
+ # 请求 / 祈使。「给我免三碗」不是在陈述事实,也不是假设——
23
+ # 说话人是真的在要求这件事。缺了这一格,它会被判成 hypothetical
24
+ # 然后 record_only,用户明确要求的事反而不记(实测踩到)。
25
+ "request",
26
+ "hypothetical",
27
+ "conditional",
28
+ "quoted",
29
+ "denied",
30
+ "planned",
31
+ "joking",
32
+ "ironic",
33
+ "in_world",
34
+ "unclear",
35
+ )
36
+
37
+
38
+ @dataclass(frozen=True)
39
+ class RelationSpec:
40
+ relation: str
41
+ version: str
42
+ risk_tier: str
43
+ required_slots: tuple[str, ...]
44
+ allowed_modalities: tuple[str, ...]
45
+ writers: tuple[str, ...]
46
+ effect_kind: str
47
+ admission_mode: str = "shadow"
48
+ value_type: str = "integer" # integer / number / string / enum
49
+ # XM-12: unit 只用于展示,不参与 state_key,也不参与校验。
50
+ # 编码器给出与规格不符的单位时不会报错——这是有意的取舍,见 ISSUES。
51
+ # 真要校验,应判 disputed 而不是拒绝(拒绝会丢掉证据)。
52
+ unit: str | None = None
53
+ allow_negative: bool = False
54
+ enum_values: tuple[str, ...] = ()
55
+ confirmation: str = "none" # none / implicit / explicit / authority
56
+ validity: str = "permanent" # permanent / ttl / reconfirm
57
+ ttl_seconds: int | None = None
58
+ conflict_policy: str = "dispute" # dispute / reject / last_write_wins
59
+ conservation: str | None = None # None / "repayment":追踪累计偿付量
60
+ feeds: tuple[str, ...] = ()
61
+ derived_from: tuple[str, ...] = ()
62
+ note: str = ""
63
+
64
+ # ------------------------------------------------------------- 自检
65
+ def validate(self) -> None:
66
+ if self.risk_tier not in RISK_TIERS:
67
+ raise ValueError(f"{self.relation}: bad risk_tier {self.risk_tier!r}")
68
+ if self.admission_mode not in ADMISSION_MODES:
69
+ raise ValueError(f"{self.relation}: bad admission_mode {self.admission_mode!r}")
70
+ if self.effect_kind not in EFFECT_KINDS:
71
+ raise ValueError(f"{self.relation}: bad effect_kind {self.effect_kind!r}")
72
+ for m in self.allowed_modalities:
73
+ if m not in MODALITIES:
74
+ raise ValueError(f"{self.relation}: unknown modality {m!r}")
75
+ if not self.required_slots:
76
+ raise ValueError(f"{self.relation}: required_slots must not be empty")
77
+ # R3 永远不允许自动提交(设计文档 7.3 / 15.5)
78
+ if self.risk_tier == "R3" and self.admission_mode == "auto":
79
+ raise ValueError(
80
+ f"{self.relation}: R3 relation must not be 'auto' "
81
+ "(permissions/deletion go through the control channel only)"
82
+ )
83
+ if self.value_type == "enum" and not self.enum_values:
84
+ raise ValueError(f"{self.relation}: enum value_type requires enum_values")
85
+ if self.conservation not in (None, "repayment"):
86
+ raise ValueError(f"{self.relation}: unknown conservation {self.conservation!r}")
87
+
88
+ @property
89
+ def spec_version(self) -> str:
90
+ return f"{self.relation}@{self.version}"
91
+
92
+
93
+ @dataclass
94
+ class SpecRegistry:
95
+ _by_relation: dict[str, RelationSpec] = field(default_factory=dict)
96
+
97
+ # ------------------------------------------------------------- 加载
98
+ @classmethod
99
+ def from_dir(cls, path: str | Path) -> "SpecRegistry":
100
+ reg = cls()
101
+ for f in sorted(Path(path).glob("*.json")):
102
+ reg.add(RelationSpec(**json.loads(f.read_text(encoding="utf-8"))))
103
+ return reg
104
+
105
+ @classmethod
106
+ def from_specs(cls, specs: list[RelationSpec]) -> "SpecRegistry":
107
+ reg = cls()
108
+ for s in specs:
109
+ reg.add(s)
110
+ return reg
111
+
112
+ def add(self, spec: RelationSpec) -> None:
113
+ spec.validate()
114
+ if spec.relation in self._by_relation:
115
+ raise ValueError(f"duplicate relation spec: {spec.relation}")
116
+ self._by_relation[spec.relation] = spec
117
+
118
+ # ------------------------------------------------------------- 查询
119
+ def get(self, relation: str) -> RelationSpec:
120
+ try:
121
+ return self._by_relation[relation]
122
+ except KeyError:
123
+ raise SpecNotFound(f"no spec registered for relation {relation!r}") from None
124
+
125
+ def all(self) -> list[RelationSpec]:
126
+ return list(self._by_relation.values())
127
+
128
+ # ------------------------------------------------------------- 依赖图
129
+ def dependency_edges(self) -> list[tuple[str, str]]:
130
+ """返回 (input_relation, derived_relation) 边列表,用于环检测。"""
131
+ edges: list[tuple[str, str]] = []
132
+ for spec in self._by_relation.values():
133
+ for src in spec.derived_from:
134
+ edges.append((src, spec.relation))
135
+ return edges
136
+
137
+ def assert_acyclic(self) -> None:
138
+ """依赖图必须为 DAG(设计文档 9.5)。"""
139
+ from .errors import DependencyCycle
140
+
141
+ graph: dict[str, set[str]] = {}
142
+ for src, dst in self.dependency_edges():
143
+ graph.setdefault(src, set()).add(dst)
144
+
145
+ WHITE, GREY, BLACK = 0, 1, 2
146
+ color: dict[str, int] = {}
147
+
148
+ def visit(node: str, stack: list[str]) -> None:
149
+ color[node] = GREY
150
+ stack.append(node)
151
+ for nxt in graph.get(node, ()):
152
+ c = color.get(nxt, WHITE)
153
+ if c == GREY:
154
+ cycle = " -> ".join(stack[stack.index(nxt):] + [nxt])
155
+ raise DependencyCycle(f"dependency cycle: {cycle}")
156
+ if c == WHITE:
157
+ visit(nxt, stack)
158
+ stack.pop()
159
+ color[node] = BLACK
160
+
161
+ for node in list(graph):
162
+ if color.get(node, WHITE) == WHITE:
163
+ visit(node, [])
164
+
165
+
166
+ def slot_value(spec: RelationSpec, slots: dict[str, Any], name: str) -> Any:
167
+ """取槽位;缺失时抛错,不做默认值填充(I5)。"""
168
+ if name not in slots:
169
+ raise KeyError(f"missing required slot {name!r} for relation {spec.relation!r}")
170
+ return slots[name]