Skip to content

Commit 5366c35

Browse files
authored
Merge pull request #644 from objectstack-ai/copilot/continue-roadmap-development
2 parents cfe89f8 + 6fa3dfb commit 5366c35

11 files changed

Lines changed: 1247 additions & 12 deletions

DX_ROADMAP.md

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,8 @@ This roadmap prioritizes improvements based on the **"Time to First Wow"** metri
2222
| **Test Coverage** | ⭐⭐⭐⭐⭐ | 191 test files, 5,157+ tests |
2323
| **IDE Autocomplete** | ⭐⭐⭐⭐ | Bundled `objectstack.json`, `.describe()` tooltips |
2424
| **Getting Started** | ⭐⭐⭐ | Docs exist but no interactive playground |
25-
| **Error Messages** | ⭐⭐⭐ | Zod defaults + some custom refinements |
26-
| **Helper Functions** | ⭐⭐⭐ | `Field.*` excellent; `ObjectSchema.create()`, `defineStack()` minimal |
25+
| **Error Messages** | ⭐⭐⭐ | Custom error map with contextual messages and "Did you mean?" suggestions |
26+
| **Helper Functions** | ⭐⭐⭐ | `Field.*`, `ObjectSchema.create()`, `defineStack()`, `defineView()`, `defineApp()`, `defineFlow()`, `defineAgent()` + strict mode |
2727
| **Reference Docs** | ⭐⭐⭐ | API docs generated but no field type gallery or error code reference |
2828
| **Examples** | ⭐⭐⭐ | 4 examples but missing "How to Run" instructions |
2929
| **Migration Story** | ⭐⭐ | V3 guide exists but no automated `codemod` tooling |
@@ -111,11 +111,11 @@ This roadmap prioritizes improvements based on the **"Time to First Wow"** metri
111111
- [x] Implement `defineView()` with column type inference
112112
- [x] Implement `defineApp()` with navigation builder
113113
- [x] Implement `defineFlow()` with step type inference
114-
- [ ] Create custom Zod error map with contextual messages
115-
- [ ] Add "Did you mean?" suggestions for FieldType typos
116-
- [ ] Create pretty-print validation error formatter for CLI
117-
- [ ] Add branded types for ObjectName, FieldName, ViewName
118-
- [ ] Add strict cross-reference validation mode to `defineStack()`
114+
- [x] Create custom Zod error map with contextual messages
115+
- [x] Add "Did you mean?" suggestions for FieldType typos
116+
- [x] Create pretty-print validation error formatter for CLI
117+
- [x] Add branded types for ObjectName, FieldName, ViewName
118+
- [x] Add strict cross-reference validation mode to `defineStack()`
119119

120120
---
121121

@@ -362,4 +362,4 @@ This roadmap prioritizes improvements based on the **"Time to First Wow"** metri
362362

363363
**Last Updated:** 2026-02-12
364364
**Maintainers:** ObjectStack Core Team
365-
**Status:** 🆕 Active — Phase 1 Ready to Start
365+
**Status:** 🔄 Active — Phase 2 Complete, Phase 3 Ready to Start

packages/spec/src/index.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,8 @@ export {
7171
ObjectOSCapabilitiesSchema
7272
} from './stack.zod';
7373

74+
export type { DefineStackOptions } from './stack.zod';
75+
7476
export * from './stack.zod';
7577

7678
// DX Helper Functions (re-exported for convenience)
@@ -79,5 +81,9 @@ export { defineApp } from './ui/app.zod';
7981
export { defineFlow } from './automation/flow.zod';
8082
export { defineAgent } from './ai/agent.zod';
8183

84+
// DX Validation Utilities (re-exported for convenience)
85+
export { objectStackErrorMap, formatZodError, formatZodIssue, safeParsePretty } from './shared/error-map.zod';
86+
export { suggestFieldType, findClosestMatches, formatSuggestion } from './shared/suggestions.zod';
87+
8288
export { type PluginContext } from './kernel/plugin.zod';
8389

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
import { describe, it, expect } from 'vitest';
2+
import {
3+
ObjectNameSchema,
4+
FieldNameSchema,
5+
ViewNameSchema,
6+
AppNameSchema,
7+
FlowNameSchema,
8+
RoleNameSchema,
9+
type ObjectName,
10+
type FieldName,
11+
type ViewName,
12+
type AppName,
13+
type FlowName,
14+
type RoleName,
15+
} from './branded-types.zod';
16+
17+
describe('ObjectNameSchema', () => {
18+
it('should accept valid snake_case object names', () => {
19+
const validNames = ['account', 'project_task', 'crm_lead', 'user_profile'];
20+
validNames.forEach((name) => {
21+
expect(() => ObjectNameSchema.parse(name)).not.toThrow();
22+
});
23+
});
24+
25+
it('should reject dots (not allowed in object names)', () => {
26+
expect(() => ObjectNameSchema.parse('user.created')).toThrow();
27+
});
28+
29+
it('should reject uppercase', () => {
30+
expect(() => ObjectNameSchema.parse('Account')).toThrow();
31+
expect(() => ObjectNameSchema.parse('projectTask')).toThrow();
32+
});
33+
34+
it('should reject too short names', () => {
35+
expect(() => ObjectNameSchema.parse('a')).toThrow();
36+
});
37+
38+
it('should produce branded type at runtime', () => {
39+
const name: ObjectName = ObjectNameSchema.parse('my_object');
40+
expect(name).toBe('my_object');
41+
});
42+
});
43+
44+
describe('FieldNameSchema', () => {
45+
it('should accept valid snake_case field names', () => {
46+
const validNames = ['first_name', 'created_at', 'total_amount', 'is_active'];
47+
validNames.forEach((name) => {
48+
expect(() => FieldNameSchema.parse(name)).not.toThrow();
49+
});
50+
});
51+
52+
it('should reject dots', () => {
53+
expect(() => FieldNameSchema.parse('user.name')).toThrow();
54+
});
55+
56+
it('should reject camelCase', () => {
57+
expect(() => FieldNameSchema.parse('firstName')).toThrow();
58+
});
59+
60+
it('should produce branded type at runtime', () => {
61+
const name: FieldName = FieldNameSchema.parse('task_name');
62+
expect(name).toBe('task_name');
63+
});
64+
});
65+
66+
describe('ViewNameSchema', () => {
67+
it('should accept valid system identifiers', () => {
68+
const validNames = ['all_tasks', 'my_open_deals', 'contact.recent'];
69+
validNames.forEach((name) => {
70+
expect(() => ViewNameSchema.parse(name)).not.toThrow();
71+
});
72+
});
73+
74+
it('should allow dots (namespacing)', () => {
75+
const name: ViewName = ViewNameSchema.parse('contact.recent');
76+
expect(name).toBe('contact.recent');
77+
});
78+
79+
it('should reject uppercase', () => {
80+
expect(() => ViewNameSchema.parse('AllTasks')).toThrow();
81+
});
82+
});
83+
84+
describe('AppNameSchema', () => {
85+
it('should accept valid app names', () => {
86+
const validNames = ['crm', 'helpdesk', 'project_management'];
87+
validNames.forEach((name) => {
88+
expect(() => AppNameSchema.parse(name)).not.toThrow();
89+
});
90+
});
91+
92+
it('should produce branded type at runtime', () => {
93+
const name: AppName = AppNameSchema.parse('crm');
94+
expect(name).toBe('crm');
95+
});
96+
});
97+
98+
describe('FlowNameSchema', () => {
99+
it('should accept valid flow names', () => {
100+
const validNames = ['approval_flow', 'onboarding_wizard', 'lead_qualification'];
101+
validNames.forEach((name) => {
102+
expect(() => FlowNameSchema.parse(name)).not.toThrow();
103+
});
104+
});
105+
106+
it('should produce branded type at runtime', () => {
107+
const name: FlowName = FlowNameSchema.parse('approval_flow');
108+
expect(name).toBe('approval_flow');
109+
});
110+
});
111+
112+
describe('RoleNameSchema', () => {
113+
it('should accept valid role names', () => {
114+
const validNames = ['admin', 'sales_manager', 'read_only'];
115+
validNames.forEach((name) => {
116+
expect(() => RoleNameSchema.parse(name)).not.toThrow();
117+
});
118+
});
119+
120+
it('should produce branded type at runtime', () => {
121+
const name: RoleName = RoleNameSchema.parse('admin');
122+
expect(name).toBe('admin');
123+
});
124+
});
125+
126+
describe('Branded type safety', () => {
127+
it('branded types parse the same underlying strings', () => {
128+
const objectName = ObjectNameSchema.parse('my_entity');
129+
const fieldName = FieldNameSchema.parse('my_entity');
130+
// At runtime both are 'my_entity' strings but TS types differ
131+
expect(objectName).toBe('my_entity');
132+
expect(fieldName).toBe('my_entity');
133+
});
134+
});
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
import { z } from 'zod';
4+
import { SnakeCaseIdentifierSchema, SystemIdentifierSchema } from './identifiers.zod';
5+
6+
/**
7+
* Branded Types for ObjectStack Identifiers
8+
*
9+
* Branded types provide compile-time safety by preventing accidental mixing
10+
* of different identifier kinds. For example, you cannot pass an ObjectName
11+
* where a FieldName is expected, even though both are strings at runtime.
12+
*
13+
* @example
14+
* ```ts
15+
* import { ObjectNameSchema, FieldNameSchema } from '@objectstack/spec';
16+
*
17+
* const objName = ObjectNameSchema.parse('project_task'); // ObjectName
18+
* const fieldName = FieldNameSchema.parse('task_name'); // FieldName
19+
*
20+
* // TypeScript will catch this at compile time:
21+
* // const fn: FieldName = objName; // Error!
22+
* ```
23+
*/
24+
25+
/**
26+
* ObjectName — Branded type for business object names.
27+
*
28+
* Must be snake_case (no dots). Used for table/collection names.
29+
*
30+
* @example 'project_task', 'crm_account', 'user_profile'
31+
*/
32+
export const ObjectNameSchema = SnakeCaseIdentifierSchema
33+
.brand<'ObjectName'>()
34+
.describe('Branded object name (snake_case, no dots)');
35+
36+
export type ObjectName = z.infer<typeof ObjectNameSchema>;
37+
38+
/**
39+
* FieldName — Branded type for field (column) names.
40+
*
41+
* Must be snake_case (no dots). Used for column/property names within objects.
42+
*
43+
* @example 'first_name', 'created_at', 'total_amount'
44+
*/
45+
export const FieldNameSchema = SnakeCaseIdentifierSchema
46+
.brand<'FieldName'>()
47+
.describe('Branded field name (snake_case, no dots)');
48+
49+
export type FieldName = z.infer<typeof FieldNameSchema>;
50+
51+
/**
52+
* ViewName — Branded type for view identifiers.
53+
*
54+
* Must be a valid system identifier (lowercase, may contain dots for namespacing).
55+
*
56+
* @example 'all_tasks', 'my_open_deals', 'contact.recent'
57+
*/
58+
export const ViewNameSchema = SystemIdentifierSchema
59+
.brand<'ViewName'>()
60+
.describe('Branded view name (system identifier)');
61+
62+
export type ViewName = z.infer<typeof ViewNameSchema>;
63+
64+
/**
65+
* AppName — Branded type for application identifiers.
66+
*
67+
* Must be a valid system identifier.
68+
*
69+
* @example 'crm', 'helpdesk', 'project_management'
70+
*/
71+
export const AppNameSchema = SystemIdentifierSchema
72+
.brand<'AppName'>()
73+
.describe('Branded app name (system identifier)');
74+
75+
export type AppName = z.infer<typeof AppNameSchema>;
76+
77+
/**
78+
* FlowName — Branded type for flow identifiers.
79+
*
80+
* Must be a valid system identifier.
81+
*
82+
* @example 'approval_flow', 'onboarding_wizard', 'lead_qualification'
83+
*/
84+
export const FlowNameSchema = SystemIdentifierSchema
85+
.brand<'FlowName'>()
86+
.describe('Branded flow name (system identifier)');
87+
88+
export type FlowName = z.infer<typeof FlowNameSchema>;
89+
90+
/**
91+
* RoleName — Branded type for role identifiers.
92+
*
93+
* Must be a valid system identifier.
94+
*
95+
* @example 'admin', 'sales_manager', 'read_only'
96+
*/
97+
export const RoleNameSchema = SystemIdentifierSchema
98+
.brand<'RoleName'>()
99+
.describe('Branded role name (system identifier)');
100+
101+
export type RoleName = z.infer<typeof RoleNameSchema>;

0 commit comments

Comments
 (0)