Orchestrata Ultimate › Plans › hub-link-plan
Hub-link plan — restoring the root-connectivity invariant in orchkg
Source: plans/hub-link-plan.md at 8a2cb42ad · last changed 2026-07-27 · first committed 2026-07-27 · 823 lines.
Prose pending.
Plan text
Hub-link plan — restoring the root-connectivity invariant in orchkg
Status: DESIGN (analyst). No production code changed by this document. Branch:
feat/hub-link. Measured against the LIVE operational KG (neo4j://10.92.53.160:7691, databaseorchkg, auth none) on 2026-07-27.
1. The invariant and the breach
Session-254 ruling: every node in the operational KG must be reachable from the
root (:Project) hub. scripts/check_kg_connectivity.py runs an
engine-portable BFS from all :Project seeds over undirected edges and exits 1
when anything is unreachable.
Measured now:
TOTAL 64913 REACHABLE 56466 UNREACHABLE 8447
| Label | Unreachable |
|---|---|
| Parameter | 5044 |
| CXPPacket | 2381 |
| Intent | 187 |
| PipelineRun | 187 |
| Message | 153 |
| GatewayAuditEvent | 144 |
| Requirement | 139 |
| ToolCall | 74 |
| Session | 54 |
| UserStory | 39 |
| Fact | 22 |
| Action | 8 |
| Spec | 6 |
| Timeout | 4 |
| Resolution | 3 |
| Artifact | 2 |
The count grew from 758 to 8447 within a session, so writers are actively minting orphans — this is not purely historical residue.
2. Anchoring topology (what "reachable" means per tier)
Diagram source shown as text: Mermaid could not parse it (Error: Parse error on line 4:).
graph LR
P[":Project (slug)"]
P -->|HAS_THREAD| T[":Thread"]
T -->|IN_THREAD (in)| CX[":CXPPacket"]
T -->|IN_THREAD (in)| MSG[":Message"]
T -->|IN_THREAD (in)| TC[":ToolCall"]
T -->|IN_THREAD (in)| ART[":Artifact"]
T -->|IN_THREAD (in)| INT[":Intent"]
T -->|IN_THREAD (in)| PR[":PipelineRun"]
P -->|HAS_SESSION| S[":Session"]
S -->|DURING (in)| GAE[":GatewayAuditEvent"]
S -->|IN_SESSION / ABOUT (in)| EV[":Event :PolicyDecision :Timeout :Resolution"]
P -->|CONTAINS| PKG[":Package"] --> MOD[":Module"]
MOD -->|DEFINES| FM[":Function / :Method"]
FM -->|HAS_PARAMETER| PARAM[":Parameter"]
P -->|HAS_SPEC| SP[":Spec"]
SP -->|HAS_STORY| US[":UserStory"]
US -->|HAS_REQUIREMENT| REQ[":Requirement"]
Three anchoring families cover every unreachable label:
- Thread family —
(:Project)-[:HAS_THREAD]->(:Thread)<-[:IN_THREAD]-(record). Already live and load-bearing: 7185 Threads, 18326IN_THREADCXPPacket edges. Prior art:scripts/backfill_thread_links.py. - Session family —
(:Project)-[:HAS_SESSION]->(:Session). Already live: 1098 of 1152 Sessions anchored. - Code-intelligence / spec family —
CONTAINS/DEFINES/HAS_PARAMETERandHAS_SPEC/HAS_STORY/HAS_REQUIREMENT.
No new anchoring concept is invented by this plan. Every verdict below reuses an edge that already exists in the live graph with a majority of its label already anchored that way.
3. Schema additions required first (RUL-016)
RUL-016 (~/workspace/knowledge-wiki/rulings/RUL-016-ontology-before-graph-import.md):
the canonical ontology must define an anchoring path for a label before
anything links it.
Already defined — no addition needed (src/orchestrata_ultimate/knowledge_graph/canonical_schema.py):
| Constant | Line | Declares |
|---|---|---|
LABEL_THREAD |
176 | "Thread" — Tier-H correlation anchor |
REL_IN_THREAD |
248 | (signal)-[:IN_THREAD]->(Thread|Session) |
REL_HAS_THREAD |
260 | (:Project)-[:HAS_THREAD]->(:Thread) |
REL_HAS_SESSION |
277 | (:Project)-[:HAS_SESSION]->(:Session) |
thread_entity_id(project, thread_id) |
850 | canonical Thread id builder |
Missing — MUST be added in commit #1. Each of these four edges is already written by production code as a bare string literal with no canonical constant, so linking against them today would violate RUL-016:
| New constant | Value | Bare-literal writers today |
|---|---|---|
REL_HAS_PARAMETER |
"HAS_PARAMETER" |
knowledge_graph/sync.py (16 sites: L32, 371, 399, 429, 439, 448, 464, 470, 475, 511, 530, 728, 770, 787, 791, 803, 1416) |
REL_HAS_SPEC |
"HAS_SPEC" |
session/kg_sync/_stage_specs_traces.py:180,188; knowledge_graph/kg_design.py:283 |
REL_HAS_STORY |
"HAS_STORY" |
knowledge_graph/kg_design.py:340; semantic_graph/spec_trace_enricher.py:175-176 |
REL_HAS_REQUIREMENT |
"HAS_REQUIREMENT" |
knowledge_graph/kg_design.py:391; semantic_graph/spec_trace_enricher.py:206-207 |
Note HAS_SPEC exists in the other, non-canonical knowledge_graph/schema.py:99
and types_enums.py:96 — that is not canonical_schema.py and does not satisfy
RUL-016.
Answer to "schema additions needed?" — YES, four REL_* constants, one commit,
before any writer or repair change. No new LABEL is needed: every unreachable
label already has a LABEL_* constant and a CANONICAL_MERGE_KEYS entry.
4. Per-label verdicts
Reconciliation: 8409 LINK + 38 DELETE = 8447.
| Label | n | Verdict | Hub / edge | Keyed on |
|---|---|---|---|---|
| Parameter | 5044 | LINK | (:Function|:Method)-[:HAS_PARAMETER]->(p) |
p.parent_id == owner.id |
| CXPPacket | 2381 | LINK | (p)-[:IN_THREAD]->(:Thread) |
thread_id |
| Intent | 187 | LINK | (i)-[:IN_THREAD]->(:Thread) |
thread_id |
| PipelineRun | 187 | LINK | (r)-[:IN_THREAD]->(:Thread) |
thread_id |
| Message | 153 | LINK | (m)-[:IN_THREAD]->(:Thread) |
thread_id |
| GatewayAuditEvent | 144 | LINK (transitive) | via its :Session |
none — fixed by Session |
| Requirement | 138 | LINK (transitive) | via :Spec hub mint |
none — fixed by Spec |
| Requirement | 1 | DELETE | husk {id:"REQ-008-023"}, degree 0 |
— |
| ToolCall | 74 | LINK | (tc)-[:IN_THREAD]->(:Thread) |
thread_id |
| Session | 54 | LINK | (:Project)-[:HAS_SESSION]->(s) |
slug |
| UserStory | 39 | LINK (transitive) | via :Spec hub mint |
none — fixed by Spec |
| Fact | 22 | DELETE | test residue, source='test' |
— |
| Action | 8 | DELETE | test residue, agent IN ('pytest','') |
— |
| Spec | 6 | LINK | (:Project{slug})-[:HAS_SPEC]->(s) |
s.project_slug |
| Timeout | 4 | DELETE | test residue, _TEST_ escalation ids |
— |
| Resolution | 3 | DELETE | test residue, conflict_id='c-resolve-neo4j' |
— |
4.1 Parameter — 5044 — LINK
Writer: src/orchestrata_ultimate/knowledge_graph/sync.py.
_sync_parameters (L767) MERGEs the node with no edge; the edge is a
separate second pass _sync_parameter_edges (L786):
UNWIND $rows AS r
MATCH (owner:Function|Method {id: r.parent_id})
MATCH (param:Parameter {id: r.id})
MERGE (owner)-[:HAS_PARAMETER]->(param)
RETURN count(*) AS cnt
Schema: LABEL_PARAMETER at L100; CANONICAL_MERGE_KEYS[Parameter] = "id" at
L484. REL_HAS_PARAMETER does not exist — add it (§3).
Evidence — the anchor key resolves 100%:
Q: MATCH (p:Parameter) WHERE NOT ()-[:HAS_PARAMETER]->(p) RETURN count(p) AS edgeless
{"edgeless": 5044}
Q: MATCH (p:Parameter) WHERE NOT ()-[:HAS_PARAMETER]->(p) WITH p LIMIT 1500
OPTIONAL MATCH (o) WHERE o.id = p.parent_id
RETURN labels(o) AS parent_labels, count(*) AS c ORDER BY c DESC
{"parent_labels": ["Method"], "c": 946}
{"parent_labels": ["Function"], "c": 554} <- 1500/1500 resolved, zero nulls
Q: MATCH (p:Parameter) WHERE NOT ()-[:HAS_PARAMETER]->(p) WITH p LIMIT 4000
RETURN p.namespace AS ns, count(*) AS c
{"ns": null, "c": 4000} <- trunk generation, not ns-scoped
Orphan sample (5 nodes, properties):
{"id": "orchestrata-ultimate:agents.communication.CommunicationError.__init__:param:1",
"name": "sender", "parent_id": "orchestrata-ultimate:agents.communication.CommunicationError.__init__",
"type": "", "default": "''"}
{"id": "...CommunicationError.__init__:param:2", "name": "receiver", ...}
{"id": "orchestrata-ultimate:agents.communication.AgentCommunication.__init__:param:1",
"name": "prolog_bridge", "parent_id": "orchestrata-ultimate:agents.communication.AgentCommunication.__init__", ...}
The parent_id for the first sample resolves to a real :Method:
Q: MATCH (n) WHERE n.id = 'orchestrata-ultimate:agents.communication.CommunicationError.__init__'
RETURN labels(n) AS labels
{"labels": ["Method"]}
Verdict: LINK. Repair MATCHes owner.id = param.parent_id and MERGEs
HAS_PARAMETER. Zero ambiguity, zero data invented.
Write-time fix: the edge pass already exists, so the code shape is right —
but 5044 nodes with resolvable parents and no edge proves the pass silently
under-delivered on the last sync (_sync_parameter_edges returns a count that
nobody asserts against len(rows)). Fix = make the pass batched and
self-verifying: chunk rows, sum the returned counts, and raise if
edges_created < len(rows). Do not swallow. Rationale for batching: the live
Memgraph aborts long single-transaction writes under storage.access_timeout,
which would produce exactly this signature.
Ruled out by measurement, so the builder should not chase it: Memgraph does accept the
MATCH (owner:Function|Method {...})label-disjunction syntax — verified live, returnscnt: 1for a known parent id.
4.2 CXPPacket — 2381 — LINK
Writer: src/orchestrata_ultimate/knowledge_graph/cxp_auditor.py,
SpikeAuditor.log_packet (def L114), write L130–153:
MERGE (p:CXPPacket {message_id: $message_id})
SET p.thread_id = $thread_id, p.from_agent = $from_agent, p.to_agent = $to_agent,
p.subject = $subject, p.payload = $payload, p.timestamp = $timestamp,
p.priority = $priority, p.delivery_status = $delivery_status,
p.work_status = $work_status
Creates no relationship. The only edge in the file is log_routing (L166–176)
which is CXPPacket -> CXPPacket (ROUTED_TO) — packets chain to each other and
the whole chain floats. Callers: postman/sync_router.py:82,127; auditor built at
postman/postman.py:132-135.
Schema: REL_IN_THREAD (L248) + REL_HAS_THREAD (L260) + LABEL_THREAD
(L176) + thread_entity_id() (L850) — all present, no addition needed.
Evidence:
Q: MATCH (c:CXPPacket) RETURN count(c) AS total
{"total": 21961} <- 19580 already reachable via IN_THREAD
Q: MATCH (t:Thread)-[r]-(m) RETURN type(r) AS t, labels(m) AS other, count(*) AS c ORDER BY c DESC
{"t":"IN_THREAD","other":["CXPPacket"],"c":18326}
{"t":"HAS_THREAD","other":["Project"],"c":7185}
Q: MATCH (n:CXPPacket) WHERE NOT (n)-[:IN_THREAD]->() WITH n LIMIT 3000
OPTIONAL MATCH (t:Thread {thread_id: n.thread_id})
RETURN count(n) AS unlinked, count(n.thread_id) AS has_tid, count(t) AS thread_exists
{"unlinked": 3000, "has_tid": 3000, "thread_exists": 61}
Orphan degree histogram: {0: 43, 1: 298, 2: 1659} — neighbour rel types are
ROUTED_TO -> CXPPacket only. Orphan sample:
{"id":"legacy-000000000413","message_id":"22d51168-0a32-5581-ab3e-02765a7ae3dd",
"thread_id":"eb041231-d6d4-4a26-99c0-be93915f2893","from_agent":"HUMAN","to_agent":"U:Cx",
"subject":"intent","payload":"{\"text\": \"test intent\"}","delivery_status":"queued"}
{"id":"legacy-000000000416","thread_id":"c156665c-d44e-411a-b9fd-35b3552275dc","subject":"intent", ...}
{"id":"legacy-000000000417","thread_id":"c156665c-...","from_agent":"U:Cx","to_agent":"HUMAN","subject":"clarification", ...}
Every orphan carries a thread_id (3000/3000) but its :Thread mostly does not
exist yet (61/3000). So the repair must MERGE the Thread and its
HAS_THREAD hub edge, not merely MATCH it — which is exactly what
backfill_thread_links.run_backfill already does (L324–331 then L338–343).
Verdict: LINK via thread_id.
Write-time fix: inline the Thread mint + anchor into log_packet, using
cs.thread_entity_id(project, thread_id):
MERGE (p:CXPPacket {message_id: $message_id})
SET ...
WITH p
MERGE (t:Thread {id: $thread_qid})
ON CREATE SET t.thread_id = $thread_id, t.project = $project
MERGE (p)-[:IN_THREAD]->(t)
WITH t
MERGE (proj:Project {slug: $project})
MERGE (proj)-[:HAS_THREAD]->(t)
$project comes from a call-time os.environ.get("ORCHESTRATA_PROJECT_ID", "orchestrata-ultimate") helper — the identical idiom already at
gateway/audit.py:43-45, memory/cold_store.py:49, factory/promotion/store.py:134
(RUL-003: read env at call time, never at import).
4.3 Message — 153 — LINK
Writer: src/orchestrata_ultimate/knowledge_graph/runtime_writer.py,
RuntimeSignalWriter.write_message (def L61), write L97–123:
MERGE (m:Message {message_id: $message_id})
SET m.id = $id, m.thread_id = $thread_id, m.sender = $sender, m.recipient = $recipient,
m.subject = $subject, m.summary = $summary, m.timestamp = datetime($timestamp),
m.project_id = $project_id
WITH m
UNWIND $mentioned_files AS path
MATCH (f:File {path: path})
MERGE (m)-[:MENTIONS]->(f)
project_id is written as a scalar property, never an edge; the MENTIONS
edge is a no-op when mentioned_files is empty (UNWIND [] kills the row).
Callers: kernel/pipeline_kg.py:732, reached from
orchestrator/pipeline_runner.py:874,974 and kernel/pipeline_escalation.py:295,322.
Schema: present (Thread family). Evidence: all 153 unlinked carry
thread_id; only 8 have an existing :Thread. Degree histogram {0: 153} —
every orphan Message is fully isolated. Sample:
{"id":"msg:04562e85-7007-4f78-8a8f-06c5483b6340:in:44136fa355b3678a",
"sender":"U:Cx","recipient":"U:PM","subject":"chain",
"thread_id":"04562e85-7007-4f78-8a8f-06c5483b6340"}
Verdict: LINK via thread_id. Write-time fix: same Thread mint+anchor
block as §4.2, plus turn the dead project_id scalar into the $project used by
the hub MERGE.
4.4 ToolCall — 74 — LINK
Three writers. Only one is correct today:
knowledge_graph/runtime_writer.py::write_tool_call(def L227, write L244–270) —MERGE (tc:ToolCall {id: $id}) SET ... tc.project_id = $project_idthen conditionalMENTIONS. No hub edge. Callerskernel/pipeline_kg.py:769andagents/volks/base.py:460(the latter does not even passproject_id). This is the source of the 74 orphans.knowledge_graph/conversation_consumer.py::TurnGraphMapper.to_statements(L376–384) —MERGE (tc:ToolCall {id:$tc_id}) ... MERGE (t:ConversationTurn)-[:TRIGGERED]->(tc), and the same method MERGEs(:Project{slug})-[:HAS_CONVERSATION]->(c)-[:HAS_TURN]->(t)at L320–343. Already hub-reachable — this is the in-repo template to copy.orchestrator/graph_audit.py::_write_event(L328) via_EVENT_LABELS— see §4.11.
Schema: present. Evidence: 74 unlinked, all carry thread_id,
0 have an existing :Thread. Degree histogram {0: 74}. Sample:
{"id":"call_019f9e3746fe71139ce981ed","tool_name":"list_dir","outcome":"error",
"thread_id":"pipeline_4075156f","arguments_json":"{\"path\": \".\"}"}
These are real operational records of a real pipeline run, not test fixtures.
Verdict: LINK via thread_id (repair MERGEs the Thread).
4.5 GatewayAuditEvent — 144 — LINK (transitive, no direct action)
Writer: src/orchestrata_ultimate/gateway/audit.py, _CREATE_AUDIT_QUERY
L56–69, executed by AuditWriter._write_event (def L176):
CREATE (e:GatewayAuditEvent {entry_id: $entry_id, event_type: $event_type,
connection_id: $connection_id, timestamp: $timestamp, data: $data})
WITH e
MERGE (s:Session {session_id: $connection_id})
MERGE (e)-[:DURING]->(s)
MERGE (p:Project {slug: $project})
MERGE (p)-[:HAS_SESSION]->(s)
This writer is already correct — it mints the Session and the Project hub edge. No write-time fix is needed here.
Evidence: all 144 orphans have degree exactly 1, and their only neighbour is
DURING -> :Session:
Q: (orphan GatewayAuditEvent) neighbour rel types
[{"t":"DURING","other":["Session"],"c":144}]
They are orphaned solely because their :Session lacks HAS_SESSION — i.e. they
predate the fix quoted above. Fixing Session (§4.6) makes all 144 reachable
transitively. The repair script must therefore not add a direct
GatewayAuditEvent→Project edge; that would be a redundant, non-canonical edge.
Distinct label, do not conflate:
runtime_writer.write_gateway_event(L126, write L143–167) writes:GatewayEvent(canonical,canonical_schema.py:165), not:GatewayAuditEvent. It has the same missing-hub defect and is in scope for the messaging brief.
4.6 Session — 54 — LINK
Ten writers. Five already anchor correctly and are the template:
| Anchored writer | Site |
|---|---|
session/kg_sync/_stage_session.py::sync_session |
L108–116 |
knowledge_graph/kg_design.py::store_session |
L793–800 |
kernel/session_manager.py::archive_session |
L473–482 |
gateway/audit.py _CREATE_AUDIT_QUERY |
L56–69 |
memory/cold_store.py::ColdStore.store |
L172–195 |
Five mint a :Session with no path to :Project — these are the defect:
| Unanchored writer | Site | Shape |
|---|---|---|
gateway/handlers/session.py::handle_open |
L332–343 | MERGE (s:Session {...}) SET ... — no relationship at all |
session/graph_store.py::persist_session |
L97–100 | MERGE (s:Session {session_id:$id}) SET ... — no edge |
orchestrator/fact_graph.py::build_create_query |
L75–76 | MERGE (s:Session {session_id:$sid}) MERGE (f)-[:ABOUT]->(s) — inbound only |
orchestrator/graph_audit.py::_write_event |
L355–360 | MERGE (s:Session {session_id:$sid}) MERGE (n)-[:IN_SESSION]->(s) — inbound only |
knowledge_graph/conversation_consumer.py::to_statements |
L345–348 | reaches Project only via :Conversation; $project is already in scope two statements earlier (L320) |
kernel/audit/writers.py L331–355 is the counter-example worth reading: it was
deliberately converted from MERGE to OPTIONAL MATCH (s:Session {...})
(L351) specifically to stop minting orphan Sessions. Its L306–325 comment block
is the clearest in-repo statement of this rule.
Schema: REL_HAS_SESSION at L277 — present. Its own docstring names this
exact defect ("gateway/audit.py, kernel/session_manager.py, and memory/cold_store.py
each independently MERGE a :Session node with no Project hub edge at all").
Evidence — orphan Sessions are normally-shaped, not garbage:
Q: MATCH (s:Session) RETURN keys(s) AS k, count(*) AS c ORDER BY c DESC
{"k":["session_id"], "c":1117} <- the dominant shape
{"k":["id","session_id","status","started_at","summary","goals","closed_at","portfolio_id"], "c":24}
{"k":["session_id","project","seq"], "c":6}
Q: MATCH (p:Project)-[r]-(m) RETURN type(r) AS t, labels(m) AS other, count(*) AS c
{"t":"HAS_SESSION","other":["Session"],"c":1098} <- 1098 of 1152 anchored
Orphan sample (5 nodes) and their degree/neighbour profile:
{"session_id":"01d29088-abd2-4f8f-95f0-4cc3f9ff3032"}
{"session_id":"0d5bb77b-0c3f-4f3c-9a98-64c4498ee0f4"}
{"session_id":"132fa890-e09d-4b18-801d-a6edd0908c67"}
degree histogram: {3: 52, 5: 2}
neighbour rel types: [{"t":"DURING","other":["GatewayAuditEvent"],"c":144},
{"t":"ABOUT","other":["Fact"],"c":22}]
The orphans have the same bare {session_id} shape as the 1098 that ARE
anchored — so this is a missing hub edge, not junk. They carry no test_id
property (MATCH (n) WHERE n.test_id IS NOT NULL returns zero rows graph-wide).
Verdict: LINK — MERGE (:Project {slug:$project})-[:HAS_SESSION]->(s).
This transitively rescues the 144 GatewayAuditEvents (§4.5).
Write-time fix: add the HAS_SESSION hub MERGE to all five unanchored
writers, using the call-time _project_slug() idiom.
4.7 Intent — 187 — LINK · 4.8 PipelineRun — 187 — LINK
Writer: src/orchestrata_ultimate/kernel/pipeline_kg.py::sync_intent_to_kg
(def L438), write L454–462:
MERGE (i:Intent {id: $intent_id})
SET i.text = $text, i.thread_id = $thread_id,
i.created_at = coalesce(i.created_at, timestamp())
MERGE (r:PipelineRun {id: $run_id})
SET r.thread_id = $thread_id, r.started_at = coalesce(r.started_at, timestamp())
MERGE (i)-[:TRIGGERED]->(r)
The pair links to each other and to nothing else. Second PipelineRun writer:
sync_artifacts_to_kg L400–412, which reaches :Volk only when
row.alias <> '' (i.e. only when stage is a registered volk alias).
Schema: LABEL_INTENT L85, LABEL_PIPELINE_RUN L86, REL_IN_THREAD L248 —
present. TRIGGERED is deliberately not re-declared (canonical_schema L302–304).
Evidence: 187 Intents and 187 PipelineRuns unlinked, all carry thread_id,
only 2 have an existing :Thread. Degree histogram {1: 187} for both; the sole
neighbour is each other. Sample:
{"id":"intent:02009b64-df29-4bfc-b058-3beb02daff5a","text":"",
"thread_id":"02009b64-df29-4bfc-b058-3beb02daff5a","created_at":"1785070347350"}
{"id":"run:02009b64-df29-4bfc-b058-3beb02daff5a","thread_id":"02009b64-...","started_at":"1785070347350"}
Verdict: LINK both via thread_id → :Thread. Write-time fix: append the
Thread mint + HAS_THREAD + IN_THREAD block to sync_intent_to_kg.
resolve_project_id is already imported at pipeline_kg.py:36 and
self._project_id is already computed (used at L358) — and L264–269 in the same
file (sync_volks_to_kg) is the working hub-MERGE template.
4.9 Artifact — 2 — LINK
Writers: kernel/pipeline_kg.py::sync_artifacts_to_kg L400–412 (edge is
inside a FOREACH guarded on row.alias <> '', so a non-volk stage mints an
edgeless Artifact) and knowledge_graph/architecture_writer.py::upsert_artifact
L1055/L1077 (via _merge_node, no edge at all — the one Tier-J writer in that
file that skips the hub, cf. seed_reference_data L1146–1170 which anchors).
Evidence: degree {0: 2}; both carry thread_id:
{"id":"pipeline_26aace4d:U:Qa:test_code:8864a166b0cf2785","type":"test_code",
"stage":"U:Qa","thread_id":"pipeline_26aace4d","format":"text","file_path":""}
{"id":"pipeline_5a714ff1:U:Qa:test_code:c762861c1da0c2eb","thread_id":"pipeline_5a714ff1", ...}
82 IN_THREAD Artifact edges already exist, so the anchoring path is established.
Verdict: LINK via thread_id.
4.10 Spec 6 / UserStory 39 / Requirement 138 — LINK (one root cause)
These 183 nodes form 6–7 self-consistent islands: each is a complete
Spec -[:HAS_STORY]-> UserStory -[:HAS_REQUIREMENT]-> Requirement tree whose
:Project hub does not exist.
Writer: src/orchestrata_ultimate/knowledge_graph/kg_design.py::store_spec
(def L250), write L275–283:
MERGE (s:Spec {id: $id})
SET s.spec_id = $spec_id, s.project_slug = $slug, s.title = $title,
s.content = $content, s.updated_at = $ts
WITH s
MATCH (p:Project {slug: $slug}) <-- MATCH, not MERGE: fails OPEN
MERGE (p)-[:HAS_SPEC]->(s)
That MATCH is the bug. When no :Project exists for the slug the match
yields no row, the MERGE never runs, and the Spec is minted unanchored — while
store_user_stories (L330–340) and store_requirements (L383–391) happily hang a
whole tree off it. Every other hub writer in the repo MERGEs the Project
(cold_store.py:193, session_manager.py:481, gateway/audit.py:68).
Evidence — the hub is missing for every generated slug:
Q: MATCH (p:Project) RETURN p.slug AS slug -> exactly ONE row: {"slug":"orchestrata-ultimate"}
Q: MATCH (s:Spec) OPTIONAL MATCH (p:Project {slug: s.project_slug}) RETURN s.project_slug AS slug, count(p) AS proj
{"slug":"reusable--thread-safe--generic-lru-cache-with-ttl","proj":0}
{"slug":null,"proj":0}
{"slug":"implement-as-specified","proj":0}
{"slug":"build-stdlib-only-python-3-14-free-threaded-in-mem","proj":0}
{"slug":"implement-a-thread-safe-generic-lru-cache-with-fix","proj":0}
{"slug":"build-a-thread-safe-generic-lru-cache-module-for-p","proj":0}
{"slug":"build-a-thread-safe--free-threaded-safe-generic-lr","proj":0}
Q: MATCH (r:Requirement) RETURN split(r.id,":")[0] AS root, count(*) AS c ORDER BY c DESC
{"root":"orchestrata-ultimate","c":559} <- reachable
{"root":"build-stdlib-only-python-3-14-free-threaded-in-mem","c":38}
{"root":"build-a-thread-safe--free-threaded-safe-generic-lr","c":24}
{"root":"reusable--thread-safe--generic-lru-cache-with-ttl","c":23}
{"root":"implement-a-thread-safe-generic-lru-cache-with-fix","c":23}
{"root":"implement-as-specified","c":16}
{"root":"build-a-thread-safe-generic-lru-cache-module-for-p","c":14}
sum = 138 <- exactly the orphan count
Spec orphan sample:
{"id":"build-a-thread-safe--free-threaded-safe-generic-lr:001","spec_id":"001",
"project_slug":"build-a-thread-safe--free-threaded-safe-generic-lr",
"title":"Thread-safe, free-threaded-safe generic LRUCache[T] module","content":""}
These are genuine dogfood pipeline output (real user stories with acceptance
criteria), not test fixtures. The multi-project invariant
(plans/kg-project-scoped-identity.md, "the KG must hold 100+ projects") says a
generated project is entitled to its own hub.
Verdict: LINK — MERGE one :Project {slug: s.project_slug} per distinct slug
and MERGE (p)-[:HAS_SPEC]->(s). UserStory and Requirement then become reachable
transitively; no direct edge is added for them. See DECISION D1 for the
slug: null / "implement-as-specified" edge cases.
Write-time fix: MATCH (p:Project {slug: $slug}) → MERGE (p:Project {slug: $slug})
in store_spec. Keep the existing _require_project_slug guard (L272) so an
empty slug still fails closed.
Also fix, same brief:
knowledge_graph/sync.py::_sync_requirements(L809, write L839–843) MERGEs:Requirementwith no relationship at all. Its 559orchestrata-ultimate:-rooted nodes are reachable today only by luck (incomingTRACES_TOedges from other writers). It should MERGE(:Project {slug:$project})-[:HAS_SPEC]->… or, more precisely, anchor via the spec it belongs to. Not counted in the 8447; a latent orphan source.
4.11 Requirement husk (1) — DELETE
Q: MATCH (r:Requirement) WHERE size(keys(r)) <= 1 RETURN r.id AS id, keys(r) AS k
{"id":"REQ-008-023","k":["id"]}
Q: MATCH (r:Requirement) WHERE r.id IN ["REQ-008-023","orchestrata-ultimate:REQ-008-023"]
RETURN r.id AS id, size(keys(r)) AS nkeys, size([(r)--() | 1]) AS deg
{"id":"REQ-008-023","nkeys":1,"deg":0}
A single-property, zero-degree husk. Writer:
knowledge_graph/runtime_writer.py::write_decision (def L170), write L219–223:
MATCH (d:Decision {id: $id})
MERGE (r:Requirement {id: $req_id})
MERGE (d)-[:JUSTIFIED_BY]->(r)
$req_id is the raw trace tag, not project-qualified — so it mints a
duplicate identity of the canonical {project}:{req} scheme
(sync.py::_sync_requirements L826). The leading MATCH (d:Decision …) failed,
leaving the bare husk behind. Same defect in
kernel/pipeline_kg.py::sync_decisions_to_kg L544–554.
Verdict: DELETE. It carries no information (one property, no edges) and the
real requirement lives on disk in specs/ and in the graph as one of 5388
:RequirementItem nodes. Linking a contentless husk would be linking garbage,
which the standing ruling forbids.
Write-time fix: project-qualify $req_id in both writers via
canonical_schema.project_qualified_id(project, req) (L792).
4.12 Fact — 22 — DELETE
Writers: orchestrator/fact_graph.py::build_create_query (L41) — the
no-session branch L86–94 is a bare CREATE (f:Fact {...}) with zero edges;
and session/graph_store.py::persist_fact L131–136 / persist_facts_batch
L171–178 which MATCH (not MERGE) the Session, so a missing Session silently drops
the edge.
Evidence — the entire label is test residue:
Q: MATCH (f:Fact) RETURN f.source AS src, count(*) AS c
{"src":"test","c":88} <- 88 of 88, no other source value exists
Orphan sample:
{"id":"fact-15d474ef1982","fact_type":"decision","data":"{\"i\": 4}","source":"test",
"created_at":"2026-07-26T10:44:00.055447"}
{"id":"fact-1dfbdfd549df","fact_type":"decision","data":"{\"i\": 2}","source":"test", ...}
data values of {"i": 2} / {"i": 3} / {"i": 4} are loop-counter fixtures.
Verdict: DELETE the 22 orphans (source = 'test', keyed to the orphan set).
See DECISION D2 for the 66 reachable siblings.
4.13 Action — 8 — DELETE
Writer: session/kg_sync/_stage_session.py::log_action (def L295), write
L323–331 — CREATE (a:Action {...}) WITH a MATCH (s:Session {...}) MERGE (s)-[:LOGGED_ACTION]->(a).
The CREATE runs before the MATCH, so a missing Session leaves an orphan.
Critically: log_action has no production caller. rg 'log_action' src/ scripts/
returns only the definition and its docstring mention; every invocation is in
tests.
Evidence — all 8 nodes, complete:
Q: MATCH (a:Action) RETURN a.agent AS agent, a.description AS d, a.type AS t
{"agent":"pytest","d":"edited module.py","t":"file_edit"} x4
{"agent":"","d":"picked option A","t":"decision"} x4
Verdict: DELETE all 8 — integration-test residue with no production origin.
4.14 Timeout — 4 — DELETE (writer fix still required)
Writer: orchestrator/graph_audit.py::_write_event (def L328) via the
dynamic _EVENT_LABELS table (L316 "timeout": "Timeout"). The emitter
log_timeout (L407) enqueues {"escalation_id": ...} with no session_id,
so session_id = data.get("session_id","") (L346) is always empty and the
no-edge branch fires unconditionally:
CREATE (n:Timeout {id: $id, data: $data, timestamp: $ts})
Every :Timeout node ever written by this path is a guaranteed zero-degree orphan.
Evidence — all 4 nodes:
Q: MATCH (t:Timeout) RETURN t.data AS data
{"data":"{\"escalation_id\": \"_TEST_7d71145ea674_esc\"}"}
{"data":"{\"escalation_id\": \"_TEST_930d835ba1e1_esc\"}"}
{"data":"{\"escalation_id\": \"_TEST_1b5fdde4bf12_esc\"}"}
{"data":"{\"escalation_id\": \"_TEST_58a4c15399d1_esc\"}"}
All four carry the _TEST_ escalation-id marker. Verdict: DELETE the 4 as
test residue — but the production path is real (kernel/pam/escalation.py::timeout_stale
L294–296), so the writer MUST still be fixed or it will re-mint orphans.
4.15 Resolution — 3 — DELETE (writer fix still required)
Two writers, both orphan-minting:
governance/escalation_handler/resolver.py::apply_resolution_strategy(def L224), write L266–276 — a bareCREATE (r:Resolution {...})with noWITH, no edge.workflow_id/conflict_idare string properties.orchestrator/graph_audit.py::_write_event— same no-session branch as Timeout;log_resolution(L396) passes nosession_id.
Note an identity conflict worth recording: canonical_schema.py:516 declares
LABEL_RESOLUTION's merge key as resolution_id, but writer (2) keys on a fresh
uuid4() id. Flagged, not fixed here.
Evidence — all 3 nodes:
Q: MATCH (r:Resolution) RETURN r.conflict_id AS cid, r.resolved_by AS by, count(*) AS c
{"cid":"c-resolve-neo4j","by":"system","c":3}
All three share the fixture conflict id c-resolve-neo4j and the identical
resolution_text "Vote resolution: v1 granted authority over comp_a".
Verdict: DELETE the 3. Writer fix still required (real production path via
kernel/pam/escalation.py::resolve L263–264).
One-site leverage:
orchestrator/graph_audit.pyL352–366 is the single write site for 12 labels (Timeout,Resolution,Escalation,Violation,ToolCall,Event,PolicyDecision,SessionStart,SessionEnd,Error,ComplianceCheck,VolkLifecycle). Fixing that oneelsebranch closes the largest single orphan tap in the codebase. It is the highest-value edit in either brief.
5. Sequencing (mandatory order)
- Commit 1 — schema only. Add
REL_HAS_PARAMETER,REL_HAS_SPEC,REL_HAS_STORY,REL_HAS_REQUIREMENTtocanonical_schema.pywith the session-254 rationale comment, plus schema tests. RUL-016: nothing may link against these before this lands. Owner: BUILDER. Nothing else in this commit. - Commit 2..n — writers. BUILDER and KIMI work in parallel on disjoint file
sets (§6). Each writer fix ships with a behavioural test that asserts the new
node is reachable from
(:Project)— not merely that the edge exists. - Commit N+1 — repair script.
scripts/repair_orphan_hub_links.py(BUILDER). Dry-run default,--applyto write. - Repair run — orchestrator only. The agents never run
--applyagainst the live graph. The orchestrator runs it once, then re-runspython scripts/check_kg_connectivity.pyand expectsUNREACHABLE 0.
Rationale for ordering: writers before repair. If the repair runs first, the still-broken writers immediately re-mint orphans and the verification is meaningless.
Repair-internal ordering: the script computes the orphan set once, up
front, then applies DELETE verdicts, then LINK verdicts. This removes an
order-dependence trap — e.g. the 22 test :Fact nodes stop being orphans the
moment their :Session gets its hub edge, so a naive "recompute per label"
implementation would silently skip them.
6. File ownership (collision-free split)
Both agents work in the same worktree, so ownership is exclusive. Touching a file owned by the other agent is a stop-and-report condition.
KIMI — KIMI-BRIEF.md — cxp / gateway / messaging:
src/orchestrata_ultimate/knowledge_graph/cxp_auditor.pysrc/orchestrata_ultimate/knowledge_graph/runtime_writer.pysrc/orchestrata_ultimate/orchestrator/graph_audit.py- tests:
tests/knowledge_graph/test_cxp_auditor.py,tests/knowledge_graph/test_runtime_writer.py,tests/orchestrator/test_graph_audit_behavioral.py
Kimi needs no schema addition — every constant its fixes require
(REL_IN_THREAD, REL_HAS_THREAD, REL_HAS_SESSION, LABEL_THREAD,
thread_entity_id) already exists. Kimi must not edit canonical_schema.py.
BUILDER — BUILDER-BRIEF.md — code-intelligence / orchestration / repair:
src/orchestrata_ultimate/knowledge_graph/canonical_schema.py(commit 1, sole owner)src/orchestrata_ultimate/knowledge_graph/sync.pysrc/orchestrata_ultimate/knowledge_graph/kg_design.pysrc/orchestrata_ultimate/kernel/pipeline_kg.pysrc/orchestrata_ultimate/gateway/handlers/session.pysrc/orchestrata_ultimate/session/graph_store.pysrc/orchestrata_ultimate/orchestrator/fact_graph.pyscripts/repair_orphan_hub_links.py(new),scripts/backfill_thread_links.py- corresponding tests
All repair scripts are BUILDER-owned, including the thread backfill that covers Kimi's labels. Kimi ships write-time fixes only.
7. DECISIONS for Mohamed
D1 — degenerate generated-project slugs (§4.10). Minting :Project hubs for
the six real dogfood slugs is straightforward. Two are degenerate:
project_slug: null— one:Specwith no slug at all.project_slug: "implement-as-specified"— a slugified instruction, not a project name (16 Requirements hang off it).
Options: (a) mint hubs for all seven, junk slugs included; (b) mint for the six
real ones and DELETE the two degenerate trees; (c) fold both into
orchestrata-ultimate. Recommendation: (b) — a null slug cannot be a project
identity, and "implement-as-specified" is the same class of writer bug that
repair_project_hubs.py was written to clean up (it already deletes a
slug='' hub and a truncated intent-slug hub). Needs your call before the repair
runs; the plan currently assumes (b).
D2 — :Fact is 100% test residue (§4.12). All 88 :Fact nodes have
source='test'; only 22 are orphaned. This plan deletes those 22. The other 66
are reachable (hanging off anchored Sessions) and therefore invisible to the
connectivity gate — but they are the same garbage. Options: delete all 88 here,
or leave 66 for a separate sweep_test_pollution.py pass.
Recommendation: delete the 22 in this wave (scope discipline — this is a
connectivity repair) and log the 66 as a sweeper backlog item. Say the word
and I will widen it to all 88.
D3 — :Resolution merge-key conflict (§4.15). canonical_schema.py:516
declares the key resolution_id; graph_audit.py keys the same label on a fresh
uuid4() id, and escalation_handler/resolver.py on resolution_id. Two
writers, two identities, one label. Out of scope for hub-linking, but it is
active ontology drift. Recommendation: file it as a follow-up, do not widen
this wave.
8. Verification
cd /home/mohamed/workspace/wt-HUB && export PYTHONPATH=$(pwd)/src
# gate — must print UNREACHABLE 0 after the repair run
python scripts/check_kg_connectivity.py
python scripts/check_kg_connectivity.py --json
# ontology conformance
python scripts/kg_schema.py check
Success criteria: check_kg_connectivity.py exits 0, and a second run 24h later
still exits 0 (proving the writer fixes, not just the repair, are holding).
9. Evidence provenance
Every figure in this document was read from the live orchkg database on
2026-07-27 with read-only Cypher (MATCH/RETURN only; no write was issued at
any point). The BFS reachability set was recomputed with the same algorithm
scripts/check_kg_connectivity.py uses (undirected traversal from all
:Project seeds, batched id(n) IN $ids adjacency queries) so the orphan set
here is identical to the one the gate reports.