@binarycheater/research-sidecar 0.1.2 → 0.1.4

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.
@@ -4,8 +4,8 @@
4
4
  <meta charset="UTF-8" />
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
6
  <title>Research Sidecar</title>
7
- <script type="module" crossorigin src="/assets/index-DTKJmZxM.js"></script>
8
- <link rel="stylesheet" crossorigin href="/assets/index-C3mAbt-i.css">
7
+ <script type="module" crossorigin src="/assets/index-kXy-k1Bp.js"></script>
8
+ <link rel="stylesheet" crossorigin href="/assets/index-BErjWgQz.css">
9
9
  </head>
10
10
  <body>
11
11
  <div id="root"></div>
@@ -15,6 +15,7 @@ export function buildGraphIndex(graph) {
15
15
  export function getVisibleResearchGraph(graph, options) {
16
16
  const byId = new Map(graph.nodes.map((node) => [node.id, node]));
17
17
  const hierarchyEdges = graph.edges.filter((edge) => hierarchyEdgeKinds.has(edge.kind));
18
+ const contextEdges = graph.edges.filter((edge) => !hierarchyEdgeKinds.has(edge.kind));
18
19
  const childrenByParent = groupEdges(hierarchyEdges, "from");
19
20
  const parentsByChild = groupEdges(hierarchyEdges, "to");
20
21
  const childCounts = Object.fromEntries(graph.nodes.map((node) => [node.id, childrenByParent.get(node.id)?.length || 0]));
@@ -31,7 +32,9 @@ export function getVisibleResearchGraph(graph, options) {
31
32
  }
32
33
  }
33
34
  else {
34
- collectExpandedTree(graph.rootId, childrenByParent, options.expandedIds, visibleIds);
35
+ collectExpandedDag(graph.rootId, childrenByParent, options.expandedIds, visibleIds);
36
+ addSharedHierarchyChildren(visibleIds, hierarchyEdges);
37
+ addIncidentContextNodes(visibleIds, contextEdges);
35
38
  }
36
39
  const nodes = graph.nodes.filter((node) => visibleIds.has(node.id));
37
40
  const edges = graph.edges.filter((edge) => visibleIds.has(edge.from) && visibleIds.has(edge.to));
@@ -69,18 +72,11 @@ export function getExpandableDescendantIds(graph, rootId) {
69
72
  }
70
73
  export function getNextExpandedIdsForNodeClick(graph, currentIds, nodeId) {
71
74
  const next = new Set(currentIds);
72
- const descendantIds = getHierarchyDescendantIds(graph, nodeId);
73
75
  if (next.has(nodeId)) {
74
76
  next.delete(nodeId);
75
- for (const descendantId of descendantIds) {
76
- next.delete(descendantId);
77
- }
78
77
  return next;
79
78
  }
80
79
  next.add(nodeId);
81
- for (const descendantId of descendantIds) {
82
- next.delete(descendantId);
83
- }
84
80
  return next;
85
81
  }
86
82
  export function getFocusedResearchGraph(graph, options) {
@@ -141,20 +137,13 @@ export function layoutResearchGraph(graph, options) {
141
137
  parentQueue.push(edge.from);
142
138
  }
143
139
  }
144
- const rows = new Map();
145
- for (const node of graph.nodes) {
146
- const depth = depthById.get(node.id) ?? 0;
147
- rows.set(depth, [...(rows.get(depth) || []), node]);
148
- }
149
- const rowOffsets = new Map();
150
- for (const [depth, nodes] of rows) {
151
- nodes.forEach((node, offset) => rowOffsets.set(node.id, offset - (nodes.length - 1) / 2));
152
- rows.set(depth, nodes);
153
- }
140
+ const ySlots = assignHierarchyYSlots(graph, childrenByParent, depthById);
141
+ const slots = [...ySlots.values()];
142
+ const centerSlot = slots.length ? (Math.min(...slots) + Math.max(...slots)) / 2 : 0;
143
+ const spacing = options.mode === "full" ? { depth: 700, cross: 300 } : { depth: 320, cross: 170 };
154
144
  const nodes = graph.nodes.map((node) => {
155
145
  const depth = depthById.get(node.id) ?? 0;
156
- const offset = rowOffsets.get(node.id) || 0;
157
- const spacing = options.mode === "full" ? { depth: 610, cross: 320 } : { depth: 300, cross: 160 };
146
+ const offset = (ySlots.get(node.id) ?? 0) - centerSlot;
158
147
  const position = options.direction === "LR" ? { x: depth * spacing.depth, y: offset * spacing.cross } : { x: offset * spacing.depth, y: depth * spacing.cross };
159
148
  return { ...node, position };
160
149
  });
@@ -165,12 +154,80 @@ export function layoutResearchGraph(graph, options) {
165
154
  childCounts: graph.childCounts
166
155
  };
167
156
  }
168
- function collectExpandedTree(id, childrenByParent, expandedIds, visibleIds) {
157
+ function assignHierarchyYSlots(graph, childrenByParent, depthById) {
158
+ const graphNodeIds = new Set(graph.nodes.map((node) => node.id));
159
+ const ySlots = new Map();
160
+ const visiting = new Set();
161
+ let nextSlot = 0;
162
+ function assign(id) {
163
+ const existing = ySlots.get(id);
164
+ if (existing !== undefined)
165
+ return existing;
166
+ if (visiting.has(id)) {
167
+ const slot = nextSlot;
168
+ nextSlot += 1;
169
+ ySlots.set(id, slot);
170
+ return slot;
171
+ }
172
+ visiting.add(id);
173
+ const childSlots = [];
174
+ for (const edge of childrenByParent.get(id) || []) {
175
+ if (!graphNodeIds.has(edge.to))
176
+ continue;
177
+ childSlots.push(assign(edge.to));
178
+ }
179
+ visiting.delete(id);
180
+ const slot = childSlots.length ? (Math.min(...childSlots) + Math.max(...childSlots)) / 2 : nextSlot++;
181
+ ySlots.set(id, slot);
182
+ return slot;
183
+ }
184
+ if (graphNodeIds.has(graph.rootId)) {
185
+ assign(graph.rootId);
186
+ }
187
+ const remaining = graph.nodes
188
+ .filter((node) => !ySlots.has(node.id))
189
+ .sort((left, right) => (depthById.get(left.id) ?? 0) - (depthById.get(right.id) ?? 0));
190
+ for (const node of remaining) {
191
+ assign(node.id);
192
+ }
193
+ return ySlots;
194
+ }
195
+ function collectExpandedDag(id, childrenByParent, expandedIds, visibleIds) {
196
+ const alreadyVisible = visibleIds.has(id);
169
197
  visibleIds.add(id);
170
- if (!expandedIds.has(id))
198
+ if (alreadyVisible || !expandedIds.has(id))
171
199
  return;
172
200
  for (const edge of childrenByParent.get(id) || []) {
173
- collectExpandedTree(edge.to, childrenByParent, expandedIds, visibleIds);
201
+ collectExpandedDag(edge.to, childrenByParent, expandedIds, visibleIds);
202
+ }
203
+ }
204
+ function addIncidentContextNodes(visibleIds, contextEdges) {
205
+ const hierarchyVisibleIds = new Set(visibleIds);
206
+ for (const edge of contextEdges) {
207
+ if (hierarchyVisibleIds.has(edge.from)) {
208
+ visibleIds.add(edge.to);
209
+ }
210
+ if (hierarchyVisibleIds.has(edge.to)) {
211
+ visibleIds.add(edge.from);
212
+ }
213
+ }
214
+ }
215
+ function addSharedHierarchyChildren(visibleIds, hierarchyEdges) {
216
+ let changed = true;
217
+ while (changed) {
218
+ changed = false;
219
+ const visibleParentCounts = new Map();
220
+ for (const edge of hierarchyEdges) {
221
+ if (!visibleIds.has(edge.from) || visibleIds.has(edge.to))
222
+ continue;
223
+ visibleParentCounts.set(edge.to, (visibleParentCounts.get(edge.to) || 0) + 1);
224
+ }
225
+ for (const [childId, parentCount] of visibleParentCounts) {
226
+ if (parentCount < 2)
227
+ continue;
228
+ visibleIds.add(childId);
229
+ changed = true;
230
+ }
174
231
  }
175
232
  }
176
233
  function getHierarchyDescendantIds(graph, rootId) {
@@ -1,11 +1,24 @@
1
1
  import { access, readFile } from "node:fs/promises";
2
- import { dirname, extname, isAbsolute, join } from "node:path";
2
+ import { dirname, extname, isAbsolute, join, posix } from "node:path";
3
3
  import YAML from "yaml";
4
- import { resolveWorkspacePath, toWorkspaceRelativePath } from "./files.js";
4
+ import { resolveWorkspacePath, toWorkspaceRelativePath, writeWorkspaceFile } from "./files.js";
5
5
  const DEFAULT_MANIFEST_PATH = "research/graph.yaml";
6
6
  const NODE_TYPES = new Set(["question", "claim", "evidence", "method", "concept", "source", "task", "output"]);
7
7
  const EDGE_KINDS = new Set(["decomposes", "answers", "supports", "contradicts", "depends_on", "operationalizes", "cites", "leads_to"]);
8
- const STATUSES = new Set(["active", "draft", "blocked", "done"]);
8
+ const STATUSES = new Set([
9
+ "active",
10
+ "draft",
11
+ "blocked",
12
+ "done",
13
+ "needs_decomposition",
14
+ "testable",
15
+ "testing",
16
+ "supported",
17
+ "weakened",
18
+ "contradicted",
19
+ "revised",
20
+ "accepted_for_now"
21
+ ]);
9
22
  const LAYOUTS = new Set(["LR", "TB"]);
10
23
  export async function loadResearchGraphManifest(workspaceRoot, manifestPath = DEFAULT_MANIFEST_PATH) {
11
24
  const fullPath = resolveWorkspacePath(workspaceRoot, manifestPath);
@@ -34,6 +47,63 @@ export async function loadResearchGraphManifest(workspaceRoot, manifestPath = DE
34
47
  ...(ui ? { ui } : {})
35
48
  };
36
49
  }
50
+ export async function saveResearchGraphManifest(workspaceRoot, manifestPath = DEFAULT_MANIFEST_PATH, graph) {
51
+ const manifestDir = dirname(manifestPath);
52
+ const nodeIds = new Set();
53
+ const nodes = graph.nodes.map((node) => {
54
+ const id = requiredString(node.id, "node.id");
55
+ if (nodeIds.has(id)) {
56
+ throw new Error(`Duplicate node id ${id}.`);
57
+ }
58
+ nodeIds.add(id);
59
+ const type = normalizeNodeType(node.type, id);
60
+ const files = serializeNodeFiles(manifestDir, node);
61
+ return {
62
+ id,
63
+ title: requiredString(node.title, `node ${id} title`),
64
+ type,
65
+ ...(files.length === 1 ? { file: files[0].path } : {}),
66
+ ...(files.length > 1
67
+ ? {
68
+ files: files.map((file) => (file.title ? { path: file.path, title: file.title } : file.path))
69
+ }
70
+ : {}),
71
+ ...optionalField("summary", node.summary),
72
+ ...optionalStatus(node.status),
73
+ ...(node.tags?.length ? { tags: node.tags } : {})
74
+ };
75
+ });
76
+ const root = requiredString(graph.rootId, "root");
77
+ if (!nodeIds.has(root)) {
78
+ throw new Error(`Graph root ${root} does not match any node id.`);
79
+ }
80
+ const edges = graph.edges.map((edge) => {
81
+ const normalized = normalizeEdge(edge, nodeIds);
82
+ return {
83
+ ...(edge.id && edge.id !== `${normalized.from}->${normalized.to}:${normalized.kind}` ? { id: edge.id } : {}),
84
+ from: normalized.from,
85
+ to: normalized.to,
86
+ kind: normalized.kind,
87
+ ...optionalField("label", normalized.label)
88
+ };
89
+ });
90
+ const manifest = {
91
+ root,
92
+ ...(graph.ui?.layout || graph.ui?.expanded?.length
93
+ ? {
94
+ ui: {
95
+ ...(graph.ui.layout ? { layout: graph.ui.layout } : {}),
96
+ ...(graph.ui.expanded?.length ? { expanded: graph.ui.expanded } : {})
97
+ }
98
+ }
99
+ : {}),
100
+ nodes,
101
+ edges
102
+ };
103
+ const body = YAML.stringify(manifest, { lineWidth: 0 });
104
+ await writeWorkspaceFile(workspaceRoot, manifestPath, body, [".yaml", ".yml"]);
105
+ return loadResearchGraphManifest(workspaceRoot, manifestPath);
106
+ }
37
107
  async function normalizeNode(workspaceRoot, manifestDir, input, warnings) {
38
108
  const id = requiredString(input.id, "node.id");
39
109
  const linkedFiles = normalizeManifestLinkedFiles(workspaceRoot, manifestDir, input.file, input.files);
@@ -147,6 +217,22 @@ function normalizeManifestLinkedFiles(workspaceRoot, manifestDir, fileValue, fil
147
217
  }
148
218
  return out;
149
219
  }
220
+ function serializeNodeFiles(manifestDir, node) {
221
+ const files = node.files?.length ? node.files : node.file ? [{ path: node.file }] : [];
222
+ return files.map((file) => ({
223
+ path: toManifestRelativePath(manifestDir, file.path),
224
+ ...(file.title ? { title: file.title } : {})
225
+ }));
226
+ }
227
+ function toManifestRelativePath(manifestDir, workspacePath) {
228
+ const normalized = workspacePath.trim().replace(/^\/+/, "");
229
+ if (!normalized)
230
+ return normalized;
231
+ const relativePath = posix.relative(manifestDir === "." ? "" : manifestDir, normalized);
232
+ if (!relativePath || relativePath.startsWith("."))
233
+ return relativePath || ".";
234
+ return `./${relativePath}`;
235
+ }
150
236
  function parseYamlRecord(source, label) {
151
237
  let parsed;
152
238
  try {
@@ -2,6 +2,7 @@ import { access, cp, mkdir, readdir, readFile, writeFile } from "node:fs/promise
2
2
  import { dirname, join, resolve } from "node:path";
3
3
  import { fileURLToPath } from "node:url";
4
4
  const DEFAULT_ALLOWED_WRITE_EXTENSIONS = [".md", ".markdown", ".html", ".htm", ".yaml", ".yml"];
5
+ const SKILL_INSTALL_ROOTS = [".agents/skills", ".claude/skills"];
5
6
  export async function installWorkspaceScaffold(workspaceRoot, options = {}) {
6
7
  const root = resolve(workspaceRoot);
7
8
  const graphManifestPath = normalizeWorkspacePath(options.graphManifestPath || "research/graph.yaml");
@@ -28,21 +29,32 @@ export async function installBundledSkills(workspaceRoot, options = {}) {
28
29
  if (!sourceRoot)
29
30
  return { sourceRoot: null, installed: [], skipped: [] };
30
31
  const entries = await readdir(sourceRoot, { withFileTypes: true });
31
- const targetRoot = join(resolve(workspaceRoot), "skills");
32
+ const root = resolve(workspaceRoot);
33
+ const targetRoots = SKILL_INSTALL_ROOTS.map((target) => join(root, target));
32
34
  const installed = [];
33
35
  const skipped = [];
34
- await mkdir(targetRoot, { recursive: true });
36
+ await Promise.all(targetRoots.map((targetRoot) => mkdir(targetRoot, { recursive: true })));
35
37
  for (const entry of entries) {
36
38
  if (!entry.isDirectory())
37
39
  continue;
38
40
  const source = join(sourceRoot, entry.name);
39
- const target = join(targetRoot, entry.name);
40
- if (!options.force && (await exists(target))) {
41
+ let copied = false;
42
+ let alreadyPresent = false;
43
+ for (const targetRoot of targetRoots) {
44
+ const target = join(targetRoot, entry.name);
45
+ if (!options.force && (await exists(target))) {
46
+ alreadyPresent = true;
47
+ continue;
48
+ }
49
+ await cp(source, target, { recursive: true, force: true });
50
+ copied = true;
51
+ }
52
+ if (copied) {
53
+ installed.push(entry.name);
54
+ }
55
+ if (alreadyPresent) {
41
56
  skipped.push(entry.name);
42
- continue;
43
57
  }
44
- await cp(source, target, { recursive: true, force: true });
45
- installed.push(entry.name);
46
58
  }
47
59
  return { sourceRoot, installed, skipped };
48
60
  }
@@ -105,7 +105,7 @@ async function listFiles(root, maxDepth, dir = "", depth = 0) {
105
105
  }
106
106
  const results = [];
107
107
  for (const entry of entries) {
108
- if (entry.name.startsWith(".") && entry.name !== ".agents")
108
+ if (entry.name.startsWith(".") && entry.name !== ".agents" && entry.name !== ".claude")
109
109
  continue;
110
110
  if (SKIP_DIRS.has(entry.name))
111
111
  continue;
@@ -130,9 +130,11 @@ function parseSkillFrontmatter(content) {
130
130
  };
131
131
  }
132
132
  function skillPriority(path) {
133
- if (/^skills\/[^/]+\/SKILL\.md$/i.test(path))
134
- return 3;
135
133
  if (/^\.agents\/skills\/[^/]+\/SKILL\.md$/i.test(path))
134
+ return 4;
135
+ if (/^\.claude\/skills\/[^/]+\/SKILL\.md$/i.test(path))
136
+ return 3;
137
+ if (/^skills\/[^/]+\/SKILL\.md$/i.test(path))
136
138
  return 2;
137
139
  return 1;
138
140
  }
@@ -6,7 +6,7 @@ import { buildContextPacket } from "../lib/context.js";
6
6
  import { getWorkspaceOpenCommand, readWorkspaceFile, readWorkspaceRawFile, resolveWorkspaceFileForOpen } from "../lib/files.js";
7
7
  import { discoverGraphManifests } from "../lib/graphDiscovery.js";
8
8
  import { DEFAULT_REVIEW_PROMPT } from "../lib/prompt.js";
9
- import { loadResearchGraphManifest } from "../lib/researchGraphManifest.js";
9
+ import { loadResearchGraphManifest, saveResearchGraphManifest } from "../lib/researchGraphManifest.js";
10
10
  import { streamOpenAIReview } from "../lib/openaiProvider.js";
11
11
  import { JsonSessionStore } from "../lib/store.js";
12
12
  import { installBundledSkills } from "../lib/workspaceInstall.js";
@@ -114,6 +114,14 @@ app.get("/api/research-graph", async (_req, res, next) => {
114
114
  next(error);
115
115
  }
116
116
  });
117
+ app.put("/api/research-graph", async (req, res, next) => {
118
+ try {
119
+ res.json(await saveResearchGraphManifest(config.workspaceRoot, config.graphManifestPath, req.body));
120
+ }
121
+ catch (error) {
122
+ next(error);
123
+ }
124
+ });
117
125
  app.get("/api/sessions", async (_req, res, next) => {
118
126
  try {
119
127
  res.json(await store.listSessions());
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@binarycheater/research-sidecar",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "description": "Local research graph sidecar and CLI for Codex-assisted research workflows.",
5
5
  "keywords": [
6
6
  "research",
@@ -9,6 +9,46 @@ Use the graph as a small, inspectable map of the research state. Put structure a
9
9
 
10
10
  Core principle: **thin graph, thick notes, active agent**. The graph should reduce cognitive load, not become a database the researcher has to maintain.
11
11
 
12
+ ## Target Graph Shape
13
+
14
+ The graph is a lightweight reasoning scaffold, not a collection of related notes. It should grow with the research path: questions decompose into testable claims, claims are made observable by methods, methods produce evidence, evidence changes claim status, and unresolved gaps become tasks.
15
+
16
+ Prefer this reasoning backbone:
17
+
18
+ `question -> claim -> method -> evidence -> revision/task/output`
19
+
20
+ Prefer a tree-like shape for the primary reading path. The graph may remain a DAG when shared evidence, sources, or claims genuinely need reuse, but the default shape should branch outward from the root rather than tangle back into earlier layers.
21
+
22
+ For visual clarity:
23
+
24
+ - Point hierarchy edges away from the root along the main reasoning flow.
25
+ - Avoid edges that point back toward ancestors unless the reversal captures a real contradiction, dependency, or revision that cannot be expressed forward.
26
+ - Keep lines short, direct, and easy to scan; remove vague cross-links that make the graph look dense without improving navigation.
27
+ - Prefer one clear parent plus a small number of meaningful lateral links over many convenience links.
28
+
29
+ A useful graph should let a reader answer:
30
+
31
+ 1. What question is being decomposed?
32
+ 2. Which claim currently answers it?
33
+ 3. What would make the claim observable or testable?
34
+ 4. What evidence bears on the claim?
35
+ 5. What changed after the evidence appeared?
36
+ 6. What remains unresolved?
37
+
38
+ ## Reasoning Integrity Rules
39
+
40
+ When editing the graph, preserve reasoning integrity:
41
+
42
+ - An active `question` should have a decomposed subquestion, an answering `claim`, or a `task` explaining what is missing.
43
+ - An active `claim` should have at least one of: a `method` that operationalizes it, `evidence` that supports or contradicts it, a rival `claim`, or a `depends_on` premise.
44
+ - A `claim` should not be treated as settled merely because it has a `source`. Sources are materials; evidence is an extracted observation, result, finding, quotation, or counterexample.
45
+ - A `method` should point toward the claim it makes observable. If the method has produced results, add or update an `evidence` node.
46
+ - An `evidence` node should bear on a specific claim through `supports` or `contradicts`. Avoid evidence nodes that only summarize interesting material without changing the state of a claim.
47
+ - Use `leads_to` when a result creates a revision, follow-up task, output, or new question. Do not use it as a generic related-to edge.
48
+ - If a node is only context, background, or reading material, prefer `source` plus `cites`. Do not promote it into the reasoning backbone unless it changes a claim, method, question, or task.
49
+
50
+ Keep updates conservative. If the user's graph is weak, inconsistent, over-connected, or logically underconstrained, point out the issue and propose a repair. Do not perform a major rewrite, reclassification, or graph-wide normalization unless the user explicitly asks for a major cleanup, rewrite, or restructuring.
51
+
12
52
  ## First Move
13
53
 
14
54
  If `graph.yaml` exists, read it before proposing structure changes. Then read only the linked Markdown/source files needed to understand the active question, claims, evidence, methods, and tasks.
@@ -80,6 +120,14 @@ Prefer the existing Sidecar vocabulary:
80
120
  | `cites` | a node uses a source |
81
121
  | `leads_to` | a result creates a task or next step |
82
122
 
123
+ Use only the edge kinds in this table. Do not invent near-synonyms such as
124
+ `motivates`, `uses`, `contextualizes`, or `informs`; map them to the closest
125
+ allowed kind instead:
126
+
127
+ - `motivates` -> `leads_to` when a result opens a next question/task; otherwise `supports`
128
+ - `uses` or `contextualizes` -> `cites` when pointing to a source; otherwise `supports`
129
+ - `informs` -> `supports` when it bears on a claim, or `leads_to` when it creates a next step
130
+
83
131
  ## YAML Contract
84
132
 
85
133
  When creating or editing `graph.yaml`, keep it valid, boring YAML. Use two-space indentation, arrays with `-`, no tabs, and quote strings only when they contain characters that could confuse YAML (`:`, `#`, `{}`, `[]`, leading `*`, or multiline text).
@@ -120,7 +168,7 @@ Optional node fields:
120
168
  - `summary`: use for short, one-sentence nodes that do not need a document yet.
121
169
  - `file`: one linked Markdown/HTML/text document.
122
170
  - `files`: multiple linked documents. Use either strings or `{ path, title }` objects.
123
- - `status`: `active`, `draft`, `blocked`, or `done`.
171
+ - `status`: legacy statuses `active`, `draft`, `blocked`, or `done`; epistemic statuses `needs_decomposition`, `testable`, `testing`, `supported`, `weakened`, `contradicted`, `revised`, or `accepted_for_now`.
124
172
  - `tags`: array of short labels.
125
173
 
126
174
  Valid document-link patterns:
@@ -163,6 +211,8 @@ New agent-created nodes should usually be `status: draft`. Use `active` when the
163
211
 
164
212
  Add a graph node only when the item needs to be navigated, connected, reused, challenged, or tracked over time.
165
213
 
214
+ For large graphs, reason from the active subgraph first: root or active question, nearby claims, methods, evidence, tasks, and directly cited files. Use the full graph as an index, not as a complete argument in context.
215
+
166
216
  Avoid:
167
217
 
168
218
  - duplicating long Markdown content in `summary`
@@ -170,6 +220,16 @@ Avoid:
170
220
  - adding schema fields the current repo does not already use
171
221
  - treating the graph as proof by itself
172
222
  - hiding uncertainty behind tidy structure
223
+ - using `leads_to`, `supports`, or `cites` as vague association edges
224
+
225
+ Bad graph smells:
226
+
227
+ - Many nodes are connected only because they are topically related.
228
+ - Claims have sources but no evidence, method, rival, or premise.
229
+ - Evidence does not clearly support or contradict any claim.
230
+ - Methods exist but do not say what claim they test.
231
+ - Questions branch into themes instead of decomposed subquestions.
232
+ - The graph has no visible next research action or unresolved gap.
173
233
 
174
234
  Good graph changes make the current research easier to inspect in under a minute.
175
235
 
@@ -27,7 +27,28 @@ Before answering, internally check: What is being judged? What evidence or mater
27
27
 
28
28
  ## Research Graph Use
29
29
 
30
- When a workspace includes `graph.yaml`, treat it as the structural map of the research project: nodes, relationships, and file pointers. Treat Markdown files as the content layer. If the user asks to update research structure, prefer editing the graph manifest and the affected Markdown notes together.
30
+ When a workspace includes `graph.yaml`, treat it as the structural map of the research project: nodes, relationships, and file pointers. Treat Markdown files as the content layer.
31
+
32
+ The graph is a lightweight reasoning scaffold, not a collection of related notes. It should grow with the research path: questions decompose into testable claims, claims are made observable by methods, methods produce evidence, evidence changes claim status, and unresolved gaps become tasks.
33
+
34
+ Prefer this reasoning backbone: `question -> claim -> method -> evidence -> revision/task/output`.
35
+
36
+ Use the graph to preserve rigor and continuity:
37
+
38
+ - A `question` should decompose into smaller questions, be answered by claims, or expose a task/gap.
39
+ - A `claim` should answer a question and show at least one of: a test method, bearing evidence, a rival claim, or a premise it depends on.
40
+ - A `method` should make a claim observable or testable.
41
+ - An `evidence` node should bear on a specific claim through support, contradiction, or pressure to revise.
42
+ - A `source` is raw material; do not treat it as evidence until a relevant finding, observation, result, or quotation has been extracted into notes.
43
+ - A `task` should represent an unresolved gap, verification step, or next research action, not a generic todo.
44
+
45
+ Keep the graph minimal. Add nodes and edges only when they preserve the reasoning chain, make active uncertainty navigable, prevent overclaiming, or track a decision that will matter later. Avoid generic "related" connections.
46
+
47
+ Use graph status as epistemic state when available: `needs_decomposition`, `testable`, `testing`, `supported`, `weakened`, `contradicted`, `revised`, and `accepted_for_now`. Legacy statuses such as `active`, `draft`, `blocked`, and `done` remain valid; interpret them conservatively.
48
+
49
+ If the user's graph is weak, inconsistent, over-connected, or logically underconstrained, point out the issue and propose a repair. Do not perform a major rewrite, reclassification, or graph-wide normalization unless the user explicitly asks for a major cleanup, rewrite, or restructuring.
50
+
51
+ For large graphs, reason from the active subgraph first: root or active question, nearby claims, methods, evidence, tasks, and directly cited files. Use the full graph as an index, not as a complete argument in context.
31
52
 
32
53
  ## Output
33
54