@polymorphism-tech/morph-spec 8.38.0 → 8.40.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 (227) hide show
  1. package/CLAUDE.md +19 -8
  2. package/README.md +2 -2
  3. package/bin/morph-spec.js +60 -43
  4. package/claude-plugin.json +1 -1
  5. package/docs/CHEATSHEET.md +1 -1
  6. package/docs/QUICKSTART.md +1 -1
  7. package/framework/CLAUDE.md +3 -2
  8. package/framework/MORPH.md +1 -1
  9. package/framework/agents.json +36 -25
  10. package/framework/docs/CLI.md +2 -2
  11. package/framework/evals/rubrics/code-quality.md +4 -4
  12. package/framework/evals/rubrics/condensed.md +2 -2
  13. package/framework/mcp/registry.json +33 -2
  14. package/framework/rules/infrastructure-standards.md +0 -1
  15. package/framework/rules/morph-workflow.md +9 -4
  16. package/framework/skills/core/create-umbraco-lp/SKILL.md +1 -1
  17. package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +1 -1
  18. package/framework/skills/core/morph-apply/SKILL.md +5 -1
  19. package/framework/skills/core/morph-brainstorming/SKILL.md +1 -1
  20. package/framework/skills/core/morph-eval/SKILL.md +1 -1
  21. package/framework/skills/core/morph-hotfix/SKILL.md +1 -1
  22. package/framework/skills/core/morph-implement/SKILL.md +6 -1
  23. package/framework/skills/core/morph-media/SKILL.md +1 -1
  24. package/framework/skills/core/morph-plan/SKILL.md +4 -3
  25. package/framework/skills/core/morph-proposal/SKILL.md +1 -1
  26. package/framework/skills/core/morph-review/SKILL.md +1 -1
  27. package/framework/skills/core/morph-stack-scan/SKILL.md +1 -1
  28. package/framework/skills/core/morph-standards/SKILL.md +1 -1
  29. package/framework/skills/core/morph-uiux/SKILL.md +6 -5
  30. package/framework/standards/STANDARDS.json +3 -3
  31. package/framework/standards/ai-agents/context-providers.md +16 -26
  32. package/framework/standards/ai-agents/durable-workflows-hitl.md +15 -21
  33. package/framework/standards/ai-agents/modalities-image-gen.md +21 -25
  34. package/framework/standards/ai-agents/modalities-speech-to-text.md +4 -15
  35. package/framework/standards/ai-agents/modalities-text-to-speech.md +2 -11
  36. package/framework/standards/ai-agents/modalities-vision.md +1 -18
  37. package/framework/standards/ai-agents/prompt-sources.md +0 -4
  38. package/framework/standards/architecture/code-comments.md +52 -16
  39. package/framework/standards/backend/integrations/neon-auth/neon-auth.md +63 -50
  40. package/framework/standards/frontend/design-system/ai-image-generation.md +5 -5
  41. package/framework/standards/integration/mcp/mcp-tools.md +48 -117
  42. package/framework/templates/code/dotnet/abstractions/result.cs +0 -2
  43. package/framework/templates/code/dotnet/ai-agents/AgentBootstrap.cs.template +26 -39
  44. package/framework/templates/code/dotnet/ai-agents/EmbeddingService.cs.template +28 -68
  45. package/framework/templates/code/dotnet/ai-agents/ModelRegistry.cs.template +99 -117
  46. package/framework/templates/code/dotnet/ai-agents/PromptRepository.cs.template +2 -44
  47. package/framework/templates/code/dotnet/ai-agents/ResilientToolMiddleware.cs.template +40 -61
  48. package/framework/templates/code/dotnet/ai-agents/VectorSearchTool.cs.template +54 -107
  49. package/framework/templates/code/dotnet/ai-agents/model-registry.json.template +53 -58
  50. package/framework/templates/code/dotnet/contracts/contracts-vsa.cs +192 -281
  51. package/framework/templates/code/dotnet/database/migration.cs +49 -83
  52. package/framework/templates/code/dotnet/database/vector-entity.cs +2 -46
  53. package/framework/templates/code/dotnet/extensions/result-extensions.cs +0 -2
  54. package/framework/templates/code/dotnet/feature/validator.cs.hbs +11 -15
  55. package/framework/templates/code/dotnet/jobs/agent.cs +3 -51
  56. package/framework/templates/code/dotnet/jobs/job.cs +2 -97
  57. package/framework/templates/code/dotnet/test.cs +2 -87
  58. package/framework/templates/code/typescript/contracts.ts +140 -167
  59. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Agents/AgentApi.cs +1 -48
  60. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Agents/AgentFactory.cs +3 -79
  61. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Agents/AgentFactoryOptions.cs +1 -70
  62. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Agents/AgentSpec.cs +1 -47
  63. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Agents/AgentSpecOptions.cs +2 -99
  64. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/AiKitAssemblyMarker.cs +0 -6
  65. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Conversation/ConversationStore.cs +3 -88
  66. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Conversation/IConversationStore.cs +1 -41
  67. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Conversation/InMemoryConversationStore.cs +0 -21
  68. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/CountingChatClient.cs +1 -25
  69. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalAssertion.cs +2 -63
  70. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalCacheMissException.cs +0 -30
  71. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalCacheOptions.cs +1 -52
  72. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalCacheStamp.cs +3 -78
  73. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalFixture.cs +4 -109
  74. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalMode.cs +0 -22
  75. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalQualityEvaluators.cs +1 -36
  76. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalScenario.cs +2 -57
  77. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalSummary.cs +0 -75
  78. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/OfflineChatClient.cs +2 -59
  79. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/ShortPathResponseCache.cs +4 -111
  80. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/IMediaGenerator.cs +0 -34
  81. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/IMediaProvider.cs +1 -57
  82. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaArtifact.cs +0 -19
  83. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaDisclosure.cs +2 -74
  84. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaGenerator.cs +2 -33
  85. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaIdempotencyKey.cs +1 -43
  86. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaJob.cs +1 -96
  87. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaJobId.cs +0 -13
  88. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaJobState.cs +0 -27
  89. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaPricing.cs +0 -33
  90. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaProvenance.cs +0 -31
  91. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaRequest.cs +3 -75
  92. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaResult.cs +0 -42
  93. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaReview.cs +0 -39
  94. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaSeams.cs +1 -55
  95. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaStore.cs +3 -59
  96. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MorphMedia.cs +0 -46
  97. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/Providers/GoogleImageProvider.cs +3 -28
  98. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/Providers/OpenAIImageProvider.cs +7 -61
  99. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/Providers/VeoVideoProvider.cs +2 -37
  100. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Middleware/IUsageCostSink.cs +1 -23
  101. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Middleware/UsageCostMiddleware.cs +2 -82
  102. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Middleware/UsageCostRecord.cs +1 -68
  103. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Observability/MorphOpenTelemetryExtensions.cs +1 -62
  104. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Observability/MorphTelemetryOptions.cs +2 -62
  105. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Providers/IProviderClientFactory.cs +1 -25
  106. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Providers/ModelAlias.cs +0 -124
  107. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Providers/ModelRegistry.cs +1 -73
  108. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Rag/DeltaIngest.cs +1 -66
  109. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Rag/DeltaPlan.cs +0 -41
  110. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/AgentFactoryTests.cs +2 -112
  111. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/AgentSpecTests.cs +2 -43
  112. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/ConversationStoreTests.cs +5 -110
  113. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/DeltaIngestTests.cs +7 -103
  114. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalAssertionTests.cs +1 -42
  115. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalCacheMissTests.cs +1 -30
  116. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalCacheStampTests.cs +1 -19
  117. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalFailuresTests.cs +0 -48
  118. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalFixtureTests.cs +1 -29
  119. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalRefreshTests.cs +2 -38
  120. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalWorkspace.cs +1 -14
  121. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/KitEvals.cs +4 -69
  122. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/OfflineChatClientTests.cs +1 -10
  123. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeChatClient.cs +1 -57
  124. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeChatClientTests.cs +0 -26
  125. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeClock.cs +1 -19
  126. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeMediaProvider.cs +0 -32
  127. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeProviderClientFactory.cs +0 -14
  128. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeUsageCostSink.cs +0 -24
  129. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/InMemoryMediaStore.cs +0 -32
  130. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaAccessibilityTests.cs +0 -37
  131. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaAliasIsNotAnAgentTests.cs +0 -24
  132. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaDisclosureTests.cs +0 -78
  133. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaGeneratorTests.cs +0 -53
  134. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaIdempotencyKeyTests.cs +0 -44
  135. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaJobStateTests.cs +0 -44
  136. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaReviewTests.cs +0 -19
  137. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaStoreTests.cs +0 -24
  138. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/ModelRegistryTests.cs +6 -48
  139. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/SkeletonSmokeTests.cs +1 -13
  140. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/TelemetryTests.cs +2 -71
  141. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/UsageCostMiddlewareTests.cs +5 -100
  142. package/framework/templates/frontend/nextjs/Dockerfile.nextjs.hbs +39 -43
  143. package/framework/templates/frontend/nextjs/env.mjs.hbs +29 -32
  144. package/framework/templates/frontend/nextjs/page.tsx.hbs +21 -22
  145. package/framework/templates/frontend/nextjs/use-feature.ts.hbs +42 -54
  146. package/framework/templates/integrations/asaas-client.cs +332 -387
  147. package/framework/templates/integrations/asaas-webhook.cs +290 -351
  148. package/framework/templates/integrations/neon-auth-config.cs +9 -105
  149. package/framework/templates/project/validate.js +97 -120
  150. package/package.json +2 -2
  151. package/src/commands/dashboard/dashboard.js +80 -53
  152. package/src/commands/mcp/mcp-setup.js +37 -9
  153. package/src/commands/project/doctor.js +3 -5
  154. package/src/commands/project/init.js +6 -3
  155. package/src/commands/project/install-plugin-cmd.js +1 -1
  156. package/src/commands/project/update.js +5 -0
  157. package/src/dashboard/app/App.jsx +80 -16
  158. package/src/dashboard/app/components/ActivityFeed.jsx +172 -0
  159. package/src/dashboard/app/components/CommandPalette.jsx +155 -0
  160. package/src/dashboard/app/components/DetailPanel.jsx +47 -11
  161. package/src/dashboard/app/components/EntityLink.jsx +55 -0
  162. package/src/dashboard/app/components/ErrorBoundary.jsx +54 -0
  163. package/src/dashboard/app/components/FlowGraph.jsx +19 -17
  164. package/src/dashboard/app/components/KnowledgeMarkdown.jsx +53 -0
  165. package/src/dashboard/app/components/Markdown.jsx +47 -0
  166. package/src/dashboard/app/components/PhaseRail.jsx +111 -0
  167. package/src/dashboard/app/components/SearchFilter.jsx +40 -22
  168. package/src/dashboard/app/components/Sidebar.jsx +146 -37
  169. package/src/dashboard/app/hooks/useMorphQuery.js +54 -0
  170. package/src/dashboard/app/hooks/usePref.js +38 -0
  171. package/src/dashboard/app/lib/format.js +69 -0
  172. package/src/dashboard/app/lib/phases.js +56 -0
  173. package/src/dashboard/app/lib/project.js +50 -0
  174. package/src/dashboard/app/pages/Activity.jsx +24 -0
  175. package/src/dashboard/app/pages/AgentMap.jsx +371 -370
  176. package/src/dashboard/app/pages/FeatureDetail.jsx +604 -0
  177. package/src/dashboard/app/pages/Features.jsx +400 -0
  178. package/src/dashboard/app/pages/Guide.jsx +146 -0
  179. package/src/dashboard/app/pages/HookInspector.jsx +357 -322
  180. package/src/dashboard/app/pages/PhasePipeline.jsx +277 -408
  181. package/src/dashboard/app/pages/Portfolio.jsx +156 -0
  182. package/src/dashboard/app/pages/Rubrics.jsx +101 -0
  183. package/src/dashboard/app/pages/RulesMap.jsx +300 -273
  184. package/src/dashboard/app/pages/SkillsLibrary.jsx +483 -366
  185. package/src/dashboard/app/pages/Standards.jsx +407 -332
  186. package/src/dashboard/app/styles/index.css +187 -47
  187. package/src/dashboard/index.html +14 -12
  188. package/src/dashboard/package.json +32 -24
  189. package/src/dashboard/scanner/activity-scanner.js +351 -0
  190. package/src/dashboard/scanner/agents-scanner.js +59 -1
  191. package/src/dashboard/scanner/docs-scanner.js +59 -0
  192. package/src/dashboard/scanner/feature-status.js +10 -0
  193. package/src/dashboard/scanner/features-scanner.js +360 -0
  194. package/src/dashboard/scanner/index.js +14 -7
  195. package/src/dashboard/scanner/phases-scanner.js +14 -15
  196. package/src/dashboard/scanner/portfolio.js +153 -0
  197. package/src/dashboard/scanner/project-api.js +195 -0
  198. package/src/dashboard/scanner/rules-scanner.js +42 -37
  199. package/src/dashboard/scanner/skills-scanner.js +105 -43
  200. package/src/dashboard/scanner/standards-scanner.js +2 -0
  201. package/src/dashboard/vite-plugin-morph-data.js +134 -17
  202. package/src/lib/detectors/claude-config-detector.js +2 -7
  203. package/src/lib/detectors/index.js +5 -1
  204. package/src/lib/evaluator-report.js +22 -0
  205. package/src/lib/generators/settings-generator.js +208 -209
  206. package/src/lib/installers/mcp-installer.js +724 -719
  207. package/src/lib/installers/memory-installer.js +1 -2
  208. package/src/lib/projects/registry.js +154 -0
  209. package/src/lib/validators/code-budget/code-budget-validator.js +32 -4
  210. package/src/scripts/install-plugin.js +1 -1
  211. package/src/scripts/setup-infra.js +2 -4
  212. package/src/utils/agents-installer.js +57 -12
  213. package/src/utils/banner.js +1 -1
  214. package/src/commands/index.js +0 -13
  215. package/src/commands/project/checkpoint.js +0 -209
  216. package/src/core/state/phase-state-machine.js +0 -197
  217. package/src/dashboard/app/hooks/useDetailPanel.js +0 -15
  218. package/src/dashboard/app/pages/LiveState.jsx +0 -206
  219. package/src/dashboard/scanner/squads-scanner.js +0 -13
  220. package/src/dashboard/scanner/state-scanner.js +0 -85
  221. package/src/lib/generators/index.js +0 -10
  222. package/src/lib/hooks/hook-executor.js +0 -167
  223. package/src/lib/validators/checkpoint-persistence.js +0 -39
  224. package/src/lib/validators/shared/emit-validator-dispatch.js +0 -14
  225. package/src/lib/validators/shared/index.js +0 -12
  226. package/src/lib/validators/shared/issue-counter.js +0 -14
  227. package/src/lib/validators/shared/result-formatter.js +0 -124
@@ -1,332 +1,407 @@
1
- import { useState, useMemo, useCallback } from 'react';
2
- import { Handle, Position } from '@xyflow/react';
3
- import morphData from 'virtual:morph-data';
4
- import FlowGraph from '../components/FlowGraph.jsx';
5
- import DetailPanel from '../components/DetailPanel.jsx';
6
- import SearchFilter from '../components/SearchFilter.jsx';
7
- import { useDetailPanel } from '../hooks/useDetailPanel.js';
8
-
9
- // ─── Category Color Palette ───────────────────────────────────────────────────
10
-
11
- const CATEGORY_COLORS = {
12
- 'ai-agents': '#34d399',
13
- 'architecture': '#f97316',
14
- 'backend': '#3b82f6',
15
- 'data': '#8b5cf6',
16
- 'frontend': '#f472b6',
17
- 'infrastructure': '#fbbf24',
18
- 'integration': '#06b6d4',
19
- };
20
-
21
- const CATEGORY_BG = {
22
- 'ai-agents': '#064e3b',
23
- 'architecture': '#431407',
24
- 'backend': '#1e3a5f',
25
- 'data': '#2e1065',
26
- 'frontend': '#500724',
27
- 'infrastructure': '#451a03',
28
- 'integration': '#083344',
29
- };
30
-
31
- function getCategoryColor(category) {
32
- return CATEGORY_COLORS[category] || '#94a3b8';
33
- }
34
- function getCategoryBg(category) {
35
- return CATEGORY_BG[category] || '#1e293b';
36
- }
37
-
38
- // ─── Node Components ──────────────────────────────────────────────────────────
39
-
40
- function StandardNode({ data }) {
41
- const color = getCategoryColor(data.category);
42
- const bg = getCategoryBg(data.category);
43
- return (
44
- <div
45
- className="rounded-lg px-3 py-2 min-w-[160px] max-w-[190px] cursor-pointer select-none"
46
- style={{
47
- background: bg,
48
- border: `1.5px solid ${data.dimmed ? bg : color}`,
49
- boxShadow: data.dimmed ? 'none' : `0 0 8px ${color}33`,
50
- opacity: data.dimmed ? 0.25 : 1,
51
- }}
52
- >
53
- <Handle type="target" position={Position.Left} style={{ background: color, width: 7, height: 7 }} />
54
- <Handle type="source" position={Position.Right} style={{ background: color, width: 7, height: 7 }} />
55
- <div className="text-white text-xs font-semibold leading-tight truncate" title={data.label}>
56
- {data.label}
57
- </div>
58
- <div className="text-[10px] mt-0.5 font-mono truncate" style={{ color }}>
59
- {data.category}
60
- {data.subcategory ? ` / ${data.subcategory}` : ''}
61
- </div>
62
- </div>
63
- );
64
- }
65
-
66
- function AgentRefNode({ data }) {
67
- return (
68
- <div
69
- className="rounded px-2 py-1 select-none cursor-default"
70
- style={{
71
- background: '#1e293b',
72
- border: '1px solid #334155',
73
- opacity: data.dimmed ? 0.2 : 1,
74
- }}
75
- >
76
- <Handle type="source" position={Position.Right} style={{ background: '#475569', width: 6, height: 6 }} />
77
- <div className="text-gray-400 text-[10px] font-mono truncate max-w-[110px]" title={data.label}>
78
- {data.label}
79
- </div>
80
- </div>
81
- );
82
- }
83
-
84
- const nodeTypes = { standardNode: StandardNode, agentRefNode: AgentRefNode };
85
-
86
- // ─── Graph Builder ────────────────────────────────────────────────────────────
87
-
88
- function buildGraph(standards, search, categoryFilter) {
89
- const nodes = [];
90
- const edges = [];
91
-
92
- const filtered = standards.filter((s) => {
93
- const q = search.toLowerCase();
94
- const matchSearch = !q || s.name.toLowerCase().includes(q) || s.id.toLowerCase().includes(q) || s.category.toLowerCase().includes(q);
95
- const matchCat = !categoryFilter || s.category === categoryFilter;
96
- return matchSearch && matchCat;
97
- });
98
- const filteredIds = new Set(filtered.map((s) => s.id));
99
- const hasFilter = search || categoryFilter;
100
-
101
- // Group by category
102
- const byCategory = {};
103
- for (const std of standards) {
104
- if (!byCategory[std.category]) byCategory[std.category] = [];
105
- byCategory[std.category].push(std);
106
- }
107
-
108
- const STD_X = 180;
109
- const AGENT_X = 0;
110
- const ROW_H = 75;
111
- const CAT_GAP = 40;
112
-
113
- let globalY = 0;
114
-
115
- // Track all agent refs that need separate nodes
116
- const agentNodeMap = {}; // agentId → { x, y }
117
- const agentYTracker = {};
118
-
119
- for (const [cat, catStandards] of Object.entries(byCategory)) {
120
- catStandards.forEach((std, i) => {
121
- const isDimmed = hasFilter && !filteredIds.has(std.id);
122
- nodes.push({
123
- id: `std-${std.id}`,
124
- type: 'standardNode',
125
- position: { x: STD_X, y: globalY + i * ROW_H },
126
- data: {
127
- label: std.name,
128
- category: std.category,
129
- subcategory: std.subcategory,
130
- stdRef: std,
131
- dimmed: isDimmed,
132
- },
133
- });
134
-
135
- // Agent ref nodes on the left
136
- std.referencedBy.forEach((ref, j) => {
137
- const agentNodeId = `agent-${ref.agentId}`;
138
- if (!agentNodeMap[agentNodeId]) {
139
- const ay = agentYTracker[ref.agentId] ?? globalY + i * ROW_H;
140
- agentYTracker[ref.agentId] = ay + 55;
141
- agentNodeMap[agentNodeId] = { x: AGENT_X, y: ay };
142
- nodes.push({
143
- id: agentNodeId,
144
- type: 'agentRefNode',
145
- position: { x: AGENT_X, y: ay },
146
- data: { label: ref.agentId, dimmed: isDimmed },
147
- });
148
- }
149
- const edgeLabel = [ref.scope, ref.priority].filter(Boolean).join('/');
150
- edges.push({
151
- id: `edge-${ref.agentId}-${std.id}-${j}`,
152
- source: agentNodeId,
153
- target: `std-${std.id}`,
154
- type: 'smoothstep',
155
- label: edgeLabel || undefined,
156
- labelStyle: { fill: '#64748b', fontSize: 9 },
157
- labelBgStyle: { fill: '#0f172a', fillOpacity: 0.85 },
158
- style: {
159
- stroke: isDimmed ? '#1e293b' : getCategoryColor(std.category),
160
- strokeWidth: 1,
161
- opacity: isDimmed ? 0.15 : 0.4,
162
- },
163
- });
164
- });
165
- });
166
-
167
- globalY += catStandards.length * ROW_H + CAT_GAP;
168
- }
169
-
170
- return { nodes, edges };
171
- }
172
-
173
- // ─── Detail Content ───────────────────────────────────────────────────────────
174
-
175
- function StandardDetail({ std }) {
176
- const [showContent, setShowContent] = useState(false);
177
- const color = getCategoryColor(std.category);
178
-
179
- return (
180
- <div className="text-sm text-gray-200 space-y-4">
181
- <div>
182
- <p className="text-gray-400 text-xs">ID: <span className="font-mono text-gray-300">{std.id}</span></p>
183
- <p className="text-gray-400 text-xs mt-0.5">Path: <span className="font-mono text-gray-300">{std.path}</span></p>
184
- </div>
185
-
186
- <div className="flex flex-wrap gap-1">
187
- <span className="px-2 py-0.5 rounded text-xs font-bold" style={{ background: getCategoryBg(std.category), color, border: `1px solid ${color}` }}>
188
- {std.category}
189
- </span>
190
- {std.subcategory && (
191
- <span className="px-2 py-0.5 rounded text-xs bg-gray-800 border border-gray-700 text-gray-400">
192
- {std.subcategory}
193
- </span>
194
- )}
195
- </div>
196
-
197
- {std.digest && (
198
- <div>
199
- <p className="text-gray-500 text-xs uppercase tracking-wide mb-1.5">Digest</p>
200
- <p className="text-gray-300 text-xs leading-relaxed">{std.digest}</p>
201
- </div>
202
- )}
203
-
204
- {std.tags?.length > 0 && (
205
- <div>
206
- <p className="text-gray-500 text-xs uppercase tracking-wide mb-1.5">Tags</p>
207
- <div className="flex flex-wrap gap-1">
208
- {std.tags.map((t) => (
209
- <span key={t} className="px-1.5 py-0.5 bg-gray-800 border border-gray-700 rounded text-xs text-gray-400">{t}</span>
210
- ))}
211
- </div>
212
- </div>
213
- )}
214
-
215
- {std.stacks?.length > 0 && (
216
- <div>
217
- <p className="text-gray-500 text-xs uppercase tracking-wide mb-1.5">Stacks</p>
218
- <div className="flex flex-wrap gap-1">
219
- {std.stacks.map((s) => (
220
- <span key={s} className="px-2 py-0.5 bg-green-900/50 border border-green-700 rounded text-xs text-green-300">{s}</span>
221
- ))}
222
- </div>
223
- </div>
224
- )}
225
-
226
- {std.referencedBy?.length > 0 && (
227
- <div>
228
- <p className="text-gray-500 text-xs uppercase tracking-wide mb-1.5">Referenced By ({std.referencedBy.length})</p>
229
- <div className="space-y-1">
230
- {std.referencedBy.map((ref, i) => (
231
- <div key={i} className="flex items-center justify-between text-xs px-2 py-1 bg-gray-800 rounded">
232
- <span className="font-mono text-gray-300 truncate">{ref.agentId}</span>
233
- <div className="flex gap-1 ml-2 flex-shrink-0">
234
- {ref.scope && <span className="px-1.5 py-0.5 bg-gray-700 rounded text-gray-400">{ref.scope}</span>}
235
- {ref.priority && <span className="px-1.5 py-0.5 bg-gray-700 rounded text-gray-400">{ref.priority}</span>}
236
- </div>
237
- </div>
238
- ))}
239
- </div>
240
- </div>
241
- )}
242
-
243
- <div>
244
- <button
245
- onClick={() => setShowContent((v) => !v)}
246
- className="w-full text-left px-3 py-1.5 bg-gray-800 hover:bg-gray-700 border border-gray-700 rounded text-xs text-gray-300 transition-colors"
247
- >
248
- {showContent ? 'Hide Content' : 'Show Full Content'}
249
- </button>
250
- {showContent && (
251
- <pre className="mt-2 p-3 bg-gray-950 border border-gray-800 rounded text-[9px] text-gray-400 overflow-x-auto whitespace-pre-wrap font-mono leading-relaxed max-h-64 overflow-y-auto">
252
- {std.content}
253
- </pre>
254
- )}
255
- </div>
256
- </div>
257
- );
258
- }
259
-
260
- // ─── Main Page ────────────────────────────────────────────────────────────────
261
-
262
- export default function Standards() {
263
- const { standards } = morphData;
264
- const [search, setSearch] = useState('');
265
- const [categoryFilter, setCategoryFilter] = useState('');
266
- const { panel, openPanel, closePanel } = useDetailPanel();
267
-
268
- const allCategories = useMemo(
269
- () => [...new Set(standards.list.map((s) => s.category))].sort(),
270
- [standards.list]
271
- );
272
-
273
- const { nodes, edges } = useMemo(
274
- () => buildGraph(standards.list, search, categoryFilter),
275
- [standards.list, search, categoryFilter]
276
- );
277
-
278
- const handleNodeClick = useCallback(
279
- (_event, node) => {
280
- if (node.type === 'standardNode') {
281
- const std = node.data.stdRef;
282
- if (std) openPanel(std.name, <StandardDetail std={std} />);
283
- }
284
- },
285
- [openPanel]
286
- );
287
-
288
- const filterConfig = [
289
- { name: 'category', label: 'Category', options: allCategories, value: categoryFilter },
290
- ];
291
-
292
- return (
293
- <div className="flex flex-col h-full">
294
- <SearchFilter
295
- placeholder="Search standards by name, ID, or category..."
296
- filters={filterConfig}
297
- onSearchChange={setSearch}
298
- onFilterChange={(name, value) => {
299
- if (name === 'category') setCategoryFilter(value);
300
- }}
301
- />
302
-
303
- <div className="flex-1 relative">
304
- <FlowGraph
305
- nodes={nodes}
306
- edges={edges}
307
- nodeTypes={nodeTypes}
308
- onNodeClick={handleNodeClick}
309
- fitView
310
- />
311
-
312
- <div className="absolute top-3 left-3 bg-gray-900/90 border border-gray-700 rounded-lg p-3 text-xs space-y-1.5 z-10">
313
- <p className="text-gray-400 font-semibold uppercase tracking-wide text-[10px] mb-2">Categories</p>
314
- {Object.entries(CATEGORY_COLORS).map(([cat, color]) => (
315
- <div key={cat} className="flex items-center gap-2">
316
- <span className="inline-block w-3 h-3 rounded-sm flex-shrink-0" style={{ background: getCategoryBg(cat), border: `1px solid ${color}` }} />
317
- <span className="text-gray-300">{cat}</span>
318
- </div>
319
- ))}
320
- </div>
321
-
322
- <div className="absolute top-3 right-3 bg-gray-900/90 border border-gray-700 rounded-lg px-3 py-1.5 text-xs text-gray-400 z-10">
323
- {standards.list.length} standards · v{standards.version}
324
- </div>
325
- </div>
326
-
327
- <DetailPanel open={panel.open} title={panel.title} onClose={closePanel}>
328
- {panel.content}
329
- </DetailPanel>
330
- </div>
331
- );
332
- }
1
+ import { useState, useMemo, useCallback } from 'react';
2
+ import { Handle, Position } from '@xyflow/react';
3
+ import { Robot } from '@phosphor-icons/react';
4
+ import morphData from 'virtual:morph-data';
5
+ import FlowGraph from '../components/FlowGraph.jsx';
6
+ import DetailPanel from '../components/DetailPanel.jsx';
7
+ import SearchFilter from '../components/SearchFilter.jsx';
8
+ import Markdown from '../components/Markdown.jsx';
9
+ import EntityLink, { useSelection, standardKey } from '../components/EntityLink.jsx';
10
+
11
+ // ─── Colors ───────────────────────────────────────────────────────────────────
12
+
13
+ /** Token palette the categories draw from, in order; the categories come from the data. */
14
+ const PALETTE = [
15
+ 'var(--color-phase-implement)',
16
+ 'var(--color-phase-uiux)',
17
+ 'var(--color-phase-proposal)',
18
+ 'var(--color-phase-plan)',
19
+ 'var(--color-ok)',
20
+ 'var(--color-warn)',
21
+ 'var(--color-phase-review)',
22
+ 'var(--color-dim)',
23
+ ];
24
+
25
+ const tint = (color, pct = 14) => `color-mix(in srgb, ${color} ${pct}%, var(--color-core))`;
26
+
27
+ /** category → color, stable by category name so a color never jumps between renders. */
28
+ function categoryColors(standards) {
29
+ const cats = [...new Set(standards.map((s) => s.category || 'outros'))].sort();
30
+ return Object.fromEntries(cats.map((c, i) => [c, PALETTE[i % PALETTE.length]]));
31
+ }
32
+
33
+ /** Resolve `?sel=` to a standard: by id first, then by the path a task cites. */
34
+ export function findStandard(list, sel) {
35
+ if (!sel) return null;
36
+ return list.find((s) => s.id === sel) || list.find((s) => s.path === standardKey(sel)) || null;
37
+ }
38
+
39
+ // ─── Nodes ────────────────────────────────────────────────────────────────────
40
+
41
+ const HANDLE = { width: 7, height: 7, border: 0 };
42
+ const NODE_W = 230;
43
+ const NODE_H = 52;
44
+
45
+ function StandardNode({ data }) {
46
+ return (
47
+ <div
48
+ className="cursor-pointer select-none rounded-xl px-3 py-1.5 transition-opacity"
49
+ style={{
50
+ width: NODE_W, height: NODE_H,
51
+ background: tint(data.color, data.selected ? 30 : 12),
52
+ boxShadow: `inset 0 0 0 ${data.selected ? 2 : 1.25}px ${data.color}`,
53
+ opacity: data.dimmed ? 0.18 : 1,
54
+ }}
55
+ >
56
+ <Handle type="target" position={Position.Top} style={{ ...HANDLE, background: data.color }} />
57
+ <div className="truncate text-[13px] font-medium leading-tight text-[var(--color-fg)]" title={data.label}>{data.label}</div>
58
+ <div className="mt-0.5 truncate text-xs text-[var(--color-dim)]" title={data.sub}>{data.sub}</div>
59
+ </div>
60
+ );
61
+ }
62
+
63
+ function AgentNode({ data }) {
64
+ return (
65
+ <div
66
+ className="flex cursor-pointer select-none items-center gap-2 rounded-full px-4 py-2 transition-opacity"
67
+ style={{
68
+ width: NODE_W,
69
+ background: data.active ? tint('var(--color-fg)', 16) : 'var(--color-raised)',
70
+ boxShadow: `inset 0 0 0 ${data.active ? 2 : 1}px ${data.active ? 'var(--color-fg)' : 'var(--color-hair-strong)'}`,
71
+ opacity: data.dimmed ? 0.3 : 1,
72
+ }}
73
+ >
74
+ <Robot weight="light" size={16} className="shrink-0 text-[var(--color-dim)]" />
75
+ <div className="min-w-0">
76
+ <div className="truncate font-mono text-[13px] text-[var(--color-fg)]" title={data.title}>morph-{data.agentId}</div>
77
+ <div className="text-xs text-[var(--color-dim)]">{data.count} standards</div>
78
+ </div>
79
+ <Handle type="source" position={Position.Bottom} style={{ ...HANDLE, background: 'var(--color-dim)' }} />
80
+ </div>
81
+ );
82
+ }
83
+
84
+ function CategoryNode({ data }) {
85
+ return (
86
+ <div
87
+ className="pointer-events-none rounded-2xl"
88
+ style={{ width: data.width, height: data.height, background: tint(data.color, 5), boxShadow: `inset 0 0 0 1px color-mix(in srgb, ${data.color} 40%, transparent)` }}
89
+ >
90
+ <div className="flex items-baseline gap-2 px-4 pt-3">
91
+ <span className="text-sm font-semibold" style={{ color: data.color }}>{data.label}</span>
92
+ <span className="text-xs text-[var(--color-dim)]">{data.count}</span>
93
+ </div>
94
+ </div>
95
+ );
96
+ }
97
+
98
+ const nodeTypes = { standardNode: StandardNode, agentNode: AgentNode, categoryNode: CategoryNode };
99
+
100
+ // ─── Layout ───────────────────────────────────────────────────────────────────
101
+
102
+ const CELL_W = NODE_W + 14;
103
+ const CELL_H = NODE_H + 12;
104
+ const PAD = 16;
105
+ const HEADER = 40;
106
+ const BLOCK_GAP = 36;
107
+ const MAX_ROW_W = 1700;
108
+
109
+ /** How many columns a category block gets: tall categories spread sideways. */
110
+ function colsFor(n) {
111
+ if (n > 12) return 3;
112
+ if (n > 5) return 2;
113
+ return 1;
114
+ }
115
+
116
+ function buildGraph({ standards, agents, colors, isMatch, hasFilter, persona, selectedId }) {
117
+ const nodes = [];
118
+ const edges = [];
119
+
120
+ const byCat = new Map();
121
+ for (const s of standards) {
122
+ const c = s.category || 'outros';
123
+ if (!byCat.has(c)) byCat.set(c, []);
124
+ byCat.get(c).push(s);
125
+ }
126
+ const cats = [...byCat.entries()]
127
+ .map(([cat, list]) => [cat, [...list].sort((a, b) => (a.subcategory || '').localeCompare(b.subcategory || '') || a.name.localeCompare(b.name))])
128
+ .sort((a, b) => b[1].length - a[1].length || a[0].localeCompare(b[0]));
129
+
130
+ // Pack the category blocks into rows no wider than MAX_ROW_W.
131
+ const blocks = cats.map(([cat, list]) => {
132
+ const cols = colsFor(list.length);
133
+ const rows = Math.ceil(list.length / cols);
134
+ return { cat, list, cols, w: cols * CELL_W + PAD * 2 - 14, h: HEADER + rows * CELL_H + PAD - 12 };
135
+ });
136
+ const rows = [];
137
+ for (const b of blocks) {
138
+ const row = rows[rows.length - 1];
139
+ if (row && row.w + BLOCK_GAP + b.w <= MAX_ROW_W) { row.items.push(b); row.w += BLOCK_GAP + b.w; row.h = Math.max(row.h, b.h); }
140
+ else rows.push({ items: [b], w: b.w, h: b.h });
141
+ }
142
+ const totalW = Math.max(...rows.map((r) => r.w), 0);
143
+
144
+ const std = new Set(); // node ids present
145
+ let y = 180;
146
+ for (const row of rows) {
147
+ let x = (totalW - row.w) / 2;
148
+ for (const b of row.items) {
149
+ const color = colors[b.cat];
150
+ nodes.push({
151
+ id: `cat:${b.cat}`, type: 'categoryNode', position: { x, y },
152
+ data: { label: b.cat, count: b.list.length, color, width: b.w, height: b.h },
153
+ selectable: false, draggable: false, focusable: false, zIndex: -1,
154
+ });
155
+ b.list.forEach((s, i) => {
156
+ const col = i % b.cols;
157
+ const r = Math.floor(i / b.cols);
158
+ nodes.push({
159
+ id: `std:${s.id}`, type: 'standardNode',
160
+ position: { x: x + PAD + col * CELL_W, y: y + HEADER + r * CELL_H },
161
+ data: {
162
+ label: s.name, sub: s.subcategory || s.path, color, stdId: s.id,
163
+ dimmed: hasFilter && !isMatch(s), selected: s.id === selectedId,
164
+ },
165
+ });
166
+ std.add(s.id);
167
+ });
168
+ x += b.w + BLOCK_GAP;
169
+ }
170
+ y += row.h + BLOCK_GAP;
171
+ }
172
+
173
+ // The personas on top, each with an edge to every standard it consults.
174
+ const withStandards = agents.filter((a) => (a.standards || []).some((id) => std.has(id)));
175
+ const agentX0 = (totalW - withStandards.length * (NODE_W + 24)) / 2;
176
+ const byId = new Map(standards.map((s) => [s.id, s]));
177
+ withStandards.forEach((a, i) => {
178
+ const ids = (a.standards || []).filter((id) => std.has(id));
179
+ const active = persona === a.id;
180
+ nodes.push({
181
+ id: `agent:${a.id}`, type: 'agentNode', position: { x: agentX0 + i * (NODE_W + 24), y: 0 },
182
+ data: { agentId: a.id, title: a.title, count: ids.length, active, dimmed: Boolean(persona) && !active },
183
+ });
184
+ for (const id of ids) {
185
+ const s = byId.get(id);
186
+ const lit = persona ? active : !hasFilter || isMatch(s);
187
+ edges.push({
188
+ id: `e:${a.id}->${id}`, source: `agent:${a.id}`, target: `std:${id}`, type: 'default',
189
+ style: { stroke: colors[s.category || 'outros'], strokeWidth: lit && persona ? 1.5 : 1, opacity: lit ? (persona ? 0.85 : 0.22) : 0.04 },
190
+ });
191
+ }
192
+ });
193
+
194
+ return { nodes, edges, categories: cats.map(([c, l]) => [c, l.length]) };
195
+ }
196
+
197
+ // ─── Detail ───────────────────────────────────────────────────────────────────
198
+
199
+ function Section({ title, children }) {
200
+ return (
201
+ <section className="space-y-2">
202
+ <h3 className="text-xs font-medium text-[var(--color-faint)]">{title}</h3>
203
+ {children}
204
+ </section>
205
+ );
206
+ }
207
+
208
+ function jumpTo(anchor) {
209
+ if (typeof document === 'undefined') return;
210
+ const id = decodeURIComponent(String(anchor).replace(/^#/, ''));
211
+ document.getElementById(id)?.scrollIntoView({ behavior: 'smooth', block: 'start' });
212
+ }
213
+
214
+ export function StandardDetail({ std, color = 'var(--color-dim)' }) {
215
+ const aliases = std.aliases || [];
216
+ const anchors = Object.entries(std.anchors || {});
217
+ const refs = std.referencedBy || [];
218
+ const provedBy = std.provedBy || [];
219
+ const stacks = std.stacks || [];
220
+ const verified = typeof std.verifiedAgainst === 'string' ? std.verifiedAgainst.trim() : '';
221
+
222
+ return (
223
+ <div className="space-y-6 text-sm text-[var(--color-dim)]">
224
+ <div className="flex flex-wrap items-center gap-1.5">
225
+ <span className="rounded-full px-2.5 py-0.5 text-xs font-medium" style={{ color, background: tint(color, 18), boxShadow: `inset 0 0 0 1px ${color}` }}>
226
+ {std.category}
227
+ </span>
228
+ {std.subcategory && (
229
+ <span className="rounded-full px-2.5 py-0.5 text-xs shadow-[inset_0_0_0_1px_var(--color-hair-strong)]">{std.subcategory}</span>
230
+ )}
231
+ {stacks.map((s) => (
232
+ <span key={s} className="rounded-full px-2 py-0.5 font-mono text-xs shadow-[inset_0_0_0_1px_var(--color-hair)]">stack {s}</span>
233
+ ))}
234
+ </div>
235
+
236
+ <div className="font-mono text-xs text-[var(--color-faint)]">
237
+ <div>id <span className="text-[var(--color-dim)]">{std.id}</span></div>
238
+ {std.path && <div>arquivo <span className="text-[var(--color-dim)]">standards/{std.path}</span></div>}
239
+ </div>
240
+
241
+ {std.digest && (
242
+ <Section title="Resumo">
243
+ <p className="leading-relaxed text-[var(--color-fg)]">{std.digest}</p>
244
+ </Section>
245
+ )}
246
+
247
+ {aliases.length > 0 && (
248
+ <Section title="Também encontrado por">
249
+ <div className="flex flex-wrap gap-1.5">
250
+ {aliases.map((a, i) => (
251
+ <span key={`${a}-${i}`} className="rounded-full px-2 py-0.5 text-xs shadow-[inset_0_0_0_1px_var(--color-hair)]">{a}</span>
252
+ ))}
253
+ </div>
254
+ </Section>
255
+ )}
256
+
257
+ {anchors.length > 0 && (
258
+ <Section title="Seções">
259
+ <ul className="grid grid-cols-[max-content_1fr] gap-x-3 gap-y-1">
260
+ {anchors.map(([name, href]) => (
261
+ <li key={name} className="contents">
262
+ <span className="font-mono text-xs leading-6 text-[var(--color-faint)]">{name}</span>
263
+ <button
264
+ type="button" onClick={() => jumpTo(href)}
265
+ className="truncate text-left font-mono text-xs leading-6 text-[var(--color-phase-proposal)] hover:underline"
266
+ >
267
+ {href}
268
+ </button>
269
+ </li>
270
+ ))}
271
+ </ul>
272
+ </Section>
273
+ )}
274
+
275
+ <Section title={refs.length ? `Consultado por (${refs.length})` : 'Consultado por'}>
276
+ {refs.length ? (
277
+ <div className="flex flex-wrap gap-1.5">
278
+ {refs.map((r, i) => <EntityLink key={`${r.agentId}-${i}`} kind="persona" id={r.agentId} chip>{`morph-${r.agentId}`}</EntityLink>)}
279
+ </div>
280
+ ) : (
281
+ <p>Nenhuma persona lista este standard; ele é achado por busca (<code className="font-mono text-xs">morph-spec standards --find</code>).</p>
282
+ )}
283
+ </Section>
284
+
285
+ {provedBy.length > 0 && (
286
+ <Section title="Provado por">
287
+ <ul className="space-y-1">
288
+ {provedBy.map((p, i) => (
289
+ <li key={`${p}-${i}`} className="break-all font-mono text-xs text-[var(--color-fg)]">{p}</li>
290
+ ))}
291
+ </ul>
292
+ </Section>
293
+ )}
294
+
295
+ {verified && (
296
+ <Section title="Verificado contra">
297
+ <Markdown text={verified} size="sm" />
298
+ </Section>
299
+ )}
300
+
301
+ {std.content && (
302
+ <Section title="Conteúdo completo">
303
+ <Markdown text={std.content} size="sm" />
304
+ </Section>
305
+ )}
306
+ </div>
307
+ );
308
+ }
309
+
310
+ // ─── Page ─────────────────────────────────────────────────────────────────────
311
+
312
+ export default function Standards() {
313
+ const standards = morphData.standards?.list || [];
314
+ const version = morphData.standards?.version;
315
+ const agents = morphData.agents?.list || [];
316
+ const [sel, setSel] = useSelection();
317
+ const [search, setSearch] = useState('');
318
+ const [categoryFilter, setCategoryFilter] = useState('');
319
+ const [persona, setPersona] = useState('');
320
+
321
+ const colors = useMemo(() => categoryColors(standards), [standards]);
322
+ const selected = useMemo(() => findStandard(standards, sel), [standards, sel]);
323
+
324
+ const isMatch = useCallback((s) => {
325
+ const q = search.trim().toLowerCase();
326
+ if (q) {
327
+ const hay = [s.id, s.name, s.path, s.digest, s.category, s.subcategory, ...(s.aliases || []), ...(s.tags || [])]
328
+ .filter(Boolean).join(' ').toLowerCase();
329
+ if (!hay.includes(q)) return false;
330
+ }
331
+ if (categoryFilter && s.category !== categoryFilter) return false;
332
+ if (persona && !(s.referencedBy || []).some((r) => r.agentId === persona)) return false;
333
+ return true;
334
+ }, [search, categoryFilter, persona]);
335
+
336
+ const hasFilter = Boolean(search.trim() || categoryFilter || persona);
337
+ const matchCount = useMemo(() => standards.filter(isMatch).length, [standards, isMatch]);
338
+ const orphanCount = standards.filter((s) => !(s.referencedBy || []).length).length;
339
+
340
+ const graph = useMemo(
341
+ () => buildGraph({ standards, agents, colors, isMatch, hasFilter, persona, selectedId: selected?.id }),
342
+ [standards, agents, colors, isMatch, hasFilter, persona, selected?.id],
343
+ );
344
+
345
+ const personaOptions = useMemo(
346
+ () => agents.filter((a) => (a.standards || []).length).map((a) => a.id),
347
+ [agents],
348
+ );
349
+
350
+ const close = useCallback(() => setSel(null), [setSel]);
351
+ const onNodeClick = useCallback((_e, node) => {
352
+ if (node.type === 'standardNode') setSel(node.data.stdId);
353
+ else if (node.type === 'agentNode') setPersona((p) => (p === node.data.agentId ? '' : node.data.agentId));
354
+ }, [setSel]);
355
+
356
+ const filters = [
357
+ { name: 'category', label: 'Categoria', options: graph.categories.map(([c]) => c), value: categoryFilter },
358
+ { name: 'persona', label: 'Persona', options: personaOptions, value: persona },
359
+ ];
360
+
361
+ return (
362
+ <div className="flex h-full flex-col">
363
+ <SearchFilter
364
+ placeholder="Buscar standard por nome, id, caminho, resumo ou apelido"
365
+ filters={filters}
366
+ onSearchChange={setSearch}
367
+ onFilterChange={(name, value) => {
368
+ if (name === 'category') setCategoryFilter(value);
369
+ if (name === 'persona') setPersona(value);
370
+ }}
371
+ />
372
+
373
+ <div className="relative flex-1">
374
+ <FlowGraph nodes={graph.nodes} edges={graph.edges} nodeTypes={nodeTypes} onNodeClick={onNodeClick} fitView />
375
+
376
+ <div className="absolute left-3 top-3 z-10 space-y-1.5 rounded-xl bg-[var(--color-ink)]/85 p-3 text-xs text-[var(--color-dim)] shadow-[inset_0_0_0_1px_var(--color-hair)] backdrop-blur">
377
+ <p className="mb-1 text-[13px] font-medium text-[var(--color-fg)]">Categorias</p>
378
+ {graph.categories.map(([cat, n]) => (
379
+ <button
380
+ key={cat} type="button"
381
+ onClick={() => setCategoryFilter((c) => (c === cat ? '' : cat))}
382
+ className="flex w-full items-center gap-2 text-left hover:text-[var(--color-fg)]"
383
+ style={{ opacity: categoryFilter && categoryFilter !== cat ? 0.45 : 1 }}
384
+ >
385
+ <span className="inline-block h-3 w-3 shrink-0 rounded" style={{ background: tint(colors[cat], 30), boxShadow: `inset 0 0 0 1px ${colors[cat]}` }} />
386
+ <span>{cat}</span>
387
+ <span className="ml-auto pl-3 text-[var(--color-faint)]">{n}</span>
388
+ </button>
389
+ ))}
390
+ <p className="border-t border-[var(--color-hair)] pt-2 text-[var(--color-faint)]">clique numa persona para ver os standards dela</p>
391
+ </div>
392
+
393
+ <div className="absolute right-3 top-3 z-10 space-y-0.5 rounded-xl bg-[var(--color-ink)]/85 px-3 py-2 text-xs text-[var(--color-dim)] shadow-[inset_0_0_0_1px_var(--color-hair)] backdrop-blur">
394
+ <div className="text-[13px] text-[var(--color-fg)]">
395
+ {hasFilter ? `${matchCount} de ${standards.length} standards` : `${standards.length} standards`}
396
+ </div>
397
+ <div>{graph.categories.length} categorias{version ? ` · registro v${version}` : ''}</div>
398
+ <div>{orphanCount} sem persona (achados por busca)</div>
399
+ </div>
400
+ </div>
401
+
402
+ <DetailPanel open={Boolean(selected)} title={selected?.name || ''} subtitle={selected?.id || ''} onClose={close}>
403
+ {selected && <StandardDetail std={selected} color={colors[selected.category || 'outros']} />}
404
+ </DetailPanel>
405
+ </div>
406
+ );
407
+ }