Skip to content

Commit 685b52d

Browse files
committed
Remove Session; DAG with cross-references
1 parent e5733b1 commit 685b52d

17 files changed

Lines changed: 1803 additions & 883 deletions

File tree

CLAUDE.md

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,24 @@
22

33
Context graph for persistent memory across Claude Code sessions.
44

5+
## Core Strategy (Required Reading)
6+
7+
**Before making design decisions**, consult these in order:
8+
9+
1. **`doc/clarifications/`** — Binding decisions that override other docs
10+
2. **`doc/PROJECT_VISION.md`** — Intended behavior and architecture
11+
3. **Gupta/Koratana Articles** — External strategy ccmemory implements (local copies in `doc/context_graphs/`):
12+
- [AI's Trillion-Dollar Opportunity](https://foundationcapital.com/context-graphs-ais-trillion-dollar-opportunity/) — Decision traces, not state; the "why" as first-class data
13+
- [How to Build a Context Graph](https://www.linkedin.com/pulse/how-build-context-graph-animesh-koratana-6abve) — Two clocks, agent trajectories, schema as output
14+
15+
**Key principles from these sources:**
16+
- **Event clock, not state clock** — Capture reasoning/decisions, not just current state
17+
- **Decision traces, not containers** — Organize by time + entity links, not sessions
18+
- **Schema as output** — Let structure emerge from use, don't over-specify upfront
19+
- **World models, not retrieval** — Goal is simulation ("what if?"), not just search
20+
21+
The vision doc synthesizes these articles with Patrick's thinking. If vision doc conflicts with the articles, flag for review.
22+
523
## Development Process
624

725
- After changes, provide concise verification steps for the user
@@ -12,7 +30,7 @@ Context graph for persistent memory across Claude Code sessions.
1230
- Dashboard debug target: port 8765
1331
- Only rebuild/redeploy docker images when debugging container-specific issues
1432
- Use `uv` for all Python commands (e.g., `uv run pytest`, `uv pip install`)
15-
- **AS_BUILT.md**: Read `doc/AS_BUILT.md` before making changes. Update it after any non-obvious implementation change (schema, session flow, error handling, hooks, etc.)
33+
- **AS_BUILT.md**: Read `doc/AS_BUILT.md` before making changes. **ALWAYS update it after completing implementation work** — this is mandatory, not optional
1634

1735
## Session Startup Requirement
1836

@@ -101,7 +119,8 @@ Container-internal (set in docker-compose.yml):
101119
2. Domain 2 (Markdown + Neo4j index): Reference knowledge — cached URLs, PDFs
102120

103121
**Node Types (Domain 1):**
104-
- Session, Decision, Correction, Exception, Insight, Question, FailedApproach, ProjectFact, Reference
122+
- Decision, Correction, Exception, Insight, Question, FailedApproach, ProjectFact, Reference
123+
- **Note:** Session is deprecated per `doc/clarifications/1-DAG-with-CROSS-REFS.md` — organize by timestamp, not session containers
105124

106125
**Detection Flow:**
107126
1. Stop hook fires after each Claude response

dashboard/app.py

Lines changed: 135 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
if os.getenv("GEVENT_SUPPORT") == "True":
1111
from gevent import monkey
12+
1213
monkey.patch_all()
1314

1415
from flask import Flask, render_template, jsonify, request
@@ -70,7 +71,6 @@ def index():
7071
"decisions": {"title": "Decisions"},
7172
"corrections": {"title": "Corrections"},
7273
"insights": {"title": "Insights"},
73-
"sessions": {"title": "Sessions"},
7474
"failed-approaches": {"title": "Failed Approaches"},
7575
"exceptions": {"title": "Exceptions"},
7676
"questions": {"title": "Questions"},
@@ -81,6 +81,7 @@ def index():
8181

8282
def _detail_page(page_type: str):
8383
"""Create detail page handler for a given page type."""
84+
8485
def view():
8586
project = request.args.get("project", "")
8687
config = _DETAIL_PAGE_CONFIG.get(page_type, {})
@@ -90,6 +91,7 @@ def view():
9091
page_type=page_type,
9192
title=config.get("title", page_type.title()),
9293
)
94+
9395
view.__name__ = f"{page_type}_view"
9496
return view
9597

@@ -114,26 +116,34 @@ def metrics():
114116
WITH total_decisions, curated, count(c) as total_corrections
115117
OPTIONAL MATCH (i:Insight {project: $project})
116118
WITH total_decisions, curated, total_corrections, count(i) as total_insights
117-
OPTIONAL MATCH (s:Session {project: $project})
118-
WITH total_decisions, curated, total_corrections, total_insights, count(s) as total_sessions
119119
OPTIONAL MATCH (f:FailedApproach {project: $project})
120-
WITH total_decisions, curated, total_corrections, total_insights, total_sessions, count(f) as total_failed_approaches
120+
WITH total_decisions, curated, total_corrections, total_insights, count(f) as total_failed_approaches
121121
OPTIONAL MATCH (pf:ProjectFact {project: $project})
122+
WITH total_decisions, curated, total_corrections, total_insights, total_failed_approaches, count(pf) as total_project_facts
123+
OPTIONAL MATCH (e:Exception {project: $project})
124+
WITH total_decisions, curated, total_corrections, total_insights, total_failed_approaches, total_project_facts, count(e) as total_exceptions
125+
OPTIONAL MATCH (:Decision {project: $project})-[sup:SUPERSEDES]->(:Decision)
122126
RETURN total_decisions, curated, total_corrections, total_insights,
123-
total_sessions, total_failed_approaches, count(pf) as total_project_facts
127+
total_failed_approaches, total_project_facts, total_exceptions, count(sup) as supersession_count
124128
""",
125129
project=project,
126130
)
127131
record = result.single()
128132

129133
total_decisions = record["total_decisions"]
130-
total_sessions = record["total_sessions"]
131134
total_corrections = record["total_corrections"]
135+
total_project_facts = record["total_project_facts"]
136+
total_exceptions = record["total_exceptions"]
137+
supersession_count = record["supersession_count"]
132138

133139
reuse_rate = record["curated"] / total_decisions if total_decisions > 0 else 0
134-
reexplanation_rate = (
135-
total_corrections / total_sessions if total_sessions > 0 else 0
140+
evolution_rate = (
141+
supersession_count / total_decisions if total_decisions > 0 else 0
142+
)
143+
exception_rate = (
144+
total_exceptions / total_project_facts if total_project_facts > 0 else 0
136145
)
146+
137147
coefficient = 1.0 + (total_decisions * 0.02) + (reuse_rate * 1.0)
138148
coefficient = min(coefficient, 4.0)
139149

@@ -143,12 +153,12 @@ def metrics():
143153
"total_decisions": total_decisions,
144154
"total_corrections": total_corrections,
145155
"total_insights": record["total_insights"],
146-
"total_sessions": total_sessions,
147156
"total_failed_approaches": record["total_failed_approaches"],
148-
"total_project_facts": record["total_project_facts"],
157+
"total_project_facts": total_project_facts,
158+
"total_exceptions": total_exceptions,
149159
"decision_reuse_rate": reuse_rate,
150-
"graph_density": 0.0,
151-
"reexplanation_rate": reexplanation_rate,
160+
"decision_evolution_rate": evolution_rate,
161+
"rule_exception_rate": exception_rate,
152162
}
153163
)
154164

@@ -194,8 +204,13 @@ def graph():
194204
project = request.args.get("project", "")
195205
limit = int(request.args.get("limit", 100))
196206
node_types = request.args.getlist("types") or [
197-
"Decision", "Correction", "Exception", "Insight",
198-
"Question", "FailedApproach", "ProjectFact"
207+
"Decision",
208+
"Correction",
209+
"Exception",
210+
"Insight",
211+
"Question",
212+
"FailedApproach",
213+
"ProjectFact",
199214
]
200215
driver = getDriver()
201216

@@ -271,13 +286,8 @@ def graph():
271286
if not record:
272287
return jsonify({"nodes": [], "edges": []})
273288

274-
nodes = record["nodes"] + [
275-
sn for sn in record["session_nodes"] if sn["id"]
276-
]
277-
edges = [
278-
e for e in record["edges"]
279-
if e["source"] and e["target"]
280-
]
289+
nodes = record["nodes"] + [sn for sn in record["session_nodes"] if sn["id"]]
290+
edges = [e for e in record["edges"] if e["source"] and e["target"]]
281291

282292
for node in nodes:
283293
if node.get("timestamp") and hasattr(node["timestamp"], "isoformat"):
@@ -304,11 +314,13 @@ def proactive_insights():
304314
project=project,
305315
)
306316
for r in result:
307-
insights.append({
308-
"type": "highly_cited",
309-
"message": f"'{r['description'][:40]}...' cited by {r['cite_count']} decisions",
310-
"node_id": r["id"],
311-
})
317+
insights.append(
318+
{
319+
"type": "highly_cited",
320+
"message": f"'{r['description'][:40]}...' cited by {r['cite_count']} decisions",
321+
"node_id": r["id"],
322+
}
323+
)
312324

313325
result = session.run(
314326
"""
@@ -321,10 +333,12 @@ def proactive_insights():
321333
project=project,
322334
)
323335
for r in result:
324-
insights.append({
325-
"type": "exception_hotspot",
326-
"message": f"'{r['rule'][:30]}...' has {r['exception_count']} exceptions",
327-
})
336+
insights.append(
337+
{
338+
"type": "exception_hotspot",
339+
"message": f"'{r['rule'][:30]}...' has {r['exception_count']} exceptions",
340+
}
341+
)
328342

329343
result = session.run(
330344
"""
@@ -335,14 +349,100 @@ def proactive_insights():
335349
)
336350
record = result.single()
337351
if record and record["correction_count"] >= 3:
338-
insights.append({
339-
"type": "correction_pattern",
340-
"message": f"{record['correction_count']} corrections recorded - review for patterns",
341-
})
352+
insights.append(
353+
{
354+
"type": "correction_pattern",
355+
"message": f"{record['correction_count']} corrections recorded - review for patterns",
356+
}
357+
)
342358

343359
return jsonify(insights)
344360

345361

362+
@app.route("/api/patterns")
363+
def patterns():
364+
"""Get detected patterns: exception clusters, supersession chains, correction hotspots."""
365+
project = request.args.get("project", "")
366+
driver = getDriver()
367+
368+
result = {
369+
"exception_clusters": [],
370+
"supersession_chains": [],
371+
"correction_hotspots": [],
372+
}
373+
374+
with driver.session() as session:
375+
# Exception clusters: rules with multiple exceptions
376+
clusters = session.run(
377+
"""
378+
MATCH (e:Exception {project: $project})
379+
WHERE e.rule_broken IS NOT NULL
380+
WITH e.rule_broken as rule, collect(e) as exceptions, count(*) as count
381+
WHERE count >= 2
382+
RETURN rule, count,
383+
[exc IN exceptions | {id: exc.id, justification: exc.justification, scope: exc.scope}] as exceptions
384+
ORDER BY count DESC
385+
LIMIT 10
386+
""",
387+
project=project,
388+
)
389+
for r in clusters:
390+
result["exception_clusters"].append(
391+
{
392+
"rule": r["rule"],
393+
"count": r["count"],
394+
"exceptions": r["exceptions"],
395+
}
396+
)
397+
398+
# Supersession chains: decisions that supersede other decisions
399+
chains = session.run(
400+
"""
401+
MATCH path = (d:Decision {project: $project})-[:SUPERSEDES*2..]->(old:Decision)
402+
WITH d, length(path) as chain_length, [n IN nodes(path) | n.description] as chain
403+
RETURN d.id as id, d.description as description, chain_length, chain
404+
ORDER BY chain_length DESC
405+
LIMIT 10
406+
""",
407+
project=project,
408+
)
409+
for r in chains:
410+
result["supersession_chains"].append(
411+
{
412+
"id": r["id"],
413+
"description": r["description"],
414+
"chain_length": r["chain_length"],
415+
"chain": r["chain"],
416+
}
417+
)
418+
419+
# Correction hotspots: topics with multiple corrections
420+
hotspots = session.run(
421+
"""
422+
MATCH (c:Correction {project: $project})
423+
WHERE c.topics IS NOT NULL AND size(c.topics) > 0
424+
UNWIND c.topics as topic
425+
WITH topic, collect(c) as corrections, count(*) as count
426+
WHERE count >= 2
427+
RETURN topic, count,
428+
[corr IN corrections[0..5] | {id: corr.id, wrong: corr.wrong_belief, right: corr.right_belief}] as corrections
429+
ORDER BY count DESC
430+
LIMIT 10
431+
""",
432+
project=project,
433+
)
434+
for r in hotspots:
435+
result["correction_hotspots"].append(
436+
{
437+
"topic": r["topic"],
438+
"count": r["count"],
439+
"corrections": r["corrections"],
440+
}
441+
)
442+
443+
return jsonify(result)
444+
445+
346446
@app.route("/api/decisions")
347447
def decisions():
348448
project = request.args.get("project", "")
@@ -439,24 +539,6 @@ def failed_approaches():
439539
return jsonify([serialize_node(dict(r["f"])) for r in result])
440540

441541

442-
@app.route("/api/sessions")
443-
def sessions():
444-
project = request.args.get("project", "")
445-
limit = int(request.args.get("limit", 50))
446-
driver = getDriver()
447-
448-
with driver.session() as session:
449-
result = session.run(
450-
"""
451-
MATCH (s:Session {project: $project})
452-
RETURN s ORDER BY s.started_at DESC LIMIT $limit
453-
""",
454-
project=project,
455-
limit=limit,
456-
)
457-
return jsonify([serialize_node(dict(r["s"])) for r in result])
458-
459-
460542
@app.route("/api/exceptions")
461543
def exceptions():
462544
project = request.args.get("project", "")
@@ -658,6 +740,7 @@ def import_conversations():
658740
return jsonify({"error": f"MCP connection failed: {e}"}), 500
659741
except Exception as e:
660742
import traceback
743+
661744
traceback.print_exc()
662745
return jsonify({"error": str(e)}), 500
663746

0 commit comments

Comments
 (0)