Skip to main content

@happyvertical/smrt-content

STI content types (Article, ContentDocument, Mirror) with governance workflows, contribution intake, fact-checking, AI reviews, transparency reports, and thumbnail generation.

Installation

pnpm add @happyvertical/smrt-content

Usage

import { Content, Contents, Article, Mirror } from '@happyvertical/smrt-content';
import { contentToString, stringToContent } from '@happyvertical/smrt-content';

// Initialize collection
const contents = await Contents.create({
db: { url: 'sqlite:./content.db' },
});

// Create and save content
const article = new Article({
title: 'AI in Content Processing',
body: 'Large language models have revolutionized...',
status: 'published',
tags: ['ai', 'nlp'],
category: 'technology/ai',
});
await article.initialize();
await article.save();

// Mirror content from a URL (idempotent -- returns existing if already mirrored)
const mirrored = await contents.mirror({
url: 'https://example.com/article.html',
context: 'research',
mirrorDir: './cache',
});

// Upsert by slug + context
const doc = await contents.getOrUpsert({
slug: 'project-notes',
context: 'docs',
title: 'Project Notes',
body: 'Notes content...',
});

// Thumbnail generation (three strategies)
await contents.generateMissingThumbnails({
strategy: 'headline-card',
db: { url: 'sqlite:./content.db' },
});

// Export to markdown with YAML frontmatter
const markdown = contentToString(article);
const parsed = stringToContent(markdown);

// Batch export articles as markdown files
await contents.syncContentDir({ contentDir: './blog-posts' });

Content Governance

import {
Content,
ContentGovernanceAssignment,
ContentGovernanceManager,
GovernedContentEditor,
configureContentGovernance,
} from '@happyvertical/smrt-content';

configureContentGovernance({
policies: [
{
key: 'editorial',
label: 'Editorial Review',
kind: 'custom',
instructions: 'Check tone, sourcing, and local publication standards.',
},
],
profiles: [
{
key: 'publication',
label: 'Publication',
requirements: [
{ policyKey: 'safety', blocking: true },
{ policyKey: 'facts', blocking: true },
{ policyKey: 'editorial', blocking: false },
],
},
],
assignments: [
{
contentType: 'article',
enabled: true,
factLinkingEnabled: true,
transparencyEnabled: true,
publicationProfileKey: 'publication',
correctionProfileKey: 'correction',
enforcePublishReadiness: true,
},
],
});

const article = new Content({
title: 'Transit service changes',
body: 'Weekend service will resume on April 3.',
type: 'article',
metadata: {
generation: {
publicPrompt: 'Summarize the service change for riders.',
aiAssisted: true,
model: 'gpt-5.4',
},
},
});

await article.initialize();
await article.save();
await article.addFact('fact_123', 'supports');
await article.runReviewAction({ kind: 'facts', policyKey: 'facts' });
await article.runReviewAction({ kind: 'safety', policyKey: 'safety' });

article.status = 'published';
await article.save();

Governance stays opt-in. Plain Content records behave like legacy smrt-content unless an assignment matches their type and optional exact variant.

Persisted governance definitions are modeled as first-class s-m-r-t objects:

  • ContentGovernancePolicy
  • ContentGovernanceProfile
  • ContentGovernanceAssignment

Reviews & Corrections

Content reviews are AI-driven quality checks tied to governance policies. Corrections track post-publication changes with accountability.

// Run an AI review against a policy
const review = await article.runReviewAction({
kind: 'facts',
policyKey: 'facts',
});
console.log(review.status); // 'accepted' | 'flagged' | 'rejected'
console.log(review.findings); // Array of issues found

// List all reviews for content
const reviews = await article.listReviews();

// Evaluate readiness against a profile
const profiles = await article.listReviewProfilesAction();
const evaluation = await article.evaluateReviewProfile('publication');
console.log(evaluation.ready); // true if all blocking requirements met

// Issue a correction
await article.issueCorrectionAction({
type: 'correction',
summary: 'Updated figures to reflect Q4 data',
note: 'Previous values were from Q3',
});
const corrections = await article.listCorrections();

Versioning

Content versions track snapshots of content state. Publication versions are created automatically when governed content is published.

// Manual version snapshot
await article.mutateVersionAction({
kind: 'manual',
summary: 'Before the editorial rewrite.',
});

// Snapshot created by an ingestion or generation worker
await article.mutateVersionAction({
kind: 'auto-generated',
summary: 'Planning asset extracted.',
metadata: { source: 'planning-asset-ingestion' },
});

// List version history
const versions = await article.listVersions();

// Restore a previous version
await versions.restoreIntoContent(versionId);

Migrating automated producers

Automated producers should use kind: 'auto-generated' instead of encoding generation as kind: 'draft' plus metadata.generated = 'auto'. Keep source-specific metadata such as metadata.source when it provides useful provenance.

References & Drift Detection

References are (source_id, target_id) edges between Content rows. They can optionally pin a targetVersion captured at citation time. When the target is later re-synced (a new ContentVersion is created), callers can detect drift between what was cited and what the target now says.

// Pin to the target's current version when citing
const latest = await citedArticle.listVersions();
const currentVersion = latest[latest.length - 1]?.version ?? null;
await article.addReference(citedArticle, { targetVersion: currentVersion });

// Or leave it unpinned — no version is recorded and `isDrifted` will
// always be false for this edge regardless of how the target evolves.
await article.addReference(otherArticle);

// Detect drift across all references
const drift = await article.getReferenceDrift();
// → [{ targetId, citedVersion, currentVersion, isDrifted }, ...]
// `isDrifted` is true only when both versions are present and differ.

// Re-link with a new pin to acknowledge drift (idempotent on source+target,
// mutable on the version column).
await article.addReference(citedArticle, { targetVersion: 2 });

serializeContent includes per-reference citedVersion, currentVersion, and isDrifted fields so SvelteKit load functions can pass them through to consumers without an extra round-trip. The fields surface in the serialized payload; rendering them (e.g. a drift badge in ContentReferencesPanel) is left to the consumer.

getReferenceDrift compares against the target's latest ContentVersion of any kind (manual or publication), so a manual snapshot of the target will trigger isDrifted: true. Consumers that only care about published drift can filter further using ContentVersion.kind.

Typical use: cite an ingested external snapshot (web page, upstream feed, asset library entry) as a Content row. Re-sync the source on a schedule, bump the version, and any article that cites the prior version surfaces a drift signal in the editor.

Published Transparency

const publishedTransparency = await article.getPublishedTransparencyAction();
const previewTransparency = await article.previewTransparencyAction();

console.log(publishedTransparency?.factsUsed);
console.log(publishedTransparency?.publicationProfileKey);
console.log(previewTransparency.references);

Published transparency is frozen into ContentVersion.metadata.transparency when a publication snapshot is created. Built sites should render the published snapshot, while editors can use the preview snapshot to inspect what will be shown publicly before publishing.

Publish Readiness

import { evaluateContentPublishReadiness } from '@happyvertical/smrt-content';

const readiness = await evaluateContentPublishReadiness({
content: article,
profileKey: 'publication',
});
console.log(readiness.ready);
console.log(readiness.blockingRequirements);

When enforcePublishReadiness is enabled on a governance assignment, content.save() will throw a ValidationError if blocking requirements are not satisfied when status is set to 'published'.

Facts Integration

Content links to facts from @happyvertical/smrt-facts when fact-linking is enabled in governance:

// Link a fact to content
await article.addFact('fact_id', 'supports');
await article.addFact('fact_id', 'contradicts');
await article.addFact('fact_id', 'referenced_in');

// Get linked facts
const facts = await article.getFacts({ latestOnly: true });
const factLinks = await article.getFactLinks();

// Sync facts state (used by API)
const factsState = await article.getFactsState();
await article.syncFactsState({ factIds: ['fact1', 'fact2'] });

Collection-level fact browsing:

const contents = await Contents.create({ db: dbConfig });
const factCatalog = await contents.browseFacts();

Content Contributions

import {
ContentContributionType,
ContentContributions,
ContentContributionForm,
ContentContributionInbox,
ContentContributionPortal,
ContentContributionTypeManager,
ContentContributorManager,
configureContentContributions,
} from '@happyvertical/smrt-content';

configureContentContributions({
types: [
{
key: 'letter',
label: 'Letter to the editor',
enabled: true,
allowedChannels: ['web', 'email'],
allowText: true,
allowFiles: true,
allowEmptyText: false,
intakeRules: {
maxFiles: 3,
allowedMimePatterns: ['image/*', 'application/pdf'],
quarantineTextPatterns: ['lawsuit', 'defamation'],
},
promotion: {
targetContentType: 'article',
targetContentVariant: 'letter',
targetContentStatus: 'draft',
autoPromoteTrusted: true,
createAssets: true,
assetRelationship: 'attachment',
},
},
],
});

const contributions = await ContentContributions.create({
db: { url: 'sqlite:./content.db' },
});

const result = await contributions.submitWebContribution({
typeKey: 'letter',
contributorEmail: 'reader@example.com',
contributorName: 'Reader',
title: 'A community letter',
body: 'Please publish this letter.',
attachments: [
{
filename: 'photo.jpg',
mimeType: 'image/jpeg',
size: 1024,
fileKey: 'uploads/photo.jpg',
},
],
tenantId: 'tenant-1',
});

const approved = await contributions.get({ id: result.contribution.id });
await approved?.approveAction({
editorNote: 'Looks good for editorial review.',
targetStatus: 'draft',
});

Content contributions are held separately from editorial Content and Asset records until they are promoted. That keeps plain smrt-content generic, while supporting community intake and moderation workflows when an app opts in.

The contribution holding layer adds these first-class s-m-r-t objects:

  • ContentContributionType
  • ContentContributor
  • ContentContribution
  • ContentContributionRevision
  • ContentContributionAttachment

Key behavior:

  • web and email intake normalize into the same contribution package
  • one contribution can contain one primary text submission plus zero or more held files
  • asset-only submissions are allowed when the type permits empty text
  • contributors are resolved by email and linked to a Profile
  • trust levels are standard, trusted, or blocked
  • intake rules can accept, quarantine, or reject before editorial review
  • approval promotes into normal draft Content and Asset records with provenance metadata
  • governance starts after promotion, based on the promoted content type and variant

Chat Integration

Content has built-in AI chat via @happyvertical/smrt-chat:

// API endpoint: GET /api/v1/contents/{id}/chat
// Returns: { session, threads } or { session: null, notice }
// API endpoint: POST /api/v1/contents/{id}/chat
// Creates a new chat thread for the content

The ContentAgentChat Svelte component provides a chat sidebar in the content editor. When chat tables aren't provisioned, it gracefully shows a "not available" notice instead of erroring.

For app-level assistants, ContentEditor and GovernedContentEditor also publish a reusable assistant registration through onAssistantContextChange. This works even when hideChat={true}:

<GovernedContentEditor
content={article}
contentId={article.id}
hideChat
onAssistantContextChange={(registration) => {
assistantStore.setContext(registration?.context ?? null);
assistantStore.setActions(registration?.actions ?? null);
}}
onSave={saveArticle}
onCancel={closeEditor}
/>

The registration includes a serializable ContentEditorAssistantContext (contentId, draft fields, current editor body, reference IDs, and governed fact/readiness summaries) plus local actions such as triggerSave, triggerReview, applyFieldUpdates, and undo for AI-applied field updates. ContentAgentChat can be mounted outside the editor by passing the context:

<ContentAgentChat apiBaseUrl="/tenant/api/v1" assistantContext={context} />

Server integrations can use getOrCreateContentEditorChatSession, createContentEditorChatThread, listContentEditorChatThreadMessages, and sendContentEditorChatThreadMessage to install /contents/:id/chat routes with app-specific tenancy, auth, and AI model resolution hooks instead of copying the package dev-server endpoints.

Dev Server

The package includes a SvelteKit dev server (npm run dev) with:

  • Contents page (/) — Content catalog with search, filters, card/list views, and full CRUD. Includes governed article creation.
  • Governance page (/governance) — Policy, profile, and assignment management via ContentGovernanceManager.
  • Contributions page (/contributions) — Sub-tabbed: editorial inbox, public submit form, contributor management, contribution type config.
  • API Explorer page (/api-explorer) — Browse all 69 auto-generated REST endpoints grouped by domain, with try-it-live for GET endpoints.

The dev server bootstraps schemas for all 13 local @smrt() classes on startup and seeds sample content (3 items) for immediate testing.

Svelte Components

Content Management

ComponentPropsDescription
ContentListcontents or query, jobs, onEdit, onDelete, onAdd, getViewHrefQuery-backed catalog with realtime freshness, background-job progress, retry, search, filters, and view toggles
ContentEditorcontent, contentId, onSave, onCancelFull content editor with metadata, assets, references
GovernedContentEditorcontent, contentId, onSave, onCancelEditor with integrated governance panel and review controls
ContentAgentChatcontentId, apiBasePathAI chat sidebar for content with thread management
ContentTitleField, ContentStatusFields, ContentMetadataFields, ContentReferencesPanel, ContentImageBrowserfocused field/section propsComposable editor primitives for application-owned layouts
ContentReviewStatusTrayitems, activeId, open, onSelectCompact review status tray for inline review drawers
ArticleCardarticleCard display for an article
ArticleListarticlesList of article cards
ImageThumbnailsrc, altThumbnail image display
MarkdownsourceMarkdown renderer

ContentList retry handlers must return a new job attempt with a distinct jobId. The controller retains the failed attempt as immutable history so a late event from it cannot overwrite the retry result.

Applications that compose their own article editor can use createContentEditorState, getContentEditorAssetImageSource, and resolveContentEditorImageSelection to share the same form normalization, thumbnail selection, and save payload behavior as the package editors.

Governance

ComponentPropsDescription
ContentGovernanceManager(self-contained)Full manager for policies, profiles, and assignments
ContentGovernancePanelcontentIdGovernance status panel for a single content item
ContentGovernancePolicyEditorpolicy, onSaveEditor for a single policy
ContentGovernanceProfileEditorprofile, onSaveEditor for a single profile
ContentGovernanceAssignmentEditorassignment, onSaveEditor for a single assignment
ContentTransparencyReportdataRenders the transparency report for published content

Contributions

ComponentPropsDescription
ContentContributionFormtypes, onSubmit, onCancelPublic submission form with file uploads
ContentContributionInboxcontributions, selectedId, onSelect, onApprove, onReject, onRequestChangesEditorial inbox with approve/reject/request-changes actions
ContentContributionPortalcontributions, onSelect, onWithdrawContributor-facing submission tracker
ContentContributionTypeManagertypes, onSave, onDeleteManage contribution types, channels, and promotion settings
ContentContributorManagercontributors, onSave, onDeleteManage contributors with trust levels

API

Classes

ExportDescription
ContentSTI base model. Fields: type, variant, status, state, category, tags, metadata, thumbnailAssetId
ContentAssetJunction model for canonical content-to-asset ownership in content_assets
ArticleSTI subclass for editorial content
ContentDocumentSTI subclass for structured documents
MirrorSTI subclass for mirrored/cached external content
ContentsCollection with mirror(), syncContentDir(), generateMissingThumbnails(), findWithGlobals(), getOrUpsert(), browseFacts(), getGovernanceDefinitionsAction(), resolveGovernanceAction()
ContentReferenceJunction model for content-to-content links; nullable targetVersion pins citation-time ContentVersion.version for drift detection
ContentReviewAI review result tied to a governance policy
ContentCorrectionPost-publication correction record
ContentVersionContent snapshot with kind ('manual', 'draft', 'review', 'publication', 'correction', or 'auto-generated') and transparency metadata
ContentContributionHeld inbound submission with approval, rejection, withdrawal, and promotion actions
ContentContributionsContribution collection with web intake, email ingestion, inbox, and contributor views
ContentContributionTypePersisted contribution-type override for app-defined intake rules and promotion mapping
ContentContributorContributor profile/trust record resolved by email
ContentContributionRevisionRevision history for held submissions
ContentContributionAttachmentHeld file metadata that only becomes an Asset on promotion
ContentGovernancePolicyPersisted review policy definition
ContentGovernanceProfilePersisted review profile with requirements
ContentGovernanceAssignmentPersisted governance assignment for content type/variant
ThumbnailGeneratorGenerates thumbnails via headline-card, static-map, or ai-generate strategies

Content Instance Methods

MethodDescription
resolveGovernance()Resolve effective governance for this content's type/variant
runReviewAction(options)Run an AI review against a policy
listReviews()List all reviews for this content
listReviewProfilesAction()Get review readiness for all profiles
evaluateReviewProfile(key)Evaluate one profile's requirements
issueCorrectionAction(options)Issue a post-publication correction
listCorrections()List corrections for this content
listVersions()List version history
mutateVersionAction(options)Create a version snapshot
getPublishedTransparencyAction()Get frozen transparency data
previewTransparencyAction()Preview live transparency state
addFact(factId, relationship)Link a fact to this content
getFacts(options)Get linked facts
getFactLinks()Get fact-content link records
getFactsState()Get full facts state (API)
syncFactsState(options)Sync fact links (API)
getAssets(relationship?)Get associated assets
addAsset(asset, relationship, sortOrder)Add asset association
removeAsset(assetId, relationship?)Remove asset association
setThumbnail(image)Set thumbnail (adds asset + updates thumbnailAssetId)
generateThumbnail(options)Generate a thumbnail
addReference(content, options?)Link to another content; pass { targetVersion } to pin the citation to a specific ContentVersion.version
getReferences()Get content references
getReferenceDrift()Per-edge { citedVersion, currentVersion, isDrifted } — surfaces references whose pinned version differs from the target's latest

Types

ExportDescription
ContentOptionsOptions for Content constructor
ContentsOptionsOptions for Contents.create()
ThumbnailStrategy'headline-card' | 'static-map' | 'ai-generate'
ThumbnailOptionsUnion of strategy-specific option types
HeadlineCardThumbnailOptionsOptions for headline-card strategy
StaticMapThumbnailOptionsOptions for static-map strategy
AIGenerateThumbnailOptionsOptions for ai-generate strategy
ContentContributionTypeDefinitionApp-defined contribution type shape
ContentGovernanceConfigShape passed to configureContentGovernance()
ContentGovernanceStateResolved governance state for a content item
ContentReviewResultAI review output with findings
ContentReviewFindingIndividual issue from a review
ContentCorrectionType'correction' | 'retraction' | 'update' | 'clarification'
ContentVersionKind'manual' | 'draft' | 'review' | 'publication' | 'correction' | 'auto-generated'
ContentTransparencyDataFull transparency report data shape
ContentPublishReadinessStateProfile evaluation result

Utilities

ExportDescription
contentToString(content)Serialize content to markdown with YAML frontmatter
stringToContent(str)Parse markdown with frontmatter back to content data
configureContentGovernance(config)Define default governance policies, profiles, and assignments
configureContentContributions(config)Define default contribution types and intake rules
evaluateContentPublishReadiness(options)Evaluate publication readiness against a profile
normalizeContentTransparency(raw)Normalize raw transparency data into standard shape

Auto-Generated Endpoints

The @smrt() decorator generates REST API, MCP tools, and CLI commands.

Content Endpoints (instance-level)

MethodPathDescription
GET/api/v1/contentsList contents
POST/api/v1/contentsCreate content
GET/api/v1/contents/{id}Get content
PUT/api/v1/contents/{id}Update content
DELETE/api/v1/contents/{id}Delete content
GET/api/v1/contents/{id}/factsGet facts state
PUT/api/v1/contents/{id}/factsSync facts state
GET/api/v1/contents/{id}/governanceGet governance state
GET/api/v1/contents/{id}/reviewsList reviews
POST/api/v1/contents/{id}/reviewsRun AI review
GET/api/v1/contents/{id}/review-profilesReview readiness
GET/api/v1/contents/{id}/review-profiles/{profileKey}Evaluate profile
GET/api/v1/contents/{id}/transparencyPublished transparency
GET/api/v1/contents/{id}/transparency/previewPreview transparency
GET/api/v1/contents/{id}/correctionsList corrections
POST/api/v1/contents/{id}/correctionsIssue correction
GET/api/v1/contents/{id}/versionsList versions
POST/api/v1/contents/{id}/versionsCreate version

Collection Endpoints

MethodPathDescription
GET/api/v1/contents/by-slug?slug=...Get by slug
GET/api/v1/contents/factsBrowse fact catalog
GET/api/v1/contents/governanceGovernance definitions
GET/api/v1/contents/governance/resolve?type=...Resolve governance

All s-m-r-t models (ContentGovernancePolicy, ContentContribution, etc.) also get standard CRUD + custom collection-level endpoints.

Dependencies

PackagePurpose
@happyvertical/smrt-coreORM base (SmrtObject, SmrtCollection)
@happyvertical/smrt-assetsAsset association support
@happyvertical/smrt-imagesImage/thumbnail creation
@happyvertical/smrt-factsFact linking and browsing
@happyvertical/smrt-chatContent chat sessions and threads
@happyvertical/smrt-messagesEmail ingestion for contribution intake
@happyvertical/smrt-profilesContributor/profile resolution by email
@happyvertical/smrt-tenancyOptional tenant scoping
@happyvertical/documentsDocument fetching and text extraction
@happyvertical/filesFilesystem operations
@happyvertical/geoStatic map thumbnails
@happyvertical/imagesHeadline card rendering
yamlYAML frontmatter parsing