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,124 @@
|
|
|
1
|
+
"""System-shipped extension packs (registries §4, D15): the Work pack.
|
|
2
|
+
|
|
3
|
+
Extensions are not second-class: a pack type lives in the same entity space,
|
|
4
|
+
graph, ER machinery, and relations as core types — the tier is a governance
|
|
5
|
+
distinction (stability commitment, golden-set obligation), not a capability
|
|
6
|
+
one. Every pack type anchors to a core parent (extend-never-fork); the
|
|
7
|
+
installer refuses a pack whose anchors don't exist.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
from typing import Final
|
|
12
|
+
|
|
13
|
+
_CORE_ROOTS: Final = (
|
|
14
|
+
"Person",
|
|
15
|
+
"Organization",
|
|
16
|
+
"Place",
|
|
17
|
+
"Document",
|
|
18
|
+
"Event",
|
|
19
|
+
"Concept",
|
|
20
|
+
"Project",
|
|
21
|
+
"Product",
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass(frozen=True)
|
|
26
|
+
class PackEntityType:
|
|
27
|
+
"""One extension entity type anchored to a core parent."""
|
|
28
|
+
|
|
29
|
+
type: str
|
|
30
|
+
parent_type: str
|
|
31
|
+
description: str
|
|
32
|
+
examples: tuple[str, ...] = ()
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass(frozen=True)
|
|
36
|
+
class PackPredicate:
|
|
37
|
+
"""One extension predicate with its domain/range signatures (D18)."""
|
|
38
|
+
|
|
39
|
+
predicate: str
|
|
40
|
+
description: str
|
|
41
|
+
signatures: tuple[tuple[str, str], ...]
|
|
42
|
+
synonyms: tuple[str, ...] = ()
|
|
43
|
+
is_change_prone: bool = False
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@dataclass(frozen=True)
|
|
47
|
+
class ExtensionPack:
|
|
48
|
+
"""A predefined bundle a deployment enables as one unit."""
|
|
49
|
+
|
|
50
|
+
pack_id: str
|
|
51
|
+
name: str
|
|
52
|
+
description: str
|
|
53
|
+
entity_types: tuple[PackEntityType, ...] = ()
|
|
54
|
+
predicates: tuple[PackPredicate, ...] = ()
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _to_any(*subjects: str) -> tuple[tuple[str, str], ...]:
|
|
58
|
+
"""Signatures pairing each subject with every core root ("→ any")."""
|
|
59
|
+
return tuple(
|
|
60
|
+
(subject, object_root) for subject in subjects for object_root in _CORE_ROOTS
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
WORK_PACK: Final = ExtensionPack(
|
|
65
|
+
pack_id="work",
|
|
66
|
+
name="Work",
|
|
67
|
+
description=(
|
|
68
|
+
"Work-shaped concepts for assistant, agency, and project-management "
|
|
69
|
+
"deployments: tasks, decisions, and goals as first-class entities."
|
|
70
|
+
),
|
|
71
|
+
entity_types=(
|
|
72
|
+
PackEntityType(
|
|
73
|
+
type="Task",
|
|
74
|
+
parent_type="Event",
|
|
75
|
+
description="an intended occurrence with a lifecycle",
|
|
76
|
+
examples=("migrate the billing tables", "draft the Q3 report"),
|
|
77
|
+
),
|
|
78
|
+
PackEntityType(
|
|
79
|
+
type="Decision",
|
|
80
|
+
parent_type="Event",
|
|
81
|
+
description="a commitment made at a point in time",
|
|
82
|
+
examples=("adopt PostgreSQL", "freeze the API surface"),
|
|
83
|
+
),
|
|
84
|
+
PackEntityType(
|
|
85
|
+
type="Goal",
|
|
86
|
+
parent_type="Concept",
|
|
87
|
+
description="a desired state — held, not occurring",
|
|
88
|
+
examples=("sub-second p99 latency", "SOC 2 compliance"),
|
|
89
|
+
),
|
|
90
|
+
),
|
|
91
|
+
predicates=(
|
|
92
|
+
PackPredicate(
|
|
93
|
+
predicate="blocks",
|
|
94
|
+
description="the subject task prevents progress on the object task",
|
|
95
|
+
signatures=(("Task", "Task"),),
|
|
96
|
+
),
|
|
97
|
+
PackPredicate(
|
|
98
|
+
predicate="depends_on",
|
|
99
|
+
description="the subject task requires the object task first",
|
|
100
|
+
signatures=(("Task", "Task"),),
|
|
101
|
+
),
|
|
102
|
+
PackPredicate(
|
|
103
|
+
predicate="concerns",
|
|
104
|
+
description="the subject task or decision is about the object",
|
|
105
|
+
signatures=_to_any("Task", "Decision"),
|
|
106
|
+
),
|
|
107
|
+
PackPredicate(
|
|
108
|
+
predicate="decided_by",
|
|
109
|
+
description="who made the decision",
|
|
110
|
+
signatures=(("Decision", "Person"), ("Decision", "Organization")),
|
|
111
|
+
),
|
|
112
|
+
PackPredicate(
|
|
113
|
+
predicate="assigned_to",
|
|
114
|
+
description="who is responsible for the task",
|
|
115
|
+
signatures=(("Task", "Person"), ("Task", "Organization")),
|
|
116
|
+
is_change_prone=True,
|
|
117
|
+
),
|
|
118
|
+
PackPredicate(
|
|
119
|
+
predicate="pursues",
|
|
120
|
+
description="the project or organization works toward the goal",
|
|
121
|
+
signatures=(("Project", "Goal"), ("Organization", "Goal")),
|
|
122
|
+
),
|
|
123
|
+
),
|
|
124
|
+
)
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""Pure canonical identity helpers for D74 hard-forget guards."""
|
|
2
|
+
|
|
3
|
+
import hashlib
|
|
4
|
+
import json
|
|
5
|
+
from uuid import UUID
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def source_identity_hash(
|
|
9
|
+
*, deployment_id: UUID, source_kind: str, source_ref: str
|
|
10
|
+
) -> str:
|
|
11
|
+
"""Hash one unambiguous deployment-owned connector identity tuple."""
|
|
12
|
+
canonical = json.dumps(
|
|
13
|
+
[str(deployment_id), source_kind, source_ref],
|
|
14
|
+
ensure_ascii=False,
|
|
15
|
+
separators=(",", ":"),
|
|
16
|
+
).encode("utf-8")
|
|
17
|
+
return hashlib.sha256(canonical).hexdigest()
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
"""Pure authored-frontmatter parsing and declaration-lint helpers."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import PurePosixPath
|
|
5
|
+
from typing import TypeAlias
|
|
6
|
+
from uuid import UUID
|
|
7
|
+
|
|
8
|
+
from pydantic import TypeAdapter
|
|
9
|
+
from pydantic import ValidationError
|
|
10
|
+
|
|
11
|
+
from rememberstack.model import CommunityRuleParams
|
|
12
|
+
from rememberstack.model import DocSetRuleParams
|
|
13
|
+
from rememberstack.model import EntityRuleParams
|
|
14
|
+
from rememberstack.model import KnowledgeAuthoredDeclaration
|
|
15
|
+
from rememberstack.model import KnowledgeCitation
|
|
16
|
+
from rememberstack.model import KnowledgeEvidenceRole
|
|
17
|
+
from rememberstack.model import KnowledgeRuleParams
|
|
18
|
+
from rememberstack.model import PredicateBeatRuleParams
|
|
19
|
+
|
|
20
|
+
_RULE_ADAPTER = TypeAdapter(KnowledgeRuleParams)
|
|
21
|
+
_FrontmatterItem: TypeAlias = str | dict[str, object]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class KnowledgeAuthoredDeclarationError(ValueError):
|
|
25
|
+
"""An authored page carries malformed ``cites`` or ``watch`` declarations."""
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def parse_knowledge_authored_frontmatter(
|
|
29
|
+
*, markdown: str
|
|
30
|
+
) -> KnowledgeAuthoredDeclaration:
|
|
31
|
+
"""Parse the strict JSON-compatible YAML subset used by authored pages.
|
|
32
|
+
|
|
33
|
+
``cites`` and ``watch`` accept an inline JSON list or an indented list whose
|
|
34
|
+
items are shorthands/JSON objects. Other frontmatter keys are left to their
|
|
35
|
+
owning surface and ignored here.
|
|
36
|
+
"""
|
|
37
|
+
lines = _frontmatter_lines(markdown=markdown)
|
|
38
|
+
if lines is None:
|
|
39
|
+
return KnowledgeAuthoredDeclaration()
|
|
40
|
+
declared = _declared_lists(lines=lines)
|
|
41
|
+
citations = (
|
|
42
|
+
None
|
|
43
|
+
if declared["cites"] is None
|
|
44
|
+
else tuple(_citation(item=item) for item in declared["cites"] or ())
|
|
45
|
+
)
|
|
46
|
+
watch_rules: tuple[KnowledgeRuleParams, ...] | None = None
|
|
47
|
+
page_paths: tuple[str, ...] | None = None
|
|
48
|
+
if declared["watch"] is not None:
|
|
49
|
+
rules: list[KnowledgeRuleParams] = []
|
|
50
|
+
paths: list[str] = []
|
|
51
|
+
for item in declared["watch"] or ():
|
|
52
|
+
rule, page_path = _watch(item=item)
|
|
53
|
+
if rule is not None:
|
|
54
|
+
rules.append(rule)
|
|
55
|
+
if page_path is not None:
|
|
56
|
+
paths.append(page_path)
|
|
57
|
+
watch_rules = tuple(rules)
|
|
58
|
+
page_paths = tuple(paths)
|
|
59
|
+
return KnowledgeAuthoredDeclaration(
|
|
60
|
+
citations=citations, watch_rules=watch_rules, watched_page_paths=page_paths
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def authored_declaration_is_empty(
|
|
65
|
+
*, citation_count: int, watch_rule_count: int, page_watch_count: int
|
|
66
|
+
) -> bool:
|
|
67
|
+
"""Return whether an authored page has no ground and can never be alerted."""
|
|
68
|
+
if min(citation_count, watch_rule_count, page_watch_count) < 0:
|
|
69
|
+
raise ValueError("declaration counts must be non-negative")
|
|
70
|
+
return citation_count + watch_rule_count + page_watch_count == 0
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def knowledge_citation_reference(*, citation: KnowledgeCitation) -> str:
|
|
74
|
+
"""Render one citation as a stable delta identifier."""
|
|
75
|
+
if citation.claim_lineage_id is not None:
|
|
76
|
+
target = (
|
|
77
|
+
f"claim:{citation.claim_lineage_id}:{citation.claim_chunk_content_hash}"
|
|
78
|
+
)
|
|
79
|
+
elif citation.relation_id is not None:
|
|
80
|
+
target = f"relation:{citation.relation_id}"
|
|
81
|
+
else:
|
|
82
|
+
target = f"doc:{citation.doc_id}"
|
|
83
|
+
return f"{citation.role.value}:{target}"
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _frontmatter_lines(*, markdown: str) -> tuple[str, ...] | None:
|
|
87
|
+
"""Return frontmatter lines, distinguishing no header from an unclosed one."""
|
|
88
|
+
lines = markdown.splitlines()
|
|
89
|
+
if not lines or lines[0].strip() != "---":
|
|
90
|
+
return None
|
|
91
|
+
try:
|
|
92
|
+
end = next(
|
|
93
|
+
index
|
|
94
|
+
for index, line in enumerate(lines[1:], start=1)
|
|
95
|
+
if line.strip() == "---"
|
|
96
|
+
)
|
|
97
|
+
except StopIteration as error:
|
|
98
|
+
raise KnowledgeAuthoredDeclarationError(
|
|
99
|
+
"authored frontmatter is missing its closing delimiter"
|
|
100
|
+
) from error
|
|
101
|
+
return tuple(lines[1:end])
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _declared_lists(
|
|
105
|
+
*, lines: tuple[str, ...]
|
|
106
|
+
) -> dict[str, tuple[_FrontmatterItem, ...] | None]:
|
|
107
|
+
"""Extract the two owned list keys without parsing unrelated frontmatter."""
|
|
108
|
+
result: dict[str, tuple[_FrontmatterItem, ...] | None] = {
|
|
109
|
+
"cites": None,
|
|
110
|
+
"watch": None,
|
|
111
|
+
}
|
|
112
|
+
seen: set[str] = set()
|
|
113
|
+
index = 0
|
|
114
|
+
while index < len(lines):
|
|
115
|
+
line = lines[index]
|
|
116
|
+
if not line or line[0].isspace() or ":" not in line:
|
|
117
|
+
index += 1
|
|
118
|
+
continue
|
|
119
|
+
key, inline = line.split(":", maxsplit=1)
|
|
120
|
+
if key not in result:
|
|
121
|
+
index += 1
|
|
122
|
+
continue
|
|
123
|
+
if key in seen:
|
|
124
|
+
raise KnowledgeAuthoredDeclarationError(
|
|
125
|
+
f"authored frontmatter repeats {key!r}"
|
|
126
|
+
)
|
|
127
|
+
seen.add(key)
|
|
128
|
+
inline = inline.strip()
|
|
129
|
+
if inline:
|
|
130
|
+
result[key] = _inline_list(key=key, value=inline)
|
|
131
|
+
index += 1
|
|
132
|
+
continue
|
|
133
|
+
values: list[_FrontmatterItem] = []
|
|
134
|
+
index += 1
|
|
135
|
+
while index < len(lines):
|
|
136
|
+
nested = lines[index]
|
|
137
|
+
if nested and not nested[0].isspace():
|
|
138
|
+
break
|
|
139
|
+
stripped = nested.strip()
|
|
140
|
+
index += 1
|
|
141
|
+
if not stripped or stripped.startswith("#"):
|
|
142
|
+
continue
|
|
143
|
+
if not stripped.startswith("- "):
|
|
144
|
+
raise KnowledgeAuthoredDeclarationError(
|
|
145
|
+
f"{key!r} must be a flat list of shorthands or JSON objects"
|
|
146
|
+
)
|
|
147
|
+
values.append(_list_item(key=key, value=stripped[2:].strip()))
|
|
148
|
+
result[key] = tuple(values)
|
|
149
|
+
return result
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def _inline_list(*, key: str, value: str) -> tuple[_FrontmatterItem, ...]:
|
|
153
|
+
"""Parse one inline JSON list for a declaration key."""
|
|
154
|
+
try:
|
|
155
|
+
parsed = json.loads(value)
|
|
156
|
+
except json.JSONDecodeError as error:
|
|
157
|
+
raise KnowledgeAuthoredDeclarationError(
|
|
158
|
+
f"inline {key!r} must be valid JSON"
|
|
159
|
+
) from error
|
|
160
|
+
if not isinstance(parsed, list):
|
|
161
|
+
raise KnowledgeAuthoredDeclarationError(f"{key!r} must be a list")
|
|
162
|
+
return tuple(_require_item(key=key, value=item) for item in parsed)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def _list_item(*, key: str, value: str) -> _FrontmatterItem:
|
|
166
|
+
"""Parse JSON-looking list items and retain plain shorthands verbatim."""
|
|
167
|
+
if not value:
|
|
168
|
+
raise KnowledgeAuthoredDeclarationError(f"{key!r} contains an empty item")
|
|
169
|
+
if value[0] not in ('"', "{"):
|
|
170
|
+
return value
|
|
171
|
+
try:
|
|
172
|
+
parsed = json.loads(value)
|
|
173
|
+
except json.JSONDecodeError as error:
|
|
174
|
+
raise KnowledgeAuthoredDeclarationError(
|
|
175
|
+
f"{key!r} contains invalid JSON"
|
|
176
|
+
) from error
|
|
177
|
+
return _require_item(key=key, value=parsed)
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def _require_item(*, key: str, value: object) -> _FrontmatterItem:
|
|
181
|
+
"""Reject declaration values outside the supported scalar/object shapes."""
|
|
182
|
+
if isinstance(value, str):
|
|
183
|
+
return value
|
|
184
|
+
if isinstance(value, dict) and all(isinstance(name, str) for name in value):
|
|
185
|
+
return value
|
|
186
|
+
raise KnowledgeAuthoredDeclarationError(
|
|
187
|
+
f"{key!r} items must be strings or JSON objects"
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def _citation(*, item: _FrontmatterItem) -> KnowledgeCitation:
|
|
192
|
+
"""Validate one citation object or compact evidence-reference string."""
|
|
193
|
+
if isinstance(item, dict):
|
|
194
|
+
try:
|
|
195
|
+
return KnowledgeCitation.model_validate(item)
|
|
196
|
+
except ValidationError as error:
|
|
197
|
+
raise KnowledgeAuthoredDeclarationError(
|
|
198
|
+
"invalid authored citation object"
|
|
199
|
+
) from error
|
|
200
|
+
parts = item.split(":")
|
|
201
|
+
role = KnowledgeEvidenceRole.CITES
|
|
202
|
+
if parts[0] in {value.value for value in KnowledgeEvidenceRole}:
|
|
203
|
+
role = KnowledgeEvidenceRole(parts.pop(0))
|
|
204
|
+
if len(parts) == 2 and parts[0] in {"relation", "doc"}:
|
|
205
|
+
try:
|
|
206
|
+
target_id = UUID(parts[1])
|
|
207
|
+
except ValueError as error:
|
|
208
|
+
raise KnowledgeAuthoredDeclarationError(
|
|
209
|
+
"citation shorthand contains an invalid UUID"
|
|
210
|
+
) from error
|
|
211
|
+
if parts[0] == "relation":
|
|
212
|
+
return KnowledgeCitation(role=role, relation_id=target_id)
|
|
213
|
+
return KnowledgeCitation(role=role, doc_id=target_id)
|
|
214
|
+
if len(parts) == 3 and parts[0] == "claim" and parts[2]:
|
|
215
|
+
try:
|
|
216
|
+
lineage_id = UUID(parts[1])
|
|
217
|
+
except ValueError as error:
|
|
218
|
+
raise KnowledgeAuthoredDeclarationError(
|
|
219
|
+
"claim citation contains an invalid lineage UUID"
|
|
220
|
+
) from error
|
|
221
|
+
return KnowledgeCitation(
|
|
222
|
+
role=role, claim_lineage_id=lineage_id, claim_chunk_content_hash=parts[2]
|
|
223
|
+
)
|
|
224
|
+
raise KnowledgeAuthoredDeclarationError(
|
|
225
|
+
"citation shorthand must target claim, relation, or doc evidence"
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def _watch(*, item: _FrontmatterItem) -> tuple[KnowledgeRuleParams | None, str | None]:
|
|
230
|
+
"""Validate one rich rule object or compact evidence/page watch."""
|
|
231
|
+
if isinstance(item, dict):
|
|
232
|
+
try:
|
|
233
|
+
return _RULE_ADAPTER.validate_python(item), None
|
|
234
|
+
except ValidationError as error:
|
|
235
|
+
raise KnowledgeAuthoredDeclarationError(
|
|
236
|
+
"invalid authored watch-rule object"
|
|
237
|
+
) from error
|
|
238
|
+
prefix, separator, raw_value = item.partition(":")
|
|
239
|
+
if not separator or not raw_value:
|
|
240
|
+
raise KnowledgeAuthoredDeclarationError(
|
|
241
|
+
"watch shorthand must contain a kind and value"
|
|
242
|
+
)
|
|
243
|
+
try:
|
|
244
|
+
if prefix == "entity":
|
|
245
|
+
return EntityRuleParams(entity_id=UUID(raw_value)), None
|
|
246
|
+
if prefix == "community":
|
|
247
|
+
return CommunityRuleParams(community_id=UUID(raw_value)), None
|
|
248
|
+
except ValueError as error:
|
|
249
|
+
raise KnowledgeAuthoredDeclarationError(
|
|
250
|
+
"watch shorthand contains an invalid UUID"
|
|
251
|
+
) from error
|
|
252
|
+
if prefix == "predicate":
|
|
253
|
+
return PredicateBeatRuleParams(predicate=raw_value), None
|
|
254
|
+
if prefix == "doc_source":
|
|
255
|
+
return DocSetRuleParams(source_kind=raw_value), None
|
|
256
|
+
if prefix == "page":
|
|
257
|
+
return None, _page_watch_path(value=raw_value)
|
|
258
|
+
raise KnowledgeAuthoredDeclarationError(f"unsupported watch shorthand {prefix!r}")
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def _page_watch_path(*, value: str) -> str:
|
|
262
|
+
"""Normalize the design's extension-optional ``page:<path>`` shorthand."""
|
|
263
|
+
path = PurePosixPath(value)
|
|
264
|
+
if path.suffix == "":
|
|
265
|
+
path = path.with_suffix(".md")
|
|
266
|
+
normalized = str(path)
|
|
267
|
+
if (
|
|
268
|
+
path.is_absolute()
|
|
269
|
+
or ".." in path.parts
|
|
270
|
+
or normalized != (f"{value}.md" if PurePosixPath(value).suffix == "" else value)
|
|
271
|
+
or path.suffix != ".md"
|
|
272
|
+
):
|
|
273
|
+
raise KnowledgeAuthoredDeclarationError(
|
|
274
|
+
"page watch must be a normalized relative Markdown path"
|
|
275
|
+
)
|
|
276
|
+
return normalized
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
"""Pure scheduling and output-validation rules for Plane-K cycles (D45)."""
|
|
2
|
+
|
|
3
|
+
from collections.abc import Collection
|
|
4
|
+
from collections.abc import Sequence
|
|
5
|
+
import posixpath
|
|
6
|
+
from urllib.parse import unquote
|
|
7
|
+
from urllib.parse import urlsplit
|
|
8
|
+
from uuid import UUID
|
|
9
|
+
|
|
10
|
+
from markdown_it import MarkdownIt
|
|
11
|
+
|
|
12
|
+
from rememberstack.core.knowledge_hashing import knowledge_content_hash
|
|
13
|
+
from rememberstack.model import KnowledgeCitation
|
|
14
|
+
from rememberstack.model import KnowledgeCompileArtifact
|
|
15
|
+
from rememberstack.model import KnowledgeEvidenceTarget
|
|
16
|
+
from rememberstack.model import KnowledgePageCompileOutput
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class KnowledgeCompileGraphError(ValueError):
|
|
20
|
+
"""The persisted compiled-page parent graph cannot be scheduled safely."""
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class KnowledgePageValidationError(ValueError):
|
|
24
|
+
"""A page compiler returned output that the driver must not publish."""
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def knowledge_compile_order(
|
|
28
|
+
*, artifacts: Sequence[KnowledgeCompileArtifact]
|
|
29
|
+
) -> tuple[KnowledgeCompileArtifact, ...]:
|
|
30
|
+
"""Order the potential stale/model propagation closure deterministically.
|
|
31
|
+
|
|
32
|
+
Active ancestors are traversed because a changed child summary can stale them
|
|
33
|
+
inside this cycle. Likewise, a stale shared model can stale every page in its
|
|
34
|
+
scope. The driver skips those potential dependants when the freshly compiled
|
|
35
|
+
summary is unchanged.
|
|
36
|
+
"""
|
|
37
|
+
by_id = {artifact.artifact_id: artifact for artifact in artifacts}
|
|
38
|
+
if len(by_id) != len(artifacts):
|
|
39
|
+
raise KnowledgeCompileGraphError(
|
|
40
|
+
"compile graph contains duplicate artifact IDs"
|
|
41
|
+
)
|
|
42
|
+
deployments = {artifact.deployment_id for artifact in artifacts}
|
|
43
|
+
if len(deployments) > 1:
|
|
44
|
+
raise KnowledgeCompileGraphError("compile graph crosses deployments")
|
|
45
|
+
model_scopes = [
|
|
46
|
+
artifact.scope_id
|
|
47
|
+
for artifact in artifacts
|
|
48
|
+
if artifact.artifact_kind == "model_page"
|
|
49
|
+
]
|
|
50
|
+
if len(model_scopes) != len(set(model_scopes)):
|
|
51
|
+
raise KnowledgeCompileGraphError(
|
|
52
|
+
"compile graph has multiple model pages for one scope"
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
depths: dict[UUID, int] = {}
|
|
56
|
+
visiting: set[UUID] = set()
|
|
57
|
+
|
|
58
|
+
def depth(*, artifact_id: UUID) -> int:
|
|
59
|
+
"""Resolve one node's root distance while detecting parent cycles."""
|
|
60
|
+
known = depths.get(artifact_id)
|
|
61
|
+
if known is not None:
|
|
62
|
+
return known
|
|
63
|
+
if artifact_id in visiting:
|
|
64
|
+
raise KnowledgeCompileGraphError("compile graph contains a parent cycle")
|
|
65
|
+
visiting.add(artifact_id)
|
|
66
|
+
parent_id = by_id[artifact_id].parent_artifact_id
|
|
67
|
+
value = (
|
|
68
|
+
0
|
|
69
|
+
if parent_id is None or parent_id not in by_id
|
|
70
|
+
else depth(artifact_id=parent_id) + 1
|
|
71
|
+
)
|
|
72
|
+
visiting.remove(artifact_id)
|
|
73
|
+
depths[artifact_id] = value
|
|
74
|
+
return value
|
|
75
|
+
|
|
76
|
+
for artifact_id in by_id:
|
|
77
|
+
depth(artifact_id=artifact_id)
|
|
78
|
+
|
|
79
|
+
selected = {artifact.artifact_id for artifact in artifacts if artifact.stale}
|
|
80
|
+
stale_model_scopes = {
|
|
81
|
+
artifact.scope_id
|
|
82
|
+
for artifact in artifacts
|
|
83
|
+
if artifact.stale and artifact.artifact_kind == "model_page"
|
|
84
|
+
}
|
|
85
|
+
selected.update(
|
|
86
|
+
artifact.artifact_id
|
|
87
|
+
for artifact in artifacts
|
|
88
|
+
if artifact.scope_id in stale_model_scopes
|
|
89
|
+
)
|
|
90
|
+
frontier = list(selected)
|
|
91
|
+
while frontier:
|
|
92
|
+
artifact_id = frontier.pop()
|
|
93
|
+
parent_id = by_id[artifact_id].parent_artifact_id
|
|
94
|
+
if parent_id is not None and parent_id in by_id and parent_id not in selected:
|
|
95
|
+
selected.add(parent_id)
|
|
96
|
+
frontier.append(parent_id)
|
|
97
|
+
|
|
98
|
+
scheduled = (artifact for artifact in artifacts if artifact.artifact_id in selected)
|
|
99
|
+
return tuple(
|
|
100
|
+
sorted(
|
|
101
|
+
scheduled,
|
|
102
|
+
key=lambda artifact: (
|
|
103
|
+
(
|
|
104
|
+
0
|
|
105
|
+
if artifact.artifact_kind == "model_page" and artifact.stale
|
|
106
|
+
else 2
|
|
107
|
+
if artifact.parent_artifact_id is None
|
|
108
|
+
and artifact.git_path == "_index.md"
|
|
109
|
+
else 1
|
|
110
|
+
),
|
|
111
|
+
-depths[artifact.artifact_id],
|
|
112
|
+
artifact.git_path,
|
|
113
|
+
str(artifact.artifact_id),
|
|
114
|
+
),
|
|
115
|
+
)
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def validate_knowledge_page_output(
|
|
120
|
+
*,
|
|
121
|
+
artifact: KnowledgeCompileArtifact,
|
|
122
|
+
output: KnowledgePageCompileOutput,
|
|
123
|
+
known_git_paths: Collection[str],
|
|
124
|
+
exclusions: Collection[KnowledgeEvidenceTarget],
|
|
125
|
+
) -> None:
|
|
126
|
+
"""Enforce artifact binding, content hash, exclusions, and internal links."""
|
|
127
|
+
compilation = output.compilation
|
|
128
|
+
if compilation.deployment_id != artifact.deployment_id:
|
|
129
|
+
raise KnowledgePageValidationError("compilation crosses deployments")
|
|
130
|
+
if compilation.artifact_id != artifact.artifact_id:
|
|
131
|
+
raise KnowledgePageValidationError("compilation targets a different artifact")
|
|
132
|
+
if compilation.content_hash != knowledge_content_hash(markdown=output.markdown):
|
|
133
|
+
raise KnowledgePageValidationError(
|
|
134
|
+
"compiled Markdown content hash does not match"
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
excluded = {_target_key(target=target) for target in exclusions}
|
|
138
|
+
used = {
|
|
139
|
+
_citation_target_key(citation=citation) for citation in compilation.citations
|
|
140
|
+
}
|
|
141
|
+
used.update(
|
|
142
|
+
target for target in excluded if f"{target[0]}:{target[1]}" in output.markdown
|
|
143
|
+
)
|
|
144
|
+
if overlap := excluded.intersection(used):
|
|
145
|
+
rendered = ", ".join(f"{kind}:{value}" for kind, value in sorted(overlap))
|
|
146
|
+
raise KnowledgePageValidationError(
|
|
147
|
+
f"compiled page uses excluded evidence: {rendered}"
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
unresolved = _unresolved_internal_links(
|
|
151
|
+
markdown=output.markdown,
|
|
152
|
+
source_path=artifact.git_path,
|
|
153
|
+
known_git_paths=known_git_paths,
|
|
154
|
+
)
|
|
155
|
+
if unresolved:
|
|
156
|
+
raise KnowledgePageValidationError(
|
|
157
|
+
f"compiled page has unresolved internal links: {', '.join(unresolved)}"
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def _target_key(*, target: KnowledgeEvidenceTarget) -> tuple[str, str]:
|
|
162
|
+
"""Return the role-independent identity of one exclusion target."""
|
|
163
|
+
if target.claim_lineage_id is not None:
|
|
164
|
+
return (
|
|
165
|
+
"claim_coordinate",
|
|
166
|
+
f"{target.claim_lineage_id}:{target.claim_chunk_content_hash}",
|
|
167
|
+
)
|
|
168
|
+
if target.relation_id is not None:
|
|
169
|
+
return "relation", str(target.relation_id)
|
|
170
|
+
if target.doc_id is not None:
|
|
171
|
+
return "doc", str(target.doc_id)
|
|
172
|
+
raise AssertionError("validated evidence target has no ID")
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def _citation_target_key(*, citation: KnowledgeCitation) -> tuple[str, str]:
|
|
176
|
+
"""Return the role-independent identity of one citation target."""
|
|
177
|
+
if citation.claim_lineage_id is not None:
|
|
178
|
+
return (
|
|
179
|
+
"claim_coordinate",
|
|
180
|
+
f"{citation.claim_lineage_id}:{citation.claim_chunk_content_hash}",
|
|
181
|
+
)
|
|
182
|
+
if citation.relation_id is not None:
|
|
183
|
+
return "relation", str(citation.relation_id)
|
|
184
|
+
if citation.doc_id is not None:
|
|
185
|
+
return "doc", str(citation.doc_id)
|
|
186
|
+
raise AssertionError("validated citation has no ID")
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def _unresolved_internal_links(
|
|
190
|
+
*, markdown: str, source_path: str, known_git_paths: Collection[str]
|
|
191
|
+
) -> tuple[str, ...]:
|
|
192
|
+
"""Return relative Markdown links that do not resolve to registered artifacts."""
|
|
193
|
+
known = set(known_git_paths)
|
|
194
|
+
unresolved: set[str] = set()
|
|
195
|
+
for block in MarkdownIt("commonmark").parse(markdown):
|
|
196
|
+
tokens = (block, *(block.children or ()))
|
|
197
|
+
for token in tokens:
|
|
198
|
+
if token.type != "link_open":
|
|
199
|
+
continue
|
|
200
|
+
href = token.attrGet("href")
|
|
201
|
+
if not isinstance(href, str):
|
|
202
|
+
continue
|
|
203
|
+
parsed = urlsplit(href)
|
|
204
|
+
if parsed.scheme or parsed.netloc or not parsed.path:
|
|
205
|
+
continue
|
|
206
|
+
raw_path = unquote(parsed.path)
|
|
207
|
+
if raw_path.startswith("/"):
|
|
208
|
+
resolved = posixpath.normpath(raw_path.lstrip("/"))
|
|
209
|
+
else:
|
|
210
|
+
resolved = posixpath.normpath(
|
|
211
|
+
posixpath.join(posixpath.dirname(source_path), raw_path)
|
|
212
|
+
)
|
|
213
|
+
if resolved == ".." or resolved.startswith("../") or resolved not in known:
|
|
214
|
+
unresolved.add(href)
|
|
215
|
+
return tuple(sorted(unresolved))
|