Models¶
Core domain models for the Imbi ecosystem.
Overview¶
The models module provides Pydantic models for all core domain entities (projects, organizations, teams) and the blueprint system for dynamic schema extension.
Model Categories¶
Domain Models¶
- Organization: Top-level organizational units
- Team: Groups within organizations
- Environment: Deployment environments (production, staging, etc.)
- ProjectType: Project categorization and templates
- Project: Services and applications
- TagFormat: A named (
label) regular-expression (pattern) policy for release/deploy tags. BothOrganizationandProjectTypecarry atag_formats: list[TagFormat]field (see Release/Deploy Tag Formats).
Software-Composition Models¶
- Component: Third-party package identity (purl with version
stripped, e.g.
pkg:npm/express) - ComponentRelease: A specific version of a
Component(Component-[:HAS_RELEASE]->ComponentRelease); attached to a projectReleasevia[:USES_COMPONENT_RELEASE] - ComponentIdentifier: Globally unique
(kind, value)pair (purl / cpe / bom-ref / swid) linked to aComponentvia[:IDENTIFIED_BY]
Collaboration Models¶
- Document: A free-form, taggable markdown document attached to
exactly one owning vertex via
[:ATTACHED_TO]— aProject, aProjectType, or aUser(theUservertex is defined byimbi-api, so only the project and project-type edges are typed on the model). - DocumentTemplate: Reusable starter content for a
Document.typedeclares which attachment contexts may use the template:project,user,project_type, orglobal(every context);project_type_slugsoptionally narrows project types further. - CommentThread: A thread of comments anchored to a
Documentvia[:ON_DOCUMENT].kindispage(whole-document) orinline(text-anchored); inline anchors are flattened into theanchor_quote/anchor_prefix/anchor_suffix/anchor_startscalar properties. - Comment: A single comment within a
CommentThread(Comment-[:IN_THREAD]->CommentThread); carriesmentionsandacknowledged_byemail lists.
Blueprint Models¶
- Blueprint: Dynamic schema definitions
- BlueprintAssignment: Blueprint-to-entity relationships
Analytics Models¶
These are not graph nodes — they are typed rows published to Apache Iggy
via iggy.publish and written to ClickHouse by the sink.
They are provider-agnostic so
any version-control plugin (GitHub, GitLab, …) can reuse them.
- CommitRecord: A VCS commit, written to the commits table
(ReplacingMergeTree keyed by (project_id, sha))
- TagRecord: A VCS tag, written to the tags table
(ReplacingMergeTree keyed by (project_id, name))
- ReleaseComponentRecord: One SBoM dependency of a release, written
to the release_components table
- ReleaseComponentBatch: The record publishing one release's
component snapshot, written to release_component_batches after every
fact row of that batch
Basic Usage¶
from imbi.common import graph, models
# Create an organization
org = models.Organization(
name="My Company",
slug="my-company",
description="Our organization"
)
await db.create(org)
# Create a team linked to an organization
team = models.Team(
name="Platform Team",
slug="platform-team",
description="Infrastructure and platform",
organization=org
)
await db.create(team)
Release/Deploy Tag Formats¶
The tags accepted when cutting a release or promoting a deployment are
governed by a list of TagFormat policies. A tag is accepted when it
matches any configured format (full-string regex match).
Resolution is hierarchical, project-type overriding organization:
- If the project's type(s) configure
tag_formats, those apply. - Otherwise the organization's
tag_formatsapply. - If neither configures any, no restriction is imposed (any tag is
accepted). Seed
models.SEMVER_TAG_FORMATto require semver.
from imbi.common import models, versioning
org = models.Organization(
name="My Company",
slug="my-company",
tag_formats=[models.SEMVER_TAG_FORMAT],
)
patterns = [fmt.pattern for fmt in org.tag_formats]
versioning.matches_tag_formats("v1.2.3", patterns) # True
versioning.matches_tag_formats("nightly", patterns) # False
matches_tag_formats ¶
Return True when tag satisfies the configured tag formats.
patterns is the resolved list of regular-expression patterns for
the project (see imbi.common.models.TagFormat). Each pattern is
matched against the whole tag with :func:re.fullmatch, so a
pattern need not anchor itself with ^/$.
An empty patterns sequence means "no configured policy" and
matches any tag -- callers that need a stricter default should seed a
format (e.g. :data:SEMVER_TAG_PATTERN) rather than relying on this.
Invalid patterns are rejected at write time
(TagFormat validates them), so a bad pattern here is treated as a
non-match rather than raising.
Source code in libraries/common/src/imbi/common/versioning.py
API Reference¶
Base Classes¶
GraphModel ¶
Bases: BaseModel
Minimal base for any model stored as a graph vertex.
Provides identity (id), timestamps, and
extra='ignore' so AGE metadata is silently dropped.
Subclass Node when you also need name/slug.
Node ¶
Bases: GraphModel
Graph node with business identity fields.
The icon attribute can either be a URL or a CSS
class name.
Domain Models¶
ProjectType ¶
TagFormat ¶
Bases: BaseModel
A named release/deploy tag-format policy.
label is the human-facing name shown in the UI (e.g. Semver
or CalVer); pattern is a regular expression a tag must match.
A tag is accepted when it matches any configured TagFormat --
see imbi.common.versioning.matches_tag_formats.
Patterns are matched with :func:re.fullmatch and validated as
compilable at assignment time so an invalid expression is rejected at
the API boundary rather than at deploy/release time.
MCPServer ¶
Bases: Node
An external MCP server reachable over streamable HTTP.
The *_encrypted fields store the ciphertext (Fernet via
:mod:imbi.common.auth.encryption, keyed off
IMBI_CONFIG_ENCRYPTION_KEY). Plaintext secrets must never be
assigned to these fields; encryption and decryption happen in the
repository/consumer layer, never on the model.
Software-Composition Models¶
Component ¶
Bases: GraphModel
A piece of third-party software that may appear as a
dependency of a project Release.
Identity is the package URL with version stripped — e.g.
pkg:npm/express for any version of express. Versions are
captured as ComponentRelease nodes linked via
HAS_RELEASE.
A component may be marked deprecated or forbidden to steer
projects off of it wholesale — every version inherits the mark.
status is the flag; clearing it removes all three status_*
properties, mirroring the blocked_* triple on Release. The
why lives in the notes on the affected versions rather than in a
reason property, per the report designs.
ComponentRelease ¶
Bases: GraphModel
A specific version of a Component.
Per-component uniqueness of version is enforced at the
application layer via MERGE on
(Component)-[:HAS_RELEASE]->(ComponentRelease {version: ...});
no graph-wide UNIQUE index is possible because two components
may legitimately ship the same version string.
status marks this one version deprecated or forbidden. The
status a report shows is the strictest of this mark and the
owning component's — see :func:effective_component_status.
ComponentIdentifier ¶
Bases: GraphModel
A unique identifier for a software Component.
Versioned identifier kinds (purl with @version, CPE with
a version segment) are normalized to their version-agnostic form
before persistence so a single ComponentIdentifier resolves
one Component regardless of release. (kind, value) is
globally unique.
Collaboration Models¶
CommentThread ¶
Bases: GraphModel
A thread of comments anchored to a project Document.
kind is 'page' for a whole-document discussion or
'inline' for a comment tied to a span of the document's
text. The inline anchor is FLATTENED into the four
anchor_* scalar properties (rather than a nested model) so
the stored agtype stays a plain map. Page-level threads leave
the anchor fields at their defaults.
Comment ¶
Bases: GraphModel
A single comment within a CommentThread.
mentions and acknowledged_by hold email addresses and
round-trip through AGE as agtype arrays. body is markdown
text and is embedded so semantic search can surface comments
alongside other corpus content.
Blueprint Models¶
Blueprint ¶
Bases: Node
generate_and_validate_slug ¶
Generate slug from name if not provided and validate it.
Source code in libraries/common/src/imbi/common/models.py
validate_kind_fields ¶
Validate kind-specific required fields.
Source code in libraries/common/src/imbi/common/models.py
BlueprintAssignment ¶
Bases: BaseModel
BlueprintEdge ¶
Bases: NamedTuple
Analytics Models¶
CommitRecord ¶
Bases: BaseModel
A VCS commit recorded in the ClickHouse commits table.
Generic across version-control providers — a GitHub, GitLab, or
Bitbucket plugin maps its API response onto these fields and publishes
them with :func:imbi.common.iggy.publish to the commits stream,
which the ClickHouse sink drains into the table. The table is a
ReplacingMergeTree keyed by (project_id, sha), so re-syncing an
overlapping commit range collapses duplicates on merge.
TagRecord ¶
Bases: BaseModel
A VCS tag recorded in the ClickHouse tags table.
Mirrors :class:CommitRecord's role for tags. The table is a
ReplacingMergeTree keyed by (project_id, name); annotated-tag
metadata (message, tagger_*, tagged_at) is populated when
the provider exposes it and left at its default otherwise.
sha is always the commit the tag resolves to. Providers that
expose annotated tags as their own objects (git, hence GitHub) must
peel the tag before recording it: consumers join this column against
commits.sha and match it against deployment committishes, so a tag
object hash silently matches nothing. A tag that cannot be peeled to a
commit must be skipped rather than recorded against the unresolved
hash -- no row is better than one nothing can join to.
ReleaseComponentRecord ¶
Bases: BaseModel
One component a release depends on, per its SBoM.
Rows land in the ClickHouse release_components table. Component
and release identity live in the graph; this table holds only the
usage fact joining them, which is what the component reports
aggregate over.
Only fields immutable for the life of the release are carried here.
Team, project type, and environment are deliberately absent: they are
mutable, and an append-only table would serve stale values for them.
project_id is the exception -- it does not change for a release,
and it is what lets the authorization checks scope to an
organization without enumerating its releases.
A row is invisible until :class:ReleaseComponentBatch publishes its
batch_id. Neither recorded_at nor batch_id has a default:
both belong to the batch, not the row, and a per-row default would
split one snapshot across several.
ReleaseComponentBatch ¶
Bases: BaseModel
The record that publishes one release's component snapshot.
Rows land in the ClickHouse release_component_batches table and
are written after every :class:ReleaseComponentRecord of the
same batch_id. A batch is what readers resolve a release to:
.. code-block:: sql
SELECT release_id,
argMax(batch_id, (source = 'ingest', recorded_at)) AS batch_id
FROM imbi.release_component_batches
WHERE release_id IN {release_ids}
GROUP BY release_id
Fact rows are then matched on (release_id, batch_id). Rows whose
batch was never published are inert, so an interrupted write leaves
readers on the previous complete snapshot.
An ingest that found no components still publishes a batch, with
component_count 0. Without one the previous batch would stay
current and the release would keep reporting components it dropped.
component_count is the rows actually written and parsed_count
the components the SBoM held. They differ when the graph upsert
dropped some component, which is deliberately non-fatal -- recording
both is what makes an incomplete snapshot detectable rather than
silently authoritative.
source leads the resolver key. A backfill row can never outrank
an ingest for the same release, whatever order they land in, so
backfilling alongside live writes needs no check-then-publish race.