@principal-ai/subsystems-react 0.55.3 → 0.56.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/dist/index.d.ts +12 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -1
- package/dist/index.js.map +1 -1
- package/dist/pierre/PierreFileView.js +3 -3
- package/dist/pierre/PierreFileView.js.map +1 -1
- package/dist/pierre/PierreTrailCodeView.d.ts +1 -6
- package/dist/pierre/PierreTrailCodeView.d.ts.map +1 -1
- package/dist/pierre/PierreTrailCodeView.js +21 -18
- package/dist/pierre/PierreTrailCodeView.js.map +1 -1
- package/dist/pierre/fileAvailability.d.ts +4 -7
- package/dist/pierre/fileAvailability.d.ts.map +1 -1
- package/dist/pierre/fileAvailability.js +5 -10
- package/dist/pierre/fileAvailability.js.map +1 -1
- package/dist/subsystem/C4Graph.d.ts.map +1 -1
- package/dist/subsystem/C4Graph.js +1 -1
- package/dist/subsystem/C4Graph.js.map +1 -1
- package/dist/subsystem/DecorationPanel.d.ts +37 -0
- package/dist/subsystem/DecorationPanel.d.ts.map +1 -0
- package/dist/subsystem/DecorationPanel.js +147 -0
- package/dist/subsystem/DecorationPanel.js.map +1 -0
- package/dist/subsystem/FileDrawer.d.ts +3 -1
- package/dist/subsystem/FileDrawer.d.ts.map +1 -1
- package/dist/subsystem/FileDrawer.js +2 -2
- package/dist/subsystem/FileDrawer.js.map +1 -1
- package/dist/subsystem/IssueList.d.ts.map +1 -1
- package/dist/subsystem/IssueList.js +26 -0
- package/dist/subsystem/IssueList.js.map +1 -1
- package/dist/subsystem/SubsystemComponentGraph.d.ts +9 -5
- package/dist/subsystem/SubsystemComponentGraph.d.ts.map +1 -1
- package/dist/subsystem/SubsystemComponentGraph.js +37 -47
- package/dist/subsystem/SubsystemComponentGraph.js.map +1 -1
- package/dist/subsystem/TrailStackBreadcrumb.d.ts +21 -0
- package/dist/subsystem/TrailStackBreadcrumb.d.ts.map +1 -0
- package/dist/subsystem/TrailStackBreadcrumb.js +77 -0
- package/dist/subsystem/TrailStackBreadcrumb.js.map +1 -0
- package/dist/subsystem/TrailStepsNav.d.ts +4 -3
- package/dist/subsystem/TrailStepsNav.d.ts.map +1 -1
- package/dist/subsystem/TrailStepsNav.js +13 -16
- package/dist/subsystem/TrailStepsNav.js.map +1 -1
- package/dist/subsystem/TrailsPanel.d.ts +9 -7
- package/dist/subsystem/TrailsPanel.d.ts.map +1 -1
- package/dist/subsystem/TrailsPanel.js +126 -76
- package/dist/subsystem/TrailsPanel.js.map +1 -1
- package/dist/subsystem/decoration.d.ts +112 -0
- package/dist/subsystem/decoration.d.ts.map +1 -0
- package/dist/subsystem/decoration.js +54 -0
- package/dist/subsystem/decoration.js.map +1 -0
- package/dist/subsystem/model.d.ts +5 -18
- package/dist/subsystem/model.d.ts.map +1 -1
- package/dist/subsystem/model.js +3 -11
- package/dist/subsystem/model.js.map +1 -1
- package/dist/subsystem/nodes.d.ts.map +1 -1
- package/dist/subsystem/nodes.js +17 -24
- package/dist/subsystem/nodes.js.map +1 -1
- package/dist/subsystem/stepEmphasis.d.ts +78 -0
- package/dist/subsystem/stepEmphasis.d.ts.map +1 -0
- package/dist/subsystem/stepEmphasis.js +89 -0
- package/dist/subsystem/stepEmphasis.js.map +1 -0
- package/dist/subsystem/trailBrief.d.ts.map +1 -1
- package/dist/subsystem/trailBrief.js +20 -0
- package/dist/subsystem/trailBrief.js.map +1 -1
- package/dist/subsystem/trailStack.d.ts +53 -0
- package/dist/subsystem/trailStack.d.ts.map +1 -0
- package/dist/subsystem/trailStack.js +154 -0
- package/dist/subsystem/trailStack.js.map +1 -0
- package/dist/subsystem/trailStepDisplay.d.ts +38 -0
- package/dist/subsystem/trailStepDisplay.d.ts.map +1 -0
- package/dist/subsystem/trailStepDisplay.js +41 -0
- package/dist/subsystem/trailStepDisplay.js.map +1 -0
- package/package.json +3 -3
- package/src/index.ts +47 -1
- package/src/pierre/PierreFileView.tsx +3 -3
- package/src/pierre/PierreTrailCodeView.tsx +25 -28
- package/src/pierre/fileAvailability.test.ts +0 -6
- package/src/pierre/fileAvailability.ts +5 -13
- package/src/stories/Canvases/Subsystem/Deprecated.stories.tsx +3 -17
- package/src/stories/Canvases/Subsystem/NodeAnatomy.stories.tsx +3 -31
- package/src/stories/Canvases/Subsystem/StepEmphasis.stories.tsx +528 -0
- package/src/stories/Canvases/Subsystem/Trails.stories.tsx +0 -187
- package/src/stories/Components/DecorationPanel.stories.tsx +160 -0
- package/src/subsystem/C4Graph.tsx +0 -1
- package/src/subsystem/DecorationPanel.test.tsx +128 -0
- package/src/subsystem/DecorationPanel.tsx +334 -0
- package/src/subsystem/FileDrawer.tsx +6 -0
- package/src/subsystem/IssueList.tsx +26 -0
- package/src/subsystem/SubsystemComponentGraph.tsx +68 -65
- package/src/subsystem/TrailStackBreadcrumb.test.tsx +109 -0
- package/src/subsystem/TrailStackBreadcrumb.tsx +172 -0
- package/src/subsystem/TrailStepsNav.test.tsx +35 -0
- package/src/subsystem/TrailStepsNav.tsx +18 -19
- package/src/subsystem/TrailsPanel.test.tsx +149 -0
- package/src/subsystem/TrailsPanel.tsx +165 -39
- package/src/subsystem/decoration.test.ts +72 -0
- package/src/subsystem/decoration.ts +151 -0
- package/src/subsystem/model.test.ts +7 -22
- package/src/subsystem/model.ts +6 -20
- package/src/subsystem/nodes.tsx +19 -28
- package/src/subsystem/stepEmphasis.test.ts +110 -0
- package/src/subsystem/stepEmphasis.ts +151 -0
- package/src/subsystem/trailBrief.test.ts +29 -0
- package/src/subsystem/trailBrief.ts +20 -0
- package/src/subsystem/trailStack.test.ts +133 -0
- package/src/subsystem/trailStack.ts +229 -0
- package/src/subsystem/trailStepDisplay.ts +68 -0
- package/src/stories/Canvases/Subsystem/Proposed.stories.tsx +0 -256
- package/src/stories/Canvases/Subsystem/ProposedTrails.stories.tsx +0 -184
|
@@ -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
|
+
}
|
|
@@ -98,7 +98,7 @@ describe('subsystem graph model', () => {
|
|
|
98
98
|
const withExternal: SubsystemComponent[] = [
|
|
99
99
|
...comps,
|
|
100
100
|
{
|
|
101
|
-
alias: "
|
|
101
|
+
alias: "external-watcher",
|
|
102
102
|
name: "watchDir",
|
|
103
103
|
construct: "external",
|
|
104
104
|
// Intentionally omit file/purl — agents often leave these off for externals.
|
|
@@ -111,10 +111,10 @@ describe('subsystem graph model', () => {
|
|
|
111
111
|
trails: [{
|
|
112
112
|
id: 'w1',
|
|
113
113
|
title: 'feed',
|
|
114
|
-
steps: [{ from: 'reader', to: '
|
|
114
|
+
steps: [{ from: 'reader', to: 'external-watcher', mechanism: 'feeds', file: 'x.ts' }],
|
|
115
115
|
}],
|
|
116
116
|
});
|
|
117
|
-
expect(nodes.find((n) => n.id === '
|
|
117
|
+
expect(nodes.find((n) => n.id === 'external-watcher')).toBeDefined();
|
|
118
118
|
});
|
|
119
119
|
|
|
120
120
|
test('buildSubsystemGraph creates external stub nodes for non-component targets', async () => {
|
|
@@ -223,33 +223,18 @@ describe('subsystem graph model', () => {
|
|
|
223
223
|
});
|
|
224
224
|
expect(withRole).toBeGreaterThan(stereotype);
|
|
225
225
|
|
|
226
|
-
const withProposed = nodeMinWidthForBadges({
|
|
227
|
-
construct: 'function',
|
|
228
|
-
proposed: true,
|
|
229
|
-
});
|
|
230
|
-
expect(withProposed).toBeGreaterThan(plain);
|
|
231
|
-
|
|
232
|
-
const withBoth = nodeMinWidthForBadges({
|
|
233
|
-
construct: 'function',
|
|
234
|
-
role: 'entry',
|
|
235
|
-
proposed: true,
|
|
236
|
-
});
|
|
237
|
-
// proposed-only label is shorter than entry · proposed; still wider than plain
|
|
238
|
-
expect(withBoth).toBe(withProposed);
|
|
239
|
-
|
|
240
226
|
// The pair floor must leave a real gap between the left and right badge,
|
|
241
227
|
// over and above both insets — regression: the node border was omitted from
|
|
242
228
|
// the floor, and the char width was underestimated, so the two badges met.
|
|
243
229
|
const left = estimateBadgeLabelWidth(constructBadgeLabel({ construct: 'function' }));
|
|
244
|
-
const right = estimateBadgeLabelWidth(rightBadgeLabel({
|
|
245
|
-
expect(
|
|
230
|
+
const right = estimateBadgeLabelWidth(rightBadgeLabel({ role: 'entry' })!);
|
|
231
|
+
expect(withRole).toBeGreaterThan(left + right + 2 * BADGE_EDGE_INSET);
|
|
246
232
|
});
|
|
247
233
|
|
|
248
|
-
test('rightBadgeLabel
|
|
234
|
+
test('rightBadgeLabel reports the role', () => {
|
|
249
235
|
expect(rightBadgeLabel({})).toBeNull();
|
|
250
|
-
expect(rightBadgeLabel({ proposed: true })).toBe('proposed');
|
|
251
236
|
expect(rightBadgeLabel({ role: 'entry' })).toBe('entry');
|
|
252
|
-
expect(rightBadgeLabel({ role: 'service'
|
|
237
|
+
expect(rightBadgeLabel({ role: 'service' })).toBe('service');
|
|
253
238
|
});
|
|
254
239
|
|
|
255
240
|
test('deriveNameFromSymbol falls back to existing name when no symbol', () => {
|
package/src/subsystem/model.ts
CHANGED
|
@@ -101,6 +101,8 @@ export type {
|
|
|
101
101
|
SubsystemModelDocument,
|
|
102
102
|
SubsystemTrail,
|
|
103
103
|
SubsystemTrailStep,
|
|
104
|
+
SubsystemSiteConstruct,
|
|
105
|
+
SubsystemTargetConstruct,
|
|
104
106
|
SubsystemComponentRole,
|
|
105
107
|
SubsystemFramework,
|
|
106
108
|
SubsystemStereotype,
|
|
@@ -1156,7 +1158,7 @@ export type SubsystemIssueRung =
|
|
|
1156
1158
|
| 'signature';
|
|
1157
1159
|
|
|
1158
1160
|
/**
|
|
1159
|
-
* Per-node diagnostics badge — the
|
|
1161
|
+
* Per-node diagnostics badge — the issue overlay (severity ring +
|
|
1160
1162
|
* earliest-rung corner chip). The graph derives one per component from the
|
|
1161
1163
|
* audit issues whose component target resolves to that node. Absent when the
|
|
1162
1164
|
* node has no rung-mapped findings.
|
|
@@ -1226,12 +1228,6 @@ export interface SubsystemGraphEdgeData extends Record<string, unknown> {
|
|
|
1226
1228
|
/** True while another edge is selected — render this edge (and its label)
|
|
1227
1229
|
* dimmed to focus the selected relationship. */
|
|
1228
1230
|
dimmed?: boolean;
|
|
1229
|
-
/**
|
|
1230
|
-
* A backing trail step declares itself `proposed` — a planned seam between
|
|
1231
|
-
* otherwise-real components. The SVG edge and its label draw dashed;
|
|
1232
|
-
* distinct from a proposed ENDPOINT, which dashes the node instead.
|
|
1233
|
-
*/
|
|
1234
|
-
proposedStep?: boolean;
|
|
1235
1231
|
/** ELK-computed label midpoint (from the actual edge path, not node centers). */
|
|
1236
1232
|
labelX?: number;
|
|
1237
1233
|
labelY?: number;
|
|
@@ -1522,9 +1518,6 @@ export const ROLE_LABEL: Record<SubsystemComponentRole, string> = {
|
|
|
1522
1518
|
service: 'service',
|
|
1523
1519
|
};
|
|
1524
1520
|
|
|
1525
|
-
/** Right-badge accent for `proposed: true` (no role, or combined with role). */
|
|
1526
|
-
export const PROPOSED_COLOR = '#b8860b'; // darkgoldenrod — not-yet-in-source
|
|
1527
|
-
|
|
1528
1521
|
/**
|
|
1529
1522
|
* Border accent for `deprecated: true` — the claim was real, its source is gone.
|
|
1530
1523
|
* A desaturated slate rather than true silver: role badges already own silver
|
|
@@ -1545,25 +1538,19 @@ export function deprecatedBadgeColor(component: {
|
|
|
1545
1538
|
}
|
|
1546
1539
|
|
|
1547
1540
|
/**
|
|
1548
|
-
* Top-right badge label:
|
|
1549
|
-
* (proposed is the temporary exception to scan for; role stays on the model).
|
|
1550
|
-
* Returns null when neither applies.
|
|
1541
|
+
* Top-right badge label: the role, when set. Returns null otherwise.
|
|
1551
1542
|
*/
|
|
1552
1543
|
export function rightBadgeLabel(component: {
|
|
1553
1544
|
role?: SubsystemComponentRole;
|
|
1554
|
-
proposed?: boolean;
|
|
1555
1545
|
}): string | null {
|
|
1556
|
-
if (component.proposed) return 'proposed';
|
|
1557
1546
|
if (component.role != null) return ROLE_LABEL[component.role];
|
|
1558
1547
|
return null;
|
|
1559
1548
|
}
|
|
1560
1549
|
|
|
1561
|
-
/** Color for the top-right
|
|
1550
|
+
/** Color for the top-right role badge. */
|
|
1562
1551
|
export function rightBadgeColor(component: {
|
|
1563
1552
|
role?: SubsystemComponentRole;
|
|
1564
|
-
proposed?: boolean;
|
|
1565
1553
|
}): string | null {
|
|
1566
|
-
if (component.proposed) return PROPOSED_COLOR;
|
|
1567
1554
|
if (component.role != null) return ROLE_COLOR[component.role];
|
|
1568
1555
|
return null;
|
|
1569
1556
|
}
|
|
@@ -1593,7 +1580,7 @@ function storeStorage(component: {
|
|
|
1593
1580
|
|
|
1594
1581
|
/** Top-right storage badge for `construct: 'store'` nodes with an authored
|
|
1595
1582
|
* `storage` declaration — how the retained state is backed. Sits beside the
|
|
1596
|
-
* role
|
|
1583
|
+
* role badge (storage is orthogonal to topology). Null otherwise. */
|
|
1597
1584
|
export function storageBadgeLabel(component: {
|
|
1598
1585
|
construct: SubsystemComponentConstruct;
|
|
1599
1586
|
declaration?: AnyComponentDetail;
|
|
@@ -1688,7 +1675,6 @@ export function nodeMinWidthForBadges(component: {
|
|
|
1688
1675
|
framework?: string;
|
|
1689
1676
|
stereotype?: string;
|
|
1690
1677
|
role?: SubsystemComponentRole;
|
|
1691
|
-
proposed?: boolean;
|
|
1692
1678
|
deprecated?: boolean;
|
|
1693
1679
|
entityKind?: string;
|
|
1694
1680
|
declaration?: AnyComponentDetail;
|
package/src/subsystem/nodes.tsx
CHANGED
|
@@ -20,7 +20,6 @@ import {
|
|
|
20
20
|
edgeColor,
|
|
21
21
|
edgeStrokeStyle,
|
|
22
22
|
DEPRECATED_COLOR,
|
|
23
|
-
PROPOSED_COLOR,
|
|
24
23
|
deprecatedBadgeColor,
|
|
25
24
|
deprecatedBadgeLabel,
|
|
26
25
|
constructBadgeColor,
|
|
@@ -221,7 +220,7 @@ export function SubsystemComponentNode(props: NodeProps<Node<SubsystemGraphNodeD
|
|
|
221
220
|
// `symbol` is the source of truth; `name` is derived from it consistently.
|
|
222
221
|
const displayName = deriveNameFromSymbol(c.symbol, c.construct, c.name, c.file, c.stereotype);
|
|
223
222
|
// Top badges are absolutely positioned — widen the node so they nowrap
|
|
224
|
-
// instead of wrapping, including when construct + role
|
|
223
|
+
// instead of wrapping, including when construct + role share the top.
|
|
225
224
|
const badgeMinWidth = nodeMinWidthForBadges(c);
|
|
226
225
|
const topRightLabel = rightBadgeLabel(c);
|
|
227
226
|
const topRightColor = rightBadgeColor(c);
|
|
@@ -243,17 +242,14 @@ export function SubsystemComponentNode(props: NodeProps<Node<SubsystemGraphNodeD
|
|
|
243
242
|
const nodeBg = theme.colors.backgroundSecondary ?? theme.colors.background;
|
|
244
243
|
const hoverBg =
|
|
245
244
|
theme.colors.backgroundHover ?? theme.colors.backgroundTertiary ?? nodeBg;
|
|
246
|
-
// Border:
|
|
247
|
-
//
|
|
248
|
-
//
|
|
249
|
-
// with primary.
|
|
245
|
+
// Border: deprecated uses the slate accent (dotted, same family as issue)
|
|
246
|
+
// since it means "not a live claim"; construct color stays on the left
|
|
247
|
+
// badge. File-open spotlight still wins with primary.
|
|
250
248
|
const borderColor = fileMatch
|
|
251
249
|
? theme.colors.primary
|
|
252
|
-
: c.
|
|
253
|
-
?
|
|
254
|
-
:
|
|
255
|
-
? DEPRECATED_COLOR
|
|
256
|
-
: color;
|
|
250
|
+
: c.deprecated
|
|
251
|
+
? DEPRECATED_COLOR
|
|
252
|
+
: color;
|
|
257
253
|
// Selection is stamped into data by the graph component — React Flow's own
|
|
258
254
|
// `selected` never updates because the node stops click propagation.
|
|
259
255
|
const isSelected = selected || (data.isSelected as boolean | undefined) === true;
|
|
@@ -269,21 +265,19 @@ export function SubsystemComponentNode(props: NodeProps<Node<SubsystemGraphNodeD
|
|
|
269
265
|
const borderW = isSelected || fileMatch ? 4 : 2;
|
|
270
266
|
const badgeTop = -9 - borderW;
|
|
271
267
|
const badgeEdge = -borderW;
|
|
272
|
-
// A node with diagnostics turns its border dotted —
|
|
273
|
-
//
|
|
274
|
-
//
|
|
275
|
-
//
|
|
268
|
+
// A node with diagnostics turns its border dotted — an "something's off"
|
|
269
|
+
// signal. The border keeps its construct color; the severity rides on the
|
|
270
|
+
// chip. Deprecated is also dotted, but slate rather than construct-colored:
|
|
271
|
+
// "this claim was retired" is closer to a diagnostic than to a placeholder.
|
|
276
272
|
const hasIssue = data.issue != null;
|
|
277
273
|
// Deprecated is dotted like issue but slate rather than construct-colored:
|
|
278
274
|
// "this claim was retired" is closer to a diagnostic than to a placeholder,
|
|
279
275
|
// and verification skips deprecated nodes so the two never stack in practice.
|
|
280
276
|
const borderStyle = hasIssue
|
|
281
277
|
? 'dotted'
|
|
282
|
-
: c.
|
|
283
|
-
? '
|
|
284
|
-
:
|
|
285
|
-
? 'dotted'
|
|
286
|
-
: 'solid';
|
|
278
|
+
: c.deprecated
|
|
279
|
+
? 'dotted'
|
|
280
|
+
: 'solid';
|
|
287
281
|
|
|
288
282
|
return (
|
|
289
283
|
<div
|
|
@@ -352,9 +346,9 @@ export function SubsystemComponentNode(props: NodeProps<Node<SubsystemGraphNodeD
|
|
|
352
346
|
{constructBadgeLabel(c)}
|
|
353
347
|
</div>
|
|
354
348
|
|
|
355
|
-
{/* Secondary badge cluster —
|
|
356
|
-
|
|
357
|
-
|
|
349
|
+
{/* Secondary badge cluster — the role badge, with store storage
|
|
350
|
+
(retention backing) beside it since it is orthogonal to topology.
|
|
351
|
+
Persistent; pointer-events pass through.
|
|
358
352
|
Sits astride the bottom edge, centered (mirroring the top construct
|
|
359
353
|
badge's straddle), so the top edge stays free for the construct tag. */}
|
|
360
354
|
{(topRightLabel != null || storageLabel != null || deprecatedLabel != null) && (
|
|
@@ -869,11 +863,8 @@ export function SubsystemEdge({
|
|
|
869
863
|
const edgeRef = { mechanism, provenance: data?.provenance };
|
|
870
864
|
const color = edgeColor(edgeRef);
|
|
871
865
|
// Dash style encodes provenance (graphify) and relationship kind (dashed =
|
|
872
|
-
// inverted-control or observational: hierarchy, registration, watches).
|
|
873
|
-
|
|
874
|
-
// the dashed edge label and a proposed node's dashed border.
|
|
875
|
-
const isDashed =
|
|
876
|
-
edgeStrokeStyle(edgeRef) === 'dashed' || data?.proposedStep === true;
|
|
866
|
+
// inverted-control or observational: hierarchy, registration, watches).
|
|
867
|
+
const isDashed = edgeStrokeStyle(edgeRef) === 'dashed';
|
|
877
868
|
const dimmed = data?.dimmed === true;
|
|
878
869
|
// Dim the stroke by color, never via path `opacity`. SVG markers are shared
|
|
879
870
|
// by id; opacity on the referencing path paints every arrowhead that uses
|
|
@@ -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
|
+
});
|