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,434 @@
|
|
|
1
|
+
"""The E3 normalizer (D2-D5, D17-D18, D43): claims → relations and observations.
|
|
2
|
+
|
|
3
|
+
Per claim, one normalizer call proposes (subject, predicate, object) relations
|
|
4
|
+
and entity-anchored observations. Deterministic gates then govern what lands:
|
|
5
|
+
the predicate must be in the registry vocabulary and its type signature must
|
|
6
|
+
match at some ancestor level (D18 — application-enforced at write time; a
|
|
7
|
+
dropped candidate is re-derivable from its immutable claim). Entities resolve
|
|
8
|
+
through T0; the fact catalog collapses redundancy (D2) and keeps the D54
|
|
9
|
+
lineage-distinct evidence counts.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import logging
|
|
13
|
+
from typing import Final
|
|
14
|
+
from uuid import UUID
|
|
15
|
+
|
|
16
|
+
from pydantic import Field
|
|
17
|
+
from pydantic_settings import BaseSettings
|
|
18
|
+
from pydantic_settings import SettingsConfigDict
|
|
19
|
+
|
|
20
|
+
from rememberstack.model import ClaimedWork
|
|
21
|
+
from rememberstack.model import ClaimForNormalization
|
|
22
|
+
from rememberstack.model import EnqueueWork
|
|
23
|
+
from rememberstack.model import ModelRequest
|
|
24
|
+
from rememberstack.model import NonRetryableHandlerError
|
|
25
|
+
from rememberstack.model import NormalizationResponse
|
|
26
|
+
from rememberstack.model import ObservationAssertion
|
|
27
|
+
from rememberstack.model import PipelineStage
|
|
28
|
+
from rememberstack.ports.cost_meter import CostMeterPort
|
|
29
|
+
from rememberstack.ports.model_provider import ModelProviderPort
|
|
30
|
+
from rememberstack.spine.chunk_catalog import ChunkCatalog
|
|
31
|
+
from rememberstack.spine.claim_catalog import ClaimCatalog
|
|
32
|
+
from rememberstack.spine.entity_registry import EntityRegistry
|
|
33
|
+
from rememberstack.spine.fact_catalog import FactCatalog
|
|
34
|
+
from rememberstack.spine.fact_catalog import OTHER_PREDICATE_GRAMMAR
|
|
35
|
+
from rememberstack.spine.observation_adjudication import ObservationAdjudicator
|
|
36
|
+
from rememberstack.spine.resolver import CascadeResolver
|
|
37
|
+
from rememberstack.spine.supersession import ADJUDICATOR_VERSION
|
|
38
|
+
from rememberstack.spine.supersession import SupersessionAdjudicator
|
|
39
|
+
from rememberstack.workers.base import HandlerOutcome
|
|
40
|
+
from rememberstack.workers.p1 import FACT_LABEL_VERSION
|
|
41
|
+
from rememberstack.workers.p1 import P1_EMBED_CLAIMS_VERSION
|
|
42
|
+
from rememberstack.workers.reconcile import RECONCILE_VERSION
|
|
43
|
+
|
|
44
|
+
_logger = logging.getLogger(__name__)
|
|
45
|
+
|
|
46
|
+
_OTHER_PREDICATE: Final = OTHER_PREDICATE_GRAMMAR
|
|
47
|
+
"""The escape-value routing check (the spine re-validates authoritatively)."""
|
|
48
|
+
|
|
49
|
+
E3_NORMALIZER_VERSION: Final = "e3-normalize-2026.07"
|
|
50
|
+
"""The normalize sub-worker's component version (D12 idempotency member)."""
|
|
51
|
+
|
|
52
|
+
_NORMALIZE_PROMPT: Final = """You are the normalizer of a memory system. Turn
|
|
53
|
+
the CLAIM into zero or more of:
|
|
54
|
+
- relations: (subject, predicate, object) between TWO named entities, using
|
|
55
|
+
ONLY the governed predicates listed below (map synonyms onto them). If a
|
|
56
|
+
clearly relational fact fits NO governed predicate, you may emit
|
|
57
|
+
`other:<short_snake_case>` (e.g. other:sponsors) — never invent a bare
|
|
58
|
+
predicate name;
|
|
59
|
+
- observations: a value/property/statement about ONE entity, as a standalone
|
|
60
|
+
statement ("Acme's headcount is 600"). An ATTRIBUTED stance claim ("X said /
|
|
61
|
+
believes / opposes Y") becomes a stance observation anchored on X — never a
|
|
62
|
+
fact about Y.
|
|
63
|
+
Entity names must be canonical nominative forms; entity types must come from
|
|
64
|
+
the registry types below. Time is never a relation object.
|
|
65
|
+
|
|
66
|
+
GOVERNED PREDICATES:
|
|
67
|
+
{predicates}
|
|
68
|
+
REGISTRY TYPES: {types}
|
|
69
|
+
|
|
70
|
+
CLAIM (attributed={is_attributed}): {claim_text}"""
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class E3Settings(BaseSettings):
|
|
74
|
+
"""The E3 model binding: interchangeable per-deployment port config (D70)."""
|
|
75
|
+
|
|
76
|
+
model_config = SettingsConfigDict(env_prefix="REMEMBERSTACK_E3_")
|
|
77
|
+
|
|
78
|
+
normalize_model: str = Field(default="openai/gpt-5.6-luna")
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class NormalizeRelationsHandler:
|
|
82
|
+
"""The normalize stage: every accepted claim of one representation."""
|
|
83
|
+
|
|
84
|
+
def __init__(
|
|
85
|
+
self,
|
|
86
|
+
*,
|
|
87
|
+
claim_catalog: ClaimCatalog,
|
|
88
|
+
chunk_catalog: ChunkCatalog,
|
|
89
|
+
registry: EntityRegistry,
|
|
90
|
+
resolver: CascadeResolver,
|
|
91
|
+
facts: FactCatalog,
|
|
92
|
+
observation_adjudicator: ObservationAdjudicator,
|
|
93
|
+
model_provider: ModelProviderPort,
|
|
94
|
+
settings: E3Settings,
|
|
95
|
+
chunker_version: str,
|
|
96
|
+
) -> None:
|
|
97
|
+
"""Bind the handler to its catalogs, registry, provider, and generation."""
|
|
98
|
+
self._claim_catalog = claim_catalog
|
|
99
|
+
self._chunk_catalog = chunk_catalog
|
|
100
|
+
self._registry = registry
|
|
101
|
+
self._resolver = resolver
|
|
102
|
+
self._facts = facts
|
|
103
|
+
self._observation_adjudicator = observation_adjudicator
|
|
104
|
+
self._model_provider = model_provider
|
|
105
|
+
self._settings = settings
|
|
106
|
+
self._chunker_version = chunker_version
|
|
107
|
+
|
|
108
|
+
def handle(self, *, work: ClaimedWork, meter: CostMeterPort) -> HandlerOutcome:
|
|
109
|
+
"""Normalize one document version's claims into relations/observations.
|
|
110
|
+
|
|
111
|
+
Newly-created relations chain the supersession adjudicator (D3/D4)
|
|
112
|
+
alongside the P1 writers."""
|
|
113
|
+
source = self._chunk_catalog.chunk_source(
|
|
114
|
+
representation_id=_payload_uuid(work=work, field="representation_id")
|
|
115
|
+
)
|
|
116
|
+
chunks = self._chunk_catalog.chunks_for_embedding(
|
|
117
|
+
representation_id=source.representation_id,
|
|
118
|
+
chunker_version=self._chunker_version,
|
|
119
|
+
)
|
|
120
|
+
claims = self._claim_catalog.claims_for_chunks(
|
|
121
|
+
chunk_ids=tuple(chunk.chunk_id for chunk in chunks)
|
|
122
|
+
)
|
|
123
|
+
if not claims:
|
|
124
|
+
# a version whose extraction yielded nothing still completes its
|
|
125
|
+
# basis change: the chain must reach reconciliation (Codex
|
|
126
|
+
# review — a living replacement of pure boilerplate would
|
|
127
|
+
# otherwise never supersede the old claims)
|
|
128
|
+
return HandlerOutcome(
|
|
129
|
+
follow_up=(
|
|
130
|
+
EnqueueWork(
|
|
131
|
+
deployment_id=work.deployment_id,
|
|
132
|
+
target_kind=work.target_kind,
|
|
133
|
+
target_id=work.target_id,
|
|
134
|
+
stage=PipelineStage.ADJUDICATE_SUPERSESSION,
|
|
135
|
+
component_version=ADJUDICATOR_VERSION,
|
|
136
|
+
content_hash=work.content_hash,
|
|
137
|
+
lane=work.lane,
|
|
138
|
+
payload={**(work.payload or {}), "relation_ids": []},
|
|
139
|
+
),
|
|
140
|
+
)
|
|
141
|
+
)
|
|
142
|
+
deployment_id = work.deployment_id
|
|
143
|
+
predicates = self._facts.active_predicates(deployment_id=deployment_id)
|
|
144
|
+
prompt_lines = self._facts.predicate_prompt_lines(deployment_id=deployment_id)
|
|
145
|
+
signatures = self._facts.predicate_signatures(deployment_id=deployment_id)
|
|
146
|
+
type_parents = self._facts.entity_type_parents(deployment_id=deployment_id)
|
|
147
|
+
created_relations: list[str] = []
|
|
148
|
+
normalized_claim_ids = self._registry.normalized_claim_ids(
|
|
149
|
+
claim_ids=tuple(claim.claim_id for claim in claims)
|
|
150
|
+
)
|
|
151
|
+
observations_by_entity: dict[UUID, list[ObservationAssertion]] = {}
|
|
152
|
+
for claim in claims:
|
|
153
|
+
if claim.claim_id in normalized_claim_ids:
|
|
154
|
+
continue # replay: stored mentions/facts are the output (D7)
|
|
155
|
+
self._normalize_claim(
|
|
156
|
+
created_relations=created_relations,
|
|
157
|
+
observations_by_entity=observations_by_entity,
|
|
158
|
+
deployment_id=deployment_id,
|
|
159
|
+
claim=claim,
|
|
160
|
+
predicates=predicates,
|
|
161
|
+
prompt_lines=prompt_lines,
|
|
162
|
+
signatures=signatures,
|
|
163
|
+
type_parents=type_parents,
|
|
164
|
+
meter=meter,
|
|
165
|
+
)
|
|
166
|
+
for entity_id, assertions in observations_by_entity.items():
|
|
167
|
+
self._observation_adjudicator.add_observations(
|
|
168
|
+
deployment_id=deployment_id,
|
|
169
|
+
subject_entity_id=entity_id,
|
|
170
|
+
assertions=tuple(assertions),
|
|
171
|
+
meter=meter,
|
|
172
|
+
call_key=f"observation:{entity_id}",
|
|
173
|
+
)
|
|
174
|
+
return HandlerOutcome(
|
|
175
|
+
follow_up=(
|
|
176
|
+
EnqueueWork(
|
|
177
|
+
deployment_id=work.deployment_id,
|
|
178
|
+
target_kind=work.target_kind,
|
|
179
|
+
target_id=work.target_id,
|
|
180
|
+
stage=PipelineStage.ADJUDICATE_SUPERSESSION,
|
|
181
|
+
component_version=ADJUDICATOR_VERSION,
|
|
182
|
+
content_hash=work.content_hash,
|
|
183
|
+
lane=work.lane,
|
|
184
|
+
payload={**(work.payload or {}), "relation_ids": created_relations},
|
|
185
|
+
),
|
|
186
|
+
EnqueueWork(
|
|
187
|
+
deployment_id=work.deployment_id,
|
|
188
|
+
target_kind=work.target_kind,
|
|
189
|
+
target_id=work.target_id,
|
|
190
|
+
stage=PipelineStage.EMBED_CLAIM,
|
|
191
|
+
component_version=P1_EMBED_CLAIMS_VERSION,
|
|
192
|
+
content_hash=work.content_hash,
|
|
193
|
+
lane=work.lane,
|
|
194
|
+
payload=dict(work.payload or {}),
|
|
195
|
+
),
|
|
196
|
+
EnqueueWork(
|
|
197
|
+
deployment_id=work.deployment_id,
|
|
198
|
+
target_kind=work.target_kind,
|
|
199
|
+
target_id=work.target_id,
|
|
200
|
+
stage=PipelineStage.LABEL_RELATION,
|
|
201
|
+
component_version=FACT_LABEL_VERSION,
|
|
202
|
+
content_hash=work.content_hash,
|
|
203
|
+
lane=work.lane,
|
|
204
|
+
payload={**(work.payload or {}), "doc_id": str(source.doc_id)},
|
|
205
|
+
),
|
|
206
|
+
)
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
def _normalize_claim(
|
|
210
|
+
self,
|
|
211
|
+
*,
|
|
212
|
+
created_relations: list[str],
|
|
213
|
+
observations_by_entity: dict[UUID, list[ObservationAssertion]],
|
|
214
|
+
deployment_id: UUID,
|
|
215
|
+
claim: ClaimForNormalization,
|
|
216
|
+
predicates: dict[str, str | None],
|
|
217
|
+
prompt_lines: str,
|
|
218
|
+
signatures: dict[str, tuple[tuple[str, str], ...]],
|
|
219
|
+
type_parents: dict[str, str | None],
|
|
220
|
+
meter: CostMeterPort,
|
|
221
|
+
) -> None:
|
|
222
|
+
"""One claim through the normalizer call and the deterministic gates."""
|
|
223
|
+
response_call = self._model_provider.generate(
|
|
224
|
+
request=ModelRequest(
|
|
225
|
+
model=self._settings.normalize_model,
|
|
226
|
+
prompt=_NORMALIZE_PROMPT.format(
|
|
227
|
+
predicates=prompt_lines,
|
|
228
|
+
types=", ".join(sorted(type_parents)),
|
|
229
|
+
is_attributed=claim.is_attributed,
|
|
230
|
+
claim_text=claim.claim_text,
|
|
231
|
+
),
|
|
232
|
+
),
|
|
233
|
+
response_type=NormalizationResponse,
|
|
234
|
+
)
|
|
235
|
+
meter.record(
|
|
236
|
+
call_key=f"normalize:{claim.claim_id}",
|
|
237
|
+
tier="normalize",
|
|
238
|
+
usage=response_call.usage,
|
|
239
|
+
)
|
|
240
|
+
response = response_call.output
|
|
241
|
+
for relation_index, relation in enumerate(response.relations):
|
|
242
|
+
if _OTHER_PREDICATE.fullmatch(relation.predicate):
|
|
243
|
+
# the D5 escape funnel: register tier=other, unconstrained
|
|
244
|
+
# by signatures, ranked by usage for periodic promotion
|
|
245
|
+
self._facts.ensure_other_predicate(
|
|
246
|
+
deployment_id=deployment_id, predicate=relation.predicate
|
|
247
|
+
)
|
|
248
|
+
predicates = {**predicates, relation.predicate: "related_to"}
|
|
249
|
+
if relation.predicate not in predicates:
|
|
250
|
+
_logger.warning(
|
|
251
|
+
"unknown predicate %r dropped for claim %s (re-derivable)",
|
|
252
|
+
relation.predicate,
|
|
253
|
+
claim.claim_id,
|
|
254
|
+
)
|
|
255
|
+
continue
|
|
256
|
+
if not _signature_allows(
|
|
257
|
+
predicate=relation.predicate,
|
|
258
|
+
subject_type=relation.subject.type,
|
|
259
|
+
object_type=relation.object.type,
|
|
260
|
+
signatures=signatures,
|
|
261
|
+
type_parents=type_parents,
|
|
262
|
+
):
|
|
263
|
+
_logger.warning(
|
|
264
|
+
"signature-rejected %r (%s -> %s) for claim %s (re-derivable)",
|
|
265
|
+
relation.predicate,
|
|
266
|
+
relation.subject.type,
|
|
267
|
+
relation.object.type,
|
|
268
|
+
claim.claim_id,
|
|
269
|
+
)
|
|
270
|
+
continue
|
|
271
|
+
subject = self._resolver.resolve(
|
|
272
|
+
deployment_id=deployment_id,
|
|
273
|
+
reference=relation.subject,
|
|
274
|
+
claim=claim,
|
|
275
|
+
meter=meter,
|
|
276
|
+
call_key=(
|
|
277
|
+
f"resolve:{claim.claim_id}:relation:{relation_index}:subject"
|
|
278
|
+
),
|
|
279
|
+
)
|
|
280
|
+
object_ = self._resolver.resolve(
|
|
281
|
+
deployment_id=deployment_id,
|
|
282
|
+
reference=relation.object,
|
|
283
|
+
claim=claim,
|
|
284
|
+
meter=meter,
|
|
285
|
+
call_key=f"resolve:{claim.claim_id}:relation:{relation_index}:object",
|
|
286
|
+
)
|
|
287
|
+
if not _signature_allows(
|
|
288
|
+
predicate=relation.predicate,
|
|
289
|
+
subject_type=subject.entity_type,
|
|
290
|
+
object_type=object_.entity_type,
|
|
291
|
+
signatures=signatures,
|
|
292
|
+
type_parents=type_parents,
|
|
293
|
+
):
|
|
294
|
+
# the gate binds on the RESOLVED entities' stored types too —
|
|
295
|
+
# T0 may map an emitted name onto a differently-typed entity
|
|
296
|
+
# (Codex review); the candidate stays re-derivable.
|
|
297
|
+
_logger.warning(
|
|
298
|
+
"signature-rejected %r on resolved types (%s -> %s), claim %s",
|
|
299
|
+
relation.predicate,
|
|
300
|
+
subject.entity_type,
|
|
301
|
+
object_.entity_type,
|
|
302
|
+
claim.claim_id,
|
|
303
|
+
)
|
|
304
|
+
continue
|
|
305
|
+
upserted = self._facts.upsert_relation(
|
|
306
|
+
deployment_id=deployment_id,
|
|
307
|
+
subject_entity_id=subject.entity_id,
|
|
308
|
+
predicate=relation.predicate,
|
|
309
|
+
object_entity_id=object_.entity_id,
|
|
310
|
+
claim_id=claim.claim_id,
|
|
311
|
+
doc_id=claim.doc_id,
|
|
312
|
+
normalizer_version=E3_NORMALIZER_VERSION,
|
|
313
|
+
)
|
|
314
|
+
if upserted.created:
|
|
315
|
+
created_relations.append(str(upserted.relation_id))
|
|
316
|
+
for observation_index, observation in enumerate(response.observations):
|
|
317
|
+
subject = self._resolver.resolve(
|
|
318
|
+
deployment_id=deployment_id,
|
|
319
|
+
reference=observation.subject,
|
|
320
|
+
claim=claim,
|
|
321
|
+
meter=meter,
|
|
322
|
+
call_key=(
|
|
323
|
+
f"resolve:{claim.claim_id}:observation:{observation_index}:subject"
|
|
324
|
+
),
|
|
325
|
+
)
|
|
326
|
+
# the one write path for observations is the D43 adjudicator:
|
|
327
|
+
# block on the entity, gate cheaply, ladder the residue,
|
|
328
|
+
# fail safe to coexist (observations §3)
|
|
329
|
+
observations_by_entity.setdefault(subject.entity_id, []).append(
|
|
330
|
+
ObservationAssertion(
|
|
331
|
+
statement=observation.statement,
|
|
332
|
+
claim_id=claim.claim_id,
|
|
333
|
+
doc_id=claim.doc_id,
|
|
334
|
+
)
|
|
335
|
+
)
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
def _signature_allows(
|
|
339
|
+
*,
|
|
340
|
+
predicate: str,
|
|
341
|
+
subject_type: str,
|
|
342
|
+
object_type: str,
|
|
343
|
+
signatures: dict[str, tuple[tuple[str, str], ...]],
|
|
344
|
+
type_parents: dict[str, str | None],
|
|
345
|
+
) -> bool:
|
|
346
|
+
"""The D18 domain/range gate: some signature matches at any ancestor level.
|
|
347
|
+
|
|
348
|
+
Unknown emitted types fail closed; a predicate with no declared signatures
|
|
349
|
+
is unconstrained (the registry's permissive parents, e.g. related_to).
|
|
350
|
+
"""
|
|
351
|
+
if subject_type not in type_parents or object_type not in type_parents:
|
|
352
|
+
return False
|
|
353
|
+
declared = signatures.get(predicate)
|
|
354
|
+
if not declared:
|
|
355
|
+
return True
|
|
356
|
+
subject_chain = _ancestor_chain(entity_type=subject_type, parents=type_parents)
|
|
357
|
+
object_chain = _ancestor_chain(entity_type=object_type, parents=type_parents)
|
|
358
|
+
return any(
|
|
359
|
+
allowed_subject in subject_chain and allowed_object in object_chain
|
|
360
|
+
for allowed_subject, allowed_object in declared
|
|
361
|
+
)
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
def _ancestor_chain(
|
|
365
|
+
*, entity_type: str, parents: dict[str, str | None]
|
|
366
|
+
) -> frozenset[str]:
|
|
367
|
+
"""The type plus every ancestor (extend-never-fork walk, cycle-safe)."""
|
|
368
|
+
chain: set[str] = set()
|
|
369
|
+
current: str | None = entity_type
|
|
370
|
+
while current is not None and current not in chain:
|
|
371
|
+
chain.add(current)
|
|
372
|
+
current = parents.get(current)
|
|
373
|
+
return frozenset(chain)
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
def _payload_uuid(*, work: ClaimedWork, field: str) -> UUID:
|
|
377
|
+
"""Read a required UUID from the claimed payload; absence is non-retryable."""
|
|
378
|
+
value = (work.payload or {}).get(field)
|
|
379
|
+
if not isinstance(value, str):
|
|
380
|
+
raise NonRetryableHandlerError(
|
|
381
|
+
f"stage {work.stage} work {work.processing_id} carries no {field!r} payload"
|
|
382
|
+
)
|
|
383
|
+
return UUID(value)
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
class AdjudicateSupersessionHandler:
|
|
387
|
+
"""The adjudication stage: each newly-created relation through the cascade."""
|
|
388
|
+
|
|
389
|
+
def __init__(self, *, adjudicator: SupersessionAdjudicator) -> None:
|
|
390
|
+
"""Bind the handler to the composed adjudicator."""
|
|
391
|
+
self._adjudicator = adjudicator
|
|
392
|
+
|
|
393
|
+
def handle(self, *, work: ClaimedWork, meter: CostMeterPort) -> HandlerOutcome:
|
|
394
|
+
"""Adjudicate every relation the normalize stage created (idempotent).
|
|
395
|
+
|
|
396
|
+
Chains the reconcile stage — the truth machinery for this version's
|
|
397
|
+
basis change is settled, so reconciliation (lifecycle §5: currency,
|
|
398
|
+
recount, zero-support policy, `evidence_changed`) can run on a
|
|
399
|
+
completed basis, never mid-flight.
|
|
400
|
+
"""
|
|
401
|
+
payload = work.payload or {}
|
|
402
|
+
relation_ids = payload.get("relation_ids") or []
|
|
403
|
+
if not isinstance(relation_ids, list):
|
|
404
|
+
raise NonRetryableHandlerError(
|
|
405
|
+
f"work {work.processing_id} carries a malformed relation_ids payload"
|
|
406
|
+
)
|
|
407
|
+
for raw in relation_ids:
|
|
408
|
+
self._adjudicator.adjudicate_new_relation(
|
|
409
|
+
deployment_id=work.deployment_id,
|
|
410
|
+
relation_id=UUID(str(raw)),
|
|
411
|
+
meter=meter,
|
|
412
|
+
call_key=f"supersession:{raw}",
|
|
413
|
+
)
|
|
414
|
+
version_id = payload.get("version_id")
|
|
415
|
+
representation_id = payload.get("representation_id")
|
|
416
|
+
if not isinstance(version_id, str) or not isinstance(representation_id, str):
|
|
417
|
+
return HandlerOutcome() # pre-lifecycle work rows: nothing to chain
|
|
418
|
+
return HandlerOutcome(
|
|
419
|
+
follow_up=(
|
|
420
|
+
EnqueueWork(
|
|
421
|
+
deployment_id=work.deployment_id,
|
|
422
|
+
target_kind=work.target_kind,
|
|
423
|
+
target_id=work.target_id,
|
|
424
|
+
stage=PipelineStage.RECONCILE,
|
|
425
|
+
component_version=RECONCILE_VERSION,
|
|
426
|
+
content_hash=work.content_hash,
|
|
427
|
+
lane=work.lane,
|
|
428
|
+
payload={
|
|
429
|
+
"version_id": version_id,
|
|
430
|
+
"representation_id": representation_id,
|
|
431
|
+
},
|
|
432
|
+
),
|
|
433
|
+
)
|
|
434
|
+
)
|