memleaf 0.2.62__tar.gz → 0.2.64__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.62 → memleaf-0.2.64}/CHANGELOG.md +14 -0
- {memleaf-0.2.62/src/memleaf.egg-info → memleaf-0.2.64}/PKG-INFO +4 -3
- {memleaf-0.2.62 → memleaf-0.2.64}/README.en.md +3 -2
- {memleaf-0.2.62 → memleaf-0.2.64}/README.md +3 -2
- {memleaf-0.2.62 → memleaf-0.2.64}/docs/semantic-extraction-protocol.md +2 -4
- {memleaf-0.2.62 → memleaf-0.2.64}/pyproject.toml +1 -1
- {memleaf-0.2.62 → memleaf-0.2.64}/src/memleaf/__init__.py +1 -1
- {memleaf-0.2.62 → memleaf-0.2.64}/src/memleaf/hermes_provider/plugin.yaml +1 -1
- {memleaf-0.2.62 → memleaf-0.2.64}/src/memleaf/planning_context.py +24 -6
- {memleaf-0.2.62 → memleaf-0.2.64}/src/memleaf/process_common.py +2 -2
- memleaf-0.2.64/src/memleaf/semantic_maintenance.py +179 -0
- {memleaf-0.2.62 → memleaf-0.2.64}/src/memleaf/semantic_protocol.py +9 -21
- {memleaf-0.2.62 → memleaf-0.2.64}/src/memleaf/single_pass_memory_planner.py +89 -74
- {memleaf-0.2.62 → memleaf-0.2.64}/src/memleaf/single_pass_plan.py +56 -19
- {memleaf-0.2.62 → memleaf-0.2.64}/src/memleaf/turn_audit.py +2 -0
- {memleaf-0.2.62 → memleaf-0.2.64}/src/memleaf/validation.py +4 -1
- {memleaf-0.2.62 → memleaf-0.2.64/src/memleaf.egg-info}/PKG-INFO +4 -3
- memleaf-0.2.62/src/memleaf/semantic_maintenance.py +0 -140
- {memleaf-0.2.62 → memleaf-0.2.64}/LICENSE +0 -0
- {memleaf-0.2.62 → memleaf-0.2.64}/MANIFEST.in +0 -0
- {memleaf-0.2.62 → memleaf-0.2.64}/docs/capture-budget-design.md +0 -0
- {memleaf-0.2.62 → memleaf-0.2.64}/docs/config-migrations.md +0 -0
- {memleaf-0.2.62 → memleaf-0.2.64}/docs/core-refactor.md +0 -0
- {memleaf-0.2.62 → memleaf-0.2.64}/docs/evidence-retention.md +0 -0
- {memleaf-0.2.62 → memleaf-0.2.64}/docs/extraction-latency.md +0 -0
- {memleaf-0.2.62 → memleaf-0.2.64}/docs/gate-evidence-boundary.md +0 -0
- {memleaf-0.2.62 → memleaf-0.2.64}/docs/general-processing.md +0 -0
- {memleaf-0.2.62 → memleaf-0.2.64}/docs/hermes-mcp-runtime.md +0 -0
- {memleaf-0.2.62 → memleaf-0.2.64}/docs/processing-quality-acceptance.md +0 -0
- {memleaf-0.2.62 → memleaf-0.2.64}/docs/v0.2.26-processing-status.md +0 -0
- {memleaf-0.2.62 → memleaf-0.2.64}/examples/README.md +0 -0
- {memleaf-0.2.62 → memleaf-0.2.64}/examples/basic_usage.py +0 -0
- {memleaf-0.2.62 → memleaf-0.2.64}/examples/mcp_stdio.ndjson +0 -0
- {memleaf-0.2.62 → memleaf-0.2.64}/install.ps1 +0 -0
- {memleaf-0.2.62 → memleaf-0.2.64}/install.sh +0 -0
- {memleaf-0.2.62 → memleaf-0.2.64}/setup.cfg +0 -0
- {memleaf-0.2.62 → memleaf-0.2.64}/src/memleaf/__main__.py +0 -0
- {memleaf-0.2.62 → memleaf-0.2.64}/src/memleaf/adapters/__init__.py +0 -0
- {memleaf-0.2.62 → memleaf-0.2.64}/src/memleaf/adapters/antigravity.py +0 -0
- {memleaf-0.2.62 → memleaf-0.2.64}/src/memleaf/adapters/base.py +0 -0
- {memleaf-0.2.62 → memleaf-0.2.64}/src/memleaf/adapters/codex.py +0 -0
- {memleaf-0.2.62 → memleaf-0.2.64}/src/memleaf/adapters/hermes.py +0 -0
- {memleaf-0.2.62 → memleaf-0.2.64}/src/memleaf/admission.py +0 -0
- {memleaf-0.2.62 → memleaf-0.2.64}/src/memleaf/batch_review.py +0 -0
- {memleaf-0.2.62 → memleaf-0.2.64}/src/memleaf/budget.py +0 -0
- {memleaf-0.2.62 → memleaf-0.2.64}/src/memleaf/capture.py +0 -0
- {memleaf-0.2.62 → memleaf-0.2.64}/src/memleaf/cli.py +0 -0
- {memleaf-0.2.62 → memleaf-0.2.64}/src/memleaf/compaction.py +0 -0
- {memleaf-0.2.62 → memleaf-0.2.64}/src/memleaf/config.py +0 -0
- {memleaf-0.2.62 → memleaf-0.2.64}/src/memleaf/create_coordinator.py +0 -0
- {memleaf-0.2.62 → memleaf-0.2.64}/src/memleaf/credentials.py +0 -0
- {memleaf-0.2.62 → memleaf-0.2.64}/src/memleaf/evidence_budget.py +0 -0
- {memleaf-0.2.62 → memleaf-0.2.64}/src/memleaf/evidence_policy.py +0 -0
- {memleaf-0.2.62 → memleaf-0.2.64}/src/memleaf/evidence_structure.py +0 -0
- {memleaf-0.2.62 → memleaf-0.2.64}/src/memleaf/evidence_syntax.py +0 -0
- {memleaf-0.2.62 → memleaf-0.2.64}/src/memleaf/extraction_budget.py +0 -0
- {memleaf-0.2.62 → memleaf-0.2.64}/src/memleaf/extraction_capability.py +0 -0
- {memleaf-0.2.62 → memleaf-0.2.64}/src/memleaf/extraction_work_state.py +0 -0
- {memleaf-0.2.62 → memleaf-0.2.64}/src/memleaf/frontmatter.py +0 -0
- {memleaf-0.2.62 → memleaf-0.2.64}/src/memleaf/hermes_provider/README.md +0 -0
- {memleaf-0.2.62 → memleaf-0.2.64}/src/memleaf/hermes_provider/__init__.py +0 -0
- {memleaf-0.2.62 → memleaf-0.2.64}/src/memleaf/hermes_provider/_mcp_client.py +0 -0
- {memleaf-0.2.62 → memleaf-0.2.64}/src/memleaf/hermes_provider/_provider.py +0 -0
- {memleaf-0.2.62 → memleaf-0.2.64}/src/memleaf/hermes_provider/_shared.py +0 -0
- {memleaf-0.2.62 → memleaf-0.2.64}/src/memleaf/hermes_provider/evidence_budget.py +0 -0
- {memleaf-0.2.62 → memleaf-0.2.64}/src/memleaf/hermes_runtime.py +0 -0
- {memleaf-0.2.62 → memleaf-0.2.64}/src/memleaf/host_events.py +0 -0
- {memleaf-0.2.62 → memleaf-0.2.64}/src/memleaf/host_runtime.py +0 -0
- {memleaf-0.2.62 → memleaf-0.2.64}/src/memleaf/inbox.py +0 -0
- {memleaf-0.2.62 → memleaf-0.2.64}/src/memleaf/index.py +0 -0
- {memleaf-0.2.62 → memleaf-0.2.64}/src/memleaf/inspection.py +0 -0
- {memleaf-0.2.62 → memleaf-0.2.64}/src/memleaf/installer.py +0 -0
- {memleaf-0.2.62 → memleaf-0.2.64}/src/memleaf/llm/__init__.py +0 -0
- {memleaf-0.2.62 → memleaf-0.2.64}/src/memleaf/llm/base.py +0 -0
- {memleaf-0.2.62 → memleaf-0.2.64}/src/memleaf/llm/claude_compatible.py +0 -0
- {memleaf-0.2.62 → memleaf-0.2.64}/src/memleaf/llm/gemini.py +0 -0
- {memleaf-0.2.62 → memleaf-0.2.64}/src/memleaf/llm/openai_compatible.py +0 -0
- {memleaf-0.2.62 → memleaf-0.2.64}/src/memleaf/llm/router.py +0 -0
- {memleaf-0.2.62 → memleaf-0.2.64}/src/memleaf/llm/thinking.py +0 -0
- {memleaf-0.2.62 → memleaf-0.2.64}/src/memleaf/locking.py +0 -0
- {memleaf-0.2.62 → memleaf-0.2.64}/src/memleaf/mcp_server.py +0 -0
- {memleaf-0.2.62 → memleaf-0.2.64}/src/memleaf/memory_commit.py +0 -0
- {memleaf-0.2.62 → memleaf-0.2.64}/src/memleaf/memory_planner.py +0 -0
- {memleaf-0.2.62 → memleaf-0.2.64}/src/memleaf/memory_writer.py +0 -0
- {memleaf-0.2.62 → memleaf-0.2.64}/src/memleaf/model_capabilities.py +0 -0
- {memleaf-0.2.62 → memleaf-0.2.64}/src/memleaf/model_discovery.py +0 -0
- {memleaf-0.2.62 → memleaf-0.2.64}/src/memleaf/model_execution.py +0 -0
- {memleaf-0.2.62 → memleaf-0.2.64}/src/memleaf/models.py +0 -0
- {memleaf-0.2.62 → memleaf-0.2.64}/src/memleaf/native_index.py +0 -0
- {memleaf-0.2.62 → memleaf-0.2.64}/src/memleaf/native_registration.py +0 -0
- {memleaf-0.2.62 → memleaf-0.2.64}/src/memleaf/parallel_model.py +0 -0
- {memleaf-0.2.62 → memleaf-0.2.64}/src/memleaf/process_jobs.py +0 -0
- {memleaf-0.2.62 → memleaf-0.2.64}/src/memleaf/process_journal.py +0 -0
- {memleaf-0.2.62 → memleaf-0.2.64}/src/memleaf/process_owner.py +0 -0
- {memleaf-0.2.62 → memleaf-0.2.64}/src/memleaf/processing.py +0 -0
- {memleaf-0.2.62 → memleaf-0.2.64}/src/memleaf/prompts.py +0 -0
- {memleaf-0.2.62 → memleaf-0.2.64}/src/memleaf/provenance.py +0 -0
- {memleaf-0.2.62 → memleaf-0.2.64}/src/memleaf/recording_policy.py +0 -0
- {memleaf-0.2.62 → memleaf-0.2.64}/src/memleaf/redaction.py +0 -0
- {memleaf-0.2.62 → memleaf-0.2.64}/src/memleaf/retention.py +0 -0
- {memleaf-0.2.62 → memleaf-0.2.64}/src/memleaf/retrieval.py +0 -0
- {memleaf-0.2.62 → memleaf-0.2.64}/src/memleaf/retrieval_gate.py +0 -0
- {memleaf-0.2.62 → memleaf-0.2.64}/src/memleaf/scope_maintenance.py +0 -0
- {memleaf-0.2.62 → memleaf-0.2.64}/src/memleaf/scope_state.py +0 -0
- {memleaf-0.2.62 → memleaf-0.2.64}/src/memleaf/service.py +0 -0
- {memleaf-0.2.62 → memleaf-0.2.64}/src/memleaf/source_policy.py +0 -0
- {memleaf-0.2.62 → memleaf-0.2.64}/src/memleaf/state_layout.py +0 -0
- {memleaf-0.2.62 → memleaf-0.2.64}/src/memleaf/subprocess_flags.py +0 -0
- {memleaf-0.2.62 → memleaf-0.2.64}/src/memleaf/summary_batch.py +0 -0
- {memleaf-0.2.62 → memleaf-0.2.64}/src/memleaf/target_reconciliation.py +0 -0
- {memleaf-0.2.62 → memleaf-0.2.64}/src/memleaf/turn_plan.py +0 -0
- {memleaf-0.2.62 → memleaf-0.2.64}/src/memleaf/update_coordinator.py +0 -0
- {memleaf-0.2.62 → memleaf-0.2.64}/src/memleaf/update_review.py +0 -0
- {memleaf-0.2.62 → memleaf-0.2.64}/src/memleaf/vault.py +0 -0
- {memleaf-0.2.62 → memleaf-0.2.64}/src/memleaf.egg-info/SOURCES.txt +0 -0
- {memleaf-0.2.62 → memleaf-0.2.64}/src/memleaf.egg-info/dependency_links.txt +0 -0
- {memleaf-0.2.62 → memleaf-0.2.64}/src/memleaf.egg-info/entry_points.txt +0 -0
- {memleaf-0.2.62 → memleaf-0.2.64}/src/memleaf.egg-info/top_level.txt +0 -0
|
@@ -2,6 +2,20 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to memleaf are documented here.
|
|
4
4
|
|
|
5
|
+
## 0.2.64 — 2026-09-16
|
|
6
|
+
|
|
7
|
+
- Keep uncertain ownership as `unscoped`: explicit cross-project corrections now require cited user evidence and an exact target, while independent project subjects are deferred instead of guessed. Candidate-local deadline grounding remains conservative, preserving only dates tied to the todo action or its explicit task basis.
|
|
8
|
+
- Preserve target type and Core-owned ownership metadata during same-target reconciliation, and carry bounded `scope_drift` and schema diagnostics through maintenance and repair without rewriting valid sibling candidates.
|
|
9
|
+
|
|
10
|
+
Verification: Python 3.11 compilation, package metadata consistency, wheel contents and `git diff --check` were checked. Provider/runtime acceptance is completed by the release CI and post-install replay.
|
|
11
|
+
|
|
12
|
+
## 0.2.63 — 2026-09-16
|
|
13
|
+
|
|
14
|
+
- Unify the retention decision across first-pass extraction, topic selection, candidate repair and `semantic_maintenance`. The model now judges future reuse, information gain, direct usability and forgetting cost, keeping only the smallest core with clear material value; the contract contains no mail, report, customer or project-specific exclusion rule.
|
|
15
|
+
- Keep maintenance complementary to extraction: it applies the same value judgment to incoming candidates, continues maintaining candidates that qualify, discards those without clear value and defers only unresolved meaning. The `0.2.62` date/ownership degradation and update-target grouping remain intact.
|
|
16
|
+
|
|
17
|
+
Verification covers prompt consistency and compactness assertions, Python 3.11 compilation/imports, package metadata consistency, a package wheel containing the Hermes provider resource, and `git diff --check`. Real-provider replay remains runtime acceptance after installation.
|
|
18
|
+
|
|
5
19
|
## 0.2.62 — 2026-09-16
|
|
6
20
|
|
|
7
21
|
- Record conclusions, not process. The contract now opens with one precedence rule -- 只记结论: what the user chose, agreed to, or what happened; options, plans and drafts offered by the assistant are process even when they look like reusable knowledge. It replaces a genuine conflict between two earlier sentences: `reusable`/通用经验 invited storing the assistant's own outline, while the assistant-proposal clause forbade it, so the model decided by coin flip. The contradictory sentence about assistant suggestions never being a task basis is gone with it. Measured on the reproduced turn, the assistant's four-section outline was written in one of two runs before the change and in none of four runs after it.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: memleaf
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.64
|
|
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,9 @@ 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
|
-
>
|
|
26
|
+
> **版本:0.2.64。**
|
|
27
|
+
> 自动提炼和复核共用同一条“未来记忆价值”标准:模型综合未来复用、信息增量、再次读取时的直接可用性和忘记成本,只保留对未来理解、判断或行动有实质影响的最小核心;没有明确价值的信息不提炼,不按具体业务场景硬编码排除。
|
|
28
|
+
> 提炼契约现在只有一份中文语义契约(旧版英文 B3 提示词已删除),首轮、主题选择、候选修复和 `semantic_maintenance` 使用同一套保留价值判断;同一件事只写一次。
|
|
28
29
|
> Core 不再因为归属、日期、任务依据或跨项目而丢弃整条候选:归属缺失会归一到证据里唯一被点名的项目、否则落 `global`;日期只接受能在本候选证据里找到出处、且能锚定成 ISO 的写法(`明天`、`周五`、`月底` 都会被解析),无法解析时只丢掉日期字段,记忆照常写入;未获授权的改归属沿用目标原归属。
|
|
29
30
|
> 待办 `due_date` 只表示待办动作本身明确声明的截止日;属于待办主题或预期结果的日期不会被误作截止日。项目归属由模型按语义判断,不要求项目名在证据里逐字出现。复核阶段会保留首轮已选定的更新目标,避免“状态变更”被降级成新增记忆。Markdown 仍是唯一事实源。
|
|
30
31
|
> **当前版本支持 Hermes 和 Codex。** Antigravity(反重力)不检测、不安装、不配置。
|
|
@@ -4,8 +4,9 @@
|
|
|
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
|
-
> Automatic extraction
|
|
7
|
+
> **Version: 0.2.64.**
|
|
8
|
+
> Automatic extraction and review share one future-value standard: the model weighs likely reuse, information gain, direct usability when read again, and the cost of forgetting. It keeps only the smallest core that can materially help future understanding, decisions, or actions; information without clear value is not extracted, and no business-specific exclusion rule is hard-coded.
|
|
9
|
+
> A single Chinese semantic contract is sent for extraction, topic selection, candidate repair, and `semantic_maintenance`; the older English B3 prompt has been deleted and B3 remains only as a compatibility read format. One topic is written once.
|
|
9
10
|
> Core no longer discards a whole candidate over ownership, dates, task basis or cross-project wording. A missing owner normalizes to the single project named by that candidate's own evidence, otherwise `global`. A deadline is accepted only when it appears in the candidate's own evidence and can be anchored to ISO (`明天`, `周五`, `月底` all resolve); when it cannot, only the date field is dropped and the memory is still written. An unauthorized ownership change keeps the target's existing scope.
|
|
10
11
|
> A todo's `due_date` means only the explicit deadline of the todo action; dates describing its subject or desired outcome are not deadlines. Project ownership remains the model's semantic judgement and needs no literal name match. The review stage now keeps the update target chosen by the first pass, so a state change is no longer downgraded into a new memory. Markdown remains the sole source of truth.
|
|
11
12
|
> **The current release supports Hermes and Codex.** Antigravity is not detected, installed, or configured.
|
|
@@ -4,8 +4,9 @@
|
|
|
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
|
-
>
|
|
7
|
+
> **版本:0.2.64。**
|
|
8
|
+
> 自动提炼和复核共用同一条“未来记忆价值”标准:模型综合未来复用、信息增量、再次读取时的直接可用性和忘记成本,只保留对未来理解、判断或行动有实质影响的最小核心;没有明确价值的信息不提炼,不按具体业务场景硬编码排除。
|
|
9
|
+
> 提炼契约现在只有一份中文语义契约(旧版英文 B3 提示词已删除),首轮、主题选择、候选修复和 `semantic_maintenance` 使用同一套保留价值判断;同一件事只写一次。
|
|
9
10
|
> Core 不再因为归属、日期、任务依据或跨项目而丢弃整条候选:归属缺失会归一到证据里唯一被点名的项目、否则落 `global`;日期只接受能在本候选证据里找到出处、且能锚定成 ISO 的写法(`明天`、`周五`、`月底` 都会被解析),无法解析时只丢掉日期字段,记忆照常写入;未获授权的改归属沿用目标原归属。
|
|
10
11
|
> 待办 `due_date` 只表示待办动作本身明确声明的截止日;属于待办主题或预期结果的日期不会被误作截止日。项目归属由模型按语义判断,不要求项目名在证据里逐字出现。复核阶段会保留首轮已选定的更新目标,避免“状态变更”被降级成新增记忆。Markdown 仍是唯一事实源。
|
|
11
12
|
> **当前版本支持 Hermes 和 Codex。** Antigravity(反重力)不检测、不安装、不配置。
|
|
@@ -13,11 +13,9 @@
|
|
|
13
13
|
|
|
14
14
|
## 提炼判据
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
契约引导模型先判断信息是否有长期记忆价值,而不按具体业务场景列禁止项:综合未来复用、信息增量、再次读取时的直接可用性和忘记成本。只有对未来理解、判断或行动可能产生实质影响的信息才保留;没有明确价值就列入 `no_memory`,并只写产生该价值的最小核心。同一件事只写一次。
|
|
17
17
|
|
|
18
|
-
|
|
19
|
-
- **同一件事只写一次**:同一条内容不会同时落成事实和待办。
|
|
20
|
-
- `retention` 与类型独立:`reusable` 表示今后仍需使用的业务状态、约定或可复用结论;`session` 表示仅说明本轮如何查询、执行和恢复的过程,Core 会把它转成 `no_memory`。
|
|
18
|
+
首轮提炼、主题选择、候选修复和 `semantic_maintenance` 共用这条判断标准。`retention` 与类型独立:`reusable` 表示符合上述标准,`session` 表示不符合,Core 会把 `session` 转成 `no_memory`。维护阶段将符合标准的 incoming 继续维护,不符合的丢弃,语义无法确定的延后;它负责维护已有候选,不重新发明主题。
|
|
21
19
|
|
|
22
20
|
## 降级而不是丢弃
|
|
23
21
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""Local-first Markdown memory core for AI agents."""
|
|
2
2
|
|
|
3
|
-
__version__ = "0.2.
|
|
3
|
+
__version__ = "0.2.64"
|
|
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
|
|
@@ -772,8 +772,8 @@ class PlanningContext:
|
|
|
772
772
|
) -> dict[str, Any] | None:
|
|
773
773
|
"""Authorize one explicit cross-project correction without guessing.
|
|
774
774
|
|
|
775
|
-
The
|
|
776
|
-
|
|
775
|
+
The cited user evidence must name the new project under explicit
|
|
776
|
+
correction wording; an exact target establishes the previous owner. A model-provided target is checked
|
|
777
777
|
against that evidence; when it is omitted, Core may recover exactly one
|
|
778
778
|
same-type, same-topic active memory from the explicitly named old
|
|
779
779
|
scope. Zero or multiple matches stay deferred rather than becoming a
|
|
@@ -793,10 +793,18 @@ class PlanningContext:
|
|
|
793
793
|
event.content for event in turn.events
|
|
794
794
|
if event.role == "user" and isinstance(event.content, str)
|
|
795
795
|
).strip()
|
|
796
|
+
bindings = candidate.get("_evidence_bindings", [])
|
|
797
|
+
quotes = [binding.get("quote") for binding in bindings if isinstance(binding, Mapping)]
|
|
798
|
+
cited_user_text = [quote for quote in quotes if isinstance(quote, str) and quote and quote in user_text]
|
|
799
|
+
if cited_user_text:
|
|
800
|
+
user_text = " ".join(cited_user_text)
|
|
796
801
|
if not user_text or not _SCOPE_CORRECTION_MARKER_RE.search(user_text):
|
|
797
802
|
return None
|
|
798
803
|
config = config if "scopes" in config else {"scopes": config}
|
|
799
|
-
scopes = config.get("scopes", {}) if isinstance(config.get("scopes", {}), Mapping) else {}
|
|
804
|
+
scopes = dict(config.get("scopes", {})) if isinstance(config.get("scopes", {}), Mapping) else {}
|
|
805
|
+
# Explicit user corrections can introduce a previously unseen owner;
|
|
806
|
+
# local matching still requires that new name in the cited evidence.
|
|
807
|
+
scopes.setdefault(new_scope, {})
|
|
800
808
|
mentioned = [
|
|
801
809
|
scope for scope in scopes
|
|
802
810
|
if isinstance(scope, str)
|
|
@@ -804,9 +812,19 @@ class PlanningContext:
|
|
|
804
812
|
and self._scope_terms_present(user_text, scope, config)
|
|
805
813
|
]
|
|
806
814
|
mentioned = list(dict.fromkeys(mentioned))
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
815
|
+
selected = self._active_memory_by_id(candidate.get("update_memory_id"))
|
|
816
|
+
if selected is not None and len(selected.scopes) == 1:
|
|
817
|
+
# Other projects in the same user turn do not invalidate an exact
|
|
818
|
+
# correction of this selected target. The new owner must occur
|
|
819
|
+
# in the cited user correction; the old owner is the target's
|
|
820
|
+
# existing metadata, not an inference from another sentence.
|
|
821
|
+
old_scope = selected.scopes[0]
|
|
822
|
+
if new_scope not in mentioned or old_scope == new_scope:
|
|
823
|
+
return None
|
|
824
|
+
else:
|
|
825
|
+
if len(mentioned) != 2 or new_scope not in mentioned:
|
|
826
|
+
return None
|
|
827
|
+
old_scope = next(scope for scope in mentioned if scope != new_scope)
|
|
810
828
|
|
|
811
829
|
topic = str(candidate.get("memory") or "")
|
|
812
830
|
removable_terms: list[str] = []
|
|
@@ -74,7 +74,7 @@ _DEADLINE_PREFIX_RE = re.compile(
|
|
|
74
74
|
re.IGNORECASE,
|
|
75
75
|
)
|
|
76
76
|
_DEADLINE_SUFFIX_RE = re.compile(
|
|
77
|
-
r"^\s*(?:(?:[01]?\d|2[0-3]):[0-5]\d\s*)?(?:前|之前|为止|截止|截至|不晚于|不得晚于|before\b|deadline\b|due\b)",
|
|
77
|
+
r"^\s*(?:(?:[01]?\d|2[0-3]):[0-5]\d\s*|(?:上午|下午|晚上|下班|中午|凌晨|傍晚|晚间)(?:\d{1,2}(?:点|时))?\s*)?(?:前|之前|为止|截止|截至|不晚于|不得晚于|before\b|deadline\b|due\b)",
|
|
78
78
|
re.IGNORECASE,
|
|
79
79
|
)
|
|
80
80
|
_EXPLICIT_PROJECT_LABEL_RE = re.compile(
|
|
@@ -89,7 +89,7 @@ _EXPLICIT_PROJECT_LABEL_RE = re.compile(
|
|
|
89
89
|
|
|
90
90
|
|
|
91
91
|
_SCOPE_CORRECTION_MARKER_RE = re.compile(
|
|
92
|
-
r"(
|
|
92
|
+
r"(?:不是|并非|不属于|归错|归属错误|错误归属|应属于|应该属于|应该(?:是|说)|改归|改为|纠正为|"
|
|
93
93
|
r"wrong\s+(?:project|scope)|belongs?\s+to|correct\s+(?:project|scope))",
|
|
94
94
|
re.IGNORECASE,
|
|
95
95
|
)
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
"""Maintain scoped memory state from small, already evidence-bound deltas."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
import json
|
|
4
|
+
from typing import Any, Mapping
|
|
5
|
+
from .semantic_protocol import RETENTION_GUIDANCE, expand_fragments, _invalid
|
|
6
|
+
from .validation import ModelOutputError, parse_strict_json
|
|
7
|
+
|
|
8
|
+
MAINTENANCE_SYSTEM = RETENTION_GUIDANCE + "\n" + '''根据 incoming 引用的原始证据维护长期记忆;incoming 仅划定待复核的主题,不提供已确认的分类或归属。catalog 是可更新的已有记忆。先识别已有事项的状态变化,再判断新建价值。同一事项合并维护当前状态,重复不新建;完成或转交也应维护已有 todo,不能另建完成事实留下旧待办。
|
|
9
|
+
返回 JSON {"memories":[{"from":["d1"],"target":null,"title":"简短主题","body":"最小可复用核心","type":"fact","scope":"global","evidence":[片段ID]}],"discard":[],"deferred":[]}。
|
|
10
|
+
每条一个独立主体与用途,可拆分同一 incoming。scope 按证据独立确定为 project:主体名、global(通用原则)或 unscoped(归属未确定)。type 为 fact/preference/project/todo/event/identity/other。target 为同事项的 catalog 真实ID,无才为null;更新保留原type,明确归属纠正可以改变scope。todo 提供 status(active/completed/cancelled)和 due_date(明确行动期限的原文写法,无则null)。正文保留当前有效内容,去掉过时状态和无复用价值的细节。每个 incoming 用 from、discard 或 deferred 覆盖;from 表示该主题已完整复核,evidence 只绑定保留内容,其余细节不记忆。from 和 evidence 只能选输入中已有的编号。'''
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def maintenance_input(raw: str, fragments: list[dict[str, Any]], catalog: list[dict[str, Any]], model_data: Mapping[str, Any]):
|
|
14
|
+
# Validate exact references before they become trusted input to maintenance.
|
|
15
|
+
expand_fragments(raw, fragments)
|
|
16
|
+
original = parse_strict_json(raw)
|
|
17
|
+
incoming = {}
|
|
18
|
+
proposals = []
|
|
19
|
+
catalog_ids = {m['memory_id'].casefold() for m in catalog}
|
|
20
|
+
for index, row in enumerate(original['memories'], 1):
|
|
21
|
+
incoming[index] = dict(row)
|
|
22
|
+
proposal = {'id': f'd{index}', 'evidence': row['evidence']}
|
|
23
|
+
if isinstance(row.get('target'), str) and row['target'].casefold() in catalog_ids:
|
|
24
|
+
proposal['target'] = row['target']
|
|
25
|
+
proposals.append(proposal)
|
|
26
|
+
related = [m for m in catalog if len(m.get('scopes', [])) == 1]
|
|
27
|
+
# Show original evidence, not the draft's classifications: the reviewer
|
|
28
|
+
# must be able to correct ownership and value without inheriting them.
|
|
29
|
+
snippets = list(model_data['fragments'])
|
|
30
|
+
payload = {'incoming': proposals, 'catalog': related, 'fragments': snippets}
|
|
31
|
+
return payload, (original, incoming, {m['memory_id'].casefold():m for m in related})
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def expand_maintenance(raw: str, context, *, diagnostics=None) -> str:
|
|
35
|
+
value = parse_strict_json(raw)
|
|
36
|
+
original, incoming, catalog = context
|
|
37
|
+
# Older host adapters can still provide fully bound compact output.
|
|
38
|
+
if isinstance(value, dict) and set(value) == {'memories','no_memory','deferred'}:
|
|
39
|
+
return raw
|
|
40
|
+
if not isinstance(value, dict) or set(value) != {'memories','discard','deferred'}:
|
|
41
|
+
raise _invalid()
|
|
42
|
+
if any(not isinstance(v,list) for v in value.values()):
|
|
43
|
+
raise _invalid()
|
|
44
|
+
seen = set()
|
|
45
|
+
reviewed_evidence = set()
|
|
46
|
+
def resolve(refs):
|
|
47
|
+
if not isinstance(refs,list) or not refs:
|
|
48
|
+
raise _invalid('invalid_evidence')
|
|
49
|
+
normalized = []
|
|
50
|
+
for ref in refs:
|
|
51
|
+
r = ref[1:] if isinstance(ref, str) and ref.startswith('d') else ref
|
|
52
|
+
normalized.append(int(r) if isinstance(r, str) and len(r) <= 20 and r.isascii() and r.isdecimal() else r)
|
|
53
|
+
if any((type(r) is not int and not isinstance(r, str)) or r not in incoming for r in normalized):
|
|
54
|
+
raise _invalid('invalid_evidence')
|
|
55
|
+
seen.update(normalized)
|
|
56
|
+
return [incoming[r] for r in normalized]
|
|
57
|
+
result = {'memories':[], 'no_memory':list(original['no_memory']), 'deferred':list(original['deferred'])}
|
|
58
|
+
# Only a genuine split may shed a previously bound update identity. A
|
|
59
|
+
# missing/null field in a one-to-one maintenance result is not a CREATE.
|
|
60
|
+
from_uses = {}
|
|
61
|
+
for proposed in value['memories']:
|
|
62
|
+
if isinstance(proposed, dict) and isinstance(proposed.get('from'), list):
|
|
63
|
+
for ref in proposed['from']:
|
|
64
|
+
key = str(ref).removeprefix('d')
|
|
65
|
+
from_uses[key] = from_uses.get(key, 0) + 1
|
|
66
|
+
def compile_row(row):
|
|
67
|
+
if isinstance(row, dict):
|
|
68
|
+
row = {'target': None, **row}
|
|
69
|
+
if isinstance(row, dict) and 'scopes' in row:
|
|
70
|
+
row = dict(row)
|
|
71
|
+
scopes_value = row.pop('scopes')
|
|
72
|
+
if not isinstance(scopes_value, list) or len(scopes_value) != 1 or ('scope' in row and row['scope'] != scopes_value[0]):
|
|
73
|
+
raise _invalid('invalid_scope')
|
|
74
|
+
row['scope'] = scopes_value[0]
|
|
75
|
+
if not isinstance(row,dict) or 'target' not in row or not {'from','title','body'} <= set(row):
|
|
76
|
+
raise _invalid()
|
|
77
|
+
if row['from'] == []:
|
|
78
|
+
# A catalog echo without fresh evidence cannot authorize a write.
|
|
79
|
+
# Ignore it only when every supplied field is literally unchanged.
|
|
80
|
+
target = row['target']
|
|
81
|
+
record = catalog.get(target.casefold()) if isinstance(target, str) else None
|
|
82
|
+
fields = {k: v for k, v in row.items() if k not in {'from', 'target'} and v is not None}
|
|
83
|
+
if record is None or any(record.get(k) != v for k, v in fields.items()):
|
|
84
|
+
raise _invalid('invalid_evidence')
|
|
85
|
+
return None
|
|
86
|
+
if set(row) - {'from', 'target', 'title', 'body', 'scope', 'type', 'status', 'due_date', 'completed_at', 'evidence'}:
|
|
87
|
+
raise _invalid()
|
|
88
|
+
if any(not isinstance(row[k], str) or not row[k].strip() for k in ('title', 'body')):
|
|
89
|
+
raise _invalid()
|
|
90
|
+
sources = resolve(row['from'])
|
|
91
|
+
scopes = {s['scope'] for s in sources}
|
|
92
|
+
if len(scopes)!=1 and 'scope' not in row:
|
|
93
|
+
raise _invalid('scope_drift')
|
|
94
|
+
scope = row.get('scope', next(iter(scopes)))
|
|
95
|
+
if not isinstance(scope, str) or not scope:
|
|
96
|
+
raise _invalid('invalid_scope')
|
|
97
|
+
kinds = {s.get('type', 'fact') for s in sources}
|
|
98
|
+
kind = row.get('type', next(iter(kinds)))
|
|
99
|
+
target = row['target']
|
|
100
|
+
inherited = {source['target'] for source in sources if isinstance(source.get('target'), str) and source['target'].casefold() in catalog}
|
|
101
|
+
if target is None and len(inherited) > 1:
|
|
102
|
+
raise _invalid('duplicate_update_target')
|
|
103
|
+
if target is None and len(inherited) == 1 and all(from_uses.get(str(ref).removeprefix('d')) == 1 for ref in row['from']):
|
|
104
|
+
target = next(iter(inherited))
|
|
105
|
+
if target is not None:
|
|
106
|
+
record = catalog.get(target.casefold()) if isinstance(target,str) else None
|
|
107
|
+
explicit_target = any(str(source.get('target', '')).casefold() == str(target).casefold() for source in sources)
|
|
108
|
+
if record is None or (record['scopes'] != [scope] and not (explicit_target or row.get('scope') == scope)):
|
|
109
|
+
raise _invalid('scope_drift')
|
|
110
|
+
if row.get('type') == 'todo' and record['type'] != 'todo':
|
|
111
|
+
raise _invalid('invalid_type')
|
|
112
|
+
kind = record['type']
|
|
113
|
+
if kind == 'todo' and 'todo' not in kinds and row.get('status') not in {'active', 'completed', 'cancelled'}:
|
|
114
|
+
raise _invalid('todo_fields')
|
|
115
|
+
elif len(kinds) != 1 and 'type' not in row:
|
|
116
|
+
raise _invalid('invalid_type')
|
|
117
|
+
if not isinstance(kind, str) or kind not in {'fact', 'preference', 'project', 'todo', 'event', 'identity', 'other'}:
|
|
118
|
+
raise _invalid('invalid_type')
|
|
119
|
+
memory = {k:v for k,v in row.items() if k!='from'}
|
|
120
|
+
memory['target']=target
|
|
121
|
+
memory['scope']=scope
|
|
122
|
+
memory['type']=kind
|
|
123
|
+
if kind != 'todo':
|
|
124
|
+
for field in ('status', 'due_date', 'completed_at'):
|
|
125
|
+
memory.pop(field, None)
|
|
126
|
+
allowed = list(dict.fromkeys(int(r) for s in sources for r in s['evidence']))
|
|
127
|
+
chosen = row.get('evidence', allowed)
|
|
128
|
+
if not isinstance(chosen, list) or not chosen or any(type(r) is not int or r not in allowed for r in chosen):
|
|
129
|
+
raise _invalid('invalid_evidence')
|
|
130
|
+
memory['evidence']=list(dict.fromkeys(chosen))
|
|
131
|
+
bases = list(dict.fromkeys(int(r) for s in sources for r in (s.get('task_basis') or [])))
|
|
132
|
+
if bases and kind == 'todo':
|
|
133
|
+
memory['task_basis']=[ref for ref in bases if ref in chosen]
|
|
134
|
+
if not memory['task_basis']:
|
|
135
|
+
del memory['task_basis']
|
|
136
|
+
reviewed_evidence.update(allowed)
|
|
137
|
+
return memory
|
|
138
|
+
|
|
139
|
+
for index, row in enumerate(value['memories'], 1):
|
|
140
|
+
try:
|
|
141
|
+
memory = compile_row(row)
|
|
142
|
+
if memory is not None:
|
|
143
|
+
result['memories'].append(memory)
|
|
144
|
+
except ModelOutputError as error:
|
|
145
|
+
refs = row.get('from') if isinstance(row, dict) else None
|
|
146
|
+
try:
|
|
147
|
+
sources = resolve(refs)
|
|
148
|
+
except ModelOutputError:
|
|
149
|
+
# Unresolvable references cannot authorize any write. Missing
|
|
150
|
+
# input coverage is deferred below, independently of valid rows.
|
|
151
|
+
sources = []
|
|
152
|
+
evidence = list(dict.fromkeys(int(r) for source in sources for r in source['evidence']))
|
|
153
|
+
result['deferred'].extend(evidence)
|
|
154
|
+
if diagnostics is not None:
|
|
155
|
+
diagnostics.append({'row': index, 'detail': error.validation_detail, 'evidence': evidence})
|
|
156
|
+
for key,dest in (('discard','no_memory'),('deferred','deferred')):
|
|
157
|
+
if value[key]:
|
|
158
|
+
try:
|
|
159
|
+
refs = []
|
|
160
|
+
for entry in value[key]:
|
|
161
|
+
if isinstance(entry, dict) and set(entry) <= {'from', 'reason'} and isinstance(entry.get('from'), list):
|
|
162
|
+
refs.extend(entry['from'])
|
|
163
|
+
else:
|
|
164
|
+
refs.append(entry)
|
|
165
|
+
sources = resolve(refs)
|
|
166
|
+
except ModelOutputError:
|
|
167
|
+
sources = []
|
|
168
|
+
for row in sources:
|
|
169
|
+
result[dest].extend(row['evidence'])
|
|
170
|
+
for ref in set(incoming) - seen:
|
|
171
|
+
evidence = incoming[ref]['evidence']
|
|
172
|
+
result['deferred'].extend(evidence)
|
|
173
|
+
if diagnostics is not None:
|
|
174
|
+
diagnostics.append({'detail': 'invalid_evidence', 'evidence': evidence})
|
|
175
|
+
claimed = {ref for memory in result['memories'] for ref in memory['evidence']}
|
|
176
|
+
result['no_memory'].extend(sorted(reviewed_evidence - claimed - set(result['deferred'])))
|
|
177
|
+
for key in ('no_memory','deferred'):
|
|
178
|
+
result[key]=list(dict.fromkeys(int(r) for r in result[key]))
|
|
179
|
+
return json.dumps(result,ensure_ascii=False)
|
|
@@ -107,16 +107,15 @@ def source_fragments(b3_prompt: str) -> dict[str, Any]:
|
|
|
107
107
|
'scope_context': data['scope_background'], 'scope_registry': data['scope_registry']}
|
|
108
108
|
|
|
109
109
|
|
|
110
|
-
RETENTION_GUIDANCE = """retention
|
|
110
|
+
RETENTION_GUIDANCE = """长期记忆服务于未来协作,不是业务流水归档。保留持续事项的行动与状态、稳定偏好及可直接复用的知识;仅供本次查阅知悉的通知、过程记录不保留。事实成立或能想象用途并不足够,未来仍须重新查询来源才可信的信息通常不保留。只留下有明确未来用途的最小核心,无此价值放 no_memory;已有事项的状态变化仍须更新。retention=reusable 表示可复用,session 表示仅本轮有用。"""
|
|
111
111
|
|
|
112
112
|
|
|
113
|
-
FRAGMENT_SYSTEM = f'''
|
|
114
|
-
根据底层长期有效的业务含义提炼记忆,不继承原文的标题、紧急程度、列表分类或建议处理方式。
|
|
113
|
+
FRAGMENT_SYSTEM = f'''根据对话语义判断每段信息是否值得形成长期记忆,不继承原文的标题、紧急程度、列表分类或建议处理方式。
|
|
115
114
|
{RETENTION_GUIDANCE}
|
|
116
115
|
返回 JSON:{{"memories":[],"no_memory":[],"deferred":[]}}。
|
|
117
|
-
每条 memory:{{"retention":"reusable 或 session","title":"简短主题","body":"脱离本轮对话仍有价值的核心内容","scope":"project:主体名 或 global","evidence":[片段ID]}}。type 默认 fact,表示业务事实或状态;可选类型 preference、project、todo、event、identity、other,event 仅用于事件本身而非其携带的业务事实。一条一个独立主题与归属,同一件事只写一次。scope
|
|
116
|
+
每条 memory:{{"retention":"reusable 或 session","title":"简短主题","body":"脱离本轮对话仍有价值的核心内容","scope":"project:主体名 或 global","evidence":[片段ID]}}。type 默认 fact,表示业务事实或状态;可选类型 preference、project、todo、event、identity、other,event 仅用于事件本身而非其携带的业务事实。一条一个独立主题与归属,同一件事只写一次。scope 表示事实所属主体,按语义判断,不要求名称与证据逐字一致;项目、客户、系统、产品等主体均可作为归属,通用原则写 global,主体所属项目尚未确认写 unscoped,不根据名称相似或当前项目猜测。同一段的独立主题分别提炼。
|
|
118
117
|
新 todo 额外提供 task_basis:[用户角色片段ID],其内容须明确建立用户自己承担的未完成动作。todo 提供 status(active/completed/cancelled)及 due_date(该任务约定日期,无则 null),completed_at 可选。日期保留原文写法,由 Core 解析相对日期。
|
|
119
|
-
no_memory
|
|
118
|
+
no_memory 填不符合上述长期记忆价值标准的片段ID;deferred 填语义尚无法确定的片段ID。每个片段须被 memory 引用或列入其中一个数组。同片段允许支持多条 memory。若为 catalog 中同一事项的补充、状态变化或重复,target 填已有真实 ID 并给出合并后的当前内容;新事项才省略 target。无需输出写入决策、生成ID或复制原文。'''
|
|
120
119
|
|
|
121
120
|
|
|
122
121
|
def expand_fragments(raw: str, fragments: list[dict[str, Any]]) -> str:
|
|
@@ -197,15 +196,6 @@ def _independent_project_subjects(text: str, scope_registry: Mapping[str, Any] |
|
|
|
197
196
|
registry = scope_registry if isinstance(scope_registry, Mapping) else {}
|
|
198
197
|
labels: set[str] = set()
|
|
199
198
|
subjects: set[str] = set()
|
|
200
|
-
# Chinese project short forms have no word boundary. A unique leading
|
|
201
|
-
# name prefix can flag a separately headed clause without registering an
|
|
202
|
-
# invented alias. Ambiguous prefixes remain unresolved.
|
|
203
|
-
names = {scope: scope.partition(":")[2] for scope in registry if scope.startswith("project:")}
|
|
204
|
-
short_names = {}
|
|
205
|
-
for scope, name in names.items():
|
|
206
|
-
if re.fullmatch(r"[\u4e00-\u9fff]{4,}", name):
|
|
207
|
-
short_names[scope] = [name[:n] for n in range(2, len(name))
|
|
208
|
-
if sum(other.startswith(name[:n]) for other in names.values()) == 1]
|
|
209
199
|
for clause in re.split(r"[。!?!?;;,\n、]+", text):
|
|
210
200
|
clause = clause.strip(" -*•0123456789.()()")
|
|
211
201
|
clause_labels = _explicit_project_scope_labels([clause], registry)
|
|
@@ -214,24 +204,22 @@ def _independent_project_subjects(text: str, scope_registry: Mapping[str, Any] |
|
|
|
214
204
|
if len(clause_labels) == 1:
|
|
215
205
|
labels.update(clause_labels)
|
|
216
206
|
matches = project_scope_matches_text(clause, {"scopes": registry})
|
|
217
|
-
matches = set(matches) | {scope for scope, terms in short_names.items()
|
|
218
|
-
if any(clause.startswith(term) for term in terms)}
|
|
219
207
|
for scope in matches:
|
|
220
208
|
node = registry.get(scope, {})
|
|
221
|
-
terms = [scope.partition(":")[2]
|
|
209
|
+
terms = [scope.partition(":")[2]]
|
|
222
210
|
if isinstance(node, Mapping):
|
|
223
211
|
terms += [v for v in node.get("aliases", []) if isinstance(v, str)]
|
|
224
|
-
if any(clause.casefold().startswith(term.casefold()) for term in terms if term):
|
|
212
|
+
if any(clause.casefold().startswith(term.casefold()) or re.search(r"(?:→|->|=>)\s*" + re.escape(term), clause, re.I) for term in terms if term):
|
|
225
213
|
subjects.add(scope)
|
|
226
214
|
return labels | subjects
|
|
227
215
|
|
|
228
216
|
|
|
229
217
|
|
|
230
|
-
TOPIC_SYSTEM = RETENTION_GUIDANCE + "\n" + '''
|
|
218
|
+
TOPIC_SYSTEM = RETENTION_GUIDANCE + "\n" + '''根据对话语义识别值得保留的独立主题,不继承原文标题、紧急程度、列表分类或建议处理方式。
|
|
231
219
|
这一阶段只选择有后续价值的主题及其证据,不写记忆正文,不分类,不处理日期,不决定数据库操作。
|
|
232
220
|
返回 JSON {"topics":[{"retention":"reusable 或 session","scope":"project:项目名 或 global","evidence":[片段ID]}],"no_memory":[片段ID],"deferred":[片段ID]}。
|
|
233
|
-
每个独立主题单独列出,scope
|
|
234
|
-
no_memory
|
|
221
|
+
每个独立主题单独列出,scope 表示主题真正所属的主体,按语义判断,不要求名称与证据逐字一致;通用原则写 global,主体所属项目尚未确认写 unscoped,不根据名称相似或当前项目猜测。其他合法 scope:domain:名称、portfolio:名称、unscoped。
|
|
222
|
+
no_memory 表示不符合上述长期记忆价值标准;deferred 表示语义无法确定。覆盖所有片段,每个被一个或多个主题引用或列入一个数组。'''
|
|
235
223
|
|
|
236
224
|
|
|
237
225
|
def compile_topics(raw: str, fragments: list[dict[str, Any]], protocol_version: str, *, contexts=None):
|