Repository: l2beat/l2beat
Pull request #7054: Add Scope of assessment to Risk analysis
Author: login: maciekzygmunt
Created: 2025-03-06T11:54:30Z | Merged: 2025-03-06T12:30:47Z

Resolves L2B-9455


Diff:
diff --git a/packages/config/src/internalTypes.ts b/packages/config/src/internalTypes.ts
index bd5938e6fc8e..03993ef930a4 100644
--- a/packages/config/src/internalTypes.ts
+++ b/packages/config/src/internalTypes.ts
@@ -29,6 +29,7 @@ import type {
   ProjectScalingDa,
   ProjectScalingPurpose,
   ProjectScalingRiskView,
+  ProjectScalingScopeOfAssessment,
   ProjectScalingStack,
   ProjectScalingStage,
   ProjectScalingStateDerivation,
@@ -82,6 +83,7 @@ export interface ScalingProject {
   badges?: Badge[]
   /** Reasons why the scaling project is included in the other categories. If defined - project will be displayed as other */
   reasonsForBeingOther?: ReasonForBeingInOther[]
+  scopeOfAssessment?: ProjectScalingScopeOfAssessment
   /** Discodrive markers */
   discoveryInfo?: ProjectDiscoveryInfo
 }
diff --git a/packages/config/src/projects/project/getProjects.ts b/packages/config/src/projects/project/getProjects.ts
index 5467e1b23831..910c3760f4ef 100644
--- a/packages/config/src/projects/project/getProjects.ts
+++ b/packages/config/src/projects/project/getProjects.ts
@@ -94,6 +94,7 @@ function layer2Or3ToProject(
       daLayer: p.dataAvailability?.layer.value ?? 'Unknown',
       stage: getStage(p.stage),
       purposes: p.display.purposes,
+      scopeOfAssessment: p.scopeOfAssessment,
     },
     scalingStage: p.stage,
     scalingRisks: {
diff --git a/packages/config/src/types.ts b/packages/config/src/types.ts
index 8239f7f24314..1b5fde5597eb 100644
--- a/packages/config/src/types.ts
+++ b/packages/config/src/types.ts
@@ -274,6 +274,7 @@ export interface ProjectScalingInfo {
   daLayer: string
   stage: ProjectStageName
   purposes: ProjectScalingPurpose[]
+  scopeOfAssessment: ProjectScalingScopeOfAssessment | undefined
 }
 
 export type ProjectScalingCategory =
@@ -286,6 +287,11 @@ export type ProjectScalingCategory =
 
 export type ProjectScalingCapability = 'universal' | 'appchain'
 
+export type ProjectScalingScopeOfAssessment = {
+  checked: string[]
+  notChecked: string[]
+}
+
 export interface ReasonForBeingInOther {
   label: string
   shortDescription: string
diff --git a/packages/frontend/src/components/projects/sections/scope-of-assessment.tsx b/packages/frontend/src/components/projects/sections/scope-of-assessment.tsx
new file mode 100644
index 000000000000..6cfa1e37cf2f
--- /dev/null
+++ b/packages/frontend/src/components/projects/sections/scope-of-assessment.tsx
@@ -0,0 +1,81 @@
+import type { ProjectScalingScopeOfAssessment } from '@l2beat/config'
+import {
+  Collapsible,
+  CollapsibleContent,
+  CollapsibleTrigger,
+} from '~/components/core/collapsible'
+import { Markdown } from '~/components/markdown/markdown'
+import { ChevronIcon } from '~/icons/chevron'
+import { MissingIcon } from '~/icons/missing'
+import { SatisfiedIcon } from '~/icons/satisfied'
+import { cn } from '~/utils/cn'
+
+export function ScopeOfAssessment({
+  scopeOfAssessment,
+  className,
+}: {
+  scopeOfAssessment: ProjectScalingScopeOfAssessment
+  className?: string
+}) {
+  return (
+    <Collapsible
+      className={cn('group rounded-lg border border-divider', className)}
+      defaultOpen
+    >
+      <CollapsibleTrigger className="flex w-full items-center justify-between px-4 py-3 font-bold md:px-6">
+        Scope of assessment
+        <ChevronIcon className="size-4 transition-transform duration-300 group-data-[state=open]:-rotate-180" />
+      </CollapsibleTrigger>
+      <CollapsibleContent className="space-y-4 px-4 pb-6 md:px-6">
+        {scopeOfAssessment.checked && (
+          <Section checked items={scopeOfAssessment.checked} />
+        )}
+        {scopeOfAssessment.notChecked && (
+          <Section checked={false} items={scopeOfAssessment.notChecked} />
+        )}
+      </CollapsibleContent>
+    </Collapsible>
+  )
+}
+
+function Section({
+  checked,
+  items,
+}: {
+  checked: boolean
+  items: string[]
+}) {
+  const Icon = checked ? SatisfiedIcon : MissingIcon
+  return (
+    <div className="flex flex-col gap-2">
+      <span
+        className={cn(
+          'text-xs font-medium',
+          checked ? 'text-positive' : 'text-negative',
+        )}
+      >
+        {checked ? 'Things we checked' : 'Things we did not check'}
+      </span>
+      <div
+        className={cn(
+          'flex flex-col gap-1 rounded-lg border px-[15px] py-3',
+          checked ? 'border-positive' : 'border-negative',
+        )}
+      >
+        {items.map((item) => (
+          <div key={item} className="flex items-start gap-2">
+            <Icon
+              className={cn(
+                'mt-[5px] size-4 shrink-0',
+                checked ? 'fill-positive' : 'fill-negative',
+              )}
+            />
+            <Markdown className="text-base font-bold leading-[170%] text-primary">
+              {item}
+            </Markdown>
+          </div>
+        ))}
+      </div>
+    </div>
+  )
+}
diff --git a/packages/frontend/src/components/projects/sections/stage-section.tsx b/packages/frontend/src/components/projects/sections/stage-section.tsx
index f50d71e35b22..22a862f43273 100644
--- a/packages/frontend/src/components/projects/sections/stage-section.tsx
+++ b/packages/frontend/src/components/projects/sections/stage-section.tsx
@@ -1,4 +1,8 @@
-import type { StageConfigured, StageUnderReview } from '@l2beat/config'
+import type {
+  ProjectScalingScopeOfAssessment,
+  StageConfigured,
+  StageUnderReview,
+} from '@l2beat/config'
 import Image from 'next/image'
 import {
   Accordion,
@@ -25,6 +29,7 @@ import { Callout } from '../../callout'
 import { Markdown } from '../../markdown/markdown'
 import { WarningBar } from '../../warning-bar'
 import { ProjectSection } from './project-section'
+import { ScopeOfAssessment } from './scope-of-assessment'
 import type { ProjectSectionProps } from './types'
 
 export interface StageSectionProps extends ProjectSectionProps {
@@ -39,6 +44,7 @@ export interface StageSectionProps extends ProjectSectionProps {
         long: string
       }
     | undefined
+  scopeOfAssessment?: ProjectScalingScopeOfAssessment
 }
 
 export function StageSection({
@@ -48,6 +54,7 @@ export function StageSection({
   stageConfig,
   isAppchain,
   additionalConsiderations,
+  scopeOfAssessment,
   ...sectionProps
 }: StageSectionProps) {
   if (stageConfig.stage === 'UnderReview' || sectionProps.isUnderReview) {
@@ -80,7 +87,7 @@ export function StageSection({
 
   return (
     <ProjectSection {...sectionProps}>
-      <span className="mb-4 inline-block w-full rounded bg-surface-secondary px-6 py-4 font-medium">
+      <span className="mb-4 inline-block w-full rounded-lg bg-surface-secondary p-4 font-medium md:px-6">
         <Image
           src={icon}
           alt={name}
@@ -98,6 +105,12 @@ export function StageSection({
         />
         <span> {type}</span>.
       </span>
+      {scopeOfAssessment && (
+        <ScopeOfAssessment
+          className="mb-4"
+          scopeOfAssessment={scopeOfAssessment}
+        />
+      )}
       {additionalConsiderations && (
         <div className="space-y-4 p-4 text-base">
           {isAppchain && (
@@ -161,7 +174,7 @@ export function StageSection({
 
           return (
             <AccordionItem key={stage.stage} value={stage.stage}>
-              <AccordionTrigger className="px-6 py-4 text-lg font-normal">
+              <AccordionTrigger className="py-4 text-lg font-normal md:px-6">
                 <div className="flex select-none items-center justify-start gap-3 max-md:text-base">
                   <StageBadge stage={stage.stage} isAppchain={false} />
                   {missingForLabel.length === 0 ? (
@@ -191,11 +204,11 @@ export function StageSection({
                   )}
                 </div>
               </AccordionTrigger>
-              <AccordionContent className="mx-6 space-y-1 text-lg md:space-y-2">
+              <AccordionContent className="space-y-1 text-lg md:mx-6 md:space-y-2">
                 {stage.principle && (
-                  <div className="rounded-lg border border-brand p-2">
+                  <>
                     <div className="flex items-center gap-1">
-                      <span className="ml-6 text-[13px] uppercase">
+                      <span className="text-[13px] uppercase leading-normal">
                         {featureFlags.stageOneRequirementsChanged()
                           ? 'Principle'
                           : 'Upcoming principle'}
@@ -211,26 +224,28 @@ export function StageSection({
                         </TooltipContent>
                       </Tooltip>
                     </div>
-                    <div className="flex">
-                      {stage.principle.satisfied === 'UnderReview' ? (
-                        <UnderReviewIcon className="mt-0.5 size-4 shrink-0" />
-                      ) : stage.principle.satisfied === true ? (
-                        <SatisfiedIcon className="mt-0.5 size-4 shrink-0 fill-positive" />
-                      ) : (
-                        <MissingIcon className="mt-0.5 size-4 shrink-0 fill-negative" />
-                      )}
-                      <Markdown className="ml-2 font-medium leading-tight max-md:text-base">
-                        {stage.principle.description}
-                      </Markdown>
+                    <div className="rounded-lg border border-brand px-[15px] py-3">
+                      <div className="flex">
+                        {stage.principle.satisfied === 'UnderReview' ? (
+                          <UnderReviewIcon className="mt-0.5 size-4 shrink-0" />
+                        ) : stage.principle.satisfied === true ? (
+                          <SatisfiedIcon className="mt-0.5 size-4 shrink-0 fill-positive" />
+                        ) : (
+                          <MissingIcon className="mt-0.5 size-4 shrink-0 fill-negative" />
+                        )}
+                        <Markdown className="ml-2 font-medium leading-tight max-md:text-base">
+                          {stage.principle.description}
+                        </Markdown>
+                      </div>
                     </div>
-                  </div>
+                  </>
                 )}
 
                 {featureFlags.stageOneRequirementsChanged() &&
                   stage.principle && (
                     <p className="ml-8 text-[13px] uppercase">Guidelines</p>
                   )}
-                <ul className="space-y-1 px-2 md:space-y-2">
+                <ul className="space-y-1 px-4 md:space-y-2">
                   {satisfiedRequirements.map((req, i) => (
                     <li key={i} className="flex">
                       <SatisfiedIcon className="relative top-0.5 size-4 shrink-0 fill-positive" />
diff --git a/packages/frontend/src/server/features/scaling/project/get-scaling-project-entry.ts b/packages/frontend/src/server/features/scaling/project/get-scaling-project-entry.ts
index 5f7711521607..7d00ea566b54 100644
--- a/packages/frontend/src/server/features/scaling/project/get-scaling-project-entry.ts
+++ b/packages/frontend/src/server/features/scaling/project/get-scaling-project-entry.ts
@@ -409,6 +409,7 @@ export async function getScalingProjectEntry(
           project.scalingStage.stage !== 'UnderReview'
             ? project.scalingStage.additionalConsiderations
             : undefined,
+        scopeOfAssessment: project.scalingInfo.scopeOfAssessment,
       },
     })
   }


Reviews:
user: login: torztomasz
state: CHANGES_REQUESTED
author_association: MEMBER
submitted_at: 2025-03-06T12:05:51Z
user: login: torztomasz
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-06T12:17:50Z
user: login: torztomasz
state: APPROVED
author_association: MEMBER
submitted_at: 2025-03-06T12:26:20Z

Review comments:
diff_hunk: @@ -0,0 +1,83 @@
+import type { ProjectScalingScopeOfAssessment } from '@l2beat/config'
+import {
+  Collapsible,
+  CollapsibleContent,
+  CollapsibleTrigger,
+} from '~/components/core/collapsible'
+import { Markdown } from '~/components/markdown/markdown'
+import { ChevronIcon } from '~/icons/chevron'
+import { MissingIcon } from '~/icons/missing'
+import { SatisfiedIcon } from '~/icons/satisfied'
+import { cn } from '~/utils/cn'
+
+export function ScopeOfAssessment({
+  scopeOfAssessment,
+  className,
+}: {
+  scopeOfAssessment: ProjectScalingScopeOfAssessment
+  className?: string
+}) {
+  if (!scopeOfAssessment) return null
path: packages/frontend/src/components/projects/sections/scope-of-assessment.tsx
user: login: torztomasz
body: ```suggestion
  scopeOfAssessment: NonNullable<ProjectScalingScopeOfAssessment>
  className?: string
}) {
```
created_at: 2025-03-06T12:04:05Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,83 @@
+import type { ProjectScalingScopeOfAssessment } from '@l2beat/config'
+import {
+  Collapsible,
+  CollapsibleContent,
+  CollapsibleTrigger,
+} from '~/components/core/collapsible'
+import { Markdown } from '~/components/markdown/markdown'
+import { ChevronIcon } from '~/icons/chevron'
+import { MissingIcon } from '~/icons/missing'
+import { SatisfiedIcon } from '~/icons/satisfied'
+import { cn } from '~/utils/cn'
+
+export function ScopeOfAssessment({
+  scopeOfAssessment,
+  className,
+}: {
+  scopeOfAssessment: ProjectScalingScopeOfAssessment
+  className?: string
+}) {
+  if (!scopeOfAssessment) return null
+
+  return (
+    <Collapsible
+      className={cn('group rounded-lg border border-divider', className)}
+      defaultOpen
+    >
+      <CollapsibleTrigger className="flex w-full items-center justify-between px-4 py-3 font-bold md:px-6">
+        Scope of assessment
+        <ChevronIcon className="size-4 transition-transform duration-300 group-data-[state=open]:-rotate-180" />
+      </CollapsibleTrigger>
+      <CollapsibleContent className="space-y-4 px-4 pb-6 md:px-6">
+        {scopeOfAssessment.checked && (
+          <Section checked items={scopeOfAssessment.checked} />
+        )}
+        {scopeOfAssessment.notChecked && (
+          <Section checked={false} items={scopeOfAssessment.notChecked} />
+        )}
+      </CollapsibleContent>
+    </Collapsible>
+  )
+}
+
+function Section({
+  checked = true,
path: packages/frontend/src/components/projects/sections/scope-of-assessment.tsx
user: login: torztomasz
body: why would default be true? leave it without default
created_at: 2025-03-06T12:05:32Z
author_association: MEMBER
diff_hunk: @@ -286,6 +287,11 @@ export type ProjectScalingCategory =
 
 export type ProjectScalingCapability = 'universal' | 'appchain'
 
+export type ProjectScalingScopeOfAssessment = {
+  checked?: string[]
+  notChecked?: string[]
+}
path: packages/config/src/types.ts
user: login: torztomasz
body: ```suggestion
export type ProjectScalingScopeOfAssessment = {
  checked: string[]
  notChecked: string[]
}
```
created_at: 2025-03-06T12:17:50Z
author_association: MEMBER

PR comments:
user: login: linear[bot]
created_at: 2025-03-06T11:54:33Z
body: <p><a href="https://linear.app/l2beat/issue/L2B-9455/add-scope-of-assessment-to-risk-analysis">L2B-9455 Add Scope of assessment to Risk analysis</a></p>
author_association: NONE
user: login: vercel[bot]
created_at: 2025-03-06T11:54:33Z
body: [vc]: #sI/xGd/n6ALyyvifviLgpsdVVndpAWMSN/ppDv+/QWM=:eyJpc01vbm9yZXBvIjp0cnVlLCJ0eXBlIjoiZ2l0aHViIiwicHJvamVjdHMiOlt7Im5hbWUiOiJ0b29scyIsInJvb3REaXJlY3RvcnkiOiJwYWNrYWdlcy90b29scyIsImluc3BlY3RvclVybCI6Imh0dHBzOi8vdmVyY2VsLmNvbS9sMmJlYXQvdG9vbHMvdzZSSzVOZjZRZVBFMnU2THA2dnV5WVhuWFV1biIsInByZXZpZXdVcmwiOiJ0b29scy1naXQtYWRkLXNjb3BlLW9mLWFzc2Vzc21lbnQtbDJiZWF0LnZlcmNlbC5hcHAiLCJuZXh0Q29tbWl0U3RhdHVzIjoiSUdOT1JFRCIsImxpdmVGZWVkYmFjayI6eyJyZXNvbHZlZCI6MCwidW5yZXNvbHZlZCI6MCwidG90YWwiOjAsImxpbmsiOiJ0b29scy1naXQtYWRkLXNjb3BlLW9mLWFzc2Vzc21lbnQtbDJiZWF0LnZlcmNlbC5hcHAifX0seyJuYW1lIjoibDJiZWF0LXN0YWdpbmciLCJyb290RGlyZWN0b3J5IjoicGFja2FnZXMvZnJvbnRlbmQiLCJpbnNwZWN0b3JVcmwiOiJodHRwczovL3ZlcmNlbC5jb20vbDJiZWF0L2wyYmVhdC1zdGFnaW5nLzdZa0FrM2JvV2haY3dQdEVialgxY3NvTHlwREIiLCJwcmV2aWV3VXJsIjoibDJiZWF0LXN0YWdpbmctZ2l0LWFkZC1zY29wZS1vZi1hc3Nlc3NtZW50LWwyYmVhdC52ZXJjZWwuYXBwIiwibmV4dENvbW1pdFN0YXR1cyI6IkRFUExPWUVEIiwibGl2ZUZlZWRiYWNrIjp7InJlc29sdmVkIjowLCJ1bnJlc29sdmVkIjowLCJ0b3RhbCI6MCwibGluayI6ImwyYmVhdC1zdGFnaW5nLWdpdC1hZGQtc2NvcGUtb2YtYXNzZXNzbWVudC1sMmJlYXQudmVyY2VsLmFwcCJ9fSx7Im5hbWUiOiJsMmJlYXQtcHJvZHVjdGlvbiIsInJvb3REaXJlY3RvcnkiOiJwYWNrYWdlcy9mcm9udGVuZCIsImluc3BlY3RvclVybCI6Imh0dHBzOi8vdmVyY2VsLmNvbS9sMmJlYXQvbDJiZWF0LXByb2R1Y3Rpb24vNFl0VFRYYndYVVRlZ0ZtMzdnUEt3SmJtNWV4TiIsInByZXZpZXdVcmwiOiJsMmJlYXQtcHJvZHVjdGlvbi1naXQtYWRkLXNjb3BlLW9mLWFzc2Vzc21lbnQtbDJiZWF0LnZlcmNlbC5hcHAiLCJuZXh0Q29tbWl0U3RhdHVzIjoiREVQTE9ZRUQiLCJsaXZlRmVlZGJhY2siOnsicmVzb2x2ZWQiOjAsInVucmVzb2x2ZWQiOjAsInRvdGFsIjowLCJsaW5rIjoibDJiZWF0LXByb2R1Y3Rpb24tZ2l0LWFkZC1zY29wZS1vZi1hc3Nlc3NtZW50LWwyYmVhdC52ZXJjZWwuYXBwIn19LHsibmFtZSI6InByb3RvY29sYmVhdCIsInJvb3REaXJlY3RvcnkiOiJwYWNrYWdlcy9wcm90b2NvbGJlYXQiLCJpbnNwZWN0b3JVcmwiOiJodHRwczovL3ZlcmNlbC5jb20vbDJiZWF0L3Byb3RvY29sYmVhdC85NG9ZMlIxUkFtaXd0YnJvNHJHcUVNRlhDZGJEIiwicHJldmlld1VybCI6InByb3RvY29sYmVhdC1naXQtYWRkLXNjb3BlLW9mLWFzc2Vzc21lbnQtbDJiZWF0LnZlcmNlbC5hcHAiLCJuZXh0Q29tbWl0U3RhdHVzIjoiSUdOT1JFRCIsImxpdmVGZWVkYmFjayI6eyJyZXNvbHZlZCI6MCwidW5yZXNvbHZlZCI6MCwidG90YWwiOjAsImxpbmsiOiJwcm90b2NvbGJlYXQtZ2l0LWFkZC1zY29wZS1vZi1hc3Nlc3NtZW50LWwyYmVhdC52ZXJjZWwuYXBwIn19LHsibmFtZSI6InVvcHMtZGFzaGJvYXJkIiwicm9vdERpcmVjdG9yeSI6InBhY2thZ2VzL3VvcHMtZGFzaGJvYXJkIiwibGl2ZUZlZWRiYWNrIjp7InJlc29sdmVkIjowLCJ1bnJlc29sdmVkIjowLCJ0b3RhbCI6MCwibGluayI6IiJ9LCJpbnNwZWN0b3JVcmwiOiJodHRwczovL3ZlcmNlbC5jb20vbDJiZWF0L3VvcHMtZGFzaGJvYXJkLzRGQzN6ZXhXTnZVNWMxR2pQaktLcWdHWTQ2R0EiLCJwcmV2aWV3VXJsIjoiIiwibmV4dENvbW1pdFN0YXR1cyI6IlNLSVBQRUQifV19
**The latest updates on your projects**. Learn more about [Vercel for Git ↗︎](https://vercel.link/github-learn-more)

| Name | Status | Preview | Comments | Updated (UTC) |
| :--- | :----- | :------ | :------- | :------ |
| **l2beat-production** | ✅ Ready ([Inspect](https://vercel.com/l2beat/l2beat-production/4YtTTXbwXUTegFm37gPKwJbm5exN)) | [Visit Preview](https://l2beat-production-git-add-scope-of-assessment-l2beat.vercel.app) | 💬 [**Add feedback**](https://vercel.live/open-feedback/l2beat-production-git-add-scope-of-assessment-l2beat.vercel.app?via=pr-comment-feedback-link) | Mar 6, 2025 0:28am |
| **l2beat-staging** | ✅ Ready ([Inspect](https://vercel.com/l2beat/l2beat-staging/7YkAk3boWhZcwPtEbjX1csoLypDB)) | [Visit Preview](https://l2beat-staging-git-add-scope-of-assessment-l2beat.vercel.app) | 💬 [**Add feedback**](https://vercel.live/open-feedback/l2beat-staging-git-add-scope-of-assessment-l2beat.vercel.app?via=pr-comment-feedback-link) | Mar 6, 2025 0:28am |

<details><summary>3 Skipped Deployments</summary>

| Name | Status | Preview | Comments | Updated (UTC) |
| :--- | :----- | :------ | :------- | :------ |
| **protocolbeat** | ⬜️ Ignored ([Inspect](https://vercel.com/l2beat/protocolbeat/94oY2R1RAmiwtbro4rGqEMFXCdbD)) | [Visit Preview](https://protocolbeat-git-add-scope-of-assessment-l2beat.vercel.app) | 💬 [**Add feedback**](https://vercel.live/open-feedback/protocolbeat-git-add-scope-of-assessment-l2beat.vercel.app?via=pr-comment-feedback-link) | Mar 6, 2025 0:28am |
| **tools** | ⬜️ Ignored ([Inspect](https://vercel.com/l2beat/tools/w6RK5Nf6QePE2u6Lp6vuyYXnXUun)) | [Visit Preview](https://tools-git-add-scope-of-assessment-l2beat.vercel.app) | 💬 [**Add feedback**](https://vercel.live/open-feedback/tools-git-add-scope-of-assessment-l2beat.vercel.app?via=pr-comment-feedback-link) | Mar 6, 2025 0:28am |
| **uops-dashboard** | ⬜️ Skipped ([Inspect](https://vercel.com/l2beat/uops-dashboard/4FC3zexWNvU5c1GjPjKKqgGY46GA)) |  |  | Mar 6, 2025 0:28am |
</details>






author_association: NONE