Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions contracts/notes/note.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@
"eventRef": { "type": "string", "description": "Calendar event if noteType=meeting_notes" },
"mailThreadRefs": { "type": "array", "items": { "type": "string" }, "default": [] },
"taskRefs": { "type": "array", "items": { "type": "string" }, "default": [] },
"noteRefs": { "type": "array", "items": { "type": "string" }, "default": [], "description": "Links to other notes (WikiLink-resolved backlink graph)" },
"workroomRef": { "type": "string" },
"driveFileRef": { "type": "string", "description": "If synced to Drive as a doc" },

Expand Down
1 change: 1 addition & 0 deletions contracts/tasks/task.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@

"createdAt": { "type": "string", "format": "date-time" },
"updatedAt": { "type": "string", "format": "date-time" },
"blockingTaskRefs": { "type": "array", "items": { "type": "string" }, "default": [], "description": "Task IDs that this task must be completed before" },
"policyRefs": { "type": "array", "items": { "type": "string" }, "default": [] },
"evidenceRefs": { "type": "array", "items": { "type": "string" }, "default": [] }
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://socioprophet.io/schemas/workspace/context-fabric/personal-context-graph.schema.json",
"title": "PersonalContextGraph",
"description": "The default graph structure for a person — the ego-scoped view of the CSKG. A ContextGraph anchored on the individual (selfRef) rather than a workroom: the seed the Memory Steward builds over. Nodes/edges are CSKG entities referenced by id (edges are CSKGEdge {node1,relation,node2,provenance_refs,source_evidence_refs}); sourceRefs are WorkspaceSource sourceIds (Layer-1 canonical objects) so every element is provenance-bound; external-KG links are reference-only and cross the membrane ONLY as ProviderProjection (nothing private egresses). Contract lives here (contract-first); the runtime that ingests WorkspaceSource objects and writes HellGraph lives in memory-mesh; deployment (managed HellGraph service) lives in prophet-platform.",
"type": "object",
"additionalProperties": false,
"required": ["schemaVersion", "personalContextGraphId", "selfRef", "subjectScope", "title", "status", "nodeRefs", "edgeRefs", "sourceRefs", "externalProjectionRefs", "policyRefs", "evidenceRefs", "receiptRefs"],
"properties": {
"schemaVersion": { "const": "v0.1" },
"personalContextGraphId": { "type": "string" },
"selfRef": { "type": "string", "description": "The Self anchor node — the individual this graph is centered on. Exactly one per graph (the single-Self invariant)." },
"subjectScope": { "const": "person", "description": "Person-scoped (ego graph), as opposed to a workroom-bound ContextGraph." },
"title": { "type": "string" },
"status": { "type": "string", "enum": ["draft", "active", "review", "sealed", "archived"] },
"nodeRefs": { "type": "array", "items": { "type": "string" }, "description": "CSKG node ids in this person-graph (typed by nodeTypeVocabulary; contacts carry csKgNodeRef here)." },
"edgeRefs": { "type": "array", "items": { "type": "string" }, "description": "CSKGEdge ids (relation typed by relationVocabulary)." },
"sourceRefs": { "type": "array", "items": { "type": "string", "pattern": "^workspace-source:" }, "description": "WorkspaceSource sourceIds — the Layer-1 canonical objects the nodes/edges were derived from. Retention/deletion of a source retracts its derived elements." },
"externalProjectionRefs": { "type": "array", "items": { "type": "string" }, "description": "ProviderProjection ids — the ONLY way external-KG links cross the membrane (reference-only; included/withheld governed by the projection). Never a data copy." },
"policyRefs": { "type": "array", "items": { "type": "string" } },
"evidenceRefs": { "type": "array", "items": { "type": "string" } },
"receiptRefs": { "type": "array", "items": { "type": "string" } },
"memoryScope": { "type": "string", "description": "The memory-scope this graph is governed under (e.g. relationship_context:approved). Owned by the memory-steward.", "default": "relationship_context:approved" },
"nodeTypeVocabulary": {
"type": "array",
"description": "The entity classes a person is built from (the seed vocabulary).",
"items": { "enum": ["Self", "Person", "Place", "Organization", "Thing", "Interest", "Event", "Document", "Communication", "Account"] }
},
"relationVocabulary": {
"type": "array",
"description": "The typed relations inside the person-graph (CSKGEdge.relation values).",
"items": { "enum": ["relatedTo", "knows", "homeTown", "residesIn", "attended", "worksAt", "memberOf", "providerFor", "owns", "uses", "interestedIn", "skilledIn", "participatedIn", "authored", "communicatedWith", "hasAccount"] }
},
"labels": { "type": "object", "additionalProperties": { "type": "string" } }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
{
"personalContextGraph": {
"schemaVersion": "v0.1",
"personalContextGraphId": "pkg-demo-0001",
"selfRef": "cskg-node://self/demo-user",
"subjectScope": "person",
"title": "Personal knowledge graph — demo user",
"status": "active",
"nodeRefs": [
"cskg-node://self/demo-user",
"cskg-node://person/mom",
"cskg-node://person/jamie",
"cskg-node://organization/acme-music",
"cskg-node://event/band-practice",
"cskg-node://document/setlist"
],
"edgeRefs": [
"cskg-edge://relatedTo/self-mom",
"cskg-edge://knows/self-jamie",
"cskg-edge://worksAt/jamie-acme",
"cskg-edge://participatedIn/self-practice",
"cskg-edge://communicatedWith/self-jamie",
"cskg-edge://authored/self-setlist"
],
"sourceRefs": [
"workspace-source:contacts/mom",
"workspace-source:contacts/jamie",
"workspace-source:calendar/band-practice",
"workspace-source:mail/jamie-setlist-thread",
"workspace-source:docs/setlist"
],
"externalProjectionRefs": ["projection-social-jamie-0001"],
"policyRefs": ["policy-decision://memory-scope/relationship-context/decision-0001"],
"evidenceRefs": ["evidence://personal-context-graph/demo-0001"],
"receiptRefs": ["receipt://personal-context-graph/demo-0001"],
"memoryScope": "relationship_context:approved",
"nodeTypeVocabulary": ["Self", "Person", "Organization", "Event", "Document"],
"relationVocabulary": ["relatedTo", "knows", "worksAt", "participatedIn", "communicatedWith", "authored"]
},
"externalProjection": {
"schemaVersion": "v0.1",
"projectionId": "projection-social-jamie-0001",
"workroomId": "person:demo-user",
"contextGraphId": "pkg-demo-0001",
"targetProvider": "social_network",
"purpose": "entity-resolution-reference-only",
"includedNodeRefs": ["cskg-node://person/jamie"],
"withheldRefs": [
{ "ref": "cskg-node://person/mom", "reason": "private-relationship; never egresses" }
],
"redactionRefs": [],
"policyRefs": ["policy-decision://memory-scope/relationship-context/decision-0001"],
"membraneDecisionRef": "membrane://personal-context-graph/projection-social-jamie-0001",
"evidenceRefs": ["evidence://personal-context-graph/projection/demo-0001"],
"receiptRefs": ["receipt://personal-context-graph/projection/demo-0001"]
}
}
66 changes: 66 additions & 0 deletions docs/personal-knowledge-graph.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# Personal Knowledge Graph (the ego-scoped CSKG)

The **default graph structure for a person** — the seed the Memory Steward
builds over. It is not a new store or a parallel vocabulary: it is the
**person-scoped view of the CSKG** (Context/Semantic Knowledge Graph) this
workspace already models. Where a `ContextGraph` is anchored on a **workroom**,
a `PersonalContextGraph` is anchored on the **individual** (`selfRef`).

Contract: [`context-fabric/personal-context-graph.schema.json`](../contracts/workspace/context-fabric/personal-context-graph.schema.json)
· example: [`personal-context-graph.v0.1.example.json`](../contracts/workspace/context-fabric/personal-context-graph.v0.1.example.json)
· validate: `python3 tools/validate_personal_context_graph.py`.

## What it is

- **Anchor.** Exactly one `Self` node (`selfRef`) — the single-Self invariant.
- **Nodes** are CSKG nodes (contacts carry their `csKgNodeRef` here). The entity
vocabulary a person is built from: `Self, Person, Place, Organization, Thing,
Interest, Event, Document, Communication, Account`.
- **Edges** are `CSKGEdge {node1, relation, node2, provenance_refs,
source_evidence_refs}`. The typed relations: `relatedTo, knows, homeTown,
residesIn, attended, worksAt, memberOf, providerFor, owns, uses, interestedIn,
skilledIn, participatedIn, authored, communicatedWith, hasAccount`.
- **Provenance.** `sourceRefs` are `WorkspaceSource` ids — the Layer-1 canonical
objects each element was derived from. This makes citations real and makes
retention actionable: when a `WorkspaceSource` is deleted per its
`retentionPolicyRef`, its derived nodes/edges retract.
- **External links are reference-only.** A person-graph node may correspond to an
entity in an external KG (general-purpose / social / domain / e-commerce), but
that link crosses the membrane **only** as a `ProviderProjection`
(`includedNodeRefs` + `withheldRefs` + `membraneDecisionRef`). The mesh
*resolves/reads* the external entity for grounding; nothing private egresses.
- **Scope.** Governed by a `memoryScope` (default `relationship_context:approved`),
owned by the **memory-steward** agent.

## Populating it: workspace records → CSKG

The runtime ingests canonical workspace objects and emits CSKG nodes/edges:

| WorkspaceSource | → node | → edge(s) |
|---|---|---|
| `contact` (person) | `Person` | `relatedTo` (family hint) / `knows`; `worksAt` if `organizationRef`; social `socialProfiles` → reference-only external projection |
| `contact` (organization) | `Organization` | — |
| `calendar-event` | `Event` | `participatedIn` (Self + attendees with `contactRef`) |
| `mail-message` | (correspondent `Person`) | `communicatedWith` |
| `office-artifact` | `Document` | `authored` |

Every emitted element carries `provenance_refs` = the originating
`WorkspaceSource` id + `source_evidence_refs`.

## Ownership split (contract-first)

This repo owns the **contract** only. Per the estate split:

- **prophet-workspace** (here) — the `PersonalContextGraph` contract + the
record→CSKG mapping. No runtime, no deployment.
- **memory-mesh** — the **runtime**: reads `WorkspaceSource` objects, resolves
`csKgNodeRef`, normalizes relations through the CSKG normalizer, and writes the
managed HellGraph via `HELLGRAPH_URL`. Recall-before-action /
writeback-after-action.
- **prophet-platform** — the **deployment**: HellGraph as a managed, durable
graph service (StatefulSet + RocksDB PVC), local flash via TopoLVM on the edge,
edge↔cloud-twin sync.
- **prophet-mesh** — the **consumer**: grounds retrieval on the person-graph.

The graph is a **Layer-2 derived store** — rebuildable from the Layer-1 canonical
objects, resident edge-first (privacy-high, per-person), never a source of truth.
68 changes: 68 additions & 0 deletions tools/validate_personal_context_graph.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
#!/usr/bin/env python3
"""Smoke validate the PersonalContextGraph (ego-scoped CSKG) contract.

Checks the schema loads, the example conforms to the key invariants (single Self
anchor, person scope, provenance-bound sources, reference-only external links via
ProviderProjection), and that cross-refs between the graph and its external
projection are consistent — mirroring validate_context_fabric.py.
"""

import json
import sys
from pathlib import Path

ROOT = Path(__file__).resolve().parents[1]
BASE = ROOT / "contracts/workspace/context-fabric"
SCHEMA = BASE / "personal-context-graph.schema.json"
PROJECTION_SCHEMA = BASE / "provider-projection.schema.json"
EXAMPLE = BASE / "personal-context-graph.v0.1.example.json"


def load(path):
return json.loads(path.read_text(encoding="utf-8"))


def main():
try:
schema = load(SCHEMA)
load(PROJECTION_SCHEMA)
example = load(EXAMPLE)
graph = example["personalContextGraph"]

# Required fields present.
for key in schema["required"]:
assert key in graph, f"missing required field {key!r}"

# Person-scoped, anchored on a single Self that is itself a node.
assert graph["subjectScope"] == "person"
assert graph["selfRef"] in graph["nodeRefs"], "selfRef must be a node in the graph"

# Every source is a WorkspaceSource (Layer-1 canonical object) → provenance-bound.
assert all(s.startswith("workspace-source:") for s in graph["sourceRefs"]), \
"sourceRefs must be workspace-source ids"

# Vocabulary conformance.
node_types = set(schema["properties"]["nodeTypeVocabulary"]["items"]["enum"])
relations = set(schema["properties"]["relationVocabulary"]["items"]["enum"])
assert set(graph["nodeTypeVocabulary"]) <= node_types, "unknown node type in vocabulary"
assert set(graph["relationVocabulary"]) <= relations, "unknown relation in vocabulary"

# External links cross the membrane ONLY as a ProviderProjection, and the
# graph's externalProjectionRefs match the projection id.
projection = example["externalProjection"]
assert projection["projectionId"] in graph["externalProjectionRefs"]
assert projection["contextGraphId"] == graph["personalContextGraphId"]
# The membrane withholds at least the private relationship (nothing private egresses).
assert projection["membraneDecisionRef"], "external projection needs a membrane decision"
withheld = {w["ref"] for w in projection["withheldRefs"]}
included = set(projection["includedNodeRefs"])
assert included.isdisjoint(withheld), "a node cannot be both included and withheld"
except Exception as exc: # noqa: BLE001
print(f"ERR: {exc}", file=sys.stderr)
return 2
print("OK: PersonalContextGraph smoke validation passed")
return 0


if __name__ == "__main__":
raise SystemExit(main())
Loading