@principal-ai/subsystems-react 0.51.4 → 0.53.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 (69) hide show
  1. package/dist/index.d.ts +4 -4
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js +2 -2
  4. package/dist/index.js.map +1 -1
  5. package/dist/subsystem/C4Graph.d.ts +3 -7
  6. package/dist/subsystem/C4Graph.d.ts.map +1 -1
  7. package/dist/subsystem/C4Graph.js +193 -47
  8. package/dist/subsystem/C4Graph.js.map +1 -1
  9. package/dist/subsystem/C4NodeCard.d.ts +33 -27
  10. package/dist/subsystem/C4NodeCard.d.ts.map +1 -1
  11. package/dist/subsystem/C4NodeCard.js +89 -43
  12. package/dist/subsystem/C4NodeCard.js.map +1 -1
  13. package/dist/subsystem/IssueList.d.ts +1 -1
  14. package/dist/subsystem/IssueList.d.ts.map +1 -1
  15. package/dist/subsystem/IssueList.js +20 -1
  16. package/dist/subsystem/IssueList.js.map +1 -1
  17. package/dist/subsystem/SubsystemComponentGraph.d.ts.map +1 -1
  18. package/dist/subsystem/SubsystemComponentGraph.js +42 -19
  19. package/dist/subsystem/SubsystemComponentGraph.js.map +1 -1
  20. package/dist/subsystem/c4.d.ts +48 -49
  21. package/dist/subsystem/c4.d.ts.map +1 -1
  22. package/dist/subsystem/c4.js +50 -0
  23. package/dist/subsystem/c4.js.map +1 -1
  24. package/dist/subsystem/c4Evidence.d.ts +88 -84
  25. package/dist/subsystem/c4Evidence.d.ts.map +1 -1
  26. package/dist/subsystem/c4Evidence.js +113 -166
  27. package/dist/subsystem/c4Evidence.js.map +1 -1
  28. package/dist/subsystem/model.d.ts +35 -245
  29. package/dist/subsystem/model.d.ts.map +1 -1
  30. package/dist/subsystem/model.js +11 -29
  31. package/dist/subsystem/model.js.map +1 -1
  32. package/dist/subsystem/nodes.js +5 -5
  33. package/dist/subsystem/nodes.js.map +1 -1
  34. package/dist/subsystem/techIcons/index.d.ts +5 -0
  35. package/dist/subsystem/techIcons/index.d.ts.map +1 -1
  36. package/dist/subsystem/techIcons/index.js +3 -0
  37. package/dist/subsystem/techIcons/index.js.map +1 -1
  38. package/dist/utils/edgeLabel.d.ts +15 -1
  39. package/dist/utils/edgeLabel.d.ts.map +1 -1
  40. package/dist/utils/edgeLabel.js +25 -1
  41. package/dist/utils/edgeLabel.js.map +1 -1
  42. package/dist/utils/elkLayout.d.ts +47 -0
  43. package/dist/utils/elkLayout.d.ts.map +1 -1
  44. package/dist/utils/elkLayout.js +71 -7
  45. package/dist/utils/elkLayout.js.map +1 -1
  46. package/package.json +3 -3
  47. package/src/index.ts +8 -6
  48. package/src/stories/Subsystem/C4Graph/C4Component.stories.tsx +198 -0
  49. package/src/stories/Subsystem/C4Graph/C4Container.stories.tsx +42 -6
  50. package/src/stories/Subsystem/C4Graph/C4NodeGallery.stories.tsx +9 -7
  51. package/src/stories/Subsystem/ComponentGraph/Issues.stories.tsx +686 -2
  52. package/src/subsystem/C4Graph.tsx +278 -82
  53. package/src/subsystem/C4NodeCard.test.ts +32 -22
  54. package/src/subsystem/C4NodeCard.tsx +134 -69
  55. package/src/subsystem/IssueList.tsx +23 -0
  56. package/src/subsystem/SubsystemComponentGraph.tsx +38 -16
  57. package/src/subsystem/c4.protocolColor.test.ts +26 -0
  58. package/src/subsystem/c4.ts +76 -52
  59. package/src/subsystem/c4Evidence.test.ts +177 -237
  60. package/src/subsystem/c4Evidence.ts +160 -235
  61. package/src/subsystem/model.ts +62 -305
  62. package/src/subsystem/nodes.group.test.tsx +15 -0
  63. package/src/subsystem/nodes.tsx +7 -7
  64. package/src/subsystem/techIcons/index.tsx +8 -0
  65. package/src/utils/edgeLabel.test.ts +21 -2
  66. package/src/utils/edgeLabel.ts +29 -1
  67. package/src/utils/elkLayout.test.ts +16 -0
  68. package/src/utils/elkLayout.ts +119 -7
  69. package/src/stories/Subsystem/ComponentGraph/IssueOverlay.stories.tsx +0 -602
@@ -1,9 +1,12 @@
1
1
  import '@xyflow/react/dist/style.css';
2
- import React from 'react';
2
+ import React, { useMemo, useState } from 'react';
3
3
  import type { Meta, StoryObj } from '@storybook/react';
4
4
  import { ThemeProvider, defaultEditorTheme } from '@principal-ade/industry-theme';
5
5
  import { SubsystemComponentGraph } from '../../../subsystem/SubsystemComponentGraph';
6
- import { SubsystemIssueList, type SubsystemIssue } from '../../../subsystem/IssueList';
6
+ import { SubsystemIssueList, issueCategory, type SubsystemIssue, type SubsystemIssueCategory } from '../../../subsystem/IssueList';
7
+ import { verifyProcessBoundaries, verificationIssues } from '../../../subsystem/c4Evidence';
8
+ import { getSubsystemRegions, type SubsystemComponent, type SubsystemTrail } from '../../../subsystem/model';
9
+ import type { C4Container } from '../../../subsystem/c4';
7
10
  import { components, graphSpecFromSteps } from './fixtures';
8
11
 
9
12
  const meta = {
@@ -347,3 +350,684 @@ export const GraphWithStepVerificationBadges: Story = {
347
350
  </div>
348
351
  ),
349
352
  };
353
+
354
+ // ---------------------------------------------------------------------------
355
+ // C4 process verification — dynamic-topology findings from the element store
356
+ // ---------------------------------------------------------------------------
357
+
358
+ // Four deployment units, one per verification state. The agent scaffold
359
+ // proposed a container for each; the human has decided on three of them.
360
+ const PROCESS_OF: Record<string, string> = {
361
+ main: 'studio/host',
362
+ store: 'studio/host',
363
+ view: 'studio/renderer',
364
+ bridge: 'studio/renderer',
365
+ worker: 'studio/worker',
366
+ queue: 'studio/agent-queue',
367
+ };
368
+
369
+ const processComponents = [
370
+ ...components([
371
+ ['main', 'main', 'function', 'src/host/main.ts', 'pkg:github/acme/studio', 'boots the host', 'main'],
372
+ ['store', 'SessionStore', 'store', 'src/host/store.ts', 'pkg:github/acme/studio', 'retained host state', 'SessionStore'],
373
+ ['view', 'TrailView', 'function', 'src/renderer/view.tsx', 'pkg:github/acme/studio', 'renders the trail', 'TrailView'],
374
+ ['bridge', 'bridge', 'function', 'src/renderer/bridge.ts', 'pkg:github/acme/studio', 'IPC bridge to the host', 'bridge'],
375
+ ['worker', 'runWorker', 'function', 'src/worker/run.ts', 'pkg:github/acme/studio', 'runs the worker loop', 'runWorker'],
376
+ ['queue', 'jobQueue', 'store', 'src/queue/jobs.ts', 'pkg:github/acme/studio', 'pending agent jobs', 'jobQueue'],
377
+ ]).map((c) => {
378
+ const process: string | undefined = PROCESS_OF[c.alias];
379
+ return process ? { ...c, process } : c;
380
+ }),
381
+ ];
382
+
383
+ const processEdges = graphSpecFromSteps([
384
+ ['main', 'store', 'writes'],
385
+ ['main', 'bridge', 'calls'],
386
+ ['bridge', 'view', 'feeds'],
387
+ ['main', 'worker', 'calls'],
388
+ ['worker', 'queue', 'writes'],
389
+ ]);
390
+
391
+ // The rollup the verifying agent would read off the model: one region per
392
+ // distinct `process` value, members included.
393
+ const processRollup = getSubsystemRegions({ components: processComponents });
394
+
395
+ function verificationContainer(
396
+ label: string,
397
+ process: string,
398
+ state: C4Container['state'],
399
+ ): C4Container {
400
+ return {
401
+ id: `container:${process}`,
402
+ kind: 'container',
403
+ process,
404
+ label,
405
+ containerKind: 'application',
406
+ technology: 'Bun',
407
+ state,
408
+ };
409
+ }
410
+
411
+ // Initial element store: three containers have been through review; the
412
+ // agent-queue boundary has none yet (unassigned — silence would mean verified).
413
+ const INITIAL_CONTAINERS: C4Container[] = [
414
+ verificationContainer('Studio host', 'studio/host', 'accepted'),
415
+ verificationContainer('Studio renderer', 'studio/renderer', 'proposed'),
416
+ verificationContainer('Studio worker', 'studio/worker', 'rejected'),
417
+ ];
418
+
419
+ /**
420
+ * C4 process verification flowing through the diagnostics pipeline. Each
421
+ * boundary that is not verified is a dynamic-topology finding — badged on the
422
+ * process frame, listed in the sidebar, fixable in place. The cards' fixes
423
+ * drive the (story-local) element store: proposing scaffolds an unclaimed
424
+ * boundary, accepting lands the decision. Recomputed on every change —
425
+ * verification is read, never stored.
426
+ */
427
+ export const GraphWithProcessVerification: Story = {
428
+ render: () => {
429
+ const [containers, setContainers] = useState(INITIAL_CONTAINERS);
430
+ const verification = verifyProcessBoundaries({
431
+ rollup: processRollup,
432
+ elements: containers,
433
+ });
434
+ const issues = verificationIssues(verification);
435
+ const applyFix = (issue: SubsystemIssue) => {
436
+ const key = issue.target?.id;
437
+ if (!key) return;
438
+ if (issue.kind === 'boundary_process_unassigned') {
439
+ setContainers((prev) => [...prev, verificationContainer(key, key, 'proposed')]);
440
+ } else if (issue.kind === 'boundary_process_proposed') {
441
+ setContainers((prev) =>
442
+ prev.map((c) => (c.process === key ? { ...c, state: 'accepted' as const } : c)),
443
+ );
444
+ }
445
+ };
446
+ return (
447
+ <div style={{ width: '100%', height: '100vh' }}>
448
+ <SubsystemComponentGraph
449
+ title="Process verification"
450
+ description="Each process boundary's C4 verification status flows through the diagnostics pipeline as a dynamic-topology finding: **unclaimed** (no container), **awaiting decision** (an agent scaffold), or **rejected** — one per boundary that is not verified, badged on the frame and listed in the sidebar. A verified boundary reports nothing: the audit reports absence only, and silence is the good state. The cards' fixes drive the element store — propose an unclaimed boundary, accept the proposed one — and the badge clears when the decision lands."
451
+ components={processComponents}
452
+ trails={processEdges.trails}
453
+ issues={issues}
454
+ onApplyIssueFix={applyFix}
455
+ />
456
+ </div>
457
+ );
458
+ },
459
+ };
460
+
461
+ // ---------------------------------------------------------------------------
462
+ // IssueOverlay — the diagnostics surface over a REAL subsystem graph
463
+ // ---------------------------------------------------------------------------
464
+ // The graph is the Maintain-run model stored at
465
+ // ~/.principal/subsystem-models/sg-1789951509967-ppkvezdy0.json
466
+ // ("Maintain run — fire-and-forget RPC, wait for the finish event"): the
467
+ // renderer picker → host RPC → background runner → maintain orchestrator →
468
+ // opencode V2 session, plus the liveness-probe handshake. Its components and
469
+ // three trails are reproduced verbatim below; the topology is carried by the
470
+ // trails, exactly as stored.
471
+ //
472
+ // What these stories exercise: the graph's diagnostics surface over the real
473
+ // model — the title-row status chip, the grouped issue list, the per-node
474
+ // overlay (dotted border + earliest-rung chip, wired via `data.issue`), and
475
+ // layer-expand dimming. Clicking a card focuses its construct; collapsing it
476
+ // unfocuses.
477
+
478
+ const PURL = 'pkg:github/principal-ai/subsystem-modeling';
479
+ const HOST = 'subsystems-studio/host';
480
+ const RENDERER = 'subsystems-studio/renderer';
481
+ /** The host barrel whose members disagree about their process (see `events-tab`). */
482
+ const BUN_INDEX = 'packages/subsystems-studio/src/bun/index.ts';
483
+
484
+ /** The Maintain-run model's components, as captured in the stored model. */
485
+ const MAINTAIN_COMPONENTS: SubsystemComponent[] = [
486
+ {
487
+ alias: 'maintain-picker',
488
+ name: 'MaintainModelPickerModal',
489
+ construct: 'function',
490
+ role: 'entry',
491
+ process: RENDERER,
492
+ file: 'packages/subsystems-studio/src/mainview/components/MaintainModelPickerModal.tsx',
493
+ purl: PURL,
494
+ symbol: 'MaintainModelPickerModal',
495
+ purpose: 'Picks the tier + model, then fires the maintain RPC and closes.',
496
+ framework: 'react',
497
+ stereotype: 'component',
498
+ },
499
+ {
500
+ alias: 'maintain-rpc-handler',
501
+ name: 'maintainSubsystemModel RPC',
502
+ construct: 'function',
503
+ role: 'entry',
504
+ process: HOST,
505
+ file: 'packages/subsystems-studio/src/bun/index.ts',
506
+ module: BUN_INDEX,
507
+ purl: PURL,
508
+ purpose: 'Fire-and-forget RPC: returns started:true and hands off to the background runner.',
509
+ },
510
+ {
511
+ alias: 'maintain-background-runner',
512
+ name: 'maintainSubsystemModelInBackground',
513
+ construct: 'function',
514
+ process: HOST,
515
+ file: 'packages/subsystems-studio/src/bun/index.ts',
516
+ module: BUN_INDEX,
517
+ purl: PURL,
518
+ symbol: 'maintainSubsystemModelInBackground',
519
+ purpose: 'Guards duplicate runs, broadcasts running/done/error, and resolves the background maintain result.',
520
+ },
521
+ {
522
+ alias: 'maintain-orchestrator',
523
+ name: 'maintainSubsystemModel',
524
+ construct: 'function',
525
+ process: HOST,
526
+ file: 'packages/subsystems-studio/src/bun/maintain-model.ts',
527
+ purl: PURL,
528
+ symbol: 'maintainSubsystemModel',
529
+ purpose: 'Installs agents, audits, routes by verdict, builds the brief, and resolves the maintainer model.',
530
+ },
531
+ {
532
+ alias: 'maintain-agent-launcher',
533
+ name: 'runMaintainAgent',
534
+ construct: 'function',
535
+ process: HOST,
536
+ file: 'packages/subsystems-studio/src/bun/maintain-model.ts',
537
+ purl: PURL,
538
+ symbol: 'runMaintainAgent',
539
+ purpose: 'Wraps an opencode V2 session run with the agent, task, and model.',
540
+ },
541
+ {
542
+ alias: 'session-runner',
543
+ name: 'runOpencodeV2AgentSession',
544
+ construct: 'function',
545
+ process: HOST,
546
+ file: 'packages/subsystems-studio/src/bun/opencode-v2-live.ts',
547
+ purl: PURL,
548
+ symbol: 'runOpencodeV2AgentSession',
549
+ purpose: 'Creates + prompts an opencode session, then blocks until a terminal event arrives on the SSE feed.',
550
+ },
551
+ {
552
+ alias: 'service-ensurer',
553
+ name: 'ensureServiceRunning',
554
+ construct: 'function',
555
+ process: HOST,
556
+ file: 'packages/subsystems-studio/src/bun/opencode-v2-live.ts',
557
+ purl: PURL,
558
+ symbol: 'ensureServiceRunning',
559
+ purpose: 'Starts the opencode2 service when the daemon is not healthy.',
560
+ },
561
+ {
562
+ alias: 'sse-feed-reader',
563
+ name: 'readSse',
564
+ construct: 'function',
565
+ process: HOST,
566
+ file: 'packages/subsystems-studio/src/bun/opencode-v2-live.ts',
567
+ purl: PURL,
568
+ symbol: 'readSse',
569
+ purpose: 'Streams /api/event once and routes raw events into the wait loop.',
570
+ },
571
+ {
572
+ alias: 'terminal-matcher',
573
+ name: 'isTerminalEvent',
574
+ construct: 'function',
575
+ process: HOST,
576
+ file: 'packages/subsystems-studio/src/bun/opencode-v2-live.ts',
577
+ purl: PURL,
578
+ symbol: 'isTerminalEvent',
579
+ purpose: 'True when an event ends the session (execution finished/succeeded/failed or session idle).',
580
+ },
581
+ {
582
+ alias: 'live-feed-store',
583
+ name: 'OpencodeLiveFeedState store',
584
+ construct: 'store',
585
+ process: HOST,
586
+ file: 'packages/subsystems-studio/src/bun/opencode-v2-live.ts',
587
+ purl: PURL,
588
+ symbol: 'subscribeOpencodeLiveFeeds',
589
+ purpose: 'Retains per-session live status (starting/running/done/error) pushed to the renderer.',
590
+ },
591
+ {
592
+ alias: 'maintain-probe-registry',
593
+ name: 'MaintainerProbeRegistry',
594
+ construct: 'store',
595
+ process: HOST,
596
+ file: 'packages/subsystems-studio/src/bun/maintainer-probe.ts',
597
+ purl: PURL,
598
+ symbol: 'getMaintainerProbeRegistry',
599
+ purpose: "Records when a maintain run's liveness probe lands.",
600
+ },
601
+ {
602
+ alias: 'probe-route',
603
+ name: 'POST /api/maintainer/probe',
604
+ construct: 'function',
605
+ role: 'entry',
606
+ process: HOST,
607
+ file: 'packages/subsystems-studio/src/bun/http-server.ts',
608
+ purl: PURL,
609
+ purpose: "Liveness endpoint the agent's first tool call curls; proves the model can run tools headless.",
610
+ },
611
+ {
612
+ alias: 'events-tab',
613
+ name: 'openMaintainEventsTab',
614
+ construct: 'function',
615
+ // Declared in the host barrel, but the only thing it does is drive the
616
+ // renderer — so it claims the renderer process. That disagreement with its
617
+ // two barrel siblings is exactly the `boundary_process_nest_disagree`
618
+ // finding below, and it is why the barrel gets a frame that refuses to
619
+ // nest under either process.
620
+ process: RENDERER,
621
+ file: 'packages/subsystems-studio/src/bun/index.ts',
622
+ module: BUN_INDEX,
623
+ purl: PURL,
624
+ symbol: 'openMaintainEventsTab',
625
+ purpose: "Opens the renderer tab that renders a session's live feed.",
626
+ },
627
+ {
628
+ alias: 'opencode-v2-server',
629
+ name: 'OpenCode V2 service',
630
+ construct: 'external',
631
+ role: 'service',
632
+ file: '',
633
+ purl: 'external:opencode2-service',
634
+ purpose: 'The opencode2 daemon owning /api/session and /api/event; executes the agent session.',
635
+ },
636
+ ];
637
+
638
+ /** The model's three runtime trails, verbatim (these carry the topology). */
639
+ const MAINTAIN_TRAILS: SubsystemTrail[] = [
640
+ {
641
+ id: 'wt-start-maintain',
642
+ title: 'Start a maintenance run',
643
+ steps: [
644
+ {
645
+ from: 'maintain-picker',
646
+ to: 'maintain-rpc-handler',
647
+ mechanism: 'calls',
648
+ file: 'packages/subsystems-studio/src/mainview/components/MaintainModelPickerModal.tsx',
649
+ line: 116,
650
+ symbol: 'MaintainModelPickerModal',
651
+ purl: `${PURL}#packages/subsystems-studio/src/mainview/components/MaintainModelPickerModal.tsx`,
652
+ annotation: 'Fire the maintainSubsystemModel RPC with the selected model.',
653
+ },
654
+ {
655
+ from: 'maintain-rpc-handler',
656
+ to: 'maintain-background-runner',
657
+ mechanism: 'calls',
658
+ file: 'packages/subsystems-studio/src/bun/index.ts',
659
+ line: 2522,
660
+ symbol: 'maintainSubsystemModelInBackground',
661
+ purl: `${PURL}#packages/subsystems-studio/src/bun/index.ts`,
662
+ annotation: 'RPC returns started:true immediately and hands off to the guarded runner.',
663
+ },
664
+ {
665
+ from: 'maintain-background-runner',
666
+ to: 'maintain-orchestrator',
667
+ mechanism: 'calls',
668
+ file: 'packages/subsystems-studio/src/bun/index.ts',
669
+ line: 3160,
670
+ symbol: 'maintainSubsystemModelInBackground',
671
+ purl: `${PURL}#packages/subsystems-studio/src/bun/index.ts`,
672
+ annotation: 'Run the full install → audit → route-by-verdict → brief pipeline.',
673
+ },
674
+ {
675
+ from: 'maintain-orchestrator',
676
+ to: 'maintain-agent-launcher',
677
+ mechanism: 'calls',
678
+ file: 'packages/subsystems-studio/src/bun/maintain-model.ts',
679
+ line: 854,
680
+ symbol: 'runMaintainAgent',
681
+ purl: `${PURL}#packages/subsystems-studio/src/bun/maintain-model.ts`,
682
+ annotation: 'Resolve the model then launch the agent run, retrying once on the credentialed fallback if unusable.',
683
+ },
684
+ {
685
+ from: 'maintain-agent-launcher',
686
+ to: 'session-runner',
687
+ mechanism: 'calls',
688
+ file: 'packages/subsystems-studio/src/bun/maintain-model.ts',
689
+ line: 742,
690
+ symbol: 'runOpencodeV2AgentSession',
691
+ purl: `${PURL}#packages/subsystems-studio/src/bun/maintain-model.ts`,
692
+ annotation: 'Hand the brief, agent, and directory to the V2 session runner.',
693
+ },
694
+ {
695
+ from: 'maintain-background-runner',
696
+ to: 'events-tab',
697
+ mechanism: 'calls',
698
+ file: 'packages/subsystems-studio/src/bun/index.ts',
699
+ line: 3164,
700
+ symbol: 'maintainSubsystemModelInBackground',
701
+ purl: `${PURL}#packages/subsystems-studio/src/bun/index.ts`,
702
+ annotation: 'Open the live feed tab the instant the session id exists.',
703
+ },
704
+ ],
705
+ },
706
+ {
707
+ id: 'wt-wait-finish-event',
708
+ title: 'Wait for the finish event',
709
+ steps: [
710
+ {
711
+ from: 'session-runner',
712
+ to: 'sse-feed-reader',
713
+ mechanism: 'calls',
714
+ file: 'packages/subsystems-studio/src/bun/opencode-v2-live.ts',
715
+ line: 355,
716
+ symbol: 'runOpencodeV2AgentSession',
717
+ purl: `${PURL}#packages/subsystems-studio/src/bun/opencode-v2-live.ts`,
718
+ annotation: 'Open the /api/event subscription before creating the session.',
719
+ },
720
+ {
721
+ from: 'sse-feed-reader',
722
+ to: 'opencode-v2-server',
723
+ mechanism: 'watches',
724
+ file: 'packages/subsystems-studio/src/bun/opencode-v2-live.ts',
725
+ line: 222,
726
+ symbol: 'readSse',
727
+ purl: `${PURL}#packages/subsystems-studio/src/bun/opencode-v2-live.ts`,
728
+ annotation: "Stream the single SSE feed that carries every session's events.",
729
+ },
730
+ {
731
+ from: 'session-runner',
732
+ to: 'service-ensurer',
733
+ mechanism: 'calls',
734
+ file: 'packages/subsystems-studio/src/bun/opencode-v2-live.ts',
735
+ line: 326,
736
+ symbol: 'runOpencodeV2AgentSession',
737
+ purl: `${PURL}#packages/subsystems-studio/src/bun/opencode-v2-live.ts`,
738
+ annotation: 'Ensure the opencode2 daemon is healthy, starting the service if it is not.',
739
+ },
740
+ {
741
+ from: 'session-runner',
742
+ to: 'opencode-v2-server',
743
+ mechanism: 'calls',
744
+ file: 'packages/subsystems-studio/src/bun/opencode-v2-live.ts',
745
+ line: 384,
746
+ symbol: 'runOpencodeV2AgentSession',
747
+ purl: `${PURL}#packages/subsystems-studio/src/bun/opencode-v2-live.ts`,
748
+ annotation: 'POST /api/session creates the session with a 15s HTTP timeout.',
749
+ },
750
+ {
751
+ from: 'session-runner',
752
+ to: 'opencode-v2-server',
753
+ mechanism: 'calls',
754
+ file: 'packages/subsystems-studio/src/bun/opencode-v2-live.ts',
755
+ line: 428,
756
+ symbol: 'runOpencodeV2AgentSession',
757
+ purl: `${PURL}#packages/subsystems-studio/src/bun/opencode-v2-live.ts`,
758
+ annotation: 'POST /api/session/{id}/prompt feeds the maintain brief with a 15s HTTP timeout.',
759
+ },
760
+ {
761
+ from: 'sse-feed-reader',
762
+ to: 'terminal-matcher',
763
+ mechanism: 'calls',
764
+ file: 'packages/subsystems-studio/src/bun/opencode-v2-live.ts',
765
+ line: 365,
766
+ symbol: 'readSse',
767
+ purl: `${PURL}#packages/subsystems-studio/src/bun/opencode-v2-live.ts`,
768
+ annotation: 'Flag sawTerminal when the event is an execution finish or session idle.',
769
+ },
770
+ {
771
+ from: 'terminal-matcher',
772
+ to: 'session-runner',
773
+ mechanism: 'feeds',
774
+ file: 'packages/subsystems-studio/src/bun/opencode-v2-live.ts',
775
+ line: 469,
776
+ symbol: 'runOpencodeV2AgentSession',
777
+ purl: `${PURL}#packages/subsystems-studio/src/bun/opencode-v2-live.ts`,
778
+ annotation: 'Wait loop polls every 400ms until sawTerminal or the 15-minute deadline.',
779
+ },
780
+ {
781
+ from: 'session-runner',
782
+ to: 'maintain-probe-registry',
783
+ mechanism: 'reads',
784
+ file: 'packages/subsystems-studio/src/bun/opencode-v2-live.ts',
785
+ line: 484,
786
+ symbol: 'runOpencodeV2AgentSession',
787
+ purl: `${PURL}#packages/subsystems-studio/src/bun/opencode-v2-live.ts`,
788
+ annotation: "Confirm the model's first tool call landed the liveness probe; abort as unusable on timeout.",
789
+ },
790
+ {
791
+ from: 'session-runner',
792
+ to: 'live-feed-store',
793
+ mechanism: 'writes',
794
+ file: 'packages/subsystems-studio/src/bun/opencode-v2-live.ts',
795
+ line: 529,
796
+ symbol: 'runOpencodeV2AgentSession',
797
+ purl: `${PURL}#packages/subsystems-studio/src/bun/opencode-v2-live.ts`,
798
+ annotation: 'Stamp the feed done (or error) when the wait loop exits.',
799
+ },
800
+ {
801
+ from: 'maintain-background-runner',
802
+ to: 'maintain-picker',
803
+ mechanism: 'feeds',
804
+ file: 'packages/subsystems-studio/src/bun/index.ts',
805
+ line: 3187,
806
+ symbol: 'maintainSubsystemModelInBackground',
807
+ purl: `${PURL}#packages/subsystems-studio/src/bun/index.ts`,
808
+ annotation: 'Broadcast done/error so the Maintain row flips off Running.',
809
+ },
810
+ ],
811
+ },
812
+ {
813
+ id: 'wt-probe-handshake',
814
+ title: 'Liveness probe handshake',
815
+ steps: [
816
+ {
817
+ from: 'opencode-v2-server',
818
+ to: 'probe-route',
819
+ mechanism: 'calls',
820
+ file: 'packages/subsystems-studio/src/bun/http-server.ts',
821
+ line: 314,
822
+ symbol: 'handleSubsystemModelRequest',
823
+ purl: `${PURL}#packages/subsystems-studio/src/bun/http-server.ts`,
824
+ annotation: "The agent's step-0 tool call curls POST /api/maintainer/probe with the run token.",
825
+ },
826
+ {
827
+ from: 'probe-route',
828
+ to: 'maintain-probe-registry',
829
+ mechanism: 'writes',
830
+ file: 'packages/subsystems-studio/src/bun/http-server.ts',
831
+ line: 318,
832
+ symbol: 'getMaintainerProbeRegistry',
833
+ purl: `${PURL}#packages/subsystems-studio/src/bun/http-server.ts`,
834
+ annotation: 'mark(runId) records when the probe landed.',
835
+ },
836
+ {
837
+ from: 'session-runner',
838
+ to: 'maintain-probe-registry',
839
+ mechanism: 'reads',
840
+ file: 'packages/subsystems-studio/src/bun/opencode-v2-live.ts',
841
+ line: 484,
842
+ symbol: 'runOpencodeV2AgentSession',
843
+ purl: `${PURL}#packages/subsystems-studio/src/bun/opencode-v2-live.ts`,
844
+ annotation: 'Each loop tick checks whether the token landed before the probe deadline.',
845
+ },
846
+ ],
847
+ },
848
+ ];
849
+
850
+ /**
851
+ * Illustrative audit findings, mapped onto the real component aliases in the
852
+ * Maintain-run model. The stored model's verification block is clean
853
+ * (13/13 anchored, 0 missing), so these are the *shape* of findings the
854
+ * diagnostics layer would receive — one per rung of the verification ladder
855
+ * (file → symbol → declaration → type → signature) plus the topology / trail
856
+ * layers — to exercise the proposal end to end.
857
+ */
858
+ const MAINTAIN_ISSUES: SubsystemIssue[] = [
859
+ {
860
+ id: 'mr-file',
861
+ severity: 'error',
862
+ kind: 'missing_file',
863
+ message:
864
+ 'File packages/subsystems-studio/src/bun/maintainer-probe.ts is not present on disk at the captured commit.',
865
+ target: { kind: 'component', id: 'maintain-probe-registry', label: 'MaintainerProbeRegistry' },
866
+ fix: { label: 'Re-anchor to the current file' },
867
+ },
868
+ {
869
+ id: 'mr-symbol',
870
+ severity: 'error',
871
+ kind: 'symbol_ambiguous',
872
+ message: 'Multiple graphify nodes match runOpencodeV2AgentSession (2 candidates).',
873
+ target: { kind: 'component', id: 'session-runner', label: 'runOpencodeV2AgentSession' },
874
+ },
875
+ {
876
+ id: 'mr-decl',
877
+ severity: 'error',
878
+ kind: 'stale_declaration',
879
+ message: 'Declaration moved since capture — recorded start line 2522, now 2588.',
880
+ target: {
881
+ kind: 'component',
882
+ id: 'maintain-background-runner',
883
+ label: 'maintainSubsystemModelInBackground',
884
+ },
885
+ fix: { label: 'Re-pin declaration' },
886
+ },
887
+ {
888
+ id: 'mr-type',
889
+ severity: 'error',
890
+ kind: 'signature_mismatch',
891
+ message: 'Claimed (agent, task, model) => Session; graphify has (opts: RunOptions) => void.',
892
+ target: { kind: 'component', id: 'maintain-agent-launcher', label: 'runMaintainAgent' },
893
+ },
894
+ {
895
+ id: 'mr-sig',
896
+ severity: 'info',
897
+ kind: 'signature_unconfirmed',
898
+ message: 'Named parameter types are not present in the graphify cache.',
899
+ target: { kind: 'component', id: 'maintain-orchestrator', label: 'maintainSubsystemModel' },
900
+ },
901
+ {
902
+ id: 'mr-symbol-unmatched',
903
+ severity: 'error',
904
+ kind: 'symbol_unmatched',
905
+ message:
906
+ 'No graphify node matches symbol readSse in packages/subsystems-studio/src/bun/opencode-v2-live.ts',
907
+ target: { kind: 'component', id: 'sse-feed-reader', label: 'readSse' },
908
+ },
909
+ {
910
+ id: 'mr-construct',
911
+ severity: 'info',
912
+ kind: 'construct_unconfirmed',
913
+ message: 'Graphify anchored the node but left the construct unclassified (store vs interface).',
914
+ target: { kind: 'component', id: 'live-feed-store', label: 'OpencodeLiveFeedState store' },
915
+ },
916
+ {
917
+ id: 'mr-boundary',
918
+ severity: 'info',
919
+ kind: 'boundary_process_nest_disagree',
920
+ message:
921
+ 'module "packages/subsystems-studio/src/bun/index.ts" members claim different processes: "subsystems-studio/host", "subsystems-studio/renderer"',
922
+ target: {
923
+ kind: 'module',
924
+ id: BUN_INDEX,
925
+ label: 'packages/subsystems-studio/src/bun/index.ts',
926
+ },
927
+ },
928
+ {
929
+ id: 'mr-wt',
930
+ severity: 'info',
931
+ kind: 'trail',
932
+ message:
933
+ 'Step 7 (terminal-matcher → session-runner) references a line that moved (was :469).',
934
+ target: {
935
+ kind: 'step',
936
+ id: 'wt-wait-finish-event',
937
+ // The host labels a step by its call-site symbol — the same row text
938
+ // the trail UI shows — with the trail title as the detail line.
939
+ label: 'matchFinishEvent',
940
+ detail: 'Wait for the finish event',
941
+ stepIndex: 6,
942
+ },
943
+ },
944
+ {
945
+ id: 'mr-repo',
946
+ severity: 'info',
947
+ kind: 'repo_unresolved',
948
+ message:
949
+ 'Repo external:opencode2-service is not available locally — start the daemon to verify its endpoints.',
950
+ target: { kind: 'repo', id: 'external:opencode2-service', label: 'external:opencode2-service' },
951
+ },
952
+ ];
953
+
954
+ function MaintainRunGraph({
955
+ issues,
956
+ status,
957
+ showIssues,
958
+ focusCategory,
959
+ }: {
960
+ issues: SubsystemIssue[];
961
+ status: 'ok' | 'issues' | 'gaps';
962
+ showIssues?: boolean;
963
+ focusCategory?: SubsystemIssueCategory;
964
+ }) {
965
+ const [lastEvent, setLastEvent] = useState<string | null>(null);
966
+ const errorCount = useMemo(
967
+ () => issues.filter((i) => i.severity === 'error').length,
968
+ [issues],
969
+ );
970
+ const gapCount = useMemo(
971
+ () => issues.filter((i) => i.severity === 'info').length,
972
+ [issues],
973
+ );
974
+ const issueCount = status === 'issues' ? errorCount : status === 'gaps' ? gapCount : 0;
975
+ return (
976
+ <div style={{ display: 'flex', flexDirection: 'column', width: '100%', height: '100vh' }}>
977
+ <div
978
+ style={{
979
+ padding: '6px 12px',
980
+ fontFamily: 'ui-monospace, monospace',
981
+ fontSize: 11,
982
+ opacity: 0.7,
983
+ borderBottom: '1px solid #333',
984
+ }}
985
+ >
986
+ proposed diagnostics layer · over the real Maintain-run graph ·{' '}
987
+ {lastEvent ?? 'click an issue to focus its target'}
988
+ </div>
989
+ <div style={{ flex: 1, minHeight: 0 }}>
990
+ <SubsystemComponentGraph
991
+ components={MAINTAIN_COMPONENTS}
992
+ trails={MAINTAIN_TRAILS}
993
+ title="Maintain run"
994
+ description="Fire-and-forget RPC, wait for the finish event — picker → RPC → background runner → orchestrator → opencode V2 session, plus the liveness-probe handshake."
995
+ diagnostic={{ status, issueCount, onToggle: () => {} }}
996
+ issues={issues}
997
+ showIssues={showIssues}
998
+ focusIssueCategory={focusCategory}
999
+ onSelectIssue={(issue) =>
1000
+ setLastEvent(
1001
+ `focused ${issue.target?.label ?? issue.id} · ${issueCategory(issue)}`,
1002
+ )
1003
+ }
1004
+ onApplyIssueFix={(issue) => setLastEvent(`applied fix · ${issue.id}`)}
1005
+ onHoverIssue={(issue) =>
1006
+ setLastEvent(issue ? `hover ${issue.target?.label ?? issue.id}` : null)
1007
+ }
1008
+ />
1009
+ </div>
1010
+ </div>
1011
+ );
1012
+ }
1013
+
1014
+ /** The model as stored — clean (13/13 anchored). Contrast for the overlay. */
1015
+ export const MaintainRunClean: Story = {
1016
+ render: () => <MaintainRunGraph issues={[]} status="ok" />,
1017
+ };
1018
+
1019
+ /** The real graph in diagnostics mode, fed the illustrative findings above. */
1020
+ export const MaintainRunIssues: Story = {
1021
+ render: () => <MaintainRunGraph issues={MAINTAIN_ISSUES} status="issues" showIssues />,
1022
+ };
1023
+
1024
+ /** Diagnostics seeded from issue presence; land the list on the construct layer. */
1025
+ export const MaintainRunConstructLayer: Story = {
1026
+ render: () => (
1027
+ <MaintainRunGraph
1028
+ issues={MAINTAIN_ISSUES}
1029
+ status="issues"
1030
+ focusCategory="construct"
1031
+ />
1032
+ ),
1033
+ };