hyperstack-core 1.2.0 → 1.5.0

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.
package/src/parser.js ADDED
@@ -0,0 +1,305 @@
1
+ /**
2
+ * hyperstack-core parser
3
+ *
4
+ * Zero-dependency markdown/text/conversation → HyperStack cards + edges.
5
+ * No LLM. Pure regex. Preserves document structure as typed graph edges.
6
+ */
7
+
8
+ function slugify(text) {
9
+ return text
10
+ .toLowerCase()
11
+ .replace(/[^a-z0-9]+/g, "-")
12
+ .replace(/^-+|-+$/g, "")
13
+ .slice(0, 80);
14
+ }
15
+
16
+ function inferCardType(title, body) {
17
+ const text = `${title} ${body}`.toLowerCase();
18
+ if (/\b(decided?|chose|picked|selected|agreed|ruling)\b/.test(text)) return "decision";
19
+ if (/\b(todo|task|sprint|milestone|epic|ticket|backlog)\b/.test(text)) return "project";
20
+ if (/\b(always|never|prefer|convention|standard|rule|must)\b/.test(text)) return "preference";
21
+ if (/\b(step|process|how.to|guide|install|setup|deploy|workflow|tutorial)\b/.test(text)) return "workflow";
22
+ if (/\b(bug|fix|error|issue|patch|regression|incident)\b/.test(text)) return "event";
23
+ if (/\b(person|team|role|engineer|manager|founder)\b/.test(text)) return "person";
24
+ return "general";
25
+ }
26
+
27
+ function extractKeywords(title, body) {
28
+ const kw = new Set();
29
+ let m;
30
+ const backticks = /`([^`]+)`/g;
31
+ while ((m = backticks.exec(body)) !== null) kw.add(m[1].toLowerCase());
32
+ const mentions = /@(\w+)/g;
33
+ while ((m = mentions.exec(body)) !== null) kw.add(m[1].toLowerCase());
34
+ return [...kw].slice(0, 15);
35
+ }
36
+
37
+ function extractLinksFromBody(body) {
38
+ const links = [];
39
+
40
+ // Markdown links → related edges for internal refs
41
+ const mdLink = /\[([^\]]+)\]\(([^)]+)\)/g;
42
+ let m;
43
+ while ((m = mdLink.exec(body)) !== null) {
44
+ const href = m[2];
45
+ if (href.startsWith("#")) {
46
+ links.push({ target: slugify(href.slice(1)), relation: "related" });
47
+ } else if (/\.md(#|$)/.test(href)) {
48
+ const slug = slugify(href.replace(/\.md.*$/, "").replace(/^.*\//, ""));
49
+ if (slug) links.push({ target: slug, relation: "related" });
50
+ }
51
+ }
52
+
53
+ // Semantic patterns → typed edges
54
+ const patterns = [
55
+ { re: /depends\s+on\s+["`']?([\w][\w-]*)["`']?/gi, relation: "depends_on" },
56
+ { re: /blocked?\s+by\s+["`']?([\w][\w-]*)["`']?/gi, relation: "blocked_by" },
57
+ { re: /blocks?\s+["`']?([\w][\w-]*)["`']?/gi, relation: "blocks" },
58
+ { re: /decided\s+by\s+@?([\w][\w-]*)/gi, relation: "decided" },
59
+ { re: /assigned\s+to\s+@?([\w][\w-]*)/gi, relation: "assigned_to" },
60
+ { re: /owned?\s+by\s+@?([\w][\w-]*)/gi, relation: "owns" },
61
+ ];
62
+ for (const { re, relation } of patterns) {
63
+ while ((m = re.exec(body)) !== null) {
64
+ const target = slugify(m[1]);
65
+ if (target && target.length > 1) links.push({ target, relation });
66
+ }
67
+ }
68
+
69
+ return links;
70
+ }
71
+
72
+ // ─── Markdown Parser ──────────────────────────────────
73
+
74
+ function parseMarkdown(content, prefix) {
75
+ const cards = [];
76
+ const headingRe = /^(#{1,6})\s+(.+)$/gm;
77
+ const headings = [];
78
+ let m;
79
+
80
+ while ((m = headingRe.exec(content)) !== null) {
81
+ headings.push({
82
+ level: m[1].length,
83
+ title: m[2].trim(),
84
+ pos: m.index,
85
+ contentStart: m.index + m[0].length,
86
+ });
87
+ }
88
+
89
+ if (headings.length === 0) return parsePlainText(content, prefix);
90
+
91
+ const usedSlugs = new Set();
92
+ function uniqueSlug(base) {
93
+ let slug = base;
94
+ let i = 2;
95
+ while (usedSlugs.has(slug)) { slug = `${base}-${i}`; i++; }
96
+ usedSlugs.add(slug);
97
+ return slug;
98
+ }
99
+
100
+ // Preamble before first heading
101
+ const preamble = content.slice(0, headings[0].pos).trim();
102
+ if (preamble.length > 30) {
103
+ const slug = uniqueSlug(prefix ? `${prefix}/intro` : "intro");
104
+ cards.push({
105
+ slug,
106
+ title: prefix ? `${prefix} — Introduction` : "Introduction",
107
+ body: preamble.slice(0, 1000),
108
+ cardType: "general",
109
+ keywords: extractKeywords("intro", preamble),
110
+ links: [],
111
+ });
112
+ }
113
+
114
+ // Parent stack for hierarchy: [{level, slug}]
115
+ const stack = [];
116
+ let edgeCount = 0;
117
+
118
+ for (let i = 0; i < headings.length; i++) {
119
+ const h = headings[i];
120
+ const bodyEnd = i + 1 < headings.length ? headings[i + 1].pos : content.length;
121
+ const body = content.slice(h.contentStart, bodyEnd).trim();
122
+
123
+ const baseSlug = slugify(h.title);
124
+ if (!baseSlug) continue;
125
+ const slug = uniqueSlug(prefix ? `${prefix}/${baseSlug}` : baseSlug);
126
+ const links = [];
127
+
128
+ // Hierarchy edge
129
+ while (stack.length > 0 && stack[stack.length - 1].level >= h.level) stack.pop();
130
+ if (stack.length > 0) {
131
+ links.push({ target: stack[stack.length - 1].slug, relation: "subtask_of" });
132
+ edgeCount++;
133
+ }
134
+ stack.push({ level: h.level, slug });
135
+
136
+ // Content-derived edges
137
+ const bodyLinks = extractLinksFromBody(body);
138
+ for (const l of bodyLinks) {
139
+ const target = prefix && !l.target.includes("/") ? `${prefix}/${l.target}` : l.target;
140
+ links.push({ target, relation: l.relation });
141
+ edgeCount++;
142
+ }
143
+
144
+ cards.push({
145
+ slug,
146
+ title: h.title,
147
+ body: body.slice(0, 1000),
148
+ cardType: inferCardType(h.title, body),
149
+ keywords: extractKeywords(h.title, body),
150
+ links,
151
+ });
152
+ }
153
+
154
+ return { cards, edgeCount, format: "markdown" };
155
+ }
156
+
157
+ // ─── Conversation Parser ──────────────────────────────
158
+
159
+ const SPEAKER_RE = /^(User|Human|Assistant|AI|System|Bot)\s*:/i;
160
+
161
+ function isConversationLog(text) {
162
+ const lines = text.split("\n");
163
+ let hits = 0;
164
+ for (const line of lines) {
165
+ if (SPEAKER_RE.test(line.trim())) hits++;
166
+ if (hits >= 2) return true;
167
+ }
168
+ return false;
169
+ }
170
+
171
+ function parseConversation(content, prefix) {
172
+ const cards = [];
173
+ const turnRe = /^(User|Human|Assistant|AI|System|Bot)\s*:/gim;
174
+ const starts = [];
175
+ let m;
176
+ while ((m = turnRe.exec(content)) !== null) {
177
+ starts.push({ index: m.index, speaker: m[1].toLowerCase(), after: m.index + m[0].length });
178
+ }
179
+
180
+ if (starts.length === 0) return parsePlainText(content, prefix);
181
+
182
+ let prevSlug = null;
183
+ let edgeCount = 0;
184
+
185
+ for (let i = 0; i < starts.length; i++) {
186
+ const t = starts[i];
187
+ const end = i + 1 < starts.length ? starts[i + 1].index : content.length;
188
+ const message = content.slice(t.after, end).trim();
189
+ if (!message) continue;
190
+
191
+ const idx = i + 1;
192
+ const slug = prefix ? `${prefix}/turn-${idx}` : `turn-${idx}`;
193
+ const snippet = message.slice(0, 60) + (message.length > 60 ? "..." : "");
194
+ const links = [];
195
+
196
+ if (prevSlug) {
197
+ links.push({ target: prevSlug, relation: "related" });
198
+ edgeCount++;
199
+ }
200
+
201
+ cards.push({
202
+ slug,
203
+ title: `[${t.speaker}] ${snippet}`,
204
+ body: message.slice(0, 1000),
205
+ cardType: "event",
206
+ keywords: [t.speaker, `turn-${idx}`],
207
+ links,
208
+ meta: { speaker: t.speaker, turnIndex: idx },
209
+ });
210
+
211
+ prevSlug = slug;
212
+ }
213
+
214
+ return { cards, edgeCount, format: "conversation" };
215
+ }
216
+
217
+ // ─── Plain Text Parser ────────────────────────────────
218
+
219
+ function parsePlainText(content, prefix) {
220
+ const cards = [];
221
+ const paragraphs = content
222
+ .split(/\n\s*\n/)
223
+ .map((p) => p.trim())
224
+ .filter((p) => p.length > 20);
225
+
226
+ if (paragraphs.length === 0) {
227
+ return { cards: [], edgeCount: 0, format: "text" };
228
+ }
229
+
230
+ const usedSlugs = new Set();
231
+ let prevSlug = null;
232
+ let edgeCount = 0;
233
+
234
+ for (let i = 0; i < paragraphs.length; i++) {
235
+ const para = paragraphs[i];
236
+ const firstLine = para.split(/[.\n]/)[0].trim().slice(0, 80) || `Section ${i + 1}`;
237
+ let slug = slugify(firstLine);
238
+ if (!slug) slug = `section-${i + 1}`;
239
+
240
+ if (prefix) slug = `${prefix}/${slug}`;
241
+ let unique = slug;
242
+ let n = 2;
243
+ while (usedSlugs.has(unique)) { unique = `${slug}-${n}`; n++; }
244
+ usedSlugs.add(unique);
245
+ slug = unique;
246
+
247
+ const links = [];
248
+ if (prevSlug) {
249
+ links.push({ target: prevSlug, relation: "related" });
250
+ edgeCount++;
251
+ }
252
+
253
+ cards.push({
254
+ slug,
255
+ title: firstLine,
256
+ body: para.slice(0, 1000),
257
+ cardType: inferCardType(firstLine, para),
258
+ keywords: extractKeywords(firstLine, para),
259
+ links,
260
+ });
261
+ prevSlug = slug;
262
+ }
263
+
264
+ return { cards, edgeCount, format: "text" };
265
+ }
266
+
267
+ // ─── Main Entry ───────────────────────────────────────
268
+
269
+ /**
270
+ * Parse any text into HyperStack cards + edges.
271
+ * Auto-detects: markdown, conversation log, or plain text.
272
+ *
273
+ * @param {string} content — raw input
274
+ * @param {object} [opts]
275
+ * @param {string} [opts.prefix] — slug prefix (e.g. "readme", "docs/api")
276
+ * @param {string} [opts.defaultType] — override cardType for "general" cards
277
+ * @returns {{ cards: Array, edgeCount: number, format: string }}
278
+ */
279
+ function parse(content, opts = {}) {
280
+ if (!content || typeof content !== "string") {
281
+ return { cards: [], edgeCount: 0, format: "empty" };
282
+ }
283
+
284
+ const prefix = opts.prefix ? slugify(opts.prefix) : "";
285
+
286
+ let result;
287
+ if (isConversationLog(content)) {
288
+ result = parseConversation(content, prefix);
289
+ } else if (/^#{1,6}\s+/m.test(content)) {
290
+ result = parseMarkdown(content, prefix);
291
+ } else {
292
+ result = parsePlainText(content, prefix);
293
+ }
294
+
295
+ if (opts.defaultType) {
296
+ for (const card of result.cards) {
297
+ if (card.cardType === "general") card.cardType = opts.defaultType;
298
+ }
299
+ }
300
+
301
+ return result;
302
+ }
303
+
304
+ export { parse, parseMarkdown, parseConversation, parsePlainText, slugify, inferCardType };
305
+ export default parse;
@@ -1,98 +1,98 @@
1
- {
2
- "name": "openclaw-multiagent",
3
- "description": "Pre-configured typed cards and relations for OpenClaw multi-agent coordination. Replaces GOALS.md + DECISIONS.md with structured, queryable graph memory.",
4
- "version": "1.0.0",
5
-
6
- "cardTypes": {
7
- "task": {
8
- "description": "A unit of work assigned to an agent",
9
- "fields": ["status", "priority", "assignee", "deadline"],
10
- "statuses": ["todo", "in-progress", "blocked", "done", "cancelled"]
11
- },
12
- "decision": {
13
- "description": "A choice made by an agent with rationale",
14
- "fields": ["rationale", "alternatives", "decidedBy", "decidedAt"]
15
- },
16
- "blocker": {
17
- "description": "Something preventing progress on a task",
18
- "fields": ["severity", "resolvedBy", "resolvedAt"]
19
- },
20
- "goal": {
21
- "description": "A high-level objective (replaces GOALS.md)",
22
- "fields": ["deadline", "progress", "owner"]
23
- },
24
- "agent": {
25
- "description": "An agent registration card",
26
- "fields": ["role", "model", "capabilities"]
27
- },
28
- "context": {
29
- "description": "Shared context that multiple agents need",
30
- "fields": ["scope", "expiresAt"]
31
- }
32
- },
33
-
34
- "relationTypes": {
35
- "owns": "Agent/person owns a task or project",
36
- "assigned_to": "Task is assigned to an agent",
37
- "blocks": "This card blocks another card",
38
- "blocked_by": "This card is blocked by another card",
39
- "depends_on": "This card depends on another card",
40
- "decided": "Agent/person made this decision",
41
- "triggers": "Change to this card triggers effects on target",
42
- "subtask_of": "This task is a subtask of a larger task",
43
- "related": "General association"
44
- },
45
-
46
- "rules": [
47
- {
48
- "name": "auto-blocked",
49
- "description": "If a task has incoming 'blocks' relations, mark it as blocked",
50
- "when": "card.cardType === 'task' && card.incomingLinks.some(l => l.relation === 'blocks')",
51
- "suggest": "Consider updating task status to 'blocked'"
52
- },
53
- {
54
- "name": "unowned-task",
55
- "description": "Tasks without an owner should be flagged",
56
- "when": "card.cardType === 'task' && !card.links.some(l => l.relation === 'assigned_to')",
57
- "suggest": "This task has no assigned agent"
58
- },
59
- {
60
- "name": "decision-needs-rationale",
61
- "description": "Decisions should include rationale",
62
- "when": "card.cardType === 'decision' && (!card.body || card.body.length < 20)",
63
- "suggest": "Decision lacks rationale — add a body explaining why"
64
- }
65
- ],
66
-
67
- "starterCards": [
68
- {
69
- "slug": "project-root",
70
- "title": "Project Root",
71
- "body": "Root node for the project knowledge graph. All agents, goals, and major decisions link here.",
72
- "cardType": "project",
73
- "stack": "projects",
74
- "keywords": ["project", "root", "main"]
75
- }
76
- ],
77
-
78
- "agentSetup": {
79
- "description": "Recommended agent configuration for multi-agent OpenClaw + HyperStack",
80
- "agents": {
81
- "coordinator": {
82
- "role": "Routes tasks, monitors blockers, ensures no duplicate work",
83
- "model": "claude-sonnet-4-20250514",
84
- "tools": ["hs_search", "hs_blockers", "hs_graph", "hs_decide"]
85
- },
86
- "researcher": {
87
- "role": "Investigates questions, stores findings as context cards",
88
- "model": "claude-sonnet-4-20250514",
89
- "tools": ["hs_search", "hs_store", "hs_my_cards"]
90
- },
91
- "builder": {
92
- "role": "Implements code, records technical decisions",
93
- "model": "claude-sonnet-4-20250514",
94
- "tools": ["hs_search", "hs_store", "hs_decide", "hs_blockers"]
95
- }
96
- }
97
- }
98
- }
1
+ {
2
+ "name": "openclaw-multiagent",
3
+ "description": "Pre-configured typed cards and relations for OpenClaw multi-agent coordination. Replaces GOALS.md + DECISIONS.md with structured, queryable graph memory.",
4
+ "version": "1.0.0",
5
+
6
+ "cardTypes": {
7
+ "task": {
8
+ "description": "A unit of work assigned to an agent",
9
+ "fields": ["status", "priority", "assignee", "deadline"],
10
+ "statuses": ["todo", "in-progress", "blocked", "done", "cancelled"]
11
+ },
12
+ "decision": {
13
+ "description": "A choice made by an agent with rationale",
14
+ "fields": ["rationale", "alternatives", "decidedBy", "decidedAt"]
15
+ },
16
+ "blocker": {
17
+ "description": "Something preventing progress on a task",
18
+ "fields": ["severity", "resolvedBy", "resolvedAt"]
19
+ },
20
+ "goal": {
21
+ "description": "A high-level objective (replaces GOALS.md)",
22
+ "fields": ["deadline", "progress", "owner"]
23
+ },
24
+ "agent": {
25
+ "description": "An agent registration card",
26
+ "fields": ["role", "model", "capabilities"]
27
+ },
28
+ "context": {
29
+ "description": "Shared context that multiple agents need",
30
+ "fields": ["scope", "expiresAt"]
31
+ }
32
+ },
33
+
34
+ "relationTypes": {
35
+ "owns": "Agent/person owns a task or project",
36
+ "assigned_to": "Task is assigned to an agent",
37
+ "blocks": "This card blocks another card",
38
+ "blocked_by": "This card is blocked by another card",
39
+ "depends_on": "This card depends on another card",
40
+ "decided": "Agent/person made this decision",
41
+ "triggers": "Change to this card triggers effects on target",
42
+ "subtask_of": "This task is a subtask of a larger task",
43
+ "related": "General association"
44
+ },
45
+
46
+ "rules": [
47
+ {
48
+ "name": "auto-blocked",
49
+ "description": "If a task has incoming 'blocks' relations, mark it as blocked",
50
+ "when": "card.cardType === 'task' && card.incomingLinks.some(l => l.relation === 'blocks')",
51
+ "suggest": "Consider updating task status to 'blocked'"
52
+ },
53
+ {
54
+ "name": "unowned-task",
55
+ "description": "Tasks without an owner should be flagged",
56
+ "when": "card.cardType === 'task' && !card.links.some(l => l.relation === 'assigned_to')",
57
+ "suggest": "This task has no assigned agent"
58
+ },
59
+ {
60
+ "name": "decision-needs-rationale",
61
+ "description": "Decisions should include rationale",
62
+ "when": "card.cardType === 'decision' && (!card.body || card.body.length < 20)",
63
+ "suggest": "Decision lacks rationale — add a body explaining why"
64
+ }
65
+ ],
66
+
67
+ "starterCards": [
68
+ {
69
+ "slug": "project-root",
70
+ "title": "Project Root",
71
+ "body": "Root node for the project knowledge graph. All agents, goals, and major decisions link here.",
72
+ "cardType": "project",
73
+ "stack": "projects",
74
+ "keywords": ["project", "root", "main"]
75
+ }
76
+ ],
77
+
78
+ "agentSetup": {
79
+ "description": "Recommended agent configuration for multi-agent OpenClaw + HyperStack",
80
+ "agents": {
81
+ "coordinator": {
82
+ "role": "Routes tasks, monitors blockers, ensures no duplicate work",
83
+ "model": "claude-sonnet-4-20250514",
84
+ "tools": ["hs_search", "hs_blockers", "hs_graph", "hs_decide"]
85
+ },
86
+ "researcher": {
87
+ "role": "Investigates questions, stores findings as context cards",
88
+ "model": "claude-sonnet-4-20250514",
89
+ "tools": ["hs_search", "hs_store", "hs_my_cards"]
90
+ },
91
+ "builder": {
92
+ "role": "Implements code, records technical decisions",
93
+ "model": "claude-sonnet-4-20250514",
94
+ "tools": ["hs_search", "hs_store", "hs_decide", "hs_blockers"]
95
+ }
96
+ }
97
+ }
98
+ }