Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@ jobs:
- name: Run linter
run: pnpm run lint

- name: TypeScript type checking
run: pnpm run typecheck

- name: Build project
run: pnpm run build

Expand Down
8 changes: 4 additions & 4 deletions .github/workflows/code-quality.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ jobs:

- name: Check for console.log statements
run: |
if grep -r "console\.log" src/ --include="*.ts" --exclude-dir=node_modules; then
if grep -r "console\.log" packages/ --include="*.ts" --exclude-dir=node_modules --exclude-dir=__tests__; then
echo "Warning: console.log statements found in source code"
echo "Please use proper logging instead"
# This is just a warning, not failing the build
Expand All @@ -50,19 +50,19 @@ jobs:
continue-on-error: true

- name: Check TypeScript compilation
run: pnpm run build
run: pnpm run typecheck

- name: Run ESLint
run: pnpm run lint

- name: Check for TODO comments
run: |
echo "Checking for TODO/FIXME comments..."
grep -r "TODO\|FIXME" src/ --include="*.ts" || echo "No TODO/FIXME found"
grep -r "TODO\|FIXME" packages/ --include="*.ts" --exclude-dir=node_modules || echo "No TODO/FIXME found"
continue-on-error: true

- name: Check file sizes
run: |
echo "Checking for large files..."
find src/ -type f -size +100k -exec ls -lh {} \; || echo "No large files found"
find packages/ -type f -size +100k -exec ls -lh {} \; || echo "No large files found"
continue-on-error: true
6 changes: 4 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -143,10 +143,12 @@ Only the compiled `dist/` folder is included in published packages (controlled b
## 🔄 Pull Request Process

1. **Before Creating a PR**
- Ensure all tests pass
- Run linting and fix any issues
- Ensure all tests pass (`pnpm test`)
- Run type checking (`pnpm typecheck`)
- Run linting and fix any issues (`pnpm lint`)
- Update documentation if needed
- Add tests for new features
- Update ROADMAP.md if the change completes a roadmap item
- **Add a changeset** if your changes affect package behavior

2. **Creating a PR**
Expand Down
16 changes: 10 additions & 6 deletions DEVELOPMENT_WORKFLOW.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,16 +86,18 @@ export default ObjectSchema.create({

```typescript
// packages/{pkg}/src/my_entity.hook.ts
export function beforeInsert(context: any) {
const { doc, broker } = context;
import type { HookContext } from '@objectstack/spec/data';

export function beforeInsert(context: HookContext) {
const doc = context.input.doc as Record<string, any>;
// Validation and business logic
if (!doc.name) {
throw new Error('Name is required');
}
}

export function afterInsert(context: any) {
const { doc, broker } = context;
export function afterInsert(context: HookContext) {
const doc = context.result as Record<string, any>;
// Post-creation side effects (notifications, related records, etc.)
}
```
Expand Down Expand Up @@ -204,8 +206,10 @@ If the field needs validation or side effects, add logic to the hook file:

```typescript
// packages/crm/src/account.hook.ts
export function beforeInsert(context: any) {
const { doc } = context;
import type { HookContext } from '@objectstack/spec/data';

export function beforeInsert(context: HookContext) {
const doc = context.input.doc as Record<string, any>;
// Validate URL format if provided
if (doc.website_url && !doc.website_url.startsWith('http')) {
doc.website_url = 'https://' + doc.website_url;
Expand Down
32 changes: 31 additions & 1 deletion ROADMAP.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# HotCRM Development Roadmap

> Comprehensive development plan for HotCRM — the world's first AI-Native CRM.
> Protocol: @objectstack/spec v3.0.8 | Last Updated: February 21, 2027
> Protocol: @objectstack/spec v3.0.8 | Last Updated: February 2026

## Strategic Direction

Expand All @@ -15,6 +15,7 @@
2027 Q1-Q2 ████████████████████████████████ Phase 13: Module Optimization & Seed Data ✅ COMPLETE
2027 Q2-Q3 ████████████████████████████████ Phase 14: v3.0.8 Feed & Interface Builder Adoption ✅ COMPLETE
2027+ ████████████████████████████████ Phase 12E: Advanced AI & Enterprise Features ✅ COMPLETE
2026 Q1 ████████████████████████████████ Phase 15: Repository Quality & DX Improvements 🔄 IN PROGRESS
```

## Current State Summary
Expand Down Expand Up @@ -171,10 +172,39 @@ These items were deferred during Phase 13 and have been completed:

---

### Phase 15: Repository Quality & DX Improvements (2026 Q1)

> Goal: Improve code quality, CI/CD, documentation accuracy, and developer experience.

#### High Priority

- [x] **DEVELOPMENT_WORKFLOW.md test count** — Remove hardcoded test count; use dynamic description
- [x] **Hook `any` type migration** — Replace `ctx: any` with `ctx: HookContext` in 18 helper functions across hook files
- [x] **pnpm-workspace.yaml documentation** — Add comment explaining core/server exclusion reason
- [x] **CI workflow typecheck** — Add `pnpm typecheck` step to CI workflow
- [x] **Code quality workflow paths** — Fix `src/` references to `packages/` in code-quality workflow

#### Medium Priority

- [x] **ESLint version alignment** — Align `@typescript-eslint/parser` (^6 → ^7) with `@typescript-eslint/eslint-plugin` (^7)
- [x] **CONTRIBUTING.md checklist** — Add pre-submission development checklist (typecheck, test, ROADMAP)
- [x] **Documentation hook examples** — Update DEVELOPMENT_WORKFLOW.md code examples to use `HookContext` instead of `any`
- [x] **ROADMAP.md date sync** — Update last-updated date and version timeline to match current state

#### Deferred / Future

- [ ] **Structured logging migration** — Replace `console.log` with pino logger across hook files
- [ ] **Legacy API cleanup** — Remove `db.ts` references and migrate to broker/ObjectQL
- [ ] **E2E test coverage** — Build API-layer end-to-end tests for Feed API / MCP API
- [ ] **FormView `as any` cleanup** — Remove `as any` casts once `@objectstack/spec` aligns FormView column types

---

## Version Upgrade History

| Date | From | To | Breaking Changes | Tests |
|------|------|----|-----------------|-------|
| 2026-02-21 | v3.0.8 | v3.0.8 | None (Phase 15: Repository quality improvements — hook `any` type migration, CI typecheck, ESLint version alignment, documentation updates, code-quality workflow fixes) | 3799 ✅ |
| 2027-02-21 | v3.0.8 | v3.0.8 | None (Phase 14 complete: Activity Feed/Chatter across 6 clouds, 4 Interface Builder blank pages, Dashboard headers & global filters on 6 clouds, ViewTabs on 18 views, Feed API 8 endpoints, Feed service contract, System metadata, Studio builder configs, Navigation areas, 327 new tests) | 3707 ✅ |
| 2026-02-21 | v3.0.6 | v3.0.8 | None (New: Activity Feed/Chatter system, Interface Builder/Blank Pages, Dashboard headers & global filters, Feed API contracts, Studio builder configs, Oclif CLI plugin, Package conventions; Phase 14 roadmap added) | 3318 ✅ |
| 2026-02-16 | v3.0.0 | v3.0.0 | None (Phase 13 roadmap: Module-by-module deep optimization, seed data foundation, vertical package UI enhancement, core cloud metadata equalization) | 3318 ✅ |
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@
"@objectstack/studio": "^3.0.8",
"@types/node": "^25.2.3",
"@typescript-eslint/eslint-plugin": "^7.18.0",
"@typescript-eslint/parser": "^6.21.0",
"@typescript-eslint/parser": "^7.18.0",
"@vitest/coverage-v8": "^1.6.1",
"@vitest/ui": "^1.6.1",
"eslint": "^8.57.1",
Expand Down
6 changes: 3 additions & 3 deletions packages/crm/src/hooks/account.hook.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ const AccountHealthScoreTrigger: Hook = {
/**
* Calculate account health score (0-100)
*/
async function calculateHealthScore(account: Record<string, any>, ctx: any): Promise<number> {
async function calculateHealthScore(account: Record<string, any>, ctx: HookContext): Promise<number> {
let score = 0;

// Base score for active customer status (20 points)
Expand Down Expand Up @@ -125,7 +125,7 @@ const AccountHierarchyTrigger: Hook = {
/**
* Update parent account metrics by aggregating child account data
*/
async function updateParentAccountMetrics(parentId: string, ctx: any): Promise<void> {
async function updateParentAccountMetrics(parentId: string, ctx: HookContext): Promise<void> {
console.log(`🔄 Updating parent account metrics: ${parentId}`);

// In real implementation, would query all child accounts and aggregate
Expand All @@ -146,7 +146,7 @@ async function updateParentAccountMetrics(parentId: string, ctx: any): Promise<v
/**
* Cascade ownership changes to child accounts
*/
async function cascadeOwnershipChange(accountId: string, newOwnerId: string, ctx: any): Promise<void> {
async function cascadeOwnershipChange(accountId: string, newOwnerId: string, ctx: HookContext): Promise<void> {
console.log(`🔄 Cascading ownership change to child accounts of ${accountId}`);

// In real implementation, would query and update all child accounts
Expand Down
6 changes: 3 additions & 3 deletions packages/crm/src/hooks/activity.hook.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ const ActivityRelatedObjectUpdatesTrigger: Hook = {
/**
* Update Contact's last activity date
*/
async function updateContactLastActivityDate(whoId: string, activityDate: string, ctx: any): Promise<void> {
async function updateContactLastActivityDate(whoId: string, activityDate: string, ctx: HookContext): Promise<void> {
console.log(`🔄 Updating LastContactDate for contact: ${whoId}`);

// In real implementation:
Expand All @@ -98,7 +98,7 @@ async function updateContactLastActivityDate(whoId: string, activityDate: string
/**
* Update related object's last activity date
*/
async function updateWhatObjectLastActivityDate(whatId: string, activityDate: string, ctx: any): Promise<void> {
async function updateWhatObjectLastActivityDate(whatId: string, activityDate: string, ctx: HookContext): Promise<void> {
console.log(`🔄 Updating LastActivityDate for related object: ${whatId}`);

// In real implementation, would need to determine object type from WhatId
Expand Down Expand Up @@ -154,7 +154,7 @@ const ActivityCompletionTrigger: Hook = {
/**
* Create next recurrence of a recurring activity
*/
async function createNextRecurrence(activity: Record<string, any>, ctx: any): Promise<void> {
async function createNextRecurrence(activity: Record<string, any>, ctx: HookContext): Promise<void> {
console.log(`🔄 Creating next recurrence for: ${activity.Subject}`);

// Calculate next occurrence date based on pattern
Expand Down
36 changes: 18 additions & 18 deletions packages/crm/src/hooks/opportunity.hook.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,9 +82,9 @@ const OpportunityStageChange: Hook = {
/**
* Handle Closed Won automation
*/
async function handleClosedWon(ctx: any): Promise<void> {
async function handleClosedWon(ctx: HookContext): Promise<void> {
console.log('✅ Processing Closed Won automation...');
const opportunity = ctx.result;
const opportunity = ctx.result as Record<string, any>;

if (!opportunity.AccountId) {
console.error('❌ Cannot process: Opportunity has no AccountId');
Expand All @@ -96,7 +96,7 @@ async function handleClosedWon(ctx: any): Promise<void> {
// 1. Create Contract
let contractId;
try {
const contract = await ctx.ql.doc.create('contract', {
const contract = await (ctx.ql as any).doc.create('contract', {
AccountId: opportunity.AccountId,
OpportunityId: opportunity.Id,
Status: 'draft',
Expand All @@ -118,7 +118,7 @@ async function handleClosedWon(ctx: any): Promise<void> {

// 2. Update Account Status
try {
await ctx.ql.doc.update('account', opportunity.AccountId, {
await (ctx.ql as any).doc.update('account', opportunity.AccountId, {
CustomerStatus: 'active_customer'
});
console.log('✅ Account status updated to Active Customer');
Expand All @@ -129,7 +129,7 @@ async function handleClosedWon(ctx: any): Promise<void> {

// 3. Log activity
try {
await ctx.ql.doc.create('activity', {
await (ctx.ql as any).doc.create('activity', {
Subject: `Deal Won: ${opportunity.Name}`,
Type: 'Milestone',
Status: 'completed',
Expand Down Expand Up @@ -160,17 +160,17 @@ async function handleClosedWon(ctx: any): Promise<void> {
}
}

async function handleClosedLost(ctx: any): Promise<void> {
async function handleClosedLost(ctx: HookContext): Promise<void> {
console.log('❌ Processing Closed Lost automation...');
const opportunity = ctx.result;
const opportunity = ctx.result as Record<string, any>;

if (!opportunity.AccountId) {
return;
}

// Log activity for lost opportunity
try {
await ctx.ql.doc.create('activity', {
await (ctx.ql as any).doc.create('activity', {
Subject: `Deal Lost: ${opportunity.Name}`,
Type: 'Milestone',
Status: 'completed',
Expand All @@ -192,20 +192,20 @@ async function handleClosedLost(ctx: any): Promise<void> {
/**
* Log activity when stage changes
*/
async function logStageChange(ctx: any): Promise<void> {
async function logStageChange(ctx: HookContext): Promise<void> {
try {
const opportunity = ctx.result;
const oldStage = ctx.previous?.Stage || 'unknown';
await ctx.ql.doc.create('activity', {
Subject: `Opportunity Stage Change: ${oldStage} → ${ctx.result.Stage}`,
const opportunity = ctx.result as Record<string, any>;
const oldStage = (ctx.previous as Record<string, any>)?.Stage || 'unknown';
await (ctx.ql as any).doc.create('activity', {
Subject: `Opportunity Stage Change: ${oldStage} → ${(ctx.result as Record<string, any>).Stage}`,
Type: 'Stage Change',
Status: 'completed',
Priority: 'normal',
AccountId: opportunity.AccountId,
WhatId: opportunity.Id,
OwnerId: ctx.user.id,
ActivityDate: new Date().toISOString().split('T')[0],
Description: `Opportunity stage changed from "${oldStage}" to "${ctx.result.Stage}"`
Description: `Opportunity stage changed from "${oldStage}" to "${(ctx.result as Record<string, any>).Stage}"`
});
} catch (error) {
console.error('❌ Failed to log stage change activity:', error);
Expand All @@ -215,8 +215,8 @@ async function logStageChange(ctx: any): Promise<void> {
/**
* Validate required fields for advanced stages
*/
async function validateStageRequirements(ctx: any): Promise<void> {
const opportunity = ctx.result;
async function validateStageRequirements(ctx: HookContext): Promise<void> {
const opportunity = ctx.result as Record<string, any>;
const stage = opportunity.Stage;
const warnings: string[] = [];

Expand Down Expand Up @@ -252,13 +252,13 @@ async function validateStageRequirements(ctx: any): Promise<void> {
/**
* Helper: Count related quotes
*/
async function countRelatedQuotes(ctx: any, opportunityId: string): Promise<number> {
async function countRelatedQuotes(ctx: HookContext, opportunityId: string): Promise<number> {
// Check if quote object exists first (it's in products package)
try {
// In a real monorepo with strict boundaries, we might use a decoupled service.
// Here we assume the broker can find 'quote' across packages.
// Mocking for now since we don't have the full runtime
const quotes = await ctx.ql.find('quote', {
const quotes = await (ctx.ql as any).find('quote', {
filters: [['opportunity', '=', opportunityId]]
});
return quotes.length;
Expand Down
14 changes: 7 additions & 7 deletions packages/hr/src/hooks/application.hook.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ const ApplicationStatusWorkflowTrigger: Hook = {
/**
* Create an interview record when application moves to interview stage
*/
async function createInterviewRecord(application: any, ctx: any): Promise<void> {
async function createInterviewRecord(application: Record<string, any>, ctx: HookContext): Promise<void> {
try {
const scheduledDate = new Date();
scheduledDate.setDate(scheduledDate.getDate() + 3); // Default: 3 days from now
Expand All @@ -91,7 +91,7 @@ async function createInterviewRecord(application: any, ctx: any): Promise<void>
/**
* Update candidate status to 'hired' when application is hired
*/
async function updateCandidateToHired(application: any, ctx: any): Promise<void> {
async function updateCandidateToHired(application: Record<string, any>, ctx: HookContext): Promise<void> {
try {
if (!application.candidate_id) {
console.warn('⚠️ No candidate linked to application, skipping candidate update');
Expand All @@ -112,10 +112,10 @@ async function updateCandidateToHired(application: any, ctx: any): Promise<void>
* Log application status change
*/
async function logApplicationStatusChange(
application: any,
application: Record<string, any>,
oldStatus: string,
newStatus: string,
ctx: any
ctx: HookContext
): Promise<void> {
try {
console.log(`📝 Logging application status change: ${oldStatus} → ${newStatus} for ${application.application_number}`);
Expand Down Expand Up @@ -179,7 +179,7 @@ const ApplicationScreeningTrigger: Hook = {
/**
* Fetch candidate record
*/
async function fetchCandidate(candidateId: string, ctx: any): Promise<any> {
async function fetchCandidate(candidateId: string, ctx: HookContext): Promise<any> {
try {
const candidates = await (ctx.ql as any).find('candidate', {
filters: [['id', '=', candidateId]],
Expand All @@ -195,7 +195,7 @@ async function fetchCandidate(candidateId: string, ctx: any): Promise<any> {
/**
* Fetch recruitment record
*/
async function fetchRecruitment(recruitmentId: string, ctx: any): Promise<any> {
async function fetchRecruitment(recruitmentId: string, ctx: HookContext): Promise<any> {
try {
if (!recruitmentId) return null;
const recruitments = await (ctx.ql as any).find('recruitment', {
Expand All @@ -212,7 +212,7 @@ async function fetchRecruitment(recruitmentId: string, ctx: any): Promise<any> {
/**
* Calculate screening score based on candidate qualifications
*/
function calculateScreeningScore(candidate: any, recruitment: any): number {
function calculateScreeningScore(candidate: Record<string, any>, recruitment: Record<string, any>): number {
let score = 0;

// Experience score (30 points)
Expand Down
Loading