@principal-ai/subsystems-react 0.19.1 → 0.20.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/graphify/consolidated.d.ts +16 -1
- package/dist/graphify/consolidated.d.ts.map +1 -1
- package/dist/graphify/index.d.ts +1 -1
- package/dist/graphify/index.d.ts.map +1 -1
- package/dist/graphify/index.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/pierre/constructColors.d.ts +10 -0
- package/dist/pierre/constructColors.d.ts.map +1 -1
- package/dist/pierre/constructColors.js +15 -0
- package/dist/pierre/constructColors.js.map +1 -1
- package/dist/subsystem/ComponentDeclaration.js +2 -2
- package/dist/subsystem/ComponentDeclaration.js.map +1 -1
- package/dist/subsystem/SubsystemComponentGraph.d.ts.map +1 -1
- package/dist/subsystem/SubsystemComponentGraph.js +8 -7
- package/dist/subsystem/SubsystemComponentGraph.js.map +1 -1
- package/dist/subsystem/formatDeclaration.d.ts.map +1 -1
- package/dist/subsystem/formatDeclaration.js +13 -0
- package/dist/subsystem/formatDeclaration.js.map +1 -1
- package/dist/subsystem/model.d.ts +21 -2
- package/dist/subsystem/model.d.ts.map +1 -1
- package/dist/subsystem/model.js +5 -1
- package/dist/subsystem/model.js.map +1 -1
- package/dist/subsystem/nodes.d.ts.map +1 -1
- package/dist/subsystem/nodes.js +4 -3
- package/dist/subsystem/nodes.js.map +1 -1
- package/dist/subsystem/tokenizeComponent.d.ts.map +1 -1
- package/dist/subsystem/tokenizeComponent.js +4 -0
- package/dist/subsystem/tokenizeComponent.js.map +1 -1
- package/package.json +2 -2
- package/src/graphify/consolidated.ts +18 -0
- package/src/graphify/index.ts +2 -0
- package/src/index.ts +3 -1
- package/src/pierre/constructColors.test.ts +32 -2
- package/src/pierre/constructColors.ts +18 -0
- package/src/stories/Subsystem/ComponentGraph/CustomEntities.stories.tsx +184 -0
- package/src/subsystem/ComponentDeclaration.tsx +2 -2
- package/src/subsystem/SubsystemComponentGraph.tsx +8 -7
- package/src/subsystem/formatDeclaration.test.ts +40 -0
- package/src/subsystem/formatDeclaration.ts +13 -0
- package/src/subsystem/model.test.ts +9 -0
- package/src/subsystem/model.ts +25 -2
- package/src/subsystem/nodes.tsx +4 -3
- package/src/subsystem/tokenizeComponent.ts +5 -0
|
@@ -43,6 +43,10 @@ export async function tokenizeComponent(component, printWidth = 80, themeName =
|
|
|
43
43
|
const escaped = label.replace(/\\/g, '\\\\').replace(/'/g, "\\'");
|
|
44
44
|
return tokenizeFormatted(`external '${escaped}'`, themeName);
|
|
45
45
|
}
|
|
46
|
+
// Custom entity — an actor (Person/agent/queue), not code; bypass Prettier.
|
|
47
|
+
if (kind === 'custom_entity') {
|
|
48
|
+
return tokenizeFormatted(generateDeclarationString(component), themeName);
|
|
49
|
+
}
|
|
46
50
|
// Generate → format → tokenize
|
|
47
51
|
const raw = generateDeclarationString(component);
|
|
48
52
|
const { prettier, plugins } = await getPrettier();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tokenizeComponent.js","sourceRoot":"","sources":["../../src/subsystem/tokenizeComponent.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAIH,OAAO,EAAE,yBAAyB,EAAE,MAAM,qBAAqB,CAAC;AAChE,OAAO,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AAExD,8CAA8C;AAC9C,IAAI,eAAe,GAAyD,IAAI,CAAC;AACjF,IAAI,sBAAsB,GAGd,IAAI,CAAC;AAEjB,KAAK,UAAU,WAAW;IACxB,IAAI,CAAC,eAAe,EAAE;QACpB,eAAe,GAAG,MAAM,CAAC,qBAAqB,CAAC,CAAC;KACjD;IACD,IAAI,CAAC,sBAAsB,EAAE;QAC3B,sBAAsB,GAAG,OAAO,CAAC,GAAG,CAAC;YACnC,MAAM,CAAC,6BAA6B,CAAC;YACrC,MAAM,CAAC,yBAAyB,CAAC;SAClC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,UAAU,EAAE,MAAM,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,UAAU,EAAE,MAAM,EAAE,CAAC,CAAC,CAAC;KAC7D;IACD,MAAM,CAAC,QAAQ,EAAE,OAAO,CAAC,GAAG,MAAM,OAAO,CAAC,GAAG,CAAC,CAAC,eAAe,EAAE,sBAAsB,CAAC,CAAC,CAAC;IACzF,OAAO,EAAE,QAAQ,EAAE,OAAO,EAAE,CAAC;AAC/B,CAAC;AAED;;;;;;GAMG;AACH,MAAM,CAAC,KAAK,UAAU,iBAAiB,CACrC,SAA6B,EAC7B,UAAU,GAAG,EAAE,EACf,YAAmC,aAAa;IAEhD,sDAAsD;IACtD,IAAI,SAAS,CAAC,MAAM;QAAE,OAAO,SAAS,CAAC,MAAM,CAAC;IAE9C,yDAAyD;IACzD,MAAM,IAAI,GAAG,SAAS,CAAC,MAAM,EAAE,IAAI,IAAI,SAAS,CAAC,SAAS,CAAC;IAC3D,IAAI,IAAI,KAAK,UAAU,EAAE;QACvB,MAAM,KAAK,GAAG,SAAS,CAAC,MAAM,EAAE,IAAI,KAAK,UAAU,CAAC,CAAC,CAAC,SAAS,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,SAAS,CAAC,IAAI,CAAC;QAC9F,MAAM,OAAO,GAAG,KAAK,CAAC,OAAO,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC,OAAO,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;QAClE,OAAO,iBAAiB,CAAC,aAAa,OAAO,GAAG,EAAE,SAAS,CAAC,CAAC;KAC9D;IAED,+BAA+B;IAC/B,MAAM,GAAG,GAAG,yBAAyB,CAAC,SAAS,CAAC,CAAC;IACjD,MAAM,EAAE,QAAQ,EAAE,OAAO,EAAE,GAAG,MAAM,WAAW,EAAE,CAAC;IAClD,MAAM,SAAS,GAAG,MAAM,QAAQ,CAAC,MAAM,CAAC,GAAG,EAAE;QAC3C,MAAM,EAAE,YAAY;QACpB,OAAO,EAAE,CAAC,OAAO,CAAC,UAAU,EAAE,OAAO,CAAC,MAAM,CAAC;QAC7C,UAAU;KACX,CAAC,CAAC;IAEH,OAAO,iBAAiB,CAAC,SAAS,EAAE,SAAS,CAAC,CAAC;AACjD,CAAC"}
|
|
1
|
+
{"version":3,"file":"tokenizeComponent.js","sourceRoot":"","sources":["../../src/subsystem/tokenizeComponent.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAIH,OAAO,EAAE,yBAAyB,EAAE,MAAM,qBAAqB,CAAC;AAChE,OAAO,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AAExD,8CAA8C;AAC9C,IAAI,eAAe,GAAyD,IAAI,CAAC;AACjF,IAAI,sBAAsB,GAGd,IAAI,CAAC;AAEjB,KAAK,UAAU,WAAW;IACxB,IAAI,CAAC,eAAe,EAAE;QACpB,eAAe,GAAG,MAAM,CAAC,qBAAqB,CAAC,CAAC;KACjD;IACD,IAAI,CAAC,sBAAsB,EAAE;QAC3B,sBAAsB,GAAG,OAAO,CAAC,GAAG,CAAC;YACnC,MAAM,CAAC,6BAA6B,CAAC;YACrC,MAAM,CAAC,yBAAyB,CAAC;SAClC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,UAAU,EAAE,MAAM,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,UAAU,EAAE,MAAM,EAAE,CAAC,CAAC,CAAC;KAC7D;IACD,MAAM,CAAC,QAAQ,EAAE,OAAO,CAAC,GAAG,MAAM,OAAO,CAAC,GAAG,CAAC,CAAC,eAAe,EAAE,sBAAsB,CAAC,CAAC,CAAC;IACzF,OAAO,EAAE,QAAQ,EAAE,OAAO,EAAE,CAAC;AAC/B,CAAC;AAED;;;;;;GAMG;AACH,MAAM,CAAC,KAAK,UAAU,iBAAiB,CACrC,SAA6B,EAC7B,UAAU,GAAG,EAAE,EACf,YAAmC,aAAa;IAEhD,sDAAsD;IACtD,IAAI,SAAS,CAAC,MAAM;QAAE,OAAO,SAAS,CAAC,MAAM,CAAC;IAE9C,yDAAyD;IACzD,MAAM,IAAI,GAAG,SAAS,CAAC,MAAM,EAAE,IAAI,IAAI,SAAS,CAAC,SAAS,CAAC;IAC3D,IAAI,IAAI,KAAK,UAAU,EAAE;QACvB,MAAM,KAAK,GAAG,SAAS,CAAC,MAAM,EAAE,IAAI,KAAK,UAAU,CAAC,CAAC,CAAC,SAAS,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,SAAS,CAAC,IAAI,CAAC;QAC9F,MAAM,OAAO,GAAG,KAAK,CAAC,OAAO,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC,OAAO,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;QAClE,OAAO,iBAAiB,CAAC,aAAa,OAAO,GAAG,EAAE,SAAS,CAAC,CAAC;KAC9D;IAED,4EAA4E;IAC5E,IAAI,IAAI,KAAK,eAAe,EAAE;QAC5B,OAAO,iBAAiB,CAAC,yBAAyB,CAAC,SAAS,CAAC,EAAE,SAAS,CAAC,CAAC;KAC3E;IAED,+BAA+B;IAC/B,MAAM,GAAG,GAAG,yBAAyB,CAAC,SAAS,CAAC,CAAC;IACjD,MAAM,EAAE,QAAQ,EAAE,OAAO,EAAE,GAAG,MAAM,WAAW,EAAE,CAAC;IAClD,MAAM,SAAS,GAAG,MAAM,QAAQ,CAAC,MAAM,CAAC,GAAG,EAAE;QAC3C,MAAM,EAAE,YAAY;QACpB,OAAO,EAAE,CAAC,OAAO,CAAC,UAAU,EAAE,OAAO,CAAC,MAAM,CAAC;QAC7C,UAAU;KACX,CAAC,CAAC;IAEH,OAAO,iBAAiB,CAAC,SAAS,EAAE,SAAS,CAAC,CAAC;AACjD,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@principal-ai/subsystems-react",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.20.0",
|
|
4
4
|
"description": "React UI for Subsystem Models — graph, Pierre code views, and session-event feeds",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
"@pierre/diffs": "1.3.6",
|
|
46
46
|
"@principal-ade/industry-theme": "0.1.7",
|
|
47
47
|
"@principal-ai/agent-monitoring": "^0.3.38",
|
|
48
|
-
"@principal-ai/subsystems-core": "0.
|
|
48
|
+
"@principal-ai/subsystems-core": "0.31.0",
|
|
49
49
|
"@storybook/addon-docs": "10.1.2",
|
|
50
50
|
"@storybook/addon-links": "10.1.2",
|
|
51
51
|
"@storybook/addon-onboarding": "10.1.2",
|
|
@@ -190,6 +190,23 @@ export interface GraphifyExternalDetail {
|
|
|
190
190
|
label: string;
|
|
191
191
|
}
|
|
192
192
|
|
|
193
|
+
/** A single authored attribute on a custom entity — free-form key/value. */
|
|
194
|
+
export interface GraphifyCustomEntityAttribute {
|
|
195
|
+
key: string;
|
|
196
|
+
value: string;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* A custom-entity (actor — Person/agent/queue) declaration. Authored, never
|
|
201
|
+
* extracted: there is no graphify node for a Person. `attributes` are
|
|
202
|
+
* free-form key/value pairs surfaced in the declaration panel; the base
|
|
203
|
+
* component fields carry the node identity (`entityKind`, `color`, `purpose`).
|
|
204
|
+
*/
|
|
205
|
+
export interface GraphifyCustomEntityDetail {
|
|
206
|
+
kind: 'custom_entity';
|
|
207
|
+
attributes: GraphifyCustomEntityAttribute[];
|
|
208
|
+
}
|
|
209
|
+
|
|
193
210
|
/**
|
|
194
211
|
* A retained-state node (`kind: 'store'`): state and its contract, no
|
|
195
212
|
* behavior. `kind` is the node's verifiable anchor — a store anchors to a
|
|
@@ -232,6 +249,7 @@ export type GraphifyComponentDetail =
|
|
|
232
249
|
| GraphifyTypeDetail
|
|
233
250
|
| GraphifyModuleDetail
|
|
234
251
|
| GraphifyExternalDetail
|
|
252
|
+
| GraphifyCustomEntityDetail
|
|
235
253
|
| GraphifyStoreDetail;
|
|
236
254
|
|
|
237
255
|
/** A raw graphify edge backing a facet claim (verification + provenance). */
|
package/src/graphify/index.ts
CHANGED
package/src/index.ts
CHANGED
|
@@ -43,6 +43,8 @@ export type {
|
|
|
43
43
|
GraphifyTypeDetail,
|
|
44
44
|
GraphifyModuleDetail,
|
|
45
45
|
GraphifyExternalDetail,
|
|
46
|
+
GraphifyCustomEntityDetail,
|
|
47
|
+
GraphifyCustomEntityAttribute,
|
|
46
48
|
GraphifyComponentDetail,
|
|
47
49
|
GraphifyEdgeRef,
|
|
48
50
|
} from './graphify';
|
|
@@ -92,7 +94,7 @@ export type {
|
|
|
92
94
|
} from './subsystem/ComponentDeclaration';
|
|
93
95
|
export { ComponentDeclaration } from './subsystem/ComponentDeclaration';
|
|
94
96
|
export { MECHANISM_COLOR, MECHANISM_STYLE } from './subsystem/model';
|
|
95
|
-
export { CONSTRUCT_COLOR, constructColorsFromPierreTheme } from './pierre/constructColors';
|
|
97
|
+
export { CONSTRUCT_COLOR, componentColor, constructColorsFromPierreTheme } from './pierre/constructColors';
|
|
96
98
|
export {
|
|
97
99
|
purlRepoKey,
|
|
98
100
|
purlOwnerName,
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import { describe, expect, test } from 'bun:test';
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
CONSTRUCT_COLOR,
|
|
4
|
+
componentColor,
|
|
5
|
+
constructColorsFromPierreTheme,
|
|
6
|
+
} from './constructColors';
|
|
3
7
|
import type { SubsystemComponentConstruct } from '../subsystem/model';
|
|
4
8
|
|
|
5
9
|
const CONSTRUCTS: SubsystemComponentConstruct[] = [
|
|
@@ -12,6 +16,7 @@ const CONSTRUCTS: SubsystemComponentConstruct[] = [
|
|
|
12
16
|
'module',
|
|
13
17
|
'store',
|
|
14
18
|
'external',
|
|
19
|
+
'custom_entity',
|
|
15
20
|
];
|
|
16
21
|
|
|
17
22
|
const HEX = /^#[0-9a-f]{6}$/;
|
|
@@ -26,7 +31,7 @@ describe('constructColorsFromPierreTheme', () => {
|
|
|
26
31
|
}
|
|
27
32
|
});
|
|
28
33
|
|
|
29
|
-
test('all
|
|
34
|
+
test('all ten constructs are distinguishable within a theme', () => {
|
|
30
35
|
for (const themeName of ['pierre-dark', 'pierre-light'] as const) {
|
|
31
36
|
const colors = constructColorsFromPierreTheme(themeName);
|
|
32
37
|
const values = CONSTRUCTS.map((c) => colors[c]);
|
|
@@ -49,4 +54,29 @@ describe('constructColorsFromPierreTheme', () => {
|
|
|
49
54
|
test('the static dark instantiation matches the derived table', () => {
|
|
50
55
|
expect(CONSTRUCT_COLOR).toEqual(constructColorsFromPierreTheme('pierre-dark'));
|
|
51
56
|
});
|
|
57
|
+
|
|
58
|
+
test('componentColor inherits the construct color when no override', () => {
|
|
59
|
+
expect(componentColor({ construct: 'class' }, 'pierre-dark')).toBe(
|
|
60
|
+
constructColorsFromPierreTheme('pierre-dark').class,
|
|
61
|
+
);
|
|
62
|
+
expect(componentColor({ construct: 'custom_entity' }, 'pierre-light')).toBe(
|
|
63
|
+
constructColorsFromPierreTheme('pierre-light').custom_entity,
|
|
64
|
+
);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test('componentColor prefers an authored color override', () => {
|
|
68
|
+
expect(
|
|
69
|
+
componentColor(
|
|
70
|
+
{ construct: 'custom_entity', color: '#ff00aa' },
|
|
71
|
+
'pierre-dark',
|
|
72
|
+
),
|
|
73
|
+
).toBe('#ff00aa');
|
|
74
|
+
// same override in both themes — the explicit color wins outright
|
|
75
|
+
expect(
|
|
76
|
+
componentColor(
|
|
77
|
+
{ construct: 'custom_entity', color: '#ff00aa' },
|
|
78
|
+
'pierre-light',
|
|
79
|
+
),
|
|
80
|
+
).toBe('#ff00aa');
|
|
81
|
+
});
|
|
52
82
|
});
|
|
@@ -19,6 +19,20 @@ import type { PierreSyntaxThemeName } from './pierreSyntaxTheme';
|
|
|
19
19
|
import pierreDark from '@pierre/theme/pierre-dark';
|
|
20
20
|
import pierreLight from '@pierre/theme/pierre-light';
|
|
21
21
|
|
|
22
|
+
/**
|
|
23
|
+
* Resolve a component's accent color: an authored `color` override wins;
|
|
24
|
+
* otherwise the construct color derived from the active Pierre syntax theme.
|
|
25
|
+
* Single source of truth so the node border/badges and the declaration panel
|
|
26
|
+
* agree on what a custom entity (or any component) renders as.
|
|
27
|
+
*/
|
|
28
|
+
export function componentColor(
|
|
29
|
+
component: { color?: string; construct: SubsystemComponentConstruct },
|
|
30
|
+
themeName: PierreSyntaxThemeName,
|
|
31
|
+
): string {
|
|
32
|
+
if (component.color) return component.color;
|
|
33
|
+
return constructColorsFromPierreTheme(themeName)[component.construct];
|
|
34
|
+
}
|
|
35
|
+
|
|
22
36
|
interface PierreThemeInput {
|
|
23
37
|
type: 'dark' | 'light';
|
|
24
38
|
colors: Record<string, string>;
|
|
@@ -93,6 +107,10 @@ export function constructColorsFromPierreTheme(
|
|
|
93
107
|
module: towardBackground(moduleColor, 0.18),
|
|
94
108
|
store: pick('variable.other.constant'),
|
|
95
109
|
external: pick('comment'),
|
|
110
|
+
// custom entities are actors, not code — the heading/emphasis coral reads
|
|
111
|
+
// as a highlight against the code-construct hues. Distinct from entry role
|
|
112
|
+
// orange (#ff6b35) which stays a badge, not a border.
|
|
113
|
+
custom_entity: pick('markup.heading'),
|
|
96
114
|
};
|
|
97
115
|
}
|
|
98
116
|
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import '@xyflow/react/dist/style.css';
|
|
3
|
+
import type { Meta, StoryObj } from '@storybook/react';
|
|
4
|
+
import { ThemeProvider, defaultEditorTheme } from '@principal-ade/industry-theme';
|
|
5
|
+
import { SubsystemComponentGraph } from '../../../subsystem/SubsystemComponentGraph';
|
|
6
|
+
import type { SubsystemComponent, SubsystemComponentEdge } from '../../../subsystem/model';
|
|
7
|
+
import type { GraphifyCustomEntityDetail } from '../../../graphify';
|
|
8
|
+
|
|
9
|
+
/** Authored attributes surfaced in the declaration panel. */
|
|
10
|
+
function entityDetail(attributes: Array<[string, string]>): GraphifyCustomEntityDetail {
|
|
11
|
+
return {
|
|
12
|
+
kind: 'custom_entity',
|
|
13
|
+
attributes: attributes.map(([key, value]) => ({ key, value })),
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
// Custom-entity prototype — the NovaTech work-request chain as it would read
|
|
19
|
+
// once the `custom_entity` construct ships. Persons, an agent, and a queue are
|
|
20
|
+
// authored as `construct: 'custom_entity'` with an open-string `entityKind`
|
|
21
|
+
// (Person / agent / queue); every entity still groups by `process` and flows
|
|
22
|
+
// through the same edge vocabulary as code nodes.
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
|
|
25
|
+
const components: SubsystemComponent[] = [
|
|
26
|
+
{
|
|
27
|
+
id: 'wr',
|
|
28
|
+
name: 'WorkRequester',
|
|
29
|
+
construct: 'custom_entity',
|
|
30
|
+
entityKind: 'Person',
|
|
31
|
+
file: '',
|
|
32
|
+
purl: 'external',
|
|
33
|
+
purpose: 'submits a facilities work request',
|
|
34
|
+
role: 'entry',
|
|
35
|
+
detail: entityDetail([
|
|
36
|
+
['vertical', 'buildings'],
|
|
37
|
+
['escalationPath', 'FacilitiesTechnician → ShiftSupervisor'],
|
|
38
|
+
]),
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
id: 'tech',
|
|
42
|
+
name: 'FacilitiesTechnician',
|
|
43
|
+
construct: 'custom_entity',
|
|
44
|
+
entityKind: 'Person',
|
|
45
|
+
file: '',
|
|
46
|
+
purl: 'pkg:github/novatech/facilities-ops',
|
|
47
|
+
purpose: 'performs repairs, resolves L1 requests',
|
|
48
|
+
process: 'novatech/core-ops',
|
|
49
|
+
layer: 1,
|
|
50
|
+
detail: entityDetail([
|
|
51
|
+
['level', 'L1'],
|
|
52
|
+
['projects', 'HVAC, plumbing'],
|
|
53
|
+
]),
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
id: 'sup',
|
|
57
|
+
name: 'ShiftSupervisor',
|
|
58
|
+
construct: 'custom_entity',
|
|
59
|
+
entityKind: 'Person',
|
|
60
|
+
file: '',
|
|
61
|
+
purl: 'pkg:github/novatech/facilities-ops',
|
|
62
|
+
purpose: 'assigns technicians, approves out-of-scope work',
|
|
63
|
+
process: 'novatech/core-ops',
|
|
64
|
+
layer: 2,
|
|
65
|
+
detail: entityDetail([
|
|
66
|
+
['level', 'L2'],
|
|
67
|
+
['approvalLimit', '$500'],
|
|
68
|
+
]),
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
id: 'mgr',
|
|
72
|
+
name: 'CampusOpsManager',
|
|
73
|
+
construct: 'custom_entity',
|
|
74
|
+
entityKind: 'Person',
|
|
75
|
+
file: '',
|
|
76
|
+
purl: 'pkg:github/novatech/facilities-ops',
|
|
77
|
+
purpose: 'owns the queue, escalates campus-wide issues',
|
|
78
|
+
process: 'novatech/core-ops',
|
|
79
|
+
layer: 3,
|
|
80
|
+
detail: entityDetail([
|
|
81
|
+
['level', 'L3'],
|
|
82
|
+
['approvalLimit', '$5,000'],
|
|
83
|
+
]),
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
id: 'vp',
|
|
87
|
+
name: 'VPOperations',
|
|
88
|
+
construct: 'custom_entity',
|
|
89
|
+
entityKind: 'Person',
|
|
90
|
+
file: '',
|
|
91
|
+
purl: 'pkg:github/novatech/facilities-ops',
|
|
92
|
+
purpose: 'funds large projects, signs off L4 requests',
|
|
93
|
+
process: 'novatech/core-ops',
|
|
94
|
+
layer: 4,
|
|
95
|
+
detail: entityDetail([
|
|
96
|
+
['level', 'L4'],
|
|
97
|
+
['approvalLimit', 'unlimited'],
|
|
98
|
+
]),
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
id: 'agent',
|
|
102
|
+
name: 'OpsInsightAgent',
|
|
103
|
+
construct: 'custom_entity',
|
|
104
|
+
entityKind: 'agent',
|
|
105
|
+
color: '#9d6afb',
|
|
106
|
+
file: '',
|
|
107
|
+
purl: 'pkg:github/novatech/facilities-ops',
|
|
108
|
+
purpose: 'proposes nudges from open-request patterns; never owns the queue',
|
|
109
|
+
process: 'novatech/core-ops',
|
|
110
|
+
layer: 2,
|
|
111
|
+
detail: entityDetail([
|
|
112
|
+
['slack', 'novatech/facilities-ops'],
|
|
113
|
+
['permission', 'propose-only'],
|
|
114
|
+
]),
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
id: 'queue',
|
|
118
|
+
name: 'NudgeQueue',
|
|
119
|
+
construct: 'custom_entity',
|
|
120
|
+
entityKind: 'queue',
|
|
121
|
+
color: '#b48ead',
|
|
122
|
+
file: '',
|
|
123
|
+
purl: 'pkg:github/novatech/facilities-ops',
|
|
124
|
+
purpose: 'holds proposed nudges until a supervisor confirms them',
|
|
125
|
+
process: 'novatech/core-ops',
|
|
126
|
+
layer: 3,
|
|
127
|
+
detail: entityDetail([
|
|
128
|
+
['durable', 'postgres + outbox'],
|
|
129
|
+
['owner', 'CampusOpsManager'],
|
|
130
|
+
]),
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
id: 'store',
|
|
134
|
+
name: 'WorkRequestStore',
|
|
135
|
+
construct: 'store',
|
|
136
|
+
file: 'src/ops/workRequests.ts',
|
|
137
|
+
purl: 'pkg:github/novatech/facilities-ops',
|
|
138
|
+
purpose: 'retained work-request state — submissions, assignments, approvals',
|
|
139
|
+
symbol: 'workRequests',
|
|
140
|
+
process: 'novatech/core-ops',
|
|
141
|
+
layer: 1,
|
|
142
|
+
},
|
|
143
|
+
];
|
|
144
|
+
|
|
145
|
+
const edges: SubsystemComponentEdge[] = [
|
|
146
|
+
['wr', 'store', 'feeds'],
|
|
147
|
+
['store', 'tech', 'feeds'],
|
|
148
|
+
['tech', 'sup', 'watches'],
|
|
149
|
+
['sup', 'queue', 'watches'],
|
|
150
|
+
['agent', 'store', 'reads'],
|
|
151
|
+
['agent', 'queue', 'feeds'],
|
|
152
|
+
['queue', 'mgr', 'feeds'],
|
|
153
|
+
['mgr', 'vp', 'watches'],
|
|
154
|
+
].map(([from, to, mechanism], i) => ({ id: `e${i}`, from, to, mechanism }));
|
|
155
|
+
|
|
156
|
+
const meta = {
|
|
157
|
+
title: 'Subsystem/ComponentGraph/CustomEntities',
|
|
158
|
+
component: SubsystemComponentGraph,
|
|
159
|
+
parameters: { layout: 'fullscreen' },
|
|
160
|
+
tags: ['autodocs'],
|
|
161
|
+
decorators: [
|
|
162
|
+
(Story) => (
|
|
163
|
+
<ThemeProvider theme={defaultEditorTheme}>
|
|
164
|
+
<Story />
|
|
165
|
+
</ThemeProvider>
|
|
166
|
+
),
|
|
167
|
+
],
|
|
168
|
+
} satisfies Meta<typeof SubsystemComponentGraph>;
|
|
169
|
+
|
|
170
|
+
export default meta;
|
|
171
|
+
type Story = StoryObj<typeof meta>;
|
|
172
|
+
|
|
173
|
+
export const NovaTechWorkRequests: Story = {
|
|
174
|
+
render: () => (
|
|
175
|
+
<div style={{ width: '100%', height: '100vh' }}>
|
|
176
|
+
<SubsystemComponentGraph
|
|
177
|
+
title="Custom entities — NovaTech work-request chain"
|
|
178
|
+
description="Persons, an agent, and a queue as `custom_entity` nodes (badge = entityKind). Actors keep the themed coral; the agent and queue wear authored `color` overrides. Click a node for its declaration — `entity 'Name' — kind` plus authored `attributes` (key: value)."
|
|
179
|
+
components={components}
|
|
180
|
+
edges={edges}
|
|
181
|
+
/>
|
|
182
|
+
</div>
|
|
183
|
+
),
|
|
184
|
+
};
|
|
@@ -18,7 +18,7 @@ import {
|
|
|
18
18
|
import type { SubsystemOpenFileOptions } from './declarationRef';
|
|
19
19
|
import { parseSourceLocation } from './declarationRef';
|
|
20
20
|
import { tokenizeComponent } from './tokenizeComponent';
|
|
21
|
-
import {
|
|
21
|
+
import { componentColor } from '../pierre/constructColors';
|
|
22
22
|
|
|
23
23
|
/** Live / result state for the declaration-panel Verify control. */
|
|
24
24
|
export type ComponentVerificationPhase =
|
|
@@ -263,7 +263,7 @@ export function ComponentDeclaration({
|
|
|
263
263
|
const [showFile, setShowFile] = useState(false);
|
|
264
264
|
const [showPurpose, setShowPurpose] = useState(false);
|
|
265
265
|
const muted = theme.colors.textMuted ?? theme.colors.textSecondary;
|
|
266
|
-
const color =
|
|
266
|
+
const color = componentColor(component, pierreSyntaxTheme);
|
|
267
267
|
const okColor = '#3d9a5f';
|
|
268
268
|
const warnColor = theme.colors.textSecondary;
|
|
269
269
|
|
|
@@ -599,7 +599,8 @@ function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measur
|
|
|
599
599
|
// React Flow's own selection state from updating.
|
|
600
600
|
const dispNodes = useMemo(() => {
|
|
601
601
|
// While a step is hovered, only the hovered step's endpoints stay bright;
|
|
602
|
-
// everything else
|
|
602
|
+
// everything else among the currently-visible (opened-throughline) nodes
|
|
603
|
+
// is dimmed. Nodes outside the opened-throughline set stay hidden.
|
|
603
604
|
return xyflowNodesBase.map((n) => {
|
|
604
605
|
// Boundary frames follow their members: hidden when no member is
|
|
605
606
|
// visible, dimmed when members are dimmed. Never selectable.
|
|
@@ -612,9 +613,9 @@ function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measur
|
|
|
612
613
|
anySelected: brightNodeIds != null,
|
|
613
614
|
});
|
|
614
615
|
const dimmed = hoverNodeIds
|
|
615
|
-
? !memberIds.some((id) => hoverNodeIds.has(id))
|
|
616
|
+
? vis.hidden || !memberIds.some((id) => hoverNodeIds.has(id))
|
|
616
617
|
: vis.dimmed;
|
|
617
|
-
const hidden =
|
|
618
|
+
const hidden = vis.hidden;
|
|
618
619
|
return {
|
|
619
620
|
...n,
|
|
620
621
|
hidden,
|
|
@@ -634,8 +635,8 @@ function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measur
|
|
|
634
635
|
anyOpened: openedNodeIds != null,
|
|
635
636
|
anySelected: brightNodeIds != null,
|
|
636
637
|
});
|
|
637
|
-
|
|
638
|
-
|
|
638
|
+
const dimmed = hoverNodeIds ? (vis.hidden || !hoverNodeIds.has(n.id)) : vis.dimmed;
|
|
639
|
+
const hidden = vis.hidden;
|
|
639
640
|
if (fileMatch === undefined && !isSelected && !dimmed) {
|
|
640
641
|
const { fileMatch: _f, isSelected: _s, dimmed: _d, ...rest } = n.data as Record<string, unknown>;
|
|
641
642
|
return { ...n, hidden, data: rest };
|
|
@@ -680,8 +681,8 @@ function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measur
|
|
|
680
681
|
anyOpened: openedEdgeIds != null,
|
|
681
682
|
anySelected: focusEdgeIds != null,
|
|
682
683
|
});
|
|
683
|
-
const dimmed = hoverEdgeIds ? !hoverEdgeIds.has(e.id) : vis.dimmed;
|
|
684
|
-
const hidden =
|
|
684
|
+
const dimmed = hoverEdgeIds ? vis.hidden || !hoverEdgeIds.has(e.id) : vis.dimmed;
|
|
685
|
+
const hidden = vis.hidden;
|
|
685
686
|
return { ...paint(e, dimmed), hidden };
|
|
686
687
|
});
|
|
687
688
|
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test';
|
|
2
|
+
import { generateDeclarationString } from './formatDeclaration';
|
|
3
|
+
import type { SubsystemComponent } from './model';
|
|
4
|
+
|
|
5
|
+
describe('generateDeclarationString — custom_entity', () => {
|
|
6
|
+
test('renders entity identity + entityKind, no attributes when none authored', () => {
|
|
7
|
+
const component: SubsystemComponent = {
|
|
8
|
+
id: 'ct',
|
|
9
|
+
name: 'FacilitiesTechnician',
|
|
10
|
+
construct: 'custom_entity',
|
|
11
|
+
entityKind: 'Person',
|
|
12
|
+
file: '',
|
|
13
|
+
purl: 'pkg:github/novatech/facilities-ops',
|
|
14
|
+
};
|
|
15
|
+
expect(generateDeclarationString(component)).toBe(
|
|
16
|
+
"entity 'FacilitiesTechnician' — Person",
|
|
17
|
+
);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test('renders authored attributes as indented key: value lines', () => {
|
|
21
|
+
const component: SubsystemComponent = {
|
|
22
|
+
id: 'ct',
|
|
23
|
+
name: 'OpsInsightAgent',
|
|
24
|
+
construct: 'custom_entity',
|
|
25
|
+
entityKind: 'agent',
|
|
26
|
+
file: '',
|
|
27
|
+
purl: 'pkg:github/novatech/facilities-ops',
|
|
28
|
+
detail: {
|
|
29
|
+
kind: 'custom_entity',
|
|
30
|
+
attributes: [
|
|
31
|
+
{ key: 'slack', value: 'novatech/facilities-ops' },
|
|
32
|
+
{ key: 'permission', value: 'propose-only' },
|
|
33
|
+
],
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
expect(generateDeclarationString(component)).toBe(
|
|
37
|
+
"entity 'OpsInsightAgent' — agent\n slack: novatech/facilities-ops\n permission: propose-only",
|
|
38
|
+
);
|
|
39
|
+
});
|
|
40
|
+
});
|
|
@@ -45,6 +45,19 @@ export function generateDeclarationString(component: SubsystemComponent): string
|
|
|
45
45
|
case 'external':
|
|
46
46
|
// Not valid TypeScript — caller should handle formatting.
|
|
47
47
|
return `external '${detail?.kind === 'external' ? detail.label : name}'`;
|
|
48
|
+
case 'custom_entity':
|
|
49
|
+
// An actor, not code — no declaration to generate. Renders as a
|
|
50
|
+
// non-TypeScript block: `entity 'Name' — kind` followed by the authored
|
|
51
|
+
// attributes (key: value) indented beneath. Not valid TS — callers
|
|
52
|
+
// bypass Prettier.
|
|
53
|
+
{
|
|
54
|
+
const kindLabel = component.entityKind ? ` — ${component.entityKind}` : '';
|
|
55
|
+
const attrs = detail?.kind === 'custom_entity' ? detail.attributes : [];
|
|
56
|
+
const attrLines = attrs.map((a) => ` ${a.key}: ${a.value}`).join('\n');
|
|
57
|
+
return attrLines
|
|
58
|
+
? `entity '${name}'${kindLabel}\n${attrLines}`
|
|
59
|
+
: `entity '${name}'${kindLabel}`;
|
|
60
|
+
}
|
|
48
61
|
default:
|
|
49
62
|
return `${construct} ${name}`;
|
|
50
63
|
}
|
|
@@ -93,6 +93,8 @@ describe('subsystem graph model', () => {
|
|
|
93
93
|
// falls back to existing name when no symbol (class stays bare).
|
|
94
94
|
expect(deriveNameFromSymbol(undefined, 'class', 'SessionReader')).toBe('SessionReader');
|
|
95
95
|
expect(deriveNameFromSymbol('', 'external', 'principal-studio-host')).toBe('principal-studio-host');
|
|
96
|
+
// custom entities render bare — no decoration, name is the identity
|
|
97
|
+
expect(deriveNameFromSymbol(undefined, 'custom_entity', 'FacilitiesTechnician')).toBe('FacilitiesTechnician');
|
|
96
98
|
});
|
|
97
99
|
|
|
98
100
|
test('executable constructs wear () on the node', () => {
|
|
@@ -127,6 +129,13 @@ describe('subsystem graph model', () => {
|
|
|
127
129
|
expect(constructBadgeLabel({ construct: 'function', stereotype: 'hook' })).toBe('hook');
|
|
128
130
|
expect(constructBadgeLabel({ construct: 'function' })).toBe('function');
|
|
129
131
|
expect(constructBadgeLabel({ construct: 'type_alias' })).toBe('type alias');
|
|
132
|
+
// custom entities wear their entityKind as the badge
|
|
133
|
+
expect(
|
|
134
|
+
constructBadgeLabel({ construct: 'custom_entity', entityKind: 'Person' }),
|
|
135
|
+
).toBe('Person');
|
|
136
|
+
expect(
|
|
137
|
+
constructBadgeLabel({ construct: 'custom_entity' }),
|
|
138
|
+
).toBe('custom_entity');
|
|
130
139
|
});
|
|
131
140
|
|
|
132
141
|
test('nodeMinWidthForBadges widens for long construct badges and role pairs', () => {
|
package/src/subsystem/model.ts
CHANGED
|
@@ -30,7 +30,8 @@ export type SubsystemComponentConstruct =
|
|
|
30
30
|
| 'enum'
|
|
31
31
|
| 'module'
|
|
32
32
|
| 'store'
|
|
33
|
-
| 'external'
|
|
33
|
+
| 'external'
|
|
34
|
+
| 'custom_entity';
|
|
34
35
|
|
|
35
36
|
/**
|
|
36
37
|
* Semantic role — where the node sits in the topology, orthogonal to
|
|
@@ -145,6 +146,22 @@ export interface SubsystemComponent {
|
|
|
145
146
|
* a React UI unit reads as "component" rather than "function".
|
|
146
147
|
*/
|
|
147
148
|
stereotype?: SubsystemStereotype;
|
|
149
|
+
/**
|
|
150
|
+
* Open-string entity kind for `construct: 'custom_entity'` nodes — what the
|
|
151
|
+
* actor/entity is (e.g. `Person`, `agent`, `queue`). Not code; there is no
|
|
152
|
+
* graphify anchor. Contrast with `construct` (what the declaration is) and
|
|
153
|
+
* `role` (where it sits topologically). Empty/unset for code constructs.
|
|
154
|
+
*/
|
|
155
|
+
entityKind?: string;
|
|
156
|
+
/**
|
|
157
|
+
* Authored color override for the node border + badges, when the themed
|
|
158
|
+
* construct color isn't wanted. Primarily for `construct: 'custom_entity'`
|
|
159
|
+
* (an author picks a fitting hue for a Person/agent/queue) — but usable on
|
|
160
|
+
* any construct. Any `#rrggbb` string; when set it wins over the
|
|
161
|
+
* construct-derived color, mirrors in the declaration panel. Leave unset to
|
|
162
|
+
* inherit the Pierre construct palette.
|
|
163
|
+
*/
|
|
164
|
+
color?: string;
|
|
148
165
|
/**
|
|
149
166
|
* Runtime process membership — which deployment unit this node is a
|
|
150
167
|
* member of (e.g. `principal-studio/host`, `principal-studio/renderer`). Nodes
|
|
@@ -512,13 +529,18 @@ export const ROLE_LABEL: Record<SubsystemComponentRole, string> = {
|
|
|
512
529
|
* Primary badge text for a node: prefer framework stereotype over the
|
|
513
530
|
* language construct so a React UI unit reads as "component" / "hook"
|
|
514
531
|
* rather than "function". When both framework and stereotype are set,
|
|
515
|
-
* show `framework · stereotype` (e.g. `react · component`).
|
|
532
|
+
* show `framework · stereotype` (e.g. `react · component`). A custom
|
|
533
|
+
* entity wears its `entityKind` (Person/agent/queue) as the badge.
|
|
516
534
|
*/
|
|
517
535
|
export function constructBadgeLabel(component: {
|
|
518
536
|
construct: SubsystemComponentConstruct;
|
|
519
537
|
framework?: string;
|
|
520
538
|
stereotype?: string;
|
|
539
|
+
entityKind?: string;
|
|
521
540
|
}): string {
|
|
541
|
+
if (component.construct === 'custom_entity' && component.entityKind) {
|
|
542
|
+
return component.entityKind;
|
|
543
|
+
}
|
|
522
544
|
const constructLabel =
|
|
523
545
|
component.construct === 'type_alias' ? 'type alias' : component.construct;
|
|
524
546
|
if (component.stereotype && component.framework) {
|
|
@@ -554,6 +576,7 @@ export function nodeMinWidthForBadges(component: {
|
|
|
554
576
|
framework?: string;
|
|
555
577
|
stereotype?: string;
|
|
556
578
|
role?: SubsystemComponentRole;
|
|
579
|
+
entityKind?: string;
|
|
557
580
|
}): number {
|
|
558
581
|
const left = estimateBadgeLabelWidth(constructBadgeLabel(component));
|
|
559
582
|
if (component.role == null) {
|
package/src/subsystem/nodes.tsx
CHANGED
|
@@ -30,7 +30,7 @@ import {
|
|
|
30
30
|
type SubsystemGroupNodeData,
|
|
31
31
|
type SubsystemGraphEdge,
|
|
32
32
|
} from './model';
|
|
33
|
-
import {
|
|
33
|
+
import { componentColor } from '../pierre/constructColors';
|
|
34
34
|
import { resolvePierreSyntaxThemeName } from '../pierre/pierreSyntaxTheme';
|
|
35
35
|
|
|
36
36
|
export const CONSTRUCT_LABEL: Record<string, string> = {
|
|
@@ -43,6 +43,7 @@ export const CONSTRUCT_LABEL: Record<string, string> = {
|
|
|
43
43
|
module: 'module',
|
|
44
44
|
store: 'store',
|
|
45
45
|
external: 'external',
|
|
46
|
+
custom_entity: 'entity',
|
|
46
47
|
};
|
|
47
48
|
|
|
48
49
|
/** Insert zero-width spaces at identifier word boundaries so long names wrap
|
|
@@ -83,8 +84,8 @@ export function SubsystemComponentNode(props: NodeProps<Node<SubsystemGraphNodeD
|
|
|
83
84
|
// Construct owns node color, derived from the active Pierre syntax theme —
|
|
84
85
|
// the same palette the declaration panel and file drawer render with. Role
|
|
85
86
|
// shows as the hover badge, not as color — for now; a role glyph/accent may
|
|
86
|
-
// come later.
|
|
87
|
-
const color =
|
|
87
|
+
// come later. A component-authored `color` override wins over the construct.
|
|
88
|
+
const color = componentColor(c, resolvePierreSyntaxThemeName(mode));
|
|
88
89
|
const [hover, setHover] = useState(false);
|
|
89
90
|
const configuredMax = SUBSYSTEM_CALLBACKS.maxNodeWidth;
|
|
90
91
|
const maxWidth = configuredMax ?? 300;
|
|
@@ -57,6 +57,11 @@ export async function tokenizeComponent(
|
|
|
57
57
|
return tokenizeFormatted(`external '${escaped}'`, themeName);
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
+
// Custom entity — an actor (Person/agent/queue), not code; bypass Prettier.
|
|
61
|
+
if (kind === 'custom_entity') {
|
|
62
|
+
return tokenizeFormatted(generateDeclarationString(component), themeName);
|
|
63
|
+
}
|
|
64
|
+
|
|
60
65
|
// Generate → format → tokenize
|
|
61
66
|
const raw = generateDeclarationString(component);
|
|
62
67
|
const { prettier, plugins } = await getPrettier();
|