lynkeus 0.2.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 (217) hide show
  1. package/lib/__tests__/args.test.d.ts +1 -0
  2. package/lib/__tests__/args.test.js +69 -0
  3. package/lib/__tests__/check.test.d.ts +1 -0
  4. package/lib/__tests__/check.test.js +60 -0
  5. package/lib/__tests__/init.test.d.ts +1 -0
  6. package/lib/__tests__/init.test.js +42 -0
  7. package/lib/__tests__/record.test.d.ts +1 -0
  8. package/lib/__tests__/record.test.js +44 -0
  9. package/lib/__tests__/snapshot.test.d.ts +1 -0
  10. package/lib/__tests__/snapshot.test.js +47 -0
  11. package/lib/__tests__/targets.test.d.ts +1 -0
  12. package/lib/__tests__/targets.test.js +127 -0
  13. package/lib/args.d.ts +30 -0
  14. package/lib/args.js +86 -0
  15. package/lib/cases/__tests__/format.test.d.ts +1 -0
  16. package/lib/cases/__tests__/format.test.js +61 -0
  17. package/lib/cases/__tests__/runner.test.d.ts +1 -0
  18. package/lib/cases/__tests__/runner.test.js +188 -0
  19. package/lib/cases/fixtures.d.ts +13 -0
  20. package/lib/cases/fixtures.js +100 -0
  21. package/lib/cases/format.d.ts +37 -0
  22. package/lib/cases/format.js +169 -0
  23. package/lib/cases/runner.d.ts +61 -0
  24. package/lib/cases/runner.js +590 -0
  25. package/lib/cases/templates.d.ts +20 -0
  26. package/lib/cases/templates.js +156 -0
  27. package/lib/cli.d.ts +6 -0
  28. package/lib/cli.js +173 -0
  29. package/lib/commands/cases.d.ts +1 -0
  30. package/lib/commands/cases.js +179 -0
  31. package/lib/commands/check.d.ts +36 -0
  32. package/lib/commands/check.js +67 -0
  33. package/lib/commands/crawl/return.d.ts +20 -0
  34. package/lib/commands/crawl/return.js +125 -0
  35. package/lib/commands/crawl/state.d.ts +30 -0
  36. package/lib/commands/crawl/state.js +56 -0
  37. package/lib/commands/crawl.d.ts +67 -0
  38. package/lib/commands/crawl.js +213 -0
  39. package/lib/commands/device.d.ts +10 -0
  40. package/lib/commands/device.js +122 -0
  41. package/lib/commands/doctor.d.ts +3 -0
  42. package/lib/commands/doctor.js +240 -0
  43. package/lib/commands/drive.d.ts +1 -0
  44. package/lib/commands/drive.js +410 -0
  45. package/lib/commands/flake.d.ts +14 -0
  46. package/lib/commands/flake.js +42 -0
  47. package/lib/commands/headless.d.ts +1 -0
  48. package/lib/commands/headless.js +127 -0
  49. package/lib/commands/init.d.ts +13 -0
  50. package/lib/commands/init.js +85 -0
  51. package/lib/commands/map.d.ts +2 -0
  52. package/lib/commands/map.js +297 -0
  53. package/lib/commands/os.d.ts +1 -0
  54. package/lib/commands/os.js +193 -0
  55. package/lib/commands/overlay.d.ts +9 -0
  56. package/lib/commands/overlay.js +12 -0
  57. package/lib/commands/record.d.ts +5 -0
  58. package/lib/commands/record.js +55 -0
  59. package/lib/commands/runs.d.ts +1 -0
  60. package/lib/commands/runs.js +452 -0
  61. package/lib/commands/smoke.d.ts +28 -0
  62. package/lib/commands/smoke.js +59 -0
  63. package/lib/commands/snapshot.d.ts +27 -0
  64. package/lib/commands/snapshot.js +69 -0
  65. package/lib/config.d.ts +72 -0
  66. package/lib/config.js +44 -0
  67. package/lib/device/devices.d.ts +11 -0
  68. package/lib/device/devices.js +33 -0
  69. package/lib/device/driver.d.ts +53 -0
  70. package/lib/device/driver.js +169 -0
  71. package/lib/device/platform.d.ts +7 -0
  72. package/lib/device/platform.js +59 -0
  73. package/lib/files.d.ts +7 -0
  74. package/lib/files.js +29 -0
  75. package/lib/flow/artifacts.d.ts +19 -0
  76. package/lib/flow/artifacts.js +49 -0
  77. package/lib/flow/runner.d.ts +13 -0
  78. package/lib/flow/runner.js +169 -0
  79. package/lib/flow/types.d.ts +111 -0
  80. package/lib/flow/types.js +1 -0
  81. package/lib/index.d.ts +18 -0
  82. package/lib/index.js +11 -0
  83. package/lib/knowledge/__tests__/edges.test.d.ts +1 -0
  84. package/lib/knowledge/__tests__/edges.test.js +25 -0
  85. package/lib/knowledge/__tests__/events.test.d.ts +1 -0
  86. package/lib/knowledge/__tests__/events.test.js +39 -0
  87. package/lib/knowledge/__tests__/extract.test.d.ts +1 -0
  88. package/lib/knowledge/__tests__/extract.test.js +69 -0
  89. package/lib/knowledge/__tests__/frontier.test.d.ts +1 -0
  90. package/lib/knowledge/__tests__/frontier.test.js +57 -0
  91. package/lib/knowledge/__tests__/funnels.test.d.ts +1 -0
  92. package/lib/knowledge/__tests__/funnels.test.js +40 -0
  93. package/lib/knowledge/__tests__/lint.test.d.ts +1 -0
  94. package/lib/knowledge/__tests__/lint.test.js +53 -0
  95. package/lib/knowledge/__tests__/perf.test.d.ts +1 -0
  96. package/lib/knowledge/__tests__/perf.test.js +34 -0
  97. package/lib/knowledge/__tests__/plan.test.d.ts +1 -0
  98. package/lib/knowledge/__tests__/plan.test.js +35 -0
  99. package/lib/knowledge/__tests__/query.test.d.ts +1 -0
  100. package/lib/knowledge/__tests__/query.test.js +55 -0
  101. package/lib/knowledge/__tests__/screens.test.d.ts +1 -0
  102. package/lib/knowledge/__tests__/screens.test.js +30 -0
  103. package/lib/knowledge/__tests__/why.test.d.ts +1 -0
  104. package/lib/knowledge/__tests__/why.test.js +28 -0
  105. package/lib/knowledge/edges.d.ts +30 -0
  106. package/lib/knowledge/edges.js +68 -0
  107. package/lib/knowledge/errors.d.ts +8 -0
  108. package/lib/knowledge/errors.js +62 -0
  109. package/lib/knowledge/events.d.ts +23 -0
  110. package/lib/knowledge/events.js +51 -0
  111. package/lib/knowledge/extract.d.ts +26 -0
  112. package/lib/knowledge/extract.js +365 -0
  113. package/lib/knowledge/frontier.d.ts +26 -0
  114. package/lib/knowledge/frontier.js +60 -0
  115. package/lib/knowledge/funnels.d.ts +46 -0
  116. package/lib/knowledge/funnels.js +103 -0
  117. package/lib/knowledge/lint.d.ts +16 -0
  118. package/lib/knowledge/lint.js +89 -0
  119. package/lib/knowledge/perf.d.ts +32 -0
  120. package/lib/knowledge/perf.js +91 -0
  121. package/lib/knowledge/plan.d.ts +22 -0
  122. package/lib/knowledge/plan.js +81 -0
  123. package/lib/knowledge/problems.d.ts +5 -0
  124. package/lib/knowledge/problems.js +8 -0
  125. package/lib/knowledge/query.d.ts +36 -0
  126. package/lib/knowledge/query.js +127 -0
  127. package/lib/knowledge/route-params.d.ts +6 -0
  128. package/lib/knowledge/route-params.js +42 -0
  129. package/lib/knowledge/screens.d.ts +30 -0
  130. package/lib/knowledge/screens.js +125 -0
  131. package/lib/knowledge/tree.d.ts +15 -0
  132. package/lib/knowledge/tree.js +63 -0
  133. package/lib/knowledge/types.d.ts +62 -0
  134. package/lib/knowledge/types.js +6 -0
  135. package/lib/knowledge/why.d.ts +24 -0
  136. package/lib/knowledge/why.js +153 -0
  137. package/lib/mcp/server.d.ts +4 -0
  138. package/lib/mcp/server.js +111 -0
  139. package/lib/registry.d.ts +78 -0
  140. package/lib/registry.js +113 -0
  141. package/lib/runtime.d.ts +12 -0
  142. package/lib/runtime.js +56 -0
  143. package/lib/targets.d.ts +17 -0
  144. package/lib/targets.js +43 -0
  145. package/package.json +47 -0
  146. package/src/__tests__/args.test.ts +82 -0
  147. package/src/__tests__/check.test.ts +71 -0
  148. package/src/__tests__/init.test.ts +52 -0
  149. package/src/__tests__/record.test.ts +49 -0
  150. package/src/__tests__/snapshot.test.ts +51 -0
  151. package/src/__tests__/targets.test.ts +132 -0
  152. package/src/args.ts +111 -0
  153. package/src/cases/__tests__/format.test.ts +76 -0
  154. package/src/cases/__tests__/runner.test.ts +230 -0
  155. package/src/cases/fixtures.ts +104 -0
  156. package/src/cases/format.ts +183 -0
  157. package/src/cases/runner.ts +575 -0
  158. package/src/cases/templates.ts +159 -0
  159. package/src/cli.ts +176 -0
  160. package/src/commands/cases.ts +170 -0
  161. package/src/commands/check.ts +83 -0
  162. package/src/commands/crawl/return.ts +130 -0
  163. package/src/commands/crawl/state.ts +70 -0
  164. package/src/commands/crawl.ts +262 -0
  165. package/src/commands/device.ts +119 -0
  166. package/src/commands/doctor.ts +237 -0
  167. package/src/commands/drive.ts +413 -0
  168. package/src/commands/flake.ts +48 -0
  169. package/src/commands/headless.ts +131 -0
  170. package/src/commands/init.ts +98 -0
  171. package/src/commands/map.ts +298 -0
  172. package/src/commands/os.ts +195 -0
  173. package/src/commands/overlay.ts +20 -0
  174. package/src/commands/record.ts +46 -0
  175. package/src/commands/runs.ts +475 -0
  176. package/src/commands/smoke.ts +86 -0
  177. package/src/commands/snapshot.ts +87 -0
  178. package/src/config.ts +103 -0
  179. package/src/device/devices.ts +44 -0
  180. package/src/device/driver.ts +197 -0
  181. package/src/device/platform.ts +74 -0
  182. package/src/files.ts +35 -0
  183. package/src/flow/artifacts.ts +61 -0
  184. package/src/flow/runner.ts +182 -0
  185. package/src/flow/types.ts +55 -0
  186. package/src/index.ts +18 -0
  187. package/src/knowledge/__tests__/edges.test.ts +34 -0
  188. package/src/knowledge/__tests__/events.test.ts +45 -0
  189. package/src/knowledge/__tests__/extract.test.ts +88 -0
  190. package/src/knowledge/__tests__/frontier.test.ts +77 -0
  191. package/src/knowledge/__tests__/funnels.test.ts +58 -0
  192. package/src/knowledge/__tests__/lint.test.ts +66 -0
  193. package/src/knowledge/__tests__/perf.test.ts +41 -0
  194. package/src/knowledge/__tests__/plan.test.ts +52 -0
  195. package/src/knowledge/__tests__/query.test.ts +74 -0
  196. package/src/knowledge/__tests__/screens.test.ts +37 -0
  197. package/src/knowledge/__tests__/why.test.ts +40 -0
  198. package/src/knowledge/edges.ts +91 -0
  199. package/src/knowledge/errors.ts +73 -0
  200. package/src/knowledge/events.ts +60 -0
  201. package/src/knowledge/extract.ts +379 -0
  202. package/src/knowledge/frontier.ts +82 -0
  203. package/src/knowledge/funnels.ts +126 -0
  204. package/src/knowledge/lint.ts +99 -0
  205. package/src/knowledge/perf.ts +114 -0
  206. package/src/knowledge/plan.ts +95 -0
  207. package/src/knowledge/problems.ts +12 -0
  208. package/src/knowledge/query.ts +146 -0
  209. package/src/knowledge/route-params.ts +39 -0
  210. package/src/knowledge/screens.ts +139 -0
  211. package/src/knowledge/tree.ts +66 -0
  212. package/src/knowledge/types.ts +60 -0
  213. package/src/knowledge/why.ts +189 -0
  214. package/src/mcp/server.ts +109 -0
  215. package/src/registry.ts +191 -0
  216. package/src/runtime.ts +54 -0
  217. package/src/targets.ts +44 -0
@@ -0,0 +1,35 @@
1
+ import assert from 'node:assert/strict';
2
+ import { test } from 'node:test';
3
+ import { describePlan, plan } from '../plan.js';
4
+ const graph = (routes) => ({
5
+ app: 'test',
6
+ root: '/tmp',
7
+ extractedAt: '2026-01-01',
8
+ screens: Object.fromEntries(routes.map((route) => [route, { route, component: route, file: null, declaredIn: 'nav.tsx' }])),
9
+ files: {},
10
+ warnings: [],
11
+ });
12
+ const edge = (e) => ({ seen: 1, last: '2026-01-01', ...e });
13
+ test('a sheet is a place the directions can reach', () => {
14
+ const steps = plan(graph(['Settings.Main', 'Account.Delete']), 'Settings.Main', 'Account.Delete', [
15
+ edge({ from: 'Settings.Main', to: 'Settings.Main', via: 'account-button', toLayer: 'account-button' }),
16
+ edge({ from: 'Settings.Main', to: 'Account.Delete', via: 'delete-account-button', fromLayer: 'account-button' }),
17
+ ]);
18
+ assert.deepEqual(steps?.map((s) => [s.from, s.press, s.to]), [
19
+ ['Settings.Main', 'account-button', 'Settings.Main#account-button'],
20
+ ['Settings.Main#account-button', 'delete-account-button', 'Account.Delete'],
21
+ ]);
22
+ assert.match(describePlan(steps), /opens a layer/);
23
+ });
24
+ test('what a screen offers does not apply while a sheet covers it', () => {
25
+ const steps = plan(graph(['Settings.Main', 'Profile.Edit']), 'Settings.Main#account-button', 'Profile.Edit', [
26
+ edge({ from: 'Settings.Main', to: 'Profile.Edit', via: 'profile-button' }),
27
+ ]);
28
+ assert.equal(steps, null);
29
+ });
30
+ test('asking for a route accepts arriving with something open over it', () => {
31
+ const steps = plan(graph(['Settings.Main', 'Checkout.Main']), 'Settings.Main', 'Checkout.Main', [
32
+ edge({ from: 'Settings.Main', to: 'Checkout.Main', via: 'send-button', toLayer: 'send-button' }),
33
+ ]);
34
+ assert.deepEqual(steps?.map((s) => s.to), ['Checkout.Main#send-button']);
35
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,55 @@
1
+ import assert from 'node:assert/strict';
2
+ import { test } from 'node:test';
3
+ import { screensAffectedBy } from '../query.js';
4
+ const graph = {
5
+ app: 'test',
6
+ root: 'Root',
7
+ extractedAt: '',
8
+ warnings: [],
9
+ screens: {
10
+ 'A.Main': { route: 'A.Main', component: 'A', file: 'app/a.tsx', declaredIn: 'nav.tsx' },
11
+ 'B.Main': { route: 'B.Main', component: 'B', file: 'app/b.tsx', declaredIn: 'nav.tsx' },
12
+ },
13
+ files: {
14
+ 'app/a.tsx': { imports: ['app/shared.tsx'] },
15
+ 'app/b.tsx': { imports: [] },
16
+ 'app/shared.tsx': { imports: [] },
17
+ },
18
+ };
19
+ test('a changed screen file is affected at distance 0', () => {
20
+ const r = screensAffectedBy(graph, ['app/a.tsx']);
21
+ assert.deepEqual(r.map((x) => [x.route, x.distance]), [['A.Main', 0]]);
22
+ });
23
+ test('a shared file reaches its importer at distance 1, naming the changed file', () => {
24
+ const r = screensAffectedBy(graph, ['app/shared.tsx']);
25
+ assert.deepEqual(r.map((x) => [x.route, x.distance, x.via]), [['A.Main', 1, 'app/shared.tsx']]);
26
+ });
27
+ test('results are nearest first', () => {
28
+ const g = {
29
+ ...graph,
30
+ screens: { ...graph.screens, 'C.Main': { route: 'C.Main', component: 'C', file: 'app/c.tsx', declaredIn: 'nav.tsx' } },
31
+ files: { ...graph.files, 'app/c.tsx': { imports: ['app/a.tsx'] } },
32
+ };
33
+ const r = screensAffectedBy(g, ['app/shared.tsx']);
34
+ assert.deepEqual(r.map((x) => [x.route, x.distance]), [
35
+ ['A.Main', 1],
36
+ ['C.Main', 2],
37
+ ]);
38
+ });
39
+ test('an unrelated change affects nothing', () => {
40
+ assert.deepEqual(screensAffectedBy(graph, ['app/unrelated.tsx']), []);
41
+ });
42
+ test('a barrel reaches who imports through it only when the barrel itself changed', () => {
43
+ const g = {
44
+ ...graph,
45
+ files: {
46
+ 'app/a.tsx': { imports: ['app/hooks/useThing.ts'], barrels: ['app/hooks/index.ts'] },
47
+ 'app/b.tsx': { imports: ['app/hooks/useOther.ts'], barrels: ['app/hooks/index.ts'] },
48
+ 'app/hooks/index.ts': { imports: ['app/hooks/useThing.ts', 'app/hooks/useOther.ts'] },
49
+ 'app/hooks/useThing.ts': { imports: [] },
50
+ 'app/hooks/useOther.ts': { imports: [] },
51
+ },
52
+ };
53
+ assert.deepEqual(screensAffectedBy(g, ['app/hooks/useThing.ts']).map((x) => x.route), ['A.Main']);
54
+ assert.deepEqual(screensAffectedBy(g, ['app/hooks/index.ts']).map((x) => x.route), ['A.Main', 'B.Main']);
55
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,30 @@
1
+ import assert from 'node:assert/strict';
2
+ import fs from 'node:fs';
3
+ import os from 'node:os';
4
+ import path from 'node:path';
5
+ import { test } from 'node:test';
6
+ import { diffGeometry, record } from '../screens.js';
7
+ const f = (x, y, w, h) => ({ x, y, w, h });
8
+ test('drift within the tolerance is not a change', () => {
9
+ assert.deepEqual(diffGeometry({ a: f(10, 10, 100, 40) }, { a: f(12, 8, 102, 41) }), []);
10
+ });
11
+ test('a move past the tolerance is reported with both positions', () => {
12
+ assert.deepEqual(diffGeometry({ a: f(10, 10, 100, 40) }, { a: f(10, 60, 100, 40) }), ['#a moved (10,10)→(10,60)']);
13
+ });
14
+ test('a resize is reported', () => {
15
+ assert.deepEqual(diffGeometry({ a: f(0, 0, 100, 40) }, { a: f(0, 0, 100, 80) }), ['#a resized 100×40→100×80']);
16
+ });
17
+ test('an element that collapsed to zero is named as such', () => {
18
+ assert.deepEqual(diffGeometry({ a: f(0, 0, 100, 40) }, { a: f(0, 0, 100, 0) }), ['#a collapsed to 100×0 (was 100×40)']);
19
+ });
20
+ test('elements only on one side are not geometry changes', () => {
21
+ assert.deepEqual(diffGeometry({ a: f(0, 0, 1, 1) }, { b: f(0, 0, 1, 1) }), []);
22
+ });
23
+ test('a recorded screen keeps what is presented, not what another tab or a covered screen holds', () => {
24
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'lynkeus-screens-'));
25
+ const el = (i, testId, hidden) => ({ i, kind: 'button', frame: f(0, i * 50, 100, 40), enabled: true, depth: 0, testId, hidden });
26
+ const screen = (elements) => ({ route: 'Settings.Main', path: ['Settings.Main'], commit: 1, busy: false, window: { w: 390, h: 844 }, costMs: 1, elements });
27
+ record(dir, screen([el(0, 'save'), el(1, 'other-tab', 'a11y'), el(2, 'slide', 'inert')]));
28
+ const change = record(dir, screen([el(0, 'save'), el(2, 'slide', 'inert')]));
29
+ assert.equal(change, null);
30
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,28 @@
1
+ import assert from 'node:assert/strict';
2
+ import { test } from 'node:test';
3
+ import { explain } from '../why.js';
4
+ const el = (p, i) => ({ i, kind: 'button', frame: { x: 0, y: 0, w: 10, h: 10 }, enabled: true, depth: 1, tag: i + 1, ...p });
5
+ const screen = (elements, route) => ({ route, path: route ? [route] : [], elements, commit: 5, costMs: 1 });
6
+ const req = (status, url, error) => ({ id: 1, method: 'GET', url, status, startedAt: 0, ms: 10, error });
7
+ const rules = (f) => f.map((x) => x.rule);
8
+ test('a dead navigator and a gone session are named', () => {
9
+ assert.deepEqual(rules(explain({ screen: screen([]), requests: [], inFlight: 0 })), ['dead-navigator']);
10
+ assert.ok(rules(explain({ screen: screen([el({}, 0)], 'Home'), requests: [req(401, 'http://x/a'), req(401, 'http://x/b')], inFlight: 0 })).includes('session-gone'));
11
+ });
12
+ test('a covered target, a near text and a capped list', () => {
13
+ const s = screen([el({ testId: 'submit', covered: true }, 0), el({ kind: 'text', text: 'Café Ünïcode wörks here' }, 1)], 'Home');
14
+ assert.ok(rules(explain({ screen: s, requests: [], inFlight: 0, target: { testId: 'submit' } })).includes('covered'));
15
+ assert.ok(rules(explain({ screen: s, requests: [], inFlight: 0, target: { text: 'Cafe Unicode works here' } })).includes('near-match'));
16
+ assert.ok(rules(explain({ screen: s, requests: [], inFlight: 0, target: { text: 'cafe unicode works here' } })).includes('near-match'));
17
+ assert.ok(!rules(explain({ screen: s, requests: [], inFlight: 0, target: { text: 'A completely different sentence' } })).includes('near-match'));
18
+ const list = screen(Array.from({ length: 12 }, (_, i) => el({ testId: `row-${i}-item`, text: `Item${i}` }, i)), 'Search');
19
+ assert.ok(rules(explain({ screen: list, requests: [req(200, 'http://x/search')], inFlight: 0, target: { text: 'itemabc' } })).includes('capped-list'));
20
+ });
21
+ test('backend and loading conditions', () => {
22
+ const s = screen([el({}, 0)], 'Home');
23
+ assert.ok(rules(explain({ screen: s, requests: [req(500, 'http://x/api/a')], inFlight: 0 })).includes('backend-error'));
24
+ assert.ok(rules(explain({ screen: s, requests: [req(200, 'http://x/a')], inFlight: 2, target: { testId: 'x' } })).includes('in-flight'));
25
+ const stall = { seq: 1, t: 1, kind: 'stall', renders: 2000, windowMs: 3000, component: 'Feed' };
26
+ assert.ok(rules(explain({ screen: s, requests: [], inFlight: 0, trace: [stall] })).includes('render-loop'));
27
+ assert.deepEqual(rules(explain({ screen: s, requests: [req(200, 'http://x/a')], inFlight: 0 })), []);
28
+ });
@@ -0,0 +1,30 @@
1
+ export type ObservedEdge = {
2
+ from: string;
3
+ to: string;
4
+ /** A testID, or `text:<label>` when the control had none. */
5
+ via: string;
6
+ /** The layer open over `from` when the press happened, named after the control that opened it. */
7
+ fromLayer?: string;
8
+ /** The layer left open over `to`, named the same way. */
9
+ toLayer?: string;
10
+ seen: number;
11
+ /** A date, not a timestamp, so a run does not churn every line. */
12
+ last: string;
13
+ };
14
+ export type EdgeObservation = Pick<ObservedEdge, 'from' | 'to' | 'via' | 'fromLayer' | 'toLayer'>;
15
+ export declare const splitNode: (node: string) => {
16
+ route: string;
17
+ layer?: string;
18
+ };
19
+ export declare const edgeFrom: (edge: EdgeObservation) => string;
20
+ export declare const edgeTo: (edge: EdgeObservation) => string;
21
+ export declare const load: (file: string) => ObservedEdge[];
22
+ /** The edges in a file, kept in memory while a run adds to them. */
23
+ export declare const open: (file: string) => {
24
+ all: () => ObservedEdge[];
25
+ /** True when the edge is new. */
26
+ record: (observation: EdgeObservation) => boolean;
27
+ compact: () => void;
28
+ };
29
+ /** Drops the edges of routes that no longer exist, so the store is collected against the app rather than a clock. */
30
+ export declare const prune: (file: string, routes: Set<string>) => number;
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Navigations a run has actually walked, as `edges.jsonl`: one sorted line per
3
+ * distinct edge so concurrent edits merge in git, and a counter instead of a
4
+ * row per sighting so the file converges instead of growing.
5
+ *
6
+ * A run appends one line per sighting and compacts once at the end; reading
7
+ * merges duplicate lines, so a run that is killed halfway loses nothing.
8
+ */
9
+ import { appendJsonl, readJsonl, today, writeJsonl } from '../files.js';
10
+ const nodeId = (route, layer) => (layer ? `${route}#${layer}` : route);
11
+ export const splitNode = (node) => {
12
+ const at = node.indexOf('#');
13
+ return at === -1 ? { route: node } : { route: node.slice(0, at), layer: node.slice(at + 1) };
14
+ };
15
+ export const edgeFrom = (edge) => nodeId(edge.from, edge.fromLayer);
16
+ export const edgeTo = (edge) => nodeId(edge.to, edge.toLayer);
17
+ const key = (edge) => `${edgeFrom(edge)} ${edge.via} ${edgeTo(edge)}`;
18
+ const merge = (into, row) => {
19
+ into.seen += row.seen;
20
+ if (row.last > into.last)
21
+ into.last = row.last;
22
+ };
23
+ const merged = (rows) => {
24
+ const byKey = new Map();
25
+ for (const row of rows) {
26
+ const found = byKey.get(key(row));
27
+ if (found)
28
+ merge(found, row);
29
+ else
30
+ byKey.set(key(row), { ...row });
31
+ }
32
+ return byKey;
33
+ };
34
+ const save = (file, edges) => writeJsonl(file, [...edges].sort((a, b) => key(a).localeCompare(key(b))));
35
+ export const load = (file) => [...merged(readJsonl(file)).values()];
36
+ /** The edges in a file, kept in memory while a run adds to them. */
37
+ export const open = (file) => {
38
+ const known = merged(readJsonl(file));
39
+ let appended = false;
40
+ return {
41
+ all: () => [...known.values()],
42
+ /** True when the edge is new. */
43
+ record: (observation) => {
44
+ const row = { ...observation, seen: 1, last: today() };
45
+ appendJsonl(file, row);
46
+ appended = true;
47
+ const found = known.get(key(row));
48
+ if (found)
49
+ merge(found, row);
50
+ else
51
+ known.set(key(row), { ...row });
52
+ return !found;
53
+ },
54
+ compact: () => {
55
+ if (appended)
56
+ save(file, load(file));
57
+ },
58
+ };
59
+ };
60
+ /** Drops the edges of routes that no longer exist, so the store is collected against the app rather than a clock. */
61
+ export const prune = (file, routes) => {
62
+ const before = load(file);
63
+ if (before.length === 0)
64
+ return 0;
65
+ const after = before.filter((e) => routes.has(e.from) && routes.has(e.to));
66
+ save(file, after);
67
+ return before.length - after.length;
68
+ };
@@ -0,0 +1,8 @@
1
+ /** The usual cause and fix of a headless failure, found in jest's output so nobody reads a 300-line stack for it. */
2
+ export type Explanation = {
3
+ cause: string;
4
+ fix: string;
5
+ match: string;
6
+ };
7
+ export declare const explainError: (text: string) => Explanation[];
8
+ export declare const describeExplanations: (list: Explanation[]) => string;
@@ -0,0 +1,62 @@
1
+ const RULES = [
2
+ {
3
+ re: /TurboModuleRegistry\.getEnforcing\(\.\.\.\): '([^']+)' could not be found/,
4
+ cause: (m) => `the native module ${m[1]} does not exist under jest`,
5
+ fix: (m) => `mock the package that owns ${m[1]} in the headless entry: jest.mock('<package>', () => ({ ... })). \`lynkeus headless doctor\` lists where it is referenced.`,
6
+ },
7
+ {
8
+ re: /Invariant Violation: (?:requireNativeComponent: )?"?([\w.]+)"? was not found in the UIManager/,
9
+ cause: (m) => `the native view ${m[1]} has no jest counterpart`,
10
+ fix: (m) => `mock the library that renders ${m[1]} with a plain View (see the vision-camera and webview stubs lynkeus ships for the shape).`,
11
+ },
12
+ {
13
+ re: /Cannot find module '([^']+)' from '([^']+)'/,
14
+ cause: (m) => `${m[2]} imports ${m[1]}, which jest cannot resolve`,
15
+ fix: (m) => `if ${m[1]} is a path alias, add it to jest moduleNameMapper; if it is an optional native package, jest.mock it in the headless entry.`,
16
+ },
17
+ {
18
+ re: /lynkeus headless only drives a local backend; API_URL is "([^"]*)"/,
19
+ cause: (m) => `API_URL is "${m[1] || '(empty)'}", not a local backend`,
20
+ fix: () => `export API_URL=http://127.0.0.1:<port>/ before \`lynkeus headless start\`; if the app inlines env at build time, clear its jest transform cache too.`,
21
+ },
22
+ {
23
+ re: /(\w+)\.(\w+) is not a function/,
24
+ cause: (m) => `${m[1]}.${m[2]} is missing — usually a mock that does not implement a method the app calls`,
25
+ fix: (m) => `extend the mock for ${m[1]} with ${m[2]}; the lynkeus defaults can be overridden per key in installDefaultMocks({ overrides }).`,
26
+ },
27
+ {
28
+ re: /Cannot read propert(?:y|ies) of undefined \(reading '(\w+)'\)/,
29
+ cause: (m) => `something the app expected to exist is undefined (reading '${m[1]}')`,
30
+ fix: () => `look one frame up the stack for the module: a mock returning {} instead of the shape the app reads, or a store not initialised because a service failed earlier.`,
31
+ },
32
+ {
33
+ re: /Jest did not exit one second after the test run has completed/,
34
+ cause: () => `the host finished but something kept the event loop alive`,
35
+ fix: () => `harmless for a host that is meant to stay up; when it is not, an open socket or timer from a mock is the usual culprit.`,
36
+ },
37
+ {
38
+ re: /ECONNREFUSED (\d+\.\d+\.\d+\.\d+):(\d+)/,
39
+ cause: (m) => `nothing is listening on ${m[1]}:${m[2]}`,
40
+ fix: () => `start the backend (or the lynkeus driver, if this is the agent's socket) and check the port the entry dials.`,
41
+ },
42
+ {
43
+ re: /Test environment jest-environment-\w+ cannot be found|Unexpected token 'export'|SyntaxError: Cannot use import statement/,
44
+ cause: () => `jest is not transforming a dependency (ESM in node_modules)`,
45
+ fix: () => `add the package to transformIgnorePatterns' allow-list in the app's jest config, the same way react-native itself is listed (lynkeus-agent and lynkeus-headless need 'lynkeus-[^/]+' there).`,
46
+ },
47
+ {
48
+ re: /ReferenceError: (\w+) is not defined/,
49
+ cause: (m) => `${m[1]} is a global the runtime does not provide under jest`,
50
+ fix: (m) => `define ${m[1]} in the headless entry before requiring the app (globalThis.${m[1]} = ...), or mock the module that expects it.`,
51
+ },
52
+ ];
53
+ export const explainError = (text) => {
54
+ const out = [];
55
+ for (const rule of RULES) {
56
+ const m = text.match(rule.re);
57
+ if (m)
58
+ out.push({ match: m[0], cause: rule.cause(m), fix: rule.fix(m) });
59
+ }
60
+ return out;
61
+ };
62
+ export const describeExplanations = (list) => list.length === 0 ? '' : ['', 'lynkeus: what this looks like', ...list.map((e) => ` ${e.match}\n cause ${e.cause}\n fix ${e.fix}`)].join('\n');
@@ -0,0 +1,23 @@
1
+ export type AppEvent = {
2
+ name: string;
3
+ props?: Record<string, unknown>;
4
+ };
5
+ export type RunEvent = AppEvent & {
6
+ step: number;
7
+ route?: string;
8
+ };
9
+ type Drainable = {
10
+ command<T = unknown>(name: string, params?: unknown): Promise<T>;
11
+ };
12
+ /** An app's own buffer may say `properties`; lynkeus says `props`. */
13
+ export declare const normalizeEvents: (raw: unknown) => AppEvent[];
14
+ export declare const drainEvents: (device: Drainable) => Promise<AppEvent[]>;
15
+ export declare const eventsFile: (runDir: string) => string;
16
+ export declare const writeEvents: (runDir: string, events: RunEvent[]) => void;
17
+ export declare const readEvents: (file: string) => RunEvent[];
18
+ /** Every run under `runs/` that recorded events, newest first. */
19
+ export declare const eventsFiles: (runsRoot: string) => string[];
20
+ /** A run folder, `last`, or an events.jsonl itself. */
21
+ export declare const resolveEventsFile: (runsRoot: string, ref: string) => string | undefined;
22
+ export declare const describeEvents: (events: RunEvent[]) => string;
23
+ export {};
@@ -0,0 +1,51 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { readJsonl } from '../files.js';
4
+ /** An app's own buffer may say `properties`; lynkeus says `props`. */
5
+ export const normalizeEvents = (raw) => (Array.isArray(raw) ? raw : []).flatMap((e) => {
6
+ const r = e;
7
+ if (typeof r?.name !== 'string')
8
+ return [];
9
+ const props = (r.props ?? r.properties);
10
+ return [{ name: r.name, ...(props && typeof props === 'object' ? { props } : {}) }];
11
+ });
12
+ export const drainEvents = async (device) => {
13
+ try {
14
+ return normalizeEvents(await device.command('events', { clear: true }));
15
+ }
16
+ catch {
17
+ return [];
18
+ }
19
+ };
20
+ export const eventsFile = (runDir) => path.join(runDir, 'events.jsonl');
21
+ export const writeEvents = (runDir, events) => {
22
+ if (events.length === 0)
23
+ return;
24
+ fs.writeFileSync(eventsFile(runDir), `${events.map((e) => JSON.stringify(e)).join('\n')}\n`);
25
+ };
26
+ export const readEvents = (file) => readJsonl(file);
27
+ /** Every run under `runs/` that recorded events, newest first. */
28
+ export const eventsFiles = (runsRoot) => {
29
+ const found = [];
30
+ const visit = (dir, depth) => {
31
+ if (depth > 3 || !fs.existsSync(dir))
32
+ return;
33
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
34
+ const full = path.join(dir, entry.name);
35
+ if (entry.isDirectory())
36
+ visit(full, depth + 1);
37
+ else if (entry.name === 'events.jsonl')
38
+ found.push({ file: full, mtime: fs.statSync(full).mtimeMs });
39
+ }
40
+ };
41
+ visit(runsRoot, 0);
42
+ return found.sort((a, b) => b.mtime - a.mtime).map((f) => f.file);
43
+ };
44
+ /** A run folder, `last`, or an events.jsonl itself. */
45
+ export const resolveEventsFile = (runsRoot, ref) => {
46
+ if (ref === 'last')
47
+ return eventsFiles(runsRoot)[0];
48
+ const full = path.resolve(ref);
49
+ return full.endsWith('.jsonl') ? full : eventsFile(full);
50
+ };
51
+ export const describeEvents = (events) => events.map((e) => `${String(e.step).padStart(2)} ${e.route ?? '?'} ${e.name}${e.props ? ` ${JSON.stringify(e.props)}` : ''}`).join('\n');
@@ -0,0 +1,26 @@
1
+ import { type SourceFile } from 'ts-morph';
2
+ import type { ScreenGraph, SourcePath } from './types.js';
3
+ type ExtractOptions = {
4
+ root: string;
5
+ navigatorGlobs?: string[];
6
+ /** First-party source, relative to the root. */
7
+ sourceDir?: string;
8
+ };
9
+ export declare const extractScreenGraph: ({ root, navigatorGlobs, sourceDir }: ExtractOptions) => ScreenGraph;
10
+ export type ScreenDeclaration = {
11
+ route: string;
12
+ component: string | null;
13
+ };
14
+ /** Routes in either of React Navigation's shapes: `<Stack.Screen name component />`, or `screens: { 'A.B': { screen: AB } }`. */
15
+ export declare const screenDeclarations: (source: SourceFile) => ScreenDeclaration[];
16
+ /**
17
+ * Import edges. A named import through a barrel (`import { useThing } from
18
+ * '~hooks'`) points at the file that defines the name, not at the barrel:
19
+ * otherwise a change to one hook reaches every screen that imports any hook.
20
+ * The barrels passed through are kept apart, for when the barrel itself changes.
21
+ */
22
+ export declare const collectImports: (source: SourceFile, root: string) => {
23
+ imports: SourcePath[];
24
+ barrels: SourcePath[];
25
+ };
26
+ export {};