@aiwayds/dsh-topics-memory 0.6.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/README.md +112 -0
- package/README.zh.md +112 -0
- package/cordis.patch.yml +8 -0
- package/lib/commands.d.ts +21 -0
- package/lib/commands.js +368 -0
- package/lib/config.d.ts +122 -0
- package/lib/config.js +123 -0
- package/lib/delegation.d.ts +23 -0
- package/lib/delegation.js +21 -0
- package/lib/digest.d.ts +42 -0
- package/lib/digest.js +101 -0
- package/lib/distill.d.ts +186 -0
- package/lib/distill.js +759 -0
- package/lib/git.d.ts +66 -0
- package/lib/git.js +169 -0
- package/lib/ilog.d.ts +53 -0
- package/lib/ilog.js +70 -0
- package/lib/index.d.ts +73 -0
- package/lib/index.js +504 -0
- package/lib/observer.d.ts +41 -0
- package/lib/observer.js +128 -0
- package/lib/okf.d.ts +112 -0
- package/lib/okf.js +279 -0
- package/lib/onboard.d.ts +205 -0
- package/lib/onboard.js +664 -0
- package/lib/paths.d.ts +48 -0
- package/lib/paths.js +84 -0
- package/lib/retrieval.d.ts +61 -0
- package/lib/retrieval.js +178 -0
- package/lib/service.d.ts +96 -0
- package/lib/service.js +375 -0
- package/lib/store.d.ts +153 -0
- package/lib/store.js +489 -0
- package/lib/sync.d.ts +52 -0
- package/lib/sync.js +154 -0
- package/lib/tools.d.ts +8 -0
- package/lib/tools.js +208 -0
- package/lib/viz.d.ts +33 -0
- package/lib/viz.js +310 -0
- package/lib/yaml.d.ts +18 -0
- package/lib/yaml.js +334 -0
- package/package.json +97 -0
- package/src/commands.ts +397 -0
- package/src/config.ts +149 -0
- package/src/delegation.ts +27 -0
- package/src/digest.ts +125 -0
- package/src/distill.ts +885 -0
- package/src/git.ts +209 -0
- package/src/ilog.ts +99 -0
- package/src/index.ts +574 -0
- package/src/observer.ts +150 -0
- package/src/okf.ts +317 -0
- package/src/onboard.ts +762 -0
- package/src/paths.ts +104 -0
- package/src/retrieval.ts +219 -0
- package/src/service.ts +400 -0
- package/src/store.ts +567 -0
- package/src/sync.ts +170 -0
- package/src/tools.ts +225 -0
- package/src/viz.ts +333 -0
- package/src/yaml.ts +322 -0
package/lib/tools.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Model-facing tools (ADR 0004 explicit channel): `topic_save`, `topic_search`,
|
|
3
|
+
* `topic_observe`, `topic_history`.
|
|
4
|
+
*
|
|
5
|
+
* @module tools
|
|
6
|
+
*/
|
|
7
|
+
import type { TopicsService } from './service.ts';
|
|
8
|
+
export declare function buildTopicTools(service: TopicsService): import("@deepseek-ai/dsh-tools").ToolDefinition[];
|
package/lib/tools.js
ADDED
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Model-facing tools (ADR 0004 explicit channel): `topic_save`, `topic_search`,
|
|
3
|
+
* `topic_observe`, `topic_history`.
|
|
4
|
+
*
|
|
5
|
+
* @module tools
|
|
6
|
+
*/
|
|
7
|
+
import { defineTool } from '@deepseek-ai/dsh-tools';
|
|
8
|
+
const STRING_ARRAY = { type: 'array', items: { type: 'string' } };
|
|
9
|
+
function preview(text, max = 240) {
|
|
10
|
+
const line = text.split('\n').find((l) => l.trim() !== '') ?? '';
|
|
11
|
+
return line.length <= max ? line : `${line.slice(0, max)}…`;
|
|
12
|
+
}
|
|
13
|
+
export function buildTopicTools(service) {
|
|
14
|
+
const topicSave = defineTool({
|
|
15
|
+
name: 'topic_save',
|
|
16
|
+
description: 'Create or update one Topic in the long-term topic memory (OKF bundle). ' +
|
|
17
|
+
'Use when a durable conclusion crystallizes: the topic name, what it depends on, open questions, ' +
|
|
18
|
+
'the current conclusion, its impact, and actionable recommendations. ' +
|
|
19
|
+
'Pass `slug` to update an existing topic; omit it to create a new one.',
|
|
20
|
+
parameters: {
|
|
21
|
+
title: { type: 'string', required: true, description: 'Human-readable topic name (immutable identity; renaming creates a new topic)' },
|
|
22
|
+
conclusion: { type: 'string', required: true, description: 'The current best conclusion, self-contained markdown prose' },
|
|
23
|
+
description: { type: 'string', description: 'One-line summary for indexes and snippets' },
|
|
24
|
+
tags: { ...STRING_ARRAY, description: 'Cross-cutting tags (project, domain, component); lowercased on save' },
|
|
25
|
+
depends: { ...STRING_ARRAY, description: 'Slugs of prerequisite topics (topics this one builds on)' },
|
|
26
|
+
open_questions: { ...STRING_ARRAY, description: 'Unresolved questions this topic still carries' },
|
|
27
|
+
impact: { ...STRING_ARRAY, description: 'What this conclusion affects: topics, projects, decisions' },
|
|
28
|
+
recommendations: { type: 'string', description: 'Actionable recommendation following from the conclusion' },
|
|
29
|
+
status: { type: 'string', description: 'Lifecycle: draft (default) | stable | deprecated' },
|
|
30
|
+
slug: { type: 'string', description: 'Existing topic slug to update; omit to create' },
|
|
31
|
+
},
|
|
32
|
+
output: {
|
|
33
|
+
schema: {
|
|
34
|
+
type: 'object',
|
|
35
|
+
additionalProperties: false,
|
|
36
|
+
properties: {
|
|
37
|
+
slug: { type: 'string', required: true },
|
|
38
|
+
path: { type: 'string', required: true },
|
|
39
|
+
created: { type: 'boolean', required: true },
|
|
40
|
+
committed: { type: 'boolean', required: true },
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
render: (_args, value) => [
|
|
44
|
+
{
|
|
45
|
+
type: 'text',
|
|
46
|
+
text: `${value.created ? 'Created' : 'Updated'} topic \`${value.slug}\` → ${value.path}${value.committed ? ' (committed)' : ''}`,
|
|
47
|
+
},
|
|
48
|
+
],
|
|
49
|
+
},
|
|
50
|
+
async execute(args) {
|
|
51
|
+
const result = await service.saveTopic({
|
|
52
|
+
title: args.title,
|
|
53
|
+
conclusion: args.conclusion,
|
|
54
|
+
description: args.description,
|
|
55
|
+
tags: args.tags,
|
|
56
|
+
depends: args.depends,
|
|
57
|
+
openQuestions: args.open_questions,
|
|
58
|
+
impact: args.impact,
|
|
59
|
+
recommendations: args.recommendations,
|
|
60
|
+
status: args.status === 'draft' || args.status === 'stable' || args.status === 'deprecated' ? args.status : undefined,
|
|
61
|
+
slug: args.slug,
|
|
62
|
+
source: 'model',
|
|
63
|
+
});
|
|
64
|
+
return { slug: result.slug, path: result.path, created: result.created, committed: result.committed };
|
|
65
|
+
},
|
|
66
|
+
});
|
|
67
|
+
const topicSearch = defineTool({
|
|
68
|
+
name: 'topic_search',
|
|
69
|
+
description: 'Search the long-term topic memory by keywords (hot path, no LLM). ' +
|
|
70
|
+
'Use when the user references past work, or when injected topic memory hints at related history.',
|
|
71
|
+
parameters: {
|
|
72
|
+
query: { type: 'string', required: true, description: 'What to recall from topic memory' },
|
|
73
|
+
top_k: { type: 'number', description: 'Max results (default 8)' },
|
|
74
|
+
},
|
|
75
|
+
output: {
|
|
76
|
+
schema: {
|
|
77
|
+
type: 'object',
|
|
78
|
+
additionalProperties: false,
|
|
79
|
+
properties: {
|
|
80
|
+
results: {
|
|
81
|
+
type: 'array',
|
|
82
|
+
required: true,
|
|
83
|
+
items: {
|
|
84
|
+
type: 'object',
|
|
85
|
+
additionalProperties: false,
|
|
86
|
+
properties: {
|
|
87
|
+
slug: { type: 'string', required: true },
|
|
88
|
+
title: { type: 'string', required: true },
|
|
89
|
+
status: { type: 'string', required: true },
|
|
90
|
+
score: { type: 'number', required: true },
|
|
91
|
+
preview: { type: 'string', required: true },
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
render: (_args, value) => [
|
|
98
|
+
{
|
|
99
|
+
type: 'text',
|
|
100
|
+
text: value.results.length === 0
|
|
101
|
+
? 'No topic memory found for this query.'
|
|
102
|
+
: value.results
|
|
103
|
+
.map((r) => `### ${r.title} [${r.status}] (${r.slug} score:${r.score.toFixed(2)})\n${r.preview}`)
|
|
104
|
+
.join('\n\n'),
|
|
105
|
+
},
|
|
106
|
+
],
|
|
107
|
+
},
|
|
108
|
+
async execute(args) {
|
|
109
|
+
const outcome = await service.search(args.query, args.top_k);
|
|
110
|
+
const roster = new Map((await service.roster()).map((r) => [r.slug, r]));
|
|
111
|
+
const docs = await Promise.all(outcome.hits.map(async (h) => {
|
|
112
|
+
const doc = await service.store.readTopic(h.slug).catch(() => undefined);
|
|
113
|
+
return { hit: h, doc };
|
|
114
|
+
}));
|
|
115
|
+
return {
|
|
116
|
+
results: docs.flatMap(({ hit, doc }) => {
|
|
117
|
+
const meta = roster.get(hit.slug);
|
|
118
|
+
if (doc === undefined || meta === undefined)
|
|
119
|
+
return [];
|
|
120
|
+
const conclusion = extractConclusion(doc.body);
|
|
121
|
+
return [
|
|
122
|
+
{
|
|
123
|
+
slug: hit.slug,
|
|
124
|
+
title: doc.fm.title,
|
|
125
|
+
status: doc.fm.status,
|
|
126
|
+
score: hit.score,
|
|
127
|
+
preview: preview(conclusion !== '' ? conclusion : (doc.fm.description ?? '')),
|
|
128
|
+
},
|
|
129
|
+
];
|
|
130
|
+
}),
|
|
131
|
+
};
|
|
132
|
+
},
|
|
133
|
+
});
|
|
134
|
+
const topicObserve = defineTool({
|
|
135
|
+
name: 'topic_observe',
|
|
136
|
+
description: 'Record one atomic observation (a decision, finding, constraint, or open question) as raw material ' +
|
|
137
|
+
'for later distillation into topic memory. Cheap and safe to call mid-task; prefer topic_save when a ' +
|
|
138
|
+
'full conclusion is already settled.',
|
|
139
|
+
parameters: {
|
|
140
|
+
kind: { type: 'string', required: true, description: 'One of: decision | finding | constraint | question' },
|
|
141
|
+
text: { type: 'string', required: true, description: 'The observation, one atomic statement' },
|
|
142
|
+
},
|
|
143
|
+
output: {
|
|
144
|
+
schema: {
|
|
145
|
+
type: 'object',
|
|
146
|
+
additionalProperties: false,
|
|
147
|
+
properties: { id: { type: 'string', required: true } },
|
|
148
|
+
},
|
|
149
|
+
render: (_args, value) => [{ type: 'text', text: `Observed (${value.id})` }],
|
|
150
|
+
},
|
|
151
|
+
async execute(args) {
|
|
152
|
+
const kind = args.kind === 'decision' || args.kind === 'finding' || args.kind === 'constraint' || args.kind === 'question' ? args.kind : 'finding';
|
|
153
|
+
const obs = await service.observe({ kind, text: args.text });
|
|
154
|
+
return { id: obs.id };
|
|
155
|
+
},
|
|
156
|
+
});
|
|
157
|
+
const topicHistory = defineTool({
|
|
158
|
+
name: 'topic_history',
|
|
159
|
+
description: 'Show the git history of one topic — how its conclusion changed over time, commit by commit. ' +
|
|
160
|
+
'Use when the user asks when/why a conclusion changed or wants to trace a decision.',
|
|
161
|
+
parameters: {
|
|
162
|
+
slug: { type: 'string', required: true, description: 'Topic slug' },
|
|
163
|
+
limit: { type: 'number', description: 'Max commits (default 20)' },
|
|
164
|
+
},
|
|
165
|
+
output: {
|
|
166
|
+
schema: {
|
|
167
|
+
type: 'object',
|
|
168
|
+
additionalProperties: false,
|
|
169
|
+
properties: {
|
|
170
|
+
entries: {
|
|
171
|
+
type: 'array',
|
|
172
|
+
required: true,
|
|
173
|
+
items: {
|
|
174
|
+
type: 'object',
|
|
175
|
+
additionalProperties: false,
|
|
176
|
+
properties: {
|
|
177
|
+
hash: { type: 'string', required: true },
|
|
178
|
+
date: { type: 'string', required: true },
|
|
179
|
+
message: { type: 'string', required: true },
|
|
180
|
+
conclusion: { type: 'string' },
|
|
181
|
+
},
|
|
182
|
+
},
|
|
183
|
+
},
|
|
184
|
+
},
|
|
185
|
+
},
|
|
186
|
+
render: (_args, value) => [
|
|
187
|
+
{
|
|
188
|
+
type: 'text',
|
|
189
|
+
text: value.entries.length === 0
|
|
190
|
+
? 'No history (topic unknown or bundle not a git repo).'
|
|
191
|
+
: value.entries
|
|
192
|
+
.map((e) => `- ${e.hash} ${e.date} ${e.message}${e.conclusion !== undefined ? `\n 结论当时: ${e.conclusion}` : ''}`)
|
|
193
|
+
.join('\n'),
|
|
194
|
+
},
|
|
195
|
+
],
|
|
196
|
+
},
|
|
197
|
+
async execute(args) {
|
|
198
|
+
const { entries } = await service.history(args.slug, args.limit ?? 20);
|
|
199
|
+
return { entries: entries.map((e) => ({ ...e, conclusion: e.conclusion })) };
|
|
200
|
+
},
|
|
201
|
+
});
|
|
202
|
+
return [topicSave, topicSearch, topicObserve, topicHistory];
|
|
203
|
+
}
|
|
204
|
+
function extractConclusion(body) {
|
|
205
|
+
// Local copy to avoid importing okf section helpers into the hot render path.
|
|
206
|
+
const m = /^# Conclusion\s*\n+([\s\S]*?)(?=\n# |\s*$)/im.exec(body);
|
|
207
|
+
return m === null ? '' : m[1].trim();
|
|
208
|
+
}
|
package/lib/viz.d.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bundle relationship graph visualization (ADR 0005 graph-shaped bundle).
|
|
3
|
+
*
|
|
4
|
+
* `buildGraph` turns the roster into nodes + edges (`depends` + body links);
|
|
5
|
+
* `renderGraphHtml` emits a fully self-contained HTML page — hand-rolled
|
|
6
|
+
* force-directed SVG, zero external scripts, works offline in any browser.
|
|
7
|
+
* The graph JSON is embedded as `application/json` with `<` escaped so a
|
|
8
|
+
* topic title can never break out of the script tag.
|
|
9
|
+
*
|
|
10
|
+
* @module viz
|
|
11
|
+
*/
|
|
12
|
+
import type { RetrievableTopic } from './retrieval.ts';
|
|
13
|
+
export interface VizNode {
|
|
14
|
+
id: string;
|
|
15
|
+
title: string;
|
|
16
|
+
status: string;
|
|
17
|
+
tags: string[];
|
|
18
|
+
degree: number;
|
|
19
|
+
}
|
|
20
|
+
export interface VizEdge {
|
|
21
|
+
from: string;
|
|
22
|
+
to: string;
|
|
23
|
+
via: 'depends' | 'link';
|
|
24
|
+
}
|
|
25
|
+
export interface VizGraph {
|
|
26
|
+
nodes: VizNode[];
|
|
27
|
+
edges: VizEdge[];
|
|
28
|
+
}
|
|
29
|
+
/** Build the relationship graph: depends edges + body-link edges, no self-loops. */
|
|
30
|
+
export declare function buildGraph(roster: readonly RetrievableTopic[]): VizGraph;
|
|
31
|
+
export declare function renderGraphHtml(graph: VizGraph, opts?: {
|
|
32
|
+
conclusions?: Record<string, string>;
|
|
33
|
+
}): string;
|
package/lib/viz.js
ADDED
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bundle relationship graph visualization (ADR 0005 graph-shaped bundle).
|
|
3
|
+
*
|
|
4
|
+
* `buildGraph` turns the roster into nodes + edges (`depends` + body links);
|
|
5
|
+
* `renderGraphHtml` emits a fully self-contained HTML page — hand-rolled
|
|
6
|
+
* force-directed SVG, zero external scripts, works offline in any browser.
|
|
7
|
+
* The graph JSON is embedded as `application/json` with `<` escaped so a
|
|
8
|
+
* topic title can never break out of the script tag.
|
|
9
|
+
*
|
|
10
|
+
* @module viz
|
|
11
|
+
*/
|
|
12
|
+
/** Build the relationship graph: depends edges + body-link edges, no self-loops. */
|
|
13
|
+
export function buildGraph(roster) {
|
|
14
|
+
const bySlug = new Set(roster.map((r) => r.slug));
|
|
15
|
+
const degree = new Map();
|
|
16
|
+
const bump = (slug) => {
|
|
17
|
+
degree.set(slug, (degree.get(slug) ?? 0) + 1);
|
|
18
|
+
};
|
|
19
|
+
const edges = [];
|
|
20
|
+
const seen = new Set();
|
|
21
|
+
const addEdge = (from, to, via) => {
|
|
22
|
+
if (from === to || !bySlug.has(to))
|
|
23
|
+
return;
|
|
24
|
+
const key = `${from}->${to}:${via}`;
|
|
25
|
+
if (seen.has(key))
|
|
26
|
+
return;
|
|
27
|
+
seen.add(key);
|
|
28
|
+
edges.push({ from, to, via });
|
|
29
|
+
bump(from);
|
|
30
|
+
bump(to);
|
|
31
|
+
};
|
|
32
|
+
for (const topic of roster) {
|
|
33
|
+
for (const dep of topic.depends) {
|
|
34
|
+
const target = dep.replace(/^topics\//, '').replace(/\.md$/, '');
|
|
35
|
+
addEdge(topic.slug, target, 'depends');
|
|
36
|
+
}
|
|
37
|
+
for (const link of topic.links ?? [])
|
|
38
|
+
addEdge(topic.slug, link, 'link');
|
|
39
|
+
}
|
|
40
|
+
const nodes = roster.map((r) => ({
|
|
41
|
+
id: r.slug,
|
|
42
|
+
title: r.title,
|
|
43
|
+
status: r.status,
|
|
44
|
+
tags: r.tags,
|
|
45
|
+
degree: degree.get(r.slug) ?? 0,
|
|
46
|
+
}));
|
|
47
|
+
return { nodes, edges };
|
|
48
|
+
}
|
|
49
|
+
function escapeHtml(s) {
|
|
50
|
+
return s.replaceAll('&', '&').replaceAll('<', '<').replaceAll('>', '>').replaceAll('"', '"');
|
|
51
|
+
}
|
|
52
|
+
/** Embed JSON safely inside a <script> tag. */
|
|
53
|
+
function embedJson(value) {
|
|
54
|
+
return JSON.stringify(value).replaceAll('<', '\\u003c').replaceAll('>', '\\u003e').replaceAll('&', '\\u0026');
|
|
55
|
+
}
|
|
56
|
+
const PAGE = `<!DOCTYPE html>
|
|
57
|
+
<html lang="zh-CN">
|
|
58
|
+
<head>
|
|
59
|
+
<meta charset="utf-8">
|
|
60
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
61
|
+
<title>dsh-topics-memory 关系图</title>
|
|
62
|
+
<style>
|
|
63
|
+
:root { color-scheme: dark; }
|
|
64
|
+
* { box-sizing: border-box; margin: 0; }
|
|
65
|
+
body { background: #101418; color: #d7dee6; font: 14px/1.5 -apple-system, "PingFang SC", "Microsoft YaHei", sans-serif; overflow: hidden; }
|
|
66
|
+
#bar { position: fixed; top: 0; left: 0; right: 0; z-index: 5; display: flex; gap: 14px; align-items: center; padding: 10px 16px; background: rgba(16,20,24,.92); border-bottom: 1px solid #232b33; flex-wrap: wrap; }
|
|
67
|
+
#bar h1 { font-size: 14px; font-weight: 600; color: #8ec7ff; margin-right: 6px; }
|
|
68
|
+
#bar input, #bar select { background: #1a2129; color: #d7dee6; border: 1px solid #2c3742; border-radius: 6px; padding: 4px 10px; font-size: 13px; }
|
|
69
|
+
#bar input { width: 200px; }
|
|
70
|
+
.legend { display: flex; gap: 10px; font-size: 12px; color: #8a97a3; }
|
|
71
|
+
.legend i { display: inline-block; width: 10px; height: 10px; border-radius: 50%; margin-right: 4px; }
|
|
72
|
+
#meta { color: #66737f; font-size: 12px; }
|
|
73
|
+
svg { display: block; width: 100vw; height: 100vh; cursor: grab; }
|
|
74
|
+
svg.dragging { cursor: grabbing; }
|
|
75
|
+
line.edge { stroke: #3a4652; stroke-opacity: .55; }
|
|
76
|
+
line.edge.via-link { stroke-dasharray: 5 4; }
|
|
77
|
+
line.edge.hot { stroke: #f5a623; stroke-opacity: .95; }
|
|
78
|
+
line.edge.dim { stroke-opacity: .08; }
|
|
79
|
+
g.node circle { stroke: #101418; stroke-width: 1.5; cursor: pointer; }
|
|
80
|
+
g.node text { fill: #aeb9c4; font-size: 11px; text-anchor: middle; pointer-events: none; paint-order: stroke; stroke: #101418; stroke-width: 3px; }
|
|
81
|
+
g.node.hot circle { stroke: #f5a623; stroke-width: 2.5; }
|
|
82
|
+
g.node.hot text { fill: #ffd27a; }
|
|
83
|
+
g.node.dim { opacity: .12; }
|
|
84
|
+
#tip { position: fixed; z-index: 9; display: none; max-width: 340px; background: #1c242d; border: 1px solid #33404c; border-radius: 8px; padding: 10px 12px; pointer-events: none; box-shadow: 0 6px 24px rgba(0,0,0,.5); }
|
|
85
|
+
#tip b { color: #8ec7ff; }
|
|
86
|
+
#tip .s { color: #9aa7b3; font-size: 12px; }
|
|
87
|
+
#tip .c { color: #c4cdd6; font-size: 12px; margin-top: 4px; white-space: pre-wrap; }
|
|
88
|
+
#tip .tags { color: #6f8fae; font-size: 11px; margin-top: 4px; }
|
|
89
|
+
</style>
|
|
90
|
+
</head>
|
|
91
|
+
<body>
|
|
92
|
+
<div id="bar">
|
|
93
|
+
<h1>dsh-topics-memory 关系图</h1>
|
|
94
|
+
<input id="q" type="search" placeholder="搜索标题 / slug…">
|
|
95
|
+
<select id="tag"><option value="">全部标签</option></select>
|
|
96
|
+
<div class="legend">
|
|
97
|
+
<span><i style="background:#4caf7d"></i>stable</span>
|
|
98
|
+
<span><i style="background:#e0a63f"></i>draft</span>
|
|
99
|
+
<span><i style="background:#6b7682"></i>deprecated</span>
|
|
100
|
+
<span style="color:#66737f">实线=depends · 虚线=正文链接 · 拖拽节点 · 悬停看详情</span>
|
|
101
|
+
</div>
|
|
102
|
+
<span id="meta"></span>
|
|
103
|
+
</div>
|
|
104
|
+
<svg id="view"></svg>
|
|
105
|
+
<div id="tip"></div>
|
|
106
|
+
<script type="application/json" id="graph-data">__GRAPH_JSON__</script>
|
|
107
|
+
<script>
|
|
108
|
+
(function () {
|
|
109
|
+
var data = JSON.parse(document.getElementById('graph-data').textContent);
|
|
110
|
+
var svg = document.getElementById('view');
|
|
111
|
+
var NS = 'http://www.w3.org/2000/svg';
|
|
112
|
+
var W = window.innerWidth, H = window.innerHeight;
|
|
113
|
+
var nodes = data.nodes.map(function (n, i) {
|
|
114
|
+
var a = (i / Math.max(1, data.nodes.length)) * Math.PI * 2;
|
|
115
|
+
return { id: n.id, title: n.title, status: n.status, tags: n.tags, degree: n.degree,
|
|
116
|
+
x: W / 2 + Math.cos(a) * Math.min(W, H) * 0.32, y: H / 2 + Math.sin(a) * Math.min(W, H) * 0.32,
|
|
117
|
+
vx: 0, vy: 0, el: null };
|
|
118
|
+
});
|
|
119
|
+
var byId = {};
|
|
120
|
+
nodes.forEach(function (n) { byId[n.id] = n; });
|
|
121
|
+
var edges = data.edges.filter(function (e) { return byId[e.from] && byId[e.to]; });
|
|
122
|
+
|
|
123
|
+
var STATUS_FILL = { stable: '#4caf7d', draft: '#e0a63f', deprecated: '#6b7682' };
|
|
124
|
+
|
|
125
|
+
var defs = document.createElementNS(NS, 'defs');
|
|
126
|
+
defs.innerHTML = '<marker id="arrow" viewBox="0 0 8 8" refX="14" refY="4" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path d="M0,0 L8,4 L0,8 z" fill="#3a4652"/></marker>';
|
|
127
|
+
svg.appendChild(defs);
|
|
128
|
+
|
|
129
|
+
var edgeEls = edges.map(function (e) {
|
|
130
|
+
var l = document.createElementNS(NS, 'line');
|
|
131
|
+
l.setAttribute('class', 'edge' + (e.via === 'link' ? ' via-link' : ''));
|
|
132
|
+
if (e.via === 'depends') l.setAttribute('marker-end', 'url(#arrow)');
|
|
133
|
+
l.dataset.from = e.from; l.dataset.to = e.to;
|
|
134
|
+
svg.appendChild(l);
|
|
135
|
+
return l;
|
|
136
|
+
});
|
|
137
|
+
var nodeEls = nodes.map(function (n) {
|
|
138
|
+
var g = document.createElementNS(NS, 'g');
|
|
139
|
+
g.setAttribute('class', 'node');
|
|
140
|
+
var r = 6 + Math.min(10, n.degree * 2);
|
|
141
|
+
var c = document.createElementNS(NS, 'circle');
|
|
142
|
+
c.setAttribute('r', r);
|
|
143
|
+
c.setAttribute('fill', STATUS_FILL[n.status] || '#4a90d9');
|
|
144
|
+
var t = document.createElementNS(NS, 'text');
|
|
145
|
+
t.setAttribute('y', r + 13);
|
|
146
|
+
t.textContent = n.title.length > 18 ? n.title.slice(0, 17) + '…' : n.title;
|
|
147
|
+
g.appendChild(c); g.appendChild(t);
|
|
148
|
+
n.el = g; n.circle = c;
|
|
149
|
+
g.dataset.id = n.id;
|
|
150
|
+
svg.appendChild(g);
|
|
151
|
+
return g;
|
|
152
|
+
});
|
|
153
|
+
document.getElementById('meta').textContent = nodes.length + ' 节点 · ' + edges.length + ' 边';
|
|
154
|
+
|
|
155
|
+
var tagSel = document.getElementById('tag');
|
|
156
|
+
var tagSet = {};
|
|
157
|
+
nodes.forEach(function (n) { n.tags.forEach(function (t) { tagSet[t] = 1; }); });
|
|
158
|
+
Object.keys(tagSet).sort().forEach(function (t) {
|
|
159
|
+
var o = document.createElement('option'); o.value = t; o.textContent = '#' + t; tagSel.appendChild(o);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
// force simulation: charge repulsion + edge springs + gentle centering
|
|
163
|
+
var alpha = 1;
|
|
164
|
+
function tick() {
|
|
165
|
+
var i, j, a, b, dx, dy, d2;
|
|
166
|
+
for (i = 0; i < nodes.length; i++) {
|
|
167
|
+
for (j = i + 1; j < nodes.length; j++) {
|
|
168
|
+
a = nodes[i]; b = nodes[j];
|
|
169
|
+
dx = a.x - b.x; dy = a.y - b.y;
|
|
170
|
+
d2 = dx * dx + dy * dy + 0.01;
|
|
171
|
+
var f = 2600 / d2 * alpha;
|
|
172
|
+
var d = Math.sqrt(d2);
|
|
173
|
+
a.vx += dx / d * f; a.vy += dy / d * f;
|
|
174
|
+
b.vx -= dx / d * f; b.vy -= dy / d * f;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
edges.forEach(function (e) {
|
|
178
|
+
a = byId[e.from]; b = byId[e.to];
|
|
179
|
+
dx = b.x - a.x; dy = b.y - a.y;
|
|
180
|
+
d = Math.sqrt(dx * dx + dy * dy) || 1;
|
|
181
|
+
var want = 130;
|
|
182
|
+
var k = (d - want) * 0.012 * alpha;
|
|
183
|
+
a.vx += dx / d * k * d * 0.01; a.vy += dy / d * k * d * 0.01;
|
|
184
|
+
b.vx -= dx / d * k * d * 0.01; b.vy -= dy / d * k * d * 0.01;
|
|
185
|
+
});
|
|
186
|
+
nodes.forEach(function (n) {
|
|
187
|
+
n.vx += (W / 2 - n.x) * 0.0015 * alpha;
|
|
188
|
+
n.vy += (H / 2 - n.y) * 0.0015 * alpha;
|
|
189
|
+
n.vx *= 0.82; n.vy *= 0.82;
|
|
190
|
+
n.x += Math.max(-14, Math.min(14, n.vx));
|
|
191
|
+
n.y += Math.max(-14, Math.min(14, n.vy));
|
|
192
|
+
n.x = Math.max(24, Math.min(W - 24, n.x));
|
|
193
|
+
n.y = Math.max(52, Math.min(H - 30, n.y));
|
|
194
|
+
});
|
|
195
|
+
alpha *= 0.995;
|
|
196
|
+
}
|
|
197
|
+
function draw() {
|
|
198
|
+
edges.forEach(function (e, i) {
|
|
199
|
+
var a = byId[e.from], b = byId[e.to], l = edgeEls[i];
|
|
200
|
+
l.setAttribute('x1', a.x); l.setAttribute('y1', a.y);
|
|
201
|
+
l.setAttribute('x2', b.x); l.setAttribute('y2', b.y);
|
|
202
|
+
});
|
|
203
|
+
nodes.forEach(function (n) { n.el.setAttribute('transform', 'translate(' + n.x + ',' + n.y + ')'); });
|
|
204
|
+
}
|
|
205
|
+
(function loop() { if (alpha > 0.02) { tick(); } draw(); requestAnimationFrame(loop); })();
|
|
206
|
+
|
|
207
|
+
// pan + drag
|
|
208
|
+
var viewBox = { x: 0, y: 0, w: W, h: H };
|
|
209
|
+
svg.setAttribute('viewBox', '0 0 ' + W + ' ' + H);
|
|
210
|
+
var dragNode = null, panning = false, px = 0, py = 0;
|
|
211
|
+
svg.addEventListener('pointerdown', function (ev) {
|
|
212
|
+
var g = ev.target.closest ? ev.target.closest('g.node') : null;
|
|
213
|
+
if (g) {
|
|
214
|
+
dragNode = byId[g.dataset.id];
|
|
215
|
+
svg.classList.add('dragging');
|
|
216
|
+
} else { panning = true; px = ev.clientX; py = ev.clientY; svg.classList.add('dragging'); }
|
|
217
|
+
});
|
|
218
|
+
window.addEventListener('pointermove', function (ev) {
|
|
219
|
+
if (dragNode) {
|
|
220
|
+
dragNode.x = viewBox.x + ev.clientX / viewBox.w * W;
|
|
221
|
+
dragNode.y = viewBox.y + ev.clientY / viewBox.h * H;
|
|
222
|
+
dragNode.vx = dragNode.vy = 0; draw(); return;
|
|
223
|
+
}
|
|
224
|
+
if (panning) {
|
|
225
|
+
viewBox.x -= (ev.clientX - px) * viewBox.w / W;
|
|
226
|
+
viewBox.y -= (ev.clientY - py) * viewBox.h / H;
|
|
227
|
+
px = ev.clientX; py = ev.clientY;
|
|
228
|
+
svg.setAttribute('viewBox', viewBox.x + ' ' + viewBox.y + ' ' + viewBox.w + ' ' + viewBox.h);
|
|
229
|
+
}
|
|
230
|
+
});
|
|
231
|
+
window.addEventListener('pointerup', function () { dragNode = null; panning = false; svg.classList.remove('dragging'); });
|
|
232
|
+
svg.addEventListener('wheel', function (ev) {
|
|
233
|
+
ev.preventDefault();
|
|
234
|
+
var k = ev.deltaY > 0 ? 1.1 : 0.9;
|
|
235
|
+
viewBox.w *= k; viewBox.h *= k;
|
|
236
|
+
svg.setAttribute('viewBox', viewBox.x + ' ' + viewBox.y + ' ' + viewBox.w + ' ' + viewBox.h);
|
|
237
|
+
}, { passive: false });
|
|
238
|
+
|
|
239
|
+
// hover: highlight neighbors + tooltip (pointermove — synthetic and real
|
|
240
|
+
// pointers both dispatch it; pointerover alone misses programmatic moves)
|
|
241
|
+
var tip = document.getElementById('tip');
|
|
242
|
+
var hotId = null;
|
|
243
|
+
function setHot(id) {
|
|
244
|
+
var hot = {};
|
|
245
|
+
if (id) {
|
|
246
|
+
hot[id] = 1;
|
|
247
|
+
edges.forEach(function (e) {
|
|
248
|
+
if (e.from === id) hot[e.to] = 1;
|
|
249
|
+
if (e.to === id) hot[e.from] = 1;
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
edges.forEach(function (e, i) {
|
|
253
|
+
var on = id && (e.from === id || e.to === id);
|
|
254
|
+
edgeEls[i].classList.toggle('hot', !!on);
|
|
255
|
+
});
|
|
256
|
+
nodeEls.forEach(function (g) { g.classList.toggle('hot', id ? !!hot[g.dataset.id] : false); });
|
|
257
|
+
}
|
|
258
|
+
svg.addEventListener('pointermove', function (ev) {
|
|
259
|
+
var g = ev.target && ev.target.closest ? ev.target.closest('g.node') : null;
|
|
260
|
+
if (!g) {
|
|
261
|
+
if (hotId !== null) { setHot(null); tip.style.display = 'none'; hotId = null; }
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
if (g.dataset.id === hotId) return; // already showing this node
|
|
265
|
+
hotId = g.dataset.id;
|
|
266
|
+
var n = byId[hotId];
|
|
267
|
+
setHot(n.id);
|
|
268
|
+
var conclusion = n.conclusion || '';
|
|
269
|
+
tip.innerHTML = '<b>' + n.title.replace(/&/g, '&').replace(/</g, '<') + '</b>' +
|
|
270
|
+
'<div class="s">' + n.id + ' · ' + n.status + '</div>' +
|
|
271
|
+
(conclusion ? '<div class="c">' + conclusion.replace(/&/g, '&').replace(/</g, '<').slice(0, 260) + '</div>' : '') +
|
|
272
|
+
(n.tags.length ? '<div class="tags">#' + n.tags.join(' #') + '</div>' : '');
|
|
273
|
+
tip.style.display = 'block';
|
|
274
|
+
tip.style.left = Math.min(window.innerWidth - 360, ev.clientX + 16) + 'px';
|
|
275
|
+
tip.style.top = (ev.clientY + 12) + 'px';
|
|
276
|
+
});
|
|
277
|
+
svg.addEventListener('pointerleave', function () { setHot(null); tip.style.display = 'none'; hotId = null; });
|
|
278
|
+
|
|
279
|
+
// search + tag filter → dim non-matches
|
|
280
|
+
function applyFilter() {
|
|
281
|
+
var q = document.getElementById('q').value.trim().toLowerCase();
|
|
282
|
+
var tag = tagSel.value;
|
|
283
|
+
nodeEls.forEach(function (g) {
|
|
284
|
+
var n = byId[g.dataset.id];
|
|
285
|
+
var okQ = !q || n.title.toLowerCase().indexOf(q) >= 0 || n.id.indexOf(q) >= 0;
|
|
286
|
+
var okT = !tag || n.tags.indexOf(tag) >= 0;
|
|
287
|
+
g.classList.toggle('dim', !okQ || !okT);
|
|
288
|
+
});
|
|
289
|
+
var any = !q && !tag;
|
|
290
|
+
edges.forEach(function (e, i) {
|
|
291
|
+
if (any) { edgeEls[i].classList.remove('dim'); return; }
|
|
292
|
+
var a = byId[e.from], b = byId[e.to];
|
|
293
|
+
var okQ = !q || [a, b].some(function (n) { return n.title.toLowerCase().indexOf(q) >= 0 || n.id.indexOf(q) >= 0; });
|
|
294
|
+
var okT = !tag || [a, b].some(function (n) { return n.tags.indexOf(tag) >= 0; });
|
|
295
|
+
edgeEls[i].classList.toggle('dim', !(okQ && okT));
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
document.getElementById('q').addEventListener('input', applyFilter);
|
|
299
|
+
tagSel.addEventListener('change', applyFilter);
|
|
300
|
+
})();
|
|
301
|
+
</script>
|
|
302
|
+
</body>
|
|
303
|
+
</html>
|
|
304
|
+
`;
|
|
305
|
+
export function renderGraphHtml(graph, opts = {}) {
|
|
306
|
+
// Tooltip conclusions ride on the nodes (kept out of VizGraph for typing
|
|
307
|
+
// clarity at the command layer).
|
|
308
|
+
const nodes = graph.nodes.map((n) => ({ ...n, conclusion: opts.conclusions?.[n.id] ?? '' }));
|
|
309
|
+
return PAGE.replace('__GRAPH_JSON__', embedJson({ nodes, edges: graph.edges }));
|
|
310
|
+
}
|
package/lib/yaml.d.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal YAML subset for OKF frontmatter — parse and serialize, zero deps.
|
|
3
|
+
*
|
|
4
|
+
* Scope is deliberately narrow: exactly what OKF v0.2 frontmatter needs and
|
|
5
|
+
* nothing more. Maps (nested), block sequences, inline sequences `[a, b]`,
|
|
6
|
+
* inline maps `{by: x, at: y}`, plain/quoted scalars, numbers, booleans, null.
|
|
7
|
+
* Comments: full-line only (trailing comments are unsafe — URLs may contain `#`).
|
|
8
|
+
* Block scalars (`|`, `>`) are NOT supported; never emitted by this plugin.
|
|
9
|
+
*
|
|
10
|
+
* @module yaml
|
|
11
|
+
*/
|
|
12
|
+
export declare class YamlError extends Error {
|
|
13
|
+
name: string;
|
|
14
|
+
}
|
|
15
|
+
/** Parse the YAML subset into a plain JS value (object at top level). */
|
|
16
|
+
export declare function parseYaml(text: string): Record<string, unknown>;
|
|
17
|
+
/** Serialize a plain JS value into the YAML subset (deterministic, stable). */
|
|
18
|
+
export declare function stringifyYaml(value: Record<string, unknown>): string;
|