memleaf 0.2.12__tar.gz → 0.2.13__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.
- {memleaf-0.2.12 → memleaf-0.2.13}/CHANGELOG.md +11 -0
- {memleaf-0.2.12/src/memleaf.egg-info → memleaf-0.2.13}/PKG-INFO +3 -3
- {memleaf-0.2.12 → memleaf-0.2.13}/README.en.md +2 -2
- {memleaf-0.2.12 → memleaf-0.2.13}/README.md +2 -2
- {memleaf-0.2.12 → memleaf-0.2.13}/pyproject.toml +1 -1
- {memleaf-0.2.12 → memleaf-0.2.13}/src/memleaf/__init__.py +1 -1
- {memleaf-0.2.12 → memleaf-0.2.13}/src/memleaf/hermes_provider/plugin.yaml +1 -1
- {memleaf-0.2.12 → memleaf-0.2.13}/src/memleaf/processing.py +55 -0
- {memleaf-0.2.12 → memleaf-0.2.13}/src/memleaf/prompts.py +6 -1
- {memleaf-0.2.12 → memleaf-0.2.13}/src/memleaf/validation.py +40 -0
- {memleaf-0.2.12 → memleaf-0.2.13/src/memleaf.egg-info}/PKG-INFO +3 -3
- {memleaf-0.2.12 → memleaf-0.2.13}/tests/test_admission_noise.py +89 -0
- {memleaf-0.2.12 → memleaf-0.2.13}/tests/test_hermes_provider.py +4 -4
- {memleaf-0.2.12 → memleaf-0.2.13}/tests/test_pypi_install.py +5 -5
- {memleaf-0.2.12 → memleaf-0.2.13}/tests/test_stage_b1.py +33 -0
- {memleaf-0.2.12 → memleaf-0.2.13}/tests/test_stage_b2a.py +87 -1
- {memleaf-0.2.12 → memleaf-0.2.13}/tests/test_stage_c1_mcp.py +1 -1
- {memleaf-0.2.12 → memleaf-0.2.13}/tests/test_stage_c3_packaging.py +1 -1
- {memleaf-0.2.12 → memleaf-0.2.13}/IMPLEMENTATION_PLAN.md +0 -0
- {memleaf-0.2.12 → memleaf-0.2.13}/LICENSE +0 -0
- {memleaf-0.2.12 → memleaf-0.2.13}/MANIFEST.in +0 -0
- {memleaf-0.2.12 → memleaf-0.2.13}/RELEASE_CHECKLIST.md +0 -0
- {memleaf-0.2.12 → memleaf-0.2.13}/examples/README.md +0 -0
- {memleaf-0.2.12 → memleaf-0.2.13}/examples/basic_usage.py +0 -0
- {memleaf-0.2.12 → memleaf-0.2.13}/examples/mcp_stdio.ndjson +0 -0
- {memleaf-0.2.12 → memleaf-0.2.13}/install.ps1 +0 -0
- {memleaf-0.2.12 → memleaf-0.2.13}/install.sh +0 -0
- {memleaf-0.2.12 → memleaf-0.2.13}/setup.cfg +0 -0
- {memleaf-0.2.12 → memleaf-0.2.13}/src/memleaf/__main__.py +0 -0
- {memleaf-0.2.12 → memleaf-0.2.13}/src/memleaf/adapters/__init__.py +0 -0
- {memleaf-0.2.12 → memleaf-0.2.13}/src/memleaf/adapters/antigravity.py +0 -0
- {memleaf-0.2.12 → memleaf-0.2.13}/src/memleaf/adapters/base.py +0 -0
- {memleaf-0.2.12 → memleaf-0.2.13}/src/memleaf/adapters/codex.py +0 -0
- {memleaf-0.2.12 → memleaf-0.2.13}/src/memleaf/adapters/hermes.py +0 -0
- {memleaf-0.2.12 → memleaf-0.2.13}/src/memleaf/budget.py +0 -0
- {memleaf-0.2.12 → memleaf-0.2.13}/src/memleaf/capture.py +0 -0
- {memleaf-0.2.12 → memleaf-0.2.13}/src/memleaf/cli.py +0 -0
- {memleaf-0.2.12 → memleaf-0.2.13}/src/memleaf/compaction.py +0 -0
- {memleaf-0.2.12 → memleaf-0.2.13}/src/memleaf/config.py +0 -0
- {memleaf-0.2.12 → memleaf-0.2.13}/src/memleaf/credentials.py +0 -0
- {memleaf-0.2.12 → memleaf-0.2.13}/src/memleaf/frontmatter.py +0 -0
- {memleaf-0.2.12 → memleaf-0.2.13}/src/memleaf/hermes_provider/README.md +0 -0
- {memleaf-0.2.12 → memleaf-0.2.13}/src/memleaf/hermes_provider/__init__.py +0 -0
- {memleaf-0.2.12 → memleaf-0.2.13}/src/memleaf/host_events.py +0 -0
- {memleaf-0.2.12 → memleaf-0.2.13}/src/memleaf/host_runtime.py +0 -0
- {memleaf-0.2.12 → memleaf-0.2.13}/src/memleaf/inbox.py +0 -0
- {memleaf-0.2.12 → memleaf-0.2.13}/src/memleaf/index.py +0 -0
- {memleaf-0.2.12 → memleaf-0.2.13}/src/memleaf/installer.py +0 -0
- {memleaf-0.2.12 → memleaf-0.2.13}/src/memleaf/llm/__init__.py +0 -0
- {memleaf-0.2.12 → memleaf-0.2.13}/src/memleaf/llm/base.py +0 -0
- {memleaf-0.2.12 → memleaf-0.2.13}/src/memleaf/llm/claude_compatible.py +0 -0
- {memleaf-0.2.12 → memleaf-0.2.13}/src/memleaf/llm/gemini.py +0 -0
- {memleaf-0.2.12 → memleaf-0.2.13}/src/memleaf/llm/openai_compatible.py +0 -0
- {memleaf-0.2.12 → memleaf-0.2.13}/src/memleaf/llm/router.py +0 -0
- {memleaf-0.2.12 → memleaf-0.2.13}/src/memleaf/locking.py +0 -0
- {memleaf-0.2.12 → memleaf-0.2.13}/src/memleaf/mcp_server.py +0 -0
- {memleaf-0.2.12 → memleaf-0.2.13}/src/memleaf/memory_writer.py +0 -0
- {memleaf-0.2.12 → memleaf-0.2.13}/src/memleaf/model_discovery.py +0 -0
- {memleaf-0.2.12 → memleaf-0.2.13}/src/memleaf/models.py +0 -0
- {memleaf-0.2.12 → memleaf-0.2.13}/src/memleaf/native_index.py +0 -0
- {memleaf-0.2.12 → memleaf-0.2.13}/src/memleaf/redaction.py +0 -0
- {memleaf-0.2.12 → memleaf-0.2.13}/src/memleaf/retrieval.py +0 -0
- {memleaf-0.2.12 → memleaf-0.2.13}/src/memleaf/retrieval_gate.py +0 -0
- {memleaf-0.2.12 → memleaf-0.2.13}/src/memleaf/scope_maintenance.py +0 -0
- {memleaf-0.2.12 → memleaf-0.2.13}/src/memleaf/scope_state.py +0 -0
- {memleaf-0.2.12 → memleaf-0.2.13}/src/memleaf/service.py +0 -0
- {memleaf-0.2.12 → memleaf-0.2.13}/src/memleaf/vault.py +0 -0
- {memleaf-0.2.12 → memleaf-0.2.13}/src/memleaf.egg-info/SOURCES.txt +0 -0
- {memleaf-0.2.12 → memleaf-0.2.13}/src/memleaf.egg-info/dependency_links.txt +0 -0
- {memleaf-0.2.12 → memleaf-0.2.13}/src/memleaf.egg-info/entry_points.txt +0 -0
- {memleaf-0.2.12 → memleaf-0.2.13}/src/memleaf.egg-info/top_level.txt +0 -0
- {memleaf-0.2.12 → memleaf-0.2.13}/tests/__init__.py +0 -0
- {memleaf-0.2.12 → memleaf-0.2.13}/tests/test_codex_install.py +0 -0
- {memleaf-0.2.12 → memleaf-0.2.13}/tests/test_codex_native_cli.py +0 -0
- {memleaf-0.2.12 → memleaf-0.2.13}/tests/test_context_budget.py +0 -0
- {memleaf-0.2.12 → memleaf-0.2.13}/tests/test_credential_safety.py +0 -0
- {memleaf-0.2.12 → memleaf-0.2.13}/tests/test_cross_host_acceptance.py +0 -0
- {memleaf-0.2.12 → memleaf-0.2.13}/tests/test_hermes_stdio_transport.py +0 -0
- {memleaf-0.2.12 → memleaf-0.2.13}/tests/test_host_events.py +0 -0
- {memleaf-0.2.12 → memleaf-0.2.13}/tests/test_host_runtime_contract.py +0 -0
- {memleaf-0.2.12 → memleaf-0.2.13}/tests/test_install.py +0 -0
- {memleaf-0.2.12 → memleaf-0.2.13}/tests/test_maintenance_v2.py +0 -0
- {memleaf-0.2.12 → memleaf-0.2.13}/tests/test_model_discovery.py +0 -0
- {memleaf-0.2.12 → memleaf-0.2.13}/tests/test_retrieval_gate.py +0 -0
- {memleaf-0.2.12 → memleaf-0.2.13}/tests/test_retrieval_v2.py +0 -0
- {memleaf-0.2.12 → memleaf-0.2.13}/tests/test_session_lineage.py +0 -0
- {memleaf-0.2.12 → memleaf-0.2.13}/tests/test_stage_a.py +0 -0
- {memleaf-0.2.12 → memleaf-0.2.13}/tests/test_stage_b2b.py +0 -0
- {memleaf-0.2.12 → memleaf-0.2.13}/tests/test_stage_b3a_commit.py +0 -0
- {memleaf-0.2.12 → memleaf-0.2.13}/tests/test_stage_b3a_contract.py +0 -0
- {memleaf-0.2.12 → memleaf-0.2.13}/tests/test_stage_b3b_native_context.py +0 -0
- {memleaf-0.2.12 → memleaf-0.2.13}/tests/test_stage_b3b_native_index.py +0 -0
- {memleaf-0.2.12 → memleaf-0.2.13}/tests/test_stage_b3b_scope.py +0 -0
- {memleaf-0.2.12 → memleaf-0.2.13}/tests/test_stage_b3c_retrieval.py +0 -0
- {memleaf-0.2.12 → memleaf-0.2.13}/tests/test_stage_b3d_scope_maintenance.py +0 -0
- {memleaf-0.2.12 → memleaf-0.2.13}/tests/test_stage_c2_init.py +0 -0
- {memleaf-0.2.12 → memleaf-0.2.13}/tests/test_upgrade_preserves_vault.py +0 -0
- {memleaf-0.2.12 → memleaf-0.2.13}/tests/test_v2_gate_limits.py +0 -0
- {memleaf-0.2.12 → memleaf-0.2.13}/tests/test_v2_host_flow.py +0 -0
- {memleaf-0.2.12 → memleaf-0.2.13}/tests/test_v2_mcp_flow.py +0 -0
- {memleaf-0.2.12 → memleaf-0.2.13}/tests/test_v2_nomatch_semantics.py +0 -0
- {memleaf-0.2.12 → memleaf-0.2.13}/tests/test_v2_search_gate_acceptance.py +0 -0
|
@@ -2,6 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to memleaf are documented here.
|
|
4
4
|
|
|
5
|
+
## 0.2.13 — 2026-09-02
|
|
6
|
+
|
|
7
|
+
- Preserve the full active project-plan body, stable title, and retrieval
|
|
8
|
+
metadata when a customer suggestion only adds constraints; explicit
|
|
9
|
+
replacements continue to replace conflicting state normally.
|
|
10
|
+
- Reject attachment, document, presentation, material, and issue-list items
|
|
11
|
+
described only by transport metadata or a generic follow-up at both the gate
|
|
12
|
+
and summary boundaries.
|
|
13
|
+
- Keep concrete attachment-backed actions eligible when they include a
|
|
14
|
+
remediation, owner, deadline, delivery rule, deployment, or migration.
|
|
15
|
+
|
|
5
16
|
## 0.2.12 — 2026-09-02
|
|
6
17
|
|
|
7
18
|
- Remove empty ASCII or Chinese parentheses left after an absolute date is
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: memleaf
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.13
|
|
4
4
|
Summary: A local-first Markdown memory core for AI agents
|
|
5
5
|
Author: memleaf contributors
|
|
6
6
|
License-Expression: MIT
|
|
@@ -23,8 +23,8 @@ Dynamic: license-file
|
|
|
23
23
|
|
|
24
24
|
[English](README.en.md) · [PyPI](https://pypi.org/project/memleaf/) · [GitHub](https://github.com/miffyblueboo/memleaf)
|
|
25
25
|
|
|
26
|
-
> **当前版本:0.2.
|
|
27
|
-
> 核心库、Vault、stdio MCP Server、初始化 CLI、模型路由、提炼流程、受控检索协议和宿主适配器已经实现。memleaf 0.2.
|
|
26
|
+
> **当前版本:0.2.13。**
|
|
27
|
+
> 核心库、Vault、stdio MCP Server、初始化 CLI、模型路由、提炼流程、受控检索协议和宿主适配器已经实现。memleaf 0.2.13 通过 PyPI 分发。
|
|
28
28
|
> **当前版本支持 Hermes 和 Codex。** Antigravity(反重力)不检测、不安装、不配置。
|
|
29
29
|
|
|
30
30
|
## 项目定位
|
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
|
|
5
5
|
[中文](README.md) · [PyPI](https://pypi.org/project/memleaf/) · [GitHub](https://github.com/miffyblueboo/memleaf)
|
|
6
6
|
|
|
7
|
-
> **Version: 0.2.
|
|
8
|
-
> The core library, Vault, stdio MCP server, initialization CLI, model routing, memory extraction, controlled retrieval protocol, and host adapters are implemented. memleaf 0.2.
|
|
7
|
+
> **Version: 0.2.13.**
|
|
8
|
+
> The core library, Vault, stdio MCP server, initialization CLI, model routing, memory extraction, controlled retrieval protocol, and host adapters are implemented. memleaf 0.2.13 is distributed through PyPI.
|
|
9
9
|
> **The current release supports Hermes and Codex.** Antigravity is not detected, installed, or configured.
|
|
10
10
|
|
|
11
11
|
## Project scope
|
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
|
|
5
5
|
[English](README.en.md) · [PyPI](https://pypi.org/project/memleaf/) · [GitHub](https://github.com/miffyblueboo/memleaf)
|
|
6
6
|
|
|
7
|
-
> **当前版本:0.2.
|
|
8
|
-
> 核心库、Vault、stdio MCP Server、初始化 CLI、模型路由、提炼流程、受控检索协议和宿主适配器已经实现。memleaf 0.2.
|
|
7
|
+
> **当前版本:0.2.13。**
|
|
8
|
+
> 核心库、Vault、stdio MCP Server、初始化 CLI、模型路由、提炼流程、受控检索协议和宿主适配器已经实现。memleaf 0.2.13 通过 PyPI 分发。
|
|
9
9
|
> **当前版本支持 Hermes 和 Codex。** Antigravity(反重力)不检测、不安装、不配置。
|
|
10
10
|
|
|
11
11
|
## 项目定位
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""Local-first Markdown memory core for AI agents."""
|
|
2
2
|
|
|
3
|
-
__version__ = "0.2.
|
|
3
|
+
__version__ = "0.2.13"
|
|
4
4
|
|
|
5
5
|
from .config import DEFAULT_CONFIG, default_config, load_config, save_config
|
|
6
6
|
from .frontmatter import FrontmatterError, dump_frontmatter, dump_yaml, load_yaml, parse_frontmatter
|
|
@@ -65,6 +65,7 @@ from .validation import (
|
|
|
65
65
|
parse_summarize_output,
|
|
66
66
|
normalize_relative_calendar_text,
|
|
67
67
|
is_aggregate_operational_text,
|
|
68
|
+
is_attachment_followup_only_text,
|
|
68
69
|
)
|
|
69
70
|
from .vault import safe_component
|
|
70
71
|
|
|
@@ -1712,6 +1713,52 @@ class Processor:
|
|
|
1712
1713
|
)
|
|
1713
1714
|
)
|
|
1714
1715
|
|
|
1716
|
+
@classmethod
|
|
1717
|
+
def _merge_additive_project_plan_update(
|
|
1718
|
+
cls,
|
|
1719
|
+
candidate: Mapping[str, Any],
|
|
1720
|
+
summary: Mapping[str, Any],
|
|
1721
|
+
target: Memory | None,
|
|
1722
|
+
) -> dict[str, Any]:
|
|
1723
|
+
"""Preserve a full active plan when current evidence only adds constraints."""
|
|
1724
|
+
|
|
1725
|
+
result = dict(summary)
|
|
1726
|
+
if (
|
|
1727
|
+
target is None
|
|
1728
|
+
or target.type != "project"
|
|
1729
|
+
or not cls._is_project_plan_title(target.title)
|
|
1730
|
+
):
|
|
1731
|
+
return result
|
|
1732
|
+
candidate_text = str(candidate.get("memory", "")).casefold()
|
|
1733
|
+
additive_markers = (
|
|
1734
|
+
"建议", "补充", "增加", "新增", "追加", "还需", "同时", "纳入", "完善",
|
|
1735
|
+
"suggest", "propose", "additional", "add ", "include",
|
|
1736
|
+
)
|
|
1737
|
+
replacement_markers = (
|
|
1738
|
+
"改为", "变更为", "替换", "取消", "删除", "不再", "更新为", "调整为", "推迟至", "提前至",
|
|
1739
|
+
"replace", "changed to", "no longer", "cancel", "remove", "instead", "moved to",
|
|
1740
|
+
)
|
|
1741
|
+
if not any(marker in candidate_text for marker in additive_markers):
|
|
1742
|
+
return result
|
|
1743
|
+
if any(marker in candidate_text for marker in replacement_markers):
|
|
1744
|
+
return result
|
|
1745
|
+
|
|
1746
|
+
old_body = target.body.strip()
|
|
1747
|
+
new_body = str(result.get("body", "")).strip()
|
|
1748
|
+
normalized_old = " ".join(old_body.casefold().split())
|
|
1749
|
+
normalized_new = " ".join(new_body.casefold().split())
|
|
1750
|
+
if old_body and normalized_old not in normalized_new:
|
|
1751
|
+
result["body"] = f"{old_body}\n\n{new_body}" if new_body else old_body
|
|
1752
|
+
result["title"] = target.title
|
|
1753
|
+
for field in ("tags", "aliases", "keywords"):
|
|
1754
|
+
merged: list[str] = []
|
|
1755
|
+
for value in list(getattr(target, field)) + list(result.get(field, [])):
|
|
1756
|
+
if isinstance(value, str) and value not in merged:
|
|
1757
|
+
merged.append(value)
|
|
1758
|
+
if merged or field in result:
|
|
1759
|
+
result[field] = merged
|
|
1760
|
+
return result
|
|
1761
|
+
|
|
1715
1762
|
def _infer_update_target(
|
|
1716
1763
|
self,
|
|
1717
1764
|
candidate: Mapping[str, Any],
|
|
@@ -2140,6 +2187,8 @@ class Processor:
|
|
|
2140
2187
|
# its own candidate; the aggregate shell itself is NO_CHANGE.
|
|
2141
2188
|
if candidate.get("worth") and is_aggregate_operational_text(candidate.get("memory")):
|
|
2142
2189
|
continue
|
|
2190
|
+
if candidate.get("worth") and is_attachment_followup_only_text(candidate.get("memory")):
|
|
2191
|
+
continue
|
|
2143
2192
|
candidate_scopes = list(candidate["scopes"])
|
|
2144
2193
|
# An automatic candidate with no reliable project attribution is
|
|
2145
2194
|
# retained as a retryable inbox turn, never silently promoted to
|
|
@@ -2322,6 +2371,10 @@ class Processor:
|
|
|
2322
2371
|
continue
|
|
2323
2372
|
if summary.get("decision") == NO_CHANGE_DECISION:
|
|
2324
2373
|
continue
|
|
2374
|
+
if is_attachment_followup_only_text(
|
|
2375
|
+
f"{summary.get('title', '')}\n{summary.get('body', '')}"
|
|
2376
|
+
):
|
|
2377
|
+
continue
|
|
2325
2378
|
if gate_update_target is not None:
|
|
2326
2379
|
summary_update_target = summary.get("update_memory_id")
|
|
2327
2380
|
if summary_update_target is None:
|
|
@@ -2338,6 +2391,8 @@ class Processor:
|
|
|
2338
2391
|
else:
|
|
2339
2392
|
summary = dict(summary)
|
|
2340
2393
|
summary["update_memory_id"] = gate_update_target
|
|
2394
|
+
target = self.service.read(gate_update_target, include_history=False)
|
|
2395
|
+
summary = self._merge_additive_project_plan_update(candidate, summary, target)
|
|
2341
2396
|
if summary["scopes"] == ["unscoped"] or summary.get("scope_source") == "insufficient_context":
|
|
2342
2397
|
self._defer_candidate(
|
|
2343
2398
|
turn_ref,
|
|
@@ -115,6 +115,10 @@ independent future use, set worth=false. A one-off meeting with no durable
|
|
|
115
115
|
decision or commitment, a request awaiting PM acceptance, and pending feedback
|
|
116
116
|
or another intermediate status normally have worth=false. Keep an independent
|
|
117
117
|
project risk or concrete future action even when it came from a digest.
|
|
118
|
+
A PPT, attachment, material, document, or issue-list item described only as
|
|
119
|
+
"needs follow-up/review/processing" has no independent action and normally has
|
|
120
|
+
worth=false; keep it only when the evidence supplies a concrete remediation,
|
|
121
|
+
owner, deadline, delivery rule, migration, or durable conclusion.
|
|
118
122
|
An intermediate state such as a draft awaiting confirmation, preparation/
|
|
119
123
|
processing status, temporary error, repeated confirmation, or unconfirmed
|
|
120
124
|
suggestion normally has no independent future use and is worth=false. Keep one
|
|
@@ -182,7 +186,8 @@ still has a concrete future-use fact or action. If not, return exactly
|
|
|
182
186
|
{"decision":"NO_CHANGE"}; this is the only no-write summary response. Use it
|
|
183
187
|
for temporary delegation, waiting for feedback, a one-off meeting arrangement with
|
|
184
188
|
no durable outcome/decision/project constraint, or a subject/attachment-only item
|
|
185
|
-
without a concrete impact or conclusion.
|
|
189
|
+
without a concrete impact or conclusion. Attachment sizes, mail/message IDs,
|
|
190
|
+
and a generic "needs follow-up" phrase do not create such an impact.
|
|
186
191
|
Explicit remember mode never permits NO_CHANGE and must return the normal summary
|
|
187
192
|
object. When producing a normal automatic summary, copy the gate candidate's
|
|
188
193
|
scopes exactly. scope_source may be omitted to inherit the gate value, but an
|
|
@@ -395,6 +395,45 @@ def is_aggregate_operational_text(value: Any) -> bool:
|
|
|
395
395
|
return count_signals >= 2 or (count_signals >= 1 and separator_count >= 1)
|
|
396
396
|
|
|
397
397
|
|
|
398
|
+
_ATTACHMENT_SUBJECT_MARKERS = (
|
|
399
|
+
"附件", "材料", "文档", "问题清单", "ppt", "slides", "deck", "attachment", "checklist",
|
|
400
|
+
)
|
|
401
|
+
_GENERIC_FOLLOWUP_MARKERS = (
|
|
402
|
+
"待跟进", "需跟进", "需要跟进", "待处理", "需处理", "需要处理", "请查看", "看下",
|
|
403
|
+
"follow up", "follow-up", "needs review", "to review",
|
|
404
|
+
)
|
|
405
|
+
_CONCRETE_ATTACHMENT_ACTION_MARKERS = (
|
|
406
|
+
"迁移", "部署", "修复", "整改", "逐项", "发送给", "发给", "抄送", "提交", "回复", "确认",
|
|
407
|
+
"负责人", "禁止", "必须", "以后", "归档至", "保存至", "migrate", "deploy", "fix",
|
|
408
|
+
"remediate", "send to", "cc ", "owner", "must",
|
|
409
|
+
)
|
|
410
|
+
_TRANSPORT_DETAIL = re.compile(
|
|
411
|
+
r"(?:\d+(?:\.\d+)?\s*(?:kb|mb|gb|字节)|(?:邮件|mail|message)\s*(?:id\s*[=::]?\s*)?#?\d+)",
|
|
412
|
+
re.IGNORECASE,
|
|
413
|
+
)
|
|
414
|
+
_CONCRETE_DEADLINE = re.compile(
|
|
415
|
+
r"(?:截至|截止|在|by|before)\s*\d{4}-\d{2}-\d{2}|\d{4}-\d{2}-\d{2}\s*(?:前|之前|截止)",
|
|
416
|
+
re.IGNORECASE,
|
|
417
|
+
)
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
def is_attachment_followup_only_text(value: Any) -> bool:
|
|
421
|
+
"""Return true for an attachment subject with no reusable concrete action."""
|
|
422
|
+
|
|
423
|
+
if not isinstance(value, str):
|
|
424
|
+
return False
|
|
425
|
+
folded = value.casefold()
|
|
426
|
+
if not any(marker in folded for marker in _ATTACHMENT_SUBJECT_MARKERS):
|
|
427
|
+
return False
|
|
428
|
+
if any(marker in folded for marker in _CONCRETE_ATTACHMENT_ACTION_MARKERS):
|
|
429
|
+
return False
|
|
430
|
+
if _CONCRETE_DEADLINE.search(value):
|
|
431
|
+
return False
|
|
432
|
+
return any(marker in folded for marker in _GENERIC_FOLLOWUP_MARKERS) or bool(
|
|
433
|
+
_TRANSPORT_DETAIL.search(value)
|
|
434
|
+
)
|
|
435
|
+
|
|
436
|
+
|
|
398
437
|
def _reject_constant(value: str) -> None:
|
|
399
438
|
raise ModelOutputError("non-finite JSON number is not allowed", validation_reason="invalid_json")
|
|
400
439
|
|
|
@@ -1335,6 +1374,7 @@ __all__ = [
|
|
|
1335
1374
|
"parse_compact_output",
|
|
1336
1375
|
"parse_strict_json",
|
|
1337
1376
|
"is_aggregate_operational_text",
|
|
1377
|
+
"is_attachment_followup_only_text",
|
|
1338
1378
|
"normalize_relative_calendar_text",
|
|
1339
1379
|
"parse_summarize_output",
|
|
1340
1380
|
"parse_summary_output",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: memleaf
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.13
|
|
4
4
|
Summary: A local-first Markdown memory core for AI agents
|
|
5
5
|
Author: memleaf contributors
|
|
6
6
|
License-Expression: MIT
|
|
@@ -23,8 +23,8 @@ Dynamic: license-file
|
|
|
23
23
|
|
|
24
24
|
[English](README.en.md) · [PyPI](https://pypi.org/project/memleaf/) · [GitHub](https://github.com/miffyblueboo/memleaf)
|
|
25
25
|
|
|
26
|
-
> **当前版本:0.2.
|
|
27
|
-
> 核心库、Vault、stdio MCP Server、初始化 CLI、模型路由、提炼流程、受控检索协议和宿主适配器已经实现。memleaf 0.2.
|
|
26
|
+
> **当前版本:0.2.13。**
|
|
27
|
+
> 核心库、Vault、stdio MCP Server、初始化 CLI、模型路由、提炼流程、受控检索协议和宿主适配器已经实现。memleaf 0.2.13 通过 PyPI 分发。
|
|
28
28
|
> **当前版本支持 Hermes 和 Codex。** Antigravity(反重力)不检测、不安装、不配置。
|
|
29
29
|
|
|
30
30
|
## 项目定位
|
|
@@ -358,6 +358,95 @@ class AdmissionFlowTests(unittest.TestCase):
|
|
|
358
358
|
self.assertNotIn("完成4条", memories[0].memory.body)
|
|
359
359
|
self.assertEqual([call["purpose"] for call in backend.calls], ["gate", "summarize"])
|
|
360
360
|
|
|
361
|
+
def test_attachment_only_followup_is_dropped_before_summary(self):
|
|
362
|
+
service = self.make_service("attachment-followup")
|
|
363
|
+
backend = QueueBackend()
|
|
364
|
+
user_key, assistant_key = self.capture_turn(
|
|
365
|
+
service,
|
|
366
|
+
session="attachment-followup",
|
|
367
|
+
turn="turn-1",
|
|
368
|
+
user="查看鑫元基金最近的邮件。",
|
|
369
|
+
assistant="评审PPT和SIT问题清单需要跟进处理。",
|
|
370
|
+
)
|
|
371
|
+
item = candidate(
|
|
372
|
+
"xinyuan-materials",
|
|
373
|
+
[user_key, assistant_key],
|
|
374
|
+
memory="鑫元基金评审PPT和SIT问题清单待跟进处理。",
|
|
375
|
+
)
|
|
376
|
+
item["scopes"] = ["project:鑫元基金"]
|
|
377
|
+
backend.responses.append(gate([item]))
|
|
378
|
+
|
|
379
|
+
result = service.process(source="hermes", session_id="attachment-followup", model=backend)
|
|
380
|
+
|
|
381
|
+
self.assertEqual(result["memories_written"], 0)
|
|
382
|
+
self.assertEqual(service._read_memories_unlocked("knowledge"), [])
|
|
383
|
+
self.assertEqual([call["purpose"] for call in backend.calls], ["gate"])
|
|
384
|
+
|
|
385
|
+
def test_attachment_summary_with_only_transport_details_is_dropped(self):
|
|
386
|
+
service = self.make_service("attachment-summary")
|
|
387
|
+
backend = QueueBackend()
|
|
388
|
+
user_key, assistant_key = self.capture_turn(
|
|
389
|
+
service,
|
|
390
|
+
session="attachment-summary",
|
|
391
|
+
turn="turn-1",
|
|
392
|
+
user="查看鑫元基金当前推进材料。",
|
|
393
|
+
assistant="有评审材料和问题清单。",
|
|
394
|
+
)
|
|
395
|
+
item = candidate(
|
|
396
|
+
"xinyuan-materials",
|
|
397
|
+
[user_key, assistant_key],
|
|
398
|
+
memory="鑫元基金当前推进材料。",
|
|
399
|
+
)
|
|
400
|
+
item["scopes"] = ["project:鑫元基金"]
|
|
401
|
+
summary_value = json.loads(
|
|
402
|
+
summary(
|
|
403
|
+
user_key,
|
|
404
|
+
title="鑫元基金评审PPT和SIT问题清单待跟进",
|
|
405
|
+
body="评审PPT(附件8MB,邮件918)和SIT问题清单(附件5.8MB,邮件920)需跟进处理。",
|
|
406
|
+
)
|
|
407
|
+
)
|
|
408
|
+
summary_value["scopes"] = ["project:鑫元基金"]
|
|
409
|
+
backend.responses.extend([gate([item]), json.dumps(summary_value, ensure_ascii=False)])
|
|
410
|
+
|
|
411
|
+
result = service.process(source="hermes", session_id="attachment-summary", model=backend)
|
|
412
|
+
|
|
413
|
+
self.assertEqual(result["memories_written"], 0)
|
|
414
|
+
self.assertEqual(service._read_memories_unlocked("knowledge"), [])
|
|
415
|
+
self.assertEqual([call["purpose"] for call in backend.calls], ["gate", "summarize"])
|
|
416
|
+
|
|
417
|
+
def test_attachment_with_owner_deadline_and_remediation_remains_admissible(self):
|
|
418
|
+
service = self.make_service("attachment-action")
|
|
419
|
+
backend = QueueBackend()
|
|
420
|
+
user_key, assistant_key = self.capture_turn(
|
|
421
|
+
service,
|
|
422
|
+
session="attachment-action",
|
|
423
|
+
turn="turn-1",
|
|
424
|
+
user="鑫元基金SIT问题清单怎么处理?",
|
|
425
|
+
assistant="张三需在2026-09-03前逐项整改SIT问题清单。",
|
|
426
|
+
)
|
|
427
|
+
item = candidate(
|
|
428
|
+
"xinyuan-sit-remediation",
|
|
429
|
+
[user_key, assistant_key],
|
|
430
|
+
memory="鑫元基金SIT问题清单需在2026-09-03前由张三逐项整改。",
|
|
431
|
+
type="todo",
|
|
432
|
+
)
|
|
433
|
+
item["scopes"] = ["project:鑫元基金"]
|
|
434
|
+
summary_value = json.loads(
|
|
435
|
+
summary(
|
|
436
|
+
user_key,
|
|
437
|
+
title="鑫元基金SIT问题清单整改",
|
|
438
|
+
body="张三需在2026-09-03前逐项整改鑫元基金SIT问题清单。",
|
|
439
|
+
type="todo",
|
|
440
|
+
)
|
|
441
|
+
)
|
|
442
|
+
summary_value.update({"scopes": ["project:鑫元基金"], "status": "active"})
|
|
443
|
+
backend.responses.extend([gate([item]), json.dumps(summary_value, ensure_ascii=False)])
|
|
444
|
+
|
|
445
|
+
result = service.process(source="hermes", session_id="attachment-action", model=backend)
|
|
446
|
+
|
|
447
|
+
self.assertEqual(result["memories_written"], 1)
|
|
448
|
+
self.assertIn("逐项整改", service._read_memories_unlocked("knowledge")[0].memory.body)
|
|
449
|
+
|
|
361
450
|
def test_mixed_preference_is_kept_while_mcp_failure_is_not(self):
|
|
362
451
|
service = self.make_service("mixed-preference")
|
|
363
452
|
backend = QueueBackend()
|
|
@@ -284,7 +284,7 @@ class HermesProviderTests(unittest.TestCase):
|
|
|
284
284
|
|
|
285
285
|
def test_provider_warns_with_one_line_update_when_core_versions_differ(self) -> None:
|
|
286
286
|
client = FakeClient(responses=[{"stats": True}])
|
|
287
|
-
client.server_version = "0.2.
|
|
287
|
+
client.server_version = "0.2.13"
|
|
288
288
|
provider = provider_module.MemleafMemoryProvider()
|
|
289
289
|
with patch.object(provider_module, "_resolve_command", return_value="memleaf-mcp"), \
|
|
290
290
|
patch.object(provider_module, "_MCPClient", return_value=client), \
|
|
@@ -303,11 +303,11 @@ class HermesProviderTests(unittest.TestCase):
|
|
|
303
303
|
|
|
304
304
|
def test_provider_does_not_warn_when_core_versions_match(self) -> None:
|
|
305
305
|
client = FakeClient(responses=[{"stats": True}])
|
|
306
|
-
client.server_version = "0.2.
|
|
306
|
+
client.server_version = "0.2.13"
|
|
307
307
|
provider = provider_module.MemleafMemoryProvider()
|
|
308
308
|
with patch.object(provider_module, "_resolve_command", return_value="memleaf-mcp"), \
|
|
309
309
|
patch.object(provider_module, "_MCPClient", return_value=client), \
|
|
310
|
-
patch.object(provider_module, "_provider_manifest_version", return_value="0.2.
|
|
310
|
+
patch.object(provider_module, "_provider_manifest_version", return_value="0.2.13"), \
|
|
311
311
|
patch.object(provider_module.logger, "warning") as warning:
|
|
312
312
|
provider.initialize(
|
|
313
313
|
"version-match-session",
|
|
@@ -324,7 +324,7 @@ class HermesProviderTests(unittest.TestCase):
|
|
|
324
324
|
provider = provider_module.MemleafMemoryProvider()
|
|
325
325
|
with patch.object(provider_module, "_resolve_command", return_value="memleaf-mcp"), \
|
|
326
326
|
patch.object(provider_module, "_MCPClient", return_value=client), \
|
|
327
|
-
patch.object(provider_module, "_provider_manifest_version", return_value="0.2.
|
|
327
|
+
patch.object(provider_module, "_provider_manifest_version", return_value="0.2.13"), \
|
|
328
328
|
patch.object(provider_module.logger, "warning") as warning:
|
|
329
329
|
provider.initialize(
|
|
330
330
|
"version-missing-session",
|
|
@@ -81,7 +81,7 @@ class PyPIInstallTests(unittest.TestCase):
|
|
|
81
81
|
|
|
82
82
|
self.assertTrue(target.is_dir())
|
|
83
83
|
self.assertFalse(target.is_symlink())
|
|
84
|
-
self.assertIn("version: 0.2.
|
|
84
|
+
self.assertIn("version: 0.2.13", (target / "plugin.yaml").read_text(encoding="utf-8"))
|
|
85
85
|
|
|
86
86
|
def test_windows_hermes_paths_follow_official_native_layout(self) -> None:
|
|
87
87
|
with tempfile.TemporaryDirectory(prefix="memleaf-win-paths-") as temporary:
|
|
@@ -212,16 +212,16 @@ class PyPIInstallTests(unittest.TestCase):
|
|
|
212
212
|
"status": "configured",
|
|
213
213
|
"reason": "ok",
|
|
214
214
|
"vault": "/tmp/memleaf-vault",
|
|
215
|
-
"core_version": "0.2.
|
|
216
|
-
"provider_version": "0.2.
|
|
215
|
+
"core_version": "0.2.13",
|
|
216
|
+
"provider_version": "0.2.13",
|
|
217
217
|
"model": {"status": "configured"},
|
|
218
218
|
}
|
|
219
219
|
with mock.patch("memleaf.installer.install_hermes", return_value=result), \
|
|
220
220
|
mock.patch("sys.stdout") as stdout:
|
|
221
221
|
self.assertEqual(cli.main(["install"]), 0)
|
|
222
222
|
output = "\n".join(str(call.args[0]) for call in stdout.write.call_args_list)
|
|
223
|
-
self.assertIn("core=0.2.
|
|
224
|
-
self.assertIn("Hermes provider=0.2.
|
|
223
|
+
self.assertIn("core=0.2.13", output)
|
|
224
|
+
self.assertIn("Hermes provider=0.2.13", output)
|
|
225
225
|
|
|
226
226
|
def test_readmes_document_one_line_core_and_provider_upgrade(self) -> None:
|
|
227
227
|
command = "python -m pip install -U memleaf && python -m memleaf install"
|
|
@@ -27,6 +27,7 @@ from memleaf.validation import (
|
|
|
27
27
|
ModelOutputError,
|
|
28
28
|
NO_CHANGE_DECISION,
|
|
29
29
|
is_aggregate_operational_text,
|
|
30
|
+
is_attachment_followup_only_text,
|
|
30
31
|
normalize_relative_calendar_text,
|
|
31
32
|
)
|
|
32
33
|
from memleaf.index import event_key, turn_key
|
|
@@ -445,6 +446,38 @@ class StageB1Test(unittest.TestCase):
|
|
|
445
446
|
)
|
|
446
447
|
)
|
|
447
448
|
|
|
449
|
+
def test_attachment_followup_detector_keeps_only_concrete_actions(self):
|
|
450
|
+
self.assertTrue(
|
|
451
|
+
is_attachment_followup_only_text(
|
|
452
|
+
"鑫元基金评审PPT和SIT问题清单待跟进处理。"
|
|
453
|
+
)
|
|
454
|
+
)
|
|
455
|
+
self.assertTrue(
|
|
456
|
+
is_attachment_followup_only_text(
|
|
457
|
+
"评审PPT附件8MB,邮件918,需跟进。"
|
|
458
|
+
)
|
|
459
|
+
)
|
|
460
|
+
self.assertTrue(
|
|
461
|
+
is_attachment_followup_only_text(
|
|
462
|
+
"评审PPT 2026-09-02版本待跟进。"
|
|
463
|
+
)
|
|
464
|
+
)
|
|
465
|
+
self.assertFalse(
|
|
466
|
+
is_attachment_followup_only_text(
|
|
467
|
+
"SIT问题清单需在2026-09-03前由张三逐项整改。"
|
|
468
|
+
)
|
|
469
|
+
)
|
|
470
|
+
self.assertFalse(
|
|
471
|
+
is_attachment_followup_only_text(
|
|
472
|
+
"以后需求文档直接发给王佳鑫、黄欣婕,并抄送谭宇来。"
|
|
473
|
+
)
|
|
474
|
+
)
|
|
475
|
+
self.assertFalse(
|
|
476
|
+
is_attachment_followup_only_text(
|
|
477
|
+
"中银国际历史数据库和附件需要全量迁移。"
|
|
478
|
+
)
|
|
479
|
+
)
|
|
480
|
+
|
|
448
481
|
def test_process_retries_schema_violation_until_third_gate_attempt_and_commits(self):
|
|
449
482
|
responses = [
|
|
450
483
|
json.dumps({"candidates": "invalid"}),
|
|
@@ -1100,6 +1100,89 @@ class StageB2ATest(unittest.TestCase):
|
|
|
1100
1100
|
self.assertEqual(processed["watermark"], 2)
|
|
1101
1101
|
self.assertEqual(processed["processing"]["status"], "idle")
|
|
1102
1102
|
|
|
1103
|
+
def test_additive_project_plan_update_retains_existing_plan_facts(self):
|
|
1104
|
+
backend = QueueBackend()
|
|
1105
|
+
service = self.service(backend, name="additive-project-plan")
|
|
1106
|
+
old_body = (
|
|
1107
|
+
"金元顺安信创实施计划采用达梦和东方通,要求38个工作日完成,"
|
|
1108
|
+
"上线日期为2026-10-27,负责人为吴江波。"
|
|
1109
|
+
)
|
|
1110
|
+
existing = service.create_memory(
|
|
1111
|
+
memory_id="mem-jinyuan-plan",
|
|
1112
|
+
title="金元顺安员工投资行为申报系统信创改造实施计划",
|
|
1113
|
+
body=old_body,
|
|
1114
|
+
tags=["金元顺安", "达梦", "东方通", "负责人"],
|
|
1115
|
+
type="project",
|
|
1116
|
+
scopes=["project:金元顺安"],
|
|
1117
|
+
)
|
|
1118
|
+
user_key, assistant_key = self.capture_turn(
|
|
1119
|
+
service,
|
|
1120
|
+
source="hermes",
|
|
1121
|
+
session="jinyuan-plan",
|
|
1122
|
+
turn="feedback",
|
|
1123
|
+
user_event="jinyuan-plan-user",
|
|
1124
|
+
assistant_event="jinyuan-plan-assistant",
|
|
1125
|
+
user="金元顺安客户对实施计划提出补充建议。",
|
|
1126
|
+
assistant="需增加数据迁移、安全基线、漏洞扫描和回滚演练。",
|
|
1127
|
+
)
|
|
1128
|
+
item = self.candidate(
|
|
1129
|
+
"jinyuan-plan-feedback",
|
|
1130
|
+
[user_key, assistant_key],
|
|
1131
|
+
memory="金元顺安实施计划需补充数据迁移、安全基线、漏洞扫描和回滚演练。",
|
|
1132
|
+
type="fact",
|
|
1133
|
+
)
|
|
1134
|
+
item["scopes"] = ["project:金元顺安"]
|
|
1135
|
+
backend.responses.extend(
|
|
1136
|
+
[
|
|
1137
|
+
self.gate([item]),
|
|
1138
|
+
self.summary(
|
|
1139
|
+
user_key,
|
|
1140
|
+
title=existing.title,
|
|
1141
|
+
body="客户建议补充数据迁移、安全基线、漏洞扫描和回滚演练。",
|
|
1142
|
+
tags=["金元顺安", "调整建议"],
|
|
1143
|
+
type="project",
|
|
1144
|
+
scopes=["project:金元顺安"],
|
|
1145
|
+
update_memory_id=existing.memory_id,
|
|
1146
|
+
),
|
|
1147
|
+
]
|
|
1148
|
+
)
|
|
1149
|
+
|
|
1150
|
+
result = service.process(source="hermes", session_id="jinyuan-plan", model=backend)
|
|
1151
|
+
|
|
1152
|
+
self.assertEqual(result["memory_ids"], [existing.memory_id])
|
|
1153
|
+
current = service.read(existing.memory_id)
|
|
1154
|
+
self.assertIn(old_body, current.body)
|
|
1155
|
+
self.assertIn("回滚演练", current.body)
|
|
1156
|
+
self.assertEqual(current.title, existing.title)
|
|
1157
|
+
self.assertEqual(current.tags, ["金元顺安", "达梦", "东方通", "负责人", "调整建议"])
|
|
1158
|
+
history = service._read_memories_unlocked("history")
|
|
1159
|
+
self.assertEqual(len(history), 1)
|
|
1160
|
+
self.assertEqual(history[0].memory.body, old_body)
|
|
1161
|
+
|
|
1162
|
+
def test_explicit_project_plan_replacement_does_not_merge_conflicting_body(self):
|
|
1163
|
+
target = self.service().create_memory(
|
|
1164
|
+
memory_id="mem-plan-replacement",
|
|
1165
|
+
title="北辰项目实施计划",
|
|
1166
|
+
body="北辰项目数据库采用达梦。",
|
|
1167
|
+
tags=["达梦"],
|
|
1168
|
+
type="project",
|
|
1169
|
+
scopes=["project:北辰"],
|
|
1170
|
+
)
|
|
1171
|
+
summary = {
|
|
1172
|
+
"title": target.title,
|
|
1173
|
+
"body": "北辰项目数据库改为PostgreSQL。",
|
|
1174
|
+
"tags": ["PostgreSQL"],
|
|
1175
|
+
"type": "project",
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
merged = Processor._merge_additive_project_plan_update(
|
|
1179
|
+
{"memory": "客户建议将北辰项目数据库改为PostgreSQL。"},
|
|
1180
|
+
summary,
|
|
1181
|
+
target,
|
|
1182
|
+
)
|
|
1183
|
+
|
|
1184
|
+
self.assertEqual(merged, summary)
|
|
1185
|
+
|
|
1103
1186
|
def test_summary_update_target_mismatch_is_retried_before_writing(self):
|
|
1104
1187
|
backend = QueueBackend()
|
|
1105
1188
|
service = self.service(backend, name="email-update-mismatch")
|
|
@@ -1252,7 +1335,10 @@ class StageB2ATest(unittest.TestCase):
|
|
|
1252
1335
|
|
|
1253
1336
|
self.assertEqual(result["memories_written"], 1)
|
|
1254
1337
|
self.assertEqual([call["purpose"] for call in backend.calls], ["gate", "summarize"])
|
|
1255
|
-
self.assertEqual(
|
|
1338
|
+
self.assertEqual(
|
|
1339
|
+
service.read(old.memory_id).body,
|
|
1340
|
+
"金元顺安实施计划按原方案执行。\n\n金元顺安实施计划新增部署要求。",
|
|
1341
|
+
)
|
|
1256
1342
|
self.assertEqual(len(service.vault.list_markdown("history")), 1)
|
|
1257
1343
|
|
|
1258
1344
|
def test_same_project_plan_fact_candidate_is_reconciled_to_immutable_project_target(self):
|
|
@@ -104,7 +104,7 @@ class StageC1MCPTest(unittest.TestCase):
|
|
|
104
104
|
testcase.assertEqual(result["resultType"], "complete")
|
|
105
105
|
testcase.assertEqual(
|
|
106
106
|
result["_meta"]["io.modelcontextprotocol/serverInfo"],
|
|
107
|
-
{"name": "memleaf", "version": "0.2.
|
|
107
|
+
{"name": "memleaf", "version": "0.2.13"},
|
|
108
108
|
)
|
|
109
109
|
return result
|
|
110
110
|
|
|
@@ -24,7 +24,7 @@ class StageC3PackagingTests(unittest.TestCase):
|
|
|
24
24
|
|
|
25
25
|
def test_project_metadata_and_version_export(self) -> None:
|
|
26
26
|
self.assertEqual(self.project["name"], "memleaf")
|
|
27
|
-
self.assertEqual(self.project["version"], "0.2.
|
|
27
|
+
self.assertEqual(self.project["version"], "0.2.13")
|
|
28
28
|
self.assertEqual(self.project["requires-python"], ">=3.11")
|
|
29
29
|
self.assertEqual(self.project["dependencies"], [])
|
|
30
30
|
self.assertEqual(self.project["license"], "MIT")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|