Skip to main content

Quest 1: Foundation Types & Models - Implementation Guide

Overview

This guide provides step-by-step instructions for implementing Quest 1 of the artifact system. This quest establishes the type system foundation that all other quests will build upon.

Prerequisites

  • Access to the b4m-core monorepo
  • Understanding of TypeScript and Zod schemas
  • Familiarity with the existing artifact system

Implementation Steps

Step 1: Extend Existing Artifact Types

First, we need to extend the existing artifact types to support our new first-class citizen model.

1.1 Update Base Types

File: b4m-core/packages/core/common/types/entities/ArtifactTypes.ts

Add these new interfaces to the existing file:

// Add after existing imports
export interface ArtifactPermissions {
canRead: string[]; // User IDs
canWrite: string[]; // User IDs
canDelete: string[]; // User IDs
isPublic: boolean;
inheritFromProject: boolean;
}

export enum ArtifactStatus {
DRAFT = 'draft',
REVIEW = 'review',
PUBLISHED = 'published',
ARCHIVED = 'archived',
DELETED = 'deleted'
}

export interface BaseArtifact {
id: string;
type: ArtifactType;
title: string;
description?: string;
metadata: ArtifactMetadata;

// Versioning
version: number;
versionTag?: string;
currentVersionId?: string;
parentVersionId?: string;

// Timestamps
createdAt: Date;
updatedAt: Date;
publishedAt?: Date;
deletedAt?: Date;

// Ownership & Access
userId: string;
projectId?: string;
organizationId?: string;
visibility: 'private' | 'project' | 'organization' | 'public';
permissions: ArtifactPermissions;

// Relationships
sourceQuestId?: string;
sessionId?: string;
parentArtifactId?: string;

// Status
status: ArtifactStatus;
tags: string[];

// Content
contentHash: string;
contentSize: number;
}

// Update existing specific artifact types to extend BaseArtifact
export interface ReactArtifact extends BaseArtifact {
type: 'react';
content: string; // The actual React code
metadata: ArtifactMetadata & {
dependencies: string[];
props?: Record<string, unknown>;
hasDefaultExport: boolean;
errorBoundary: boolean;
};
}

export interface HtmlArtifact extends BaseArtifact {
type: 'html';
content: string; // The HTML content
metadata: ArtifactMetadata & {
allowedScripts: string[];
cspPolicy?: string;
sanitized: boolean;
};
}

export interface SvgArtifact extends BaseArtifact {
type: 'svg';
content: string; // The SVG content
metadata: ArtifactMetadata & {
viewBox?: string;
width?: number;
height?: number;
sanitized: boolean;
};
}

export interface MermaidArtifact extends BaseArtifact {
type: 'mermaid';
content: string; // The Mermaid diagram code
metadata: ArtifactMetadata & {
chartType?: 'flowchart' | 'sequenceDiagram' | 'classDiagram' |
'stateDiagram' | 'entityRelationshipDiagram' |
'gantt' | 'pie' | 'mindmap';
description?: string;
};
}

// Type union for all specific artifacts
export type SpecificArtifact =
| ReactArtifact
| HtmlArtifact
| SvgArtifact
| MermaidArtifact
| QuestMasterArtifact; // Will be defined in next step

Step 2: Create QuestMaster Specific Types

2.1 Create QuestMaster Types

File: b4m-core/packages/core/common/types/entities/QuestMasterArtifactTypes.ts (new file)

import { BaseArtifact, ArtifactType } from './ArtifactTypes';

export interface Quest {
id: string;
title: string;
description: string;
status: 'pending' | 'in-progress' | 'completed' | 'skipped';
order: number;
dependencies?: string[]; // IDs of prerequisite quests
estimatedMinutes?: number;
completedAt?: Date;
completedBy?: string;
metadata?: Record<string, unknown>;
}

export interface QuestMasterArtifact extends BaseArtifact {
type: 'questmaster';
content: QuestMasterContent;
}

export interface QuestMasterContent {
goal: string;
quests: Quest[];
totalSteps: number;
estimatedDuration?: number; // Total minutes
complexity: 'low' | 'medium' | 'high';
category?: string;
prerequisites?: string[];
completionCriteria?: string[];
resources?: QuestResource[];
}

export interface QuestResource {
type: 'documentation' | 'tutorial' | 'example' | 'tool';
title: string;
url: string;
description?: string;
}

// Helper type guards
export function isQuestMasterArtifact(artifact: BaseArtifact): artifact is QuestMasterArtifact {
return artifact.type === 'questmaster';
}

export function isQuestCompleted(quest: Quest): boolean {
return quest.status === 'completed';
}

export function calculateQuestProgress(artifact: QuestMasterArtifact): {
completed: number;
total: number;
percentage: number;
} {
const completed = artifact.content.quests.filter(isQuestCompleted).length;
const total = artifact.content.quests.length;
const percentage = total > 0 ? Math.round((completed / total) * 100) : 0;

return { completed, total, percentage };
}

Step 3: Create Zod Schemas

3.1 Create Artifact Schemas

File: b4m-core/packages/core/common/schemas/artifacts.ts (new file)

import { z } from 'zod';
import { ArtifactTypeSchema } from './common'; // Assuming this exists

// Enums
export const ArtifactStatusSchema = z.enum([
'draft',
'review',
'published',
'archived',
'deleted'
]);

export const VisibilitySchema = z.enum([
'private',
'project',
'organization',
'public'
]);

// Permissions schema
export const ArtifactPermissionsSchema = z.object({
canRead: z.array(z.string()),
canWrite: z.array(z.string()),
canDelete: z.array(z.string()),
isPublic: z.boolean().default(false),
inheritFromProject: z.boolean().default(true)
});

// Base artifact schema
export const BaseArtifactSchema = z.object({
id: z.string().uuid(),
type: ArtifactTypeSchema,
title: z.string().min(1).max(255),
description: z.string().max(1000).optional(),

// Versioning
version: z.number().int().positive().default(1),
versionTag: z.string().max(100).optional(),
currentVersionId: z.string().uuid().optional(),
parentVersionId: z.string().uuid().optional(),

// Timestamps
createdAt: z.date(),
updatedAt: z.date(),
publishedAt: z.date().optional(),
deletedAt: z.date().optional(),

// Ownership & Access
userId: z.string(),
projectId: z.string().optional(),
organizationId: z.string().optional(),
visibility: VisibilitySchema.default('private'),
permissions: ArtifactPermissionsSchema,

// Relationships
sourceQuestId: z.string().optional(),
sessionId: z.string().optional(),
parentArtifactId: z.string().uuid().optional(),

// Status
status: ArtifactStatusSchema.default('draft'),
tags: z.array(z.string().max(50)).max(20).default([]),

// Content
contentHash: z.string(),
contentSize: z.number().int().nonnegative()
});

// Specific artifact schemas
export const ReactArtifactSchema = BaseArtifactSchema.extend({
type: z.literal('react'),
content: z.string(),
metadata: z.object({
dependencies: z.array(z.string()),
props: z.record(z.unknown()).optional(),
hasDefaultExport: z.boolean(),
errorBoundary: z.boolean().default(true)
})
});

export const HtmlArtifactSchema = BaseArtifactSchema.extend({
type: z.literal('html'),
content: z.string(),
metadata: z.object({
allowedScripts: z.array(z.string()).default([]),
cspPolicy: z.string().optional(),
sanitized: z.boolean().default(false)
})
});

export const SvgArtifactSchema = BaseArtifactSchema.extend({
type: z.literal('svg'),
content: z.string(),
metadata: z.object({
viewBox: z.string().optional(),
width: z.number().positive().optional(),
height: z.number().positive().optional(),
sanitized: z.boolean().default(false)
})
});

export const MermaidArtifactSchema = BaseArtifactSchema.extend({
type: z.literal('mermaid'),
content: z.string(),
metadata: z.object({
chartType: z.enum([
'flowchart', 'sequenceDiagram', 'classDiagram',
'stateDiagram', 'entityRelationshipDiagram',
'gantt', 'pie', 'mindmap'
]).optional(),
description: z.string().optional()
})
});

// Type exports
export type ArtifactStatus = z.infer<typeof ArtifactStatusSchema>;
export type Visibility = z.infer<typeof VisibilitySchema>;
export type ArtifactPermissions = z.infer<typeof ArtifactPermissionsSchema>;
export type BaseArtifact = z.infer<typeof BaseArtifactSchema>;
export type ReactArtifact = z.infer<typeof ReactArtifactSchema>;
export type HtmlArtifact = z.infer<typeof HtmlArtifactSchema>;
export type SvgArtifact = z.infer<typeof SvgArtifactSchema>;
export type MermaidArtifact = z.infer<typeof MermaidArtifactSchema>;

3.2 Create QuestMaster Schemas

File: b4m-core/packages/core/common/schemas/questmaster.ts (new file)

import { z } from 'zod';
import { BaseArtifactSchema } from './artifacts';

// Quest schemas
export const QuestStatusSchema = z.enum([
'pending',
'in-progress',
'completed',
'skipped'
]);

export const QuestSchema = z.object({
id: z.string().uuid(),
title: z.string().min(1).max(255),
description: z.string().max(1000),
status: QuestStatusSchema.default('pending'),
order: z.number().int().nonnegative(),
dependencies: z.array(z.string().uuid()).optional(),
estimatedMinutes: z.number().int().positive().optional(),
completedAt: z.date().optional(),
completedBy: z.string().optional(),
metadata: z.record(z.unknown()).optional()
});

export const QuestResourceSchema = z.object({
type: z.enum(['documentation', 'tutorial', 'example', 'tool']),
title: z.string().min(1).max(255),
url: z.string().url(),
description: z.string().max(500).optional()
});

export const QuestMasterContentSchema = z.object({
goal: z.string().min(1).max(500),
quests: z.array(QuestSchema).min(1),
totalSteps: z.number().int().positive(),
estimatedDuration: z.number().int().positive().optional(),
complexity: z.enum(['low', 'medium', 'high']),
category: z.string().max(100).optional(),
prerequisites: z.array(z.string().max(255)).optional(),
completionCriteria: z.array(z.string().max(500)).optional(),
resources: z.array(QuestResourceSchema).optional()
});

export const QuestMasterArtifactSchema = BaseArtifactSchema.extend({
type: z.literal('questmaster'),
content: QuestMasterContentSchema
});

// Type exports
export type QuestStatus = z.infer<typeof QuestStatusSchema>;
export type Quest = z.infer<typeof QuestSchema>;
export type QuestResource = z.infer<typeof QuestResourceSchema>;
export type QuestMasterContent = z.infer<typeof QuestMasterContentSchema>;
export type QuestMasterArtifact = z.infer<typeof QuestMasterArtifactSchema>;

// Validation helpers
export const validateQuest = (data: unknown): Quest => {
return QuestSchema.parse(data);
};

export const validateQuestMasterArtifact = (data: unknown): QuestMasterArtifact => {
return QuestMasterArtifactSchema.parse(data);
};

Step 4: Create Utility Functions

4.1 Create Type Guards and Helpers

File: b4m-core/packages/core/common/utils/artifactHelpers.ts (new file)

import { BaseArtifact, ArtifactType, ArtifactStatus } from '../types/entities/ArtifactTypes';
import { createHash } from 'crypto';

// Type guards
export function isArtifact(obj: unknown): obj is BaseArtifact {
return (
typeof obj === 'object' &&
obj !== null &&
'id' in obj &&
'type' in obj &&
'title' in obj
);
}

export function isPublicArtifact(artifact: BaseArtifact): boolean {
return artifact.visibility === 'public' || artifact.permissions.isPublic;
}

export function isDraftArtifact(artifact: BaseArtifact): boolean {
return artifact.status === ArtifactStatus.DRAFT;
}

export function isPublishedArtifact(artifact: BaseArtifact): boolean {
return artifact.status === ArtifactStatus.PUBLISHED;
}

// Content helpers
export function calculateContentHash(content: string): string {
return createHash('sha256').update(content).digest('hex');
}

export function calculateContentSize(content: string): number {
return Buffer.byteLength(content, 'utf8');
}

// Permission helpers
export function canUserReadArtifact(artifact: BaseArtifact, userId: string): boolean {
if (isPublicArtifact(artifact)) return true;
if (artifact.userId === userId) return true;
if (artifact.permissions.canRead.includes(userId)) return true;
return false;
}

export function canUserWriteArtifact(artifact: BaseArtifact, userId: string): boolean {
if (artifact.userId === userId) return true;
if (artifact.permissions.canWrite.includes(userId)) return true;
return false;
}

export function canUserDeleteArtifact(artifact: BaseArtifact, userId: string): boolean {
if (artifact.userId === userId) return true;
if (artifact.permissions.canDelete.includes(userId)) return true;
return false;
}

// Factory functions
export function createDefaultPermissions(userId: string): ArtifactPermissions {
return {
canRead: [userId],
canWrite: [userId],
canDelete: [userId],
isPublic: false,
inheritFromProject: true
};
}

export function createArtifactId(): string {
// Use your existing ID generation logic
return `artifact_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
}

// Validation helpers
export function validateArtifactTitle(title: string): { valid: boolean; error?: string } {
if (!title || title.trim().length === 0) {
return { valid: false, error: 'Title is required' };
}
if (title.length > 255) {
return { valid: false, error: 'Title must be 255 characters or less' };
}
return { valid: true };
}

export function validateArtifactTags(tags: string[]): { valid: boolean; error?: string } {
if (tags.length > 20) {
return { valid: false, error: 'Maximum 20 tags allowed' };
}
const invalidTag = tags.find(tag => tag.length > 50);
if (invalidTag) {
return { valid: false, error: 'Tags must be 50 characters or less' };
}
return { valid: true };
}

Step 5: Create Unit Tests

5.1 Test Type Guards

File: b4m-core/packages/core/common/__tests__/artifactHelpers.test.ts (new file)

import { describe, it, expect } from 'vitest';
import {
isArtifact,
isPublicArtifact,
canUserReadArtifact,
calculateContentHash,
validateArtifactTitle
} from '../utils/artifactHelpers';
import { BaseArtifact, ArtifactStatus } from '../types/entities/ArtifactTypes';

describe('Artifact Helpers', () => {
const mockArtifact: BaseArtifact = {
id: 'test-123',
type: 'react',
title: 'Test Artifact',
version: 1,
status: ArtifactStatus.PUBLISHED,
userId: 'user-123',
visibility: 'private',
permissions: {
canRead: ['user-123'],
canWrite: ['user-123'],
canDelete: ['user-123'],
isPublic: false,
inheritFromProject: true
},
tags: [],
contentHash: 'abc123',
contentSize: 1024,
createdAt: new Date(),
updatedAt: new Date()
};

describe('isArtifact', () => {
it('should return true for valid artifact', () => {
expect(isArtifact(mockArtifact)).toBe(true);
});

it('should return false for invalid objects', () => {
expect(isArtifact(null)).toBe(false);
expect(isArtifact({})).toBe(false);
expect(isArtifact({ id: '123' })).toBe(false);
});
});

describe('isPublicArtifact', () => {
it('should return true for public visibility', () => {
const publicArtifact = { ...mockArtifact, visibility: 'public' as const };
expect(isPublicArtifact(publicArtifact)).toBe(true);
});

it('should return true for public permission', () => {
const publicArtifact = {
...mockArtifact,
permissions: { ...mockArtifact.permissions, isPublic: true }
};
expect(isPublicArtifact(publicArtifact)).toBe(true);
});

it('should return false for private artifact', () => {
expect(isPublicArtifact(mockArtifact)).toBe(false);
});
});

describe('canUserReadArtifact', () => {
it('should allow owner to read', () => {
expect(canUserReadArtifact(mockArtifact, 'user-123')).toBe(true);
});

it('should allow users in read permissions', () => {
const artifact = {
...mockArtifact,
permissions: {
...mockArtifact.permissions,
canRead: ['user-123', 'user-456']
}
};
expect(canUserReadArtifact(artifact, 'user-456')).toBe(true);
});

it('should deny users not in permissions', () => {
expect(canUserReadArtifact(mockArtifact, 'user-999')).toBe(false);
});
});

describe('calculateContentHash', () => {
it('should generate consistent hash for same content', () => {
const content = 'Hello, World!';
const hash1 = calculateContentHash(content);
const hash2 = calculateContentHash(content);
expect(hash1).toBe(hash2);
});

it('should generate different hash for different content', () => {
const hash1 = calculateContentHash('Hello');
const hash2 = calculateContentHash('World');
expect(hash1).not.toBe(hash2);
});
});

describe('validateArtifactTitle', () => {
it('should accept valid titles', () => {
expect(validateArtifactTitle('Valid Title')).toEqual({ valid: true });
});

it('should reject empty titles', () => {
expect(validateArtifactTitle('')).toEqual({
valid: false,
error: 'Title is required'
});
});

it('should reject titles over 255 characters', () => {
const longTitle = 'a'.repeat(256);
expect(validateArtifactTitle(longTitle)).toEqual({
valid: false,
error: 'Title must be 255 characters or less'
});
});
});
});

5.2 Test Zod Schemas

File: b4m-core/packages/core/common/__tests__/artifactSchemas.test.ts (new file)

import { describe, it, expect } from 'vitest';
import {
BaseArtifactSchema,
ReactArtifactSchema,
QuestMasterArtifactSchema
} from '../schemas/artifacts';
import { ArtifactStatus } from '../types/entities/ArtifactTypes';

describe('Artifact Schemas', () => {
describe('BaseArtifactSchema', () => {
it('should validate a complete artifact', () => {
const artifact = {
id: '550e8400-e29b-41d4-a716-446655440000',
type: 'react',
title: 'Test Component',
version: 1,
status: 'published',
userId: 'user-123',
visibility: 'private',
permissions: {
canRead: ['user-123'],
canWrite: ['user-123'],
canDelete: ['user-123'],
isPublic: false,
inheritFromProject: true
},
tags: ['test', 'component'],
contentHash: 'abc123',
contentSize: 1024,
createdAt: new Date(),
updatedAt: new Date()
};

const result = BaseArtifactSchema.safeParse(artifact);
expect(result.success).toBe(true);
});

it('should provide defaults for optional fields', () => {
const minimal = {
id: '550e8400-e29b-41d4-a716-446655440000',
type: 'react',
title: 'Test',
userId: 'user-123',
permissions: {
canRead: [],
canWrite: [],
canDelete: []
},
contentHash: 'abc123',
contentSize: 0,
createdAt: new Date(),
updatedAt: new Date()
};

const result = BaseArtifactSchema.parse(minimal);
expect(result.version).toBe(1);
expect(result.status).toBe('draft');
expect(result.visibility).toBe('private');
expect(result.tags).toEqual([]);
});

it('should reject invalid UUIDs', () => {
const invalid = {
id: 'not-a-uuid',
type: 'react',
title: 'Test',
// ... other required fields
};

const result = BaseArtifactSchema.safeParse(invalid);
expect(result.success).toBe(false);
});
});

describe('ReactArtifactSchema', () => {
it('should validate React artifact with metadata', () => {
const reactArtifact = {
id: '550e8400-e29b-41d4-a716-446655440000',
type: 'react',
title: 'Button Component',
content: 'export default function Button() { return <button>Click me</button>; }',
metadata: {
dependencies: ['react'],
hasDefaultExport: true,
errorBoundary: true
},
// ... other required fields
version: 1,
status: 'published',
userId: 'user-123',
visibility: 'private',
permissions: {
canRead: ['user-123'],
canWrite: ['user-123'],
canDelete: ['user-123'],
isPublic: false,
inheritFromProject: true
},
tags: [],
contentHash: 'xyz789',
contentSize: 2048,
createdAt: new Date(),
updatedAt: new Date()
};

const result = ReactArtifactSchema.safeParse(reactArtifact);
expect(result.success).toBe(true);
});

it('should enforce type literal', () => {
const wrongType = {
type: 'html', // Wrong type
content: 'function Component() {}',
metadata: {
dependencies: [],
hasDefaultExport: true
},
// ... other fields
};

const result = ReactArtifactSchema.safeParse(wrongType);
expect(result.success).toBe(false);
});
});
});

Step 6: Integration Checklist

After implementing all the above files, ensure:

  1. TypeScript Compilation

    cd b4m-core
    pnpm tsc --noEmit
  2. Run Tests

    pnpm test artifactHelpers
    pnpm test artifactSchemas
  3. Update Exports

    File: b4m-core/packages/core/common/index.ts

    // Add new exports
    export * from './types/entities/ArtifactTypes';
    export * from './types/entities/QuestMasterArtifactTypes';
    export * from './schemas/artifacts';
    export * from './schemas/questmaster';
    export * from './utils/artifactHelpers';
  4. Documentation

    • Update type documentation
    • Add JSDoc comments to all exported functions
    • Create migration guide for existing code

Testing the Implementation

Manual Testing

  1. Create a test script to validate the types:

File: test-artifact-types.ts

import { 
BaseArtifact,
ReactArtifact,
QuestMasterArtifact,
validateArtifactTitle,
createDefaultPermissions
} from '@b4m-core/common';

// Test creating an artifact
const testArtifact: ReactArtifact = {
id: 'test-123',
type: 'react',
title: 'Test Component',
content: 'export default function Test() { return <div>Test</div>; }',
metadata: {
dependencies: ['react'],
hasDefaultExport: true,
errorBoundary: false
},
version: 1,
status: 'draft',
userId: 'user-123',
visibility: 'private',
permissions: createDefaultPermissions('user-123'),
tags: ['test'],
contentHash: 'abc123',
contentSize: 100,
createdAt: new Date(),
updatedAt: new Date()
};

console.log('Artifact created:', testArtifact);
console.log('Title valid:', validateArtifactTitle(testArtifact.title));

Automated Testing

Run the full test suite:

pnpm test:watch

Next Steps

Once Quest 1 is complete:

  1. Review with team for feedback
  2. Document any deviations from the plan
  3. Prepare for Quest 2 (Database Schema)
  4. Update the progress tracker

Troubleshooting

Common Issues

  1. Type conflicts with existing code

    • Solution: Use type assertions temporarily
    • Plan refactoring in later quests
  2. Circular dependencies

    • Solution: Ensure proper file organization
    • Use type-only imports where needed
  3. Schema validation failures

    • Solution: Check for missing required fields
    • Verify date serialization

Resources