@principal-ai/subsystems-react 0.51.3 → 0.52.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 +4 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/subsystem/C4Graph.d.ts +3 -7
- package/dist/subsystem/C4Graph.d.ts.map +1 -1
- package/dist/subsystem/C4Graph.js +202 -63
- package/dist/subsystem/C4Graph.js.map +1 -1
- package/dist/subsystem/C4NodeCard.d.ts +26 -26
- package/dist/subsystem/C4NodeCard.d.ts.map +1 -1
- package/dist/subsystem/C4NodeCard.js +56 -30
- package/dist/subsystem/C4NodeCard.js.map +1 -1
- package/dist/subsystem/IssueList.d.ts +1 -1
- package/dist/subsystem/IssueList.d.ts.map +1 -1
- package/dist/subsystem/IssueList.js +20 -1
- package/dist/subsystem/IssueList.js.map +1 -1
- package/dist/subsystem/SubsystemComponentGraph.d.ts.map +1 -1
- package/dist/subsystem/SubsystemComponentGraph.js +74 -26
- package/dist/subsystem/SubsystemComponentGraph.js.map +1 -1
- package/dist/subsystem/c4.d.ts +48 -49
- package/dist/subsystem/c4.d.ts.map +1 -1
- package/dist/subsystem/c4.js +50 -0
- package/dist/subsystem/c4.js.map +1 -1
- package/dist/subsystem/c4Evidence.d.ts +88 -84
- package/dist/subsystem/c4Evidence.d.ts.map +1 -1
- package/dist/subsystem/c4Evidence.js +113 -166
- package/dist/subsystem/c4Evidence.js.map +1 -1
- package/dist/subsystem/model.d.ts +6 -0
- package/dist/subsystem/model.d.ts.map +1 -1
- package/dist/subsystem/model.js.map +1 -1
- package/dist/subsystem/nodes.d.ts.map +1 -1
- package/dist/subsystem/nodes.js +9 -7
- package/dist/subsystem/nodes.js.map +1 -1
- package/dist/subsystem/techIcons/index.d.ts +5 -0
- package/dist/subsystem/techIcons/index.d.ts.map +1 -1
- package/dist/subsystem/techIcons/index.js +3 -0
- package/dist/subsystem/techIcons/index.js.map +1 -1
- package/dist/utils/edgeLabel.d.ts +14 -0
- package/dist/utils/edgeLabel.d.ts.map +1 -1
- package/dist/utils/edgeLabel.js +24 -0
- package/dist/utils/edgeLabel.js.map +1 -1
- package/dist/utils/elkLayout.d.ts +65 -0
- package/dist/utils/elkLayout.d.ts.map +1 -1
- package/dist/utils/elkLayout.js +145 -11
- package/dist/utils/elkLayout.js.map +1 -1
- package/package.json +1 -1
- package/src/index.ts +8 -6
- package/src/stories/Subsystem/C4Graph/C4Component.stories.tsx +198 -0
- package/src/stories/Subsystem/C4Graph/C4Container.stories.tsx +42 -6
- package/src/stories/Subsystem/C4Graph/C4NodeGallery.stories.tsx +9 -7
- package/src/stories/Subsystem/ComponentGraph/Issues.stories.tsx +686 -2
- package/src/stories/Subsystem/ComponentGraph/ProposedTrails.stories.tsx +21 -1
- package/src/subsystem/C4Graph.tsx +296 -99
- package/src/subsystem/C4NodeCard.test.ts +32 -22
- package/src/subsystem/C4NodeCard.tsx +67 -39
- package/src/subsystem/IssueList.tsx +23 -0
- package/src/subsystem/SubsystemComponentGraph.tsx +79 -23
- package/src/subsystem/c4.protocolColor.test.ts +26 -0
- package/src/subsystem/c4.ts +76 -52
- package/src/subsystem/c4Evidence.test.ts +177 -237
- package/src/subsystem/c4Evidence.ts +160 -235
- package/src/subsystem/model.ts +6 -0
- package/src/subsystem/nodes.group.test.tsx +15 -0
- package/src/subsystem/nodes.tsx +12 -9
- package/src/subsystem/techIcons/index.tsx +8 -0
- package/src/utils/edgeLabel.test.ts +21 -2
- package/src/utils/edgeLabel.ts +28 -0
- package/src/utils/elkLayout.test.ts +50 -0
- package/src/utils/elkLayout.ts +191 -11
- package/src/stories/Subsystem/ComponentGraph/IssueOverlay.stories.tsx +0 -602
package/dist/subsystem/c4.js
CHANGED
|
@@ -12,6 +12,12 @@
|
|
|
12
12
|
* supplies the drawable model directly rather than having trails converted into
|
|
13
13
|
* lines.
|
|
14
14
|
*
|
|
15
|
+
* The element set is an umbrella over a model, not a standalone artifact: what
|
|
16
|
+
* sits inside a box resolves from the model document's `process` fields, folded
|
|
17
|
+
* to the claiming container's key by accepted consolidation records (see
|
|
18
|
+
* `c4Evidence`). Nothing here stores membership, so nothing here can drift from
|
|
19
|
+
* the model it organizes.
|
|
20
|
+
*
|
|
15
21
|
* ## Why a union and not one type with two axes
|
|
16
22
|
*
|
|
17
23
|
* C4's vocabulary is closed: software system, container, component, code —
|
|
@@ -31,6 +37,50 @@
|
|
|
31
37
|
*
|
|
32
38
|
* [C4 notation]: https://c4model.com/diagrams/notation
|
|
33
39
|
*/
|
|
40
|
+
/**
|
|
41
|
+
* Protocol hues, keyed by a canonical protocol token.
|
|
42
|
+
*
|
|
43
|
+
* C4's notation is colour-free, so this is a local choice — but on a container
|
|
44
|
+
* diagram the protocol is what a reader wants to distinguish at a glance
|
|
45
|
+
* (in-process RPC vs HTTP vs file I/O), more than the trail verb the line came
|
|
46
|
+
* from. Keys are lowercase, matched loosely (see `protocolColor`).
|
|
47
|
+
*/
|
|
48
|
+
export const PROTOCOL_COLOR = {
|
|
49
|
+
rpc: '#22c55e',
|
|
50
|
+
http: '#e8853a',
|
|
51
|
+
https: '#e8853a',
|
|
52
|
+
rest: '#e8853a',
|
|
53
|
+
graphql: '#e061a3',
|
|
54
|
+
fs: '#0ea5e9',
|
|
55
|
+
'file i/o': '#0ea5e9',
|
|
56
|
+
sql: '#a78bfa',
|
|
57
|
+
sqlite: '#a78bfa',
|
|
58
|
+
postgres: '#a78bfa',
|
|
59
|
+
'json-rpc': '#22c55e',
|
|
60
|
+
ws: '#e3b341',
|
|
61
|
+
websocket: '#e3b341',
|
|
62
|
+
mcp: '#4ec9b0',
|
|
63
|
+
stdin: '#9ca3af',
|
|
64
|
+
stdout: '#9ca3af',
|
|
65
|
+
};
|
|
66
|
+
/** Fallback hue for an edge whose protocol we have no colour for (or none). */
|
|
67
|
+
export const PROTOCOL_COLOR_FALLBACK = '#9ca3af';
|
|
68
|
+
/**
|
|
69
|
+
* Colour for an edge's protocol. Matches the longest known token contained in
|
|
70
|
+
* the (lowercased) protocol string, so `Electrobun RPC` finds `rpc` and
|
|
71
|
+
* `JSON-RPC` finds the `json-rpc` entry before `rpc`.
|
|
72
|
+
*/
|
|
73
|
+
export function protocolColor(protocol) {
|
|
74
|
+
if (!protocol)
|
|
75
|
+
return PROTOCOL_COLOR_FALLBACK;
|
|
76
|
+
const p = protocol.toLowerCase();
|
|
77
|
+
let best = '';
|
|
78
|
+
for (const key of Object.keys(PROTOCOL_COLOR)) {
|
|
79
|
+
if (key.length > best.length && p.includes(key))
|
|
80
|
+
best = key;
|
|
81
|
+
}
|
|
82
|
+
return best ? PROTOCOL_COLOR[best] : PROTOCOL_COLOR_FALLBACK;
|
|
83
|
+
}
|
|
34
84
|
/**
|
|
35
85
|
* `owner/name` from a purl, trimmed of scheme and fragment. Used to label the
|
|
36
86
|
* system frame when a repo key is known.
|
package/dist/subsystem/c4.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"c4.js","sourceRoot":"","sources":["../../src/subsystem/c4.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"c4.js","sourceRoot":"","sources":["../../src/subsystem/c4.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsCG;AAoJH;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,cAAc,GAA2B;IACpD,GAAG,EAAE,SAAS;IACd,IAAI,EAAE,SAAS;IACf,KAAK,EAAE,SAAS;IAChB,IAAI,EAAE,SAAS;IACf,OAAO,EAAE,SAAS;IAClB,EAAE,EAAE,SAAS;IACb,UAAU,EAAE,SAAS;IACrB,GAAG,EAAE,SAAS;IACd,MAAM,EAAE,SAAS;IACjB,QAAQ,EAAE,SAAS;IACnB,UAAU,EAAE,SAAS;IACrB,EAAE,EAAE,SAAS;IACb,SAAS,EAAE,SAAS;IACpB,GAAG,EAAE,SAAS;IACd,KAAK,EAAE,SAAS;IAChB,MAAM,EAAE,SAAS;CAClB,CAAC;AAEF,+EAA+E;AAC/E,MAAM,CAAC,MAAM,uBAAuB,GAAG,SAAS,CAAC;AAEjD;;;;GAIG;AACH,MAAM,UAAU,aAAa,CAAC,QAA4B;IACxD,IAAI,CAAC,QAAQ;QAAE,OAAO,uBAAuB,CAAC;IAC9C,MAAM,CAAC,GAAG,QAAQ,CAAC,WAAW,EAAE,CAAC;IACjC,IAAI,IAAI,GAAG,EAAE,CAAC;IACd,KAAK,MAAM,GAAG,IAAI,MAAM,CAAC,IAAI,CAAC,cAAc,CAAC,EAAE;QAC7C,IAAI,GAAG,CAAC,MAAM,GAAG,IAAI,CAAC,MAAM,IAAI,CAAC,CAAC,QAAQ,CAAC,GAAG,CAAC;YAAE,IAAI,GAAG,GAAG,CAAC;KAC7D;IACD,OAAO,IAAI,CAAC,CAAC,CAAC,cAAc,CAAC,IAAI,CAAE,CAAC,CAAC,CAAC,uBAAuB,CAAC;AAChE,CAAC;AAWD;;;GAGG;AACH,MAAM,UAAU,aAAa,CAAC,IAAY;IACxC,MAAM,IAAI,GAAG,IAAI,CAAC,OAAO,CAAC,YAAY,EAAE,EAAE,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC;IACxE,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;IAC9C,OAAO,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,IAAI,IAAI,IAAI,CAAC;AACnD,CAAC"}
|
|
@@ -1,53 +1,27 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* C4 evidence —
|
|
2
|
+
* C4 evidence — the pure read-side of proposing and verifying C4 containers.
|
|
3
3
|
*
|
|
4
|
-
* This module does not decide anything.
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
* This module does not decide anything. The proposing agent reads the
|
|
5
|
+
* subsystem diagrams, queries the existing containers, reasons, and writes
|
|
6
|
+
* its own rationale; what lives here is only the mechanical part of that
|
|
7
|
+
* loop:
|
|
8
8
|
*
|
|
9
|
-
* -
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* -
|
|
9
|
+
* - `proposeElements` — the proposal scaffold: skip keys containers
|
|
10
|
+
* already claim, skip boundaries no container can be discerned from
|
|
11
|
+
* (no runtime code behind them)
|
|
12
|
+
* - `verifyProcessBoundaries` — a boundary's verification status, read
|
|
13
|
+
* off the claiming container
|
|
14
|
+
* - `verificationIssues` — those statuses as dynamic-topology findings,
|
|
15
|
+
* so verification surfaces through the standard diagnostics pipeline
|
|
16
|
+
* - `findUnassigned` — runtime components no `process` names at all
|
|
13
17
|
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
18
|
+
* Judgements — is this the same unit under a different name? is one function
|
|
19
|
+
* a deployable unit? — belong to the agent and the human, never to a
|
|
20
|
+
* matcher.
|
|
16
21
|
*/
|
|
17
22
|
import type { C4ContainerKind, C4Element, C4ElementSet } from './c4';
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
/** The process key, module path, or element id this concerns. */
|
|
21
|
-
target: string;
|
|
22
|
-
kind: C4ConcernKind;
|
|
23
|
-
message: string;
|
|
24
|
-
/** Aliases that triggered it. */
|
|
25
|
-
members?: string[];
|
|
26
|
-
}
|
|
27
|
-
/**
|
|
28
|
-
* The questions worth a human's attention.
|
|
29
|
-
*
|
|
30
|
-
* Each maps to something an agent cannot settle by reading a string — it has
|
|
31
|
-
* to open the source, or ask the user.
|
|
32
|
-
*/
|
|
33
|
-
export type C4ConcernKind =
|
|
34
|
-
/** two or more process keys may be the same deployable unit */
|
|
35
|
-
'merge_candidate'
|
|
36
|
-
/** exactly one member — a lone function is not a deployable unit */
|
|
37
|
-
| 'singleton'
|
|
38
|
-
/** no member executes at runtime — a library is not a container */
|
|
39
|
-
| 'library_shaped'
|
|
40
|
-
/** a store whose storage kind was never decided */
|
|
41
|
-
| 'store_unclassified'
|
|
42
|
-
/** members claim more than one repo — a container cannot straddle systems */
|
|
43
|
-
| 'spans_repos'
|
|
44
|
-
/** an external with no resolvable identity */
|
|
45
|
-
| 'external_unidentified'
|
|
46
|
-
/** a runtime component with no process at all */
|
|
47
|
-
| 'unassigned'
|
|
48
|
-
/** C4 requires technology on every container */
|
|
49
|
-
| 'missing_technology';
|
|
50
|
-
/** The fields a concern check reads off a component. */
|
|
23
|
+
import type { SubsystemIssue } from './IssueList';
|
|
24
|
+
/** The fields the propose/verify gates read off a component. */
|
|
51
25
|
export interface C4MemberView {
|
|
52
26
|
alias: string;
|
|
53
27
|
construct: string;
|
|
@@ -60,31 +34,6 @@ export interface C4MemberView {
|
|
|
60
34
|
}
|
|
61
35
|
/** True when a component runs, and so can be part of a deployable unit. */
|
|
62
36
|
export declare function isRuntimeConstruct(construct: string): boolean;
|
|
63
|
-
/**
|
|
64
|
-
* Derive the questions worth asking about one candidate element.
|
|
65
|
-
*
|
|
66
|
-
* Pure over the rollup, so it runs identically against a single model, the 40
|
|
67
|
-
* composed models, or a fixture. Pure *questions* — it never proposes an
|
|
68
|
-
* answer, because the answers are architectural.
|
|
69
|
-
*/
|
|
70
|
-
export declare function deriveConcerns(key: string, members: readonly C4MemberView[], tech?: {
|
|
71
|
-
frameworks: readonly string[];
|
|
72
|
-
}): C4Concern[];
|
|
73
|
-
/**
|
|
74
|
-
* Suggest merges between keys that share a trailing role.
|
|
75
|
-
*
|
|
76
|
-
* `subsystems-studio/host` and `principal-studio/host` are the same role under
|
|
77
|
-
* two app names. This is a *question*, never a merge — only the user knows
|
|
78
|
-
* whether they are one process or two.
|
|
79
|
-
*/
|
|
80
|
-
export declare function suggestMerges(rollup: ReadonlyArray<{
|
|
81
|
-
key: string;
|
|
82
|
-
memberCount: number;
|
|
83
|
-
}>): Array<{
|
|
84
|
-
role: string;
|
|
85
|
-
keys: string[];
|
|
86
|
-
reason: string;
|
|
87
|
-
}>;
|
|
88
37
|
/**
|
|
89
38
|
* Runtime components that state no `process` at all.
|
|
90
39
|
*
|
|
@@ -94,26 +43,80 @@ export declare function suggestMerges(rollup: ReadonlyArray<{
|
|
|
94
43
|
*/
|
|
95
44
|
export declare function findUnassigned(members: readonly C4MemberView[]): C4MemberView[];
|
|
96
45
|
/**
|
|
97
|
-
*
|
|
46
|
+
* How far a process boundary has travelled toward being a container.
|
|
98
47
|
*
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
-
*
|
|
48
|
+
* A boundary starts `unassigned` — no container claims it. Proposing an
|
|
49
|
+
* element for it (see `proposeElements`) assigns the boundary to a container,
|
|
50
|
+
* which reads as `proposed` until a human decides. `accepted` verifies the
|
|
51
|
+
* boundary: someone approved this run of code as one deployable unit.
|
|
52
|
+
* `rejected` means the proposal was wrong and the boundary should not be
|
|
53
|
+
* drawn as a box.
|
|
54
|
+
*/
|
|
55
|
+
export type C4ProcessVerification = 'unassigned' | 'proposed' | 'rejected' | 'verified';
|
|
56
|
+
/** One process boundary's assignment and its review state. */
|
|
57
|
+
export interface C4ProcessBoundaryStatus {
|
|
58
|
+
/** The `process` key, e.g. `principal-studio/host`. */
|
|
59
|
+
key: string;
|
|
60
|
+
/** Aliases carrying this key in the model, when the caller supplied them. */
|
|
61
|
+
memberAliases: string[];
|
|
62
|
+
status: C4ProcessVerification;
|
|
63
|
+
/** The container whose state decided the status. Absent when `unassigned`. */
|
|
64
|
+
element?: C4Element;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Read a process boundary's verification off its container assignment.
|
|
102
68
|
*
|
|
103
|
-
*
|
|
104
|
-
*
|
|
105
|
-
*
|
|
106
|
-
*
|
|
69
|
+
* The claimant is the container whose `process` equals the boundary's key —
|
|
70
|
+
* a plain string match, no indirection. Two keys that are one deployable unit
|
|
71
|
+
* under two names are fixed in the model document (a consolidation proposal
|
|
72
|
+
* rewrites the components' `process` fields), so the reader never sees a
|
|
73
|
+
* discrepancy to resolve. When several containers claim one key (a
|
|
74
|
+
* hand-edited set can do this), the decided one wins: accepted over rejected
|
|
75
|
+
* over proposed, so one reviewer's yes outranks another's no.
|
|
107
76
|
*
|
|
108
|
-
*
|
|
77
|
+
* Pure: the same rollup and elements always read the same. `rollup` accepts
|
|
78
|
+
* `SubsystemProcessRegion`s directly — only `key` and `memberAliases` are read.
|
|
109
79
|
*/
|
|
110
|
-
export declare function
|
|
80
|
+
export declare function verifyProcessBoundaries(options: {
|
|
81
|
+
rollup: ReadonlyArray<{
|
|
82
|
+
key: string;
|
|
83
|
+
memberAliases?: readonly string[];
|
|
84
|
+
}>;
|
|
85
|
+
elements: readonly C4Element[];
|
|
86
|
+
}): C4ProcessBoundaryStatus[];
|
|
111
87
|
/**
|
|
112
|
-
*
|
|
113
|
-
*
|
|
88
|
+
* Verification statuses as diagnostics findings — the dynamic-topology layer
|
|
89
|
+
* of the issues list. One finding per boundary that is NOT verified: an
|
|
90
|
+
* unclaimed boundary is the absence a proposing agent run resolves, a
|
|
91
|
+
* `proposed` one awaits the human's decision, a `rejected` one records that
|
|
92
|
+
* the decision was no. `verified` reports nothing — the audit reports
|
|
93
|
+
* absence only, and silence is the good state.
|
|
94
|
+
*
|
|
95
|
+
* The findings ride the existing pipeline untouched: they badge the process
|
|
96
|
+
* frame (target kind `process`), list under Dynamic topology, focus their
|
|
97
|
+
* members on click, and carry the fix the decision needs. Pure over its
|
|
98
|
+
* input, like the rest of this module.
|
|
99
|
+
*/
|
|
100
|
+
export declare function verificationIssues(statuses: readonly C4ProcessBoundaryStatus[]): SubsystemIssue[];
|
|
101
|
+
/**
|
|
102
|
+
* A starting point for an agent, shaped by the two questions that matter when
|
|
103
|
+
* assessing a process boundary:
|
|
104
|
+
*
|
|
105
|
+
* 1. **Does a container already exist for it?** (`elements`) — a key one
|
|
106
|
+
* already claims is never re-proposed; proposing another would be
|
|
107
|
+
* exactly the duplicate this module exists to prevent. The exact claim
|
|
108
|
+
* is a fact and skipped here; whether a *similarly named* container is
|
|
109
|
+
* the same unit is a judgement, so it belongs to the agent querying the
|
|
110
|
+
* container list — the fix there is a consolidation proposal, not a
|
|
111
|
+
* second box.
|
|
112
|
+
* 2. **Can the boundary be discerned into a container?** — code that never
|
|
113
|
+
* runs cannot be, so a library-shaped group gets no proposal; that
|
|
114
|
+
* fact is the agent's to report, not the scaffold's.
|
|
114
115
|
*
|
|
115
|
-
*
|
|
116
|
-
*
|
|
116
|
+
* For boundaries passing both, one `proposed` container per key. The
|
|
117
|
+
* rationale is the proposing agent's own reasoning, written on the proposal
|
|
118
|
+
* it POSTs — this scaffold does not fabricate one. A scaffold to edit and
|
|
119
|
+
* argue with — not an answer, and never `accepted`.
|
|
117
120
|
*/
|
|
118
121
|
export declare function proposeElements(options: {
|
|
119
122
|
repoKey: string;
|
|
@@ -122,7 +125,8 @@ export declare function proposeElements(options: {
|
|
|
122
125
|
members: readonly C4MemberView[];
|
|
123
126
|
frameworks: readonly string[];
|
|
124
127
|
}>;
|
|
125
|
-
|
|
128
|
+
/** Containers that already exist (from the element store), by `process` claim. */
|
|
129
|
+
elements?: readonly C4Element[];
|
|
126
130
|
}): C4ElementSet;
|
|
127
131
|
/**
|
|
128
132
|
* A first guess at what sort of container this is, offered as a *default* for
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"c4Evidence.d.ts","sourceRoot":"","sources":["../../src/subsystem/c4Evidence.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"c4Evidence.d.ts","sourceRoot":"","sources":["../../src/subsystem/c4Evidence.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,OAAO,KAAK,EAAe,eAAe,EAAE,SAAS,EAAE,YAAY,EAAE,MAAM,MAAM,CAAC;AAClF,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAElD,gEAAgE;AAChE,MAAM,WAAW,YAAY;IAC3B,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB;AAUD,2EAA2E;AAC3E,wBAAgB,kBAAkB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAE7D;AAED;;;;;;GAMG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,SAAS,YAAY,EAAE,GAAG,YAAY,EAAE,CAE/E;AAED;;;;;;;;;GASG;AACH,MAAM,MAAM,qBAAqB,GAC7B,YAAY,GACZ,UAAU,GACV,UAAU,GACV,UAAU,CAAC;AAEf,8DAA8D;AAC9D,MAAM,WAAW,uBAAuB;IACtC,uDAAuD;IACvD,GAAG,EAAE,MAAM,CAAC;IACZ,6EAA6E;IAC7E,aAAa,EAAE,MAAM,EAAE,CAAC;IACxB,MAAM,EAAE,qBAAqB,CAAC;IAC9B,8EAA8E;IAC9E,OAAO,CAAC,EAAE,SAAS,CAAC;CACrB;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,uBAAuB,CAAC,OAAO,EAAE;IAC/C,MAAM,EAAE,aAAa,CAAC;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,aAAa,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;KAAE,CAAC,CAAC;IAC1E,QAAQ,EAAE,SAAS,SAAS,EAAE,CAAC;CAChC,GAAG,uBAAuB,EAAE,CAsB5B;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,kBAAkB,CAChC,QAAQ,EAAE,SAAS,uBAAuB,EAAE,GAC3C,cAAc,EAAE,CAyBlB;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE;IACvC,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,aAAa,CAAC;QACpB,GAAG,EAAE,MAAM,CAAC;QACZ,OAAO,EAAE,SAAS,YAAY,EAAE,CAAC;QACjC,UAAU,EAAE,SAAS,MAAM,EAAE,CAAC;KAC/B,CAAC,CAAC;IACH,kFAAkF;IAClF,QAAQ,CAAC,EAAE,SAAS,SAAS,EAAE,CAAC;CACjC,GAAG,YAAY,CA6Bf;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,SAAS,YAAY,EAAE,GAAG,eAAe,CAOtF"}
|
|
@@ -1,18 +1,23 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* C4 evidence —
|
|
2
|
+
* C4 evidence — the pure read-side of proposing and verifying C4 containers.
|
|
3
3
|
*
|
|
4
|
-
* This module does not decide anything.
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
* This module does not decide anything. The proposing agent reads the
|
|
5
|
+
* subsystem diagrams, queries the existing containers, reasons, and writes
|
|
6
|
+
* its own rationale; what lives here is only the mechanical part of that
|
|
7
|
+
* loop:
|
|
8
8
|
*
|
|
9
|
-
* -
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* -
|
|
9
|
+
* - `proposeElements` — the proposal scaffold: skip keys containers
|
|
10
|
+
* already claim, skip boundaries no container can be discerned from
|
|
11
|
+
* (no runtime code behind them)
|
|
12
|
+
* - `verifyProcessBoundaries` — a boundary's verification status, read
|
|
13
|
+
* off the claiming container
|
|
14
|
+
* - `verificationIssues` — those statuses as dynamic-topology findings,
|
|
15
|
+
* so verification surfaces through the standard diagnostics pipeline
|
|
16
|
+
* - `findUnassigned` — runtime components no `process` names at all
|
|
13
17
|
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
18
|
+
* Judgements — is this the same unit under a different name? is one function
|
|
19
|
+
* a deployable unit? — belong to the agent and the human, never to a
|
|
20
|
+
* matcher.
|
|
16
21
|
*/
|
|
17
22
|
/** Constructs that execute at runtime — the only things a container can be. */
|
|
18
23
|
const RUNTIME_CONSTRUCTS = new Set(['function', 'class', 'custom_entity']);
|
|
@@ -24,105 +29,6 @@ function storageOf(m) {
|
|
|
24
29
|
export function isRuntimeConstruct(construct) {
|
|
25
30
|
return RUNTIME_CONSTRUCTS.has(construct);
|
|
26
31
|
}
|
|
27
|
-
/**
|
|
28
|
-
* Derive the questions worth asking about one candidate element.
|
|
29
|
-
*
|
|
30
|
-
* Pure over the rollup, so it runs identically against a single model, the 40
|
|
31
|
-
* composed models, or a fixture. Pure *questions* — it never proposes an
|
|
32
|
-
* answer, because the answers are architectural.
|
|
33
|
-
*/
|
|
34
|
-
export function deriveConcerns(key, members, tech = { frameworks: [] }) {
|
|
35
|
-
const out = [];
|
|
36
|
-
const aliases = members.map((m) => m.alias);
|
|
37
|
-
const runtime = members.filter((m) => isRuntimeConstruct(m.construct));
|
|
38
|
-
if (members.length === 1) {
|
|
39
|
-
out.push({
|
|
40
|
-
target: key,
|
|
41
|
-
kind: 'singleton',
|
|
42
|
-
message: `process "${key}" has exactly one member (${aliases[0]}) — a lone function is not a deployable unit`,
|
|
43
|
-
members: aliases,
|
|
44
|
-
});
|
|
45
|
-
}
|
|
46
|
-
// No member executes at runtime ⇒ this cannot be a deployable unit,
|
|
47
|
-
// regardless of how many members it has. A lone interface and a package of
|
|
48
|
-
// interfaces fail the same way.
|
|
49
|
-
if (members.length > 0 && runtime.length === 0) {
|
|
50
|
-
out.push({
|
|
51
|
-
target: key,
|
|
52
|
-
kind: 'library_shaped',
|
|
53
|
-
message: `process "${key}" has ${members.length} member(s) but none execute at runtime (${[...new Set(members.map((m) => m.construct))].join(', ')}) — this reads as a library, not a container`,
|
|
54
|
-
members: aliases,
|
|
55
|
-
});
|
|
56
|
-
}
|
|
57
|
-
if (runtime.length > 0 && tech.frameworks.length === 0) {
|
|
58
|
-
out.push({
|
|
59
|
-
target: key,
|
|
60
|
-
kind: 'missing_technology',
|
|
61
|
-
message: `container "${key}" has no framework signal — C4 requires a technology on every container`,
|
|
62
|
-
members: aliases,
|
|
63
|
-
});
|
|
64
|
-
}
|
|
65
|
-
const repos = new Set(members
|
|
66
|
-
.map((m) => (m.purl ?? '').split('#')[0])
|
|
67
|
-
.filter((r) => r && !r.startsWith('external:')));
|
|
68
|
-
if (repos.size > 1) {
|
|
69
|
-
out.push({
|
|
70
|
-
target: key,
|
|
71
|
-
kind: 'spans_repos',
|
|
72
|
-
message: `process "${key}" spans ${repos.size} repo keys (${[...repos].join(', ')}) — a C4 container cannot straddle a system boundary`,
|
|
73
|
-
members: aliases,
|
|
74
|
-
});
|
|
75
|
-
}
|
|
76
|
-
const unclassified = members.filter((m) => m.construct === 'store' && !storageOf(m));
|
|
77
|
-
if (unclassified.length > 0) {
|
|
78
|
-
out.push({
|
|
79
|
-
target: key,
|
|
80
|
-
kind: 'store_unclassified',
|
|
81
|
-
message: `${unclassified.length} store(s) in "${key}" declare no storage kind (${unclassified.map((m) => m.alias).join(', ')}) — decide whether each is a C4 data store or an internal detail`,
|
|
82
|
-
members: unclassified.map((m) => m.alias),
|
|
83
|
-
});
|
|
84
|
-
}
|
|
85
|
-
const unidentified = members.filter((m) => m.construct === 'external' && (!m.purl || m.purl === 'external' || m.purl.startsWith('external:proposed')));
|
|
86
|
-
if (unidentified.length > 0) {
|
|
87
|
-
out.push({
|
|
88
|
-
target: key,
|
|
89
|
-
kind: 'external_unidentified',
|
|
90
|
-
message: `${unidentified.length} external(s) have no resolvable identity (${unidentified.map((m) => m.alias).join(', ')}) — a C4 box named "external" is not a diagram`,
|
|
91
|
-
members: unidentified.map((m) => m.alias),
|
|
92
|
-
});
|
|
93
|
-
}
|
|
94
|
-
return out;
|
|
95
|
-
}
|
|
96
|
-
/**
|
|
97
|
-
* Suggest merges between keys that share a trailing role.
|
|
98
|
-
*
|
|
99
|
-
* `subsystems-studio/host` and `principal-studio/host` are the same role under
|
|
100
|
-
* two app names. This is a *question*, never a merge — only the user knows
|
|
101
|
-
* whether they are one process or two.
|
|
102
|
-
*/
|
|
103
|
-
export function suggestMerges(rollup) {
|
|
104
|
-
const byRole = new Map();
|
|
105
|
-
for (const { key } of rollup) {
|
|
106
|
-
const slash = key.lastIndexOf('/');
|
|
107
|
-
if (slash <= 0)
|
|
108
|
-
continue; // a bare key has no app/role shape
|
|
109
|
-
const role = key.slice(slash + 1);
|
|
110
|
-
const list = byRole.get(role) ?? [];
|
|
111
|
-
list.push(key);
|
|
112
|
-
byRole.set(role, list);
|
|
113
|
-
}
|
|
114
|
-
const out = [];
|
|
115
|
-
for (const [role, keys] of [...byRole].sort((a, b) => a[0].localeCompare(b[0]))) {
|
|
116
|
-
if (keys.length < 2)
|
|
117
|
-
continue;
|
|
118
|
-
out.push({
|
|
119
|
-
role,
|
|
120
|
-
keys: [...keys].sort(),
|
|
121
|
-
reason: `${keys.length} process keys end in "/${role}" (${[...keys].sort().join(', ')}) — one deployable unit under two names, or two units?`,
|
|
122
|
-
});
|
|
123
|
-
}
|
|
124
|
-
return out;
|
|
125
|
-
}
|
|
126
32
|
/**
|
|
127
33
|
* Runtime components that state no `process` at all.
|
|
128
34
|
*
|
|
@@ -134,69 +40,112 @@ export function findUnassigned(members) {
|
|
|
134
40
|
return members.filter((m) => !m.process && isRuntimeConstruct(m.construct));
|
|
135
41
|
}
|
|
136
42
|
/**
|
|
137
|
-
*
|
|
43
|
+
* Read a process boundary's verification off its container assignment.
|
|
138
44
|
*
|
|
139
|
-
*
|
|
140
|
-
*
|
|
141
|
-
*
|
|
45
|
+
* The claimant is the container whose `process` equals the boundary's key —
|
|
46
|
+
* a plain string match, no indirection. Two keys that are one deployable unit
|
|
47
|
+
* under two names are fixed in the model document (a consolidation proposal
|
|
48
|
+
* rewrites the components' `process` fields), so the reader never sees a
|
|
49
|
+
* discrepancy to resolve. When several containers claim one key (a
|
|
50
|
+
* hand-edited set can do this), the decided one wins: accepted over rejected
|
|
51
|
+
* over proposed, so one reviewer's yes outranks another's no.
|
|
142
52
|
*
|
|
143
|
-
*
|
|
144
|
-
*
|
|
145
|
-
* leave the box labelled after an almost-empty process and lose its confirmed
|
|
146
|
-
* technology. Pass `preferId` to override.
|
|
147
|
-
*
|
|
148
|
-
* Returns a new set; the input is untouched. Idempotent.
|
|
53
|
+
* Pure: the same rollup and elements always read the same. `rollup` accepts
|
|
54
|
+
* `SubsystemProcessRegion`s directly — only `key` and `memberAliases` are read.
|
|
149
55
|
*/
|
|
150
|
-
export function
|
|
151
|
-
const
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
n += memberCountByKey.get(k) ?? 0;
|
|
166
|
-
return n;
|
|
56
|
+
export function verifyProcessBoundaries(options) {
|
|
57
|
+
const byKey = new Map();
|
|
58
|
+
for (const e of options.elements) {
|
|
59
|
+
if (e.kind !== 'container' || !e.process)
|
|
60
|
+
continue;
|
|
61
|
+
const list = byKey.get(e.process) ?? [];
|
|
62
|
+
list.push(e);
|
|
63
|
+
byKey.set(e.process, list);
|
|
64
|
+
}
|
|
65
|
+
const precedence = { accepted: 0, rejected: 1, proposed: 2 };
|
|
66
|
+
return options.rollup.map(({ key, memberAliases }) => {
|
|
67
|
+
const aliases = [...(memberAliases ?? [])];
|
|
68
|
+
const claimants = byKey.get(key) ?? [];
|
|
69
|
+
if (claimants.length === 0) {
|
|
70
|
+
return { key, memberAliases: aliases, status: 'unassigned' };
|
|
167
71
|
}
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
const absorbed = involved.filter((e) => e.id !== survivor.id);
|
|
173
|
-
const absorbedIds = new Set(absorbed.map((e) => e.id));
|
|
174
|
-
const merged = {
|
|
175
|
-
...survivor,
|
|
176
|
-
members: [...new Set([...survivor.members, ...absorbed.flatMap((e) => e.members)])],
|
|
177
|
-
derivedFrom: [
|
|
178
|
-
...new Set([...(survivor.derivedFrom ?? []), ...absorbed.flatMap((e) => e.derivedFrom ?? [])]),
|
|
179
|
-
],
|
|
180
|
-
rationale: [survivor.rationale, ...absorbed.map((e) => e.rationale)].filter(Boolean).join(' | ') ||
|
|
181
|
-
undefined,
|
|
182
|
-
};
|
|
183
|
-
return [...elements.filter((e) => e.id !== survivor.id && !absorbedIds.has(e.id)), merged].sort((a, b) => a.id.localeCompare(b.id));
|
|
72
|
+
const decided = [...claimants].sort((a, b) => precedence[a.state] - precedence[b.state] || a.id.localeCompare(b.id))[0];
|
|
73
|
+
const status = decided.state === 'accepted' ? 'verified' : decided.state;
|
|
74
|
+
return { key, memberAliases: aliases, status, element: decided };
|
|
75
|
+
});
|
|
184
76
|
}
|
|
185
77
|
/**
|
|
186
|
-
*
|
|
187
|
-
*
|
|
78
|
+
* Verification statuses as diagnostics findings — the dynamic-topology layer
|
|
79
|
+
* of the issues list. One finding per boundary that is NOT verified: an
|
|
80
|
+
* unclaimed boundary is the absence a proposing agent run resolves, a
|
|
81
|
+
* `proposed` one awaits the human's decision, a `rejected` one records that
|
|
82
|
+
* the decision was no. `verified` reports nothing — the audit reports
|
|
83
|
+
* absence only, and silence is the good state.
|
|
188
84
|
*
|
|
189
|
-
*
|
|
190
|
-
*
|
|
85
|
+
* The findings ride the existing pipeline untouched: they badge the process
|
|
86
|
+
* frame (target kind `process`), list under Dynamic topology, focus their
|
|
87
|
+
* members on click, and carry the fix the decision needs. Pure over its
|
|
88
|
+
* input, like the rest of this module.
|
|
89
|
+
*/
|
|
90
|
+
export function verificationIssues(statuses) {
|
|
91
|
+
const out = [];
|
|
92
|
+
for (const { key, status, element } of statuses) {
|
|
93
|
+
if (status === 'verified')
|
|
94
|
+
continue;
|
|
95
|
+
const label = element?.label ?? key;
|
|
96
|
+
out.push({
|
|
97
|
+
id: `process-verification:${key}`,
|
|
98
|
+
severity: 'info',
|
|
99
|
+
kind: `boundary_process_${status}`,
|
|
100
|
+
message: status === 'unassigned'
|
|
101
|
+
? `No C4 container claims this process boundary yet — a proposing agent run would scaffold one.`
|
|
102
|
+
: status === 'proposed'
|
|
103
|
+
? `Container "${label}" claims this boundary and awaits a decision.`
|
|
104
|
+
: `Container "${label}" was rejected for this boundary — it stays unclaimed until a container is accepted.`,
|
|
105
|
+
target: { kind: 'process', id: key, label: key },
|
|
106
|
+
fix: status === 'unassigned'
|
|
107
|
+
? { label: 'Propose container' }
|
|
108
|
+
: status === 'proposed'
|
|
109
|
+
? { label: 'Accept container' }
|
|
110
|
+
: undefined,
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
return out;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* A starting point for an agent, shaped by the two questions that matter when
|
|
117
|
+
* assessing a process boundary:
|
|
118
|
+
*
|
|
119
|
+
* 1. **Does a container already exist for it?** (`elements`) — a key one
|
|
120
|
+
* already claims is never re-proposed; proposing another would be
|
|
121
|
+
* exactly the duplicate this module exists to prevent. The exact claim
|
|
122
|
+
* is a fact and skipped here; whether a *similarly named* container is
|
|
123
|
+
* the same unit is a judgement, so it belongs to the agent querying the
|
|
124
|
+
* container list — the fix there is a consolidation proposal, not a
|
|
125
|
+
* second box.
|
|
126
|
+
* 2. **Can the boundary be discerned into a container?** — code that never
|
|
127
|
+
* runs cannot be, so a library-shaped group gets no proposal; that
|
|
128
|
+
* fact is the agent's to report, not the scaffold's.
|
|
129
|
+
*
|
|
130
|
+
* For boundaries passing both, one `proposed` container per key. The
|
|
131
|
+
* rationale is the proposing agent's own reasoning, written on the proposal
|
|
132
|
+
* it POSTs — this scaffold does not fabricate one. A scaffold to edit and
|
|
133
|
+
* argue with — not an answer, and never `accepted`.
|
|
191
134
|
*/
|
|
192
135
|
export function proposeElements(options) {
|
|
193
|
-
const
|
|
194
|
-
|
|
195
|
-
|
|
136
|
+
const existing = options.elements ?? [];
|
|
137
|
+
const claimed = new Set(existing.flatMap((e) => (e.kind === 'container' && e.process ? [e.process] : [])));
|
|
138
|
+
const elements = [];
|
|
139
|
+
for (const { key, members, frameworks } of options.rollup) {
|
|
140
|
+
if (claimed.has(key))
|
|
141
|
+
continue; // already covered — exact claim, a fact
|
|
142
|
+
const runtime = members.filter((m) => isRuntimeConstruct(m.construct));
|
|
143
|
+
if (members.length > 0 && runtime.length === 0)
|
|
144
|
+
continue; // not discernible: code that never runs
|
|
145
|
+
elements.push({
|
|
196
146
|
id: `container:${key}`,
|
|
197
147
|
kind: 'container',
|
|
198
|
-
|
|
199
|
-
derivedFrom: [key],
|
|
148
|
+
process: key,
|
|
200
149
|
label: key,
|
|
201
150
|
containerKind: suggestContainerKind(members),
|
|
202
151
|
// C4 requires a technology on every container. With no framework signal
|
|
@@ -205,10 +154,8 @@ export function proposeElements(options) {
|
|
|
205
154
|
// module stopped doing.
|
|
206
155
|
technology: frameworks.length > 0 ? frameworks.join(' + ') : '',
|
|
207
156
|
state: 'proposed',
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
};
|
|
211
|
-
});
|
|
157
|
+
});
|
|
158
|
+
}
|
|
212
159
|
return {
|
|
213
160
|
repoKey: options.repoKey,
|
|
214
161
|
elements: elements.sort((a, b) => a.id.localeCompare(b.id)),
|