Skip to content

Analysis Capability

AnalysisCapability is the contract base for a capability that inspects a project and emits Project Doctor findings — pass / warn / fail items with a markdown body. Bind it with a Capability(kind='analysis', handler=...) in the plugin's manifest. The host (imbi-api) resolves applicable analysis capabilities for a project through the project-type USES assignment and via any Integration the project EXISTS_IN whose plugin declares an analysis capability.

Surfaces: ui, api.

See Authoring Plugins for the manifest, capabilities, context, credential decryption, and error conventions shared by every plugin.

Contract

AnalysisCapability has a single abstract method, analyze:

from imbi.common.plugins import (
    AnalysisCapability,
    AnalysisResultItem,
    PluginContext,
)


class GitHubAnalysis(AnalysisCapability):
    async def analyze(
        self,
        ctx: PluginContext,
        credentials: dict[str, str],
    ) -> list[AnalysisResultItem]:
        return [
            AnalysisResultItem(
                slug='github:url-drift',
                title='Repository URL drift',
                description=(
                    'The configured `github-repository` link points at '
                    '`old/repo` but GitHub redirects to `new/repo`. '
                    'Update the link to clear this finding.'
                ),
                status='warn',
            )
        ]

Each AnalysisResultItem carries a stable per-capability slug so the UI and any matching analysis_result scoring policy can refer to the finding across runs. The description is rendered as Markdown in the Doctor panel.

Remediating a finding

A finding is fixable when analyze attaches a RemediationOffer to it. The Doctor panel renders a button labelled offer.label; clicking it asks the host to call the emitting capability's remediate with the offer's id. Implement remediate whenever you emit an offer:

from imbi.common.plugins import (
    AnalysisResultItem,
    RemediationOffer,
    RemediationResult,
    ServiceWriteback,
)


class GitHubAnalysis(AnalysisCapability):
    async def analyze(self, ctx, credentials):
        return [
            AnalysisResultItem(
                slug='github:id-drift',
                title='Repository id drift',
                description='The stored identifier no longer matches GitHub.',
                status='fail',
                remediation=RemediationOffer(
                    id='id-drift',
                    label='Repair repository edge',
                ),
            )
        ]

    async def remediate(self, ctx, credentials, remediation_id):
        if remediation_id != 'id-drift':
            return await super().remediate(ctx, credentials, remediation_id)
        repo = await self._fetch_repo(ctx, credentials)
        # Re-verify before writing so the call is idempotent.
        conn = next(
            (c for c in ctx.service_connections
             if c.integration_slug == ctx.integration_slug),
            None,
        )
        if conn and conn.identifier == str(repo['id']):
            return RemediationResult(status='noop', message='Already correct.')
        # Capabilities have no DB handle: effect the change via the
        # write-back channel, exactly as lifecycle capabilities do.
        ctx.service_writeback = ServiceWriteback(
            identifier=str(repo['id']),
            canonical_url=f"{api_base}/repositories/{repo['id']}",
        )
        return RemediationResult(status='fixed', message='Repaired the edge.')

remediate must re-verify the discrepancy and return a noop RemediationResult when the finding is already resolved, so a double-click — or the bulk "fix all" pass — is safe. The default implementation raises PluginRemediationNotSupported; only capabilities that emit offers need override it. Set RemediationOffer.destructive for fixes that create/remove an edge or delete a value (the UI then requires explicit confirmation).

How findings reach the panel

The host fans the call out across every applicable analysis capability (via asyncio.gather), captures exceptions as a synthetic fail result, and persists the merged report on (:Project)-[:HAS_ANALYSIS_REPORT]->(:AnalysisReport) -[:HAS_RESULT]->(:AnalysisResult). Only the latest report is retained — re-running analysis replaces the previous one.

The AnalysisReport.overall_status is the worst observed result; the project page colour-codes the Doctor icon from it.

Feeding the score

A scoring policy of category analysis_result references an AnalysisResult.slug (the same slug your capability emits) and maps its status to a 0-100 score via status_score_map (defaults: {'pass': 100, 'warn': 50, 'fail': 0}). When the project's latest report contains a matching result the policy contributes to the Health & Compliance score; missing results contribute None.

Hints

  • cacheable — the host may cache reads from this capability.

API reference

AnalysisCapability

Bases: CapabilityHandler

Inspect a project (via its links, Integration connections, and the Integration credentials) and return pass/warn/fail findings that surface in the Project Doctor panel and feed the analysis_result scoring-policy category.

A capability that attaches a :class:RemediationOffer to any finding must also override :meth:remediate to apply that fix. Like every other capability method, remediate has no database handle: it effects graph changes by setting ctx.service_writeback / ctx.link_writeback, which the host captures and persists.

remediate async

remediate(
    ctx: PluginContext,
    credentials: dict[str, str],
    remediation_id: str,
) -> RemediationResult

Apply the fix identified by remediation_id.

remediation_id is the id of a :class:RemediationOffer this capability previously emitted on a finding. Implementations should re-verify the discrepancy against fresh state and return a noop :class:RemediationResult when it is already resolved, so the call is idempotent.

Report the outcome by returning a :class:RemediationResult: fixed when state changed, noop when already resolved, and failed (with a user-facing message) when the fix could not be applied. Implementations should catch their own errors and translate them rather than letting exceptions escape.

The default raises :class:PluginRemediationNotSupported; only capabilities that emit remediation offers need override it.

Source code in libraries/common/src/imbi/common/plugins/base.py
async def remediate(
    self,
    ctx: PluginContext,
    credentials: dict[str, str],
    remediation_id: str,
) -> RemediationResult:
    """Apply the fix identified by ``remediation_id``.

    ``remediation_id`` is the ``id`` of a :class:`RemediationOffer`
    this capability previously emitted on a finding.  Implementations
    should re-verify the discrepancy against fresh state and return a
    ``noop`` :class:`RemediationResult` when it is already resolved,
    so the call is idempotent.

    Report the outcome by **returning** a :class:`RemediationResult`:
    ``fixed`` when state changed, ``noop`` when already resolved, and
    ``failed`` (with a user-facing ``message``) when the fix could not
    be applied.  Implementations should catch their own errors and
    translate them rather than letting exceptions escape.

    The default raises :class:`PluginRemediationNotSupported`; only
    capabilities that emit remediation offers need override it.
    """
    raise PluginRemediationNotSupported(
        type(self).__name__, remediation_id
    )

AnalysisResultItem

Bases: BaseModel

A single finding emitted by an :class:AnalysisCapability.

Stable per-capability slug lets scoring policies and the UI refer to a result across runs. description is rendered as Markdown by the Project Doctor panel. When remediation is set the finding is fixable — see :class:RemediationOffer.

AnalysisResultStatus module-attribute

AnalysisResultStatus = Literal['pass', 'warn', 'fail']

RemediationOffer

Bases: BaseModel

An offer to fix the finding it is attached to.

A finding is fixable iff it carries a RemediationOffer. The Doctor panel renders a button labelled label; clicking it asks the host to call the emitting capability's :meth:AnalysisCapability.remediate with this offer's id. The id is opaque and plugin-defined (it only has to be unique within the capability's own findings) — it round-trips back unchanged so the capability knows which fix to perform.

RemediationResult

Bases: BaseModel

Outcome of an :meth:AnalysisCapability.remediate call.

status is fixed when the capability changed state, noop when the finding was already resolved (so a double-click or a bulk "fix all" pass is safe), and failed when the fix could not be applied. message is human-facing and rendered as Markdown.