rememberstack 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- rememberstack/__init__.py +9 -0
- rememberstack/adapters/__init__.py +42 -0
- rememberstack/adapters/codex_writer.py +221 -0
- rememberstack/adapters/markitdown_converter.py +42 -0
- rememberstack/adapters/openrouter.py +136 -0
- rememberstack/adapters/selfhost/__init__.py +54 -0
- rememberstack/adapters/selfhost/forget.py +66 -0
- rememberstack/adapters/selfhost/git.py +374 -0
- rememberstack/adapters/selfhost/lance.py +328 -0
- rememberstack/adapters/selfhost/minio.py +279 -0
- rememberstack/adapters/selfhost/mounts.py +249 -0
- rememberstack/adapters/selfhost/object_store.py +130 -0
- rememberstack/adapters/selfhost/projection.py +80 -0
- rememberstack/adapters/selfhost/queue.py +137 -0
- rememberstack/adapters/selfhost/telemetry.py +45 -0
- rememberstack/adapters/selfhost/watcher.py +70 -0
- rememberstack/adapters/testing/__init__.py +15 -0
- rememberstack/adapters/testing/cost_meter.py +13 -0
- rememberstack/adapters/testing/model_provider.py +83 -0
- rememberstack/adapters/testing/queue.py +43 -0
- rememberstack/adapters/testing/telemetry.py +22 -0
- rememberstack/client.py +19 -0
- rememberstack/core/__init__.py +127 -0
- rememberstack/core/blockizer.py +189 -0
- rememberstack/core/chunker.py +216 -0
- rememberstack/core/consumption_skill.py +275 -0
- rememberstack/core/conversion.py +76 -0
- rememberstack/core/core_manifest.py +598 -0
- rememberstack/core/extension_packs.py +124 -0
- rememberstack/core/forget.py +17 -0
- rememberstack/core/knowledge_authored.py +276 -0
- rememberstack/core/knowledge_compile.py +215 -0
- rememberstack/core/knowledge_fact_sheet.py +210 -0
- rememberstack/core/knowledge_hashing.py +68 -0
- rememberstack/core/knowledge_planner.py +64 -0
- rememberstack/core/knowledge_writer.py +175 -0
- rememberstack/core/ranking.py +200 -0
- rememberstack/core/recipe_linter.py +149 -0
- rememberstack/core/section_snap.py +209 -0
- rememberstack/core/storage_routing.py +27 -0
- rememberstack/eval/__init__.py +53 -0
- rememberstack/eval/consumption.py +141 -0
- rememberstack/eval/contradiction.py +184 -0
- rememberstack/eval/harness.py +136 -0
- rememberstack/eval/lifecycle.py +400 -0
- rememberstack/eval/operational_scale.py +49 -0
- rememberstack/eval/resolution.py +255 -0
- rememberstack/eval/retrieval_spikes.py +50 -0
- rememberstack/eval/skeleton.py +231 -0
- rememberstack/llm/__init__.py +1 -0
- rememberstack/model/__init__.py +589 -0
- rememberstack/model/adjudication.py +100 -0
- rememberstack/model/auth.py +27 -0
- rememberstack/model/blocks.py +30 -0
- rememberstack/model/chunks.py +190 -0
- rememberstack/model/claims.py +162 -0
- rememberstack/model/client.py +98 -0
- rememberstack/model/clustering.py +54 -0
- rememberstack/model/component_version.py +124 -0
- rememberstack/model/consumption.py +88 -0
- rememberstack/model/conversion.py +31 -0
- rememberstack/model/deployment.py +53 -0
- rememberstack/model/documents.py +168 -0
- rememberstack/model/envelope.py +513 -0
- rememberstack/model/evaluation.py +72 -0
- rememberstack/model/forget.py +143 -0
- rememberstack/model/git.py +13 -0
- rememberstack/model/knowledge.py +840 -0
- rememberstack/model/knowledge_authored.py +325 -0
- rememberstack/model/knowledge_planner.py +431 -0
- rememberstack/model/lifecycle.py +42 -0
- rememberstack/model/model_provider.py +78 -0
- rememberstack/model/mounts.py +24 -0
- rememberstack/model/object_store.py +21 -0
- rememberstack/model/operational_scale.py +59 -0
- rememberstack/model/operations.py +153 -0
- rememberstack/model/processing.py +228 -0
- rememberstack/model/queue.py +73 -0
- rememberstack/model/recipes.py +83 -0
- rememberstack/model/relations.py +79 -0
- rememberstack/model/resolution.py +83 -0
- rememberstack/model/retrieval_spikes.py +62 -0
- rememberstack/model/sections.py +120 -0
- rememberstack/model/telemetry.py +30 -0
- rememberstack/ports/__init__.py +29 -0
- rememberstack/ports/auth.py +16 -0
- rememberstack/ports/connector.py +23 -0
- rememberstack/ports/cost_meter.py +17 -0
- rememberstack/ports/forget.py +20 -0
- rememberstack/ports/git.py +20 -0
- rememberstack/ports/model_provider.py +28 -0
- rememberstack/ports/mounts.py +16 -0
- rememberstack/ports/object_store.py +27 -0
- rememberstack/ports/p1_index.py +92 -0
- rememberstack/ports/purge.py +93 -0
- rememberstack/ports/queue.py +23 -0
- rememberstack/ports/telemetry.py +21 -0
- rememberstack/profiles/__init__.py +22 -0
- rememberstack/profiles/selfhost.py +324 -0
- rememberstack/profiles/selfhost_forget.py +158 -0
- rememberstack/profiles/selfhost_operations.py +95 -0
- rememberstack/py.typed +1 -0
- rememberstack/spine/__init__.py +93 -0
- rememberstack/spine/admission.py +26 -0
- rememberstack/spine/backfill.py +168 -0
- rememberstack/spine/catalog_contract.py +742 -0
- rememberstack/spine/chunk_catalog.py +237 -0
- rememberstack/spine/claim_catalog.py +298 -0
- rememberstack/spine/clustering.py +740 -0
- rememberstack/spine/component_versions.py +208 -0
- rememberstack/spine/consumption.py +81 -0
- rememberstack/spine/deployment_bootstrap.py +445 -0
- rememberstack/spine/document_catalog.py +621 -0
- rememberstack/spine/entity_registry.py +205 -0
- rememberstack/spine/extension_packs.py +220 -0
- rememberstack/spine/fact_catalog.py +571 -0
- rememberstack/spine/forget.py +1753 -0
- rememberstack/spine/knowledge.py +5467 -0
- rememberstack/spine/lifecycle.py +1071 -0
- rememberstack/spine/migrations/__init__.py +1 -0
- rememberstack/spine/migrations/_helpers.py +153 -0
- rememberstack/spine/migrations/env.py +58 -0
- rememberstack/spine/migrations/script.py.mako +27 -0
- rememberstack/spine/migrations/versions/__init__.py +1 -0
- rememberstack/spine/migrations/versions/p0_02_0001_extensions_enums.py +189 -0
- rememberstack/spine/migrations/versions/p0_02_0002_infrastructure_registries.py +321 -0
- rememberstack/spine/migrations/versions/p0_02_0003_entities_evaluation_e0_e1.py +631 -0
- rememberstack/spine/migrations/versions/p0_02_0004_claims_facts_evidence.py +411 -0
- rememberstack/spine/migrations/versions/p0_02_0005_projection_knowledge_retrieval.py +391 -0
- rememberstack/spine/migrations/versions/p0_02_0006_partitions_views.py +158 -0
- rememberstack/spine/migrations/versions/p2_06_0007_invalidated_outcome.py +26 -0
- rememberstack/spine/migrations/versions/p3_01_0008_document_version_target.py +58 -0
- rememberstack/spine/migrations/versions/p3_05_0009_reconcile_stage.py +27 -0
- rememberstack/spine/migrations/versions/p3_07_0010_lifecycle_eval_suite.py +25 -0
- rememberstack/spine/migrations/versions/p4_01_0011_survivor_view_rewrite.py +57 -0
- rememberstack/spine/migrations/versions/p6_02_0012_knowledge_compile_recovery.py +58 -0
- rememberstack/spine/migrations/versions/p6_04_0013_knowledge_writer_ledger.py +46 -0
- rememberstack/spine/migrations/versions/p6_05_0014_knowledge_planner_runtime.py +217 -0
- rememberstack/spine/migrations/versions/p6_06_0015_authored_dispatch_runtime.py +38 -0
- rememberstack/spine/migrations/versions/p7_02_0016_operational_eval_suite.py +19 -0
- rememberstack/spine/migrations/versions/p7_05_0017_hard_forget.py +55 -0
- rememberstack/spine/observation_adjudication.py +778 -0
- rememberstack/spine/operations.py +298 -0
- rememberstack/spine/projection.py +662 -0
- rememberstack/spine/recipes.py +276 -0
- rememberstack/spine/resolver.py +763 -0
- rememberstack/spine/review.py +650 -0
- rememberstack/spine/settings.py +22 -0
- rememberstack/spine/supersession.py +510 -0
- rememberstack/spine/sync.py +128 -0
- rememberstack/spine/work_ledger.py +816 -0
- rememberstack/surfaces/__init__.py +110 -0
- rememberstack/surfaces/cli.py +447 -0
- rememberstack/surfaces/consumption_skill.py +87 -0
- rememberstack/surfaces/graph_queries.py +698 -0
- rememberstack/surfaces/http_api.py +377 -0
- rememberstack/surfaces/mcp.py +67 -0
- rememberstack/surfaces/query_engine.py +1591 -0
- rememberstack/surfaces/recipe_executor.py +185 -0
- rememberstack/surfaces/recipe_surface.py +219 -0
- rememberstack/surfaces/remote_mcp.py +133 -0
- rememberstack/surfaces/sdk.py +324 -0
- rememberstack/workers/__init__.py +155 -0
- rememberstack/workers/base.py +312 -0
- rememberstack/workers/e0.py +577 -0
- rememberstack/workers/e1.py +425 -0
- rememberstack/workers/e2.py +525 -0
- rememberstack/workers/e3.py +434 -0
- rememberstack/workers/forget.py +299 -0
- rememberstack/workers/knowledge_authored.py +146 -0
- rememberstack/workers/knowledge_driver.py +735 -0
- rememberstack/workers/knowledge_fact_sheet.py +123 -0
- rememberstack/workers/knowledge_planner.py +325 -0
- rememberstack/workers/knowledge_writer.py +393 -0
- rememberstack/workers/operations.py +42 -0
- rememberstack/workers/p1.py +234 -0
- rememberstack/workers/p2.py +513 -0
- rememberstack/workers/p2_analytics.py +276 -0
- rememberstack/workers/p3.py +673 -0
- rememberstack/workers/reconcile.py +485 -0
- rememberstack/workers/sync.py +168 -0
- rememberstack-0.1.0.dist-info/METADATA +213 -0
- rememberstack-0.1.0.dist-info/RECORD +186 -0
- rememberstack-0.1.0.dist-info/WHEEL +4 -0
- rememberstack-0.1.0.dist-info/entry_points.txt +2 -0
- rememberstack-0.1.0.dist-info/licenses/LICENSE +201 -0
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
"""Pure deterministic Plane-K fact-sheet rendering (D45, WP-6.3)."""
|
|
2
|
+
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from enum import StrEnum
|
|
5
|
+
from uuid import UUID
|
|
6
|
+
|
|
7
|
+
from pydantic import TypeAdapter
|
|
8
|
+
|
|
9
|
+
from rememberstack.model import KnowledgeFactSheetFact
|
|
10
|
+
from rememberstack.model import KnowledgeFactSheetSnapshot
|
|
11
|
+
from rememberstack.model import KnowledgeRenderedFactSheet
|
|
12
|
+
from rememberstack.model import UTCDateTime
|
|
13
|
+
|
|
14
|
+
_UTC_ADAPTER = TypeAdapter(UTCDateTime)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class KnowledgeFactLifecycle(StrEnum):
|
|
18
|
+
"""A fact's literal state at the fixed evidence snapshot timestamp."""
|
|
19
|
+
|
|
20
|
+
CURRENT = "current"
|
|
21
|
+
ENDED = "ended"
|
|
22
|
+
INVALIDATED = "invalidated"
|
|
23
|
+
NOT_YET_VALID = "not yet valid"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def render_knowledge_fact_sheet(
|
|
27
|
+
*,
|
|
28
|
+
snapshot: KnowledgeFactSheetSnapshot,
|
|
29
|
+
compiled_at: UTCDateTime,
|
|
30
|
+
citation_count: int,
|
|
31
|
+
candidate_count: int | None = None,
|
|
32
|
+
) -> KnowledgeRenderedFactSheet:
|
|
33
|
+
"""Render exact current relations, observation history, and open tensions."""
|
|
34
|
+
if citation_count < 0:
|
|
35
|
+
raise ValueError("citation_count must be non-negative")
|
|
36
|
+
if candidate_count is not None and candidate_count < 0:
|
|
37
|
+
raise ValueError("candidate_count must be non-negative")
|
|
38
|
+
compiled_at = _UTC_ADAPTER.validate_python(compiled_at)
|
|
39
|
+
current_relations = tuple(
|
|
40
|
+
sorted(
|
|
41
|
+
(
|
|
42
|
+
fact
|
|
43
|
+
for fact in snapshot.facts
|
|
44
|
+
if fact.kind == "relation"
|
|
45
|
+
and _lifecycle(fact=fact, evidence_as_of=snapshot.evidence_as_of)
|
|
46
|
+
is KnowledgeFactLifecycle.CURRENT
|
|
47
|
+
),
|
|
48
|
+
key=lambda fact: (
|
|
49
|
+
-fact.evidence_count,
|
|
50
|
+
fact.label.casefold(),
|
|
51
|
+
str(fact.fact_id),
|
|
52
|
+
),
|
|
53
|
+
)
|
|
54
|
+
)
|
|
55
|
+
observations = tuple(
|
|
56
|
+
sorted(
|
|
57
|
+
(fact for fact in snapshot.facts if fact.kind == "observation"),
|
|
58
|
+
key=lambda fact: (
|
|
59
|
+
_time_key(value=fact.valid_from),
|
|
60
|
+
_time_key(value=fact.ingested_at),
|
|
61
|
+
str(fact.fact_id),
|
|
62
|
+
),
|
|
63
|
+
)
|
|
64
|
+
)
|
|
65
|
+
contradiction_groups = _open_contradiction_groups(facts=snapshot.facts)
|
|
66
|
+
|
|
67
|
+
lines = [
|
|
68
|
+
"## Fact sheet (generated)",
|
|
69
|
+
"",
|
|
70
|
+
"### Current relations",
|
|
71
|
+
"",
|
|
72
|
+
"| fact | valid since | evidence | reference |",
|
|
73
|
+
"|---|---|---:|---|",
|
|
74
|
+
]
|
|
75
|
+
if current_relations:
|
|
76
|
+
lines.extend(
|
|
77
|
+
f"| {_cell(fact.label)} | {_timestamp(fact.valid_from)} | "
|
|
78
|
+
f"{fact.evidence_count} docs | `relation:{fact.fact_id}` |"
|
|
79
|
+
for fact in current_relations
|
|
80
|
+
)
|
|
81
|
+
else:
|
|
82
|
+
lines.append("| _None._ | — | 0 docs | — |")
|
|
83
|
+
|
|
84
|
+
lines.extend(
|
|
85
|
+
(
|
|
86
|
+
"",
|
|
87
|
+
"### Observation history",
|
|
88
|
+
"",
|
|
89
|
+
"| observation | valid from | valid until | state | evidence | reference |",
|
|
90
|
+
"|---|---|---|---|---:|---|",
|
|
91
|
+
)
|
|
92
|
+
)
|
|
93
|
+
if observations:
|
|
94
|
+
lines.extend(
|
|
95
|
+
f"| {_cell(fact.label)} | {_timestamp(fact.valid_from)} | "
|
|
96
|
+
f"{_timestamp(fact.valid_until)} | "
|
|
97
|
+
f"{_lifecycle(fact=fact, evidence_as_of=snapshot.evidence_as_of).value} | "
|
|
98
|
+
f"{fact.evidence_count} docs | `observation:{fact.fact_id}` |"
|
|
99
|
+
for fact in observations
|
|
100
|
+
)
|
|
101
|
+
else:
|
|
102
|
+
lines.append("| _None._ | — | — | — | 0 docs | — |")
|
|
103
|
+
|
|
104
|
+
lines.extend(("", "### Open contradictions", ""))
|
|
105
|
+
if contradiction_groups:
|
|
106
|
+
lines.extend(
|
|
107
|
+
(
|
|
108
|
+
"| group | fact | state | evidence | reference |",
|
|
109
|
+
"|---|---|---|---:|---|",
|
|
110
|
+
)
|
|
111
|
+
)
|
|
112
|
+
for group_id, members in contradiction_groups:
|
|
113
|
+
lines.extend(
|
|
114
|
+
f"| `{group_id}` | {_cell(fact.label)} | "
|
|
115
|
+
f"{_lifecycle(fact=fact, evidence_as_of=snapshot.evidence_as_of).value} | "
|
|
116
|
+
f"{fact.evidence_count} docs | `{fact.kind}:{fact.fact_id}` |"
|
|
117
|
+
for fact in members
|
|
118
|
+
)
|
|
119
|
+
else:
|
|
120
|
+
lines.append("_None._")
|
|
121
|
+
|
|
122
|
+
if candidate_count is None:
|
|
123
|
+
candidate_count = len(snapshot.input_snapshot.facts) + len(
|
|
124
|
+
snapshot.input_snapshot.claims
|
|
125
|
+
)
|
|
126
|
+
lines.extend(
|
|
127
|
+
(
|
|
128
|
+
"",
|
|
129
|
+
"---",
|
|
130
|
+
f"_compiled {_timestamp(compiled_at)} · "
|
|
131
|
+
f"evidence as of {_timestamp(snapshot.evidence_as_of)} · "
|
|
132
|
+
f"{candidate_count} candidates · {citation_count} citations_",
|
|
133
|
+
"",
|
|
134
|
+
)
|
|
135
|
+
)
|
|
136
|
+
return KnowledgeRenderedFactSheet(
|
|
137
|
+
markdown="\n".join(lines),
|
|
138
|
+
current_relation_count=len(current_relations),
|
|
139
|
+
observation_count=len(observations),
|
|
140
|
+
contradiction_group_count=len(contradiction_groups),
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def compose_knowledge_page(
|
|
145
|
+
*, prose_markdown: str | None, fact_sheet_markdown: str
|
|
146
|
+
) -> str:
|
|
147
|
+
"""Compose a two-band page, or the fact-sheet band alone with zero prose."""
|
|
148
|
+
fact_sheet = fact_sheet_markdown.strip()
|
|
149
|
+
if not fact_sheet:
|
|
150
|
+
raise ValueError("fact_sheet_markdown must be non-empty")
|
|
151
|
+
if prose_markdown is None or not prose_markdown.strip():
|
|
152
|
+
return f"{fact_sheet}\n"
|
|
153
|
+
return f"{prose_markdown.rstrip()}\n\n---\n{fact_sheet}\n"
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def _lifecycle(
|
|
157
|
+
*, fact: KnowledgeFactSheetFact, evidence_as_of: datetime
|
|
158
|
+
) -> KnowledgeFactLifecycle:
|
|
159
|
+
"""Classify one fact without confusing validity end with invalidation."""
|
|
160
|
+
if fact.invalidated_at is not None:
|
|
161
|
+
return KnowledgeFactLifecycle.INVALIDATED
|
|
162
|
+
if fact.valid_from is not None and fact.valid_from > evidence_as_of:
|
|
163
|
+
return KnowledgeFactLifecycle.NOT_YET_VALID
|
|
164
|
+
if fact.valid_until is not None and fact.valid_until <= evidence_as_of:
|
|
165
|
+
return KnowledgeFactLifecycle.ENDED
|
|
166
|
+
return KnowledgeFactLifecycle.CURRENT
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def _open_contradiction_groups(
|
|
170
|
+
*, facts: tuple[KnowledgeFactSheetFact, ...]
|
|
171
|
+
) -> tuple[tuple[UUID, tuple[KnowledgeFactSheetFact, ...]], ...]:
|
|
172
|
+
"""Group every non-invalidated selected side of an unresolved contradiction."""
|
|
173
|
+
grouped: dict[UUID, list[KnowledgeFactSheetFact]] = {}
|
|
174
|
+
for fact in facts:
|
|
175
|
+
if fact.contradiction_group is None or fact.invalidated_at is not None:
|
|
176
|
+
continue
|
|
177
|
+
grouped.setdefault(fact.contradiction_group, []).append(fact)
|
|
178
|
+
return tuple(
|
|
179
|
+
(
|
|
180
|
+
group_id,
|
|
181
|
+
tuple(
|
|
182
|
+
sorted(
|
|
183
|
+
members,
|
|
184
|
+
key=lambda fact: (
|
|
185
|
+
fact.kind,
|
|
186
|
+
fact.label.casefold(),
|
|
187
|
+
str(fact.fact_id),
|
|
188
|
+
),
|
|
189
|
+
)
|
|
190
|
+
),
|
|
191
|
+
)
|
|
192
|
+
for group_id, members in sorted(grouped.items(), key=lambda item: str(item[0]))
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def _timestamp(value: datetime | None) -> str:
|
|
197
|
+
"""Render full UTC precision without locale or machine-timezone dependence."""
|
|
198
|
+
if value is None:
|
|
199
|
+
return "—"
|
|
200
|
+
return value.isoformat().replace("+00:00", "Z")
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def _time_key(*, value: datetime | None) -> tuple[int, str]:
|
|
204
|
+
"""Sort unknown starts first, then aware timestamps in chronological order."""
|
|
205
|
+
return (0, "") if value is None else (1, value.isoformat())
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def _cell(value: str) -> str:
|
|
209
|
+
"""Keep arbitrary fact labels inside one Markdown table cell."""
|
|
210
|
+
return " ".join(value.split()).replace("\\", "\\\\").replace("|", "\\|")
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"""Canonical Plane-K input hashing (D45/D54)."""
|
|
2
|
+
|
|
3
|
+
import hashlib
|
|
4
|
+
import json
|
|
5
|
+
|
|
6
|
+
from pydantic import JsonValue
|
|
7
|
+
from pydantic import TypeAdapter
|
|
8
|
+
|
|
9
|
+
from rememberstack.model import KnowledgeInputSnapshot
|
|
10
|
+
|
|
11
|
+
_JSON_ADAPTER = TypeAdapter(JsonValue)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def knowledge_inputs_hash(*, snapshot: KnowledgeInputSnapshot) -> str:
|
|
15
|
+
"""Return the order-independent D45 staleness key for one page.
|
|
16
|
+
|
|
17
|
+
Relations and observations retain their state fingerprints. Claim-grain
|
|
18
|
+
candidates deliberately contain only ``(lineage, chunk_content_hash)``;
|
|
19
|
+
raw claim IDs and text are absent so a semantically unchanged extraction
|
|
20
|
+
generation cannot create a stale storm (D54).
|
|
21
|
+
"""
|
|
22
|
+
payload = _JSON_ADAPTER.validate_python(
|
|
23
|
+
{
|
|
24
|
+
"facts": _sorted_unique(
|
|
25
|
+
values=[fact.model_dump(mode="json") for fact in snapshot.facts]
|
|
26
|
+
),
|
|
27
|
+
"claims": _sorted_unique(
|
|
28
|
+
values=[claim.model_dump(mode="json") for claim in snapshot.claims]
|
|
29
|
+
),
|
|
30
|
+
"rules": _sorted_unique(
|
|
31
|
+
values=[
|
|
32
|
+
{"kind": rule.kind.value, "params": rule.params}
|
|
33
|
+
for rule in snapshot.rules
|
|
34
|
+
]
|
|
35
|
+
),
|
|
36
|
+
"curation_hash": snapshot.curation_hash,
|
|
37
|
+
# Multiplicity is meaningful here: two children with identical
|
|
38
|
+
# summaries are still two DAG inputs, while candidate rules are a set.
|
|
39
|
+
"child_summary_hashes": sorted(snapshot.child_summary_hashes),
|
|
40
|
+
"shared_model_summary_hash": snapshot.shared_model_summary_hash,
|
|
41
|
+
"writer_version": snapshot.writer_version,
|
|
42
|
+
}
|
|
43
|
+
)
|
|
44
|
+
canonical = json.dumps(
|
|
45
|
+
payload, ensure_ascii=False, separators=(",", ":"), sort_keys=True
|
|
46
|
+
)
|
|
47
|
+
return hashlib.sha256(canonical.encode("utf-8")).hexdigest()
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def knowledge_summary_hash(*, summary: str) -> str:
|
|
51
|
+
"""Hash a child/model page summary for inclusion in a parent's manifest."""
|
|
52
|
+
return hashlib.sha256(summary.encode("utf-8")).hexdigest()
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def knowledge_content_hash(*, markdown: str) -> str:
|
|
56
|
+
"""Hash the exact UTF-8 bytes of a compiled Markdown page."""
|
|
57
|
+
return hashlib.sha256(markdown.encode("utf-8")).hexdigest()
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _sorted_unique(*, values: list[JsonValue]) -> list[JsonValue]:
|
|
61
|
+
"""Sort JSON values canonically and remove set-union duplicates."""
|
|
62
|
+
by_json = {
|
|
63
|
+
json.dumps(
|
|
64
|
+
value, ensure_ascii=False, separators=(",", ":"), sort_keys=True
|
|
65
|
+
): value
|
|
66
|
+
for value in values
|
|
67
|
+
}
|
|
68
|
+
return [by_json[key] for key in sorted(by_json)]
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""Pure Plane-K planning hashes, trigger selection, and D24-style banding."""
|
|
2
|
+
|
|
3
|
+
from decimal import Decimal
|
|
4
|
+
import hashlib
|
|
5
|
+
import json
|
|
6
|
+
|
|
7
|
+
from rememberstack.model import KnowledgeConvertKindProposal
|
|
8
|
+
from rememberstack.model import KnowledgePageKind
|
|
9
|
+
from rememberstack.model import KnowledgePlanBand
|
|
10
|
+
from rememberstack.model import KnowledgePlanningSnapshot
|
|
11
|
+
from rememberstack.model import KnowledgePlanProposal
|
|
12
|
+
from rememberstack.model import KnowledgePlanRunKind
|
|
13
|
+
from rememberstack.model import KnowledgePlanTrigger
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def knowledge_planning_input_hash(*, snapshot: KnowledgePlanningSnapshot) -> str:
|
|
17
|
+
"""Hash one canonical planning snapshot for replay and cost attribution."""
|
|
18
|
+
payload = json.dumps(
|
|
19
|
+
snapshot.model_dump(mode="json"), sort_keys=True, separators=(",", ":")
|
|
20
|
+
).encode()
|
|
21
|
+
return hashlib.sha256(payload).hexdigest()
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def primary_knowledge_plan_trigger(
|
|
25
|
+
*, snapshot: KnowledgePlanningSnapshot, run_kind: KnowledgePlanRunKind
|
|
26
|
+
) -> KnowledgePlanTrigger:
|
|
27
|
+
"""Choose one stable ledger trigger while retaining every input in the snapshot."""
|
|
28
|
+
if run_kind is KnowledgePlanRunKind.REFLECTION:
|
|
29
|
+
return KnowledgePlanTrigger.REFLECTION
|
|
30
|
+
if snapshot.orphan_aggregates:
|
|
31
|
+
return KnowledgePlanTrigger.ORPHAN_EVIDENCE
|
|
32
|
+
if snapshot.overflow_artifact_ids:
|
|
33
|
+
return KnowledgePlanTrigger.SIZE_OVERFLOW
|
|
34
|
+
if snapshot.community_ids:
|
|
35
|
+
return KnowledgePlanTrigger.COMMUNITY_CHANGE
|
|
36
|
+
if snapshot.writer_suggestions:
|
|
37
|
+
return KnowledgePlanTrigger.WRITER_SUGGESTION
|
|
38
|
+
return KnowledgePlanTrigger.HUMAN
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def route_knowledge_plan(
|
|
42
|
+
*,
|
|
43
|
+
proposal: KnowledgePlanProposal,
|
|
44
|
+
run_kind: KnowledgePlanRunKind,
|
|
45
|
+
blast_radius: int,
|
|
46
|
+
auto_apply_max_expected_impact: Decimal,
|
|
47
|
+
) -> tuple[KnowledgePlanBand, Decimal]:
|
|
48
|
+
"""Route a proposal mechanically from impact, with the two binding exceptions."""
|
|
49
|
+
if blast_radius < 0:
|
|
50
|
+
raise ValueError("blast_radius must be non-negative")
|
|
51
|
+
if auto_apply_max_expected_impact < 0:
|
|
52
|
+
raise ValueError("auto-apply threshold must be non-negative")
|
|
53
|
+
expected_impact = Decimal(blast_radius) * (Decimal("1") - proposal.confidence)
|
|
54
|
+
requires_author_confirmation = (
|
|
55
|
+
isinstance(proposal, KnowledgeConvertKindProposal)
|
|
56
|
+
and proposal.to_kind is KnowledgePageKind.COMPILED
|
|
57
|
+
)
|
|
58
|
+
if (
|
|
59
|
+
run_kind is KnowledgePlanRunKind.REFLECTION
|
|
60
|
+
or requires_author_confirmation
|
|
61
|
+
or expected_impact > auto_apply_max_expected_impact
|
|
62
|
+
):
|
|
63
|
+
return KnowledgePlanBand.REVIEW, expected_impact
|
|
64
|
+
return KnowledgePlanBand.AUTO_APPLY, expected_impact
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
"""Deterministic bundle capping, rendering, and completeness accounting."""
|
|
2
|
+
|
|
3
|
+
from collections.abc import Collection
|
|
4
|
+
import json
|
|
5
|
+
from uuid import UUID
|
|
6
|
+
|
|
7
|
+
from rememberstack.model import KnowledgeCitation
|
|
8
|
+
from rememberstack.model import KnowledgeEvidenceRole
|
|
9
|
+
from rememberstack.model import KnowledgeEvidenceTarget
|
|
10
|
+
from rememberstack.model import KnowledgeWriterBundle
|
|
11
|
+
from rememberstack.model import KnowledgeWriterClaimGroup
|
|
12
|
+
from rememberstack.model import KnowledgeWriterCoverage
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def cap_knowledge_writer_bundle(
|
|
16
|
+
*,
|
|
17
|
+
bundle: KnowledgeWriterBundle,
|
|
18
|
+
exclusions: Collection[KnowledgeEvidenceTarget],
|
|
19
|
+
residue_claim_limit: int,
|
|
20
|
+
evidence_claims_per_fact: int,
|
|
21
|
+
) -> KnowledgeWriterBundle:
|
|
22
|
+
"""Filter curation exclusions and cap D54 claim coordinates deterministically."""
|
|
23
|
+
if residue_claim_limit < 0 or evidence_claims_per_fact < 0:
|
|
24
|
+
raise ValueError("writer claim limits must be non-negative")
|
|
25
|
+
excluded_relations = {
|
|
26
|
+
item.relation_id for item in exclusions if item.relation_id is not None
|
|
27
|
+
}
|
|
28
|
+
excluded_claim_coordinates = {
|
|
29
|
+
(item.claim_lineage_id, item.claim_chunk_content_hash)
|
|
30
|
+
for item in exclusions
|
|
31
|
+
if item.claim_lineage_id is not None
|
|
32
|
+
}
|
|
33
|
+
excluded_docs = {item.doc_id for item in exclusions if item.doc_id is not None}
|
|
34
|
+
facts = tuple(
|
|
35
|
+
fact
|
|
36
|
+
for fact in bundle.fact_sheet.facts
|
|
37
|
+
if fact.kind != "relation" or fact.fact_id not in excluded_relations
|
|
38
|
+
)
|
|
39
|
+
fact_keys = {(fact.kind, fact.fact_id) for fact in facts}
|
|
40
|
+
groups: list[KnowledgeWriterClaimGroup] = []
|
|
41
|
+
for group in bundle.claim_groups:
|
|
42
|
+
if (
|
|
43
|
+
group.lineage_id in excluded_docs
|
|
44
|
+
or _claim_group_key(group) in excluded_claim_coordinates
|
|
45
|
+
):
|
|
46
|
+
continue
|
|
47
|
+
claims = []
|
|
48
|
+
for claim in group.claims:
|
|
49
|
+
references = tuple(
|
|
50
|
+
reference
|
|
51
|
+
for reference in claim.fact_references
|
|
52
|
+
if (reference.kind, reference.fact_id) in fact_keys
|
|
53
|
+
)
|
|
54
|
+
if claim.fact_references and not references:
|
|
55
|
+
# Evidence for an excluded fact must not silently re-enter as
|
|
56
|
+
# uncategorized residue after its fact reference is stripped.
|
|
57
|
+
continue
|
|
58
|
+
claims.append(claim.model_copy(update={"fact_references": references}))
|
|
59
|
+
if claims:
|
|
60
|
+
groups.append(group.model_copy(update={"claims": tuple(claims)}))
|
|
61
|
+
|
|
62
|
+
ordered = tuple(sorted(groups, key=_claim_group_key))
|
|
63
|
+
by_fact: dict[tuple[str, UUID], list[KnowledgeWriterClaimGroup]] = {}
|
|
64
|
+
residue: list[KnowledgeWriterClaimGroup] = []
|
|
65
|
+
for group in ordered:
|
|
66
|
+
references = {
|
|
67
|
+
(reference.kind, reference.fact_id)
|
|
68
|
+
for claim in group.claims
|
|
69
|
+
for reference in claim.fact_references
|
|
70
|
+
}
|
|
71
|
+
if not references:
|
|
72
|
+
residue.append(group)
|
|
73
|
+
for reference in references:
|
|
74
|
+
by_fact.setdefault(reference, []).append(group)
|
|
75
|
+
|
|
76
|
+
selected: dict[tuple[UUID, str], KnowledgeWriterClaimGroup] = {
|
|
77
|
+
_claim_group_key(group): group for group in residue[:residue_claim_limit]
|
|
78
|
+
}
|
|
79
|
+
for fact in sorted(
|
|
80
|
+
facts, key=lambda item: (-item.evidence_count, item.kind, str(item.fact_id))
|
|
81
|
+
):
|
|
82
|
+
for group in by_fact.get((fact.kind, fact.fact_id), ())[
|
|
83
|
+
:evidence_claims_per_fact
|
|
84
|
+
]:
|
|
85
|
+
selected[_claim_group_key(group)] = group
|
|
86
|
+
offered = tuple(selected[key] for key in sorted(selected, key=_sortable_group_key))
|
|
87
|
+
return KnowledgeWriterBundle(
|
|
88
|
+
fact_sheet=bundle.fact_sheet.model_copy(update={"facts": facts}),
|
|
89
|
+
claim_groups=offered,
|
|
90
|
+
claim_candidate_count=len(ordered),
|
|
91
|
+
claims_cut_count=len(ordered) - len(offered),
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def render_knowledge_writer_bundle(*, bundle: KnowledgeWriterBundle) -> str:
|
|
96
|
+
"""Render only offered evidence into stable JSON with citation IDs inline."""
|
|
97
|
+
payload = {
|
|
98
|
+
"artifact_id": str(bundle.fact_sheet.artifact_id),
|
|
99
|
+
"deployment_id": str(bundle.fact_sheet.deployment_id),
|
|
100
|
+
"evidence_as_of": bundle.fact_sheet.evidence_as_of.isoformat(),
|
|
101
|
+
"facts": [item.model_dump(mode="json") for item in bundle.fact_sheet.facts],
|
|
102
|
+
"claim_groups": [item.model_dump(mode="json") for item in bundle.claim_groups],
|
|
103
|
+
"ledger": {
|
|
104
|
+
"fact_candidates_offered": len(bundle.fact_sheet.facts),
|
|
105
|
+
"claim_candidates_offered": len(bundle.claim_groups),
|
|
106
|
+
"claim_candidates_cut": bundle.claims_cut_count,
|
|
107
|
+
},
|
|
108
|
+
}
|
|
109
|
+
return f"{json.dumps(payload, sort_keys=True, indent=2)}\n"
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def knowledge_writer_coverage(
|
|
113
|
+
*, bundle: KnowledgeWriterBundle, citations: tuple[KnowledgeCitation, ...]
|
|
114
|
+
) -> KnowledgeWriterCoverage:
|
|
115
|
+
"""Count direct and support-claim coverage without trusting writer-reported totals."""
|
|
116
|
+
cited_relations = {
|
|
117
|
+
item.relation_id for item in citations if item.relation_id is not None
|
|
118
|
+
}
|
|
119
|
+
cited_claims = {
|
|
120
|
+
(item.claim_lineage_id, item.claim_chunk_content_hash)
|
|
121
|
+
for item in citations
|
|
122
|
+
if item.claim_lineage_id is not None
|
|
123
|
+
}
|
|
124
|
+
cited_docs = {item.doc_id for item in citations if item.doc_id is not None}
|
|
125
|
+
supporting_claims = {
|
|
126
|
+
(item.claim_lineage_id, item.claim_chunk_content_hash)
|
|
127
|
+
for item in citations
|
|
128
|
+
if item.claim_lineage_id is not None
|
|
129
|
+
and item.role in (KnowledgeEvidenceRole.SUPPORTS, KnowledgeEvidenceRole.CITES)
|
|
130
|
+
}
|
|
131
|
+
supporting_docs = {
|
|
132
|
+
item.doc_id
|
|
133
|
+
for item in citations
|
|
134
|
+
if item.doc_id is not None
|
|
135
|
+
and item.role in (KnowledgeEvidenceRole.SUPPORTS, KnowledgeEvidenceRole.CITES)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
cited_groups: set[tuple[UUID, str]] = set()
|
|
139
|
+
supported_facts: set[tuple[str, UUID]] = set()
|
|
140
|
+
for group in bundle.claim_groups:
|
|
141
|
+
coordinate = _claim_group_key(group)
|
|
142
|
+
group_used = group.lineage_id in cited_docs or coordinate in cited_claims
|
|
143
|
+
for claim in group.claims:
|
|
144
|
+
if coordinate in supporting_claims or claim.lineage_id in supporting_docs:
|
|
145
|
+
supported_facts.update(
|
|
146
|
+
(reference.kind, reference.fact_id)
|
|
147
|
+
for reference in claim.fact_references
|
|
148
|
+
if reference.stance == "supports"
|
|
149
|
+
)
|
|
150
|
+
if group_used:
|
|
151
|
+
cited_groups.add(_claim_group_key(group))
|
|
152
|
+
|
|
153
|
+
cited_facts = {
|
|
154
|
+
(fact.kind, fact.fact_id)
|
|
155
|
+
for fact in bundle.fact_sheet.facts
|
|
156
|
+
if (fact.kind == "relation" and fact.fact_id in cited_relations)
|
|
157
|
+
or (fact.kind, fact.fact_id) in supported_facts
|
|
158
|
+
}
|
|
159
|
+
candidate_count = len(bundle.fact_sheet.facts) + len(bundle.claim_groups)
|
|
160
|
+
cited_candidate_count = len(cited_facts) + len(cited_groups)
|
|
161
|
+
return KnowledgeWriterCoverage(
|
|
162
|
+
candidate_count=candidate_count,
|
|
163
|
+
cited_candidate_count=cited_candidate_count,
|
|
164
|
+
uncited_count=candidate_count - cited_candidate_count,
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _claim_group_key(group: KnowledgeWriterClaimGroup) -> tuple[UUID, str]:
|
|
169
|
+
"""Return one D54 coordinate in its typed form."""
|
|
170
|
+
return group.lineage_id, group.chunk_content_hash
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def _sortable_group_key(key: tuple[UUID, str]) -> tuple[str, str]:
|
|
174
|
+
"""Convert one D54 coordinate to a stable total-order key."""
|
|
175
|
+
return str(key[0]), key[1]
|