lynkeus 0.2.0 → 0.4.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 (134) hide show
  1. package/lib/cases/ahead.d.ts +31 -0
  2. package/lib/cases/ahead.js +90 -0
  3. package/lib/cases/fixtures.d.ts +12 -0
  4. package/lib/cases/fixtures.js +32 -0
  5. package/lib/cases/flakes.d.ts +28 -0
  6. package/lib/cases/flakes.js +61 -0
  7. package/lib/cases/format.d.ts +5 -0
  8. package/lib/cases/format.js +30 -4
  9. package/lib/cases/portClaim.d.ts +10 -0
  10. package/lib/cases/portClaim.js +55 -0
  11. package/lib/cases/runfile.d.ts +24 -0
  12. package/lib/cases/runfile.js +108 -0
  13. package/lib/cases/runner.d.ts +82 -3
  14. package/lib/cases/runner.js +394 -40
  15. package/lib/cases/templates.d.ts +2 -2
  16. package/lib/cases/templates.js +14 -4
  17. package/lib/cases/traces.d.ts +58 -0
  18. package/lib/cases/traces.js +250 -0
  19. package/lib/cli.js +28 -7
  20. package/lib/commands/cases.js +411 -42
  21. package/lib/commands/check.d.ts +6 -0
  22. package/lib/commands/check.js +8 -3
  23. package/lib/commands/doctor.js +8 -2
  24. package/lib/commands/drive.js +32 -8
  25. package/lib/commands/headless.js +132 -8
  26. package/lib/commands/init.d.ts +29 -0
  27. package/lib/commands/init.js +295 -33
  28. package/lib/commands/report.d.ts +97 -0
  29. package/lib/commands/report.js +228 -0
  30. package/lib/config.d.ts +26 -1
  31. package/lib/config.js +30 -0
  32. package/lib/device/agentVersion.d.ts +7 -0
  33. package/lib/device/agentVersion.js +12 -0
  34. package/lib/device/devices.d.ts +5 -0
  35. package/lib/device/devices.js +27 -0
  36. package/lib/device/driver.js +2 -2
  37. package/lib/flow/runner.js +33 -1
  38. package/lib/flow/types.d.ts +2 -1
  39. package/lib/knowledge/contrast.d.ts +45 -0
  40. package/lib/knowledge/contrast.js +114 -0
  41. package/lib/knowledge/errors.js +1 -1
  42. package/lib/knowledge/lint.d.ts +2 -0
  43. package/lib/knowledge/lint.js +13 -3
  44. package/lib/mcp/server.js +31 -2
  45. package/lib/registry.d.ts +5 -0
  46. package/lib/targets.d.ts +10 -1
  47. package/lib/targets.js +16 -3
  48. package/package.json +10 -6
  49. package/src/cases/ahead.ts +111 -0
  50. package/src/cases/fixtures.ts +37 -0
  51. package/src/cases/flakes.ts +67 -0
  52. package/src/cases/format.ts +31 -6
  53. package/src/cases/portClaim.ts +52 -0
  54. package/src/cases/runfile.ts +119 -0
  55. package/src/cases/runner.ts +454 -46
  56. package/src/cases/templates.ts +14 -4
  57. package/src/cases/traces.ts +298 -0
  58. package/src/cli.ts +29 -7
  59. package/src/commands/cases.ts +428 -41
  60. package/src/commands/check.ts +11 -4
  61. package/src/commands/doctor.ts +9 -2
  62. package/src/commands/drive.ts +31 -8
  63. package/src/commands/headless.ts +143 -8
  64. package/src/commands/init.ts +338 -30
  65. package/src/commands/report.ts +272 -0
  66. package/src/config.ts +39 -2
  67. package/src/device/agentVersion.ts +15 -0
  68. package/src/device/devices.ts +28 -0
  69. package/src/device/driver.ts +2 -2
  70. package/src/flow/runner.ts +35 -1
  71. package/src/flow/types.ts +2 -2
  72. package/src/knowledge/contrast.ts +135 -0
  73. package/src/knowledge/errors.ts +1 -1
  74. package/src/knowledge/lint.ts +15 -4
  75. package/src/mcp/server.ts +31 -2
  76. package/src/registry.ts +2 -0
  77. package/src/targets.ts +17 -3
  78. package/lib/__tests__/args.test.d.ts +0 -1
  79. package/lib/__tests__/args.test.js +0 -69
  80. package/lib/__tests__/check.test.d.ts +0 -1
  81. package/lib/__tests__/check.test.js +0 -60
  82. package/lib/__tests__/init.test.d.ts +0 -1
  83. package/lib/__tests__/init.test.js +0 -42
  84. package/lib/__tests__/record.test.d.ts +0 -1
  85. package/lib/__tests__/record.test.js +0 -44
  86. package/lib/__tests__/snapshot.test.d.ts +0 -1
  87. package/lib/__tests__/snapshot.test.js +0 -47
  88. package/lib/__tests__/targets.test.d.ts +0 -1
  89. package/lib/__tests__/targets.test.js +0 -127
  90. package/lib/cases/__tests__/format.test.d.ts +0 -1
  91. package/lib/cases/__tests__/format.test.js +0 -61
  92. package/lib/cases/__tests__/runner.test.d.ts +0 -1
  93. package/lib/cases/__tests__/runner.test.js +0 -188
  94. package/lib/knowledge/__tests__/edges.test.d.ts +0 -1
  95. package/lib/knowledge/__tests__/edges.test.js +0 -25
  96. package/lib/knowledge/__tests__/events.test.d.ts +0 -1
  97. package/lib/knowledge/__tests__/events.test.js +0 -39
  98. package/lib/knowledge/__tests__/extract.test.d.ts +0 -1
  99. package/lib/knowledge/__tests__/extract.test.js +0 -69
  100. package/lib/knowledge/__tests__/frontier.test.d.ts +0 -1
  101. package/lib/knowledge/__tests__/frontier.test.js +0 -57
  102. package/lib/knowledge/__tests__/funnels.test.d.ts +0 -1
  103. package/lib/knowledge/__tests__/funnels.test.js +0 -40
  104. package/lib/knowledge/__tests__/lint.test.d.ts +0 -1
  105. package/lib/knowledge/__tests__/lint.test.js +0 -53
  106. package/lib/knowledge/__tests__/perf.test.d.ts +0 -1
  107. package/lib/knowledge/__tests__/perf.test.js +0 -34
  108. package/lib/knowledge/__tests__/plan.test.d.ts +0 -1
  109. package/lib/knowledge/__tests__/plan.test.js +0 -35
  110. package/lib/knowledge/__tests__/query.test.d.ts +0 -1
  111. package/lib/knowledge/__tests__/query.test.js +0 -55
  112. package/lib/knowledge/__tests__/screens.test.d.ts +0 -1
  113. package/lib/knowledge/__tests__/screens.test.js +0 -30
  114. package/lib/knowledge/__tests__/why.test.d.ts +0 -1
  115. package/lib/knowledge/__tests__/why.test.js +0 -28
  116. package/src/__tests__/args.test.ts +0 -82
  117. package/src/__tests__/check.test.ts +0 -71
  118. package/src/__tests__/init.test.ts +0 -52
  119. package/src/__tests__/record.test.ts +0 -49
  120. package/src/__tests__/snapshot.test.ts +0 -51
  121. package/src/__tests__/targets.test.ts +0 -132
  122. package/src/cases/__tests__/format.test.ts +0 -76
  123. package/src/cases/__tests__/runner.test.ts +0 -230
  124. package/src/knowledge/__tests__/edges.test.ts +0 -34
  125. package/src/knowledge/__tests__/events.test.ts +0 -45
  126. package/src/knowledge/__tests__/extract.test.ts +0 -88
  127. package/src/knowledge/__tests__/frontier.test.ts +0 -77
  128. package/src/knowledge/__tests__/funnels.test.ts +0 -58
  129. package/src/knowledge/__tests__/lint.test.ts +0 -66
  130. package/src/knowledge/__tests__/perf.test.ts +0 -41
  131. package/src/knowledge/__tests__/plan.test.ts +0 -52
  132. package/src/knowledge/__tests__/query.test.ts +0 -74
  133. package/src/knowledge/__tests__/screens.test.ts +0 -37
  134. package/src/knowledge/__tests__/why.test.ts +0 -40
@@ -1,52 +0,0 @@
1
- import assert from 'node:assert/strict';
2
- import { test } from 'node:test';
3
-
4
- import type { ObservedEdge } from '../edges.js';
5
- import { describePlan, plan } from '../plan.js';
6
- import type { ScreenGraph } from '../types.js';
7
-
8
- const graph = (routes: string[]): ScreenGraph => ({
9
- app: 'test',
10
- root: '/tmp',
11
- extractedAt: '2026-01-01',
12
- screens: Object.fromEntries(routes.map((route) => [route, { route, component: route, file: null, declaredIn: 'nav.tsx' }])),
13
- files: {},
14
- warnings: [],
15
- });
16
-
17
- const edge = (e: Partial<ObservedEdge> & Pick<ObservedEdge, 'from' | 'to' | 'via'>): ObservedEdge => ({ seen: 1, last: '2026-01-01', ...e });
18
-
19
- test('a sheet is a place the directions can reach', () => {
20
- const steps = plan(graph(['Settings.Main', 'Account.Delete']), 'Settings.Main', 'Account.Delete', [
21
- edge({ from: 'Settings.Main', to: 'Settings.Main', via: 'account-button', toLayer: 'account-button' }),
22
- edge({ from: 'Settings.Main', to: 'Account.Delete', via: 'delete-account-button', fromLayer: 'account-button' }),
23
- ]);
24
-
25
- assert.deepEqual(
26
- steps?.map((s) => [s.from, s.press, s.to]),
27
- [
28
- ['Settings.Main', 'account-button', 'Settings.Main#account-button'],
29
- ['Settings.Main#account-button', 'delete-account-button', 'Account.Delete'],
30
- ],
31
- );
32
- assert.match(describePlan(steps!), /opens a layer/);
33
- });
34
-
35
- test('what a screen offers does not apply while a sheet covers it', () => {
36
- const steps = plan(graph(['Settings.Main', 'Profile.Edit']), 'Settings.Main#account-button', 'Profile.Edit', [
37
- edge({ from: 'Settings.Main', to: 'Profile.Edit', via: 'profile-button' }),
38
- ]);
39
-
40
- assert.equal(steps, null);
41
- });
42
-
43
- test('asking for a route accepts arriving with something open over it', () => {
44
- const steps = plan(graph(['Settings.Main', 'Checkout.Main']), 'Settings.Main', 'Checkout.Main', [
45
- edge({ from: 'Settings.Main', to: 'Checkout.Main', via: 'send-button', toLayer: 'send-button' }),
46
- ]);
47
-
48
- assert.deepEqual(
49
- steps?.map((s) => s.to),
50
- ['Checkout.Main#send-button'],
51
- );
52
- });
@@ -1,74 +0,0 @@
1
- import assert from 'node:assert/strict';
2
- import { test } from 'node:test';
3
-
4
- import { screensAffectedBy } from '../query.js';
5
- import type { ScreenGraph } from '../types.js';
6
-
7
- const graph = {
8
- app: 'test',
9
- root: 'Root',
10
- extractedAt: '',
11
- warnings: [],
12
- screens: {
13
- 'A.Main': { route: 'A.Main', component: 'A', file: 'app/a.tsx', declaredIn: 'nav.tsx' },
14
- 'B.Main': { route: 'B.Main', component: 'B', file: 'app/b.tsx', declaredIn: 'nav.tsx' },
15
- },
16
- files: {
17
- 'app/a.tsx': { imports: ['app/shared.tsx'] },
18
- 'app/b.tsx': { imports: [] },
19
- 'app/shared.tsx': { imports: [] },
20
- },
21
- } as unknown as ScreenGraph;
22
-
23
- test('a changed screen file is affected at distance 0', () => {
24
- const r = screensAffectedBy(graph, ['app/a.tsx']);
25
- assert.deepEqual(
26
- r.map((x) => [x.route, x.distance]),
27
- [['A.Main', 0]],
28
- );
29
- });
30
- test('a shared file reaches its importer at distance 1, naming the changed file', () => {
31
- const r = screensAffectedBy(graph, ['app/shared.tsx']);
32
- assert.deepEqual(
33
- r.map((x) => [x.route, x.distance, x.via]),
34
- [['A.Main', 1, 'app/shared.tsx']],
35
- );
36
- });
37
- test('results are nearest first', () => {
38
- const g = {
39
- ...graph,
40
- screens: { ...graph.screens, 'C.Main': { route: 'C.Main', component: 'C', file: 'app/c.tsx', declaredIn: 'nav.tsx' } },
41
- files: { ...graph.files, 'app/c.tsx': { imports: ['app/a.tsx'] } },
42
- } as unknown as ScreenGraph;
43
- const r = screensAffectedBy(g, ['app/shared.tsx']);
44
- assert.deepEqual(
45
- r.map((x) => [x.route, x.distance]),
46
- [
47
- ['A.Main', 1],
48
- ['C.Main', 2],
49
- ],
50
- );
51
- });
52
- test('an unrelated change affects nothing', () => {
53
- assert.deepEqual(screensAffectedBy(graph, ['app/unrelated.tsx']), []);
54
- });
55
- test('a barrel reaches who imports through it only when the barrel itself changed', () => {
56
- const g = {
57
- ...graph,
58
- files: {
59
- 'app/a.tsx': { imports: ['app/hooks/useThing.ts'], barrels: ['app/hooks/index.ts'] },
60
- 'app/b.tsx': { imports: ['app/hooks/useOther.ts'], barrels: ['app/hooks/index.ts'] },
61
- 'app/hooks/index.ts': { imports: ['app/hooks/useThing.ts', 'app/hooks/useOther.ts'] },
62
- 'app/hooks/useThing.ts': { imports: [] },
63
- 'app/hooks/useOther.ts': { imports: [] },
64
- },
65
- } as unknown as ScreenGraph;
66
- assert.deepEqual(
67
- screensAffectedBy(g, ['app/hooks/useThing.ts']).map((x) => x.route),
68
- ['A.Main'],
69
- );
70
- assert.deepEqual(
71
- screensAffectedBy(g, ['app/hooks/index.ts']).map((x) => x.route),
72
- ['A.Main', 'B.Main'],
73
- );
74
- });
@@ -1,37 +0,0 @@
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
-
7
- import type { Screen } from 'lynkeus-protocol';
8
-
9
- import { diffGeometry, record } from '../screens.js';
10
-
11
- const f = (x: number, y: number, w: number, h: number) => ({ x, y, w, h });
12
-
13
- test('drift within the tolerance is not a change', () => {
14
- assert.deepEqual(diffGeometry({ a: f(10, 10, 100, 40) }, { a: f(12, 8, 102, 41) }), []);
15
- });
16
- test('a move past the tolerance is reported with both positions', () => {
17
- assert.deepEqual(diffGeometry({ a: f(10, 10, 100, 40) }, { a: f(10, 60, 100, 40) }), ['#a moved (10,10)→(10,60)']);
18
- });
19
- test('a resize is reported', () => {
20
- assert.deepEqual(diffGeometry({ a: f(0, 0, 100, 40) }, { a: f(0, 0, 100, 80) }), ['#a resized 100×40→100×80']);
21
- });
22
- test('an element that collapsed to zero is named as such', () => {
23
- assert.deepEqual(diffGeometry({ a: f(0, 0, 100, 40) }, { a: f(0, 0, 100, 0) }), ['#a collapsed to 100×0 (was 100×40)']);
24
- });
25
- test('elements only on one side are not geometry changes', () => {
26
- assert.deepEqual(diffGeometry({ a: f(0, 0, 1, 1) }, { b: f(0, 0, 1, 1) }), []);
27
- });
28
-
29
- test('a recorded screen keeps what is presented, not what another tab or a covered screen holds', () => {
30
- const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'lynkeus-screens-'));
31
- const el = (i: number, testId: string, hidden?: string) => ({ i, kind: 'button', frame: f(0, i * 50, 100, 40), enabled: true, depth: 0, testId, hidden });
32
- const screen = (elements: object[]) =>
33
- ({ route: 'Settings.Main', path: ['Settings.Main'], commit: 1, busy: false, window: { w: 390, h: 844 }, costMs: 1, elements }) as unknown as Screen;
34
- record(dir, screen([el(0, 'save'), el(1, 'other-tab', 'a11y'), el(2, 'slide', 'inert')]));
35
- const change = record(dir, screen([el(0, 'save'), el(2, 'slide', 'inert')]));
36
- assert.equal(change, null);
37
- });
@@ -1,40 +0,0 @@
1
- import assert from 'node:assert/strict';
2
- import { test } from 'node:test';
3
-
4
- import { explain } from '../why.js';
5
- import type { Element, RequestRecord, Screen, TraceEvent } from 'lynkeus-protocol';
6
-
7
- const el = (p: Partial<Element>, i: number): Element =>
8
- ({ i, kind: 'button', frame: { x: 0, y: 0, w: 10, h: 10 }, enabled: true, depth: 1, tag: i + 1, ...p }) as Element;
9
- const screen = (elements: Element[], route?: string): Screen => ({ route, path: route ? [route] : [], elements, commit: 5, costMs: 1 }) as unknown as Screen;
10
- const req = (status: number | undefined, url: string, error?: string): RequestRecord => ({ id: 1, method: 'GET', url, status, startedAt: 0, ms: 10, error });
11
- const rules = (f: ReturnType<typeof explain>) => f.map((x) => x.rule);
12
-
13
- test('a dead navigator and a gone session are named', () => {
14
- assert.deepEqual(rules(explain({ screen: screen([]), requests: [], inFlight: 0 })), ['dead-navigator']);
15
- assert.ok(
16
- rules(explain({ screen: screen([el({}, 0)], 'Home'), requests: [req(401, 'http://x/a'), req(401, 'http://x/b')], inFlight: 0 })).includes('session-gone'),
17
- );
18
- });
19
-
20
- test('a covered target, a near text and a capped list', () => {
21
- const s = screen([el({ testId: 'submit', covered: true }, 0), el({ kind: 'text', text: 'Café Ünïcode wörks here' }, 1)], 'Home');
22
- assert.ok(rules(explain({ screen: s, requests: [], inFlight: 0, target: { testId: 'submit' } })).includes('covered'));
23
- assert.ok(rules(explain({ screen: s, requests: [], inFlight: 0, target: { text: 'Cafe Unicode works here' } })).includes('near-match'));
24
- assert.ok(rules(explain({ screen: s, requests: [], inFlight: 0, target: { text: 'cafe unicode works here' } })).includes('near-match'));
25
- assert.ok(!rules(explain({ screen: s, requests: [], inFlight: 0, target: { text: 'A completely different sentence' } })).includes('near-match'));
26
- const list = screen(
27
- Array.from({ length: 12 }, (_, i) => el({ testId: `row-${i}-item`, text: `Item${i}` }, i)),
28
- 'Search',
29
- );
30
- assert.ok(rules(explain({ screen: list, requests: [req(200, 'http://x/search')], inFlight: 0, target: { text: 'itemabc' } })).includes('capped-list'));
31
- });
32
-
33
- test('backend and loading conditions', () => {
34
- const s = screen([el({}, 0)], 'Home');
35
- assert.ok(rules(explain({ screen: s, requests: [req(500, 'http://x/api/a')], inFlight: 0 })).includes('backend-error'));
36
- assert.ok(rules(explain({ screen: s, requests: [req(200, 'http://x/a')], inFlight: 2, target: { testId: 'x' } })).includes('in-flight'));
37
- const stall: TraceEvent = { seq: 1, t: 1, kind: 'stall', renders: 2000, windowMs: 3000, component: 'Feed' };
38
- assert.ok(rules(explain({ screen: s, requests: [], inFlight: 0, trace: [stall] })).includes('render-loop'));
39
- assert.deepEqual(rules(explain({ screen: s, requests: [req(200, 'http://x/a')], inFlight: 0 })), []);
40
- });