memleaf 0.2.13__tar.gz → 0.2.14__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.
Files changed (103) hide show
  1. {memleaf-0.2.13 → memleaf-0.2.14}/CHANGELOG.md +11 -0
  2. {memleaf-0.2.13/src/memleaf.egg-info → memleaf-0.2.14}/PKG-INFO +3 -3
  3. {memleaf-0.2.13 → memleaf-0.2.14}/README.en.md +2 -2
  4. {memleaf-0.2.13 → memleaf-0.2.14}/README.md +2 -2
  5. {memleaf-0.2.13 → memleaf-0.2.14}/pyproject.toml +1 -1
  6. {memleaf-0.2.13 → memleaf-0.2.14}/src/memleaf/__init__.py +1 -1
  7. {memleaf-0.2.13 → memleaf-0.2.14}/src/memleaf/hermes_provider/__init__.py +1 -0
  8. {memleaf-0.2.13 → memleaf-0.2.14}/src/memleaf/hermes_provider/plugin.yaml +1 -1
  9. {memleaf-0.2.13 → memleaf-0.2.14}/src/memleaf/processing.py +13 -20
  10. {memleaf-0.2.13 → memleaf-0.2.14}/src/memleaf/prompts.py +39 -0
  11. {memleaf-0.2.13 → memleaf-0.2.14}/src/memleaf/validation.py +265 -0
  12. {memleaf-0.2.13 → memleaf-0.2.14/src/memleaf.egg-info}/PKG-INFO +3 -3
  13. {memleaf-0.2.13 → memleaf-0.2.14}/src/memleaf.egg-info/SOURCES.txt +1 -0
  14. memleaf-0.2.14/tests/test_extraction_quality_regressions.py +680 -0
  15. {memleaf-0.2.13 → memleaf-0.2.14}/tests/test_hermes_provider.py +42 -4
  16. {memleaf-0.2.13 → memleaf-0.2.14}/tests/test_pypi_install.py +5 -5
  17. {memleaf-0.2.13 → memleaf-0.2.14}/tests/test_stage_b2b.py +1 -1
  18. {memleaf-0.2.13 → memleaf-0.2.14}/tests/test_stage_c1_mcp.py +1 -1
  19. {memleaf-0.2.13 → memleaf-0.2.14}/tests/test_stage_c3_packaging.py +1 -1
  20. {memleaf-0.2.13 → memleaf-0.2.14}/IMPLEMENTATION_PLAN.md +0 -0
  21. {memleaf-0.2.13 → memleaf-0.2.14}/LICENSE +0 -0
  22. {memleaf-0.2.13 → memleaf-0.2.14}/MANIFEST.in +0 -0
  23. {memleaf-0.2.13 → memleaf-0.2.14}/RELEASE_CHECKLIST.md +0 -0
  24. {memleaf-0.2.13 → memleaf-0.2.14}/examples/README.md +0 -0
  25. {memleaf-0.2.13 → memleaf-0.2.14}/examples/basic_usage.py +0 -0
  26. {memleaf-0.2.13 → memleaf-0.2.14}/examples/mcp_stdio.ndjson +0 -0
  27. {memleaf-0.2.13 → memleaf-0.2.14}/install.ps1 +0 -0
  28. {memleaf-0.2.13 → memleaf-0.2.14}/install.sh +0 -0
  29. {memleaf-0.2.13 → memleaf-0.2.14}/setup.cfg +0 -0
  30. {memleaf-0.2.13 → memleaf-0.2.14}/src/memleaf/__main__.py +0 -0
  31. {memleaf-0.2.13 → memleaf-0.2.14}/src/memleaf/adapters/__init__.py +0 -0
  32. {memleaf-0.2.13 → memleaf-0.2.14}/src/memleaf/adapters/antigravity.py +0 -0
  33. {memleaf-0.2.13 → memleaf-0.2.14}/src/memleaf/adapters/base.py +0 -0
  34. {memleaf-0.2.13 → memleaf-0.2.14}/src/memleaf/adapters/codex.py +0 -0
  35. {memleaf-0.2.13 → memleaf-0.2.14}/src/memleaf/adapters/hermes.py +0 -0
  36. {memleaf-0.2.13 → memleaf-0.2.14}/src/memleaf/budget.py +0 -0
  37. {memleaf-0.2.13 → memleaf-0.2.14}/src/memleaf/capture.py +0 -0
  38. {memleaf-0.2.13 → memleaf-0.2.14}/src/memleaf/cli.py +0 -0
  39. {memleaf-0.2.13 → memleaf-0.2.14}/src/memleaf/compaction.py +0 -0
  40. {memleaf-0.2.13 → memleaf-0.2.14}/src/memleaf/config.py +0 -0
  41. {memleaf-0.2.13 → memleaf-0.2.14}/src/memleaf/credentials.py +0 -0
  42. {memleaf-0.2.13 → memleaf-0.2.14}/src/memleaf/frontmatter.py +0 -0
  43. {memleaf-0.2.13 → memleaf-0.2.14}/src/memleaf/hermes_provider/README.md +0 -0
  44. {memleaf-0.2.13 → memleaf-0.2.14}/src/memleaf/host_events.py +0 -0
  45. {memleaf-0.2.13 → memleaf-0.2.14}/src/memleaf/host_runtime.py +0 -0
  46. {memleaf-0.2.13 → memleaf-0.2.14}/src/memleaf/inbox.py +0 -0
  47. {memleaf-0.2.13 → memleaf-0.2.14}/src/memleaf/index.py +0 -0
  48. {memleaf-0.2.13 → memleaf-0.2.14}/src/memleaf/installer.py +0 -0
  49. {memleaf-0.2.13 → memleaf-0.2.14}/src/memleaf/llm/__init__.py +0 -0
  50. {memleaf-0.2.13 → memleaf-0.2.14}/src/memleaf/llm/base.py +0 -0
  51. {memleaf-0.2.13 → memleaf-0.2.14}/src/memleaf/llm/claude_compatible.py +0 -0
  52. {memleaf-0.2.13 → memleaf-0.2.14}/src/memleaf/llm/gemini.py +0 -0
  53. {memleaf-0.2.13 → memleaf-0.2.14}/src/memleaf/llm/openai_compatible.py +0 -0
  54. {memleaf-0.2.13 → memleaf-0.2.14}/src/memleaf/llm/router.py +0 -0
  55. {memleaf-0.2.13 → memleaf-0.2.14}/src/memleaf/locking.py +0 -0
  56. {memleaf-0.2.13 → memleaf-0.2.14}/src/memleaf/mcp_server.py +0 -0
  57. {memleaf-0.2.13 → memleaf-0.2.14}/src/memleaf/memory_writer.py +0 -0
  58. {memleaf-0.2.13 → memleaf-0.2.14}/src/memleaf/model_discovery.py +0 -0
  59. {memleaf-0.2.13 → memleaf-0.2.14}/src/memleaf/models.py +0 -0
  60. {memleaf-0.2.13 → memleaf-0.2.14}/src/memleaf/native_index.py +0 -0
  61. {memleaf-0.2.13 → memleaf-0.2.14}/src/memleaf/redaction.py +0 -0
  62. {memleaf-0.2.13 → memleaf-0.2.14}/src/memleaf/retrieval.py +0 -0
  63. {memleaf-0.2.13 → memleaf-0.2.14}/src/memleaf/retrieval_gate.py +0 -0
  64. {memleaf-0.2.13 → memleaf-0.2.14}/src/memleaf/scope_maintenance.py +0 -0
  65. {memleaf-0.2.13 → memleaf-0.2.14}/src/memleaf/scope_state.py +0 -0
  66. {memleaf-0.2.13 → memleaf-0.2.14}/src/memleaf/service.py +0 -0
  67. {memleaf-0.2.13 → memleaf-0.2.14}/src/memleaf/vault.py +0 -0
  68. {memleaf-0.2.13 → memleaf-0.2.14}/src/memleaf.egg-info/dependency_links.txt +0 -0
  69. {memleaf-0.2.13 → memleaf-0.2.14}/src/memleaf.egg-info/entry_points.txt +0 -0
  70. {memleaf-0.2.13 → memleaf-0.2.14}/src/memleaf.egg-info/top_level.txt +0 -0
  71. {memleaf-0.2.13 → memleaf-0.2.14}/tests/__init__.py +0 -0
  72. {memleaf-0.2.13 → memleaf-0.2.14}/tests/test_admission_noise.py +0 -0
  73. {memleaf-0.2.13 → memleaf-0.2.14}/tests/test_codex_install.py +0 -0
  74. {memleaf-0.2.13 → memleaf-0.2.14}/tests/test_codex_native_cli.py +0 -0
  75. {memleaf-0.2.13 → memleaf-0.2.14}/tests/test_context_budget.py +0 -0
  76. {memleaf-0.2.13 → memleaf-0.2.14}/tests/test_credential_safety.py +0 -0
  77. {memleaf-0.2.13 → memleaf-0.2.14}/tests/test_cross_host_acceptance.py +0 -0
  78. {memleaf-0.2.13 → memleaf-0.2.14}/tests/test_hermes_stdio_transport.py +0 -0
  79. {memleaf-0.2.13 → memleaf-0.2.14}/tests/test_host_events.py +0 -0
  80. {memleaf-0.2.13 → memleaf-0.2.14}/tests/test_host_runtime_contract.py +0 -0
  81. {memleaf-0.2.13 → memleaf-0.2.14}/tests/test_install.py +0 -0
  82. {memleaf-0.2.13 → memleaf-0.2.14}/tests/test_maintenance_v2.py +0 -0
  83. {memleaf-0.2.13 → memleaf-0.2.14}/tests/test_model_discovery.py +0 -0
  84. {memleaf-0.2.13 → memleaf-0.2.14}/tests/test_retrieval_gate.py +0 -0
  85. {memleaf-0.2.13 → memleaf-0.2.14}/tests/test_retrieval_v2.py +0 -0
  86. {memleaf-0.2.13 → memleaf-0.2.14}/tests/test_session_lineage.py +0 -0
  87. {memleaf-0.2.13 → memleaf-0.2.14}/tests/test_stage_a.py +0 -0
  88. {memleaf-0.2.13 → memleaf-0.2.14}/tests/test_stage_b1.py +0 -0
  89. {memleaf-0.2.13 → memleaf-0.2.14}/tests/test_stage_b2a.py +0 -0
  90. {memleaf-0.2.13 → memleaf-0.2.14}/tests/test_stage_b3a_commit.py +0 -0
  91. {memleaf-0.2.13 → memleaf-0.2.14}/tests/test_stage_b3a_contract.py +0 -0
  92. {memleaf-0.2.13 → memleaf-0.2.14}/tests/test_stage_b3b_native_context.py +0 -0
  93. {memleaf-0.2.13 → memleaf-0.2.14}/tests/test_stage_b3b_native_index.py +0 -0
  94. {memleaf-0.2.13 → memleaf-0.2.14}/tests/test_stage_b3b_scope.py +0 -0
  95. {memleaf-0.2.13 → memleaf-0.2.14}/tests/test_stage_b3c_retrieval.py +0 -0
  96. {memleaf-0.2.13 → memleaf-0.2.14}/tests/test_stage_b3d_scope_maintenance.py +0 -0
  97. {memleaf-0.2.13 → memleaf-0.2.14}/tests/test_stage_c2_init.py +0 -0
  98. {memleaf-0.2.13 → memleaf-0.2.14}/tests/test_upgrade_preserves_vault.py +0 -0
  99. {memleaf-0.2.13 → memleaf-0.2.14}/tests/test_v2_gate_limits.py +0 -0
  100. {memleaf-0.2.13 → memleaf-0.2.14}/tests/test_v2_host_flow.py +0 -0
  101. {memleaf-0.2.13 → memleaf-0.2.14}/tests/test_v2_mcp_flow.py +0 -0
  102. {memleaf-0.2.13 → memleaf-0.2.14}/tests/test_v2_nomatch_semantics.py +0 -0
  103. {memleaf-0.2.13 → memleaf-0.2.14}/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.14 — 2026-09-02
6
+
7
+ - Reject a permanent-memory candidate that combines a durable project rule or
8
+ implementation-plan change with an independent dated todo, while preserving
9
+ multiple dated milestones that belong to one project plan.
10
+ - Normalize explicit implementation plans and plan adjustments to `project`
11
+ without promoting adjacent mail, attachment, meeting, or archive records.
12
+ - Keep the gate-selected type immutable through summarization for both CREATE
13
+ and UPDATE, preserve multiple legal update candidates, and expose the new
14
+ `mixed_future_use` diagnostic through the Hermes provider.
15
+
5
16
  ## 0.2.13 — 2026-09-02
6
17
 
7
18
  - Preserve the full active project-plan body, stable title, and retrieval
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: memleaf
3
- Version: 0.2.13
3
+ Version: 0.2.14
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.13。**
27
- > 核心库、Vault、stdio MCP Server、初始化 CLI、模型路由、提炼流程、受控检索协议和宿主适配器已经实现。memleaf 0.2.13 通过 PyPI 分发。
26
+ > **当前版本:0.2.14。**
27
+ > 核心库、Vault、stdio MCP Server、初始化 CLI、模型路由、提炼流程、受控检索协议和宿主适配器已经实现。memleaf 0.2.14 通过 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.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.
7
+ > **Version: 0.2.14.**
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.14 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.13。**
8
- > 核心库、Vault、stdio MCP Server、初始化 CLI、模型路由、提炼流程、受控检索协议和宿主适配器已经实现。memleaf 0.2.13 通过 PyPI 分发。
7
+ > **当前版本:0.2.14。**
8
+ > 核心库、Vault、stdio MCP Server、初始化 CLI、模型路由、提炼流程、受控检索协议和宿主适配器已经实现。memleaf 0.2.14 通过 PyPI 分发。
9
9
  > **当前版本支持 Hermes 和 Codex。** Antigravity(反重力)不检测、不安装、不配置。
10
10
 
11
11
  ## 项目定位
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "memleaf"
7
- version = "0.2.13"
7
+ version = "0.2.14"
8
8
  description = "A local-first Markdown memory core for AI agents"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -1,6 +1,6 @@
1
1
  """Local-first Markdown memory core for AI agents."""
2
2
 
3
- __version__ = "0.2.13"
3
+ __version__ = "0.2.14"
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
@@ -94,6 +94,7 @@ _MODEL_VALIDATION_DETAILS = frozenset(
94
94
  "source_shape",
95
95
  "todo_fields",
96
96
  "relative_time",
97
+ "mixed_future_use",
97
98
  "other_schema_violation",
98
99
  }
99
100
  )
@@ -1,5 +1,5 @@
1
1
  name: memleaf
2
- version: 0.2.13
2
+ version: 0.2.14
3
3
  description: "Hermes-native external MemoryProvider for local-first Markdown memory shared with the memleaf MCP server."
4
4
  hooks:
5
5
  - prefetch
@@ -34,6 +34,7 @@ from .prompts import (
34
34
  DUPLICATE_TARGET_CORRECTION,
35
35
  GATE_TYPE_CORRECTION,
36
36
  JSON_CORRECTION,
37
+ MIXED_FUTURE_USE_CORRECTION,
37
38
  MIXED_PROJECT_SCOPES_CORRECTION,
38
39
  RELATIVE_TIME_CORRECTION,
39
40
  GATE_SYSTEM,
@@ -66,6 +67,7 @@ from .validation import (
66
67
  normalize_relative_calendar_text,
67
68
  is_aggregate_operational_text,
68
69
  is_attachment_followup_only_text,
70
+ is_project_plan_text,
69
71
  )
70
72
  from .vault import safe_component
71
73
 
@@ -599,6 +601,8 @@ class Processor:
599
601
  return DUPLICATE_TARGET_CORRECTION
600
602
  if hint == "mixed_project_scopes":
601
603
  return MIXED_PROJECT_SCOPES_CORRECTION
604
+ if hint == "mixed_future_use":
605
+ return MIXED_FUTURE_USE_CORRECTION
602
606
  if stage == "gate" and hint == "update_target_type_mismatch":
603
607
  return UPDATE_TARGET_TYPE_CORRECTION
604
608
  if stage == "gate" and hint == "invalid_type":
@@ -1696,22 +1700,18 @@ class Processor:
1696
1700
 
1697
1701
  @staticmethod
1698
1702
  def _is_project_plan_title(value: Any) -> bool:
1699
- text = normalize_term(value) if isinstance(value, str) else ""
1700
- return any(
1701
- marker in text
1702
- for marker in ("实施计划", "项目计划", "implementation plan", "project plan")
1703
- )
1703
+ return is_project_plan_text(value)
1704
1704
 
1705
1705
  @staticmethod
1706
1706
  def _is_adjacent_plan_record(value: Any) -> bool:
1707
1707
  text = normalize_term(value) if isinstance(value, str) else ""
1708
- return any(
1708
+ return bool(text) and any(
1709
1709
  marker in text
1710
1710
  for marker in (
1711
1711
  "已发送", "发送", "邮件", "附件", "存档", "会议", "启动会", "纪要",
1712
1712
  "sent", "email", "mail", "attachment", "archive", "meeting", "minutes",
1713
1713
  )
1714
- )
1714
+ ) and not is_project_plan_text(value)
1715
1715
 
1716
1716
  @classmethod
1717
1717
  def _merge_additive_project_plan_update(
@@ -2335,20 +2335,13 @@ class Processor:
2335
2335
  expected_scopes=candidate["scopes"],
2336
2336
  expected_scope_source=candidate["scope_source"],
2337
2337
  allow_no_change=True,
2338
- expected_type=(
2339
- candidate.get("type")
2340
- if (
2341
- gate_update_target is not None
2342
- and gate_target_type == candidate.get("type")
2343
- )
2344
- else None
2345
- ),
2338
+ # The summarize stage may not reinterpret a gate
2339
+ # candidate, including CREATE candidates. Updates
2340
+ # additionally retain the active target's immutable
2341
+ # type below.
2342
+ expected_type=candidate.get("type"),
2346
2343
  expected_update_memory_id=gate_update_target,
2347
- expected_target_type=(
2348
- gate_target_type
2349
- if gate_target_type == candidate.get("type")
2350
- else None
2351
- ),
2344
+ expected_target_type=gate_target_type,
2352
2345
  ),
2353
2346
  diagnostic_context={
2354
2347
  "source": turn.source,
@@ -108,6 +108,13 @@ be independently retrieved and updated. Prefer one coherent current-state memory
108
108
  over adjacent overlapping snapshots; do not use a score or fixed category rule.
109
109
  Most ordinary turns should produce zero or one candidate; multiple require
110
110
  genuinely independent future questions or actions.
111
+ Never combine a durable project rule or implementation-plan change with an
112
+ independent dated todo in one candidate. For example, a project plan/rule and
113
+ "reply or investigate by YYYY-MM-DD" are two future uses: emit separate
114
+ candidates, preserving each candidate's own evidence and project scope. A
115
+ date that is an intrinsic milestone of the same implementation plan may remain
116
+ inside that one project candidate. A candidate that combines these independent
117
+ uses is invalid and must be split before summarization.
111
118
  Never persist an aggregate mailbox sweep, daily report, or "items to watch"
112
119
  digest as one cross-project memory. Inspect each item and split genuinely
113
120
  independent project risks or actionable todo items; if an item has no
@@ -155,6 +162,16 @@ same-use project targets remain, do not guess an update target. A candidate
155
162
  that still combines a mailbox or daily digest shell with counts/statuses is not
156
163
  atomic: set worth=false for the aggregate, and emit any independent concrete
157
164
  action as a separate candidate.
165
+ If the same confirmed change applies to multiple same-project active memories,
166
+ emit one candidate per independently changed memory and set each exact
167
+ update_memory_id; never select one target and silently omit the others. Do not
168
+ target adjacent sent-mail, attachment, meeting, or archive records unless the
169
+ current evidence changes their own future-use topic.
170
+ An explicit implementation plan, project implementation plan, or plan
171
+ adjustment is type project. Do not label it fact merely because it came from
172
+ an email or because no existing project memory was retrieved. Keep an actual
173
+ todo as todo only when its future use is the action itself, not the durable
174
+ plan it mentions.
158
175
  Return no prose, markdown fences, comments, or trailing text."""
159
176
 
160
177
 
@@ -265,6 +282,16 @@ plan target over same-topic sent-mail, attachment/archive, or meeting records;
265
282
  the target type remains immutable. Never summarize a combined mailbox/daily
266
283
  digest shell with counts or statuses; return NO_CHANGE for that aggregate,
267
284
  while a separate atomic future-use action may still be summarized.
285
+ If the current confirmed change applies to multiple same-project active
286
+ memories, the gate will provide separate candidates and exact update targets;
287
+ preserve each candidate's target and do not collapse them into one summary.
288
+ Never combine a durable project rule or implementation-plan change with an
289
+ independent dated todo in one summary. A plan/rule and a dated reply,
290
+ investigation, submission, or follow-up are separate future uses; such output
291
+ is invalid and must be retried as separate gate candidates. A date that is an
292
+ intrinsic milestone of the same project plan may remain in its project summary.
293
+ An explicit implementation plan or plan adjustment must use type project;
294
+ preserve an existing update target's type when it is the same project plan.
268
295
  Return no prose, markdown fences, comments, or trailing text."""
269
296
 
270
297
 
@@ -325,6 +352,18 @@ MIXED_PROJECT_SCOPES_CORRECTION = (
325
352
  )
326
353
 
327
354
 
355
+ MIXED_FUTURE_USE_CORRECTION = (
356
+ "Previous output violated: mixed_future_use. Keep every candidate and "
357
+ "summary atomic. Split a durable project rule or implementation-plan "
358
+ "change from an independent dated todo such as a reply, investigation, "
359
+ "submission, or follow-up; preserve each part's own evidence and project "
360
+ "scope. A date intrinsic to the same implementation plan may remain in "
361
+ "that project candidate. Return separate gate candidates or return "
362
+ "NO_CHANGE for a part with no independent future use. Return only the "
363
+ "strict JSON object."
364
+ )
365
+
366
+
328
367
  DUPLICATE_TARGET_CORRECTION = (
329
368
  "Previous output violated: duplicate_update_target. Merge every candidate "
330
369
  "that refers to the same active memory target into one candidate for one "
@@ -75,6 +75,7 @@ MODEL_VALIDATION_DETAILS = frozenset(
75
75
  "source_shape",
76
76
  "todo_fields",
77
77
  "relative_time",
78
+ "mixed_future_use",
78
79
  "other_schema_violation",
79
80
  )
80
81
  )
@@ -377,6 +378,229 @@ _AGGREGATE_COUNT = re.compile(
377
378
  )
378
379
 
379
380
 
381
+ # These are intentionally narrow, high-signal markers. The gate is still
382
+ # responsible for deciding whether a candidate is worth retaining; these
383
+ # helpers only protect the write boundary from a known class of semantic
384
+ # contracts that cannot safely be represented by one memory.
385
+ _PROJECT_PLAN_MARKERS = (
386
+ "实施计划",
387
+ "项目计划",
388
+ "实施方案",
389
+ "项目方案",
390
+ "计划调整",
391
+ "调整计划",
392
+ "调整实施计划",
393
+ "重新压实计划",
394
+ "重新压实实施计划",
395
+ "重新制定计划",
396
+ "部署计划",
397
+ "上线计划",
398
+ "implementation plan",
399
+ "project plan",
400
+ "implementation schedule",
401
+ "plan adjustment",
402
+ "adjust the plan",
403
+ "revise the plan",
404
+ "rebaseline the plan",
405
+ )
406
+ _ADJACENT_PLAN_RECORD_MARKERS = (
407
+ "邮件",
408
+ "附件",
409
+ "会议纪要",
410
+ "会议记录",
411
+ "启动会",
412
+ "存档",
413
+ "归档",
414
+ "email",
415
+ "mail",
416
+ "attachment",
417
+ "meeting minutes",
418
+ "meeting record",
419
+ "archive",
420
+ )
421
+ _PROJECT_RULE_MARKERS = (
422
+ "项目要求",
423
+ "项目约束",
424
+ "交付要求",
425
+ "技术路线",
426
+ "长期要求",
427
+ "长期约束",
428
+ "统一规范",
429
+ "固定规则",
430
+ "以后",
431
+ "project requirement",
432
+ "project constraint",
433
+ "delivery requirement",
434
+ "technical approach",
435
+ )
436
+ _DATED_TODO_MARKERS = (
437
+ "待办",
438
+ "排查",
439
+ "答复",
440
+ "回复",
441
+ "提交",
442
+ "跟进",
443
+ "提醒",
444
+ "联系",
445
+ "发给",
446
+ "发送",
447
+ "处理",
448
+ "确认",
449
+ "整改",
450
+ "修复",
451
+ "回访",
452
+ "催办",
453
+ "反馈",
454
+ "完成",
455
+ "补充材料",
456
+ "提供",
457
+ "todo",
458
+ "deadline",
459
+ "due",
460
+ "follow up",
461
+ "follow-up",
462
+ )
463
+ _INDEPENDENT_TODO_MARKERS = tuple(
464
+ marker for marker in _DATED_TODO_MARKERS if marker != "完成"
465
+ )
466
+ _PLAN_MILESTONE_MARKERS = (
467
+ "计划",
468
+ "上线日期",
469
+ "交付日期",
470
+ "完成日期",
471
+ "工期",
472
+ "工作日",
473
+ "里程碑",
474
+ "上线",
475
+ "验收",
476
+ "交付",
477
+ "迁移",
478
+ "milestone",
479
+ )
480
+ _PLAN_STAGE_TEXT = re.compile(
481
+ r"(?:第[一二三四五六七八九十百\d]+阶段|阶段[一二三四五六七八九十百\d]+|"
482
+ r"phase\s*(?:one|two|three|four|five|\d+))",
483
+ re.IGNORECASE,
484
+ )
485
+ _CALENDAR_TEXT = re.compile(
486
+ rf"(?:{_RELATIVE_DATE_TOKEN}|"
487
+ r"(?<![A-Za-z\d./-])(?:\d{4}[/\-](?:0?[1-9]|1[0-2])[/\-](?:0?[1-9]|[12]\d|3[01])|"
488
+ r"(?:\d{4}\s*年\s*)?(?:0?[1-9]|1[0-2])\s*月\s*(?:0?[1-9]|[12]\d|3[01])\s*日?)"
489
+ r")",
490
+ re.IGNORECASE,
491
+ )
492
+
493
+
494
+ def is_project_plan_text(value: Any) -> bool:
495
+ """Return whether text explicitly describes a project plan or adjustment."""
496
+
497
+ if not isinstance(value, str):
498
+ return False
499
+ folded = value.casefold()
500
+ # Only a record whose own label/phrase is a plan mail, attachment, meeting
501
+ # record, or archive is adjacent. A durable plan may still mention that
502
+ # an attachment must be migrated, an email sent, or a meeting held.
503
+ compact = re.sub(r"[\s::,,;;。.!!??()()\[\]【】_-]+", "", folded)
504
+ plan_markers = tuple(
505
+ re.sub(r"\s+", "", marker.casefold()) for marker in _PROJECT_PLAN_MARKERS
506
+ )
507
+ adjacent_markers = tuple(
508
+ re.sub(r"\s+", "", marker.casefold())
509
+ for marker in _ADJACENT_PLAN_RECORD_MARKERS
510
+ )
511
+ direct_adjacent = any(
512
+ f"{plan}{adjacent}" in compact or f"{adjacent}{plan}" in compact
513
+ for plan in plan_markers
514
+ for adjacent in adjacent_markers
515
+ )
516
+ archived_plan = any(
517
+ f"{plan}已{archive}" in compact or f"已{archive}{plan}" in compact
518
+ for plan in plan_markers
519
+ for archive in ("存档", "归档", "archive")
520
+ )
521
+ sent_plan_mail = bool(
522
+ re.search(
523
+ r"(?:已发送|发送|sent|sentto).{0,12}"
524
+ r"(?:实施计划|项目计划|实施方案|项目方案|部署计划|上线计划|"
525
+ r"implementationplan|projectplan|implementationschedule)"
526
+ r"(?:邮件|email|mail)",
527
+ compact,
528
+ re.IGNORECASE,
529
+ )
530
+ )
531
+ if direct_adjacent or archived_plan or sent_plan_mail:
532
+ return False
533
+ return any(marker in folded for marker in _PROJECT_PLAN_MARKERS)
534
+
535
+
536
+ def _split_future_use_clauses(value: str) -> list[str]:
537
+ # Semicolons, sentence boundaries, and explicit contrast/conjunction
538
+ # markers are the only boundaries used here. Commas are deliberately not
539
+ # boundaries because implementation plans commonly enumerate milestones.
540
+ return [
541
+ part.strip()
542
+ for part in re.split(
543
+ r"[;;。!?!?\n]+|(?:同时|另外|此外|其次|并且|并要求|还需|另需|in addition|separately)",
544
+ value,
545
+ flags=re.IGNORECASE,
546
+ )
547
+ if part.strip()
548
+ ]
549
+
550
+
551
+ def is_mixed_future_use_text(value: Any) -> bool:
552
+ """Detect a plan/rule combined with an independent dated todo.
553
+
554
+ This is a safety boundary, not a general-purpose classifier. It only
555
+ returns true when the durable plan/rule and a strong dated action occur in
556
+ separate clauses. A plan's own milestone or delivery date therefore
557
+ remains representable as one project memory.
558
+ """
559
+
560
+ if not isinstance(value, str):
561
+ return False
562
+ clauses = _split_future_use_clauses(value)
563
+ if len(clauses) < 2:
564
+ return False
565
+ durable_indexes = {
566
+ index
567
+ for index, clause in enumerate(clauses)
568
+ if is_project_plan_text(clause)
569
+ or any(marker in clause.casefold() for marker in _PROJECT_RULE_MARKERS)
570
+ }
571
+ if not durable_indexes:
572
+ return False
573
+ plan_milestone_context = any(
574
+ any(marker in clauses[index].casefold() for marker in ("里程碑", "milestone"))
575
+ for index in durable_indexes
576
+ )
577
+ plan_stage_context = sum(bool(_PLAN_STAGE_TEXT.search(clause)) for clause in clauses) >= 2
578
+ todo_indexes = {
579
+ index
580
+ for index, clause in enumerate(clauses)
581
+ if _CALENDAR_TEXT.search(clause)
582
+ and any(marker in clause.casefold() for marker in _DATED_TODO_MARKERS)
583
+ and not (
584
+ "完成" in clause
585
+ and any(marker in clause.casefold() for marker in _PLAN_MILESTONE_MARKERS)
586
+ and not any(
587
+ marker in clause.casefold() for marker in _INDEPENDENT_TODO_MARKERS
588
+ )
589
+ )
590
+ and not (
591
+ (plan_milestone_context or plan_stage_context)
592
+ and "完成" in clause
593
+ and not any(
594
+ marker in clause.casefold() for marker in _INDEPENDENT_TODO_MARKERS
595
+ )
596
+ )
597
+ # A second clause that is itself the project plan is a plan milestone,
598
+ # not evidence of a second future-use object.
599
+ and not is_project_plan_text(clause)
600
+ }
601
+ return bool(todo_indexes - durable_indexes)
602
+
603
+
380
604
  def is_aggregate_operational_text(value: Any) -> bool:
381
605
  """Recognize a combined operational digest, not one concrete work item.
382
606
 
@@ -910,6 +1134,23 @@ def validate_gate_output(
910
1134
  raise ModelOutputError("duplicate candidate cannot be worth remembering", validation_detail="invalid_flags")
911
1135
  if item["worth"] and candidate_type is None:
912
1136
  raise ModelOutputError("worth candidate must have a type", validation_detail="invalid_type")
1137
+ if item["worth"] and is_mixed_future_use_text(item["memory"]):
1138
+ raise ModelOutputError(
1139
+ "one candidate combines independent future uses",
1140
+ validation_detail="mixed_future_use",
1141
+ )
1142
+ # A clearly named implementation plan/plan adjustment is a project
1143
+ # memory, even when the model labels it as a generic fact. Keep todo
1144
+ # candidates intact: a task list that merely mentions a plan is still
1145
+ # an actionable todo, while fact/event/other are the common accidental
1146
+ # labels observed for durable plan changes.
1147
+ if (
1148
+ item["worth"]
1149
+ and candidate_type in {"fact", "event", "other"}
1150
+ and is_project_plan_text(item["memory"])
1151
+ ):
1152
+ item["type"] = "project"
1153
+ candidate_type = "project"
913
1154
  if "update_memory_id" in item and "duplicate_memory_id" in item:
914
1155
  raise ModelOutputError(
915
1156
  "a candidate cannot set both duplicate_memory_id and update_memory_id",
@@ -1117,10 +1358,32 @@ def validate_summarize_output(
1117
1358
  _string(item["title"], "title")
1118
1359
  _string(item["body"], "body", multiline=True)
1119
1360
  _reject_relative_calendar_expression(item)
1361
+ if is_mixed_future_use_text(f"{item['title']}\n{item['body']}"):
1362
+ raise ModelOutputError(
1363
+ "one summary combines independent future uses",
1364
+ validation_detail="mixed_future_use",
1365
+ )
1120
1366
  item["tags"] = _string_list(item["tags"], "tags")
1121
1367
  candidate_type = item["type"]
1122
1368
  if not isinstance(candidate_type, str) or candidate_type not in MEMORY_TYPES:
1123
1369
  raise ModelOutputError("invalid memory type", validation_detail="invalid_type")
1370
+ inferred_project_type = (
1371
+ candidate_type in {"fact", "event", "other"}
1372
+ and is_project_plan_text(f"{item['title']}\n{item['body']}")
1373
+ )
1374
+ if inferred_project_type:
1375
+ if expected_type is not None and expected_type != "project":
1376
+ raise ModelOutputError(
1377
+ "project plan summary cannot use a non-project gate type",
1378
+ validation_detail="invalid_type",
1379
+ )
1380
+ if expected_target_type is not None and expected_target_type != "project":
1381
+ raise ModelOutputError(
1382
+ "project plan summary cannot update a non-project target",
1383
+ validation_detail="invalid_type",
1384
+ )
1385
+ candidate_type = "project"
1386
+ item["type"] = candidate_type
1124
1387
  if expected_type is not None and candidate_type != expected_type:
1125
1388
  raise ModelOutputError(
1126
1389
  "summary type differs from gate candidate",
@@ -1375,6 +1638,8 @@ __all__ = [
1375
1638
  "parse_strict_json",
1376
1639
  "is_aggregate_operational_text",
1377
1640
  "is_attachment_followup_only_text",
1641
+ "is_project_plan_text",
1642
+ "is_mixed_future_use_text",
1378
1643
  "normalize_relative_calendar_text",
1379
1644
  "parse_summarize_output",
1380
1645
  "parse_summary_output",
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: memleaf
3
- Version: 0.2.13
3
+ Version: 0.2.14
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.13。**
27
- > 核心库、Vault、stdio MCP Server、初始化 CLI、模型路由、提炼流程、受控检索协议和宿主适配器已经实现。memleaf 0.2.13 通过 PyPI 分发。
26
+ > **当前版本:0.2.14。**
27
+ > 核心库、Vault、stdio MCP Server、初始化 CLI、模型路由、提炼流程、受控检索协议和宿主适配器已经实现。memleaf 0.2.14 通过 PyPI 分发。
28
28
  > **当前版本支持 Hermes 和 Codex。** Antigravity(反重力)不检测、不安装、不配置。
29
29
 
30
30
  ## 项目定位
@@ -67,6 +67,7 @@ tests/test_codex_native_cli.py
67
67
  tests/test_context_budget.py
68
68
  tests/test_credential_safety.py
69
69
  tests/test_cross_host_acceptance.py
70
+ tests/test_extraction_quality_regressions.py
70
71
  tests/test_hermes_provider.py
71
72
  tests/test_hermes_stdio_transport.py
72
73
  tests/test_host_events.py