@principal-ai/subsystems-react 0.56.3 → 0.56.5

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 (50) hide show
  1. package/dist/index.d.ts +6 -0
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js +3 -0
  4. package/dist/index.js.map +1 -1
  5. package/dist/subsystem/DecorationPanel.d.ts +37 -0
  6. package/dist/subsystem/DecorationPanel.d.ts.map +1 -0
  7. package/dist/subsystem/DecorationPanel.js +147 -0
  8. package/dist/subsystem/DecorationPanel.js.map +1 -0
  9. package/dist/subsystem/DiagnosticToggle.d.ts +7 -1
  10. package/dist/subsystem/DiagnosticToggle.d.ts.map +1 -1
  11. package/dist/subsystem/DiagnosticToggle.js +8 -7
  12. package/dist/subsystem/DiagnosticToggle.js.map +1 -1
  13. package/dist/subsystem/GraphSidebar.d.ts +4 -2
  14. package/dist/subsystem/GraphSidebar.d.ts.map +1 -1
  15. package/dist/subsystem/GraphSidebar.js +3 -1
  16. package/dist/subsystem/GraphSidebar.js.map +1 -1
  17. package/dist/subsystem/SubsystemComponentGraph.d.ts +10 -13
  18. package/dist/subsystem/SubsystemComponentGraph.d.ts.map +1 -1
  19. package/dist/subsystem/SubsystemComponentGraph.js +112 -272
  20. package/dist/subsystem/SubsystemComponentGraph.js.map +1 -1
  21. package/dist/subsystem/TrailStepsNav.d.ts.map +1 -1
  22. package/dist/subsystem/TrailStepsNav.js +6 -4
  23. package/dist/subsystem/TrailStepsNav.js.map +1 -1
  24. package/dist/subsystem/TrailsPanel.d.ts.map +1 -1
  25. package/dist/subsystem/TrailsPanel.js +14 -10
  26. package/dist/subsystem/TrailsPanel.js.map +1 -1
  27. package/dist/subsystem/decoration.d.ts +112 -0
  28. package/dist/subsystem/decoration.d.ts.map +1 -0
  29. package/dist/subsystem/decoration.js +54 -0
  30. package/dist/subsystem/decoration.js.map +1 -0
  31. package/package.json +3 -3
  32. package/src/index.ts +37 -0
  33. package/src/stories/Canvases/C4/C4ToComponentDrill.stories.tsx +38 -71
  34. package/src/stories/Canvases/Subsystem/GraphTitle.stories.tsx +1 -2
  35. package/src/stories/Canvases/Subsystem/StepEmphasis.stories.tsx +528 -0
  36. package/src/stories/Components/DecorationPanel.stories.tsx +160 -0
  37. package/src/subsystem/DecorationPanel.test.tsx +128 -0
  38. package/src/subsystem/DecorationPanel.tsx +334 -0
  39. package/src/subsystem/DiagnosticToggle.tsx +20 -5
  40. package/src/subsystem/GraphSidebar.test.tsx +18 -0
  41. package/src/subsystem/GraphSidebar.tsx +6 -1
  42. package/src/subsystem/SubsystemComponentGraph.sidebar.test.tsx +218 -0
  43. package/src/subsystem/SubsystemComponentGraph.tsx +184 -414
  44. package/src/subsystem/TrailStepsNav.test.tsx +32 -0
  45. package/src/subsystem/TrailStepsNav.tsx +6 -4
  46. package/src/subsystem/TrailsPanel.test.tsx +20 -0
  47. package/src/subsystem/TrailsPanel.tsx +11 -1
  48. package/src/subsystem/decoration.test.ts +72 -0
  49. package/src/subsystem/decoration.ts +151 -0
  50. package/src/subsystem/stepEmphasis.test.ts +110 -0
@@ -149,12 +149,44 @@ describe('TrailStepsNav', () => {
149
149
  expect(getByText('A.go')).toBeTruthy();
150
150
  expect(getByText('B.go')).toBeTruthy();
151
151
  expect(getByText('Read the source seam.')).toBeTruthy();
152
+ const accent = document.createElement('span');
153
+ accent.style.color = defaultEditorTheme.colors.primary;
154
+ const normal = document.createElement('span');
155
+ normal.style.color = defaultEditorTheme.colors.text;
156
+ expect(getByText('A.go').style.color).toBe(normal.style.color);
157
+ expect(getByText('B.go').style.color).toBe(normal.style.color);
158
+ expect(getByText('Read the source seam.').style.color).toBe(accent.style.color);
159
+ expect(getByRole('button', { name: /A\.go/ }).style.paddingLeft).toBe('12px');
160
+ expect(getByRole('button', { name: /B\.go/ }).style.paddingLeft).toBe('22px');
152
161
  fireEvent.click(getByRole('button', { name: /B\.go/ }));
153
162
  expect(focusedStep).toBe(1);
154
163
  fireEvent.click(getByLabelText('Back to trails'));
155
164
  expect(cleared).toBe(true);
156
165
  });
157
166
 
167
+ test('returns sibling calls to their parent indentation level', () => {
168
+ const trail: SubsystemTrail = {
169
+ id: 'nested',
170
+ title: 'Nested calls',
171
+ steps: [step('a', 'b', 'A.go'), step('b', 'c', 'B.go'), step('a', 'd', 'A.next')],
172
+ };
173
+ const { getByRole } = renderNav({
174
+ trails: [trail],
175
+ focusedTrailId: trail.id,
176
+ focusedStepIndex: null,
177
+ hoveredTrailStep: null,
178
+ onFocusFlow: () => {},
179
+ onClearFocus: () => {},
180
+ onFocusStep: () => {},
181
+ onHoverStep: () => {},
182
+ onHoverFlow: () => {},
183
+ onLeaveStep: () => {},
184
+ });
185
+ expect(getByRole('button', { name: /A\.go/ }).style.paddingLeft).toBe('12px');
186
+ expect(getByRole('button', { name: /B\.go/ }).style.paddingLeft).toBe('22px');
187
+ expect(getByRole('button', { name: /A\.next/ }).style.paddingLeft).toBe('12px');
188
+ });
189
+
158
190
  test('uses rows show the logical hop and the via site', () => {
159
191
  const usesTrail: SubsystemTrail = {
160
192
  id: 'uses',
@@ -7,6 +7,7 @@ import {
7
7
  trailStepDisplay,
8
8
  type TrailComponentLabels,
9
9
  } from './trailStepDisplay';
10
+ import { buildTrailStack } from './trailStack';
10
11
 
11
12
  /**
12
13
  * TrailStepsNav — the trails side panel as a two-step slide: a list of trails,
@@ -288,6 +289,7 @@ export function TrailStepsNav({
288
289
  hoveredTrailStep?.trailId === displayedTrail.id &&
289
290
  hoveredTrailStep.stepIndex === i;
290
291
  const display = trailStepDisplay(step, componentByAlias);
292
+ const stackDepth = buildTrailStack(displayedTrail, i, componentByAlias).frames.length;
291
293
  return (
292
294
  <button
293
295
  key={`${step.from}->${step.to}-${i}`}
@@ -300,7 +302,7 @@ export function TrailStepsNav({
300
302
  gap: 8,
301
303
  width: '100%',
302
304
  minWidth: 0,
303
- padding: '8px 16px 8px 12px',
305
+ padding: `8px 16px 8px ${12 + Math.max(0, stackDepth - 1) * 10}px`,
304
306
  border: 'none',
305
307
  borderRadius: 0,
306
308
  background: active || hovered ? hoverBg : 'transparent',
@@ -312,8 +314,8 @@ export function TrailStepsNav({
312
314
  <span
313
315
  style={{
314
316
  flexShrink: 0,
315
- // Same 14px leading column as the header's trail icon,
316
- // so the digit sits under it.
317
+ // Root steps align with the header icon; nested steps
318
+ // move the number and text together.
317
319
  width: 14,
318
320
  textAlign: 'center',
319
321
  paddingTop: 1,
@@ -340,7 +342,7 @@ export function TrailStepsNav({
340
342
  overflowWrap: 'anywhere',
341
343
  fontSize: theme.fontSizes[1],
342
344
  fontFamily: theme.fonts.body,
343
- color: theme.colors.text,
345
+ color: theme.colors.primary,
344
346
  lineHeight: 1.35,
345
347
  }}
346
348
  >
@@ -115,6 +115,26 @@ function renderPanel(overrides: Partial<TrailsPanelProps> = {}) {
115
115
 
116
116
  const COPY_LABEL = 'Copy step 1 of Auth flow for an agent';
117
117
 
118
+ describe('TrailsPanel trail titles', () => {
119
+ for (const collapsed of [true, false]) {
120
+ test(`shows a decorative trail icon before the ${collapsed ? 'collapsed' : 'expanded'} trail name`, () => {
121
+ const onFocusFlow = mock(() => {});
122
+ const { getByRole } = renderPanel({
123
+ expandedTrails: collapsed ? new Set() : new Set(['auth-flow']),
124
+ onFocusFlow,
125
+ });
126
+ const button = getByRole('button', { name: 'Auth flow', exact: true });
127
+ const icon = button.firstElementChild;
128
+ expect(icon?.tagName.toLowerCase()).toBe('svg');
129
+ expect(icon?.classList.contains('lucide-route')).toBe(true);
130
+ expect(icon?.getAttribute('aria-hidden')).toBe('true');
131
+ expect(icon?.nextElementSibling?.textContent).toBe('Auth flow');
132
+ fireEvent.click(button);
133
+ expect(onFocusFlow).toHaveBeenCalledWith(trails[0]);
134
+ });
135
+ }
136
+ });
137
+
118
138
  describe('TrailsPanel step copy', () => {
119
139
  test('copy icon is hidden until the step row is hovered', () => {
120
140
  const { getByText, queryByLabelText } = renderPanel();
@@ -13,7 +13,7 @@
13
13
  import { useCallback, useEffect, useRef, useState } from 'react';
14
14
  import type { DragEvent as ReactDragEvent } from 'react';
15
15
  import { useTheme } from '@principal-ade/industry-theme';
16
- import { Check, ChevronDown, Copy, Focus, GripVertical, Minimize2, Pause, Play, Star } from 'lucide-react';
16
+ import { Check, ChevronDown, Copy, Focus, GripVertical, Minimize2, Pause, Play, Route, Star } from 'lucide-react';
17
17
  import { IndustryMarkdownSlide } from 'themed-markdown';
18
18
  import {
19
19
  reorderTargetIndex,
@@ -316,6 +316,7 @@ function TrailFlow({
316
316
  display: 'flex',
317
317
  alignItems: 'center',
318
318
  minWidth: 0,
319
+ gap: 8,
319
320
  // Vertical padding, not just the row's min-height: a title that
320
321
  // wraps to two+ lines must grow the header past `minHeight`, and
321
322
  // without this the text sits flush against the row's top and bottom
@@ -329,6 +330,15 @@ function TrailFlow({
329
330
  cursor: 'pointer',
330
331
  }}
331
332
  >
333
+ <Route
334
+ size={14}
335
+ strokeWidth={2}
336
+ aria-hidden
337
+ style={{
338
+ flexShrink: 0,
339
+ color: theme.colors.textMuted ?? theme.colors.textSecondary,
340
+ }}
341
+ />
332
342
  <span
333
343
  style={{
334
344
  minWidth: 0,
@@ -0,0 +1,72 @@
1
+ import { describe, expect, test } from 'bun:test';
2
+ import type { SubsystemTrail } from './model';
3
+ import {
4
+ decorationModelIds,
5
+ decorationTrailId,
6
+ decorationTrailKey,
7
+ findDecorationTrail,
8
+ groupDecorationTrailsByModel,
9
+ type DecorationTrailRef,
10
+ } from './decoration';
11
+
12
+ function ref(modelId: string, trailId: string): DecorationTrailRef {
13
+ return { kind: 'base', modelId, trailId };
14
+ }
15
+
16
+ const draftTrail: SubsystemTrail = {
17
+ id: 'planned-open',
18
+ title: 'Planned open',
19
+ steps: [],
20
+ };
21
+
22
+ describe('decoration trail refs', () => {
23
+ test('keys are scoped by model', () => {
24
+ expect(decorationTrailKey(ref('model-a', 'open'))).toBe('model-a:open');
25
+ expect(decorationTrailKey(ref('model-a', 'open'))).not.toBe(
26
+ decorationTrailKey(ref('model-b', 'open')),
27
+ );
28
+ });
29
+
30
+ test('a draft trail keys like a base trail', () => {
31
+ const draft: DecorationTrailRef = {
32
+ kind: 'draft',
33
+ modelId: 'model-a',
34
+ trail: draftTrail,
35
+ };
36
+ expect(decorationTrailKey(draft)).toBe('model-a:planned-open');
37
+ expect(decorationTrailId(draft)).toBe('planned-open');
38
+ });
39
+
40
+ test('groups trails by their own model, preserving authored order', () => {
41
+ const groups = groupDecorationTrailsByModel([
42
+ ref('web', 'open'),
43
+ ref('server', 'handle'),
44
+ ref('web', 'save'),
45
+ ref('worker', 'consume'),
46
+ ]);
47
+ expect(groups.map((g) => g.modelId)).toEqual(['web', 'server', 'worker']);
48
+ expect(groups[0]!.trails.map((t) => decorationTrailId(t))).toEqual([
49
+ 'open',
50
+ 'save',
51
+ ]);
52
+ expect(groups[1]!.trails.map((t) => decorationTrailId(t))).toEqual([
53
+ 'handle',
54
+ ]);
55
+ });
56
+
57
+ test('lists distinct models in first-seen order', () => {
58
+ expect(
59
+ decorationModelIds([
60
+ ref('web', 'open'),
61
+ ref('server', 'handle'),
62
+ ref('web', 'save'),
63
+ ]),
64
+ ).toEqual(['web', 'server']);
65
+ });
66
+
67
+ test('finds a trail by model and trail id, not trail id alone', () => {
68
+ const refs = [ref('web', 'open'), ref('server', 'open')];
69
+ expect(findDecorationTrail(refs, 'server', 'open')?.modelId).toBe('server');
70
+ expect(findDecorationTrail(refs, 'worker', 'open')).toBeUndefined();
71
+ });
72
+ });
@@ -0,0 +1,151 @@
1
+ /**
2
+ * Subsystem decoration — PROTOTYPE contract for the ephemeral, unverified layer.
3
+ *
4
+ * A decoration composes trails from one or more models, carries a markdown
5
+ * description, and may add draft structure. The base model stays verified-only;
6
+ * a decoration is never audited against source.
7
+ *
8
+ * **Each trail ref names its own model.** There is no decoration-level `base`
9
+ * field: a decoration can mix models by construction, and a single-model
10
+ * decoration is just the case where every ref happens to share a `modelId`.
11
+ *
12
+ * A trail can be either:
13
+ * - `base` — a reference to a trail that already exists in a model; or
14
+ * - `draft` — a trail that only exists in the decoration. This is what
15
+ * migration needs when a base trail depends on draft components: the whole
16
+ * trail moves into the decoration rather than dangling in the base.
17
+ *
18
+ * The contract is deliberately host-local and not part of the portable
19
+ * subsystem document. If decorations become shareable, `modelId` needs a
20
+ * portable identity key (the local `sg-...` ids are re-minted).
21
+ */
22
+
23
+ import type { SubsystemTrail, SubsystemTrailMechanism } from './model';
24
+
25
+ export interface DecorationStepAnchor {
26
+ /** Claim fingerprint of the anchored step (see `stepClaimKey`). */
27
+ stepKey: string;
28
+ /** Optional per-anchor note. The decoration description carries the prose. */
29
+ note?: string;
30
+ }
31
+
32
+ interface DecorationTrailRefBase {
33
+ /** Store-local model id. The trail id is only unique within this model. */
34
+ modelId: string;
35
+ /** Optional step-level anchors inside this trail. */
36
+ stepAnchors?: DecorationStepAnchor[];
37
+ }
38
+
39
+ /** A trail that already exists in the base model. */
40
+ export interface DecorationBaseTrailRef extends DecorationTrailRefBase {
41
+ kind: 'base';
42
+ trailId: string;
43
+ }
44
+
45
+ /** A speculative trail that exists only in the decoration. */
46
+ export interface DecorationDraftTrailRef extends DecorationTrailRefBase {
47
+ kind: 'draft';
48
+ trail: SubsystemTrail;
49
+ }
50
+
51
+ export type DecorationTrailRef =
52
+ | DecorationBaseTrailRef
53
+ | DecorationDraftTrailRef;
54
+
55
+ /**
56
+ * Draft structure: a component that does not exist in source yet. No
57
+ * `file`/`symbol` — those are the verification coordinates the base model
58
+ * requires, and a draft component deliberately has none.
59
+ */
60
+ export interface DecorationDraftComponent {
61
+ alias: string;
62
+ name: string;
63
+ /** Intended construct shape (`class`, `function`, …). */
64
+ construct?: string;
65
+ purpose?: string;
66
+ module?: string;
67
+ process?: string;
68
+ }
69
+
70
+ /** A speculative edge between base or draft aliases. */
71
+ export interface DecorationDraftEdge {
72
+ from: string;
73
+ to: string;
74
+ mechanism: SubsystemTrailMechanism;
75
+ }
76
+
77
+ export interface DecorationDraft {
78
+ components?: DecorationDraftComponent[];
79
+ edges?: DecorationDraftEdge[];
80
+ }
81
+
82
+ export interface DecorationProvenance {
83
+ source: 'author' | 'agent';
84
+ author?: string;
85
+ /** ISO timestamp. */
86
+ at: string;
87
+ }
88
+
89
+ export interface SubsystemDecoration {
90
+ version: 1;
91
+ id: string;
92
+ title: string;
93
+ /** Markdown, rendered like the model description. */
94
+ description: string;
95
+ /**
96
+ * Ordered trails the decoration cares about. Order is the reading order;
97
+ * `modelId` may vary from ref to ref. A trail is either a base ref or a
98
+ * draft trail that lives only here.
99
+ */
100
+ trails: DecorationTrailRef[];
101
+ /** Optional draft delta. Absent = a pure reading path. */
102
+ draft?: DecorationDraft;
103
+ provenance: DecorationProvenance;
104
+ }
105
+
106
+ /** The trail id for either kind of ref. */
107
+ export function decorationTrailId(ref: DecorationTrailRef): string {
108
+ return ref.kind === 'base' ? ref.trailId : ref.trail.id;
109
+ }
110
+
111
+ /** Stable key for a trail ref across models. `trailId` alone is not enough. */
112
+ export function decorationTrailKey(
113
+ ref: Pick<DecorationTrailRef, 'modelId'> & { trailId?: string; trail?: SubsystemTrail },
114
+ ): string {
115
+ const trailId = ref.trailId ?? ref.trail?.id ?? '';
116
+ return `${ref.modelId}:${trailId}`;
117
+ }
118
+
119
+ /**
120
+ * Group trail refs by their owning model, preserving first-seen model order
121
+ * and the authored trail order within each group. The UI uses this to render
122
+ * "which trails it cares about" without a separate model list.
123
+ */
124
+ export function groupDecorationTrailsByModel(
125
+ refs: readonly DecorationTrailRef[],
126
+ ): Array<{ modelId: string; trails: DecorationTrailRef[] }> {
127
+ const groups = new Map<string, DecorationTrailRef[]>();
128
+ for (const ref of refs) {
129
+ const existing = groups.get(ref.modelId);
130
+ if (existing) existing.push(ref);
131
+ else groups.set(ref.modelId, [ref]);
132
+ }
133
+ return [...groups].map(([modelId, trails]) => ({ modelId, trails }));
134
+ }
135
+
136
+ /** The distinct models a decoration touches, in first-seen order. */
137
+ export function decorationModelIds(
138
+ refs: readonly DecorationTrailRef[],
139
+ ): string[] {
140
+ return [...new Set(refs.map((ref) => ref.modelId))];
141
+ }
142
+
143
+ export function findDecorationTrail(
144
+ refs: readonly DecorationTrailRef[],
145
+ modelId: string,
146
+ trailId: string,
147
+ ): DecorationTrailRef | undefined {
148
+ return refs.find(
149
+ (ref) => ref.modelId === modelId && decorationTrailId(ref) === trailId,
150
+ );
151
+ }
@@ -0,0 +1,110 @@
1
+ import { describe, expect, test } from 'bun:test';
2
+ import type { SubsystemTrail } from './model';
3
+ import {
4
+ buildStepEmphasisMap,
5
+ emphasisForTrail,
6
+ emphasisKey,
7
+ findStepEmphasis,
8
+ orphanedEmphasis,
9
+ stepClaimKey,
10
+ type StepEmphasisRecord,
11
+ } from './stepEmphasis';
12
+
13
+ function step(overrides: Partial<SubsystemTrail['steps'][number]> = {}): SubsystemTrail['steps'][number] {
14
+ return {
15
+ from: 'ui',
16
+ to: 'api',
17
+ mechanism: 'calls',
18
+ file: 'src/ui/login.tsx',
19
+ span: { startLine: 42, endLine: 42, contentHash: 'abc' },
20
+ purl: 'pkg:github/acme/app#src/ui/login.tsx',
21
+ symbol: 'Login.submit',
22
+ ...overrides,
23
+ };
24
+ }
25
+
26
+ function trail(id: string, steps: SubsystemTrail['steps']): SubsystemTrail {
27
+ return { id, title: id, steps };
28
+ }
29
+
30
+ function record(trailId: string, stepKey: string): StepEmphasisRecord {
31
+ return {
32
+ trailId,
33
+ stepKey,
34
+ guidance: 'Why this step matters.',
35
+ provenance: { source: 'author', at: '2026-10-19T00:00:00.000Z' },
36
+ };
37
+ }
38
+
39
+ describe('stepClaimKey', () => {
40
+ test('is stable for the same claim', () => {
41
+ expect(stepClaimKey(step())).toBe(stepClaimKey(step()));
42
+ });
43
+
44
+ test('ignores annotation, which is not the claim', () => {
45
+ expect(stepClaimKey(step({ annotation: 'why' }))).toBe(
46
+ stepClaimKey(step()),
47
+ );
48
+ });
49
+
50
+ test('changes when the claim changes', () => {
51
+ const base = stepClaimKey(step());
52
+ expect(base).not.toBe(stepClaimKey(step({ from: 'other' })));
53
+ expect(base).not.toBe(stepClaimKey(step({ to: 'other' })));
54
+ expect(base).not.toBe(stepClaimKey(step({ mechanism: 'feeds' })));
55
+ expect(base).not.toBe(stepClaimKey(step({ file: 'src/other.ts' })));
56
+ expect(base).not.toBe(stepClaimKey(step({ symbol: 'Other.submit' })));
57
+ expect(base).not.toBe(
58
+ stepClaimKey(step({ span: { startLine: 43, endLine: 43, contentHash: 'abc' } })),
59
+ );
60
+ expect(base).not.toBe(
61
+ stepClaimKey(step({ span: { startLine: 42, endLine: 42, contentHash: 'changed' } })),
62
+ );
63
+ });
64
+ });
65
+
66
+ describe('step emphasis lookup', () => {
67
+ test('resolves after steps are reordered', () => {
68
+ const first = step({ symbol: 'first' });
69
+ const second = step({ symbol: 'second' });
70
+ const map = buildStepEmphasisMap([record('flow', stepClaimKey(first))]);
71
+ const reordered = trail('flow', [second, first]);
72
+ expect(findStepEmphasis(map, 'flow', reordered.steps[1]!)).toBeDefined();
73
+ expect(findStepEmphasis(map, 'flow', reordered.steps[0]!)).toBeUndefined();
74
+ });
75
+
76
+ test('emphasisForTrail reports marks in current step order', () => {
77
+ const a = step({ symbol: 'a' });
78
+ const b = step({ symbol: 'b' });
79
+ const c = step({ symbol: 'c' });
80
+ const map = buildStepEmphasisMap([
81
+ record('flow', stepClaimKey(c)),
82
+ record('flow', stepClaimKey(a)),
83
+ ]);
84
+ expect(emphasisForTrail(map, trail('flow', [b, a, c])).map((r) => r.stepIndex)).toEqual([
85
+ 1, 2,
86
+ ]);
87
+ });
88
+
89
+ test('an edited claim orphans its mark rather than re-binding it', () => {
90
+ const original = step();
91
+ const edited = step({ file: 'src/ui/login-v2.tsx' });
92
+ const records = [record('flow', stepClaimKey(original))];
93
+ const orphans = orphanedEmphasis(records, [trail('flow', [edited])]);
94
+ expect(orphans).toHaveLength(1);
95
+ expect(orphans[0]!.stepKey).toBe(stepClaimKey(original));
96
+ });
97
+
98
+ test('live marks are not reported as orphans', () => {
99
+ const live = step();
100
+ const records = [record('flow', stepClaimKey(live))];
101
+ expect(orphanedEmphasis(records, [trail('flow', [live])])).toEqual([]);
102
+ });
103
+
104
+ test('keys are scoped by trail id', () => {
105
+ const shared = step();
106
+ const map = buildStepEmphasisMap([record('flow-a', stepClaimKey(shared))]);
107
+ expect(emphasisKey('flow-b', shared)).not.toBe(emphasisKey('flow-a', shared));
108
+ expect(findStepEmphasis(map, 'flow-b', shared)).toBeUndefined();
109
+ });
110
+ });