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:
-
TypeScript Compilation
cd b4m-core
pnpm tsc --noEmit -
Run Tests
pnpm test artifactHelpers
pnpm test artifactSchemas -
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'; -
Documentation
- Update type documentation
- Add JSDoc comments to all exported functions
- Create migration guide for existing code
Testing the Implementation
Manual Testing
- 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:
- Review with team for feedback
- Document any deviations from the plan
- Prepare for Quest 2 (Database Schema)
- Update the progress tracker
Troubleshooting
Common Issues
-
Type conflicts with existing code
- Solution: Use type assertions temporarily
- Plan refactoring in later quests
-
Circular dependencies
- Solution: Ensure proper file organization
- Use type-only imports where needed
-
Schema validation failures
- Solution: Check for missing required fields
- Verify date serialization