From 3b224841157a4f66389fed21757dceb838409a96 Mon Sep 17 00:00:00 2001 From: Michael Heller <21163552+mdheller@users.noreply.github.com> Date: Tue, 30 Jun 2026 00:57:08 -0400 Subject: [PATCH 1/3] =?UTF-8?q?wip(snapshot):=20note.schema.json=20update?= =?UTF-8?q?=20=E2=80=94=20preserve=20at-risk=20work?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- contracts/notes/note.schema.json | 1 + 1 file changed, 1 insertion(+) diff --git a/contracts/notes/note.schema.json b/contracts/notes/note.schema.json index 18b7ae1..f09d696 100644 --- a/contracts/notes/note.schema.json +++ b/contracts/notes/note.schema.json @@ -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" }, From 4fd068e5df54bcd83914659ccf80b4f7d1f5b712 Mon Sep 17 00:00:00 2001 From: Michael Heller <21163552+mdheller@users.noreply.github.com> Date: Tue, 30 Jun 2026 02:02:23 -0400 Subject: [PATCH 2/3] add blockingTaskRefs to task schema (goose-notes wave 48) --- contracts/tasks/task.schema.json | 1 + 1 file changed, 1 insertion(+) diff --git a/contracts/tasks/task.schema.json b/contracts/tasks/task.schema.json index c301db0..1413b3d 100644 --- a/contracts/tasks/task.schema.json +++ b/contracts/tasks/task.schema.json @@ -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": [] } } From de9cd66ddc13b9b74dc3262e1a45f421fd175309 Mon Sep 17 00:00:00 2001 From: Michael Heller <21163552+mdheller@users.noreply.github.com> Date: Wed, 1 Jul 2026 15:10:38 -0400 Subject: [PATCH 3/3] =?UTF-8?q?feat(context-fabric):=20PersonalContextGrap?= =?UTF-8?q?h=20=E2=80=94=20the=20ego-scoped=20CSKG=20(person-graph=20contr?= =?UTF-8?q?act)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Step 3 of the person-graph substrate plan: re-home the default person-graph as a first-class workspace contract, conforming to the existing CSKG rather than a parallel vocabulary. PersonalContextGraph = the person-scoped view of the CSKG. Where a ContextGraph is workroom-bound, this is anchored on the individual (selfRef, single-Self invariant). Nodes are CSKG nodes (contacts carry csKgNodeRef); edges are CSKGEdge {node1,relation,node2,provenance_refs,source_evidence_refs}. sourceRefs are WorkspaceSource ids (Layer-1 canonical objects) so every element is provenance-bound and retention-actionable. External-KG links cross the membrane ONLY as a ProviderProjection (reference-only; included/withheld) — nothing private egresses. Carries the seed node-type + relation vocabulary as canonical enums; governed by memoryScope, owned by the memory-steward. - personal-context-graph.schema.json + .v0.1.example.json - tools/validate_personal_context_graph.py (smoke: single-Self, person scope, workspace-source provenance, vocabulary, membrane include/withhold) — passes - docs/personal-knowledge-graph.md: design + record→CSKG mapping + the 3-repo split (contract=workspace / runtime=memory-mesh / deploy=platform / consume=mesh) Contract-only, as this repo requires; runtime + deployment land in memory-mesh + prophet-platform (PR #675 already made HellGraph a durable managed service). --- .../personal-context-graph.schema.json | 36 ++++++++++ .../personal-context-graph.v0.1.example.json | 57 ++++++++++++++++ docs/personal-knowledge-graph.md | 66 ++++++++++++++++++ tools/validate_personal_context_graph.py | 68 +++++++++++++++++++ 4 files changed, 227 insertions(+) create mode 100644 contracts/workspace/context-fabric/personal-context-graph.schema.json create mode 100644 contracts/workspace/context-fabric/personal-context-graph.v0.1.example.json create mode 100644 docs/personal-knowledge-graph.md create mode 100644 tools/validate_personal_context_graph.py diff --git a/contracts/workspace/context-fabric/personal-context-graph.schema.json b/contracts/workspace/context-fabric/personal-context-graph.schema.json new file mode 100644 index 0000000..df59863 --- /dev/null +++ b/contracts/workspace/context-fabric/personal-context-graph.schema.json @@ -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" } } + } +} diff --git a/contracts/workspace/context-fabric/personal-context-graph.v0.1.example.json b/contracts/workspace/context-fabric/personal-context-graph.v0.1.example.json new file mode 100644 index 0000000..0dcbaba --- /dev/null +++ b/contracts/workspace/context-fabric/personal-context-graph.v0.1.example.json @@ -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"] + } +} diff --git a/docs/personal-knowledge-graph.md b/docs/personal-knowledge-graph.md new file mode 100644 index 0000000..661696f --- /dev/null +++ b/docs/personal-knowledge-graph.md @@ -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. diff --git a/tools/validate_personal_context_graph.py b/tools/validate_personal_context_graph.py new file mode 100644 index 0000000..6c8d690 --- /dev/null +++ b/tools/validate_personal_context_graph.py @@ -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())