@principal-ai/subsystems-react 0.53.6 → 0.54.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (104) hide show
  1. package/dist/index.d.ts +8 -2
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js +3 -0
  4. package/dist/index.js.map +1 -1
  5. package/dist/pierre/PierreTrailCodeView.d.ts.map +1 -1
  6. package/dist/pierre/PierreTrailCodeView.js +7 -5
  7. package/dist/pierre/PierreTrailCodeView.js.map +1 -1
  8. package/dist/stories/Subsystem/ComponentGraph/fixtures.d.ts.map +1 -1
  9. package/dist/stories/Subsystem/ComponentGraph/fixtures.js +1 -2
  10. package/dist/stories/Subsystem/ComponentGraph/fixtures.js.map +1 -1
  11. package/dist/subsystem/C4Graph.d.ts +144 -9
  12. package/dist/subsystem/C4Graph.d.ts.map +1 -1
  13. package/dist/subsystem/C4Graph.js +924 -148
  14. package/dist/subsystem/C4Graph.js.map +1 -1
  15. package/dist/subsystem/C4NodeCard.d.ts +22 -1
  16. package/dist/subsystem/C4NodeCard.d.ts.map +1 -1
  17. package/dist/subsystem/C4NodeCard.js +10 -2
  18. package/dist/subsystem/C4NodeCard.js.map +1 -1
  19. package/dist/subsystem/ComponentDeclaration.d.ts +5 -4
  20. package/dist/subsystem/ComponentDeclaration.d.ts.map +1 -1
  21. package/dist/subsystem/ComponentDeclaration.js +12 -4
  22. package/dist/subsystem/ComponentDeclaration.js.map +1 -1
  23. package/dist/subsystem/ConstructsCatalog.d.ts.map +1 -1
  24. package/dist/subsystem/ConstructsCatalog.js +10 -7
  25. package/dist/subsystem/ConstructsCatalog.js.map +1 -1
  26. package/dist/subsystem/GraphSidebar.d.ts +71 -0
  27. package/dist/subsystem/GraphSidebar.d.ts.map +1 -0
  28. package/dist/subsystem/GraphSidebar.js +145 -0
  29. package/dist/subsystem/GraphSidebar.js.map +1 -0
  30. package/dist/subsystem/GraphWithSidebar.d.ts +24 -0
  31. package/dist/subsystem/GraphWithSidebar.d.ts.map +1 -0
  32. package/dist/subsystem/GraphWithSidebar.js +25 -0
  33. package/dist/subsystem/GraphWithSidebar.js.map +1 -0
  34. package/dist/subsystem/SubsystemComponentGraph.d.ts +33 -0
  35. package/dist/subsystem/SubsystemComponentGraph.d.ts.map +1 -1
  36. package/dist/subsystem/SubsystemComponentGraph.js +364 -411
  37. package/dist/subsystem/SubsystemComponentGraph.js.map +1 -1
  38. package/dist/subsystem/TrailStepsNav.d.ts +48 -0
  39. package/dist/subsystem/TrailStepsNav.d.ts.map +1 -0
  40. package/dist/subsystem/TrailStepsNav.js +189 -0
  41. package/dist/subsystem/TrailStepsNav.js.map +1 -0
  42. package/dist/subsystem/TrailsPanel.d.ts.map +1 -1
  43. package/dist/subsystem/TrailsPanel.js +3 -4
  44. package/dist/subsystem/TrailsPanel.js.map +1 -1
  45. package/dist/subsystem/__fixtures__/c4Drilldown.d.ts +151 -0
  46. package/dist/subsystem/__fixtures__/c4Drilldown.d.ts.map +1 -0
  47. package/dist/subsystem/__fixtures__/c4Drilldown.js +783 -0
  48. package/dist/subsystem/__fixtures__/c4Drilldown.js.map +1 -0
  49. package/dist/subsystem/c4.d.ts +39 -8
  50. package/dist/subsystem/c4.d.ts.map +1 -1
  51. package/dist/subsystem/c4.js.map +1 -1
  52. package/dist/subsystem/declarationRef.d.ts +26 -18
  53. package/dist/subsystem/declarationRef.d.ts.map +1 -1
  54. package/dist/subsystem/declarationRef.js +9 -4
  55. package/dist/subsystem/declarationRef.js.map +1 -1
  56. package/dist/subsystem/model.d.ts +19 -1
  57. package/dist/subsystem/model.d.ts.map +1 -1
  58. package/dist/subsystem/model.js +12 -2
  59. package/dist/subsystem/model.js.map +1 -1
  60. package/dist/subsystem/nodes.d.ts.map +1 -1
  61. package/dist/subsystem/nodes.js +24 -4
  62. package/dist/subsystem/nodes.js.map +1 -1
  63. package/dist/subsystem/trailBrief.d.ts.map +1 -1
  64. package/dist/subsystem/trailBrief.js +6 -1
  65. package/dist/subsystem/trailBrief.js.map +1 -1
  66. package/package.json +3 -3
  67. package/src/index.ts +9 -2
  68. package/src/pierre/PierreTrailCodeView.tsx +9 -13
  69. package/src/stories/Subsystem/C4Graph/C4ToComponentDrill.stories.tsx +496 -0
  70. package/src/stories/Subsystem/ComponentGraph/AnimatedProgression.stories.tsx +7 -7
  71. package/src/stories/Subsystem/ComponentGraph/Constructs.stories.tsx +19 -25
  72. package/src/stories/Subsystem/ComponentGraph/DetailPanel.stories.tsx +8 -10
  73. package/src/stories/Subsystem/ComponentGraph/Issues.stories.tsx +19 -19
  74. package/src/stories/Subsystem/ComponentGraph/ProposedTrails.stories.tsx +6 -6
  75. package/src/stories/Subsystem/ComponentGraph/TrailAutoplay.stories.tsx +7 -7
  76. package/src/stories/Subsystem/ComponentGraph/Trails.stories.tsx +27 -30
  77. package/src/stories/Subsystem/ComponentGraph/fixtures.ts +1 -2
  78. package/src/stories/Subsystem/ConstructsCatalog.stories.tsx +10 -13
  79. package/src/stories/Subsystem/EgoGraph/EgoGraph.stories.tsx +3 -4
  80. package/src/stories/Subsystem/Sidebar/GraphSidebar.stories.tsx +206 -0
  81. package/src/subsystem/C4Graph.groups.test.ts +96 -2
  82. package/src/subsystem/C4Graph.tsx +1377 -165
  83. package/src/subsystem/C4NodeCard.test.ts +1 -2
  84. package/src/subsystem/C4NodeCard.tsx +24 -2
  85. package/src/subsystem/ComponentDeclaration.tsx +18 -8
  86. package/src/subsystem/ConstructsCatalog.tsx +23 -8
  87. package/src/subsystem/GraphSidebar.test.tsx +152 -0
  88. package/src/subsystem/GraphSidebar.tsx +300 -0
  89. package/src/subsystem/GraphWithSidebar.test.tsx +78 -0
  90. package/src/subsystem/GraphWithSidebar.tsx +60 -0
  91. package/src/subsystem/SubsystemComponentGraph.tsx +284 -370
  92. package/src/subsystem/TrailStepsNav.test.tsx +179 -0
  93. package/src/subsystem/TrailStepsNav.tsx +372 -0
  94. package/src/subsystem/TrailsPanel.tsx +3 -4
  95. package/src/subsystem/__fixtures__/c4Drilldown.test.ts +338 -0
  96. package/src/subsystem/__fixtures__/c4Drilldown.ts +847 -0
  97. package/src/subsystem/c4.ts +41 -8
  98. package/src/subsystem/declarationRef.ts +28 -19
  99. package/src/subsystem/model.test.ts +32 -12
  100. package/src/subsystem/model.ts +30 -1
  101. package/src/subsystem/nodes.group.test.tsx +82 -0
  102. package/src/subsystem/nodes.tsx +30 -2
  103. package/src/subsystem/trailBrief.test.ts +2 -2
  104. package/src/subsystem/trailBrief.ts +6 -1
@@ -0,0 +1,847 @@
1
+ /**
2
+ * C4 → component-graph drill-down fixture — grounded in a real, reviewed
3
+ * subsystem model.
4
+ *
5
+ * The document half is `sg-1788913589907-ieokymqg9` "Gist share from Studio"
6
+ * — a verified model for THIS repo (`principal-ai/subsystem-modeling`, 19
7
+ * components, 15/15 verified claims, 7 walkthroughs checked) from
8
+ * `~/.principal/subsystem-models`. Components, trails, and step sites are the
9
+ * model's own, not invented.
10
+ *
11
+ * The C4 half is the "assume they exist" part: a plausible accepted element
12
+ * set built ON TOP of that model — four containers claiming its real process
13
+ * keys, two externals (GitHub Gists API, the shared disk store), and authored
14
+ * Level-3 groupings of real aliases. Everything the C4 layer claims is
15
+ * checkable against the document, and `c4Drilldown.test.ts` asserts it:
16
+ *
17
+ * - container `process` claims exist in the doc, and membership resolves by
18
+ * exact key match (`getSubsystemProcessRegions`-style);
19
+ * - externals carry their one alias; authored components group only real
20
+ * aliases;
21
+ * - the container-level edges are EXACTLY the fold of cross-boundary trail
22
+ * steps (`rollupEdgeTrailRefs`), which is the spec for the eventual
23
+ * `buildC4Model` rollup;
24
+ * - the library processes (`subsystems-core/types`, `subsystems-react/ui`)
25
+ * and the proposed process (`proposed/studio-gist`) are claimed by no
26
+ * container — the anti-"library" stance, applied to the fixture itself.
27
+ *
28
+ * Note what the accepted fold leaves out on purpose: the renderer container
29
+ * has no incident edges (only the *proposed* trail touches it) and the CLI
30
+ * never writes the store in a trail (the store's description says it *can*,
31
+ * but no trail does it). Both absences are honest.
32
+ */
33
+
34
+ import type {
35
+ C4Component,
36
+ C4Container,
37
+ C4Edge,
38
+ C4Element,
39
+ C4ExternalSystem,
40
+ C4Model,
41
+ C4TrailRef,
42
+ } from '../c4';
43
+ import type { SubsystemComponent, SubsystemTrail } from '../model';
44
+
45
+ /** The real model this fixture is grounded in. */
46
+ export const DRILLDOWN_MODEL_SOURCE = {
47
+ /** `~/.principal/subsystem-models/sg-1788913589907-ieokymqg9.json` */
48
+ id: 'sg-1788913589907-ieokymqg9',
49
+ title: 'Gist share from Studio (local store + host gist ref)',
50
+ repo: 'pkg:github/principal-ai/subsystem-modeling',
51
+ repoRoot: '/Users/griever/Developer/principal-ai/subsystem-modeling',
52
+ verifiedCount: 15,
53
+ } as const;
54
+
55
+ export const DRILLDOWN_PURL = 'pkg:github/principal-ai/subsystem-modeling';
56
+
57
+ /** The system boundary. */
58
+ export const SYSTEM = {
59
+ id: 'system:studio',
60
+ label: 'Subsystem Modeling',
61
+ repoKey: DRILLDOWN_PURL,
62
+ } as const;
63
+
64
+ /** `container:studio-host` — claims process key `subsystems-studio/host`. */
65
+ export const HOST = 'container:studio-host';
66
+ /** `container:studio-renderer` — claims `subsystems-studio/renderer`. */
67
+ export const RENDERER = 'container:studio-renderer';
68
+ /** `container:principal-cli` — claims `principal-studio-cli`. */
69
+ export const CLI = 'container:principal-cli';
70
+ /** `container:site-web` — claims `site/web`. */
71
+ export const SITE = 'container:site-web';
72
+ /** `external:github` — GitHub Gists API. */
73
+ export const GITHUB = 'external:github';
74
+ /** `external:disk-store` — the shared `~/.principal/subsystem-models` store. */
75
+ export const DISK = 'external:disk-store';
76
+
77
+ const stepPurl = (file: string) => `${DRILLDOWN_PURL}#${file}`;
78
+
79
+ // ---------------------------------------------------------------------------
80
+ // The subsystem model document — verbatim from sg-1788913589907-ieokymqg9.
81
+ // ---------------------------------------------------------------------------
82
+
83
+ export const DRILLDOWN_COMPONENTS: SubsystemComponent[] = [
84
+ {
85
+ alias: 'site-gist-page',
86
+ name: 'Gist',
87
+ construct: 'function',
88
+ role: 'entry',
89
+ file: 'site/src/pages/Gist.tsx',
90
+ purl: stepPurl('site/src/pages/Gist.tsx'),
91
+ symbol: 'Gist',
92
+ purpose: 'Consumer surface: open a public gist URL and render the portable model.',
93
+ process: 'site/web',
94
+ layer: 0,
95
+ },
96
+ {
97
+ alias: 'gist-load',
98
+ name: 'loadSubsystemModelFromGist',
99
+ construct: 'function',
100
+ file: 'site/src/lib/gist.ts',
101
+ purl: stepPurl('site/src/lib/gist.ts'),
102
+ symbol: 'loadSubsystemModelFromGist',
103
+ purpose: 'Fetch and parse a public gist into a portable document (site viewer today).',
104
+ process: 'site/web',
105
+ layer: 1,
106
+ },
107
+ {
108
+ alias: 'to-portable',
109
+ name: 'toPortableDocument',
110
+ construct: 'function',
111
+ file: 'site/src/lib/gist.ts',
112
+ purl: stepPurl('site/src/lib/gist.ts'),
113
+ symbol: 'toPortableDocument',
114
+ purpose: 'Keep only shareable fields — what a gist file must contain.',
115
+ process: 'site/web',
116
+ layer: 2,
117
+ },
118
+ {
119
+ alias: 'is-portable',
120
+ name: 'isSubsystemModelDocument',
121
+ construct: 'function',
122
+ file: 'packages/subsystems-core/src/types/subsystem-model.ts',
123
+ purl: stepPurl('packages/subsystems-core/src/types/subsystem-model.ts'),
124
+ symbol: 'isSubsystemModelDocument',
125
+ purpose: 'Shallow-guard title + components[] + edges[].',
126
+ process: 'subsystems-core/types',
127
+ layer: 2,
128
+ },
129
+ {
130
+ alias: 'portable-doc',
131
+ name: 'SubsystemModelDocument',
132
+ construct: 'interface',
133
+ file: 'packages/subsystems-core/src/types/subsystem-model.ts',
134
+ purl: stepPurl('packages/subsystems-core/src/types/subsystem-model.ts'),
135
+ symbol: 'SubsystemModelDocument',
136
+ purpose: 'Shareable standard — no host paths, store ids, or gist provenance.',
137
+ process: 'subsystems-core/types',
138
+ layer: 3,
139
+ },
140
+ {
141
+ alias: 'host-binding',
142
+ name: 'SubsystemModelHydrated',
143
+ construct: 'interface',
144
+ file: 'packages/subsystems-core/src/types/subsystem-model.ts',
145
+ purl: stepPurl('packages/subsystems-core/src/types/subsystem-model.ts'),
146
+ symbol: 'SubsystemModelHydrated',
147
+ purpose: 'Machine envelope layered onto a portable document for local Studio use.',
148
+ process: 'subsystems-core/types',
149
+ layer: 3,
150
+ },
151
+ {
152
+ alias: 'github-gists-api',
153
+ name: 'GitHub Gists API',
154
+ construct: 'external',
155
+ role: 'service',
156
+ file: '',
157
+ purl: 'external:api.github.com/gists',
158
+ purpose: 'Create or update a public gist that holds the portable JSON.',
159
+ process: 'external/github',
160
+ layer: 2,
161
+ declaration: {
162
+ kind: 'external',
163
+ attributes: [
164
+ { key: 'read', value: 'GET /gists/{id}' },
165
+ { key: 'create', value: 'POST /gists' },
166
+ { key: 'update', value: 'PATCH /gists/{id}' },
167
+ ],
168
+ },
169
+ },
170
+ {
171
+ alias: 'react-graph',
172
+ name: 'SubsystemComponentGraph',
173
+ construct: 'function',
174
+ file: 'packages/subsystems-react/src/subsystem/SubsystemComponentGraph.tsx',
175
+ purl: stepPurl('packages/subsystems-react/src/subsystem/SubsystemComponentGraph.tsx'),
176
+ symbol: 'SubsystemComponentGraph',
177
+ purpose: 'Shared graph viewer for site gist pages and Studio model tabs.',
178
+ process: 'subsystems-react/ui',
179
+ layer: 1,
180
+ },
181
+ {
182
+ alias: 'cli-create',
183
+ name: 'createAction',
184
+ construct: 'function',
185
+ role: 'entry',
186
+ file: 'packages/principal-studio-cli/src/commands/subsystem-model.ts',
187
+ purl: stepPurl('packages/principal-studio-cli/src/commands/subsystem-model.ts'),
188
+ symbol: 'createAction',
189
+ purpose: 'Author path: validate payload and persist a new local sg- id.',
190
+ process: 'principal-studio-cli',
191
+ layer: 0,
192
+ },
193
+ {
194
+ alias: 'studio-create',
195
+ name: 'createSubsystemModel',
196
+ construct: 'function',
197
+ file: 'packages/subsystems-studio/src/bun/subsystem-model-store.ts',
198
+ purl: stepPurl('packages/subsystems-studio/src/bun/subsystem-model-store.ts'),
199
+ symbol: 'createSubsystemModel',
200
+ purpose: 'Mint sg- id and write the hydrated record into the disk store.',
201
+ process: 'subsystems-studio/host',
202
+ layer: 1,
203
+ },
204
+ {
205
+ alias: 'studio-update',
206
+ name: 'updateSubsystemModel',
207
+ construct: 'function',
208
+ file: 'packages/subsystems-studio/src/bun/subsystem-model-store.ts',
209
+ purl: stepPurl('packages/subsystems-studio/src/bun/subsystem-model-store.ts'),
210
+ symbol: 'updateSubsystemModel',
211
+ purpose: 'Patch a local record in place — used to stamp or refresh the host gist ref.',
212
+ process: 'subsystems-studio/host',
213
+ layer: 1,
214
+ },
215
+ {
216
+ alias: 'studio-get',
217
+ name: 'getSubsystemModel',
218
+ construct: 'function',
219
+ file: 'packages/subsystems-studio/src/bun/subsystem-model-store.ts',
220
+ purl: stepPurl('packages/subsystems-studio/src/bun/subsystem-model-store.ts'),
221
+ symbol: 'getSubsystemModel',
222
+ purpose: 'Read one hydrated record from the disk store by sg- id.',
223
+ process: 'subsystems-studio/host',
224
+ layer: 1,
225
+ },
226
+ {
227
+ alias: 'model-disk-store',
228
+ name: '~/.principal/subsystem-models',
229
+ construct: 'store',
230
+ file: '',
231
+ purl: 'external:file:~/.principal/subsystem-models',
232
+ purpose:
233
+ 'Shared on-disk store (<id>.json + _index.json). Studio host and the CLI both read/write here — the directory is not owned by either process.',
234
+ process: 'local/disk',
235
+ layer: 2,
236
+ declaration: { kind: 'store', properties: [] },
237
+ },
238
+ {
239
+ alias: 'index-entry',
240
+ name: 'indexEntryFor',
241
+ construct: 'function',
242
+ file: 'packages/subsystems-studio/src/bun/subsystem-model-store.ts',
243
+ purl: stepPurl('packages/subsystems-studio/src/bun/subsystem-model-store.ts'),
244
+ symbol: 'indexEntryFor',
245
+ purpose: 'Build the lightweight listing row written into _index.json.',
246
+ process: 'subsystems-studio/host',
247
+ layer: 2,
248
+ },
249
+ {
250
+ alias: 'studio-list',
251
+ name: 'SubsystemModelsView',
252
+ construct: 'function',
253
+ role: 'entry',
254
+ file: 'packages/subsystems-studio/src/mainview/views/SubsystemModelsView.tsx',
255
+ purl: stepPurl('packages/subsystems-studio/src/mainview/views/SubsystemModelsView.tsx'),
256
+ symbol: 'SubsystemModelsView',
257
+ purpose: 'Studio list of local models — one place a Share as gist action can live.',
258
+ process: 'subsystems-studio/renderer',
259
+ layer: 0,
260
+ },
261
+ {
262
+ alias: 'studio-model-view',
263
+ name: 'SubsystemModelView',
264
+ construct: 'function',
265
+ role: 'entry',
266
+ file: 'packages/subsystems-studio/src/mainview/views/SubsystemModelView.tsx',
267
+ purl: stepPurl('packages/subsystems-studio/src/mainview/views/SubsystemModelView.tsx'),
268
+ symbol: 'SubsystemModelView',
269
+ purpose: 'Open model tab chrome — alternate home for a per-model Gist button.',
270
+ process: 'subsystems-studio/renderer',
271
+ layer: 0,
272
+ },
273
+ {
274
+ alias: 'open-tab',
275
+ name: 'openSubsystemModelTab',
276
+ construct: 'function',
277
+ file: 'packages/subsystems-studio/src/bun/index.ts',
278
+ purl: stepPurl('packages/subsystems-studio/src/bun/index.ts'),
279
+ symbol: 'openSubsystemModelTab',
280
+ purpose: 'Focus or create a subsystem-model tab for a stored sg- id.',
281
+ process: 'subsystems-studio/host',
282
+ layer: 1,
283
+ },
284
+ {
285
+ alias: 'gist-share-ui',
286
+ name: 'StudioGistShare',
287
+ construct: 'external',
288
+ role: 'entry',
289
+ file: '',
290
+ purl: 'external:proposed',
291
+ purpose: 'Proposed Gist button: publish the local model as a gist, or update the linked one.',
292
+ process: 'proposed/studio-gist',
293
+ layer: 0,
294
+ declaration: {
295
+ kind: 'external',
296
+ attributes: [
297
+ { key: 'surface', value: 'SubsystemModelsView or SubsystemModelView' },
298
+ { key: 'action', value: 'Share as gist / Update gist' },
299
+ { key: 'auth', value: 'GitHub token required for write' },
300
+ ],
301
+ },
302
+ },
303
+ {
304
+ alias: 'gist-ref',
305
+ name: 'StoredGistRef',
306
+ construct: 'external',
307
+ file: '',
308
+ purl: 'external:proposed',
309
+ purpose:
310
+ 'Host-only gist id (+ fileName) on the stored record and index so share updates the same gist.',
311
+ process: 'proposed/studio-gist',
312
+ layer: 2,
313
+ declaration: {
314
+ kind: 'external',
315
+ attributes: [
316
+ { key: 'shape', value: 'gist?: { id, fileName? }' },
317
+ { key: 'livesOn', value: 'StoredSubsystemModel + _index entry' },
318
+ { key: 'notIn', value: 'portable SubsystemModelDocument' },
319
+ ],
320
+ },
321
+ },
322
+ ];
323
+
324
+ export const DRILLDOWN_TRAILS: SubsystemTrail[] = [
325
+ {
326
+ id: 'tl-site-view-gist',
327
+ title: 'Someone opens the shared gist on the site',
328
+ steps: [
329
+ {
330
+ from: 'site-gist-page',
331
+ to: 'gist-load',
332
+ mechanism: 'calls',
333
+ file: 'site/src/pages/Gist.tsx',
334
+ span: { startLine: 67, endLine: 67, contentHash: 'fixture' },
335
+ symbol: 'Gist.load',
336
+ annotation: 'Consumer lands on /gist?gist=... — the end of a Studio share.',
337
+ purl: stepPurl('site/src/pages/Gist.tsx'),
338
+ },
339
+ {
340
+ from: 'gist-load',
341
+ to: 'github-gists-api',
342
+ mechanism: 'calls',
343
+ file: 'site/src/lib/gist.ts',
344
+ span: { startLine: 94, endLine: 94, contentHash: 'fixture' },
345
+ symbol: 'loadSubsystemModelFromGist',
346
+ annotation: 'Read the public gist JSON (no Studio disk involved).',
347
+ purl: stepPurl('site/src/lib/gist.ts'),
348
+ },
349
+ {
350
+ from: 'gist-load',
351
+ to: 'to-portable',
352
+ mechanism: 'calls',
353
+ file: 'site/src/lib/gist.ts',
354
+ span: { startLine: 154, endLine: 154, contentHash: 'fixture' },
355
+ symbol: 'loadSubsystemModelFromGist',
356
+ annotation: 'Only portable fields are shown — host gist refs never travel in the file.',
357
+ purl: stepPurl('site/src/lib/gist.ts'),
358
+ },
359
+ {
360
+ from: 'site-gist-page',
361
+ to: 'react-graph',
362
+ mechanism: 'uses',
363
+ file: 'site/src/pages/Gist.tsx',
364
+ span: { startLine: 184, endLine: 184, contentHash: 'fixture' },
365
+ symbol: 'Gist',
366
+ annotation: 'Render the shared model in the browser.',
367
+ purl: stepPurl('site/src/pages/Gist.tsx'),
368
+ },
369
+ ],
370
+ },
371
+ {
372
+ id: 'tl-persist-local-store',
373
+ title: 'Persist a local model to ~/.principal/subsystem-models',
374
+ steps: [
375
+ {
376
+ from: 'cli-create',
377
+ to: 'studio-create',
378
+ mechanism: 'calls',
379
+ file: 'packages/principal-studio-cli/src/commands/subsystem-model.ts',
380
+ span: { startLine: 186, endLine: 186, contentHash: 'fixture' },
381
+ symbol: 'createAction',
382
+ annotation: 'CLI/agent create path — always a new local sg- id today.',
383
+ purl: stepPurl('packages/principal-studio-cli/src/commands/subsystem-model.ts'),
384
+ },
385
+ {
386
+ from: 'studio-create',
387
+ to: 'model-disk-store',
388
+ mechanism: 'writes',
389
+ file: 'packages/subsystems-studio/src/bun/subsystem-model-store.ts',
390
+ span: { startLine: 929, endLine: 929, contentHash: 'fixture' },
391
+ symbol: 'createSubsystemModel',
392
+ annotation:
393
+ 'Host create writes a record into the shared ~/.principal/subsystem-models directory (CLI can write the same store).',
394
+ purl: stepPurl('packages/subsystems-studio/src/bun/subsystem-model-store.ts'),
395
+ },
396
+ {
397
+ from: 'studio-create',
398
+ to: 'index-entry',
399
+ mechanism: 'writes',
400
+ file: 'packages/subsystems-studio/src/bun/subsystem-model-store.ts',
401
+ span: { startLine: 951, endLine: 951, contentHash: 'fixture' },
402
+ symbol: 'createSubsystemModel',
403
+ annotation: 'Mirror listing fields into _index.json.',
404
+ purl: stepPurl('packages/subsystems-studio/src/bun/subsystem-model-store.ts'),
405
+ },
406
+ {
407
+ from: 'cli-create',
408
+ to: 'open-tab',
409
+ mechanism: 'calls',
410
+ file: 'packages/principal-studio-cli/src/commands/subsystem-model.ts',
411
+ span: { startLine: 205, endLine: 205, contentHash: 'fixture' },
412
+ symbol: 'createAction',
413
+ annotation: 'Open the local model in Studio.',
414
+ purl: stepPurl('packages/principal-studio-cli/src/commands/subsystem-model.ts'),
415
+ },
416
+ ],
417
+ },
418
+ {
419
+ id: 'tl-proposed-share-gist',
420
+ title: 'Proposed: share local model as gist (create or update)',
421
+ steps: [
422
+ {
423
+ from: 'gist-share-ui',
424
+ to: 'studio-get',
425
+ mechanism: 'calls',
426
+ file: 'packages/subsystems-studio/src/bun/subsystem-model-store.ts',
427
+ span: { startLine: 919, endLine: 919, contentHash: 'fixture' },
428
+ symbol: 'getSubsystemModel',
429
+ annotation: 'Load the hydrated local record from the disk store.',
430
+ purl: 'external:proposed#packages/subsystems-studio/src/bun/subsystem-model-store.ts',
431
+ },
432
+ {
433
+ from: 'gist-share-ui',
434
+ to: 'to-portable',
435
+ mechanism: 'calls',
436
+ file: 'site/src/lib/gist.ts',
437
+ span: { startLine: 161, endLine: 161, contentHash: 'fixture' },
438
+ symbol: 'toPortableDocument',
439
+ annotation: 'Strip host fields so the gist file is portable-only.',
440
+ purl: 'external:proposed#site/src/lib/gist.ts',
441
+ },
442
+ {
443
+ from: 'index-entry',
444
+ to: 'gist-ref',
445
+ mechanism: 'reads',
446
+ file: 'packages/subsystems-studio/src/bun/subsystem-model-store.ts',
447
+ span: { startLine: 849, endLine: 849, contentHash: 'fixture' },
448
+ symbol: 'indexEntryFor',
449
+ annotation: 'Index/host gist ref decides PATCH existing gist vs POST a new one.',
450
+ purl: stepPurl('packages/subsystems-studio/src/bun/subsystem-model-store.ts'),
451
+ },
452
+ {
453
+ from: 'gist-share-ui',
454
+ to: 'github-gists-api',
455
+ mechanism: 'calls',
456
+ file: 'site/src/lib/gist.ts',
457
+ span: { startLine: 94, endLine: 94, contentHash: 'fixture' },
458
+ symbol: 'loadSubsystemModelFromGist',
459
+ annotation: 'POST /gists (first share) or PATCH /gists/{id} (subsequent) — token required.',
460
+ purl: 'external:proposed#site/src/lib/gist.ts',
461
+ },
462
+ {
463
+ from: 'gist-share-ui',
464
+ to: 'studio-update',
465
+ mechanism: 'calls',
466
+ file: 'packages/subsystems-studio/src/bun/subsystem-model-store.ts',
467
+ span: { startLine: 957, endLine: 957, contentHash: 'fixture' },
468
+ symbol: 'updateSubsystemModel',
469
+ annotation:
470
+ 'Stamp gist.id on the local record + index so the next share updates in place.',
471
+ purl: 'external:proposed#packages/subsystems-studio/src/bun/subsystem-model-store.ts',
472
+ },
473
+ ],
474
+ },
475
+ ];
476
+
477
+ export const DRILLDOWN_DOC = {
478
+ title: DRILLDOWN_MODEL_SOURCE.title,
479
+ components: DRILLDOWN_COMPONENTS,
480
+ trails: DRILLDOWN_TRAILS,
481
+ } as const;
482
+
483
+ // ---------------------------------------------------------------------------
484
+ // Membership: containers resolve from their `process` claim exactly like the
485
+ // product's `getSubsystemProcessRegions` join; externals and authored
486
+ // components carry their members explicitly.
487
+ // ---------------------------------------------------------------------------
488
+
489
+ export type ElementMembership = ReadonlyMap<string, readonly string[]>;
490
+
491
+ /**
492
+ * The fixture's membership lookup: C4 element id → component aliases.
493
+ * Containers claim a `process` key (members = doc components with that key);
494
+ * externals and authored components carry `members` on the element.
495
+ */
496
+ export function deriveElementMembers(
497
+ elements: readonly C4Element[],
498
+ components: readonly SubsystemComponent[] = DRILLDOWN_COMPONENTS,
499
+ ): ElementMembership {
500
+ const byProcess = new Map<string, string[]>();
501
+ for (const c of components) {
502
+ if (!c.process) continue;
503
+ let list = byProcess.get(c.process);
504
+ if (!list) {
505
+ list = [];
506
+ byProcess.set(c.process, list);
507
+ }
508
+ list.push(c.alias);
509
+ }
510
+ const out = new Map<string, readonly string[]>();
511
+ for (const el of elements) {
512
+ if (el.kind === 'container' && el.process) {
513
+ out.set(el.id, byProcess.get(el.process) ?? []);
514
+ } else if ((el.kind === 'component' || el.kind === 'external-system') && el.members) {
515
+ out.set(el.id, el.members);
516
+ }
517
+ }
518
+ return out;
519
+ }
520
+
521
+ // ---------------------------------------------------------------------------
522
+ // The authored C4 element set — the "assume they exist" part, but every claim
523
+ // is checkable against the verified document above.
524
+ // ---------------------------------------------------------------------------
525
+
526
+ function container(
527
+ over: Partial<C4Container> & Pick<C4Container, 'id' | 'label' | 'process'>,
528
+ ): C4Element {
529
+ return {
530
+ kind: 'container',
531
+ containerKind: 'application',
532
+ technology: 'Bun',
533
+ state: 'accepted',
534
+ parentId: SYSTEM.id,
535
+ ...over,
536
+ };
537
+ }
538
+
539
+ function component(
540
+ over: Partial<C4Component> & Pick<C4Component, 'id' | 'label' | 'container'>,
541
+ ): C4Element {
542
+ return {
543
+ kind: 'component',
544
+ members: [],
545
+ state: 'accepted',
546
+ parentId: over.container,
547
+ technology: 'Bun',
548
+ ...over,
549
+ };
550
+ }
551
+
552
+ function external(
553
+ over: Partial<C4ExternalSystem> & Pick<C4ExternalSystem, 'id' | 'label'>,
554
+ ): C4Element {
555
+ return {
556
+ kind: 'external-system',
557
+ members: [],
558
+ state: 'accepted',
559
+ technology: 'REST',
560
+ ...over,
561
+ };
562
+ }
563
+
564
+ export const DRILLDOWN_ELEMENTS: C4Element[] = [
565
+ container({
566
+ id: HOST,
567
+ label: 'Studio host',
568
+ process: 'subsystems-studio/host',
569
+ technology: 'Bun + Electrobun',
570
+ description: 'Bun process owning the disk store API: create/update/get, index rows, tab opening.',
571
+ }),
572
+ component({
573
+ id: 'component:host-store',
574
+ label: 'Subsystem model store',
575
+ container: HOST,
576
+ members: ['studio-create', 'studio-update', 'studio-get', 'index-entry'],
577
+ technology: 'Bun fs',
578
+ description: 'The subsystem-model-store.ts API surface.',
579
+ }),
580
+ component({
581
+ id: 'component:host-tabs',
582
+ label: 'Tab opening',
583
+ container: HOST,
584
+ members: ['open-tab'],
585
+ technology: 'Bun',
586
+ description: 'Focus or create a subsystem-model tab.',
587
+ }),
588
+ container({
589
+ id: RENDERER,
590
+ label: 'Studio renderer',
591
+ process: 'subsystems-studio/renderer',
592
+ technology: 'Electrobun',
593
+ description: 'Model list and model tab chrome — where a Share-as-gist action would sit.',
594
+ }),
595
+ component({
596
+ id: 'component:renderer-views',
597
+ label: 'Model views',
598
+ container: RENDERER,
599
+ members: ['studio-list', 'studio-model-view'],
600
+ technology: 'Electrobun',
601
+ description: 'The two model surfaces in the renderer.',
602
+ }),
603
+ container({
604
+ id: CLI,
605
+ label: 'Principal CLI',
606
+ process: 'principal-studio-cli',
607
+ technology: 'Bun',
608
+ description: "Agent-facing author path: the subsystem-model create command.",
609
+ }),
610
+ component({
611
+ id: 'component:cli-command',
612
+ label: 'subsystem-model command',
613
+ container: CLI,
614
+ members: ['cli-create'],
615
+ technology: 'Bun',
616
+ description: 'The create subcommand.',
617
+ }),
618
+ container({
619
+ id: SITE,
620
+ label: 'Site — web',
621
+ process: 'site/web',
622
+ technology: 'React',
623
+ description: 'The public /gist viewer — the consumer of the shared file.',
624
+ }),
625
+ component({
626
+ id: 'component:site-gist',
627
+ label: 'Gist page & loader',
628
+ container: SITE,
629
+ members: ['site-gist-page', 'gist-load', 'to-portable'],
630
+ technology: 'React',
631
+ description: '/gist page + fetch/parse + portable strip.',
632
+ }),
633
+ external({
634
+ id: GITHUB,
635
+ label: 'GitHub Gists API',
636
+ members: ['github-gists-api'],
637
+ technology: 'REST',
638
+ description: 'Create or update a public gist that holds the portable JSON.',
639
+ }),
640
+ external({
641
+ id: DISK,
642
+ label: '~/.principal/subsystem-models',
643
+ members: ['model-disk-store'],
644
+ technology: 'JSON files',
645
+ description: 'Shared on-disk store; not owned by any single process.',
646
+ }),
647
+ ];
648
+
649
+ function edge(
650
+ over: Pick<C4Edge, 'id' | 'source' | 'target' | 'label'> &
651
+ Partial<Pick<C4Edge, 'protocol' | 'mechanisms' | 'count' | 'trailRefs'>>,
652
+ ): C4Edge {
653
+ return {
654
+ protocol: over.label,
655
+ mechanisms: [],
656
+ count: 0,
657
+ ...over,
658
+ };
659
+ }
660
+
661
+ /**
662
+ * The container-level edges — the fold of every cross-boundary trail step.
663
+ * Hand-authored `trailRefs` must agree with what `rollupEdgeTrailRefs`
664
+ * computes; `c4Drilldown.test.ts` asserts the agreement.
665
+ *
666
+ * The renderer has no incident line (only the proposed trail touches it) and
667
+ * the CLI never writes the store in a trail — both absences are real.
668
+ */
669
+ export const DRILLDOWN_EDGES: C4Edge[] = [
670
+ edge({
671
+ id: 'e:site-github',
672
+ source: SITE,
673
+ target: GITHUB,
674
+ label: 'HTTPS',
675
+ protocol: 'HTTPS · GET /gists/{id}',
676
+ mechanisms: ['calls'],
677
+ count: 1,
678
+ trailRefs: [{ trailId: 'tl-site-view-gist', stepIndices: [1] }],
679
+ }),
680
+ edge({
681
+ id: 'e:cli-host',
682
+ source: CLI,
683
+ target: HOST,
684
+ label: 'RPC',
685
+ protocol: 'RPC',
686
+ mechanisms: ['calls'],
687
+ count: 2,
688
+ trailRefs: [{ trailId: 'tl-persist-local-store', stepIndices: [0, 3] }],
689
+ }),
690
+ edge({
691
+ id: 'e:host-disk',
692
+ source: HOST,
693
+ target: DISK,
694
+ label: 'fs · JSON',
695
+ protocol: 'fs · JSON',
696
+ mechanisms: ['writes'],
697
+ count: 1,
698
+ trailRefs: [{ trailId: 'tl-persist-local-store', stepIndices: [1] }],
699
+ }),
700
+ ];
701
+
702
+ /** The drawable C4 projection — container view, closed. */
703
+ export const DRILLDOWN_C4_MODEL: C4Model = {
704
+ view: 'container',
705
+ system: SYSTEM,
706
+ nodes: DRILLDOWN_ELEMENTS,
707
+ edges: DRILLDOWN_EDGES,
708
+ };
709
+
710
+ /** Widely-readable form of the derived memberships, for tests and callers. */
711
+ export const DRILLDOWN_MEMBERS: Readonly<Record<string, readonly string[]>> =
712
+ Object.fromEntries(deriveElementMembers(DRILLDOWN_ELEMENTS)) as Readonly<
713
+ Record<string, readonly string[]>
714
+ >;
715
+
716
+ export function drilldownMembersOf(elementId: string): ReadonlySet<string> | undefined {
717
+ const members = DRILLDOWN_MEMBERS[elementId];
718
+ return members ? new Set(members) : undefined;
719
+ }
720
+
721
+ // ---------------------------------------------------------------------------
722
+ // The rollup prototype — the read side a `buildC4Model` will do for real.
723
+ // ---------------------------------------------------------------------------
724
+
725
+ /**
726
+ * Which trail steps cross between two elements' member sets — the provenance
727
+ * of a rolled-up C4 edge. Pure, and the spec the fixture's hand-authored
728
+ * `trailRefs` are asserted against.
729
+ *
730
+ * `membersOf` decides membership so callers can serve containers (via their
731
+ * `process` claims), externals (single alias), or any future element kind
732
+ * without this function knowing how membership is stored.
733
+ */
734
+ export function rollupEdgeTrailRefs(opts: {
735
+ source: string;
736
+ target: string;
737
+ trails: readonly SubsystemTrail[];
738
+ membersOf: (elementId: string) => ReadonlySet<string> | undefined;
739
+ }): C4TrailRef[] {
740
+ const byTrail = new Map<string, number[]>();
741
+ for (const trail of opts.trails) {
742
+ const sourceMembers = opts.membersOf(opts.source) ?? new Set<string>();
743
+ const targetMembers = opts.membersOf(opts.target) ?? new Set<string>();
744
+ trail.steps.forEach((step, index) => {
745
+ if (sourceMembers.has(step.from) && targetMembers.has(step.to)) {
746
+ const list = byTrail.get(trail.id) ?? [];
747
+ list.push(index);
748
+ byTrail.set(trail.id, list);
749
+ }
750
+ });
751
+ }
752
+ return [...byTrail.entries()].map(([trailId, stepIndices]) => ({ trailId, stepIndices }));
753
+ }
754
+
755
+ /**
756
+ * The C4 elements a trail touches — every element that owns a step endpoint.
757
+ *
758
+ * This is the reverse of the memberships above: given a step's `from`/`to`
759
+ * alias, which C4 box(es) hold it. Because a container's own process-claim
760
+ * membership AND the authored components nested inside it both claim the
761
+ * aliases, BOTH end up in the set: the container is the visible card at
762
+ * container view, its components are the visible cards when it is open, so
763
+ * either view stays honest.
764
+ *
765
+ * The complement of the result (all element ids minus these) is what a
766
+ * trail-driven emphasis pass should dim. Pure, and the spec the story's
767
+ * "players stay bright" is asserted against.
768
+ */
769
+ export function deriveTrailPlayerElements(
770
+ steps: SubsystemTrail['steps'],
771
+ members: Readonly<Record<string, readonly string[]>>,
772
+ ): Set<string> {
773
+ const byAlias = new Map<string, string[]>();
774
+ for (const [elementId, aliases] of Object.entries(members)) {
775
+ for (const alias of aliases) {
776
+ const list = byAlias.get(alias);
777
+ if (list) list.push(elementId);
778
+ else byAlias.set(alias, [elementId]);
779
+ }
780
+ }
781
+ const players = new Set<string>();
782
+ for (const step of steps) {
783
+ for (const id of byAlias.get(step.from) ?? []) players.add(id);
784
+ for (const id of byAlias.get(step.to) ?? []) players.add(id);
785
+ }
786
+ return players;
787
+ }
788
+
789
+ /**
790
+ * The C4 edges that carry the current focus — the line-level half of the
791
+ * emphasis pair above.
792
+ *
793
+ * An edge is lit iff its authored `trailRefs` cover the source: the whole
794
+ * trail, or one specific step of it. A step that is entirely internal to a
795
+ * container folds into no C4 line (the rollup only keeps cross-boundary
796
+ * hops), so its source lights NOTHING — every line dims, which is honest:
797
+ * no drawn edge represents that hop.
798
+ */
799
+ export function deriveLitC4Edges(
800
+ source: { trailId: string; stepIndex?: number },
801
+ edges: readonly C4Edge[],
802
+ ): Set<string> {
803
+ const lit = new Set<string>();
804
+ for (const edge of edges) {
805
+ for (const ref of edge.trailRefs ?? []) {
806
+ if (ref.trailId !== source.trailId) continue;
807
+ if (source.stepIndex == null || ref.stepIndices.includes(source.stepIndex)) {
808
+ lit.add(edge.id);
809
+ break;
810
+ }
811
+ }
812
+ }
813
+ return lit;
814
+ }
815
+
816
+ /**
817
+ * Process claims the accepted C4 element set backs NOT at all — the set the
818
+ * host hands the component graph so those frames draw as code boundaries.
819
+ *
820
+ * An element backs a process when it claims the key outright (a container's
821
+ * `process` claim) or carries a member whose component claims it (externals
822
+ * and L3 children carry explicit members). What nobody backs is code
823
+ * organisation in the accepted model's eyes — the shared libraries
824
+ * (`subsystems-react/ui`, `subsystems-core/types`) and the unbuilt proposal
825
+ * (`proposed/studio-gist`), exactly the anti-library trio above. Those frames
826
+ * forgo the solid runtime chrome ("deployment unit") and draw with package
827
+ * chrome — rounded, dashed — while keeping their process identity for issue
828
+ * targeting.
829
+ */
830
+ export function deriveCodeProcessKeys(
831
+ elements: readonly C4Element[],
832
+ components: readonly SubsystemComponent[],
833
+ ): Set<string> {
834
+ const backed = new Set<string>();
835
+ const processByAlias = new Map(components.map((c) => [c.alias, c.process]));
836
+ for (const el of elements) {
837
+ if (el.kind === 'container' && el.process) backed.add(el.process);
838
+ if (el.kind !== 'component' && el.kind !== 'external-system') continue;
839
+ for (const alias of el.members ?? []) {
840
+ const p = processByAlias.get(alias);
841
+ if (p) backed.add(p);
842
+ }
843
+ }
844
+ const keys = new Set<string>();
845
+ for (const c of components) if (c.process) keys.add(c.process);
846
+ return new Set([...keys].filter((k) => !backed.has(k)));
847
+ }