graphlin 0.1.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.
Files changed (60) hide show
  1. package/.claude-plugin/plugin.json +12 -0
  2. package/.codex-plugin/plugin.json +29 -0
  3. package/.mcp.json +9 -0
  4. package/LICENSE +21 -0
  5. package/README.md +71 -0
  6. package/adapters/README.md +32 -0
  7. package/adapters/claude/hooks.json +10 -0
  8. package/adapters/claude/profile.json +18 -0
  9. package/adapters/codex/hooks.json +9 -0
  10. package/adapters/codex/profile.json +22 -0
  11. package/adapters/kiro/profile.json +8 -0
  12. package/mcp.json +11 -0
  13. package/package.json +114 -0
  14. package/plugin.json +20 -0
  15. package/runtime/collector/index.mjs +23 -0
  16. package/runtime/core/candidates.mjs +300 -0
  17. package/runtime/core/common.mjs +69 -0
  18. package/runtime/core/evidence.mjs +150 -0
  19. package/runtime/core/graph.mjs +398 -0
  20. package/runtime/core/index.mjs +4 -0
  21. package/runtime/core/lexical.mjs +255 -0
  22. package/runtime/core/privacy.mjs +206 -0
  23. package/runtime/core/tool-discovery.mjs +122 -0
  24. package/runtime/daemon/auth.mjs +50 -0
  25. package/runtime/daemon/connection-info.mjs +249 -0
  26. package/runtime/daemon/demo.mjs +195 -0
  27. package/runtime/daemon/diagnostics.mjs +404 -0
  28. package/runtime/daemon/export.mjs +7 -0
  29. package/runtime/daemon/ipc.mjs +28 -0
  30. package/runtime/daemon/lock.mjs +137 -0
  31. package/runtime/daemon/manager.mjs +320 -0
  32. package/runtime/daemon/paths.mjs +108 -0
  33. package/runtime/daemon/persistence.mjs +64 -0
  34. package/runtime/daemon/server.mjs +292 -0
  35. package/runtime/daemon/settings.mjs +103 -0
  36. package/runtime/jev/fixture.mjs +99 -0
  37. package/runtime/jev/index.mjs +784 -0
  38. package/runtime/jev/questions.mjs +268 -0
  39. package/runtime/jev/wire.mjs +152 -0
  40. package/runtime/pipeline.mjs +1071 -0
  41. package/runtime/web/app.js +2596 -0
  42. package/runtime/web/index.html +265 -0
  43. package/runtime/web/layout.js +336 -0
  44. package/runtime/web/sidebar.js +525 -0
  45. package/runtime/web/sketch.js +347 -0
  46. package/runtime/web/style.css +593 -0
  47. package/schemas/bundle.schema.json +243 -0
  48. package/schemas/event.schema.json +108 -0
  49. package/schemas/graph.schema.json +449 -0
  50. package/schemas/patch.schema.json +111 -0
  51. package/scripts/arguments.mjs +37 -0
  52. package/scripts/build-packages.mjs +160 -0
  53. package/scripts/collect.sh +23 -0
  54. package/scripts/collector.mjs +11 -0
  55. package/scripts/control.mjs +80 -0
  56. package/scripts/daemon.mjs +28 -0
  57. package/scripts/graphlin.mjs +112 -0
  58. package/scripts/onboarding.mjs +413 -0
  59. package/scripts/validate-packages.mjs +118 -0
  60. package/skills/graphlin/SKILL.md +103 -0
@@ -0,0 +1,347 @@
1
+ // Fixed 190 × 104 geometry matching the viewer's fifteen named silhouettes.
2
+ // These are local numeric contours, never parsed or constructed from hook text.
3
+ const WIDTH = 190;
4
+ const HEIGHT = 104;
5
+ // Chosen line-study option D, "Tidy sketch". Opacity belongs to the renderer:
6
+ // use the second path as a lighter echo (the study uses 0.46).
7
+ const TIDY = Object.freeze({ bend: 2.6, join: 0.8, overshoot: 1.65, separation: 0.85 });
8
+ const MAX_OFFSET = 5;
9
+ const MAX_ID_UNITS = 180;
10
+ const MAX_COORDINATE = 1e7;
11
+ const KAPPA = 0.5522847498307936;
12
+
13
+ function line(from, to) {
14
+ return [
15
+ from[0] + (to[0] - from[0]) / 3, from[1] + (to[1] - from[1]) / 3,
16
+ from[0] + (to[0] - from[0]) * 2 / 3, from[1] + (to[1] - from[1]) * 2 / 3,
17
+ ...to,
18
+ ];
19
+ }
20
+
21
+ function polyline(points, closed = true) {
22
+ const ends = closed ? [...points.slice(1), points[0]] : points.slice(1);
23
+ return { start: points[0], curves: ends.map((point, index) => line(points[index], point)), closed };
24
+ }
25
+
26
+ function roundedBox(radius) {
27
+ const r = radius, k = r * KAPPA, w = WIDTH, h = HEIGHT;
28
+ return {
29
+ start: [r, 0], closed: true,
30
+ curves: [
31
+ line([r, 0], [w - r, 0]),
32
+ [w - r + k, 0, w, r - k, w, r],
33
+ line([w, r], [w, h - r]),
34
+ [w, h - r + k, w - r + k, h, w - r, h],
35
+ line([w - r, h], [r, h]),
36
+ [r - k, h, 0, h - r + k, 0, h - r],
37
+ line([0, h - r], [0, r]),
38
+ [0, r - k, r - k, 0, r, 0],
39
+ ],
40
+ };
41
+ }
42
+
43
+ const OUTLINES = Object.freeze({
44
+ rounded_rect: [roundedBox(10)],
45
+ rect: [roundedBox(5)],
46
+ cylinder: [{
47
+ start: [0, 17], closed: true,
48
+ curves: [
49
+ [0, -2, 190, -2, 190, 17],
50
+ line([190, 17], [190, 87]),
51
+ [190, 109, 0, 109, 0, 87],
52
+ line([0, 87], [0, 17]),
53
+ ],
54
+ }],
55
+ cloud: [{
56
+ start: [18, 99], closed: true,
57
+ curves: [
58
+ [-8, 99, -9, 53, 13, 45],
59
+ [-2, 16, 34, 1, 58, 13],
60
+ [76, -7, 132, -5, 145, 16],
61
+ [182, 7, 202, 36, 183, 57],
62
+ [207, 73, 191, 103, 169, 99],
63
+ line([169, 99], [18, 99]),
64
+ ],
65
+ }],
66
+ diamond: [polyline([[95, -12], [204, 52], [95, 116], [-14, 52]])],
67
+ group: [roundedBox(2)],
68
+ browser: [roundedBox(3)],
69
+ component: [
70
+ polyline([[10, 0], [190, 0], [190, 104], [10, 104], [10, 85], [0, 85],
71
+ [0, 68], [10, 68], [10, 35], [0, 35], [0, 18], [10, 18]]),
72
+ // The remaining tab borders preserve the component symbol without drawing
73
+ // the main body's vertical edge through the tabs' canonical fills.
74
+ polyline([[10, 18], [21, 18], [21, 35], [10, 35]], false),
75
+ polyline([[10, 68], [21, 68], [21, 85], [10, 85]], false),
76
+ ],
77
+ queue: [roundedBox(3)],
78
+ hexagon: [polyline([[23, 0], [167, 0], [190, 52], [167, 104], [23, 104], [0, 52]])],
79
+ class_box: [roundedBox(3)],
80
+ interface_box: [roundedBox(3)],
81
+ document: [polyline([[0, 0], [167, 0], [190, 23], [190, 104], [0, 104]])],
82
+ parallelogram: [polyline([[22, 0], [190, 0], [168, 104], [0, 104]])],
83
+ folder: [polyline([[0, 10], [66, 10], [78, 0], [190, 0], [190, 104], [0, 104]])],
84
+ });
85
+
86
+ const DETAILS = Object.freeze({
87
+ cylinder: [{ start: [0, 17], curves: [[0, 37, 190, 37, 190, 17]] }],
88
+ browser: [polyline([[0, 23], [190, 23]], false)],
89
+ queue: [
90
+ polyline([[17, 0], [17, 104]], false),
91
+ polyline([[173, 0], [173, 104]], false),
92
+ polyline([[40, 16], [150, 16]], false),
93
+ polyline([[140, 11], [150, 16], [140, 21]], false),
94
+ ],
95
+ class_box: [
96
+ polyline([[0, 25], [190, 25]], false),
97
+ polyline([[0, 72], [190, 72]], false),
98
+ ],
99
+ interface_box: [polyline([[0, 25], [190, 25]], false)],
100
+ document: [polyline([[167, 0], [167, 23], [190, 23]], false)],
101
+ folder: [polyline([[0, 24], [190, 24]], false)],
102
+ });
103
+
104
+ function seedFor(shape, id) {
105
+ const key = shape + '\0' + (typeof id === 'string' ? id.slice(0, MAX_ID_UNITS) : '');
106
+ let hash = 2166136261;
107
+ for (let index = 0; index < key.length; index++) hash = Math.imul(hash ^ key.charCodeAt(index), 16777619);
108
+ return hash >>> 0;
109
+ }
110
+
111
+ function randomFor(seed) {
112
+ let state = seed >>> 0;
113
+ return () => {
114
+ state = (Math.imul(state, 1664525) + 1013904223) >>> 0;
115
+ return state / 4294967296;
116
+ };
117
+ }
118
+
119
+ function point(pair, exact = false) {
120
+ return pair.map(value => exact
121
+ ? (Object.is(value, -0) ? '-0' : String(value))
122
+ : String(Math.round(value * 1000) / 1000)).join(' ');
123
+ }
124
+
125
+ function mix(a, b, t) {
126
+ return [a[0] + (b[0] - a[0]) * t, a[1] + (b[1] - a[1]) * t];
127
+ }
128
+
129
+ function subtract(a, b) {
130
+ return [a[0] - b[0], a[1] - b[1]];
131
+ }
132
+
133
+ function unit(vector) {
134
+ const scale = Math.max(...vector.map(Math.abs));
135
+ if (!scale) return null;
136
+ const scaled = vector.map(value => value / scale);
137
+ const length = Math.hypot(...scaled);
138
+ return scaled.map(value => value / length);
139
+ }
140
+
141
+ function endDirection(points) {
142
+ for (let index = 2; index >= 0; index--) {
143
+ const direction = unit(subtract(points[3], points[index]));
144
+ if (direction) return direction;
145
+ }
146
+ return [1, 0];
147
+ }
148
+
149
+ function tangent(points, t) {
150
+ const s = 1 - t;
151
+ const derivative = [0, 1].map(axis =>
152
+ s * s * (points[1][axis] - points[0][axis])
153
+ + 2 * s * t * (points[2][axis] - points[1][axis])
154
+ + t * t * (points[3][axis] - points[2][axis]));
155
+ return unit(derivative) || endDirection(points);
156
+ }
157
+
158
+ // Exact de Casteljau subdivision retains the original curve's silhouette.
159
+ // Only the bounded displacement field is interpolated through loose anchors.
160
+ function split(points, t) {
161
+ const a = mix(points[0], points[1], t);
162
+ const b = mix(points[1], points[2], t);
163
+ const c = mix(points[2], points[3], t);
164
+ const d = mix(a, b, t), e = mix(b, c, t), f = mix(d, e, t);
165
+ return [[points[0], a, d, f], [f, e, c, points[3]]];
166
+ }
167
+
168
+ function shifted(base, offset) {
169
+ const length = Math.hypot(...offset);
170
+ const scale = length > MAX_OFFSET ? MAX_OFFSET / length : 1;
171
+ return [base[0] + offset[0] * scale, base[1] + offset[1] * scale];
172
+ }
173
+
174
+ function roughCurve(points, random, pass, { pinStart = false, pinEnd = false, strength = 1, exact = false } = {}) {
175
+ const signed = () => random() * 2 - 1;
176
+ const length = points.slice(1).reduce((sum, p, index) =>
177
+ sum + Math.hypot(...subtract(p, points[index])), 0);
178
+ const scale = Math.min(1, length / 150) * strength;
179
+ const stations = [0, .23 + random() * .13, .62 + random() * .14, 1];
180
+ const gesture = signed() < 0 ? -1 : 1;
181
+ const bias = pass ? signed() * TIDY.separation : 0;
182
+ const normalOffsets = [
183
+ signed() * TIDY.join,
184
+ gesture * TIDY.bend * (.58 + random() * .56),
185
+ TIDY.bend * (signed() * .85 - gesture * .12),
186
+ signed() * TIDY.join,
187
+ ];
188
+ const overshoots = [-TIDY.overshoot * (.25 + random() * .75), 0, 0,
189
+ TIDY.overshoot * (.25 + random() * .75)];
190
+ const offsets = stations.map((t, index) => {
191
+ if ((index === 0 && pinStart) || (index === 3 && pinEnd)) return [0, 0];
192
+ const direction = tangent(points, t);
193
+ const normal = (normalOffsets[index] + bias) * scale;
194
+ const along = overshoots[index] * scale;
195
+ return [-direction[1] * normal + direction[0] * along,
196
+ direction[0] * normal + direction[1] * along];
197
+ });
198
+ // With two coincident endpoint controls, the limiting tangent comes from
199
+ // the whole final (or first) piece. Keep that piece canonical rather than
200
+ // allowing its displaced interior anchor to change the third derivative.
201
+ const flatStart = pinStart && points.slice(1, 3).every(p => p.every((value, axis) => value === points[0][axis]));
202
+ const flatEnd = pinEnd && points.slice(1, 3).every(p => p.every((value, axis) => value === points[3][axis]));
203
+ if (flatStart) offsets[1] = [0, 0];
204
+ if (flatEnd) offsets[2] = [0, 0];
205
+ const slopes = offsets.map((offset, index) => {
206
+ if ((index === 0 && pinStart) || (index === 3 && pinEnd)
207
+ || (index === 1 && flatStart) || (index === 2 && flatEnd)) return [0, 0];
208
+ const before = Math.max(0, index - 1), after = Math.min(3, index + 1);
209
+ const interval = stations[after] - stations[before];
210
+ return subtract(offsets[after], offsets[before]).map(value => value / interval);
211
+ });
212
+ const pieces = [];
213
+ let remaining = points, previous = 0;
214
+ for (const station of stations.slice(1, -1)) {
215
+ const [piece, rest] = split(remaining, (station - previous) / (1 - previous));
216
+ pieces.push(piece);
217
+ remaining = rest;
218
+ previous = station;
219
+ }
220
+ pieces.push(remaining);
221
+ const commands = [`M ${point(pinStart ? points[0] : shifted(points[0], offsets[0]), exact)}`];
222
+ for (let index = 0; index < pieces.length; index++) {
223
+ const interval = (stations[index + 1] - stations[index]) / 3;
224
+ const firstOffset = offsets[index].map((value, axis) => value + slopes[index][axis] * interval);
225
+ const secondOffset = offsets[index + 1].map((value, axis) => value - slopes[index + 1][axis] * interval);
226
+ const piece = pieces[index];
227
+ const controls = [shifted(piece[1], firstOffset), shifted(piece[2], secondOffset),
228
+ shifted(piece[3], offsets[index + 1])];
229
+ // Preserve the limiting tangent even when the first derivative is zero.
230
+ // A constant cubic also stays constant; it does not acquire a tiny loop.
231
+ if (index === 0 && pinStart && points[0].every((value, axis) => value === points[1][axis])) controls[1] = piece[2];
232
+ if (index === 2 && pinEnd && points[3].every((value, axis) => value === points[2][axis])) controls[0] = piece[1];
233
+ if (index === 2 && pinEnd) controls[2] = points[3];
234
+ commands.push(`C ${controls.map(control => point(control, exact)).join(' ')}`);
235
+ }
236
+ return commands.join(' ');
237
+ }
238
+
239
+ function twoPasses(kind, id, draw) {
240
+ const seed = seedFor(kind, id);
241
+ return [0, 1].map(pass => draw(randomFor(seed ^ Math.imul(pass + 1, 0x9e3779b9)), pass));
242
+ }
243
+
244
+ function drawContours(contours, random, pass) {
245
+ const strokes = [];
246
+ for (const contour of contours) {
247
+ let from = contour.start;
248
+ for (const curve of contour.curves) {
249
+ strokes.push(roughCurve([from, curve.slice(0, 2), curve.slice(2, 4), curve.slice(4)], random, pass));
250
+ from = curve.slice(4);
251
+ }
252
+ }
253
+ return strokes.join(' ');
254
+ }
255
+
256
+ function knownShape(shapes, shape) {
257
+ return typeof shape === 'string' && shape.length <= 32 && Object.hasOwn(shapes, shape);
258
+ }
259
+
260
+ function connectionPoints(input) {
261
+ // Accept only four own pairs of finite numbers. Accessors, sparse arrays and
262
+ // coercible text are not coordinates. Copy without calling input methods.
263
+ try {
264
+ if (!Array.isArray(input) || Object.getOwnPropertyDescriptor(input, 'length')?.value !== 4) return null;
265
+ const points = [];
266
+ for (let index = 0; index < 4; index++) {
267
+ const row = Object.getOwnPropertyDescriptor(input, index)?.value;
268
+ if (!Array.isArray(row) || Object.getOwnPropertyDescriptor(row, 'length')?.value !== 2) return null;
269
+ const pair = [];
270
+ for (let axis = 0; axis < 2; axis++) {
271
+ const value = Object.getOwnPropertyDescriptor(row, axis)?.value;
272
+ if (typeof value !== 'number' || !Number.isFinite(value) || Math.abs(value) > MAX_COORDINATE) return null;
273
+ pair.push(value);
274
+ }
275
+ points.push(pair);
276
+ }
277
+ return points;
278
+ } catch {
279
+ return null;
280
+ }
281
+ }
282
+
283
+ /**
284
+ * Return two deterministic, stroke-only SVG path strings for a known shape,
285
+ * or [] for an unknown/non-string shape. Geometry uses fixed 190 × 104 units.
286
+ *
287
+ * D's broad bends, loose joins and slight overshoots are applied to exact
288
+ * subdivisions of the canonical cubics. Each pen stroke uses three cubics;
289
+ * short edges and round corners scale down the gesture. Displacement is
290
+ * bounded by 5px (plus <0.001px numeric rounding) from the canonical curve.
291
+ * Small pen lifts at joins are intentional; do not use these paths as fills.
292
+ * Canonical fills, attachment geometry, and hit targets remain external.
293
+ * IDs seed a bounded hash of their first 180 UTF-16 units. Non-string IDs use
294
+ * the same stable empty-ID fallback, without coercion or property access.
295
+ *
296
+ * No DOM, colors, mutable state or result cache. Each call returns a new array.
297
+ * At most 18 pen strokes / 54 cubics per path, independent of ID length.
298
+ * Render with fill="none", round caps/joins, pointer-events="none" and
299
+ * aria-hidden="true", beneath the clipped title. Secondary opacity: 0.46.
300
+ */
301
+ export function sketchOutline(shape, id) {
302
+ if (!knownShape(OUTLINES, shape)) return [];
303
+ return twoPasses(`outline/${shape}`, id, (random, pass) => drawContours(OUTLINES[shape], random, pass));
304
+ }
305
+
306
+ /**
307
+ * Return two seeded stroke-only detail paths, or [] for shapes without details
308
+ * and unknown/non-string shapes. Browser dots stay in the canonical renderer.
309
+ * Component tab borders are already in sketchOutline and are not duplicated.
310
+ * Same seed/ID/bounds contract as sketchOutline; at most 5 strokes / 15 cubics.
311
+ */
312
+ export function sketchDetails(shape, id) {
313
+ if (!knownShape(DETAILS, shape)) return [];
314
+ return twoPasses(`details/${shape}`, id, (random, pass) => drawContours(DETAILS[shape], random, pass));
315
+ }
316
+
317
+ /**
318
+ * points: exactly four [x, y] number arrays describing a canonical cubic.
319
+ * Each coordinate must be finite and within ±1e7. Invalid input returns fresh
320
+ * { lines: [], heads: [] }; valid input returns two path strings in each array.
321
+ *
322
+ * Shafts keep both exact ports and their canonical limiting tangents. Heads
323
+ * are two open wings meeting at the exact end, aligned with the final tangent;
324
+ * a constant cubic uses +x. Shaft displacement is <=5px; all head ink lies
325
+ * within 18px of the end, with wing spread <9px, inside the viewer's existing
326
+ * 64px drawing padding / 20px hit width at its normal stroke weight.
327
+ *
328
+ * Each shaft has three cubics and each head six. Finite numeric serialization
329
+ * has a constant output budget even for extreme coordinates or oversized IDs.
330
+ * ID handling and rendering requirements match sketchOutline. No input mutates.
331
+ */
332
+ export function sketchConnection(points, id) {
333
+ const canonical = connectionPoints(points);
334
+ if (!canonical) return { lines: [], heads: [] };
335
+ const lines = twoPasses('connection/shaft', id, (random, pass) =>
336
+ roughCurve(canonical, random, pass, { pinStart: true, pinEnd: true, exact: true }));
337
+ const tip = canonical[3], direction = endDirection(canonical);
338
+ const heads = twoPasses('connection/head', id, (random, pass) => [-1, 1].map(side => {
339
+ const length = 13 + (random() - .5) * TIDY.bend * .6;
340
+ const spread = 7 + (random() - .5) * TIDY.bend * .35;
341
+ const tail = [tip[0] - direction[0] * length - direction[1] * side * spread,
342
+ tip[1] - direction[1] * length + direction[0] * side * spread];
343
+ return roughCurve([tail, mix(tail, tip, 1 / 3), mix(tail, tip, 2 / 3), tip], random, pass,
344
+ { pinEnd: true, strength: 1.7, exact: true });
345
+ }).join(' '));
346
+ return { lines, heads };
347
+ }